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.
313 lines
18 KiB
Java
313 lines
18 KiB
Java
package bench;
|
||
|
||
import com.google.common.collect.HashMultimap;
|
||
import com.google.common.collect.Multimap;
|
||
|
||
import java.util.*;
|
||
import java.util.concurrent.*;
|
||
import java.util.concurrent.atomic.*;
|
||
|
||
/**
|
||
* LoadSimBenchmark — simulates concurrent player connections and game-loop ticks
|
||
* against both VANILLA (unpatched) and PATCHED server algorithms.
|
||
*
|
||
* Three simulation axes:
|
||
*
|
||
* 1. World load / /reload
|
||
* N threads each trigger a full tag-graph resolution at modpack depth D.
|
||
* Models: players queued outside door while server does /reload.
|
||
* Metric: wall-clock time until all N joins complete.
|
||
*
|
||
* 2. TPS under concurrent player load
|
||
* Game loop runs at 20 TPS target (50 ms budget per tick).
|
||
* Each tick evaluates redstone contraptions for all connected players.
|
||
* VANILLA: O(W) wire membership test per evaluation (Deque.contains).
|
||
* PATCHED: O(1) wire membership test (HashSet companion).
|
||
* Metric: achieved TPS and tick overrun rate at increasing player counts.
|
||
*
|
||
* 3. Join throughput and latency
|
||
* Measures p50 / p95 / p99 join latency and sustained joins/sec.
|
||
*
|
||
* Modpack profile (Create + AE2 + Mekanism + Thermal):
|
||
* - Tag depth: 12 (diamond inheritance chains across mods)
|
||
* - Total cross-mod tags: ~800 entries resolved at load time
|
||
* - Wires per player: uncalibrated — treat §2/§3 as exploratory
|
||
*
|
||
* NOTE: §1 (world load) is algorithm-isolation micro-benchmark, not real server wall-clock.
|
||
* Real server /reload: vanilla 19,255 ms → patched 3,087 ms (6.2×, bench-server.sh).
|
||
*
|
||
* §2/§3 (redstone TPS) is EXPLORATORY. minecraft-0003 (ExperimentalRedstoneWireEvaluator)
|
||
* is confirmed but WIRES_PER_PLAYER is not sourced from real measurements.
|
||
* Do not cite TPS numbers from §2/§3 as confirmed defect impact.
|
||
*/
|
||
public class LoadSimBenchmark {
|
||
|
||
// ── Modpack profile ───────────────────────────────────────────────────────
|
||
static final int TAG_DEPTH = 12;
|
||
static final int TAG_GRAPH_REPS = 20; // 20 independent tag namespaces per modpack
|
||
// WIRES_PER_PLAYER: exploratory — not sourced from real server measurement.
|
||
// minecraft-0003 defect is confirmed but this constant determines all TPS numbers.
|
||
static final int WIRES_PER_PLAYER = 400;
|
||
static final int TICK_BUDGET_MS = 50; // 20 TPS = 50 ms/tick
|
||
static final int TICKS_TO_SIMULATE = 200;
|
||
static final int WARMUP_TICKS = 40;
|
||
static final int[] PLAYER_COUNTS = {10, 20, 40, 60, 80, 100};
|
||
|
||
// ── Diamond tag graph ─────────────────────────────────────────────────────
|
||
static Map<String, List<String>> buildDiamondTags(int depth, String prefix) {
|
||
Map<String, List<String>> deps = new LinkedHashMap<>();
|
||
deps.put(prefix + "_base", List.of());
|
||
String prevLeft = prefix + "_base", prevRight = prefix + "_base";
|
||
for (int d = 1; d <= depth; d++) {
|
||
String left = prefix + "_L" + d;
|
||
String right = prefix + "_R" + d;
|
||
deps.put(left, List.of(prevLeft, prevRight));
|
||
deps.put(right, List.of(prevLeft, prevRight));
|
||
prevLeft = left; prevRight = right;
|
||
}
|
||
deps.put(prefix + "_root", List.of(prevLeft, prevRight));
|
||
return deps;
|
||
}
|
||
|
||
/** All tag namespaces merged — simulates a full modpack tag registry. */
|
||
static List<Map<String, List<String>>> buildModpackTags(int depth, int reps) {
|
||
List<Map<String, List<String>>> all = new ArrayList<>();
|
||
for (int i = 0; i < reps; i++) all.add(buildDiamondTags(depth, "ns" + i));
|
||
return all;
|
||
}
|
||
|
||
// ── VANILLA: isCyclic without visited set (O(2^D)) ────────────────────────
|
||
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 void addDepVanilla(Multimap<String,String> m, String from, String to) {
|
||
if (!isCyclicVanilla(m, from, to)) m.put(from, to);
|
||
}
|
||
static int resolveVanilla(List<Map<String,List<String>>> tagSpaces) {
|
||
int total = 0;
|
||
for (Map<String,List<String>> tags : tagSpaces) {
|
||
HashMultimap<String,String> m = HashMultimap.create();
|
||
for (Map.Entry<String,List<String>> e : tags.entrySet())
|
||
for (String dep : e.getValue()) addDepVanilla(m, e.getKey(), dep);
|
||
total += m.size();
|
||
}
|
||
return total;
|
||
}
|
||
|
||
// ── PATCHED: isCyclic with visited set (O(V+E)) ───────────────────────────
|
||
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 void addDepPatched(Multimap<String,String> m, String from, String to) {
|
||
if (!isCyclicPatched(m, from, to, new HashSet<>())) m.put(from, to);
|
||
}
|
||
static int resolvePatched(List<Map<String,List<String>>> tagSpaces) {
|
||
int total = 0;
|
||
for (Map<String,List<String>> tags : tagSpaces) {
|
||
HashMultimap<String,String> m = HashMultimap.create();
|
||
for (Map.Entry<String,List<String>> e : tags.entrySet())
|
||
for (String dep : e.getValue()) addDepPatched(m, e.getKey(), dep);
|
||
total += m.size();
|
||
}
|
||
return total;
|
||
}
|
||
|
||
// ── VANILLA: redstone wire eval — Deque membership O(W) ──────────────────
|
||
static long tickRedstoneVanilla(int players, int wiresPerPlayer) {
|
||
long ops = 0;
|
||
for (int p = 0; p < players; p++) {
|
||
ArrayDeque<Integer> wiresToProcess = new ArrayDeque<>();
|
||
ArrayDeque<Integer> wiresToTurnOff = new ArrayDeque<>();
|
||
// seed wires
|
||
for (int w = 0; w < wiresPerPlayer; w++) wiresToProcess.add(w);
|
||
while (!wiresToProcess.isEmpty()) {
|
||
int wire = wiresToProcess.poll();
|
||
if (!wiresToTurnOff.contains(wire)) { // O(W) — the defect
|
||
wiresToTurnOff.add(wire);
|
||
ops++;
|
||
// propagate to ~3 neighbours
|
||
if (wire > 0 && !wiresToTurnOff.contains(wire - 1)) wiresToProcess.add(wire - 1);
|
||
if (wire + 1 < wiresPerPlayer && !wiresToTurnOff.contains(wire + 1)) wiresToProcess.add(wire + 1);
|
||
}
|
||
}
|
||
}
|
||
return ops;
|
||
}
|
||
|
||
// ── PATCHED: redstone wire eval — HashSet companion O(1) ─────────────────
|
||
static long tickRedstonePatched(int players, int wiresPerPlayer) {
|
||
long ops = 0;
|
||
for (int p = 0; p < players; p++) {
|
||
ArrayDeque<Integer> wiresToProcess = new ArrayDeque<>();
|
||
ArrayDeque<Integer> wiresToTurnOff = new ArrayDeque<>();
|
||
Set<Integer> wiresToTurnOffSet = new HashSet<>();
|
||
for (int w = 0; w < wiresPerPlayer; w++) wiresToProcess.add(w);
|
||
while (!wiresToProcess.isEmpty()) {
|
||
int wire = wiresToProcess.poll();
|
||
if (wiresToTurnOffSet.add(wire)) { // O(1) — the fix
|
||
wiresToTurnOff.add(wire);
|
||
ops++;
|
||
if (wire > 0 && !wiresToTurnOffSet.contains(wire - 1)) wiresToProcess.add(wire - 1);
|
||
if (wire + 1 < wiresPerPlayer && !wiresToTurnOffSet.contains(wire + 1)) wiresToProcess.add(wire + 1);
|
||
}
|
||
}
|
||
}
|
||
return ops;
|
||
}
|
||
|
||
// ── Percentile helper ─────────────────────────────────────────────────────
|
||
static long pct(long[] sorted, double p) {
|
||
int i = (int) Math.ceil(p / 100.0 * sorted.length) - 1;
|
||
return sorted[Math.max(0, Math.min(sorted.length - 1, i))];
|
||
}
|
||
|
||
// ── Simulation 1: World load — concurrent joins ───────────────────────────
|
||
static void simWorldLoad(int players) throws Exception {
|
||
List<Map<String,List<String>>> modpack = buildModpackTags(TAG_DEPTH, TAG_GRAPH_REPS);
|
||
|
||
ExecutorService pool = Executors.newFixedThreadPool(players);
|
||
long[] latVanilla = new long[players];
|
||
long[] latPatched = new long[players];
|
||
CountDownLatch startGate = new CountDownLatch(1);
|
||
|
||
// --- VANILLA ---
|
||
List<Future<?>> futures = new ArrayList<>();
|
||
for (int i = 0; i < players; i++) {
|
||
final int idx = i;
|
||
futures.add(pool.submit(() -> {
|
||
try { startGate.await(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
|
||
long t0 = System.nanoTime();
|
||
resolveVanilla(modpack);
|
||
latVanilla[idx] = System.nanoTime() - t0;
|
||
}));
|
||
}
|
||
long wallStart = System.nanoTime();
|
||
startGate.countDown();
|
||
for (Future<?> f : futures) f.get();
|
||
long vanillaWall = System.nanoTime() - wallStart;
|
||
|
||
// --- PATCHED ---
|
||
CountDownLatch gate2 = new CountDownLatch(1);
|
||
futures.clear();
|
||
for (int i = 0; i < players; i++) {
|
||
final int idx = i;
|
||
futures.add(pool.submit(() -> {
|
||
try { gate2.await(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
|
||
long t0 = System.nanoTime();
|
||
resolvePatched(modpack);
|
||
latPatched[idx] = System.nanoTime() - t0;
|
||
}));
|
||
}
|
||
long wallStart2 = System.nanoTime();
|
||
gate2.countDown();
|
||
for (Future<?> f : futures) f.get();
|
||
long patchedWall = System.nanoTime() - wallStart2;
|
||
|
||
pool.shutdown();
|
||
|
||
Arrays.sort(latVanilla);
|
||
Arrays.sort(latPatched);
|
||
|
||
long vp50 = pct(latVanilla, 50), vp95 = pct(latVanilla, 95), vp99 = pct(latVanilla, 99);
|
||
long pp50 = pct(latPatched, 50), pp95 = pct(latPatched, 95), pp99 = pct(latPatched, 99);
|
||
|
||
System.out.printf(" %4d players | wall: %6.1f s → %5.3f s (%5.1f×) | p50 %5.0f ms → %4.0f ms | p99 %5.0f ms → %4.0f ms%n",
|
||
players,
|
||
vanillaWall / 1e9, patchedWall / 1e9, (double) vanillaWall / patchedWall,
|
||
vp50 / 1e6, pp50 / 1e6,
|
||
vp99 / 1e6, pp99 / 1e6);
|
||
}
|
||
|
||
// ── Simulation 2: TPS stability under load ────────────────────────────────
|
||
static void simTPS(int players, boolean patched) {
|
||
int overruns = 0;
|
||
long totalTickMs = 0;
|
||
long worstTickMs = 0;
|
||
|
||
for (int tick = -WARMUP_TICKS; tick < TICKS_TO_SIMULATE; tick++) {
|
||
long t0 = System.nanoTime();
|
||
if (patched) tickRedstonePatched(players, WIRES_PER_PLAYER);
|
||
else tickRedstoneVanilla(players, WIRES_PER_PLAYER);
|
||
long tickMs = (System.nanoTime() - t0) / 1_000_000;
|
||
if (tick >= 0) {
|
||
totalTickMs += tickMs;
|
||
if (tickMs > TICK_BUDGET_MS) overruns++;
|
||
if (tickMs > worstTickMs) worstTickMs = tickMs;
|
||
}
|
||
}
|
||
|
||
double avgTickMs = (double) totalTickMs / TICKS_TO_SIMULATE;
|
||
double achievedTPS = 1000.0 / Math.max(avgTickMs, 1);
|
||
double overrunPct = 100.0 * overruns / TICKS_TO_SIMULATE;
|
||
System.out.printf(" %4d players | avg tick %5.1f ms | worst %4d ms | overruns %4.0f%% | TPS ~%.1f%n",
|
||
players, avgTickMs, worstTickMs, overrunPct, Math.min(20.0, achievedTPS));
|
||
}
|
||
|
||
// ── Main ──────────────────────────────────────────────────────────────────
|
||
public static void main(String[] args) throws Exception {
|
||
System.out.println("=================================================================");
|
||
System.out.println(" LoadSimBenchmark — Minecraft Server Load Simulation");
|
||
System.out.println("=================================================================");
|
||
System.out.printf(" Modpack profile: depth-%d diamond tag graph × %d namespaces%n", TAG_DEPTH, TAG_GRAPH_REPS);
|
||
System.out.printf(" Redstone profile: %d active wires per player base%n", WIRES_PER_PLAYER);
|
||
System.out.printf(" TPS target: 20 TPS (%d ms/tick budget)%n", TICK_BUDGET_MS);
|
||
System.out.println();
|
||
|
||
// ── Section 1: World load ──────────────────────────────────────────────
|
||
System.out.println("─────────────────────────────────────────────────────────────────");
|
||
System.out.println(" §1 World Load / /reload (all players blocked until complete)");
|
||
System.out.println("─────────────────────────────────────────────────────────────────");
|
||
System.out.printf(" %-12s %-44s %-24s %-20s%n",
|
||
"Players", "Wall time (VANILLA → PATCHED)", "p50 join latency", "p99 join latency");
|
||
System.out.println(" " + "-".repeat(100));
|
||
|
||
// warmup
|
||
List<Map<String,List<String>>> modpack = buildModpackTags(TAG_DEPTH, TAG_GRAPH_REPS);
|
||
for (int i = 0; i < 3; i++) { resolveVanilla(modpack); resolvePatched(modpack); }
|
||
|
||
for (int n : PLAYER_COUNTS) simWorldLoad(n);
|
||
|
||
System.out.println();
|
||
|
||
// ── Section 2: TPS — VANILLA ──────────────────────────────────────────
|
||
System.out.println("─────────────────────────────────────────────────────────────────");
|
||
System.out.println(" §2 Game Loop TPS — VANILLA [EXPLORATORY — wire count uncalibrated]");
|
||
System.out.println("─────────────────────────────────────────────────────────────────");
|
||
System.out.printf(" %-12s %-22s %-18s %-16s %-12s%n",
|
||
"Players", "Avg tick ms", "Worst tick ms", "Overrun %", "Achieved TPS");
|
||
System.out.println(" " + "-".repeat(80));
|
||
for (int n : PLAYER_COUNTS) simTPS(n, false);
|
||
|
||
System.out.println();
|
||
|
||
// ── Section 3: TPS — PATCHED ──────────────────────────────────────────
|
||
System.out.println("─────────────────────────────────────────────────────────────────");
|
||
System.out.println(" §3 Game Loop TPS — PATCHED [EXPLORATORY — wire count uncalibrated]");
|
||
System.out.println("─────────────────────────────────────────────────────────────────");
|
||
System.out.printf(" %-12s %-22s %-18s %-16s %-12s%n",
|
||
"Players", "Avg tick ms", "Worst tick ms", "Overrun %", "Achieved TPS");
|
||
System.out.println(" " + "-".repeat(80));
|
||
for (int n : PLAYER_COUNTS) simTPS(n, true);
|
||
|
||
System.out.println();
|
||
System.out.println("─────────────────────────────────────────────────────────────────");
|
||
System.out.println(" §4 Summary");
|
||
System.out.println("─────────────────────────────────────────────────────────────────");
|
||
System.out.printf(" §1 World load [algorithm isolation]:%n");
|
||
System.out.printf(" DependencySorter.isCyclic depth-%d, O(2^D) → O(V+E)%n", TAG_DEPTH);
|
||
System.out.printf(" Real server baseline: vanilla 19,255 ms → patched 3,087 ms (6.2×)%n");
|
||
System.out.printf(" §2/§3 Redstone TPS [exploratory — wire count uncalibrated]:%n");
|
||
System.out.printf(" ExperimentalRedstoneWireEvaluator, O(W) → O(1), %d wires/player assumed%n", WIRES_PER_PLAYER);
|
||
System.out.printf(" minecraft-0003 defect confirmed; TPS numbers require real measurement%n");
|
||
System.out.println(" ─────────────────────────────────────────────────────────────");
|
||
System.out.println(" Confirmed: DependencySorter fix eliminates exponential world load.");
|
||
System.out.println(" Exploratory: redstone fix may improve TPS at high wire density.");
|
||
System.out.println(" Do not cite §2/§3 TPS numbers without a real server calibration.");
|
||
System.out.println("=================================================================");
|
||
}
|
||
}
|