diff --git a/folia-phantom/folia-phantom-core/src/main/java/com/patch/foliaphantom/patcher/PluginPatcher.java b/folia-phantom/folia-phantom-core/src/main/java/com/patch/foliaphantom/patcher/PluginPatcher.java
index 417784e..a5a213e 100644
--- a/folia-phantom/folia-phantom-core/src/main/java/com/patch/foliaphantom/patcher/PluginPatcher.java
+++ b/folia-phantom/folia-phantom-core/src/main/java/com/patch/foliaphantom/patcher/PluginPatcher.java
@@ -26,22 +26,25 @@
import java.util.Set;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
+import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;
import java.util.jar.JarOutputStream;
+import java.util.zip.Deflater;
/**
* JAR 全体のパッチ処理を統括するオーケストレーター。
*
- *
v2.1 では、入力の読み取りと出力順序を決定的に保ちながら、CPU 負荷の高い
- * ASM クラス変換だけを並列化する。非 class エントリはコピーし、署名ファイルは
- * 除去し、plugin.yml とランタイムブリッジを出力時に処理する。
+ * CPU 負荷の高い ASM クラス変換のみを並列化し、非 class エントリは
+ * 不要なタスク生成を行わず直接コピーする。JAR 出力は速度優先の圧縮を使用する。
*/
public final class PluginPatcher {
private static final Logger log = LoggerFactory.getLogger(PluginPatcher.class);
private static final int ASM_API = Opcodes.ASM9;
+ private static final byte[] BUKKIT_CONSTANT_POOL_MARKER =
+ "org/bukkit/".getBytes(StandardCharsets.US_ASCII);
private static final String[] RUNTIME_CLASSES = {
"com/patch/foliaphantom/patcher/FoliaPatcher.class",
"com/patch/foliaphantom/patcher/FoliaPatcher$FoliaBukkitTask.class",
@@ -60,7 +63,8 @@ public final class PluginPatcher {
public PluginPatcher(Path outputDir, boolean verbose) {
this.outputDir = outputDir;
this.verbose = verbose;
- int parallelism = Math.max(1, Runtime.getRuntime().availableProcessors() - 1);
+ int processors = Runtime.getRuntime().availableProcessors();
+ int parallelism = Math.max(1, processors > 2 ? processors - 1 : processors);
this.forkJoinPool = new ForkJoinPool(parallelism);
this.transformers = List.of(
new ThreadSafetyTransformer(),
@@ -77,20 +81,20 @@ public Path patchPlugin(Path jarPath) throws IOException {
return jarPath;
}
+ long startedAt = System.nanoTime();
patchedClassCount.set(0);
skippedClassCount.set(0);
Files.createDirectories(outputDir);
Path outputPath = outputDir.resolve("patched-" + fileName);
log.info("Patching plugin: {} with {} worker(s)", fileName, forkJoinPool.getParallelism());
- List entries = readEntries(jarPath);
- List prepared = prepareEntries(entries);
+ List prepared = readAndPrepareEntries(jarPath);
try (JarOutputStream output = new JarOutputStream(Files.newOutputStream(outputPath))) {
+ output.setLevel(Deflater.BEST_SPEED);
Set writtenNames = new HashSet<>();
for (PreparedEntry entry : prepared) {
- byte[] data = entry.contentTask().join();
- addJarEntry(output, entry.name(), data);
+ addJarEntry(output, entry.name(), entry.content());
writtenNames.add(entry.name());
}
bundleRuntimeClasses(output, writtenNames);
@@ -103,13 +107,14 @@ public Path patchPlugin(Path jarPath) throws IOException {
throw exception;
}
- log.info("Patch complete for: {} (patched: {}, skipped: {})",
- fileName, patchedClassCount.get(), skippedClassCount.get());
+ long elapsedMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startedAt);
+ log.info("Patch complete for: {} (patched: {}, skipped: {}, elapsed: {} ms)",
+ fileName, patchedClassCount.get(), skippedClassCount.get(), elapsedMs);
return outputPath;
}
- private List readEntries(Path jarPath) throws IOException {
- List entries = new ArrayList<>();
+ private List readAndPrepareEntries(Path jarPath) throws IOException {
+ List entries = new ArrayList<>();
try (JarInputStream input = new JarInputStream(Files.newInputStream(jarPath))) {
JarEntry entry;
while ((entry = input.getNextJarEntry()) != null) {
@@ -117,36 +122,28 @@ private List readEntries(Path jarPath) throws IOException {
if (entry.isDirectory() || isSignatureFile(name)) {
continue;
}
- entries.add(new InputEntry(name, input.readAllBytes()));
- }
- }
- return entries;
- }
- private List prepareEntries(List entries) {
- List prepared = new ArrayList<>(entries.size());
- for (InputEntry entry : entries) {
- if ("plugin.yml".equals(entry.name())) {
- byte[] modified = modifyPluginYml(entry.content());
- prepared.add(new PreparedEntry(entry.name(),
- forkJoinPool.submit(() -> modified)));
- } else if (entry.name().endsWith(".class")) {
- ForkJoinTask task = forkJoinPool.submit(
- () -> transformClass(entry.name(), entry.content()));
- prepared.add(new PreparedEntry(entry.name(), task));
- } else {
- prepared.add(new PreparedEntry(entry.name(),
- forkJoinPool.submit(entry::content)));
+ byte[] content = input.readAllBytes();
+ if ("plugin.yml".equals(name)) {
+ entries.add(PreparedEntry.direct(name, modifyPluginYml(content)));
+ } else if (name.endsWith(".class")) {
+ ForkJoinTask task = forkJoinPool.submit(
+ () -> transformClass(name, content));
+ entries.add(PreparedEntry.async(name, task));
+ } else {
+ entries.add(PreparedEntry.direct(name, content));
+ }
}
}
- return prepared;
+ return entries;
}
private byte[] transformClass(String entryName, byte[] classBytes) {
String className = entryName.substring(0, entryName.length() - ".class".length())
.replace('/', '.');
- if (!needsPatching(classBytes)) {
+ // Most library classes do not reference Bukkit at all. Avoid constructing ASM visitors for them.
+ if (!containsBytes(classBytes, BUKKIT_CONSTANT_POOL_MARKER) || !needsPatching(classBytes)) {
skippedClassCount.incrementAndGet();
if (verbose) {
log.debug("Skipping class (no patch needed): {}", className);
@@ -180,11 +177,24 @@ private byte[] transformClass(String entryName, byte[] classBytes) {
private boolean needsPatching(byte[] classBytes) {
ClassReader reader = new ClassReader(classBytes);
ScanningClassVisitor scanner = new ScanningClassVisitor(ASM_API, null);
- // Method instructions are the primary detection signal; SKIP_CODE would disable them.
reader.accept(scanner, ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
return scanner.needsPatching();
}
+ private static boolean containsBytes(byte[] haystack, byte[] needle) {
+ int limit = haystack.length - needle.length;
+ outer:
+ for (int i = 0; i <= limit; i++) {
+ for (int j = 0; j < needle.length; j++) {
+ if (haystack[i + j] != needle[j]) {
+ continue outer;
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+
private byte[] modifyPluginYml(byte[] bytes) {
String content = new String(bytes, StandardCharsets.UTF_8);
if (content.contains("folia-supported: true")) {
@@ -242,9 +252,21 @@ public int getSkippedClassCount() {
return skippedClassCount.get();
}
- private record InputEntry(String name, byte[] content) {
- }
+ private record PreparedEntry(
+ String name,
+ byte[] directContent,
+ ForkJoinTask contentTask) {
+
+ private static PreparedEntry direct(String name, byte[] content) {
+ return new PreparedEntry(name, content, null);
+ }
+
+ private static PreparedEntry async(String name, ForkJoinTask task) {
+ return new PreparedEntry(name, null, task);
+ }
- private record PreparedEntry(String name, ForkJoinTask contentTask) {
+ private byte[] content() {
+ return contentTask == null ? directContent : contentTask.join();
+ }
}
}