Bug 570896: Optimise happy path

By optimising the happy path of the lookup, we can save about 10% of
the cost of comparisons.

Refactor existing code into FileStoreUtil for convenience and to mark
other methods as private.

Change-Id: Ie828af3d31edfd032f7cdbb915f4757d516db0ab
Signed-off-by: Alex Blewitt <alex.blewitt@gmail.com>
diff --git a/bundles/org.eclipse.core.filesystem/src/org/eclipse/core/filesystem/IFileStore.java b/bundles/org.eclipse.core.filesystem/src/org/eclipse/core/filesystem/IFileStore.java
index 15f3397..0e5386f 100644
--- a/bundles/org.eclipse.core.filesystem/src/org/eclipse/core/filesystem/IFileStore.java
+++ b/bundles/org.eclipse.core.filesystem/src/org/eclipse/core/filesystem/IFileStore.java
@@ -577,25 +577,9 @@
 	 * @since org.eclipse.core.filesystem 1.9
 	 */
 	public default int compareTo(IFileStore other) {
-		int compare = FileStoreUtil.compareStringOrNull(this.getFileSystem().getScheme(), other.getFileSystem().getScheme());
-		if (compare != 0)
-			return compare;
-		// compare based on URI path segment values
-		URI uri1;
-		URI uri2;
-		try {
-			uri1 = this.toURI();
-		} catch (Exception e1) {
-			// protect against misbehaving 3rd party code in file system implementations
-			uri1 = null;
+		if (other == null) {
+			return 1;
 		}
-		try {
-			uri2 = other.toURI();
-		} catch (Exception e2) {
-			// protect against misbehaving 3rd party code in file system implementations
-			uri2 = null;
-		}
-		// use old slow compare for compatibility reason. Does have a memory hotspot see bug 570896
-		return FileStoreUtil.comparePathUri(uri1, uri2);
+		return FileStoreUtil.compareFileStore(this, other);
 	}
 }
diff --git a/bundles/org.eclipse.core.filesystem/src/org/eclipse/core/internal/filesystem/FileStoreUtil.java b/bundles/org.eclipse.core.filesystem/src/org/eclipse/core/internal/filesystem/FileStoreUtil.java
index 0dbc549..4560d56 100644
--- a/bundles/org.eclipse.core.filesystem/src/org/eclipse/core/internal/filesystem/FileStoreUtil.java
+++ b/bundles/org.eclipse.core.filesystem/src/org/eclipse/core/internal/filesystem/FileStoreUtil.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2021  Joerg Kubitz and others.
+ * Copyright (c) 2021 Joerg Kubitz and others.
  *
  * This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License 2.0
@@ -14,6 +14,7 @@
 package org.eclipse.core.internal.filesystem;
 
 import java.net.URI;
+import org.eclipse.core.filesystem.IFileStore;
 import org.eclipse.core.runtime.IPath;
 import org.eclipse.core.runtime.Path;
 
@@ -31,7 +32,7 @@
 	 * This is the old slow implementation and has a memory hotspot see bug 570896. Prefer to use compareNormalisedUri!
 	 * @since org.eclipse.core.filesystem 1.9
 	 */
-	public static int comparePathUri(URI uri1, URI uri2) {
+	private static int comparePathUri(URI uri1, URI uri2) {
 		if (uri1 == null && uri2 == null)
 			return 0;
 		int compare;
@@ -110,7 +111,7 @@
 		return 0;
 	}
 
-	static int comparePathSegments(String p1, String p2) {
+	public static int comparePathSegments(String p1, String p2) {
 		int compare;
 		compare = compareSlashFirst(p1, p2);
 		if (compare != 0)
@@ -159,7 +160,7 @@
 	 * Compares two strings that are possibly null.
 	 * @since org.eclipse.core.filesystem 1.9
 	 */
-	public static int compareStringOrNull(String string1, String string2) {
+	private static int compareStringOrNull(String string1, String string2) {
 		if (string1 == null) {
 			if (string2 == null)
 				return 0;
@@ -169,4 +170,33 @@
 			return -1;
 		return string1.compareTo(string2);
 	}
+
+	/**
+	 * Compares two file stores by comparing their URIs.
+	 * @param fileStore1 the first fileStore to compare, cannot be null
+	 * @param fileStore2 the second fileStore to compare, cannot be null
+	 * @return 0 if the fileStores are equal, 1 if fileStore1 is bigger than fileStore2, -1 otherwise
+	 */
+	public static int compareFileStore(IFileStore fileStore1, IFileStore fileStore2) {
+		int compare = compareStringOrNull(fileStore1.getFileSystem().getScheme(), fileStore2.getFileSystem().getScheme());
+		if (compare != 0)
+			return compare;
+		// compare based on URI path segment values
+		URI uri1;
+		URI uri2;
+		try {
+			uri1 = fileStore1.toURI();
+		} catch (Exception e1) {
+			// protect against misbehaving 3rd party code in file system implementations
+			uri1 = null;
+		}
+		try {
+			uri2 = fileStore2.toURI();
+		} catch (Exception e2) {
+			// protect against misbehaving 3rd party code in file system implementations
+			uri2 = null;
+		}
+		// use old slow compare for compatibility reason. Does have a memory hotspot see bug 570896
+		return comparePathUri(uri1, uri2);
+	}
 }
diff --git a/bundles/org.eclipse.core.filesystem/src/org/eclipse/core/internal/filesystem/local/LocalFile.java b/bundles/org.eclipse.core.filesystem/src/org/eclipse/core/internal/filesystem/local/LocalFile.java
index dc68df5..d40504d 100644
--- a/bundles/org.eclipse.core.filesystem/src/org/eclipse/core/internal/filesystem/local/LocalFile.java
+++ b/bundles/org.eclipse.core.filesystem/src/org/eclipse/core/internal/filesystem/local/LocalFile.java
@@ -471,10 +471,12 @@
 
 	@Override
 	public int compareTo(IFileStore other) {
-		int compare = FileStoreUtil.compareStringOrNull(this.getFileSystem().getScheme(), other.getFileSystem().getScheme());
-		if (compare != 0)
-			return compare;
-		// override with fast implementation:
-		return FileStoreUtil.compareNormalisedUri(this.toURI(), other.toURI());
+		if (other instanceof LocalFile) {
+			// We can compare paths in the local file implementation, because LocalFile don't have a query string, port, or authority
+			// We use `toURI` here because it performs file normalisation e.g. /a/b/../c -> /a/c
+			// The URI is cached by the LocalFile after normalisation so this effectively results in a straight lookup
+			return FileStoreUtil.comparePathSegments(this.toURI().getPath(), ((LocalFile) other).toURI().getPath());
+		}
+		return super.compareTo(other);
 	}
 }
diff --git a/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/filesystem/FileStoreTest.java b/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/filesystem/FileStoreTest.java
index 28300e0..263edce 100755
--- a/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/filesystem/FileStoreTest.java
+++ b/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/filesystem/FileStoreTest.java
@@ -23,6 +23,7 @@
 import org.eclipse.core.filesystem.*;
 import org.eclipse.core.filesystem.provider.FileSystem;
 import org.eclipse.core.internal.filesystem.NullFileSystem;
+import org.eclipse.core.internal.filesystem.local.LocalFile;
 import org.eclipse.core.internal.filesystem.local.LocalFileSystem;
 import org.eclipse.core.resources.IResource;
 import org.eclipse.core.runtime.*;
@@ -649,6 +650,8 @@
 		assertEquals("3.1", schemeCompare, nabc.compareTo(labd));
 		assertEquals("3.2", -schemeCompare, labd.compareTo(nabc));
 		assertEquals("3.3", -schemeCompare, labc.compareTo(nabd));
+		assertEquals("4.0", 1, labc.compareTo(null));
+		assertEquals("4.1", 1, nabc.compareTo(null));
 	}
 
 	public void testSortOrderPaths() {
@@ -670,8 +673,15 @@
 		).collect(Collectors.toList());
 		paths = new ArrayList<>(paths); // to get a mutable copy for shuffling
 		Collections.shuffle(paths);
-		Stream<IFileStore> stores = paths.stream().map(Path::new).map(lfs::getStore);
-		List<String> sortedPaths = stores.sorted(IFileStore::compareTo).map(IFileStore::toURI).map(URI::getPath).collect(Collectors.toList());
-		assertEquals("1.0 ", pathsTrimmed, sortedPaths);
+		// Test with new Path(string).getStore()
+		Stream<IFileStore> pathStores = paths.stream().map(Path::new).map(lfs::getStore);
+		List<String> sortedPathStores = pathStores.sorted(IFileStore::compareTo).map(IFileStore::toURI)
+				.map(URI::getPath).collect(Collectors.toList());
+		assertEquals("1.0 ", pathsTrimmed, sortedPathStores);
+		// Test with new LocalFile(new File(string)))
+		Stream<IFileStore> localFileStores = paths.stream().map(File::new).map(LocalFile::new);
+		List<String> sortedLocalFileStores = localFileStores.sorted(IFileStore::compareTo).map(IFileStore::toURI)
+				.map(URI::getPath).collect(Collectors.toList());
+		assertEquals("2.0 ", pathsTrimmed, sortedLocalFileStores);
 	}
 }