From 3b8595533aec668ecedc3b8eebd0d850836f777a Mon Sep 17 00:00:00 2001 From: Devin Ivy Date: Sat, 18 Apr 2026 23:07:07 -0400 Subject: [PATCH] perf(worker): cache fn-per-id; sync fast-path for trivial args Two small savings on the worker's per-task hot path: 1. Cache the resolved moroutine function by id. Previously every task did id.slice() + imported.has() + registry.get() every call; now it's a single fnCache.get() after the first call per moroutine. Applies to both handleTask and task-arg resolution. 2. When a task's args contain no MessagePort and no task-args, skip Promise.all and synchronously map through deserializeArg. Avoids allocating N promises + awaiting Promise.all for tasks with primitive args (the common case). Eliminates ~3 map/string ops and N promise allocations per call after warmup. Small but measurable gain in dispatch throughput. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/worker-entry.ts | 46 ++++++++++++++++++++++++++------------------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/src/worker-entry.ts b/src/worker-entry.ts index 4e5b6b1..7072312 100644 --- a/src/worker-entry.ts +++ b/src/worker-entry.ts @@ -6,11 +6,26 @@ import { collectTransferables } from './transfer.ts'; const imported = new Set(); const taskCache = new Map(); +const fnCache = new Map unknown>(); function isTaskArg(arg: unknown): arg is { __task__: number; id: string; args: unknown[] } { return typeof arg === 'object' && arg !== null && '__task__' in arg; } +async function resolveFn(id: string): Promise<(...args: unknown[]) => unknown> { + const cached = fnCache.get(id); + if (cached) return cached; + const url = id.slice(0, id.lastIndexOf('#')); + if (!imported.has(url)) { + await import(url); + imported.add(url); + } + const fn = registry.get(id) as ((...args: unknown[]) => unknown) | undefined; + if (!fn) throw new Error(`Moroutine not found: ${id}`); + fnCache.set(id, fn); + return fn; +} + function portToAsyncIterable(port: MessagePort): AsyncIterable { const queue: T[] = []; let done = false; @@ -78,6 +93,14 @@ function portToAsyncIterable(port: MessagePort): AsyncIterable { }; } +function needsAsyncResolve(arg: unknown): boolean { + return arg instanceof MessagePort || isTaskArg(arg); +} + +function resolveArgs(args: unknown[]): unknown[] | Promise { + return args.some(needsAsyncResolve) ? Promise.all(args.map(resolveArg)) : args.map(deserializeArg); +} + async function resolveArg(arg: unknown): Promise { if (arg instanceof MessagePort) { return portToAsyncIterable(arg); @@ -86,16 +109,8 @@ async function resolveArg(arg: unknown): Promise { if (taskCache.has(arg.__task__)) { return taskCache.get(arg.__task__); } - // Resolve the task's own args recursively - const resolvedArgs = await Promise.all(arg.args.map(resolveArg)); - // Import the module and run the function - const url = arg.id.slice(0, arg.id.lastIndexOf('#')); - if (!imported.has(url)) { - await import(url); - imported.add(url); - } - const fn = registry.get(arg.id); - if (!fn) throw new Error(`Moroutine not found: ${arg.id}`); + const resolvedArgs = await resolveArgs(arg.args); + const fn = await resolveFn(arg.id); const value = await fn(...resolvedArgs); taskCache.set(arg.__task__, value); return value; @@ -106,16 +121,9 @@ async function resolveArg(arg: unknown): Promise { parentPort!.on('message', async (msg: { callId?: number; id: string; args: unknown[]; port?: MessagePort }) => { const { id, args, port } = msg; try { - const url = id.slice(0, id.lastIndexOf('#')); - if (!imported.has(url)) { - await import(url); - imported.add(url); - } - - const fn = registry.get(id); - if (!fn) throw new Error(`Moroutine not found: ${id}`); + const fn = await resolveFn(id); - const resolvedArgs = await Promise.all(args.map(resolveArg)); + const resolvedArgs = await resolveArgs(args); if (port) { let paused = false; -- 2.51.2