diff --git a/packages/SwingSet/docs/metering.md b/packages/SwingSet/docs/metering.md new file mode 100644 index 00000000000..63107e377e7 --- /dev/null +++ b/packages/SwingSet/docs/metering.md @@ -0,0 +1,114 @@ +# 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 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. + +## 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. + +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. + +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..1d9169258d8 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,44 @@ export default function buildKernel( } let terminationTrigger; + let postAbortActions; function resetDeliveryTriggers() { terminationTrigger = undefined; + postAbortActions = { + meterDeductions: [], // list of { meterID, compute } + }; } 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 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); + } + 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 +424,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 +446,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 +482,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 +595,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 +619,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 +731,15 @@ export default function buildKernel( // errors unwind any changes the vat made abortCrank(); didAbort = true; + // but metering deductions and underflow notifications must survive + const { meterDeductions } = postAbortActions; + for (const { meterID, compute } of meterDeductions) { + deductMeter(meterID, compute, false); + // that will re-push any notifications + } } - // 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 +963,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 2c6057e5189..bef40c3be70 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) @@ -53,6 +54,10 @@ 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 // d$NN.o.nextID = $NN // d$NN.c.$kernelSlot = $deviceSlot = o-$NN/d+$NN/d-$NN @@ -90,6 +95,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 +117,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 +240,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 +719,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 +1294,13 @@ export default function makeKernelKeeper( getRunQueueLength, getNextMsg, + allocateMeter, + addMeterRemaining, + setMeterThreshold, + getMeter, + deductMeter, + deleteMeter, + hasVatWithName, getVatIDForName, allocateVatIDForNameIfNeeded, 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..2de06233071 100644 --- a/packages/SwingSet/test/metering/test-dynamic-vat-metered.js +++ b/packages/SwingSet/test/metering/test-dynamic-vat-metered.js @@ -1,20 +1,15 @@ /* global __dirname */ +/* eslint-disable no-await-in-loop */ // eslint-disable-next-line import/order 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 +28,60 @@ 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, + }); + t.teardown(c.shutdown); + 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); @@ -59,14 +108,21 @@ 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 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 +191,287 @@ 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, + }); + t.teardown(c.shutdown); + 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, + }); + t.teardown(c.shutdown); + 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'); +}); + +// 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, + }); + t.teardown(c.shutdown); + 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'); +}); 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/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 }); +}); 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) { 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/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 { 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; } 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(