Skip to content

Commit

Permalink
Trying to share full dir names to save some old gen memory
Browse files Browse the repository at this point in the history
  • Loading branch information
franz1981 committed Nov 16, 2024
1 parent 51c4bb0 commit 6f34ec9
Show file tree
Hide file tree
Showing 4 changed files with 154 additions and 29 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, ClassLoadingResource[]> resourceDirectoryMap;
private final Map<StringView, ClassLoadingResource[]> resourceDirectoryMap;

private final Set<String> parentFirstPackages;
private final Set<String> nonExistentResources;
Expand All @@ -53,7 +53,7 @@ public final class RunnerClassLoader extends ClassLoader {

private final CracResource resource;

RunnerClassLoader(ClassLoader parent, Map<String, ClassLoadingResource[]> resourceDirectoryMap,
RunnerClassLoader(ClassLoader parent, Map<StringView, ClassLoadingResource[]> resourceDirectoryMap,
Set<String> parentFirstPackages, Set<String> nonExistentResources,
List<String> fullyIndexedDirectories, Map<String, ClassLoadingResource[]> directlyIndexedResourcesIndexMap) {
super(parent);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -311,7 +310,7 @@ protected Class<?> findClass(String moduleName, String name) {
}

public void close() {
for (Map.Entry<String, ClassLoadingResource[]> entry : resourceDirectoryMap.entrySet()) {
for (var entry : resourceDirectoryMap.entrySet()) {
for (ClassLoadingResource i : entry.getValue()) {
i.close();
}
Expand All @@ -320,7 +319,7 @@ public void close() {

public void resetInternalCaches() {
synchronized (this.currentlyBufferedResources) {
for (Map.Entry<String, ClassLoadingResource[]> entry : resourceDirectoryMap.entrySet()) {
for (var entry : resourceDirectoryMap.entrySet()) {
for (ClassLoadingResource i : entry.getValue()) {
i.resetInternalCaches();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public class SerializedApplication {
private static final List<String> 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"));
Expand Down Expand Up @@ -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<String, List<Integer>> directlyIndexedResourcesToCPJarIndex = new LinkedHashMap<>();
for (int i = 0; i < classPath.size(); i++) {
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -249,8 +252,26 @@ private static List<String> 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<String>();
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<String> result = new ArrayList<>();
for (List<String> values : fullyIndexedPaths.values()) {
Expand Down Expand Up @@ -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<String, ClassLoadingResource[]> result = new HashMap<>();
private final Map<String, Set<ClassLoadingResource>> overrides = new HashMap<>();
private final Map<StringView, ClassLoadingResource[]> result = new HashMap<>();
private final Map<StringView, Set<ClassLoadingResource>> 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
Expand Down Expand Up @@ -361,12 +382,12 @@ void addResourceDir(String dir, JarResource resource) {
}
}

Map<String, ClassLoadingResource[]> getResult() {
Map<StringView, ClassLoadingResource[]> getResult() {
overrides.forEach(this::addToResult);
return result;
}

private void addToResult(String dir, Set<? extends ClassLoadingResource> jarResources) {
private void addToResult(StringView dir, Set<? extends ClassLoadingResource> jarResources) {
result.put(dir, jarResources.toArray(EMPTY_ARRAY));
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,22 @@ public class RunnerClassLoaderTest {

@Test
public void testConcurrentJarCloseAndReload() throws Exception {
Map<String, ClassLoadingResource[]> resourceDirectoryMap = new HashMap<>();
Map<StringView, ClassLoadingResource[]> 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,
Expand Down

0 comments on commit 6f34ec9

Please sign in to comment.