From bdca68c6bd15195ae31738ce7d349b93fa9bc002 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 26 Oct 2025 04:41:15 +0000 Subject: [PATCH 1/4] Initial plan From 8b0310d6731f1d1589dba14af33f6bed6f0efd31 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 26 Oct 2025 04:48:29 +0000 Subject: [PATCH 2/4] Implement job-based WorkspaceDeltaProcessor Co-authored-by: laeubi <1331477+laeubi@users.noreply.github.com> --- .../internal/WorkspaceDeltaProcessor.java | 96 ++++++++++++++++++- .../tools/internal/provisional/ApiPlugin.java | 1 + 2 files changed, 95 insertions(+), 2 deletions(-) diff --git a/apitools/org.eclipse.pde.api.tools/src/org/eclipse/pde/api/tools/internal/WorkspaceDeltaProcessor.java b/apitools/org.eclipse.pde.api.tools/src/org/eclipse/pde/api/tools/internal/WorkspaceDeltaProcessor.java index 559527c4c82..f20228d2bba 100644 --- a/apitools/org.eclipse.pde.api.tools/src/org/eclipse/pde/api/tools/internal/WorkspaceDeltaProcessor.java +++ b/apitools/org.eclipse.pde.api.tools/src/org/eclipse/pde/api/tools/internal/WorkspaceDeltaProcessor.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2010, 2016 IBM Corporation and others. + * Copyright (c) 2010, 2025 IBM Corporation and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 @@ -13,6 +13,8 @@ *******************************************************************************/ package org.eclipse.pde.api.tools.internal; +import java.util.concurrent.ConcurrentLinkedQueue; + import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.IResourceChangeEvent; @@ -22,6 +24,10 @@ import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.core.runtime.jobs.Job; import org.eclipse.jdt.core.ElementChangedEvent; import org.eclipse.jdt.core.IElementChangedListener; import org.eclipse.jdt.core.IJavaElement; @@ -30,6 +36,7 @@ import org.eclipse.jdt.core.IPackageFragment; import org.eclipse.jdt.core.IPackageFragmentRoot; import org.eclipse.jdt.core.JavaCore; +import org.eclipse.pde.api.tools.internal.builder.ApiAnalysisBuilder; import org.eclipse.pde.api.tools.internal.builder.BuildState; import org.eclipse.pde.api.tools.internal.model.ApiBaseline; import org.eclipse.pde.api.tools.internal.provisional.ApiPlugin; @@ -39,6 +46,10 @@ /** * Standard delta processor for us to track element state changes in the workspace * using {@link IJavaElementDelta}s and {@link IResourceDelta}s. + *

+ * This processor uses a background job to process changes asynchronously, avoiding + * blocking of listener threads and allowing coordination with API builder jobs. + *

* * @since 1.1 */ @@ -47,9 +58,69 @@ public class WorkspaceDeltaProcessor implements IElementChangedListener, IResour ApiBaselineManager bmanager = ApiBaselineManager.getManager(); ApiDescriptionManager dmanager = ApiDescriptionManager.getManager(); + /** + * Queue of work items to be processed by the delta processing job + */ + private final ConcurrentLinkedQueue workQueue = new ConcurrentLinkedQueue<>(); + + /** + * The background job that processes workspace changes + */ + private final DeltaProcessingJob processingJob = new DeltaProcessingJob(); + + /** + * Job for processing workspace deltas asynchronously + */ + private class DeltaProcessingJob extends Job { + + public DeltaProcessingJob() { + super("API Tools Workspace Delta Processor"); //$NON-NLS-1$ + setSystem(true); + setPriority(Job.BUILD); + } + + @Override + protected IStatus run(IProgressMonitor monitor) { + try { + // Wait for API analysis jobs to complete before processing + Job.getJobManager().join(ApiAnalysisBuilder.ApiAnalysisJob.class, monitor); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return Status.CANCEL_STATUS; + } + + // Process all queued work items + Runnable work; + while ((work = workQueue.poll()) != null && !monitor.isCanceled()) { + try { + work.run(); + } catch (Exception e) { + ApiPlugin.log("Error processing workspace delta", e); //$NON-NLS-1$ + } + } + + return Status.OK_STATUS; + } + + @Override + public boolean belongsTo(Object family) { + return super.belongsTo(family) || WorkspaceDeltaProcessor.class == family; + } + } + + /** + * Schedules work to be processed by the background job + */ + private void scheduleWork(Runnable work) { + workQueue.offer(work); + processingJob.cancel(); // Cancel any pending job + processingJob.schedule(100); // Debounce: delay to batch multiple changes + } + @Override public void elementChanged(ElementChangedEvent event) { - processJavaElementDeltas(event.getDelta().getAffectedChildren(), null); + IJavaElementDelta[] affectedChildren = event.getDelta().getAffectedChildren(); + scheduleWork(() -> processJavaElementDeltas(affectedChildren, null)); } /** @@ -217,6 +288,13 @@ void processJavaElementDeltas(IJavaElementDelta[] deltas, IJavaProject project) @Override public void resourceChanged(IResourceChangeEvent event) { + scheduleWork(() -> processResourceChange(event)); + } + + /** + * Processes resource change events + */ + void processResourceChange(IResourceChangeEvent event) { IResource resource = event.getResource(); switch (event.getType()) { case IResourceChangeEvent.PRE_BUILD: { @@ -289,6 +367,20 @@ public void resourceChanged(IResourceChangeEvent event) { } } + /** + * Shuts down the delta processor, canceling any pending work and waiting + * for the processing job to complete. + */ + public void shutdown() { + processingJob.cancel(); + try { + processingJob.join(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + workQueue.clear(); + } + private void cleanAndDisposeWorkspaceBaseline(IResource resource) { IJavaProject jp = (IJavaProject) JavaCore.create(resource); dmanager.clean(jp, true, true); diff --git a/apitools/org.eclipse.pde.api.tools/src/org/eclipse/pde/api/tools/internal/provisional/ApiPlugin.java b/apitools/org.eclipse.pde.api.tools/src/org/eclipse/pde/api/tools/internal/provisional/ApiPlugin.java index 4a3ffee9f6b..a03d16500da 100644 --- a/apitools/org.eclipse.pde.api.tools/src/org/eclipse/pde/api/tools/internal/provisional/ApiPlugin.java +++ b/apitools/org.eclipse.pde.api.tools/src/org/eclipse/pde/api/tools/internal/provisional/ApiPlugin.java @@ -696,6 +696,7 @@ public void stop(BundleContext context) throws Exception { if (deltaProcessor != null) { JavaCore.removeElementChangedListener(deltaProcessor); ResourcesPlugin.getWorkspace().removeResourceChangeListener(deltaProcessor); + deltaProcessor.shutdown(); } } finally { super.stop(context); From 453fdeb56c337f5d94056741ec62eb8dc8cbae39 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 26 Oct 2025 04:49:59 +0000 Subject: [PATCH 3/4] Fix event data capture for thread safety Co-authored-by: laeubi <1331477+laeubi@users.noreply.github.com> --- .../internal/WorkspaceDeltaProcessor.java | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/apitools/org.eclipse.pde.api.tools/src/org/eclipse/pde/api/tools/internal/WorkspaceDeltaProcessor.java b/apitools/org.eclipse.pde.api.tools/src/org/eclipse/pde/api/tools/internal/WorkspaceDeltaProcessor.java index f20228d2bba..74901de4530 100644 --- a/apitools/org.eclipse.pde.api.tools/src/org/eclipse/pde/api/tools/internal/WorkspaceDeltaProcessor.java +++ b/apitools/org.eclipse.pde.api.tools/src/org/eclipse/pde/api/tools/internal/WorkspaceDeltaProcessor.java @@ -288,15 +288,21 @@ void processJavaElementDeltas(IJavaElementDelta[] deltas, IJavaProject project) @Override public void resourceChanged(IResourceChangeEvent event) { - scheduleWork(() -> processResourceChange(event)); + // Capture event data in the listener thread before scheduling + final IResource resource = event.getResource(); + final int eventType = event.getType(); + final int buildKind = event.getBuildKind(); + final IResourceDelta delta = event.getDelta(); + + scheduleWork(() -> processResourceChange(resource, eventType, buildKind, delta)); } /** * Processes resource change events */ - void processResourceChange(IResourceChangeEvent event) { - IResource resource = event.getResource(); - switch (event.getType()) { + void processResourceChange(IResource initialResource, int eventType, int buildKind, IResourceDelta delta) { + IResource resource = initialResource; + switch (eventType) { case IResourceChangeEvent.PRE_BUILD: { if (ApiPlugin.DEBUG_WORKSPACE_DELTA_PROCESSOR) { if (resource == null) { @@ -306,12 +312,11 @@ void processResourceChange(IResourceChangeEvent event) { } } - if (event.getBuildKind() == IncrementalProjectBuilder.AUTO_BUILD + if (buildKind == IncrementalProjectBuilder.AUTO_BUILD && !ResourcesPlugin.getWorkspace().isAutoBuilding()) { return; } - IResourceDelta delta = event.getDelta(); if (delta != null) { IResourceDelta[] children = delta.getAffectedChildren(IResourceDelta.CHANGED); for (IResourceDelta element : children) { @@ -342,11 +347,11 @@ void processResourceChange(IResourceChangeEvent event) { } case IResourceChangeEvent.PRE_CLOSE: case IResourceChangeEvent.PRE_DELETE: { - if (resource.getType() == IResource.PROJECT) { + if (resource != null && resource.getType() == IResource.PROJECT) { IProject project = (IProject) resource; if (Util.isApiProject(project) || PluginProject.isJavaProject(project)) { if (ApiPlugin.DEBUG_WORKSPACE_DELTA_PROCESSOR) { - if (event.getType() == IResourceChangeEvent.PRE_CLOSE) { + if (eventType == IResourceChangeEvent.PRE_CLOSE) { System.out.println("processed PRE_CLOSE delta for project: [" + resource.getName() + "]"); //$NON-NLS-1$ //$NON-NLS-2$ } else { if (ApiPlugin.DEBUG_WORKSPACE_DELTA_PROCESSOR) { From 4ba14ecf3a74e12ccb9684ea0d3576d1bf70a860 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 26 Oct 2025 04:51:45 +0000 Subject: [PATCH 4/4] Address code review feedback - add javadoc and remove redundant check Co-authored-by: laeubi <1331477+laeubi@users.noreply.github.com> --- .../pde/api/tools/internal/WorkspaceDeltaProcessor.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/apitools/org.eclipse.pde.api.tools/src/org/eclipse/pde/api/tools/internal/WorkspaceDeltaProcessor.java b/apitools/org.eclipse.pde.api.tools/src/org/eclipse/pde/api/tools/internal/WorkspaceDeltaProcessor.java index 74901de4530..841330179d5 100644 --- a/apitools/org.eclipse.pde.api.tools/src/org/eclipse/pde/api/tools/internal/WorkspaceDeltaProcessor.java +++ b/apitools/org.eclipse.pde.api.tools/src/org/eclipse/pde/api/tools/internal/WorkspaceDeltaProcessor.java @@ -299,6 +299,11 @@ public void resourceChanged(IResourceChangeEvent event) { /** * Processes resource change events + * + * @param initialResource the resource associated with the event (may be null for workspace-level events) + * @param eventType the type of event (e.g., PRE_BUILD, PRE_CLOSE, PRE_DELETE) + * @param buildKind the kind of build (for PRE_BUILD events) + * @param delta the resource delta (may be null) */ void processResourceChange(IResource initialResource, int eventType, int buildKind, IResourceDelta delta) { IResource resource = initialResource; @@ -354,9 +359,7 @@ void processResourceChange(IResource initialResource, int eventType, int buildKi if (eventType == IResourceChangeEvent.PRE_CLOSE) { System.out.println("processed PRE_CLOSE delta for project: [" + resource.getName() + "]"); //$NON-NLS-1$ //$NON-NLS-2$ } else { - if (ApiPlugin.DEBUG_WORKSPACE_DELTA_PROCESSOR) { - System.out.println("processed PRE_DELETE delta for project: [" + resource.getName() + "]"); //$NON-NLS-1$ //$NON-NLS-2$ - } + System.out.println("processed PRE_DELETE delta for project: [" + resource.getName() + "]"); //$NON-NLS-1$ //$NON-NLS-2$ } } bmanager.disposeWorkspaceBaseline();