Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions packages/kernel-test/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@
],
"scripts": {
"build": "ts-bridge --project tsconfig.build.json --clean && yarn build:vats",
"build:vats": "ocap bundle src",
"build:vats": "ocap bundle src/vats",
"build:docs": "typedoc",
"changelog:validate": "../../scripts/validate-changelog.sh @ocap/kernel-test",
"clean": "rimraf --glob './*.tsbuildinfo' ./.eslintcache ./coverage ./dist './src/*.bundle'",
"clean": "rimraf --glob './*.tsbuildinfo' ./.eslintcache ./coverage ./dist './src/**/*.bundle'",
"lint": "yarn lint:eslint && yarn lint:misc --check && yarn constraints && yarn lint:dependencies",
"lint:dependencies": "depcheck",
"lint:eslint": "eslint . --cache",
Expand All @@ -53,8 +53,6 @@
"@metamask/eslint-config-nodejs": "^14.0.0",
"@metamask/eslint-config-typescript": "^14.0.0",
"@ocap/cli": "workspace:^",
"@ocap/store": "workspace:^",
"@ocap/streams": "workspace:^",
"@ts-bridge/cli": "^0.6.3",
"@ts-bridge/shims": "^0.1.1",
"@typescript-eslint/eslint-plugin": "^8.29.0",
Expand Down Expand Up @@ -88,6 +86,10 @@
"@endo/marshal": "^1.6.4",
"@endo/promise-kit": "^1.1.10",
"@ocap/kernel": "workspace:^",
"@ocap/shims": "workspace:^"
"@ocap/nodejs": "workspace:^",
"@ocap/shims": "workspace:^",
"@ocap/store": "workspace:^",
"@ocap/streams": "workspace:^",
"@ocap/utils": "workspace:^"
}
}
207 changes: 207 additions & 0 deletions packages/kernel-test/src/garbage-collection.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
import '@ocap/shims/endoify';
import { Kernel, kunser, makeKernelStore } from '@ocap/kernel';
import type { ClusterConfig, KRef, KernelStore, VatId } from '@ocap/kernel';
import type { KernelDatabase } from '@ocap/store';
import { makeSQLKernelDatabase } from '@ocap/store/sqlite/nodejs';
import { waitUntilQuiescent } from '@ocap/utils';
import { expect, beforeEach, describe, it } from 'vitest';

import {
getBundleSpec,
makeKernel,
parseReplyBody,
runTestVats,
} from './utils.ts';

/**
* Make a test subcluster with vats for GC testing
*
* @returns The test subcluster
*/
function makeTestSubcluster(): ClusterConfig {
return {
bootstrap: 'exporter',
forceReset: true,
vats: {
exporter: {
bundleSpec: getBundleSpec('exporter-vat'),
parameters: {
name: 'Exporter',
},
},
importer: {
bundleSpec: getBundleSpec('importer-vat'),
parameters: {
name: 'Importer',
},
},
},
};
}

describe('Garbage Collection', () => {
let kernel: Kernel;
let kernelDatabase: KernelDatabase;
let kernelStore: KernelStore;
let exporterKRef: KRef;
let importerKRef: KRef;
let exporterVatId: VatId;
let importerVatId: VatId;

beforeEach(async () => {
kernelDatabase = await makeSQLKernelDatabase({
dbFilename: ':memory:',
});
kernelStore = makeKernelStore(kernelDatabase);
kernel = await makeKernel(kernelDatabase, true);
await runTestVats(kernel, makeTestSubcluster());

const vats = kernel.getVats();
exporterVatId = vats.find(
(rows) => rows.config.parameters?.name === 'Exporter',
)?.id as VatId;
importerVatId = vats.find(
(rows) => rows.config.parameters?.name === 'Importer',
)?.id as VatId;
exporterKRef = kernelStore.erefToKref(exporterVatId, 'o+0') as KRef;
importerKRef = kernelStore.erefToKref(importerVatId, 'o+0') as KRef;
});

it('objects are tracked with reference counts', async () => {
const objectId = 'test-object';
// Create an object in the exporter vat
const createObjectData = await kernel.queueMessageFromKernel(
exporterKRef,
'createObject',
[objectId],
);
const createObjectRef = createObjectData.slots[0] as KRef;
// Verify initial reference counts from database
const initialRefCounts = kernelStore.getObjectRefCount(createObjectRef);
expect(initialRefCounts.reachable).toBe(1);
expect(initialRefCounts.recognizable).toBe(1);
// Send the object to the importer vat
const objectRef = kunser(createObjectData);
await kernel.queueMessageFromKernel(importerKRef, 'storeImport', [
objectRef,
]);
await waitUntilQuiescent();
// Check that the object is reachable from the exporter vat
const exporterReachable = kernelStore.getReachableFlag(
exporterVatId,
createObjectRef,
);
expect(exporterReachable).toBe(true);
// Check that the object is reachable as a promise from the importer vat
const importerKref = kernelStore.erefToKref(importerVatId, 'p-1') as KRef;
expect(kernelStore.hasCListEntry(importerVatId, importerKref)).toBe(true);
expect(kernelStore.getRefCount(importerKref)).toBe(1);
// Use the object
const useResult = await kernel.queueMessageFromKernel(
importerKRef,
'useImport',
[],
);
await waitUntilQuiescent();
expect(parseReplyBody(useResult.body)).toBe(objectId);
});

it('should trigger GC syscalls through bringOutYourDead', async () => {
// Create an object in the exporter vat with a known ID
const objectId = 'test-object';
const createObjectData = await kernel.queueMessageFromKernel(
exporterKRef,
'createObject',
[objectId],
);
const createObjectRef = createObjectData.slots[0] as KRef;

// Store initial reference count information
const initialRefCounts = kernelStore.getObjectRefCount(createObjectRef);
expect(initialRefCounts.reachable).toBe(1);
expect(initialRefCounts.recognizable).toBe(1);

// Store the reference in the importer vat
const objectRef = kunser(createObjectData);
await kernel.queueMessageFromKernel(importerKRef, 'storeImport', [
objectRef,
objectId,
]);
await waitUntilQuiescent();

// Verify object is tracked in both vats
const importerHasObject = await kernel.queueMessageFromKernel(
importerKRef,
'listImportedObjects',
[],
);
expect(parseReplyBody(importerHasObject.body)).toContain(objectId);

const exporterHasObject = await kernel.queueMessageFromKernel(
exporterKRef,
'isObjectPresent',
[objectId],
);
expect(parseReplyBody(exporterHasObject.body)).toBe(true);

// Make a weak reference to the object in the importer vat
// This should eventually trigger dropImports when GC runs
await kernel.queueMessageFromKernel(importerKRef, 'makeWeak', [objectId]);
await waitUntilQuiescent();

// Schedule reap to trigger bringOutYourDead on next crank
kernel.reapVats((vatId) => vatId === importerVatId);

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

// Check reference counts after dropImports
const afterWeakRefCounts = kernelStore.getObjectRefCount(createObjectRef);
expect(afterWeakRefCounts.reachable).toBe(0);
expect(afterWeakRefCounts.recognizable).toBe(1);

// Now completely forget the import in the importer vat
// This should trigger retireImports when GC runs
await kernel.queueMessageFromKernel(importerKRef, 'forgetImport', []);
await waitUntilQuiescent();

// Schedule another reap
kernel.reapVats((vatId) => vatId === importerVatId);

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

// Check reference counts after retireImports (both should be decreased)
const afterForgetRefCounts = kernelStore.getObjectRefCount(createObjectRef);
expect(afterForgetRefCounts.reachable).toBe(0);
expect(afterForgetRefCounts.recognizable).toBe(0);

// Now forget the object in the exporter vat
// This should trigger retireExports when GC runs
await kernel.queueMessageFromKernel(exporterKRef, 'forgetObject', [
objectId,
]);
await waitUntilQuiescent();

// Schedule a final reap
kernel.reapVats((vatId) => vatId === exporterVatId);

// Run a crank to ensure GC completes
await kernel.queueMessageFromKernel(exporterKRef, 'noop', []);
await waitUntilQuiescent(50);

// Verify the object has been completely removed
const exporterFinalCheck = await kernel.queueMessageFromKernel(
exporterKRef,
'isObjectPresent',
[objectId],
);
expect(parseReplyBody(exporterFinalCheck.body)).toBe(false);
}, 40000);
});
39 changes: 7 additions & 32 deletions packages/kernel-test/src/liveslots.test.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import '@ocap/shims/endoify';
import { makePromiseKit } from '@endo/promise-kit';
import { Kernel } from '@ocap/kernel';
import { Kernel, kunser } from '@ocap/kernel';
import type { ClusterConfig } from '@ocap/kernel';
import { makeKernel } from '@ocap/nodejs';
import { waitUntilQuiescent } from '@ocap/utils';
import {
MessagePort as NodeMessagePort,
MessageChannel as NodeMessageChannel,
} from 'node:worker_threads';
import { beforeEach, describe, expect, it } from 'vitest';

import { kunser } from '../../kernel/src/services/kernel-marshal.ts';
import { makeKernel } from '../../nodejs/src/kernel/make-kernel.ts';
import { extractVatLogs, getBundleSpec } from './utils.ts';

const origStdoutWrite = process.stdout.write.bind(process.stdout);
let buffered: string = '';
Expand Down Expand Up @@ -73,37 +73,12 @@ describe('liveslots promise handling', () => {
testName: string,
): Promise<[unknown, string[]]> {
buffered = '';
const bundleSpec = new URL(
`${bundleName}.bundle`,
import.meta.url,
).toString();
const bundleSpec = getBundleSpec(bundleName);
const bootstrapResultRaw = await kernel.launchSubcluster(
makeTestSubcluster(testName, bundleSpec),
);

const { promise, resolve } = makePromiseKit();
setTimeout(() => resolve(null), 1000);
await promise;

const vatLogs = buffered
.split('\n')
.filter((line: string) => line.startsWith('::> '))
.map((line: string) => line.slice(4));

// de-interleave various vats' output to squeeze out interprocess I/O non-determinism in CI
vatLogs.sort((a: string, b: string): number => {
const colonA = a.indexOf(':');
if (colonA < 0) {
return 0;
}
const prefixA = a.substring(0, colonA);
const colonB = b.indexOf(':');
if (colonB < 0) {
return 0;
}
const prefixB = b.substring(0, colonB);
return prefixA.localeCompare(prefixB);
});
await waitUntilQuiescent(1000);
const vatLogs = extractVatLogs(buffered);
if (bootstrapResultRaw === undefined) {
throw Error(`this can't happen but eslint is stupid`);
}
Expand Down
Loading
Loading