Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(swingset): boot xs-worker vats using snapshots from snapstore #2370

Merged
merged 3 commits into from
Apr 1, 2021
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
4 changes: 3 additions & 1 deletion packages/SwingSet/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"scripts": {
"build": "exit 0",
"test": "ava",
"test:xs": "exit 0",
"test:xs": "SWINGSET_WORKER_TYPE=xs-worker ava",
"test:xs-worker": "ava test/workers/test-worker.js -m 'xs vat manager'",
"pretty-fix": "prettier --write '**/*.js'",
"pretty-check": "prettier --check '**/*.js'",
Expand Down Expand Up @@ -53,13 +53,15 @@
"@babel/core": "^7.5.0",
"@babel/generator": "^7.6.4",
"@endo/base64": "^0.1.0",
"@types/tmp": "^0.2.0",
"anylogger": "^0.21.0",
"esm": "^3.2.5",
"re2": "^1.10.5",
"rollup": "^1.23.1",
"rollup-plugin-node-resolve": "^5.2.0",
"semver": "^6.3.0",
"ses": "^0.12.6",
"tmp": "^0.2.1",
"yargs": "^14.2.0"
},
"files": [
Expand Down
80 changes: 62 additions & 18 deletions packages/SwingSet/src/controller.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/* global require */
import fs from 'fs';
import path from 'path';
import process from 'process';
import re2 from 're2';
import { spawn } from 'child_process';
Expand All @@ -9,14 +10,15 @@ import * as babelCore from '@babel/core';
import * as babelParser from '@agoric/babel-parser';
import babelGenerate from '@babel/generator';
import anylogger from 'anylogger';
import { tmpName } from 'tmp';

import { assert, details as X } from '@agoric/assert';
import { isTamed, tameMetering } from '@agoric/tame-metering';
import { importBundle } from '@agoric/import-bundle';
import { initSwingStore } from '@agoric/swing-store-simple';
import { makeMeteringTransformer } from '@agoric/transform-metering';
import { makeTransform } from '@agoric/transform-eventual-send';
import { xsnap } from '@agoric/xsnap';
import { xsnap, makeSnapstore } from '@agoric/xsnap';

import { WeakRef, FinalizationRegistry } from './weakref';
import { startSubprocessWorker } from './spawnSubprocessWorker';
Expand All @@ -42,6 +44,57 @@ function unhandledRejectionHandler(e) {
console.error('UnhandledPromiseRejectionWarning:', e);
}

export function makeStartXSnap(bundles, { snapstorePath, env }) {
const xsnapOpts = {
os: osType(),
spawn,
stdout: 'inherit',
stderr: 'inherit',
debug: !!env.XSNAP_DEBUG,
};

let snapStore;

if (snapstorePath) {
fs.mkdirSync(snapstorePath, { recursive: true });

snapStore = makeSnapstore(snapstorePath, {
tmpName,
existsSync: fs.existsSync,
createReadStream: fs.createReadStream,
createWriteStream: fs.createWriteStream,
rename: fs.promises.rename,
unlink: fs.promises.unlink,
resolve: path.resolve,
});
}

let supervisorHash = '';
return async function startXSnap(name, handleCommand) {
if (supervisorHash) {
return snapStore.load(supervisorHash, async snapshot => {
dckc marked this conversation as resolved.
Show resolved Hide resolved
const xs = xsnap({ snapshot, name, handleCommand, ...xsnapOpts });
await xs.evaluate('null'); // ensure that spawn is done
return xs;
});
}
const worker = xsnap({ handleCommand, name, ...xsnapOpts });

for (const bundle of bundles) {
assert(
bundle.moduleFormat === 'getExport',
X`unexpected: ${bundle.moduleFormat}`,
);
// eslint-disable-next-line no-await-in-loop
await worker.evaluate(`(${bundle.source}\n)()`.trim());
}
if (snapStore) {
supervisorHash = await snapStore.save(async fn => worker.snapshot(fn));
}
return worker;
};
}

export async function makeSwingsetController(
hostStorage = initSwingStore().storage,
deviceEndowments = {},
Expand All @@ -59,6 +112,7 @@ export async function makeSwingsetController(
slogCallbacks,
slogFile,
testTrackDecref,
snapstorePath,
} = runtimeOptions;
if (typeof Compartment === 'undefined') {
throw Error('SES must be installed before calling makeSwingsetController');
Expand Down Expand Up @@ -177,23 +231,11 @@ export async function makeSwingsetController(
return startSubprocessWorker(process.execPath, ['-r', 'esm', supercode]);
}

const startXSnap = (name, handleCommand) => {
const worker = xsnap({
os: osType(),
spawn,
handleCommand,
name,
stdout: 'inherit',
stderr: 'inherit',
debug: !!env.XSNAP_DEBUG,
});

const bundles = {
lockdown: JSON.parse(hostStorage.get('lockdownBundle')),
supervisor: JSON.parse(hostStorage.get('supervisorBundle')),
};
return harden({ worker, bundles });
};
const bundles = [
JSON.parse(hostStorage.get('lockdownBundle')),
JSON.parse(hostStorage.get('supervisorBundle')),
];
const startXSnap = makeStartXSnap(bundles, { snapstorePath, env });

const slogF =
slogFile && (await fs.createWriteStream(slogFile, { flags: 'a' })); // append
Expand Down Expand Up @@ -323,12 +365,14 @@ export async function buildVatController(
debugPrefix,
slogCallbacks,
testTrackDecref,
snapstorePath,
} = runtimeOptions;
const actualRuntimeOptions = {
verbose,
debugPrefix,
testTrackDecref,
slogCallbacks,
snapstorePath,
};
const initializationOptions = { verbose, kernelBundles };
let bootstrapResult;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const decoder = new TextDecoder();
* @param {{
* allVatPowers: VatPowers,
* kernelKeeper: KernelKeeper,
* startXSnap: (name: string, handleCommand: SyncHandler) => { worker: XSnap, bundles: Record<string, ExportBundle> },
* startXSnap: (name: string, handleCommand: SyncHandler) => Promise<XSnap>,
* testLog: (...args: unknown[]) => void,
* decref: (vatID: unknown, vref: unknown, count: number) => void,
* }} tools
Expand Down Expand Up @@ -128,15 +128,7 @@ export function makeXsSubprocessFactory({
}

// start the worker and establish a connection
const { worker, bundles } = startXSnap(`${vatID}:${name}`, handleCommand);
for await (const [it, superCode] of Object.entries(bundles)) {
parentLog(vatID, 'eval bundle', it);
assert(
superCode.moduleFormat === 'getExport',
X`${it} unexpected: ${superCode.moduleFormat}`,
);
await worker.evaluate(`(${superCode.source}\n)()`.trim());
}
const worker = await startXSnap(`${vatID}:${name}`, handleCommand);

/** @type { (item: Tagged) => Promise<CrankResults> } */
async function issueTagged(item) {
Expand Down
2 changes: 1 addition & 1 deletion packages/xsnap/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"parsers": {
"js": "mjs"
},
"main": "./src/xsnap.js",
"main": "./src/index.js",
"bin": {
"ava-xs": "./src/ava-xs.js",
"xsrepl": "./src/xsrepl"
Expand Down
2 changes: 2 additions & 0 deletions packages/xsnap/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { xsnap } from './xsnap';
export { makeSnapstore } from './snapStore';
129 changes: 129 additions & 0 deletions packages/xsnap/src/snapStore.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// @ts-check
import { createHash } from 'crypto';
import { pipeline } from 'stream';
import { createGzip, createGunzip } from 'zlib';
import { assert, details as d } from '@agoric/assert';
import { promisify } from 'util';

const pipe = promisify(pipeline);

const { freeze } = Object;

/**
* @param {string} root
* @param {{
* tmpName: typeof import('tmp').tmpName,
* existsSync: typeof import('fs').existsSync
* createReadStream: typeof import('fs').createReadStream,
* createWriteStream: typeof import('fs').createWriteStream,
* resolve: typeof import('path').resolve,
* rename: typeof import('fs').promises.rename,
* unlink: typeof import('fs').promises.unlink,
* }} io
*/
export function makeSnapstore(
root,
{
tmpName,
existsSync,
createReadStream,
createWriteStream,
resolve,
rename,
unlink,
},
) {
/** @type {(opts: unknown) => Promise<string>} */
const ptmpName = promisify(tmpName);
const tmpOpts = { tmpdir: root, template: 'tmp-XXXXXX.xss' };
/**
* @param { (name: string) => Promise<T> } thunk
* @returns { Promise<T> }
* @template T
*/
async function withTempName(thunk) {
const name = await ptmpName(tmpOpts);
let result;
try {
result = await thunk(name);
} finally {
try {
await unlink(name);
} catch (ignore) {
// ignore
}
}
return result;
}

/**
* @param {string} dest
* @param { (name: string) => Promise<T> } thunk
* @returns { Promise<T> }
* @template T
*/
async function atomicWrite(dest, thunk) {
const tmp = await ptmpName(tmpOpts);
let result;
try {
result = await thunk(tmp);
await rename(tmp, resolve(root, dest));
} finally {
try {
await unlink(tmp);
} catch (ignore) {
// ignore
}
}
return result;
}

/** @type {(input: string, f: NodeJS.ReadWriteStream, output: string) => Promise<void>} */
async function filter(input, f, output) {
const source = createReadStream(input);
const destination = createWriteStream(output);
await pipe(source, f, destination);
}

/** @type {(filename: string) => Promise<string>} */
async function fileHash(filename) {
const hash = createHash('sha256');
const input = createReadStream(filename);
await pipe(input, hash);
return hash.digest('hex');
}

/**
* @param {(fn: string) => Promise<void>} saveRaw
* @returns { Promise<string> } sha256 hash of (uncompressed) snapshot
*/
async function save(saveRaw) {
return withTempName(async snapFile => {
await saveRaw(snapFile);
const h = await fileHash(snapFile);
if (existsSync(`${h}.gz`)) return h;
await atomicWrite(`${h}.gz`, gztmp =>
filter(snapFile, createGzip(), gztmp),
);
return h;
});
}

/**
* @param {string} hash
* @param {(fn: string) => Promise<T>} loadRaw
* @template T
*/
async function load(hash, loadRaw) {
return withTempName(async raw => {
await filter(resolve(root, `${hash}.gz`), createGunzip(), raw);
const actual = await fileHash(raw);
assert(actual === hash, d`actual hash ${actual} !== expected ${hash}`);
// be sure to await loadRaw before exiting withTempName
const result = await loadRaw(raw);
return result;
});
}

return freeze({ load, save });
}
Loading