Authors: russell@unturf.com · brackishbert@gmail.com · foxhop.net · TimeHexOn.com Patches, unit tests, benchmarks, whitepaper, and outreach briefs. Public domain — no copyright claimed. Use freely.
317 lines
17 KiB
Java
317 lines
17 KiB
Java
package bench;
|
||
|
||
import com.google.common.collect.HashMultimap;
|
||
import com.google.common.collect.Multimap;
|
||
|
||
import java.util.*;
|
||
|
||
/**
|
||
* WalkeruinElytraStressBenchmark — final benchmark.
|
||
*
|
||
* Scenario: 30 players launched from spawn (Y=200) with elytra + 64 fireworks each.
|
||
* Directions: evenly spread 360° (12° apart). Firework boost every 3 s.
|
||
* All players above terrain; consistent speed/magnitude.
|
||
*
|
||
* NOTE: This benchmark has two distinct sections:
|
||
*
|
||
* §A World load — measures DependencySorter.isCyclic algorithm in isolation.
|
||
* Numbers are ALGORITHM MICRO-BENCHMARK, not real server wall-clock.
|
||
* Real server /reload: vanilla 19,255 ms → patched 3,087 ms (6.2×).
|
||
* Algorithm isolation shows higher speedup because real server time
|
||
* includes I/O, JSON parsing, and other /reload work.
|
||
*
|
||
* §B Elytra flight — PHYSICS SIMULATION ONLY.
|
||
* Models player spread, chunk boundary crossings, firework timing.
|
||
* Distance/chunk numbers reflect the simulation model, not verified
|
||
* Minecraft server behavior.
|
||
*
|
||
* Defect exercised:
|
||
* minecraft-0001: DependencySorter.isCyclic O(2^D) → O(V+E) (world load)
|
||
*
|
||
* Three tiers:
|
||
* unpatched D=16 / 200 NS — vanilla isCyclic (O(2^D))
|
||
* mitigated D=16 / 200 NS — visited-set fix (O(V+E))
|
||
* enriched D=48 / 1000 NS — new territory, vanilla StackOverflows at D>20
|
||
*
|
||
* Server config note: PLAYERS=30 requires max-players=30 in server.properties
|
||
* (default Minecraft server cap is 20).
|
||
*/
|
||
public class WalkeruinElytraStressBenchmark {
|
||
|
||
// ── Simulation constants ──────────────────────────────────────────────────
|
||
static final int PLAYERS = 30;
|
||
static final int SIM_TICKS = 1200; // 60 s at 20 TPS
|
||
static final long TICK_BUDGET_NS = 50_000_000L; // 50 ms
|
||
static final double RENDER_DIST = 128.0; // blocks
|
||
static final double CHUNK_SZ = 16.0;
|
||
static final double START_Y = 200.0; // above terrain (simulation)
|
||
static final double DRAG = 0.99; // per-tick horizontal drag
|
||
static final double BASE_SPEED = 0.55; // blocks/tick (glide launch)
|
||
static final double MAX_SPEED = 2.2; // blocks/tick at full boost (~44 m/s)
|
||
static final double BOOST_IMPULSE = 1.5; // velocity delta per firework
|
||
static final int BOOST_INTERVAL = 60; // ticks between fireworks (3 s)
|
||
static final int WARMUP_ROUNDS = 5; // JIT warmup rounds before measuring
|
||
|
||
// Tag graph
|
||
static final int UNPATCHED_DEPTH = 16;
|
||
static final int UNPATCHED_NS = 200;
|
||
static final int ENRICHED_DEPTH = 48;
|
||
static final int ENRICHED_NS = 1000;
|
||
static final int VANILLA_DEPTH_LIMIT = 20; // beyond this, vanilla StackOverflows
|
||
|
||
// Real server numbers for reference (bench-server.sh, server-26.1, JDK 25)
|
||
static final long REAL_VANILLA_RELOAD_MS = 19_255;
|
||
static final long REAL_PATCHED_RELOAD_MS = 3_087;
|
||
|
||
// ── Player (physics simulation) ───────────────────────────────────────────
|
||
static final class Player {
|
||
final int id;
|
||
final double dirX, dirZ;
|
||
double x, y, z, vx, vy, vz;
|
||
int fw, fwCool, boostLeft;
|
||
long chunks;
|
||
double dist;
|
||
int lastCx, lastCz;
|
||
|
||
Player(int id) {
|
||
this.id = id;
|
||
double a = Math.toRadians(id * (360.0 / PLAYERS));
|
||
dirX = Math.sin(a);
|
||
dirZ = Math.cos(a);
|
||
reset();
|
||
}
|
||
|
||
void reset() {
|
||
x = 0; y = START_Y; z = 0;
|
||
vx = dirX * BASE_SPEED; vy = 0; vz = dirZ * BASE_SPEED;
|
||
fw = 64;
|
||
// stagger launches so not all 30 rockets fire same tick
|
||
fwCool = id % BOOST_INTERVAL;
|
||
boostLeft = 0;
|
||
chunks = 0; dist = 0;
|
||
lastCx = Integer.MIN_VALUE; lastCz = Integer.MIN_VALUE;
|
||
}
|
||
|
||
void tick() {
|
||
// Firework boost
|
||
if (fwCool > 0) {
|
||
fwCool--;
|
||
} else if (fw > 0) {
|
||
fw--;
|
||
fwCool = BOOST_INTERVAL;
|
||
boostLeft = 20;
|
||
vx += dirX * BOOST_IMPULSE;
|
||
vz += dirZ * BOOST_IMPULSE;
|
||
double spd = Math.hypot(vx, vz);
|
||
if (spd > MAX_SPEED) { vx = vx / spd * MAX_SPEED; vz = vz / spd * MAX_SPEED; }
|
||
}
|
||
if (boostLeft > 0) boostLeft--;
|
||
|
||
// Simplified elytra physics (simulation model — not verified Minecraft physics)
|
||
vx *= DRAG; vz *= DRAG;
|
||
if (y < START_Y) vy += 0.08; else vy *= 0.7;
|
||
double px = x, pz = z;
|
||
x += vx; y = Math.max(START_Y, y + vy); z += vz;
|
||
dist += Math.hypot(x - px, z - pz);
|
||
|
||
// Chunk boundary crossing
|
||
int cx = (int) Math.floor(x / CHUNK_SZ);
|
||
int cz = (int) Math.floor(z / CHUNK_SZ);
|
||
if (cx != lastCx || cz != lastCz) { chunks++; lastCx = cx; lastCz = cz; }
|
||
}
|
||
}
|
||
|
||
// ── DependencySorter algorithm simulation ─────────────────────────────────
|
||
// Models the diamond DAG that triggers O(2^D) isCyclic behavior.
|
||
// Each namespace gets its own multimap (conservative — real Minecraft uses
|
||
// one shared multimap, making cross-namespace contamination worse).
|
||
static Map<String, List<String>> buildDiamondTags(int depth, String pfx) {
|
||
Map<String, List<String>> m = new LinkedHashMap<>();
|
||
m.put(pfx + "_base", List.of());
|
||
String pL = pfx + "_base", pR = pfx + "_base";
|
||
for (int d = 1; d <= depth; d++) {
|
||
String l = pfx + "_L" + d, r = pfx + "_R" + d;
|
||
m.put(l, List.of(pL, pR));
|
||
m.put(r, List.of(pL, pR));
|
||
pL = l; pR = r;
|
||
}
|
||
m.put(pfx + "_root", List.of(pL, pR));
|
||
return m;
|
||
}
|
||
|
||
static boolean isCyclicVanilla(Multimap<String,String> m, String from, String to) {
|
||
Collection<String> d = m.get(to);
|
||
if (d.contains(from)) return true;
|
||
return d.stream().anyMatch(dep -> isCyclicVanilla(m, from, dep));
|
||
}
|
||
|
||
static boolean isCyclicPatched(Multimap<String,String> m, String from, String to, Set<String> seen) {
|
||
if (!seen.add(to)) return false;
|
||
Collection<String> d = m.get(to);
|
||
if (d.contains(from)) return true;
|
||
return d.stream().anyMatch(dep -> isCyclicPatched(m, from, dep, seen));
|
||
}
|
||
|
||
static long resolveWorldLoad(int depth, int ns, boolean patched) {
|
||
long t0 = System.nanoTime();
|
||
for (int i = 0; i < ns; i++) {
|
||
Map<String, List<String>> tags = buildDiamondTags(depth, "ns" + i);
|
||
HashMultimap<String,String> mm = HashMultimap.create();
|
||
for (Map.Entry<String, List<String>> e : tags.entrySet()) {
|
||
for (String dep : e.getValue()) {
|
||
if (patched) {
|
||
if (!isCyclicPatched(mm, e.getKey(), dep, new HashSet<>())) mm.put(e.getKey(), dep);
|
||
} else {
|
||
if (!isCyclicVanilla(mm, e.getKey(), dep)) mm.put(e.getKey(), dep);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return System.nanoTime() - t0;
|
||
}
|
||
|
||
// ── Tier runner ───────────────────────────────────────────────────────────
|
||
static record TierResult(long worldLoadMs, double maxDist, long totalChunks) {}
|
||
|
||
static TierResult runTier(String name, int depth, int ns, boolean patched) {
|
||
System.out.println();
|
||
System.out.println("══════════════════════════════════════════════════════════════════");
|
||
System.out.printf(" %s%n", name);
|
||
System.out.printf(" datapack: D=%-2d NS=%-4d isCyclic: %s%n",
|
||
depth, ns, patched ? "visited-set O(V+E)" : "vanilla O(2^D)");
|
||
System.out.println("══════════════════════════════════════════════════════════════════");
|
||
|
||
// §A — World load (algorithm micro-benchmark)
|
||
System.out.printf(" §A World load — algorithm micro-benchmark (%d NS × D=%d)... ", ns, depth);
|
||
System.out.flush();
|
||
|
||
if (!patched && depth > VANILLA_DEPTH_LIMIT) {
|
||
System.out.printf("ABORT%n");
|
||
System.out.printf(" Vanilla D=%d: ~%.2e isCyclic node visits.%n",
|
||
depth, (double) ns * 4 * Math.pow(2, depth));
|
||
System.out.println(" StackOverflow — server never reaches play state at this depth.");
|
||
return null;
|
||
}
|
||
|
||
// JIT warmup — enough rounds to fully compile the hot paths at this depth
|
||
int warmupDepth = Math.min(depth, 10);
|
||
for (int i = 0; i < WARMUP_ROUNDS; i++) resolveWorldLoad(warmupDepth, 10, patched);
|
||
|
||
long worldNs = resolveWorldLoad(depth, ns, patched);
|
||
long worldMs = worldNs / 1_000_000;
|
||
System.out.printf("%,d ms [algorithm isolation]%n", worldMs);
|
||
|
||
// §B — Elytra flight (physics simulation)
|
||
Player[] players = new Player[PLAYERS];
|
||
for (int i = 0; i < PLAYERS; i++) players[i] = new Player(i);
|
||
|
||
// warmup physics
|
||
for (Player p : players) p.reset();
|
||
for (int t = 0; t < 200; t++) for (Player p : players) p.tick();
|
||
|
||
// measure
|
||
for (Player p : players) p.reset();
|
||
long flightStart = System.nanoTime();
|
||
for (int t = 0; t < SIM_TICKS; t++) for (Player p : players) p.tick();
|
||
long flightNs = System.nanoTime() - flightStart;
|
||
|
||
long totalChunks = 0;
|
||
double totalDist = 0, maxDist = 0;
|
||
for (Player p : players) {
|
||
totalChunks += p.chunks;
|
||
totalDist += p.dist;
|
||
if (p.dist > maxDist) maxDist = p.dist;
|
||
}
|
||
|
||
System.out.printf(" §B Elytra flight — physics simulation [not verified Minecraft physics]:%n");
|
||
System.out.printf(" sim time: %,d µs%n", flightNs / 1000);
|
||
System.out.printf(" max distance: %,.0f m from spawn (simulation)%n", maxDist);
|
||
System.out.printf(" avg distance: %,.0f m per player (simulation)%n", totalDist / PLAYERS);
|
||
System.out.printf(" chunk crossings: %,d total (%,.0f per player, %,.1f/s)%n",
|
||
totalChunks, totalChunks / (double) PLAYERS, totalChunks / 60.0);
|
||
|
||
// §C — Tier summary
|
||
long tagNodes = (long) ns * (depth * 2 + 1); // in-memory nodes, not files
|
||
System.out.printf(" §C Tier summary:%n");
|
||
System.out.printf(" world load: %,d ms [algorithm isolation]%n", worldMs);
|
||
System.out.printf(" datapack scale: D=%d × %d NS = %,d tag nodes (in-memory)%n",
|
||
depth, ns, tagNodes);
|
||
System.out.printf(" players airborne: %d / %d%n", PLAYERS, PLAYERS);
|
||
System.out.printf(" max range: %,.0f m from spawn (simulation)%n", maxDist);
|
||
|
||
return new TierResult(worldMs, maxDist, totalChunks);
|
||
}
|
||
|
||
// ── Main ─────────────────────────────────────────────────────────────────
|
||
public static void main(String[] args) {
|
||
System.out.println();
|
||
System.out.println("╔══════════════════════════════════════════════════════════════════╗");
|
||
System.out.println("║ walkeruin ElytraStressBenchmark · Final Benchmark ║");
|
||
System.out.println("║ 30 players · elytra + 64 fireworks · all directions · Y=200 ║");
|
||
System.out.println("╠══════════════════════════════════════════════════════════════════╣");
|
||
System.out.println("║ §A = algorithm micro-benchmark · §B = physics simulation ║");
|
||
System.out.println("║ Real server /reload: vanilla 19,255 ms → patched 3,087 ms (6.2×)║");
|
||
System.out.println("╚══════════════════════════════════════════════════════════════════╝");
|
||
System.out.printf(" Players: %d (%.1f° apart, evenly spread 360°, requires max-players=30)%n",
|
||
PLAYERS, 360.0 / PLAYERS);
|
||
System.out.printf(" Altitude: Y=%.0f (simulation — above terrain model)%n", START_Y);
|
||
System.out.printf(" Duration: %d ticks = %d s at 20 TPS%n", SIM_TICKS, SIM_TICKS / 20);
|
||
System.out.printf(" Max speed: %.0f m/s (elytra + firework boost, simulation)%n", MAX_SPEED * 20);
|
||
System.out.printf(" Defect: minecraft-0001 — DependencySorter.isCyclic O(2^D) → O(V+E)%n");
|
||
|
||
TierResult unpatched = runTier(
|
||
"UNPATCHED (control — vanilla isCyclic O(2^D))",
|
||
UNPATCHED_DEPTH, UNPATCHED_NS, false);
|
||
|
||
TierResult mitigated = runTier(
|
||
"MITIGATED (fix applied — visited-set O(V+E))",
|
||
UNPATCHED_DEPTH, UNPATCHED_NS, true);
|
||
|
||
TierResult enriched = runTier(
|
||
"ENRICHED (D=48 / 1000 NS — new territory, vanilla StackOverflows at D>20)",
|
||
ENRICHED_DEPTH, ENRICHED_NS, true);
|
||
|
||
// ── Final comparison table ────────────────────────────────────────────
|
||
System.out.println();
|
||
System.out.println("══════════════════════════════════════════════════════════════════");
|
||
System.out.println(" FINAL COMPARISON — 30-player elytra stress, three tiers");
|
||
System.out.println(" §A = algorithm isolation · not real server wall-clock");
|
||
System.out.println("══════════════════════════════════════════════════════════════════");
|
||
System.out.printf(" %-16s %-20s %-14s %s%n",
|
||
"Tier", "World load [§A]", "Max range [§B]", "Tag nodes");
|
||
System.out.println(" " + "─".repeat(72));
|
||
|
||
if (unpatched != null)
|
||
System.out.printf(" %-16s %,7d ms %,7.0f m D=%d/%dNS%n",
|
||
"unpatched", unpatched.worldLoadMs(), unpatched.maxDist(),
|
||
UNPATCHED_DEPTH, UNPATCHED_NS);
|
||
else
|
||
System.out.printf(" %-16s %-20s%n", "unpatched", "CRASH (StackOverflow)");
|
||
|
||
if (mitigated != null)
|
||
System.out.printf(" %-16s %,7d ms %,7.0f m D=%d/%dNS%n",
|
||
"mitigated", mitigated.worldLoadMs(), mitigated.maxDist(),
|
||
UNPATCHED_DEPTH, UNPATCHED_NS);
|
||
|
||
if (enriched != null)
|
||
System.out.printf(" %-16s %,7d ms %,7.0f m D=%d/%dNS%n",
|
||
"enriched", enriched.worldLoadMs(), enriched.maxDist(),
|
||
ENRICHED_DEPTH, ENRICHED_NS);
|
||
else
|
||
System.out.printf(" %-16s %-20s %-14s D=%d/%dNS%n",
|
||
"enriched (vanilla)", "CRASH", "N/A", ENRICHED_DEPTH, ENRICHED_NS);
|
||
|
||
System.out.println();
|
||
if (unpatched != null && mitigated != null) {
|
||
System.out.printf(" Algorithm isolation speedup (D=%d): %.1f×%n",
|
||
UNPATCHED_DEPTH, (double) unpatched.worldLoadMs() / mitigated.worldLoadMs());
|
||
System.out.printf(" Real server /reload speedup (6.2×): 19,255 ms → 3,087 ms%n");
|
||
System.out.printf(" (algorithm numbers exceed real server — isolates isCyclic only)%n");
|
||
}
|
||
System.out.printf(" Enriched (D=48/1000NS): %,d tag nodes in-memory%n",
|
||
(long) ENRICHED_NS * (ENRICHED_DEPTH * 2 + 1));
|
||
System.out.printf(" Vanilla at D=%d: StackOverflow — server never starts%n",
|
||
ENRICHED_DEPTH);
|
||
System.out.println("══════════════════════════════════════════════════════════════════");
|
||
}
|
||
}
|