From f8079c982f3394cfef279a50fd96a5aa088b3aef Mon Sep 17 00:00:00 2001 From: Tomasz Andrzejak Date: Mon, 19 Oct 2026 10:47:16 +0200 Subject: [PATCH] perf: reuse splicer instances by fs scope Restore instance reuse to avoid repeated cold-splice costs and windows test timeouts in CI. --- src/splicer.js | 34 +++++++++++++++++++++++++++++----- test/splicer.js | 45 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 73 insertions(+), 6 deletions(-) diff --git a/src/splicer.js b/src/splicer.js index 21e08fd3..2a700a66 100644 --- a/src/splicer.js +++ b/src/splicer.js @@ -1,11 +1,13 @@ import { readFile, realpath, stat } from 'node:fs/promises'; -import { basename, dirname } from 'node:path'; +import { basename, dirname, toNamespacedPath } from 'node:path'; import { WASIShim } from '@bytecodealliance/preview2-shim/instantiation'; import { instantiate } from '../lib/spidermonkey-embedding-splicer.js'; -// Compiled modules can be shared, but instances must retain isolated preopens. +// Warmed instances retain large Wasm heaps, so bound the cached filesystem scopes. +const MAX_CACHED_SPLICERS = 2; const coreModules = new Map(); +const splicerCache = new Map(); function getCoreModule(name) { if (!coreModules.has(name)) { @@ -20,15 +22,37 @@ function getCoreModule(name) { export async function createSplicer(witPath) { const preopens = {}; let guestWitPath; + let scope; if (witPath != null) { const hostPath = await realpath(witPath); const isDirectory = (await stat(hostPath)).isDirectory(); preopens['/wit'] = isDirectory ? hostPath : dirname(hostPath); + scope = toNamespacedPath(preopens['/wit']); guestWitPath = isDirectory ? '/wit' : `/wit/${basename(hostPath)}`; } - const wasi = new WASIShim({ sandbox: { preopens } }); - const { splicer } = await instantiate(getCoreModule, wasi.getImportObject()); - return { splicer, witPath: guestWitPath }; + let splicerPromise = splicerCache.get(scope); + if (splicerPromise) { + splicerCache.delete(scope); + } else { + const wasi = new WASIShim({ sandbox: { preopens } }); + splicerPromise = instantiate(getCoreModule, wasi.getImportObject()).then( + ({ splicer }) => splicer, + ); + } + + splicerCache.set(scope, splicerPromise); + if (splicerCache.size > MAX_CACHED_SPLICERS) { + splicerCache.delete(splicerCache.keys().next().value); + } + + try { + return { splicer: await splicerPromise, witPath: guestWitPath }; + } catch (err) { + if (splicerCache.get(scope) === splicerPromise) { + splicerCache.delete(scope); + } + throw err; + } } diff --git a/test/splicer.js b/test/splicer.js index 72bad153..36215565 100644 --- a/test/splicer.js +++ b/test/splicer.js @@ -2,8 +2,9 @@ import { mkdir, mkdtemp, realpath, rm, symlink, writeFile } from 'node:fs/promis import { tmpdir } from 'node:os'; import { join, relative, toNamespacedPath } from 'node:path'; import { preopens } from '@bytecodealliance/preview2-shim/filesystem'; -import { afterAll, assert, beforeAll, expect, suite, test } from 'vitest'; +import { afterAll, assert, beforeAll, expect, suite, test, vi } from 'vitest'; +import * as generatedSplicer from '../lib/spidermonkey-embedding-splicer.js'; import { createSplicer } from '../src/splicer.js'; const emptyModule = Uint8Array.from([0, 97, 115, 109, 1, 0, 0, 0]); @@ -112,4 +113,46 @@ suite('Splicer filesystem', () => { readWorld(await createSplicer(toNamespacedPath(witFile)), 'first'); readWorld(await createSplicer(toNamespacedPath(firstDir)), 'dependent'); }); + + test('reuses concurrent calls with the same filesystem scope', async () => { + const [inline, repeated, file, directory] = await Promise.all([ + createSplicer(), + createSplicer(), + createSplicer(witFile), + createSplicer(firstDir), + ]); + assert.strictEqual(inline.splicer, repeated.splicer); + assert.strictEqual(file.splicer, directory.splicer); + assert.notStrictEqual(inline.splicer, file.splicer); + assert.equal(file.witPath, '/wit/world file.wit'); + assert.equal(directory.witPath, '/wit'); + }); + + test('evicts the least recently used scope without retargeting live instances', async () => { + const first = await createSplicer(firstDir); + const second = await createSplicer(secondDir); + assert.strictEqual((await createSplicer(firstDir)).splicer, first.splicer); + await createSplicer(); + assert.strictEqual((await createSplicer(firstDir)).splicer, first.splicer); + const replacement = await createSplicer(secondDir); + assert.notStrictEqual(replacement.splicer, second.splicer); + readWorld(first, 'first'); + readWorld(second, 'second'); + readWorld(replacement, 'second'); + assert.throws(() => readWorld(second, 'first'), /World `first` not found/); + }); + + test('does not cache failed instantiation', async () => { + const directory = await mkdtemp(join(tempDir, 'retry-')); + const error = new Error('Splicer initialization failed'); + const instantiate = vi.spyOn(generatedSplicer, 'instantiate') + .mockRejectedValueOnce(error); + try { + await expect(createSplicer(directory)).rejects.toBe(error); + await expect(createSplicer(directory)).resolves.toHaveProperty('splicer'); + expect(instantiate).toHaveBeenCalledTimes(2); + } finally { + instantiate.mockRestore(); + } + }); });