Skip to content

Commit d19cf9e

Browse files
committed
Process refCounts and collect garbage
1 parent 202d24c commit d19cf9e

File tree

10 files changed

+314
-72
lines changed

10 files changed

+314
-72
lines changed

packages/kernel-test/package.json

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,6 @@
5353
"@metamask/eslint-config-nodejs": "^14.0.0",
5454
"@metamask/eslint-config-typescript": "^14.0.0",
5555
"@ocap/cli": "workspace:^",
56-
"@ocap/kernel": "workspace:^",
57-
"@ocap/nodejs": "workspace:^",
58-
"@ocap/store": "workspace:^",
59-
"@ocap/streams": "workspace:^",
60-
"@ocap/utils": "workspace:^",
6156
"@ts-bridge/cli": "^0.6.2",
6257
"@ts-bridge/shims": "^0.1.1",
6358
"@typescript-eslint/eslint-plugin": "^8.26.1",
@@ -91,6 +86,10 @@
9186
"@endo/marshal": "^1.6.2",
9287
"@endo/promise-kit": "^1.1.6",
9388
"@ocap/kernel": "workspace:^",
94-
"@ocap/shims": "workspace:^"
89+
"@ocap/nodejs": "workspace:^",
90+
"@ocap/shims": "workspace:^",
91+
"@ocap/store": "workspace:^",
92+
"@ocap/streams": "workspace:^",
93+
"@ocap/utils": "workspace:^"
9594
}
9695
}

packages/kernel/src/Kernel.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ export class Kernel {
183183
for await (const item of this.#runQueueItems()) {
184184
console.log('*** run loop item', item);
185185
await this.#deliver(item);
186+
this.#kernelStore.collectGarbage();
186187
}
187188
}
188189

packages/kernel/src/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,6 @@ export {
1919
ClusterConfigStruct,
2020
} from './types.ts';
2121
export { kunser, kser } from './services/kernel-marshal.ts';
22-
export { makeKernelStore } from './store/kernel-store.ts';
23-
export type { KernelStore } from './store/kernel-store.ts';
22+
export { makeKernelStore } from './store/index.ts';
23+
export type { KernelStore } from './store/index.ts';
2424
export { parseRef } from './store/utils/parse-ref.ts';

packages/kernel/src/store/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ import { getIdMethods } from './methods/id.ts';
6464
import { getObjectMethods } from './methods/object.ts';
6565
import { getPromiseMethods } from './methods/promise.ts';
6666
import { getQueueMethods } from './methods/queue.ts';
67+
import { getReachableMethods } from './methods/reachable.ts';
6768
import { getRefCountMethods } from './methods/refcount.ts';
6869
import { getVatMethods } from './methods/vat.ts';
6970
import type { StoreContext } from './types.ts';
@@ -120,6 +121,8 @@ export function makeKernelStore(kdb: KernelDatabase) {
120121
// Garbage collection
121122
gcActions: provideCachedStoredValue('gcActions', '[]'),
122123
reapQueue: provideCachedStoredValue('reapQueue', '[]'),
124+
// TODO: Store terminated vats in DB and fetch from there
125+
terminatedVats: [],
123126
};
124127

125128
const id = getIdMethods(context);
@@ -130,6 +133,7 @@ export function makeKernelStore(kdb: KernelDatabase) {
130133
const cList = getCListMethods(context);
131134
const queue = getQueueMethods(context);
132135
const vat = getVatMethods(context);
136+
const reachable = getReachableMethods(context);
133137

134138
/**
135139
* Create a new VatStore for a vat.
@@ -159,6 +163,7 @@ export function makeKernelStore(kdb: KernelDatabase) {
159163
function reset(): void {
160164
kdb.clear();
161165
context.maybeFreeKrefs.clear();
166+
context.terminatedVats = [];
162167
context.runQueue = provideStoredQueue('run', true);
163168
context.gcActions = provideCachedStoredValue('gcActions', '[]');
164169
context.reapQueue = provideCachedStoredValue('reapQueue', '[]');
@@ -182,6 +187,7 @@ export function makeKernelStore(kdb: KernelDatabase) {
182187
...object,
183188
...promise,
184189
...gc,
190+
...reachable,
185191
...cList,
186192
...vat,
187193
makeVatStore,

packages/kernel/src/store/methods/clist.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { Fail } from '@endo/errors';
22

33
import { getBaseMethods } from './base.ts';
4-
import { getGCMethods } from './gc.ts';
54
import { getObjectMethods } from './object.ts';
5+
import { getReachableMethods } from './reachable.ts';
66
import { getRefCountMethods } from './refcount.ts';
77
import type { EndpointId, KRef, ERef } from '../../types.ts';
88
import type { StoreContext } from '../types.ts';
@@ -22,7 +22,7 @@ import {
2222
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
2323
export function getCListMethods(ctx: StoreContext) {
2424
const { getSlotKey } = getBaseMethods(ctx.kv);
25-
const { clearReachableFlag } = getGCMethods(ctx);
25+
const { clearReachableFlag } = getReachableMethods(ctx);
2626
const { getObjectRefCount, setObjectRefCount } = getObjectMethods(ctx);
2727
const { kernelRefExists, refCountKey } = getRefCountMethods(ctx);
2828

@@ -218,7 +218,7 @@ export function getCListMethods(ctx: StoreContext) {
218218
{
219219
isExport = false,
220220
onlyRecognizable = false,
221-
}: { isExport?: boolean; onlyRecognizable?: boolean },
221+
}: { isExport?: boolean; onlyRecognizable?: boolean } = {},
222222
): boolean {
223223
kref || Fail`decrementRefCount called with empty kref`;
224224

Lines changed: 126 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
1+
import { Fail } from '@endo/errors';
2+
13
import { getBaseMethods } from './base.ts';
4+
import { getCListMethods } from './clist.ts';
25
import { getObjectMethods } from './object.ts';
6+
import { getPromiseMethods } from './promise.ts';
7+
import { getReachableMethods } from './reachable.ts';
38
import { getRefCountMethods } from './refcount.ts';
9+
import { getVatMethods } from './vat.ts';
410
import type {
511
VatId,
6-
EndpointId,
712
KRef,
813
GCAction,
914
RunQueueItemBringOutYourDead,
@@ -14,12 +19,7 @@ import {
1419
RunQueueItemType,
1520
} from '../../types.ts';
1621
import type { StoreContext } from '../types.ts';
17-
import { insistKernelType } from '../utils/kernel-slots.ts';
18-
import { parseRef } from '../utils/parse-ref.ts';
19-
import {
20-
buildReachableAndVatSlot,
21-
parseReachableAndVatSlot,
22-
} from '../utils/reachable.ts';
22+
import { insistKernelType, parseKernelSlot } from '../utils/kernel-slots.ts';
2323

2424
/**
2525
* Create a store for garbage collection.
@@ -30,9 +30,12 @@ import {
3030
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
3131
export function getGCMethods(ctx: StoreContext) {
3232
const { getSlotKey } = getBaseMethods(ctx.kv);
33-
const { getObjectRefCount, setObjectRefCount } = getObjectMethods(ctx);
34-
const { kernelRefExists } = getRefCountMethods(ctx);
35-
33+
const { getRefCount } = getRefCountMethods(ctx);
34+
const { getObjectRefCount, deleteKernelObject } = getObjectMethods(ctx);
35+
const { getKernelPromise, deleteKernelPromise } = getPromiseMethods(ctx);
36+
const { decrementRefCount } = getCListMethods(ctx);
37+
const { getImporters } = getVatMethods(ctx);
38+
const { getReachableFlag, getReachableAndVatSlot } = getReachableMethods(ctx);
3639
/**
3740
* Get the set of GC actions to perform.
3841
*
@@ -71,49 +74,6 @@ export function getGCMethods(ctx: StoreContext) {
7174
setGCActions(actions);
7275
}
7376

74-
/**
75-
* Check if a kernel object is reachable.
76-
*
77-
* @param endpointId - The endpoint for which the reachable flag is being checked.
78-
* @param kref - The kref.
79-
* @returns True if the kernel object is reachable, false otherwise.
80-
*/
81-
function getReachableFlag(endpointId: EndpointId, kref: KRef): boolean {
82-
const key = getSlotKey(endpointId, kref);
83-
const data = ctx.kv.getRequired(key);
84-
const { isReachable } = parseReachableAndVatSlot(data);
85-
return isReachable;
86-
}
87-
88-
/**
89-
* Clear the reachable flag for a given endpoint and kref.
90-
*
91-
* @param endpointId - The endpoint for which the reachable flag is being cleared.
92-
* @param kref - The kref.
93-
*/
94-
function clearReachableFlag(endpointId: EndpointId, kref: KRef): void {
95-
const key = getSlotKey(endpointId, kref);
96-
const { isReachable, vatSlot } = parseReachableAndVatSlot(
97-
ctx.kv.getRequired(key),
98-
);
99-
ctx.kv.set(key, buildReachableAndVatSlot(false, vatSlot));
100-
const { direction, isPromise } = parseRef(vatSlot);
101-
// decrement 'reachable' part of refcount, but only for object imports
102-
if (
103-
isReachable &&
104-
!isPromise &&
105-
direction === 'import' &&
106-
kernelRefExists(kref)
107-
) {
108-
const counts = getObjectRefCount(kref);
109-
counts.reachable -= 1;
110-
setObjectRefCount(kref, counts);
111-
if (counts.reachable === 0) {
112-
ctx.maybeFreeKrefs.add(kref);
113-
}
114-
}
115-
}
116-
11777
/**
11878
* Schedule a vat for reaping.
11979
*
@@ -142,16 +102,124 @@ export function getGCMethods(ctx: StoreContext) {
142102
return undefined;
143103
}
144104

105+
/**
106+
* Retires kernel objects by notifying importers and removing the objects.
107+
*
108+
* @param koids - Array of kernel object IDs to retire.
109+
*/
110+
function retireKernelObjects(koids: KRef[]): void {
111+
Array.isArray(koids) || Fail`retireExports given non-Array ${koids}`;
112+
const newActions: GCAction[] = [];
113+
for (const koid of koids) {
114+
const importers = getImporters(koid);
115+
for (const vatID of importers) {
116+
newActions.push(`${vatID} retireImport ${koid}`);
117+
}
118+
deleteKernelObject(koid);
119+
}
120+
addGCActions(newActions);
121+
}
122+
123+
/**
124+
* Processes reference counts for kernel resources and performs garbage collection actions
125+
* for resources that are no longer referenced or should be retired.
126+
*/
127+
function collectGarbage(): void {
128+
const actions: Set<GCAction> = new Set();
129+
for (const kref of ctx.maybeFreeKrefs.values()) {
130+
const { type } = parseKernelSlot(kref);
131+
if (type === 'promise') {
132+
const kpid = kref;
133+
const kp = getKernelPromise(kpid);
134+
const refCount = getRefCount(kpid);
135+
if (refCount === 0) {
136+
if (kp.state === 'fulfilled' || kp.state === 'rejected') {
137+
// https://github.com/Agoric/agoric-sdk/issues/9888 don't assume promise is settled
138+
for (const slot of kp.value?.slots ?? []) {
139+
// Note: the following decrement can result in an addition to the
140+
// maybeFreeKrefs set, which we are in the midst of iterating.
141+
// TC39 went to a lot of trouble to ensure that this is kosher.
142+
decrementRefCount(slot);
143+
}
144+
}
145+
deleteKernelPromise(kpid);
146+
}
147+
}
148+
149+
if (type === 'object') {
150+
const { reachable, recognizable } = getObjectRefCount(kref);
151+
if (reachable === 0) {
152+
// We avoid ownerOfKernelObject(), which will report
153+
// 'undefined' if the owner is dead (and being slowly
154+
// deleted). Message delivery should use that, but not us.
155+
const ownerKey = `${kref}.owner`;
156+
let ownerVatID = ctx.kv.get(ownerKey);
157+
const terminated = ctx.terminatedVats.includes(ownerVatID as VatId);
158+
159+
// Some objects that are still owned, but the owning vat
160+
// might still alive, or might be terminated and in the
161+
// process of being deleted. These two clauses are
162+
// mutually exclusive.
163+
if (ownerVatID && !terminated) {
164+
const vatConsidersReachable = getReachableFlag(ownerVatID, kref);
165+
if (vatConsidersReachable) {
166+
// the reachable count is zero, but the vat doesn't realize it
167+
actions.add(`${ownerVatID} dropExport ${kref}`);
168+
}
169+
if (recognizable === 0) {
170+
// TODO: rethink this assert
171+
// assert.equal(vatConsidersReachable, false, `${kref} is reachable but not recognizable`);
172+
actions.add(`${ownerVatID} retireExport ${kref}`);
173+
}
174+
} else if (ownerVatID && terminated) {
175+
// When we're slowly deleting a vat, and one of its
176+
// exports becomes unreferenced, we obviously must not
177+
// send dropExports or retireExports into the dead vat.
178+
// We fast-forward the abandonment that slow-deletion
179+
// would have done, then treat the object as orphaned.
180+
181+
const { vatSlot } = getReachableAndVatSlot(ownerVatID, kref);
182+
// delete directly, not orphanKernelObject(), which
183+
// would re-submit to maybeFreeKrefs
184+
ctx.kv.delete(ownerKey);
185+
ctx.kv.delete(getSlotKey(ownerVatID, kref));
186+
ctx.kv.delete(getSlotKey(ownerVatID, vatSlot));
187+
// now fall through to the orphaned case
188+
ownerVatID = undefined;
189+
}
190+
191+
// Now handle objects which were orphaned. NOTE: this
192+
// includes objects which were owned by a terminated (but
193+
// not fully deleted) vat, where `ownerVatID` was cleared
194+
// in the last line of that previous clause (the
195+
// fall-through case). Don't try to change this `if
196+
// (!ownerVatID)` into an `else if`: the two clauses are
197+
// *not* mutually-exclusive.
198+
if (!ownerVatID) {
199+
// orphaned and unreachable, so retire it. If the kref
200+
// is recognizable, then we need retireKernelObjects()
201+
// to scan for importers and send retireImports (and
202+
// delete), else we can call deleteKernelObject directly
203+
if (recognizable) {
204+
retireKernelObjects([kref]);
205+
} else {
206+
deleteKernelObject(kref);
207+
}
208+
}
209+
}
210+
}
211+
}
212+
addGCActions([...actions]);
213+
ctx.maybeFreeKrefs.clear();
214+
}
215+
145216
return {
146-
// GC actions
147217
getGCActions,
148218
setGCActions,
149219
addGCActions,
150-
// Reachability tracking
151-
getReachableFlag,
152-
clearReachableFlag,
153-
// Reaping
154220
scheduleReap,
155221
nextReapAction,
222+
retireKernelObjects,
223+
collectGarbage,
156224
};
157225
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { describe, it, expect, beforeEach } from 'vitest';
2+
3+
import { makeMapKernelDatabase } from '../../../test/storage.ts';
4+
import { makeKernelStore } from '../index.ts';
5+
6+
describe('GC methods', () => {
7+
let kernelStore: ReturnType<typeof makeKernelStore>;
8+
9+
beforeEach(() => {
10+
kernelStore = makeKernelStore(makeMapKernelDatabase());
11+
});
12+
13+
describe('reachability tracking', () => {
14+
it('manages reachable flags', () => {
15+
const ko1 = kernelStore.initKernelObject('v1');
16+
kernelStore.addClistEntry('v1', ko1, 'o-1');
17+
18+
expect(kernelStore.getReachableFlag('v1', ko1)).toBe(true);
19+
20+
kernelStore.clearReachableFlag('v1', ko1);
21+
expect(kernelStore.getReachableFlag('v1', ko1)).toBe(false);
22+
23+
const refCounts = kernelStore.getObjectRefCount(ko1);
24+
expect(refCounts.reachable).toBe(0);
25+
});
26+
});
27+
});

0 commit comments

Comments
 (0)