Skip to content

Commit e934ac6

Browse files
authored
feat: Implement refcounting and vat termination cleanup (#478)
1 parent 3c0380b commit e934ac6

File tree

22 files changed

+641
-113
lines changed

22 files changed

+641
-113
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { literal } from '@metamask/superstruct';
2+
import type { Kernel } from '@ocap/kernel';
3+
import type { MethodSpec, Handler } from '@ocap/rpc-methods';
4+
import { EmptyJsonArray } from '@ocap/utils';
5+
6+
export const collectGarbageSpec: MethodSpec<
7+
'collectGarbage',
8+
EmptyJsonArray,
9+
null
10+
> = {
11+
method: 'collectGarbage',
12+
params: EmptyJsonArray,
13+
result: literal(null),
14+
};
15+
16+
export type CollectGarbageHooks = { kernel: Pick<Kernel, 'collectGarbage'> };
17+
18+
export const collectGarbageHandler: Handler<
19+
'collectGarbage',
20+
EmptyJsonArray,
21+
null,
22+
CollectGarbageHooks
23+
> = {
24+
...collectGarbageSpec,
25+
hooks: { kernel: true },
26+
implementation: async ({ kernel }): Promise<null> => {
27+
kernel.collectGarbage();
28+
return null;
29+
},
30+
};

packages/extension/src/kernel-integration/handlers/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
import { clearStateHandler, clearStateSpec } from './clear-state.ts';
2+
import {
3+
collectGarbageHandler,
4+
collectGarbageSpec,
5+
} from './collect-garbage.ts';
26
import {
37
executeDBQueryHandler,
48
executeDBQuerySpec,
@@ -33,6 +37,7 @@ export const handlers = {
3337
restartVat: restartVatHandler,
3438
sendVatCommand: sendVatCommandHandler,
3539
terminateAllVats: terminateAllVatsHandler,
40+
collectGarbage: collectGarbageHandler,
3641
terminateVat: terminateVatHandler,
3742
updateClusterConfig: updateClusterConfigHandler,
3843
} as const;
@@ -49,6 +54,7 @@ export const methodSpecs = {
4954
restartVat: restartVatSpec,
5055
sendVatCommand: sendVatCommandSpec,
5156
terminateAllVats: terminateAllVatsSpec,
57+
collectGarbage: collectGarbageSpec,
5258
terminateVat: terminateVatSpec,
5359
updateClusterConfig: updateClusterConfigSpec,
5460
} as const;

packages/extension/src/ui/App.module.css

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,10 +151,18 @@ h6 {
151151
border: none;
152152
}
153153

154+
.buttonGray {
155+
composes: button;
156+
background-color: var(--color-gray-300);
157+
color: var(--color-black);
158+
border: none;
159+
}
160+
154161
.button:hover:not(:disabled) {
155162
background-color: var(--color-gray-800);
156163
color: var(--color-white);
157164
}
165+
158166
.textButton {
159167
padding: 0;
160168
border: 0;

packages/extension/src/ui/components/KernelControls.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ import { useVats } from '../hooks/useVats.ts';
66
* @returns A panel for controlling the kernel.
77
*/
88
export const KernelControls: React.FC = () => {
9-
const { terminateAllVats, clearState, reload } = useKernelActions();
9+
const { terminateAllVats, collectGarbage, clearState, reload } =
10+
useKernelActions();
1011
const { vats } = useVats();
1112

1213
return (
@@ -16,6 +17,9 @@ export const KernelControls: React.FC = () => {
1617
Terminate All Vats
1718
</button>
1819
)}
20+
<button onClick={collectGarbage} className={styles.buttonGray}>
21+
Collect Garbage
22+
</button>
1923
<button className={styles.buttonDanger} onClick={clearState}>
2024
Clear All State
2125
</button>

packages/extension/src/ui/hooks/useKernelActions.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { usePanelContext } from '../context/PanelContext.tsx';
1111
export function useKernelActions(): {
1212
sendKernelCommand: () => void;
1313
terminateAllVats: () => void;
14+
collectGarbage: () => void;
1415
clearState: () => void;
1516
reload: () => void;
1617
launchVat: (bundleUrl: string, vatName: string) => void;
@@ -42,6 +43,18 @@ export function useKernelActions(): {
4243
.catch(() => logMessage('Failed to terminate all vats', 'error'));
4344
}, [callKernelMethod, logMessage]);
4445

46+
/**
47+
* Collects garbage.
48+
*/
49+
const collectGarbage = useCallback(() => {
50+
callKernelMethod({
51+
method: 'collectGarbage',
52+
params: [],
53+
})
54+
.then(() => logMessage('Garbage collected', 'success'))
55+
.catch(() => logMessage('Failed to collect garbage', 'error'));
56+
}, [callKernelMethod, logMessage]);
57+
4558
/**
4659
* Clears the kernel state.
4760
*/
@@ -104,6 +117,7 @@ export function useKernelActions(): {
104117
return {
105118
sendKernelCommand,
106119
terminateAllVats,
120+
collectGarbage,
107121
clearState,
108122
reload,
109123
launchVat,

packages/extension/test/e2e/vat-manager.test.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,4 +263,85 @@ test.describe('Vat Manager', () => {
263263
minimalClusterConfig.vats.main.parameters.name,
264264
);
265265
});
266+
267+
test('should collect garbage', async () => {
268+
await popupPage.click('button:text("Database Inspector")');
269+
await expect(messageOutput).toContainText(
270+
'{"key":"vats.terminated","value":"[]"}',
271+
);
272+
const v3Values = [
273+
'{"key":"e.nextPromiseId.v3","value":"2"}',
274+
'{"key":"e.nextObjectId.v3","value":"1"}',
275+
'{"key":"ko3.owner","value":"v3"}',
276+
'{"key":"v3.c.ko3","value":"R o+0"}',
277+
'{"key":"v3.c.o+0","value":"ko3"}',
278+
'{"key":"v3.c.kp3","value":"R p-1"}',
279+
'{"key":"v3.c.p-1","value":"kp3"}',
280+
];
281+
const v1ko3Values = [
282+
'{"key":"v1.c.ko3","value":"R o-2"}',
283+
'{"key":"v1.c.o-2","value":"ko3"}',
284+
'{"key":"ko3.refCount","value":"2,2"}',
285+
'{"key":"kp3.state","value":"fulfilled"}',
286+
'{"key":"kp3.value","value"',
287+
];
288+
await expect(messageOutput).toContainText(
289+
'{"key":"kp3.refCount","value":"2"}',
290+
);
291+
await expect(messageOutput).toContainText('{"key":"vatConfig.v3","value"');
292+
for (const value of v3Values) {
293+
await expect(messageOutput).toContainText(value);
294+
}
295+
for (const value of v1ko3Values) {
296+
await expect(messageOutput).toContainText(value);
297+
}
298+
await popupPage.click('button:text("Vat Manager")');
299+
await popupPage.locator('td button:text("Terminate")').last().click();
300+
await expect(messageOutput).toContainText('Terminated vat "v3"');
301+
await popupPage.locator('[data-testid="clear-logs-button"]').click();
302+
await expect(messageOutput).toContainText('');
303+
await popupPage.click('button:text("Database Inspector")');
304+
await expect(messageOutput).toContainText(
305+
'{"key":"vats.terminated","value":"[\\"v3\\"]"}',
306+
);
307+
await expect(messageOutput).not.toContainText(
308+
'{"key":"vatConfig.v3","value"',
309+
);
310+
for (const value of v3Values) {
311+
await expect(messageOutput).toContainText(value);
312+
}
313+
await popupPage.click('button:text("Vat Manager")');
314+
await popupPage.click('button:text("Collect Garbage")');
315+
await expect(messageOutput).toContainText('Garbage collected');
316+
await popupPage.locator('[data-testid="clear-logs-button"]').click();
317+
await expect(messageOutput).toContainText('');
318+
await popupPage.click('button:text("Database Inspector")');
319+
// v3 is gone
320+
for (const value of v3Values) {
321+
await expect(messageOutput).not.toContainText(value);
322+
}
323+
// ko3 reference still exists for v1
324+
for (const value of v1ko3Values) {
325+
await expect(messageOutput).toContainText(value);
326+
}
327+
// kp3 reference dropped to 1
328+
await expect(messageOutput).toContainText(
329+
'{"key":"kp3.refCount","value":"1"}',
330+
);
331+
await popupPage.click('button:text("Vat Manager")');
332+
// delete v1
333+
await popupPage.locator('td button:text("Terminate")').first().click();
334+
await expect(messageOutput).toContainText('Terminated vat "v1"');
335+
await popupPage.click('button:text("Collect Garbage")');
336+
await expect(messageOutput).toContainText('Garbage collected');
337+
await popupPage.locator('[data-testid="clear-logs-button"]').click();
338+
await expect(messageOutput).toContainText('');
339+
await popupPage.click('button:text("Database Inspector")');
340+
await expect(messageOutput).toContainText(
341+
'{"key":"vats.terminated","value":"[]"}',
342+
);
343+
for (const value of v1ko3Values) {
344+
await expect(messageOutput).not.toContainText(value);
345+
}
346+
});
266347
});

packages/kernel-test/src/garbage-collection.test.ts

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,8 @@ describe('Garbage Collection', () => {
7878
const createObjectRef = createObjectData.slots[0] as KRef;
7979
// Verify initial reference counts from database
8080
const initialRefCounts = kernelStore.getObjectRefCount(createObjectRef);
81-
expect(initialRefCounts.reachable).toBe(1);
82-
expect(initialRefCounts.recognizable).toBe(1);
81+
expect(initialRefCounts.reachable).toBe(3);
82+
expect(initialRefCounts.recognizable).toBe(3);
8383
// Send the object to the importer vat
8484
const objectRef = kunser(createObjectData);
8585
await kernel.queueMessageFromKernel(importerKRef, 'storeImport', [
@@ -95,7 +95,7 @@ describe('Garbage Collection', () => {
9595
// Check that the object is reachable as a promise from the importer vat
9696
const importerKref = kernelStore.erefToKref(importerVatId, 'p-1') as KRef;
9797
expect(kernelStore.hasCListEntry(importerVatId, importerKref)).toBe(true);
98-
expect(kernelStore.getRefCount(importerKref)).toBe(1);
98+
expect(kernelStore.getRefCount(importerKref)).toBe(2);
9999
// Use the object
100100
const useResult = await kernel.queueMessageFromKernel(
101101
importerKRef,
@@ -118,8 +118,8 @@ describe('Garbage Collection', () => {
118118

119119
// Store initial reference count information
120120
const initialRefCounts = kernelStore.getObjectRefCount(createObjectRef);
121-
expect(initialRefCounts.reachable).toBe(1);
122-
expect(initialRefCounts.recognizable).toBe(1);
121+
expect(initialRefCounts.reachable).toBe(3);
122+
expect(initialRefCounts.recognizable).toBe(3);
123123

124124
// Store the reference in the importer vat
125125
const objectRef = kunser(createObjectData);
@@ -160,8 +160,8 @@ describe('Garbage Collection', () => {
160160

161161
// Check reference counts after dropImports
162162
const afterWeakRefCounts = kernelStore.getObjectRefCount(createObjectRef);
163-
expect(afterWeakRefCounts.reachable).toBe(0);
164-
expect(afterWeakRefCounts.recognizable).toBe(1);
163+
expect(afterWeakRefCounts.reachable).toBe(2);
164+
expect(afterWeakRefCounts.recognizable).toBe(3);
165165

166166
// Now completely forget the import in the importer vat
167167
// This should trigger retireImports when GC runs
@@ -171,16 +171,15 @@ describe('Garbage Collection', () => {
171171
// Schedule another reap
172172
kernel.reapVats((vatId) => vatId === importerVatId);
173173

174-
// Run 3 cranks to allow bringOutYourDead to be processed
175174
for (let i = 0; i < 3; i++) {
176175
await kernel.queueMessageFromKernel(importerKRef, 'noop', []);
177176
await waitUntilQuiescent(500);
178177
}
179178

180-
// Check reference counts after retireImports (both should be decreased)
179+
// Check reference counts after retireImports
181180
const afterForgetRefCounts = kernelStore.getObjectRefCount(createObjectRef);
182-
expect(afterForgetRefCounts.reachable).toBe(0);
183-
expect(afterForgetRefCounts.recognizable).toBe(0);
181+
expect(afterForgetRefCounts.reachable).toBe(2);
182+
expect(afterForgetRefCounts.recognizable).toBe(2);
184183

185184
// Now forget the object in the exporter vat
186185
// This should trigger retireExports when GC runs

packages/kernel-test/src/utils.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,3 +150,13 @@ export function parseReplyBody(body: string): unknown {
150150
return body;
151151
}
152152
}
153+
154+
/**
155+
* Debug the database.
156+
*
157+
* @param kernelDatabase - The database to debug.
158+
*/
159+
export function logDatabase(kernelDatabase: KernelDatabase): void {
160+
const result = kernelDatabase.executeQuery('SELECT * FROM kv');
161+
console.log(result);
162+
}

0 commit comments

Comments
 (0)