@metamask/ocap-kernel
Version:
OCap kernel core components
202 lines • 8.91 kB
JavaScript
import { Fail } from "@endo/errors";
import { getBaseMethods } from "./base.mjs";
import { getObjectMethods } from "./object.mjs";
import { getPromiseMethods } from "./promise.mjs";
import { getReachableMethods } from "./reachable.mjs";
import { getRefCountMethods } from "./refcount.mjs";
import { getSubclusterMethods } from "./subclusters.mjs";
import { getVatMethods } from "./vat.mjs";
import { insistGCActionType, insistVatId } from "../../types.mjs";
import { insistKernelType, parseKernelSlot } from "../utils/kernel-slots.mjs";
/**
* Create a store for garbage collection.
*
* @param ctx - The store context.
* @returns The GC store.
*/
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export function getGCMethods(ctx) {
const { getSlotKey, getOwnerKey } = getBaseMethods(ctx.kv);
const { getRefCount, decrementRefCount } = getRefCountMethods(ctx);
const { getObjectRefCount, deleteKernelObject } = getObjectMethods(ctx);
const { getKernelPromise, deleteKernelPromise } = getPromiseMethods(ctx);
const { getImporters, isVatTerminated } = getVatMethods(ctx);
const { getReachableFlag, getReachableAndVatSlot } = getReachableMethods(ctx);
const { clearEmptySubclusters } = getSubclusterMethods(ctx);
/**
* Get the set of GC actions to perform.
*
* @returns The set of GC actions to perform.
*/
function getGCActions() {
return new Set(JSON.parse(ctx.gcActions.get() ?? '[]'));
}
/**
* Set the set of GC actions to perform.
*
* @param actions - The set of GC actions to perform.
*/
function setGCActions(actions) {
const a = Array.from(actions);
a.sort();
ctx.gcActions.set(JSON.stringify(a));
}
/**
* Add a new GC action to the set of GC actions to perform.
*
* @param newActions - The new GC action to add.
*/
function addGCActions(newActions) {
const actions = getGCActions();
for (const action of newActions) {
assert.typeof(action, 'string', 'addGCActions given bad action');
const [vatId, type, kref] = action.split(' ');
insistVatId(vatId);
insistGCActionType(type);
insistKernelType('object', kref);
actions.add(action);
}
setGCActions(actions);
}
/**
* Schedule a vat for reaping.
*
* @param vatId - The vat to schedule for reaping.
*/
function scheduleReap(vatId) {
const queue = JSON.parse(ctx.reapQueue.get() ?? '[]');
if (!queue.includes(vatId)) {
queue.push(vatId);
ctx.reapQueue.set(JSON.stringify(queue));
}
}
/**
* Get the next reap action.
*
* @returns The next reap action, or undefined if the queue is empty.
*/
function nextReapAction() {
const queue = JSON.parse(ctx.reapQueue.get() ?? '[]');
if (queue.length > 0) {
const vatId = queue.shift();
ctx.reapQueue.set(JSON.stringify(queue));
return harden({ type: 'bringOutYourDead', vatId });
}
return undefined;
}
/**
* Retires kernel objects by notifying importers and removing the objects.
*
* @param koids - Array of kernel object IDs to retire.
*/
function retireKernelObjects(koids) {
Array.isArray(koids) || Fail `retireExports given non-Array ${koids}`;
const newActions = [];
for (const koid of koids) {
const importers = getImporters(koid);
for (const vatID of importers) {
newActions.push(`${vatID} retireImport ${koid}`);
}
deleteKernelObject(koid);
}
addGCActions(newActions);
}
/**
* Processes reference counts for kernel resources and performs garbage collection actions
* for resources that are no longer referenced or should be retired.
*/
function collectGarbage() {
const actions = new Set();
for (const kref of ctx.maybeFreeKrefs.values()) {
const { type } = parseKernelSlot(kref);
if (type === 'promise') {
const kpid = kref;
const kp = getKernelPromise(kpid);
const refCount = getRefCount(kpid);
if (refCount === 0) {
if (kp.state === 'fulfilled' || kp.state === 'rejected') {
// https://github.com/Agoric/agoric-sdk/issues/9888 don't assume promise is settled
for (const slot of kp.value?.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|promise|slot');
}
}
deleteKernelPromise(kpid);
}
}
if (type === 'object') {
const { reachable, recognizable } = getObjectRefCount(kref);
if (reachable === 0) {
const ownerKey = getOwnerKey(kref);
let ownerVatID = ctx.kv.get(ownerKey);
const terminated = isVatTerminated(ownerVatID);
// Some objects that are still owned, but the owning vat
// might still alive, or might be terminated and in the
// process of being deleted. These two clauses are
// mutually exclusive.
if (ownerVatID && !terminated) {
const vatConsidersReachable = getReachableFlag(ownerVatID, kref);
if (vatConsidersReachable) {
// the reachable count is zero, but the vat doesn't realize it
actions.add(`${ownerVatID} dropExport ${kref}`);
}
if (recognizable === 0) {
// TODO: rethink this assert
// assert.equal(vatConsidersReachable, false, `${kref} is reachable but not recognizable`);
actions.add(`${ownerVatID} retireExport ${kref}`);
}
}
else if (ownerVatID && terminated) {
// When we're slowly deleting a vat, and one of its
// exports becomes unreferenced, we obviously must not
// send dropExports or retireExports into the dead vat.
// We fast-forward the abandonment that slow-deletion
// would have done, then treat the object as orphaned.
const { vatSlot } = getReachableAndVatSlot(ownerVatID, kref);
// delete directly, not orphanKernelObject(), which
// would re-submit to maybeFreeKrefs
ctx.kv.delete(ownerKey);
ctx.kv.delete(getSlotKey(ownerVatID, kref));
ctx.kv.delete(getSlotKey(ownerVatID, vatSlot));
// now fall through to the orphaned case
ownerVatID = undefined;
}
// Now handle objects which were orphaned. NOTE: this
// includes objects which were owned by a terminated (but
// not fully deleted) vat, where `ownerVatID` was cleared
// in the last line of that previous clause (the
// fall-through case). Don't try to change this `if
// (!ownerVatID)` into an `else if`: the two clauses are
// *not* mutually-exclusive.
if (!ownerVatID) {
// orphaned and unreachable, so retire it. If the kref
// is recognizable, then we need retireKernelObjects()
// to scan for importers and send retireImports (and
// delete), else we can call deleteKernelObject directly
if (recognizable) {
retireKernelObjects([kref]);
}
else {
deleteKernelObject(kref);
}
}
}
}
}
addGCActions([...actions]);
ctx.maybeFreeKrefs.clear();
clearEmptySubclusters();
}
return {
getGCActions,
setGCActions,
addGCActions,
scheduleReap,
nextReapAction,
retireKernelObjects,
collectGarbage,
};
}
//# sourceMappingURL=gc.mjs.map