Skip to content

Commit

Permalink
Merge pull request #3513 from Agoric/3456-vom-gc
Browse files Browse the repository at this point in the history
Virtual object garbage collection
  • Loading branch information
FUDCo authored Jul 27, 2021
2 parents 0b86d4d + fe777e4 commit 459bea2
Show file tree
Hide file tree
Showing 11 changed files with 720 additions and 73 deletions.
72 changes: 31 additions & 41 deletions packages/SwingSet/src/kernel/liveSlots.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,14 +102,19 @@ function build(
const importedDevices = new Set(); // device nodes
const deadSet = new Set(); // vrefs that are finalized but not yet reported

function retainExportedRemotable(vref) {
function retainExportedVref(vref) {
// if the vref corresponds to a Remotable, keep a strong reference to it
// until the kernel tells us to release it
const { type, allocatedByVat, virtual } = parseVatSlot(vref);
if (type === 'object' && allocatedByVat && !virtual) {
const remotable = slotToVal.get(vref).deref();
assert(remotable, X`somehow lost Remotable for ${vref}`);
exportedRemotables.add(remotable);
if (type === 'object' && allocatedByVat) {
if (virtual) {
// eslint-disable-next-line no-use-before-define
vom.setExported(vref, true);
} else {
const remotable = slotToVal.get(vref).deref();
assert(remotable, X`somehow lost Remotable for ${vref}`);
exportedRemotables.add(remotable);
}
}
}

Expand Down Expand Up @@ -179,11 +184,6 @@ function build(
}
const droppedRegistry = new FinalizationRegistry(finalizeDroppedImport);

function processDroppedRepresentative(_vref) {
// no-op, to be implemented by virtual object manager
return false;
}

function processDeadSet() {
let doMore = false;
const [importsToDrop, importsToRetire, exportsToRetire] = [[], [], []];
Expand All @@ -193,17 +193,18 @@ function build(
assert(type === 'object', `unprepared to track ${type}`);
if (virtual) {
// Representative: send nothing, but perform refcount checking
doMore = doMore || processDroppedRepresentative(vref);
// 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 (!isVrefReachable(vref)) {
if (!vom.isVrefReachable(vref)) {
importsToDrop.push(vref);
// eslint-disable-next-line no-use-before-define
if (!isVrefRecognizable(vref)) {
if (!vom.isVrefRecognizable(vref)) {
importsToRetire.push(vref);
}
}
Expand Down Expand Up @@ -412,15 +413,7 @@ function build(
return wr && wr.deref();
}

const {
makeVirtualObjectRepresentative,
makeWeakStore,
makeKind,
VirtualObjectAwareWeakMap,
VirtualObjectAwareWeakSet,
isVrefReachable,
isVrefRecognizable,
} = makeVirtualObjectManager(
const vom = makeVirtualObjectManager(
syscall,
allocateExportID,
getSlotForVal,
Expand Down Expand Up @@ -506,20 +499,13 @@ function build(
// detect reanimation by playing games inside their instanceKitMaker to
// try to observe when new representatives are created (e.g., by
// counting calls or squirreling things away in hidden WeakMaps).
makeVirtualObjectRepresentative(slot, true); // N.b.: throwing away the result
vom.makeVirtualObjectRepresentative(slot, true); // N.b.: throwing away the result
}
return val;
}
if (virtual) {
// Virtual objects should never be put in the slotToVal table, as their
// entire raison d'etre is to be absent from memory when they're not being
// used. They *do* get put in the valToSlot table, which is OK because
// it's a WeakMap, but they don't get put there here. Instead, they are
// put there by makeVirtualObjectRepresentative, who already has to do
// this anyway in the cases of creating virtual objects in the first place
// and swapping them in from disk.
assert.equal(type, 'object');
val = makeVirtualObjectRepresentative(slot, false);
val = vom.makeVirtualObjectRepresentative(slot, false);
} else {
assert(!allocatedByVat, X`I don't remember allocating ${slot}`);
if (type === 'object') {
Expand Down Expand Up @@ -577,7 +563,7 @@ function build(
function collect(promiseID, rejected, value) {
doneResolutions.add(promiseID);
const valueSer = m.serialize(value);
valueSer.slots.map(retainExportedRemotable);
valueSer.slots.map(retainExportedVref);
resolutions.push([promiseID, rejected, valueSer]);
scanSlots(valueSer.slots);
}
Expand Down Expand Up @@ -609,7 +595,7 @@ function build(
}

const serArgs = m.serialize(harden(args));
serArgs.slots.map(retainExportedRemotable);
serArgs.slots.map(retainExportedVref);
const resultVPID = allocatePromiseID();
lsdebug(`Promise allocation ${forVatID}:${resultVPID} in queueMessage`);
// create a Promise which callers follow for the result, give it a
Expand Down Expand Up @@ -669,7 +655,7 @@ function build(
}
return (...args) => {
const serArgs = m.serialize(harden(args));
serArgs.slots.map(retainExportedRemotable);
serArgs.slots.map(retainExportedVref);
forbidPromises(serArgs);
const ret = syscall.callNow(slot, prop, serArgs);
insistCapData(ret);
Expand Down Expand Up @@ -849,6 +835,10 @@ function build(
if (o) {
exportedRemotables.delete(o);
}
const { virtual } = parseVatSlot(vref);
if (virtual) {
vom.setExported(vref, false);
}
}
}

Expand Down Expand Up @@ -909,13 +899,13 @@ function build(

function exitVat(completion) {
const args = m.serialize(harden(completion));
args.slots.map(retainExportedRemotable);
args.slots.map(retainExportedVref);
syscall.exit(false, args);
}

function exitVatWithFailure(reason) {
const args = m.serialize(harden(reason));
args.slots.map(retainExportedRemotable);
args.slots.map(retainExportedVref);
syscall.exit(true, args);
}

Expand All @@ -937,13 +927,13 @@ function build(
}

const vatGlobals = harden({
makeWeakStore,
makeKind,
makeWeakStore: vom.makeWeakStore,
makeKind: vom.makeKind,
});

const inescapableGlobalProperties = harden({
WeakMap: VirtualObjectAwareWeakMap,
WeakSet: VirtualObjectAwareWeakSet,
WeakMap: vom.VirtualObjectAwareWeakMap,
WeakSet: vom.VirtualObjectAwareWeakSet,
});

function setBuildRootObject(buildRootObject) {
Expand Down Expand Up @@ -989,7 +979,7 @@ function build(
const rootSlot = makeVatSlot('object', true, BigInt(0));
valToSlot.set(rootObject, rootSlot);
slotToVal.set(rootSlot, new WeakRef(rootObject));
retainExportedRemotable(rootSlot);
retainExportedVref(rootSlot);
// we do not use droppedRegistry for exports
}

Expand Down
142 changes: 110 additions & 32 deletions packages/SwingSet/src/kernel/virtualObjectManager.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
/* eslint-disable no-use-before-define */

import { assert, details as X, quote as q } from '@agoric/assert';
import { Nat } from '@agoric/nat';
import { parseVatSlot } from '../parseVatSlots.js';
// import { kdebug } from './kdebug.js';

Expand Down Expand Up @@ -50,7 +53,10 @@ export function makeCache(size, fetch, store) {
} else {
lruHead = undefined;
}
const deadEntry = lruTail;
lruTail = lruTail.prev;
deadEntry.next = undefined;
deadEntry.prev = undefined;
}
},
flush() {
Expand Down Expand Up @@ -148,7 +154,7 @@ export function makeCache(size, fetch, store) {
* instances, writing any changed state to the persistent store. This
* provided for testing; it otherwise has little use.
*
* - `makeVirtualObjectRepresentation` will provide a useeable, in-memory
* - `makeVirtualObjectRepresentative` will provide a useeable, in-memory
* version of a virtual object, given its vat slot ID. This is used when
* deserializing a reference to an object that has been received in a message
* or is part of the persistent state of another virtual object that is being
Expand Down Expand Up @@ -188,6 +194,65 @@ export function makeVirtualObjectManager(
syscall.vatstoreSet(`vom.${vobjID}`, JSON.stringify(rawData));
}

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`);
}
}
}

function getRefCounts(vobjID) {
const rawCounts = syscall.vatstoreGet(`vom.${vobjID}.refCount`);
if (rawCounts) {
return rawCounts.split(' ').map(Number);
} else {
return [0, 0];
}
}

function setRefCounts(vobjID, exported, count) {
syscall.vatstoreSet(
`vom.${vobjID}.refCount`,
`${Nat(exported)} ${Nat(count)}`,
);
if (exported === 0 && count === 0) {
possibleVirtualObjectDeath(vobjID);
}
}

function setExported(vobjID, newSetting) {
const [wasExported, refCount] = getRefCounts(vobjID);
const isNowExported = Number(newSetting);
if (wasExported !== isNowExported) {
setRefCounts(vobjID, isNowExported, refCount);
}
}

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);
}
}

// TODO: reenable once used; it's commented out just to make eslint shut up
/*
function decRefCount(vobjID) {
const [exported, oldCount] = getRefCounts(vobjID);
assert(oldCount > 0, `attempt to decref ${vobjID} below 0`);
setRefCounts(vobjID, exported, oldCount - 1);
}
*/

const cache = makeCache(cacheSize, fetch, store);

/**
Expand All @@ -212,8 +277,9 @@ export function makeVirtualObjectManager(
// We track imports, to preserve their vrefs against syscall.dropImport
// when the Presence goes away.
function addReachablePresenceRef(vref) {
const { type, allocatedByVat } = parseVatSlot(vref);
if (type === 'object' && !allocatedByVat) {
// 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);
}
}
Expand Down Expand Up @@ -264,12 +330,16 @@ export function makeVirtualObjectManager(
const reachableRemotables = new Set();
function addReachableRemotableRef(vref) {
const { type, virtual, allocatedByVat } = parseVatSlot(vref);
if (type === 'object' && !virtual && allocatedByVat) {
// 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);
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);
}
}
}

Expand Down Expand Up @@ -542,29 +612,35 @@ export function makeVirtualObjectManager(
* selves, and state data.
*
* A representative is the manifestation of a virtual object that vat code has
* direct access to. A given virtual object can have multiple
* representatives: one is created when the instance is initially made and
* another is generated each time the instance's virtual object ID is
* deserialized, either when delivered as part of an incoming message or read
* as part of another virtual object's state. These representatives are not
* === but do obey the `sameKey` equivalence relation. In particular, methods
* invoked on them all operate on the same underyling virtual object state. A
* representative is garbage collectable once it becomes unreferenced in the
* vat.
* direct access to. A given virtual object can have at most one
* representative, which will be created as needed. This will happen when the
* instance is initially made, and can also happen (if it does not already
* exist) when the instance's virtual object ID is deserialized, either when
* delivered as part of an incoming message or read as part of another virtual
* object's state. A representative will be kept alive in memory as long as
* there is a variable somewhere that references it directly or indirectly.
* However, if a representative becomes unreferenced in memory it is subject
* to garbage collection, leaving the representation that is kept in the vat
* store as the record of its state from which a mew representative can be
* reconsituted at need. Since only one representative exists at a time,
* references to them may be compared with the equality operator (===).
* Although the identity of a representative can change over time, this is
* never visible to code running in the vat. Methods invoked on a
* representative always operate on the underyling virtual object state.
*
* The inner self represents the in-memory information about an object, aside
* from its state. There is an inner self for each virtual object that is
* currently resident in memory; that is, there is an inner self for each
* virtual object for which there is currently at least one representative
* present somewhere in the vat. The inner self maintains two pieces of
* information: its corresponding virtual object's virtual object ID, and a
* pointer to the virtual object's state in memory if the virtual object's
* state is, in fact, currently resident in memory. If the state is not in
* memory, the inner self's pointer to the state is null. In addition, the
* virtual object manager maintains an LRU cache of inner selves. Inner
* selves that are in the cache are not necessarily referenced by any existing
* representative, but are available to be used should such a representative
* be needed. How this all works will be explained in a moment.
* virtual object for which there is currently a representative present
* somewhere in the vat. The inner self maintains two pieces of information:
* its corresponding virtual object's virtual object ID, and a pointer to the
* virtual object's state in memory if the virtual object's state is, in fact,
* currently resident in memory. If the state is not in memory, the inner
* self's pointer to the state is null. In addition, the virtual object
* manager maintains an LRU cache of inner selves. Inner selves that are in
* the cache are not necessarily referenced by any existing representative,
* but are available to be used should such a representative be needed. How
* this all works will be explained in a moment.
*
* The state of a virtual object is a collection of mutable properties, each
* of whose values is itself immutable and serializable. The methods of a
Expand All @@ -585,10 +661,10 @@ export function makeVirtualObjectManager(
* corresponding inner self is made to point at it, and then the inner self is
* placed at the head of the LRU cache (causing the least recently used inner
* self to fall off the end of the cache). If it *is* in memory, it is
* promoted to the head of the LRU cache but the contents of the cache remains
* unchanged. When an inner self falls off the end of the LRU, its reference
* to the state is nulled out and the object holding the state becomes garbage
* collectable.
* promoted to the head of the LRU cache but the overall contents of the cache
* remain unchanged. When an inner self falls off the end of the LRU, its
* reference to the state is nulled out and the object holding the state
* becomes garbage collectable.
*/
function makeKind(instanceKitMaker) {
const kindID = `${allocateExportID()}`;
Expand Down Expand Up @@ -712,7 +788,9 @@ export function makeVirtualObjectManager(
VirtualObjectAwareWeakSet,
isVrefReachable,
isVrefRecognizable,
setExported,
flushCache: cache.flush,
makeVirtualObjectRepresentative,
possibleVirtualObjectDeath,
});
}
Loading

0 comments on commit 459bea2

Please sign in to comment.