Bug 506009 -  UI freezes reported in PackageFragment.getElementName

The Package Explorer View has two options which are very slow for large
package structures. Those are the hierarchical resp. flat representation
of packages.

This is the case, since the Java model structure does not match the
hierarchical structure presented in the view. The view computes the
package hierarchy on its own. The view does so by going over all
packages in a package root, for its every operation. This results in
e.g. refresh operations which are quadratic in the number of packages of
a project. For package numbers above 1000, refreshes can freeze the UI
for minutes.

With this change the PackageExplorerContentProvider computes the
hierarchical structure of packages once and caches it. The cache is
cleaned on every Java model change. This reduces the complexity of
operations that run on the view, including refreshing. The  UI freeze
duration is reduced to a few seconds, down from minutes.

Change-Id: Ib28aa66d5718a879491b9076f2d78c3c7e4e73b6
Signed-off-by: Simeon Andreev <simeon.danailov.andreev@gmail.com>
diff --git a/org.eclipse.jdt.ui.tests/ui/org/eclipse/jdt/ui/tests/packageview/PackageCacheTest.java b/org.eclipse.jdt.ui.tests/ui/org/eclipse/jdt/ui/tests/packageview/PackageCacheTest.java
new file mode 100644
index 0000000..4c789f7
--- /dev/null
+++ b/org.eclipse.jdt.ui.tests/ui/org/eclipse/jdt/ui/tests/packageview/PackageCacheTest.java
@@ -0,0 +1,233 @@
+/*******************************************************************************
+ * Copyright (c) 2017 Simeon Andreev and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     Simeon Andreev - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.jdt.ui.tests.packageview;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.junit.Test;
+
+import org.eclipse.jdt.testplugin.JavaProjectHelper;
+
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.NullProgressMonitor;
+
+import org.eclipse.jdt.core.IJavaElement;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.IPackageFragment;
+import org.eclipse.jdt.core.IPackageFragmentRoot;
+
+import org.eclipse.jdt.internal.ui.packageview.PackageCache;
+
+import junit.framework.TestCase;
+
+/**
+ * Tests for {@link PackageCache}.
+ *
+ */
+public class PackageCacheTest extends TestCase {
+
+	private IJavaProject testProject;
+
+	private IPackageFragmentRoot src;
+
+	private IPackageFragment package_a;
+	private IPackageFragment package_a_b;
+	private IPackageFragment package_a_b_c;
+	private IPackageFragment package_a_b_c_d1;
+	private IPackageFragment package_a_b_c_d2;
+	private IPackageFragment package_a_b_e;
+	private IPackageFragment package_f;
+	private IPackageFragment package_f_g;
+
+	private PackageCache packageCache;
+
+
+	@Override
+	protected void setUp() throws Exception {
+		super.setUp();
+
+		IProgressMonitor monitor= new NullProgressMonitor();
+
+		String projectName= getClass().getSimpleName();
+		testProject= JavaProjectHelper.createJavaProject(projectName, "bin");
+		src= JavaProjectHelper.addSourceContainer(testProject, "src");
+
+		boolean force= true;
+		package_a= src.createPackageFragment("a", force, monitor);
+		package_a_b= src.createPackageFragment("a.b", force, monitor);
+		package_a_b_c= src.createPackageFragment("a.b.c", force, monitor);
+		package_a_b_c_d1= src.createPackageFragment("a.b.c.d1", force, monitor);
+		package_a_b_c_d2= src.createPackageFragment("a.b.c.d2", force, monitor);
+		package_a_b_e= src.createPackageFragment("a.b.e", force, monitor);
+		package_f= src.createPackageFragment("f", force, monitor);
+		package_f_g= src.createPackageFragment("f.g", force, monitor);
+
+		packageCache= new PackageCache(src);
+	}
+
+	@Override
+	protected void tearDown() throws Exception {
+		testProject.getProject().delete(true, false, new NullProgressMonitor());
+		super.tearDown();
+	}
+
+
+	@Test
+	public void testGetDirectChildren() throws Exception {
+		List<IPackageFragment> allPackages= allPackages();
+
+		Map<IPackageFragment, List<IPackageFragment>> actualChildren = new LinkedHashMap<>();
+		for (IPackageFragment packageFragment : allPackages) {
+			List<IPackageFragment> childrenOfPackage= packageCache.getDirectChildren(packageFragment);
+			actualChildren.put(packageFragment, childrenOfPackage);
+		}
+
+		Map<IPackageFragment, List<IPackageFragment>> expectedChildren = new LinkedHashMap<>();
+		expectedChildren.put(package_a, Arrays.asList(package_a_b));
+		expectedChildren.put(package_a_b, Arrays.asList(package_a_b_c, package_a_b_e));
+		expectedChildren.put(package_a_b_c, Arrays.asList(package_a_b_c_d1, package_a_b_c_d2));
+		expectedChildren.put(package_a_b_c_d1, Collections.emptyList());
+		expectedChildren.put(package_a_b_c_d2, Collections.emptyList());
+		expectedChildren.put(package_a_b_e, Collections.emptyList());
+		expectedChildren.put(package_f, Arrays.asList(package_f_g));
+		expectedChildren.put(package_f_g, Collections.emptyList());
+
+		assertEquals("method returned wrong results",
+				expectedChildren, actualChildren);
+	}
+
+	@Test
+	public void testGetSingleChild() throws Exception {
+		Map<IPackageFragment, IPackageFragment> actualSingleChildren= actualSingleChildren();
+		Map<IPackageFragment, IPackageFragment> expectedSingleChildren= expectedSingleChildren();
+
+		assertEquals("method returned wrong results",
+				expectedSingleChildren, actualSingleChildren);
+	}
+
+	@Test
+	public void testSingleChildAgainstOldImplementation() throws Exception {
+		Map<IPackageFragment, IPackageFragment> actualSingleChildren= actualSingleChildren();
+
+		List<IPackageFragment> allPackages= allPackages();
+		Map<IPackageFragment, IPackageFragment> expectedSingleChildren= new LinkedHashMap<>();
+		for (IPackageFragment packageFragment : allPackages) {
+			IPackageFragment singleChild= findSinglePackageChild(packageFragment);
+			if (singleChild != null) {
+				expectedSingleChildren.put(packageFragment, singleChild);
+			}
+		}
+
+		assertEquals("method returned wrong results",
+				expectedSingleChildren, actualSingleChildren);
+	}
+
+	@Test
+	public void testHasSingleChild() throws Exception {
+		List<IPackageFragment> actualPackagesWithSingleChild= actualPackagesWithSingleChild();
+		List<IPackageFragment> expectedPackagesWithSingleChild= expectedPackagesWithSingleChild();
+
+		assertEquals("method returned wrong results",
+				expectedPackagesWithSingleChild, actualPackagesWithSingleChild);
+	}
+
+	@Test
+	public void testHasSingleChildAgainstOldImplementation() throws Exception {
+		List<IPackageFragment> actualPackagesWithSingleChild= actualPackagesWithSingleChild();
+
+		List<IPackageFragment> allPackages= allPackages();
+		List<IPackageFragment> expectedPackagesWithSingleChild= new ArrayList<>();
+		for (IPackageFragment packageFragment : allPackages) {
+			IPackageFragment singleChild= findSinglePackageChild(packageFragment);
+			if (singleChild != null) {
+				expectedPackagesWithSingleChild.add(packageFragment);
+			}
+		}
+
+		assertEquals("method returned wrong results",
+				expectedPackagesWithSingleChild, actualPackagesWithSingleChild);
+	}
+
+	private Map<IPackageFragment, IPackageFragment> actualSingleChildren() throws Exception {
+		List<IPackageFragment> allPackages= allPackages();
+		Map<IPackageFragment, IPackageFragment> actualSingleChildren= new LinkedHashMap<>();
+		for (IPackageFragment packageFragment : allPackages) {
+			IPackageFragment singleChild= packageCache.getSingleChild(packageFragment);
+			if (singleChild != null) {
+				actualSingleChildren.put(packageFragment, singleChild);
+			}
+		}
+		return actualSingleChildren;
+	}
+
+	private Map<IPackageFragment, IPackageFragment> expectedSingleChildren() {
+		Map<IPackageFragment, IPackageFragment> expectedSingleChildren= new LinkedHashMap<>();
+		expectedSingleChildren.put(package_a, package_a_b);
+		expectedSingleChildren.put(package_f, package_f_g);
+		return expectedSingleChildren;
+	}
+
+	private List<IPackageFragment> actualPackagesWithSingleChild() throws Exception {
+		List<IPackageFragment> allPackages= allPackages();
+		List<IPackageFragment> actualPackagesWithSingleChild= new ArrayList<>();
+		for (IPackageFragment packageFragment : allPackages) {
+			if (packageCache.hasSingleChild(packageFragment)) {
+				actualPackagesWithSingleChild.add(packageFragment);
+			}
+		}
+		return actualPackagesWithSingleChild;
+	}
+
+	private List<IPackageFragment> expectedPackagesWithSingleChild() throws Exception {
+		Map<IPackageFragment, IPackageFragment> expectedSingleChildren= expectedSingleChildren();
+		return new ArrayList<>(expectedSingleChildren.keySet());
+	}
+
+	private List<IPackageFragment> allPackages() {
+		return Arrays.asList(
+				package_a,
+				package_a_b,
+				package_a_b_c,
+				package_a_b_c_d1,
+				package_a_b_c_d2,
+				package_a_b_e,
+				package_f,
+				package_f_g);
+	}
+
+	/*
+	 * Old "reference" implementation from org.eclipse.jdt.internal.ui.packageview.PackageExplorerContentProvider,
+	 * which computes the single child package of a package (if any).
+	 */
+	private IPackageFragment findSinglePackageChild(IPackageFragment fragment) throws Exception {
+		IJavaElement[] children= src.getChildren();
+		String prefix= fragment.getElementName() + '.';
+		int prefixLen= prefix.length();
+		IPackageFragment found= null;
+		for (int i= 0; i < children.length; ++i) {
+			IJavaElement element= children[i];
+			String name= element.getElementName();
+			if (name.startsWith(prefix) && name.length() > prefixLen && name.indexOf('.', prefixLen) == -1) {
+				if (found == null) {
+					found= (IPackageFragment) element;
+				} else {
+					return null;
+				}
+			}
+		}
+		return found;
+	}
+}
diff --git a/org.eclipse.jdt.ui.tests/ui/org/eclipse/jdt/ui/tests/packageview/PackageExplorerTests.java b/org.eclipse.jdt.ui.tests/ui/org/eclipse/jdt/ui/tests/packageview/PackageExplorerTests.java
index d75d70b..6a35492 100644
--- a/org.eclipse.jdt.ui.tests/ui/org/eclipse/jdt/ui/tests/packageview/PackageExplorerTests.java
+++ b/org.eclipse.jdt.ui.tests/ui/org/eclipse/jdt/ui/tests/packageview/PackageExplorerTests.java
@@ -29,6 +29,7 @@
 		suite.addTest(PackageExplorerShowInTests.suite());
 		suite.addTestSuite(WorkingSetDropAdapterTest.class);
 		suite.addTest(HierarchicalContentProviderTests.suite());
+		suite.addTestSuite(PackageCacheTest.class);
 		//$JUnit-END$
 		return suite;
 	}
diff --git a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/packageview/PackageCache.java b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/packageview/PackageCache.java
new file mode 100644
index 0000000..0ab2110
--- /dev/null
+++ b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/packageview/PackageCache.java
@@ -0,0 +1,233 @@
+/*******************************************************************************
+ * Copyright (c) 2017 Simeon Andreev and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     Simeon Andreev - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.jdt.internal.ui.packageview;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.jdt.core.IJavaElement;
+import org.eclipse.jdt.core.IPackageFragment;
+import org.eclipse.jdt.core.IPackageFragmentRoot;
+import org.eclipse.jdt.core.JavaModelException;
+
+/**
+ * Determines whether a package has only a single child as well as retrieves that child if it
+ * exists. Also provides all package children of a package.
+ *
+ * <p>
+ * For this class, the parent of a package is its hierarchical parent package, not the package root.
+ * E.g. for packages {@code a}, {@code a.b.} and {@code a.b.c}, the parent of {@code a.b.c} is
+ * {@code a.b} and the parent of {@code a.b} is {@code a}. The package {@code a} has no parent.
+ * </p>
+ *
+ * <p>
+ * A single query runs in constant time. Preparing for queries runs in time linear to the number of
+ * packages in the package root. The first query on this object will run the preparation step.
+ * </p>
+ *
+ * <p>
+ * Not thread safe.
+ * </p>
+ *
+ * @see #getDirectChildren(IPackageFragment)
+ */
+public class PackageCache {
+
+	/**
+	 * Caches the children of a package in a package root. The cache for a package root is built on the
+	 * first query.
+	 */
+	static class PerRootCache {
+
+		private final Map<IPackageFragmentRoot, PackageCache> packageCaches= new HashMap<>();
+
+		boolean hasSingleChild(IPackageFragment packageFragment) throws JavaModelException {
+			PackageCache packagesOfRoot= getPackageCache(packageFragment);
+			return packagesOfRoot.hasSingleChild(packageFragment);
+		}
+
+		IPackageFragment getSingleChild(IPackageFragment packageFragment) throws JavaModelException {
+			PackageCache packagesOfRoot= getPackageCache(packageFragment);
+			return packagesOfRoot.getSingleChild(packageFragment);
+		}
+
+		List<IPackageFragment> getDirectChildren(IPackageFragment packageFragment) throws JavaModelException {
+			PackageCache packagesOfRoot= getPackageCache(packageFragment);
+			return packagesOfRoot.getDirectChildren(packageFragment);
+		}
+
+		private PackageCache getPackageCache(IPackageFragment packageFragment) {
+			IPackageFragmentRoot packageRoot= (IPackageFragmentRoot) packageFragment.getParent();
+			PackageCache packageCache= getPackageCache(packageRoot);
+			return packageCache;
+		}
+
+		private PackageCache getPackageCache(IPackageFragmentRoot root) {
+			PackageCache packageCacheOfRoot;
+			synchronized (packageCaches) {
+				packageCacheOfRoot= packageCaches.get(root);
+				if (packageCacheOfRoot == null) {
+					packageCacheOfRoot= new PackageCache(root);
+					packageCaches.put(root, packageCacheOfRoot);
+				}
+			}
+			return packageCacheOfRoot;
+		}
+
+		/**
+		 * Can be called from a different (not only UI) thread.
+		 */
+		void clear() {
+			synchronized (packageCaches) {
+				packageCaches.clear();
+			}
+		}
+	}
+
+
+	private final IPackageFragmentRoot packageRoot;
+
+	/**
+	 * Key is {@link IPackageFragment#getElementName()}, value is the list of the direct children
+	 * packages.
+	 */
+	private final Map<String, List<IPackageFragment>> packagesCache;
+
+	private boolean initialized;
+
+	/**
+	 * @param packageRoot The package root for packages of which the queries will be issued.
+	 */
+	public PackageCache(IPackageFragmentRoot packageRoot) {
+		this.packageRoot= packageRoot;
+		packagesCache= new HashMap<>();
+		initialized= false;
+	}
+
+	/**
+	 * @return {@code true} iff the specified fragment has exactly one child.
+	 *
+	 * @param packageFragment The fragment for which to check.
+	 * @throws JavaModelException If accessing the packages in the package root fails.
+	 *
+	 * @see #getSingleChild(IPackageFragment)
+	 */
+	public boolean hasSingleChild(IPackageFragment packageFragment) throws JavaModelException {
+		IPackageFragment singleChild= getSingleChild(packageFragment);
+		boolean hasSingleChild= singleChild != null;
+		return hasSingleChild;
+	}
+
+	/**
+	 * @return The single child of the specified package or {@code null} if the package does not have
+	 *         exactly one child.
+	 *
+	 * @param packageFragment The single child of this fragment will be retrieved.
+	 * @throws JavaModelException If accessing the packages in the package root fails.
+	 *
+	 * @see #getDirectChildren(IPackageFragment)
+	 */
+	public IPackageFragment getSingleChild(IPackageFragment packageFragment) throws JavaModelException {
+		List<IPackageFragment> children= getDirectChildren(packageFragment);
+		boolean hasSingleChild= children.size() == 1;
+		if (hasSingleChild) {
+			IPackageFragment singleChild= children.get(0);
+			return singleChild;
+		}
+		return null;
+	}
+
+	/**
+	 * <b>Example:</b> The following holds for a package root folder {@code src}, with packages:
+	 *
+	 * <pre>
+	 *   a
+	 *   |
+	 *   |-- b
+	 *       |
+	 *       |-- c
+	 *       |   |
+	 *       |   |-- d
+	 *       |
+	 *       |-- e
+	 *
+	 *   f
+	 *   |
+	 *   |-- g
+	 * </pre>
+	 * <p>
+	 * The packages that have a single child are {@code a}, {@code a.b.c} and {@code f}. Their children
+	 * are {@code a.b}, {@code a.b.c.d} and {@code f.g}, respectively.
+	 * </p>
+	 * <p>
+	 * Package {@code a.b} has children {@code a.b.c} and {@code a.b.e}. Packages {@code a.b.c.d},
+	 * {@code a.b.e} and {@code f.g} have no children.
+	 * </p>
+	 * <p>
+	 * All packages are {@code a}, {@code a.b}, {@code a.b.c}, {@code a.b.c.d}, {@code a.b.e}, {@code f}
+	 * and {@code f.g}.
+	 * </p>
+	 *
+	 * @return The direct children of the specified package. Never {@code null}.
+	 *
+	 * @param packageFragment The direct children of this fragment will be retrieved.
+	 * @throws JavaModelException If accessing the packages in the package root fails.
+	 */
+	public List<IPackageFragment> getDirectChildren(IPackageFragment packageFragment) throws JavaModelException {
+		initialize();
+		String packageName= packageFragment.getElementName();
+		List<IPackageFragment> childrenOfPackage= packagesCache.get(packageName);
+		if (childrenOfPackage == null) {
+			return Collections.EMPTY_LIST;
+		}
+		return Collections.unmodifiableList(childrenOfPackage);
+	}
+
+	private void initialize() throws JavaModelException {
+		if (!initialized) {
+			collectChildrenOfPackages();
+			initialized= true;
+		}
+	}
+
+	/**
+	 * Prepares for queries.
+	 *
+	 * @throws JavaModelException If accessing the packages in the package root fails.
+	 */
+	private void collectChildrenOfPackages() throws JavaModelException {
+		packagesCache.clear();
+
+		IJavaElement[] allPackages= packageRoot.getChildren();
+
+		for (IJavaElement child : allPackages) {
+			IPackageFragment currentPackage= (IPackageFragment) child;
+
+			String packageName= currentPackage.getElementName();
+
+			int index= packageName.lastIndexOf('.');
+			boolean hasParentPackage= index != -1;
+			if (hasParentPackage) {
+				String parentName= packageName.substring(0, index);
+
+				List<IPackageFragment> siblingsOfCurrentPackage= packagesCache.get(parentName);
+				if (siblingsOfCurrentPackage == null) {
+					siblingsOfCurrentPackage= new ArrayList<>();
+					packagesCache.put(parentName, siblingsOfCurrentPackage);
+				}
+				siblingsOfCurrentPackage.add(currentPackage);
+			}
+		}
+	}
+}
diff --git a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/packageview/PackageExplorerContentProvider.java b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/packageview/PackageExplorerContentProvider.java
index 1ac82f9..23f945a 100644
--- a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/packageview/PackageExplorerContentProvider.java
+++ b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/packageview/PackageExplorerContentProvider.java
@@ -90,6 +90,14 @@
 	private UIJob fUpdateJob;
 
 	/**
+	 * We use a cache to know whether a package has a single child for the hierarchical representation.
+	 * This avoids looping over all packages for each call to
+	 * {@link #getHierarchicalPackageParent(IPackageFragment)}. The cache is cleared on any Java model
+	 * change, as we aim to improve operations which go over all packages on by one.
+	 */
+	private final PackageCache.PerRootCache packageCache;
+
+	/**
 	 * Creates a new content provider for Java elements.
 	 * @param provideMembers if set, members of compilation units and class files are shown
 	 */
@@ -102,6 +110,7 @@
 		JavaPlugin.getDefault().getPreferenceStore().addPropertyChangeListener(this);
 
 		fUpdateJob= null;
+		packageCache= new PackageCache.PerRootCache();
 	}
 
 	private boolean arePackagesFoldedInHierarchicalLayout(){
@@ -116,6 +125,8 @@
 	public void elementChanged(final ElementChangedEvent event) {
 		final ArrayList<Runnable> runnables= new ArrayList<>();
 		try {
+			clearPackageCache();
+
 			// 58952 delete project does not update Package Explorer [package explorer]
 			// if the input to the viewer is deleted then refresh to avoid the display of stale elements
 			if (inputDeleted(runnables))
@@ -214,9 +225,14 @@
 
 	@Override
 	public void dispose() {
-		super.dispose();
+		clearPackageCache();
 		JavaCore.removeElementChangedListener(this);
 		JavaPlugin.getDefault().getPreferenceStore().removePropertyChangeListener(this);
+		super.dispose();
+	}
+
+	private void clearPackageCache() {
+		packageCache.clear();
 	}
 
 	@Override
@@ -227,7 +243,7 @@
 
 		// hierarchical package mode
 		ArrayList<Object> result= new ArrayList<>();
-		getHierarchicalPackageChildren(root, null, result);
+		getHierarchicalPackageRootChildren(root, result);
 		if (!isProjectPackageFragmentRoot(root)) {
 			Object[] nonJavaResources= root.getNonJavaResources();
 			for (int i= 0; i < nonJavaResources.length; i++) {
@@ -246,7 +262,7 @@
 		// hierarchical package mode
 		ArrayList<Object> result= new ArrayList<>();
 
-		getHierarchicalPackageChildren((IPackageFragmentRoot) fragment.getParent(), fragment, result);
+		getHierarchicalPackageChildren(fragment, result);
 		Object[] nonPackages= super.getPackageContent(fragment);
 		if (result.isEmpty())
 			return nonPackages;
@@ -402,27 +418,24 @@
 
 	// hierarchical packages
 	/**
-	 * Returns the hierarchical packages inside a given fragment or root.
+	 * Returns the hierarchical packages inside a given root.
 	 *
 	 * @param parent the parent package fragment root
-	 * @param fragment the package to get the children for or 'null' to get the children of the root
 	 * @param result Collection where the resulting elements are added
 	 * @throws JavaModelException if fetching the children fails
 	 */
-	private void getHierarchicalPackageChildren(IPackageFragmentRoot parent, IPackageFragment fragment, Collection<Object> result) throws JavaModelException {
+	private void getHierarchicalPackageRootChildren(IPackageFragmentRoot parent, Collection<Object> result) throws JavaModelException {
 		IJavaElement[] children= parent.getChildren();
-		String prefix= fragment != null ? fragment.getElementName() + '.' : ""; //$NON-NLS-1$
-		int prefixLen= prefix.length();
 		boolean is9OrHigher= JavaModelUtil.is9OrHigher(parent.getJavaProject());
 		for (int i= 0; i < children.length; i++) {
 			IPackageFragment curr= (IPackageFragment) children[i];
 			String name= curr.getElementName();
-			if (name.startsWith(prefix) && name.length() > prefixLen && name.indexOf('.', prefixLen) == -1) {
+			if (!name.isEmpty() && name.indexOf('.') == -1) {
 				if (fFoldPackages) {
-					curr= getFolded(children, curr);
+					curr= getFolded(curr);
 				}
 				result.add(curr);
-			} else if (fragment == null && curr.isDefaultPackage()) {
+			} else if (curr.isDefaultPackage()) {
 				if (isRelevantPackage(curr, is9OrHigher))
 					result.add(curr);
 				IJavaElement emptyModuleInfo= emptyModuleInfo(curr, is9OrHigher);
@@ -430,16 +443,32 @@
 					result.add(emptyModuleInfo);
 			}
 		}
-		if (fragment == null) {
-			if (is9OrHigher) {
-				IModuleDescription module= parent.getModuleDescription();
-				if (module != null) {
-					result.add(module.getParent());
-				}
+
+		if (is9OrHigher) {
+			IModuleDescription module= parent.getModuleDescription();
+			if (module != null) {
+				result.add(module.getParent());
 			}
 		}
 	}
 
+	/**
+	 * Returns the hierarchical packages inside a given fragment.
+	 *
+	 * @param fragment the package to get the children for or 'null' to get the children of the root
+	 * @param result Collection where the resulting elements are added
+	 * @throws JavaModelException if fetching the children fails
+	 */
+	private void getHierarchicalPackageChildren(IPackageFragment fragment, Collection<Object> result) throws JavaModelException {
+		List<IPackageFragment> children = packageCache.getDirectChildren(fragment);
+		for (IPackageFragment child : children) {
+			if (fFoldPackages) {
+				child= getFolded(child);
+			}
+			result.add(child);
+		}
+	}
+
 	boolean isRelevantPackage(IPackageFragment fragment, boolean is9OrHigher) throws JavaModelException {
 		if (is9OrHigher && !JavaModelUtil.containsOrdinaryCompilationUnit(fragment)) {
 			// at 9, a default package containing only module-info should be hidden:
@@ -475,8 +504,7 @@
 				if (element instanceof IPackageFragment) {
 					if (fFoldPackages) {
 						IPackageFragment fragment= (IPackageFragment) element;
-						IPackageFragmentRoot root= (IPackageFragmentRoot) fragment.getParent();
-						element= getFolded(root.getChildren(), fragment);
+						element= getFolded(fragment);
 					}
 					result.add(element);
 				}
@@ -493,7 +521,7 @@
 			IPackageFragment element= parent.getPackageFragment(realParentName);
 			if (element.exists()) {
 				try {
-					if (fFoldPackages && isEmpty(element) && findSinglePackageChild(element, parent.getChildren()) != null) {
+					if (fFoldPackages && isEmpty(element) && packageCache.hasSingleChild(element)) {
 						return getHierarchicalPackageParent(element);
 					}
 				} catch (JavaModelException e) {
@@ -513,9 +541,9 @@
 		return parent;
 	}
 
-	private static IPackageFragment getFolded(IJavaElement[] children, IPackageFragment pack) throws JavaModelException {
+	private IPackageFragment getFolded(IPackageFragment pack) throws JavaModelException {
 		while (isEmpty(pack)) {
-			IPackageFragment collapsed= findSinglePackageChild(pack, children);
+			IPackageFragment collapsed= packageCache.getSingleChild(pack);
 			if (collapsed == null) {
 				return pack;
 			}
@@ -528,24 +556,6 @@
 		return !fragment.containsJavaResources() && fragment.getNonJavaResources().length == 0;
 	}
 
-	private static IPackageFragment findSinglePackageChild(IPackageFragment fragment, IJavaElement[] children) {
-		String prefix= fragment.getElementName() + '.';
-		int prefixLen= prefix.length();
-		IPackageFragment found= null;
-		for (int i= 0; i < children.length; i++) {
-			IJavaElement element= children[i];
-			String name= element.getElementName();
-			if (name.startsWith(prefix) && name.length() > prefixLen && name.indexOf('.', prefixLen) == -1) {
-				if (found == null) {
-					found= (IPackageFragment) element;
-				} else {
-					return null;
-				}
-			}
-		}
-		return found;
-	}
-
 	// ------ delta processing ------
 
 	/**