Repository: eclipse-platform/eclipse.platform
Pull request #1778: Cleanup progress monitor handling in resource plugin
Author: login: ptziegler
Created: 2025-03-18T12:48:01Z | Merged: 2025-03-21T11:36:06Z

This resolves several trace errors thrown by an improper use of the
progress monitor while moving, deleting or copying files or folders.

- ResourceTree

The deprecated SubProgressMonitor has been replaced by a SubMonitor, due
to an improved handling of rounding errors when reporting fractional
work back to its parent. In addition, the standardMoveFile() method now
explicitly calls beginTask() on its given progress monitor, to remain
consistent with the standardMoveFolder() and standardMoveProject()
methods and to avoid an error being reported when moving files.

- DeleteVisitor

beginTask() is called on the given progress monitor by converting it to
a SubMonitor with the given number of ticks. This number is based on the
number of to-be-deleted files. When deleting a file, a SubMonitor is
used instead of the deprecated SubProgressMonitor, consuming a number of
ticks based on the total number of ticks.

- CopyVisitor

This method has been adapted similarly to the DeleteVisitor as
beginTask() also needs to be called on the given progress monitor here.
Additionally, the progress handling has been adapted to avoid creating
"zero ticks" SubMonitors, as this also causes an error.


Closes https://github.com/eclipse-platform/eclipse.platform/issues/1776


Diff:
diff --git a/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/localstore/CopyVisitor.java b/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/localstore/CopyVisitor.java
index f4fd75e3293..9687c8b95cc 100644
--- a/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/localstore/CopyVisitor.java
+++ b/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/localstore/CopyVisitor.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2000, 2015 IBM Corporation and others.
+ * Copyright (c) 2000, 2025 IBM Corporation and others.
  *
  * This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License 2.0
@@ -68,13 +68,16 @@ public class CopyVisitor implements IUnifiedTreeVisitor {
 
 	private final FileSystemResourceManager localManager;
 
-	public CopyVisitor(IResource rootSource, IResource destination, int updateFlags, IProgressMonitor monitor) {
+	/** amount of ticks consumed by the current file. Either 0 (out-of-sync) or 1 (in-sync) */
+	private int work;
+
+	public CopyVisitor(IResource rootSource, IResource destination, int updateFlags, IProgressMonitor monitor, int ticks) {
 		this.localManager = ((Resource) rootSource).getLocalManager();
 		this.rootDestination = destination;
 		this.updateFlags = updateFlags;
 		this.isDeep = (updateFlags & IResource.SHALLOW) == 0;
 		this.force = (updateFlags & IResource.FORCE) != 0;
-		this.monitor = SubMonitor.convert(monitor);
+		this.monitor = SubMonitor.convert(monitor, ticks);
 		this.segmentsToDrop = rootSource.getFullPath().segmentCount();
 		this.status = new MultiStatus(ResourcesPlugin.PI_RESOURCES, IStatus.INFO, Messages.localstore_copyProblem, null);
 	}
@@ -111,10 +114,12 @@ protected boolean copyContents(UnifiedTreeNode node, Resource source, Resource d
 
 			IFileStore sourceStore = node.getStore();
 			IFileStore destinationStore = destination.getStore();
+			SubMonitor subMonitor = SubMonitor.convert(monitor.newChild(work), 2);
+			work = 0;
 			//ensure the parent of the root destination exists (bug 126104)
 			if (destination == rootDestination)
-				destinationStore.getParent().mkdir(EFS.NONE, monitor.newChild(0));
-			sourceStore.copy(destinationStore, EFS.SHALLOW, monitor.newChild(0));
+				destinationStore.getParent().mkdir(EFS.NONE, subMonitor.newChild(1));
+			sourceStore.copy(destinationStore, EFS.SHALLOW, subMonitor.newChild(1));
 			//create the destination in the workspace
 			ResourceInfo info = localManager.getWorkspace().createResource(destination, updateFlags);
 			localManager.updateLocalSync(info, destinationStore.fetchInfo().getLastModified());
@@ -189,7 +194,7 @@ protected void synchronize(UnifiedTreeNode node) throws CoreException {
 	@Override
 	public boolean visit(UnifiedTreeNode node) throws CoreException {
 		monitor.checkCanceled();
-		int work = 1;
+		work = 1;
 		try {
 			//location can be null if based on an undefined variable
 			if (node.getStore() == null) {
@@ -221,7 +226,10 @@ public boolean visit(UnifiedTreeNode node) throws CoreException {
 			}
 			return copy(node);
 		} finally {
-			monitor.worked(work);
+			if (work != 0) {
+				monitor.worked(work);
+			}
+			work = 0;
 		}
 	}
 
diff --git a/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/localstore/DeleteVisitor.java b/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/localstore/DeleteVisitor.java
index ff9ba1891ce..3ed0de10148 100644
--- a/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/localstore/DeleteVisitor.java
+++ b/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/localstore/DeleteVisitor.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2000, 2015 IBM Corporation and others.
+ * Copyright (c) 2000, 2025 IBM Corporation and others.
  *
  * This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License 2.0
@@ -30,7 +30,7 @@
 public class DeleteVisitor implements IUnifiedTreeVisitor, ICoreConstants {
 	protected boolean force;
 	protected boolean keepHistory;
-	protected IProgressMonitor monitor;
+	protected SubMonitor monitor;
 	protected List<Resource> skipList;
 	protected MultiStatus status;
 
@@ -44,7 +44,7 @@ public DeleteVisitor(List<Resource> skipList, int flags, IProgressMonitor monito
 		this.ticks = ticks;
 		this.force = (flags & IResource.FORCE) != 0;
 		this.keepHistory = (flags & IResource.KEEP_HISTORY) != 0;
-		this.monitor = monitor;
+		this.monitor = SubMonitor.convert(monitor, ticks);
 		status = new MultiStatus(ResourcesPlugin.PI_RESOURCES, IResourceStatus.FAILED_DELETE_LOCAL, Messages.localstore_deleteProblem, null);
 	}
 
@@ -63,7 +63,7 @@ protected void delete(UnifiedTreeNode node, boolean shouldKeepHistory) {
 			int work = ticks < 0 ? 0 : ticks;
 			ticks -= work;
 			if (deleteLocalFile)
-				localFile.delete(EFS.NONE, Policy.subMonitorFor(monitor, work));
+				localFile.delete(EFS.NONE, monitor.split(work));
 			else
 				monitor.worked(work);
 			//delete from tree
diff --git a/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/localstore/FileSystemResourceManager.java b/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/localstore/FileSystemResourceManager.java
index 15931305333..2b200644e34 100644
--- a/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/localstore/FileSystemResourceManager.java
+++ b/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/localstore/FileSystemResourceManager.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2000, 2017 IBM Corporation and others.
+ * Copyright (c) 2000, 2025 IBM Corporation and others.
  *
  * This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License 2.0
@@ -378,7 +378,8 @@ public void copy(IResource target, IResource destination, int updateFlags, IProg
 			throw new ResourceException(IResourceStatus.FAILED_WRITE_LOCAL, destination.getFullPath(), message, null);
 		}
 		getHistoryStore().copyHistory(target, destination, false);
-		CopyVisitor visitor = new CopyVisitor(target, destination, updateFlags, subMonitor.split(100));
+		int totalWork = ((Resource) target).countResources(IResource.DEPTH_INFINITE, false);
+		CopyVisitor visitor = new CopyVisitor(target, destination, updateFlags, subMonitor.split(100), totalWork);
 		UnifiedTree tree = new UnifiedTree(target);
 		tree.accept(visitor, IResource.DEPTH_INFINITE);
 		IStatus status = visitor.getStatus();
diff --git a/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/ResourceTree.java b/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/ResourceTree.java
index d60f0ea05dd..370ada4b88c 100644
--- a/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/ResourceTree.java
+++ b/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/ResourceTree.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2000, 2015 IBM Corporation and others.
+ * Copyright (c) 2000, 2025 IBM Corporation and others.
  *
  * This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License 2.0
@@ -337,9 +337,8 @@ private boolean internalDeleteFile(IFile file, int flags, IProgressMonitor monit
 	 */
 	private boolean internalDeleteFolder(IFolder folder, int flags, IProgressMonitor monitor) {
 		String message = NLS.bind(Messages.resources_deleting, folder.getFullPath());
-		monitor.beginTask("", Policy.totalWork); //$NON-NLS-1$
-		monitor.subTask(message);
-		Policy.checkCanceled(monitor);
+		SubMonitor subMonitor = SubMonitor.convert(monitor, Policy.totalWork).checkCanceled();
+		subMonitor.subTask(message);
 
 		// Do nothing if the folder doesn't exist in the workspace.
 		if (!folder.exists())
@@ -360,7 +359,7 @@ private boolean internalDeleteFolder(IFolder folder, int flags, IProgressMonitor
 
 		try {
 			//this will delete local and workspace
-			localManager.delete(folder, flags, Policy.subMonitorFor(monitor, Policy.totalWork));
+			localManager.delete(folder, flags, subMonitor.split(Policy.totalWork, SubMonitor.SUPPRESS_NONE));
 		} catch (CoreException ce) {
 			message = NLS.bind(Messages.localstore_couldnotDelete, folder.getFullPath());
 			IStatus status = new ResourceStatus(IStatus.ERROR, IResourceStatus.FAILED_DELETE_LOCAL, folder.getFullPath(), message, ce);
@@ -907,7 +906,7 @@ public void standardMoveFile(IFile source, IFile destination, int flags, IProgre
 		try {
 			lock.acquire();
 			String message = NLS.bind(Messages.resources_moving, source.getFullPath());
-			monitor.subTask(message);
+			monitor.beginTask(message, Policy.totalWork);
 
 			// These pre-conditions should all be ok but just in case...
 			if (!source.exists() || destination.exists() || !destination.getParent().isAccessible())
diff --git a/resources/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/resources/ResourceTestUtil.java b/resources/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/resources/ResourceTestUtil.java
index feaa54b6ff3..9b795b527bc 100644
--- a/resources/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/resources/ResourceTestUtil.java
+++ b/resources/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/resources/ResourceTestUtil.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2023, 2024 Vector Informatik GmbH and others.
+ * Copyright (c) 2023, 2025 Vector Informatik GmbH and others.
  *
  * This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License 2.0
@@ -63,6 +63,7 @@
 import org.eclipse.core.runtime.OperationCanceledException;
 import org.eclipse.core.runtime.Platform;
 import org.eclipse.core.runtime.Status;
+import org.eclipse.core.runtime.SubMonitor;
 import org.eclipse.core.runtime.jobs.ISchedulingRule;
 import org.eclipse.core.runtime.jobs.Job;
 import org.eclipse.core.tests.harness.FileSystemHelper;
@@ -187,8 +188,9 @@ public static void createInWorkspace(final IResource resource) throws CoreExcept
 	 */
 	public static void createInWorkspace(final IResource[] resources) throws CoreException {
 		IWorkspaceRunnable body = monitor -> {
+			SubMonitor subMonitor = SubMonitor.convert(monitor, resources.length);
 			for (IResource resource : resources) {
-				createInWorkspace(resource, monitor);
+				createInWorkspace(resource, subMonitor.split(1));
 			}
 		};
 		getWorkspace().run(body, createTestMonitor());
@@ -198,19 +200,21 @@ private static void createInWorkspace(final IResource resource, IProgressMonitor
 		if (resource == null || resource.exists()) {
 			return;
 		}
+		SubMonitor subMonitor = SubMonitor.convert(monitor, 2);
 		if (!resource.getParent().exists()) {
-			createInWorkspace(resource.getParent(), monitor);
+			createInWorkspace(resource.getParent(), subMonitor.split(1));
 		}
 		switch (resource.getType()) {
 		case IResource.FILE:
-			((IFile) resource).create(nullInputStream(), true, monitor);
+			((IFile) resource).create(nullInputStream(), true, subMonitor.split(1));
 			break;
 		case IResource.FOLDER:
-			((IFolder) resource).create(true, true, monitor);
+			((IFolder) resource).create(true, true, subMonitor.split(1));
 			break;
 		case IResource.PROJECT:
-			((IProject) resource).create(monitor);
-			((IProject) resource).open(monitor);
+			subMonitor.setWorkRemaining(2);
+			((IProject) resource).create(subMonitor.split(1));
+			((IProject) resource).open(subMonitor.split(1));
 			break;
 		}
 	}


Reviews:
user: login: ptziegler
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T12:50:22Z
user: login: ptziegler
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T12:50:50Z

Review comments:
diff_hunk: @@ -188,41 +195,37 @@ protected void synchronize(UnifiedTreeNode node) throws CoreException {
 
 	@Override
 	public boolean visit(UnifiedTreeNode node) throws CoreException {
-		monitor.checkCanceled();
-		int work = 1;
-		try {
-			//location can be null if based on an undefined variable
-			if (node.getStore() == null) {
-				//should still be a best effort copy
+		IProgressMonitor monitor = rootMonitor.split(1);
+		ticks--;
+		//location can be null if based on an undefined variable
+		if (node.getStore() == null) {
+			//should still be a best effort copy
+			IPath path = node.getResource().getFullPath();
+			String message = NLS.bind(Messages.localstore_locationUndefined, path);
+			status.add(new ResourceStatus(IResourceStatus.FAILED_READ_LOCAL, path, message, null));
+			return false;
+		}
+		boolean wasSynchronized = isSynchronized(node);
+		if (force && !wasSynchronized) {
+			synchronize(node);
+			// If not synchronized, the monitor did not take this resource into account.
+			ticks++;
+			rootMonitor.setWorkRemaining(ticks);
path: resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/localstore/CopyVisitor.java
user: login: ptziegler
body: I'm not overly happy with this solution, as it might happen that the last file is out-of-sync and wherefore all ticks have already been consumed. The original implementation doesn't really work either, as the progress monitor complains if you work for zero ticks.
created_at: 2025-03-18T12:50:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -113,8 +118,8 @@ protected boolean copyContents(UnifiedTreeNode node, Resource source, Resource d
 			IFileStore destinationStore = destination.getStore();
 			//ensure the parent of the root destination exists (bug 126104)
 			if (destination == rootDestination)
-				destinationStore.getParent().mkdir(EFS.NONE, monitor.newChild(0));
-			sourceStore.copy(destinationStore, EFS.SHALLOW, monitor.newChild(0));
+				destinationStore.getParent().mkdir(EFS.NONE, subMonitor.newChild(1));
+			sourceStore.copy(destinationStore, EFS.SHALLOW, subMonitor.newChild(1));
 			//create the destination in the workspace
path: resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/localstore/CopyVisitor.java
user: login: ptziegler
body: Similarly to the visit method, the progress monitor complains if you create children with zero ticks...
created_at: 2025-03-18T12:50:50Z
author_association: CONTRIBUTOR

PR comments:
user: login: ptziegler
created_at: 2025-03-18T12:48:39Z
body: CC @vogella due to changes to 8539653d6fb34e6955f678c43fac91c83484ae1d and b7534d00886d1047427f61bfec1bf35cb7753f0c.
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-18T13:32:58Z
body: ## Test Results
1 174 files   -  16  1 174 suites   - 16   54m 24s ⏱️ - 8m 19s
4 173 tests ±  0  4 126 ✅ ±  0   47 💤 ±0  0 ❌ ±0 
8 747 runs   - 208  8 628 ✅  - 207  119 💤  - 1  0 ❌ ±0 

Results for commit 0e405232. ± Comparison against base commit f4e6c3be.

[test-results]:data:application/gzip;base64,H4sIAPn+22cC/03MTQ6DIBCG4asY1l0wgDD0Mg0gJKT+NAgr07sXbLUuv4fh3UiIo1/JvQNQ4taRtcR83UNJJsdlrsKZbFKfczsQoPgxH2txbjcm//aMr2bqlGDiWIWe4FNa0k9SmVsW1f6hraOKkuFJ3yiAPuQS3fe16ZZpirkOQr2gPePMCB84F2CD1halFZKi8dgH1GqwgOT9AQhcLRYTAQAA

:recycle: This comment has been updated with latest results.
author_association: CONTRIBUTOR
user: login: ptziegler
created_at: 2025-03-20T11:42:48Z
body: The OOM error on Windows seems sadly unrelated. Unless there are any objections, I'd like to merge this at the end of the week.
author_association: CONTRIBUTOR
user: login: ptziegler
created_at: 2025-03-21T11:36:03Z
body: The other builds are looking fine. Merging.
author_association: CONTRIBUTOR