Skip to content

Commit fefe840

Browse files
committed
perf(basin): cache baseline vacuum across candidates (skill/uma basin)
Profiling a basin (localStorage.torena_perf) showed wall=28s was ~all genuine, well-parallelized worker compute (14.5x on a 16-pool; main-thread plan-build was 0%). The dominant redundancy: every candidate re-simulates the *baseline* vacuum (runner without the tracked skill), which is identical across all candidates at a given sample count — the planner already caches this, the basin pools did not. Pool workers now memoize the baseline result per run (keyed by seed + sample count + baseline skill order; reset on init), so the baseline is simulated once per stage per worker instead of once per skill. Each cached result is read-only in the reduction, and the key captures the exact (rare) per-candidate sort variations — so every simulation is byte-identical to before, just not recomputed. Roughly halves basin worker compute (baseline is ~half the runCompare calls). typecheck + lint + 221 tests + build + worker guard green.
1 parent e7e2884 commit fefe840

3 files changed

Lines changed: 46 additions & 9 deletions

File tree

src/modules/simulation/simulators/wasm-skill-compare.ts

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -213,16 +213,40 @@ class RepresentativeRunTracker {
213213
}
214214

215215
/** Run a WASM-backed skill comparison for a prebuilt plan entry. Worker-safe. */
216+
/**
217+
* Identity key for a baseline vacuum: every candidate in a basin run shares the
218+
* same baseline runner, so its result can be reused instead of re-simulated per
219+
* skill. Within a run the only things that vary are the sample count (per stage)
220+
* and — rarely — the skill ordering, so the key captures seed + samples + the
221+
* baseline skill order. Reused results are read-only in the reduction.
222+
*/
223+
function baselineKey(params: WasmCompareParams): string {
224+
const skillOrder = (params.runners[0]?.skills ?? [])
225+
.map((skill) => skill.skillId)
226+
.join(',');
227+
return `${params.masterSeed}:${params.nsamples}:${skillOrder}`;
228+
}
229+
230+
/** Per-run cache of baseline vacuum results, keyed by {@link baselineKey}. */
231+
export type BaselineCache = Map<string, Promise<WasmCompareData>>;
232+
216233
export async function runSkillComparisonFromEntry(
217-
entry: SkillSamplingPlanEntry
234+
entry: SkillSamplingPlanEntry,
235+
baselineCache?: BaselineCache
218236
): Promise<SkillComparisonResult> {
219237
const { skillId: trackedSkillId, nsamples, fallback, wasmParamsBaseline, wasmParamsTracked } =
220238
entry;
221239

222-
const [dataA, dataB] = await Promise.all([
223-
runCompare(wasmParamsBaseline),
224-
runCompare(wasmParamsTracked)
225-
]);
240+
// The baseline (runner without the tracked skill) is identical across every
241+
// candidate in the run — run it once and reuse, instead of per skill.
242+
const key = baselineKey(wasmParamsBaseline);
243+
let baselinePromise = baselineCache?.get(key);
244+
if (!baselinePromise) {
245+
baselinePromise = runCompare(wasmParamsBaseline);
246+
baselineCache?.set(key, baselinePromise);
247+
}
248+
249+
const [dataA, dataB] = await Promise.all([baselinePromise, runCompare(wasmParamsTracked)]);
226250

227251
const diff: Array<number> = [];
228252
const trackedMetaCollection: Array<SkillTrackedMeta> = [];
@@ -268,13 +292,14 @@ export async function runSkillComparisonFromEntry(
268292
* `buildSkillSamplingPlan`.
269293
*/
270294
export async function runSamplingFromPlan(
271-
plan: SkillSamplingPlan
295+
plan: SkillSamplingPlan,
296+
baselineCache?: BaselineCache
272297
): Promise<SkillComparisonResponse> {
273298
const data: SkillComparisonResponse = {};
274299

275300
for (const entry of plan.entries) {
276301
const { results, runData, min, max, mean, median, skillActivations } =
277-
await runSkillComparisonFromEntry(entry);
302+
await runSkillComparisonFromEntry(entry, baselineCache);
278303

279304
data[entry.skillId] = {
280305
id: entry.skillId,

src/workers/pool/skill-basin/skill-basin-wasm.pool.worker.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,22 @@ import type { WorkerInMessage, WorkerOutMessage } from '../types';
1111
import { initUmaSimWasm, initUmaSimWasmFromModule } from '@/lib/uma-sim-wasm/loader';
1212
import {
1313
runSamplingFromPlan,
14+
type BaselineCache,
1415
type SkillSamplingPlan
1516
} from '@/modules/simulation/simulators/wasm-skill-compare';
1617

1718
let workerId = -1;
19+
// Baseline vacuum results are identical across candidates; cache them for the
20+
// lifetime of a run (reset on init) so the baseline is simulated once per stage
21+
// instead of once per skill.
22+
let baselineCache: BaselineCache = new Map();
1823

1924
function sendMessage(message: WorkerOutMessage): void {
2025
postMessage(message);
2126
}
2227

2328
async function processBatch(batchId: number, plan: SkillSamplingPlan): Promise<void> {
24-
const results: SkillComparisonResponse = await runSamplingFromPlan(plan);
29+
const results: SkillComparisonResponse = await runSamplingFromPlan(plan, baselineCache);
2530
sendMessage({ type: 'batch-complete', workerId, batchId, results });
2631
}
2732

@@ -31,6 +36,7 @@ self.addEventListener('message', (event: MessageEvent<WorkerInMessage>) => {
3136
switch (message.type) {
3237
case 'init':
3338
workerId = message.workerId;
39+
baselineCache = new Map(); // fresh run — drop any prior baseline results
3440
(message.compiledModule ? initUmaSimWasmFromModule(message.compiledModule) : initUmaSimWasm())
3541
.then(() => sendMessage({ type: 'worker-ready', workerId }))
3642
.catch((error) =>

src/workers/pool/uma-basin/uma-basin-wasm.pool.worker.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,22 @@ import type { WorkerInMessage, WorkerOutMessage } from '../types';
1111
import { initUmaSimWasm, initUmaSimWasmFromModule } from '@/lib/uma-sim-wasm/loader';
1212
import {
1313
runSamplingFromPlan,
14+
type BaselineCache,
1415
type SkillSamplingPlan
1516
} from '@/modules/simulation/simulators/wasm-skill-compare';
1617

1718
let workerId = -1;
19+
// Baseline vacuum results are identical across candidates; cache them for the
20+
// lifetime of a run (reset on init) so the baseline is simulated once per stage
21+
// instead of once per skill.
22+
let baselineCache: BaselineCache = new Map();
1823

1924
function sendMessage(message: WorkerOutMessage): void {
2025
postMessage(message);
2126
}
2227

2328
async function processBatch(batchId: number, plan: SkillSamplingPlan): Promise<void> {
24-
const results: SkillComparisonResponse = await runSamplingFromPlan(plan);
29+
const results: SkillComparisonResponse = await runSamplingFromPlan(plan, baselineCache);
2530
sendMessage({ type: 'batch-complete', workerId, batchId, results });
2631
}
2732

@@ -31,6 +36,7 @@ self.addEventListener('message', (event: MessageEvent<WorkerInMessage>) => {
3136
switch (message.type) {
3237
case 'init':
3338
workerId = message.workerId;
39+
baselineCache = new Map(); // fresh run — drop any prior baseline results
3440
(message.compiledModule ? initUmaSimWasmFromModule(message.compiledModule) : initUmaSimWasm())
3541
.then(() => sendMessage({ type: 'worker-ready', workerId }))
3642
.catch((error) =>

0 commit comments

Comments
 (0)