diff --git a/packages/SwingSet/src/kernel/liveSlots.js b/packages/SwingSet/src/kernel/liveSlots.js index 5ca1151cc57..555fe387ebd 100644 --- a/packages/SwingSet/src/kernel/liveSlots.js +++ b/packages/SwingSet/src/kernel/liveSlots.js @@ -69,12 +69,12 @@ function build( * Translation and tracking tables to map in-vat object/promise references * to/from vat-format slot strings. * - * Exports: pass-by-presence objects (Remotables) in the vat are exported - * as o+NN slots, as are "virtual object" exports. Promises are exported as - * p+NN slots. We retain a strong reference to all exports via the - * `exportedRemotables` Set until (TODO) the kernel tells us all external - * references have been dropped via dispatch.dropExports, or by some - * unilateral revoke-object operation executed by our user-level code. + * Exports: pass-by-presence objects (Remotables) in the vat are exported as + * o+NN slots, as are "virtual object" exports. Promises are exported as p+NN + * slots. We retain a strong reference to all exports via the + * `exportedRemotables` Set until the kernel tells us all external references + * have been dropped via dispatch.dropExports, or by some unilateral + * revoke-object operation executed by our user-level code. * * Imports: o-NN slots are represented as a Presence. p-NN slots are * represented as an imported Promise, with the resolver held in an @@ -104,9 +104,11 @@ function build( const valToSlot = new WeakMap(); // object -> vref const slotToVal = new Map(); // vref -> WeakRef(object) const exportedRemotables = new Set(); // objects + const kernelRecognizableRemotables = new Set(); // vrefs const pendingPromises = new Set(); // Promises const importedDevices = new Set(); // device nodes - const deadSet = new Set(); // vrefs that are finalized but not yet reported + const possiblyDeadSet = new Set(); // vrefs that need to be checked for being dead + const possiblyRetiredSet = new Set(); // vrefs that might need to be rechecked for being retired function retainExportedVref(vref) { // if the vref corresponds to a Remotable, keep a strong reference to it @@ -115,11 +117,12 @@ function build( if (type === 'object' && allocatedByVat) { if (virtual) { // eslint-disable-next-line no-use-before-define - vom.setExported(vref, true); + vom.setExportStatus(vref, 'reachable'); } else { - const remotable = slotToVal.get(vref).deref(); - assert(remotable, X`somehow lost Remotable for ${vref}`); + // eslint-disable-next-line no-use-before-define + const remotable = requiredValForSlot(vref); exportedRemotables.add(remotable); + kernelRecognizableRemotables.add(vref); } } } @@ -171,6 +174,10 @@ function build( */ function finalizeDroppedImport(vref) { + // TODO: Ideally this function should assert that it is not metered. This + // appears to be fine in practice, but it breaks a number of unit tests in + // ways that are not obvious how to fix. + // meterControl.assertNotMetered(); const wr = slotToVal.get(vref); // The finalizer for a given Presence might run in any state: // * COLLECTED: most common. Action: move to FINALIZED @@ -183,40 +190,88 @@ function build( if (wr && !wr.deref()) { // we're in the COLLECTED state, or FINALIZED after a re-introduction - deadSet.add(vref); + // eslint-disable-next-line no-use-before-define + addToPossiblyDeadSet(vref); slotToVal.delete(vref); - // console.log(`-- adding ${vref} to deadSet`); } } const droppedRegistry = new FinalizationRegistry(finalizeDroppedImport); - function processDeadSet() { - let doMore = false; + async function scanForDeadObjects() { + // `possiblyDeadSet` accumulates vrefs which have lost a supporting + // pillar (in-memory, export, or virtualized data refcount) since the + // last call to scanForDeadObjects. The vref might still be supported + // by a remaining pillar, or the pillar which was dropped might be back + // (e.g., given a new in-memory manifestation). + const [importsToDrop, importsToRetire, exportsToRetire] = [[], [], []]; + let doMore; + do { + doMore = false; + + // Yes, we know this is an await inside a loop. Too bad. (Also, it's a + // `do {} while` loop, which means there's no conditional bypass of the + // await.) + // eslint-disable-next-line no-await-in-loop + await gcTools.gcAndFinalize(); + + // `deadSet` is the subset of those vrefs which lack an in-memory + // manifestation *right now* (i.e. the non-resurrected ones), for which + // we must check the remaining pillars. + const deadSet = new Set(); + for (const vref of possiblyDeadSet) { + // eslint-disable-next-line no-use-before-define + if (!slotToVal.has(vref)) { + deadSet.add(vref); + } + } + possiblyDeadSet.clear(); - for (const vref of deadSet) { - const { virtual, allocatedByVat, type } = parseVatSlot(vref); - assert(type === 'object', `unprepared to track ${type}`); - if (virtual) { - // Representative: send nothing, but perform refcount checking + for (const vref of possiblyRetiredSet) { // eslint-disable-next-line no-use-before-define - doMore = doMore || vom.possibleVirtualObjectDeath(vref); - } else if (allocatedByVat) { - // Remotable: send retireExport - exportsToRetire.push(vref); - } else { - // Presence: send dropImport unless reachable by VOM - // eslint-disable-next-line no-lonely-if, no-use-before-define - if (!vom.isVrefReachable(vref)) { - importsToDrop.push(vref); + if (!getValForSlot(vref) && !deadSet.has(vref)) { + // Don't retire things that haven't yet made the transition to dead, + // i.e., always drop before retiring // eslint-disable-next-line no-use-before-define if (!vom.isVrefRecognizable(vref)) { importsToRetire.push(vref); } } } - } - deadSet.clear(); + possiblyRetiredSet.clear(); + + const deadVrefs = Array.from(deadSet); + deadVrefs.sort(); + for (const vref of deadVrefs) { + const { virtual, allocatedByVat, type } = parseVatSlot(vref); + assert(type === 'object', `unprepared to track ${type}`); + if (virtual) { + // Representative: send nothing, but perform refcount checking + // eslint-disable-next-line no-use-before-define + const [gcAgain, doRetire] = vom.possibleVirtualObjectDeath(vref); + if (doRetire) { + exportsToRetire.push(vref); + } + doMore = doMore || gcAgain; + } else if (allocatedByVat) { + // Remotable: send retireExport + if (kernelRecognizableRemotables.has(vref)) { + kernelRecognizableRemotables.delete(vref); + exportsToRetire.push(vref); + } + } else { + // Presence: send dropImport unless reachable by VOM + // eslint-disable-next-line no-lonely-if, no-use-before-define + if (!vom.isPresenceReachable(vref)) { + importsToDrop.push(vref); + // eslint-disable-next-line no-use-before-define + if (!vom.isVrefRecognizable(vref)) { + importsToRetire.push(vref); + } + } + } + } + } while (possiblyDeadSet.size > 0 || possiblyRetiredSet.size > 0 || doMore); if (importsToDrop.length) { importsToDrop.sort(); @@ -230,11 +285,6 @@ function build( exportsToRetire.sort(); syscall.retireExports(exportsToRetire); } - - // TODO: doMore=true when we've done something that might free more local - // objects, which probably won't happen until we sense entire WeakMaps - // going away or something involving virtual collections - return doMore; } /** Remember disavowed Presences which will kill the vat if you try to talk @@ -420,19 +470,39 @@ function build( } function getValForSlot(slot) { + meterControl.assertNotMetered(); const wr = slotToVal.get(slot); return wr && wr.deref(); } + function requiredValForSlot(slot) { + const wr = slotToVal.get(slot); + const result = wr && wr.deref(); + assert(result, X`no value for ${slot}`); + return result; + } + + function addToPossiblyDeadSet(vref) { + possiblyDeadSet.add(vref); + } + + function addToPossiblyRetiredSet(vref) { + possiblyRetiredSet.add(vref); + } + const vom = makeVirtualObjectManager( syscall, allocateExportID, getSlotForVal, - getValForSlot, + requiredValForSlot, // eslint-disable-next-line no-use-before-define registerValue, - m, + m.serialize, + unmeteredUnserialize, cacheSize, + FinalizationRegistry, + addToPossiblyDeadSet, + addToPossiblyRetiredSet, ); function convertValToSlot(val) { @@ -471,7 +541,6 @@ function build( // doesn't matter anyway because deadSet.add only happens when // finializers run, and we wrote xsnap.c to ensure they only run // deterministically (during gcAndFinalize) - deadSet.delete(slot); droppedRegistry.register(val, slot, val); } } @@ -494,7 +563,6 @@ function build( valToSlot.set(val, slot); // we don't dropImports on promises, to avoid interaction with retire if (type === 'object') { - deadSet.delete(slot); // might have been FINALIZED before, no longer droppedRegistry.register(val, slot, val); } } @@ -508,8 +576,7 @@ function build( function convertSlotToVal(slot, iface = undefined) { meterControl.assertNotMetered(); const { type, allocatedByVat, virtual } = parseVatSlot(slot); - const wr = slotToVal.get(slot); - let val = wr && wr.deref(); + let val = getValForSlot(slot); if (val) { if (virtual) { // If it's a virtual object for which we already have a representative, @@ -570,9 +637,7 @@ function build( const { type } = parseVatSlot(slot); if (type === 'promise') { // this can run metered because it's supposed to always be present - const wr = slotToVal.get(slot); - const p = wr && wr.deref(); - assert(p, X`should have a value for ${slot} but didn't`); + const p = requiredValForSlot(slot); const priorResolution = knownResolutions.get(p); if (priorResolution && !doneResolutions.has(slot)) { const [priorRejected, priorRes] = priorResolution; @@ -770,9 +835,7 @@ function build( function retirePromiseID(promiseID) { lsdebug(`Retiring ${forVatID}:${promiseID}`); importedPromisesByPromiseID.delete(promiseID); - meterControl.assertNotMetered(); - const wr = slotToVal.get(promiseID); - const p = wr && wr.deref(); + const p = getValForSlot(promiseID); if (p) { valToSlot.delete(p); pendingPromises.delete(p); @@ -848,6 +911,7 @@ function build( retirePromiseID(vpid); } const imports = finishCollectingPromiseImports(); + meterControl.assertNotMetered(); for (const slot of imports) { if (slotToVal.get(slot)) { // we'll only subscribe to new promises, which is within consensus @@ -861,15 +925,15 @@ function build( vrefs.map(vref => insistVatType('object', vref)); vrefs.map(vref => assert(parseVatSlot(vref).allocatedByVat)); // console.log(`-- liveslots acting upon dropExports ${vrefs.join(',')}`); + meterControl.assertNotMetered(); for (const vref of vrefs) { - const wr = slotToVal.get(vref); - const o = wr && wr.deref(); + const o = getValForSlot(vref); if (o) { exportedRemotables.delete(o); } const { virtual } = parseVatSlot(vref); if (virtual) { - vom.setExported(vref, false); + vom.setExportStatus(vref, 'recognizable'); } } } @@ -879,11 +943,9 @@ function build( const { virtual, allocatedByVat, type } = parseVatSlot(vref); assert(allocatedByVat); assert.equal(type, 'object'); + // console.log(`-- liveslots acting on retireExports ${vref}`); if (virtual) { - // virtual object: ignore for now, but TODO we must still not make - // syscall.retireExport for vrefs that were already retired by the - // kernel - // console.log(`-- liveslots ignoring retireExports ${vref}`); + vom.setExportStatus(vref, 'none'); } else { // Remotable // console.log(`-- liveslots acting on retireExports ${vref}`); @@ -911,6 +973,7 @@ function build( valToSlot.delete(val); droppedRegistry.unregister(val); } + kernelRecognizableRemotables.delete(vref); slotToVal.delete(vref); } } @@ -925,7 +988,7 @@ function build( assert(Array.isArray(vrefs)); vrefs.map(vref => insistVatType('object', vref)); vrefs.map(vref => assert(!parseVatSlot(vref).allocatedByVat)); - // console.log(`-- liveslots ignoring retireImports ${vrefs.join(',')}`); + vrefs.forEach(vom.ceaseRecognition); } // TODO: when we add notifyForward, guard against cycles @@ -971,15 +1034,23 @@ function build( WeakSet: vom.VirtualObjectAwareWeakSet, }); + const testHooks = harden({ ...vom.testHooks }); + function setBuildRootObject(buildRootObject) { assert(!didRoot); didRoot = true; - // vats which use D are: (bootstrap, bridge, vat-http), swingset + // Build the `vatPowers` provided to `buildRootObject`. We include + // vatGlobals and inescapableGlobalProperties to make it easier to write + // unit tests that share their vatPowers with the test program, for + // direct manipulation). 'D' is used by only a few vats: (bootstrap, + // bridge, vat-http). const vpow = { D, exitVat, exitVatWithFailure, + ...vatGlobals, + ...inescapableGlobalProperties, ...vatPowers, }; if (enableDisavow) { @@ -1064,17 +1135,6 @@ function build( // metered const unmeteredDispatch = meterControl.unmetered(dispatchToUserspace); - const { waitUntilQuiescent, gcAndFinalize } = gcTools; - - async function finish() { - await gcAndFinalize(); - const doMore = processDeadSet(); - if (doMore) { - return finish(); - } - return undefined; - } - /** * This low-level liveslots code is responsible for deciding when userspace * is done with a crank. Userspace code can use Promises, so it can add as @@ -1098,23 +1158,24 @@ function build( // Instead, we wait for userspace to become idle by draining the promise // queue. - await waitUntilQuiescent(); + await gcTools.waitUntilQuiescent(); // Userspace will not get control again within this crank. // Now that userspace is idle, we can drive GC until we think we've // stopped. - return meterControl.runWithoutMeteringAsync(finish); + return meterControl.runWithoutMeteringAsync(scanForDeadObjects); } harden(dispatch); - // we return 'deadSet' for unit tests + // we return 'possiblyDeadSet' for unit tests return harden({ vatGlobals, inescapableGlobalProperties, setBuildRootObject, dispatch, m, - deadSet, + possiblyDeadSet, + testHooks, }); } @@ -1186,14 +1247,16 @@ export function makeLiveSlots( inescapableGlobalProperties, dispatch, setBuildRootObject, - deadSet, + possiblyDeadSet, + testHooks, } = r; // omit 'm' return harden({ vatGlobals, inescapableGlobalProperties, dispatch, setBuildRootObject, - deadSet, + possiblyDeadSet, + testHooks, }); } diff --git a/packages/SwingSet/src/kernel/virtualObjectManager.js b/packages/SwingSet/src/kernel/virtualObjectManager.js index d92661fd946..d0cbda07b5f 100644 --- a/packages/SwingSet/src/kernel/virtualObjectManager.js +++ b/packages/SwingSet/src/kernel/virtualObjectManager.js @@ -1,3 +1,4 @@ +// @ts-check /* eslint-disable no-use-before-define */ import { assert, details as X, quote as q } from '@agoric/assert'; @@ -130,13 +131,19 @@ export function makeCache(size, fetch, store) { * export ID for the enclosing vat. * @param { (val: Object) => string} getSlotForVal A function that returns the object ID (vref) for a given object, if any. * their corresponding export IDs - * @param { (slot: string) => Object} getValForSlot A function that converts an object ID (vref) to an - * object, if any, else undefined. + * @param { (slot: string) => Object} requiredValForSlot A function that converts an object ID (vref) to an + * object. * @param {*} registerEntry Function to register a new slot+value in liveSlot's * various tables - * @param {*} m The vat's marshaler. + * @param {*} serialize Serializer for this vat + * @param {*} unserialize Unserializer for this vat * @param {number} cacheSize How many virtual objects this manager should cache * in memory. + * @param {*} FinalizationRegistry Powerful JavaScript intrinsic normally denied by SES + * @param {*} addToPossiblyDeadSet Function to record objects whose deaths should be reinvestigated + * @param {*} addToPossiblyRetiredSet Function to record dead objects whose retirement should be + * reinvestigated + * * @returns {Object} a new virtual object manager. * * The virtual object manager allows the creation of persistent objects that do @@ -168,11 +175,19 @@ export function makeVirtualObjectManager( syscall, allocateExportID, getSlotForVal, - getValForSlot, + requiredValForSlot, registerEntry, - m, + serialize, + unserialize, cacheSize, + FinalizationRegistry, + addToPossiblyDeadSet, + addToPossiblyRetiredSet, ) { + const droppedCollectionRegistry = new FinalizationRegistry( + finalizeDroppedCollection, + ); + /** * Fetch an object's state from secondary storage. * @@ -195,64 +210,122 @@ export function makeVirtualObjectManager( syscall.vatstoreSet(`vom.${vobjID}`, JSON.stringify(rawData)); } + /** + * Check if a virtual object is truly dead - i.e., unreachable - and truly + * delete it if so. + * + * A virtual object is kept alive by being reachable by any of three legs: + * - any in-memory references to it (if so, it will have a representative and + * thus a non-null slot-to-val entry) + * - any virtual references to it (if so, it will have a refcount > 0) + * - being exported (if so, its export flag will be set) + * + * This function is called after a leg has been reported missing, and only + * if the memory (Representative) leg is currently missing, to see if the + * other two legs are now gone also. + * + * Deletion consists of removing the vatstore entries that describe its state + * and track its refcount status. In addition, when a virtual object is + * deleted, we delete any weak collection entries for which it was a key. If + * it had been exported, we also inform the kernel that the vref has been + * retired, so other vats can delete their weak collection entries too. + * + * @param {string} vobjID The vref of the virtual object that's plausibly dead + * + * @returns {[boolean, boolean]} A pair of flags: the first is true if this + * possibly created a new GC opportunity, the second is true if the object + * should now be regarded as unrecognizable + */ function possibleVirtualObjectDeath(vobjID) { - if (!isVrefReachable(vobjID) && !getValForSlot(vobjID)) { - const [exported, refCount] = getRefCounts(vobjID); - if (exported === 0 && refCount === 0) { - // TODO: decrement refcounts on vrefs in the virtualized data being deleted - syscall.vatstoreDelete(`vom.${vobjID}`); - syscall.vatstoreDelete(`vom.${vobjID}.refCount`); + const refCount = getRefCount(vobjID); + const exportStatus = getExportStatus(vobjID); + if (exportStatus !== 'reachable' && refCount === 0) { + const rawState = fetch(vobjID); + let doMoreGC = false; + for (const propValue of Object.values(rawState)) { + propValue.slots.map( + vref => (doMoreGC = doMoreGC || removeReachableVref(vref)), + ); } + syscall.vatstoreDelete(`vom.${vobjID}`); + syscall.vatstoreDelete(`vom.rc.${vobjID}`); + syscall.vatstoreDelete(`vom.es.${vobjID}`); + doMoreGC = doMoreGC || ceaseRecognition(vobjID); + return [doMoreGC, exportStatus !== 'none']; } + return [false, false]; } - function getRefCounts(vobjID) { - const rawCounts = syscall.vatstoreGet(`vom.${vobjID}.refCount`); - if (rawCounts) { - return rawCounts.split(' ').map(Number); + function getRefCount(vobjID) { + const raw = syscall.vatstoreGet(`vom.rc.${vobjID}`); + if (raw) { + return Number(raw); } else { - return [0, 0]; + return 0; } } - function setRefCounts(vobjID, exported, count) { - syscall.vatstoreSet( - `vom.${vobjID}.refCount`, - `${Nat(exported)} ${Nat(count)}`, - ); - if (exported === 0 && count === 0) { - possibleVirtualObjectDeath(vobjID); + function getExportStatus(vobjID) { + const raw = syscall.vatstoreGet(`vom.es.${vobjID}`); + switch (raw) { + case '0': + return 'recognizable'; + case '1': + return 'reachable'; + default: + return 'none'; } } - function setExported(vobjID, newSetting) { - const [wasExported, refCount] = getRefCounts(vobjID); - const isNowExported = Number(newSetting); - if (wasExported !== isNowExported) { - setRefCounts(vobjID, isNowExported, refCount); + function setRefCount(vobjID, refCount) { + const { virtual } = parseVatSlot(vobjID); + syscall.vatstoreSet(`vom.rc.${vobjID}`, `${Nat(refCount)}`); + if (refCount === 0) { + if (!virtual) { + syscall.vatstoreDelete(`vom.rc.${vobjID}`); + } + addToPossiblyDeadSet(vobjID); } } - function incRefCount(vobjID) { - const [exported, oldCount] = getRefCounts(vobjID); - if (oldCount === 0) { - // TODO: right now we are not tracking actual refcounts, so for now a - // refcount of 0 means never referenced and a refcount of 1 means - // referenced at least once in the past. Once actual refcounts are - // working (notably including calling decref at the appropriate times), - // take out the above if. - setRefCounts(vobjID, exported, oldCount + 1); + function setExportStatus(vobjID, exportStatus) { + const key = `vom.es.${vobjID}`; + switch (exportStatus) { + // POSSIBLE TODO: An anticipated refactoring may merge + // dispatch.dropExports with dispatch.retireExports. If this happens, and + // the export status can drop from 'reachable' to 'none' in a single step, we + // must perform this "the export pillar has dropped" check in both the + // reachable and the none cases (possibly reading the old status first, if + // we must ensure addToPossiblyDeadSet only happens once). + case 'recognizable': { + syscall.vatstoreSet(key, '0'); + const refCount = getRefCount(vobjID); + if (refCount === 0) { + addToPossiblyDeadSet(vobjID); + } + break; + } + case 'reachable': + syscall.vatstoreSet(key, '1'); + break; + case 'none': + syscall.vatstoreDelete(key); + break; + default: + assert.fail(`invalid set export status ${exportStatus}`); } } - // TODO: reenable once used; it's commented out just to make eslint shut up - /* + function incRefCount(vobjID) { + const oldRefCount = getRefCount(vobjID); + setRefCount(vobjID, oldRefCount + 1); + } + function decRefCount(vobjID) { - const [exported, oldCount] = getRefCounts(vobjID); - assert(oldCount > 0, `attempt to decref ${vobjID} below 0`); - setRefCounts(vobjID, exported, oldCount - 1); + const oldRefCount = getRefCount(vobjID); + assert(oldRefCount > 0, `attempt to decref ${vobjID} below 0`); + setRefCount(vobjID, oldRefCount - 1); } - */ const cache = makeCache(cacheSize, fetch, store); @@ -263,85 +336,222 @@ export function makeVirtualObjectManager( const kindTable = new Map(); /** - * Set of all import vrefs which are reachable from our virtualized data. - * These were Presences at one point. We add to this set whenever we store - * a Presence into the state of a virtual object, or the value of a - * makeVirtualScalarWeakMap() instance. We currently never remove anything from the - * set, but eventually we'll use refcounts within virtual data to figure - * out when the vref becomes unreachable, allowing the vat to send a - * dropImport into the kernel and release the object. - * + * Map of all Remotables which are reachable by our virtualized data, e.g. + * `makeWeakStore().set(key, remotable)` or `virtualObject.state.foo = + * remotable`. The serialization process stores the Remotable's vref to disk, + * but doesn't actually retain the Remotable. To correctly unserialize that + * offline data later, we must ensure the Remotable remains alive. This Map + * keeps a strong reference to the Remotable along with its (virtual) refcount. */ - /** @type {Set} of vrefs */ - const reachableVrefs = new Set(); + /** @type {Map} Remotable->refcount */ + const remotableRefCounts = new Map(); - // We track imports, to preserve their vrefs against syscall.dropImport - // when the Presence goes away. - function addReachablePresenceRef(vref) { - // XXX TODO including virtual objects gives lie to the name, but this hack should go away with VO refcounts - const { type, allocatedByVat, virtual } = parseVatSlot(vref); - if (type === 'object' && (!allocatedByVat || virtual)) { - reachableVrefs.add(vref); + // Note that since presence refCounts are keyed by vref, `processDeadSet` must + // query the refCount directly in order to determine if a presence that found + // its way into the dead set is live or not, whereas it never needs to query + // the `remotableRefCounts` map because that map holds actual live references + // as keys and so Remotable references will only find their way into the dead + // set if they are actually unreferenced (including, notably, their absence + // from the `remotableRefCounts` map). + + function addReachableVref(vref) { + const { type, virtual, allocatedByVat } = parseVatSlot(vref); + if (type === 'object') { + if (allocatedByVat) { + if (virtual) { + incRefCount(vref); + } else { + // exported non-virtual object: Remotable + const remotable = requiredValForSlot(vref); + if (remotableRefCounts.has(remotable)) { + /** @type {number} */ + const oldRefCount = (remotableRefCounts.get(remotable)); + remotableRefCounts.set(remotable, oldRefCount + 1); + } else { + remotableRefCounts.set(remotable, 1); + } + } + } else { + // We refcount imports, to preserve their vrefs against + // syscall.dropImport when the Presence itself goes away. + incRefCount(vref); + } } } - function isVrefReachable(vref) { - return reachableVrefs.has(vref); + function removeReachableVref(vref) { + let droppedMemoryReference = false; + const { type, virtual, allocatedByVat } = parseVatSlot(vref); + if (type === 'object') { + if (allocatedByVat) { + if (virtual) { + decRefCount(vref); + } else { + // exported non-virtual object: Remotable + const remotable = requiredValForSlot(vref); + /** @type {number} */ + const oldRefCount = (remotableRefCounts.get(remotable)); + assert(oldRefCount > 0, `attempt to decref ${vref} below 0`); + if (oldRefCount === 1) { + remotableRefCounts.delete(remotable); + droppedMemoryReference = true; + } else { + remotableRefCounts.set(remotable, oldRefCount - 1); + } + } + } else { + decRefCount(vref); + } + } + return droppedMemoryReference; + } + + function updateReferenceCounts(beforeSlots, afterSlots) { + // Note that the slots of a capdata object are not required to be + // deduplicated nor are they expected to be presented in any particular + // order, so the comparison of which references appear in the before state + // to which appear in the after state must look only at the presence or + // absence of individual elements from the slots arrays and pay no attention + // to the organization of the slots arrays themselves. + const vrefStatus = {}; + for (const vref of beforeSlots) { + vrefStatus[vref] = 'drop'; + } + for (const vref of afterSlots) { + if (vrefStatus[vref] === 'drop') { + vrefStatus[vref] = 'keep'; + } else if (!vrefStatus[vref]) { + vrefStatus[vref] = 'add'; + } + } + for (const [vref, status] of Object.entries(vrefStatus)) { + switch (status) { + case 'add': + addReachableVref(vref); + break; + case 'drop': + removeReachableVref(vref); + break; + default: + break; + } + } + } + + /** + * Check if a given vref points to a reachable presence. + * + * @param {string} vref The vref of the presence being enquired about + * + * @returns {boolean} true if the indicated presence remains reachable. + */ + function isPresenceReachable(vref) { + return !!getRefCount(vref); } /** - * Set of all import vrefs which are recognizable by our virtualized data. - * These were Presences at one point. We add to this set whenever we use a - * Presence as a key into a makeVirtualScalarWeakMap() instance or an instance of - * VirtualObjectAwareWeakMap or VirtualObjectAwareWeakSet. We currently never - * remove anything from the set, but eventually we'll use refcounts to figure - * out when the vref becomes unrecognizable, allowing the vat to send a - * retireImport into the kernel. + * A map from vrefs (those which are recognizable by (i.e., used as keys in) + * VOM aware collections) to sets of recognizers (the collections for which + * they are respectively used as keys). These vrefs correspond to either + * imported Presences or virtual objects (Remotables do not participate in + * this as they are not keyed by vref but by the actual Remotable objects + * themselves). We add to a vref's recognizer set whenever we use a Presence + * or virtual object as a key into a makeVirtualScalarWeakMap() instance or an + * instance of VirtualObjectAwareWeakMap or VirtualObjectAwareWeakSet. We + * remove it whenever that key (or the whole collection containing it) is + * deleted. + * + * A recognizer is one of: + * Map - the map contained within a VirtualObjectAwareWeakMap to point to its vref-keyed entries. + * Set - the set contained within a VirtualObjectAwareWeakSet to point to its vref-keyed entries. + * deleter - a function within a WeakStore that can be called to remove an entry from that store. * + * It is critical that each collection have exactly one recognizer that is + * unique to that collection, because the recognizers themselves will be + * tracked by their object identities, but the recognizer cannot be the + * collection itself else it would prevent the collection from being garbage + * collected. + * + * TODO: all the "recognizers" in principle could be, and probably should be, + * reduced to deleter functions. However, since the VirtualObjectAware + * collections are actual JavaScript classes I need to take some care to + * ensure that I've got the exactly-one-per-collection invariant handled + * correctly. + * + * TODO: concoct a better type def than Set + */ + /** + * @typedef { Map | Set | ((string) => void) } Recognizer */ - /** @type {Set} of vrefs */ - const recognizableVrefs = new Set(); + /** @type {Map>} */ + const vrefRecognizers = new Map(); - function addRecognizablePresenceValue(value) { + function addRecognizableValue(value, recognizer) { const vref = getSlotForVal(value); if (vref) { - const { type, allocatedByVat } = parseVatSlot(vref); - if (type === 'object' && !allocatedByVat) { - recognizableVrefs.add(vref); + const { type, allocatedByVat, virtual } = parseVatSlot(vref); + if (type === 'object' && (!allocatedByVat || virtual)) { + let recognizerSet = vrefRecognizers.get(vref); + if (!recognizerSet) { + recognizerSet = new Set(); + vrefRecognizers.set(vref, recognizerSet); + } + recognizerSet.add(recognizer); } } } - function isVrefRecognizable(vref) { - return recognizableVrefs.has(vref); + function removeRecognizableVref(vref, recognizer) { + const { type, allocatedByVat, virtual } = parseVatSlot(vref); + if (type === 'object' && (!allocatedByVat || virtual)) { + const recognizerSet = vrefRecognizers.get(vref); + assert(recognizerSet && recognizerSet.has(recognizer)); + recognizerSet.delete(recognizer); + if (recognizerSet.size === 0) { + vrefRecognizers.delete(vref); + if (!allocatedByVat) { + addToPossiblyRetiredSet(vref); + } + } + } + } + + function removeRecognizableValue(value, recognizer) { + const vref = getSlotForVal(value); + if (vref) { + removeRecognizableVref(vref, recognizer); + } } /** - * Set of all Remotables which are reachable by our virtualized data, e.g. - * `makeVirtualScalarWeakMap().set(key, remotable)` or `virtualObject.state.foo = - * remotable`. The serialization process stores the Remotable's vref to - * disk, but doesn't actually retain the Remotable. To correctly - * unserialize that offline data later, we must ensure the Remotable - * remains alive. This Set keeps a strong reference to the Remotable. We - * currently never remove anything from the set, but eventually refcounts - * will let us discover when it is no longer reachable, and we'll drop the - * strong reference. + * Remove a given vref from all weak collections in which it was used as a + * key. + * + * @param {string} vref The vref that shall henceforth no longer be recognized + * + * @returns {boolean} true if this possibly creates a GC opportunity */ - /** @type {Set} of Remotables */ - const reachableRemotables = new Set(); - function addReachableRemotableRef(vref) { - const { type, virtual, allocatedByVat } = parseVatSlot(vref); - if (type === 'object' && allocatedByVat) { - if (virtual) { - incRefCount(vref); - } else { - // exported non-virtual object: Remotable - const remotable = getValForSlot(vref); - assert(remotable, X`no remotable for ${vref}`); - // console.log(`adding ${vref} to reachableRemotables`); - reachableRemotables.add(remotable); + function ceaseRecognition(vref) { + const recognizerSet = vrefRecognizers.get(vref); + let doMoreGC = false; + if (recognizerSet) { + vrefRecognizers.delete(vref); + for (const recognizer of recognizerSet) { + if (recognizer instanceof Map) { + recognizer.delete(vref); + doMoreGC = true; + } else if (recognizer instanceof Set) { + recognizer.delete(vref); + } else if (typeof recognizer === 'function') { + recognizer(vref); + } } } + return doMoreGC; + } + + function isVrefRecognizable(vref) { + return vrefRecognizers.has(vref); } /** @@ -412,7 +622,11 @@ export function makeVirtualObjectManager( return undefined; } - return harden({ + function deleter(vobjID) { + syscall.vatstoreDelete(`vom.ws${storeID}.${vobjID}`); + } + + const result = harden({ has(key) { const vkey = virtualObjectKey(key); if (vkey) { @@ -428,10 +642,9 @@ export function makeVirtualObjectManager( !syscall.vatstoreGet(vkey), X`${q(keyName)} already registered: ${key}`, ); - addRecognizablePresenceValue(key); - const data = m.serialize(value); - data.slots.map(addReachablePresenceRef); - data.slots.map(addReachableRemotableRef); + addRecognizableValue(key, deleter); + const data = serialize(value); + data.slots.map(addReachableVref); syscall.vatstoreSet(vkey, JSON.stringify(data)); } else { assertKeyDoesNotExist(key); @@ -443,7 +656,7 @@ export function makeVirtualObjectManager( if (vkey) { const rawValue = syscall.vatstoreGet(vkey); assert(rawValue, X`${q(keyName)} not found: ${key}`); - return m.unserialize(JSON.parse(rawValue)); + return unserialize(JSON.parse(rawValue)); } else { assertKeyExists(key); return backingMap.get(key); @@ -452,12 +665,12 @@ export function makeVirtualObjectManager( set(key, value) { const vkey = virtualObjectKey(key); if (vkey) { - assert(syscall.vatstoreGet(vkey), X`${q(keyName)} not found: ${key}`); - addRecognizablePresenceValue(key); - const data = m.serialize(harden(value)); - data.slots.map(addReachablePresenceRef); - data.slots.map(addReachableRemotableRef); - syscall.vatstoreSet(vkey, JSON.stringify(data)); + const rawBefore = syscall.vatstoreGet(vkey); + assert(rawBefore, X`${q(keyName)} not found: ${key}`); + const before = JSON.parse(rawBefore); + const after = serialize(harden(value)); + updateReferenceCounts(before.slots, after.slots); + syscall.vatstoreSet(vkey, JSON.stringify(after)); } else { assertKeyExists(key); backingMap.set(key, value); @@ -468,12 +681,37 @@ export function makeVirtualObjectManager( if (vkey) { assert(syscall.vatstoreGet(vkey), X`${q(keyName)} not found: ${key}`); syscall.vatstoreDelete(vkey); + removeRecognizableValue(key, deleter); } else { assertKeyExists(key); backingMap.delete(key); } }, }); + droppedCollectionRegistry.register( + result, + { + storeKey: `vom.ws${storeID}`, + deleter, + }, + result, + ); + return result; + } + + function finalizeDroppedCollection(body) { + if (body instanceof Map) { + for (const vref of body.keys()) { + removeRecognizableVref(vref, body); + } + } else if (body instanceof Set) { + for (const vref of body.values()) { + removeRecognizableVref(vref, body); + } + } else if (typeof body === 'object') { + // XXX oops, need to iterate vatstore keys and the API doesn't support that + console.log(`can't finalize WeakStore ${body} yet`); + } } function vrefKey(value) { @@ -495,7 +733,9 @@ export function makeVirtualObjectManager( class VirtualObjectAwareWeakMap { constructor() { actualWeakMaps.set(this, new WeakMap()); - virtualObjectMaps.set(this, new Map()); + const vmap = new Map(); + virtualObjectMaps.set(this, vmap); + droppedCollectionRegistry.register(this, vmap, this); } has(key) { @@ -519,8 +759,11 @@ export function makeVirtualObjectManager( set(key, value) { const vkey = vrefKey(key); if (vkey) { - addRecognizablePresenceValue(key); - virtualObjectMaps.get(this).set(vkey, value); + const vmap = virtualObjectMaps.get(this); + if (!vmap.has(vkey)) { + addRecognizableValue(key, vmap); + } + vmap.set(vkey, value); } else { actualWeakMaps.get(this).set(key, value); } @@ -530,7 +773,13 @@ export function makeVirtualObjectManager( delete(key) { const vkey = vrefKey(key); if (vkey) { - return virtualObjectMaps.get(this).delete(vkey); + const vmap = virtualObjectMaps.get(this); + if (vmap.has(vkey)) { + removeRecognizableValue(key, vmap); + return vmap.delete(vkey); + } else { + return false; + } } else { return actualWeakMaps.get(this).delete(key); } @@ -550,7 +799,9 @@ export function makeVirtualObjectManager( class VirtualObjectAwareWeakSet { constructor() { actualWeakSets.set(this, new WeakSet()); - virtualObjectSets.set(this, new Set()); + const vset = new Set(); + virtualObjectSets.set(this, vset); + droppedCollectionRegistry.register(this, vset, this); } has(value) { @@ -565,8 +816,11 @@ export function makeVirtualObjectManager( add(value) { const vkey = vrefKey(value); if (vkey) { - addRecognizablePresenceValue(value); - virtualObjectSets.get(this).add(vkey); + const vset = virtualObjectSets.get(this); + if (!vset.has(value)) { + addRecognizableValue(value, vset); + vset.add(vkey); + } } else { actualWeakSets.get(this).add(value); } @@ -576,7 +830,13 @@ export function makeVirtualObjectManager( delete(value) { const vkey = vrefKey(value); if (vkey) { - return virtualObjectSets.get(this).delete(vkey); + const vset = virtualObjectSets.get(this); + if (vset.has(vkey)) { + removeRecognizableValue(value, vset); + return vset.delete(vkey); + } else { + return false; + } } else { return actualWeakSets.get(this).delete(value); } @@ -698,14 +958,14 @@ export function makeVirtualObjectManager( Object.defineProperty(target, prop, { get: () => { ensureState(); - return m.unserialize(innerSelf.rawData[prop]); + return unserialize(innerSelf.rawData[prop]); }, set: value => { - const serializedValue = m.serialize(value); - serializedValue.slots.map(addReachablePresenceRef); - serializedValue.slots.map(addReachableRemotableRef); ensureState(); - innerSelf.rawData[prop] = serializedValue; + const before = innerSelf.rawData[prop]; + const after = serialize(value); + updateReferenceCounts(before.slots, after.slots); + innerSelf.rawData[prop] = after; innerSelf.dirty = true; }, }); @@ -765,9 +1025,8 @@ export function makeVirtualObjectManager( const rawData = {}; for (const prop of Object.getOwnPropertyNames(initialData)) { try { - const data = m.serialize(initialData[prop]); - data.slots.map(addReachablePresenceRef); - data.slots.map(addReachableRemotableRef); + const data = serialize(initialData[prop]); + data.slots.map(addReachableVref); rawData[prop] = data; } catch (e) { console.error(`state property ${String(prop)} is not serializable`); @@ -786,16 +1045,40 @@ export function makeVirtualObjectManager( return makeNewInstance; } + function countCollectionsForWeakKey(vref) { + const recognizerSet = vrefRecognizers.get(vref); + return recognizerSet ? recognizerSet.size : 0; + } + + function countWeakKeysForCollection(collection) { + const virtualObjectMap = virtualObjectMaps.get(collection); + if (virtualObjectMap) { + return virtualObjectMap.size; + } + const virtualObjectSet = virtualObjectSets.get(collection); + if (virtualObjectSet) { + return virtualObjectSet.size; + } + return 0; + } + + const testHooks = { + countCollectionsForWeakKey, + countWeakKeysForCollection, + }; + return harden({ makeVirtualScalarWeakMap, makeKind, VirtualObjectAwareWeakMap, VirtualObjectAwareWeakSet, - isVrefReachable, + isPresenceReachable, isVrefRecognizable, - setExported, + setExportStatus, flushCache: cache.flush, makeVirtualObjectRepresentative, possibleVirtualObjectDeath, + ceaseRecognition, + testHooks, }); } diff --git a/packages/SwingSet/test/gc/test-gc-vat.js b/packages/SwingSet/test/gc/test-gc-vat.js index 1a9749462b7..5b9a3affde2 100644 --- a/packages/SwingSet/test/gc/test-gc-vat.js +++ b/packages/SwingSet/test/gc/test-gc-vat.js @@ -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 = { diff --git a/packages/SwingSet/test/liveslots-helpers.js b/packages/SwingSet/test/liveslots-helpers.js index 1a8775c73ca..9b2b7a569f1 100644 --- a/packages/SwingSet/test/liveslots-helpers.js +++ b/packages/SwingSet/test/liveslots-helpers.js @@ -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) { @@ -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 }; @@ -41,6 +54,8 @@ export function makeDispatch( build, vatID = 'vatA', enableDisavow = false, + cacheSize = undefined, + returnTestHooks = undefined, ) { const gcTools = harden({ WeakRef, @@ -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; } diff --git a/packages/SwingSet/test/test-device-bridge.js b/packages/SwingSet/test/test-device-bridge.js index eba08d1d45b..c645e7846e3 100644 --- a/packages/SwingSet/test/test-device-bridge.js +++ b/packages/SwingSet/test/test-device-bridge.js @@ -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) diff --git a/packages/SwingSet/test/test-gc-kernel.js b/packages/SwingSet/test/test-gc-kernel.js index e394ac36a7d..a3f954e5216 100644 --- a/packages/SwingSet/test/test-gc-kernel.js +++ b/packages/SwingSet/test/test-gc-kernel.js @@ -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, diff --git a/packages/SwingSet/test/test-liveslots.js b/packages/SwingSet/test/test-liveslots.js index 7ed5e9b5317..1498c0183af 100644 --- a/packages/SwingSet/test/test-liveslots.js +++ b/packages/SwingSet/test/test-liveslots.js @@ -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], }); @@ -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); }); diff --git a/packages/SwingSet/test/vat-admin/terminate/test-terminate.js b/packages/SwingSet/test/vat-admin/terminate/test-terminate.js index e252e6f6a50..648f0ce66f3 100644 --- a/packages/SwingSet/test/vat-admin/terminate/test-terminate.js +++ b/packages/SwingSet/test/vat-admin/terminate/test-terminate.js @@ -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); @@ -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); diff --git a/packages/SwingSet/test/vat-admin/test-replay.js b/packages/SwingSet/test/vat-admin/test-replay.js index e00e05fbe48..a72abc91520 100644 --- a/packages/SwingSet/test/vat-admin/test-replay.js +++ b/packages/SwingSet/test/vat-admin/test-replay.js @@ -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: { @@ -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: { diff --git a/packages/SwingSet/test/virtualObjects/test-reachable-vrefs.js b/packages/SwingSet/test/virtualObjects/test-reachable-vrefs.js index 9999bef3ef4..75474a882c1 100644 --- a/packages/SwingSet/test/virtualObjects/test-reachable-vrefs.js +++ b/packages/SwingSet/test/virtualObjects/test-reachable-vrefs.js @@ -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 @@ -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)); }); diff --git a/packages/SwingSet/test/virtualObjects/test-representatives.js b/packages/SwingSet/test/virtualObjects/test-representatives.js index 148a63f36f8..def9cf3628e 100644 --- a/packages/SwingSet/test/virtualObjects/test-representatives.js +++ b/packages/SwingSet/test/virtualObjects/test-representatives.js @@ -150,7 +150,7 @@ test('virtual object representatives', async t => { t.deepEqual(c.kpResolution(rz2), capdata('"overflow"')); }); -test('exercise cache', async t => { +test.serial('exercise cache', async t => { const config = { bootstrap: 'bootstrap', vats: { @@ -257,9 +257,15 @@ test('exercise cache', async t => { async function hold(what) { await doSimple('holdThing', what); } - function thingID(num) { + function dataKey(num) { return `v1.vs.vom.o+1/${num}`; } + function esKey(num) { + return `v1.vs.vom.es.o+1/${num}`; + } + function rcKey(num) { + return `v1.vs.vom.rc.o+1/${num}`; + } function thingVal(name) { return JSON.stringify({ name: capdata(JSON.stringify(name)), @@ -278,13 +284,15 @@ test('exercise cache', async t => { // init cache - [] await make('thing1', true, T1); // make t1 - [t1] - t.deepEqual(log.shift(), ['get', `${thingID(1)}.refCount`, undefined]); - t.deepEqual(log.shift(), ['set', `${thingID(1)}.refCount`, '1 0']); + t.deepEqual(log.shift(), ['get', 'v1.vs.vom.rc.o-50', undefined]); + t.deepEqual(log.shift(), ['get', 'v1.vs.vom.rc.o-51', undefined]); + t.deepEqual(log.shift(), ['get', 'v1.vs.vom.rc.o-52', undefined]); + t.deepEqual(log.shift(), ['get', 'v1.vs.vom.rc.o-53', undefined]); + t.deepEqual(log.shift(), ['set', esKey(1), '1']); t.deepEqual(log, []); await make('thing2', false, T2); // make t2 - [t2 t1] - t.deepEqual(log.shift(), ['get', `${thingID(2)}.refCount`, undefined]); - t.deepEqual(log.shift(), ['set', `${thingID(2)}.refCount`, '1 0']); + t.deepEqual(log.shift(), ['set', esKey(2), '1']); t.deepEqual(log, []); await read(T1, 'thing1'); // refresh t1 - [t1 t2] @@ -292,52 +300,51 @@ test('exercise cache', async t => { await readHeld('thing1'); // refresh t1 - [t1 t2] await make('thing3', false, T3); // make t3 - [t3 t1 t2] - t.deepEqual(log.shift(), ['get', `${thingID(3)}.refCount`, undefined]); - t.deepEqual(log.shift(), ['set', `${thingID(3)}.refCount`, '1 0']); + t.deepEqual(log.shift(), ['set', esKey(3), '1']); t.deepEqual(log, []); await make('thing4', false, T4); // make t4 - [t4 t3 t1 t2] - t.deepEqual(log.shift(), ['get', `${thingID(4)}.refCount`, undefined]); - t.deepEqual(log.shift(), ['set', `${thingID(4)}.refCount`, '1 0']); + t.deepEqual(log.shift(), ['set', esKey(4), '1']); t.deepEqual(log, []); await make('thing5', false, T5); // evict t2, make t5 - [t5 t4 t3 t1] - t.deepEqual(log.shift(), ['get', `${thingID(5)}.refCount`, undefined]); - t.deepEqual(log.shift(), ['get', `${thingID(2)}.refCount`, '1 0']); - t.deepEqual(log.shift(), ['set', thingID(2), thingVal('thing2')]); - t.deepEqual(log.shift(), ['set', `${thingID(5)}.refCount`, '1 0']); + t.deepEqual(log.shift(), ['get', rcKey(2), undefined]); + t.deepEqual(log.shift(), ['get', esKey(2), '1']); + t.deepEqual(log.shift(), ['set', dataKey(2), thingVal('thing2')]); + t.deepEqual(log.shift(), ['set', esKey(5), '1']); t.deepEqual(log, []); await make('thing6', false, T6); // evict t1, make t6 - [t6 t5 t4 t3] - t.deepEqual(log.shift(), ['get', `${thingID(6)}.refCount`, undefined]); - t.deepEqual(log.shift(), ['set', thingID(1), thingVal('thing1')]); - t.deepEqual(log.shift(), ['set', `${thingID(6)}.refCount`, '1 0']); + t.deepEqual(log.shift(), ['set', dataKey(1), thingVal('thing1')]); + t.deepEqual(log.shift(), ['set', esKey(6), '1']); t.deepEqual(log, []); await make('thing7', false, T7); // evict t3, make t7 - [t7 t6 t5 t4] - t.deepEqual(log.shift(), ['get', `${thingID(7)}.refCount`, undefined]); - t.deepEqual(log.shift(), ['get', `${thingID(3)}.refCount`, '1 0']); - t.deepEqual(log.shift(), ['set', thingID(3), thingVal('thing3')]); - t.deepEqual(log.shift(), ['set', `${thingID(7)}.refCount`, '1 0']); + t.deepEqual(log.shift(), ['get', rcKey(3), undefined]); + t.deepEqual(log.shift(), ['get', esKey(3), '1']); + t.deepEqual(log.shift(), ['set', dataKey(3), thingVal('thing3')]); + t.deepEqual(log.shift(), ['set', esKey(7), '1']); t.deepEqual(log, []); await make('thing8', false, T8); // evict t4, make t8 - [t8 t7 t6 t5] - t.deepEqual(log.shift(), ['get', `${thingID(8)}.refCount`, undefined]); - t.deepEqual(log.shift(), ['get', `${thingID(4)}.refCount`, '1 0']); - t.deepEqual(log.shift(), ['set', thingID(4), thingVal('thing4')]); - t.deepEqual(log.shift(), ['set', `${thingID(8)}.refCount`, '1 0']); + t.deepEqual(log.shift(), ['get', rcKey(4), undefined]); + t.deepEqual(log.shift(), ['get', esKey(4), '1']); + t.deepEqual(log.shift(), ['set', dataKey(4), thingVal('thing4')]); + t.deepEqual(log.shift(), ['set', esKey(8), '1']); t.deepEqual(log, []); await read(T2, 'thing2'); // reanimate t2, evict t5 - [t2 t8 t7 t6] - t.deepEqual(log.shift(), ['get', thingID(2), thingVal('thing2')]); - t.deepEqual(log.shift(), ['get', `${thingID(5)}.refCount`, '1 0']); - t.deepEqual(log.shift(), ['set', thingID(5), thingVal('thing5')]); + t.deepEqual(log.shift(), ['get', dataKey(2), thingVal('thing2')]); + t.deepEqual(log.shift(), ['get', rcKey(5), undefined]); + t.deepEqual(log.shift(), ['get', esKey(5), '1']); + t.deepEqual(log.shift(), ['set', dataKey(5), thingVal('thing5')]); t.deepEqual(log, []); await readHeld('thing1'); // reanimate t1, evict t6 - [t1 t2 t8 t7] - t.deepEqual(log.shift(), ['get', thingID(1), thingVal('thing1')]); - t.deepEqual(log.shift(), ['get', `${thingID(6)}.refCount`, '1 0']); - t.deepEqual(log.shift(), ['set', thingID(6), thingVal('thing6')]); + t.deepEqual(log.shift(), ['get', dataKey(1), thingVal('thing1')]); + t.deepEqual(log.shift(), ['get', rcKey(6), undefined]); + t.deepEqual(log.shift(), ['get', esKey(6), '1']); + t.deepEqual(log.shift(), ['set', dataKey(6), thingVal('thing6')]); t.deepEqual(log, []); await write(T2, 'thing2 updated'); // refresh t2 - [t2 t1 t8 t7] @@ -348,52 +355,59 @@ test('exercise cache', async t => { t.deepEqual(log, []); await read(T6, 'thing6'); // reanimate t6, evict t2 - [t6 t7 t8 t1] - t.deepEqual(log.shift(), ['get', thingID(6), thingVal('thing6')]); - t.deepEqual(log.shift(), ['get', `${thingID(2)}.refCount`, '1 0']); - t.deepEqual(log.shift(), ['set', thingID(2), thingVal('thing2 updated')]); + t.deepEqual(log.shift(), ['get', dataKey(6), thingVal('thing6')]); + t.deepEqual(log.shift(), ['get', rcKey(2), undefined]); + t.deepEqual(log.shift(), ['get', esKey(2), '1']); + t.deepEqual(log.shift(), ['set', dataKey(2), thingVal('thing2 updated')]); t.deepEqual(log, []); await read(T5, 'thing5'); // reanimate t5, evict t1 - [t5 t6 t7 t8] - t.deepEqual(log.shift(), ['get', thingID(5), thingVal('thing5')]); - t.deepEqual(log.shift(), ['set', thingID(1), thingVal('thing1 updated')]); + t.deepEqual(log.shift(), ['get', dataKey(5), thingVal('thing5')]); + t.deepEqual(log.shift(), ['set', dataKey(1), thingVal('thing1 updated')]); t.deepEqual(log, []); await read(T4, 'thing4'); // reanimate t4, evict t8 - [t4 t5 t6 t7] - t.deepEqual(log.shift(), ['get', thingID(4), thingVal('thing4')]); - t.deepEqual(log.shift(), ['set', thingID(8), thingVal('thing8')]); + t.deepEqual(log.shift(), ['get', dataKey(4), thingVal('thing4')]); + t.deepEqual(log.shift(), ['set', dataKey(8), thingVal('thing8')]); t.deepEqual(log, []); await read(T3, 'thing3'); // reanimate t3, evict t7 - [t3 t4 t5 t6] - t.deepEqual(log.shift(), ['get', thingID(3), thingVal('thing3')]); - t.deepEqual(log.shift(), ['get', `${thingID(7)}.refCount`, '1 0']); - t.deepEqual(log.shift(), ['set', thingID(7), thingVal('thing7')]); + t.deepEqual(log.shift(), ['get', dataKey(3), thingVal('thing3')]); + t.deepEqual(log.shift(), ['get', rcKey(7), undefined]); + t.deepEqual(log.shift(), ['get', esKey(7), '1']); + t.deepEqual(log.shift(), ['set', dataKey(7), thingVal('thing7')]); t.deepEqual(log, []); await read(T2, 'thing2 updated'); // reanimate t2, evict t6 - [t2 t3 t4 t5] - t.deepEqual(log.shift(), ['get', thingID(2), thingVal('thing2 updated')]); - t.deepEqual(log.shift(), ['get', `${thingID(6)}.refCount`, '1 0']); + t.deepEqual(log.shift(), ['get', dataKey(2), thingVal('thing2 updated')]); + t.deepEqual(log.shift(), ['get', rcKey(6), undefined]); + t.deepEqual(log.shift(), ['get', esKey(6), '1']); t.deepEqual(log, []); await readHeld('thing1 updated'); // reanimate t1, evict t5 - [t1 t2 t3 t4] - t.deepEqual(log.shift(), ['get', thingID(1), thingVal('thing1 updated')]); - t.deepEqual(log.shift(), ['get', `${thingID(5)}.refCount`, '1 0']); + t.deepEqual(log.shift(), ['get', dataKey(1), thingVal('thing1 updated')]); + t.deepEqual(log.shift(), ['get', rcKey(5), undefined]); + t.deepEqual(log.shift(), ['get', esKey(5), '1']); t.deepEqual(log, []); await forgetHeld(); // cache unchanged - [t1 t2 t3 t4] - t.deepEqual(log.shift(), ['get', `${thingID(1)}.refCount`, '1 0']); + t.deepEqual(log.shift(), ['get', rcKey(1), undefined]); + t.deepEqual(log.shift(), ['get', esKey(1), '1']); t.deepEqual(log, []); await hold(T8); // cache unchanged - [t1 t2 t3 t4] - t.deepEqual(log.shift(), ['get', `${thingID(4)}.refCount`, '1 0']); + t.deepEqual(log.shift(), ['get', rcKey(4), undefined]); + t.deepEqual(log.shift(), ['get', esKey(4), '1']); t.deepEqual(log, []); await read(T7, 'thing7'); // reanimate t7, evict t4 - [t7 t1 t2 t3] - t.deepEqual(log.shift(), ['get', thingID(7), thingVal('thing7')]); - t.deepEqual(log.shift(), ['get', `${thingID(3)}.refCount`, '1 0']); + t.deepEqual(log.shift(), ['get', dataKey(7), thingVal('thing7')]); + t.deepEqual(log.shift(), ['get', rcKey(3), undefined]); + t.deepEqual(log.shift(), ['get', esKey(3), '1']); t.deepEqual(log, []); await writeHeld('thing8 updated'); // reanimate t8, evict t3 - [t8 t7 t1 t2] - t.deepEqual(log.shift(), ['get', thingID(8), thingVal('thing8')]); + t.deepEqual(log.shift(), ['get', dataKey(8), thingVal('thing8')]); t.deepEqual(log, []); }); @@ -467,10 +481,9 @@ test('virtual object gc', async t => { remainingVOs[key] = hostStorage.kvStore.get(key); } t.deepEqual(remainingVOs, { + 'v1.vs.vom.es.o+1/3': '1', 'v1.vs.vom.o+1/2': '{"label":{"body":"\\"thing #2\\"","slots":[]}}', - 'v1.vs.vom.o+1/2.refCount': '0 0', 'v1.vs.vom.o+1/3': '{"label":{"body":"\\"thing #3\\"","slots":[]}}', - 'v1.vs.vom.o+1/3.refCount': '1 0', 'v1.vs.vom.o+1/8': '{"label":{"body":"\\"thing #8\\"","slots":[]}}', 'v1.vs.vom.o+1/9': '{"label":{"body":"\\"thing #9\\"","slots":[]}}', }); diff --git a/packages/SwingSet/test/virtualObjects/test-virtualObjectGC.js b/packages/SwingSet/test/virtualObjects/test-virtualObjectGC.js new file mode 100644 index 00000000000..9a0dd3e209a --- /dev/null +++ b/packages/SwingSet/test/virtualObjects/test-virtualObjectGC.js @@ -0,0 +1,1152 @@ +import { test } from '../../tools/prepare-test-env-ava.js'; + +// eslint-disable-next-line import/order +import { Far } from '@agoric/marshal'; +import { buildSyscall, makeDispatch } from '../liveslots-helpers.js'; +import { + capargs, + makeMessage, + makeDropExports, + makeRetireImports, + makeRetireExports, +} from '../util.js'; + +// Legs: +// +// Possible retainers of a VO (ensuring continued reachability): +// L: variable in local memory +// E: export to kernel (reachable) +// V: virtual object property (vprop) state +// Additionally, the kernel may remember a VO (ensuring continued recognizability) +// R: recognizable to kernel +// +// We denote the presence of a leg these via its letter in upper case, its +// absence by the letter in lower case, and "don't care" by ".". +// +// In principle there are 2^4 = 16 conceivable states, but not all are possible. +// States .er.., ..ER.., and .eR.. are possible, but .Er.. is not, since +// reachability always implies recognizability, which reduces the number of +// states to 12. +// +// In addition, any transition into the state leRv implies the loss of +// reachability, which results in the issuance of a `retireExports` syscall, +// resulting in turn to an immediate and automatic transition into state +// lerv. Thus, for purposes of analysis of state transitions driven by events +// *external* to the VO garbage collection machinery, the state leRv does not +// exist and so the state diagram in the model here has 11 states. +// +// The initial state is lerv, which is essentially the state of non-existence. +// The act of creation yields the first local reference to the object and thus +// the first state transition is always to Lerv. +// +// When the state reaches le.v the VO is no longer reachable and may be garbage +// collected. +// +// When the state reaches lerv the VO is no longer recognizable anywhere and +// any weak collection entries that use the VO as a key should be removed. + +// There may be more than one local reference, hence L subsumes all states with +// 1 or more, whereas l implies there are 0. Detection of the transition from L +// to l is handled by a JS finalizer. + +// There may be more than one vprop reference, hence V subsumes all states with +// 1 or more, whereas v implies there are 0. The number of vprop references to +// a virtual object is tracked via explicit reference counting. + +// The transitions from E to e and R to r happen as the result of explicit +// deliveries (dropExport and retireExport respectively) from the kernel. (The +// retireExport syscall does not result in a transition but rather informs the +// kernel of the consequences of a state transition that resulted in loss of +// recognizability.) + +// The possible state transitions are: +// lerv -create-> Lerv (creation) [1] +// Ler. -export-> LER. (export) [2] +// L..v -vstore-> L..V (store in vprop) [3] +// +// L... -droplr-> l... (drop local reference) [6] +// lER. -delivr-> LER. (reacquire local reference via delivery) [2] +// l..V -readvp-> L..V (reacquire local reference via read from vprop) [3] +// +// .ER. -dropex-> .eR. (d.dropExport) [4] +// .eR. -retexp-> .er. (d.retireExport) [3] +// +// ...V -overwr-> ...v (overwrite last vprop) [6] +// +// While in the above notation "." denotes "don't care", the legs not cared +// about are always the same in the before and after states of each state +// transition, hence each of the transition patterns above represents 2^N +// transitions, where N is the number of dots on either side of the arrow +// (minus, once again, the excluded Er states). Since each of these 2^N +// transitions represents a potentially different path through the code, test +// cases must be constructed that exercise each of them. Although there's a +// total of 30 of state transitions, testing each requires setting up the before +// state, which can be the after state of some earlier transition being tested, +// thus the actual tests consist of a smaller number of manually constructed +// (and somewhat ad hoc) paths through state space that collectively hit all the +// possible state transitions. + +// Any transition to lerv from L... or .E.. or ...V should trigger the garbage +// collection of the virtual object. + +// Any Transition to lerv (from ..R.) should trigger retirement of the virtual +// object's identity. + +// lerv -create-> Lerv + +// Lerv -export-> LERv +// LerV -export-> LERV + +// Lerv -vstore-> LerV +// LeRv -vstore-> LeRV +// LERv -vstore-> LERV + +// Lerv -droplr-> lerv gc +// LerV -droplr-> lerV +// LeRv -droplr-> lerv gc, ret +// LeRV -droplr-> leRV +// LERv -droplr-> lERv +// LERV -droplr-> lERV + +// lERv -delivr-> LERv +// lERV -delivr-> LERV + +// lerV -readvp-> LerV +// leRV -readvp-> LeRV +// lERV -readvp-> LERV + +// lERv -dropex-> lerv gc, ret +// lERV -dropex-> leRV +// LERv -dropex-> LeRv +// LERV -dropex-> LeRV + +// leRV -retexp-> lerV +// LeRv -retexp-> Lerv +// LeRV -retexp-> LerV + +// lerV -overwr-> lerv gc +// leRV -overwr-> lerv gc, ret +// LerV -overwr-> Lerv +// LeRV -overwr-> LeRv +// lERV -overwr-> lERv +// LERV -overwr-> LERv + +let aWeakMap; +let aWeakSet; + +function buildRootObject(vatPowers) { + const { makeKind, WeakMap, WeakSet } = vatPowers; + function makeThingInstance(state) { + return { + init(label) { + state.label = label; + }, + self: Far('thing', { + getLabel() { + return state.label; + }, + }), + }; + } + + function makeVirtualHolderInstance(state) { + return { + init(value = null) { + state.held = value; + }, + self: Far('holder', { + setValue(value) { + state.held = value; + }, + getValue() { + return state.held; + }, + }), + }; + } + + const thingMaker = makeKind(makeThingInstance); + const cacheDisplacer = thingMaker('cacheDisplacer'); + const virtualHolderMaker = makeKind(makeVirtualHolderInstance); + const virtualHolder = virtualHolderMaker(); + let nextThingNumber = 0; + let heldThing = null; + aWeakMap = new WeakMap(); + aWeakSet = new WeakSet(); + + const holders = []; + + function displaceCache() { + return cacheDisplacer.getLabel(); + } + + function makeNextThing() { + const thing = thingMaker(`thing #${nextThingNumber}`); + nextThingNumber += 1; + return thing; + } + + return Far('root', { + makeAndHold() { + heldThing = makeNextThing(); + displaceCache(); + }, + makeAndHoldAndKey() { + heldThing = makeNextThing(); + aWeakMap.set(heldThing, 'arbitrary'); + aWeakSet.add(heldThing); + displaceCache(); + }, + makeAndHoldRemotable() { + heldThing = Far('thing', {}); + displaceCache(); + }, + dropHeld() { + heldThing = null; + displaceCache(); + }, + storeHeld() { + virtualHolder.setValue(heldThing); + displaceCache(); + }, + dropStored() { + virtualHolder.setValue(null); + displaceCache(); + }, + fetchAndHold() { + heldThing = virtualHolder.getValue(); + displaceCache(); + }, + exportHeld() { + return heldThing; + }, + importAndHold(thing) { + heldThing = thing; + displaceCache(); + }, + importAndHoldAndKey(thing) { + heldThing = thing; + aWeakMap.set(heldThing, 'arbitrary'); + aWeakSet.add(heldThing); + displaceCache(); + }, + + prepareStore3() { + holders.push(virtualHolderMaker(heldThing)); + holders.push(virtualHolderMaker(heldThing)); + holders.push(virtualHolderMaker(heldThing)); + heldThing = null; + displaceCache(); + }, + finishClearHolders() { + for (let i = 0; i < holders.length; i += 1) { + holders[i].setValue(null); + } + displaceCache(); + }, + finishDropHolders() { + for (let i = 0; i < holders.length; i += 1) { + holders[i] = null; + } + displaceCache(); + }, + prepareStoreLinked() { + let holder = virtualHolderMaker(heldThing); + holder = virtualHolderMaker(holder); + holder = virtualHolderMaker(holder); + holders.push(holder); + heldThing = null; + displaceCache(); + }, + noOp() { + // used when an extra cycle is needed to pump GC + }, + }); +} + +function makeRPMaker() { + let idx = 0; + return () => { + idx += 1; + return `p-${idx}`; + }; +} + +function capdata(data, slots = []) { + return { body: JSON.stringify(data), slots }; +} + +const undefinedVal = capargs({ '@qclass': 'undefined' }); + +function thingRef(vref) { + if (vref) { + return capargs({ '@qclass': 'slot', iface: 'Alleged: thing', index: 0 }, [ + vref, + ]); + } else { + return capargs(null, []); + } +} + +function holderRef(vref) { + if (vref) { + return capargs({ '@qclass': 'slot', iface: 'Alleged: holder', index: 0 }, [ + vref, + ]); + } else { + return capargs(null, []); + } +} + +function thingArg(vref) { + return capargs( + [{ '@qclass': 'slot', iface: 'Alleged: thing', index: 0 }], + [vref], + ); +} + +function thingValue(label) { + return JSON.stringify({ label: capdata(label) }); +} + +function heldThingValue(vref) { + return JSON.stringify({ held: thingRef(vref) }); +} + +function heldHolderValue(vref) { + return JSON.stringify({ held: holderRef(vref) }); +} + +function matchResolveOne(vref, value) { + return { type: 'resolve', resolutions: [[vref, false, value]] }; +} + +function matchVatstoreGet(key) { + return { type: 'vatstoreGet', key }; +} + +function matchVatstoreDelete(key) { + return { type: 'vatstoreDelete', key }; +} + +function matchVatstoreSet(key, value) { + return { type: 'vatstoreSet', key, value }; +} + +function matchRetireExports(...slots) { + return { type: 'retireExports', slots }; +} + +function matchDropImports(...slots) { + return { type: 'dropImports', slots }; +} + +function matchRetireImports(...slots) { + return { type: 'retireImports', slots }; +} + +const root = 'o+0'; + +const testObjValue = thingValue('thing #0'); +const cacheObjValue = thingValue('cacheDisplacer'); + +function setupLifecycleTest(t) { + const { log, syscall } = buildSyscall(); + const nextRP = makeRPMaker(); + const th = []; + const dispatch = makeDispatch(syscall, buildRootObject, 'bob', false, 0, th); + const [testHooks] = th; + + async function dispatchMessage(message, args = capargs([])) { + const rp = nextRP(); + await dispatch(makeMessage(root, message, args, rp)); + return rp; + } + async function dispatchDropExports(...vrefs) { + await dispatch(makeDropExports(...vrefs)); + } + async function dispatchRetireImports(...vrefs) { + await dispatch(makeRetireImports(...vrefs)); + } + async function dispatchRetireExports(...vrefs) { + await dispatch(makeRetireExports(...vrefs)); + } + + const v = { t, log }; + + return { + v, + dispatchMessage, + dispatchDropExports, + dispatchRetireExports, + dispatchRetireImports, + testHooks, + }; +} + +function validate(v, match) { + v.t.deepEqual(v.log.shift(), match); +} + +function validateDone(v) { + v.t.deepEqual(v.log, []); +} + +function validateReturned(v, rp) { + validate(v, matchResolveOne(rp, undefinedVal)); +} + +function validateDelete(v, vobjID) { + validate(v, matchVatstoreDelete(`vom.${vobjID}`)); + validate(v, matchVatstoreDelete(`vom.rc.${vobjID}`)); + validate(v, matchVatstoreDelete(`vom.es.${vobjID}`)); +} + +function validateStatusCheck(v, vobjID) { + validate(v, matchVatstoreGet(`vom.rc.${vobjID}`)); + validate(v, matchVatstoreGet(`vom.es.${vobjID}`)); + validate(v, matchVatstoreGet(`vom.${vobjID}`)); +} + +function validateCreate(v, rp) { + validate(v, matchVatstoreSet('vom.o+1/1', cacheObjValue)); + validate(v, matchVatstoreSet('vom.o+2/1', heldThingValue(null))); + validate(v, matchVatstoreSet('vom.o+1/2', testObjValue)); + validate(v, matchVatstoreGet('vom.o+1/1')); + validateReturned(v, rp); + validateDone(v); +} + +function validateStore(v, rp) { + validate(v, matchVatstoreGet('vom.o+2/1')); + validate(v, matchVatstoreGet('vom.rc.o+1/2')); + validate(v, matchVatstoreSet('vom.rc.o+1/2', '1')); + validate(v, matchVatstoreSet('vom.o+2/1', heldThingValue('o+1/2'))); + validate(v, matchVatstoreGet('vom.o+1/1')); + validateReturned(v, rp); + validateDone(v); +} + +function validateDropStore(v, rp, postCheck) { + validate(v, matchVatstoreGet('vom.o+2/1')); + validate(v, matchVatstoreGet('vom.rc.o+1/2')); + validate(v, matchVatstoreSet('vom.rc.o+1/2', '0')); + validate(v, matchVatstoreSet('vom.o+2/1', heldThingValue(null))); + validate(v, matchVatstoreGet('vom.o+1/1')); + validateReturned(v, rp); + if (postCheck) { + validate(v, matchVatstoreGet('vom.rc.o+1/2')); + validate(v, matchVatstoreGet('vom.es.o+1/2')); + } + validateDone(v); +} + +function validateDropStoreAndRetire(v, rp) { + validate(v, matchVatstoreGet('vom.o+2/1')); + validate(v, matchVatstoreGet('vom.rc.o+1/2')); + validate(v, matchVatstoreSet('vom.rc.o+1/2', '0')); + validate(v, matchVatstoreSet('vom.o+2/1', heldThingValue(null))); + validate(v, matchVatstoreGet('vom.o+1/1')); + validateReturned(v, rp); + validateStatusCheck(v, 'o+1/2'); + validateDelete(v, 'o+1/2'); + validateDone(v); +} + +function validateDropStoreWithGCAndRetire(v, rp) { + validate(v, matchVatstoreGet('vom.o+2/1')); + validate(v, matchVatstoreGet('vom.rc.o+1/2')); + validate(v, matchVatstoreSet('vom.rc.o+1/2', '0')); + validate(v, matchVatstoreSet('vom.o+2/1', heldThingValue(null))); + validate(v, matchVatstoreGet('vom.o+1/1')); + validateReturned(v, rp); + validateStatusCheck(v, 'o+1/2'); + validateDelete(v, 'o+1/2'); + validate(v, matchRetireExports('o+1/2')); + validateDone(v); +} + +function validateExport(v, rp) { + validate(v, matchVatstoreSet('vom.es.o+1/2', '1')); + validate(v, matchResolveOne(rp, thingRef('o+1/2'))); + validateDone(v); +} + +function validateImport(v, rp) { + validate(v, matchVatstoreGet('vom.o+1/1')); + validateReturned(v, rp); + validateDone(v); +} + +function validateLoad(v, rp) { + validate(v, matchVatstoreGet('vom.o+2/1')); + validate(v, matchVatstoreGet('vom.o+1/1')); + validateReturned(v, rp); + validateDone(v); +} + +function validateDropHeld(v, rp) { + validateReturned(v, rp); + validate(v, matchVatstoreGet('vom.rc.o+1/2')); + validate(v, matchVatstoreGet('vom.es.o+1/2')); + validateDone(v); +} + +function validateDropHeldWithGC(v, rp) { + validateReturned(v, rp); + validateStatusCheck(v, 'o+1/2'); + validateDelete(v, 'o+1/2'); + validateDone(v); +} + +function validateDropHeldWithGCAndRetire(v, rp) { + validateReturned(v, rp); + validateStatusCheck(v, 'o+1/2'); + validateDelete(v, 'o+1/2'); + validate(v, matchRetireExports('o+1/2')); + validateDone(v); +} + +function validateDropExport(v) { + validate(v, matchVatstoreSet('vom.es.o+1/2', '0')); + validate(v, matchVatstoreGet('vom.rc.o+1/2')); + validateDone(v); +} + +function validateDropExportWithGCAndRetire(v) { + validate(v, matchVatstoreSet('vom.es.o+1/2', '0')); + validate(v, matchVatstoreGet('vom.rc.o+1/2')); + validateStatusCheck(v, 'o+1/2'); + validateDelete(v, 'o+1/2'); + validate(v, matchRetireExports('o+1/2')); + validateDone(v); +} + +function validateRetireExport(v) { + validate(v, matchVatstoreDelete('vom.es.o+1/2')); + validateDone(v); +} + +// NOTE: these tests must be run serially, since they share a heap and garbage +// collection during one test can interfere with the deterministic behavior of a +// different test. + +// test 1: lerv -> Lerv -> LerV -> Lerv -> lerv +test.serial('VO lifecycle 1', async t => { + const { v, dispatchMessage } = setupLifecycleTest(t); + + // lerv -> Lerv Create VO + let rp = await dispatchMessage('makeAndHold'); + validateCreate(v, rp); + + // Lerv -> LerV Store VO reference virtually + rp = await dispatchMessage('storeHeld'); + validateStore(v, rp); + + // LerV -> Lerv Overwrite virtual reference + rp = await dispatchMessage('dropStored'); + validateDropStore(v, rp, false); + + // Lerv -> lerv Drop in-memory reference, unreferenced VO gets GC'd + rp = await dispatchMessage('dropHeld'); + validateDropHeldWithGC(v, rp); +}); + +// test 2: lerv -> Lerv -> LerV -> lerV -> LerV -> LERV -> lERV -> LERV -> +// lERV -> LERV -> lERV -> leRV -> LeRV -> leRV -> LeRV -> LerV +test.serial('VO lifecycle 2', async t => { + const { + v, + dispatchMessage, + dispatchDropExports, + dispatchRetireExports, + } = setupLifecycleTest(t); + + // lerv -> Lerv Create VO + let rp = await dispatchMessage('makeAndHold'); + validateCreate(v, rp); + + // Lerv -> LerV Store VO reference virtually (permanent for now) + rp = await dispatchMessage('storeHeld'); + validateStore(v, rp); + + // LerV -> lerV Drop in-memory reference, no GC because virtual reference + rp = await dispatchMessage('dropHeld'); + validateDropHeld(v, rp); + + // lerV -> LerV Read virtual reference, now there's an in-memory reference again too + rp = await dispatchMessage('fetchAndHold'); + validateLoad(v, rp); + + // LerV -> LERV Export the reference, now all three legs hold it + rp = await dispatchMessage('exportHeld'); + validateExport(v, rp); + + // LERV -> lERV Drop the in-memory reference again, but it's still exported and virtual referenced + rp = await dispatchMessage('dropHeld'); + validateDropHeld(v, rp); + + // lERV -> LERV Reread from storage, all three legs again + rp = await dispatchMessage('fetchAndHold'); + validateLoad(v, rp); + + // LERV -> lERV Drop in-memory reference (stepping stone to other states) + rp = await dispatchMessage('dropHeld'); + validateDropHeld(v, rp); + + // lERV -> LERV Reintroduce the in-memory reference via message + rp = await dispatchMessage('importAndHold', thingArg('o+1/2')); + validateImport(v, rp); + + // LERV -> lERV Drop in-memory reference + rp = await dispatchMessage('dropHeld'); + validateDropHeld(v, rp); + + // lERV -> leRV Drop the export + await dispatchDropExports('o+1/2'); + validateDropExport(v); + + // leRV -> LeRV Fetch from storage + rp = await dispatchMessage('fetchAndHold'); + validateLoad(v, rp); + + // LeRV -> leRV Forget about it *again* + rp = await dispatchMessage('dropHeld'); + validateDropHeld(v, rp); + + // leRV -> LeRV Fetch from storage *again* + rp = await dispatchMessage('fetchAndHold'); + validateLoad(v, rp); + + // LeRV -> LerV Retire the export + await dispatchRetireExports('o+1/2'); + validateRetireExport(v); +}); + +// test 3: lerv -> Lerv -> LerV -> LERV -> LeRV -> leRV -> lerV -> lerv +test.serial('VO lifecycle 3', async t => { + const { + v, + dispatchMessage, + dispatchDropExports, + dispatchRetireExports, + } = setupLifecycleTest(t); + + // lerv -> Lerv Create VO + let rp = await dispatchMessage('makeAndHold'); + validateCreate(v, rp); + + // Lerv -> LerV Store VO reference virtually (permanent for now) + rp = await dispatchMessage('storeHeld'); + validateStore(v, rp); + + // LerV -> LERV Export the reference, now all three legs hold it + rp = await dispatchMessage('exportHeld'); + validateExport(v, rp); + + // LERV -> LeRV Drop the export + await dispatchDropExports('o+1/2'); + validateDropExport(v); + + // LeRV -> leRV Drop in-memory reference + rp = await dispatchMessage('dropHeld'); + validateDropHeld(v, rp); + + // leRV -> lerV Retire the export + await dispatchRetireExports('o+1/2'); + validateRetireExport(v); + + // lerV -> lerv Drop stored reference (gc and retire) + rp = await dispatchMessage('dropStored'); + validateDropStoreAndRetire(v, rp); +}); + +// test 4: lerv -> Lerv -> LERv -> LeRv -> lerv +test.serial('VO lifecycle 4', async t => { + const { v, dispatchMessage, dispatchDropExports } = setupLifecycleTest(t); + + // lerv -> Lerv Create VO + let rp = await dispatchMessage('makeAndHold'); + validateCreate(v, rp); + + // Lerv -> LERv Export the reference, now all three legs hold it + rp = await dispatchMessage('exportHeld'); + validateExport(v, rp); + + // LERv -> LeRv Drop the export + await dispatchDropExports('o+1/2'); + validateDropExport(v); + + // LeRv -> lerv Drop in-memory reference (gc and retire) + rp = await dispatchMessage('dropHeld'); + validateDropHeldWithGCAndRetire(v, rp); +}); + +// test 5: lerv -> Lerv -> LERv -> LeRv -> Lerv -> lerv +test.serial('VO lifecycle 5', async t => { + const { + v, + dispatchMessage, + dispatchDropExports, + dispatchRetireExports, + } = setupLifecycleTest(t); + + // lerv -> Lerv Create VO + let rp = await dispatchMessage('makeAndHold'); + validateCreate(v, rp); + + // Lerv -> LERv Export the reference, now all three legs hold it + rp = await dispatchMessage('exportHeld'); + validateExport(v, rp); + + // LERv -> LeRv Drop the export + await dispatchDropExports('o+1/2'); + validateDropExport(v); + + // LeRv -> Lerv Retire the export + await dispatchRetireExports('o+1/2'); + validateRetireExport(v); + + // Lerv -> lerv Drop in-memory reference, unreferenced VO gets GC'd + rp = await dispatchMessage('dropHeld'); + validateDropHeldWithGC(v, rp); +}); + +// test 6: lerv -> Lerv -> LERv -> LeRv -> LeRV -> LeRv -> LeRV -> leRV -> lerv +test.serial('VO lifecycle 6', async t => { + const { v, dispatchMessage, dispatchDropExports } = setupLifecycleTest(t); + + // lerv -> Lerv Create VO + let rp = await dispatchMessage('makeAndHold'); + validateCreate(v, rp); + + // Lerv -> LERv Export the reference, now all three legs hold it + rp = await dispatchMessage('exportHeld'); + validateExport(v, rp); + + // LERv -> LeRv Drop the export + await dispatchDropExports('o+1/2'); + validateDropExport(v); + + // LeRv -> LeRV Store VO reference virtually + rp = await dispatchMessage('storeHeld'); + validateStore(v, rp); + + // LeRV -> LeRv Overwrite virtual reference + rp = await dispatchMessage('dropStored'); + validateDropStore(v, rp, false); + + // LeRv -> LeRV Store VO reference virtually again + rp = await dispatchMessage('storeHeld'); + validateStore(v, rp); + + // LeRV -> leRV Drop in-memory reference + rp = await dispatchMessage('dropHeld'); + validateDropHeld(v, rp); + + // leRV -> lerv Drop stored reference (gc and retire) + rp = await dispatchMessage('dropStored'); + validateDropStoreWithGCAndRetire(v, rp); +}); + +// test 7: lerv -> Lerv -> LERv -> lERv -> LERv -> lERv -> lerv +test.serial('VO lifecycle 7', async t => { + const { v, dispatchMessage, dispatchDropExports } = setupLifecycleTest(t); + + // lerv -> Lerv Create VO + let rp = await dispatchMessage('makeAndHold'); + validateCreate(v, rp); + + // Lerv -> LERv Export the reference, now all three legs hold it + rp = await dispatchMessage('exportHeld'); + validateExport(v, rp); + + // LERv -> lERv Drop in-memory reference, no GC because exported + rp = await dispatchMessage('dropHeld'); + validateDropHeld(v, rp); + + // lERv -> LERv Reintroduce the in-memory reference via message + rp = await dispatchMessage('importAndHold', thingArg('o+1/2')); + validateImport(v, rp); + + // LERv -> lERv Drop in-memory reference again, still no GC because exported + rp = await dispatchMessage('dropHeld'); + validateDropHeld(v, rp); + + // lERv -> lerv Drop the export (gc and retire) + await dispatchDropExports('o+1/2'); + validateDropExportWithGCAndRetire(v); +}); + +// test 8: lerv -> Lerv -> LERv -> LERV -> LERv -> LERV -> lERV -> lERv -> lerv +test.serial('VO lifecycle 8', async t => { + const { v, dispatchMessage, dispatchDropExports } = setupLifecycleTest(t); + + // lerv -> Lerv Create VO + let rp = await dispatchMessage('makeAndHold'); + validateCreate(v, rp); + + // Lerv -> LERv Export the reference + rp = await dispatchMessage('exportHeld'); + validateExport(v, rp); + + // LERv -> LERV Store VO reference virtually + rp = await dispatchMessage('storeHeld'); + validateStore(v, rp); + + // LERV -> LERv Overwrite virtual reference + rp = await dispatchMessage('dropStored'); + validateDropStore(v, rp, false); + + // LERv -> LERV Store VO reference virtually + rp = await dispatchMessage('storeHeld'); + validateStore(v, rp); + + // LERV -> lERV Drop the in-memory reference + rp = await dispatchMessage('dropHeld'); + validateDropHeld(v, rp); + + // lERV -> lERv Overwrite virtual reference + rp = await dispatchMessage('dropStored'); + validateDropStore(v, rp, true); + + // lERv -> lerv Drop the export (gc and retire) + await dispatchDropExports('o+1/2'); + validateDropExportWithGCAndRetire(v); +}); + +function validatePrepareStore3(v, rp) { + validate(v, matchVatstoreGet('vom.rc.o+1/2')); + validate(v, matchVatstoreSet('vom.rc.o+1/2', '1')); + validate(v, matchVatstoreSet('vom.o+2/2', heldThingValue('o+1/2'))); + validate(v, matchVatstoreGet('vom.rc.o+1/2')); + validate(v, matchVatstoreSet('vom.rc.o+1/2', '2')); + validate(v, matchVatstoreSet('vom.o+2/3', heldThingValue('o+1/2'))); + validate(v, matchVatstoreGet('vom.rc.o+1/2')); + validate(v, matchVatstoreSet('vom.rc.o+1/2', '3')); + validate(v, matchVatstoreSet('vom.o+2/4', heldThingValue('o+1/2'))); + validate(v, matchVatstoreGet('vom.o+1/1')); + validateReturned(v, rp); + validate(v, matchVatstoreGet('vom.rc.o+1/2')); + validate(v, matchVatstoreGet('vom.es.o+1/2')); + validateDone(v); +} + +test.serial('VO refcount management 1', async t => { + const { v, dispatchMessage } = setupLifecycleTest(t); + + let rp = await dispatchMessage('makeAndHold'); + validateCreate(v, rp); + + rp = await dispatchMessage('prepareStore3'); + validatePrepareStore3(v, rp); + + rp = await dispatchMessage('finishClearHolders'); + validate(v, matchVatstoreGet('vom.o+2/2')); + validate(v, matchVatstoreGet('vom.rc.o+1/2')); + validate(v, matchVatstoreSet('vom.rc.o+1/2', '2')); + validate(v, matchVatstoreSet('vom.o+2/2', heldThingValue(null))); + validate(v, matchVatstoreGet('vom.o+2/3')); + validate(v, matchVatstoreGet('vom.rc.o+1/2')); + validate(v, matchVatstoreSet('vom.rc.o+1/2', '1')); + validate(v, matchVatstoreSet('vom.o+2/3', heldThingValue(null))); + validate(v, matchVatstoreGet('vom.o+2/4')); + validate(v, matchVatstoreGet('vom.rc.o+1/2')); + validate(v, matchVatstoreSet('vom.rc.o+1/2', '0')); + validate(v, matchVatstoreSet('vom.o+2/4', heldThingValue(null))); + validate(v, matchVatstoreGet('vom.o+1/1')); + validateReturned(v, rp); + validateStatusCheck(v, 'o+1/2'); + validateDelete(v, 'o+1/2'); + validateDone(v); +}); + +test.serial('VO refcount management 2', async t => { + const { v, dispatchMessage } = setupLifecycleTest(t); + + let rp = await dispatchMessage('makeAndHold'); + validateCreate(v, rp); + + rp = await dispatchMessage('prepareStore3'); + validatePrepareStore3(v, rp); + + rp = await dispatchMessage('finishDropHolders'); + validateReturned(v, rp); + + validateStatusCheck(v, 'o+2/2'); + validate(v, matchVatstoreGet('vom.rc.o+1/2')); + validate(v, matchVatstoreSet('vom.rc.o+1/2', '2')); + validateDelete(v, 'o+2/2'); + + validateStatusCheck(v, 'o+2/3'); + validate(v, matchVatstoreGet('vom.rc.o+1/2')); + validate(v, matchVatstoreSet('vom.rc.o+1/2', '1')); + validateDelete(v, 'o+2/3'); + + validateStatusCheck(v, 'o+2/4'); + validate(v, matchVatstoreGet('vom.rc.o+1/2')); + validate(v, matchVatstoreSet('vom.rc.o+1/2', '0')); + + validateDelete(v, 'o+2/4'); + + validateStatusCheck(v, 'o+1/2'); + validateDelete(v, 'o+1/2'); + + validateDone(v); +}); + +test.serial('VO refcount management 3', async t => { + const { v, dispatchMessage } = setupLifecycleTest(t); + + let rp = await dispatchMessage('makeAndHold'); + validateCreate(v, rp); + + rp = await dispatchMessage('prepareStoreLinked'); + validate(v, matchVatstoreGet('vom.rc.o+1/2')); + validate(v, matchVatstoreSet('vom.rc.o+1/2', '1')); + validate(v, matchVatstoreSet('vom.o+2/2', heldThingValue('o+1/2'))); + validate(v, matchVatstoreGet('vom.rc.o+2/2')); + validate(v, matchVatstoreSet('vom.rc.o+2/2', '1')); + validate(v, matchVatstoreSet('vom.o+2/3', heldHolderValue('o+2/2'))); + validate(v, matchVatstoreGet('vom.rc.o+2/3')); + validate(v, matchVatstoreSet('vom.rc.o+2/3', '1')); + validate(v, matchVatstoreSet('vom.o+2/4', heldHolderValue('o+2/3'))); + validate(v, matchVatstoreGet('vom.o+1/1')); + validateReturned(v, rp); + validate(v, matchVatstoreGet('vom.rc.o+1/2')); + validate(v, matchVatstoreGet('vom.es.o+1/2')); + validate(v, matchVatstoreGet('vom.rc.o+2/2')); + validate(v, matchVatstoreGet('vom.es.o+2/2')); + validate(v, matchVatstoreGet('vom.rc.o+2/3')); + validate(v, matchVatstoreGet('vom.es.o+2/3')); + validateDone(v); + + rp = await dispatchMessage('finishDropHolders'); + validateReturned(v, rp); + validateStatusCheck(v, 'o+2/4'); + validate(v, matchVatstoreGet('vom.rc.o+2/3')); + validate(v, matchVatstoreSet('vom.rc.o+2/3', '0')); + + validateDelete(v, 'o+2/4'); + + validateStatusCheck(v, 'o+2/3'); + validate(v, matchVatstoreGet('vom.rc.o+2/2')); + validate(v, matchVatstoreSet('vom.rc.o+2/2', '0')); + validateDelete(v, 'o+2/3'); + + validateStatusCheck(v, 'o+2/2'); + validate(v, matchVatstoreGet('vom.rc.o+1/2')); + validate(v, matchVatstoreSet('vom.rc.o+1/2', '0')); + validateDelete(v, 'o+2/2'); + + validateStatusCheck(v, 'o+1/2'); + validateDelete(v, 'o+1/2'); + + validateDone(v); +}); + +test.serial('presence refcount management 1', async t => { + const { v, dispatchMessage } = setupLifecycleTest(t); + + let rp = await dispatchMessage('importAndHold', thingArg('o-5')); + validate(v, matchVatstoreSet('vom.o+1/1', cacheObjValue)); + validate(v, matchVatstoreSet('vom.o+2/1', heldThingValue(null))); + validate(v, matchVatstoreGet('vom.o+1/1')); + validateReturned(v, rp); + validateDone(v); + + rp = await dispatchMessage('prepareStore3'); + validate(v, matchVatstoreGet('vom.rc.o-5')); + validate(v, matchVatstoreSet('vom.rc.o-5', '1')); + validate(v, matchVatstoreSet('vom.o+2/2', heldThingValue('o-5'))); + validate(v, matchVatstoreGet('vom.rc.o-5')); + validate(v, matchVatstoreSet('vom.rc.o-5', '2')); + validate(v, matchVatstoreSet('vom.o+2/3', heldThingValue('o-5'))); + validate(v, matchVatstoreGet('vom.rc.o-5')); + validate(v, matchVatstoreSet('vom.rc.o-5', '3')); + validate(v, matchVatstoreSet('vom.o+2/4', heldThingValue('o-5'))); + validate(v, matchVatstoreGet('vom.o+1/1')); + validateReturned(v, rp); + validate(v, matchVatstoreGet('vom.rc.o-5')); + validateDone(v); + + rp = await dispatchMessage('finishClearHolders'); + validate(v, matchVatstoreGet('vom.o+2/2')); + validate(v, matchVatstoreGet('vom.rc.o-5')); + validate(v, matchVatstoreSet('vom.rc.o-5', '2')); + validate(v, matchVatstoreSet('vom.o+2/2', heldThingValue(null))); + validate(v, matchVatstoreGet('vom.o+2/3')); + validate(v, matchVatstoreGet('vom.rc.o-5')); + validate(v, matchVatstoreSet('vom.rc.o-5', '1')); + validate(v, matchVatstoreSet('vom.o+2/3', heldThingValue(null))); + validate(v, matchVatstoreGet('vom.o+2/4')); + validate(v, matchVatstoreGet('vom.rc.o-5')); + validate(v, matchVatstoreSet('vom.rc.o-5', '0')); + validate(v, matchVatstoreDelete('vom.rc.o-5')); + validate(v, matchVatstoreSet('vom.o+2/4', heldThingValue(null))); + validate(v, matchVatstoreGet('vom.o+1/1')); + validateReturned(v, rp); + validate(v, matchVatstoreGet('vom.rc.o-5')); + validate(v, matchDropImports('o-5')); + validate(v, matchRetireImports('o-5')); + validateDone(v); +}); + +test.serial('presence refcount management 2', async t => { + const { v, dispatchMessage } = setupLifecycleTest(t); + + let rp = await dispatchMessage('importAndHold', thingArg('o-5')); + validate(v, matchVatstoreSet('vom.o+1/1', cacheObjValue)); + validate(v, matchVatstoreSet('vom.o+2/1', heldThingValue(null))); + validate(v, matchVatstoreGet('vom.o+1/1')); + validateReturned(v, rp); + validateDone(v); + + rp = await dispatchMessage('prepareStore3'); + validate(v, matchVatstoreGet('vom.rc.o-5')); + validate(v, matchVatstoreSet('vom.rc.o-5', '1')); + validate(v, matchVatstoreSet('vom.o+2/2', heldThingValue('o-5'))); + validate(v, matchVatstoreGet('vom.rc.o-5')); + validate(v, matchVatstoreSet('vom.rc.o-5', '2')); + validate(v, matchVatstoreSet('vom.o+2/3', heldThingValue('o-5'))); + validate(v, matchVatstoreGet('vom.rc.o-5')); + validate(v, matchVatstoreSet('vom.rc.o-5', '3')); + validate(v, matchVatstoreSet('vom.o+2/4', heldThingValue('o-5'))); + validate(v, matchVatstoreGet('vom.o+1/1')); + validateReturned(v, rp); + validate(v, matchVatstoreGet('vom.rc.o-5')); + validateDone(v); + + rp = await dispatchMessage('finishDropHolders'); + validateReturned(v, rp); + validateStatusCheck(v, 'o+2/2'); + validate(v, matchVatstoreGet('vom.rc.o-5')); + validate(v, matchVatstoreSet('vom.rc.o-5', '2')); + validateDelete(v, 'o+2/2'); + validateStatusCheck(v, 'o+2/3'); + validate(v, matchVatstoreGet('vom.rc.o-5')); + validate(v, matchVatstoreSet('vom.rc.o-5', '1')); + validateDelete(v, 'o+2/3'); + validateStatusCheck(v, 'o+2/4'); + validate(v, matchVatstoreGet('vom.rc.o-5')); + validate(v, matchVatstoreSet('vom.rc.o-5', '0')); + validate(v, matchVatstoreDelete('vom.rc.o-5')); + validateDelete(v, 'o+2/4'); + validate(v, matchVatstoreGet('vom.rc.o-5')); + validate(v, matchDropImports('o-5')); + validate(v, matchRetireImports('o-5')); + validateDone(v); +}); + +test.serial('remotable refcount management 1', async t => { + const { v, dispatchMessage } = setupLifecycleTest(t); + + let rp = await dispatchMessage('makeAndHoldRemotable'); + validate(v, matchVatstoreSet('vom.o+1/1', cacheObjValue)); + validate(v, matchVatstoreSet('vom.o+2/1', heldThingValue(null))); + validate(v, matchVatstoreGet('vom.o+1/1')); + validateReturned(v, rp); + validateDone(v); + + rp = await dispatchMessage('prepareStore3'); + validate(v, matchVatstoreSet('vom.o+2/2', heldThingValue('o+3'))); + validate(v, matchVatstoreSet('vom.o+2/3', heldThingValue('o+3'))); + validate(v, matchVatstoreSet('vom.o+2/4', heldThingValue('o+3'))); + validate(v, matchVatstoreGet('vom.o+1/1')); + validateReturned(v, rp); + validateDone(v); + + rp = await dispatchMessage('finishClearHolders'); + validate(v, matchVatstoreGet('vom.o+2/2')); + validate(v, matchVatstoreSet('vom.o+2/2', heldThingValue(null))); + validate(v, matchVatstoreGet('vom.o+2/3')); + validate(v, matchVatstoreSet('vom.o+2/3', heldThingValue(null))); + validate(v, matchVatstoreGet('vom.o+2/4')); + validate(v, matchVatstoreSet('vom.o+2/4', heldThingValue(null))); + validate(v, matchVatstoreGet('vom.o+1/1')); + validateReturned(v, rp); + validateDone(v); +}); + +test.serial('remotable refcount management 2', async t => { + const { v, dispatchMessage } = setupLifecycleTest(t); + + let rp = await dispatchMessage('makeAndHoldRemotable'); + validate(v, matchVatstoreSet('vom.o+1/1', cacheObjValue)); + validate(v, matchVatstoreSet('vom.o+2/1', heldThingValue(null))); + validate(v, matchVatstoreGet('vom.o+1/1')); + validateReturned(v, rp); + validateDone(v); + + rp = await dispatchMessage('prepareStore3'); + validate(v, matchVatstoreSet('vom.o+2/2', heldThingValue('o+3'))); + validate(v, matchVatstoreSet('vom.o+2/3', heldThingValue('o+3'))); + validate(v, matchVatstoreSet('vom.o+2/4', heldThingValue('o+3'))); + validate(v, matchVatstoreGet('vom.o+1/1')); + validateReturned(v, rp); + validateDone(v); + + rp = await dispatchMessage('finishDropHolders'); + validateReturned(v, rp); + validateStatusCheck(v, 'o+2/2'); + validateDelete(v, 'o+2/2'); + validateStatusCheck(v, 'o+2/3'); + validateDelete(v, 'o+2/3'); + validateStatusCheck(v, 'o+2/4'); + validateDelete(v, 'o+2/4'); + validateDone(v); +}); + +test.serial('verify VO weak key GC', async t => { + const { v, dispatchMessage, testHooks } = setupLifecycleTest(t); + + // Create VO and hold onto it weakly + let rp = await dispatchMessage('makeAndHoldAndKey'); + validateCreate(v, rp); + t.is(testHooks.countCollectionsForWeakKey('o+1/2'), 2); + t.is(testHooks.countWeakKeysForCollection(aWeakMap), 1); + t.is(testHooks.countWeakKeysForCollection(aWeakSet), 1); + + // Drop in-memory reference, GC should cause weak entries to disappear + rp = await dispatchMessage('dropHeld'); + validateDropHeldWithGC(v, rp); + t.is(testHooks.countCollectionsForWeakKey('o+1/2'), 0); + t.is(testHooks.countWeakKeysForCollection(aWeakMap), 0); + t.is(testHooks.countWeakKeysForCollection(aWeakSet), 0); +}); + +test.serial('verify presence weak key GC', async t => { + const { + v, + dispatchMessage, + dispatchRetireImports, + testHooks, + } = setupLifecycleTest(t); + + validate(v, matchVatstoreSet('vom.o+1/1', cacheObjValue)); + validateDone(v); + + let rp = await dispatchMessage('importAndHoldAndKey', thingArg('o-5')); + validate(v, matchVatstoreSet('vom.o+2/1', heldThingValue(null))); + validate(v, matchVatstoreGet('vom.o+1/1')); + validateReturned(v, rp); + validateDone(v); + t.is(testHooks.countCollectionsForWeakKey('o-5'), 2); + t.is(testHooks.countWeakKeysForCollection(aWeakMap), 1); + t.is(testHooks.countWeakKeysForCollection(aWeakSet), 1); + + rp = await dispatchMessage('dropHeld'); + validateReturned(v, rp); + validate(v, matchVatstoreGet('vom.rc.o-5')); + validate(v, matchDropImports('o-5')); + validateDone(v); + t.is(testHooks.countCollectionsForWeakKey('o-5'), 2); + t.is(testHooks.countWeakKeysForCollection(aWeakMap), 1); + t.is(testHooks.countWeakKeysForCollection(aWeakSet), 1); + + await dispatchRetireImports('o-5'); + validateDone(v); + t.is(testHooks.countCollectionsForWeakKey('o-5'), 0); + t.is(testHooks.countWeakKeysForCollection(aWeakMap), 0); + t.is(testHooks.countWeakKeysForCollection(aWeakSet), 0); +}); diff --git a/packages/SwingSet/test/virtualObjects/test-virtualObjectManager.js b/packages/SwingSet/test/virtualObjects/test-virtualObjectManager.js index 9c2185c9f70..d0a6b80aa7d 100644 --- a/packages/SwingSet/test/virtualObjects/test-virtualObjectManager.js +++ b/packages/SwingSet/test/virtualObjects/test-virtualObjectManager.js @@ -319,7 +319,7 @@ test('virtual object gc', t => { const { makeKind, dumpStore, - setExported, + setExportStatus, deleteEntry, possibleVirtualObjectDeath, } = makeFakeVirtualObjectManager({ cacheSize: 3, log }); @@ -355,29 +355,34 @@ test('virtual object gc', t => { // case 1: export, drop local ref, drop export // export - setExported('o+1/1', true); - t.is(log.shift(), `get vom.o+1/1.refCount => undefined`); - t.is(log.shift(), `set vom.o+1/1.refCount 1 0`); + setExportStatus('o+1/1', 'reachable'); + t.is(log.shift(), `set vom.es.o+1/1 1`); t.deepEqual(log, []); // drop local ref -- should not delete because exported pretendGC('o+1/1'); - t.is(log.shift(), `get vom.o+1/1.refCount => 1 0`); + t.is(log.shift(), `get vom.rc.o+1/1 => undefined`); + t.is(log.shift(), `get vom.es.o+1/1 => 1`); t.deepEqual(log, []); t.deepEqual(dumpStore(), [ + ['vom.es.o+1/1', '1'], ['vom.o+1/1', minThing('thing #1')], - ['vom.o+1/1.refCount', '1 0'], ['vom.o+1/2', minThing('thing #2')], ['vom.o+1/3', minThing('thing #3')], ['vom.o+1/4', minThing('thing #4')], ['vom.o+1/5', minThing('thing #5')], ]); // drop export -- should delete - setExported('o+1/1', false); - t.is(log.shift(), `get vom.o+1/1.refCount => 1 0`); - t.is(log.shift(), `set vom.o+1/1.refCount 0 0`); - t.is(log.shift(), `get vom.o+1/1.refCount => 0 0`); + setExportStatus('o+1/1', 'recognizable'); + t.is(log.shift(), `set vom.es.o+1/1 0`); + t.is(log.shift(), `get vom.rc.o+1/1 => undefined`); + t.deepEqual(log, []); + pretendGC('o+1/1'); + t.is(log.shift(), `get vom.rc.o+1/1 => undefined`); + t.is(log.shift(), `get vom.es.o+1/1 => 0`); + t.is(log.shift(), `get vom.o+1/1 => ${thingVal(0, 'thing #1', 0)}`); t.is(log.shift(), `delete vom.o+1/1`); - t.is(log.shift(), `delete vom.o+1/1.refCount`); + t.is(log.shift(), `delete vom.rc.o+1/1`); + t.is(log.shift(), `delete vom.es.o+1/1`); t.deepEqual(log, []); t.deepEqual(dumpStore(), [ ['vom.o+1/2', minThing('thing #2')], @@ -388,27 +393,29 @@ test('virtual object gc', t => { // case 2: export, drop export, drop local ref // export - setExported('o+1/2', true); - t.is(log.shift(), `get vom.o+1/2.refCount => undefined`); - t.is(log.shift(), `set vom.o+1/2.refCount 1 0`); + setExportStatus('o+1/2', 'reachable'); + t.is(log.shift(), `set vom.es.o+1/2 1`); t.deepEqual(log, []); // drop export -- should not delete because ref'd locally - setExported('o+1/2', false); - t.is(log.shift(), `get vom.o+1/2.refCount => 1 0`); - t.is(log.shift(), `set vom.o+1/2.refCount 0 0`); + setExportStatus('o+1/2', 'recognizable'); + t.is(log.shift(), `set vom.es.o+1/2 0`); + t.is(log.shift(), `get vom.rc.o+1/2 => undefined`); t.deepEqual(log, []); t.deepEqual(dumpStore(), [ + ['vom.es.o+1/2', '0'], ['vom.o+1/2', minThing('thing #2')], - ['vom.o+1/2.refCount', '0 0'], ['vom.o+1/3', minThing('thing #3')], ['vom.o+1/4', minThing('thing #4')], ['vom.o+1/5', minThing('thing #5')], ]); // drop local ref -- should delete pretendGC('o+1/2'); - t.is(log.shift(), `get vom.o+1/2.refCount => 0 0`); + t.is(log.shift(), `get vom.rc.o+1/2 => undefined`); + t.is(log.shift(), `get vom.es.o+1/2 => 0`); + t.is(log.shift(), `get vom.o+1/2 => ${thingVal(0, 'thing #2', 0)}`); t.is(log.shift(), `delete vom.o+1/2`); - t.is(log.shift(), `delete vom.o+1/2.refCount`); + t.is(log.shift(), `delete vom.rc.o+1/2`); + t.is(log.shift(), `delete vom.es.o+1/2`); t.deepEqual(log, []); t.deepEqual(dumpStore(), [ ['vom.o+1/3', minThing('thing #3')], @@ -419,9 +426,12 @@ test('virtual object gc', t => { // case 3: drop local ref with no prior export // drop local ref -- should delete pretendGC('o+1/3'); - t.is(log.shift(), `get vom.o+1/3.refCount => undefined`); + t.is(log.shift(), `get vom.rc.o+1/3 => undefined`); + t.is(log.shift(), `get vom.es.o+1/3 => undefined`); + t.is(log.shift(), `get vom.o+1/3 => ${thingVal(0, 'thing #3', 0)}`); t.is(log.shift(), `delete vom.o+1/3`); - t.is(log.shift(), `delete vom.o+1/3.refCount`); + t.is(log.shift(), `delete vom.rc.o+1/3`); + t.is(log.shift(), `delete vom.es.o+1/3`); t.deepEqual(log, []); t.deepEqual(dumpStore(), [ ['vom.o+1/4', minThing('thing #4')], @@ -433,57 +443,61 @@ test('virtual object gc', t => { // eslint-disable-next-line no-unused-vars const ref1 = refMaker(things[3]); t.is(log.shift(), `set vom.o+1/6 ${minThing('thing #6')}`); - t.is(log.shift(), `get vom.o+1/4.refCount => undefined`); - t.is(log.shift(), `set vom.o+1/4.refCount 0 1`); + t.is(log.shift(), `get vom.rc.o+1/4 => undefined`); + t.is(log.shift(), `set vom.rc.o+1/4 1`); t.deepEqual(log, []); t.deepEqual(dumpStore(), [ ['vom.o+1/4', minThing('thing #4')], - ['vom.o+1/4.refCount', '0 1'], ['vom.o+1/5', minThing('thing #5')], ['vom.o+1/6', minThing('thing #6')], + ['vom.rc.o+1/4', '1'], ]); // export - setExported('o+1/4', true); - t.is(log.shift(), `get vom.o+1/4.refCount => 0 1`); - t.is(log.shift(), `set vom.o+1/4.refCount 1 1`); + setExportStatus('o+1/4', 'reachable'); + t.is(log.shift(), `set vom.es.o+1/4 1`); t.deepEqual(log, []); // drop local ref -- should not delete because ref'd virtually AND exported pretendGC('o+1/4'); + t.is(log.shift(), `get vom.rc.o+1/4 => 1`); + t.is(log.shift(), `get vom.es.o+1/4 => 1`); t.deepEqual(log, []); // drop export -- should not delete because ref'd virtually - setExported('o+1/4', false); - t.is(log.shift(), `get vom.o+1/4.refCount => 1 1`); - t.is(log.shift(), `set vom.o+1/4.refCount 0 1`); + setExportStatus('o+1/4', 'recognizable'); + t.is(log.shift(), `set vom.es.o+1/4 0`); + t.is(log.shift(), `get vom.rc.o+1/4 => 1`); t.deepEqual(log, []); // case 5: export, ref virtually, drop local ref, drop export // export - setExported('o+1/5', true); - t.is(log.shift(), `get vom.o+1/5.refCount => undefined`); - t.is(log.shift(), `set vom.o+1/5.refCount 1 0`); + setExportStatus('o+1/5', 'reachable'); + t.is(log.shift(), `set vom.es.o+1/5 1`); t.deepEqual(log, []); // ref virtually // eslint-disable-next-line no-unused-vars const ref2 = refMaker(things[4]); t.is(log.shift(), `set vom.o+1/7 ${minThing('thing #7')}`); - t.is(log.shift(), `get vom.o+1/5.refCount => 1 0`); - t.is(log.shift(), `set vom.o+1/5.refCount 1 1`); + t.is(log.shift(), `get vom.rc.o+1/5 => undefined`); + t.is(log.shift(), `set vom.rc.o+1/5 1`); t.deepEqual(log, []); t.deepEqual(dumpStore(), [ + ['vom.es.o+1/4', '0'], + ['vom.es.o+1/5', '1'], ['vom.o+1/4', minThing('thing #4')], - ['vom.o+1/4.refCount', '0 1'], ['vom.o+1/5', minThing('thing #5')], - ['vom.o+1/5.refCount', '1 1'], ['vom.o+1/6', minThing('thing #6')], ['vom.o+1/7', minThing('thing #7')], + ['vom.rc.o+1/4', '1'], + ['vom.rc.o+1/5', '1'], ]); // drop local ref -- should not delete because ref'd virtually AND exported pretendGC('o+1/5'); + t.is(log.shift(), `get vom.rc.o+1/5 => 1`); + t.is(log.shift(), `get vom.es.o+1/5 => 1`); t.deepEqual(log, []); // drop export -- should not delete because ref'd virtually - setExported('o+1/5', false); - t.is(log.shift(), `get vom.o+1/5.refCount => 1 1`); - t.is(log.shift(), `set vom.o+1/5.refCount 0 1`); + setExportStatus('o+1/5', 'recognizable'); + t.is(log.shift(), `set vom.es.o+1/5 0`); + t.is(log.shift(), `get vom.rc.o+1/5 => 1`); t.deepEqual(log, []); // case 6: ref virtually, drop local ref @@ -491,31 +505,37 @@ test('virtual object gc', t => { // eslint-disable-next-line no-unused-vars const ref3 = refMaker(things[5]); t.is(log.shift(), `set vom.o+1/8 ${minThing('thing #8')}`); - t.is(log.shift(), `get vom.o+1/6.refCount => undefined`); - t.is(log.shift(), `set vom.o+1/6.refCount 0 1`); + t.is(log.shift(), `get vom.rc.o+1/6 => undefined`); + t.is(log.shift(), `set vom.rc.o+1/6 1`); t.deepEqual(log, []); t.deepEqual(dumpStore(), [ + ['vom.es.o+1/4', '0'], + ['vom.es.o+1/5', '0'], ['vom.o+1/4', minThing('thing #4')], - ['vom.o+1/4.refCount', '0 1'], ['vom.o+1/5', minThing('thing #5')], - ['vom.o+1/5.refCount', '0 1'], ['vom.o+1/6', minThing('thing #6')], - ['vom.o+1/6.refCount', '0 1'], ['vom.o+1/7', minThing('thing #7')], ['vom.o+1/8', minThing('thing #8')], + ['vom.rc.o+1/4', '1'], + ['vom.rc.o+1/5', '1'], + ['vom.rc.o+1/6', '1'], ]); // drop local ref -- should not delete because ref'd virtually pretendGC('o+1/6'); + t.is(log.shift(), `get vom.rc.o+1/6 => 1`); + t.is(log.shift(), `get vom.es.o+1/6 => undefined`); t.deepEqual(log, []); t.deepEqual(dumpStore(), [ + ['vom.es.o+1/4', '0'], + ['vom.es.o+1/5', '0'], ['vom.o+1/4', minThing('thing #4')], - ['vom.o+1/4.refCount', '0 1'], ['vom.o+1/5', minThing('thing #5')], - ['vom.o+1/5.refCount', '0 1'], ['vom.o+1/6', minThing('thing #6')], - ['vom.o+1/6.refCount', '0 1'], ['vom.o+1/7', minThing('thing #7')], ['vom.o+1/8', minThing('thing #8')], + ['vom.rc.o+1/4', '1'], + ['vom.rc.o+1/5', '1'], + ['vom.rc.o+1/6', '1'], ]); }); diff --git a/packages/SwingSet/tools/fakeVirtualObjectManager.js b/packages/SwingSet/tools/fakeVirtualObjectManager.js index 7aee8280f52..f2a14a24004 100644 --- a/packages/SwingSet/tools/fakeVirtualObjectManager.js +++ b/packages/SwingSet/tools/fakeVirtualObjectManager.js @@ -5,8 +5,26 @@ import { parseVatSlot } from '../src/parseVatSlots.js'; import { makeVirtualObjectManager } from '../src/kernel/virtualObjectManager.js'; +class FakeFinalizationRegistry { + // eslint-disable-next-line no-useless-constructor, no-empty-function + constructor() {} + + // eslint-disable-next-line class-methods-use-this + register(_target, _heldValue, _unregisterToken) {} + + // eslint-disable-next-line class-methods-use-this + unregister(_unregisterToken) {} +} + export function makeFakeVirtualObjectManager(options = {}) { - const { cacheSize = 100, log, weak = false } = options; + const { + cacheSize = 100, + log, + weak = false, + // eslint-disable-next-line no-use-before-define + FinalizationRegistry = FakeFinalizationRegistry, + addToPossiblyDeadSet = () => {}, + } = options; const fakeStore = new Map(); function dumpStore() { @@ -109,8 +127,8 @@ export function makeFakeVirtualObjectManager(options = {}) { makeKind, VirtualObjectAwareWeakMap, VirtualObjectAwareWeakSet, - isVrefReachable, - setExported, + isPresenceReachable, + setExportStatus, possibleVirtualObjectDeath, flushCache, } = makeVirtualObjectManager( @@ -119,8 +137,11 @@ export function makeFakeVirtualObjectManager(options = {}) { getSlotForVal, getValForSlot, registerEntry, - fakeMarshal, + fakeMarshal.serialize, + fakeMarshal.unserialize, cacheSize, + FinalizationRegistry, + addToPossiblyDeadSet, ); const normalVOM = { @@ -128,8 +149,8 @@ export function makeFakeVirtualObjectManager(options = {}) { makeVirtualScalarWeakMap, VirtualObjectAwareWeakMap, VirtualObjectAwareWeakSet, - isVrefReachable, - setExported, + isPresenceReachable, + setExportStatus, possibleVirtualObjectDeath, }; diff --git a/packages/swingset-runner/demo/virtualObjectGC/bootstrap.js b/packages/swingset-runner/demo/virtualObjectGC/bootstrap.js new file mode 100644 index 00000000000..907e61dc72f --- /dev/null +++ b/packages/swingset-runner/demo/virtualObjectGC/bootstrap.js @@ -0,0 +1,216 @@ +import { E } from '@agoric/eventual-send'; +import { Far } from '@agoric/marshal'; + +export function buildRootObject(_vatPowers) { + let bob; + let me; + let exportedThing; + let exportedThingRecognizer; + let exerciseWeakKeys; + + function maybeRecognize(thing) { + if (exerciseWeakKeys) { + exportedThingRecognizer = new WeakSet(); + exportedThingRecognizer.add(thing); + } + } + + function maybeDontRecognizeAnything() { + if (exerciseWeakKeys) { + exportedThingRecognizer = null; + } + } + + return Far('root', { + async bootstrap(vats) { + me = vats.bootstrap; + bob = vats.bob; + // exerciseWeakKeys = true; + // E(me).continueTest('test1'); + // E(me).continueTest('test2'); + // E(me).continueTest('test3'); + // E(me).continueTest('test4'); + // E(me).continueTest('test5'); + // E(me).continueTest('test6'); + exerciseWeakKeys = false; + // E(me).continueTest('test1'); + // E(me).continueTest('test2'); + // E(me).continueTest('test3'); + // E(me).continueTest('test4'); + // E(me).continueTest('test5'); + E(me).continueTest('test6'); + }, + async continueTest(testTag) { + switch (testTag) { + case 'test1': { + // test 1: + // - create a locally referenced VO lerv -> Lerv + // - forget about it Lerv -> lerv + E(bob).makeAndHold(); + E(bob).dropHeld(); + E(bob).assess(); + // E(me).continueTest('test2'); + break; + } + case 'test2': { + // test 2: + // - create a locally referenced VO lerv -> Lerv + // - store virtually Lerv -> LerV + // - forget about it LerV -> lerV + // - read from virtual storage lerV -> LerV + // - export it LerV -> LERV + // - forget about it LERV -> lERV + // - read from virtual storage lERV -> LERV + // - forget about it LERV -> lERV + // - reintroduce via delivery lERV -> LERV + // - forget abut it LERV -> lERV + // - drop export lERV -> leRV + // - read from virtual storage leRV -> LeRV + // - forget about it LeRV -> leRV + // - read from virtual storage leRV -> LeRV + // - retire export LeRV -> LerV + E(bob).makeAndHold(); + E(bob).storeHeld(); + E(bob).dropHeld(); + E(bob).fetchAndHold(); + exportedThing = await E(bob).exportHeld(); + maybeRecognize(exportedThing); + E(bob).dropHeld(); + E(bob).fetchAndHold(); + E(bob).dropHeld(); + E(bob).importAndHold(exportedThing); + E(bob).dropHeld(); + exportedThing = null; + E(bob).tellMeToContinueTest(me, 'test2a'); + break; + } + case 'test2a': { + E(bob).fetchAndHold(); + E(bob).dropHeld(); + E(bob).fetchAndHold(); + maybeDontRecognizeAnything(); + E(bob).tellMeToContinueTest(me, 'test2b'); + break; + } + case 'test2b': { + E(bob).assess(); + // E(me).continueTest('test3'); + break; + } + case 'test3': { + // - create a locally referenced VO lerv -> Lerv + // - store virtually Lerv -> LerV + // - export it LerV -> LERV + // - drop export LERV -> LeRV + // - drop held LeRV -> leRV + // - retire export leRV -> lerV + E(bob).makeAndHold(); + E(bob).storeHeld(); + exportedThing = await E(bob).exportHeld(); + maybeRecognize(exportedThing); + E(bob).dropHeld(); + exportedThing = null; + E(bob).tellMeToContinueTest(me, 'test3a'); + break; + } + case 'test3a': { + maybeDontRecognizeAnything(); + E(bob).tellMeToContinueTest(me, 'test3b'); + break; + } + case 'test3b': { + E(bob).assess(); + // E(me).continueTest('test4'); + break; + } + case 'test4': { + // - create a locally referenced VO lerv -> Lerv + // - export it Lerv -> LERv + // - drop export LERv -> LeRv + // - drop it LeRv -> leRv + // - retire export leRv -> lerv + E(bob).makeAndHold(); + exportedThing = await E(bob).exportHeld(); + maybeRecognize(exportedThing); + E(bob).tellMeToContinueTest(me, 'test4a'); + break; + } + case 'test4a': { + exportedThing = null; + E(bob).tellMeToContinueTest(me, 'test4b'); + break; + } + case 'test4b': { + E(bob).dropHeld(); + maybeDontRecognizeAnything(); + E(bob).tellMeToContinueTest(me, 'test4c'); + break; + } + case 'test4c': { + E(bob).assess(); + // E(me).continueTest('test5'); + break; + } + case 'test5': { + // - create a locally referenced VO lerv -> Lerv + // - export it Lerv -> LERv + // - drop export LERv -> LeRv + // - retire export LeRv -> lerv + // - drop it LeRv -> leRv + E(bob).makeAndHold(); + exportedThing = await E(bob).exportHeld(); + maybeRecognize(exportedThing); + E(bob).tellMeToContinueTest(me, 'test5a'); + break; + } + case 'test5a': { + exportedThing = null; + E(bob).tellMeToContinueTest(me, 'test5b'); + break; + } + case 'test5b': { + maybeDontRecognizeAnything(); + E(bob).tellMeToContinueTest(me, 'test5c'); + break; + } + case 'test5c': { + E(bob).dropHeld(); + E(bob).assess(); + // E(me).continueTest('test6'); + break; + } + case 'test6': { + // - create a locally referenced VO lerv -> Lerv + // - export it Lerv -> LERv + // - drop export LERv -> LeRv + // - store virtually LeRv -> LeRV + // - drop it LeRV -> leRV + // - retire export leRV -> lerV + E(bob).makeAndHold(); + exportedThing = await E(bob).exportHeld(); + maybeRecognize(exportedThing); + E(bob).tellMeToContinueTest(me, 'test6a'); + break; + } + case 'test6a': { + exportedThing = null; + E(bob).tellMeToContinueTest(me, 'test6b'); + break; + } + case 'test6b': { + E(bob).storeHeld(); + E(bob).dropHeld(); + maybeDontRecognizeAnything(); + E(bob).tellMeToContinueTest(me, 'test6c'); + break; + } + case 'test6c': { + E(bob).assess(); + break; + } + default: + break; + } + }, + }); +} diff --git a/packages/swingset-runner/demo/virtualObjectGC/swingset.json b/packages/swingset-runner/demo/virtualObjectGC/swingset.json new file mode 100644 index 00000000000..613882ef641 --- /dev/null +++ b/packages/swingset-runner/demo/virtualObjectGC/swingset.json @@ -0,0 +1,14 @@ +{ + "bootstrap": "bootstrap", + "vats": { + "bootstrap": { + "sourceSpec": "bootstrap.js" + }, + "bob": { + "sourceSpec": "vat-bob.js", + "creationOptions": { + "virtualObjectCacheSize": 0 + } + } + } +} diff --git a/packages/swingset-runner/demo/virtualObjectGC/vat-bob.js b/packages/swingset-runner/demo/virtualObjectGC/vat-bob.js new file mode 100644 index 00000000000..89e0c98f3e7 --- /dev/null +++ b/packages/swingset-runner/demo/virtualObjectGC/vat-bob.js @@ -0,0 +1,82 @@ +/* global makeKind */ +import { E } from '@agoric/eventual-send'; +import { Far } from '@agoric/marshal'; + +export function buildRootObject(_vatPowers) { + function makeThingInstance(state) { + return { + init(label) { + state.label = label; + }, + self: Far('thing', { + getLabel() { + return state.label; + }, + }), + }; + } + + function makeVirtualHolderInstance(state) { + return { + init(value) { + state.value = value; + }, + self: Far('holder', { + getValue() { + return state.value; + }, + }), + }; + } + + const thingMaker = makeKind(makeThingInstance); + const virtualHolderMaker = makeKind(makeVirtualHolderInstance); + const cacheDisplacer = thingMaker('cacheDisplacer'); + let nextThingNumber = 0; + let heldThing = null; + let virtualHolder = null; + + function displaceCache() { + return cacheDisplacer.getLabel(); + } + + function makeNextThing() { + const thing = thingMaker(`thing #${nextThingNumber}`); + nextThingNumber += 1; + return thing; + } + + return Far('root', { + makeAndHold() { + heldThing = makeNextThing(); + displaceCache(); + }, + dropHeld() { + heldThing = null; + displaceCache(); + }, + storeHeld() { + virtualHolder = virtualHolderMaker(heldThing); + displaceCache(); + }, + fetchAndHold() { + heldThing = virtualHolder.getValue(); + displaceCache(); + }, + exportHeld() { + return heldThing; + }, + importAndHold(thing) { + heldThing = thing; + displaceCache(); + }, + tellMeToContinueTest(other, testTag) { + displaceCache(); + E(other).continueTest(testTag); + }, + assess() { + displaceCache(); + console.log('assess here'); + }, + }); +}