Skip to content

Build Monitor: per-builder events and example view#6

Open
vogella wants to merge 3 commits into
masterfrom
feature/build-monitor-per-builder-events
Open

Build Monitor: per-builder events and example view#6
vogella wants to merge 3 commits into
masterfrom
feature/build-monitor-per-builder-events

Conversation

@vogella
Copy link
Copy Markdown
Owner

@vogella vogella commented May 18, 2026

CI verification PR. Adds PRE_PROJECT_BUILD / POST_PROJECT_BUILD events to IResourceChangeEvent, fired per builder from BuildManager.basicBuild, and a new org.eclipse.ui.examples.buildmonitor example view that consumes them.

This branch is stacked on top of upstream PR eclipse-platform#2664 (Replace per-bit listener count fields with an AtomicIntegerArray on eclipse-platform/eclipse.platform). The array-based bookkeeping makes the new 64/128 bitmasks work without any change to ResourceChangeListenerList.


Fallback if upstream PR eclipse-platform#2664 is rejected

If the AtomicIntegerArray refactor is not merged, the per-builder events commit needs the following addition to ResourceChangeListenerList.java so that hasListenerFor(64) and hasListenerFor(128) actually return true for registered listeners (otherwise both new events are silently dropped):

@@ -60,6 +61,8 @@ public class ResourceChangeListenerList {
 	private volatile int count8 = 0;
 	private volatile int count16 = 0;
 	private volatile int count32 = 0;
+	private volatile int count64 = 0;
+	private volatile int count128 = 0;

@@ -114,6 +117,12 @@ public class ResourceChangeListenerList {
 		if ((mask & 32) != 0) {
 			count32++;
 		}
+		if ((mask & 64) != 0) {
+			count64++;
+		}
+		if ((mask & 128) != 0) {
+			count128++;
+		}
 	}

@@ -138,6 +147,10 @@ public class ResourceChangeListenerList {
 			return count16 > 0;
 		case 32:
 			return count32 > 0;
+		case 64:
+			return count64 > 0;
+		case 128:
+			return count128 > 0;
 		default:
 			return false;
 		}

@@ -170,6 +183,8 @@ public class ResourceChangeListenerList {
 		count8 = 0;
 		count16 = 0;
 		count32 = 0;
+		count64 = 0;
+		count128 = 0;
 	}

@@ -191,6 +206,12 @@ public class ResourceChangeListenerList {
 		if ((mask & 32) != 0) {
 			count32--;
 		}
+		if ((mask & 64) != 0) {
+			count64--;
+		}
+		if ((mask & 128) != 0) {
+			count128--;
+		}
 	}

Recovery flow in that case: git rebase --onto origin/master aa8786ac75 to drop the refactor commit, then apply the diff above to the per-builder events commit via git commit --amend.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces per-builder build events (PRE_PROJECT_BUILD and POST_PROJECT_BUILD) to the Eclipse Resources framework, enabling more granular monitoring of the build process. The implementation includes updates to the event notification system, a new 'Build Monitor' example plugin, and comprehensive unit tests. Review feedback identified a potential violation of the IResourceChangeEvent contract regarding the timing of the POST_PROJECT_BUILD event relative to the workspace lock. Additionally, improvements were suggested for the example plugin to safely handle widget disposal during asynchronous UI refreshes and to ensure that exceptions from listeners are logged rather than silently ignored.

Comment on lines +310 to +313
try {
workspace.broadcastProjectBuildEvent(builderProject, builderName,
IResourceChangeEvent.POST_PROJECT_BUILD, trigger);
} finally {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The POST_PROJECT_BUILD event is currently fired before the workspace lock is re-acquired (via endUnprotected). This violates the general contract of IResourceChangeEvent which states that the workspace is closed for changes during notification. Since PRE_PROJECT_BUILD is fired with the lock held, POST_PROJECT_BUILD should ideally also be fired after endUnprotected to ensure consistency and that listeners see a locked workspace, unless the concurrent delivery while the lock is released is explicitly intended for performance reasons.

Comment on lines +141 to +151
private void refreshAsync() {
Display display = viewer.getControl().getDisplay();
if (display.isDisposed()) {
return;
}
display.asyncExec(() -> {
if (!viewer.getControl().isDisposed()) {
viewer.refresh();
}
});
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The refreshAsync method should safely handle cases where the view or its control might be disposed. Accessing viewer.getControl().getDisplay() on a disposed widget will throw an SWTException. Although the listener is removed in dispose(), events already in the queue or concurrent notifications could still trigger this.

	private void refreshAsync() {
		if (viewer == null || viewer.getControl() == null || viewer.getControl().isDisposed()) {
			return;
		}
		Display display = viewer.getControl().getDisplay();
		if (display.isDisposed()) {
			return;
		}
		display.asyncExec(() -> {
			if (viewer != null && viewer.getControl() != null && !viewer.getControl().isDisposed()) {
				viewer.refresh();
			}
		});
	}

Comment on lines +146 to +148
} catch (RuntimeException ignored) {
// a broken listener must not poison the notification
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

It is generally better to log exceptions from listeners rather than silently ignoring them, even in an example plugin. This helps in diagnosing issues with custom listeners.

			} catch (RuntimeException e) {
				// a broken listener must not poison the notification
				e.printStackTrace();
			}

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces per-builder build events, specifically PRE_PROJECT_BUILD and POST_PROJECT_BUILD, to the Eclipse core resources framework. These events enable granular tracking of individual builders within a project build cycle, supported by updates to the event notification system and a new 'Build Monitor' example plugin. A potential NullPointerException was identified in BuildManager.java where builder.getCommand() might return null, and a defensive check was suggested to prevent build interruptions.

Comment on lines +266 to +269
IProject builderProject = builder.getProject();
String builderName = builder.getCommand().getBuilderName();
workspace.broadcastProjectBuildEvent(builderProject, builderName,
IResourceChangeEvent.PRE_PROJECT_BUILD, trigger);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Potential NullPointerException if builder.getCommand() returns null. According to the IncrementalProjectBuilder#getCommand() documentation, the returned value may be null if the builder was not created from a build command. A defensive check should be added before accessing the builder name to ensure the build process is not interrupted by an unexpected NPE.

Suggested change
IProject builderProject = builder.getProject();
String builderName = builder.getCommand().getBuilderName();
workspace.broadcastProjectBuildEvent(builderProject, builderName,
IResourceChangeEvent.PRE_PROJECT_BUILD, trigger);
IProject builderProject = builder.getProject();
String builderName = builder.getCommand() != null ? builder.getCommand().getBuilderName() : null;
workspace.broadcastProjectBuildEvent(builderProject, builderName,
IResourceChangeEvent.PRE_PROJECT_BUILD, trigger);

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces per-builder build events (PRE_PROJECT_BUILD and POST_PROJECT_BUILD) to the Eclipse core resources framework, allowing listeners to track individual builder execution within a project build. The changes span the event notification system, workspace management, and include a new 'Build Monitor' example plugin along with unit tests. Feedback highlights a potential NullPointerException risk when accessing builder commands, suggests simplifying array reversal logic using standard library methods, and recommends using a fixed locale for formatting technical timing data to ensure consistency across environments.

int depth = -1;
ISchedulingRule rule = null;
IProject builderProject = builder.getProject();
String builderName = builder.getCommand().getBuilderName();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

While builder.getCommand() is generally expected to be non-null during a build, it is safer to perform a null check before accessing getBuilderName() to prevent a potential NullPointerException in edge cases where a builder might not be correctly initialized or associated with a command.

Comment on lines +200 to +205
Object[] arr = sessions.toArray();
for (int i = 0, j = arr.length - 1; i < j; i++, j--) {
Object tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The manual array reversal in getElements is functional but can be simplified using Collections.reverse() on a list view of the array. This improves readability and leverages standard library utilities.

Object[] arr = sessions.toArray();
Collections.reverse(Arrays.asList(arr));
return arr;

Comment on lines +153 to +165
private static String formatMillis(long nanos) {
if (nanos <= 0) {
return "—";
}
double ms = nanos / 1_000_000.0;
if (ms < 1) {
return String.format("%.2f ms", ms);
}
if (ms < 1000) {
return String.format("%.0f ms", ms);
}
return String.format("%.2f s", ms / 1000.0);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The formatMillis method uses String.format with the default locale. While usually acceptable for UI, technical timing data often benefits from a consistent format (e.g., using Locale.ROOT) to avoid variations in decimal separators (comma vs. dot) across different system locales.

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.
@vogella vogella force-pushed the feature/build-monitor-per-builder-events branch 3 times, most recently from 09460c2 to da3ec31 Compare May 18, 2026 12:52
vogella and others added 2 commits May 18, 2026 14:58
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
@vogella vogella force-pushed the feature/build-monitor-per-builder-events branch from da3ec31 to 13e747d Compare May 18, 2026 12:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant