From 7550c6fe97df12700a12636d787e9c3d9793d44b Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Fri, 18 Aug 2023 16:05:06 -0700 Subject: [PATCH] feat(swingset): add controller.resetAllMeters() Add a feature which allows the host application to reset all non-unlimited Meters to a new absolute value. refs #7938 --- packages/SwingSet/docs/metering.md | 21 +++++++++++++++++-- .../SwingSet/src/controller/controller.js | 4 ++++ packages/SwingSet/src/kernel/kernel.js | 5 +++++ .../SwingSet/src/kernel/state/kernelKeeper.js | 16 ++++++++++++++ .../test/metering/test-dynamic-vat-metered.js | 18 ++++++++++++---- 5 files changed, 58 insertions(+), 6 deletions(-) diff --git a/packages/SwingSet/docs/metering.md b/packages/SwingSet/docs/metering.md index 63107e377e7..46606f9276d 100644 --- a/packages/SwingSet/docs/metering.md +++ b/packages/SwingSet/docs/metering.md @@ -109,6 +109,23 @@ 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). +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 `runPolicy` object can tell the kernel to exit the `run()` loop before the run-queue is drained, leaving some work for the future. -This is a work in process, please follow issue #3460 for progress. +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). + +## External Refill + +In addition to refilling a `Meter` from *within* a Vat (using `E(meter).addRemaining(99n)`), the kernel offers an *external* API to refill all existing Meter objects. This may be useful for runtime-limitation approaches where the host application is willing to reset the meters on a periodic basis, but the vats holding the meters are not prepared to assist. + +Calling `controller.resetAllMeters(remaining)` will immediately change all existing Meters to the provided `remaining` value. It takes an absolute number of computrons, as a BigInt (unlike `E(meter).addRemaining()`, which takes a positive delta). This change in value will not trigger the `setThreshold` notification, even if `remaining` is actually lower than the old value. + +Only regular `Meter` objects, existing at the time of the call, will have their value changed. Meters created after the call will get whatever `remaining` value their `E(vatAdmin).createMeter()` call provides. `UnlimitedMeter` objects will remain unlimited. + +`resetAllMeters` should only be called while the kernel is idle: not during a `controller.step()` or `controller.run()`. Otherwise, the exact timing of when vats experience the meter's new value will be hard to predict. + +`resetAllMeters` causes the kernel to write changes to the swing-store DB, which should eventually be committed. Host applications should generally use a sequence like: + +* `controller.resetAllMeters()` +* call device inputs +* `await controller.run(runPolicy)` +* `await swingstore.hostStorage.commit()` diff --git a/packages/SwingSet/src/controller/controller.js b/packages/SwingSet/src/controller/controller.js index b9331909b7e..dff3bb4748e 100644 --- a/packages/SwingSet/src/controller/controller.js +++ b/packages/SwingSet/src/controller/controller.js @@ -323,6 +323,10 @@ export async function makeSwingsetController( kernel.changeKernelOptions(options); }, + resetAllMeters(remaining) { + kernel.resetAllMeters(remaining); + }, + getStats() { return defensiveCopy(kernel.getStats()); }, diff --git a/packages/SwingSet/src/kernel/kernel.js b/packages/SwingSet/src/kernel/kernel.js index 99a0b22533c..138eeb8c763 100644 --- a/packages/SwingSet/src/kernel/kernel.js +++ b/packages/SwingSet/src/kernel/kernel.js @@ -1871,6 +1871,10 @@ export default function buildKernel( } } + function resetAllMeters(remaining) { + kernelKeeper.resetAllMeters(remaining); + } + function kpRegisterInterest(kpid) { kernelKeeper.incrementRefCount(kpid, 'external'); } @@ -1934,6 +1938,7 @@ export default function buildKernel( run, shutdown, changeKernelOptions, + resetAllMeters, // the rest are for testing and debugging diff --git a/packages/SwingSet/src/kernel/state/kernelKeeper.js b/packages/SwingSet/src/kernel/state/kernelKeeper.js index b362774536d..0e0d50937fc 100644 --- a/packages/SwingSet/src/kernel/state/kernelKeeper.js +++ b/packages/SwingSet/src/kernel/state/kernelKeeper.js @@ -1063,6 +1063,21 @@ export default function makeKernelKeeper(kernelStorage, kernelSlog) { kvStore.delete(`${meterID}.threshold`); } + function resetAllMeters(remaining) { + remaining = Nat(BigInt(remaining)); + const keyRE = /^m\d+\.remaining$/; + for (const key of enumeratePrefixedKeys(kvStore, 'm')) { + // meter data is stored in m$NN keys, however there are other keys + // that start with 'm' too + if (keyRE.test(key)) { + // UnlimitedMeters remain unchanged + if (getRequired(key) !== 'unlimited') { + kvStore.set(key, `${remaining}`); + } + } + } + } + function hasVatWithName(name) { return kvStore.has(`vat.name.${name}`); } @@ -1604,6 +1619,7 @@ export default function makeKernelKeeper(kernelStorage, kernelSlog) { checkMeter, deductMeter, deleteMeter, + resetAllMeters, hasVatWithName, getVatIDForName, diff --git a/packages/SwingSet/test/metering/test-dynamic-vat-metered.js b/packages/SwingSet/test/metering/test-dynamic-vat-metered.js index 3839c5f94c1..907cc35bcef 100644 --- a/packages/SwingSet/test/metering/test-dynamic-vat-metered.js +++ b/packages/SwingSet/test/metering/test-dynamic-vat-metered.js @@ -10,6 +10,8 @@ import { restartVatAdminVat } from '../util.js'; import { kunser, krefOf } from '../../src/lib/kmarshal.js'; import { enumeratePrefixedKeys } from '../../src/kernel/state/storageHelper.js'; +const LOTS = 100_000_000n; // 100Mc + async function prepare() { const kernelBundles = await buildKernelBundles(); // we'll give this bundle to the loader vat, which will use it to create a @@ -80,6 +82,10 @@ async function meterObjectsTest(t, doVatAdminRestarts) { await doMeter('setMeterThreshold', 7n); await vaRestart(); t.deepEqual(await getMeter(), { remaining: 18n, threshold: 7n }); + + // resetAllMeters() will update it + c.resetAllMeters(99n); + t.deepEqual(await getMeter(), { remaining: 99n, threshold: 7n }); } test('meter objects', async t => { @@ -293,12 +299,11 @@ test('meter decrements', async t => { // times, but this is sensitive to SES and other libraries, so we // try to be tolerant of variation over time. - const lots = 100_000_000n; // TODO 100m - const t0 = await createMeteredVat(c, t, dynamicVatBundle, lots, 0n); + const t0 = await createMeteredVat(c, t, dynamicVatBundle, LOTS, 0n); let remaining0 = await t0.getMeter(); - const consumedByStartVat = lots - remaining0; + const consumedByStartVat = LOTS - remaining0; console.log(`-- consumedByStartVat`, consumedByStartVat); - t.not(remaining0, lots); + t.not(remaining0, LOTS); await t0.consume(true); const remaining1 = await t0.getMeter(); const firstConsume = remaining0 - remaining1; @@ -440,6 +445,11 @@ async function unlimitedMeterTest(t, doVatAdminRestarts) { t.is(remaining, 'unlimited'); await vaRestart(); + // resetAllMeters() will not change an UnlimitedMeter + c.resetAllMeters(LOTS); + 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', ['compute']); await c.run();