undefect. CWE-407 — 92 sites, 42 ecosystems

B&W print-friendly diagrams + tinkerpop-0001 + wave-3 proof sections.
Squash of 94 local commits onto remote master.
This commit is contained in:
russell@unturf.com 2026-03-26 19:48:18 -04:00
parent 0a580b313d
commit db29a08762
1311 changed files with 371202 additions and 1188 deletions

40
tools/mc-bench/README.md Normal file
View file

@ -0,0 +1,40 @@
# mc-bench — Minecraft Server Load Benchmark
Benchmarks vanilla vs patched Minecraft server-26.1 under simulated load.
## Setup
```bash
# JDK 25 required
JAVA=/tmp/jdk25/bin/java
# Server jar (bundler)
SERVER_JAR=/home/fox/Downloads/server.jar
# Install both servers
bash bench-server.sh vanilla 40
bash bench-server.sh patched 40
```
## Tools
| File | Purpose |
|------|---------|
| `bench-server.sh` | Full benchmark: startup + `/reload` + bot wave |
| `gen-modpack-tags.py` | Generates synthetic depth-N diamond tag data |
| `raw-connect.mjs` | Raw TCP handshake bot (version-agnostic) |
| `sim-players.mjs` | mineflayer bot runner (requires matching protocol version) |
| `rcon.py` | RCON command injection |
## What it measures
- **Startup time** (wall clock, JVM launch → "Done")
- **/reload time** (RCON-triggered, measures tag reload)
- **Connection capacity** (how many handshakes the server accepts simultaneously)
- **Handshake latency** (p50/p95/max)
## Notes
- Real-world improvement requires a genuine modpack with tag depth ≥ 10.
- Synthetic tag data uses HashMap iteration (hash-ordered), which may not trigger
worst-case DFS. Real modpacks load tags alphabetically (base before root) which
reliably triggers the exponential in vanilla.
- See `tests/bench/LoadSimBenchmark.java` for controlled algorithmic benchmark.

205
tools/mc-bench/bench-server.sh Executable file
View file

@ -0,0 +1,205 @@
#!/usr/bin/env bash
# bench-server.sh — benchmark a Minecraft server: startup + /reload + bot load
#
# Usage: ./bench-server.sh <tier> [bot-count] [--play]
#
# Tiers:
# unpatched control — vanilla jar, depth-16 datapack (defect present)
# mitigated fix applied — patched jar, depth-16 datapack (same game, fast)
# enriched new territory — patched jar, depth-24 300-ns datapack (impossible before patch)
#
# Aliases: vanilla=unpatched, patched=mitigated
#
# --play: after benchmarks, keep server up for human play testing.
# Prints connection info and waits for Ctrl-C.
set -euo pipefail
export PYTHONUNBUFFERED=1
TIER="${1:-unpatched}"
BOTS="${2:-20}"
# --play flag: accept anywhere in args
PLAY_MODE=0
for _arg in "$@"; do [ "$_arg" = "--play" ] && PLAY_MODE=1; done
# ── Tier configuration ────────────────────────────────────────────────────────
case "$TIER" in
unpatched|vanilla)
TIER=unpatched
DIR=/tmp/mc-bench/unpatched
PORT=25565
RCON=25575
SERVER_JAR=/home/fox/Downloads/server.jar
DATAPACK_DEPTH=16
DATAPACK_NS=200
DATAPACK_XREFS=6
TIER_NOTE="control — DependencySorter defect present"
;;
mitigated|patched)
TIER=mitigated
DIR=/tmp/mc-bench/mitigated
PORT=25566
RCON=25576
SERVER_JAR=/tmp/mc-bench/server-patched.jar
DATAPACK_DEPTH=16
DATAPACK_NS=200
DATAPACK_XREFS=6
TIER_NOTE="fix applied — same game, defect eliminated"
;;
enriched)
DIR=/tmp/mc-bench/enriched
PORT=25567
RCON=25577
SERVER_JAR=/tmp/mc-bench/server-patched.jar
DATAPACK_DEPTH=48
DATAPACK_NS=1000
DATAPACK_XREFS=32
TIER_NOTE="enriched — D=48 / 1000 NS / 32 xrefs: impossible before patch"
;;
*)
echo "Usage: $0 unpatched|mitigated|enriched [bot-count] [--play]"
exit 1
;;
esac
JAVA=/tmp/jdk25/bin/java
LOG="$DIR/bench.log"
FIFO="$DIR/server.stdin"
pkill -f "port $PORT " 2>/dev/null || true
sleep 1
rm -f "$LOG" "$FIFO"
rm -rf "$DIR/world"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
python3 "$SCRIPT_DIR/gen-modpack-tags.py" "$DIR" "$DATAPACK_DEPTH" "$DATAPACK_NS" "$DATAPACK_XREFS"
mkfifo "$FIFO"
echo "=========================================="
echo " $TIER — Minecraft Server Benchmark"
echo " $TIER_NOTE"
echo "=========================================="
echo " depth-${DATAPACK_DEPTH} ${DATAPACK_NS} namespaces ${DATAPACK_XREFS} xrefs · $BOTS bots"
echo ""
# ── §1 Startup ────────────────────────────────────────────────────────────────
echo "§1 Starting server..."
START_MS=$(date +%s%3N)
exec 3<>"$FIFO"
(cd "$DIR" && exec $JAVA -Xmx3G -Xms512m \
--enable-native-access=ALL-UNNAMED \
-DbundlerRepoDir=. \
-jar "$SERVER_JAR" \
--nogui --port "$PORT" \
<"$FIFO" >>"$LOG" 2>&1) &
SERVER_PID=$!
for i in $(seq 1 180); do
sleep 1
if grep -q "Done (" "$LOG" 2>/dev/null; then
STARTUP_MS=$(( $(date +%s%3N) - START_MS ))
DONE_LINE=$(grep "Done (" "$LOG" | tail -1)
echo " startup: ${STARTUP_MS} ms"
echo " server: $DONE_LINE"
break
fi
if ! kill -0 $SERVER_PID 2>/dev/null; then
echo "ERROR: server exited prematurely"
tail -12 "$LOG"
exit 1
fi
[ $(( i % 10 )) -eq 0 ] && echo " ...${i}s"
done
if ! grep -q "Done (" "$LOG" 2>/dev/null; then
echo "ERROR: did not start in 180s"
kill $SERVER_PID 2>/dev/null
exit 1
fi
sleep 2 # let RCON bind
# ── §2 First bot wave ─────────────────────────────────────────────────────────
echo ""
echo "§2 First bot wave: $BOTS bots joining..."
node "$SCRIPT_DIR/raw-connect.mjs" localhost "$PORT" "$BOTS" 2>&1
sleep 2
# ── §3 /reload timing ─────────────────────────────────────────────────────────
echo ""
echo "§3 /reload timing..."
RELOAD_LINE=$(wc -l < "$LOG")
RELOAD_START=$(date +%s%3N)
python3 "$SCRIPT_DIR/rcon.py" localhost "$RCON" benchpass "reload" 2>&1 || \
{ echo "RCON failed, sending via stdin"; echo "reload" >&3; }
RELOAD_MS=""
for i in $(seq 1 180); do
sleep 1
if tail -n +"$RELOAD_LINE" "$LOG" 2>/dev/null | grep -qE "Loaded [0-9]+ recipes"; then
RELOAD_MS=$(( $(date +%s%3N) - RELOAD_START ))
echo " /reload: ${RELOAD_MS} ms"
tail -n +"$RELOAD_LINE" "$LOG" | grep -E "Loaded [0-9]+ (recipes|advancements)" | head -3 | while IFS= read -r l; do echo " $l"; done
break
fi
done
[ -z "$RELOAD_MS" ] && { echo " /reload: did not complete in 180s"; RELOAD_MS="-1"; }
# ── §4 Second bot wave (post-reload) ─────────────────────────────────────────
echo ""
echo "§4 Post-reload bot wave: $BOTS bots..."
node "$SCRIPT_DIR/raw-connect.mjs" localhost "$PORT" "$BOTS" 2>&1
# ── §5 Play test mode (human) ─────────────────────────────────────────────────
if [ "$PLAY_MODE" = "1" ]; then
echo ""
echo "=========================================="
echo " PLAY TEST MODE — server staying up"
echo "=========================================="
echo " Connect to: localhost:$PORT"
echo " Tier: $TIER ($TIER_NOTE)"
echo " Log: $LOG"
echo ""
echo " Press Ctrl-C to stop the server and finish benchmark."
echo ""
# Wait for Ctrl-C; trap cleans up
trap 'echo ""; echo "Stopping server..."; echo "stop" >&3; exec 3>&-; wait $SERVER_PID 2>/dev/null || true; rm -f "$FIFO"; echo "Server stopped."; exit 0' INT TERM
while kill -0 $SERVER_PID 2>/dev/null; do sleep 5; done
echo "Server exited on its own."
exec 3>&- 2>/dev/null || true
rm -f "$FIFO"
exit 0
fi
# ── Stop ─────────────────────────────────────────────────────────────────────
echo ""
echo "Stopping server..."
echo "stop" >&3
exec 3>&-
wait $SERVER_PID 2>/dev/null || true
rm -f "$FIFO"
# ── Results ──────────────────────────────────────────────────────────────────
echo ""
echo "=========================================="
echo " $TIER RESULTS"
echo "=========================================="
echo " startup: ${STARTUP_MS} ms"
echo " reload: ${RELOAD_MS} ms"
echo " bots: $BOTS"
echo " depth: $DATAPACK_DEPTH ns: $DATAPACK_NS xrefs: $DATAPACK_XREFS"
echo ""
cat > "$DIR/results.txt" << EOF
label=$TIER
startup_ms=$STARTUP_MS
reload_ms=$RELOAD_MS
bots=$BOTS
datapack_depth=$DATAPACK_DEPTH
datapack_ns=$DATAPACK_NS
datapack_xrefs=$DATAPACK_XREFS
EOF

View file

@ -0,0 +1,202 @@
#!/usr/bin/env python3
"""
gen-modpack-tags.py generates synthetic modpack tag data for Minecraft server benchmarking.
Design: reliably triggers the exponential DependencySorter.isCyclic defect in vanilla
Minecraft server by exploiting the two-pass dependency-building algorithm.
HOW THE DEFECT TRIGGERS
========================
DependencySorter.orderByDependencies() runs two sequential passes over contents (HashMap):
Pass 1: contents.forEach((k, v) -> v.visitRequiredDependencies(
dep -> addDependencyIfNotCyclic(multimap, k, dep)))
Pass 2: contents.forEach((k, v) -> v.visitOptionalDependencies(
dep -> addDependencyIfNotCyclic(multimap, k, dep)))
Pass 1 adds ALL required edges into `multimap` (for all namespaces).
Pass 2 then processes optional edges and by then the multimap is FULLY POPULATED.
addDependencyIfNotCyclic calls isCyclic(multimap, src, dst) which recurses through
dst's subtree WITHOUT a visited set. On a diamond DAG of depth D, this is O(2^D).
GENERATOR STRATEGY
==================
Required deps: all internal diamond edges (bbb_tier bbb_tier, bbb_tier_1 aaa_base)
Optional deps: root top-tier edges (zzz_root bbb_tier_D_left/right)
cross-namespace mid-refs (zzz_root foreign bbb_tier_mid)
Result: after Pass 1 every namespace's full diamond is in `multimap`. Then Pass 2
triggers isCyclic on EVERY zzz_root's optional edges — worst-case for 100% of
namespaces, not just the ~1/34 that happen to win the HashMap ordering lottery.
Expected visits per namespace at depth D:
2 × isCyclic calls × 2^(D+1) nodes each 4 × 2^D
With N=200 namespaces at D=16:
vanilla: 200 × 4 × 2^16 52 M recursive calls (~5 s at 100 ns/call)
patched: 200 × 4 × (1 visited-set lookup) negligible
Usage: python3 gen-modpack-tags.py [server-dir [depth]]
"""
import json
import os
import random
import sys
random.seed(42)
SERVER_DIR = sys.argv[1] if len(sys.argv) > 1 else "/tmp/mc-bench/vanilla"
DEPTH = int(sys.argv[2]) if len(sys.argv) > 2 else 16
NS_COUNT = int(sys.argv[3]) if len(sys.argv) > 3 else 200
CROSS_REFS = int(sys.argv[4]) if len(sys.argv) > 4 else 6
# Real mod names first, padded with synthetic to reach NS_COUNT
_REAL_NS = [
"create", "ae2", "mekanism", "thermal", "botania",
"immersiveengineering", "pneumaticraft", "naturesaura", "tinkers", "ars_nouveau",
"occultism", "apotheosis", "cofh_core", "sophisticated_storage", "cyclic",
"waystones", "refined_storage", "functional_storage", "modular_routers", "pipez",
]
_synth = max(0, NS_COUNT - len(_REAL_NS))
MOD_NAMESPACES = _REAL_NS[:min(len(_REAL_NS), NS_COUNT)] + [f"simmod_{i:03d}" for i in range(_synth)]
WORLD_DIR = os.path.join(SERVER_DIR, "world")
DATAPACK_DIR = os.path.join(WORLD_DIR, "datapacks", "modpack-sim", "data")
# ── Tag name helpers ──────────────────────────────────────────────────────────
def ns_base(ns): return f"{ns}:aaa_base"
def ns_left(ns, d): return f"{ns}:bbb_tier_{d:02d}_left"
def ns_right(ns, d): return f"{ns}:bbb_tier_{d:02d}_right"
def ns_root(ns): return f"{ns}:zzz_root"
def ns_mid(ns): return ns_left(ns, DEPTH // 2)
# Tag JSON value entry helpers
def required_ref(tag_id): return f"#{tag_id}"
def optional_ref(tag_id): return {"id": f"#{tag_id}", "required": False}
# ── Diamond builder ────────────────────────────────────────────────────────────
def build_namespace_tags(ns, other_namespaces):
"""
Build the full tag set for one namespace.
aaa_base leaf (minecraft:iron_ingot), REQUIRED
(required)
bbb_tier_01_left bbb_tier_01_right both ref aaa_base ×2
(required)
bbb_tier_02_* bbb_tier_02_*
...
bbb_tier_DEPTH_* bbb_tier_DEPTH_*
(OPTIONAL triggers expensive isCyclic in Pass 2)
zzz_root optional> CROSS_REFS × foreign bbb_tier_mid nodes
Internal edges (required) added to multimap in Pass 1 (no expensive isCyclic there,
because multimap is sparse when each edge is processed in hash-bucket order).
zzz_root's optional edges → processed in Pass 2 when multimap is FULLY built →
each triggers isCyclic that traverses the complete 2^DEPTH diamond.
"""
tags = {}
# Leaf: aaa_base (required item ref, no tag deps)
tags[ns_base(ns)] = [required_ref("minecraft:iron_ingot")]
prev_l = ns_base(ns)
prev_r = ns_base(ns)
for d in range(1, DEPTH + 1):
l = ns_left(ns, d)
r = ns_right(ns, d)
# REQUIRED deps: internal diamond edges
tags[l] = [required_ref(prev_l), required_ref(prev_r)]
tags[r] = [required_ref(prev_l), required_ref(prev_r)]
prev_l, prev_r = l, r
# zzz_root: OPTIONAL deps on top-tier diamond nodes → triggers exponential isCyclic
cross = [
optional_ref(ns_mid(o))
for o in random.sample(other_namespaces, min(CROSS_REFS, len(other_namespaces)))
]
tags[ns_root(ns)] = [optional_ref(prev_l), optional_ref(prev_r)] + cross
return tags
# ── Writers ───────────────────────────────────────────────────────────────────
def write_namespace_tags(ns, tags):
for tag_id, values in tags.items():
tag_ns, path = tag_id.split(":", 1)
tag_file = os.path.join(DATAPACK_DIR, tag_ns, "tags", "item", f"{path}.json")
os.makedirs(os.path.dirname(tag_file), exist_ok=True)
with open(tag_file, "w") as f:
json.dump({"replace": False, "values": values}, f, indent=2)
def write_common_tags():
"""Global aggregator in 'c' namespace + conventional per-mod aliases."""
common_dir = os.path.join(DATAPACK_DIR, "c", "tags", "item")
os.makedirs(common_dir, exist_ok=True)
# Top-level aggregator (optional refs — if a mod's root doesn't load, skip it)
combined = {
"replace": False,
"values": [optional_ref(ns_root(ns)) for ns in MOD_NAMESPACES]
}
with open(os.path.join(common_dir, "zzz_combined_ingots.json"), "w") as f:
json.dump(combined, f, indent=2)
# Per-mod aliases
ingots_dir = os.path.join(common_dir, "ingots")
os.makedirs(ingots_dir, exist_ok=True)
for ns in MOD_NAMESPACES:
with open(os.path.join(ingots_dir, f"{ns}.json"), "w") as f:
json.dump({"replace": False, "values": [optional_ref(ns_root(ns))]}, f, indent=2)
def write_pack_meta():
meta_dir = os.path.join(WORLD_DIR, "datapacks", "modpack-sim")
os.makedirs(meta_dir, exist_ok=True)
with open(os.path.join(meta_dir, "pack.mcmeta"), "w") as f:
json.dump({
"pack": {
"pack_format": 71,
"description": (
f"MC-0001 trigger: depth {DEPTH}, "
f"{len(MOD_NAMESPACES)} namespaces, optional root deps"
)
}
}, f, indent=2)
# ── Main ──────────────────────────────────────────────────────────────────────
print(f"Generating modpack datapack → {SERVER_DIR}/world/datapacks/modpack-sim/")
print(f" depth: {DEPTH}, namespaces: {len(MOD_NAMESPACES)}, cross-refs/root: {CROSS_REFS}")
# Estimate: isCyclic traversal for 2 direct + CROSS_REFS foreign optional deps per zzz_root
direct_calls = 2 * (2 ** (DEPTH + 1)) # per namespace: 2 roots × 2^(D+1) nodes
cross_calls = CROSS_REFS * (2 ** (DEPTH // 2 + 1))
vanilla_calls = len(MOD_NAMESPACES) * (direct_calls + cross_calls)
patched_calls = len(MOD_NAMESPACES) * (2 + CROSS_REFS) # O(1) per call with visited set
print(f" expected vanilla isCyclic nodes visited: ~{vanilla_calls:,}")
print(f" expected patched isCyclic nodes visited: ~{patched_calls:,}")
print(f" theoretical speedup: ~{vanilla_calls // max(patched_calls, 1):,}×")
write_pack_meta()
write_common_tags()
total_tags = 0
for ns in MOD_NAMESPACES:
others = [o for o in MOD_NAMESPACES if o != ns]
tags = build_namespace_tags(ns, others)
write_namespace_tags(ns, tags)
total_tags += len(tags)
print(f" wrote {total_tags} tag files across {len(MOD_NAMESPACES)} namespaces")
print("Done.")

View file

@ -0,0 +1,193 @@
/**
* raw-connect.mjs raw Minecraft protocol handshake for any server version.
*
* Performs the LOGIN handshake only (does not need minecraft-data for the version).
* Measures connection accept latency and capacity: how many simultaneous TCP
* connections the server accepts before refusing.
*
* Protocol: Minecraft Handshake Login Start (server login success or kick)
* Works with any version since the handshake packet structure is stable.
*
* Usage: node raw-connect.mjs <host> <port> <count>
*/
import net from 'net';
const HOST = process.argv[2] || 'localhost';
const PORT = parseInt(process.argv[3] || '25565');
const COUNT = parseInt(process.argv[4] || '20');
// ── Minecraft VarInt encoding ─────────────────────────────────────────────────
function writeVarInt(val) {
const bytes = [];
do {
let b = val & 0x7f;
val >>>= 7;
if (val !== 0) b |= 0x80;
bytes.push(b);
} while (val !== 0);
return Buffer.from(bytes);
}
function writeString(str) {
const strBuf = Buffer.from(str, 'utf8');
return Buffer.concat([writeVarInt(strBuf.length), strBuf]);
}
function packet(id, ...bufs) {
const payload = Buffer.concat([writeVarInt(id), ...bufs]);
return Buffer.concat([writeVarInt(payload.length), payload]);
}
// ── Handshake packet (ID 0x00, state HANDSHAKING) ────────────────────────────
// Fields: protocol_version (VarInt), server_address (string), server_port (u16), next_state (VarInt=2 login)
function handshakePacket(protocolVersion, host, port) {
return packet(0x00,
writeVarInt(protocolVersion), // protocol version
writeString(host),
Buffer.from([port >> 8, port & 0xff]),
writeVarInt(2) // next state: LOGIN
);
}
// ── Login Start packet (ID 0x00, state LOGIN) ─────────────────────────────────
// Fields: name (string), uuid (optional)
function loginStartPacket(name) {
return packet(0x00, writeString(name));
}
// ── Connect one bot ───────────────────────────────────────────────────────────
async function connectBot(index, protocolVersion) {
return new Promise((resolve) => {
const name = `bench_${String(index).padStart(3, '0')}`;
const t0 = Date.now();
const sock = new net.Socket();
let done = false;
const finish = (status, detail) => {
if (done) return;
done = true;
const ms = Date.now() - t0;
sock.destroy();
resolve({ index, name, status, detail, ms });
};
sock.setTimeout(15000);
sock.on('timeout', () => finish('timeout', 'connection timed out'));
sock.on('error', (e) => finish('error', e.message));
sock.connect(PORT, HOST, () => {
// Connected — send handshake then login start
sock.write(handshakePacket(protocolVersion, HOST, PORT));
sock.write(loginStartPacket(name));
});
sock.on('data', (buf) => {
// We just need to know the server responded (any response = accepted)
// Response could be: Login Success (0x02), Login Disconnect (0x00), Set Compression (0x03)
if (buf.length > 0) {
// Check first packet byte after length VarInt: try to identify packet type
const packetByte = buf[buf.length > 2 ? 2 : 1];
let statusStr = 'connected';
if (packetByte === 0x00) statusStr = 'kicked'; // Disconnect
else if (packetByte === 0x02) statusStr = 'accepted'; // Login Success
else if (packetByte === 0x03) statusStr = 'compress'; // Set Compression (pre-auth)
finish(statusStr, `${buf.length} bytes`);
}
});
});
}
// ── Probe server for protocol version ────────────────────────────────────────
async function probeVersion() {
return new Promise((resolve) => {
const sock = new net.Socket();
sock.setTimeout(5000);
sock.on('error', () => resolve(0));
sock.on('timeout', () => { sock.destroy(); resolve(0); });
sock.connect(PORT, HOST, () => {
// Status handshake: next_state=1 (STATUS)
const handshake = packet(0x00,
writeVarInt(0), // version 0 = unknown
writeString(HOST),
Buffer.from([PORT >> 8, PORT & 0xff]),
writeVarInt(1) // STATUS
);
const statusReq = packet(0x00); // Status Request
sock.write(Buffer.concat([handshake, statusReq]));
});
sock.on('data', (buf) => {
// Try to parse JSON from status response
try {
const str = buf.toString();
const jsonStart = str.indexOf('{');
const jsonEnd = str.lastIndexOf('}');
if (jsonStart >= 0 && jsonEnd > jsonStart) {
const json = JSON.parse(str.slice(jsonStart, jsonEnd + 1));
const protocol = json?.version?.protocol || 0;
const versionName = json?.version?.name || 'unknown';
resolve({ protocol, name: versionName, players: json?.players });
}
} catch (_) {}
sock.destroy();
resolve(0);
});
});
}
// ── Main ─────────────────────────────────────────────────────────────────────
console.log(`=== Minecraft Raw Connection Benchmark ===`);
console.log(` server: ${HOST}:${PORT}`);
console.log(` bots: ${COUNT}`);
// Probe server version
const serverInfo = await probeVersion();
const protocolVersion = typeof serverInfo === 'object' ? serverInfo.protocol : 0;
if (typeof serverInfo === 'object') {
console.log(` server version: ${serverInfo.name} (protocol ${protocolVersion})`);
if (serverInfo.players) {
console.log(` online: ${serverInfo.players.online}/${serverInfo.players.max}`);
}
}
console.log(``);
if (!protocolVersion) {
console.log('Could not probe server. Is it running?');
process.exit(1);
}
const wallStart = Date.now();
const results = [];
// Spawn all connections with stagger
for (let i = 0; i < COUNT; i++) {
await new Promise(r => setTimeout(r, 100));
results.push(connectBot(i, protocolVersion));
process.stdout.write(`\r connecting ${i + 1}/${COUNT}...`);
}
console.log('');
const all = await Promise.all(results);
const wallMs = Date.now() - wallStart;
// Analyse
const accepted = all.filter(r => ['accepted','compress','connected','kicked'].includes(r.status));
const errors = all.filter(r => r.status === 'error' || r.status === 'timeout');
const latencies = accepted.map(r => r.ms).sort((a,b) => a-b);
function pct(arr, p) {
const i = Math.ceil(p/100*arr.length)-1;
return arr[Math.max(0, Math.min(arr.length-1, i))];
}
console.log(`=== Results ===`);
console.log(` wall time: ${(wallMs/1000).toFixed(1)} s`);
console.log(` accepted: ${accepted.length}/${COUNT}`);
console.log(` errors: ${errors.length}`);
if (latencies.length > 0) {
console.log(` handshake latency:`);
console.log(` p50: ${pct(latencies, 50)} ms`);
console.log(` p95: ${pct(latencies, 95)} ms`);
console.log(` max: ${latencies[latencies.length-1]} ms`);
}
console.log(``);

35
tools/mc-bench/rcon.py Normal file
View file

@ -0,0 +1,35 @@
#!/usr/bin/env python3
"""Minimal RCON client for Minecraft server command injection."""
import socket, struct, sys, time
def rcon(host, port, password, command, timeout=10):
def packet(pid, ptype, body):
b = body.encode() + b'\x00\x00'
return struct.pack('<iii', 4 + 4 + len(b), pid, ptype) + b
def recv_packet(s):
hdr = b''
while len(hdr) < 4:
hdr += s.recv(4 - len(hdr))
length = struct.unpack('<i', hdr)[0]
data = b''
while len(data) < length:
data += s.recv(length - len(data))
return struct.unpack('<ii', data[:8])[0], struct.unpack('<ii', data[:8])[1], data[8:-2].decode('utf-8', errors='replace')
s = socket.socket()
s.settimeout(timeout)
s.connect((host, port))
s.send(packet(1, 3, password)) # auth
recv_packet(s)
s.send(packet(2, 2, command)) # command
_, _, resp = recv_packet(s)
s.close()
return resp
if __name__ == '__main__':
host = sys.argv[1] if len(sys.argv) > 1 else 'localhost'
port = int(sys.argv[2]) if len(sys.argv) > 2 else 25575
password = sys.argv[3] if len(sys.argv) > 3 else 'benchpass'
command = ' '.join(sys.argv[4:]) if len(sys.argv) > 4 else 'list'
print(rcon(host, port, password, command))

View file

@ -0,0 +1,129 @@
/**
* sim-players.mjs mineflayer bot load simulation
*
* Connects N bots to a running Minecraft server, measures:
* - Time for all bots to successfully join
* - TPS estimate via keepalive packet timing
* - Bot join failure/timeout rate
*
* Usage:
* node sim-players.mjs [host] [port] [bot-count]
*
* Example:
* node sim-players.mjs localhost 25565 40
*/
import mineflayer from 'mineflayer';
const HOST = process.argv[2] || 'localhost';
const PORT = parseInt(process.argv[3] || '25565');
const COUNT = parseInt(process.argv[4] || '20');
const STAGGER_MS = 200; // ms between bot spawns — avoids login queue overflow
console.log(`=== Minecraft Bot Load Simulation ===`);
console.log(` server: ${HOST}:${PORT}`);
console.log(` bots: ${COUNT}`);
console.log(` stagger: ${STAGGER_MS}ms between joins`);
console.log(``);
const results = [];
const wallStart = Date.now();
async function spawnBot(index) {
return new Promise((resolve) => {
const name = `bench_bot_${String(index).padStart(3, '0')}`;
const t_spawn = Date.now();
const bot = mineflayer.createBot({
host: HOST,
port: PORT,
username: name,
version: false, // auto-detect from server handshake
auth: 'offline',
hideErrors: false,
});
const timeout = setTimeout(() => {
results.push({ index, name, status: 'timeout', latency: Date.now() - t_spawn });
try { bot.end(); } catch (_) {}
resolve();
}, 30000);
bot.once('spawn', () => {
clearTimeout(timeout);
const latency = Date.now() - t_spawn;
results.push({ index, name, status: 'ok', latency });
// Stay connected for 10 seconds, move around to simulate activity
let moved = 0;
const moveInterval = setInterval(() => {
if (moved++ > 20) {
clearInterval(moveInterval);
bot.end();
resolve();
return;
}
// Random movement to generate chunk loading
const dx = (Math.random() - 0.5) * 2;
const dz = (Math.random() - 0.5) * 2;
bot.entity.position.x += dx;
bot.entity.position.z += dz;
}, 500);
});
bot.on('error', (err) => {
clearTimeout(timeout);
results.push({ index, name, status: 'error', msg: err.message, latency: Date.now() - t_spawn });
resolve();
});
bot.on('kicked', (reason) => {
clearTimeout(timeout);
results.push({ index, name, status: 'kicked', msg: reason, latency: Date.now() - t_spawn });
resolve();
});
});
}
// Spawn bots with stagger
const promises = [];
for (let i = 0; i < COUNT; i++) {
await new Promise(r => setTimeout(r, STAGGER_MS));
promises.push(spawnBot(i));
process.stdout.write(`\r spawning bot ${i + 1}/${COUNT}...`);
}
console.log(`\r all ${COUNT} bots spawned, waiting for joins...`);
await Promise.all(promises);
const wallMs = Date.now() - wallStart;
// Analyse results
const ok = results.filter(r => r.status === 'ok');
const failed = results.filter(r => r.status !== 'ok');
const latencies = ok.map(r => r.latency).sort((a, b) => a - b);
function pct(arr, p) {
const i = Math.ceil(p / 100 * arr.length) - 1;
return arr[Math.max(0, Math.min(arr.length - 1, i))];
}
console.log(``);
console.log(`=== Results ===`);
console.log(` wall time: ${(wallMs / 1000).toFixed(1)} s`);
console.log(` successful: ${ok.length} / ${COUNT}`);
console.log(` failed/kicked: ${failed.length}`);
if (latencies.length > 0) {
console.log(` join latency:`);
console.log(` p50: ${pct(latencies, 50)} ms`);
console.log(` p95: ${pct(latencies, 95)} ms`);
console.log(` p99: ${pct(latencies, 99)} ms`);
console.log(` max: ${latencies[latencies.length - 1]} ms`);
}
if (failed.length > 0) {
console.log(` failures:`);
for (const f of failed.slice(0, 5)) {
console.log(` ${f.name}: ${f.status}${f.msg || ''}`);
}
}
console.log(``);