// Explicit browser benchmark; never imported by the application. // await (await import('/assets/sculpt/latency-check.js')).check() import { Input } from "./input.js"; import { Renderer } from "./renderer.js"; import { StrokeClient } from "./stroke-client.js"; import { Sculpt } from "./engine.js"; export async function check() { const viewport = document.getElementById("viewport"); const move = Input.prototype.move, publish = Renderer.prototype.publish, message = StrokeClient.prototype.message, dab = Sculpt.prototype.dab; const capture = viewport.setPointerCapture; const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); const frame = () => new Promise(requestAnimationFrame); const wait = async (predicate) => { const start = performance.now(); while (!predicate()) { if (performance.now() - start > 15000) throw new Error("Latency check timed out"); await sleep(10); } }; let input, durations = [], uploads = [], packets = [], mainDabs = 0, canceled = false; Input.prototype.move = function (event) { input = this; const start = performance.now(); const result = move.call(this, event); durations.push(performance.now() - start); return result; }; Sculpt.prototype.dab = function (...args) { mainDabs++; return dab.apply(this, args); }; StrokeClient.prototype.message = function (data) { const start = performance.now(); const result = message.call(this, data); if (data.type === "geometry") packets.push(performance.now() - start); return result; }; Renderer.prototype.publish = async function (...args) { const start = performance.now(); try { return await publish.apply(this, args); } finally { uploads.push(performance.now() - start); } }; viewport.setPointerCapture = () => {}; const pointer = (type, x, y, pressure = 0.8) => { const r = viewport.getBoundingClientRect(); return new PointerEvent(type, { bubbles: true, pointerId: 7, pointerType: "pen", clientX: r.left + r.width / 2 + x, clientY: r.top + r.height / 2 + y, pressure, button: 0, buttons: type === "pointerup" ? 0 : 1, }); }; const send = (event) => viewport.querySelector("canvas").dispatchEvent(event); const percentile = (values, p) => [...values].sort((a, b) => a - b)[ Math.min(values.length - 1, Math.floor(values.length * p)) ] ?? 0; const results = []; try { for (const brush of ["draw", "grab"]) for (const perFrame of [1, 4, 16]) { await wait( () => document.getElementById("loading").hidden && !document.getElementById("new").disabled, ); const old = viewport.querySelector("canvas"); document.getElementById("new").click(); document.getElementById("density").value = "5"; document.getElementById("new-dialog").close("create"); await wait( () => old !== viewport.querySelector("canvas") && document.getElementById("loading").hidden, ); document.querySelector(`[data-brush="${brush}"]`).click(); await sleep(300); durations = []; uploads = []; packets = []; mainDabs = 0; canceled = false; send(pointer("pointerdown", -80, -30)); for (let f = 0; f < 60; f++) { await frame(); const samples = Array.from({ length: perFrame }, (_, i) => { const t = (f + (i + 1) / perFrame) / 60; return pointer( "pointermove", -80 + 160 * t, -30 + 30 * Math.sin(t * Math.PI), 0.3 + 0.6 * t, ); }); const event = samples.at(-1); Object.defineProperty(event, "getCoalescedEvents", { value: () => samples, }); send(event); if (input && !input.busy) canceled = true; } const release = performance.now(); send(pointer("pointerup", 80, -30, 0)); await wait(() => !document.getElementById("save").disabled); const releaseMs = performance.now() - release; const undoEntries = input.host.engine().undoStack.length; canceled ||= undoEntries !== 1; if (canceled || mainDabs || input.queue !== undefined) throw new Error("Worker input regression"); results.push({ brush, samplesPerFrame: perFrame, sharedInputBytes: input.host.processor().mailbox.words.byteLength, mainDabs, canceled, releaseMs, mainInputP95: percentile(durations, 0.95), mainPacketP95: percentile(packets, 0.95), uploadP95: percentile(uploads, 0.95), undoEntries, }); } return results; } finally { Input.prototype.move = move; StrokeClient.prototype.message = message; Sculpt.prototype.dab = dab; Renderer.prototype.publish = publish; viewport.setPointerCapture = capture; } }