From 4045954c8e2ff5d6f9169a733e0a69030b79b548 Mon Sep 17 00:00:00 2001 From: Lars Vogel Date: Wed, 22 Apr 2026 07:47:44 +0200 Subject: [PATCH] Defer IWorkspace service registration onto a background job registerService(IWorkspace, ...) dispatches ServiceEvent.REGISTERED synchronously to every ServiceTracker 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 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 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) --- .../core/resources/ResourcesPlugin.java | 44 ++++++++++++++++--- 1 file changed, 38 insertions(+), 6 deletions(-) diff --git a/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/resources/ResourcesPlugin.java b/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/resources/ResourcesPlugin.java index efc710e73d2..76ce99b9724 100644 --- a/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/resources/ResourcesPlugin.java +++ b/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/resources/ResourcesPlugin.java @@ -568,7 +568,8 @@ public void start(BundleContext context) throws Exception { private final class WorkspaceInitCustomizer implements ServiceTrackerCustomizer { private final BundleContext context; private volatile Workspace workspace; - private ServiceRegistration workspaceRegistration; + private volatile ServiceRegistration workspaceRegistration; + private volatile Job workspaceRegistrationJob; private WorkspaceInitCustomizer(BundleContext context) { this.context = context; @@ -592,7 +593,27 @@ public Workspace addingService(ServiceReference reference) { if (!result.isOK()) { getLog().log(result); } - workspaceRegistration = context.registerService(IWorkspace.class, workspace, null); + // Publish IWorkspace asynchronously: registerService dispatches + // ServiceEvent.REGISTERED synchronously to every + // ServiceTracker listener, and several of them + // (e.g. save-participant registrations) do non-trivial work + // that would otherwise block the startup critical path. + // Static callers using ResourcesPlugin.getWorkspace() are + // unaffected since it reads the field directly. + 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.setSystem(true); + workspaceRegistrationJob = job; + job.schedule(); return workspace; } catch (CoreException e) { getLog().log(e.getStatus()); @@ -611,10 +632,21 @@ public void modifiedService(ServiceReference reference, Workspace serv @Override public void removedService(ServiceReference reference, Workspace service) { if (service == workspace) { - try { - workspaceRegistration.unregister(); - } catch (RuntimeException e) { - getLog().log(Status.warning("Unregistering workspaces throws an exception", e)); //$NON-NLS-1$ + Job job = workspaceRegistrationJob; + if (job != null) { + try { + job.join(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + ServiceRegistration registration = workspaceRegistration; + if (registration != null) { + try { + registration.unregister(); + } catch (RuntimeException e) { + getLog().log(Status.warning("Unregistering workspaces throws an exception", e)); //$NON-NLS-1$ + } } try { service.close(null);