Skip to content

Commit

Permalink
feat(swingset): drop Presences, activate syscall.dropImports
Browse files Browse the repository at this point in the history
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
  • Loading branch information
warner committed May 24, 2021
1 parent d2e7753 commit ea306fe
Show file tree
Hide file tree
Showing 2 changed files with 176 additions and 21 deletions.
75 changes: 72 additions & 3 deletions packages/SwingSet/src/kernel/liveSlots.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -366,6 +418,7 @@ function build(
makeKind,
VirtualObjectAwareWeakMap,
VirtualObjectAwareWeakSet,
isVrefReachable,
} = makeVirtualObjectManager(
syscall,
allocateExportID,
Expand Down Expand Up @@ -427,7 +480,7 @@ function build(
}

function registerValue(slot, val) {
const { type, virtual } = parseVatSlot(slot);
const { type } = parseVatSlot(slot);
slotToVal.set(slot, new WeakRef(val));
valToSlot.set(val, slot);
// we don't dropImports on promises, to avoid interaction with retire
Expand Down Expand Up @@ -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
Expand All @@ -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);

Expand Down
122 changes: 104 additions & 18 deletions packages/SwingSet/test/test-liveslots.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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;
Expand All @@ -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, {
Expand All @@ -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
Expand Down

0 comments on commit ea306fe

Please sign in to comment.