From fca9d6b8cced6aa8b7f8d0a96bbd4f209135475b Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Wed, 8 Mar 2023 17:59:37 -0800 Subject: [PATCH 1/2] extract mock-gc.js out from test-liveslots.js This utility creates manually-controlled GC utilities: WeakRef and FinalizationRegistry --- packages/swingset-liveslots/test/mock-gc.js | 104 ++++++++++++++++++ .../swingset-liveslots/test/test-liveslots.js | 98 +---------------- 2 files changed, 106 insertions(+), 96 deletions(-) create mode 100644 packages/swingset-liveslots/test/mock-gc.js diff --git a/packages/swingset-liveslots/test/mock-gc.js b/packages/swingset-liveslots/test/mock-gc.js new file mode 100644 index 00000000000..f6c6d4ae7b2 --- /dev/null +++ b/packages/swingset-liveslots/test/mock-gc.js @@ -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(), + }); +} diff --git a/packages/swingset-liveslots/test/test-liveslots.js b/packages/swingset-liveslots/test/test-liveslots.js index a03d2649825..d36cfa5ce55 100644 --- a/packages/swingset-liveslots/test/test-liveslots.js +++ b/packages/swingset-liveslots/test/test-liveslots.js @@ -5,11 +5,9 @@ import '@endo/init/debug.js'; import { E } from '@endo/eventual-send'; import { Far } from '@endo/marshal'; import { makePromiseKit } from '@endo/promise-kit'; -import { assert, Fail } from '@agoric/assert'; +import { Fail } from '@agoric/assert'; import engineGC from './engine-gc.js'; -import { waitUntilQuiescent } from './waitUntilQuiescent.js'; import { makeGcAndFinalize } from './gc-and-finalize.js'; -import { makeDummyMeterControl } from './dummyMeterControl.js'; import { makeLiveSlots, makeMarshaller } from '../src/liveslots.js'; import { kslot, kser, kunser } from './kmarshal.js'; import { buildSyscall, makeDispatch } from './liveslots-helpers.js'; @@ -23,6 +21,7 @@ import { makeRetireExports, makeRetireImports, } from './util.js'; +import { makeMockGC } from './mock-gc.js'; function matchIDCounterSet(t, log) { t.like(log.shift(), { type: 'vatstoreSet', key: 'idCounters' }); @@ -1201,99 +1200,6 @@ test('GC dispatch.retireExports', async t => { t.deepEqual(log, []); }); -// 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; - } - - function mockGCAndFinalize() {} - - return harden({ - WeakRef: mockWeakRef, - FinalizationRegistry: mockFinalizationRegistry, - kill, - getAllFRs, - waitUntilQuiescent, - gcAndFinalize: mockGCAndFinalize, - meterControl: makeDummyMeterControl(), - }); -} - test('dropImports', async t => { const { syscall } = buildSyscall(); const imports = []; From d3f63be5f195e8638aac4098d2bc0cbbb7ad6a26 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Wed, 8 Mar 2023 18:03:35 -0800 Subject: [PATCH 2/2] add failing tests: syscalls GC sensitivity via reanimation This exercises three cases in which syscalls are sensitive to GC activity: * reanimateDurableKindID, when regenerating a KindHandle (#7142) * reanimate, when regenerating a plain Representative (#7142) * reanimateCollection, when regenerating a virtual collection (#6360) --- .../test/test-gc-sensitivity.js | 206 ++++++++++++++++++ 1 file changed, 206 insertions(+) create mode 100644 packages/swingset-liveslots/test/test-gc-sensitivity.js diff --git a/packages/swingset-liveslots/test/test-gc-sensitivity.js b/packages/swingset-liveslots/test/test-gc-sensitivity.js new file mode 100644 index 00000000000..0994a2a96d1 --- /dev/null +++ b/packages/swingset-liveslots/test/test-gc-sensitivity.js @@ -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); +});