Skip to content

Commit d35b2a8

Browse files
committed
feat: add isolated Wurst benchmark mode
1 parent f383a55 commit d35b2a8

17 files changed

Lines changed: 1597 additions & 4 deletions

File tree

de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/CompilationProcess.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
package de.peeeq.wurstio;
22

33
import org.wurstscript.projectconfig.WurstProjectConfigData;
4+
import de.peeeq.wurstio.benchmark.BenchmarkOptions;
5+
import de.peeeq.wurstio.benchmark.BenchmarkResult;
6+
import de.peeeq.wurstio.benchmark.BenchmarkWorkerOutput;
7+
import de.peeeq.wurstio.benchmark.RunBenchmarks;
48
import de.peeeq.wurstio.languageserver.requests.RunTests;
59
import de.peeeq.wurstio.mpq.MpqEditor;
610
import de.peeeq.wurstio.utils.FileUtils;
@@ -19,6 +23,8 @@
1923
import java.io.File;
2024
import java.io.IOException;
2125
import java.io.PrintStream;
26+
import java.nio.file.Path;
27+
import java.nio.file.Paths;
2228
import java.util.Optional;
2329
import java.util.function.Supplier;
2430

@@ -80,6 +86,11 @@ public CompilationProcess(WurstGui gui, RunArgs runArgs) {
8086
return null;
8187
}
8288

89+
if (runArgs.isRunBenchmarks()) {
90+
timeTaker.measure("Run benchmark worker", () -> runBenchmarks(compiler));
91+
return null;
92+
}
93+
8394
if (runArgs.isRunTests()) {
8495
timeTaker.measure("Run tests",
8596
() -> runTests(compiler.getImTranslator(), compiler, runArgs.getTestTimeout(), runArgs.getTestFilter()));
@@ -116,6 +127,32 @@ public CompilationProcess(WurstGui gui, RunArgs runArgs) {
116127
return mapScript;
117128
}
118129

130+
private void runBenchmarks(WurstCompilerJassImpl compiler) {
131+
try {
132+
RunBenchmarks runner = new RunBenchmarks();
133+
Path output = Paths.get(runArgs.getBenchmarkOutput());
134+
if (runArgs.isBenchmarkList()) {
135+
BenchmarkWorkerOutput.writeDiscovery(
136+
output,
137+
runner.discover(compiler.getImProg(), Optional.ofNullable(runArgs.getBenchmarkFilter())));
138+
} else {
139+
BenchmarkResult result = runner.run(
140+
compiler.getImTranslator(),
141+
compiler.getImProg(),
142+
runArgs.getBenchmarkName(),
143+
new BenchmarkOptions(runArgs.getBenchmarkWarmup(), runArgs.getBenchmarkIterations(), 1_000_000L));
144+
BenchmarkWorkerOutput.writeResult(output, result);
145+
}
146+
} catch (Throwable e) {
147+
String message = e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage();
148+
gui.sendError(new CompileError(
149+
null,
150+
"Benchmark worker failed: " + message,
151+
CompileError.ErrorType.ERROR,
152+
e));
153+
}
154+
}
155+
119156
private boolean runPjass(File outputMapscript) {
120157
File commonJ = new File(outputMapscript.getParent(), "common.j");
121158
File blizzJ = new File(outputMapscript.getParent(), "blizzard.j");

de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/Main.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,10 @@ public static void main(String[] args) {
191191
compiledScript = compilationProcess.doCompilation(null, true);
192192
}
193193

194-
if (compiledScript != null) {
194+
if (runArgs.isRunBenchmarks()) {
195+
// Benchmark workers write their JSON result during compilation;
196+
// a null script is the successful worker result, not a failure.
197+
} else if (compiledScript != null) {
195198
File scriptFile = new File("compiled.j.txt");
196199
Files.write(compiledScript.toString().getBytes(Charsets.UTF_8), scriptFile);
197200
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package de.peeeq.wurstio.benchmark;
2+
3+
@FunctionalInterface
4+
public interface BenchmarkClock {
5+
long nanoTime();
6+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package de.peeeq.wurstio.benchmark;
2+
3+
public record BenchmarkOptions(int warmupIterations, int measurementIterations, long minimumSampleNanos) {
4+
public BenchmarkOptions {
5+
if (warmupIterations < 0) {
6+
throw new IllegalArgumentException("warmupIterations must be non-negative");
7+
}
8+
if (measurementIterations <= 0) {
9+
throw new IllegalArgumentException("measurementIterations must be positive");
10+
}
11+
if (minimumSampleNanos < 0) {
12+
throw new IllegalArgumentException("minimumSampleNanos must be non-negative");
13+
}
14+
}
15+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package de.peeeq.wurstio.benchmark;
2+
3+
import java.util.List;
4+
import java.util.Objects;
5+
6+
public record BenchmarkResult(
7+
String qualifiedName,
8+
int checksum,
9+
int batchSize,
10+
List<Long> samplesNanos
11+
) {
12+
public BenchmarkResult {
13+
Objects.requireNonNull(qualifiedName, "qualifiedName");
14+
if (batchSize <= 0) {
15+
throw new IllegalArgumentException("batchSize must be positive");
16+
}
17+
samplesNanos = List.copyOf(Objects.requireNonNull(samplesNanos, "samplesNanos"));
18+
if (samplesNanos.isEmpty()) {
19+
throw new IllegalArgumentException("at least one benchmark sample is required");
20+
}
21+
for (Long sample : samplesNanos) {
22+
if (sample < 0) {
23+
throw new IllegalArgumentException("benchmark samples must be non-negative");
24+
}
25+
}
26+
}
27+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package de.peeeq.wurstio.benchmark;
2+
3+
import com.google.gson.Gson;
4+
import com.google.gson.GsonBuilder;
5+
import com.google.gson.JsonArray;
6+
import com.google.gson.JsonObject;
7+
8+
import java.io.IOException;
9+
import java.nio.charset.StandardCharsets;
10+
import java.nio.file.AtomicMoveNotSupportedException;
11+
import java.nio.file.Files;
12+
import java.nio.file.Path;
13+
import java.nio.file.StandardCopyOption;
14+
import java.nio.file.StandardOpenOption;
15+
import java.util.List;
16+
import java.util.Objects;
17+
18+
/** Writes the machine-readable result of one isolated benchmark compiler worker. */
19+
public final class BenchmarkWorkerOutput {
20+
public static final String SCHEMA = "wurst-benchmark-worker-v2";
21+
22+
@FunctionalInterface
23+
public interface TemporaryFileWriter {
24+
void write(Path temporary, String json) throws IOException;
25+
}
26+
27+
private static final Gson GSON = new GsonBuilder()
28+
.disableHtmlEscaping()
29+
.create();
30+
31+
private BenchmarkWorkerOutput() {
32+
}
33+
34+
public static void writeDiscovery(Path output, List<String> benchmarkNames) throws IOException {
35+
Objects.requireNonNull(benchmarkNames, "benchmarkNames");
36+
JsonObject json = envelope("discovery");
37+
JsonArray benchmarks = new JsonArray();
38+
for (String benchmarkName : benchmarkNames) {
39+
benchmarks.add(Objects.requireNonNull(benchmarkName, "benchmarkName"));
40+
}
41+
json.add("benchmarks", benchmarks);
42+
writeAtomically(output, GSON.toJson(json));
43+
}
44+
45+
public static void writeResult(Path output, BenchmarkResult result) throws IOException {
46+
Objects.requireNonNull(result, "result");
47+
JsonObject json = envelope("execution");
48+
json.addProperty("qualifiedName", result.qualifiedName());
49+
json.addProperty("checksum", result.checksum());
50+
json.addProperty("batchSize", result.batchSize());
51+
json.add("samplesNanos", GSON.toJsonTree(result.samplesNanos()));
52+
writeAtomically(output, GSON.toJson(json));
53+
}
54+
55+
/**
56+
* Write a complete JSON document to a sibling temporary file, then rename it
57+
* over the destination. Serialization happens before touching the destination.
58+
*/
59+
public static void writeAtomically(Path output, String json) throws IOException {
60+
writeAtomically(output, json, BenchmarkWorkerOutput::writeTemporaryFile);
61+
}
62+
63+
public static void writeAtomically(
64+
Path output,
65+
String json,
66+
TemporaryFileWriter temporaryFileWriter
67+
) throws IOException {
68+
Objects.requireNonNull(output, "output");
69+
Objects.requireNonNull(json, "json");
70+
Objects.requireNonNull(temporaryFileWriter, "temporaryFileWriter");
71+
Path absoluteOutput = output.toAbsolutePath();
72+
Path parent = absoluteOutput.getParent();
73+
if (parent == null) {
74+
throw new IOException("benchmark output has no parent directory: " + output);
75+
}
76+
Files.createDirectories(parent);
77+
Path temporary = Files.createTempFile(parent, "wurst-benchmark-", ".tmp");
78+
try {
79+
temporaryFileWriter.write(temporary, json);
80+
try {
81+
Files.move(
82+
temporary,
83+
absoluteOutput,
84+
StandardCopyOption.ATOMIC_MOVE,
85+
StandardCopyOption.REPLACE_EXISTING);
86+
} catch (AtomicMoveNotSupportedException e) {
87+
Files.move(temporary, absoluteOutput, StandardCopyOption.REPLACE_EXISTING);
88+
}
89+
} finally {
90+
Files.deleteIfExists(temporary);
91+
}
92+
}
93+
94+
private static void writeTemporaryFile(Path temporary, String json) throws IOException {
95+
Files.writeString(
96+
temporary,
97+
json,
98+
StandardCharsets.UTF_8,
99+
StandardOpenOption.WRITE,
100+
StandardOpenOption.TRUNCATE_EXISTING);
101+
}
102+
103+
private static JsonObject envelope(String mode) {
104+
JsonObject json = new JsonObject();
105+
json.addProperty("schema", SCHEMA);
106+
json.addProperty("mode", mode);
107+
return json;
108+
}
109+
}

0 commit comments

Comments
 (0)