/** * What one pose actually costs, in triangles and draw calls. * * `scripts/performance-budget.mjs` measures the poses a board is *judged* on and * refuses to move its caps. That is the right instrument for "did this change * regress the product" and the wrong one for "how much room is there at the * bottom of the descent" — it never goes there, because no budget cell does. * * node scripts/cost-at.mjs --url "/?city=california&one=1" \ * --lat 37.7897 --lng -122.3972 --standoffs 200000,60000,20000,7700,2000 * * The instrumentation is `performance-budget.mjs`'s, deliberately: the same four * patched GL entry points and the same triangle arithmetic, so a number from * here is comparable with a number from there rather than nearly comparable. * * Camera aiming goes through `__teraCamera.seek`, which takes true metres, so * one stand-off list reads the same on a 94 m board and a 1,919 m one. */ import { spawn } from "node:child_process"; import { readFileSync } from "node:fs"; import { createServer } from "node:net"; import { chromium } from "playwright"; const args = process.argv.slice(2); const flag = (name, fallback) => { const at = args.indexOf(name); return at < 0 ? fallback : args[at + 1]; }; const PORT = await new Promise((resolve, reject) => { const probe = createServer(); probe.once("error", reject); probe.listen(0, "127.0.0.1", () => { const { port } = probe.address(); probe.close(() => resolve(port)); }); }); const server = spawn( new URL("../node_modules/.bin/vite", import.meta.url).pathname, ["preview", "--port", String(PORT), "--strictPort"], { detached: true, stdio: ["ignore", "pipe", "pipe"] }, ); const shutdown = () => { try { process.kill(-server.pid, "SIGTERM"); } catch { /* already gone */ } }; process.on("exit", shutdown); const bound = await new Promise((resolve) => { let seen = ""; const settle = setTimeout(() => resolve(null), 30000); server.stdout.on("data", (c) => { seen += String(c); const m = /http:\/\/(?:localhost|127\.0\.0\.1):(\d+)/.exec(seen); if (m) { clearTimeout(settle); resolve(Number(m[1])); } }); server.stderr.on("data", (c) => { seen += String(c); }); }); if (bound !== PORT) { console.error(`cost-at: preview bound ${bound}, not ${PORT}`); process.exit(1); } { const served = await fetch(`http://localhost:${PORT}/index.html`).then((r) => r.text()); const onDisk = readFileSync(new URL("../dist/index.html", import.meta.url), "utf8"); const bundle = (html) => /src="([^"]*\/assets\/[^"]+\.js)"/.exec(html)?.[1] ?? null; if (bundle(served) === null || bundle(served) !== bundle(onDisk)) { console.error("cost-at: that port is not serving this dist/"); process.exit(1); } } const browser = await chromium.launch({ channel: "chrome", args: ["--use-gl=angle", "--use-angle=vulkan", "--enable-unsafe-swiftshader", "--ignore-gpu-blocklist"], }); const page = await browser.newPage({ viewport: { width: 1600, height: 1000 } }); /** `performance-budget.mjs`'s counters, restated so the numbers are comparable. */ await page.addInitScript(() => { const state = { calls: 0, tris: 0 }; globalThis.__teraCost = state; const triangleCount = (mode, count) => mode === 4 ? count / 3 : mode === 5 || mode === 6 ? Math.max(0, count - 2) : 0; const patch = (proto, countAt, instancesAt = null) => (method) => { if (!proto) return; const original = proto[method]; if (!original) return; proto[method] = function (...v) { state.calls += 1; const instances = instancesAt === null ? 1 : Number(v[instancesAt]) || 0; state.tris += triangleCount(Number(v[0]), Number(v[countAt]) || 0) * instances; return original.apply(this, v); }; }; for (const proto of [globalThis.WebGLRenderingContext?.prototype, globalThis.WebGL2RenderingContext?.prototype]) { patch(proto, 2)("drawArrays"); patch(proto, 1)("drawElements"); patch(proto, 2, 3)("drawArraysInstanced"); patch(proto, 1, 4)("drawElementsInstanced"); } }); await page.goto(`http://localhost:${PORT}${flag("--url", "/")}`, { waitUntil: "networkidle", timeout: 60000 }); await page.waitForTimeout(Number(flag("--wait", "11000"))); try { await page.getByText(/^Skip$/).first().click({ timeout: 2500 }); await page.waitForTimeout(1000); } catch { /* not shown */ } const lat = Number(flag("--lat", "37.7897")); const lng = Number(flag("--lng", "-122.3972")); const standoffs = String(flag("--standoffs", "200000,60000,20000,7700,2000")).split(",").map(Number); console.log(`cost-at: ${flag("--url", "/")} at ${lat}, ${lng}`); console.log(" standoff triangles draws lots lot"); for (const standoffM of standoffs) { const placed = await page.evaluate((pose) => { const cam = globalThis.__teraCamera; if (!cam || typeof cam.seek !== "function") return null; return { board: cam.board, ...cam.seek(pose) }; }, { lat, lng, standoffM }); if (placed === null) { console.error("cost-at: no __teraCamera hook on this build"); break; } // Let the detail LOD settle: the repack is driven from the frame loop and a // measurement taken on the frame the camera moved is a measurement of the // previous pose. await page.waitForTimeout(2500); // One frame's worth, sampled over several so a single odd frame cannot decide it. const sample = await page.evaluate(async () => { const s = globalThis.__teraCost; const frames = []; for (let i = 0; i < 12; i++) { const c0 = s.calls, t0 = s.tris; await new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r))); frames.push({ calls: s.calls - c0, tris: s.tris - t0 }); } frames.sort((a, b) => a.tris - b.tris); return frames[Math.floor(frames.length / 2)]; }); /* * What the city layer itself is drawing, when the board offers the readout. * * A triangle total cannot answer "did the board coarsen": at these poses the * terrain, the water and the roads are more than half of it, so a total that * fell could be either. `__teraCamera.lots()` reads the packed instance count * and the lot size off the mesh instead. Older builds have no such hook and * print blanks. */ const lots = await page.evaluate(() => globalThis.__teraCamera?.lots?.() ?? null); const km = (standoffM / 1000).toFixed(1).padStart(8); const packed = lots === null ? "" : String(lots.packed); const lot = lots === null || lots.lotMetres === null ? "" : `${lots.lotMetres} m`; console.log( ` ${km} km ${String(Math.round(sample.tris / 2)).padStart(9)} ${String(Math.round(sample.calls / 2)).padStart(5)}` + ` ${packed.padStart(8)} ${lot.padStart(5)}`, ); } await browser.close(); shutdown();