Skip to content

Commit

Permalink
chore(swingset): add refcounts, implement decref processing
Browse files Browse the repository at this point in the history
This adds a new DB key for each kernel object, which contains a
comma-separated pair of (reachable, recognizable) reference counts. The
vatKeeper functions that add/remove clist entries now update these counts:

* the 'reachable' count is incremented whenever the reachable flag in the
clist entry is changed from false to true, and decremented in the other
direction
* increment/decrementRefCount take options to indicate whether the kref is
being uses as an import, or an export (since exports don't count towards
object refcounts), and whether both reachable+recognizable counts are being
changed or merely the recognizable count
* deleteCListEntry takes a flag to disable decref, which will be used when
deleting an object export (since the refcounts will be zero by that point)

`deadKernelPromises` and `purgeDeadKernelPromises` were generalized into
`maybeFreeKrefs` and `processRefcounts()`.

A few other cleanups/improvements were made:
* `kernel.dump()` adds `.objects` with refcounts
* `kernelObjectExists()` was added as a stable way to ask the question

refs #3109
  • Loading branch information
warner committed Jun 11, 2021
1 parent 75b41c0 commit 49f0d7a
Show file tree
Hide file tree
Showing 5 changed files with 258 additions and 61 deletions.
2 changes: 1 addition & 1 deletion packages/SwingSet/src/kernel/kernel.js
Original file line number Diff line number Diff line change
Expand Up @@ -633,7 +633,7 @@ export default function buildKernel(
kdebug(`vat terminated: ${JSON.stringify(info)}`);
}
if (!didAbort) {
kernelKeeper.purgeDeadKernelPromises();
kernelKeeper.processRefcounts();
kernelKeeper.saveStats();
}
commitCrank();
Expand Down
186 changes: 159 additions & 27 deletions packages/SwingSet/src/kernel/state/kernelKeeper.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand All @@ -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`);
Expand Down Expand Up @@ -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) {
Expand All @@ -766,9 +880,13 @@ export default function makeKernelKeeper(kvStore, streamStore, kernelSlog) {
vatID,
addKernelObject,
addKernelPromiseForVat,
kernelObjectExists,
incrementRefCount,
decrementRefCount,
getObjectRefCount,
setObjectRefCount,
getReachableAndVatSlot,
addMaybeFreeKref,
incStat,
decStat,
getCrankNumber,
Expand Down Expand Up @@ -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();

Expand All @@ -950,6 +1079,7 @@ export default function makeKernelKeeper(kvStore, streamStore, kernelSlog) {
vatTables,
kernelTable,
promises,
objects,
gcActions,
runQueue,
});
Expand All @@ -965,7 +1095,7 @@ export default function makeKernelKeeper(kvStore, streamStore, kernelSlog) {

getCrankNumber,
incrementCrankNumber,
purgeDeadKernelPromises,
processRefcounts,

incStat,
decStat,
Expand All @@ -980,6 +1110,7 @@ export default function makeKernelKeeper(kvStore, streamStore, kernelSlog) {
addKernelObject,
ownerOfKernelObject,
ownerOfKernelDevice,
kernelObjectExists,
getImporters,
deleteKernelObject,

Expand All @@ -995,6 +1126,7 @@ export default function makeKernelKeeper(kvStore, streamStore, kernelSlog) {
clearDecider,
incrementRefCount,
decrementRefCount,
getObjectRefCount,

addToRunQueue,
isRunQueueEmpty,
Expand Down
Loading

0 comments on commit 49f0d7a

Please sign in to comment.