-
Notifications
You must be signed in to change notification settings - Fork 212
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #7158 from Agoric/7142-gc-sensitivity
add (failing) tests of liveslots GC sensitivity
- Loading branch information
Showing
3 changed files
with
312 additions
and
96 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
206
packages/swingset-liveslots/test/test-gc-sensitivity.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); |
Oops, something went wrong.