Skip to content

Commit

Permalink
Merge pull request #7158 from Agoric/7142-gc-sensitivity
Browse files Browse the repository at this point in the history
add (failing) tests of liveslots GC sensitivity
  • Loading branch information
mergify[bot] authored Mar 17, 2023
2 parents a2fa2fb + d3f63be commit d5d2624
Show file tree
Hide file tree
Showing 3 changed files with 312 additions and 96 deletions.
104 changes: 104 additions & 0 deletions packages/swingset-liveslots/test/mock-gc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { waitUntilQuiescent } from './waitUntilQuiescent.js';
import { makeDummyMeterControl } from './dummyMeterControl.js';

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

export 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);
},
flush() {
while (this.ready.length) {
this.runOneCallback();
}
},
};

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

function getAllFRs() {
return allFRs;
}
function flushAllFRs() {
allFRs.map(fr => fr.flush());
}

function mockGCAndFinalize() {}

return harden({
WeakRef: mockWeakRef,
FinalizationRegistry: mockFinalizationRegistry,
kill,
getAllFRs,
flushAllFRs,
waitUntilQuiescent,
gcAndFinalize: mockGCAndFinalize,
meterControl: makeDummyMeterControl(),
});
}
206 changes: 206 additions & 0 deletions packages/swingset-liveslots/test/test-gc-sensitivity.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
import test from 'ava';
import '@endo/init/debug.js';
import { Far } from '@endo/marshal';
import { buildSyscall } from './liveslots-helpers.js';
import { makeLiveSlots } from '../src/liveslots.js';
import { kser } from './kmarshal.js';
import { makeMockGC } from './mock-gc.js';
import { makeMessage, makeStartVat } from './util.js';

test.failing('kind handle reanimation', async t => {
const { syscall, log } = buildSyscall();
const gcTools = makeMockGC();

function buildRootObject(vatPowers, vatParameters, baggage) {
const { VatData } = vatPowers;
const kh0 = VatData.makeKindHandle('kh');
VatData.defineDurableKind(kh0, () => ({}), {});
baggage.init('kh', kh0);

const root = Far('root', {
fetch1() {
// console.log(`--fetch1`);
baggage.get('kh');
},
gc() {
// console.log(`--gc`);
gcTools.kill(kh0);
gcTools.flushAllFRs();
},
fetch2() {
// console.log(`--fetch2`);
baggage.get('kh');
},
});
return root;
}

const rootA = 'o+0';
const ls = makeLiveSlots(syscall, 'vatA', {}, {}, gcTools, undefined, () => ({
buildRootObject,
}));
const { dispatch } = ls;
await dispatch(makeStartVat(kser()));

// Imagine a vat which allocates a KindID for 'kh', and stores it in
// baggage, and then drops the Representative (the Handle). Then
// time passes, during which GC may or may not happen, and then the
// vat pulls 'kh' out of baggage.

log.length = 0;
// this simulates the GC-did-not-happen case: the kh0 Representative
// is still around from buildRootObject (liveslots has not seen the
// FinalizationRegistry fire, and the WeakRef is still populated)
await dispatch(makeMessage(rootA, 'fetch1', []));
const noGCLog = [...log];
log.length = 0;

// this simulates the GC-did-happen case: liveslots has seen kh0
// die, so it must reanimate a new one
await dispatch(makeMessage(rootA, 'gc', []));
log.length = 0;
await dispatch(makeMessage(rootA, 'fetch2', []));
const yesGCLog = [...log];
log.length = 0;

// we need the syscall behavior of both cases to be the same
t.deepEqual(noGCLog, yesGCLog);
});

test.failing('representative reanimation', async t => {
const { syscall, log } = buildSyscall();
const gcTools = makeMockGC();

function buildRootObject(vatPowers, vatParameters, baggage) {
const { VatData } = vatPowers;
const kh0 = VatData.makeKindHandle('kh');
const behavior = { get: ({ state }) => state.data };
const initState = { data: 0 };
const make = VatData.defineDurableKind(kh0, () => initState, behavior);
const r0 = make();
baggage.init('k', r0);
const r1 = make();
// knock r0.innerSelf out of the cache, leave only r1
make();
r1.get();

const root = Far('root', {
fetch1() {
// console.log(`--fetch1`);
baggage.get('k');
},
gc() {
// console.log(`--gc`);
gcTools.kill(r0);
gcTools.flushAllFRs();
// knock r0.innerSelf out of the cache, leave only r1
make();
r1.get();
},
fetch2() {
// console.log(`--fetch2`);
baggage.get('k');
},
});
return root;
}

const rootA = 'o+0';
const opts = { virtualObjectCacheSize: 0 };
const ls = makeLiveSlots(
syscall,
'vatA',
{},
opts,
gcTools,
undefined,
() => ({
buildRootObject,
}),
);
const { dispatch } = ls;
await dispatch(makeStartVat(kser()));

// Imagine a vat which creates an initial Representative of some
// Kind and stores it in baggage, then drops the Representative (the
// Handle). Then time passes, during which GC may or may not happen,
// and then the vat pulls it back out of baggage.

log.length = 0;
// this simulates the GC-did-not-happen case: the r0 Representative
// is still around from buildRootObject (liveslots has not seen the
// FinalizationRegistry fire, and the WeakRef is still populated)
await dispatch(makeMessage(rootA, 'fetch1', []));
const noGCLog = [...log];
log.length = 0;

// this simulates the GC-did-happen case: liveslots has seen r0 die,
// so it must reanimate a new one
await dispatch(makeMessage(rootA, 'gc', []));
log.length = 0;
await dispatch(makeMessage(rootA, 'fetch2', []));
const yesGCLog = [...log];
log.length = 0;

// we need the syscall behavior of both cases to be the same
t.deepEqual(noGCLog, yesGCLog);
});

test.failing('collection reanimation', async t => {
const { syscall, log } = buildSyscall();
const gcTools = makeMockGC();

function buildRootObject(vatPowers, vatParameters, baggage) {
const { VatData } = vatPowers;
const c0 = VatData.makeScalarBigMapStore('c', { durable: true });
baggage.init('c', c0);

const root = Far('root', {
fetch1() {
// console.log(`--fetch1`);
baggage.get('c');
},
gc() {
// console.log(`--gc`);
gcTools.kill(c0);
gcTools.flushAllFRs();
},
fetch2() {
// console.log(`--fetch2`);
baggage.get('c');
},
});
return root;
}

const rootA = 'o+0';
const ls = makeLiveSlots(syscall, 'vatA', {}, {}, gcTools, undefined, () => ({
buildRootObject,
}));
const { dispatch } = ls;
await dispatch(makeStartVat(kser()));

// Imagine a vat which creates a durable collection 'c0' and stores
// it in baggage, and then drops the Representative. Then time
// passes, during which GC may or may not happen, and then the vat
// pulls 'c' out of baggage.

log.length = 0;
// this simulates the GC-did-not-happen case: the c0 Representative
// is still around from buildRootObject (liveslots has not seen the
// FinalizationRegistry fire, and the WeakRef is still populated)
await dispatch(makeMessage(rootA, 'fetch1', []));
const noGCLog = [...log];
log.length = 0;

// this simulates the GC-did-happen case: liveslots has seen c0
// die, so it must reanimate a new one
await dispatch(makeMessage(rootA, 'gc', []));
log.length = 0;
await dispatch(makeMessage(rootA, 'fetch2', []));
const yesGCLog = [...log];
log.length = 0;

// we need the syscall behavior of both cases to be the same
t.deepEqual(noGCLog, yesGCLog);
});
Loading

0 comments on commit d5d2624

Please sign in to comment.