From 2bc84dfe3643bbb63edfd993a56549978672abd8 Mon Sep 17 00:00:00 2001 From: Lars Vogel Date: Thu, 20 Aug 2026 13:46:09 +0200 Subject: [PATCH] Move the Project Explorer performance test to the performance bundle The navigator test bundle carried a PerformanceTest that no suite ran: it was commented out of NavigatorTestSuite for bug 264449 and reported its timings with System.out.println, so it could neither fail nor be compared. It belongs with the other performance tests, which report through UIPerformanceTestUtil since they were migrated off the performance database. ProjectExplorerPerformanceTest keeps the project deletion and label provider mapping scenarios, measures each of them over several rounds rather than once, and drops the dependency on NavigatorTestBase. It adds testRemoveFilesOfCollapsedProject, which removes files that have no item in the tree because their project was never expanded. That is what a delete below a collapsed project comes down to, and it is the case where the viewer has to ask the content service for a parent. With 2000 files ten folders deep it reports 3.5ms against 18.6ms when the viewer walks the whole ancestor chain instead of asking for the immediate parent. --- .../tests/navigator/NavigatorTestSuite.java | 1 - .../ui/tests/navigator/PerformanceTest.java | 304 ---------------- .../ProjectExplorerPerformanceTest.java | 338 ++++++++++++++++++ 3 files changed, 338 insertions(+), 305 deletions(-) delete mode 100644 tests/org.eclipse.ui.tests.navigator/src/org/eclipse/ui/tests/navigator/PerformanceTest.java create mode 100644 tests/org.eclipse.ui.tests.performance/src/org/eclipse/ui/tests/performance/ProjectExplorerPerformanceTest.java diff --git a/tests/org.eclipse.ui.tests.navigator/src/org/eclipse/ui/tests/navigator/NavigatorTestSuite.java b/tests/org.eclipse.ui.tests.navigator/src/org/eclipse/ui/tests/navigator/NavigatorTestSuite.java index 21ff0cb531a..fe0e346ae1c 100644 --- a/tests/org.eclipse.ui.tests.navigator/src/org/eclipse/ui/tests/navigator/NavigatorTestSuite.java +++ b/tests/org.eclipse.ui.tests.navigator/src/org/eclipse/ui/tests/navigator/NavigatorTestSuite.java @@ -41,7 +41,6 @@ GoBackForwardsTest.class, CopyPasteActionTest.class, CloseUnrelatedProjectsActionTest.class, // DnDTest.class, // DnDTest.testSetDragOperation() fails - // PerformanceTest.class // Does not pass on all platforms see bug 264449 }) public final class NavigatorTestSuite { } diff --git a/tests/org.eclipse.ui.tests.navigator/src/org/eclipse/ui/tests/navigator/PerformanceTest.java b/tests/org.eclipse.ui.tests.navigator/src/org/eclipse/ui/tests/navigator/PerformanceTest.java deleted file mode 100644 index 5a7df23d2e7..00000000000 --- a/tests/org.eclipse.ui.tests.navigator/src/org/eclipse/ui/tests/navigator/PerformanceTest.java +++ /dev/null @@ -1,304 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2008, 2015 webtekie@gmail.com, IBM Corporation and others. - * - * This program and the accompanying materials - * are made available under the terms of the Eclipse Public License 2.0 - * which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-2.0/ - * - * SPDX-License-Identifier: EPL-2.0 - * - * Contributors: - * webtekie@gmail.com - initial API and implementation - * IBM Corporation - fixed dead code warning - * Thibault Le Ouay - Bug 457870 - *******************************************************************************/ -package org.eclipse.ui.tests.navigator; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; - -import java.io.ByteArrayInputStream; -import java.text.DecimalFormat; - -import org.eclipse.core.resources.IFile; -import org.eclipse.core.resources.IProject; -import org.eclipse.core.resources.ResourcesPlugin; -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.swt.widgets.Display; -import org.eclipse.ui.PartInitException; -import org.eclipse.ui.navigator.ICommonViewerMapper; -import org.eclipse.ui.navigator.resources.ProjectExplorer; -import org.eclipse.ui.tests.harness.util.DisplayHelper; -import org.eclipse.ui.tests.harness.util.EditorTestHelper; -import org.junit.jupiter.api.Test; - -/** - * A test to see if created projects are reflected in Project Explorer - */ -public class PerformanceTest extends NavigatorTestBase { - - private int _numProjects = 50; // number of projects to - private int _numFiles = 1000; - private static final boolean DEBUG = false; - - DecimalFormat _df = new DecimalFormat("000"); - - public PerformanceTest() { - _navigatorInstanceId = ProjectExplorer.VIEW_ID; - _initTestData = false; - } - - protected void createProjects() throws InterruptedException { - Job createJob = new Job("Create projects") { - - @Override - protected IStatus run(IProgressMonitor monitor) { - try { - for (int i = 0; i < _numProjects; i++) { - String name = _df.format(i); - IProject p1 = ResourcesPlugin.getWorkspace().getRoot() - .getProject("p" + name); - p1.create(null); - p1.open(null); - IFile f1 = p1.getFile("f" + _df.format(0)); - f1.create(new ByteArrayInputStream(new byte[] {}), - true, null); - } - } catch (Exception ex) { - ex.printStackTrace(); - fail("Unexpected exception: " + ex); - } - return Status.OK_STATUS; - } - }; - - createJob.setRule(ResourcesPlugin.getWorkspace().getRoot()); - createJob.schedule(); - createJob.join(); - - assertEquals(createJob.getResult(), Status.OK_STATUS); - - DisplayHelper.runEventLoop(Display.getCurrent(), 10); - - int numOfProjects = _viewer.getTree().getItemCount(); - System.out.println("Projects created: " + numOfProjects); - - if (DEBUG) - DisplayHelper.sleep(Display.getCurrent(), 10000000); - - assertEquals(_numProjects, numOfProjects); - } - - protected void deleteProjects() throws InterruptedException { - Job deleteJob = new Job("Delete Projects") { - - @Override - protected IStatus run(IProgressMonitor monitor) { - try { - for (int i = 0; i < _numProjects; i++) { - String name = _df.format(i); - IProject p1 = ResourcesPlugin.getWorkspace().getRoot() - .getProject("p" + name); - p1.delete(true, null); - } - } catch (Exception ex) { - ex.printStackTrace(); - fail("Unexpected exception: " + ex); - } - return Status.OK_STATUS; - } - }; - - deleteJob.setRule(ResourcesPlugin.getWorkspace().getRoot()); - deleteJob.schedule(); - deleteJob.join(); - - assertEquals(deleteJob.getResult(), Status.OK_STATUS); - - DisplayHelper.runEventLoop(Display.getCurrent(), 10); - } - - protected void createFiles(final IProject project, final int startNumber) throws InterruptedException - { - Job createJob = new Job("Create Files") { - - @Override - protected IStatus run(IProgressMonitor monitor) { - try { - for (int i = startNumber; i < _numFiles; i++) { - String name = _df.format(i); - IFile f1 = project.getFile("f" + name); - f1.create(new ByteArrayInputStream(new byte[] {}), - true, null); - } - } catch (Exception ex) { - ex.printStackTrace(); - fail("Unexpected exception: " + ex); - } - return Status.OK_STATUS; - } - }; - - createJob.setRule(ResourcesPlugin.getWorkspace().getRoot()); - createJob.schedule(); - createJob.join(); - - assertEquals(createJob.getResult(), Status.OK_STATUS); - } - - protected void touchFiles(final IProject p1) throws Exception { - Job touchJob = new Job("Touch Files") { - - @Override - protected IStatus run(IProgressMonitor monitor) { - try { - for (int i = 0; i < _numFiles; i++) { - String name = _df.format(i); - IFile f1 = p1.getFile("f" + name); - f1.touch(null); - } - } catch (Exception ex) { - ex.printStackTrace(); - fail("Unexpected exception: " + ex); - } - return Status.OK_STATUS; - } - }; - - touchJob.setRule(ResourcesPlugin.getWorkspace().getRoot()); - touchJob.schedule(); - touchJob.join(); - assertEquals(touchJob.getResult(), Status.OK_STATUS); - } - - // bug 159828 deleting large number of projects takes too long - @Test - public void testCreateAndDeleteProjects() throws PartInitException, InterruptedException { - - _numProjects = 100; - - createProjects(); - - // Hide it - EditorTestHelper.showView(_navigatorInstanceId, false); - - long start = System.currentTimeMillis(); - deleteProjects(); - DisplayHelper.sleep(500); - System.out.println("No project explorer delete " + _numProjects + " Time: " - + (System.currentTimeMillis() - start)); - - showNavigator(); - DisplayHelper.sleep(100); - createProjects(); - DisplayHelper.sleep(200); - - start = System.currentTimeMillis(); - deleteProjects(); - DisplayHelper.sleep(500); - System.out.println("Project explorer " + _numProjects + " Time: " - + (System.currentTimeMillis() - start)); - - DisplayHelper.sleep(500); - - } - - public void XXXtestCreateAndTouchFiles() throws Exception { - - final IProject p1 = ResourcesPlugin.getWorkspace().getRoot() - .getProject("p1"); - p1.create(null); - p1.open(null); - - createFiles(p1, 0); - - _viewer.collapseAll(); - - if (DEBUG) - DisplayHelper.sleep(Display.getCurrent(), 10000000); - - long start = System.currentTimeMillis(); - - Job touchJob = new Job("Touch Files") { - - @Override - protected IStatus run(IProgressMonitor monitor) { - try { - for (int i = 0; i < _numFiles; i++) { - String name = _df.format(i); - IFile f1 = p1.getFile("f" + name); - f1.touch(null); - } - } catch (Exception ex) { - ex.printStackTrace(); - fail("Unexpected exception: " + ex); - } - return Status.OK_STATUS; - } - }; - - touchJob.setRule(ResourcesPlugin.getWorkspace().getRoot()); - touchJob.schedule(); - touchJob.join(); - assertEquals(touchJob.getResult(), Status.OK_STATUS); - - System.out.println("Touch " + _numFiles + " Time: " - + (System.currentTimeMillis() - start)); - } - - protected void createFilesForProjects() throws InterruptedException { - for (int i = 0; i < _numProjects; i++) { - String name = _df.format(i); - IProject p1 = ResourcesPlugin.getWorkspace().getRoot().getProject( - "p" + name); - createFiles(p1, 1); - } - } - - // bug 194209 updating lots of label providers does not scale well - @Test - public void testLabelProviderMapping() throws Exception { - - ICommonViewerMapper mapper = _viewer.getMapper(); - - _numProjects = 1; - _numFiles = 2000; - - createProjects(); - createFilesForProjects(); - - // Warm up - final IProject p1 = ResourcesPlugin.getWorkspace().getRoot() - .getProject("p000"); - - p1.close(null); - - long start = System.currentTimeMillis(); - _viewer.setMapper(null); - p1.open(null); - // Let the updates run - DisplayHelper.sleep(200); - - long createUnMappedTime = System.currentTimeMillis() - start; - System.out.println("Unmapped Time: " + createUnMappedTime); - - p1.close(null); - DisplayHelper.sleep(200); - _viewer.setMapper(mapper); - - start = System.currentTimeMillis(); - p1.open(null); - // Let the updates run - DisplayHelper.sleep(200); - long createMappedTime = System.currentTimeMillis() - start; - System.out.println("Mapped Time: " + createMappedTime); - - assertTrue(createMappedTime < createUnMappedTime); - } - -} \ No newline at end of file diff --git a/tests/org.eclipse.ui.tests.performance/src/org/eclipse/ui/tests/performance/ProjectExplorerPerformanceTest.java b/tests/org.eclipse.ui.tests.performance/src/org/eclipse/ui/tests/performance/ProjectExplorerPerformanceTest.java new file mode 100644 index 00000000000..b2116760cc8 --- /dev/null +++ b/tests/org.eclipse.ui.tests.performance/src/org/eclipse/ui/tests/performance/ProjectExplorerPerformanceTest.java @@ -0,0 +1,338 @@ +/******************************************************************************* + * Copyright (c) 2008, 2026 webtekie@gmail.com, IBM Corporation and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * webtekie@gmail.com - initial API and implementation + * IBM Corporation - fixed dead code warning + * Thibault Le Ouay - Bug 457870 + *******************************************************************************/ +package org.eclipse.ui.tests.performance; + +import static org.eclipse.ui.tests.harness.util.UITestUtil.processEvents; +import static org.eclipse.ui.tests.performance.UIPerformanceTestUtil.reportTimings; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayInputStream; +import java.text.DecimalFormat; +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.core.resources.IContainer; +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.ICoreRunnable; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.core.runtime.Path; +import org.eclipse.ui.IViewPart; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.navigator.CommonViewer; +import org.eclipse.ui.navigator.ICommonViewerMapper; +import org.eclipse.ui.navigator.resources.ProjectExplorer; +import org.eclipse.ui.tests.harness.util.CloseTestWindowsExtension; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.api.extension.RegisterExtension; + +/** + * Performance tests for the Project Explorer: deleting many projects, deleting + * many files below a collapsed project, removing such files from the viewer + * directly, and mapping the label provider updates. + *

+ * The scenarios that a workspace change drives are measured twice, once with + * the view hidden and once with it showing, because only the difference between + * the two is the cost of the view. + */ +public class ProjectExplorerPerformanceTest { + + @RegisterExtension + CloseTestWindowsExtension closeTestWindows = new CloseTestWindowsExtension(); + + private static final DecimalFormat NAME_FORMAT = new DecimalFormat("000"); + + /** Rounds per scenario, so that the reported minimum is not a single sample. */ + private static final int ROUNDS = 10; + + /** + * Folders between the project and the deleted files. The viewer asks the + * content service for a parent once per level, so the depth is what the + * scenario is about. + */ + private static final int FOLDER_DEPTH = 10; + + private final List timings = new ArrayList<>(); + + private String scenario; + + private long measuringSince; + + private CommonViewer viewer; + + @BeforeEach + public void startScenario(TestInfo testInfo) { + scenario = getClass().getSimpleName() + "." + testInfo.getDisplayName(); + timings.clear(); + } + + @AfterEach + public void cleanUpWorkspace() throws CoreException { + hideProjectExplorer(); + runInWorkspace(monitor -> { + for (IProject project : ResourcesPlugin.getWorkspace().getRoot().getProjects()) { + project.delete(true, true, null); + } + }); + } + + /** + * Runs the given change as one workspace operation, so the view sees a single + * resource delta rather than one per resource. + */ + private void runInWorkspace(ICoreRunnable change) throws CoreException { + ResourcesPlugin.getWorkspace().run(change, null); + processEvents(); + } + + private void startMeasuring() { + measuringSince = System.nanoTime(); + } + + private void stopMeasuring() { + timings.add(System.nanoTime() - measuringSince); + } + + private void report(String suffix) { + reportTimings(scenario + " " + suffix, timings); + timings.clear(); + } + + private void showProjectExplorer() throws CoreException { + IWorkbenchPage activePage = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage(); + IViewPart view = activePage.showView(ProjectExplorer.VIEW_ID); + viewer = ((ProjectExplorer) view).getCommonViewer(); + processEvents(); + } + + private void hideProjectExplorer() { + IWorkbenchPage activePage = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage(); + IViewPart view = activePage.findView(ProjectExplorer.VIEW_ID); + if (view != null) { + activePage.hideView(view); + } + viewer = null; + processEvents(); + } + + private IProject createProject(String name) throws CoreException { + IProject project = ResourcesPlugin.getWorkspace().getRoot().getProject(name); + runInWorkspace(monitor -> { + if (!project.exists()) { + project.create(null); + } + project.open(null); + }); + return project; + } + + private void createFiles(IContainer container, int fileCount) throws CoreException { + runInWorkspace(monitor -> addFiles(container, fileCount)); + } + + private void addFiles(IContainer container, int fileCount) throws CoreException { + for (int i = 0; i < fileCount; i++) { + IFile file = container.getFile(new Path("f" + NAME_FORMAT.format(i))); + if (!file.exists()) { + file.create(new ByteArrayInputStream(new byte[0]), true, new NullProgressMonitor()); + } + } + } + + /** + * Creates a chain of nested folders and answers the innermost one. + */ + private IContainer createNestedFolders(IProject project, int depth) throws CoreException { + IContainer[] innermost = new IContainer[] { project }; + runInWorkspace(monitor -> { + IContainer container = project; + for (int i = 0; i < depth; i++) { + IFolder folder = container.getFolder(new Path("d" + NAME_FORMAT.format(i))); + if (!folder.exists()) { + folder.create(true, true, null); + } + container = folder; + } + innermost[0] = container; + }); + return innermost[0]; + } + + private void createProjects(int projectCount) throws CoreException { + runInWorkspace(monitor -> { + for (int i = 0; i < projectCount; i++) { + IProject project = ResourcesPlugin.getWorkspace().getRoot().getProject("p" + NAME_FORMAT.format(i)); + project.create(null); + project.open(null); + project.getFile("f" + NAME_FORMAT.format(0)).create(new ByteArrayInputStream(new byte[0]), true, null); + } + }); + } + + private void deleteAllProjects() throws CoreException { + runInWorkspace(monitor -> { + for (IProject project : ResourcesPlugin.getWorkspace().getRoot().getProjects()) { + project.delete(true, null); + } + }); + } + + private void deleteAllFiles(IContainer container) throws CoreException { + runInWorkspace(monitor -> { + for (IResource member : container.members()) { + if (member instanceof IFile) { + member.delete(true, null); + } + } + }); + } + + /** + * Deleting many projects, which the view shows as top level elements. + */ + @Test + public void testDeleteProjects() throws CoreException { + int projectCount = 100; + + hideProjectExplorer(); + for (int round = 0; round < ROUNDS; round++) { + createProjects(projectCount); + startMeasuring(); + deleteAllProjects(); + stopMeasuring(); + } + report("without Project Explorer"); + + showProjectExplorer(); + for (int round = 0; round < ROUNDS; round++) { + createProjects(projectCount); + assertEquals(projectCount, viewer.getTree().getItemCount()); + startMeasuring(); + deleteAllProjects(); + stopMeasuring(); + assertEquals(0, viewer.getTree().getItemCount()); + } + report("with Project Explorer"); + } + + /** + * Deleting many files nested below a project that was never expanded. The + * files have no item in the tree, so the viewer asks the content service for + * their parent, and it does so once per level of the ancestor chain. + */ + @Test + public void testDeleteFilesInCollapsedProject() throws CoreException { + int fileCount = 2000; + + hideProjectExplorer(); + IProject project = createProject("collapsedProject"); + IContainer folder = createNestedFolders(project, FOLDER_DEPTH); + for (int round = 0; round < ROUNDS; round++) { + createFiles(folder, fileCount); + startMeasuring(); + deleteAllFiles(folder); + stopMeasuring(); + } + report("without Project Explorer"); + + showProjectExplorer(); + for (int round = 0; round < ROUNDS; round++) { + createFiles(folder, fileCount); + assertEquals(1, viewer.getTree().getItemCount()); + assertFalse(viewer.getTree().getItem(0).getExpanded(), "the project must stay collapsed"); + startMeasuring(); + deleteAllFiles(folder); + stopMeasuring(); + } + report("with Project Explorer"); + } + + /** + * Removing elements that have no item in the tree, which is what a delete + * below a collapsed project comes down to. The viewer cannot find a widget + * for them, so it asks the content service for their parent instead, and + * that is the whole measured cost here. + */ + @Test + public void testRemoveFilesOfCollapsedProject() throws CoreException { + int fileCount = 2000; + + IProject project = createProject("collapsedProject"); + IContainer folder = createNestedFolders(project, FOLDER_DEPTH); + createFiles(folder, fileCount); + showProjectExplorer(); + assertEquals(1, viewer.getTree().getItemCount()); + assertFalse(viewer.getTree().getItem(0).getExpanded(), "the project must stay collapsed"); + + Object[] files = folder.members(); + assertEquals(fileCount, files.length); + + for (int round = 0; round < ROUNDS; round++) { + startMeasuring(); + viewer.remove(files); + stopMeasuring(); + } + report("per " + fileCount + " files"); + } + + /** + * Opening a project with many files, with and without the mapper that keeps + * the label provider updates from scanning the whole tree. + */ + @Test + public void testLabelProviderMapping() throws CoreException { + int fileCount = 2000; + + showProjectExplorer(); + IProject project = createProject("mappedProject"); + createFiles(project, fileCount); + ICommonViewerMapper mapper = viewer.getMapper(); + processEvents(); + + project.close(null); + processEvents(); + viewer.setMapper(null); + startMeasuring(); + project.open(null); + processEvents(); + stopMeasuring(); + long unmapped = timings.get(0); + report("without mapper"); + + project.close(null); + processEvents(); + viewer.setMapper(mapper); + startMeasuring(); + project.open(null); + processEvents(); + stopMeasuring(); + long mapped = timings.get(0); + report("with mapper"); + + assertTrue(mapped < unmapped, "the mapper should make opening the project cheaper"); + } + +}