285b5b19e7
An ultracode investigation mapped the single-board work across five parallel readers and three adversarial reviewers. It found things this session would have walked into, and two of them are fixed here. **THE MERGED BOARD WAS SHIPPING WITH THREE FEEDS SILENTLY OFF.** Live ADS-B and live weather were gated on `id !== "california"` — "not the coarse statewide board", correct the day it was written, since live aircraft over a board at 1,919 m to the unit are a glyph problem and one station cannot speak for a thousand kilometres of coast. `cities/unify.ts` then built a board that is the whole state AND metro-detailed, keeping the `california` id deliberately so the fire gate, the ladder's region table and every `?city=` deep link keep working. It inherited a gate meant for something else. Measured before the fix: the `#source` badge read `""` on the merged board and `live weather · live traffic` on the Bay Area's. A defect that reads as "it feels less alive" and never as an error. `carriesMetroDetail(city)` asks the pack instead: `focusRegions` is the honest predicate and needs no new field, because the coarse state pack declares none and every pack with ground worth drawing at metro resolution declares one. **AND ASKING WAS NOT ENOUGH, BECAUSE THE FEEDS ARE PER-METRO.** With the gate fixed the board asked — and was refused: `GET /flights?lat=37.30&lng=-119.25& radiusNm=402 → 400 bad_request, "Nothing this deployment serves is near 37.3,-119.25"`. Correctly: `regionOf` derives its circle from the board's bounds, which on a statewide board is 402 nautical miles centred on the middle of the state, and what the deployment serves is San Francisco and the Southland, five hundred and sixty kilometres apart. `mergedTraffic` asks for both. In `adapters/http.ts` and not `engine/flights.ts` for the reason `TrafficSource` itself lives there: the engine draws darts at coordinates and has no business with provenance, and every interesting part of this merge is provenance. De-duplicated by id, because overlapping circles both see the aircraft between them and `flights.ts` measures a track's span from repeated observations — a duplicate is not merely a double image. `live()` is `some` and not `every`, so one dark metro does not make the other's observed traffic claim to be simulated. Verified: `?lat=37.77` → 200 live, 24 aircraft; `?lat=33.82` → 200 live, 15 aircraft; `#source` now reads `live traffic` on the merged board and stays `""` on the coarse one. **TWO INSTRUMENTS, BOTH BECAUSE THIS SESSION KEPT FAILING WITHOUT THEM.** `scripts/performance-budget.mjs` gains a `california-one` cell. Until now the only way to measure the merged board was to hand-edit the `california` cell's query, run, and edit it back — done six times in one session, which is exactly the procedure that gets half-done. Its `ready` asserts `#sea-section`, not just the signature chapter: `california-overview` is on the coarse board too, so a cell whose `?one=1` quietly stopped working would measure the coarse board and pass. No ports, no section, no readiness. Caps are RECORDED from its first run with headroom, in the same spirit as bay-area and socal — they were briefly copied from `california` and that is wrong for the same reason that cell's numbers are wrong for this board. **No existing cap was raised.** `scripts/look.mjs` gains `--lat/--lng/--standoff/--height`. Every aim in this harness goes through a control a reader also uses, which is right and stays the default — but it means a board can only be photographed where a chapter already points, and the merged board carries the state pack's six: the whole state, the north, two corridors, two doors. None is near a city. The board exists to put cities on the state and there was no way to photograph one; three attempts by clicking the minimap and guessing wheel notches landed in open ocean twice and on empty coast once. The seek moves the camera and nothing else. **A regression the new cell caught within one run.** The first version of the marker fix gated on bounds alone, like the office doors. The coarse state board's rectangle contains San Francisco, so it picked up forty-four company markers it has no business drawing at 1,919 m to the unit: 373 draw calls → 417. Now gated on `carriesMetroDetail` as well, and `california` measures 372,415 triangles / 373 draws — identical to before this commit. All twelve budget cells pass. 1,705 tests pass. Recorded for the next round, from the review: **SoCal's two focus rectangles overlap by 1.7 x 4.6 km** (verified: lat 34.075–34.090, lng −118.300–−118.250), so any per-rectangle terrain tier must clip them to a disjoint cover first or it draws that ground twice. Today's per-axis lattice is what hides it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
636 lines
34 KiB
JavaScript
Executable File
636 lines
34 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
/** Repeatable anonymous WebGL performance gate over a built Tera bundle. */
|
|
|
|
import { chromium } from "playwright";
|
|
import { createServer } from "node:http";
|
|
import { access, readdir, readFile, writeFile } from "node:fs/promises";
|
|
import { extname, join, normalize, resolve } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const ROOT = fileURLToPath(new URL("..", import.meta.url));
|
|
const DIST = join(ROOT, "dist");
|
|
const DEFAULT_BUDGETS = join(ROOT, "scripts", "performance-budgets.json");
|
|
const PRIVATE_PATH = /^\/api\/v1\/(?:media|realtime|presence|offices)(?:\/|$)|^\/assets\/(?:remoteMedia|presenceIndicator|scenePeers)-/;
|
|
const MIME = { ".html": "text/html; charset=utf-8", ".js": "text/javascript", ".css": "text/css", ".json": "application/json", ".png": "image/png", ".svg": "image/svg+xml", ".webp": "image/webp", ".webmanifest": "application/manifest+json" };
|
|
const VIEWPORTS = {
|
|
desktop: { width: 1440, height: 900, deviceScaleFactor: 1 },
|
|
mobile: { width: 390, height: 844, deviceScaleFactor: 2, isMobile: true, hasTouch: true },
|
|
};
|
|
/*
|
|
* EVERY SCENE NAMES THE BOARD IT MEANS, AND CHECKS IT GOT IT.
|
|
*
|
|
* `?city=` falls back to the first board rather than failing on an unknown id —
|
|
* deliberately, so a deep link to a city this build does not have shows a city
|
|
* instead of a black screen — and that fallback is silent. A `bay-area` cell
|
|
* that quietly measured California would pass its 2.6M-triangle cap by a factor
|
|
* of six and would keep passing through any regression a phone would choke on:
|
|
* a budget that cannot fail. So every scene names a `signature` — one chapter
|
|
* `data-view` that exists on that board and on no other — and does not become
|
|
* ready until it is on screen.
|
|
*
|
|
* A SIGNATURE CHAPTER RATHER THAN THE BOARD TAB, deliberately. The obvious check
|
|
* is the `data-board` of the pressed tab, and it was written that way first; it
|
|
* broke within the hour, because the tab strip is chrome and chrome is exactly
|
|
* what the continuity work is allowed to change. A chapter id is pack data, it
|
|
* is what `scripts/fixtures/chapter-identity.json` pins, and it is what every
|
|
* capture aim resolves to. `hayes-valley` and `dtla` are unique across the three
|
|
* packs; `all` is not, which is why neither metro is identified by its first
|
|
* chapter.
|
|
*
|
|
* `california-drive` used to reach its chapter with
|
|
* `document.querySelectorAll("#chapters .chapter")[1].click()`. That is an
|
|
* unguarded index into pack data, and it is the exact pattern that once shipped
|
|
* a share card of a chase camera on a freeway under the headline "Cities from
|
|
* above" — see the note at `scripts/brand-assets/capture.mjs:70`. It now aims at
|
|
* `data-view="la-sf-us-101"` and asserts the printed label, so a reordered pack
|
|
* stops the run instead of quietly measuring a different scene under the old
|
|
* scene's caps.
|
|
*/
|
|
const SCENES = {
|
|
california: {
|
|
host: "tera.lumbridgecorp.com",
|
|
// Explicit, and it was implicit until the app's default board moved off the
|
|
// state tier. `signature` would have caught it — a board with no
|
|
// `california-overview` chapter never becomes ready — but a harness that
|
|
// depends on an app default is a harness that reports someone else's change
|
|
// as its own flake.
|
|
query: "?city=california&handover=0",
|
|
signature: "california-overview",
|
|
ready: () => document.getElementById("boot")?.hidden === true && document.querySelector("#chapters .chapter[data-view='california-overview']") !== null,
|
|
},
|
|
"california-drive": {
|
|
host: "tera.lumbridgecorp.com",
|
|
query: "?city=california&handover=0",
|
|
signature: "california-overview",
|
|
ready: () => document.getElementById("boot")?.hidden === true && document.querySelector("#chapters .chapter[data-view='california-overview']") !== null && document.querySelector("#chapters .chapter[data-view='la-sf-us-101']") !== null,
|
|
activate: () => {
|
|
const button = document.querySelector("#chapters .chapter[data-view='la-sf-us-101']");
|
|
if (button === null) throw new Error("no chapter with data-view='la-sf-us-101' on this board");
|
|
const spans = [...button.querySelectorAll("span")];
|
|
const label = (spans[spans.length - 1]?.textContent ?? "").trim();
|
|
if (label !== "101") throw new Error(`chapter la-sf-us-101 prints "${label}", not "101" — the pack was re-labelled`);
|
|
button.click();
|
|
},
|
|
active: () => document.querySelector("[data-control-mode='drive']")?.getAttribute("aria-pressed") === "true" && document.getElementById("play-hud")?.hidden === false,
|
|
},
|
|
office: { host: "office.lumbridgecorp.com", query: "?handover=0", ready: () => document.getElementById("boot")?.hidden === true && document.getElementById("enter")?.textContent?.includes("Back to the city") === true },
|
|
/*
|
|
* The two metro boards, added after they became the boards with the most on
|
|
* them and were still the boards nothing measured.
|
|
*
|
|
* For a long time the matrix was california, california-drive and office. That
|
|
* was defensible while the metro boards were terrain and buildings, and stopped
|
|
* being defensible the moment SFO, LAX, the Golden Gate, the Bay Bridge and a
|
|
* surfaced freeway all landed on them — every one of those is city-frame
|
|
* geometry, and every one of them arrived in a frame with no budget watching
|
|
* it. A cap you do not measure is a cap you do not have.
|
|
*
|
|
* Their own numbers rather than California's: `sf` carries the densest built
|
|
* ground in the product and `socal` the widest basin, and holding either to a
|
|
* board tuned for a whole state would be arbitrary in both directions. These
|
|
* are set from the first measured run with headroom deliberately left, in the
|
|
* same spirit as the others.
|
|
*/
|
|
"bay-area": {
|
|
host: "tera.lumbridgecorp.com",
|
|
query: "?city=sf&handover=0",
|
|
signature: "hayes-valley",
|
|
ready: () => document.getElementById("boot")?.hidden === true && document.querySelector("#chapters .chapter[data-view='hayes-valley']") !== null,
|
|
},
|
|
/*
|
|
* One California — the merged board, behind `?one=1`.
|
|
*
|
|
* It needs a cell of its own, and until it had one the only way to measure it
|
|
* was to hand-edit the `california` cell's query, run, and edit it back. That
|
|
* was done six times in one session and is exactly the sort of procedure that
|
|
* gets half-done: the numbers quoted for this board came from a harness that
|
|
* was, at the time, lying about which board it measured.
|
|
*
|
|
* **`ready` asserts the merged board specifically, not just California.** The
|
|
* signature chapter cannot do it — `california-overview` is on the coarse
|
|
* state board too, so a cell whose `?one=1` silently stopped working would
|
|
* measure the coarse board and pass, which is the "a budget that cannot fail"
|
|
* failure this harness already has a comment about. `#sea-section` is the
|
|
* discriminator and it is a real one: the note is written only for a board
|
|
* that has ports, `california.ts` authors none, and `cities/unify.ts` folds in
|
|
* the Southland's two. No ports, no section, no readiness.
|
|
*
|
|
* The caps are RECORDED from the first measured run with headroom left, in the
|
|
* same spirit as `bay-area` and `socal` above — not copied from `california`,
|
|
* whose numbers describe a board with no cities on it.
|
|
*/
|
|
"california-one": {
|
|
host: "tera.lumbridgecorp.com",
|
|
query: "?city=california&one=1&handover=0",
|
|
signature: "california-overview",
|
|
ready: () =>
|
|
document.getElementById("boot")?.hidden === true &&
|
|
document.querySelector("#chapters .chapter[data-view='california-overview']") !== null &&
|
|
document.getElementById("sea-section") !== null,
|
|
},
|
|
socal: {
|
|
host: "tera.lumbridgecorp.com",
|
|
query: "?city=socal&handover=0",
|
|
signature: "dtla",
|
|
ready: () => document.getElementById("boot")?.hidden === true && document.querySelector("#chapters .chapter[data-view='dtla']") !== null,
|
|
},
|
|
};
|
|
|
|
const args = process.argv.slice(2);
|
|
function option(name, fallback) {
|
|
const at = args.indexOf(`--${name}`);
|
|
return at < 0 ? fallback : args[at + 1];
|
|
}
|
|
function positive(name, fallback) {
|
|
const value = Number(option(name, fallback));
|
|
if (!Number.isFinite(value) || value <= 0) throw new Error(`--${name} must be a positive number`);
|
|
return value;
|
|
}
|
|
/*
|
|
* WHY THE TWO METRO BOARDS CARRY CAPS THREE TIMES CALIFORNIA'S.
|
|
*
|
|
* Measured on the first run that included them: bay-area 2,265,056 triangles and
|
|
* socal 1,417,648, against california's 391,169. That is not a regression and it
|
|
* is not slack — it is what those boards are. California is one state at a
|
|
* standoff where a building is a speck; the Bay Area is the densest built ground
|
|
* in the product with every lot, the freeway network, both bridges and SFO in
|
|
* frame at once, and the Southland is the widest basin with LAX and five more
|
|
* fields on it. Holding either to a cap tuned for a whole state would be
|
|
* arbitrary in both directions.
|
|
*
|
|
* Both render at 60 fps (p95 16.7-16.8 ms) on the box that measured them, which
|
|
* has a Radeon RX 6700 XT. That is the honest limit of what these numbers prove.
|
|
*
|
|
* WHY THE MOBILE CELLS NOW CARRY THEIR OWN GEOMETRY CAPS.
|
|
*
|
|
* They used to carry desktop's, and a cap that cannot move is a cap that catches
|
|
* nothing. Until 2026-08-22 the handheld path reduced the pixel ratio and the
|
|
* shadow-map edge and reduced *nothing about the scene*: the mobile cell drew
|
|
* 2,263,784 triangles against desktop's 2,265,056 — a phone drew every triangle a
|
|
* desktop did — so the mobile row was a frame-time gate wearing a geometry gate's
|
|
* clothes, and it would have stayed green through any geometry regression a phone
|
|
* would choke on. `blocksCastShadow()` in `src/engine/blocks.ts` now takes the
|
|
* anonymous city out of the shadow caster set on a handheld, and these caps are
|
|
* set from what that reduced path actually produces, with the same headroom the
|
|
* desktop rows were given. Every one of them is a tightening — the mobile rows
|
|
* used to carry desktop's numbers, so all ten moved down. Measured 2026-08-22:
|
|
*
|
|
* | cell | triangles before | after | cap | draws | cap |
|
|
* |---|---|---|---|---|---|
|
|
* | bay-area/mobile | 2,263,784 | 1,266,176 | 1,450,000 | 201 | 240 |
|
|
* | socal/mobile | 1,410,800 | 765,596 | 900,000 | 137 | 170 |
|
|
* | california/mobile | 389,843 | 389,843 | 460,000 | 368 | 420 |
|
|
* | california-drive/mobile | 371,599 | 371,599 | 440,000 | 232 | 280 |
|
|
* | office/mobile | 143,780 | 143,780 | 200,000 | 431 | 520 |
|
|
*
|
|
* California does not move because its 806 m lots were never in the caster set
|
|
* (see `NEIGHBOURHOOD_LOT_METRES`), and the office does not move because a desk
|
|
* shadow at 1 unit = 1 m is the whole read of depth.
|
|
*
|
|
* ONE CORRECTION WORTH LEAVING HERE, because the plan for this work carried it:
|
|
* the shadow pass on bay-area is ~70 draw calls, and those 70 are NOT the
|
|
* buildings. The anonymous city is one `InstancedMesh`, so taking it out of the
|
|
* caster set is worth 997,608 triangles and exactly ONE draw call. The other 69
|
|
* are the landmarks, the bridges, the surfaced freeway and the airports — every
|
|
* one of them a separate mesh, and every one of them a silhouette somebody put
|
|
* there on purpose. Dropping them too would get the cell under 150 draws and
|
|
* would be a visual regression bought with a number. Triangles were the lever;
|
|
* draw calls were never going to be.
|
|
*
|
|
* AND THE MOBILE CELL'S PIXEL COUNT IS 0.74 MP, NOT 1.32.
|
|
*
|
|
* This file used to claim the mobile cell rendered "1.32 MP against desktop's
|
|
* 1.30", which was arithmetic on the CSS size times `deviceScaleFactor: 2` and
|
|
* was never what the renderer did. Measured in-page — `renderer.getPixelRatio()`
|
|
* is 1.5 and the canvas backing store is 585x1266 — the mobile cell renders
|
|
* **0.74 MP, 57% of desktop's 1.30**, because `deviceProfile()` caps the handheld
|
|
* pixel ratio at 1.5. Every cell now records its own `measuredPixels`, so nobody
|
|
* has to take a comment's word for it again.
|
|
*
|
|
* ITEM 8 IS CLOSED: THE BAY AREA ALLOWANCE IS GONE AND THE SUSPECT WAS WRONG.
|
|
*
|
|
* `bay-area.desktop.p95FrameIntervalMs` carried 33.4 as a recorded defect, with
|
|
* the desktop shadow map named as the first thing to rule in or out. It is ruled
|
|
* OUT, by measurement rather than by argument. On the same build in one session,
|
|
* timed with `EXT_disjoint_timer_query_webgl2`: a 4096-texel map renders the
|
|
* bay-area frame in 1.31 ms, 2048 in 1.22, 1024 in 1.21 and 256 in 1.26 — the
|
|
* whole spread is inside the run-to-run noise of a 1.2 ms frame, because the
|
|
* shadow pass costs geometry submission and not rasterisation.
|
|
*
|
|
* What did correlate is the card. Sampling `/sys/class/drm/card1/device/pp_dpm_sclk`
|
|
* and `pp_dpm_mclk` every 250 ms through a session that reproduced p95 33.3, the
|
|
* GPU never left its lowest DPM states — 500 MHz core out of 2725, and 96 or
|
|
* 456 MHz memory out of 1000 — for every one of ~250 samples. A board that needs
|
|
* 1.2 ms at boost needs an order of magnitude more at 18% of core clock, which
|
|
* puts it near the 16.7 ms deadline and makes the miss a coin flip on when the
|
|
* DPM state machine steps. That is why the drop rate is stochastic, why it scales
|
|
* with board weight (bay-area > socal > california = never), and why two
|
|
* consecutive runs over a byte-identical `dist` gave socal desktop 33.4 then 16.7
|
|
* with triangle and draw counts identical to the digit.
|
|
*
|
|
* So two things changed here rather than the cap being left raised:
|
|
*
|
|
* - Every cell is measured up to `--repeats` times and the first passing run is
|
|
* the one recorded, with all attempts kept in `attempts[]`. A single red p95
|
|
* on this class of box is noise until it reproduces; a geometry regression
|
|
* reproduces on the first attempt and every attempt after it.
|
|
* - The report records the GPU's DPM state next to the renderer string, so the
|
|
* next person reading a red frame-time cell can see whether the card was
|
|
* awake. The confirming experiment, if you have root:
|
|
* `echo high > /sys/class/drm/card1/device/power_dpm_force_performance_level`.
|
|
*
|
|
* Frame times on a shared box are not trustworthy from a single run. Triangle and
|
|
* draw-call counts are: they were identical to the digit across every run of a
|
|
* given build in every experiment above. Believe the geometry columns; re-run
|
|
* before believing a frame-time column.
|
|
*/
|
|
function sceneBudget(value, label) {
|
|
if (!value || typeof value !== "object") throw new Error(`missing budget for ${label}`);
|
|
for (const key of ["p95FrameIntervalMs", "maxDrawCalls", "maxTriangles"]) {
|
|
if (typeof value[key] !== "number" || !Number.isFinite(value[key]) || value[key] <= 0) {
|
|
throw new Error(`${label}.${key} must be a positive number`);
|
|
}
|
|
}
|
|
return value;
|
|
}
|
|
const outputPath = resolve(option("output", "/tmp/tera-performance-budget.json"));
|
|
const budgetPath = resolve(option("budgets", DEFAULT_BUDGETS));
|
|
const warmupMs = positive("warmup-ms", 3_000);
|
|
const sampleMs = positive("sample-ms", 8_000);
|
|
const readyTimeoutMs = positive("ready-timeout-ms", 180_000);
|
|
const softwareOnly = args.includes("--software");
|
|
/*
|
|
* How many times a cell may be measured before its result is believed.
|
|
*
|
|
* See the DPM paragraph above. The first attempt that passes is the recorded
|
|
* one; if none passes, the last is recorded and every attempt is kept in
|
|
* `attempts[]` so a reader can see whether a red cell was one bad run or a
|
|
* property of the build. Three because two consecutive runs over a
|
|
* byte-identical `dist` have been observed to disagree by a factor of two on
|
|
* frame time while agreeing to the digit on triangles and draw calls.
|
|
*/
|
|
const repeats = Math.max(1, Math.round(positive("repeats", 3)));
|
|
// Chrome exposes rAF timestamps at 0.1 ms precision, so an ideal 60 Hz cadence
|
|
// can quantize to 16.8 ms. This tolerance is measurement resolution, not budget
|
|
// headroom; reported values and declared budgets remain unchanged.
|
|
const FRAME_COMPARISON_EPSILON_MS = 0.1;
|
|
|
|
function percentile(values, fraction) {
|
|
if (values.length === 0) return null;
|
|
const ordered = [...values].sort((a, b) => a - b);
|
|
return ordered[Math.max(0, Math.ceil(ordered.length * fraction) - 1)];
|
|
}
|
|
const rounded = (value) => value === null ? null : Math.round(value * 1_000) / 1_000;
|
|
|
|
async function serve() {
|
|
const requests = [];
|
|
const server = createServer(async (req, res) => {
|
|
const url = new URL(req.url ?? "/", "http://local.invalid");
|
|
requests.push({ method: req.method ?? "GET", path: url.pathname });
|
|
if (url.pathname === "/api/v1/health") {
|
|
res.writeHead(200, { "content-type": "application/json", "cache-control": "no-store" });
|
|
res.end(JSON.stringify({ auth: { mode: "jwt", entryUrl: "/login.html" }, sources: { weather: "none", flights: "none", satellites: "none", markers: "none" } }));
|
|
return;
|
|
}
|
|
if (url.pathname === "/api/v1/session") {
|
|
res.writeHead(200, { "content-type": "application/json", "cache-control": "no-store" });
|
|
res.end(JSON.stringify({ authenticated: false, subject: null, passwordLogin: false, admin: false }));
|
|
return;
|
|
}
|
|
if (url.pathname.startsWith("/api/v1/")) {
|
|
res.writeHead(404, { "content-type": "application/json", "cache-control": "no-store" });
|
|
res.end(JSON.stringify({ error: "not_found" }));
|
|
return;
|
|
}
|
|
const requested = normalize(decodeURIComponent(url.pathname)).replace(/^(?:\.\.[/\\])+/, "");
|
|
for (const relative of [requested === "/" ? "/index.html" : requested, "/index.html"]) {
|
|
const target = resolve(DIST, `.${relative}`);
|
|
if (!target.startsWith(`${resolve(DIST)}/`)) continue;
|
|
try {
|
|
const body = await readFile(target);
|
|
res.writeHead(200, { "content-type": MIME[extname(target)] ?? "application/octet-stream" });
|
|
res.end(body);
|
|
return;
|
|
} catch { /* SPA fallback */ }
|
|
}
|
|
res.writeHead(404).end("not found");
|
|
});
|
|
await new Promise((ok, fail) => { server.once("error", fail); server.listen(0, "127.0.0.1", ok); });
|
|
const address = server.address();
|
|
if (!address || typeof address === "string") throw new Error("performance server did not expose a TCP port");
|
|
return { server, requests, port: address.port };
|
|
}
|
|
|
|
/*
|
|
* What the GPU's power management was doing while this ran.
|
|
*
|
|
* Recorded next to the renderer string because a red frame-time cell on this
|
|
* class of box is very often a downclocked card rather than a heavy scene: a
|
|
* Radeon sitting at 500 MHz of a possible 2725 needs an order of magnitude more
|
|
* time for a frame it renders in 1.2 ms at boost, which is enough to put a
|
|
* healthy board near the 16.7 ms deadline. amdgpu marks the active state with a
|
|
* trailing `*` in `pp_dpm_sclk` / `pp_dpm_mclk`. Best-effort and Linux-only —
|
|
* every read is allowed to fail, and a machine without these files simply
|
|
* records `null`, because a missing instrument must never fail a build.
|
|
*/
|
|
async function gpuPowerState() {
|
|
try {
|
|
const cards = (await readdir("/sys/class/drm")).filter((name) => /^card\d+$/.test(name)).sort();
|
|
const states = [];
|
|
for (const card of cards) {
|
|
const base = `/sys/class/drm/${card}/device`;
|
|
const read = async (leaf) => {
|
|
try { return (await readFile(`${base}/${leaf}`, "utf8")).trim(); } catch { return null; }
|
|
};
|
|
const active = (table) => {
|
|
if (!table) return null;
|
|
const line = table.split("\n").find((row) => row.trimEnd().endsWith("*"));
|
|
return line ? line.replace(/^\s*\d+:\s*/, "").replace(/\s*\*$/, "").trim() : null;
|
|
};
|
|
const ceiling = (table) => {
|
|
if (!table) return null;
|
|
const rows = table.split("\n").filter(Boolean);
|
|
const last = rows[rows.length - 1];
|
|
return last ? last.replace(/^\s*\d+:\s*/, "").replace(/\s*\*$/, "").trim() : null;
|
|
};
|
|
const sclk = await read("pp_dpm_sclk");
|
|
const mclk = await read("pp_dpm_mclk");
|
|
if (sclk === null && mclk === null) continue;
|
|
states.push({
|
|
card,
|
|
forcePerformanceLevel: await read("power_dpm_force_performance_level"),
|
|
coreClock: active(sclk), coreClockCeiling: ceiling(sclk),
|
|
memoryClock: active(mclk), memoryClockCeiling: ceiling(mclk),
|
|
});
|
|
}
|
|
return states.length ? states : null;
|
|
} catch { return null; }
|
|
}
|
|
|
|
const COMMON_ARGS = ["--no-sandbox", "--disable-dev-shm-usage"];
|
|
async function rendererOf(browser) {
|
|
const page = await browser.newPage();
|
|
try {
|
|
await page.goto("about:blank");
|
|
return await page.evaluate(() => {
|
|
const gl = document.createElement("canvas").getContext("webgl2");
|
|
const extension = gl?.getExtension("WEBGL_debug_renderer_info");
|
|
return extension ? String(gl.getParameter(extension.UNMASKED_RENDERER_WEBGL)) : null;
|
|
});
|
|
} finally { await page.close(); }
|
|
}
|
|
async function launchBrowser(port) {
|
|
const resolver = `--host-resolver-rules=MAP tera.lumbridgecorp.com 127.0.0.1, MAP office.lumbridgecorp.com 127.0.0.1`;
|
|
const attempts = softwareOnly
|
|
? [["swiftshader", ["--use-gl=angle", "--use-angle=swiftshader"]]]
|
|
: [["vulkan", ["--use-gl=angle", "--use-angle=vulkan"]], ["swiftshader", ["--use-gl=angle", "--use-angle=swiftshader"]]];
|
|
let last;
|
|
for (const [backend, flags] of attempts) {
|
|
try {
|
|
const browser = await chromium.launch({ channel: "chrome", args: [...COMMON_ARGS, resolver, ...flags] });
|
|
const renderer = await rendererOf(browser);
|
|
const softwareRenderer = renderer !== null && /SwiftShader|llvmpipe/i.test(renderer);
|
|
if (renderer && (backend === "swiftshader" || !softwareRenderer)) return { browser, backend, renderer, port };
|
|
await browser.close();
|
|
} catch (error) { last = error; }
|
|
}
|
|
for (const executablePath of ["/usr/bin/google-chrome", "/usr/bin/google-chrome-stable", "/usr/bin/chromium", "/usr/bin/chromium-browser"]) {
|
|
try {
|
|
await access(executablePath);
|
|
const browser = await chromium.launch({ executablePath, args: [...COMMON_ARGS, resolver, "--use-gl=angle", "--use-angle=swiftshader"] });
|
|
return { browser, backend: "system-chrome-swiftshader", renderer: await rendererOf(browser), port };
|
|
} catch (error) { last = error; }
|
|
}
|
|
throw new Error(`Chrome launch failed: ${last instanceof Error ? last.message : String(last)}`);
|
|
}
|
|
|
|
function instrumentation() {
|
|
// Custom production hostnames mapped to loopback are not secure contexts over
|
|
// HTTP, so Chrome withholds randomUUID even though getRandomValues remains.
|
|
// Production is HTTPS; this shim only restores that API in the local harness.
|
|
if (typeof crypto.randomUUID !== "function") {
|
|
crypto.randomUUID = () => {
|
|
const bytes = crypto.getRandomValues(new Uint8Array(16));
|
|
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
|
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
const hex = [...bytes].map((value) => value.toString(16).padStart(2, "0")).join("");
|
|
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
};
|
|
}
|
|
const state = { frames: [], longTasks: [], drawCalls: [], triangles: [], currentCalls: 0, currentTriangles: 0, last: 0, longTaskSupported: false };
|
|
Object.defineProperty(globalThis, "__teraPerformanceBudget", { value: state });
|
|
const triangleCount = (mode, count) => mode === 4 ? count / 3 : (mode === 5 || mode === 6) ? Math.max(0, count - 2) : 0;
|
|
const patch = (prototype, method, countAt, instancesAt = null) => {
|
|
if (!prototype || typeof prototype[method] !== "function") return;
|
|
const original = prototype[method];
|
|
if (original.__teraPerformancePatched) return;
|
|
const wrapped = function (...values) {
|
|
state.currentCalls += 1;
|
|
const instances = instancesAt === null ? 1 : Number(values[instancesAt]) || 0;
|
|
state.currentTriangles += triangleCount(Number(values[0]), Number(values[countAt]) || 0) * instances;
|
|
return original.apply(this, values);
|
|
};
|
|
Object.defineProperty(wrapped, "__teraPerformancePatched", { value: true });
|
|
prototype[method] = wrapped;
|
|
};
|
|
for (const prototype of [globalThis.WebGLRenderingContext?.prototype, globalThis.WebGL2RenderingContext?.prototype]) {
|
|
patch(prototype, "drawArrays", 2);
|
|
patch(prototype, "drawElements", 1);
|
|
patch(prototype, "drawArraysInstanced", 2, 3);
|
|
patch(prototype, "drawElementsInstanced", 1, 4);
|
|
}
|
|
try {
|
|
new PerformanceObserver((list) => state.longTasks.push(...list.getEntries().map((entry) => ({ startTime: entry.startTime, duration: entry.duration })))).observe({ type: "longtask", buffered: true });
|
|
state.longTaskSupported = true;
|
|
} catch { /* Long Tasks API is optional. */ }
|
|
requestAnimationFrame(function sample(now) {
|
|
if (state.last > 0) {
|
|
state.frames.push(now - state.last);
|
|
state.drawCalls.push(state.currentCalls);
|
|
state.triangles.push(state.currentTriangles);
|
|
}
|
|
state.last = now;
|
|
state.currentCalls = 0;
|
|
state.currentTriangles = 0;
|
|
requestAnimationFrame(sample);
|
|
});
|
|
}
|
|
|
|
async function measure(browser, port, sceneName, viewportName, budget, requestLog) {
|
|
const viewport = VIEWPORTS[viewportName];
|
|
/*
|
|
* `reducedMotion: "reduce"` because the harness measures a *settled* frame.
|
|
*
|
|
* The opening arrival — `arrive()` in `scene.ts`, and `flyTo` before it —
|
|
* collapses to a cut under the media query, exactly as a viewer who has asked
|
|
* their operating system for less motion gets. Without it the shot's length
|
|
* has to be chosen against `warmup-ms`, since a move still running inside the
|
|
* sample window is a moving frustum and everything downstream of it is
|
|
* measuring a fly-in rather than a board. With it the two are decoupled: the
|
|
* arrival can be as long as it wants to be and this file still samples a
|
|
* stationary camera. Requested by whoever owns the hero landing; if you raise
|
|
* `warmup-ms`, tell them, because the shot can then be lengthened.
|
|
*/
|
|
const context = await browser.newContext({ viewport: { width: viewport.width, height: viewport.height }, deviceScaleFactor: viewport.deviceScaleFactor, isMobile: viewport.isMobile, hasTouch: viewport.hasTouch, reducedMotion: "reduce" });
|
|
const page = await context.newPage();
|
|
const consoleErrors = [];
|
|
page.on("pageerror", (error) => consoleErrors.push(String(error)));
|
|
page.on("console", (message) => { if (message.type() === "error") consoleErrors.push(message.text()); });
|
|
const before = requestLog.length;
|
|
await page.addInitScript(instrumentation);
|
|
const scene = SCENES[sceneName];
|
|
const url = `http://${scene.host}:${port}/${scene.query ?? ""}`;
|
|
try {
|
|
await page.goto(url, { waitUntil: "networkidle", timeout: readyTimeoutMs });
|
|
try {
|
|
await page.waitForFunction(scene.ready, null, { timeout: readyTimeoutMs });
|
|
} catch (error) {
|
|
const state = await page.evaluate(() => ({
|
|
title: document.title,
|
|
bootHidden: document.getElementById("boot")?.hidden ?? null,
|
|
bootText: document.getElementById("boot")?.textContent?.trim().slice(0, 240) ?? null,
|
|
chapters: document.querySelectorAll("#chapters .chapter").length,
|
|
// Which chapters actually answered. A readiness timeout on a metro cell
|
|
// is now most likely to mean `?city=` fell back to California, and
|
|
// reading the ids back makes that a one-word diagnosis rather than a
|
|
// twenty-minute one. `activeBoard` is best-effort: the board tab strip
|
|
// is chrome and may not exist.
|
|
views: [...document.querySelectorAll("#chapters .chapter")].map((node) => node.getAttribute("data-view")),
|
|
activeBoard: document.querySelector("[data-board][aria-pressed='true']")?.getAttribute("data-board") ?? null,
|
|
enter: document.getElementById("enter")?.textContent?.trim() ?? null,
|
|
})).catch(() => null);
|
|
throw new Error(`scene readiness timed out: ${JSON.stringify({ state, consoleErrors, requests: requestLog.slice(before) })}`, { cause: error });
|
|
}
|
|
if (scene.activate) {
|
|
await page.evaluate(scene.activate);
|
|
await page.waitForFunction(scene.active, null, { timeout: readyTimeoutMs });
|
|
}
|
|
await page.waitForTimeout(warmupMs);
|
|
await page.evaluate(() => {
|
|
const state = globalThis.__teraPerformanceBudget;
|
|
state.frames.length = state.drawCalls.length = state.triangles.length = state.longTasks.length = 0;
|
|
state.last = performance.now(); state.currentCalls = 0; state.currentTriangles = 0;
|
|
});
|
|
await page.waitForTimeout(sampleMs);
|
|
const raw = await page.evaluate(() => {
|
|
const state = globalThis.__teraPerformanceBudget;
|
|
const canvas = document.querySelector("canvas");
|
|
const gl = canvas?.getContext("webgl2") ?? canvas?.getContext("webgl");
|
|
const extension = gl?.getExtension("WEBGL_debug_renderer_info");
|
|
return {
|
|
frames: [...state.frames], drawCalls: [...state.drawCalls], triangles: [...state.triangles],
|
|
longTasks: [...state.longTasks], longTaskSupported: state.longTaskSupported,
|
|
renderer: extension ? String(gl.getParameter(extension.UNMASKED_RENDERER_WEBGL)) : null,
|
|
// The canvas backing store, which is the only honest pixel count: the
|
|
// renderer caps its own pixel ratio (1.5 on a handheld) and does not
|
|
// care what `deviceScaleFactor` the harness asked for. See the header.
|
|
drawingBuffer: canvas ? { width: canvas.width, height: canvas.height, cssWidth: canvas.clientWidth, cssHeight: canvas.clientHeight } : null,
|
|
};
|
|
});
|
|
const relevantRequests = requestLog.slice(before);
|
|
const privateRequests = relevantRequests.filter((request) => PRIVATE_PATH.test(request.path));
|
|
const metrics = {
|
|
frameSamples: raw.frames.length,
|
|
p50FrameIntervalMs: rounded(percentile(raw.frames, 0.5)),
|
|
p95FrameIntervalMs: rounded(percentile(raw.frames, 0.95)),
|
|
p99FrameIntervalMs: rounded(percentile(raw.frames, 0.99)),
|
|
maxFrameIntervalMs: rounded(raw.frames.length ? Math.max(...raw.frames) : null),
|
|
p95DrawCalls: rounded(percentile(raw.drawCalls, 0.95)),
|
|
maxDrawCalls: raw.drawCalls.length ? Math.max(...raw.drawCalls) : null,
|
|
p95Triangles: rounded(percentile(raw.triangles, 0.95)),
|
|
maxTriangles: raw.triangles.length ? Math.max(...raw.triangles) : null,
|
|
longTasksSupported: raw.longTaskSupported,
|
|
longTaskCount: raw.longTasks.length,
|
|
longTaskTotalMs: raw.longTasks.reduce((sum, task) => sum + task.duration, 0),
|
|
};
|
|
const checks = {
|
|
p95FrameIntervalMs: metrics.p95FrameIntervalMs !== null &&
|
|
Math.round(metrics.p95FrameIntervalMs * 10) <=
|
|
Math.round((budget.p95FrameIntervalMs + FRAME_COMPARISON_EPSILON_MS) * 10),
|
|
maxDrawCalls: metrics.maxDrawCalls !== null && metrics.maxDrawCalls <= budget.maxDrawCalls,
|
|
maxTriangles: metrics.maxTriangles !== null && metrics.maxTriangles <= budget.maxTriangles,
|
|
anonymousPrivateRequests: privateRequests.length === 0,
|
|
consoleErrors: consoleErrors.length === 0,
|
|
enoughFrameSamples: metrics.frameSamples >= Math.max(30, Math.floor(sampleMs / 100)),
|
|
};
|
|
const measuredPixels = raw.drawingBuffer
|
|
? {
|
|
...raw.drawingBuffer,
|
|
megapixels: Math.round((raw.drawingBuffer.width * raw.drawingBuffer.height) / 10_000) / 100,
|
|
}
|
|
: null;
|
|
return { scene: sceneName, viewport: viewportName, url, viewportPixels: viewport, measuredPixels, renderer: raw.renderer, budget, metrics, checks, passed: Object.values(checks).every(Boolean), privateRequests, consoleErrors };
|
|
} finally { await context.close(); }
|
|
}
|
|
|
|
let server;
|
|
let browser;
|
|
try {
|
|
await access(join(DIST, "index.html"));
|
|
const budgets = JSON.parse(await readFile(budgetPath, "utf8"));
|
|
const hosted = await serve(); server = hosted.server;
|
|
const launched = await launchBrowser(hosted.port); browser = launched.browser;
|
|
const results = [];
|
|
const gpuBefore = await gpuPowerState();
|
|
for (const scene of Object.keys(SCENES)) for (const viewport of Object.keys(VIEWPORTS)) {
|
|
const budget = sceneBudget(budgets?.scenes?.[scene]?.[viewport], `${scene}.${viewport}`);
|
|
/*
|
|
* Repeat until stable, and record why.
|
|
*
|
|
* The first attempt that passes is the recorded result. A cell that never
|
|
* passes records its LAST attempt — not its best — because picking the
|
|
* kindest of three runs is how a gate stops meaning anything, and the point
|
|
* of the retry is to distinguish "this box stuttered once" from "this build
|
|
* is slow", not to shop for a green.
|
|
*
|
|
* `attempts[]` keeps every run's headline numbers either way. In practice a
|
|
* geometry regression is identical across all three attempts (triangles and
|
|
* draw calls have never disagreed between runs of one build) and a
|
|
* downclocked card is the only thing that moves, which is exactly the
|
|
* signal this is here to separate.
|
|
*/
|
|
const attempts = [];
|
|
let accepted = null;
|
|
for (let attempt = 1; attempt <= repeats; attempt++) {
|
|
process.stderr.write(`performance-budget: ${scene}/${viewport}${attempt > 1 ? ` (attempt ${attempt}/${repeats})` : ""}\n`);
|
|
const run = await measure(browser, hosted.port, scene, viewport, budget, hosted.requests);
|
|
attempts.push({
|
|
attempt,
|
|
passed: run.passed,
|
|
failed: Object.entries(run.checks).filter(([, ok]) => !ok).map(([name]) => name),
|
|
frameSamples: run.metrics.frameSamples,
|
|
p50FrameIntervalMs: run.metrics.p50FrameIntervalMs,
|
|
p95FrameIntervalMs: run.metrics.p95FrameIntervalMs,
|
|
maxTriangles: run.metrics.maxTriangles,
|
|
maxDrawCalls: run.metrics.maxDrawCalls,
|
|
});
|
|
accepted = run;
|
|
if (run.passed) break;
|
|
}
|
|
results.push({ ...accepted, attempts, attemptsRun: attempts.length, repeatsAllowed: repeats });
|
|
}
|
|
const report = {
|
|
schemaVersion: 1, generatedAt: new Date().toISOString(), browserPlugin: "not available; Playwright system Chrome fallback used",
|
|
browser: { backend: launched.backend, renderer: launched.renderer }, warmupMs, sampleMs, repeats,
|
|
/*
|
|
* The card's own power state, before and after the matrix. A frame-time cell
|
|
* measured against a GPU pinned at its lowest DPM step is measuring the
|
|
* power manager; see the header. `power_dpm_force_performance_level: "auto"`
|
|
* with a core clock far below its ceiling is the signature.
|
|
*/
|
|
gpu: { before: gpuBefore, after: await gpuPowerState() },
|
|
frameComparisonEpsilonMs: FRAME_COMPARISON_EPSILON_MS,
|
|
budgets: budgetPath, passed: results.every((result) => result.passed), results,
|
|
};
|
|
await writeFile(outputPath, `${JSON.stringify(report, null, 2)}\n`);
|
|
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
process.stderr.write(`performance-budget: ${report.passed ? "PASS" : "FAIL"} — ${outputPath}\n`);
|
|
if (!report.passed) process.exitCode = 1;
|
|
} catch (error) {
|
|
console.error(`performance-budget: ERROR — ${error instanceof Error ? error.stack : String(error)}`);
|
|
process.exitCode = 2;
|
|
} finally {
|
|
await browser?.close().catch(() => undefined);
|
|
if (server) await new Promise((resolveClose) => server.close(resolveClose));
|
|
}
|