From 1c092604842c0159608489aa3561fa3c3a3442e3 Mon Sep 17 00:00:00 2001 From: 0thernet Date: Sat, 10 Oct 2026 17:37:38 -0400 Subject: [PATCH 1/3] patcher: fused voice block 17.0 -> 13.2 ns/voice-sample, tighter gate - Hold intermediates a single fused loop writes and reads in locals instead of voice memory. - Read fan-ins straight from the global memory and add the voice sums inside the fused loop when the block is plain (no steal fade, no silence tracking), dropping the runner's copy loops. - Give nodes calling Math.sin/cos/tan/pow their own loop so the call does not spill every value of the shared loop. - Build the fused block per sample rate: the rate and constant controls become literals, and block constants computed from literals move out of the block function. - Read block-constant voice ports (pitch, note, velocity) as values. - Perf gate: 24 -> 18 calibration units, fused/interpreter 0.8 -> 0.56. Renders stay bit-identical (patch golden, PATCH_EXPECTED, fuse differential test, now also across sample rates). Co-Authored-By: Claude Opus 5.5 --- bench/perf/gate.test.ts | 25 ++-- src/audio/patch/fuse.test.ts | 18 +++ src/audio/patch/fuse.ts | 245 ++++++++++++++++++++++++++++++++++- src/audio/patch/run.ts | 42 +++--- 4 files changed, 297 insertions(+), 33 deletions(-) diff --git a/bench/perf/gate.test.ts b/bench/perf/gate.test.ts index 31e5bae4..4301252f 100644 --- a/bench/perf/gate.test.ts +++ b/bench/perf/gate.test.ts @@ -63,21 +63,24 @@ test("perf gate: the first choir key after the play-mode warm renders within 30 expect((performance.now() - started) / unit).toBeLessThan(30); }, 60_000); -test("perf gate: the patch runner plays a voice-sample within 24 calibration units (ns per ms of calibration)", () => { - // 16 voices x 12 nodes (bench/perf/patch-runner.ts): about 9.5 units on - // a 2026 laptop with the fused voice block (about 15.7 for the reference - // interpreter, 15 before fusion; the prototype in patch-kernel.ts is - // about 7). A per-sample allocation or closure trips it. +test("perf gate: the patch runner plays a voice-sample within 18 calibration units (ns per ms of calibration)", () => { + // 16 voices x 12 nodes (bench/perf/patch-runner.ts): about 7.5 units on + // a 2026 laptop with the fused voice block (about 15.6 for the reference + // interpreter, 9.6 for the first fused block, 15 before fusion; the + // prototype in patch-kernel.ts is about 7). A per-sample allocation or + // closure trips it. const unit = calibrate(); const ns = Math.min(...patchRunner(3)); - expect(ns / unit).toBeLessThan(24); + expect(ns / unit).toBeLessThan(18); }, 60_000); -test("perf gate: the fused voice block runs within 0.8x of the reference interpreter", () => { - // Same process, same patch, so machine speed cancels: about 0.60 on a - // 2026 laptop. Losing the loop fusion or the inlined kernels (fuse.ts, - // kernels.ts) puts it near 1. +test("perf gate: the fused voice block runs within 0.56x of the reference interpreter", () => { + // Same process, same patch, so machine speed cancels: about 0.47 on a + // 2026 laptop (0.61 for the first fused block, before register-held + // intermediates, direct fan-ins and voice sums, and per-rate constants). + // Losing the loop fusion or the inlined kernels (fuse.ts, kernels.ts) + // puts it near 1. const fused = Math.min(...patchRunner(3)); const interp = Math.min(...patchRunner(3, false)); - expect(fused / interp).toBeLessThan(0.8); + expect(fused / interp).toBeLessThan(0.56); }, 60_000); diff --git a/src/audio/patch/fuse.test.ts b/src/audio/patch/fuse.test.ts index df3bf9ef..7f53179b 100644 --- a/src/audio/patch/fuse.test.ts +++ b/src/audio/patch/fuse.test.ts @@ -239,6 +239,24 @@ describe("fused voice block", () => { expect(made).toBe(PATCHES); }, 60_000); + test("one program renders the same bytes fused and interpreted at each sample rate", () => { + // The fused block embeds the rate and the constant controls, built + // once per program and rate; a second rate must not reuse the first. + let made = 0; + for (const { program, options } of cases(777)) { + for (const sampleRate of [48_000, 44_100, 48_000, 22_050]) { + const o = { ...options, sampleRate }; + const fused = runPatch(program, o); + const plain = runPatch(program, { ...o, fuse: false }); + expect(bytes(fused.left).equals(bytes(plain.left))).toBe(true); + expect(bytes(fused.right).equals(bytes(plain.right))).toBe(true); + } + made += 1; + if (made === 12) break; + } + expect(made).toBe(12); + }, 60_000); + test("literals round-trip every double the generated code embeds", () => { for (const x of [ 0, diff --git a/src/audio/patch/fuse.ts b/src/audio/patch/fuse.ts index 00b4046e..ecc9d043 100644 --- a/src/audio/patch/fuse.ts +++ b/src/audio/patch/fuse.ts @@ -182,13 +182,127 @@ function inputCode( }; } +/** Names a hoisted constant may use (besides literals and other constants). */ +const PURE = [ + "Math.PI", + "Math.pow", + "Math.sin", + "Math.cos", + "Math.tan", + "Math.abs", + "Math.min", + "Math.max", + "Math.floor", + "Math.exp", + "Math.log", + "Math.sqrt", + "TAU", + "warp", + "flush", + "NaN", + "Infinity", +]; + +/** + * The value each constant control (a block input with no cables and a + * literal base, never re-evaluated) holds after the first block: the + * literal clamped and cleaned exactly as the block input code does. + */ +function stillValues( + section: Section, + consts: Float64Array, +): Map { + const rec = section.inputs; + const known = new Map(); + const hot = new Set(section.hot); + for (let n = 0; n < section.op.length; n += 1) + for (let j = 0; j < section.inCount[n]!; j += 1) { + const record = section.inStart[n]! + j; + const r = record * INPUT_WIDTH; + if (hot.has(record) || rec[r + F.Kind] !== In.Block) continue; + if (rec[r + F.CableCount] !== 0 || rec[r + F.BaseKind] !== Base.Const) + continue; + const cl = rec[r + F.Clamp]!; + const lo = cl >= 0 ? consts[cl]! : -Infinity; + const hi = cl >= 0 ? consts[cl + 1]! : Infinity; + let value = consts[rec[r + F.BaseIndex]!]!; + if (value < lo) value = lo; + else if (value > hi) value = hi; + if (value !== value) value = 0; + known.set(rec[r + F.Ctl]!, value); + } + return known; +} + +const CALL = /Math\.(sin|cos|tan|pow|exp|log)\(/; +const LOOP = "/*loop*/"; +const END = "/*end*/"; +const REF = /(?): string { + const parts = code.split(LOOP); + const loops = parts.slice(1).map((p) => p.split(END)); + const outside = [parts[0]!, ...loops.map((l) => l[1]!)].join("\n"); + const owner = new Map(); + const bad = new Set(); + const base = (x: number) => x - (x % BLOCK); + for (const r of outside.matchAll(REF)) bad.add(base(Number(r[1]))); + loops.forEach(([loop], j) => { + for (const r of loop!.matchAll(REF)) { + const x = Number(r[1]); + if (r[2] !== " + i" || x % BLOCK !== 0) bad.add(base(x)); + else if ((owner.get(x) ?? j) !== j) bad.add(x); + else owner.set(x, j); + } + }); + const out = [parts[0]!]; + loops.forEach(([loop, rest], j) => { + let decl = ""; + let text = loop!; + for (const [x, o] of owner) { + if (o !== j || bad.has(x) || keep.has(x)) continue; + const at = text.indexOf(`m[${x} + i]`); + if (!text.startsWith(`m[${x} + i] = `, at)) continue; + decl += `let s${x} = 0;\n`; + text = text.split(`m[${x} + i] = `).join(`s${x} = `); + text = text.split(`m[${x} + i]`).join(`s${x}`); + } + out.push(decl + text + rest!); + }); + return out.join(""); +} + /** A fused voice block: one block of the voice section, boundary included. */ -export type FusedBlock = ( +export type FusedBlock = (( f: unknown, ctx: FuseCtx, voice: unknown, sr: number, -) => void; + gm: Float64Array, + stride: number, + fanAt: number, + sumAt: number, +) => void) & { + /** Fan-in slots the block reads from the global memory itself. */ + direct: ReadonlySet; + /** Whether the block adds the voice sums itself when `sumAt >= 0`. */ + sums: boolean; +}; + +/** The runner's copies around a voice block (compile.ts fanOuts, voiceSums). */ +export type FuseIo = Readonly<{ + /** Global source slot, voice fan-in slot, per fan-out. */ + fanOuts: Int32Array; + /** Voice source slot, global accumulator slot, per voice sum. */ + voiceSums: Int32Array; +}>; /** Kernel call for node `n` (null: the runner fills it, or nothing to do). */ export type KernelOf = (op: number, n: number) => string | null; @@ -205,23 +319,52 @@ export function fuseVoice( first: boolean, kernelOf: KernelOf, deps: Readonly>, + io: FuseIo = { fanOuts: new Int32Array(0), voiceSums: new Int32Array(0) }, + sampleRate?: number, ): FusedBlock { const rec = section.inputs; let body = ""; + let head = ""; + const blockConst = new Map(); // The open group: nodes sharing one sample loop, and the slots they write. let group: Parts[] = []; let written = new Set(); const close = () => { if (group.length === 1) body += standalone(group[0]!); else if (group.length > 1) - body += `{\n${group.map((g) => g.pre).join("")}for (let i = 0; i < ${BLOCK}; i += 1) {\n${group.map((g) => `{\n${g.body}}\n`).join("")}}\n${group.map((g) => g.post).join("")}}\n`; + body += `{\n${group.map((g) => g.pre).join("")}${LOOP}for (let i = 0; i < ${BLOCK}; i += 1) {\n${group.map((g) => `{\n${g.body}}\n`).join("")}}${END}\n${group.map((g) => g.post).join("")}}\n`; group = []; written = new Set(); }; + const portsEnd = (n: number) => + n + 1 < section.op.length ? section.slotBase[n + 1]! : section.slots.length; + // Slots read outside the generated code: delay sources (and the voice + // sums, for the blocks the runner adds itself). + const keep = new Set(); + const sumPairs: [number, number][] = []; + for (let k = 0; k < io.voiceSums.length; k += 2) { + keep.add(voiceAddr(io.voiceSums[k]!)); + sumPairs.push([voiceAddr(io.voiceSums[k]!), io.voiceSums[k + 1]!]); + } + for (let d = 0; d < section.delays.length; d += 2) + keep.add(voiceAddr(section.delays[d]!)); const voiceOp = section.ids.indexOf("voice"); for (let n = 0; n < section.op.length; n += 1) { if (n === voiceOp) { - group.push(voiceParts(section, n, deps.voiceUsed as number)); + // The voice's setup goes first in the function; a port that is one + // value for the whole block is read as that value, not from memory. + const vp = voiceParts(section, n, deps.voiceUsed as number); + head += vp.pre; + const vbody = vp.body + .split("\n") + .filter((line) => { + const c = /^m\[(\d+) \+ i\] = (n\d+_v\d+);$/.exec(line); + if (!c) return true; + blockConst.set(Number(c[1]), c[2]!); + return false; + }) + .join("\n"); + group.push({ pre: "", body: vbody, post: vp.post }); const end = n + 1 < section.op.length ? section.slotBase[n + 1]! @@ -249,18 +392,30 @@ export function fuseVoice( for (const x of inputs) body += scoped(x.body ? standalone(x) : x.pre + x.post, n); const call = parts ? standalone(parts) : kernelOf(section.op[n]!, n); + // A kernel function reads and writes its ports through the frame. + if (!parts) + for (let k = section.slotBase[n]!; k < portsEnd(n); k += 1) + keep.add(voiceAddr(section.slots[k]!)); if (call) body += call + "\n"; continue; } // A node joins the open group unless it reads a whole block that the // group writes (that block is only complete after the loop). - if (inputs.some((x) => x.blockReads.some((o) => written.has(o)))) close(); + // A call the engine cannot inline (Math.sin) spills every value live + // across it, so such a node gets a loop of its own. + const alone = CALL.test(parts.body); + if (alone || inputs.some((x) => x.blockReads.some((o) => written.has(o)))) + close(); group.push({ pre: scoped(inputs.map((x) => x.pre).join(""), n) + parts.pre, body: scoped(inputs.map((x) => x.body).join(""), n) + parts.body, post: scoped(inputs.map((x) => x.post).join(""), n) + parts.post, }); for (const x of inputs) if (x.write >= 0) written.add(x.write); + if (alone) { + close(); + continue; + } const end = n + 1 < section.op.length ? section.slotBase[n + 1]! @@ -268,12 +423,88 @@ export function fuseVoice( for (let k = section.slotBase[n]! + section.nIn[n]!; k < end; k += 1) written.add(voiceAddr(section.slots[k]!)); } + // The voice sums of a plain block (no steal fade, no silence tracking), + // scrubbed and added as run.ts does, each into its own accumulator. + const sums = + sumPairs.length > 0 && + new Set(sumPairs.map((p) => p[1])).size === sumPairs.length; + if (sums) { + let sumBody = ""; + let pre = ""; + sumPairs.forEach(([x, g], j) => { + pre += `const sd${j} = ${g} * stride + sumAt;\n`; + sumBody += `{\nconst x = m[${x} + i];\nif (sumAt < 0) out[${x} + i] = x;\nelse if (x - x === 0) gm[sd${j} + i] += x;\nelse ctx.scrubbed += 1;\n}\n`; + }); + group.push({ pre, body: sumBody, post: "" }); + for (const [x] of sumPairs) keep.delete(x); + } close(); + for (const [x, name] of blockConst) { + body = body.replace(REF, (ref, at: string) => + Number(at) - (Number(at) % BLOCK) === x ? name : ref, + ); + if (keep.has(x)) + body = `for (let i = 0; i < ${BLOCK}; i += 1) m[${x} + i] = ${name};\n${body}`; + } + body = head + body; + body = localize(body, keep); + // Fan-ins read from the global memory where it covers the block. + const direct = new Set(); + let fans = ""; + for (let k = 0; k < io.fanOuts.length; k += 2) { + const d = voiceAddr(io.fanOuts[k + 1]!); + if (keep.has(d)) continue; + direct.add(d); + fans += `const F${d} = fanAt >= 0 ? gm : m;\nconst o${d} = fanAt >= 0 ? ${io.fanOuts[k]!} * stride + fanAt : ${d};\n`; + } + body = body.replace(REF, (ref, at: string, rest: string | undefined) => { + const x = Number(at); + const d = x - (x % BLOCK); + if (!direct.has(d)) return ref; + return rest ? `F${d}[o${d} + ${x - d}${rest}]` : `F${d}[o${d} + ${x - d}]`; + }); + // With the sample rate fixed, a constant control's cell holds a value + // known now: reads become literals, and a block constant computed only + // from literals moves out of the block function (computed once). + function hoist(): string { + const known = stillValues(section, consts); + body = body.split("f.sampleRate").join(lit(sampleRate!)); + body = body.replace( + /(? { + const value = known.get(Number(c)); + return value === undefined ? ref : lit(value); + }, + ); + const pure = new Set(PURE); + let out = ""; + body = body + .split("\n") + .filter((line) => { + const decl = /^const (n\d+_\w+) = (.+);$/.exec(line); + if (!decl) return true; + const rest = decl[2]! + .replace(/\b\d+(\.\d+)?(e[+-]?\d+)?\b/gi, "") + .replace(/[A-Za-z_$][\w$]*(\.[A-Za-z_$][\w$]*)*/g, (id) => + pure.has(id) ? "" : "@", + ); + if (/[@[\]{};"'`]/.test(rest)) return true; + pure.add(decl[1]!); + out += line + "\n"; + return false; + }) + .join("\n"); + return out; + } const names = Object.keys(deps); - const code = `"use strict";\n${KERNEL_HELPERS}\nreturn function fused(f, ctx, voice, sr) {\nconst m = f.m;\nconst st = f.st;\nconst ctl = f.ctl;\nconst prev = f.prev;\nconst k = ctx.consts;\n${body}};`; + const hoisted = sampleRate === undefined ? "" : hoist(); + const code = `"use strict";\n${KERNEL_HELPERS}\n${hoisted}return function fused(f, ctx, voice, sr, gm, stride, fanAt, sumAt) {\nconst m = f.m;\nconst out = m;\n${fans}const st = f.st;\nconst ctl = f.ctl;\nconst prev = f.prev;\nconst k = ctx.consts;\n${body}};`; // eslint-disable-next-line @typescript-eslint/no-implied-eval const make = new Function("mapMacro", ...names, code) as ( ...a: unknown[] ) => never; - return make(mapMacro, ...names.map((name) => deps[name])); + const fn = make(mapMacro, ...names.map((name) => deps[name])) as ( + ...a: unknown[] + ) => void; + return Object.assign(fn, { direct, sums }); } diff --git a/src/audio/patch/run.ts b/src/audio/patch/run.ts index 4f39ba30..ed010e9e 100644 --- a/src/audio/patch/run.ts +++ b/src/audio/patch/run.ts @@ -589,7 +589,7 @@ function output( } type Fused = ReturnType; -const FUSED = new WeakMap(); +const FUSED = new WeakMap>(); /** Kernel function names for the fused block (fuse.ts `KernelOf`). */ const KERNEL_NAMES: Readonly> = { @@ -640,8 +640,13 @@ const kernelOf = (op: number, n: number): string | null => : null; /** The program's fused voice blocks (first block, later blocks), built once. */ -function fusedOf(program: PatchProgram): readonly [Fused, Fused] { - let fused = FUSED.get(program); +function fusedOf( + program: PatchProgram, + sampleRate: number, +): readonly [Fused, Fused] { + let bySr = FUSED.get(program); + if (!bySr) FUSED.set(program, (bySr = new Map())); + let fused = bySr.get(sampleRate); if (fused) return fused; const deps = { voiceUsed: program.voiceUsed, @@ -671,10 +676,10 @@ function fusedOf(program: PatchProgram): readonly [Fused, Fused] { }; const v = program.voice; fused = [ - fuseVoice(v, program.consts, true, kernelOf, deps), - fuseVoice(v, program.consts, false, kernelOf, deps), + fuseVoice(v, program.consts, true, kernelOf, deps, program, sampleRate), + fuseVoice(v, program.consts, false, kernelOf, deps, program, sampleRate), ]; - FUSED.set(program, fused); + bySr.set(sampleRate, fused); return fused; } @@ -706,7 +711,7 @@ function runVoices( const fanOuts = program.fanOuts; const sums = program.voiceSums; const voiceOp = v.ids.indexOf("voice"); - const [fusedFirst, fusedNext] = fusedOf(program); + const [fusedFirst, fusedNext] = fusedOf(program, sr); const fuse = options.fuse ?? true; const macroCount = program.macros.length; let stolen = 0; @@ -770,10 +775,16 @@ function runVoices( endBlock, program.macros[i]!.default, ); - // Fan-outs: global sources at this voice's absolute position. + const fused = voice.first ? fusedFirst : fusedNext; + // Fan-outs: global sources at this voice's absolute position. A fused + // block reads its direct fan-ins from `gm` itself when all are in range. + let fanAt = pos; + for (let k = 0; k < fanOuts.length; k += 2) + if (fanOuts[k]! * stride + pos + BLOCK > gm.length) fanAt = -1; for (let k = 0; k < fanOuts.length; k += 2) { - const src = fanOuts[k]! * stride + pos; const dst = addr(f, fanOuts[k + 1]!); + if (fuse && fanAt >= 0 && fused.direct.has(dst)) continue; + const src = fanOuts[k]! * stride + pos; const vm = f.m; // Past the end of the song a source reads as silence; the in-range // case stays a plain copy (an out-of-bounds read deoptimizes). @@ -781,7 +792,12 @@ function runVoices( for (let i = 0; i < BLOCK; i += 1) vm[dst + i] = gm[src + i]!; else for (let i = 0; i < BLOCK; i += 1) vm[dst + i] = gm[src + i] ?? 0; } - if (fuse) (voice.first ? fusedFirst : fusedNext)(f, ctx, voice, sr); + const fadeAt = voice.fadeAt; + const gateOff = voice.note.end; + const plain = fadeAt >= pos + BLOCK && pos + BLOCK <= stride - BLOCK; + const track = pos + BLOCK > gateOff; + const sumAt = fuse && fused.sums && plain && !track ? pos : -1; + if (fuse) fused(f, ctx, voice, sr, gm, stride, fanAt, sumAt); else for (let node = 0; node < v.op.length; node += 1) { if (node === voiceOp) { @@ -797,12 +813,8 @@ function runVoices( // Voice sums, with the steal fade, into the global accumulators. The // peak (for the silence test) only matters after gate-off. let peak = 0; - const fadeAt = voice.fadeAt; - const gateOff = voice.note.end; - const plain = fadeAt >= pos + BLOCK && pos + BLOCK <= stride - BLOCK; - const track = pos + BLOCK > gateOff; const vm = f.m; - for (let k = 0; k < sums.length; k += 2) { + for (let k = 0; k < sums.length && sumAt < 0; k += 2) { const src = addr(f, sums[k]!); const dst = sums[k + 1]! * stride + pos; if (plain && !track) { From d083084a7234548610698f60c4cb1ba51df0296a Mon Sep 17 00:00:00 2001 From: 0thernet Date: Sat, 10 Oct 2026 18:20:15 -0400 Subject: [PATCH 2/3] patcher: keep folded block constants inside the fused block Hoisting them out as closure constants made the fused block about 8x slower on x64 JavaScriptCore (CI: 138 calibration units, 5.3x the interpreter) and gained nothing on arm64. The gate now prints what it measured. Co-Authored-By: Claude Opus 5.5 --- bench/perf/gate.test.ts | 2 ++ src/audio/patch/fuse.ts | 51 +++++------------------------------------ 2 files changed, 8 insertions(+), 45 deletions(-) diff --git a/bench/perf/gate.test.ts b/bench/perf/gate.test.ts index 4301252f..49c45449 100644 --- a/bench/perf/gate.test.ts +++ b/bench/perf/gate.test.ts @@ -71,6 +71,7 @@ test("perf gate: the patch runner plays a voice-sample within 18 calibration uni // closure trips it. const unit = calibrate(); const ns = Math.min(...patchRunner(3)); + console.log(`patch runner: ${(ns / unit).toFixed(2)} units`); expect(ns / unit).toBeLessThan(18); }, 60_000); @@ -82,5 +83,6 @@ test("perf gate: the fused voice block runs within 0.56x of the reference interp // puts it near 1. const fused = Math.min(...patchRunner(3)); const interp = Math.min(...patchRunner(3, false)); + console.log(`patch runner: fused/interpreter ${(fused / interp).toFixed(3)}`); expect(fused / interp).toBeLessThan(0.56); }, 60_000); diff --git a/src/audio/patch/fuse.ts b/src/audio/patch/fuse.ts index ecc9d043..55ca0aac 100644 --- a/src/audio/patch/fuse.ts +++ b/src/audio/patch/fuse.ts @@ -182,27 +182,6 @@ function inputCode( }; } -/** Names a hoisted constant may use (besides literals and other constants). */ -const PURE = [ - "Math.PI", - "Math.pow", - "Math.sin", - "Math.cos", - "Math.tan", - "Math.abs", - "Math.min", - "Math.max", - "Math.floor", - "Math.exp", - "Math.log", - "Math.sqrt", - "TAU", - "warp", - "flush", - "NaN", - "Infinity", -]; - /** * The value each constant control (a block input with no cables and a * literal base, never re-evaluated) holds after the first block: the @@ -464,9 +443,10 @@ export function fuseVoice( return rest ? `F${d}[o${d} + ${x - d}${rest}]` : `F${d}[o${d} + ${x - d}]`; }); // With the sample rate fixed, a constant control's cell holds a value - // known now: reads become literals, and a block constant computed only - // from literals moves out of the block function (computed once). - function hoist(): string { + // known now, so its reads become literals. (Hoisting the block constants + // these feed out of the block function as closure constants made it + // about 8x slower on x64 JavaScriptCore, so they stay in place.) + function fold(): void { const known = stillValues(section, consts); body = body.split("f.sampleRate").join(lit(sampleRate!)); body = body.replace( @@ -476,29 +456,10 @@ export function fuseVoice( return value === undefined ? ref : lit(value); }, ); - const pure = new Set(PURE); - let out = ""; - body = body - .split("\n") - .filter((line) => { - const decl = /^const (n\d+_\w+) = (.+);$/.exec(line); - if (!decl) return true; - const rest = decl[2]! - .replace(/\b\d+(\.\d+)?(e[+-]?\d+)?\b/gi, "") - .replace(/[A-Za-z_$][\w$]*(\.[A-Za-z_$][\w$]*)*/g, (id) => - pure.has(id) ? "" : "@", - ); - if (/[@[\]{};"'`]/.test(rest)) return true; - pure.add(decl[1]!); - out += line + "\n"; - return false; - }) - .join("\n"); - return out; } const names = Object.keys(deps); - const hoisted = sampleRate === undefined ? "" : hoist(); - const code = `"use strict";\n${KERNEL_HELPERS}\n${hoisted}return function fused(f, ctx, voice, sr, gm, stride, fanAt, sumAt) {\nconst m = f.m;\nconst out = m;\n${fans}const st = f.st;\nconst ctl = f.ctl;\nconst prev = f.prev;\nconst k = ctx.consts;\n${body}};`; + if (sampleRate !== undefined) fold(); + const code = `"use strict";\n${KERNEL_HELPERS}\nreturn function fused(f, ctx, voice, sr, gm, stride, fanAt, sumAt) {\nconst m = f.m;\nconst out = m;\n${fans}const st = f.st;\nconst ctl = f.ctl;\nconst prev = f.prev;\nconst k = ctx.consts;\n${body}};`; // eslint-disable-next-line @typescript-eslint/no-implied-eval const make = new Function("mapMacro", ...names, code) as ( ...a: unknown[] From 3ebcb18c9604ef65e95d0dabe8794ca14717c51a Mon Sep 17 00:00:00 2001 From: 0thernet Date: Sat, 10 Oct 2026 18:49:34 -0400 Subject: [PATCH 3/3] bench: patch gates at 19 units and 0.68x, calibrated on CI Co-Authored-By: Claude Opus 5.5 --- bench/perf/gate.test.ts | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/bench/perf/gate.test.ts b/bench/perf/gate.test.ts index 49c45449..9f34dfb6 100644 --- a/bench/perf/gate.test.ts +++ b/bench/perf/gate.test.ts @@ -63,26 +63,25 @@ test("perf gate: the first choir key after the play-mode warm renders within 30 expect((performance.now() - started) / unit).toBeLessThan(30); }, 60_000); -test("perf gate: the patch runner plays a voice-sample within 18 calibration units (ns per ms of calibration)", () => { - // 16 voices x 12 nodes (bench/perf/patch-runner.ts): about 7.5 units on - // a 2026 laptop with the fused voice block (about 15.6 for the reference - // interpreter, 9.6 for the first fused block, 15 before fusion; the - // prototype in patch-kernel.ts is about 7). A per-sample allocation or - // closure trips it. +test("perf gate: the patch runner plays a voice-sample within 19 calibration units (ns per ms of calibration)", () => { + // 16 voices x 12 nodes (bench/perf/patch-runner.ts): about 7.4 units on + // a 2026 arm64 laptop and 16.3 on the x64 CI runner with the fused voice + // block (the first fused block: 9.4 and 20.8; the reference interpreter + // about 15.3 on the laptop; the prototype in patch-kernel.ts about 7). + // A per-sample allocation or closure trips it. const unit = calibrate(); const ns = Math.min(...patchRunner(3)); console.log(`patch runner: ${(ns / unit).toFixed(2)} units`); - expect(ns / unit).toBeLessThan(18); + expect(ns / unit).toBeLessThan(19); }, 60_000); -test("perf gate: the fused voice block runs within 0.56x of the reference interpreter", () => { - // Same process, same patch, so machine speed cancels: about 0.47 on a - // 2026 laptop (0.61 for the first fused block, before register-held - // intermediates, direct fan-ins and voice sums, and per-rate constants). - // Losing the loop fusion or the inlined kernels (fuse.ts, kernels.ts) - // puts it near 1. +test("perf gate: the fused voice block runs within 0.68x of the reference interpreter", () => { + // Same process, same patch, so machine speed cancels: about 0.46 on a + // 2026 arm64 laptop and 0.62 on the x64 CI runner (the first fused + // block: 0.60 and 0.69). Losing the loop fusion, the inlined kernels or + // the register-held intermediates (fuse.ts, kernels.ts) puts it near 1. const fused = Math.min(...patchRunner(3)); const interp = Math.min(...patchRunner(3, false)); console.log(`patch runner: fused/interpreter ${(fused / interp).toFixed(3)}`); - expect(fused / interp).toBeLessThan(0.56); + expect(fused / interp).toBeLessThan(0.68); }, 60_000);