diff --git a/packages/SwingSet/src/kernel/kernel.js b/packages/SwingSet/src/kernel/kernel.js index 53040adf8229..5c09efdc3be9 100644 --- a/packages/SwingSet/src/kernel/kernel.js +++ b/packages/SwingSet/src/kernel/kernel.js @@ -633,7 +633,7 @@ export default function buildKernel( kdebug(`vat terminated: ${JSON.stringify(info)}`); } if (!didAbort) { - kernelKeeper.purgeDeadKernelPromises(); + kernelKeeper.processRefcounts(); kernelKeeper.saveStats(); } commitCrank(); diff --git a/packages/SwingSet/src/kernel/state/kernelKeeper.js b/packages/SwingSet/src/kernel/state/kernelKeeper.js index 1c1cc4283a5b..1fcd1bc9c4cd 100644 --- a/packages/SwingSet/src/kernel/state/kernelKeeper.js +++ b/packages/SwingSet/src/kernel/state/kernelKeeper.js @@ -18,7 +18,7 @@ import { KERNEL_STATS_UPDOWN_METRICS, } from '../metrics'; -const enableKernelPromiseGC = true; +const enableKernelGC = true; // Kernel state lives in a key-value store. All keys and values are strings. // We simulate a tree by concatenating path-name components with ".". When we @@ -64,6 +64,7 @@ const enableKernelPromiseGC = true; // ko.nextID = $NN // ko$NN.owner = $vatID +// ko$NN.refCount = $NN,$MM // reachable count, recognizable count // kd.nextID = $NN // kd$NN.owner = $vatID // kp.nextID = $NN @@ -256,6 +257,31 @@ export default function makeKernelKeeper(kvStore, streamStore, kernelSlog) { return parseReachableAndVatSlot(kvStore.get(kernelKey)); } + function getObjectRefCount(kernelSlot) { + const data = kvStore.get(`${kernelSlot}.refCount`); + assert(data, `getObjectRefCount(${kernelSlot}) was missing`); + const [reachable, recognizable] = commaSplit(data).map(Number); + assert( + reachable <= recognizable, + `refmismatch(get) ${kernelSlot} ${reachable},${recognizable}`, + ); + return { reachable, recognizable }; + } + + function setObjectRefCount(kernelSlot, { reachable, recognizable }) { + assert.typeof(reachable, 'number'); + assert.typeof(recognizable, 'number'); + assert( + reachable >= 0 && recognizable >= 0, + `${kernelSlot} underflow ${reachable},${recognizable}`, + ); + assert( + reachable <= recognizable, + `refmismatch(set) ${kernelSlot} ${reachable},${recognizable}`, + ); + kvStore.set(`${kernelSlot}.refCount`, `${reachable},${recognizable}`); + } + function addKernelObject(ownerID, id = undefined) { // providing id= is only for unit tests insistVatID(ownerID); @@ -266,10 +292,15 @@ export default function makeKernelKeeper(kvStore, streamStore, kernelSlog) { kdebug(`Adding kernel object ko${id} for ${ownerID}`); const s = makeKernelSlot('object', id); kvStore.set(`${s}.owner`, ownerID); + setObjectRefCount(s, { reachable: 0, recognizable: 0 }); incStat('kernelObjects'); return s; } + function kernelObjectExists(kref) { + return kvStore.has(`${kref}.owner`); + } + function ownerOfKernelObject(kernelSlot) { insistKernelType('object', kernelSlot); const owner = kvStore.get(`${kernelSlot}.owner`); @@ -685,69 +716,152 @@ export default function makeKernelKeeper(kvStore, streamStore, kernelSlog) { return JSON.parse(getRequired('vat.dynamicIDs')); } - const deadKernelPromises = new Set(); + // As refcounts are decremented, we accumulate a set of krefs for which + // action might need to be taken: + // * promises which are now resolved and unreferenced can be deleted + // * objects which are no longer reachable: export can be dropped + // * objects which are no longer recognizable: export can be retired + + // This set is ephemeral: it lives in RAM, grows as deliveries and syscalls + // cause decrefs, and is harvested by processRefcounts(). This needs to be + // called in the same transaction window as the syscalls/etc which prompted + // the change, else removals might be lost (not performed during the next + // replay). + + const maybeFreeKrefs = new Set(); + function addMaybeFreeKref(kref) { + insistKernelType('object', kref); + maybeFreeKrefs.add(kref); + } /** * Increment the reference count associated with some kernel object. * - * Note that currently we are only reference counting promises, but ultimately - * we intend to keep track of all objects with kernel slots. + * We track references to promises and objects, but not devices. Promises + * have only a "reachable" count, whereas objects track both "reachable" + * and "recognizable" counts. * * @param {unknown} kernelSlot The kernel slot whose refcount is to be incremented. - * @param {string} _tag + * @param {string?} tag Debugging note with rough source of the reference. + * @param { { isExport: boolean?, onlyRecognizable: boolean? } } options + * 'isExport' means the reference comes from a clist export, which counts + * for promises but not objects. 'onlyRecognizable' means the reference + * provides only recognition, not reachability */ - function incrementRefCount(kernelSlot, _tag) { - if (kernelSlot && parseKernelSlot(kernelSlot).type === 'promise') { + function incrementRefCount(kernelSlot, tag, options = {}) { + const { isExport = false, onlyRecognizable = false } = options; + assert( + kernelSlot, + `incrementRefCount called with empty kernelSlot, tag=${tag}`, + ); + const { type } = parseKernelSlot(kernelSlot); + if (type === 'promise') { const refCount = Nat(BigInt(kvStore.get(`${kernelSlot}.refCount`))) + 1n; // kdebug(`++ ${kernelSlot} ${tag} ${refCount}`); kvStore.set(`${kernelSlot}.refCount`, `${refCount}`); } + if (type === 'object' && !isExport) { + let { reachable, recognizable } = getObjectRefCount(kernelSlot); + if (!onlyRecognizable) { + reachable += 1; + } + recognizable += 1; + setObjectRefCount(kernelSlot, { reachable, recognizable }); + } } /** * Decrement the reference count associated with some kernel object. * - * Note that currently we are only reference counting promises, but ultimately - * we intend to keep track of all objects with kernel slots. + * We track references to promises and objects. * * @param {string} kernelSlot The kernel slot whose refcount is to be decremented. * @param {string} tag + * @param { { isExport: boolean?, onlyRecognizable: boolean? } } options + * 'isExport' means the reference comes from a clist export, which counts + * for promises but not objects. 'onlyRecognizable' means the reference + * deing deleted only provided recognition, not reachability * @returns {boolean} true if the reference count has been decremented to zero, false if it is still non-zero * @throws if this tries to decrement the reference count below zero. */ - function decrementRefCount(kernelSlot, tag) { - if (kernelSlot && parseKernelSlot(kernelSlot).type === 'promise') { + function decrementRefCount(kernelSlot, tag, options = {}) { + const { isExport = false, onlyRecognizable = false } = options; + assert( + kernelSlot, + `decrementRefCount called with empty kernelSlot, tag=${tag}`, + ); + const { type } = parseKernelSlot(kernelSlot); + if (type === 'promise') { let refCount = Nat(BigInt(kvStore.get(`${kernelSlot}.refCount`))); assert(refCount > 0n, X`refCount underflow {kernelSlot} ${tag}`); refCount -= 1n; // kdebug(`-- ${kernelSlot} ${tag} ${refCount}`); kvStore.set(`${kernelSlot}.refCount`, `${refCount}`); if (refCount === 0n) { - deadKernelPromises.add(kernelSlot); + maybeFreeKrefs.add(kernelSlot); return true; } } + if (type === 'object' && !isExport && kernelObjectExists(kernelSlot)) { + let { reachable, recognizable } = getObjectRefCount(kernelSlot); + if (!onlyRecognizable) { + reachable -= 1; + } + recognizable -= 1; + maybeFreeKrefs.add(kernelSlot); + setObjectRefCount(kernelSlot, { reachable, recognizable }); + } + return false; } - function purgeDeadKernelPromises() { - if (enableKernelPromiseGC) { - for (const kpid of deadKernelPromises.values()) { - const kp = getKernelPromise(kpid); - if (kp.refCount === 0) { - let idx = 0; - for (const slot of kp.data.slots) { - // Note: the following decrement can result in an addition to the - // deadKernelPromises set, which we are in the midst of iterating. - // TC39 went to a lot of trouble to ensure that this is kosher. - decrementRefCount(slot, `gc|${kpid}|s${idx}`); - idx += 1; + function processRefcounts() { + if (enableKernelGC) { + const actions = getGCActions(); // local cache + // TODO (else buggy): change the iteration to handle krefs that get + // added multiple times (while we're iterating), because we might do + // different work the second time around. Something like an ordered + // Set, and a loop that: pops the first kref off, processes it (maybe + // adding more krefs), repeats until the thing is empty. + for (const kref of maybeFreeKrefs.values()) { + const { type } = parseKernelSlot(kref); + if (type === 'promise') { + const kpid = kref; + const kp = getKernelPromise(kpid); + if (kp.refCount === 0) { + let idx = 0; + for (const slot of kp.data.slots) { + // Note: the following decrement can result in an addition to the + // maybeFreeKrefs set, which we are in the midst of iterating. + // TC39 went to a lot of trouble to ensure that this is kosher. + decrementRefCount(slot, `gc|${kpid}|s${idx}`); + idx += 1; + } + deleteKernelPromise(kpid); + } + } + if (type === 'object') { + const { reachable, recognizable } = getObjectRefCount(kref); + if (reachable === 0) { + const ownerVatID = ownerOfKernelObject(kref); + // eslint-disable-next-line no-use-before-define + const vatKeeper = provideVatKeeper(ownerVatID); + const isReachable = vatKeeper.getReachableFlag(kref); + if (isReachable) { + // the reachable count is zero, but the vat doesn't realize it + actions.add(`${ownerVatID} dropExport ${kref}`); + } + if (recognizable === 0) { + // TODO: rethink this + // assert.equal(isReachable, false, `${kref} is reachable but not recognizable`); + actions.add(`${ownerVatID} retireExport ${kref}`); + } } - deleteKernelPromise(kpid); } } + setGCActions(actions); } - deadKernelPromises.clear(); + maybeFreeKrefs.clear(); } function provideVatKeeper(vatID) { @@ -766,9 +880,13 @@ export default function makeKernelKeeper(kvStore, streamStore, kernelSlog) { vatID, addKernelObject, addKernelPromiseForVat, + kernelObjectExists, incrementRefCount, decrementRefCount, + getObjectRefCount, + setObjectRefCount, getReachableAndVatSlot, + addMaybeFreeKref, incStat, decStat, getCrankNumber, @@ -941,6 +1059,17 @@ export default function makeKernelKeeper(kvStore, streamStore, kernelSlog) { } promises.sort((a, b) => compareStrings(a.id, b.id)); + const objects = []; + const nextObjectID = Nat(BigInt(getRequired('ko.nextID'))); + for (let i = FIRST_OBJECT_ID; i < nextObjectID; i += 1n) { + const koid = makeKernelSlot('object', i); + if (kvStore.has(`${koid}.refCount`)) { + const owner = kvStore.get(`${koid}.owner`); // missing for orphans + const { reachable, recognizable } = getObjectRefCount(koid); + objects.push([koid, owner, reachable, recognizable]); + } + } + const gcActions = Array.from(getGCActions()); gcActions.sort(); @@ -950,6 +1079,7 @@ export default function makeKernelKeeper(kvStore, streamStore, kernelSlog) { vatTables, kernelTable, promises, + objects, gcActions, runQueue, }); @@ -965,7 +1095,7 @@ export default function makeKernelKeeper(kvStore, streamStore, kernelSlog) { getCrankNumber, incrementCrankNumber, - purgeDeadKernelPromises, + processRefcounts, incStat, decStat, @@ -980,6 +1110,7 @@ export default function makeKernelKeeper(kvStore, streamStore, kernelSlog) { addKernelObject, ownerOfKernelObject, ownerOfKernelDevice, + kernelObjectExists, getImporters, deleteKernelObject, @@ -995,6 +1126,7 @@ export default function makeKernelKeeper(kvStore, streamStore, kernelSlog) { clearDecider, incrementRefCount, decrementRefCount, + getObjectRefCount, addToRunQueue, isRunQueueEmpty, diff --git a/packages/SwingSet/src/kernel/state/vatKeeper.js b/packages/SwingSet/src/kernel/state/vatKeeper.js index 9fe6ab1ae139..9c3eee3b2b55 100644 --- a/packages/SwingSet/src/kernel/state/vatKeeper.js +++ b/packages/SwingSet/src/kernel/state/vatKeeper.js @@ -50,9 +50,13 @@ export function initializeVatState(kvStore, streamStore, vatID) { * mapping tables. * @param {*} addKernelPromiseForVat Kernel function to add a new promise to the * kernel's mapping tables. + * @param {(kernelSlot: string) => boolean} kernelObjectExists * @param {*} incrementRefCount * @param {*} decrementRefCount + * @param {(kernelSlot: string) => {reachable: number, recognizable: number}} getObjectRefCount + * @param {(kernelSlot: string, { reachable: Number, recognizable: Number }) => undefined} setObjectRefCount * @param {(vatID: string, kernelSlot: string) => {reachable: boolean, vatSlot: string}} getReachableAndVatSlot + * @param {(kernelSlot: string) => undefined} addMaybeFreeKref * @param {*} incStat * @param {*} decStat * @param {*} getCrankNumber @@ -65,9 +69,13 @@ export function makeVatKeeper( vatID, addKernelObject, addKernelPromiseForVat, + kernelObjectExists, incrementRefCount, decrementRefCount, + getObjectRefCount, + setObjectRefCount, getReachableAndVatSlot, + addMaybeFreeKref, incStat, decStat, getCrankNumber, @@ -115,19 +123,45 @@ export function makeVatKeeper( } function setReachableFlag(kernelSlot) { - // const { type } = parseKernelSlot(kernelSlot); + const { type } = parseKernelSlot(kernelSlot); const kernelKey = `${vatID}.c.${kernelSlot}`; - const { vatSlot } = parseReachableAndVatSlot(kvStore.get(kernelKey)); - // const { allocatedByVat } = parseVatSlot(vatSlot); + const { isReachable, vatSlot } = parseReachableAndVatSlot( + kvStore.get(kernelKey), + ); + const { allocatedByVat } = parseVatSlot(vatSlot); kvStore.set(kernelKey, buildReachableAndVatSlot(true, vatSlot)); + // increment 'reachable' part of refcount, but only for object imports + if (!isReachable && type === 'object' && !allocatedByVat) { + // eslint-disable-next-line prefer-const + let { reachable, recognizable } = getObjectRefCount(kernelSlot); + reachable += 1; + setObjectRefCount(kernelSlot, { reachable, recognizable }); + } } function clearReachableFlag(kernelSlot) { - // const { type } = parseKernelSlot(kernelSlot); + const { type } = parseKernelSlot(kernelSlot); const kernelKey = `${vatID}.c.${kernelSlot}`; - const { vatSlot } = parseReachableAndVatSlot(kvStore.get(kernelKey)); - // const { allocatedByVat } = parseVatSlot(vatSlot); + const { isReachable, vatSlot } = parseReachableAndVatSlot( + kvStore.get(kernelKey), + ); + const { allocatedByVat } = parseVatSlot(vatSlot); kvStore.set(kernelKey, buildReachableAndVatSlot(false, vatSlot)); + // decrement 'reachable' part of refcount, but only for object imports + if ( + isReachable && + type === 'object' && + !allocatedByVat && + kernelObjectExists(kernelSlot) + ) { + // eslint-disable-next-line prefer-const + let { reachable, recognizable } = getObjectRefCount(kernelSlot); + reachable -= 1; + setObjectRefCount(kernelSlot, { reachable, recognizable }); + if (reachable === 0) { + addMaybeFreeKref(kernelSlot); + } + } } function importsKernelSlot(kernelSlot) { @@ -151,19 +185,22 @@ export function makeVatKeeper( * allocate, we insist that the reachable flag was already set. * * @param {string} vatSlot The vat slot of interest - * @param {bool} setReachable Set the 'reachable' flag on vat exports. + * @param { { setReachable: bool, required: bool } } options 'setReachable' will set the 'reachable' flag on vat exports, while 'required' means we refuse to allocate a missing entry * @returns {string} the kernel slot that vatSlot maps to * @throws {Error} if vatSlot is not a kind of thing that can be exported by vats * or is otherwise invalid. */ - function mapVatSlotToKernelSlot(vatSlot, setReachable = true) { + function mapVatSlotToKernelSlot(vatSlot, options = {}) { + const { setReachable = true, required = false } = options; assert.typeof(vatSlot, 'string', X`non-string vatSlot: ${vatSlot}`); const { type, allocatedByVat } = parseVatSlot(vatSlot); const vatKey = `${vatID}.c.${vatSlot}`; if (!kvStore.has(vatKey)) { + assert(!required, `vref ${vatSlot} not in clist`); if (allocatedByVat) { let kernelSlot; if (type === 'object') { + // this sets the initial refcount to reachable:0 recognizable:0 kernelSlot = addKernelObject(vatID); } else if (type === 'device') { assert.fail(X`normal vats aren't allowed to export device nodes`); @@ -172,15 +209,19 @@ export function makeVatKeeper( } else { assert.fail(X`unknown type ${type}`); } - incrementRefCount(kernelSlot, `${vatID}|vk|clist`); + // now increment the refcount with isExport=true and + // onlyRecognizable=true, which will skip object exports (we only + // count imports) and leave the reachability count at zero + const incopts = { isExport: true, onlyRecognizable: true }; + incrementRefCount(kernelSlot, `${vatID}|vk|clist`, incopts); const kernelKey = `${vatID}.c.${kernelSlot}`; incStat('clistEntries'); - // we add the key as "unreachable", and then rely on - // setReachableFlag() at the end to both mark it reachable and to + // we add the key as "unreachable" but "recognizable", and then rely + // on setReachableFlag() at the end to both mark it reachable and to // update any necessary refcounts consistently kvStore.set(kernelKey, buildReachableAndVatSlot(false, vatSlot)); kvStore.set(vatKey, kernelSlot); - kernelSlog && + if (kernelSlog) { kernelSlog.changeCList( vatID, getCrankNumber(), @@ -188,6 +229,7 @@ export function makeVatKeeper( kernelSlot, vatSlot, ); + } kdebug(`Add mapping v->k ${kernelKey}<=>${vatKey}`); } else { // the vat didn't allocate it, and the kernel didn't allocate it @@ -215,15 +257,17 @@ export function makeVatKeeper( * creating the vat slot if it doesn't already exist. * * @param {string} kernelSlot The kernel slot of interest - * @param {bool} setReachable Set the 'reachable' flag on vat imports. + * @param { { setReachable: bool, required: bool } } options 'setReachable' will set the 'reachable' flag on vat imports, while 'required' means we refuse to allocate a missing entry * @returns {string} the vat slot kernelSlot maps to * @throws {Error} if kernelSlot is not a kind of thing that can be imported by vats * or is otherwise invalid. */ - function mapKernelSlotToVatSlot(kernelSlot, setReachable = true) { + function mapKernelSlotToVatSlot(kernelSlot, options = {}) { + const { setReachable = true, required = false } = options; assert.typeof(kernelSlot, 'string', 'non-string kernelSlot'); const kernelKey = `${vatID}.c.${kernelSlot}`; if (!kvStore.has(kernelKey)) { + assert(!required, `kref ${kernelSlot} not in clist`); const { type } = parseKernelSlot(kernelSlot); let id; @@ -239,14 +283,18 @@ export function makeVatKeeper( } else { assert.fail(X`unknown type ${type}`); } - incrementRefCount(kernelSlot, `${vatID}|kv|clist`); + // use isExport=false, since this is an import, and leave reachable + // alone to defer to setReachableFlag below + incrementRefCount(kernelSlot, `${vatID}|kv|clist`, { + onlyRecognizable: true, + }); const vatSlot = makeVatSlot(type, false, id); const vatKey = `${vatID}.c.${vatSlot}`; incStat('clistEntries'); kvStore.set(vatKey, kernelSlot); kvStore.set(kernelKey, buildReachableAndVatSlot(false, vatSlot)); - kernelSlog && + if (kernelSlog) { kernelSlog.changeCList( vatID, getCrankNumber(), @@ -254,6 +302,7 @@ export function makeVatKeeper( kernelSlot, vatSlot, ); + } kdebug(`Add mapping k->v ${kernelKey}<=>${vatKey}`); } @@ -288,14 +337,16 @@ export function makeVatKeeper( * * @param {string} kernelSlot The kernel slot being removed * @param {string} vatSlot The vat slot being removed + * @param {boolean} decref Decrement the related refcounts. Use 'false' when the object is already deleted. */ - function deleteCListEntry(kernelSlot, vatSlot) { - parseKernelSlot(kernelSlot); - parseVatSlot(vatSlot); + function deleteCListEntry(kernelSlot, vatSlot, decref) { + parseKernelSlot(kernelSlot); // used for its assert() + const { allocatedByVat } = parseVatSlot(vatSlot); const kernelKey = `${vatID}.c.${kernelSlot}`; const vatKey = `${vatID}.c.${vatSlot}`; + assert(kvStore.has(kernelKey)); kdebug(`Delete mapping ${kernelKey}<=>${vatKey}`); - kernelSlog && + if (kernelSlog) { kernelSlog.changeCList( vatID, getCrankNumber(), @@ -303,18 +354,29 @@ export function makeVatKeeper( kernelSlot, vatSlot, ); - if (kvStore.has(kernelKey)) { - decrementRefCount(kernelSlot, `${vatID}|del|clist`); - decStat('clistEntries'); - kvStore.delete(kernelKey); - kvStore.delete(vatKey); } + // tolerate the object kref not being present in the kernel object table, + // because the exporter's syscall.retireExport raced ahead of the + // importer's syscall.retireImports (retireImports calls deleteCListEntry). + if (decref) { + const isExport = allocatedByVat; + // First, make sure the reachable flag is clear, which might reduce the + // reachable refcount. Note that we need the clist entry to find this, + // so decref before delete. + clearReachableFlag(kernelSlot); + // then decrementRefCount only the recognizable portion of the refcount + const decopts = { isExport, onlyRecognizable: true }; + decrementRefCount(kernelSlot, `${vatID}|del|clist`, decopts); + } + decStat('clistEntries'); + kvStore.delete(kernelKey); + kvStore.delete(vatKey); } function deleteCListEntriesForKernelSlots(kernelSlots) { for (const kernelSlot of kernelSlots) { const vatSlot = mapKernelSlotToVatSlot(kernelSlot); - deleteCListEntry(kernelSlot, vatSlot); + deleteCListEntry(kernelSlot, vatSlot, true); } } diff --git a/packages/SwingSet/test/test-clist.js b/packages/SwingSet/test/test-clist.js index bfc6ca75f118..219d0b199982 100644 --- a/packages/SwingSet/test/test-clist.js +++ b/packages/SwingSet/test/test-clist.js @@ -77,8 +77,8 @@ test(`clist reachability`, async t => { t.throws(() => vk.mapVatSlotToKernelSlot('o-52'), { message: /vat tried to access unreachable import/, }); - t.is(vk.mapVatSlotToKernelSlot('o-52', false), ko3); - t.is(vk.mapKernelSlotToVatSlot(ko3, false), 'o-52'); + t.is(vk.mapVatSlotToKernelSlot('o-52', { setReachable: false }), ko3); + t.is(vk.mapKernelSlotToVatSlot(ko3, { setReachable: false }), 'o-52'); t.is(s.get(`${vatID}.c.ko3`), '_ o-52'); t.is(vk.mapVatSlotToKernelSlot('o+3'), 'ko22'); @@ -87,8 +87,8 @@ test(`clist reachability`, async t => { t.throws(() => vk.mapKernelSlotToVatSlot('ko22'), { message: /kernel sent unreachable export/, }); - t.is(vk.mapKernelSlotToVatSlot('ko22', false), 'o+3'); - t.is(vk.mapVatSlotToKernelSlot('o+3', false), 'ko22'); + t.is(vk.mapKernelSlotToVatSlot('ko22', { setReachable: false }), 'o+3'); + t.is(vk.mapVatSlotToKernelSlot('o+3', { setReachable: false }), 'ko22'); t.is(s.get(`${vatID}.c.ko22`), '_ o+3'); }); diff --git a/packages/SwingSet/test/test-state.js b/packages/SwingSet/test/test-state.js index 301bea8b6865..8f50badbbe2e 100644 --- a/packages/SwingSet/test/test-state.js +++ b/packages/SwingSet/test/test-state.js @@ -521,11 +521,12 @@ test('kernelKeeper promises', async t => { k2 = duplicateKeeper(getState); t.deepEqual(k2.getKernelPromise(p1).queue, [m1, m2]); + const ko = k.addKernelObject('v1'); // when we resolve the promise, all its queued messages are moved to the // run-queue, and its refcount remains the same const capdata = harden({ body: '{"@qclass":"slot","index":0}', - slots: ['ko44'], + slots: [ko], }); k.resolveKernelPromise(p1, false, capdata); t.deepEqual(k.getKernelPromise(p1), { @@ -547,12 +548,14 @@ test('kernelKeeper promises', async t => { ['gcActions', '[]'], ['runQueue', JSON.stringify(expectedRunqueue)], ['kd.nextID', '30'], - ['ko.nextID', '20'], + ['ko.nextID', '21'], ['kp.nextID', '41'], ['kp40.data.body', '{"@qclass":"slot","index":0}'], - ['kp40.data.slots', 'ko44'], + ['kp40.data.slots', ko], ['kp40.state', 'fulfilled'], ['kp40.refCount', '2'], + [`${ko}.owner`, 'v1'], + [`${ko}.refCount`, '1,1'], ['kernel.defaultManagerType', 'local'], ]); });