From 6f34ec950509118985612dc07de4f1a279fd5dbc Mon Sep 17 00:00:00 2001 From: Francesco Nigro Date: Sat, 16 Nov 2024 01:13:38 +0100 Subject: [PATCH] Trying to share full dir names to save some old gen memory --- .../bootstrap/runner/RunnerClassLoader.java | 19 ++-- .../runner/SerializedApplication.java | 49 +++++--- .../quarkus/bootstrap/runner/StringView.java | 105 ++++++++++++++++++ .../runner/RunnerClassLoaderTest.java | 10 +- 4 files changed, 154 insertions(+), 29 deletions(-) create mode 100644 independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/runner/StringView.java diff --git a/independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/runner/RunnerClassLoader.java b/independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/runner/RunnerClassLoader.java index 42599e6ab11315..e27727f7fb23a4 100644 --- a/independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/runner/RunnerClassLoader.java +++ b/independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/runner/RunnerClassLoader.java @@ -36,7 +36,7 @@ public final class RunnerClassLoader extends ClassLoader { /** * A map of resources by dir name. Root dir/default package is represented by the empty string */ - private final Map resourceDirectoryMap; + private final Map resourceDirectoryMap; private final Set parentFirstPackages; private final Set nonExistentResources; @@ -53,7 +53,7 @@ public final class RunnerClassLoader extends ClassLoader { private final CracResource resource; - RunnerClassLoader(ClassLoader parent, Map resourceDirectoryMap, + RunnerClassLoader(ClassLoader parent, Map resourceDirectoryMap, Set parentFirstPackages, Set nonExistentResources, List fullyIndexedDirectories, Map directlyIndexedResourcesIndexMap) { super(parent); @@ -94,10 +94,9 @@ public Class loadClass(String name, boolean resolve) throws ClassNotFoundExce } final ClassLoadingResource[] resources; if (packageName == null) { - resources = resourceDirectoryMap.get(""); + resources = resourceDirectoryMap.get(StringView.EMPTY); } else { - String dirName = packageName.replace('.', '/'); - resources = resourceDirectoryMap.get(dirName); + resources = resourceDirectoryMap.get(StringView.of(packageName.replace('.', '/'))); } if (resources != null) { String classResource = fromClassNameToResourceName(name); @@ -237,16 +236,16 @@ private ClassLoadingResource[] getClassLoadingResources(final String name) { } if (!dirName.equals(name) && fullyIndexedDirectories.contains(dirName)) { if (dirName.isEmpty()) { - return resourceDirectoryMap.get(name); + return resourceDirectoryMap.get(StringView.of(name)); } // If we arrive here, we know that resource being queried belongs to one of the fully indexed directories // Had that resource existed however, it would have been present in directlyIndexedResourcesIndexMap return null; } - resources = resourceDirectoryMap.get(dirName); + resources = resourceDirectoryMap.get(StringView.of(dirName)); if (resources == null) { // the resource could itself be a directory - resources = resourceDirectoryMap.get(name); + resources = resourceDirectoryMap.get(StringView.of(name)); } return resources; } @@ -311,7 +310,7 @@ protected Class findClass(String moduleName, String name) { } public void close() { - for (Map.Entry entry : resourceDirectoryMap.entrySet()) { + for (var entry : resourceDirectoryMap.entrySet()) { for (ClassLoadingResource i : entry.getValue()) { i.close(); } @@ -320,7 +319,7 @@ public void close() { public void resetInternalCaches() { synchronized (this.currentlyBufferedResources) { - for (Map.Entry entry : resourceDirectoryMap.entrySet()) { + for (var entry : resourceDirectoryMap.entrySet()) { for (ClassLoadingResource i : entry.getValue()) { i.resetInternalCaches(); } diff --git a/independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/runner/SerializedApplication.java b/independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/runner/SerializedApplication.java index 1ba0a0fbbaf124..15fa82ab59e422 100644 --- a/independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/runner/SerializedApplication.java +++ b/independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/runner/SerializedApplication.java @@ -38,7 +38,7 @@ public class SerializedApplication { private static final List FULLY_INDEXED_PATHS = List.of("", "META-INF/services"); private static final int MAGIC = 0XF0315432; - private static final int VERSION = 2; + private static final int VERSION = 3; private static final ClassLoadingResource[] EMPTY_ARRAY = new ClassLoadingResource[0]; private static final JarResource SENTINEL = new JarResource(null, Path.of("wqxehxivam")); @@ -66,6 +66,7 @@ public static void write(OutputStream outputStream, String mainClass, Path appli data.writeInt(MAGIC); data.writeInt(VERSION); data.writeUTF(mainClass); + // numpaths data.writeShort(classPath.size()); Map> directlyIndexedResourcesToCPJarIndex = new LinkedHashMap<>(); for (int i = 0; i < classPath.size(); i++) { @@ -127,13 +128,15 @@ public static SerializedApplication read(InputStream inputStream, Path appRoot) allClassLoadingResources[pathCount] = resource; int numDirs = in.readUnsignedShort(); for (int i = 0; i < numDirs; ++i) { - String dir = in.readUTF(); - int j = dir.indexOf('/'); - while (j >= 0) { - resourceDirectoryTracker.addResourceDir(dir.substring(0, j), resource); - j = dir.indexOf('/', j + 1); + String fullDirName = in.readUTF(); + var dirName = StringView.of(fullDirName); + // now try to be smart and save some memory by NOT having substrings over and over again + final int subDirs = in.readInt(); + for (int j = 0; j < subDirs; j++) { + var subDirName = StringView.subOf(fullDirName, in.readInt(), in.readInt()); + resourceDirectoryTracker.addResourceDir(subDirName, resource); } - resourceDirectoryTracker.addResourceDir(dir, resource); + resourceDirectoryTracker.addResourceDir(dirName, resource); } } int packages = in.readUnsignedShort(); @@ -249,8 +252,26 @@ private static List writeJar(DataOutputStream out, Path jar) throws IOEx dirs.add(""); } out.writeShort(dirs.size()); - for (String i : dirs) { - out.writeUTF(i); + for (String dirName : dirs) { + // push this a bit forward to help the read to get faster! + // TODO: we could check if it's an ASCII string too and further optimize it + // write the positions of each / in the string + var subDirs = new ArrayList(); + int subDirLength = dirName.indexOf('/'); + while (subDirLength >= 0) { + var subDirName = dirName.substring(0, subDirLength); + subDirName.hashCode(); + subDirs.add(subDirName); + subDirLength = dirName.indexOf('/', subDirLength + 1); + } + // write in the opposite order here, to hydrate StringView(s) in the right order + out.writeUTF(dirName); + // TODO these could be made cheaper + out.writeInt(subDirs.size()); + for (String subDir : subDirs) { + out.writeInt(subDir.hashCode()); + out.writeInt(subDir.length()); + } } List result = new ArrayList<>(); for (List values : fullyIndexedPaths.values()) { @@ -327,10 +348,10 @@ private static void writeNullableString(DataOutputStream out, String string) thr * The reason for doing it this way to only create Sets when needed (which is only a fraction of the cases) */ private static class ResourceDirectoryTracker { - private final Map result = new HashMap<>(); - private final Map> overrides = new HashMap<>(); + private final Map result = new HashMap<>(); + private final Map> overrides = new HashMap<>(); - void addResourceDir(String dir, JarResource resource) { + void addResourceDir(StringView dir, JarResource resource) { ClassLoadingResource[] existing = result.get(dir); if (existing == null) { // this is the first the dir was ever tracked @@ -361,12 +382,12 @@ void addResourceDir(String dir, JarResource resource) { } } - Map getResult() { + Map getResult() { overrides.forEach(this::addToResult); return result; } - private void addToResult(String dir, Set jarResources) { + private void addToResult(StringView dir, Set jarResources) { result.put(dir, jarResources.toArray(EMPTY_ARRAY)); } } diff --git a/independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/runner/StringView.java b/independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/runner/StringView.java new file mode 100644 index 00000000000000..b7690a7548af02 --- /dev/null +++ b/independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/runner/StringView.java @@ -0,0 +1,105 @@ +package io.quarkus.bootstrap.runner; + +class StringView { + + private String s; + + private StringView(String s) { + this.s = s; + } + + private static final class SubStringView extends StringView { + private final int hash; + private final int length; + + private SubStringView(String fullString, int hash, int length) { + super(fullString); + this.hash = hash; + this.length = length; + } + } + + /** + * In theory the JIT is perfectly capable of performing bimorphic inlining here, if hot enough, but since + * we expect to run this code while not yet fully warmed up, let's help it a bit by having a single implementation + */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null) { + return false; + } + if (!(o instanceof StringView otherView)) { + return false; + } + final int length; + if (this instanceof SubStringView thisSub) { + length = thisSub.length; + } else { + length = s.length(); + } + final int otherLength; + if (o instanceof SubStringView otherSub) { + otherLength = otherSub.length; + } else { + otherLength = otherView.s.length(); + } + if (length != otherLength) { + return false; + } + return regionMatches(s, otherView.s, length); + } + + private static boolean regionMatches(String a, String b, int length) { + if (length == a.length() && length == b.length()) { + return a.equals(b); + } + // Intrinsified from https://github.com/openjdk/jdk/commit/861e302011bb3aaf0c8431c121b58a57b78481e3 + return a.regionMatches(0, b, 0, length); + } + + public static final StringView EMPTY = new StringView(""); + + public static StringView subOf(String s, int hashCode, int length) { + // we're not performing any specific check at runtime since this is a likely cold path + // and have to trust the data read from the serialized form + assert validateView(s, hashCode, length); + if (length == 0) { + return EMPTY; + } + if (length == s.length()) { + return new StringView(s); + } + if (length > s.length()) { + throw new IllegalArgumentException("Length must be less than or equal to the full string length"); + } + return new SubStringView(s, hashCode, length); + } + + private static boolean validateView(String s, int hashCode, int length) { + if (length < 0) { + throw new IllegalArgumentException("Length must be positive"); + } + if (length > s.length()) { + throw new IllegalArgumentException("Length must be less than or equal to the full string length"); + } + if (s.substring(0, length).hashCode() != hashCode) { + throw new IllegalArgumentException("Hash code does not match the substring hash code"); + } + return true; + } + + public static StringView of(String s) { + return new StringView(s); + } + + @Override + public int hashCode() { + if (this instanceof SubStringView sub) { + return sub.hash; + } + return s.hashCode(); + } +} diff --git a/independent-projects/bootstrap/runner/src/test/java/io/quarkus/bootstrap/runner/RunnerClassLoaderTest.java b/independent-projects/bootstrap/runner/src/test/java/io/quarkus/bootstrap/runner/RunnerClassLoaderTest.java index 6e83f00dd5f284..c85ddb10044572 100644 --- a/independent-projects/bootstrap/runner/src/test/java/io/quarkus/bootstrap/runner/RunnerClassLoaderTest.java +++ b/independent-projects/bootstrap/runner/src/test/java/io/quarkus/bootstrap/runner/RunnerClassLoaderTest.java @@ -17,22 +17,22 @@ public class RunnerClassLoaderTest { @Test public void testConcurrentJarCloseAndReload() throws Exception { - Map resourceDirectoryMap = new HashMap<>(); + Map resourceDirectoryMap = new HashMap<>(); - resourceDirectoryMap.put("org/simple", new ClassLoadingResource[] { + resourceDirectoryMap.put(StringView.of("org/simple"), new ClassLoadingResource[] { createProjectJarResource("simple-project-1.0.jar") }); // These jars are simply used to fill the RunnerClassLoader's jars cache - resourceDirectoryMap.put("org/easy", new ClassLoadingResource[] { + resourceDirectoryMap.put(StringView.of("org/easy"), new ClassLoadingResource[] { createProjectJarResource("empty-project-a-1.0.jar"), createProjectJarResource("empty-project-b-1.0.jar"), createProjectJarResource("easy-project-1.0.jar") }); // These jars will be used to evict the simple-project-1.0.jar from the cache again - resourceDirectoryMap.put("org/evict", new ClassLoadingResource[] { + resourceDirectoryMap.put(StringView.of("org/evict"), new ClassLoadingResource[] { createProjectJarResource("empty-project-c-1.0.jar"), createProjectJarResource("empty-project-d-1.0.jar"), createProjectJarResource("empty-project-e-1.0.jar"), createProjectJarResource("evict-project-1.0.jar") }); - resourceDirectoryMap.put("org/trivial", new ClassLoadingResource[] { + resourceDirectoryMap.put(StringView.of("org/trivial"), new ClassLoadingResource[] { createProjectJarResource("trivial-project-1.0.jar") }); RunnerClassLoader runnerClassLoader = new RunnerClassLoader(ClassLoader.getSystemClassLoader(), resourceDirectoryMap,