From aa8786ac75b34320c212a26ea34c46758cb8b6ca Mon Sep 17 00:00:00 2001 From: Lars Vogel Date: Mon, 18 May 2026 09:35:36 +0200 Subject: [PATCH 1/3] Replace per-bit listener count fields with an AtomicIntegerArray ResourceChangeListenerList tracked per-event-type listener counts in six discrete volatile int fields (count1..count32), with adding(), removing(), hasListenerFor(), and clear() each duplicating a hard-coded switch over the same six bitmask values. Every new IResourceChangeEvent type required adding a field and four matching cases, and any mask bit that no one remembered to wire up was silently dropped by hasListenerFor. Collapse the bookkeeping into a single AtomicIntegerArray indexed by Integer.numberOfTrailingZeros(mask). adjust(mask, delta) walks the set bits of the mask once, replacing the two near-identical adding/removing helpers. hasListenerFor short-circuits non-single-bit and non-positive arguments, matching the previous switch's default-false behavior, and otherwise reads the counter for the single bit position. AtomicIntegerArray preserves the per-element volatile read semantics that hasListenerFor depends on outside the synchronized add/remove/clear paths. Drops the unreachable "listeners != null" guard in toString since the field is final and initialized at declaration, and inlines the ListenerEntry.toString StringBuilder into a concatenation expression for parity with the simplified outer toString. --- .../events/ResourceChangeListenerList.java | 115 +++++------------- 1 file changed, 29 insertions(+), 86 deletions(-) diff --git a/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/events/ResourceChangeListenerList.java b/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/events/ResourceChangeListenerList.java index 3fc2de8496e..48ddd480dcd 100644 --- a/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/events/ResourceChangeListenerList.java +++ b/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/events/ResourceChangeListenerList.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2000, 2008 IBM Corporation and others. + * Copyright (c) 2000, 2026 IBM Corporation and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 @@ -15,6 +15,7 @@ import java.util.Objects; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicIntegerArray; import org.eclipse.core.resources.IResourceChangeListener; /** @@ -44,22 +45,18 @@ static final class ListenerEntry { @Override public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("Listener [eventMask="); //$NON-NLS-1$ - sb.append(eventMask); - sb.append(", "); //$NON-NLS-1$ - sb.append(listener); - sb.append("]"); //$NON-NLS-1$ - return sb.toString(); + return "Listener [eventMask=" + eventMask + ", " + listener + "]"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ } } - private volatile int count1 = 0; - private volatile int count2 = 0; - private volatile int count4 = 0; - private volatile int count8 = 0; - private volatile int count16 = 0; - private volatile int count32 = 0; + /** + * Per-event-bit listener counts, indexed by + * {@code Integer.numberOfTrailingZeros(eventMask)}. An + * {@link AtomicIntegerArray} preserves the per-element volatile read + * semantics that {@link #hasListenerFor(int)} relies on outside the + * synchronized {@link #add}/{@link #remove}/{@link #clear} paths. + */ + private final AtomicIntegerArray bitCounts = new AtomicIntegerArray(Integer.SIZE); /** * The list of listeners. @@ -79,43 +76,22 @@ public synchronized void add(IResourceChangeListener listener, int mask) { remove(listener); return; } - ResourceChangeListenerList.ListenerEntry entry = new ResourceChangeListenerList.ListenerEntry(listener, mask); + ListenerEntry entry = new ListenerEntry(listener, mask); final int oldSize = listeners.size(); // check for duplicates using identity for (int i = 0; i < oldSize; ++i) { ListenerEntry oldEntry = listeners.get(i); if (oldEntry.listener == listener) { - removing(oldEntry.eventMask); - adding(mask); + adjust(oldEntry.eventMask, -1); + adjust(mask, +1); listeners.set(i, entry); return; } } - adding(mask); + adjust(mask, +1); listeners.add(entry); } - private void adding(int mask) { - if ((mask & 1) != 0) { - count1++; - } - if ((mask & 2) != 0) { - count2++; - } - if ((mask & 4) != 0) { - count4++; - } - if ((mask & 8) != 0) { - count8++; - } - if ((mask & 16) != 0) { - count16++; - } - if ((mask & 32) != 0) { - count32++; - } - } - /** * Returns a copy of the registered listeners. * @return the list of registered listeners that must not be modified @@ -125,22 +101,11 @@ public ListenerEntry[] getListeners() { } public boolean hasListenerFor(int event) { - switch (event) { - case 1: - return count1 > 0; - case 2: - return count2 > 0; - case 4: - return count4 > 0; - case 8: - return count8 > 0; - case 16: - return count16 > 0; - case 32: - return count32 > 0; - default: + // event is expected to be a single bit (a power of two) + if (event <= 0 || Integer.bitCount(event) != 1) { return false; } + return bitCounts.get(Integer.numberOfTrailingZeros(event)) > 0; } /** @@ -155,7 +120,7 @@ public synchronized void remove(IResourceChangeListener listener) { for (int i = 0; i < oldSize; ++i) { ListenerEntry oldEntry = listeners.get(i); if (oldEntry.listener == listener) { - removing(oldEntry.eventMask); + adjust(oldEntry.eventMask, -1); listeners.remove(i); return; } @@ -164,44 +129,22 @@ public synchronized void remove(IResourceChangeListener listener) { public synchronized void clear() { listeners.clear(); - count1 = 0; - count2 = 0; - count4 = 0; - count8 = 0; - count16 = 0; - count32 = 0; + for (int i = 0; i < bitCounts.length(); i++) { + bitCounts.set(i, 0); + } } - private void removing(int mask) { - if ((mask & 1) != 0) { - count1--; - } - if ((mask & 2) != 0) { - count2--; - } - if ((mask & 4) != 0) { - count4--; - } - if ((mask & 8) != 0) { - count8--; - } - if ((mask & 16) != 0) { - count16--; - } - if ((mask & 32) != 0) { - count32--; + private void adjust(int mask, int delta) { + int remaining = mask; + while (remaining != 0) { + int bit = Integer.numberOfTrailingZeros(remaining); + bitCounts.addAndGet(bit, delta); + remaining &= remaining - 1; } } @Override public String toString() { - StringBuilder builder = new StringBuilder(); - builder.append("ResourceChangeListenerList ["); //$NON-NLS-1$ - if (listeners != null) { - builder.append("listeners="); //$NON-NLS-1$ - builder.append(listeners.toString()); - } - builder.append("]"); //$NON-NLS-1$ - return builder.toString(); + return "ResourceChangeListenerList [listeners=" + listeners + "]"; //$NON-NLS-1$ //$NON-NLS-2$ } } From 6f5ac49e6a1f820c90aa09db05b1b367d2ef4aa4 Mon Sep 17 00:00:00 2001 From: Lars Vogel Date: Mon, 20 Apr 2026 19:56:50 +0200 Subject: [PATCH 2/3] Add per-builder build events to IResourceChangeEvent Introduce PRE_PROJECT_BUILD and POST_PROJECT_BUILD event types on IResourceChangeEvent, fired once per builder execution from BuildManager.basicBuild around the SafeRunner.run that invokes the builder. Listeners receive the IProject as source, the builder id (from the project's build spec) via the new getBuilderName() default method, and the build kind via getBuildKind(). No resource delta is attached; the surrounding workspace-level PRE_BUILD/POST_BUILD pair continues to carry the aggregated delta. Events fire above the needsBuild short-circuit so every builder that is considered reports a matched PRE/POST pair; builders whose project has no delta produce near-zero durations but still appear to listeners, giving tools complete visibility of the build sequence. Dispatch goes through a new NotificationManager.broadcastProjectBuildEvent path that skips delta computation and does not update lastPostBuildTree, so per-builder events remain pure timing/metadata signals nested inside the workspace build. ResourceChangeListenerList.hasListenerFor was a hard-coded switch over bitmasks 1..32 and silently dropped anything else; extend its counter bookkeeping to include 64 and 128 so listeners registered for the new event types actually receive them. Bumps org.eclipse.core.resources to 3.25.0 and promotes two pending @since 3.24 tags on IFile to 3.25. Adds ProjectBuildEventTest covering FULL, CLEAN, INCREMENTAL-with-no-delta, nesting order against the enclosing workspace events, and the getBuilderName() contract. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../META-INF/MANIFEST.MF | 2 +- .../core/internal/events/BuildManager.java | 47 +++-- .../internal/events/NotificationManager.java | 18 ++ .../internal/events/ResourceChangeEvent.java | 31 ++- .../core/internal/resources/Workspace.java | 9 + .../core/resources/IResourceChangeEvent.java | 78 +++++++- .../builders/ProjectBuildEventTest.java | 182 ++++++++++++++++++ 7 files changed, 347 insertions(+), 20 deletions(-) create mode 100644 resources/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/internal/builders/ProjectBuildEventTest.java diff --git a/resources/bundles/org.eclipse.core.resources/META-INF/MANIFEST.MF b/resources/bundles/org.eclipse.core.resources/META-INF/MANIFEST.MF index bcfaaeb9ae8..5402c07519f 100644 --- a/resources/bundles/org.eclipse.core.resources/META-INF/MANIFEST.MF +++ b/resources/bundles/org.eclipse.core.resources/META-INF/MANIFEST.MF @@ -2,7 +2,7 @@ Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: %pluginName Bundle-SymbolicName: org.eclipse.core.resources; singleton:=true -Bundle-Version: 3.24.0.qualifier +Bundle-Version: 3.25.0.qualifier Bundle-Activator: org.eclipse.core.resources.ResourcesPlugin Bundle-Vendor: %providerName Bundle-Localization: plugin diff --git a/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/events/BuildManager.java b/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/events/BuildManager.java index 170f4d33de5..30976341ead 100644 --- a/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/events/BuildManager.java +++ b/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/events/BuildManager.java @@ -49,6 +49,7 @@ import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IProjectDescription; import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IResourceChangeEvent; import org.eclipse.core.resources.IResourceDelta; import org.eclipse.core.resources.IResourceStatus; import org.eclipse.core.resources.IncrementalProjectBuilder; @@ -262,6 +263,11 @@ private void basicBuild(int trigger, IncrementalProjectBuilder builder, Map= 0) { getWorkManager().endUnprotected(depth); } @@ -313,23 +321,28 @@ private void basicBuild(int trigger, IncrementalProjectBuilder builder, Map null for next build - if (trigger == IncrementalProjectBuilder.FULL_BUILD) { + try { + workspace.broadcastProjectBuildEvent(builderProject, builderName, + IResourceChangeEvent.POST_PROJECT_BUILD, trigger); + } finally { + // Be sure to clean up after ourselves. + if (clean || currentBuilder.wasForgetStateRequested()) { currentBuilder.setLastBuiltTree(null); + } else if (currentBuilder.wasRememberStateRequested()) { + // If remember last build state, and FULL_BUILD + // last tree must be set to => null for next build + if (trigger == IncrementalProjectBuilder.FULL_BUILD) { + currentBuilder.setLastBuiltTree(null); + } + // else don't modify the last built tree + } else { + // remember the current state as the last built state. + ElementTree lastTree = workspace.getElementTree(); + lastTree.immutable(); + currentBuilder.setLastBuiltTree(lastTree); } - // else don't modify the last built tree - } else { - // remember the current state as the last built state. - ElementTree lastTree = workspace.getElementTree(); - lastTree.immutable(); - currentBuilder.setLastBuiltTree(lastTree); + hookEndBuild(builder); } - hookEndBuild(builder); } } finally { currentBuilders.remove(currentBuilder); diff --git a/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/events/NotificationManager.java b/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/events/NotificationManager.java index 017afaec0c3..d1400ba774f 100644 --- a/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/events/NotificationManager.java +++ b/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/events/NotificationManager.java @@ -214,6 +214,24 @@ private void cleanUp(ElementTree lastState, int type) { } } + /** + * Dispatches a per-builder build event (PRE_PROJECT_BUILD / POST_PROJECT_BUILD). + *

+ * Unlike {@link #broadcastChanges}, this path does not compute a resource + * delta, does not coalesce against the last build tree, and does not update + * {@code lastPostBuildTree}. Per-builder events are pure timing/metadata + * signals nested inside the surrounding workspace-level PRE_BUILD/POST_BUILD + * pair, which continues to carry the aggregated delta. + *

+ */ + public void broadcastProjectBuildEvent(IProject project, String builderName, int type, int buildKind) { + if (!listeners.hasListenerFor(type)) { + return; + } + ResourceChangeEvent event = new ResourceChangeEvent(project, type, buildKind, project, builderName); + notify(getListeners(), event, false); + } + /** * Helper method for the save participant lifecycle computation. */ public void broadcastChanges(IResourceChangeListener listener, int type, IResourceDelta delta) { diff --git a/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/events/ResourceChangeEvent.java b/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/events/ResourceChangeEvent.java index 43daf101a56..bbf0f8073b9 100644 --- a/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/events/ResourceChangeEvent.java +++ b/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/events/ResourceChangeEvent.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2000, 2015 IBM Corporation and others. + * Copyright (c) 2000, 2026 IBM Corporation and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 @@ -12,6 +12,7 @@ * IBM Corporation - initial API and implementation * James Blackburn (Broadcom Corp.) - ongoing development * Lars Vogel - Bug 473427 + * Vogella GmbH - per-builder build events *******************************************************************************/ package org.eclipse.core.internal.events; @@ -32,6 +33,11 @@ public class ResourceChangeEvent extends EventObject implements IResourceChangeE private int trigger = 0; int type; + /** + * The builder id for PRE_PROJECT_BUILD / POST_PROJECT_BUILD events, or null. + */ + private String builderName; + protected ResourceChangeEvent(Object source, int type, IResource resource) { super(source); this.resource = resource; @@ -45,6 +51,12 @@ public ResourceChangeEvent(Object source, int type, int buildKind, IResourceDelt this.type = type; } + public ResourceChangeEvent(Object source, int type, int buildKind, IResource resource, String builderName) { + this(source, type, resource); + this.trigger = buildKind; + this.builderName = builderName; + } + /** * @see IResourceChangeEvent#findMarkerDeltas(String, boolean) */ @@ -110,6 +122,14 @@ public int getType() { return type; } + /** + * @see IResourceChangeEvent#getBuilderName() + */ + @Override + public String getBuilderName() { + return builderName; + } + public void setDelta(IResourceDelta value) { delta = value; } @@ -136,6 +156,12 @@ public String toDebugString() { case PRE_REFRESH : output.append("PRE_REFRESH"); //$NON-NLS-1$ break; + case PRE_PROJECT_BUILD : + output.append("PRE_PROJECT_BUILD"); //$NON-NLS-1$ + break; + case POST_PROJECT_BUILD : + output.append("POST_PROJECT_BUILD"); //$NON-NLS-1$ + break; default : output.append("?"); //$NON-NLS-1$ break; @@ -157,6 +183,9 @@ public String toDebugString() { break; } output.append("\nResource: " + (resource == null ? "null" : resource)); //$NON-NLS-1$ //$NON-NLS-2$ + if (builderName != null) { + output.append("\nBuilder: " + builderName); //$NON-NLS-1$ + } output.append("\nDelta:" + (delta == null ? " null" : ((ResourceDelta) delta).toDeepDebugString())); //$NON-NLS-1$ //$NON-NLS-2$ return output.toString(); } diff --git a/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/Workspace.java b/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/Workspace.java index 690cb8fe72e..c6cbc2e0443 100644 --- a/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/Workspace.java +++ b/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/Workspace.java @@ -458,6 +458,15 @@ public void broadcastBuildEvent(Object source, int type, int buildTrigger) { notificationManager.broadcastChanges(tree, event, false); } + /** + * Broadcasts a per-builder build event. The {@code type} must be either + * {@link IResourceChangeEvent#PRE_PROJECT_BUILD} or + * {@link IResourceChangeEvent#POST_PROJECT_BUILD}. + */ + public void broadcastProjectBuildEvent(IProject project, String builderName, int type, int buildTrigger) { + notificationManager.broadcastProjectBuildEvent(project, builderName, type, buildTrigger); + } + /** * Broadcasts an internal workspace lifecycle event to interested * internal listeners. diff --git a/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/resources/IResourceChangeEvent.java b/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/resources/IResourceChangeEvent.java index b4d0d30fb93..3e54bb344c7 100644 --- a/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/resources/IResourceChangeEvent.java +++ b/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/resources/IResourceChangeEvent.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2000, 2014 IBM Corporation and others. + * Copyright (c) 2000, 2026 IBM Corporation and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 @@ -10,6 +10,7 @@ * * Contributors: * IBM Corporation - initial API and implementation + * Vogella GmbH - per-builder build events *******************************************************************************/ package org.eclipse.core.resources; @@ -86,6 +87,26 @@ * method returns the project being refreshed. * The workspace is closed for changes during notification of these events. * + *
  • + * Before-the-fact reports of a single builder about to run on a single + * project. Event type is PRE_PROJECT_BUILD, getSource + * returns the {@link IProject} being built, getBuilderName + * returns the id of the builder that is about to run, and getBuildKind + * returns the kind of build. Fired once per builder per project, nested + * inside a surrounding PRE_BUILD/POST_BUILD pair. + * No resource delta is provided. + *
  • + *
  • + * After-the-fact reports of a single builder having run on a single project. + * Event type is POST_PROJECT_BUILD, getSource + * returns the {@link IProject} that was built, getBuilderName + * returns the id of the builder that ran, and getBuildKind + * returns the kind of build. Fired once per builder per project, nested + * inside a surrounding PRE_BUILD/POST_BUILD pair. + * No resource delta is provided. + * When the workspace is configured for parallel builds, events for + * different projects may be delivered concurrently on different threads. + *
  • * *

    * In order to handle additional event types that may be introduced @@ -179,6 +200,42 @@ public interface IResourceChangeEvent { */ int PRE_REFRESH = 32; + /** + * Event type constant (bit mask) indicating a before-the-fact report + * that a single builder is about to run on a single project. + *

    + * getSource returns the {@link IProject} being built, + * getBuilderName returns the id of the builder, and + * getBuildKind returns the kind of build. No resource + * delta is provided. See class comments for further details. + *

    + * + * @see #getType() + * @see #getSource() + * @see #getBuilderName() + * @see #getBuildKind() + * @since 3.25 + */ + int PRE_PROJECT_BUILD = 64; + + /** + * Event type constant (bit mask) indicating an after-the-fact report + * that a single builder has run on a single project. + *

    + * getSource returns the {@link IProject} that was built, + * getBuilderName returns the id of the builder, and + * getBuildKind returns the kind of build. No resource + * delta is provided. See class comments for further details. + *

    + * + * @see #getType() + * @see #getSource() + * @see #getBuilderName() + * @see #getBuildKind() + * @since 3.25 + */ + int POST_PROJECT_BUILD = 128; + /** * Returns all marker deltas of the specified type that are associated * with resource deltas for this event. If includeSubtypes @@ -262,6 +319,25 @@ public interface IResourceChangeEvent { * @see #PRE_CLOSE * @see #PRE_DELETE * @see #PRE_REFRESH + * @see #PRE_PROJECT_BUILD + * @see #POST_PROJECT_BUILD */ int getType(); + + /** + * Returns the id of the builder this event relates to, or null + * if not applicable to this type of event. + *

    + * For {@link #PRE_PROJECT_BUILD} and {@link #POST_PROJECT_BUILD} events + * this returns the builder id as declared in the project's build spec + * (the ICommand's builder name). For all other event types + * this returns null. + *

    + * + * @return the builder id, or null if not applicable + * @since 3.25 + */ + default String getBuilderName() { + return null; + } } diff --git a/resources/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/internal/builders/ProjectBuildEventTest.java b/resources/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/internal/builders/ProjectBuildEventTest.java new file mode 100644 index 00000000000..8ec50040079 --- /dev/null +++ b/resources/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/internal/builders/ProjectBuildEventTest.java @@ -0,0 +1,182 @@ +/******************************************************************************* + * Copyright (c) 2026 Vogella GmbH and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package org.eclipse.core.tests.internal.builders; + +import static org.eclipse.core.resources.ResourcesPlugin.getWorkspace; +import static org.eclipse.core.tests.resources.ResourceTestUtil.createTestMonitor; +import static org.eclipse.core.tests.resources.ResourceTestUtil.setAutoBuilding; +import static org.eclipse.core.tests.resources.ResourceTestUtil.updateProjectDescription; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResourceChangeEvent; +import org.eclipse.core.resources.IResourceChangeListener; +import org.eclipse.core.resources.IncrementalProjectBuilder; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.tests.resources.util.WorkspaceResetExtension; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +/** + * Tests the PRE_PROJECT_BUILD and POST_PROJECT_BUILD events. In particular it + * asserts that events fire even when a builder is short-circuited by the + * needsBuild check (e.g. an incremental build after Project > Clean where no + * source changed). + */ +@ExtendWith(WorkspaceResetExtension.class) +public class ProjectBuildEventTest { + + private record Record(int type, Object source, String builderName, int buildKind) { + } + + private final List events = new CopyOnWriteArrayList<>(); + private final IResourceChangeListener listener = event -> events.add( + new Record(event.getType(), event.getSource(), event.getBuilderName(), event.getBuildKind())); + + @BeforeEach + public void setUp() { + int mask = IResourceChangeEvent.PRE_BUILD | IResourceChangeEvent.POST_BUILD + | IResourceChangeEvent.PRE_PROJECT_BUILD | IResourceChangeEvent.POST_PROJECT_BUILD; + getWorkspace().addResourceChangeListener(listener, mask); + } + + @AfterEach + public void tearDown() { + getWorkspace().removeResourceChangeListener(listener); + } + + @Test + public void testPerBuilderEventsFireOnFullBuild() throws CoreException { + IProject project = createProjectWithBuilder("FULL"); + + events.clear(); + getWorkspace().build(IncrementalProjectBuilder.FULL_BUILD, createTestMonitor()); + + List perBuilder = filter(IResourceChangeEvent.PRE_PROJECT_BUILD, + IResourceChangeEvent.POST_PROJECT_BUILD); + assertEquals(2, perBuilder.size(), "expected one PRE/POST_PROJECT_BUILD pair, got events: " + events); + + Record pre = perBuilder.get(0); + Record post = perBuilder.get(1); + assertEquals(IResourceChangeEvent.PRE_PROJECT_BUILD, pre.type()); + assertEquals(IResourceChangeEvent.POST_PROJECT_BUILD, post.type()); + assertEquals(project, pre.source()); + assertEquals(project, post.source()); + assertEquals(DeltaVerifierBuilder.BUILDER_NAME, pre.builderName()); + assertEquals(DeltaVerifierBuilder.BUILDER_NAME, post.builderName()); + assertEquals(IncrementalProjectBuilder.FULL_BUILD, pre.buildKind()); + assertEquals(IncrementalProjectBuilder.FULL_BUILD, post.buildKind()); + } + + @Test + public void testPerBuilderEventsFireOnCleanBuild() throws CoreException { + createProjectWithBuilder("CLEAN"); + getWorkspace().build(IncrementalProjectBuilder.FULL_BUILD, createTestMonitor()); + + events.clear(); + getWorkspace().build(IncrementalProjectBuilder.CLEAN_BUILD, createTestMonitor()); + List perBuilder = filter(IResourceChangeEvent.PRE_PROJECT_BUILD, + IResourceChangeEvent.POST_PROJECT_BUILD); + assertEquals(2, perBuilder.size(), "CLEAN should fire one PRE/POST_PROJECT_BUILD pair, got: " + events); + assertEquals(IncrementalProjectBuilder.CLEAN_BUILD, perBuilder.get(0).buildKind()); + assertEquals(IncrementalProjectBuilder.CLEAN_BUILD, perBuilder.get(1).buildKind()); + } + + @Test + public void testPerBuilderEventsFireOnIncrementalBuildWithNoDelta() throws CoreException { + createProjectWithBuilder("INC-NO-DELTA"); + // Prime: run a full build so the builder has a last-built tree. + getWorkspace().build(IncrementalProjectBuilder.FULL_BUILD, createTestMonitor()); + + // Second incremental build without any intervening source change. The + // builder should be considered but short-circuited by needsBuild. Events + // must still fire so the Build Monitor view can render the session. + events.clear(); + getWorkspace().build(IncrementalProjectBuilder.INCREMENTAL_BUILD, createTestMonitor()); + List perBuilder = filter(IResourceChangeEvent.PRE_PROJECT_BUILD, + IResourceChangeEvent.POST_PROJECT_BUILD); + assertEquals(2, perBuilder.size(), + "INCREMENTAL with no delta must still fire PRE/POST_PROJECT_BUILD, got: " + events); + } + + @Test + public void testPerBuilderEventsNestedInsideWorkspaceBuild() throws CoreException { + createProjectWithBuilder("NESTED"); + + events.clear(); + getWorkspace().build(IncrementalProjectBuilder.FULL_BUILD, createTestMonitor()); + + List types = new ArrayList<>(); + for (Record r : events) { + types.add(r.type()); + } + int preBuild = types.indexOf(IResourceChangeEvent.PRE_BUILD); + int postBuild = types.indexOf(IResourceChangeEvent.POST_BUILD); + int prePrj = types.indexOf(IResourceChangeEvent.PRE_PROJECT_BUILD); + int postPrj = types.indexOf(IResourceChangeEvent.POST_PROJECT_BUILD); + assertTrue(preBuild >= 0 && postBuild > preBuild, "workspace PRE/POST_BUILD must be present and ordered: " + events); + assertTrue(prePrj > preBuild && prePrj < postBuild, + "PRE_PROJECT_BUILD must be nested inside workspace build: " + events); + assertTrue(postPrj > prePrj && postPrj < postBuild, + "POST_PROJECT_BUILD must come after its PRE and before workspace POST_BUILD: " + events); + } + + @Test + public void testBuilderNameOnlyPopulatedForProjectBuildEvents() throws CoreException { + createProjectWithBuilder("NAME"); + events.clear(); + getWorkspace().build(IncrementalProjectBuilder.FULL_BUILD, createTestMonitor()); + + boolean sawProjectBuildWithName = false; + for (Record r : events) { + if (r.type() == IResourceChangeEvent.PRE_PROJECT_BUILD + || r.type() == IResourceChangeEvent.POST_PROJECT_BUILD) { + assertNotNull(r.builderName(), "per-builder event must carry builder name"); + sawProjectBuildWithName = true; + } else { + assertNull(r.builderName(), "workspace-level event must not carry builder name, got: " + r); + } + } + assertTrue(sawProjectBuildWithName, "expected at least one per-builder event: " + events); + } + + private IProject createProjectWithBuilder(String tag) throws CoreException { + IProject project = getWorkspace().getRoot().getProject("PROJECT-" + tag); + setAutoBuilding(false); + project.create(createTestMonitor()); + project.open(createTestMonitor()); + updateProjectDescription(project).addingCommand(DeltaVerifierBuilder.BUILDER_NAME) + .withTestBuilderId("Builder-" + tag).apply(); + return project; + } + + private List filter(int... types) { + List out = new ArrayList<>(); + for (Record r : events) { + for (int t : types) { + if (r.type() == t) { + out.add(r); + break; + } + } + } + return out; + } +} From 13e747d4d0fa85e9c6568877570351d3ee818c80 Mon Sep 17 00:00:00 2001 From: Lars Vogel Date: Mon, 20 Apr 2026 19:57:10 +0200 Subject: [PATCH 3/3] Add Build Monitor example view Introduce a new example bundle org.eclipse.ui.examples.buildmonitor that contributes a "Build Monitor" view. The view listens to the per-builder PRE_PROJECT_BUILD / POST_PROJECT_BUILD events and aggregates them into a TreeViewer: top-level rows are build sessions, expanding reveals per-project timings, and expanding a project reveals the individual builders that ran for it. Two timing figures are shown per project: * Pure build time: sum of each builder's (end - start). Reflects the CPU work that was done on the project, independent of scheduling. * Time until ready: from the enclosing workspace PRE_BUILD to the moment the project's last builder finished. The recorder matches per-builder PRE/POST events using a thread-local stack, which is race-free under parallel builds because a single builder invocation runs synchronously on one thread. Builders that BuildManager.needsBuild short-circuits still fire a PRE/POST pair with near-zero duration; filter those out at a 100 microsecond threshold so projects that were only considered but did not rebuild (e.g. every workspace project on an AUTO build triggered by one file change) do not clutter the view. Skipped builders still flow through the event API itself for tools that want to see them. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../META-INF/MANIFEST.MF | 14 + .../about.html | 36 +++ .../build.properties | 7 + .../plugin.xml | 17 + .../buildmonitor/BuildMonitorView.java | 238 ++++++++++++++ .../examples/buildmonitor/BuildRecorder.java | 299 ++++++++++++++++++ resources/examples/pom.xml | 1 + 7 files changed, 612 insertions(+) create mode 100644 resources/examples/org.eclipse.ui.examples.buildmonitor/META-INF/MANIFEST.MF create mode 100644 resources/examples/org.eclipse.ui.examples.buildmonitor/about.html create mode 100644 resources/examples/org.eclipse.ui.examples.buildmonitor/build.properties create mode 100644 resources/examples/org.eclipse.ui.examples.buildmonitor/plugin.xml create mode 100644 resources/examples/org.eclipse.ui.examples.buildmonitor/src/org/eclipse/ui/examples/buildmonitor/BuildMonitorView.java create mode 100644 resources/examples/org.eclipse.ui.examples.buildmonitor/src/org/eclipse/ui/examples/buildmonitor/BuildRecorder.java diff --git a/resources/examples/org.eclipse.ui.examples.buildmonitor/META-INF/MANIFEST.MF b/resources/examples/org.eclipse.ui.examples.buildmonitor/META-INF/MANIFEST.MF new file mode 100644 index 00000000000..37d7b91ccfc --- /dev/null +++ b/resources/examples/org.eclipse.ui.examples.buildmonitor/META-INF/MANIFEST.MF @@ -0,0 +1,14 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Automatic-Module-Name: org.eclipse.ui.examples.buildmonitor +Bundle-Name: Build Monitor Example +Bundle-SymbolicName: org.eclipse.ui.examples.buildmonitor;singleton:=true +Bundle-Version: 1.0.0.qualifier +Bundle-Vendor: Eclipse.org +Require-Bundle: org.eclipse.core.resources;bundle-version="[3.25.0,4.0.0)", + org.eclipse.core.runtime;bundle-version="[3.34.0,4.0.0)", + org.eclipse.ui;bundle-version="3.208.0", + org.eclipse.ui.ide, + org.eclipse.jface +Bundle-ActivationPolicy: lazy +Bundle-RequiredExecutionEnvironment: JavaSE-21 diff --git a/resources/examples/org.eclipse.ui.examples.buildmonitor/about.html b/resources/examples/org.eclipse.ui.examples.buildmonitor/about.html new file mode 100644 index 00000000000..c747b51f170 --- /dev/null +++ b/resources/examples/org.eclipse.ui.examples.buildmonitor/about.html @@ -0,0 +1,36 @@ + + + + +About + + +

    About This Content

    + +

    April 20, 2026

    +

    License

    + +

    + The Eclipse Foundation makes available all content in this plug-in + ("Content"). Unless otherwise indicated below, the Content + is provided to you under the terms and conditions of the Eclipse + Public License Version 2.0 ("EPL"). A copy of the EPL is + available at http://www.eclipse.org/legal/epl-2.0. + For purposes of the EPL, "Program" will mean the Content. +

    + +

    + If you did not receive this Content directly from the Eclipse + Foundation, the Content is being redistributed by another party + ("Redistributor") and different terms and conditions may + apply to your use of any object code in the Content. Check the + Redistributor's license that was provided with the Content. If no such + license exists, contact the Redistributor. Unless otherwise indicated + below, the terms and conditions of the EPL still apply to any source + code in the Content and such source code may be obtained at http://www.eclipse.org. +

    + + + diff --git a/resources/examples/org.eclipse.ui.examples.buildmonitor/build.properties b/resources/examples/org.eclipse.ui.examples.buildmonitor/build.properties new file mode 100644 index 00000000000..e5a949cc43c --- /dev/null +++ b/resources/examples/org.eclipse.ui.examples.buildmonitor/build.properties @@ -0,0 +1,7 @@ +source.. = src/ +output.. = bin/ +bin.includes = META-INF/,\ + .,\ + plugin.xml,\ + about.html +src.includes = about.html diff --git a/resources/examples/org.eclipse.ui.examples.buildmonitor/plugin.xml b/resources/examples/org.eclipse.ui.examples.buildmonitor/plugin.xml new file mode 100644 index 00000000000..ade5733ec2b --- /dev/null +++ b/resources/examples/org.eclipse.ui.examples.buildmonitor/plugin.xml @@ -0,0 +1,17 @@ + + + + + + + + diff --git a/resources/examples/org.eclipse.ui.examples.buildmonitor/src/org/eclipse/ui/examples/buildmonitor/BuildMonitorView.java b/resources/examples/org.eclipse.ui.examples.buildmonitor/src/org/eclipse/ui/examples/buildmonitor/BuildMonitorView.java new file mode 100644 index 00000000000..261e65b0c33 --- /dev/null +++ b/resources/examples/org.eclipse.ui.examples.buildmonitor/src/org/eclipse/ui/examples/buildmonitor/BuildMonitorView.java @@ -0,0 +1,238 @@ +/******************************************************************************* + * Copyright (c) 2026 Vogella GmbH and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package org.eclipse.ui.examples.buildmonitor; + +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Locale; + +import org.eclipse.core.resources.IncrementalProjectBuilder; +import org.eclipse.jface.action.Action; +import org.eclipse.jface.action.IToolBarManager; +import org.eclipse.jface.viewers.ColumnLabelProvider; +import org.eclipse.jface.viewers.ITreeContentProvider; +import org.eclipse.jface.viewers.TreeViewer; +import org.eclipse.jface.viewers.TreeViewerColumn; +import org.eclipse.swt.SWT; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Tree; +import org.eclipse.ui.examples.buildmonitor.BuildRecorder.BuilderEntry; +import org.eclipse.ui.examples.buildmonitor.BuildRecorder.ProjectEntry; +import org.eclipse.ui.examples.buildmonitor.BuildRecorder.Session; +import org.eclipse.ui.part.ViewPart; + +/** + * View that shows recent build sessions with per-project and per-builder timing. + * Sessions are top-level rows; expand to see per-project aggregates, expand again + * to see individual builder runs. + */ +public class BuildMonitorView extends ViewPart { + + private static final DateTimeFormatter TIME = DateTimeFormatter.ofPattern("HH:mm:ss").withZone(ZoneId.systemDefault()); + + private BuildRecorder recorder; + private BuildRecorder.Listener recorderListener; + private TreeViewer viewer; + + @Override + public void createPartControl(Composite parent) { + recorder = new BuildRecorder(); + recorder.start(); + + viewer = new TreeViewer(parent, SWT.FULL_SELECTION | SWT.H_SCROLL | SWT.V_SCROLL); + Tree tree = viewer.getTree(); + tree.setHeaderVisible(true); + tree.setLinesVisible(true); + + addColumn("Build / Project / Builder", 320, new ColumnLabelProvider() { + @Override + public String getText(Object element) { + if (element instanceof Session s) { + return "Build " + TIME.format(s.getStartedAt()) + " — " + kindLabel(s.getBuildKind()); + } + if (element instanceof ProjectEntry p) { + return p.getProject().getName(); + } + if (element instanceof BuilderEntry b) { + return b.getBuilderName() == null ? "" : b.getBuilderName(); + } + return String.valueOf(element); + } + }); + addColumn("Pure build time", 120, new ColumnLabelProvider() { + @Override + public String getText(Object element) { + if (element instanceof Session s) { + long sum = 0; + for (ProjectEntry p : s.getProjects()) { + sum += p.getPureBuildTimeNanos(); + } + return formatMillis(sum); + } + if (element instanceof ProjectEntry p) { + return formatMillis(p.getPureBuildTimeNanos()); + } + if (element instanceof BuilderEntry b) { + return formatMillis(b.getDurationNanos()); + } + return ""; + } + }); + addColumn("Time until ready", 140, new ColumnLabelProvider() { + @Override + public String getText(Object element) { + if (element instanceof Session s) { + return formatMillis(s.getDurationNanos()); + } + if (element instanceof ProjectEntry p) { + return formatMillis(p.getTimeUntilReadyNanos()); + } + return ""; + } + }); + + viewer.setContentProvider(new BuildMonitorContentProvider()); + viewer.setInput(recorder); + + recorderListener = this::refreshAsync; + recorder.addListener(recorderListener); + + IToolBarManager toolBar = getViewSite().getActionBars().getToolBarManager(); + toolBar.add(new Action("Clear") { + @Override + public void run() { + recorder.clear(); + } + }); + } + + @Override + public void setFocus() { + viewer.getControl().setFocus(); + } + + @Override + public void dispose() { + if (recorder != null) { + if (recorderListener != null) { + recorder.removeListener(recorderListener); + } + recorder.stop(); + } + super.dispose(); + } + + private void addColumn(String title, int width, ColumnLabelProvider provider) { + TreeViewerColumn col = new TreeViewerColumn(viewer, SWT.NONE); + col.getColumn().setText(title); + col.getColumn().setWidth(width); + col.setLabelProvider(provider); + } + + private void refreshAsync() { + if (viewer == null || viewer.getControl().isDisposed()) { + return; + } + Display display = viewer.getControl().getDisplay(); + if (display.isDisposed()) { + return; + } + display.asyncExec(() -> { + if (viewer != null && !viewer.getControl().isDisposed()) { + viewer.refresh(); + } + }); + } + + private static String formatMillis(long nanos) { + if (nanos <= 0) { + return "—"; + } + double ms = nanos / 1_000_000.0; + if (ms < 1) { + return String.format(Locale.ROOT, "%.2f ms", ms); + } + if (ms < 1000) { + return String.format(Locale.ROOT, "%.0f ms", ms); + } + return String.format(Locale.ROOT, "%.2f s", ms / 1000.0); + } + + private static boolean builderDidWork(BuilderEntry b) { + return b.getDurationNanos() >= SKIPPED_BUILDER_THRESHOLD_NANOS; + } + + private static boolean projectDidWork(ProjectEntry p) { + return p.getBuilders().stream().anyMatch(BuildMonitorView::builderDidWork); + } + + private static String kindLabel(int kind) { + return switch (kind) { + case IncrementalProjectBuilder.FULL_BUILD -> "FULL"; + case IncrementalProjectBuilder.INCREMENTAL_BUILD -> "INCREMENTAL"; + case IncrementalProjectBuilder.AUTO_BUILD -> "AUTO"; + case IncrementalProjectBuilder.CLEAN_BUILD -> "CLEAN"; + default -> "KIND=" + kind; + }; + } + + /** Hide builders that short-circuited via needsBuild (< 100 µs overhead). */ + private static final long SKIPPED_BUILDER_THRESHOLD_NANOS = 100_000L; + + private static final class BuildMonitorContentProvider implements ITreeContentProvider { + @Override + public Object[] getElements(Object input) { + if (input instanceof BuildRecorder r) { + List sessions = r.getSessions(); + // newest first + Object[] arr = sessions.toArray(); + Collections.reverse(Arrays.asList(arr)); + return arr; + } + return new Object[0]; + } + + @Override + public Object[] getChildren(Object parent) { + if (parent instanceof Session s) { + return s.getProjects().stream() + .filter(BuildMonitorView::projectDidWork) + .toArray(); + } + if (parent instanceof ProjectEntry p) { + return p.getBuilders().stream() + .filter(BuildMonitorView::builderDidWork) + .toArray(); + } + return new Object[0]; + } + + @Override + public Object getParent(Object element) { + return null; + } + + @Override + public boolean hasChildren(Object element) { + if (element instanceof Session s) { + return s.getProjects().stream().anyMatch(BuildMonitorView::projectDidWork); + } + if (element instanceof ProjectEntry p) { + return p.getBuilders().stream().anyMatch(BuildMonitorView::builderDidWork); + } + return false; + } + } +} diff --git a/resources/examples/org.eclipse.ui.examples.buildmonitor/src/org/eclipse/ui/examples/buildmonitor/BuildRecorder.java b/resources/examples/org.eclipse.ui.examples.buildmonitor/src/org/eclipse/ui/examples/buildmonitor/BuildRecorder.java new file mode 100644 index 00000000000..479488b6648 --- /dev/null +++ b/resources/examples/org.eclipse.ui.examples.buildmonitor/src/org/eclipse/ui/examples/buildmonitor/BuildRecorder.java @@ -0,0 +1,299 @@ +/******************************************************************************* + * Copyright (c) 2026 Vogella GmbH and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package org.eclipse.ui.examples.buildmonitor; + +import java.time.Instant; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Deque; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CopyOnWriteArrayList; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResourceChangeEvent; +import org.eclipse.core.resources.IResourceChangeListener; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.ILog; + +/** + * Listens to workspace build events and per-builder build events, and records + * a session tree that the view can display. + *

    + * One {@link Session} is created for each workspace-level PRE_BUILD / POST_BUILD + * pair. Inside a session, per-builder events are attributed to their + * {@link ProjectEntry} and {@link BuilderEntry}. To match PRE / POST events + * correctly under parallel builds, each thread uses its own stack of + * in-progress builder entries; a given builder runs synchronously on one + * thread, so push-on-PRE / pop-on-POST is race-free. + */ +public final class BuildRecorder { + + public interface Listener { + void sessionsChanged(); + } + + private static final int MAX_SESSIONS = 50; + + private final int mask = IResourceChangeEvent.PRE_BUILD | IResourceChangeEvent.POST_BUILD + | IResourceChangeEvent.PRE_PROJECT_BUILD | IResourceChangeEvent.POST_PROJECT_BUILD; + + private final IResourceChangeListener eventListener = this::onResourceChangeEvent; + + private final List sessions = Collections.synchronizedList(new ArrayList<>()); + private final List listeners = new CopyOnWriteArrayList<>(); + + private final ThreadLocal> threadStack = ThreadLocal.withInitial(ArrayDeque::new); + + private volatile Session currentSession; + + public void start() { + ResourcesPlugin.getWorkspace().addResourceChangeListener(eventListener, mask); + } + + public void stop() { + ResourcesPlugin.getWorkspace().removeResourceChangeListener(eventListener); + } + + public void addListener(Listener listener) { + listeners.add(listener); + } + + public void removeListener(Listener listener) { + listeners.remove(listener); + } + + public List getSessions() { + synchronized (sessions) { + return new ArrayList<>(sessions); + } + } + + public void clear() { + synchronized (sessions) { + sessions.clear(); + } + fireChanged(); + } + + private void onResourceChangeEvent(IResourceChangeEvent event) { + switch (event.getType()) { + case IResourceChangeEvent.PRE_BUILD -> onWorkspacePreBuild(event); + case IResourceChangeEvent.POST_BUILD -> onWorkspacePostBuild(event); + case IResourceChangeEvent.PRE_PROJECT_BUILD -> onProjectPreBuild(event); + case IResourceChangeEvent.POST_PROJECT_BUILD -> onProjectPostBuild(event); + default -> { /* ignore */ } + } + } + + private void onWorkspacePreBuild(IResourceChangeEvent event) { + Session session = new Session(System.nanoTime(), Instant.now(), event.getBuildKind()); + currentSession = session; + } + + private void onWorkspacePostBuild(IResourceChangeEvent event) { + Session session = currentSession; + if (session == null) { + return; + } + session.finish(System.nanoTime()); + currentSession = null; + synchronized (sessions) { + sessions.add(session); + while (sessions.size() > MAX_SESSIONS) { + sessions.remove(0); + } + } + fireChanged(); + } + + private void onProjectPreBuild(IResourceChangeEvent event) { + Session session = currentSession; + if (session == null || !(event.getSource() instanceof IProject project)) { + return; + } + BuilderEntry entry = new BuilderEntry(event.getBuilderName(), event.getBuildKind(), System.nanoTime()); + session.addBuilderStart(project, entry); + threadStack.get().push(entry); + } + + private void onProjectPostBuild(IResourceChangeEvent event) { + Deque stack = threadStack.get(); + BuilderEntry entry = stack.pollFirst(); + if (entry == null) { + return; + } + entry.finish(System.nanoTime()); + Session session = currentSession; + if (session != null && event.getSource() instanceof IProject project) { + session.noteBuilderEnd(project, entry); + } + } + + private void fireChanged() { + for (Listener l : listeners) { + try { + l.sessionsChanged(); + } catch (RuntimeException e) { + // a broken listener must not poison the notification, but log it + ILog.get().error("Build Monitor listener threw while handling sessionsChanged", e); //$NON-NLS-1$ + } + } + } + + public static final class Session { + private final long startNanos; + private final Instant startedAt; + private final int buildKind; + private final Map projects = Collections.synchronizedMap(new LinkedHashMap<>()); + private volatile long endNanos = -1; + + Session(long startNanos, Instant startedAt, int buildKind) { + this.startNanos = startNanos; + this.startedAt = startedAt; + this.buildKind = buildKind; + } + + void addBuilderStart(IProject project, BuilderEntry entry) { + projects.computeIfAbsent(project, ProjectEntry::new).add(entry, startNanos); + } + + void noteBuilderEnd(IProject project, BuilderEntry entry) { + ProjectEntry p = projects.get(project); + if (p != null) { + p.noteEnd(entry); + } + } + + void finish(long endNanos) { + this.endNanos = endNanos; + } + + public Instant getStartedAt() { + return startedAt; + } + + public int getBuildKind() { + return buildKind; + } + + public long getDurationNanos() { + return endNanos < 0 ? System.nanoTime() - startNanos : endNanos - startNanos; + } + + public long getStartNanos() { + return startNanos; + } + + public List getProjects() { + synchronized (projects) { + return new ArrayList<>(projects.values()); + } + } + } + + public static final class ProjectEntry { + private final IProject project; + private final List builders = Collections.synchronizedList(new ArrayList<>()); + private volatile long lastEndNanos = Long.MIN_VALUE; + private volatile long sessionStartNanos = -1; + + ProjectEntry(IProject project) { + this.project = project; + } + + synchronized void add(BuilderEntry entry, long sessionStart) { + if (sessionStartNanos < 0) { + sessionStartNanos = sessionStart; + } + builders.add(entry); + } + + synchronized void noteEnd(BuilderEntry entry) { + if (entry.getEndNanos() > lastEndNanos) { + lastEndNanos = entry.getEndNanos(); + } + } + + public IProject getProject() { + return project; + } + + public List getBuilders() { + synchronized (builders) { + return new ArrayList<>(builders); + } + } + + /** + * Pure build time: sum of each builder's (end - start). + */ + public long getPureBuildTimeNanos() { + long sum = 0; + synchronized (builders) { + for (BuilderEntry b : builders) { + sum += b.getDurationNanos(); + } + } + return sum; + } + + /** + * Time until ready: from the start of the enclosing workspace build to + * the moment this project's last builder finished. + */ + public long getTimeUntilReadyNanos() { + if (lastEndNanos == Long.MIN_VALUE) { + return 0; + } + return lastEndNanos - sessionStartNanos; + } + } + + public static final class BuilderEntry { + private final String builderName; + private final int buildKind; + private final long startNanos; + private volatile long endNanos = -1; + + BuilderEntry(String builderName, int buildKind, long startNanos) { + this.builderName = builderName; + this.buildKind = buildKind; + this.startNanos = startNanos; + } + + void finish(long endNanos) { + this.endNanos = endNanos; + } + + public String getBuilderName() { + return builderName; + } + + public int getBuildKind() { + return buildKind; + } + + public long getStartNanos() { + return startNanos; + } + + public long getEndNanos() { + return endNanos; + } + + public long getDurationNanos() { + return endNanos < 0 ? 0 : endNanos - startNanos; + } + } +} diff --git a/resources/examples/pom.xml b/resources/examples/pom.xml index b113229b8f9..5452faa876f 100644 --- a/resources/examples/pom.xml +++ b/resources/examples/pom.xml @@ -25,5 +25,6 @@ org.eclipse.ui.examples.filesystem + org.eclipse.ui.examples.buildmonitor