Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ public final class CompanionApp {
private static volatile CodeInsightService codeInsightService;
private static volatile RuntimeSourceCatalog runtimeSourceCatalog = RuntimeSourceCatalog.empty();
private static RuntimeIndexService runtimeIndexService;
private static final ScriptCompilationService scriptCompiler = new ScriptCompilationService(CompanionApp::send);
private static final ScriptCompilationService scriptCompiler = new ScriptCompilationService(CompanionApp::send, CompanionApp::send);
private static volatile String evaluationClasspath;
private static volatile Path activeIndexFile;
private static volatile String activeRuntimeSignature;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,6 @@

import com.github.minecraft_ta.totalDebugCompanion.bytecode.RuntimeSnapshotBytecodeSource.Source;
import com.github.tth05.jindex.ClassIndex;
import com.github.minecraft_ta.totaldebug.evaluation.ClassDeclarations;
import com.github.minecraft_ta.totaldebug.evaluation.ServerManifest;
import java.io.ByteArrayInputStream;
import java.util.function.Supplier;
import com.github.tth05.jindex.IndexedClass;
import com.github.tth05.jindex.IndexedPackage;

Expand All @@ -15,6 +11,7 @@
import javax.tools.SimpleJavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.StandardLocation;

import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
Expand All @@ -25,21 +22,21 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Supplier;
import java.util.jar.JarFile;
import java.util.zip.ZipFile;

/** Borrows Companion's index and opens only the archives javac actually reads. */
final class IndexedJavaFileManager extends ForwardingJavaFileManager<StandardJavaFileManager> {
private final ClassIndex index;
private final Supplier<ServerManifest> serverManifest;
private final Map<Integer, String> archiveHashes = new HashMap<>();
private final Supplier<Set<String>> unsupportedClasses;
private final Map<Integer, Path> sources = new HashMap<>();
private final Map<Integer, JarFile> archives = new HashMap<>();

IndexedJavaFileManager(StandardJavaFileManager standard, ClassIndex index, List<Source> sources, Supplier<ServerManifest> serverManifest) {
IndexedJavaFileManager(StandardJavaFileManager standard, ClassIndex index, List<Source> sources, Supplier<Set<String>> unsupportedClasses) {
super(standard);
this.index = index;
this.serverManifest = serverManifest;
this.unsupportedClasses = unsupportedClasses;
for (Source source : sources) {
// --release supplies the platform classes from javac's own standard manager.
if (!"jrt:/".equals(source.logicalUri())) this.sources.put(source.sourceId(), source.path());
Expand Down Expand Up @@ -122,31 +119,22 @@ private IndexedInput(String name, int sourceId, Path path) {

@Override
public InputStream openInputStream() throws IOException {
Set<String> unsupported = unsupportedClasses.get();
if (unsupported != null && unsupported.contains(this.name)) {
throw new IOException("Server compilation unsupported: class " + this.name
+ " is absent or its declarations differ on the server. Use matching client/server classes.");
}
String resource = this.name.replace('.', '/') + ".class";
if (Files.isDirectory(this.path)) return checked(Files.newInputStream(this.path.resolve(resource)));
if (Files.isDirectory(this.path)) return Files.newInputStream(this.path.resolve(resource));
JarFile archive = archives.get(this.sourceId);
if (archive == null) {
archive = new JarFile(this.path.toFile(), false, ZipFile.OPEN_READ, Runtime.Version.parse("21"));
archives.put(this.sourceId, archive);
}
var entry = archive.getJarEntry(resource);
if (entry == null) throw new IOException("Runtime index points to a missing class: " + this.path + " / " + resource);
return checked(archive.getInputStream(entry));
return archive.getInputStream(entry);
}

private InputStream checked(InputStream input) throws IOException {
ServerManifest manifest = serverManifest.get();
if (manifest == null) return input;
try (input) {
byte[] bytes = input.readAllBytes();
String hash = archiveHashes.get(this.sourceId);
if (hash == null) {
hash = Files.isDirectory(this.path) ? "" : ClassDeclarations.archiveFingerprint(this.path);
archiveHashes.put(this.sourceId, hash);
}
manifest.requireCompatible(this.name, hash, bytes);
return new ByteArrayInputStream(bytes);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,20 @@
import com.github.minecraft_ta.totalDebugCompanion.runtime.RuntimeIndexService.ReadySnapshot;
import com.github.minecraft_ta.totaldebug.evaluation.InMemoryJavaCompiler;
import com.github.minecraft_ta.totaldebug.evaluation.ServerManifest;
import com.github.minecraft_ta.totaldebug.protocol.scnet.ServerManifestMessage;
import com.github.minecraft_ta.totaldebug.protocol.execution.ExecutionResult;
import com.github.minecraft_ta.totaldebug.protocol.execution.ExecutionStatus;
import com.github.minecraft_ta.totaldebug.protocol.execution.ScriptBytecode;
import com.github.minecraft_ta.totaldebug.protocol.execution.ScriptExecutionEnvironment;
import com.github.minecraft_ta.totaldebug.protocol.scnet.RunScriptMessage;
import com.github.minecraft_ta.totaldebug.protocol.scnet.ServerManifestMessage;
import com.github.minecraft_ta.totaldebug.protocol.scnet.ServerSourceRequestMessage;
import com.github.minecraft_ta.totaldebug.storage.CacheFiles;
import com.github.minecraft_ta.totaldebug.storage.RuntimePhase;

import java.io.IOException;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
Expand Down Expand Up @@ -41,49 +45,149 @@ public record CompilationResult(ScriptBytecode bytecode, String inventoryId) {}
private volatile ReadySnapshot snapshot;
private InMemoryJavaCompiler compiler;
private volatile boolean closed;
private record ServerSnapshot(String sessionId, ServerManifest manifest) {}
private record ServerSnapshot(String sessionId, String inventoryId, Set<String> unsupported) {}
private record Baseline(String sessionId, ServerManifest manifest) {}
private record Comparison(String requestId, ReadySnapshot selected, ServerCompatibility work) {}
private final Predicate<ServerSourceRequestMessage> sourceRequester;
private volatile ServerSnapshot serverSnapshot;
private volatile String serverUnavailable = "No server handshake is available";
private final ServerManifestMessage.Assembler manifestTransfer = new ServerManifestMessage.Assembler();
private final ServerManifestMessage.Assembler detailTransfer = new ServerManifestMessage.Assembler();
private long serverGeneration;
private long manifestGeneration;
private ServerManifest compilingForServer;
private Baseline baseline;
private Comparison comparison;
private Set<String> compilingForServer;

public ScriptCompilationService(Predicate<RunScriptMessage> sender,
Predicate<ServerSourceRequestMessage> sourceRequester) {
this.sender = sender;
this.sourceRequester = sourceRequester;
}

public synchronized void acceptServerManifest(ServerManifestMessage message) {
if (this.closed) return;
if (message.offset() == 0) {
this.manifestGeneration++;
this.serverSnapshot = null;
if (!message.baseline() && (this.baseline == null || this.comparison == null
|| !this.baseline.sessionId().equals(message.sessionId())
|| !this.comparison.requestId().equals(message.requestId())
|| this.comparison.work().nextSource() != message.source())) return;
if (message.baseline() && message.offset() == 0) {
this.serverGeneration++;
this.manifestTransfer.clear();
invalidateComparison();
this.baseline = null;
this.serverUnavailable = message.total() == 0 ? message.detail() : "Preparing server class compatibility";
}
if (!message.baseline() && message.total() == 0) {
failComparison(this.manifestGeneration, message.detail());
return;
}
byte[] bytes;
try {
bytes = this.manifestTransfer.accept(message);
} catch (IllegalArgumentException exception) {
this.manifestGeneration++;
this.serverSnapshot = null;
this.serverUnavailable = exception.getMessage();
try { bytes = (message.baseline() ? this.manifestTransfer : this.detailTransfer).accept(message); }
catch (IllegalArgumentException exception) {
failComparison(this.manifestGeneration, exception.getMessage());
return;
}
if (bytes == null) return;
long generation = this.manifestGeneration;
long serverGeneration = this.serverGeneration;
this.worker.execute(() -> {
try {
var manifest = ServerManifest.decode(bytes);
synchronized (this) {
if (!this.closed && generation == this.manifestGeneration) {
this.serverSnapshot = new ServerSnapshot(message.sessionId(), manifest);
if (message.baseline()) {
var decoded = ServerManifest.decode(bytes);
long currentGeneration;
synchronized (this) {
if (this.closed || serverGeneration != this.serverGeneration) return;
this.baseline = new Baseline(message.sessionId(), decoded);
currentGeneration = this.manifestGeneration;
}
prepareComparison(currentGeneration);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Move compatibility preparation off the compilation worker

After a server baseline arrives, this synchronously enters prepareComparison, which fingerprints every client archive and enumerates all class entries on the same single-thread executor used by compile and submit. Any client-side compilation queued behind this work stalls until the entire local comparison scan finishes even though it does not require server compatibility; on a large modpack this can noticeably block script execution after every join, reconnect, or index rebind. Run the comparison I/O on a separate worker or otherwise keep client compilations from queuing behind it.

Useful? React with 👍 / 👎.

} else {
var details = ServerManifest.decodeDetails(bytes);
synchronized (this) {
if (this.closed || generation != this.manifestGeneration || this.comparison == null) return;
this.comparison.work().accept(message.source(), details);
}
advanceComparison(generation);
}
} catch (IOException exception) {
} catch (Exception exception) {
synchronized (this) {
if (generation == this.manifestGeneration) this.serverUnavailable = exception.getMessage();
if (message.baseline() && serverGeneration != this.serverGeneration) return;
failComparison(message.baseline() ? this.manifestGeneration : generation, exception.getMessage());
}
}
});
}

public ScriptCompilationService(Predicate<RunScriptMessage> sender) {
this.sender = sender;
private void prepareComparison(long generation) {
ReadySnapshot selected;
Baseline baseline;
synchronized (this) {
if (this.closed || generation != this.manifestGeneration) return;
selected = this.snapshot;
baseline = this.baseline;
}
if (selected == null || baseline == null) return;
try {
ServerCompatibility work = CacheFiles.locked(selected.indexFile().getParent(), () -> {
CacheFiles.requireIdentity(selected.indexFile().getParent().resolve("inventory.json"), "id", selected.inventoryId());
try (var phase = RuntimePhase.start("server.local-baseline")) {
return new ServerCompatibility(baseline.manifest(), selected.sources());
}
});
synchronized (this) {
if (this.closed || generation != this.manifestGeneration || this.snapshot != selected) return;
this.comparison = new Comparison(UUID.randomUUID().toString(), selected, work);
}
advanceComparison(generation);
} catch (Exception exception) { failComparison(generation, exception.getMessage()); }
}

private void advanceComparison(long generation) throws Exception {
Comparison current;
Baseline baseline;
synchronized (this) {
if (this.closed || generation != this.manifestGeneration || this.comparison == null) return;
current = this.comparison;
baseline = this.baseline;
int source = current.work().nextSource();
if (source != -1) {
this.serverUnavailable = "Comparing server source " + baseline.manifest().sources().get(source).name();
if (!this.sourceRequester.test(new ServerSourceRequestMessage(baseline.sessionId(), current.requestId(), source))) {
throw new IOException("Minecraft disconnected before server source details could be requested");
}
return;
}
}
Set<String> unsupported = CacheFiles.locked(current.selected().indexFile().getParent(), () -> {
synchronized (this.compilerLock) {
if (this.closed || this.snapshot != current.selected()) throw new IOException("The client inventory changed");
CacheFiles.requireIdentity(current.selected().indexFile().getParent().resolve("inventory.json"),
"id", current.selected().inventoryId());
try (var phase = RuntimePhase.start("server.compare-declarations")) {
return current.work().finish(current.selected());
}
}
});
synchronized (this) {
if (this.closed || generation != this.manifestGeneration || this.comparison != current
|| this.snapshot != current.selected()) return;
this.serverSnapshot = new ServerSnapshot(baseline.sessionId(), current.selected().inventoryId(), unsupported);
this.comparison = null;
}
}

private synchronized void failComparison(long generation, String detail) {
if (generation != this.manifestGeneration) return;
invalidateComparison();
this.serverUnavailable = detail == null ? "Server class comparison failed" : detail;
}

private void invalidateComparison() {
this.manifestGeneration++;
this.serverSnapshot = null;
this.comparison = null;
this.detailTransfer.clear();
}

/** Must complete before the old native index is closed by its owner. */
Expand All @@ -99,6 +203,14 @@ public void bind(ReadySnapshot snapshot) {
this.compiler = new InMemoryJavaCompiler(standard ->
new IndexedJavaFileManager(standard, snapshot.index(), snapshot.sources(), () -> this.compilingForServer));
}
synchronized (this) {
invalidateComparison();
this.serverUnavailable = "Waiting for the client index and server handshake comparison";
long generation = this.manifestGeneration;
if (!this.closed && snapshot != null && this.baseline != null) {
this.worker.execute(() -> prepareComparison(generation));
}
}
}
}

Expand Down Expand Up @@ -141,7 +253,7 @@ public void submit(int id, String source, boolean serverSide, ScriptExecutionEnv
return;
}
ServerSnapshot server = serverSide ? this.serverSnapshot : null;
if (serverSide && server == null) {
if (serverSide && (server == null || !server.inventoryId().equals(selected.inventoryId()))) {
failureHandler.accept(failure(this.serverUnavailable));
return;
}
Expand Down Expand Up @@ -191,7 +303,7 @@ private CompilationResult compileSelected(ReadySnapshot selected, ServerSnapshot
}
CacheFiles.requireIdentity(selected.indexFile().getParent().resolve("inventory.json"),
"id", selected.inventoryId());
this.compilingForServer = server == null ? null : server.manifest();
this.compilingForServer = server == null ? null : server.unsupported();
try {
return new CompilationResult(new ScriptBytecode(entryClass,
this.compiler.compile(source, entryClass, "")), selected.inventoryId());
Expand All @@ -216,10 +328,11 @@ public boolean cancel(int id) {

public void runtimeDisconnected() {
synchronized (this) {
this.manifestGeneration++;
this.serverSnapshot = null;
this.serverUnavailable = "Minecraft disconnected";
this.serverGeneration++;
this.manifestTransfer.clear();
invalidateComparison();
this.baseline = null;
this.serverUnavailable = "Minecraft disconnected";
}
for (int id : this.pending.keySet()) cancel(id);
}
Expand Down
Loading