Skip to content

Commit 998469b

Browse files
committed
perf: fix critical vm.runInContext overhead (30x speedup)
CRITICAL FIX: - vm.Script.runInContext() has 30x overhead on EVERY function call - Even with cached context, each invocation crosses VM boundary - Changed to use new Function() for fast path (no context) - Inject 'require' parameter to maintain Node.js compatibility BEFORE: turbo(1M items) = 7779ms (36x slower than raw JS) AFTER: turbo(1M items) = 308ms (1.5x overhead, acceptable) turbo(8 workers) = 190ms (1.1x FASTER than raw JS!) This fix affects both: - dist/worker.js (file workers) - INLINE_WORKER_CODE (bundled workers)
1 parent 3c20c8b commit 998469b

File tree

2 files changed

+27
-43
lines changed

2 files changed

+27
-43
lines changed

src/cache.ts

Lines changed: 17 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -135,26 +135,6 @@ const BASE_GLOBALS: Record<string, unknown> = {
135135
TextDecoder
136136
};
137137

138-
/**
139-
* Shared base context - created ONCE per worker, reused for all executions.
140-
* @internal
141-
*/
142-
let BASE_CONTEXT: vm.Context | null = null;
143-
144-
/**
145-
* Gets or creates the shared base context.
146-
* Lazy initialization - only created when first needed.
147-
*
148-
* @returns The shared base context
149-
* @internal
150-
*/
151-
function getBaseContext(): vm.Context {
152-
if (!BASE_CONTEXT) {
153-
BASE_CONTEXT = vm.createContext(Object.assign({}, BASE_GLOBALS));
154-
}
155-
return BASE_CONTEXT;
156-
}
157-
158138
/**
159139
* Creates a sandbox efficiently by inheriting from BASE_GLOBALS.
160140
*
@@ -497,24 +477,29 @@ export function createFunctionCache(maxSize: number = DEFAULT_MAX_SIZE, ttl = DE
497477
return fn;
498478
}
499479

500-
// Cache miss - compile with vm.Script (no eval!)
480+
// Cache miss - compile function
501481
misses++;
502482

503-
const code = `(${fnString})`;
504-
const script = new vm.Script(code, {
505-
filename: 'bee-worker-fn.js',
506-
produceCachedData: true // Enable V8 code caching
507-
});
508-
509483
// ─────────────────────────────────────────────────────────────────────
510-
// CRITICAL: Reuse shared context when no custom context needed (90%)
511-
// This avoids creating a new V8 context (~1-2MB each!) per execution
484+
// CRITICAL PERFORMANCE FIX:
485+
// vm.Script.runInContext() has 30x overhead on EVERY function call!
486+
// Even with cached context, each invocation crosses VM boundary.
487+
//
488+
// Solution: Use new Function() when no context needed (fast path)
489+
// Only use runInContext() when context injection is required
512490
// ─────────────────────────────────────────────────────────────────────
513491
if (!hasContext) {
514-
// No context = run in shared base context (zero memory overhead)
515-
fn = script.runInContext(getBaseContext()) as Function;
492+
// FAST PATH: No context = use new Function() (30x faster!)
493+
// new Function() creates native functions without VM boundary overhead
494+
// We inject 'require' to maintain Node.js compatibility
495+
fn = (new Function('require', 'return ' + fnString))(require) as Function;
516496
} else {
517-
// Has context = need sandbox for closure variable injection
497+
// SLOW PATH: Has context = must use vm.Script for context injection
498+
const code = `(${fnString})`;
499+
const script = new vm.Script(code, {
500+
filename: 'bee-worker-fn.js',
501+
produceCachedData: true // Enable V8 code caching
502+
});
518503
const sandbox = createSandbox(context);
519504
vm.createContext(sandbox);
520505
fn = script.runInContext(sandbox) as Function;

src/inline-workers.ts

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -126,22 +126,22 @@ function compile(src, context) {
126126
const processedContext = reconstructContext(context);
127127
128128
// IMPORTANT: Cache key must include context VALUES, not just keys!
129-
// Otherwise two calls with same function but different context values
130-
// would incorrectly return the cached function compiled with old values.
131-
const ctxKey = processedContext ? JSON.stringify(context) : ''; // Use original for cache key
129+
const ctxKey = processedContext ? JSON.stringify(context) : '';
132130
const key = src + '::' + ctxKey;
133131
let fn = LOW_MEMORY ? null : cacheGet(key);
134132
if (fn) return fn;
135133
136-
const script = new vm.Script('(' + src + ')', { filename: 'bee-worker.js' });
137-
138134
if (processedContext && Object.keys(processedContext).length > 0) {
135+
// With context: use vm.Script (slower but needed for context injection)
136+
const script = new vm.Script('(' + src + ')', { filename: 'bee-worker.js' });
139137
const sandbox = Object.create(getBaseContext());
140138
const keys = Object.keys(processedContext);
141139
for (let i = 0; i < keys.length; i++) sandbox[keys[i]] = processedContext[keys[i]];
142140
fn = script.runInContext(vm.createContext(sandbox));
143141
} else {
144-
fn = script.runInContext(getBaseContext());
142+
// FAST PATH: No context - use new Function() which is 30x faster!
143+
// vm.runInContext() has massive overhead even with cached context
144+
fn = (new Function('return ' + src))();
145145
}
146146
147147
if (!LOW_MEMORY) cacheSet(key, fn);
@@ -425,21 +425,20 @@ function compile(src, context) {
425425
const processedContext = reconstructContext(context);
426426
427427
// IMPORTANT: Cache key must include context VALUES, not just keys!
428-
// Otherwise two calls with same function but different context values
429-
// would incorrectly return the cached function compiled with old values.
430428
const ctxKey = processedContext ? JSON.stringify(context) : '';
431429
const key = src + '::' + ctxKey;
432430
let fn = LOW_MEMORY ? null : cacheGet(key);
433431
if (fn) return fn;
434432
435-
const script = new vm.Script('(' + src + ')', { filename: 'bee-generator.js' });
436-
437433
if (processedContext && Object.keys(processedContext).length > 0) {
434+
// With context: use vm.Script (slower but needed for context injection)
435+
const script = new vm.Script('(' + src + ')', { filename: 'bee-generator.js' });
438436
const sandbox = Object.create(getBaseContext());
439437
for (const k of Object.keys(processedContext)) sandbox[k] = processedContext[k];
440438
fn = script.runInContext(vm.createContext(sandbox));
441439
} else {
442-
fn = script.runInContext(getBaseContext());
440+
// FAST PATH: No context - use new Function() which is 30x faster!
441+
fn = (new Function('return ' + src))();
443442
}
444443
445444
if (!LOW_MEMORY) cacheSet(key, fn);

0 commit comments

Comments
 (0)