From 4793f88e812e83740fd30a2e3ea12b7d407340ff Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Tue, 9 Feb 2021 17:09:38 -0600 Subject: [PATCH] test: vat warehouse for LRU demand paged vats - add append method to target vat - expose deliverToVat rather than provideVatManager - persist vat details to storage - console.log -> t.log - update API: startingKernelState, CrankResults --- .../test/workers/test-vatwarehouse.js | 379 ++++++++++++++++++ packages/SwingSet/test/workers/vat-target.js | 7 + 2 files changed, 386 insertions(+) create mode 100644 packages/SwingSet/test/workers/test-vatwarehouse.js diff --git a/packages/SwingSet/test/workers/test-vatwarehouse.js b/packages/SwingSet/test/workers/test-vatwarehouse.js new file mode 100644 index 000000000000..5563751a9cc0 --- /dev/null +++ b/packages/SwingSet/test/workers/test-vatwarehouse.js @@ -0,0 +1,379 @@ +/* global process, require, __dirname */ +// @ts-check +import path from 'path'; + +// eslint-disable-next-line import/order +import { test } from '../../tools/prepare-test-env-ava'; +import { assert, q, details as d } from '@agoric/assert'; +import { initSwingStore } from '@agoric/swing-store-simple'; +import bundleSource from '@agoric/bundle-source'; +import { Remotable, getInterfaceOf } from '@agoric/marshal'; + +import { makeXsSubprocessFactory } from '../../src/kernel/vatManager/manager-subprocess-xsnap'; +import makeKernelKeeper from '../../src/kernel/state/kernelKeeper'; +import { wrapStorage } from '../../src/kernel/state/storageWrapper'; + +import { loadBasedir } from '../../src'; +import { makeStartXSnap } from '../../src/controller'; +import { makeVatTranslators } from '../../src/kernel/vatTranslator'; + +/** + * @param { ReturnType } kernelKeeper + * @param { SwingStore['storage'] } storage + * @param { Record } factories + * @param { (vatID: string, translators: unknown) => VatSyscallHandler } buildVatSyscallHandler + * @param {{ sizeHint?: number }=} policyOptions + * + * @typedef { import('../../src/kernel/vatManager/manager-subprocess-xsnap').VatManagerFactory } VatManagerFactory + * @typedef { { + * replayTranscript: () => Promise, + * setVatSyscallHandler: (handler: VatSyscallHandler) => void, + * deliver: (d: Tagged) => Promise, + * shutdown: () => Promise, + * } } VatManager + * + * @typedef { ReturnType } SwingStore + * @typedef {(syscall: Tagged) => ['error', string] | ['ok', null] | ['ok', Capdata]} VatSyscallHandler + * @typedef {{ body: string, slots: unknown[] }} Capdata + * @typedef { [unknown, ...unknown[]] } Tagged + * @typedef { { moduleFormat: string }} Bundle + */ +export function makeVatWarehouse( + kernelKeeper, + storage, + factories, + buildVatSyscallHandler, + policyOptions, +) { + const { sizeHint = 10 } = policyOptions || {}; + /** @type { Map } */ + const idToManager = new Map(); + + const { parse, stringify } = JSON; + + // v$NN.source = JSON({ bundle }) or JSON({ bundleName }) + // v$NN.options = JSON + // TODO: check against createVatDynamically + // TODO transaction boundaries? + // ensure kernel doesn't start transaction until after it gets manager? + const idToInitDetail = { + /** @type { (vatID: string) => { vatID: string, bundle: Bundle, options: ManagerOptions } } */ + get(vatID) { + const { bundle } = parse(storage.get(`${vatID}.source`)); + assert(bundle, d`${q(vatID)}: bundleName not supported`); + + const options = parse(storage.get(`${vatID}.options`)); + // TODO: validate options + + return { vatID, bundle, options }; + }, + /** @type { (vatID: string, d: { bundle: Bundle, options: ManagerOptions }) => void } */ + set(vatID, { options, bundle }) { + storage.set(`${vatID}.source`, stringify({ bundle })); + storage.set(`${vatID}.options`, stringify(options)); + }, + has(/** @type { unknown } */ vatID) { + return storage.has(`${vatID}.source`) && storage.has(`${vatID}.options`); + }, + }; + + /** + * @param {string} vatID + * @param {Bundle} bundle + * @param {ManagerOptions} options + */ + function initVatManager(vatID, bundle, options) { + assert( + !idToInitDetail.has(vatID), + d`vat with id ${vatID} already initialized`, + ); + const { managerType } = options; + assert(managerType in factories, d`unknown managerType ${managerType}`); + + idToInitDetail.set(vatID, { bundle, options }); + + // TODO: add a way to remove a gatekeeper from ephemeral in kernel.js + // so that we can get rid of a vatKeeper when we evict its vat. + kernelKeeper.allocateVatKeeper(vatID); + } + + /** + * @param {string} vatID + * @returns { Promise } + */ + async function provideVatManager(vatID) { + const mgr = idToManager.get(vatID); + if (mgr) return mgr; + const detail = idToInitDetail.get(vatID); + assert(detail, d`no vat with ID ${vatID} initialized`); + + // TODO: move kernelKeeper.allocateVatKeeper(vatID); + // to here + + // TODO: load from snapshot + + const { bundle, options } = detail; + const { managerType } = options; + // console.log('provide: creating from bundle', vatID); + const manager = await factories[managerType].createFromBundle( + vatID, + bundle, + options, + ); + + const translators = makeVatTranslators(vatID, kernelKeeper); + + const vatSyscallHandler = buildVatSyscallHandler(vatID, translators); + manager.setVatSyscallHandler(vatSyscallHandler); + await manager.replayTranscript(); + idToManager.set(vatID, manager); + return manager; + } + + /** @type { (vatID: string) => Promise } */ + async function evict(vatID) { + assert(idToInitDetail.has(vatID), d`no vat with ID ${vatID} initialized`); + const mgr = idToManager.get(vatID); + if (!mgr) return; + idToManager.delete(vatID); + // console.log('evict: shutting down', vatID); + await mgr.shutdown(); + } + + /** @type { string[] } */ + const recent = []; + + /** + * Simple fixed-size LRU cache policy + * + * TODO: policy input: did a vat get a message? how long ago? + * "important" vat option? + * options: pay $/block to keep in RAM - advisory; not consensus + * creation arg: # of vats to keep in RAM (LRU 10~50~100) + * + * @param {string} currentVatID + */ + async function applyAvailabilityPolicy(currentVatID) { + // console.log('applyAvailabilityPolicy', currentVatID, recent); + const pos = recent.indexOf(currentVatID); + // already most recently used + if (pos + 1 === sizeHint) return; + if (pos >= 0) recent.splice(pos, 1); + recent.push(currentVatID); + // not yet full + if (recent.length <= sizeHint) return; + const [lru] = recent.splice(0, 1); + await evict(lru); + } + + /** @type {(vatID: string, d: Tagged) => Promise } */ + async function deliverToVat(vatID, delivery) { + await applyAvailabilityPolicy(vatID); + return (await provideVatManager(vatID)).deliver(delivery); + } + + /** @type { (vatID: string) => Promise } */ + async function shutdown(vatID) { + await evict(vatID); + idToInitDetail.delete(vatID); + } + + return harden({ + // TODO: startup() method for start of kernel process + // see // instantiate all static vats + // in kernel.js + + initVatManager, + deliverToVat, + + // mostly for testing? + activeVatIDs: () => [...idToManager.keys()], + + shutdown, // should this be shutdown for the whole thing? + }); +} + +function aStorageAndKeeper() { + const { storage: hostStorage } = initSwingStore(undefined); + const { enhancedCrankBuffer, _commitCrank } = wrapStorage(hostStorage); + + const kernelKeeper = makeKernelKeeper(enhancedCrankBuffer); + kernelKeeper.createStartingKernelState('local'); + return { storage: enhancedCrankBuffer, kernelKeeper }; +} + +const json = JSON.stringify; + +async function theXSFactory( + /** @type { ReturnType } */ kernelKeeper, +) { + const load = rel => + bundleSource(require.resolve(`../../src/${rel}`), 'getExport'); + const bundles = [ + await load('kernel/vatManager/lockdown-subprocess-xsnap.js'), + await load('kernel/vatManager/supervisor-subprocess-xsnap.js'), + ]; + const snapstorePath = path.resolve(__dirname, './fixture-snap-pool/'); + + const startXSnap = makeStartXSnap(bundles, { + snapstorePath, + env: process.env, + }); + + /** @type { unknown } */ + const TODO = undefined; + const xsf = makeXsSubprocessFactory({ + allVatPowers: { + transformTildot: src => src, + Remotable, + getInterfaceOf, + exitVat: TODO, + exitVatWithFailure: TODO, + }, + kernelKeeper, + startXSnap, + testLog: _ => {}, + decref: _ => {}, + }); + + return xsf; +} + +/** @type {(r: string, m: string, ...args: unknown[]) => Tagged} */ +const msg = (result, method, ...args) => [ + 'message', + 'o+0', + { + method, + args: { body: json(args), slots: [] }, + result, + }, +]; +/** @type {(p: string, val: unknown) => Tagged} */ +const res = (p, val) => [ + 'resolve', + [[p, false, { body: json(val), slots: [] }]], +]; + +function mockSyscallHandler() { + /** @type { Tagged[] } */ + const syscalls = []; + + return { + /** @type { VatSyscallHandler } */ + handle(syscall) { + // console.log('syscall', syscall); + syscalls.push(syscall); + return ['ok', null]; + }, + get() { + return [...syscalls]; + }, + reset() { + syscalls.splice(0, syscalls.length); + }, + }; +} + +test('initialize vat; ensure deliveries maintain state', async t => { + const { storage, kernelKeeper } = aStorageAndKeeper(); + const config = loadBasedir(__dirname); + + const syscalls = mockSyscallHandler(); + + // factory for real vatmanagers + const xsFactory = await theXSFactory(kernelKeeper); + + assert('sourceSpec' in config.vats.target); + const targetBundle = await bundleSource(config.vats.target.sourceSpec); + + // Now we have what we need to make a warehouse. + // and initialize a vat. + const warehouse = makeVatWarehouse( + kernelKeeper, + storage, + { 'xs-worker': xsFactory }, + () => syscalls.handle, + ); + + warehouse.initVatManager('v1', targetBundle, { + managerType: 'xs-worker', + vatParameters: {}, + virtualObjectCacheSize: 1, + enableSetup: false, + bundle: undefined, + }); + + // send delivery to vatmanager + // check impact on mock kernel, or at least; on mock syscall handler + + t.like( + await warehouse.deliverToVat('v1', msg('p-62', 'append', 1)), + // ignore 3rd item of result (meter info) + { 0: 'ok', 1: null }, + 'p-62', + ); + t.deepEqual(syscalls.get(), [ + ['resolve', [['p-62', false, { body: '[1]', slots: [] }]]], + ]); + + syscalls.reset(); + + t.like( + await warehouse.deliverToVat('v1', msg('p-63', 'append', 2)), + { 0: 'ok', 1: null }, + 'p-63', + ); + t.deepEqual(syscalls.get(), [res('p-63', [1, 2])], 'p-63 syscall'); +}); + +test('deliver to lots of vats', async t => { + const vatIDs = ['v1', 'v2', 'v3', 'v4']; + + const { storage, kernelKeeper } = aStorageAndKeeper(); + const config = loadBasedir(__dirname); + const xsFactory = await theXSFactory(kernelKeeper); + assert('sourceSpec' in config.vats.target); + const targetBundle = await bundleSource(config.vats.target.sourceSpec); + const syscalls = mockSyscallHandler(); + + const warehouse = makeVatWarehouse( + kernelKeeper, + storage, + { 'xs-worker': xsFactory }, + () => syscalls.handle, + { sizeHint: 3 }, + ); + + // initialize a bunch of vats + vatIDs.forEach(id => { + warehouse.initVatManager(id, targetBundle, { + managerType: 'xs-worker', + vatParameters: {}, + virtualObjectCacheSize: 1, + enableSetup: false, + bundle: undefined, + }); + }); + + // Do various deliveries, sometimes to the same vat, + // sometimes to a different vat. + const range = n => [...Array(n).keys()]; + const expected = {}; + for await (const iter of range(20)) { + const id = vatIDs[Math.floor(iter * 17 - iter / 3) % vatIDs.length]; + t.log('delivery', iter, 'to', id); + t.like( + await warehouse.deliverToVat(id, msg(`p-1${iter}`, 'append', iter)), + { 0: 'ok', 1: null }, + ); + + t.truthy(warehouse.activeVatIDs().length <= 3, 'limit active vats'); + + if (!(id in expected)) expected[id] = []; + expected[id].push(iter); + t.log('checking syscalls', iter, id, expected[id]); + t.deepEqual(syscalls.get(), [res(`p-1${iter}`, expected[id])]); + syscalls.reset(); + } +}); diff --git a/packages/SwingSet/test/workers/vat-target.js b/packages/SwingSet/test/workers/vat-target.js index 20f6629cb95f..ed344d8c5b5c 100644 --- a/packages/SwingSet/test/workers/vat-target.js +++ b/packages/SwingSet/test/workers/vat-target.js @@ -58,9 +58,16 @@ export function buildRootObject(vatPowers, vatParameters) { // crank 5: dispatch.notify(pF, false, ['data', callbackObj]) + const contents = []; + function append(thing) { + contents.push(thing); + return harden([...contents]); + } + const target = Far('root', { zero, one, + append, }); return target;