Skip to content

Commit

Permalink
Use NIO FileSystem for zips, which is much faster then ZipInputStream.
Browse files Browse the repository at this point in the history
  • Loading branch information
LexManos committed Dec 6, 2024
1 parent ca7c10a commit 399e5ec
Show file tree
Hide file tree
Showing 3 changed files with 157 additions and 74 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

import java.nio.file.Path;
import java.util.List;
import java.util.Map;

/*
* This is mean to allow modifications to the classpath before booting.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ private boolean processAuto(List<Path[]> classpath) {
lst.add(path);
} //else if (DEBUG) log("Unknown directory format: " + path);
} else {
var module = Util.findModuleName(path, null);
var module = Util.findModuleNameImpl(path, false);
if (module.name() == null) {
//var meta = JarMetadata.fromFileName(path, Set.of(), List.of());
//module = new ModuleVersion(meta.name(), meta.version(), module.layer());
Expand Down
228 changes: 156 additions & 72 deletions bs-dev/src/main/java/net/minecraftforge/bootstrap/dev/Util.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,21 @@
package net.minecraftforge.bootstrap.dev;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.lang.module.ModuleDescriptor;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.jar.JarFile;
import java.util.List;
import java.util.jar.Manifest;
import java.util.jar.Attributes.Name;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.jar.Attributes.Name;
import java.util.jar.JarFile;

class Util {
private static final Name AUTOMATIC_MODULE_NAME = new Name("Automatic-Module-Name");
Expand All @@ -29,7 +33,7 @@ class Util {

static ModuleVersion findModuleName(Collection<Path> paths) {
for (var path : paths) {
var ret = findModuleName(path, null);
var ret = findModuleNameImpl(path, false);
if (ret.name != null)
return ret;
}
Expand All @@ -38,87 +42,31 @@ static ModuleVersion findModuleName(Collection<Path> paths) {

static ModuleVersion findModuleName(Path... paths) {
for (var path : paths) {
var ret = findModuleName(path, null);
var ret = findModuleNameImpl(path, false);
if (ret.name != null)
return ret;
}
return null;
}

record ModuleVersion(String name, String version, String layer) {}
static ModuleVersion findModuleName(Path path, String _default) {
static ModuleVersion findModuleNameImpl(Path path, boolean slow) {
try {
Manifest mf = null;
record InfoData(int version, byte[] data) {}
var infos = new ArrayList<InfoData>();

Candidates data = null;
if (Files.isDirectory(path)) {
var meta_inf = findInsensitive(path, META_INF);
var manifest = findInsensitive(meta_inf, MANIFEST);

if (manifest != null && Files.exists(manifest)) {
try (var is = Files.newInputStream(manifest)) {
mf = new Manifest(is);
}
}

var module_info = path.resolve(MODULE_INFO);
if (Files.exists(module_info)) {
infos.add(new InfoData(0, Files.readAllBytes(path)));
}

if (isMultiRelease(mf)) {
var versions = findInsensitive(meta_inf, VERSIONS);
if (versions != null) {
Files.list(versions)
.forEach(v -> {
try {
int version = Integer.parseInt(v.getFileName().toString());
var mod_info = v.resolve(MODULE_INFO);
if (version <= Runtime.version().feature() && Files.exists(mod_info))
infos.add(new InfoData(version, Files.readAllBytes(mod_info)));
} catch (NumberFormatException e) {
// If its not a numerical directory we don't care
} catch (IOException e) {
sneak(e);
}
});
}
}
data = findCandidatesDirectory(path);
} else if (slow) {
data = findCandidatesZip(path);
} else {
// I would use JarFile here and let the JVM handle all this.
// but it only works on File objects not Paths
// Normal java gets around this by extracting all non-file paths
// to a temp directory and opening them as normal files.
// I do not want to do this. So this is what you get.
try (var zip = new ZipInputStream(Files.newInputStream(path))) {
ZipEntry entry;
while ((entry = zip.getNextEntry()) != null) {
var name = entry.getName();
if (mf == null && JarFile.MANIFEST_NAME.equalsIgnoreCase(name)) {
mf = new Manifest(zip);
} else if (name.endsWith(MODULE_INFO)) {
int version = 0;
if (name.startsWith(VERSION_DIR)) {
if (!isMultiRelease(mf))
continue;

int idx = name.indexOf('/', VERSION_DIR.length());
var ver = name.substring(VERSION_DIR.length(), idx);
try {
version = Integer.parseInt(ver);
} catch (NumberFormatException e) {
// If its not a numerical directory we don't care
version = Integer.MAX_VALUE;
}
}
if (version <= Runtime.version().feature())
infos.add(new InfoData(version, zip.readAllBytes()));
}
}
try (FileSystem jarFS = FileSystems.newFileSystem(path)) {
var root = jarFS.getPath("/");
data = findCandidatesDirectory(root);
}
}

var mf = data.mf;
var infos = data.modules;

InfoData info = null;
if (infos.size() == 1) {
info = infos.get(0);
Expand All @@ -129,7 +77,7 @@ record InfoData(int version, byte[] data) {}
.orElse(null);
}

String name = _default;
String name = null;
String version = null;
String layer = null;

Expand All @@ -150,6 +98,89 @@ record InfoData(int version, byte[] data) {}
}
}

record InfoData(int version, byte[] data) {}
record Candidates(Manifest mf, List<InfoData> modules) {}
private static Candidates findCandidatesDirectory(Path path) {
try {
var infos = new ArrayList<InfoData>();
var meta_inf = findInsensitive(path, META_INF);
var manifest = findInsensitive(meta_inf, MANIFEST);
Manifest mf = null;

if (manifest != null && Files.exists(manifest)) {
try (var is = Files.newInputStream(manifest)) {
mf = new Manifest(is);
}
}

var module_info = path.resolve(MODULE_INFO);
if (Files.exists(module_info)) {
infos.add(new InfoData(0, Files.readAllBytes(module_info)));
}

if (isMultiRelease(mf)) {
var versions = findInsensitive(meta_inf, VERSIONS);
if (versions != null) {
Files.list(versions)
.forEach(v -> {
try {
int version = Integer.parseInt(v.getFileName().toString());
var mod_info = v.resolve(MODULE_INFO);
if (version <= Runtime.version().feature() && Files.exists(mod_info))
infos.add(new InfoData(version, Files.readAllBytes(mod_info)));
} catch (NumberFormatException e) {
// If its not a numerical directory we don't care
} catch (IOException e) {
sneak(e);
}
});
}
}
return new Candidates(mf, infos);
} catch (IOException e) {
return sneak(e);
}
}

private static Candidates findCandidatesZip(Path path) {
// I would use JarFile here and let the JVM handle all this.
// but it only works on File objects not Paths
// Normal java gets around this by extracting all non-file paths
// to a temp directory and opening them as normal files.
// I do not want to do this. So this is what you get.
try (var zip = new ZipInputStream(Files.newInputStream(path))) {
var infos = new ArrayList<InfoData>();
Manifest mf = null;
ZipEntry entry;
while ((entry = zip.getNextEntry()) != null) {
var name = entry.getName();
if (mf == null && JarFile.MANIFEST_NAME.equalsIgnoreCase(name)) {
mf = new Manifest(zip);
} else if (name.endsWith(MODULE_INFO)) {
int version = 0;
if (name.startsWith(VERSION_DIR)) {
if (!isMultiRelease(mf))
continue;

int idx = name.indexOf('/', VERSION_DIR.length());
var ver = name.substring(VERSION_DIR.length(), idx);
try {
version = Integer.parseInt(ver);
} catch (NumberFormatException e) {
// If its not a numerical directory we don't care
version = Integer.MAX_VALUE;
}
}
if (version <= Runtime.version().feature())
infos.add(new InfoData(version, zip.readAllBytes()));
}
}
return new Candidates(mf, infos);
} catch (IOException e) {
return sneak(e);
}
}

private static Path findInsensitive(Path root, String name) {
if (root == null)
return null;
Expand Down Expand Up @@ -178,4 +209,57 @@ private static boolean isMultiRelease(Manifest mf) {
static <E extends Throwable, R> R sneak(Exception exception) throws E {
throw (E)exception;
}

// Only here for testing/profiling because I don't want to setup a whole benchmark sub-project
// Intentionally left in so others can reproduce and I dont have to write this later.
public static void main(String[] args) {
var runs = 100;
var slow = false; // Wither or not to force the slow code path
var forgeWorkspace = Path.of(args.length == 1 ? args[0] : "Z:/Projects/Forge_1214");
if (Files.exists(forgeWorkspace)) {
var exploded = forgeWorkspace.resolve("projects/forge/bin/main");
if (Files.exists(exploded)) {
for (int x = 0; x < runs; x++) {
var mod = findModuleNameImpl(exploded, slow);
if (mod == null || !"net.minecraftforge.forge".equals(mod.name()))
throw new IllegalStateException("Failed to find correct module name from exploded: " + mod);
}
}
var jared = forgeWorkspace.resolve("projects/mcp/build/mcp/downloadServer/server.jar");
if (Files.exists(jared)) {
for (int x = 0; x < runs; x++) {
var mod = findModuleNameImpl(jared, slow);
if (mod != null && mod.name() != null)
throw new IllegalStateException("Expected null module from server jar but was " + mod);
}
}
}

var paths = findAllClassPathEntries();

for (int x = 0; x < runs; x++) {
for (var path : paths) {
var mod = findModuleNameImpl(path, slow);
System.out.println(mod);
}
}
}

private static List<Path> findAllClassPathEntries() {
try {
var parts = System.getProperty("java.class.path").split(File.pathSeparator);
var paths = new ArrayList<Path>();
for (var part : parts) {
var path = new File(part).getCanonicalFile().toPath();
if (!Files.exists(path))
continue;
if (Files.isDirectory(path) && Files.list(path).findAny().isEmpty())
continue;
paths.add(path);
}
return paths;
} catch (IOException e) {
return sneak(e);
}
}
}

0 comments on commit 399e5ec

Please sign in to comment.