Skip to content

Commit

Permalink
test(swingset): test liveslots GC with mocks
Browse files Browse the repository at this point in the history
Liveslots is not yet calling syscall.dropImports, but by mocking WeakRef and
FinalizationRegistry, we can test to make sure it updates the deadSet
correctly.
  • Loading branch information
warner committed Mar 24, 2021
1 parent b67e8ca commit 8fe8077
Show file tree
Hide file tree
Showing 2 changed files with 254 additions and 4 deletions.
15 changes: 12 additions & 3 deletions packages/SwingSet/src/kernel/liveSlots.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,14 @@ function build(
* deadSet holds the vref only in FINALIZED
* re-introduction must ensure the vref is not in the deadSet
Each state thus has a set of perhaps-measurable properties:
* UNKNOWN: slotToVal[vref] is missing, vref not in deadSet
* REACHABLE: slotToVal has live weakref, userspace can reach
* UNREACHABLE: slotToVal has live weakref, userspace cannot reach
* COLLECTED: slotToVal[vref] has dead weakref
* FINALIZED: slotToVal[vref] is missing, vref is in deadSet
Our finalizer callback is queued by the engine's transition from
UNREACHABLE to COLLECTED, but the vref might be re-introduced before the
callback has a chance to run. There might even be multiple copies of the
Expand Down Expand Up @@ -792,7 +800,8 @@ function build(
}

const dispatch = harden({ deliver, notify, dropExports });
return harden({ vatGlobals, setBuildRootObject, dispatch, m });
// we return 'deadSet' for unit tests
return harden({ vatGlobals, setBuildRootObject, dispatch, m, deadSet });
}

/**
Expand Down Expand Up @@ -855,8 +864,8 @@ export function makeLiveSlots(
gcTools,
liveSlotsConsole,
);
const { vatGlobals, dispatch, setBuildRootObject } = r; // omit 'm'
return harden({ vatGlobals, dispatch, setBuildRootObject });
const { vatGlobals, dispatch, setBuildRootObject, deadSet } = r; // omit 'm'
return harden({ vatGlobals, dispatch, setBuildRootObject, deadSet });
}

// for tests
Expand Down
243 changes: 242 additions & 1 deletion packages/SwingSet/test/test-liveslots.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { wrapTest } from '@agoric/ses-ava';
import '@agoric/install-ses';
import test from 'ava';
import rawTest from 'ava';
import { E } from '@agoric/eventual-send';
import { Far } from '@agoric/marshal';
import { assert, details as X } from '@agoric/assert';
import { WeakRef, FinalizationRegistry } from '../src/weakref';
import { waitUntilQuiescent } from '../src/waitUntilQuiescent';
import { makeLiveSlots } from '../src/kernel/liveSlots';

const test = wrapTest(rawTest);

function capdata(body, slots = []) {
return harden({ body, slots });
}
Expand Down Expand Up @@ -715,3 +718,241 @@ test('dropExports', async t => {
dispatch.dropExports([ex1]);
// for now, all that we care about is that liveslots doesn't crash
});

// Create a WeakRef/FinalizationRegistry pair that can be manipulated for
// tests. Limitations:
// * only one WeakRef per object
// * no deregister
// * extra debugging properties like FR.countCallbacks and FR.runOneCallback
// * nothing is hardened

function makeMockGC() {
const weakRefToVal = new Map();
const valToWeakRef = new Map();
const allFRs = [];
// eslint-disable-next-line no-unused-vars
function log(...args) {
// console.log(...args);
}

const mockWeakRefProto = {
deref() {
return weakRefToVal.get(this);
},
};
function mockWeakRef(val) {
assert(!valToWeakRef.has(val));
weakRefToVal.set(this, val);
valToWeakRef.set(val, this);
}
mockWeakRef.prototype = mockWeakRefProto;

function kill(val) {
log(`kill`, val);
if (valToWeakRef.has(val)) {
log(` killing weakref`);
const wr = valToWeakRef.get(val);
valToWeakRef.delete(val);
weakRefToVal.delete(wr);
}
for (const fr of allFRs) {
if (fr.registry.has(val)) {
log(` pushed on FR queue, context=`, fr.registry.get(val));
fr.ready.push(val);
}
}
log(` kill done`);
}

const mockFinalizationRegistryProto = {
register(val, context) {
log(`FR.register(context=${context})`);
this.registry.set(val, context);
},
countCallbacks() {
log(`countCallbacks:`);
log(` ready:`, this.ready);
log(` registry:`, this.registry);
return this.ready.length;
},
runOneCallback() {
log(`runOneCallback`);
const val = this.ready.shift();
log(` val:`, val);
assert(this.registry.has(val));
const context = this.registry.get(val);
log(` context:`, context);
this.registry.delete(val);
this.callback(context);
},
};

function mockFinalizationRegistry(callback) {
this.registry = new Map();
this.callback = callback;
this.ready = [];
allFRs.push(this);
}
mockFinalizationRegistry.prototype = mockFinalizationRegistryProto;

function getAllFRs() {
return allFRs;
}

return harden({
WeakRef: mockWeakRef,
FinalizationRegistry: mockFinalizationRegistry,
kill,
getAllFRs,
});
}

test('dropImports', async t => {
const { syscall } = buildSyscall();
const imports = [];
const gcTools = makeMockGC();

function build(_vatPowers) {
const root = Far('root', {
hold(imp) {
imports.push(imp);
},
free() {
gcTools.kill(imports.pop());
},
ignore(imp) {
gcTools.kill(imp);
},
});
return root;
}

const ls = makeLiveSlots(syscall, 'vatA', {}, {}, undefined, false, gcTools);
const { setBuildRootObject, dispatch, deadSet } = ls;
setBuildRootObject(build);
const allFRs = gcTools.getAllFRs();
t.is(allFRs.length, 1);
const FR = allFRs[0];

const rootA = 'o+0';

// immediate drop should push import to deadSet after finalizer runs
dispatch.deliver(rootA, 'ignore', capargsOneSlot('o-1'), null);
await waitUntilQuiescent();
// the immediate gcTools.kill() means that the import should now be in the
// "COLLECTED" state
t.deepEqual(deadSet, 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

// separate hold and free should do the same
dispatch.deliver(rootA, 'hold', capargsOneSlot('o-2'), null);
await waitUntilQuiescent();
t.deepEqual(deadSet, new Set());
t.is(FR.countCallbacks(), 0);
dispatch.deliver(rootA, 'free', capargs([]), null);
await waitUntilQuiescent();
t.deepEqual(deadSet, 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

// re-introduction during COLLECTED should return to REACHABLE

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

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

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

dispatch.deliver(rootA, 'free', capargs([]), null);
await waitUntilQuiescent();
// now COLLECTED
t.deepEqual(deadSet, 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

// multiple queued finalizers are idempotent, remains REACHABLE

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

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

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

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

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

// multiple queued finalizers are idempotent, remains FINALIZED

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

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

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

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

// re-introduction during FINALIZED moves back to REACHABLE

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

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

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

0 comments on commit 8fe8077

Please sign in to comment.