Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

VOM virtual object and vref weak key GC #3649

Merged
merged 11 commits into from
Oct 14, 2021
209 changes: 136 additions & 73 deletions packages/SwingSet/src/kernel/liveSlots.js

Large diffs are not rendered by default.

541 changes: 412 additions & 129 deletions packages/SwingSet/src/kernel/virtualObjectManager.js

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions packages/SwingSet/test/gc/test-gc-vat.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,8 @@ async function dropPresence(t, dropExport) {
}
}

test('drop presence (export retains)', t => dropPresence(t, false));
test('drop presence (export drops)', t => dropPresence(t, true));
test.serial('drop presence (export retains)', t => dropPresence(t, false));
test.serial('drop presence (export drops)', t => dropPresence(t, true));

test('forward to fake zoe', async t => {
const config = {
Expand Down
22 changes: 20 additions & 2 deletions packages/SwingSet/test/liveslots-helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { makeLiveSlots } from '../src/kernel/liveSlots.js';

export function buildSyscall() {
const log = [];
const fakestore = new Map();

const syscall = {
send(targetSlot, method, args, resultSlot) {
Expand All @@ -31,6 +32,18 @@ export function buildSyscall() {
exit(isFailure, info) {
log.push({ type: 'exit', isFailure, info });
},
vatstoreGet(key) {
log.push({ type: 'vatstoreGet', key });
return fakestore.get(key);
},
vatstoreSet(key, value) {
log.push({ type: 'vatstoreSet', key, value });
fakestore.set(key, value);
},
vatstoreDelete(key) {
log.push({ type: 'vatstoreDelete', key });
fakestore.delete(key);
},
};

return { log, syscall };
Expand All @@ -41,6 +54,8 @@ export function makeDispatch(
build,
vatID = 'vatA',
enableDisavow = false,
cacheSize = undefined,
returnTestHooks = undefined,
) {
const gcTools = harden({
WeakRef,
Expand All @@ -49,16 +64,19 @@ export function makeDispatch(
gcAndFinalize: makeGcAndFinalize(engineGC),
meterControl: makeDummyMeterControl(),
});
const { setBuildRootObject, dispatch } = makeLiveSlots(
const { setBuildRootObject, dispatch, testHooks } = makeLiveSlots(
syscall,
vatID,
{},
{},
undefined,
cacheSize,
enableDisavow,
false,
gcTools,
);
if (returnTestHooks) {
returnTestHooks[0] = testHooks;
}
setBuildRootObject(build);
return dispatch;
}
1 change: 1 addition & 0 deletions packages/SwingSet/test/test-device-bridge.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ test('bridge device', async t => {
const hostStorage = provideHostStorage();
const config = {
bootstrap: 'bootstrap',
defaultManagerType: 'xs-worker',
vats: {
bootstrap: {
sourceSpec: new URL('device-bridge-bootstrap.js', import.meta.url)
Expand Down
2 changes: 1 addition & 1 deletion packages/SwingSet/test/test-gc-kernel.js
Original file line number Diff line number Diff line change
Expand Up @@ -1140,7 +1140,7 @@ test('terminated vat', async t => {

// device receives object from vat a, returns to vat b

test('device transfer', async t => {
test.serial('device transfer', async t => {
function vatpath(fn) {
return {
sourceSpec: new URL(`gc-device-transfer/${fn}`, import.meta.url).pathname,
Expand Down
73 changes: 40 additions & 33 deletions packages/SwingSet/test/test-liveslots.js
Original file line number Diff line number Diff line change
Expand Up @@ -730,17 +730,24 @@ test('GC syscall.dropImports', async t => {
// the presence itself should be gone
t.falsy(wr.deref());

// since nothing else is holding onto it, the vat should emit a dropImports
// first it will check that there are no VO's holding onto it
const l2 = log.shift();
t.deepEqual(l2, {
type: 'vatstoreGet',
key: 'vom.rc.o-1',
});

// since nothing else is holding onto it, the vat should emit a dropImports
const l3 = log.shift();
t.deepEqual(l3, {
type: 'dropImports',
slots: [arg],
});

// 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, {
const l4 = log.shift();
t.deepEqual(l4, {
type: 'retireImports',
slots: [arg],
});
Expand Down Expand Up @@ -941,119 +948,119 @@ test('dropImports', async t => {
false,
gcTools,
);
const { setBuildRootObject, dispatch, deadSet } = ls;
const { setBuildRootObject, dispatch, possiblyDeadSet } = ls;
setBuildRootObject(build);
const allFRs = gcTools.getAllFRs();
t.is(allFRs.length, 1);
t.is(allFRs.length, 2);
const FR = allFRs[0];

const rootA = 'o+0';

// immediate drop should push import to deadSet after finalizer runs
// immediate drop should push import to possiblyDeadSet after finalizer runs
await dispatch(makeMessage(rootA, 'ignore', capargsOneSlot('o-1')));
// the immediate gcTools.kill() means that the import should now be in the
// "COLLECTED" state
t.deepEqual(deadSet, new Set());
t.deepEqual(possiblyDeadSet, new Set());
t.is(FR.countCallbacks(), 1);
FR.runOneCallback(); // moves to FINALIZED
t.deepEqual(deadSet, new Set(['o-1']));
deadSet.delete('o-1'); // pretend liveslots did syscall.dropImport
t.deepEqual(possiblyDeadSet, new Set(['o-1']));
possiblyDeadSet.delete('o-1'); // pretend liveslots did syscall.dropImport

// separate hold and free should do the same
await dispatch(makeMessage(rootA, 'hold', capargsOneSlot('o-2')));
t.deepEqual(deadSet, new Set());
t.deepEqual(possiblyDeadSet, new Set());
t.is(FR.countCallbacks(), 0);
await dispatch(makeMessage(rootA, 'free', capargs([])));
t.deepEqual(deadSet, new Set());
t.deepEqual(possiblyDeadSet, new Set());
t.is(FR.countCallbacks(), 1);
FR.runOneCallback(); // moves to FINALIZED
t.deepEqual(deadSet, new Set(['o-2']));
deadSet.delete('o-2'); // pretend liveslots did syscall.dropImport
t.deepEqual(possiblyDeadSet, new Set(['o-2']));
possiblyDeadSet.delete('o-2'); // pretend liveslots did syscall.dropImport

// re-introduction during COLLECTED should return to REACHABLE

await dispatch(makeMessage(rootA, 'ignore', capargsOneSlot('o-3')));
// now COLLECTED
t.deepEqual(deadSet, new Set());
t.deepEqual(possiblyDeadSet, new Set());
t.is(FR.countCallbacks(), 1);

await dispatch(makeMessage(rootA, 'hold', capargsOneSlot('o-3')));
// back to REACHABLE
t.deepEqual(deadSet, new Set());
t.deepEqual(possiblyDeadSet, new Set());
t.is(FR.countCallbacks(), 1);

FR.runOneCallback(); // stays at REACHABLE
t.deepEqual(deadSet, new Set());
t.deepEqual(possiblyDeadSet, new Set());

await dispatch(makeMessage(rootA, 'free', capargs([])));
// now COLLECTED
t.deepEqual(deadSet, new Set());
t.deepEqual(possiblyDeadSet, new Set());
t.is(FR.countCallbacks(), 1);

FR.runOneCallback(); // moves to FINALIZED
t.deepEqual(deadSet, new Set(['o-3']));
deadSet.delete('o-3'); // pretend liveslots did syscall.dropImport
t.deepEqual(possiblyDeadSet, new Set(['o-3']));
possiblyDeadSet.delete('o-3'); // pretend liveslots did syscall.dropImport

// multiple queued finalizers are idempotent, remains REACHABLE

await dispatch(makeMessage(rootA, 'ignore', capargsOneSlot('o-4')));
// now COLLECTED
t.deepEqual(deadSet, new Set());
t.deepEqual(possiblyDeadSet, new Set());
t.is(FR.countCallbacks(), 1);

await dispatch(makeMessage(rootA, 'ignore', capargsOneSlot('o-4')));
// moves to REACHABLE and then back to COLLECTED
t.deepEqual(deadSet, new Set());
t.deepEqual(possiblyDeadSet, new Set());
t.is(FR.countCallbacks(), 2);

await dispatch(makeMessage(rootA, 'hold', capargsOneSlot('o-4')));
// back to REACHABLE
t.deepEqual(deadSet, new Set());
t.deepEqual(possiblyDeadSet, new Set());
t.is(FR.countCallbacks(), 2);

FR.runOneCallback(); // stays at REACHABLE
t.deepEqual(deadSet, new Set());
t.deepEqual(possiblyDeadSet, new Set());
t.is(FR.countCallbacks(), 1);

FR.runOneCallback(); // stays at REACHABLE
t.deepEqual(deadSet, new Set());
t.deepEqual(possiblyDeadSet, new Set());
t.is(FR.countCallbacks(), 0);

// multiple queued finalizers are idempotent, remains FINALIZED

await dispatch(makeMessage(rootA, 'ignore', capargsOneSlot('o-5')));
// now COLLECTED
t.deepEqual(deadSet, new Set());
t.deepEqual(possiblyDeadSet, new Set());
t.is(FR.countCallbacks(), 1);

await dispatch(makeMessage(rootA, 'ignore', capargsOneSlot('o-5')));
// moves to REACHABLE and then back to COLLECTED
t.deepEqual(deadSet, new Set());
t.deepEqual(possiblyDeadSet, new Set());
t.is(FR.countCallbacks(), 2);

FR.runOneCallback(); // moves to FINALIZED
t.deepEqual(deadSet, new Set(['o-5']));
t.deepEqual(possiblyDeadSet, new Set(['o-5']));
t.is(FR.countCallbacks(), 1);

FR.runOneCallback(); // stays at FINALIZED
t.deepEqual(deadSet, new Set(['o-5']));
t.deepEqual(possiblyDeadSet, new Set(['o-5']));
t.is(FR.countCallbacks(), 0);
deadSet.delete('o-5'); // pretend liveslots did syscall.dropImport
possiblyDeadSet.delete('o-5'); // pretend liveslots did syscall.dropImport

// re-introduction during FINALIZED moves back to REACHABLE

await dispatch(makeMessage(rootA, 'ignore', capargsOneSlot('o-6')));
// moves to REACHABLE and then back to COLLECTED
t.deepEqual(deadSet, new Set());
t.deepEqual(possiblyDeadSet, new Set());
t.is(FR.countCallbacks(), 1);

FR.runOneCallback(); // moves to FINALIZED
t.deepEqual(deadSet, new Set(['o-6']));
t.deepEqual(possiblyDeadSet, new Set(['o-6']));
t.is(FR.countCallbacks(), 0);

await dispatch(makeMessage(rootA, 'hold', capargsOneSlot('o-6')));
// back to REACHABLE, removed from deadSet
t.deepEqual(deadSet, new Set());
// back to REACHABLE, removed from possiblyDeadSet
t.deepEqual(possiblyDeadSet, new Set());
t.is(FR.countCallbacks(), 0);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ test('exit with presence', async t => {
]);
});

test('dispatches to the dead do not harm kernel', async t => {
test.serial('dispatches to the dead do not harm kernel', async t => {
const configPath = new URL('swingset-speak-to-dead.json', import.meta.url)
.pathname;
const config = await loadSwingsetConfigFile(configPath);
Expand Down Expand Up @@ -162,7 +162,7 @@ test('dispatches to the dead do not harm kernel', async t => {
}
});

test('replay does not resurrect dead vat', async t => {
test.serial('replay does not resurrect dead vat', async t => {
const configPath = new URL('swingset-no-zombies.json', import.meta.url)
.pathname;
const config = await loadSwingsetConfigFile(configPath);
Expand Down
4 changes: 2 additions & 2 deletions packages/SwingSet/test/vat-admin/test-replay.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ test.before(async t => {
t.context.data = { kernelBundles, dynamicBundle };
});

test('replay bundleSource-based dynamic vat', async t => {
test.serial('replay bundleSource-based dynamic vat', async t => {
const config = {
vats: {
bootstrap: {
Expand Down Expand Up @@ -72,7 +72,7 @@ test('replay bundleSource-based dynamic vat', async t => {
}
});

test('replay bundleName-based dynamic vat', async t => {
test.serial('replay bundleName-based dynamic vat', async t => {
const config = {
vats: {
bootstrap: {
Expand Down
18 changes: 9 additions & 9 deletions packages/SwingSet/test/virtualObjects/test-reachable-vrefs.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,16 +52,16 @@ test('VOM tracks reachable vrefs', async t => {

const [vref1, obj1] = makePresence();
const key1 = keyMaker();
t.falsy(vom.isVrefReachable(vref1));
t.falsy(vom.isPresenceReachable(vref1));
weakStore.init(key1, obj1);
t.truthy(vom.isVrefReachable(vref1));
t.truthy(vom.isPresenceReachable(vref1));

const [vref2, obj2] = makePresence();
const key2 = keyMaker();
weakStore.init(key2, 'not yet');
t.falsy(vom.isVrefReachable(vref2));
t.falsy(vom.isPresenceReachable(vref2));
weakStore.set(key2, obj2);
t.truthy(vom.isVrefReachable(vref2));
t.truthy(vom.isPresenceReachable(vref2));

// storing Presences as the value for a non-virtual key just holds on to
// the Presence directly, and does not track the vref
Expand All @@ -70,19 +70,19 @@ test('VOM tracks reachable vrefs', async t => {
const key3 = {};
weakStore.init(key3, obj3);
weakStore.set(key3, obj3);
t.falsy(vom.isVrefReachable(vref3));
t.falsy(vom.isPresenceReachable(vref3));

// now check that Presences are tracked when in the state of a virtual
// object
const [vref4, obj4] = makePresence();
t.falsy(vom.isVrefReachable(vref4));
t.falsy(vom.isPresenceReachable(vref4));
// eslint-disable-next-line no-unused-vars
const holder4 = holderMaker(obj4);
t.truthy(vom.isVrefReachable(vref4));
t.truthy(vom.isPresenceReachable(vref4));

const [vref5, obj5] = makePresence();
const holder5 = holderMaker('not yet');
t.falsy(vom.isVrefReachable(vref5));
t.falsy(vom.isPresenceReachable(vref5));
holder5.setHeld(obj5);
t.truthy(vom.isVrefReachable(vref5));
t.truthy(vom.isPresenceReachable(vref5));
});
Loading