Skill Tracer
Skills › Anatomy › anatomy › turn-bench.mjs

turn-bench.mjs

74 lines · scripts/turn-bench.mjs at 1482e84

1import { resolve } from "node:path";
2import { pathToFileURL } from "node:url";
3import { TURN } from "../kit/turn.mjs";
4
5const argv = process.argv.slice(2);
6if (!argv.length || argv.includes("--help")) {
7 console.log(`Time a 3D figure's runtime in Node: emit and order over 360 angles, CPU time, median of 7 rounds.
8
9 node scripts/turn-bench.mjs examples/turning-dial/build.mjs [--rounds 7] [--unit 1]
11The build module must export T (from turning()); if it also exports follow(values), every angle is mapped through it.`);
12 process.exit(argv.length ? 0 : 1);
14const take = (flag, fallback) => {
15 const at = argv.indexOf(flag);
16 return at < 0 ? fallback : Number(argv[at + 1]);
17};
18const rounds = take("--rounds", 7);
19const unit = take("--unit", 1);
20const mod = await import(pathToFileURL(resolve(argv[0])).href);
21const T = mod.T;
22const follow = mod.follow ?? ((values) => values);
23const scene = TURN.prepare(T.data, { unit });
24const runs = scene.runs;
25const frames = runs.map((run) => TURN.frame(run));
26const turn = T.groups.find((g) => g.kind === "turn");
27const angles = Array.from({ length: 360 }, (_, i) => i + 0.37);
28const cams = angles.map((angle) => TURN.cams(scene, follow({ [turn.name]: angle })));
29for (let i = 0; i < 20; i++) for (const run of runs) TURN.emit(run, run.group >= 0 ? cams[i][run.group] : cams[i].world, frames[run.index]);
30const perPart = runs.map(() => []);
31const orderTimes = [];
32const frameTimes = [];
33const cpuFrames = [];
34for (let round = 0; round < rounds; round++) {
35 const sums = new Float64Array(runs.length);
36 let orderSum = 0;
37 const cpu0 = process.cpuUsage();
38 const wall0 = performance.now();
39 const previous = scene.layers.map((layer) => layer.rest);
40 for (let a = 0; a < angles.length; a++) {
41 const c = cams[a];
42 for (const run of runs) {
43 const t0 = performance.now();
44 TURN.emit(run, run.group >= 0 ? c[run.group] : c.world, frames[run.index]);
45 sums[run.index] += performance.now() - t0;
46 }
47 const t1 = performance.now();
48 scene.layers.forEach((layer, index) => {
49 previous[index] = TURN.order(layer, frames, c, previous[index], {}, scene);
50 });
51 orderSum += performance.now() - t1;
52 }
53 const cpu = process.cpuUsage(cpu0);
54 cpuFrames.push((cpu.user + cpu.system) / 1000 / angles.length);
55 frameTimes.push((performance.now() - wall0) / angles.length);
56 orderTimes.push(orderSum / angles.length);
57 runs.forEach((run, index) => perPart[index].push(sums[index] / angles.length));
59const median = (list) => list.slice().sort((x, y) => x - y)[Math.floor(list.length / 2)];
60const kinds = new Map();
61runs.forEach((run, index) => {
62 const kind = run.kind === "tube" ? "tube chunk" : run.kind;
63 if (!kinds.has(kind)) kinds.set(kind, []);
64 kinds.get(kind).push(median(perPart[index]));
65});
66console.log(`turn-bench ${argv[0]} · ${runs.length} parts · ${angles.length} angles · median of ${rounds} rounds · unit ${unit}`);
67for (const [kind, list] of [...kinds].sort((a, b) => b[1].reduce((s, v) => s + v, 0) - a[1].reduce((s, v) => s + v, 0))) {
68 const total = list.reduce((s, v) => s + v, 0);
69 console.log(` ${kind.padEnd(11)} ${String(list.length).padStart(4)} parts · ${(total / list.length).toFixed(4)} ms per part · ${total.toFixed(3)} ms per frame · slowest ${Math.max(...list).toFixed(4)} ms`);
71const slow = runs.map((run, index) => [run.name, median(perPart[index])]).sort((a, b) => b[1] - a[1]).slice(0, 6);
72console.log(` slowest parts: ${slow.map(([name, ms]) => `${name} ${ms.toFixed(3)}`).join(" · ")}`);
73console.log(` order ${median(orderTimes).toFixed(3)} ms per frame (${scene.layers.map((layer) => `${layer.name}: ${layer.parts.length} parts, ${layer.planar.length / 5} planes, ${layer.dynamic.length / 2} dynamic`).join("; ")})`);
74console.log(` frame ${median(frameTimes).toFixed(3)} ms wall · ${median(cpuFrames).toFixed(3)} ms CPU`);