diff --git a/packages/SwingSet/src/kernel/kernel.js b/packages/SwingSet/src/kernel/kernel.js index a344e1eb07cc..326cb9e201eb 100644 --- a/packages/SwingSet/src/kernel/kernel.js +++ b/packages/SwingSet/src/kernel/kernel.js @@ -379,12 +379,42 @@ 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: JSON.stringify([meterID, remaining]), slots: [] }; + queueToKref(vatAdminRootKref, 'meterCrossedThreshold', args, 'logFailure'); + } + + function deductMeter(meterID, compute, firstTime) { + assert.typeof(compute, 'number'); + 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) { @@ -396,12 +426,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,11 +443,21 @@ export default function buildKernel( const deliveryResult = await vatWarehouse.deliverToVat(vatID, vd); insistVatDeliveryResult(deliveryResult); finish(deliveryResult); - const [status, problem] = deliveryResult; + const [status, problem, metering] = deliveryResult; if (status !== 'ok') { // 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 (useMeter && meterID) { + const underflow = deductMeter(meterID, metering.compute, 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 @@ -438,7 +479,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); } } @@ -551,7 +592,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); } } @@ -575,7 +616,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) { @@ -687,9 +728,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)}`); @@ -915,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 37f68de87511..009d4e75ebc0 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 true for - * dynamic vats; static vats may not be metered. + * @param {string} [options.meter] 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 = isDynamic, + 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 7908340d7ccd..189800772d6a 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 // non-negative remaining capacity (in computrons) // 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 87fdec7a8eeb..71f7b51250b8 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 531f4bbe8635..ac50ece6d951 100644 --- a/packages/SwingSet/src/kernel/vatAdmin/vatAdmin-src.js +++ b/packages/SwingSet/src/kernel/vatAdmin/vatAdmin-src.js @@ -18,12 +18,33 @@ */ 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)); + }, + 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 30d359387c32..baf96af3ce9a 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,26 @@ 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 Numbers + getNotifier: () => notifier, + }); + return { meterID, meter, updater }; + } function finishVatCreation(vatAdminNode, vatID) { const [promise, pendingRR] = producePRR(); @@ -39,14 +61,35 @@ 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) { + const m = makeMeter(vatAdminNode, remaining, threshold); + const { meterID, meter, updater } = m; + meterByID.set(meterID, harden({ meter, updater })); + meterIDByMeter.set(meter, meterID); + return meter; + }, + 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 +106,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 +133,6 @@ export function buildRootObject(vatPowers) { createVatAdminService, newVatCallback, vatTerminated, + meterCrossedThreshold, }); } diff --git a/packages/SwingSet/test/gc-dead-vat/bootstrap.js b/packages/SwingSet/test/gc-dead-vat/bootstrap.js index 70fd03dd5fb9..fc44dac059d5 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 15e0fd1d9193..6853b3e370c5 100644 --- a/packages/SwingSet/test/metering/test-dynamic-vat-metered.js +++ b/packages/SwingSet/test/metering/test-dynamic-vat-metered.js @@ -33,6 +33,55 @@ 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([10e6, 5e6]); // 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 JSON.parse(c.kpResolution(kp).body); + } + + t.deepEqual(await doMeter('getMeter'), { remaining: 10e6, threshold: 5e6 }); + await doMeter('addMeterRemaining', 8e6); + t.deepEqual(await doMeter('getMeter'), { remaining: 18e6, threshold: 5e6 }); + await doMeter('setMeterThreshold', 7e6); + t.deepEqual(await doMeter('getMeter'), { remaining: 18e6, threshold: 7e6 }); +}); + function kpidRejected(t, c, kpid, message) { t.is(c.kpStatus(kpid), 'rejected'); const resCapdata = c.kpResolution(kpid); @@ -64,8 +113,17 @@ async function overflowCrank(t, explosion) { // let the vatAdminService get wired up before we create any new vats await c.run(); + // create a meter + const cmargs = capargs([10e6, 5e6]); // 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 cvargs = capargs([dynamicVatBundle, { managerType }], []); + const cvargs = capargs( + [dynamicVatBundle, { managerType, meter: marg }], + [meterKref], + ); const kp2 = c.queueToVatRoot('bootstrap', 'createVat', cvargs); await c.run(); const res2 = c.kpResolution(kp2); @@ -134,3 +192,115 @@ 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 + const cmargs = capargs([200e3, 100e3]); // 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 { remaining } = JSON.parse(c.kpResolution(kp).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.deepEqual(remaining, 200e3); + + // messages to the metered vat should decrement the meter + await consume(true); + remaining = await getMeter(); + console.log(remaining); + t.not(remaining, 200e3); + + // 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(JSON.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, 0); // 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'); +}); + +// 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 9c5519db959f..4933854ab577 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 cdb63747e04b..c72f5203585c 100644 --- a/packages/SwingSet/test/metering/vat-load-dynamic.js +++ b/packages/SwingSet/test/metering/vat-load-dynamic.js @@ -11,6 +11,28 @@ export function buildRootObject(vatPowers) { service = await E(vats.vatAdmin).createVatAdminService(devices.vatAdmin); }, + createMeter(remaining, notifyThreshold) { + return E(service).createMeter(remaining, notifyThreshold); + }, + + 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();