From 03f148b20de7f0f7d5b56da63c8358dde8d7de16 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Wed, 21 Jul 2021 18:29:47 -0700 Subject: [PATCH 1/8] feat(swingset): add Meters to kernel state This introduces the "meterID" and the "meter record": a pair of Nats (`remaining` and `threshold`). kernelKeeper functions are added to create and manipulate them, and test-state.js is enhanced to exercise these. refs #3308 --- .../SwingSet/src/kernel/state/kernelKeeper.js | 95 +++++++++++++++++++ packages/SwingSet/test/test-state.js | 44 +++++++++ 2 files changed, 139 insertions(+) diff --git a/packages/SwingSet/src/kernel/state/kernelKeeper.js b/packages/SwingSet/src/kernel/state/kernelKeeper.js index 2c6057e5189..7e85ad3cac2 100644 --- a/packages/SwingSet/src/kernel/state/kernelKeeper.js +++ b/packages/SwingSet/src/kernel/state/kernelKeeper.js @@ -36,6 +36,7 @@ const enableKernelGC = true; // device.names = JSON([names..]) // device.name.$NAME = $deviceID = d$NN // device.nextID = $NN +// meter.nextID = $NN // used to make m$NN // kernelBundle = JSON(bundle) // bundle.$NAME = JSON(bundle) @@ -54,6 +55,9 @@ const enableKernelGC = true; // v$NN.vs.$key = string // v$NN.lastSnapshot = JSON({ snapshotID, startPos }) +// m$NN.remaining = $NN // remaining capacity (in computrons) or 'unlimited' +// m$NN.threshold = $NN // notify when .remaining first drops below this + // d$NN.o.nextID = $NN // d$NN.c.$kernelSlot = $deviceSlot = o-$NN/d+$NN/d-$NN // d$NN.c.$deviceSlot = $kernelSlot = ko$NN/kd$NN @@ -90,6 +94,12 @@ export function commaSplit(s) { return s.split(','); } +function insistMeterID(m) { + assert.typeof(m, 'string'); + assert.equal(m[0], 'm'); + Nat(BigInt(m.slice(1))); +} + // we use different starting index values for the various vNN/koNN/kdNN/kpNN // slots, to reduce confusing overlap when looking at debug messages (e.g. // seeing both kp1 and ko1, which are completely unrelated despite having the @@ -106,6 +116,7 @@ const FIRST_OBJECT_ID = 20n; const FIRST_DEVNODE_ID = 30n; const FIRST_PROMISE_ID = 40n; const FIRST_CRANK_NUMBER = 0n; +const FIRST_METER_ID = 1n; /** * @param {KVStorePlus} kvStore @@ -228,6 +239,7 @@ export default function makeKernelKeeper( kvStore.set('ko.nextID', `${FIRST_OBJECT_ID}`); kvStore.set('kd.nextID', `${FIRST_DEVNODE_ID}`); kvStore.set('kp.nextID', `${FIRST_PROMISE_ID}`); + kvStore.set('meter.nextID', `${FIRST_METER_ID}`); kvStore.set('gcActions', '[]'); kvStore.set('runQueue', JSON.stringify([])); kvStore.set('crankNumber', `${FIRST_CRANK_NUMBER}`); @@ -706,6 +718,82 @@ export default function makeKernelKeeper( return msg; } + function allocateMeter(remaining, threshold) { + if (remaining !== 'unlimited') { + assert.typeof(remaining, 'bigint'); + Nat(remaining); + } + assert.typeof(threshold, 'bigint'); + Nat(threshold); + const nextID = Nat(BigInt(getRequired('meter.nextID'))); + kvStore.set('meter.nextID', `${nextID + 1n}`); + const meterID = `m${nextID}`; + kvStore.set(`${meterID}.remaining`, `${remaining}`); + kvStore.set(`${meterID}.threshold`, `${threshold}`); + return meterID; + } + + function addMeterRemaining(meterID, delta) { + insistMeterID(meterID); + assert.typeof(delta, 'bigint'); + Nat(delta); + /** @type { bigint | string } */ + let remaining = getRequired(`${meterID}.remaining`); + if (remaining !== 'unlimited') { + remaining = Nat(BigInt(remaining)); + kvStore.set(`${meterID}.remaining`, `${remaining + delta}`); + } + } + + function setMeterThreshold(meterID, threshold) { + insistMeterID(meterID); + assert.typeof(threshold, 'bigint'); + Nat(threshold); + kvStore.set(`${meterID}.threshold`, `${threshold}`); + } + + function getMeter(meterID) { + insistMeterID(meterID); + /** @type { bigint | string } */ + let remaining = getRequired(`${meterID}.remaining`); + if (remaining !== 'unlimited') { + remaining = BigInt(remaining); + } + const threshold = BigInt(getRequired(`${meterID}.threshold`)); + return harden({ remaining, threshold }); + } + + function deductMeter(meterID, spent) { + insistMeterID(meterID); + assert.typeof(spent, 'bigint'); + Nat(spent); + let underflow = false; + let notify = false; + /** @type { bigint | string } */ + let oldRemaining = getRequired(`${meterID}.remaining`); + if (oldRemaining !== 'unlimited') { + oldRemaining = BigInt(oldRemaining); + const threshold = BigInt(getRequired(`${meterID}.threshold`)); + let remaining = oldRemaining - spent; + if (remaining < 0n) { + underflow = true; + remaining = 0n; + } + if (remaining < threshold && oldRemaining >= threshold) { + // only notify once per crossing + notify = true; + } + kvStore.set(`${meterID}.remaining`, `${Nat(remaining)}`); + } + return harden({ underflow, notify }); + } + + function deleteMeter(meterID) { + insistMeterID(meterID); + kvStore.delete(`${meterID}.remaining`); + kvStore.delete(`${meterID}.threshold`); + } + function hasVatWithName(name) { return kvStore.has(`vat.name.${name}`); } @@ -1205,6 +1293,13 @@ export default function makeKernelKeeper( getRunQueueLength, getNextMsg, + allocateMeter, + addMeterRemaining, + setMeterThreshold, + getMeter, + deductMeter, + deleteMeter, + hasVatWithName, getVatIDForName, allocateVatIDForNameIfNeeded, diff --git a/packages/SwingSet/test/test-state.js b/packages/SwingSet/test/test-state.js index dc3e7495700..109eda4f1ea 100644 --- a/packages/SwingSet/test/test-state.js +++ b/packages/SwingSet/test/test-state.js @@ -287,6 +287,7 @@ test('kernel state', async t => { ['kd.nextID', '30'], ['kp.nextID', '40'], ['kernel.defaultManagerType', 'local'], + ['meter.nextID', '1'], ]); }); @@ -321,6 +322,7 @@ test('kernelKeeper vat names', async t => { ['vat.name.vatname5', 'v1'], ['vat.name.Frank', 'v2'], ['kernel.defaultManagerType', 'local'], + ['meter.nextID', '1'], ]); t.deepEqual(k.getStaticVats(), [ ['Frank', 'v2'], @@ -369,6 +371,7 @@ test('kernelKeeper device names', async t => { ['device.name.devicename5', 'd7'], ['device.name.Frank', 'd8'], ['kernel.defaultManagerType', 'local'], + ['meter.nextID', '1'], ]); t.deepEqual(k.getDevices(), [ ['Frank', 'd8'], @@ -550,6 +553,7 @@ test('kernelKeeper promises', async t => { [`${ko}.owner`, 'v1'], [`${ko}.refCount`, '1,1'], ['kernel.defaultManagerType', 'local'], + ['meter.nextID', '1'], ]); }); @@ -663,3 +667,43 @@ test('XS vatKeeper defaultManagerType', async t => { k.createStartingKernelState('xs-worker'); t.is(k.getDefaultManagerType(), 'xs-worker'); }); + +test('meters', async t => { + const { kvStore, streamStore } = buildKeeperStorageInMemory(); + const k = makeKernelKeeper(kvStore, streamStore); + k.createStartingKernelState('local'); + const m1 = k.allocateMeter(100n, 10n); + const m2 = k.allocateMeter(200n, 150n); + t.not(m1, m2); + k.deleteMeter(m2); + t.deepEqual(k.getMeter(m1), { remaining: 100n, threshold: 10n }); + t.deepEqual(k.deductMeter(m1, 10n), { underflow: false, notify: false }); + t.deepEqual(k.deductMeter(m1, 10n), { underflow: false, notify: false }); + t.deepEqual(k.getMeter(m1), { remaining: 80n, threshold: 10n }); + t.deepEqual(k.deductMeter(m1, 70n), { underflow: false, notify: false }); + t.deepEqual(k.deductMeter(m1, 1n), { underflow: false, notify: true }); + t.deepEqual(k.getMeter(m1), { remaining: 9n, threshold: 10n }); + t.deepEqual(k.deductMeter(m1, 1n), { underflow: false, notify: false }); + t.deepEqual(k.deductMeter(m1, 9n), { underflow: true, notify: false }); + t.deepEqual(k.getMeter(m1), { remaining: 0n, threshold: 10n }); + t.deepEqual(k.deductMeter(m1, 2n), { underflow: true, notify: false }); + t.deepEqual(k.getMeter(m1), { remaining: 0n, threshold: 10n }); + k.addMeterRemaining(m1, 50n); + t.deepEqual(k.getMeter(m1), { remaining: 50n, threshold: 10n }); + t.deepEqual(k.deductMeter(m1, 30n), { underflow: false, notify: false }); + t.deepEqual(k.deductMeter(m1, 25n), { underflow: true, notify: true }); + t.deepEqual(k.getMeter(m1), { remaining: 0n, threshold: 10n }); + + k.addMeterRemaining(m1, 50n); + k.setMeterThreshold(m1, 40n); + t.deepEqual(k.getMeter(m1), { remaining: 50n, threshold: 40n }); + t.deepEqual(k.deductMeter(m1, 10n), { underflow: false, notify: false }); + t.deepEqual(k.deductMeter(m1, 10n), { underflow: false, notify: true }); + t.deepEqual(k.getMeter(m1), { remaining: 30n, threshold: 40n }); + + const m3 = k.allocateMeter('unlimited', 10n); + k.setMeterThreshold(m3, 5n); + t.deepEqual(k.getMeter(m3), { remaining: 'unlimited', threshold: 5n }); + t.deepEqual(k.deductMeter(m3, 1000n), { underflow: false, notify: false }); + t.deepEqual(k.getMeter(m3), { remaining: 'unlimited', threshold: 5n }); +}); From 7a7d61670baedf1968fd8086cdb8824bd006bad4 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Wed, 21 Jul 2021 18:35:39 -0700 Subject: [PATCH 2/8] feat(swingset): implement Meters for crank computation charges This introduces user-visible Meter objects, and allows new dynamic vats to be bound to a single Meter, both exposed through the vatAdmin facet. Meters are long-term reservoirs of execution credits, denominated in "computrons". Each bound vat will deduct credits from its meter until exhausted, at which point the vat will be terminated. This limits the long-term CPU consumption of a vat, in addition to the fixed per-crank computron limit applied to any metered vat. Meters can be refilled and queried through their API. Each meter also has a Notifier, and a configurable notification threshold: the notifier will be updated if/when the remaining credits drop below the threshold. This should allow a supervisor in userspace enough time to refill the meter before the associated vat(s) are terminated. See `docs/metering.md` for documentation. Some notes: * The vatAdmin facet now offers `createMeter()`, which returns a `Meter` object with methods to manipulate its `remaining` and `threshold` values, as well as a `getNotifier` to subscribe to threshold-passing events. * It also offers `createUnlimitedMeter()`, which never deducts. * The vatAdmin `createVat()` call now takes a `meter: Meter` option instead of `metered: boolean`. If a Meter is provided, two things happen: * Each delivery to that (XS) vat is subject to a per-crank compute limit. * Each delivery deducts the compute usage from the Meter. * When a Meter's `remaining` drops below its `threshold`, the notifier is triggered with the current `remaining` value. The actual Meter's value might have changed by the time the subscriber hears about the update. * When a vat's Meter reaches zero, the vat is terminated, just as if it had violated the per-crank limit. * Currently the termination message (used to reject the control facet's `.done()` Promise) is different for the per-crank limit vs the Meter limit, but this may change. * Meter deductions and threshold notifications are stashed in a new 'postAbortActions' record, to make sure they happen even if the crank is aborted and all other state changes are unwound. * The vatManager `managerOptions` still use `metered: boolean`, because the vat manager doesn't know about Meters: it only need to know whether to apply the per-crank limits or not. closes #3308 --- packages/SwingSet/docs/metering.md | 96 +++++++ packages/SwingSet/src/kernel/kernel.js | 76 ++++- packages/SwingSet/src/kernel/loadVat.js | 21 +- .../SwingSet/src/kernel/state/kernelKeeper.js | 1 + .../SwingSet/src/kernel/state/vatKeeper.js | 2 +- .../src/kernel/vatAdmin/vatAdmin-src.js | 26 +- .../src/kernel/vatAdmin/vatAdminWrapper.js | 72 ++++- .../src/kernel/vatManager/vat-warehouse.js | 2 +- packages/SwingSet/src/types.js | 3 +- .../SwingSet/test/gc-dead-vat/bootstrap.js | 2 +- .../test/metering/test-dynamic-vat-metered.js | 260 +++++++++++++++++- .../metering/test-dynamic-vat-unmetered.js | 6 +- .../test/metering/vat-load-dynamic.js | 26 ++ packages/SwingSet/test/test-gc-kernel.js | 22 +- packages/SwingSet/test/util.js | 12 +- 15 files changed, 572 insertions(+), 55 deletions(-) create mode 100644 packages/SwingSet/docs/metering.md diff --git a/packages/SwingSet/docs/metering.md b/packages/SwingSet/docs/metering.md new file mode 100644 index 00000000000..aecc1066c4c --- /dev/null +++ b/packages/SwingSet/docs/metering.md @@ -0,0 +1,96 @@ +# Metering CPU Usage + +The Halting Problem is unsolvable: no amount of static analysis or human auditing can pre-determine how many steps an arbitrary Turing-complete program will take before it finishes, or if it will ever finish. To prevent the code in one vat from preventing execution of code in other vats (or the kernel itself), SwingSet provides a mechanism to limit the amount of computation that each vat can perform. Any vat which exceeds its limit is terminated, and any messages it sent before the limit was reached are cancelled. + +Two limits can be imposed. The first is a per-crank limit. Each message delivered to a vat results in a sequence of "turns" known as a "crank". A crank is also triggered when the vat receives notification of a kernel-side promise being resolved or rejected. Cranks run until the vat stops adding work to the resolved-promise queue, and there is nothing left to do until the next message or notification arrives. A per-crank limit imports a ceiling on the amount of computation that can be done during each crank, but does not say anything about the number of cranks that can be run. + +The second limit spans multiple cranks and is managed by the "Meter": a variable-sized reservoir of execution credits. Each vat can be associated with a single Meter, and the remaining capacity of the Meter is reduced at the end of each crank by whatever amount the vat consumed during that crank. The Meter can be refilled by sending it a message, but if any crank causes the Meter's remaining value to drop below zero, the vat is terminated. + +## The Computron + +SwingSet measures computation with a unit named the "computron": the smallest unit of indivisible computation. The number of computrons used by a given piece of code depends upon its inputs, the state it can access, and the history of its previous activity, but it does *not* depend upon the activity of other vats, other processes on the same host computer, wall-clock time, or type of CPU being used (32-bit vs 64-bit, Intel vs ARM). The metering usage is meant to be consistent across any SwingSet using the same version of the kernel and vat code, which receives the same sequence of vat inputs (the transcript), making it safe to use in a consensus machine. + +Metering is provided by low-level code in the JavaScript engine, which is counting basic operations like "read a property from an object" and "add two numbers". This is larger than a CPU cycle. The exact mapping depends upon intricate details of the engine, and is likely to change if/when the JS engine is upgraded. SwingSet kernels that participate in a consensus machine must be careful to synchronize upgrades to prevent divergence of metering results. + +TODO: add examples of computron usage numbers + +Computrons have a loose relationship to wallclock time, but are generally correlated, so tracking the cumulative computrons spent during SwingSet cranks can provide a rough measure of how much time is being spent, which can be useful to e.g. limit blocks to a reasonable amount of execution time. + +The SwingSet Meter APIs accept and deliver computron values in BigInts. + +## Meter Objects + +The kernel manages `Meter` objects. Each one has a `remaining` capacity and a notification `threshold`. The Meter has a `Notifier` which can inform interested parties when the capacity drops below the threshold, so they can refill it before any associated vats are in danger of being terminated due to an underflow. + +Vats can create a Meter object by invoking the `createMeter` method on the `vatAdmin` object. This is the same object used to create new dynamic vats. `createMeter` takes two arguments, both denominated in computrons: + +* `remaining`: sets the initial capacity of the Meter +* `threshold`: set the notification threshold + +If you want to impose a per-crank limit, but not a cumulative limit, you can use `createUnlimitedMeter` to make a Meter that never deducts (`remaining` is always the special string `'unlimited'`) and never notifies. + +```js +const remaining = 100_000_000n; // 100M computrons +const threshold = 20_000_000n: // notify below 20M +const meter = await E(vatAdmin).createMeter(remaining, threshold); +const umeter = await E(vatAdmin).createUnlimitedMeter(); +``` + +The holder of a Meter object can manipulate the meter with the following API: + +* `meter.addRemaining(delta)`: increment the capacity by some amount +* `meter.setThreshold(threshold)`: replace the notification threshold +* `meter.get() -> { remaining, threshold }`: read the remaining capacity and current notification threshold +* `meter.getNotifier() -> Notifier`: access the Notifier object + +```js +await E(meter).get(); // -> { remaining: 100_000_000n, threshold: 20_000_000n } +await E(meter).setThreshold(50n); +await E(meter).get(); // -> { remaining: 100_000_000n, threshold: 50n } +await E(meter).addRemaining(999n); +await E(meter).get(); // -> { remaining: 100_000_999n, threshold: 50n } +``` + +## Notification + +The meter's `remaining` value will be deducted over time. When it crosses below `threshold`, the Notifier is updated. This is an instance of `@agoric/notifier`: + +```js +const notifier = await E(meter).getNotifier(); +const initial = await E(notifier).getUpdateSince(); +const p1 = E(notifier).getUpdateSince(initial); +p1.then(remaining => console.log(`meter down to ${remaining}, must refill`)); +``` + +Note that the notification will occur only once for each transition from "above threshold" to "below threshold". So even if the vat continues to operate (and keeps deducting from the Meter), the notification will not be repeated. + +The notification may be triggered again if the meter is refilled above the current threshold, or if the threshold is reduced below the current remaining capacity. + +## Per-Crank Limits + +The per-crank limit is currently hardcoded to 100M computrons, defined by `DEFAULT_CRANK_METERING_LIMIT` in `packages/xsnap/src/xsnap.js`. This has experimentally been determined to be sufficient for loading large contract bundles, which is the single largest operation we've observed so far. + +This per-crank limit is intended to maintain fairness even among vats with a large Meter capacity: just because the Meter allows the vat to spend 17 hours of CPU time, we don't want it to spend it all at once. It also provides a safety mechanism when the vat is using an "unlimited" meter, which allows the vat to use as make cranks as it wants, but each crank is limited. + +## Assigning Meters to Vats + +Each vat can be associated with a single Meter. A Meter can be attached to multiple vats (although that may make it difficult to assign responsibility for the consumption it measures). To attach a Meter, include it in the options bag to the `vatAdmin`'s `createVat` or `createVatByName` methods: + +```js +const control = await E(vatAdmin).createVat(bundle, { meter }); +``` + +The default (omitting a `meter` option) leaves the vat unmetered. + +Assigning a Meter to a vat activates the per-crank limit. To achieve a per-crank limit without a Meter object (which must be refilled occasionally to keep the vat from being terminated), use an unlimited meter: + +```js +const meter = await E(vatAdmin).createUnlimitedMeter(); +const control = await E(vatAdmin).createVat(bundle, { meter }); +``` + +## runPolicy + +TODO: The host application can limit the number of cranks processed in a single call to `controller.run()` by providing a `runPolicy` object. This policy object is informed about each crank and the number of computrons it consumed. By comparing the cumulative computrons against an experimentally (and externally) determined threshold, the `runLimit` object can tell the kernel to stop processing before the run-queue is drained. For a busy kernel, with an ever-increasing amount of work to do, this can limit the size of a commitment domain (e.g. the "block" in a blockchain / consensus machine). + +This is a work in process, please follow issue #3460 for progress. diff --git a/packages/SwingSet/src/kernel/kernel.js b/packages/SwingSet/src/kernel/kernel.js index 06c61dc742a..01b4d54979c 100644 --- a/packages/SwingSet/src/kernel/kernel.js +++ b/packages/SwingSet/src/kernel/kernel.js @@ -1,6 +1,7 @@ // @ts-check import { assert, details as X } from '@agoric/assert'; import { importBundle } from '@agoric/import-bundle'; +import { stringify } from '@agoric/marshal'; import { assertKnownOptions } from '../assertOptions.js'; import { makeVatManagerFactory } from './vatManager/factory.js'; import { makeVatWarehouse } from './vatManager/vat-warehouse.js'; @@ -374,12 +375,43 @@ export default function buildKernel( } let terminationTrigger; + let postAbortActions; function resetDeliveryTriggers() { terminationTrigger = undefined; + postAbortActions = { + meterDeductions: [], // list of { meterID, compute } + meterNotifications: [], // list of meterID + }; } resetDeliveryTriggers(); + function notifyMeterThreshold(meterID) { + // tell vatAdmin that a meter has dropped below its notifyThreshold + const { remaining } = kernelKeeper.getMeter(meterID); + const args = { body: stringify(harden([meterID, remaining])), slots: [] }; + assert.typeof(vatAdminRootKref, 'string', 'vatAdminRootKref missing'); + queueToKref(vatAdminRootKref, 'meterCrossedThreshold', args, 'logFailure'); + } + + function deductMeter(meterID, compute, firstTime) { + assert.typeof(compute, 'bigint'); + const res = kernelKeeper.deductMeter(meterID, compute); + // we also recode deduction and any notification in postAbortActions, + // which are executed if the delivery is being rewound for any reason + // (syscall error, res.underflow), so their side-effects survive + if (firstTime) { + postAbortActions.meterDeductions.push({ meterID, compute }); + } + if (res.notify) { + notifyMeterThreshold(meterID); + if (firstTime) { + postAbortActions.meterNotifications.push(meterID); + } + } + return res.underflow; + } + // this is called for syscall.exit (shouldAbortCrank=false), and for any // vat-fatal errors (shouldAbortCrank=true) function setTerminationTrigger(vatID, shouldAbortCrank, shouldReject, info) { @@ -391,12 +423,13 @@ export default function buildKernel( } } - async function deliverAndLogToVat(vatID, kd, vd) { + async function deliverAndLogToVat(vatID, kd, vd, useMeter) { // eslint-disable-next-line no-use-before-define assert(vatWarehouse.lookup(vatID)); const vatKeeper = kernelKeeper.provideVatKeeper(vatID); const crankNum = kernelKeeper.getCrankNumber(); const deliveryNum = vatKeeper.nextDeliveryNum(); // increments + const { meterID } = vatKeeper.getOptions(); /** @typedef { any } FinishFunction TODO: static types for slog? */ /** @type { FinishFunction } */ const finish = kernelSlog.delivery(vatID, crankNum, deliveryNum, kd, vd); @@ -412,6 +445,21 @@ export default function buildKernel( // probably a metering fault, or a bug in the vat's dispatch() console.log(`delivery problem, terminating vat ${vatID}`, problem); setTerminationTrigger(vatID, true, true, makeError(problem)); + return; + } + if (deliveryResult[0] === 'ok' && useMeter && meterID) { + const metering = deliveryResult[2]; + assert(metering); + const consumed = metering.compute; + assert.typeof(consumed, 'number'); + const used = BigInt(consumed); + const underflow = deductMeter(meterID, used, true); + if (underflow) { + console.log(`meter ${meterID} underflow, terminating vat ${vatID}`); + const err = makeError('meter underflow, vat terminated'); + setTerminationTrigger(vatID, true, true, err); + return; + } } } catch (e) { // log so we get a stack trace @@ -433,7 +481,7 @@ export default function buildKernel( const kd = harden(['message', target, msg]); // eslint-disable-next-line no-use-before-define const vd = vatWarehouse.kernelDeliveryToVatDelivery(vatID, kd); - await deliverAndLogToVat(vatID, kd, vd); + await deliverAndLogToVat(vatID, kd, vd, true); } } @@ -546,7 +594,7 @@ export default function buildKernel( // eslint-disable-next-line no-use-before-define const vd = vatWarehouse.kernelDeliveryToVatDelivery(vatID, kd); vatKeeper.deleteCListEntriesForKernelSlots(targets); - await deliverAndLogToVat(vatID, kd, vd); + await deliverAndLogToVat(vatID, kd, vd, true); } } @@ -570,7 +618,7 @@ export default function buildKernel( } // eslint-disable-next-line no-use-before-define const vd = vatWarehouse.kernelDeliveryToVatDelivery(vatID, kd); - await deliverAndLogToVat(vatID, kd, vd); + await deliverAndLogToVat(vatID, kd, vd, false); } async function processCreateVat(message) { @@ -682,9 +730,18 @@ export default function buildKernel( // errors unwind any changes the vat made abortCrank(); didAbort = true; + // but metering deductions or underflow notifications must survive + const { meterDeductions, meterNotifications } = postAbortActions; + for (const { meterID, compute } of meterDeductions) { + deductMeter(meterID, compute, false); + } + for (const meterID of meterNotifications) { + // reads meter.remaining, so must happen after deductMeter + notifyMeterThreshold(meterID); + } } - // state changes reflecting the termination must survive, so these - // happen after a possible abortCrank() + // state changes reflecting the termination must also survive, so + // these happen after a possible abortCrank() terminateVat(vatID, shouldReject, info); kernelSlog.terminateVat(vatID, shouldReject, info); kdebug(`vat terminated: ${JSON.stringify(info)}`); @@ -908,6 +965,13 @@ export default function buildKernel( return vatID; }, terminate: (vatID, reason) => terminateVat(vatID, true, reason), + meterCreate: (remaining, threshold) => + kernelKeeper.allocateMeter(remaining, threshold), + meterAddRemaining: (meterID, delta) => + kernelKeeper.addMeterRemaining(meterID, delta), + meterSetThreshold: (meterID, threshold) => + kernelKeeper.setMeterThreshold(meterID, threshold), + meterGet: meterID => kernelKeeper.getMeter(meterID), }; // instantiate all devices diff --git a/packages/SwingSet/src/kernel/loadVat.js b/packages/SwingSet/src/kernel/loadVat.js index 09daff61b42..15efb414dc1 100644 --- a/packages/SwingSet/src/kernel/loadVat.js +++ b/packages/SwingSet/src/kernel/loadVat.js @@ -87,7 +87,7 @@ export function makeVatLoader(stuff) { const allowedDynamicOptions = [ 'description', - 'metered', + 'meterID', 'managerType', // TODO: not sure we want vats to be able to control this 'vatParameters', 'enableSetup', @@ -133,13 +133,14 @@ export function makeVatLoader(stuff) { * * @param {number} options.virtualObjectCacheSize * - * @param {boolean} [options.metered] if true, - * subjects the new dynamic vat to a meter that limits - * the amount of computation and allocation that can occur during any - * given crank. Stack frames are limited as well. The meter is refilled - * between cranks, but if the meter ever underflows, the vat is - * terminated. If false, the vat is unmetered. Defaults to false for - * dynamic vats; static vats may not be metered. + * @param {string} [options.meterID] If a meterID is provided, the new + * dynamic vat is limited to a fixed amount of computation and + * allocation that can occur during any given crank. Peak stack + * frames are limited as well. In addition, the given meter's + * "remaining" value will be reduced by the amount of computation + * used by each crank. The meter will eventually underflow unless it + * is topped up, at which point the vat is terminated. If undefined, + * the vat is unmetered. Static vats cannot be metered. * * @param {Record} [options.vatParameters] provides * the contents of the second argument to @@ -199,7 +200,7 @@ export function makeVatLoader(stuff) { isDynamic ? allowedDynamicOptions : allowedStaticOptions, ); const { - metered = false, + meterID, vatParameters = {}, managerType, enableSetup = false, @@ -231,7 +232,7 @@ export function makeVatLoader(stuff) { const managerOptions = { managerType, bundle: vatSourceBundle, - metered, + metered: !!meterID, enableDisavow, enableSetup, enablePipelining, diff --git a/packages/SwingSet/src/kernel/state/kernelKeeper.js b/packages/SwingSet/src/kernel/state/kernelKeeper.js index 7e85ad3cac2..bef40c3be70 100644 --- a/packages/SwingSet/src/kernel/state/kernelKeeper.js +++ b/packages/SwingSet/src/kernel/state/kernelKeeper.js @@ -54,6 +54,7 @@ const enableKernelGC = true; // v$NN.t.endPosition = $NN // v$NN.vs.$key = string // v$NN.lastSnapshot = JSON({ snapshotID, startPos }) +// v$NN.meter = m$NN // m$NN.remaining = $NN // remaining capacity (in computrons) or 'unlimited' // m$NN.threshold = $NN // notify when .remaining first drops below this diff --git a/packages/SwingSet/src/kernel/state/vatKeeper.js b/packages/SwingSet/src/kernel/state/vatKeeper.js index 87fdec7a8ee..71f7b51250b 100644 --- a/packages/SwingSet/src/kernel/state/vatKeeper.js +++ b/packages/SwingSet/src/kernel/state/vatKeeper.js @@ -102,7 +102,7 @@ export function makeVatKeeper( } function getOptions() { - const options = JSON.parse(kvStore.get(`${vatID}.options`)); + const options = JSON.parse(kvStore.get(`${vatID}.options`) || '{}'); return harden(options); } diff --git a/packages/SwingSet/src/kernel/vatAdmin/vatAdmin-src.js b/packages/SwingSet/src/kernel/vatAdmin/vatAdmin-src.js index 531f4bbe863..d515bbbdb12 100644 --- a/packages/SwingSet/src/kernel/vatAdmin/vatAdmin-src.js +++ b/packages/SwingSet/src/kernel/vatAdmin/vatAdmin-src.js @@ -18,12 +18,36 @@ */ import { Far } from '@agoric/marshal'; +import { Nat } from '@agoric/nat'; export function buildRootDeviceNode({ endowments, serialize }) { - const { pushCreateVatEvent, terminate: kernelTerminateVatFn } = endowments; + const { + pushCreateVatEvent, + terminate: kernelTerminateVatFn, + meterCreate, + meterAddRemaining, + meterSetThreshold, + meterGet, + } = endowments; // The Root Device Node. return Far('root', { + createMeter(remaining, threshold) { + return meterCreate(Nat(remaining), Nat(threshold)); + }, + createUnlimitedMeter() { + return meterCreate('unlimited', 0n); + }, + addMeterRemaining(meterID, delta) { + meterAddRemaining(meterID, Nat(delta)); + }, + setMeterThreshold(meterID, threshold) { + meterSetThreshold(meterID, Nat(threshold)); + }, + getMeter(meterID) { + return meterGet(meterID); + }, + // Called by the wrapper vat to create a new vat. Gets a new ID from the // kernel's vat creator fn. Remember that the root object will arrive // separately. Clean up the outgoing and incoming arguments. diff --git a/packages/SwingSet/src/kernel/vatAdmin/vatAdminWrapper.js b/packages/SwingSet/src/kernel/vatAdmin/vatAdminWrapper.js index 30d359387c3..5b8f77b22ae 100644 --- a/packages/SwingSet/src/kernel/vatAdmin/vatAdminWrapper.js +++ b/packages/SwingSet/src/kernel/vatAdmin/vatAdminWrapper.js @@ -6,7 +6,9 @@ * device affordances into objects that can be used by code in other vats. */ import { makePromiseKit } from '@agoric/promise-kit'; +import { makeNotifierKit } from '@agoric/notifier'; import { Far } from '@agoric/marshal'; +import { Nat } from '@agoric/nat'; function producePRR() { const { promise, resolve, reject } = makePromiseKit(); @@ -17,6 +19,42 @@ export function buildRootObject(vatPowers) { const { D } = vatPowers; const pending = new Map(); // vatID -> { resolve, reject } for promise const running = new Map(); // vatID -> { resolve, reject } for doneP + const meterByID = new Map(); // meterID -> { meter, updater } + const meterIDByMeter = new WeakMap(); // meter -> meterID + + function makeMeter(vatAdminNode, remaining, threshold) { + Nat(remaining); + Nat(threshold); + const meterID = D(vatAdminNode).createMeter(remaining, threshold); + const { updater, notifier } = makeNotifierKit(); + const meter = Far('meter', { + addRemaining(delta) { + D(vatAdminNode).addMeterRemaining(meterID, Nat(delta)); + }, + setThreshold(newThreshold) { + D(vatAdminNode).setMeterThreshold(meterID, Nat(newThreshold)); + }, + get: () => D(vatAdminNode).getMeter(meterID), // returns BigInts + getNotifier: () => notifier, + }); + meterByID.set(meterID, harden({ meter, updater })); + meterIDByMeter.set(meter, meterID); + return meter; + } + + function makeUnlimitedMeter(vatAdminNode) { + const meterID = D(vatAdminNode).createUnlimitedMeter(); + const { updater, notifier } = makeNotifierKit(); + const meter = Far('meter', { + addRemaining(_delta) {}, + setThreshold(_newThreshold) {}, + get: () => harden({ remaining: 'unlimited', threshold: 0 }), + getNotifier: () => notifier, // will never fire + }); + meterByID.set(meterID, harden({ meter, updater })); + meterIDByMeter.set(meter, meterID); + return meter; + } function finishVatCreation(vatAdminNode, vatID) { const [promise, pendingRR] = producePRR(); @@ -39,14 +77,34 @@ export function buildRootObject(vatPowers) { }); } + function convertOptions(origOptions) { + const options = { ...origOptions }; + delete options.meterID; + delete options.meter; + if (origOptions.meter) { + const meterID = meterIDByMeter.get(origOptions.meter); + options.meterID = meterID; + } + return harden(options); + } + function createVatAdminService(vatAdminNode) { return Far('vatAdminService', { - createVat(code, options) { - const vatID = D(vatAdminNode).create(code, options); + createMeter(remaining, threshold) { + return makeMeter(vatAdminNode, remaining, threshold); + }, + createUnlimitedMeter() { + return makeUnlimitedMeter(vatAdminNode); + }, + createVat(code, options = {}) { + const vatID = D(vatAdminNode).create(code, convertOptions(options)); return finishVatCreation(vatAdminNode, vatID); }, - createVatByName(bundleName, options) { - const vatID = D(vatAdminNode).createByName(bundleName, options); + createVatByName(bundleName, options = {}) { + const vatID = D(vatAdminNode).createByName( + bundleName, + convertOptions(options), + ); return finishVatCreation(vatAdminNode, vatID); }, }); @@ -63,6 +121,11 @@ export function buildRootObject(vatPowers) { } } + function meterCrossedThreshold(meterID, remaining) { + const { updater } = meterByID.get(meterID); + updater.updateState(remaining); + } + // the kernel sends this when the vat halts function vatTerminated(vatID, shouldReject, info) { if (!running.has(vatID)) { @@ -85,5 +148,6 @@ export function buildRootObject(vatPowers) { createVatAdminService, newVatCallback, vatTerminated, + meterCrossedThreshold, }); } diff --git a/packages/SwingSet/src/kernel/vatManager/vat-warehouse.js b/packages/SwingSet/src/kernel/vatManager/vat-warehouse.js index ed4735f4a31..ceea90ec508 100644 --- a/packages/SwingSet/src/kernel/vatManager/vat-warehouse.js +++ b/packages/SwingSet/src/kernel/vatManager/vat-warehouse.js @@ -257,7 +257,7 @@ export function makeVatWarehouse(kernelKeeper, vatLoader, policyOptions) { /** @type { string | undefined } */ let lastVatID; - /** @type {(vatID: string, d: VatDeliveryObject) => Promise } */ + /** @type {(vatID: string, d: VatDeliveryObject) => Promise } */ async function deliverToVat(vatID, delivery) { await applyAvailabilityPolicy(vatID); lastVatID = vatID; diff --git a/packages/SwingSet/src/types.js b/packages/SwingSet/src/types.js index 44098183c18..76ef1b90261 100644 --- a/packages/SwingSet/src/types.js +++ b/packages/SwingSet/src/types.js @@ -87,7 +87,8 @@ * @typedef { VatDeliveryMessage | VatDeliveryNotify | VatDeliveryDropExports * | VatDeliveryRetireExports | VatDeliveryRetireImports * } VatDeliveryObject - * @typedef { [tag: 'ok', message: null, usage: unknown] | [tag: 'error', message: string, usage: unknown | null] } VatDeliveryResult + * @typedef { [tag: 'ok', message: null, usage: { compute: number } | null] | + * [tag: 'error', message: string, usage: unknown | null] } VatDeliveryResult * * @typedef { [tag: 'send', target: string, msg: Message] } VatSyscallSend * @typedef { [tag: 'callNow', target: string, method: string, args: SwingSetCapData]} VatSyscallCallNow diff --git a/packages/SwingSet/test/gc-dead-vat/bootstrap.js b/packages/SwingSet/test/gc-dead-vat/bootstrap.js index 70fd03dd5fb..fc44dac059d 100644 --- a/packages/SwingSet/test/gc-dead-vat/bootstrap.js +++ b/packages/SwingSet/test/gc-dead-vat/bootstrap.js @@ -15,7 +15,7 @@ export function buildRootObject() { return Far('root', { async bootstrap(vats, devices) { const vatMaker = E(vats.vatAdmin).createVatAdminService(devices.vatAdmin); - vat = await E(vatMaker).createVatByName('doomed', { metered: false }); + vat = await E(vatMaker).createVatByName('doomed'); doomedRoot = vat.root; await sendExport(doomedRoot); const doomedExport1Presence = await E(doomedRoot).getDoomedExport1(); diff --git a/packages/SwingSet/test/metering/test-dynamic-vat-metered.js b/packages/SwingSet/test/metering/test-dynamic-vat-metered.js index f0e17df429c..1886e899455 100644 --- a/packages/SwingSet/test/metering/test-dynamic-vat-metered.js +++ b/packages/SwingSet/test/metering/test-dynamic-vat-metered.js @@ -5,16 +5,10 @@ import { test } from '../../tools/prepare-test-env-ava.js'; // eslint-disable-next-line import/order import path from 'path'; import bundleSource from '@agoric/bundle-source'; +import { parse } from '@agoric/marshal'; import { provideHostStorage } from '../../src/hostStorage.js'; import { buildKernelBundles, buildVatController } from '../../src/index.js'; - -function capdata(body, slots = []) { - return harden({ body, slots }); -} - -function capargs(args, slots = []) { - return capdata(JSON.stringify(args), slots); -} +import { capargs } from '../util.js'; async function prepare() { const kernelBundles = await buildKernelBundles(); @@ -33,6 +27,59 @@ test.before(async t => { t.context.data = await prepare(); }); +function extractSlot(t, data) { + const marg = JSON.parse(data.body); + t.is(marg['@qclass'], 'slot'); + t.is(marg.index, 0); + return { marg, meterKref: data.slots[0] }; +} + +test('meter objects', async t => { + const { kernelBundles, bootstrapBundle } = t.context.data; + const config = { + bootstrap: 'bootstrap', + vats: { + bootstrap: { + bundle: bootstrapBundle, + }, + }, + }; + const hostStorage = provideHostStorage(); + const c = await buildVatController(config, [], { + hostStorage, + kernelBundles, + }); + c.pinVatRoot('bootstrap'); + + // let the vatAdminService get wired up before we create any new vats + await c.run(); + + // create and manipulate a meter without attaching it to a vat + const cmargs = capargs([10n, 5n]); // remaining, notify threshold + const kp1 = c.queueToVatRoot('bootstrap', 'createMeter', cmargs); + await c.run(); + const { marg, meterKref } = extractSlot(t, c.kpResolution(kp1)); + async function doMeter(method, ...args) { + const kp = c.queueToVatRoot( + 'bootstrap', + method, + capargs([marg, ...args], [meterKref]), + ); + await c.run(); + return c.kpResolution(kp); + } + async function getMeter() { + const res = await doMeter('getMeter'); + return parse(res.body); + } + + t.deepEqual(await getMeter(), { remaining: 10n, threshold: 5n }); + await doMeter('addMeterRemaining', 8n); + t.deepEqual(await getMeter(), { remaining: 18n, threshold: 5n }); + await doMeter('setMeterThreshold', 7n); + t.deepEqual(await getMeter(), { remaining: 18n, threshold: 7n }); +}); + function kpidRejected(t, c, kpid, message) { t.is(c.kpStatus(kpid), 'rejected'); const resCapdata = c.kpResolution(kpid); @@ -64,9 +111,15 @@ async function overflowCrank(t, explosion) { // let the vatAdminService get wired up before we create any new vats await c.run(); + // create a meter with 10M remaining + const cmargs = capargs([10000000n, 5000000n]); // remaining, notifyThreshold + const kp1 = c.queueToVatRoot('bootstrap', 'createMeter', cmargs); + await c.run(); + const { marg, meterKref } = extractSlot(t, c.kpResolution(kp1)); + // 'createVat' will import the bundle - const cvopts = { managerType, metered: true }; - const cvargs = capargs([dynamicVatBundle, cvopts]); + const cvopts = { managerType, meter: marg }; + const cvargs = capargs([dynamicVatBundle, cvopts], [meterKref]); const kp2 = c.queueToVatRoot('bootstrap', 'createVat', cvargs); await c.run(); const res2 = c.kpResolution(kp2); @@ -135,3 +188,190 @@ test('exceed per-crank compute', t => { test('exceed stack', t => { return overflowCrank(t, 'stack'); }); + +test('meter decrements', async t => { + const managerType = 'xs-worker'; + const { kernelBundles, dynamicVatBundle, bootstrapBundle } = t.context.data; + const config = { + bootstrap: 'bootstrap', + vats: { + bootstrap: { + bundle: bootstrapBundle, + }, + }, + }; + const hostStorage = provideHostStorage(); + const c = await buildVatController(config, [], { + hostStorage, + kernelBundles, + }); + c.pinVatRoot('bootstrap'); + + // let the vatAdminService get wired up before we create any new vats + await c.run(); + + // create a meter with 200k remaining and a 100K notification threshold + const cmargs = capargs([200000n, 100000n]); // remaining, notifyThreshold + const kp1 = c.queueToVatRoot('bootstrap', 'createMeter', cmargs); + await c.run(); + const { marg, meterKref } = extractSlot(t, c.kpResolution(kp1)); + // and watch for its notifyThreshold to fire + const notifyKPID = c.queueToVatRoot( + 'bootstrap', + 'whenMeterNotifiesNext', + capargs([marg], [meterKref]), + ); + + // 'createVat' will import the bundle + const cvargs = capargs( + [dynamicVatBundle, { managerType, meter: marg }], + [meterKref], + ); + const kp2 = c.queueToVatRoot('bootstrap', 'createVat', cvargs); + await c.run(); + const res2 = c.kpResolution(kp2); + t.is(JSON.parse(res2.body)[0], 'created', res2.body); + const doneKPID = res2.slots[0]; + + async function getMeter() { + const args = capargs([marg], [meterKref]); + const kp = c.queueToVatRoot('bootstrap', 'getMeter', args); + await c.run(); + const res = c.kpResolution(kp); + const { remaining } = parse(res.body); + return remaining; + } + + async function consume(shouldComplete) { + const kp = c.queueToVatRoot('bootstrap', 'run', capargs([])); + await c.run(); + if (shouldComplete) { + t.is(c.kpStatus(kp), 'fulfilled'); + t.deepEqual(c.kpResolution(kp), capargs(42)); + } else { + t.is(c.kpStatus(kp), 'rejected'); + kpidRejected(t, c, kp, 'vat terminated'); + } + } + + let remaining = await getMeter(); + t.is(remaining, 200000n); + + // messages to the metered vat should decrement the meter + await consume(true); + remaining = await getMeter(); + console.log(remaining); + t.not(remaining, 200000n); + + // experiments show a simple 'run()' currently uses 36918 computrons the + // first time, 36504 the subsequent times, so two more calls ought to + // trigger the notification threshold + await consume(true); + remaining = await getMeter(); + console.log(remaining); + t.is(c.kpStatus(notifyKPID), 'unresolved'); + // this one will trigger notification + await consume(true); + remaining = await getMeter(); + console.log(remaining); + t.is(c.kpStatus(notifyKPID), 'fulfilled'); + const notification = c.kpResolution(notifyKPID); + t.is(parse(notification.body).value, remaining); + + // doneP should still be unresolved + t.is(c.kpStatus(doneKPID), 'unresolved'); + + // three more calls should cause the meter to underflow, killing the vat + await consume(true); + remaining = await getMeter(); + console.log(remaining); + await consume(true); + remaining = await getMeter(); + console.log(remaining); + console.log(`consume() about to underflow`); + await consume(false); + remaining = await getMeter(); + console.log(remaining); + t.is(remaining, 0n); // this checks postAbortActions.deductMeter + + // TODO: we currently provide a different .done error message for 1: a + // single crank exceeds the fixed per-crank limit, and 2: the cumulative + // usage caused the meterID to underflow. Should these be the same? + kpidRejected(t, c, doneKPID, 'meter underflow, vat terminated'); +}); + +test('unlimited meter', async t => { + const managerType = 'xs-worker'; + const { kernelBundles, dynamicVatBundle, bootstrapBundle } = t.context.data; + const config = { + bootstrap: 'bootstrap', + vats: { + bootstrap: { + bundle: bootstrapBundle, + }, + }, + }; + const hostStorage = provideHostStorage(); + const c = await buildVatController(config, [], { + hostStorage, + kernelBundles, + }); + c.pinVatRoot('bootstrap'); + + // let the vatAdminService get wired up before we create any new vats + await c.run(); + + // create an unlimited meter + const cmargs = capargs([]); + const kp1 = c.queueToVatRoot('bootstrap', 'createUnlimitedMeter', cmargs); + await c.run(); + const { marg, meterKref } = extractSlot(t, c.kpResolution(kp1)); + + // 'createVat' will import the bundle + const cvargs = capargs( + [dynamicVatBundle, { managerType, meter: marg }], + [meterKref], + ); + const kp2 = c.queueToVatRoot('bootstrap', 'createVat', cvargs); + await c.run(); + const res2 = c.kpResolution(kp2); + t.is(JSON.parse(res2.body)[0], 'created', res2.body); + const doneKPID = res2.slots[0]; + + async function getMeter() { + const args = capargs([marg], [meterKref]); + const kp = c.queueToVatRoot('bootstrap', 'getMeter', args); + await c.run(); + const res = c.kpResolution(kp); + const { remaining } = parse(res.body); + return remaining; + } + + async function consume(shouldComplete) { + const kp = c.queueToVatRoot('bootstrap', 'run', capargs([])); + await c.run(); + if (shouldComplete) { + t.is(c.kpStatus(kp), 'fulfilled'); + t.deepEqual(c.kpResolution(kp), capargs(42)); + } else { + t.is(c.kpStatus(kp), 'rejected'); + kpidRejected(t, c, kp, 'vat terminated'); + } + } + + let remaining = await getMeter(); + t.is(remaining, 'unlimited'); + + // messages to the vat do not decrement the meter + await consume(true); + remaining = await getMeter(); + t.is(remaining, 'unlimited'); + + // but each crank is still limited, so an infinite loop will kill the vat + const kp4 = c.queueToVatRoot('bootstrap', 'explode', capargs(['compute'])); + await c.run(); + kpidRejected(t, c, kp4, 'vat terminated'); + kpidRejected(t, c, doneKPID, 'Compute meter exceeded'); +}); + +// TODO notify and underflow in the same delivery, to exercise postAbortActions diff --git a/packages/SwingSet/test/metering/test-dynamic-vat-unmetered.js b/packages/SwingSet/test/metering/test-dynamic-vat-unmetered.js index 9c5519db959..4933854ab57 100644 --- a/packages/SwingSet/test/metering/test-dynamic-vat-unmetered.js +++ b/packages/SwingSet/test/metering/test-dynamic-vat-unmetered.js @@ -15,12 +15,12 @@ function capargs(args, slots = []) { return capdata(JSON.stringify(args), slots); } -// Dynamic vats can be created without metering +// Dynamic vats are created without metering by default test('unmetered dynamic vat', async t => { const config = { bootstrap: 'bootstrap', - defaultManagerType: 'local', + defaultManagerType: 'xs-worker', vats: { bootstrap: { sourceSpec: path.join(__dirname, 'vat-load-dynamic.js'), @@ -43,7 +43,7 @@ test('unmetered dynamic vat', async t => { const kp1 = c.queueToVatRoot( 'bootstrap', 'createVat', - capargs([dynamicVatBundle, { metered: false }]), + capargs([dynamicVatBundle]), 'panic', ); await c.run(); diff --git a/packages/SwingSet/test/metering/vat-load-dynamic.js b/packages/SwingSet/test/metering/vat-load-dynamic.js index cdb63747e04..565c1d5ec8f 100644 --- a/packages/SwingSet/test/metering/vat-load-dynamic.js +++ b/packages/SwingSet/test/metering/vat-load-dynamic.js @@ -11,6 +11,32 @@ export function buildRootObject(vatPowers) { service = await E(vats.vatAdmin).createVatAdminService(devices.vatAdmin); }, + createMeter(remaining, notifyThreshold) { + return E(service).createMeter(remaining, notifyThreshold); + }, + + createUnlimitedMeter() { + return E(service).createUnlimitedMeter(); + }, + + addMeterRemaining(meter, remaining) { + return E(meter).addRemaining(remaining); + }, + + setMeterThreshold(meter, threshold) { + return E(meter).setThreshold(threshold); + }, + + getMeter(meter) { + return E(meter).get(); + }, + + async whenMeterNotifiesNext(meter) { + const notifier = await E(meter).getNotifier(); + const initial = await E(notifier).getUpdateSince(); + return E(notifier).getUpdateSince(initial); + }, + async createVat(bundle, dynamicOptions) { control = await E(service).createVat(bundle, dynamicOptions); const done = E(control.adminNode).done(); diff --git a/packages/SwingSet/test/test-gc-kernel.js b/packages/SwingSet/test/test-gc-kernel.js index d2cade5a4f6..5cda7ed875f 100644 --- a/packages/SwingSet/test/test-gc-kernel.js +++ b/packages/SwingSet/test/test-gc-kernel.js @@ -22,17 +22,10 @@ import { makeDropExports, makeRetireExports, makeRetireImports, + capargs, capdataOneSlot, } from './util'; -function capdata(body, slots = []) { - return harden({ body, slots }); -} - -function capargs(args, slots = []) { - return capdata(JSON.stringify(args), slots); -} - function makeConsole(tag) { const log = anylogger(tag); const cons = {}; @@ -42,14 +35,13 @@ function makeConsole(tag) { return harden(cons); } -function bigintReplacer(_, arg) { - if (typeof arg === 'bigint') { - return Number(arg); - } - return arg; -} - function writeSlogObject(o) { + function bigintReplacer(_, arg) { + if (typeof arg === 'bigint') { + return Number(arg); + } + return arg; + } 0 && console.log(JSON.stringify(o, bigintReplacer)); } diff --git a/packages/SwingSet/test/util.js b/packages/SwingSet/test/util.js index fba457673ca..737988542b3 100644 --- a/packages/SwingSet/test/util.js +++ b/packages/SwingSet/test/util.js @@ -1,4 +1,5 @@ import { assert } from '@agoric/assert'; +import { QCLASS } from '@agoric/marshal'; function compareArraysOfStrings(a, b) { a = a.join(' '); @@ -84,12 +85,19 @@ export function extractMessage(vatDeliverObject) { return { facetID, method, args, result }; } -function capdata(body, slots = []) { +export function capdata(body, slots = []) { return harden({ body, slots }); } +function marshalBigIntReplacer(_, arg) { + if (typeof arg === 'bigint') { + return { [QCLASS]: 'bigint', digits: String(arg) }; + } + return arg; +} + export function capargs(args, slots = []) { - return capdata(JSON.stringify(args), slots); + return capdata(JSON.stringify(args, marshalBigIntReplacer), slots); } export function capdataOneSlot(slot) { From cc1d3c5604cf848a69ef55e61662c6094d5ff341 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Thu, 22 Jul 2021 00:51:15 -0700 Subject: [PATCH 3/8] fix(swingset-runner): remove --meter controls Now that dynamic vats require a Meter object in the `meter:` option, rather than just a `metered:` boolean flag, the swingset-runniner `--meter` option doesn't work anymore. I'm removing it to allow the test to keep passing. Eventually it might be interesting to improve the CLI arguments to create and provide a Meter, but the need for user-level code to supervise and refill it makes that non-trivial. --- packages/swingset-runner/src/main.js | 19 ------------------- packages/swingset-runner/src/vat-launcher.js | 2 +- 2 files changed, 1 insertion(+), 20 deletions(-) diff --git a/packages/swingset-runner/src/main.js b/packages/swingset-runner/src/main.js index c3c03080f43..808ca5281cf 100644 --- a/packages/swingset-runner/src/main.js +++ b/packages/swingset-runner/src/main.js @@ -74,8 +74,6 @@ FLAGS may be: --statsfile FILE - output performance stats to FILE as a JSON object --benchmark N - perform an N round benchmark after the initial run --indirect - launch swingset from a vat instead of launching directly - --globalmetering - install metering on global objects - --meter - run metered vats (implies --globalmetering and --indirect) --config FILE - read swingset config from FILE instead of inferring it CMD is one of: @@ -175,8 +173,6 @@ export async function main() { let dumpTag = 't'; let rawMode = false; let shouldPrintStats = false; - let globalMeteringActive = false; - let meterVats = false; let launchIndirectly = false; let benchmarkRounds = 0; let configPath = null; @@ -267,14 +263,6 @@ export async function main() { case '--statsfile': statsFile = argv.shift(); break; - case '--globalmetering': - globalMeteringActive = true; - break; - case '--meter': - meterVats = true; - globalMeteringActive = true; - launchIndirectly = true; - break; case '--indirect': launchIndirectly = true; break; @@ -309,10 +297,6 @@ export async function main() { process.exit(0); } - if (globalMeteringActive) { - log('global metering is active'); - } - if (forceGC) { if (!logMem) { log('Warning: --forcegc without --logmem may be a mistake'); @@ -382,9 +366,6 @@ export async function main() { default: fail(`invalid database mode ${dbMode}`, true); } - if (config.bootstrap) { - config.vats[config.bootstrap].parameters.metered = meterVats; - } const runtimeOptions = {}; if (verbose) { runtimeOptions.verbose = true; diff --git a/packages/swingset-runner/src/vat-launcher.js b/packages/swingset-runner/src/vat-launcher.js index 6d3f2c5e35a..f849292c65a 100644 --- a/packages/swingset-runner/src/vat-launcher.js +++ b/packages/swingset-runner/src/vat-launcher.js @@ -41,7 +41,7 @@ export function buildRootObject(_vatPowers, vatParameters) { // eslint-disable-next-line no-await-in-loop const vat = await E(vatMaker).createVatByName( bundleName, - { metered: vatParameters.metered, vatParameters: harden(subvatParameters) }, + { vatParameters: harden(subvatParameters) }, ); vatRoots[vatName] = vat.root; } From 04d4fd96982ecd02de50f09fa38c6e2800cca527 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Sat, 24 Jul 2021 16:41:27 -0700 Subject: [PATCH 4/8] fix: zoe/spawner/pegasus: use unlimited Meter, not metered: true Zoe and spawner use `createVat()` to do their jobs. Previously, they used `{ metered: true }`, which provided a per-crank limit but not cumulative limit. This changes both to use `createUnlimitedMeter()`, then pass that as a `{ meter }` option, to achieve the same effect. When Zoe is ready to maintain (and refill) a Meter, change that to use `createMeter()` instead of `createUnlimitedMeter()`. This also adds `createMeter` and `createUnlimitedMeter` methods to the fake vatAdmin objects used by the zoe/spawner/pegasus unit tests, so the new Zoe/spawner code will work against the mocks. refs #3308 --- packages/pegasus/test/fakeVatAdmin.js | 2 ++ packages/spawner/src/contractHost.js | 3 ++- packages/zoe/src/zoeService/createZCFVat.js | 10 ++++++---- packages/zoe/test/unitTests/zoe/test-createZCFVat.js | 2 ++ packages/zoe/tools/fakeVatAdmin.js | 2 ++ 5 files changed, 14 insertions(+), 5 deletions(-) diff --git a/packages/pegasus/test/fakeVatAdmin.js b/packages/pegasus/test/fakeVatAdmin.js index 57ec454ba3a..41eec4834c2 100644 --- a/packages/pegasus/test/fakeVatAdmin.js +++ b/packages/pegasus/test/fakeVatAdmin.js @@ -4,6 +4,8 @@ import { makePromiseKit } from '@agoric/promise-kit'; import { evalContractBundle } from '@agoric/zoe/src/contractFacet/evalContractCode'; export default harden({ + createMeter: () => {}, + createUnlimitedMeter: () => {}, createVat: bundle => { return harden({ root: E(evalContractBundle(bundle)).buildRootObject(), diff --git a/packages/spawner/src/contractHost.js b/packages/spawner/src/contractHost.js index 0605781a7f4..d4df6a62b4f 100644 --- a/packages/spawner/src/contractHost.js +++ b/packages/spawner/src/contractHost.js @@ -13,7 +13,8 @@ function makeSpawner(vatAdminSvc) { assert(!oldModuleFormat, 'oldModuleFormat not supported'); return Far('installer', { async spawn(argsP) { - const opts = { metered: true }; + const meter = await E(vatAdminSvc).createUnlimitedMeter(); + const opts = { meter }; const { root } = await E(vatAdminSvc).createVat(spawnBundle, opts); return E(E(root).loadBundle(bundle)).start(argsP); }, diff --git a/packages/zoe/src/zoeService/createZCFVat.js b/packages/zoe/src/zoeService/createZCFVat.js index 459c1b32fc6..718e2676dd6 100644 --- a/packages/zoe/src/zoeService/createZCFVat.js +++ b/packages/zoe/src/zoeService/createZCFVat.js @@ -12,9 +12,11 @@ import zcfContractBundle from '../../bundles/bundle-contractFacet'; */ export const setupCreateZCFVat = (vatAdminSvc, zcfBundleName = undefined) => { /** @type {CreateZCFVat} */ - const createZCFVat = () => - typeof zcfBundleName === 'string' - ? E(vatAdminSvc).createVatByName(zcfBundleName, { metered: true }) - : E(vatAdminSvc).createVat(zcfContractBundle, { metered: true }); + const createZCFVat = async () => { + const meter = await E(vatAdminSvc).createUnlimitedMeter(); + return typeof zcfBundleName === 'string' + ? E(vatAdminSvc).createVatByName(zcfBundleName, { meter }) + : E(vatAdminSvc).createVat(zcfContractBundle, { meter }); + }; return createZCFVat; }; diff --git a/packages/zoe/test/unitTests/zoe/test-createZCFVat.js b/packages/zoe/test/unitTests/zoe/test-createZCFVat.js index 743ff94fc86..bfd8b27d343 100644 --- a/packages/zoe/test/unitTests/zoe/test-createZCFVat.js +++ b/packages/zoe/test/unitTests/zoe/test-createZCFVat.js @@ -12,6 +12,8 @@ test('setupCreateZCFVat', async t => { // creates a new vat const fakeVatAdminSvc = Far('fakeVatAdminSvc', { + createMeter: () => {}, + createUnlimitedMeter: () => {}, createVatByName: name => name, createVat: _bundle => 'zcfBundle', }); diff --git a/packages/zoe/tools/fakeVatAdmin.js b/packages/zoe/tools/fakeVatAdmin.js index ac8b03c3a12..aae2068ee37 100644 --- a/packages/zoe/tools/fakeVatAdmin.js +++ b/packages/zoe/tools/fakeVatAdmin.js @@ -31,6 +31,8 @@ function makeFakeVatAdmin(testContextSetter = undefined, makeRemote = x => x) { // test-only state can be provided from contracts // to their tests. const admin = Far('vatAdmin', { + createMeter: () => {}, + createUnlimitedMeter: () => {}, createVat: bundle => { return harden({ root: makeRemote( From a886f412ccb7cfc8b05cdf4f186e0feaae7ac104 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Sat, 24 Jul 2021 16:42:21 -0700 Subject: [PATCH 5/8] chore(spawner): update one test to use prepare-test-env-ava.js This improves the error messages when something goes wrong. --- .../test/swingsetTests/contractHost/test-contractHost.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/spawner/test/swingsetTests/contractHost/test-contractHost.js b/packages/spawner/test/swingsetTests/contractHost/test-contractHost.js index e806a661ce8..4dd218386fe 100644 --- a/packages/spawner/test/swingsetTests/contractHost/test-contractHost.js +++ b/packages/spawner/test/swingsetTests/contractHost/test-contractHost.js @@ -1,9 +1,7 @@ /* global __dirname */ -// TODO Remove babel-standalone preinitialization -// https://github.com/endojs/endo/issues/768 -import '@agoric/babel-standalone'; -import '@agoric/install-ses'; -import test from 'ava'; +// eslint-disable-next-line import/order +import { test } from '@agoric/swingset-vat/tools/prepare-test-env-ava.js'; +// eslint-disable-next-line import/order import path from 'path'; import bundleSource from '@agoric/bundle-source'; import { From 077dcec47f2b999326846c561953b911f42c93f8 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Sat, 24 Jul 2021 18:46:16 -0700 Subject: [PATCH 6/8] fix(swingset): test simultaneous underflow+notify, simplify kernel When a crank causes the compute Meter to both hit zero *and* cross the notification threshold at the same time, we need extra code to make sure the run-queue push of the notification message does not get deleted by the `abortCrank()` that unwinds the vat's side-effects. The mechanism I wrote for this worked, but was overkill. After writing a test for it, I noticed the test still passed even if I commented out the mechanism that I thought was necessary. I simplified that code (we only need to repeat the `deductMeter`, because that will take care of repeating the notification), and added the test. refs #3308 --- packages/SwingSet/src/kernel/kernel.js | 24 +++-- .../test/metering/test-dynamic-vat-metered.js | 97 ++++++++++++++++++- 2 files changed, 107 insertions(+), 14 deletions(-) diff --git a/packages/SwingSet/src/kernel/kernel.js b/packages/SwingSet/src/kernel/kernel.js index 01b4d54979c..1d9169258d8 100644 --- a/packages/SwingSet/src/kernel/kernel.js +++ b/packages/SwingSet/src/kernel/kernel.js @@ -381,7 +381,6 @@ export default function buildKernel( terminationTrigger = undefined; postAbortActions = { meterDeductions: [], // list of { meterID, compute } - meterNotifications: [], // list of meterID }; } resetDeliveryTriggers(); @@ -397,17 +396,19 @@ export default function buildKernel( function deductMeter(meterID, compute, firstTime) { assert.typeof(compute, 'bigint'); const res = kernelKeeper.deductMeter(meterID, compute); - // we also recode deduction and any notification in postAbortActions, - // which are executed if the delivery is being rewound for any reason - // (syscall error, res.underflow), so their side-effects survive + + // We record the deductMeter() in postAbortActions.meterDeductions. If + // the delivery is rewound for any reason (syscall error, res.underflow), + // then deliverAndLogToVat will repeat the deductMeter (which will repeat + // the notifyMeterThreshold), so their side-effects will survive the + // abortCrank(). But we don't record it (again) during the repeat, to + // make sure exactly one copy of the changes will be committed. + if (firstTime) { postAbortActions.meterDeductions.push({ meterID, compute }); } if (res.notify) { notifyMeterThreshold(meterID); - if (firstTime) { - postAbortActions.meterNotifications.push(meterID); - } } return res.underflow; } @@ -730,14 +731,11 @@ export default function buildKernel( // errors unwind any changes the vat made abortCrank(); didAbort = true; - // but metering deductions or underflow notifications must survive - const { meterDeductions, meterNotifications } = postAbortActions; + // but metering deductions and underflow notifications must survive + const { meterDeductions } = postAbortActions; for (const { meterID, compute } of meterDeductions) { deductMeter(meterID, compute, false); - } - for (const meterID of meterNotifications) { - // reads meter.remaining, so must happen after deductMeter - notifyMeterThreshold(meterID); + // that will re-push any notifications } } // state changes reflecting the termination must also survive, so diff --git a/packages/SwingSet/test/metering/test-dynamic-vat-metered.js b/packages/SwingSet/test/metering/test-dynamic-vat-metered.js index 1886e899455..09bc1304444 100644 --- a/packages/SwingSet/test/metering/test-dynamic-vat-metered.js +++ b/packages/SwingSet/test/metering/test-dynamic-vat-metered.js @@ -1,4 +1,5 @@ /* global __dirname */ +/* eslint-disable no-await-in-loop */ // eslint-disable-next-line import/order import { test } from '../../tools/prepare-test-env-ava.js'; @@ -374,4 +375,98 @@ test('unlimited meter', async t => { kpidRejected(t, c, doneKPID, 'Compute meter exceeded'); }); -// TODO notify and underflow in the same delivery, to exercise postAbortActions +// Cause both a notify and an underflow in the same delivery. Without +// postAbortActions, the notify would get unwound by the vat termination, and +// would never be delivered. +test('notify and underflow', async t => { + const managerType = 'xs-worker'; + const { kernelBundles, dynamicVatBundle, bootstrapBundle } = t.context.data; + const config = { + bootstrap: 'bootstrap', + vats: { + bootstrap: { + bundle: bootstrapBundle, + }, + }, + }; + const hostStorage = provideHostStorage(); + const c = await buildVatController(config, [], { + hostStorage, + kernelBundles, + }); + c.pinVatRoot('bootstrap'); + + // let the vatAdminService get wired up before we create any new vats + await c.run(); + + // create a meter with 200k remaining and a notification threshold of 1 + const cmargs = capargs([200000n, 1n]); // remaining, notifyThreshold + const kp1 = c.queueToVatRoot('bootstrap', 'createMeter', cmargs); + await c.run(); + const { marg, meterKref } = extractSlot(t, c.kpResolution(kp1)); + // and watch for its notifyThreshold to fire + const notifyKPID = c.queueToVatRoot( + 'bootstrap', + 'whenMeterNotifiesNext', + capargs([marg], [meterKref]), + ); + + // 'createVat' will import the bundle + const cvargs = capargs( + [dynamicVatBundle, { managerType, meter: marg }], + [meterKref], + ); + const kp2 = c.queueToVatRoot('bootstrap', 'createVat', cvargs); + await c.run(); + const res2 = c.kpResolution(kp2); + t.is(JSON.parse(res2.body)[0], 'created', res2.body); + const doneKPID = res2.slots[0]; + + async function getMeter() { + const args = capargs([marg], [meterKref]); + const kp = c.queueToVatRoot('bootstrap', 'getMeter', args); + await c.run(); + const res = c.kpResolution(kp); + const { remaining } = parse(res.body); + return remaining; + } + + async function consume(shouldComplete) { + const kp = c.queueToVatRoot('bootstrap', 'run', capargs([])); + await c.run(); + if (shouldComplete) { + t.is(c.kpStatus(kp), 'fulfilled'); + t.deepEqual(c.kpResolution(kp), capargs(42)); + } else { + t.is(c.kpStatus(kp), 'rejected'); + kpidRejected(t, c, kp, 'vat terminated'); + } + } + + // run three consume() calls to measure the usage of the last + await consume(true); + await consume(true); + const remaining1 = await getMeter(); + await consume(true); + let remaining = await getMeter(); + const oneCycle = remaining1 - remaining; + // console.log(`one cycle appears to use ${oneCycle} computrons`); + + // keep consuming until there is less than oneCycle remaining + while (remaining > oneCycle) { + await consume(true); + remaining = await getMeter(); + // console.log(` now ${remaining}`); + } + + // the next cycle should underflow *and* trip the absurdly low notification + // threshold + // console.log(`-- doing last consume()`); + await consume(false); + remaining = await getMeter(); + t.is(remaining, 0n); // this checks postAbortActions.deductMeter + t.is(c.kpStatus(notifyKPID), 'fulfilled'); // and pAA.meterNotifications + const notification = c.kpResolution(notifyKPID); + t.is(parse(notification.body).value, 0n); + kpidRejected(t, c, doneKPID, 'meter underflow, vat terminated'); +}); From dc63cc285f7ae532f683788ccfdc91e2df1ddde9 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Sun, 25 Jul 2021 01:14:34 -0700 Subject: [PATCH 7/8] chore: improve metering docs * add examples of computron usage numbers * fix typo --- packages/SwingSet/docs/metering.md | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/packages/SwingSet/docs/metering.md b/packages/SwingSet/docs/metering.md index aecc1066c4c..63107e377e7 100644 --- a/packages/SwingSet/docs/metering.md +++ b/packages/SwingSet/docs/metering.md @@ -2,7 +2,7 @@ The Halting Problem is unsolvable: no amount of static analysis or human auditing can pre-determine how many steps an arbitrary Turing-complete program will take before it finishes, or if it will ever finish. To prevent the code in one vat from preventing execution of code in other vats (or the kernel itself), SwingSet provides a mechanism to limit the amount of computation that each vat can perform. Any vat which exceeds its limit is terminated, and any messages it sent before the limit was reached are cancelled. -Two limits can be imposed. The first is a per-crank limit. Each message delivered to a vat results in a sequence of "turns" known as a "crank". A crank is also triggered when the vat receives notification of a kernel-side promise being resolved or rejected. Cranks run until the vat stops adding work to the resolved-promise queue, and there is nothing left to do until the next message or notification arrives. A per-crank limit imports a ceiling on the amount of computation that can be done during each crank, but does not say anything about the number of cranks that can be run. +Two limits can be imposed. The first is a per-crank limit. Each message delivered to a vat results in a sequence of "turns" known as a "crank". A crank is also triggered when the vat receives notification of a kernel-side promise being resolved or rejected. Cranks run until the vat stops adding work to the resolved-promise queue, and there is nothing left to do until the next message or notification arrives. A per-crank limit imparts a ceiling on the amount of computation that can be done during each crank, but does not say anything about the number of cranks that can be run. The second limit spans multiple cranks and is managed by the "Meter": a variable-sized reservoir of execution credits. Each vat can be associated with a single Meter, and the remaining capacity of the Meter is reduced at the end of each crank by whatever amount the vat consumed during that crank. The Meter can be refilled by sending it a message, but if any crank causes the Meter's remaining value to drop below zero, the vat is terminated. @@ -12,7 +12,25 @@ SwingSet measures computation with a unit named the "computron": the smallest un Metering is provided by low-level code in the JavaScript engine, which is counting basic operations like "read a property from an object" and "add two numbers". This is larger than a CPU cycle. The exact mapping depends upon intricate details of the engine, and is likely to change if/when the JS engine is upgraded. SwingSet kernels that participate in a consensus machine must be careful to synchronize upgrades to prevent divergence of metering results. -TODO: add examples of computron usage numbers +To gain some intuition on how "big" a computron is, here are some examples: + +* An empty function: 36560 computrons. This is the base overhead for each message delivery (dispatch.deliver) +* Adding `async` to a function (which creates a return Promise): 98 +* `let i = 1`: 3 +* `i += 2`: 4 +* `let sum; for (let i=0; i<100; i++) { sum += i; }`: 1412 + * same, but adding to 1000: 14012 +* defining a `harden()`ed add/read "counter" object: 1475 + * invoking `add()`: 19 +* `console.log('')`: 1011 computrons +* ERTP `getBrand()`: 49300 +* ERTP `getCurrentAmount()`: 54240 +* ERTP `getUpdateSince()`: 59084 +* ERTP `deposit()`: 124775 +* ERTP `withdraw()`: 111141 +* Zoe `install()`: 62901 +* ZCF `executeContract()` of the Multi-Pool Autoswap contract: 12.9M +* ZCF `executeContract()` (importBundle) of the Treasury contract: 13.5M Computrons have a loose relationship to wallclock time, but are generally correlated, so tracking the cumulative computrons spent during SwingSet cranks can provide a rough measure of how much time is being spent, which can be useful to e.g. limit blocks to a reasonable amount of execution time. From d3471b8f2eb13a904bb4513d9924b4c658805b4a Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Sun, 25 Jul 2021 01:15:20 -0700 Subject: [PATCH 8/8] chore: teardown vats properly in test-dynamic-vat-metered.js --- packages/SwingSet/test/metering/test-dynamic-vat-metered.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/SwingSet/test/metering/test-dynamic-vat-metered.js b/packages/SwingSet/test/metering/test-dynamic-vat-metered.js index 09bc1304444..2de06233071 100644 --- a/packages/SwingSet/test/metering/test-dynamic-vat-metered.js +++ b/packages/SwingSet/test/metering/test-dynamic-vat-metered.js @@ -50,6 +50,7 @@ test('meter objects', async t => { hostStorage, kernelBundles, }); + t.teardown(c.shutdown); c.pinVatRoot('bootstrap'); // let the vatAdminService get wired up before we create any new vats @@ -107,6 +108,7 @@ async function overflowCrank(t, explosion) { hostStorage, kernelBundles, }); + t.teardown(c.shutdown); c.pinVatRoot('bootstrap'); // let the vatAdminService get wired up before we create any new vats @@ -206,6 +208,7 @@ test('meter decrements', async t => { hostStorage, kernelBundles, }); + t.teardown(c.shutdown); c.pinVatRoot('bootstrap'); // let the vatAdminService get wired up before we create any new vats @@ -317,6 +320,7 @@ test('unlimited meter', async t => { hostStorage, kernelBundles, }); + t.teardown(c.shutdown); c.pinVatRoot('bootstrap'); // let the vatAdminService get wired up before we create any new vats @@ -394,6 +398,7 @@ test('notify and underflow', async t => { hostStorage, kernelBundles, }); + t.teardown(c.shutdown); c.pinVatRoot('bootstrap'); // let the vatAdminService get wired up before we create any new vats