Skip to content

Commit

Permalink
fix(swingset): make test less sensitive to changes in metering
Browse files Browse the repository at this point in the history
This changes test-dynamic-vat-metered.js to be more adaptive to changes in
the number of computrons consumed by the test function, by setting the
capacity and notification thresholds to be between small integral multiples
of a measured usage, instead of hard-coded values.

refs #3308
fixes #3538
  • Loading branch information
warner committed Jul 27, 2021
1 parent 459bea2 commit a0806ef
Showing 1 changed file with 109 additions and 187 deletions.
296 changes: 109 additions & 187 deletions packages/SwingSet/test/metering/test-dynamic-vat-metered.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,55 @@ function kpidRejected(t, c, kpid, message) {
t.deepEqual(body, { '@qclass': 'error', name: 'Error', message });
}

async function createMeteredVat(c, t, dynamicVatBundle, capacity, threshold) {
assert.typeof(capacity, 'bigint');
assert.typeof(threshold, 'bigint');
const cmargs = capargs([capacity, threshold]);
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: 'xs-worker', 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');
}
}

return { consume, getMeter, notifyKPID, doneKPID };
}

async function overflowCrank(t, explosion) {
const managerType = 'xs-worker';
const { kernelBundles, dynamicVatBundle, bootstrapBundle } = t.context.data;
Expand Down Expand Up @@ -192,9 +241,7 @@ test('exceed stack', t => {
return overflowCrank(t, 'stack');
});

// See the TODO comment below about `test.skip`.
test.skip('meter decrements', async t => {
const managerType = 'xs-worker';
test('meter decrements', async t => {
const { kernelBundles, dynamicVatBundle, bootstrapBundle } = t.context.data;
const config = {
bootstrap: 'bootstrap',
Expand All @@ -215,103 +262,75 @@ test.skip('meter decrements', async t => {
// 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();
// First we need to measure how much a consume() costs: create a
// large-capacity meter with a zero notifyThreshold, and run consume()
// twice. Initial experiments showed a simple 'run()' used 36918 computrons
// the first time, 36504 the subsequent times, but this is sensitive to SES
// and other libraries, so we try to be tolerant of variation over time.

const lots = 1000000n;
const t0 = await createMeteredVat(c, t, dynamicVatBundle, lots, 0n);
const remaining0 = await t0.getMeter();
t.is(remaining0, lots);
await t0.consume(true);
const remaining1 = await t0.getMeter();
const firstConsume = remaining0 - remaining1;
await t0.consume(true);
const remaining2 = await t0.getMeter();
const secondConsume = remaining1 - remaining2;
console.log(`consume usage: ${firstConsume} then ${secondConsume}`);

// now test that meters are decremented at all, notifications happen when
// they should, and the vat is terminated upon underflow

// first create a meter with capacity FIRST+1.5*SECOND
const cap = firstConsume + (3n * secondConsume) / 2n;
const thresh = secondConsume;

const t1 = await createMeteredVat(c, t, dynamicVatBundle, cap, thresh);
let remaining = await t1.getMeter();
t.is(remaining, cap);

// message one should decrement the meter, but not trigger a notification
await t1.consume(true);
remaining = await t1.getMeter();
console.log(remaining);
t.not(remaining, 200000n);
t.not(remaining, cap);
t.is(c.kpStatus(t1.notifyKPID), 'unresolved');
t.is(c.kpStatus(t1.doneKPID), 'unresolved');

// 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();
// message two should trigger notification, but not underflow
await t1.consume(true);
remaining = await t1.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(c.kpStatus(t1.notifyKPID), 'fulfilled');
const notification = c.kpResolution(t1.notifyKPID);
t.is(parse(notification.body).value, remaining);
t.is(c.kpStatus(t1.doneKPID), 'unresolved');

// 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);

// It looks like #3499 has made this flaky, reducing it to two on
// my (markm) local development env but not under CI. Locally, if
// I comment out the following three lines, it passes locally
// but not under CI. Leaving them uncommented passes on CI but
// not locally.
// TODO Marking this `test.skip` until this flakiness is cleared up.
// See https://github.com/Agoric/agoric-sdk/issues/3538
await consume(true);
remaining = await getMeter();
console.log(remaining);

console.log(`consume() about to underflow`);
await consume(false);
remaining = await getMeter();
// message three should underflow
await t1.consume(false);
remaining = await t1.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');
kpidRejected(t, c, t1.doneKPID, 'meter underflow, vat terminated');

// Now test that notification and termination can happen during the same
// crank (the very first one). Without postAbortActions, the notify would
// get unwound by the vat termination, and would never be delivered.
const cap2 = firstConsume / 2n;
const t2 = await createMeteredVat(c, t, dynamicVatBundle, cap2, 1n);

await t2.consume(false);
remaining = await t2.getMeter();
t.is(remaining, 0n); // this checks postAbortActions.deductMeter
t.is(c.kpStatus(t2.notifyKPID), 'fulfilled'); // and pAA.meterNotifications
const notify2 = c.kpResolution(t2.notifyKPID);
t.is(parse(notify2.body).value, 0n);
kpidRejected(t, c, t2.doneKPID, 'meter underflow, vat terminated');
});

test('unlimited meter', async t => {
Expand Down Expand Up @@ -388,100 +407,3 @@ test('unlimited meter', async t => {
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');
});

0 comments on commit a0806ef

Please sign in to comment.