Complete local voxel MVP with server, client, MCP, packages and world interchange
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
// Fixed local release-server budgets. This is a reproducible Shacraft-only workload,
|
||||
// not a comparison with Minecraft and not a production player-capacity claim.
|
||||
import assert from "node:assert/strict";
|
||||
import path from "node:path";
|
||||
import {readFile} from "node:fs/promises";
|
||||
import { harness, Client, delay, options, root } from "./server-harness.mjs";
|
||||
const opts = options();
|
||||
opts.binary ??= path.join(root, "target/release/shacraft-server");
|
||||
const budgets = { ten_client_rss_bytes: 256 * 1024 * 1024, p95_tick_ms: 50 };
|
||||
const duration = Number(opts["sample-ms"] || 15000);
|
||||
assert.ok(
|
||||
duration >= 3000 && duration <= 120000,
|
||||
"--sample-ms must be 3000..120000",
|
||||
);
|
||||
const h = await harness(opts),
|
||||
clients = [],
|
||||
samples = [],
|
||||
phases = [];
|
||||
let reportWritten = false;
|
||||
const percentile = (values, p) =>
|
||||
[...values].sort((a, b) => a - b)[
|
||||
Math.min(values.length - 1, Math.floor((values.length - 1) * p))
|
||||
];
|
||||
function summary(name, list) {
|
||||
const rss = list.map((s) => s.rss_bytes).filter(Number.isFinite),
|
||||
ticks = list.map((s) => s.tick_ms).filter(Number.isFinite);
|
||||
assert.ok(
|
||||
rss.length && ticks.length,
|
||||
"Measured RSS and tick durations are required",
|
||||
);
|
||||
const last = list.at(-1);
|
||||
return {
|
||||
phase: name,
|
||||
samples: list.length,
|
||||
rss_bytes: {
|
||||
p50: percentile(rss, 0.5),
|
||||
p95: percentile(rss, 0.95),
|
||||
max: Math.max(...rss),
|
||||
},
|
||||
tick_ms: {
|
||||
p50: percentile(ticks, 0.5),
|
||||
p95: percentile(ticks, 0.95),
|
||||
max: Math.max(...ticks),
|
||||
},
|
||||
process_high_water_bytes: last.process_high_water_bytes,
|
||||
players: last.players,
|
||||
active_worlds: last.active_worlds,
|
||||
world_count: last.world_count,
|
||||
storage: last.storage,
|
||||
slow_clients_disconnected: last.slow_clients_disconnected,
|
||||
queued_client_bytes: last.queued_client_bytes,
|
||||
};
|
||||
}
|
||||
async function sample(name, milliseconds = duration) {
|
||||
const list = [],
|
||||
end = Date.now() + milliseconds;
|
||||
while (Date.now() < end) {
|
||||
const m = await h.get("/api/metrics");
|
||||
const processStatus = await readFile(`/proc/${h.pid}/status`, "utf8");
|
||||
const highWater = Number(processStatus.match(/^VmHWM:\s+(\d+)/m)?.[1]) * 1024;
|
||||
assert.ok(Number.isFinite(highWater), "Linux process high-water RSS required");
|
||||
m.process_high_water_bytes = highWater;
|
||||
const s = { phase: name, time: Date.now(), ...m };
|
||||
samples.push(s);
|
||||
list.push(s);
|
||||
await delay(100);
|
||||
}
|
||||
const result = summary(name, list);
|
||||
phases.push(result);
|
||||
return result;
|
||||
}
|
||||
let mover;
|
||||
async function moving(count, distinct) {
|
||||
const manifest = await h.get("/api/manifest");
|
||||
for (let i = 0; i < count; i++) {
|
||||
const c = new Client(h.url);
|
||||
clients.push(c);
|
||||
await c.join(
|
||||
manifest,
|
||||
`test_bench_${String(distinct ? i : 0).padStart(3, "0")}`,
|
||||
`Bench${i}`,
|
||||
);
|
||||
}
|
||||
const started = Date.now();
|
||||
mover = setInterval(() => {
|
||||
const t = (Date.now() - started) / 1000;
|
||||
for (let i = 0; i < clients.length; i++)
|
||||
clients[i].input({
|
||||
yaw: (t * 0.75 + (i * Math.PI) / 5) % (Math.PI * 2),
|
||||
pitch: -0.1,
|
||||
forward: 1,
|
||||
strafe: 0,
|
||||
jump: Math.floor(t) % 5 === 0,
|
||||
});
|
||||
}, 50);
|
||||
await delay(2000);
|
||||
}
|
||||
async function disconnect() {
|
||||
clearInterval(mover);
|
||||
await Promise.all(clients.splice(0).map((c) => c.close()));
|
||||
for (let n = 0; n < 50; n++) {
|
||||
const m = await h.get("/api/metrics");
|
||||
if (m.players === 0) return;
|
||||
await delay(100);
|
||||
}
|
||||
throw Error("Players did not disconnect within 5 seconds");
|
||||
}
|
||||
try {
|
||||
let created = 0;
|
||||
for (const count of [1, 10, 100]) {
|
||||
while (created < count) {
|
||||
await h.control("world.create", {
|
||||
world: `test_bench_${String(created).padStart(3, "0")}`,
|
||||
template: "lobby",
|
||||
});
|
||||
created++;
|
||||
}
|
||||
await delay(1000);
|
||||
const phase = await sample(`idle_${count}_template_forks`, 3000);
|
||||
assert.equal(phase.players, 0);
|
||||
assert.equal(phase.active_worlds, 0);
|
||||
}
|
||||
await moving(10, false);
|
||||
await sample("10_moving_clients_shared_world");
|
||||
await disconnect();
|
||||
await moving(10, true);
|
||||
await sample("10_moving_clients_separate_worlds");
|
||||
await disconnect();
|
||||
await delay(1500);
|
||||
const idle = await sample("after_disconnect_idle", 3000);
|
||||
assert.equal(idle.players, 0);
|
||||
assert.equal(idle.active_worlds, 0);
|
||||
const active = phases.filter((p) => p.phase.includes("moving_clients"));
|
||||
const budgetResults = {
|
||||
rss_passed: active.every(
|
||||
(p) => p.rss_bytes.max <= budgets.ten_client_rss_bytes && p.process_high_water_bytes <= budgets.ten_client_rss_bytes,
|
||||
),
|
||||
tick_passed: active.every((p) => p.tick_ms.p95 <= budgets.p95_tick_ms),
|
||||
};
|
||||
const cacheSamples = samples.map((s) => {
|
||||
const storage = s.storage;
|
||||
return {
|
||||
entries: storage.cache_entries ?? storage.cache?.entries,
|
||||
capacity:
|
||||
storage.cache_capacity_sections ??
|
||||
storage.cache_capacity ??
|
||||
storage.cache_sections ??
|
||||
storage.cache?.capacity,
|
||||
};
|
||||
});
|
||||
const recognizedCache = cacheSamples.filter(
|
||||
(s) => Number.isFinite(s.entries) && Number.isFinite(s.capacity),
|
||||
);
|
||||
const cacheBounded =
|
||||
recognizedCache.length === cacheSamples.length &&
|
||||
recognizedCache.every((s) => s.entries <= s.capacity);
|
||||
const passed =
|
||||
budgetResults.rss_passed && budgetResults.tick_passed && cacheBounded;
|
||||
await h.report({
|
||||
passed,
|
||||
profile: h.binary.includes("/release/")
|
||||
? "release"
|
||||
: "custom binary; verify optimization separately",
|
||||
binary: h.binary,
|
||||
budgets,
|
||||
budget_results: budgetResults,
|
||||
cache_bounded: cacheBounded,
|
||||
workload: {
|
||||
fork_counts: [1, 10, 100],
|
||||
players: 10,
|
||||
input_hz: 20,
|
||||
sampling_interval_ms: 100,
|
||||
active_sample_ms: duration,
|
||||
template: "lobby",
|
||||
scope:
|
||||
"local seeded map; storage and authoritative movement; not full vanilla mechanics",
|
||||
},
|
||||
limitations: [
|
||||
"No comparison with Minecraft or a modded server",
|
||||
"Tick metrics measure server step work, not end-to-end latency",
|
||||
"Process RSS excludes browser clients and operating-system disk cache",
|
||||
"Cache entries remain bounded after disconnect; zero active worlds does not imply zero retained cache",
|
||||
],
|
||||
phases,
|
||||
samples,
|
||||
});
|
||||
reportWritten = true;
|
||||
assert.ok(
|
||||
passed,
|
||||
"Predetermined benchmark budget or bounded-cache check failed; inspect report",
|
||||
);
|
||||
} catch (error) {
|
||||
if (!reportWritten)
|
||||
await h.report({
|
||||
passed: false,
|
||||
budgets,
|
||||
error: error.stack,
|
||||
server_logs: h.logs(),
|
||||
phases,
|
||||
samples,
|
||||
});
|
||||
throw error;
|
||||
} finally {
|
||||
await disconnect().catch(() => {});
|
||||
await h.stop();
|
||||
}
|
||||
Reference in New Issue
Block a user