Files
aego/tools/websmoke/traffic.mjs
T
sanspie a6822156cd
ci-platforms / windows amd64 smoke (push) Skipped
ci / release (push) Successful in 8m58s
ci-platforms / darwin amd64 smoke (push) Skipped
ci-platforms / darwin arm64 smoke (push) Skipped
ci / checks (push) Successful in 4m46s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 13m28s
Finish Stage 12 editor fixes and refreshed platformer
2026-11-05 17:36:42 +03:00

123 lines
7.0 KiB
JavaScript

// Optional smoke-runner measurement does not change the wasm apps or claim a paired performance result; install before boot because the WebGL backend retains bound method references.
export async function installTrafficProbe(page) {
await page.addInitScript(() => {
const records = new WeakMap();
const probe = { active: null, gaps: [], work: [], last: 0 };
const queries = new Set(["createQuery", "deleteQuery", "beginQuery", "endQuery", "getQuery", "getQueryParameter", "isQuery"]);
const draws = new Set(["drawArrays", "drawArraysInstanced", "drawElements", "drawElementsInstanced", "drawRangeElements"]);
function record(canvas) {
let r = records.get(canvas);
if (!r) {
r = { calls: 0, queries: 0, draws: 0, bufferUploads: 0, bufferBytes: 0, renderer: "" };
records.set(canvas, r);
}
return r;
}
function bytes(source, offset, length) {
if (!source || typeof source.byteLength !== "number") return 0;
const unit = source.BYTES_PER_ELEMENT ?? 1;
const available = Math.max(0, source.byteLength - (offset ?? 0) * unit);
return length > 0 ? Math.min(available, length * unit) : available;
}
const proto = globalThis.WebGL2RenderingContext?.prototype;
if (proto) for (const name of Object.getOwnPropertyNames(proto)) {
const descriptor = Object.getOwnPropertyDescriptor(proto, name);
if (name === "constructor" || typeof descriptor.value !== "function") continue;
const original = descriptor.value;
Object.defineProperty(proto, name, { ...descriptor, value: function (...args) {
const r = record(this.canvas);
if (queries.has(name) || (name === "getParameter" && args[0] === 0x8fbb)) r.queries++;
else r.calls++;
if (draws.has(name)) r.draws++;
if (name === "bufferSubData") {
r.bufferUploads++;
r.bufferBytes += bytes(args[2], args[3], args[4]);
} else if (name === "bufferData" && typeof args[1] !== "number") {
r.bufferUploads++;
r.bufferBytes += bytes(args[1], args[3], args[4]);
}
const value = original.apply(this, args);
if (name === "getParameter" && (args[0] === 0x9246 || (args[0] === 0x1f01 && !r.renderer))) r.renderer = String(value);
return value;
} });
}
const raf = requestAnimationFrame.bind(globalThis);
globalThis.requestAnimationFrame = callback => raf(time => {
const active = probe.active;
const before = active ? record(active).calls : 0;
const start = performance.now();
callback(time);
if (active && active === probe.active && record(active).calls > before) {
probe.work.push(performance.now() - start);
if (probe.last) probe.gaps.push(time - probe.last);
probe.last = time;
}
});
probe.begin = selector => {
const canvas = document.querySelector(selector);
if (!canvas) throw new Error("traffic probe canvas is missing: " + selector);
probe.active = canvas;
probe.gaps = []; probe.work = []; probe.last = 0;
return { ...record(canvas), time: performance.now() };
};
probe.end = () => {
if (!probe.active) throw new Error("traffic window was not started");
const r = { ...record(probe.active), time: performance.now(), gaps: probe.gaps, work: probe.work };
probe.active = null;
return r;
};
globalThis.__aegoTraffic = probe;
});
}
export function summarizeTraffic(first, last, frames) {
if (!(frames > 0)) throw new Error("traffic window did not advance frames");
const quantiles = values => {
const sorted = [...values].sort((a, b) => a - b);
const at = p => sorted.length ? sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * p))] : null;
return { samples: sorted.length, p50: at(.5), p95: at(.95), max: at(1) };
};
const out = { method: "after-only JavaScript WebGL2 interception; query calls reported separately; timing includes probe overhead and host contention", frames, windowMs: last.time - first.time, renderer: last.renderer };
for (const key of ["calls", "queries", "draws", "bufferUploads", "bufferBytes"]) out[key + "PerFrame"] = (last[key] - first[key]) / frames;
out.frameIntervalMs = quantiles(last.gaps);
out.frameWorkMs = quantiles(last.work);
return out;
}
// Uses the same completed engine-frame boundaries in both source checkouts.
// It measures only: normal smoke assertions remain in the default runner mode.
export async function measureTrafficFrames(page, profile, selector, warmup = 120, frames = 120) {
if (!Number.isInteger(warmup) || warmup < 1 || !Number.isInteger(frames) || frames < 1) throw new Error("invalid traffic frame window");
const result = await page.evaluate(({ profile, selector, warmup, frames }) => new Promise((resolve, reject) => {
const count = profile === "playground"
? () => JSON.parse(globalThis.aego.call("poll", "{}")).stats.frames
: () => JSON.parse(globalThis.aegoDocs.call("state", "{}")).state.frames;
const pose = () => profile === "try" ? JSON.parse(globalThis.aegoDocs.call("pose", '{"node":"Box"}')).pose : null;
const warmTo = count() + warmup;
let first, initialPose;
let stopped = false;
const timeout = setTimeout(() => { stopped = true; reject(new Error("traffic frame window timed out")); }, 120000);
const tick = () => {
if (stopped) return;
try {
const current = count();
if (!first && current >= warmTo) {
initialPose = pose();
first = { frames: current, traffic: globalThis.__aegoTraffic.begin(selector) };
} else if (first && current >= first.frames + frames) {
const last = { frames: current, traffic: globalThis.__aegoTraffic.end() };
clearTimeout(timeout); stopped = true;
resolve({ first, last, initialPose, finalPose: pose() });
return;
}
requestAnimationFrame(tick);
} catch (error) { clearTimeout(timeout); stopped = true; reject(error); }
};
requestAnimationFrame(tick);
}), { profile, selector, warmup, frames });
const measured = result.last.frames - result.first.frames;
if (measured !== frames) throw new Error(`traffic frame window measured ${measured}, want exactly ${frames}`);
return { ...summarizeTraffic(result.first.traffic, result.last.traffic, measured), warmupFrames: warmup,
firstFrame: result.first.frames, lastFrame: result.last.frames, initialPose: result.initialPose, finalPose: result.finalPose };
}