From 84e383a409846f2b6d38c2d443fd390d65da5d30 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Mon, 24 May 2021 08:16:02 -0700 Subject: [PATCH] feat(swingset): drop Presences, activate `syscall.dropImports` Change liveslots to provoke GC at mid-crank, then process the "dead set" and emit `syscall.dropImports` for everything we can. For now, we conservatively assume that everything remains recognizable, so we do *not* emit `syscall.retireImport` for anything. The vref might be used as a WeakMap/WeakSet key, and the VOM isn't yet tracking those. When #3161 is done, we'll change liveslots to ask the VOM about each vref, and retire the ones it does not know how to recognize (which will hopefully be the majority of them). closes #3147 refs #2615 refs #2660 --- packages/SwingSet/src/kernel/liveSlots.js | 73 ++++++++++++- packages/SwingSet/test/test-liveslots.js | 122 ++++++++++++++++++---- 2 files changed, 175 insertions(+), 20 deletions(-) diff --git a/packages/SwingSet/src/kernel/liveSlots.js b/packages/SwingSet/src/kernel/liveSlots.js index d0fd49a8f55..54e00cbd354 100644 --- a/packages/SwingSet/src/kernel/liveSlots.js +++ b/packages/SwingSet/src/kernel/liveSlots.js @@ -45,7 +45,7 @@ function build( gcTools, console, ) { - const { WeakRef, FinalizationRegistry, waitUntilQuiescent } = gcTools; + const { WeakRef, FinalizationRegistry } = gcTools; const enableLSDebug = false; function lsdebug(...args) { if (enableLSDebug) { @@ -177,6 +177,58 @@ function build( } const droppedRegistry = new FinalizationRegistry(finalizeDroppedImport); + function processDroppedRepresentative(_vref) { + // no-op, to be implemented by virtual object manager + return false; + } + + function processDeadSet() { + let doMore = false; + const [importsToDrop, importsToRetire, exportsToRetire] = [[], [], []]; + + for (const vref of deadSet) { + const { virtual, allocatedByVat, type } = parseVatSlot(vref); + assert(type === 'object', `unprepared to track ${type}`); + if (virtual) { + // Representative: send nothing, but perform refcount checking + doMore = doMore || processDroppedRepresentative(vref); + } else if (allocatedByVat) { + // Remotable: send retireExport + exportsToRetire.push(vref); + } else { + // Presence: send dropImport unless reachable by VOM + // eslint-disable-next-line no-lonely-if, no-use-before-define + if (!isVrefReachable(vref)) { + importsToDrop.push(vref); + // and retireExport if unrecognizable (TODO: needs + // VOM.vrefIsRecognizable) + // if (!vrefIsRecognizable(vref)) { + // importsToRetire.push(vref); + // } + } + } + } + deadSet.clear(); + + if (importsToDrop.length) { + importsToDrop.sort(); + syscall.dropImports(importsToDrop); + } + if (importsToRetire.length) { + importsToRetire.sort(); + syscall.retireImports(importsToRetire); + } + if (exportsToRetire.length) { + exportsToRetire.sort(); + syscall.retireExports(exportsToRetire); + } + + // TODO: doMore=true when we've done something that might free more local + // objects, which probably won't happen until we sense entire WeakMaps + // going away or something involving virtual collections + return doMore; + } + /** Remember disavowed Presences which will kill the vat if you try to talk * to them */ const disavowedPresences = new WeakSet(); @@ -366,6 +418,7 @@ function build( makeKind, VirtualObjectAwareWeakMap, VirtualObjectAwareWeakSet, + isVrefReachable, } = makeVirtualObjectManager( syscall, allocateExportID, @@ -906,6 +959,8 @@ function build( } } + const { waitUntilQuiescent, gcAndFinalize } = gcTools; + /** * This low-level liveslots code is responsible for deciding when userspace * is done with a crank. Userspace code can use Promises, so it can add as @@ -926,9 +981,23 @@ function build( .catch(err => console.log(`liveslots error ${err} during delivery ${delivery}`), ); + // Instead, we wait for userspace to become idle by draining the promise // queue. - return waitUntilQuiescent(); + await waitUntilQuiescent(); + // Userspace will not get control again within this crank. + + // Now that userspace is idle, we can drive GC until we think we've + // stopped. + async function finish() { + await gcAndFinalize(); + const doMore = processDeadSet(); + if (doMore) { + return finish(); + } + return undefined; + } + return finish(); } harden(dispatch); diff --git a/packages/SwingSet/test/test-liveslots.js b/packages/SwingSet/test/test-liveslots.js index 66bc53b1f5b..930bc4ad82b 100644 --- a/packages/SwingSet/test/test-liveslots.js +++ b/packages/SwingSet/test/test-liveslots.js @@ -370,8 +370,12 @@ async function doResultPromise(t, mode) { const { log, syscall } = buildSyscall(); function build(_vatPowers) { + let pin; return Far('root', { async run(target1) { + // inhibit GC of the Presence, so the tests see stable syscalls + // eslint-disable-next-line no-unused-vars + pin = target1; const p1 = E(target1).getTarget2(); hush(p1); const p2 = E(p1).one(); @@ -684,14 +688,73 @@ test('liveslots retains device nodes', async t => { t.deepEqual(success, [true]); }); -test('GC operations', async t => { +test('GC syscall.dropImports', async t => { const { log, syscall } = buildSyscall(); + let wr; + function build(_vatPowers) { + let presence1; + const root = Far('root', { + one(arg) { + presence1 = arg; + wr = new WeakRef(arg); + }, + two() {}, + three() { + // eslint-disable-next-line no-unused-vars + presence1 = undefined; // drops the import + }, + }); + return root; + } + const dispatch = makeDispatch(syscall, build, 'vatA', true); + t.deepEqual(log, []); + const rootA = 'o+0'; + const arg = 'o-1'; + + // tell the vat make a Presence and hold it for a moment + // rp1 = root~.one(arg) + await dispatch(makeMessage(rootA, 'one', capargsOneSlot(arg))); + t.truthy(wr.deref()); + // an intermediate message will trigger GC, but the presence is still held + await dispatch(makeMessage(rootA, 'two', capargs([]))); + t.truthy(wr.deref()); + + // now tell the vat to drop the 'arg' presence we gave them earlier + await dispatch(makeMessage(rootA, 'three', capargs([]), null)); + + // the presence itself should be gone + t.falsy(wr.deref()); + + // since nothing else is holding onto it, the vat should emit a dropImports + const l2 = log.shift(); + t.deepEqual(l2, { + type: 'dropImports', + slots: [arg], + }); + + const todo = false; // enable this once we have VOM.vrefIsRecognizable + if (todo) { + // and since the vat never used the Presence in a WeakMap/WeakSet, they + // cannot recognize it either, and will emit retireImports + const l3 = log.shift(); + t.deepEqual(l3, { + type: 'retireImports', + slots: [arg], + }); + } + + t.deepEqual(log, []); +}); + +test('GC dispatch.retireImports', async t => { + const { log, syscall } = buildSyscall(); function build(_vatPowers) { - const ex1 = Far('export', {}); + let presence1; const root = Far('root', { - one(_arg) { - return ex1; + one(arg) { + // eslint-disable-next-line no-unused-vars + presence1 = arg; }, }); return root; @@ -701,10 +764,38 @@ test('GC operations', async t => { const rootA = 'o+0'; const arg = 'o-1'; + // tell the vat make a Presence and hold it // rp1 = root~.one(arg) + await dispatch(makeMessage(rootA, 'one', capargsOneSlot(arg))); + + // when the upstream export goes away, the kernel will send a + // dispatch.retireImport into the vat + await dispatch(makeRetireImports(arg)); + // for now, we only care that it doesn't crash + t.deepEqual(log, []); + + // when we implement VOM.vrefIsRecognizable, this test might do more +}); + +test('GC dispatch.retireExports', async t => { + const { log, syscall } = buildSyscall(); + function build(_vatPowers) { + const ex1 = Far('export', {}); + const root = Far('root', { + one() { + return ex1; + }, + }); + return root; + } + const dispatch = makeDispatch(syscall, build, 'vatA', true); + t.deepEqual(log, []); + const rootA = 'o+0'; + + // rp1 = root~.one() // ex1 = await rp1 const rp1 = 'p-1'; - await dispatch(makeMessage(rootA, 'one', capargsOneSlot(arg), rp1)); + await dispatch(makeMessage(rootA, 'one', capargs([]), rp1)); const l1 = log.shift(); const ex1 = l1.resolutions[0][2].slots[0]; t.deepEqual(l1, { @@ -713,22 +804,17 @@ test('GC operations', async t => { }); t.deepEqual(log, []); - // now tell the vat we don't need a strong reference to that export - // for now, all that we care about is that liveslots doesn't crash + // All other vats drop the export, but since the vat holds it strongly, the + // vat says nothing await dispatch(makeDropExports(ex1)); + t.deepEqual(log, []); - // and release its identity too + // Also, all other vats cease to be able to recognize it, which will delete + // the clist entry and allows the vat to delete some slotToVal tables. The + // vat does not need to react, but we want to make sure the dispatch + // doesn't crash anything. await dispatch(makeRetireExports(ex1)); - - // Sending retireImport into a vat that hasn't yet emitted dropImport is - // rude, and would only happen if the exporter unilaterally revoked the - // object's identity. Normally the kernel would only send retireImport - // after receiving dropImport (and sending a dropExport into the exporter, - // and getting a retireExport from the exporter, gracefully terminating the - // object's identity). We do it the rude way because it's good enough to - // test that liveslots can tolerate it, but we may have to update this when - // we implement retireImport for real. - await dispatch(makeRetireImports(arg)); + t.deepEqual(log, []); }); // Create a WeakRef/FinalizationRegistry pair that can be manipulated for