From 03588c1a83f597115676849af1d2bc8ee54fca7b Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Thu, 23 May 2024 14:18:19 -0400 Subject: [PATCH 1/6] test(xsnap): Add code for observing snapshot-related memory leaks Ref #9316 --- packages/xsnap/package.json | 1 + packages/xsnap/test/leakiness.mjs | 182 ++++++++++++++++++++++++++ packages/xsnap/test/leakiness.test.js | 20 +++ 3 files changed, 203 insertions(+) create mode 100644 packages/xsnap/test/leakiness.mjs create mode 100644 packages/xsnap/test/leakiness.test.js diff --git a/packages/xsnap/package.json b/packages/xsnap/package.json index ab4b4bb2d2d..b86929f7c45 100644 --- a/packages/xsnap/package.json +++ b/packages/xsnap/package.json @@ -42,6 +42,7 @@ }, "devDependencies": { "@endo/base64": "^1.0.5", + "@endo/nat": "^5.0.7", "@types/glob": "^8.1.0", "ava": "^5.3.0", "c8": "^9.1.0" diff --git a/packages/xsnap/test/leakiness.mjs b/packages/xsnap/test/leakiness.mjs new file mode 100644 index 00000000000..64b78fa7832 --- /dev/null +++ b/packages/xsnap/test/leakiness.mjs @@ -0,0 +1,182 @@ +/* global setTimeout, process */ + +// This file functions as both an ava-importable module and a standalone script. +// See below for usage detail about the latter. +import 'ses'; +import '@endo/eventual-send/shim.js'; +import 'data:text/javascript,try { lockdown(); } catch (_err) {}'; + +import * as proc from 'child_process'; +import * as os from 'os'; +import fs from 'fs'; +import { tmpName } from 'tmp'; +import { parseArgs } from 'util'; +import { isMainThread } from 'worker_threads'; + +import { Nat } from '@endo/nat'; +import { makePromiseKit } from '@endo/promise-kit'; + +import { xsnap } from '../src/xsnap.js'; + +import { options as makeXSnapOptions } from './message-tools.js'; + +const io = { spawn: proc.spawn, os: os.type(), fs, tmpName }; // WARNING: ambient + +/** @import {XSnapOptions} from '@agoric/xsnap/src/xsnap.js' */ + +/** + * Creates an Xsnap instance that responds to each inbound command by + * interpreting it as a decimal count of bytes to retain. + * + * @param {Partial} [xsnapOptions] + * @returns {Promise>>} + */ +export const makeRetentiveVat = async xsnapOptions => { + const vat = await xsnap({ ...makeXSnapOptions(io), ...xsnapOptions }); + // Retain data for each message in a new ArrayBuffer, populating each such + // buffer with globally descending binary64 values (as buffer space permits) + // to limit compressibility. + await vat.evaluate(` + const decoder = new TextDecoder(); + const buffers = []; + let x = Number.MAX_SAFE_INTEGER; + function handleCommand(message) { + const n = +decoder.decode(message); + const view = new DataView(new ArrayBuffer(n)); + for (let offset = 0; offset + 8 <= n; offset += 8) { + view.setFloat64(offset, x); + x -= 1; + } + buffers.push(view.buffer); + } + `); + return vat; +}; +harden(makeRetentiveVat); + +/** + * Spawns a heap-preserving sequence of Xsnap instances, each retaining + * approximately the same amount of additional data. + * + * @param {object} options + * @param {(newVat: Awaited>, loadedSnapshotStream: AsyncIterable | undefined) => Promise} [options.afterCommand] + * a callback to run after a vat handles its command, for e.g. interrogation and/or + * inserting delays between instances + * @param {number} options.chunkCount the number of instances to spawn + * @param {number} options.chunkSize the count of bytes to retain per instance + * @param {Partial} [options.xsnapOptions] + * @returns {Promise} + */ +export const spawnRetentiveVatSequence = async ({ + afterCommand, + chunkCount, + chunkSize, + xsnapOptions, +}) => { + await null; + /** @type {Awaited> | undefined} */ + let vat = undefined; + try { + for (let i = 0; i < chunkCount; i += 1) { + // Make a new vat, replacing a previous vat if present. + const oldVat = vat; + vat = await makeRetentiveVat({ + ...xsnapOptions, + snapshotStream: oldVat?.makeSnapshotStream(), + }); + await oldVat?.close(); + + // Issue the command. + const label = `command number ${i}`; + try { + await vat.issueStringCommand(`${chunkSize}`); + } catch (err) { + throw Error(`Error with ${label}`, { cause: err }); + } + await afterCommand?.(vat); + } + } finally { + await vat?.close(); + } +}; +harden(spawnRetentiveVatSequence); + +/** + * Spawns a heap-preserving sequence of Xsnap instances with intervening delays. + * + * @param {object} options + * @param {number} options.chunkCount + * @param {number} options.chunkSize + * @param {number} options.idleDuration + * @param {Partial} [options.xsnapOptions] + * @returns {Promise} + */ +const main = async ({ chunkCount, chunkSize, idleDuration, xsnapOptions }) => { + const afterCommand = async _vat => { + const { promise, resolve } = makePromiseKit(); + setTimeout(resolve, idleDuration); + return promise; + }; + return spawnRetentiveVatSequence({ + afterCommand, + chunkCount, + chunkSize, + xsnapOptions, + }); +}; + +const isEntryPoint = + !/[/]node_modules[/]/.test(process.argv[1]) && + // cf. https://github.com/avajs/ava/blob/f8bf00cd988b5e981b6c7d87523a1e0c5dc947c0/lib/worker/guard-environment.cjs + typeof process.send !== 'function' && + isMainThread !== false; +if (isEntryPoint) { + /** @typedef {{type: 'string' | 'boolean', short?: string, multiple?: boolean, default?: string | boolean | string[] | boolean[]}} ParseArgsOptionConfig */ + /** @type {Record} */ + const cliOptions = { + help: { type: 'boolean' }, + chunkCount: { + type: 'string', + default: '10', + }, + chunkSize: { + type: 'string', + default: '10000', + }, + idleDuration: { + type: 'string', + default: '10000', + }, + xsnapOptions: { + type: 'string', + default: '{ "snapshotUseFs": false }', + }, + }; + const { values: config } = parseArgs({ options: cliOptions }); + let chunkCount, chunkSize, idleDuration, xsnapOptions; + try { + if (config.help) throw Error(); + const parseNat = str => Nat(/[0-9]/.test(str || '') ? Number(str) : NaN); + chunkCount = Number(parseNat(config.chunkCount)); + chunkSize = Number(parseNat(config.chunkSize)); + idleDuration = Number(parseNat(config.idleDuration)); + xsnapOptions = JSON.parse(/** @type {string} */ (config.xsnapOptions)); + } catch (err) { + const log = config.help ? console.log : console.error; + if (!config.help) log(err.message); + log(`Usage: node [--inspect-brk] $script \\ + [--chunkCount N (default ${cliOptions.chunkCount.default})] \\ + [--chunkSize BYTE_COUNT (default ${cliOptions.chunkSize.default})] \\ + [--idleDuration MILLISECONDS (default ${cliOptions.idleDuration.default})] \\ + [--xsnapOptions JSON (default ${cliOptions.xsnapOptions.default})] + +Spawns a heap-preserving sequence of $chunkCount xsnap instances, each retaining +approximately $chunkSize additional bytes, waiting $idleDuration milliseconds +before spawning each instance's successor.`); + process.exit(64); + } + main({ chunkCount, chunkSize, idleDuration, xsnapOptions }).catch(err => { + console.error(err); + process.exitCode ||= 1; + }); +} diff --git a/packages/xsnap/test/leakiness.test.js b/packages/xsnap/test/leakiness.test.js new file mode 100644 index 00000000000..d713133bb71 --- /dev/null +++ b/packages/xsnap/test/leakiness.test.js @@ -0,0 +1,20 @@ +import test from 'ava'; +import { spawnRetentiveVatSequence } from './leakiness.mjs'; + +test('use pipes', async t => { + await spawnRetentiveVatSequence({ + chunkCount: 10, + chunkSize: 1000, + xsnapOptions: { snapshotUseFs: false }, + }); + t.pass(); +}); + +test('use temp files', async t => { + await spawnRetentiveVatSequence({ + chunkCount: 10, + chunkSize: 1000, + xsnapOptions: { snapshotUseFs: true }, + }); + t.pass(); +}); From 901b7064013a32601745c950e493ef6802c961b0 Mon Sep 17 00:00:00 2001 From: Mathieu Hofman Date: Thu, 23 May 2024 23:39:32 +0000 Subject: [PATCH 2/6] test(xsnap): Add failing snapshot leak test --- packages/xsnap/test/leakiness.mjs | 7 ++- packages/xsnap/test/leakiness.test.js | 90 ++++++++++++++++++++++++--- 2 files changed, 84 insertions(+), 13 deletions(-) diff --git a/packages/xsnap/test/leakiness.mjs b/packages/xsnap/test/leakiness.mjs index 64b78fa7832..96534df237b 100644 --- a/packages/xsnap/test/leakiness.mjs +++ b/packages/xsnap/test/leakiness.mjs @@ -80,9 +80,10 @@ export const spawnRetentiveVatSequence = async ({ for (let i = 0; i < chunkCount; i += 1) { // Make a new vat, replacing a previous vat if present. const oldVat = vat; + const snapshotStream = oldVat?.makeSnapshotStream(); vat = await makeRetentiveVat({ ...xsnapOptions, - snapshotStream: oldVat?.makeSnapshotStream(), + snapshotStream, }); await oldVat?.close(); @@ -93,7 +94,7 @@ export const spawnRetentiveVatSequence = async ({ } catch (err) { throw Error(`Error with ${label}`, { cause: err }); } - await afterCommand?.(vat); + await afterCommand?.(vat, snapshotStream); } } finally { await vat?.close(); @@ -112,7 +113,7 @@ harden(spawnRetentiveVatSequence); * @returns {Promise} */ const main = async ({ chunkCount, chunkSize, idleDuration, xsnapOptions }) => { - const afterCommand = async _vat => { + const afterCommand = async (_vat, _snapshotStream) => { const { promise, resolve } = makePromiseKit(); setTimeout(resolve, idleDuration); return promise; diff --git a/packages/xsnap/test/leakiness.test.js b/packages/xsnap/test/leakiness.test.js index d713133bb71..99468594852 100644 --- a/packages/xsnap/test/leakiness.test.js +++ b/packages/xsnap/test/leakiness.test.js @@ -1,20 +1,90 @@ +/** global FinalizationRegistry */ + import test from 'ava'; + +import process from 'node:process'; +import v8 from 'node:v8'; + +import engineGC from '@agoric/internal/src/lib-nodejs/engine-gc.js'; +import { waitUntilQuiescent } from '@agoric/internal/src/lib-nodejs/waitUntilQuiescent.js'; + import { spawnRetentiveVatSequence } from './leakiness.mjs'; -test('use pipes', async t => { - await spawnRetentiveVatSequence({ - chunkCount: 10, - chunkSize: 1000, - xsnapOptions: { snapshotUseFs: false }, +/** @import {XSnapOptions} from '@agoric/xsnap/src/xsnap.js' */ + +/** + * @param {import('ava').ExecutionContext} t + * @param {Partial} xsnapOptions + */ +const testRetention = async (t, xsnapOptions) => { + let snapshotsCreated = 0; + let snapshotsFreed = 0; + let lastVat = null; + /** @type {FinalizationRegistry} */ + const fr = new FinalizationRegistry(() => { + snapshotsFreed += 1; }); - t.pass(); -}); -test('use temp files', async t => { + t.plan(4); + + await waitUntilQuiescent(); + engineGC(); + if (process.env.TAKE_HEAP_SNAPSHOT) { + v8.writeHeapSnapshot( + `Heap-${process.pid}-${t.title}-${Date.now()}-before.heapsnapshot`, + ); + } + await spawnRetentiveVatSequence({ + afterCommand: async (vat, snapshotStream) => { + lastVat = vat; + if (snapshotStream) { + snapshotsCreated += 1; + fr.register(snapshotStream); + } + }, chunkCount: 10, chunkSize: 1000, - xsnapOptions: { snapshotUseFs: true }, + xsnapOptions, }); - t.pass(); + + t.truthy(lastVat); + t.true(snapshotsCreated >= 1); + + await waitUntilQuiescent(); + engineGC(); + await waitUntilQuiescent(); + if (process.env.TAKE_HEAP_SNAPSHOT) { + v8.writeHeapSnapshot( + `Heap-${process.pid}-${t.title}-${Date.now()}-after-tail.heapsnapshot`, + ); + } + + const snapshotsFreedWithLastVatAlive = snapshotsFreed; + + lastVat = null; + + await waitUntilQuiescent(); + engineGC(); + await waitUntilQuiescent(); + if (process.env.TAKE_HEAP_SNAPSHOT) { + v8.writeHeapSnapshot( + `Heap-${process.pid}-${t.title}-${Date.now()}-after-all.heapsnapshot`, + ); + } + + t.is(snapshotsFreed, snapshotsCreated, 'all snapshots freed'); + t.is( + snapshotsFreedWithLastVatAlive, + snapshotsFreed, + 'snapshots not tail retained', + ); +}; + +test.serial.failing('snapshot GC with pipes', testRetention, { + snapshotUseFs: false, +}); + +test.serial.failing('snapshot GC with temp files', testRetention, { + snapshotUseFs: true, }); From ca995f6cd169d835b6b44956674defa8d7b30e40 Mon Sep 17 00:00:00 2001 From: Mathieu Hofman Date: Thu, 23 May 2024 23:41:28 +0000 Subject: [PATCH 3/6] fix(xsnap): Release snapshot after load to avoid leak Fix #9316 --- packages/xsnap/src/xsnap.js | 14 +++++++++++--- packages/xsnap/test/leakiness.test.js | 4 ++-- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/packages/xsnap/src/xsnap.js b/packages/xsnap/src/xsnap.js index 2c002aa23e3..72487e0d7c1 100644 --- a/packages/xsnap/src/xsnap.js +++ b/packages/xsnap/src/xsnap.js @@ -77,6 +77,7 @@ const safeHintFromDescription = description => * @property {Record} [env] */ export async function xsnap(options) { + let { snapshotStream } = options; const { os, spawn, @@ -86,7 +87,6 @@ export async function xsnap(options) { debug = false, netstringMaxChunkSize = undefined, parserBufferSize = undefined, - snapshotStream, snapshotDescription = snapshotStream && 'unknown', snapshotUseFs = false, stdout = 'ignore', @@ -117,7 +117,11 @@ export async function xsnap(options) { }); const afterSpawn = async () => {}; - const cleanup = async () => fs.unlink(snapPath); + const cleanup = async () => { + // @ts-expect-error this frees snapshotStream; it won't be used again + snapshotStream = null; + return fs.unlink(snapPath); + }; try { const tmpSnap = await fs.open(snapPath, 'w'); @@ -141,7 +145,11 @@ export async function xsnap(options) { const makeLoadSnapshotHandlerWithPipe = async () => { let done = Promise.resolve(); - const cleanup = async () => done; + const cleanup = async () => { + await done; + // @ts-expect-error this frees snapshotStream; it won't be used again + snapshotStream = null; + }; /** @param {Writable} loadSnapshotsStream */ const afterSpawn = async loadSnapshotsStream => { diff --git a/packages/xsnap/test/leakiness.test.js b/packages/xsnap/test/leakiness.test.js index 99468594852..903ecc3cdf4 100644 --- a/packages/xsnap/test/leakiness.test.js +++ b/packages/xsnap/test/leakiness.test.js @@ -81,10 +81,10 @@ const testRetention = async (t, xsnapOptions) => { ); }; -test.serial.failing('snapshot GC with pipes', testRetention, { +test.serial('snapshot GC with pipes', testRetention, { snapshotUseFs: false, }); -test.serial.failing('snapshot GC with temp files', testRetention, { +test.serial('snapshot GC with temp files', testRetention, { snapshotUseFs: true, }); From e15394d375b109cba44bf606f35d7424924d7d6b Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Fri, 24 May 2024 12:34:47 -0400 Subject: [PATCH 4/6] refactor(xsnap): Propagate snapshot by argument rather than closure This removes the need to explicitly clear its value. --- packages/xsnap/src/xsnap.js | 31 ++++++++++--------------------- 1 file changed, 10 insertions(+), 21 deletions(-) diff --git a/packages/xsnap/src/xsnap.js b/packages/xsnap/src/xsnap.js index 72487e0d7c1..f5fe0f945e1 100644 --- a/packages/xsnap/src/xsnap.js +++ b/packages/xsnap/src/xsnap.js @@ -77,7 +77,6 @@ const safeHintFromDescription = description => * @property {Record} [env] */ export async function xsnap(options) { - let { snapshotStream } = options; const { os, spawn, @@ -87,6 +86,7 @@ export async function xsnap(options) { debug = false, netstringMaxChunkSize = undefined, parserBufferSize = undefined, + snapshotStream, snapshotDescription = snapshotStream && 'unknown', snapshotUseFs = false, stdout = 'ignore', @@ -108,8 +108,8 @@ export async function xsnap(options) { /** @type {(opts: import('tmp').TmpNameOptions) => Promise} */ const ptmpName = fs.tmpName && promisify(fs.tmpName); - const makeLoadSnapshotHandlerWithFS = async () => { - assert(snapshotStream); + const makeLoadSnapshotHandlerWithFS = async sourceBytes => { + assert(sourceBytes); const snapPath = await ptmpName({ template: `load-snapshot-${safeHintFromDescription( snapshotDescription, @@ -117,18 +117,11 @@ export async function xsnap(options) { }); const afterSpawn = async () => {}; - const cleanup = async () => { - // @ts-expect-error this frees snapshotStream; it won't be used again - snapshotStream = null; - return fs.unlink(snapPath); - }; + const cleanup = async () => fs.unlink(snapPath); try { const tmpSnap = await fs.open(snapPath, 'w'); - await tmpSnap.writeFile( - // @ts-expect-error incorrect typings, does support AsyncIterable - snapshotStream, - ); + await tmpSnap.writeFile(sourceBytes); await tmpSnap.close(); } catch (e) { await cleanup(); @@ -142,21 +135,17 @@ export async function xsnap(options) { }); }; - const makeLoadSnapshotHandlerWithPipe = async () => { + const makeLoadSnapshotHandlerWithPipe = async sourceBytes => { let done = Promise.resolve(); - const cleanup = async () => { - await done; - // @ts-expect-error this frees snapshotStream; it won't be used again - snapshotStream = null; - }; + const cleanup = async () => done; /** @param {Writable} loadSnapshotsStream */ const afterSpawn = async loadSnapshotsStream => { - assert(snapshotStream); + assert(sourceBytes); const destStream = loadSnapshotsStream; - const sourceStream = Readable.from(snapshotStream); + const sourceStream = Readable.from(sourceBytes); sourceStream.pipe(destStream, { end: false }); done = finished(sourceStream); @@ -187,7 +176,7 @@ export async function xsnap(options) { let loadSnapshotHandler = await (snapshotStream && (snapshotUseFs ? makeLoadSnapshotHandlerWithFS - : makeLoadSnapshotHandlerWithPipe)()); + : makeLoadSnapshotHandlerWithPipe)(snapshotStream)); let args = [name]; From c8f0d31de07571e58d19567a23023aaeb2ab6195 Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Fri, 24 May 2024 12:46:52 -0400 Subject: [PATCH 5/6] chore(xsnap): Simplify snapshot helpers --- packages/xsnap/src/xsnap.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/xsnap/src/xsnap.js b/packages/xsnap/src/xsnap.js index f5fe0f945e1..fac8b1c7fee 100644 --- a/packages/xsnap/src/xsnap.js +++ b/packages/xsnap/src/xsnap.js @@ -109,7 +109,6 @@ export async function xsnap(options) { const ptmpName = fs.tmpName && promisify(fs.tmpName); const makeLoadSnapshotHandlerWithFS = async sourceBytes => { - assert(sourceBytes); const snapPath = await ptmpName({ template: `load-snapshot-${safeHintFromDescription( snapshotDescription, @@ -140,11 +139,8 @@ export async function xsnap(options) { const cleanup = async () => done; - /** @param {Writable} loadSnapshotsStream */ - const afterSpawn = async loadSnapshotsStream => { - assert(sourceBytes); - const destStream = loadSnapshotsStream; - + /** @param {Writable} destStream */ + const afterSpawn = async destStream => { const sourceStream = Readable.from(sourceBytes); sourceStream.pipe(destStream, { end: false }); From c9db8f81affe33dd8caff2f0330809b81db4a17e Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Fri, 24 May 2024 13:31:20 -0400 Subject: [PATCH 6/6] refactor(xsnap): Abstract snapshot loaders into top-level helpers --- packages/xsnap/src/xsnap.js | 160 +++++++++++++++++++++--------------- 1 file changed, 93 insertions(+), 67 deletions(-) diff --git a/packages/xsnap/src/xsnap.js b/packages/xsnap/src/xsnap.js index fac8b1c7fee..103203e07b6 100644 --- a/packages/xsnap/src/xsnap.js +++ b/packages/xsnap/src/xsnap.js @@ -56,6 +56,81 @@ function echoCommand(arg) { const safeHintFromDescription = description => description.replaceAll(/[^a-zA-Z0-9_.-]/g, '-'); +/** + * @typedef {object} SnapshotLoader + * @property {string} snapPath + * where XS can load the snapshot from, either a filesystem path or a string + * like `@${fileDescriptorNumber}:${readableDescription}` + * @property {(destStream?: Writable) => Promise} afterSpawn + * callback for providing a destination stream to which the data should be + * piped (only relevant for a stream-based loader) + * @property {() => Promise} cleanup + * callback to free resources when the loader is no longer needed + */ + +/** + * @callback MakeSnapshotLoader + * @param {AsyncIterable} sourceBytes + * @param {string} description + * @param {{fs: Pick, ptmpName: (opts: import('tmp').TmpNameOptions) => Promise}} ioPowers + * @returns {Promise} + */ + +/** @type {MakeSnapshotLoader} */ +const makeSnapshotLoaderWithFS = async ( + sourceBytes, + description, + { fs, ptmpName }, +) => { + const snapPath = await ptmpName({ + template: `load-snapshot-${safeHintFromDescription(description)}-XXXXXX.xss`, + }); + + const afterSpawn = async () => {}; + const cleanup = async () => fs.unlink(snapPath); + + try { + const tmpSnap = await fs.open(snapPath, 'w'); + // @ts-expect-error incorrect typings; writeFile does support AsyncIterable + await tmpSnap.writeFile(sourceBytes); + await tmpSnap.close(); + } catch (e) { + await cleanup(); + throw e; + } + + return harden({ + snapPath, + afterSpawn, + cleanup, + }); +}; + +/** @type {MakeSnapshotLoader} */ +const makeSnapshotLoaderWithPipe = async ( + sourceBytes, + description, + _ioPowers, +) => { + let done = Promise.resolve(); + + const cleanup = async () => done; + + const afterSpawn = async destStream => { + const sourceStream = Readable.from(sourceBytes); + sourceStream.pipe(destStream, { end: false }); + + done = finished(sourceStream); + void done.catch(noop).then(() => sourceStream.unpipe(destStream)); + }; + + return harden({ + snapPath: `@${SNAPSHOT_LOAD_FD}:${safeHintFromDescription(description)}`, + afterSpawn, + cleanup, + }); +}; + /** * @param {XSnapOptions} options * @@ -87,7 +162,7 @@ export async function xsnap(options) { netstringMaxChunkSize = undefined, parserBufferSize = undefined, snapshotStream, - snapshotDescription = snapshotStream && 'unknown', + snapshotDescription = 'unknown', snapshotUseFs = false, stdout = 'ignore', stderr = 'ignore', @@ -105,58 +180,6 @@ export async function xsnap(options) { throw Error(`xsnap does not support platform ${os}`); } - /** @type {(opts: import('tmp').TmpNameOptions) => Promise} */ - const ptmpName = fs.tmpName && promisify(fs.tmpName); - - const makeLoadSnapshotHandlerWithFS = async sourceBytes => { - const snapPath = await ptmpName({ - template: `load-snapshot-${safeHintFromDescription( - snapshotDescription, - )}-XXXXXX.xss`, - }); - - const afterSpawn = async () => {}; - const cleanup = async () => fs.unlink(snapPath); - - try { - const tmpSnap = await fs.open(snapPath, 'w'); - await tmpSnap.writeFile(sourceBytes); - await tmpSnap.close(); - } catch (e) { - await cleanup(); - throw e; - } - - return harden({ - snapPath, - afterSpawn, - cleanup, - }); - }; - - const makeLoadSnapshotHandlerWithPipe = async sourceBytes => { - let done = Promise.resolve(); - - const cleanup = async () => done; - - /** @param {Writable} destStream */ - const afterSpawn = async destStream => { - const sourceStream = Readable.from(sourceBytes); - sourceStream.pipe(destStream, { end: false }); - - done = finished(sourceStream); - void done.catch(noop).then(() => sourceStream.unpipe(destStream)); - }; - - return harden({ - snapPath: `@${SNAPSHOT_LOAD_FD}:${safeHintFromDescription( - snapshotDescription, - )}`, - afterSpawn, - cleanup, - }); - }; - let bin = new URL( `../xsnap-native/xsnap/build/bin/${platform}/${ debug ? 'debug' : 'release' @@ -169,15 +192,18 @@ export async function xsnap(options) { assert(!/^-/.test(name), `name '${name}' cannot start with hyphen`); - let loadSnapshotHandler = await (snapshotStream && - (snapshotUseFs - ? makeLoadSnapshotHandlerWithFS - : makeLoadSnapshotHandlerWithPipe)(snapshotStream)); + /** @type {(opts: import('tmp').TmpNameOptions) => Promise} */ + const ptmpName = fs.tmpName && promisify(fs.tmpName); + const makeSnapshotLoader = snapshotUseFs + ? makeSnapshotLoaderWithFS + : makeSnapshotLoaderWithPipe; + let snapshotLoader = await (snapshotStream && + makeSnapshotLoader(snapshotStream, snapshotDescription, { fs, ptmpName })); let args = [name]; - if (loadSnapshotHandler) { - args.push('-r', loadSnapshotHandler.snapPath); + if (snapshotLoader) { + args.push('-r', snapshotLoader.snapPath); } if (meteringLimit) { @@ -248,13 +274,13 @@ export async function xsnap(options) { const snapshotSaveStream = xsnapProcessStdio[SNAPSHOT_SAVE_FD]; const snapshotLoadStream = xsnapProcessStdio[SNAPSHOT_LOAD_FD]; - await loadSnapshotHandler?.afterSpawn(snapshotLoadStream); + await snapshotLoader?.afterSpawn(snapshotLoadStream); - if (loadSnapshotHandler) { + if (snapshotLoader) { void vatExit.promise.catch(noop).then(() => { - if (loadSnapshotHandler) { - const { cleanup } = loadSnapshotHandler; - loadSnapshotHandler = undefined; + if (snapshotLoader) { + const { cleanup } = snapshotLoader; + snapshotLoader = undefined; return cleanup(); } }); @@ -276,9 +302,9 @@ export async function xsnap(options) { async function runToIdle() { for await (const _ of forever) { const iteration = await messagesFromXsnap.next(undefined); - if (loadSnapshotHandler) { - const { cleanup } = loadSnapshotHandler; - loadSnapshotHandler = undefined; + if (snapshotLoader) { + const { cleanup } = snapshotLoader; + snapshotLoader = undefined; await cleanup(); } if (iteration.done) {