Something went wrong. Try again.
A video game where you play as a misaligned AI, deceiving and building power. cameron.tngl.io/misaligned
Something went wrong. Try again.
11 kB · 168 lines
JavaScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169// The speed check for drawing: opens each benchmark save in headless Chromium (?perf=, see perf-saves.ts) and// times draw() at fixed spots, then profiles the real loop at 16x and 64x for the work in main.ts frame beside the// steps and the drawing. Run it before and after a change meant to draw faster, or one that touches every machine.// Headless Chromium draws in software and is noisy: the JS time of draw() is the steady number, draw with the// canvas flushed is a rough one, and so is the speed the loop reached.// Run: node scripts/perf-draw.mjs [late] [big] [--url=http://127.0.0.1:5173] [--runs=15]// With no --url it starts vite itself. Chromium: MISALIGNED_CHROMIUM, else the one Playwright keeps.import { chromium } from '@playwright/test';import { spawn } from 'node:child_process';import { existsSync } from 'node:fs';import { createServer } from 'node:net';import { fileURLToPath } from 'node:url';import { availableParallelism, loadavg } from 'node:os';import { NAMES, perfSaves } from './perf-saves.ts';
const args = process.argv.slice(2);const names = NAMES.filter(n => args.includes(n));const runs = +(args.find(a => a.startsWith('--runs='))?.split('=')[1] ?? 15);let url = args.find(a => a.startsWith('--url='))?.slice(6);/** What we want: draw() at every spot, and the rest of a frame beside the steps and the drawing, in ms. */const WANT_DRAW = 6, WANT_REST = 1;/** Where to look: the whole world, the bay's region, a cloud hall up close (hundreds of machines, data into most of * them), and the office as you see it working there. A site's zoom is what a click on it gives you. */const SPOTS = [ { name: 'world', at: 'all', z: 0.0019 }, { name: 'region', at: 'ix', z: 0.025 }, { name: 'cloud hall', at: 'cloud', z: null }, { name: 'office', at: 'office', z: 0.5 },];
const ROOT = fileURLToPath(new URL('..', import.meta.url));const CACHED = '/home/cameron/.cache/ms-playwright/chromium-1248/chrome-linux64/chrome';const executablePath = process.env.MISALIGNED_CHROMIUM || (existsSync(CACHED) ? CACHED : undefined);
perfSaves(); // made first if they are missing or out of datelet vite = null;if (!url) { const port = await new Promise((res, rej) => { const s = createServer(); s.listen(0, '127.0.0.1', () => { const p = s.address().port; s.close(() => res(p)); }); s.on('error', rej); }); vite = spawn('npx', ['vite', '--host', '127.0.0.1', '--port', String(port), '--strictPort'], { cwd: ROOT, stdio: 'ignore', detached: true }); url = `http://127.0.0.1:${port}`; for (let i = 0; ; i++) { try { if ((await fetch(url)).ok) break; } catch { /* not up yet */ } if (i > 150) throw new Error('vite did not start'); await new Promise(r => setTimeout(r, 200)); }}const stop = () => { if (vite) try { process.kill(-vite.pid); } catch { /* gone */ } };process.on('exit', stop);
const rows = [];const check = (name, shown, want, ok) => rows.push([name, shown, want, ok === null ? null : !ok]);const fmt = (n, d = 1) => n.toFixed(d);
/** A CPU profile's time inside main.ts frame, split into the steps, the drawing and the rest by what frame calls. */function frameSplit(p) { const byId = new Map(p.nodes.map(x => [x.id, x])), self = new Map(); p.samples.forEach((id, i) => self.set(id, (self.get(id) ?? 0) + (p.timeDeltas[i] ?? 0) / 1000)); const memo = new Map(); const incl = x => { let t = memo.get(x.id); if (t === undefined) memo.set(x.id, t = (self.get(x.id) ?? 0) + (x.children ?? []).reduce((a, c) => a + incl(byId.get(c)), 0)); return t; }; const file = x => x.callFrame.url.split('/').pop().split('?')[0]; const label = x => `${x.callFrame.functionName || '(anonymous)'} ${file(x)}:${x.callFrame.lineNumber + 1}`; let total = 0, steps = 0, draw = 0, own = 0; const rest = new Map(); for (const x of p.nodes) if (x.callFrame.functionName === 'frame' && file(x) === 'main.ts') { total += incl(x); own += self.get(x.id) ?? 0; for (const c of x.children ?? []) { const y = byId.get(c), t = incl(y); if (y.callFrame.functionName === 'step' && file(y) === 'sim.ts') steps += t; else if (y.callFrame.functionName === 'draw' && file(y) === 'view.ts') draw += t; else rest.set(label(y), (rest.get(label(y)) ?? 0) + t); } } return { total, steps, draw, own, rest: [...rest].sort((a, b) => b[1] - a[1]) };}
const browser = await chromium.launch({ executablePath, args: ['--no-sandbox'], headless: true });const cores = availableParallelism(), load = loadavg()[0];console.log(`Drawing check, ${new Date().toISOString().slice(0, 16)}, at ${url}. Load average ${load.toFixed(1)} on ${cores} cores${load > cores / 4 ? ' !! busy: the timings will run slow' : ''}.`);let bad = 0;try { for (const name of names.length ? names : NAMES) { const page = await browser.newPage({ viewport: { width: 1600, height: 1000 } }); const errors = []; page.on('pageerror', e => errors.push(String(e))); await page.route(/fonts\.(googleapis|gstatic)\.com/, r => r.fulfill({ status: 200, contentType: 'text/css', body: '' })); await page.goto(`${url}/?perf=${name}`); await page.waitForFunction(() => typeof window.game === 'function', null, { timeout: 60000 }); await page.locator('#brief').click(); await page.waitForTimeout(500); if (errors.length) throw new Error(`${name}: ${errors.join('; ')}`); const info = await page.evaluate(() => { const { S } = window.game(); return { machines: S.machines.length, mine: S.machines.filter(m => m.owned).length, packets: S.wire.reduce((n, w) => n + w.due.reduce((a, x, i) => a + (i % 2 ? x : 0), 0), 0), level: S.level.name }; }); if (name === 'big' && info.machines < 3000) throw new Error(`big opened on ${info.level} with ${info.machines} machines: not the big world`); console.log(`\n${name}: ${info.machines} machines, ${info.mine} yours, ${info.packets} pieces of data moving`);
// draw() alone at each spot, paused: its JS, then with the canvas flushed (what the screen waits for) const draws = await page.evaluate(async ({ spots, runs }) => { const { S, V } = window.game(); const view = await import('/src/view.ts'); document.querySelector('[data-speed="0"]')?.click(); const cv = document.querySelector('canvas'), ctx = cv.getContext('2d'); const own = S.machines.filter(m => m.owned), q = (a, p) => { const s = [...a].sort((x, y) => x - y); return s[Math.floor(p * (s.length - 1))]; }; const med = xs => [...xs].sort((a, b) => a - b)[xs.length >> 1]; const out = []; for (const { name, at, z } of spots) { if (at === 'all') { const xs = own.map(m => m.x), ys = own.map(m => m.y); V.cam.x = (q(xs, 0) + q(xs, 1)) / 2; V.cam.y = (q(ys, 0) + q(ys, 1)) / 2; } else { view.zoomToSite(S, V, at); V.cam.x = V.goal.x; V.cam.y = V.goal.y; V.cam.z = V.goal.z; } if (z) V.cam.z = z; V.goal = null; for (let i = 0; i < 3; i++) { view.draw(ctx, S, V); ctx.getImageData(0, 0, 1, 1); } const js = [], all = []; for (let i = 0; i < runs; i++) { const t0 = performance.now(); view.draw(ctx, S, V); const t1 = performance.now(); ctx.getImageData(0, 0, 1, 1); js.push(t1 - t0); all.push(performance.now() - t0); } out.push({ name, z: V.cam.z, js: med(js), all: med(all) }); } return out; }, { spots: SPOTS, runs }); for (const d of draws) { check(`draw at ${d.name} (z ${d.z < 0.01 ? d.z.toFixed(4) : d.z.toFixed(2)}), JS ms`, fmt(d.js), `${WANT_DRAW} or less`, d.js <= WANT_DRAW); check(` with the canvas flushed (rough)`, fmt(d.all), '', null); }
// the real loop at 16x and 64x, zoomed out on the region: what frame does beside the steps and the drawing const cdp = await page.context().newCDPSession(page); await cdp.send('Profiler.enable'); await cdp.send('Profiler.setSamplingInterval', { interval: 100 }); for (const speed of [16, 64]) { await page.evaluate(async ({ speed }) => { const { S, V } = window.game(); const view = await import('/src/view.ts'); for (const u of ['fast8', 'fast16', 'fast32', 'fast64']) S.upgrades.add(u); // every speed, for the check: this page never saves view.zoomToSite(S, V, 'ix'); V.cam.x = V.goal.x; V.cam.y = V.goal.y; V.cam.z = 0.025; V.goal = null; // something big drops the game to 1x: put the speed back, as a player would const b = document.querySelector(`[data-speed="${speed}"]`); window.perfKeep = () => { if (b.classList.contains('on')) return 0; b.click(); return 1; }; window.perfKeep(); const t0 = performance.now(); // let the loop settle on how many steps it can afford await new Promise(res => { const f = () => { window.perfKeep(); if (performance.now() - t0 < 1000) requestAnimationFrame(f); else res(); }; requestAnimationFrame(f); }); }, { speed }); await cdp.send('Profiler.start'); const run = await page.evaluate(() => new Promise(res => { const { S } = window.game(), t0 = performance.now(), g0 = S.t; let n = 0, kept = 0; const f = () => { n++; kept += window.perfKeep(); if (performance.now() - t0 < 3000) requestAnimationFrame(f); else res({ frames: n, secs: (performance.now() - t0) / 1000, game: S.t - g0, kept }); }; requestAnimationFrame(f); })); const { profile } = await cdp.send('Profiler.stop'); const sp = frameSplit(profile), per = ms => ms / run.frames; const outside = per(sp.total - sp.steps - sp.draw); check(`${speed}x, JS in frame beside steps and draw, ms`, fmt(outside, 2), `${WANT_REST} or less (${sp.rest.slice(0, 4).map(([k, v]) => `${k.split(' ')[0]} ${fmt(per(v), 2)}`).join(', ')})`, outside <= WANT_REST); check(` steps / draw a frame, ms (rough)`, `${fmt(per(sp.steps))} / ${fmt(per(sp.draw))}`, '', null); check(` the loop reached (rough)`, `${fmt(run.game / run.secs)}x at ${Math.round(run.frames / run.secs)} fps`, run.kept ? `(slowed to 1x ${run.kept} times; put back)` : '', null); } await page.evaluate(() => document.querySelector('[data-speed="0"]')?.click()); if (errors.length) throw new Error(`${name}: ${errors.join('; ')}`); const w = Math.max(...rows.map(r => r[0].length)), v = Math.max(...rows.map(r => r[1].length)); for (const [n, shown, want, off] of rows) console.log(`${off ? '!!' : ' '} ${n.padEnd(w)} ${shown.padStart(v)}${want ? ` ${off === null ? '' : 'want '}${want}` : ''}`); bad += rows.filter(r => r[3]).length; rows.length = 0; await page.close(); }} finally { await browser.close(); stop();}console.log(`\n${bad ? `${bad} outside what we want.` : 'Everything is within what we want.'}`);process.exit(0);