Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 29 additions & 5 deletions src/splicer.js
Original file line number Diff line number Diff line change
@@ -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)) {
Expand All @@ -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;
}
}
45 changes: 44 additions & 1 deletion test/splicer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
Expand Down Expand Up @@ -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();
}
});
});
Loading