Skip to content

Defer IWorkspace service registration onto a background job#5

Open
vogella wants to merge 1 commit into
masterfrom
defer-iworkspace-registration
Open

Defer IWorkspace service registration onto a background job#5
vogella wants to merge 1 commit into
masterfrom
defer-iworkspace-registration

Conversation

@vogella
Copy link
Copy Markdown
Owner

@vogella vogella commented Apr 22, 2026

Summary

  • context.registerService(IWorkspace.class, workspace, null) dispatches ServiceEvent.REGISTERED synchronously to every ServiceTracker<IWorkspace> listener. Several listeners (e.g. save-participant registrations in DebugPlugin, LaunchingPlugin) do non-trivial work that blocks the startup critical path by ~250 ms on a medium workspace.
  • Schedule the registration on a system Job so the fan-out runs off the main thread. removedService now joins the job before unregistering to preserve shutdown ordering.
  • ResourcesPlugin.getWorkspace() is unaffected — it reads the field directly rather than going through the OSGi service.

Test plan

  • Re-measure startup with the existing instrumentation: expect openInstanceLocationTracker leaf (~257 ms) to drop near zero, with the work reappearing off the critical path.
  • Verify startup.complete headline drops by a comparable amount; if not, something downstream is serialized on the IWorkspace service.
  • Exercise shutdown (bundle stop / framework stop) to confirm removedService joins the job correctly with no spurious warnings.
  • Run org.eclipse.core.tests.resources bundle test suite.
  • Smoke test: launch IDE, confirm save participants still run (e.g. Debug, JDT Launching) by invoking a workspace save.

🤖 Generated with Claude Code

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 updates ResourcesPlugin to perform IWorkspace service registration asynchronously, optimizing the startup path. The review feedback suggests enhancing the lifecycle management of the registration job by checking for cancellation within the task, explicitly canceling the job during service removal, and clearing service registration references to prevent potential leaks or stale states.

Comment on lines +603 to +613
Job job = Job.create("Register IWorkspace service", (IProgressMonitor monitor) -> { //$NON-NLS-1$
Workspace ws = workspace;
if (ws != null) {
try {
workspaceRegistration = context.registerService(IWorkspace.class, ws, null);
} catch (IllegalStateException e) {
// bundle context became invalid during shutdown
}
}
return Status.OK_STATUS;
});
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

To prevent potential service leaks during rapid startup/shutdown or service removal, the background job should check if it has been canceled before proceeding with the service registration. This works in conjunction with calling job.cancel() in removedService.

Suggested change
Job job = Job.create("Register IWorkspace service", (IProgressMonitor monitor) -> { //$NON-NLS-1$
Workspace ws = workspace;
if (ws != null) {
try {
workspaceRegistration = context.registerService(IWorkspace.class, ws, null);
} catch (IllegalStateException e) {
// bundle context became invalid during shutdown
}
}
return Status.OK_STATUS;
});
Job job = Job.create("Register IWorkspace service", (IProgressMonitor monitor) -> { //$NON-NLS-1$
Workspace ws = workspace;
if (ws != null && !monitor.isCanceled()) {
try {
workspaceRegistration = context.registerService(IWorkspace.class, ws, null);
} catch (IllegalStateException e) {
// bundle context became invalid during shutdown
}
}
return Status.OK_STATUS;
});

Comment on lines +635 to 650
Job job = workspaceRegistrationJob;
if (job != null) {
try {
job.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
ServiceRegistration<IWorkspace> registration = workspaceRegistration;
if (registration != null) {
try {
registration.unregister();
} catch (RuntimeException e) {
getLog().log(Status.warning("Unregistering workspaces throws an exception", e)); //$NON-NLS-1$
}
}
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 recommended to cancel the registration job before joining it to ensure that if the job hasn't started yet, it won't run at all. Additionally, nulling out the registration fields after unregistering is a good practice to avoid stale references and potential double-unregistration issues.

				Job job = workspaceRegistrationJob;
				if (job != null) {
					job.cancel();
					try {
						job.join();
					} catch (InterruptedException e) {
						Thread.currentThread().interrupt();
					}
					workspaceRegistrationJob = null;
				}
				ServiceRegistration<IWorkspace> registration = workspaceRegistration;
				if (registration != null) {
					workspaceRegistration = null;
					try {
						registration.unregister();
					} catch (RuntimeException e) {
						getLog().log(Status.warning("Unregistering workspaces throws an exception", e)); //$NON-NLS-1$
					}
				}

registerService(IWorkspace, ...) dispatches ServiceEvent.REGISTERED
synchronously to every ServiceTracker<IWorkspace> listener. Several
listeners (e.g. save-participant registrations in DebugPlugin and
LaunchingPlugin) do non-trivial work that blocks the startup critical
path by ~250 ms on a medium workspace.

Schedule the registration on a system Job so the fan-out runs off the
main thread. removedService joins the job before unregistering to
preserve shutdown ordering.

Why this is safe
----------------

- ResourcesPlugin.getWorkspace() reads the workspace field directly
  rather than going through the OSGi service registry. The field is
  populated synchronously in addingService before the Job is scheduled,
  so every static caller sees the workspace the moment ResourcesPlugin
  bundle activation returns. The Job only defers OSGi service
  publication, not workspace creation.

- ServiceTracker<IWorkspace> consumers are inherently asynchronous:
  they register a tracker and react to the addingService callback
  whenever it arrives. Receiving that callback a few milliseconds
  later, on a worker thread, does not change the contract.

- Shutdown ordering is preserved: removedService joins the registration
  job before calling unregister(), so the OSGi ServiceRegistration
  handle always refers to a completed registration (or stays null and
  the null-check skips unregister).

- The Job runnable does not check monitor.isCanceled(), so a cancel
  issued while the job is running has no effect. There is no user-
  or framework-initiated cancel path for this job under normal
  operation.

Areas to watch for potential regressions
----------------------------------------

- ServiceTracker<IWorkspace> addingService callbacks now run on a Jobs
  worker thread instead of the main thread. Listeners that implicitly
  assume UI-thread or main-thread affinity in addingService could
  break. Known in-tree consumers (DebugPlugin, jdt LaunchingPlugin)
  only register save participants and do not depend on thread
  identity.

- Code that looks up IWorkspace via
  context.getServiceReference(IWorkspace.class) immediately after
  bundle activation will now briefly return null until the Job has
  run. Static ResourcesPlugin.getWorkspace() is unaffected.

- If startup.complete headline time does not drop by a comparable
  amount after this change, something downstream is serialised on the
  IWorkspace OSGi service in a way the trace did not show, and the
  fix needs to be reconsidered.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vogella vogella force-pushed the defer-iworkspace-registration branch from c82101f to 4045954 Compare April 22, 2026 06:19
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