diff --git a/packages/SwingSet/src/controller/initializeKernel.js b/packages/SwingSet/src/controller/initializeKernel.js index 06f128cfbfb..96902c93b08 100644 --- a/packages/SwingSet/src/controller/initializeKernel.js +++ b/packages/SwingSet/src/controller/initializeKernel.js @@ -81,7 +81,6 @@ export async function initializeKernel(config, kernelStorage, options = {}) { 'managerType', 'enableDisavow', 'enableSetup', - 'virtualObjectCacheSize', 'useTranscript', 'critical', 'reapInterval', diff --git a/packages/SwingSet/src/kernel/vat-loader/manager-factory.js b/packages/SwingSet/src/kernel/vat-loader/manager-factory.js index 097564d62dd..067bd8e3fac 100644 --- a/packages/SwingSet/src/kernel/vat-loader/manager-factory.js +++ b/packages/SwingSet/src/kernel/vat-loader/manager-factory.js @@ -36,7 +36,6 @@ export function makeVatManagerFactory({ 'metered', 'enableDisavow', 'enableSetup', - 'virtualObjectCacheSize', 'useTranscript', 'critical', 'reapInterval', diff --git a/packages/SwingSet/src/kernel/vat-loader/vat-loader.js b/packages/SwingSet/src/kernel/vat-loader/vat-loader.js index f643025c0d2..cd7b8f3ee18 100644 --- a/packages/SwingSet/src/kernel/vat-loader/vat-loader.js +++ b/packages/SwingSet/src/kernel/vat-loader/vat-loader.js @@ -26,7 +26,6 @@ export function makeVatLoader(stuff) { 'enableDisavow', 'enableSetup', 'enablePipelining', - 'virtualObjectCacheSize', 'useTranscript', 'critical', 'reapInterval', @@ -92,7 +91,6 @@ export function makeVatLoader(stuff) { enableSetup = false, enableDisavow = false, enablePipelining = false, - virtualObjectCacheSize, useTranscript = true, critical = false, name, @@ -128,7 +126,6 @@ export function makeVatLoader(stuff) { ...overrideVatManagerOptions, }; const liveSlotsOptions = { - virtualObjectCacheSize, enableDisavow, relaxDurabilityRules: kernelKeeper.getRelaxDurabilityRules(), }; diff --git a/packages/SwingSet/src/types-external.js b/packages/SwingSet/src/types-external.js index 43c0b86f2df..192fccd4606 100644 --- a/packages/SwingSet/src/types-external.js +++ b/packages/SwingSet/src/types-external.js @@ -285,7 +285,6 @@ export {}; * waiting for the promises to be resolved. If false, such * messages will be queued inside the kernel. Defaults to * false. - * @property { number } [virtualObjectCacheSize] * @property { boolean } [useTranscript] * If true, saves a transcript of a vat's inbound deliveries and * outbound syscalls so that the vat's internal state can be diff --git a/packages/SwingSet/src/types-internal.js b/packages/SwingSet/src/types-internal.js index c480fff9977..758c28efa80 100644 --- a/packages/SwingSet/src/types-internal.js +++ b/packages/SwingSet/src/types-internal.js @@ -33,7 +33,6 @@ export {}; * critical: boolean, * enableDisavow: boolean, * useTranscript: boolean, - * virtualObjectCacheSize: number, * name: string, * compareSyscalls?: (originalSyscall: {}, newSyscall: {}) => Error | undefined, * sourcedConsole: Pick, diff --git a/packages/SwingSet/src/vats/vat-admin/vat-vat-admin.js b/packages/SwingSet/src/vats/vat-admin/vat-vat-admin.js index d7d117f08ea..1a6a4795de1 100644 --- a/packages/SwingSet/src/vats/vat-admin/vat-vat-admin.js +++ b/packages/SwingSet/src/vats/vat-admin/vat-vat-admin.js @@ -314,9 +314,6 @@ export function buildRootObject(vatPowers, _vatParameters, baggage) { 'invalid reapInterval value', ); break; - case 'virtualObjectCacheSize': - assert(isNat(value), 'invalid virtualObjectCacheSize value'); - break; default: assert.fail(`invalid option "${option}"`); } @@ -357,7 +354,6 @@ export function buildRootObject(vatPowers, _vatParameters, baggage) { vatParameters, // stripped out and re-added enableSetup, enablePipelining, - virtualObjectCacheSize, useTranscript, reapInterval, critical, // converted from cap key to boolean @@ -391,7 +387,6 @@ export function buildRootObject(vatPowers, _vatParameters, baggage) { } assertType('enableSetup', enableSetup, 'boolean'); assertType('enablePipelining', enablePipelining, 'boolean'); - assertType('virtualObjectCacheSize', virtualObjectCacheSize, 'number'); assertType('useTranscript', useTranscript, 'boolean'); assertType('reapInterval', reapInterval, 'number'); @@ -423,7 +418,6 @@ export function buildRootObject(vatPowers, _vatParameters, baggage) { vatParameters, enableSetup, enablePipelining, - virtualObjectCacheSize, useTranscript, reapInterval, critical: isCriticalVat, diff --git a/packages/SwingSet/test/change-parameters/test-change-parameters.js b/packages/SwingSet/test/change-parameters/test-change-parameters.js index b5f0bae078d..251dcd6f8ae 100644 --- a/packages/SwingSet/test/change-parameters/test-change-parameters.js +++ b/packages/SwingSet/test/change-parameters/test-change-parameters.js @@ -64,19 +64,11 @@ async function testChangeParameters(t) { t.is(c1Status, 'fulfilled'); t.is(c1Result, 'invalid option "foo"'); - const [c2Status, c2Result] = await run('change', [ - { virtualObjectCacheSize: -1 }, - ]); - t.is(c2Status, 'fulfilled'); - t.is(c2Result, 'invalid virtualObjectCacheSize value'); - const [c3Status, c3Result] = await run('change', [{ reapInterval: 'maybe' }]); t.is(c3Status, 'fulfilled'); t.is(c3Result, 'invalid reapInterval value'); - const [c4Status, c4Result] = await run('change', [ - { virtualObjectCacheSize: 100, reapInterval: 20 }, - ]); + const [c4Status, c4Result] = await run('change', [{ reapInterval: 20 }]); t.is(c4Status, 'fulfilled'); t.is(c4Result, 'ok'); t.is(kvStore.get('v6.reapInterval'), '20'); diff --git a/packages/SwingSet/test/upgrade/vat-ulrik-1.js b/packages/SwingSet/test/upgrade/vat-ulrik-1.js index aefcf5e9298..a69f2a5810b 100644 --- a/packages/SwingSet/test/upgrade/vat-ulrik-1.js +++ b/packages/SwingSet/test/upgrade/vat-ulrik-1.js @@ -28,7 +28,6 @@ const holderMethods = { }; const makeVir = defineKind('virtual', initHolder, holderMethods); const makeDur = defineDurableKind(durandalHandle, initHolder, holderMethods); -const makeDummy = defineKind('dummy', initHolder, holderMethods); // TODO: explore 'export modRetains' // eslint-disable-next-line no-unused-vars @@ -115,13 +114,6 @@ const buildExports = (baggage, imp) => { baggage.init('dur37', dur[37]); baggage.init('imp38', imp[38]); - // we set virtualObjectCacheSize=0 to ensure all data writes are - // made promptly, But the cache will still retain the last - // Representative, which inhibits GC. So the last thing we do here - // should be to create/deserialize a throwaway object, to - // flush the last dur/vir/vc/dc from the cache. - makeDummy(); - // we share dur1/vir2 with the test harness so it can glean the // baserefs and interpolate the full vrefs for everything else // without holding a GC pin on them diff --git a/packages/SwingSet/test/vat-admin/terminate/test-terminate-replay.js b/packages/SwingSet/test/vat-admin/terminate/test-terminate-replay.js index 180b92c58a4..5fc20b1eaa0 100644 --- a/packages/SwingSet/test/vat-admin/terminate/test-terminate-replay.js +++ b/packages/SwingSet/test/vat-admin/terminate/test-terminate-replay.js @@ -19,6 +19,7 @@ test.serial('replay does not resurrect dead vat', async t => { const configPath = new URL('swingset-no-zombies.json', import.meta.url) .pathname; const config = await loadSwingsetConfigFile(configPath); + config.defaultReapInterval = 'never'; const ss1 = initSwingStore(); { diff --git a/packages/SwingSet/test/vat-admin/terminate/test-terminate.js b/packages/SwingSet/test/vat-admin/terminate/test-terminate.js index 1bc8c73572c..d0115310a6d 100644 --- a/packages/SwingSet/test/vat-admin/terminate/test-terminate.js +++ b/packages/SwingSet/test/vat-admin/terminate/test-terminate.js @@ -41,6 +41,7 @@ async function doTerminateNonCritical( const configPath = new URL('swingset-terminate.json', import.meta.url) .pathname; const config = await loadSwingsetConfigFile(configPath); + config.defaultReapInterval = 'never'; const kernelStorage = initSwingStore().kernelStorage; const controller = await buildVatController(config, [], { ...t.context.data, @@ -104,6 +105,7 @@ async function doTerminateCritical( const configPath = new URL('swingset-terminate.json', import.meta.url) .pathname; const config = await loadSwingsetConfigFile(configPath); + config.defaultReapInterval = 'never'; const kernelStorage = initSwingStore().kernelStorage; const controller = await buildVatController(config, [], { ...t.context.data, @@ -374,6 +376,7 @@ test.serial('exit with presence', async t => { const configPath = new URL('swingset-die-with-presence.json', import.meta.url) .pathname; const config = await loadSwingsetConfigFile(configPath); + config.defaultReapInterval = 'never'; const controller = await buildVatController(config, [], t.context.data); t.teardown(controller.shutdown); await controller.run(); @@ -389,6 +392,7 @@ test.serial('dispatches to the dead do not harm kernel', async t => { const configPath = new URL('swingset-speak-to-dead.json', import.meta.url) .pathname; const config = await loadSwingsetConfigFile(configPath); + config.defaultReapInterval = 'never'; const ss1 = initSwingStore(); { @@ -432,6 +436,7 @@ test.serial('invalid criticalVatKey causes vat creation to fail', async t => { const configPath = new URL('swingset-bad-vat-key.json', import.meta.url) .pathname; const config = await loadSwingsetConfigFile(configPath); + config.defaultReapInterval = 'never'; const controller = await buildVatController(config, [], t.context.data); t.teardown(controller.shutdown); await t.throwsAsync(() => controller.run(), { @@ -443,6 +448,7 @@ test.serial('dead vat state removed', async t => { const configPath = new URL('swingset-die-cleanly.json', import.meta.url) .pathname; const config = await loadSwingsetConfigFile(configPath); + config.defaultReapInterval = 'never'; const { kernelStorage, debug } = initSwingStore(); const controller = await buildVatController(config, [], { @@ -480,6 +486,7 @@ test.serial('terminate with presence', async t => { import.meta.url, ).pathname; const config = await loadSwingsetConfigFile(configPath); + config.defaultReapInterval = 'never'; const controller = await buildVatController(config, [], t.context.data); t.teardown(controller.shutdown); await controller.run(); diff --git a/packages/SwingSet/test/virtualObjects/collection-slots/test-collection-slots.js b/packages/SwingSet/test/virtualObjects/collection-slots/test-collection-slots.js index f28317b0f64..5cf9149af44 100644 --- a/packages/SwingSet/test/virtualObjects/collection-slots/test-collection-slots.js +++ b/packages/SwingSet/test/virtualObjects/collection-slots/test-collection-slots.js @@ -30,9 +30,6 @@ test('collection entry slots trigger doMoreGC', async t => { bootstrap: { sourceSpec: bfile('bootstrap-collection-slots.js') }, target: { sourceSpec: bfile('vat-collection-slots.js'), - creationOptions: { - virtualObjectCacheSize: 0, - }, }, }, }; diff --git a/packages/SwingSet/test/virtualObjects/delete-stored-vo/test-delete-stored-vo.js b/packages/SwingSet/test/virtualObjects/delete-stored-vo/test-delete-stored-vo.js index d01532fb89e..5484513e0ed 100644 --- a/packages/SwingSet/test/virtualObjects/delete-stored-vo/test-delete-stored-vo.js +++ b/packages/SwingSet/test/virtualObjects/delete-stored-vo/test-delete-stored-vo.js @@ -29,9 +29,6 @@ test('VO property deletion is not short-circuited', async t => { bootstrap: { sourceSpec: bfile('bootstrap-delete-stored-vo.js') }, target: { sourceSpec: bfile('vat-delete-stored-vo.js'), - creationOptions: { - virtualObjectCacheSize: 0, - }, }, }, }; diff --git a/packages/SwingSet/test/virtualObjects/double-retire-import/bootstrap-dri.js b/packages/SwingSet/test/virtualObjects/double-retire-import/bootstrap-dri.js index 291ff7cf525..496e4dd9bb2 100644 --- a/packages/SwingSet/test/virtualObjects/double-retire-import/bootstrap-dri.js +++ b/packages/SwingSet/test/virtualObjects/double-retire-import/bootstrap-dri.js @@ -14,9 +14,7 @@ export function buildRootObject() { async build() { // build the target vat const bcap = await E(vatAdmin).getNamedBundleCap('dri'); - const options = {}; - options.virtualObjectCacheSize = 0; - const res = await E(vatAdmin).createVat(bcap, options); + const res = await E(vatAdmin).createVat(bcap); root = res.root; await E(root).buildVir(sensor0, sensor1); await E(root).ping(); diff --git a/packages/SwingSet/test/virtualObjects/test-facet-retention.js b/packages/SwingSet/test/virtualObjects/test-facet-retention.js index ce7bdf6dcc1..4d0d28d1f66 100644 --- a/packages/SwingSet/test/virtualObjects/test-facet-retention.js +++ b/packages/SwingSet/test/virtualObjects/test-facet-retention.js @@ -52,9 +52,14 @@ import { kunser } from '../../src/lib/kmarshal.js'; // Representative objects). All facets of a given cohort/instance // share the same 'context' object. -// Currently, we create the 'context' and 'state' objects via an LRU -// cache, so their lifetime is somewhat complicated, but still a -// deterministic function of userspace behavior. +// In any given crank, a new 'context'/'state' object pair is created +// the first time a VO method is invoked, and is given to all VO +// method invocations within that crank. They remain functional +// forever. The VOM discards the context/state pair at end-of-crank, +// and will make new ones in subsequent cranks (if they invoke VO +// methods again). Userspace might retain either one, and use them in +// a recognizer, but their lifetime is not related to VO GC behavior, +// so they do not provide a GC sensor, merely a crank sensor. // The 'facets' cohort is a record, one property per facet. So a // defineKindMulti with a pair of `incrementer` and `decrementer` @@ -150,16 +155,16 @@ test('retention', async t => { await go('multi', 'method', 'weakset', true); await go('multi', 'proto', 'weakset', true); - // 'context' currently shares a lifetime with the facet cluster - await go('single', 'context', 'retain', true); - await go('multi', 'context', 'retain', true); - await go('single', 'context', 'weakset', true); - await go('multi', 'context', 'weakset', true); - // as does 'state' - await go('single', 'state', 'retain', true); - await go('multi', 'state', 'retain', true); - await go('single', 'state', 'weakset', true); - await go('multi', 'state', 'weakset', true); + // 'context' is remade on each crank + await go('single', 'context', 'retain', false); + await go('multi', 'context', 'retain', false); + await go('single', 'context', 'weakset', false); + await go('multi', 'context', 'weakset', false); + // as is 'state' + await go('single', 'state', 'retain', false); + await go('multi', 'state', 'retain', false); + await go('single', 'state', 'weakset', false); + await go('multi', 'state', 'weakset', false); if (remaining.size) { const missed = [...remaining].join(', '); diff --git a/packages/SwingSet/test/virtualObjects/test-representatives.js b/packages/SwingSet/test/virtualObjects/test-representatives.js index 556d4d97590..0336c6d0d22 100644 --- a/packages/SwingSet/test/virtualObjects/test-representatives.js +++ b/packages/SwingSet/test/virtualObjects/test-representatives.js @@ -17,28 +17,27 @@ import { vstr } from '../util.js'; test.serial('exercise cache', async t => { const config = { includeDevDependencies: true, // for vat-data - bootstrap: 'bootstrap', + defaultManagerType: 'xs-worker', // for stability against GC + defaultReapInterval: 1, // for explicitness (kernel defaults to 1 anyways) + // no bootstrap, to remove bootstrap(vats), to remove noise of GC drops + // bootstrap: 'bootstrap', vats: { - bootstrap: { - sourceSpec: new URL('vat-representative-bootstrap.js', import.meta.url) - .pathname, - creationOptions: { - virtualObjectCacheSize: 3, - }, + representatives: { + sourceSpec: new URL('vat-representatives.js', import.meta.url).pathname, }, }, }; const log = []; - const expectedVatID = 'v1'; + let vatID; // set after initialization finishes const kernelStorage = initSwingStore().kernelStorage; const kvStore = kernelStorage.kvStore; function vsKey(key) { // ignore everything except vatStores on the one vat under test // (especially ignore comms, which performs vatstore operations during // startup) - return key.startsWith(`${expectedVatID}.`) && key.match(/^\w+\.vs\./); + return vatID && key.startsWith(`${vatID}.`) && key.match(/^\w+\.vs\./); } const loggingKVStore = { has: key => kvStore.has(key), @@ -67,15 +66,13 @@ test.serial('exercise cache', async t => { ...kernelStorage, kvStore: loggingKVStore, }; - - const bootstrapResult = await initializeSwingset( - config, - [], - loggingKernelStorage, - ); + // TODO: it'd be nice to { addVatAdmin: false } too, but kernel is stubborn + const initOpts = { addComms: false, addVattp: false, addTimer: false }; + await initializeSwingset(config, [], loggingKernelStorage, initOpts); const c = await makeSwingsetController(loggingKernelStorage, {}); t.teardown(c.shutdown); - c.pinVatRoot('bootstrap'); + c.pinVatRoot('representatives'); + vatID = c.vatNameToID('representatives'); const nextLog = makeNextLog(c); @@ -84,7 +81,7 @@ test.serial('exercise cache', async t => { if (what) { sendArgs = [kslot(what, 'thing'), ...args]; } - const r = c.queueToVatRoot('bootstrap', method, sendArgs, 'ignore'); + const r = c.queueToVatRoot('representatives', method, sendArgs, 'ignore'); await c.run(); t.is(c.kpStatus(r), 'fulfilled'); t.deepEqual(nextLog(), []); @@ -118,13 +115,13 @@ test.serial('exercise cache', async t => { await doSimple('holdThing', what); } function dataKey(num) { - return `v1.vs.vom.o+v10/${num}`; + return `${vatID}.vs.vom.o+v10/${num}`; } function esKey(num) { - return `v1.vs.vom.es.o+v10/${num}`; + return `${vatID}.vs.vom.es.o+v10/${num}`; } function rcKey(num) { - return `v1.vs.vom.rc.o+v10/${num}`; + return `${vatID}.vs.vom.rc.o+v10/${num}`; } function thingVal(name) { return JSON.stringify({ @@ -141,158 +138,242 @@ test.serial('exercise cache', async t => { } // expected kernel object ID allocations - const T1 = 'ko25'; - const T2 = 'ko26'; - const T3 = 'ko27'; - const T4 = 'ko28'; - const T5 = 'ko29'; - const T6 = 'ko30'; - const T7 = 'ko31'; - const T8 = 'ko32'; - - // these tests are hard-coded to expect our vat-under-test to be 'v1', so - // double-check here - t.is(c.vatNameToID('bootstrap'), expectedVatID); + const T1 = 'ko22'; + const T2 = 'ko23'; + const T3 = 'ko24'; + const T4 = 'ko25'; + const T5 = 'ko26'; + const T6 = 'ko27'; + const T7 = 'ko28'; + const T8 = 'ko29'; await c.run(); - t.deepEqual(c.kpResolution(bootstrapResult), kser('bootstrap done')); log.length = 0; // assume all the irrelevant setup stuff worked correctly - // init cache - [] + // note: defaultReapInterval=1, so every operation is followed by a + // BOYD. We aren't asserting the separation between the vatstore + // get/sets that happen during the real operation and during the + // subsequent BOYD, but we'll annotate them here for clarity. Also + // note that this test was more important back when we had a cache + // that spanned multiple cranks, whereas the current implementation + // is explicitly flushed at the end of every delivery. - await make('thing1', true, T1); // make t1 - [t1] + // thing1 is exported (so rs get/set) + await make('thing1', true, T1); ck('get', esKey(1), undefined); ck('set', esKey(1), 'r'); + // end-of-crank: ck('set', dataKey(1), thingVal('thing1')); + // BOYD: the Representative is held in RAM, no extra queries done(); - await make('thing2', false, T2); // make t2 - [t2 t1] + await make('thing2', false, T2); ck('get', esKey(2), undefined); ck('set', esKey(2), 'r'); + // end-of-crank: ck('set', dataKey(2), thingVal('thing2')); + // BOYD: thing2 is not held, so the Representative drops, causing + // extra rc/es queries to decide whether to delete or not + ck('get', rcKey(2), undefined); + ck('get', esKey(2), 'r'); + done(); + + await read(T1, 'thing1'); // still in RAM + // T1 was in RAM, so no new representative was needed, but creating + // a Representative wouldn't cause a data read. However invoking a + // method *does* require a data read, one per crank + ck('get', dataKey(1), thingVal('thing1')); + // end-of-crank: (none) + // BOYD: (none): thing1 is held, no extra queries done(); - await read(T1, 'thing1'); // refresh t1 - [t1 t2] - await read(T2, 'thing2'); // refresh t2 - [t2 t1] - await readHeld('thing1'); // refresh t1 - [t1 t2] + await read(T2, 'thing2'); // reanimated + // T2 was not in RAM, so reanimateVO() makes a new Representative, + // but that doesn't cause a data read. But invoking a method does. + ck('get', dataKey(2), thingVal('thing2')); + // end-of-crank: (none) + // BOYD: thing2 is dropped, so extra queries + ck('get', rcKey(2), undefined); + ck('get', esKey(2), 'r'); + done(); + + await readHeld('thing1'); // still in RAM + // same story: one data read per crank when a method is invoked + ck('get', dataKey(1), thingVal('thing1')); + // end-of-crank: (none) + // BOYD: (none) + done(); - await make('thing3', false, T3); // make t3 - [t3 t1 t2] + await make('thing3', false, T3); ck('get', esKey(3), undefined); ck('set', esKey(3), 'r'); + // end-of-crank: write T3 data ck('set', dataKey(3), thingVal('thing3')); + // BOYD: T3 Representative dropped + ck('get', rcKey(3), undefined); + ck('get', esKey(3), 'r'); done(); - await make('thing4', false, T4); // make t4 - [t4 t3 t1 t2] + await make('thing4', false, T4); ck('get', esKey(4), undefined); ck('set', esKey(4), 'r'); + // end-of-crank: write T4 data ck('set', dataKey(4), thingVal('thing4')); + // BOYD: T4 Representative dropped + ck('get', rcKey(4), undefined); + ck('get', esKey(4), 'r'); done(); - await make('thing5', false, T5); // evict t2, make t5 - [t5 t4 t3 t1] + await make('thing5', false, T5); ck('get', esKey(5), undefined); ck('set', esKey(5), 'r'); + // end-of-crank: write T5 data ck('set', dataKey(5), thingVal('thing5')); - ck('get', rcKey(2), undefined); - ck('get', esKey(2), 'r'); + // BOYD: T5 Representative dropped + ck('get', rcKey(5), undefined); + ck('get', esKey(5), 'r'); done(); - await make('thing6', false, T6); // evict t1, make t6 - [t6 t5 t4 t3] + await make('thing6', false, T6); ck('get', esKey(6), undefined); ck('set', esKey(6), 'r'); + // end-of-crank: write T6 data ck('set', dataKey(6), thingVal('thing6')); + // BOYD: T6 Representative dropped + ck('get', rcKey(6), undefined); + ck('get', esKey(6), 'r'); done(); - await make('thing7', false, T7); // evict t3, make t7 - [t7 t6 t5 t4] + await make('thing7', false, T7); ck('get', esKey(7), undefined); ck('set', esKey(7), 'r'); + // end-of-crank: write T7 data ck('set', dataKey(7), thingVal('thing7')); - ck('get', rcKey(3), undefined); - ck('get', esKey(3), 'r'); + // BOYD: T7 Representative dropped + ck('get', rcKey(7), undefined); + ck('get', esKey(7), 'r'); done(); - await make('thing8', false, T8); // evict t4, make t8 - [t8 t7 t6 t5] + await make('thing8', false, T8); ck('get', esKey(8), undefined); ck('set', esKey(8), 'r'); + // end-of-crank: write T8 data ck('set', dataKey(8), thingVal('thing8')); - ck('get', rcKey(4), undefined); - ck('get', esKey(4), 'r'); + // BOYD: T8 Representative dropped + ck('get', rcKey(8), undefined); + ck('get', esKey(8), 'r'); done(); - await read(T2, 'thing2'); // reanimate t2, evict t5 - [t2 t8 t7 t6] + await read(T2, 'thing2'); // reanimate t2 ck('get', dataKey(2), thingVal('thing2')); - ck('get', rcKey(5), undefined); - ck('get', esKey(5), 'r'); + // end-of-crank: (none) + // BOYD: T2 Representative dropped + ck('get', rcKey(2), undefined); + ck('get', esKey(2), 'r'); done(); - await readHeld('thing1'); // reanimate t1, evict t6 - [t1 t2 t8 t7] + await readHeld('thing1'); // still in RAM ck('get', dataKey(1), thingVal('thing1')); - ck('get', rcKey(6), undefined); - ck('get', esKey(6), 'r'); + // end-of-crank: (none) + // BOYD: (none) done(); - await write(T2, 'thing2 updated'); // refresh t2 - [t2 t1 t8 t7] + await write(T2, 'thing2 updated'); // reanimated + ck('get', dataKey(2), thingVal('thing2')); + // end-of-crank: flush T2 ck('set', dataKey(2), thingVal('thing2 updated')); + // BOYD: T2 Representative dropped + ck('get', rcKey(2), undefined); + ck('get', esKey(2), 'r'); + done(); - await writeHeld('thing1 updated'); // refresh t1 - [t1 t2 t8 t7] + await writeHeld('thing1 updated'); // still in RAM + ck('get', dataKey(1), thingVal('thing1')); // but data is not + // end-of-crank: flush T1 ck('set', dataKey(1), thingVal('thing1 updated')); + // BOYD: (none) + done(); - await read(T8, 'thing8'); // refresh t8 - [t8 t1 t2 t7] - await read(T7, 'thing7'); // refresh t7 - [t7 t8 t1 t2] + await read(T8, 'thing8'); // reanimated T8 + ck('get', dataKey(8), thingVal('thing8')); + // end-of-crank: (none) + // BOYD: T8 Representative dropped + ck('get', rcKey(8), undefined); + ck('get', esKey(8), 'r'); done(); - await read(T6, 'thing6'); // reanimate t6, evict t2 - [t6 t7 t8 t1] + await read(T6, 'thing6'); // reanimate t6 ck('get', dataKey(6), thingVal('thing6')); - ck('get', rcKey(2), undefined); - ck('get', esKey(2), 'r'); + // end-of-crank: (none) + // BOYD: T6 Representative dropped + ck('get', rcKey(6), undefined); + ck('get', esKey(6), 'r'); done(); - await read(T5, 'thing5'); // reanimate t5, evict t1 - [t5 t6 t7 t8] + await read(T5, 'thing5'); // reanimate t5 ck('get', dataKey(5), thingVal('thing5')); + // end-of-crank: (none) + // BOYD: T5 Representative dropped + ck('get', rcKey(5), undefined); + ck('get', esKey(5), 'r'); done(); - await read(T4, 'thing4'); // reanimate t4, evict t8 - [t4 t5 t6 t7] + await read(T4, 'thing4'); // reanimate t4 ck('get', dataKey(4), thingVal('thing4')); - ck('get', rcKey(8), undefined); - ck('get', esKey(8), 'r'); + // end-of-crank: (none) + // BOYD: T4 Representative dropped + ck('get', rcKey(4), undefined); + ck('get', esKey(4), 'r'); done(); - await read(T3, 'thing3'); // reanimate t3, evict t7 - [t3 t4 t5 t6] + await read(T3, 'thing3'); // reanimate t3 ck('get', dataKey(3), thingVal('thing3')); - ck('get', rcKey(7), undefined); - ck('get', esKey(7), 'r'); + // end-of-crank: (none) + // BOYD: T3 Representative dropped + ck('get', rcKey(3), undefined); + ck('get', esKey(3), 'r'); done(); - await read(T2, 'thing2 updated'); // reanimate t2, evict t6 - [t2 t3 t4 t5] + await read(T2, 'thing2 updated'); // reanimate t2 ck('get', dataKey(2), thingVal('thing2 updated')); - ck('get', rcKey(6), undefined); - ck('get', esKey(6), 'r'); + // end-of-crank: (none) + // BOYD: T2 Representative dropped + ck('get', rcKey(2), undefined); + ck('get', esKey(2), 'r'); done(); - await readHeld('thing1 updated'); // reanimate t1, evict t5 - [t1 t2 t3 t4] + await readHeld('thing1 updated'); // reanimate t1 ck('get', dataKey(1), thingVal('thing1 updated')); - ck('get', rcKey(5), undefined); - ck('get', esKey(5), 'r'); + // end-of-crank: (none) + // BOYD: (none) done(); - await forgetHeld(); // cache unchanged - [t1 t2 t3 t4] + await forgetHeld(); + // end-of-crank: (none) + // BOYD: T1 Representative dropped ck('get', rcKey(1), undefined); ck('get', esKey(1), 'r'); done(); - await hold(T8); // cache unchanged - [t1 t2 t3 t4] - ck('get', dataKey(8), thingVal('thing8')); - ck('get', rcKey(4), undefined); - ck('get', esKey(4), 'r'); + await hold(T8); // reanimate T8, add to RAM + // we don't invoke any methods, so we don't need its data + // end-of-crank: (none) + // BOYD: (none) done(); - await read(T7, 'thing7'); // reanimate t7, evict t4 - [t7 t1 t2 t3] + await read(T7, 'thing7'); // reanimate t7 ck('get', dataKey(7), thingVal('thing7')); - ck('get', rcKey(3), undefined); - ck('get', esKey(3), 'r'); + // end-of-crank: (none) + // BOYD: T7 Representative dropped + ck('get', rcKey(7), undefined); + ck('get', esKey(7), 'r'); done(); - await writeHeld('thing8 updated'); // reanimate t8, evict t3 - [t8 t7 t1 t2] + await writeHeld('thing8 updated'); // T8 already in RAM + ck('get', dataKey(8), thingVal('thing8')); + // end-of-crank: flush T8 data ck('set', dataKey(8), thingVal('thing8 updated')); + // BOYD: (none, T8 stays in RAM) done(); }); @@ -342,9 +423,6 @@ test('virtual object gc', async t => { vats: { bob: { sourceSpec: new URL('vat-vom-gc-bob.js', import.meta.url).pathname, - creationOptions: { - virtualObjectCacheSize: 3, - }, }, bootstrap: { sourceSpec: new URL('vat-vom-gc-bootstrap.js', import.meta.url) diff --git a/packages/SwingSet/test/virtualObjects/vat-representative-bootstrap.js b/packages/SwingSet/test/virtualObjects/vat-representatives.js similarity index 94% rename from packages/SwingSet/test/virtualObjects/vat-representative-bootstrap.js rename to packages/SwingSet/test/virtualObjects/vat-representatives.js index 06083bc70ff..be307428199 100644 --- a/packages/SwingSet/test/virtualObjects/vat-representative-bootstrap.js +++ b/packages/SwingSet/test/virtualObjects/vat-representatives.js @@ -18,9 +18,6 @@ export function buildRootObject() { let heldThing; return Far('root', { - bootstrap() { - return 'bootstrap done'; - }, makeThing(name, hold) { const thing = makeThing(name); if (hold) { diff --git a/packages/SwingSet/test/virtualObjects/vdata-promises/test-vdata-promises.js b/packages/SwingSet/test/virtualObjects/vdata-promises/test-vdata-promises.js index c5b3781eb63..6c8676586d5 100644 --- a/packages/SwingSet/test/virtualObjects/vdata-promises/test-vdata-promises.js +++ b/packages/SwingSet/test/virtualObjects/vdata-promises/test-vdata-promises.js @@ -23,9 +23,6 @@ const config = { bootstrap: { sourceSpec: bfile('bootstrap-vdata-promises.js') }, target: { sourceSpec: bfile('vat-vdata-promises.js'), - creationOptions: { - virtualObjectCacheSize: 0, - }, }, }, }; diff --git a/packages/SwingSet/tools/prepare-test-env.js b/packages/SwingSet/tools/prepare-test-env.js index 8fa049dd23b..1911f131bd5 100644 --- a/packages/SwingSet/tools/prepare-test-env.js +++ b/packages/SwingSet/tools/prepare-test-env.js @@ -11,7 +11,7 @@ import '@endo/init/pre-bundle-source.js'; import './install-ses-debug.js'; import { makeFakeVirtualStuff } from '@agoric/swingset-liveslots/tools/fakeVirtualSupport.js'; -const { vom, cm, wpm } = makeFakeVirtualStuff({ cacheSize: 3 }); +const { vom, cm, wpm } = makeFakeVirtualStuff(); const { defineKind, diff --git a/packages/swingset-liveslots/src/cache.js b/packages/swingset-liveslots/src/cache.js new file mode 100644 index 00000000000..72a7ff0ff71 --- /dev/null +++ b/packages/swingset-liveslots/src/cache.js @@ -0,0 +1,103 @@ +import { Fail } from '@agoric/assert'; + +/** + * @template V + * @callback CacheGet + * @param {string} key + * @returns {V} + */ + +/** + * @template V + * @callback CacheSet + * @param {string} key + * @param {V} value + * @returns {void} + */ +/** + * @callback CacheDelete + * @param {string} key + * @returns {void} + * + * @callback CacheFlush + * @returns {void} + * + * @callback CacheInsistClear + * @returns {void} + */ +/** + * @template V + * @typedef {object} Cache + * @property {CacheGet} get + * @property {CacheSet} set + * @property {CacheDelete} delete + * @property {CacheFlush} flush + * @property {CacheInsistClear} insistClear + */ + +/** + * Cache of virtual object/collection state + * + * This cache is empty between deliveries. Within a delivery, the + * first access to some data will cause vatstore reads to populate the + * cache, then the data is retained until end-of-delivery. Writes to + * data will update the cache entry and mark it as dirty. At + * end-of-delivery, we flush the cache, writing out any dirty entries, + * and deleting all entries. + * + * This needs RAM for everything read during a delivery (rather than + * having a fixed maximum size), but yields a simple (easy to debug) + * deterministic relationship between data access and reads/writes to + * the backing store. + * + * @template V + * @param {(key: string) => V} readBacking + * @param {(key: string, value: V) => void} writeBacking + * @param {(key: string) => void} deleteBacking + * @returns {Cache} + * + * This cache is part of the virtual object manager and is not intended to be + * used independently; it is exported only for the benefit of test code. + */ +export function makeCache(readBacking, writeBacking, deleteBacking) { + const stash = new Map(); + const dirtyKeys = new Set(); + const cache = { + get: key => { + if (stash.has(key)) { + return stash.get(key); + } else if (dirtyKeys.has(key)) { + // Respect a pending deletion. + return undefined; + } + const value = readBacking(key); + stash.set(key, value); + return value; + }, + set: (key, value) => { + stash.set(key, value); + dirtyKeys.add(key); + }, + delete: key => { + stash.delete(key); + dirtyKeys.add(key); + }, + flush: () => { + const keys = [...dirtyKeys.keys()]; + for (const key of keys.sort()) { + if (stash.has(key)) { + writeBacking(key, stash.get(key)); + } else { + deleteBacking(key); + } + } + stash.clear(); + dirtyKeys.clear(); + }, + insistClear: () => { + dirtyKeys.size === 0 || Fail`cache still has dirtyKeys`; + stash.size === 0 || Fail`cache still has stash`; + }, + }; + return harden(cache); +} diff --git a/packages/swingset-liveslots/src/facetiousness.js b/packages/swingset-liveslots/src/facetiousness.js new file mode 100644 index 00000000000..b4cacac7aed --- /dev/null +++ b/packages/swingset-liveslots/src/facetiousness.js @@ -0,0 +1,43 @@ +/** + * Assess the facetiousness of a value. If the value is an object containing + * only named properties and each such property's value is a function, `obj` + * represents a single facet and 'one' is returned. If each property's value + * is instead an object of facetiousness 'one', `obj` represents multiple + * facets and 'many' is returned. In all other cases `obj` does not represent + * any kind of facet abstraction and 'not' is returned. + * + * @typedef {'one'|'many'|'not'} Facetiousness + * + * @param {*} obj The (alleged) object to be assessed + * @returns {Facetiousness} an assessment of the facetiousness of `obj` + */ +export function assessFacetiousness(obj) { + if (typeof obj !== 'object') { + return 'not'; + } + let result; + for (const prop of Reflect.ownKeys(obj)) { + const value = obj[prop]; + let resultFromProp; + if (typeof value === 'function') { + resultFromProp = 'one'; + } else if ( + // symbols are not permitted as facet names + typeof prop !== 'symbol' && + assessFacetiousness(value) === 'one' + ) { + resultFromProp = 'many'; + } else { + return 'not'; + } + if (!result) { + // capture the result of inspecting the first property + result = resultFromProp; + } else if (resultFromProp !== result) { + // and bail out upon encountering any deviation + return 'not'; + } + } + // empty objects are methodless Far objects + return /** @type {Facetiousness} */ (result || 'one'); +} diff --git a/packages/swingset-liveslots/src/liveslots.js b/packages/swingset-liveslots/src/liveslots.js index f9d39383e44..fbc21f637f4 100644 --- a/packages/swingset-liveslots/src/liveslots.js +++ b/packages/swingset-liveslots/src/liveslots.js @@ -5,7 +5,6 @@ import { makeMarshal, } from '@endo/marshal'; import { assert, details as X, Fail } from '@agoric/assert'; -import { isNat } from '@endo/nat'; import { isPromise } from '@endo/promise-kit'; import { E, HandledPromise } from '@endo/eventual-send'; import { insistVatType, makeVatSlot, parseVatSlot } from './parseVatSlots.js'; @@ -17,8 +16,6 @@ import { makeVirtualObjectManager } from './virtualObjectManager.js'; import { makeCollectionManager } from './collectionManager.js'; import { makeWatchedPromiseManager } from './watchedPromises.js'; -const DEFAULT_VIRTUAL_OBJECT_CACHE_SIZE = 3; // XXX ridiculously small value to force churn for testing - const SYSCALL_CAPDATA_BODY_SIZE_LIMIT = 10_000_000; const SYSCALL_CAPDATA_SLOTS_LENGTH_LIMIT = 10_000; @@ -51,11 +48,8 @@ function build( console, buildVatNamespace, ) { - const { - virtualObjectCacheSize = DEFAULT_VIRTUAL_OBJECT_CACHE_SIZE, - enableDisavow = false, - relaxDurabilityRules = false, - } = liveSlotsOptions; + const { enableDisavow = false, relaxDurabilityRules = false } = + liveSlotsOptions; const { WeakRef, FinalizationRegistry, meterControl } = gcTools; const enableLSDebug = false; function lsdebug(...args) { @@ -645,11 +639,11 @@ function build( vrm, allocateExportID, getSlotForVal, + requiredValForSlot, // eslint-disable-next-line no-use-before-define registerValue, m.serialize, unmeteredUnserialize, - virtualObjectCacheSize, assertAcceptableSyscallCapdataSize, ); @@ -735,16 +729,16 @@ function build( } function registerValue(baseRef, val, valIsCohort) { - const { type, facet } = parseVatSlot(baseRef); + const { type, id, facet } = parseVatSlot(baseRef); assert( !facet, `registerValue(${baseRef} should not receive individual facets`, ); slotToVal.set(baseRef, new WeakRef(val)); if (valIsCohort) { - for (let i = 0; i < val.length; i += 1) { - valToSlot.set(val[i], `${baseRef}:${i}`); - } + vrm.getFacetNames(id).forEach((name, index) => { + valToSlot.set(val[name], `${baseRef}:${index}`); + }); } else { valToSlot.set(val, baseRef); } @@ -762,13 +756,13 @@ function build( // m.unserialize) must be wrapped by unmetered(). function convertSlotToVal(slot, iface = undefined) { meterControl.assertNotMetered(); - const { type, allocatedByVat, virtual, durable, facet, baseRef } = + const { type, allocatedByVat, id, virtual, durable, facet, baseRef } = parseVatSlot(slot); let val = getValForSlot(baseRef); if (val) { if (virtual || durable) { if (facet !== undefined) { - return val[facet]; + return vrm.getFacet(id, val, facet); } } return val; @@ -778,7 +772,7 @@ function build( assert.equal(type, 'object'); val = vrm.reanimate(baseRef); if (facet !== undefined) { - result = val[facet]; + result = vrm.getFacet(id, val, facet); } } else { !allocatedByVat || Fail`I don't remember allocating ${slot}`; @@ -1356,16 +1350,11 @@ function build( vatGlobals, }); - function setVatOption(option, value) { + function setVatOption(option, _value) { + // note: we removed the only settable option in #7138, but we'll + // retain dispatch.changeVatOptions to make it easier to add a new + // one in the future switch (option) { - case 'virtualObjectCacheSize': { - if (isNat(value)) { - vom.setCacheSize(value); - } else { - console.warn(`WARNING: invalid virtualObjectCacheSize value`, value); - } - break; - } default: console.warn(`WARNING setVatOption unknown option ${option}`); } @@ -1508,14 +1497,9 @@ function build( const unmeteredDispatch = meterControl.unmetered(dispatchToUserspace); async function bringOutYourDead() { - vom.flushCache(); - await gcTools.gcAndFinalize(); - const doMore = await scanForDeadObjects(); - // @ts-expect-error FIXME doMore is void - if (doMore) { - return bringOutYourDead(); - } - return undefined; + await scanForDeadObjects(); + // now flush all the vatstore changes (deletions) we made + vom.flushStateCache(); } /** @@ -1532,6 +1516,7 @@ function build( */ function afterDispatchActions() { flushIDCounters(); + vom.flushStateCache(); } /** diff --git a/packages/swingset-liveslots/src/parseVatSlots.js b/packages/swingset-liveslots/src/parseVatSlots.js index 28863aaaf8d..a5fc21047b9 100644 --- a/packages/swingset-liveslots/src/parseVatSlots.js +++ b/packages/swingset-liveslots/src/parseVatSlots.js @@ -79,10 +79,8 @@ import { assert, Fail } from '@agoric/assert'; * be a virtual object or collection, which can be in memory or on disk or both. * Let's call such an entity a "base object". In most cases this is one and the * same with the addressable object that the vref designates, but in the case of - * a faceted object it is the faceted object as a whole (represented in memory, - * though not on disk, as the cohort array) rather than any particular - * individual facet (the faceted object per se is never exposed directly to code - * running within the vat; only its facets are). + * a faceted object it is the cohort record as a whole rather than any particular + * individual facet. * * XXX TODO: The previous comment suggests some renaming is warranted: * diff --git a/packages/swingset-liveslots/src/types.js b/packages/swingset-liveslots/src/types.js index 4d07218ac9a..06ca26db2e3 100644 --- a/packages/swingset-liveslots/src/types.js +++ b/packages/swingset-liveslots/src/types.js @@ -20,7 +20,6 @@ /** * @typedef {{ - * virtualObjectCacheSize?: number, // Maximum number of entries in the virtual object state cache * enableDisavow?: boolean, * relaxDurabilityRules?: boolean, * }} LiveSlotsOptions diff --git a/packages/swingset-liveslots/src/virtualObjectManager.js b/packages/swingset-liveslots/src/virtualObjectManager.js index 5072f9f42c9..a267b942b25 100644 --- a/packages/swingset-liveslots/src/virtualObjectManager.js +++ b/packages/swingset-liveslots/src/virtualObjectManager.js @@ -1,151 +1,260 @@ /* eslint-disable no-use-before-define, jsdoc/require-returns-type */ import { assert, Fail } from '@agoric/assert'; -import { objectMap } from '@agoric/internal'; import { assertPattern, mustMatch } from '@agoric/store'; import { defendPrototype, defendPrototypeKit } from '@agoric/store/tools.js'; import { Far, hasOwnPropertyOf, passStyleOf } from '@endo/marshal'; import { parseVatSlot, makeBaseRef } from './parseVatSlots.js'; import { enumerateKeysWithPrefix } from './vatstore-iterators.js'; +import { makeCache } from './cache.js'; +import { assessFacetiousness } from './facetiousness.js'; /** @template T @typedef {import('@agoric/vat-data').DefineKindOptions} DefineKindOptions */ const { ownKeys } = Reflect; -const { details: X, quote: q } = assert; +const { quote: q } = assert; // import { kdebug } from './kdebug.js'; -// Marker associated to flag objects that should be held onto strongly if -// somebody attempts to use them as keys in a VirtualObjectAwareWeakSet or -// VirtualObjectAwareWeakMap, despite the fact that keys in such collections are -// nominally held onto weakly. This to thwart attempts to observe GC by -// squirreling away a piece of a VO while the rest of the VO gets GC'd and then -// later regenerated. -const unweakable = new WeakSet(); +// This file implements the "Virtual Objects" system, currently documented in +// {@link https://github.com/Agoric/agoric-sdk/blob/master/packages/SwingSet/docs/virtual-objects.md}) +// +// All virtual-object state is keyed by baseRef, like o+v11/5 . For single-facet +// Kinds (created with `defineKind`), this is the entire vref. For +// multiple-facet Kinds (created with `defineKindMulti`), the cohort of facets +// (which all share the same state, but offer different methods, generally +// representing different authorities) will each have a vref that extends the +// baseRef with a facet identifier, e.g. o+v11/5:0 for the first facet, and +// o+v11/5:1 for the second. +// +// To manage context and state and data correctly (not sensitive to GC), we need +// two Caches. The first is "dataCache", and maps baseRef to state data. This +// data includes the serialized capdata for all properties, and the unserialized +// value for properties that have been read or written by an accessor on the +// `state` object. +// +// The second cache is "contextCache", and maps baseRef to a context object, +// which is either { state, self } or { state, facets } depending on the +// facetiousness of the VO. "state" is an object with one accessor pair +// (getter+setter) per state property name. The "state" getters/setters know +// which baseRef they should use. When invoked, they pull the state data from +// `dataCache.get(baseRef).valueMap`. The setter will modify valueMap in place +// and mark the entry as dirty, so it can be serialized and written back at +// end-of-crank. +// +// Each Representative is built as an Exo with defendPrototype (cohorts of +// facets are built with defendPrototypeKit). These are given a +// "contextProvider" for later use. For each facet, they build a prototype +// object with wrappers for all the methods of that particular facet. When those +// wrappers are invoked, the first thing they do is to call +// `contextProvider(this)` (where `this` is the representative) to get a +// "context" object: { state, self } or { state, facets }, which is passed to +// the behavior functions. The contextProvider function uses valToSlot() to +// figure out the representative's vref, then derives the baseRef, then consults +// contextCache to get (or create) the context. +// +// Our GC sensitivity contraints are: +// * userspace must not be able to sense garbage collection +// * Representatives are created on-demand when userspace deserializes a vref +// * they disappear when UNREACHABLE and GC collects them +// {@link https://github.com/Agoric/agoric-sdk/blob/master/packages/SwingSet/docs/garbage-collection.md}) +// * syscalls must be a deterministic function of userspace behavior +// * that includes method invocation and "state" property read/writes +// * they should not be influenced by GC until a bringOutYourDead delivery +// +// See the discussion below (near `makeRepresentative`) for more details on how +// we meet these constraints. + +/* + * Make a cache which maps baseRef to a (mutable) record of { + * capdatas, valueMap }. + * + * 'capdatas' is a mutable Object (record) with state property names + * as keys, and their capdata { body, slots } as values, and + * 'valueMap' is a Map with state property names as keys, and their + * unmarshalled values as values. We need the 'capdatas' record to be + * mutable because we will modify its contents in place during + * setters, to retain the insertion order later (during flush). We + * need capdata at all so we can compare the slots before and after + * the update, to adjust the refcounts. Only the values of 'valueMap' + * are exposed to userspace. + */ + +function makeDataCache(syscall) { + /** @type {(baseRef: string) => { capdatas: any, valueMap: Map }} */ + const readBacking = baseRef => { + const rawState = syscall.vatstoreGet(`vom.${baseRef}`); + assert(rawState); + const capdatas = JSON.parse(rawState); + const valueMap = new Map(); // populated lazily by each state getter + return { capdatas, valueMap }; // both mutable + }; + /** @type {(baseRef: string, value: { capdatas: any, valueMap: Map }) => void} */ + const writeBacking = (baseRef, value) => { + const rawState = JSON.stringify(value.capdatas); + syscall.vatstoreSet(`vom.${baseRef}`, rawState); + }; + /** @type {(collectionID: string) => void} */ + const deleteBacking = baseRef => syscall.vatstoreDelete(`vom.${baseRef}`); + return makeCache(readBacking, writeBacking, deleteBacking); +} + +function makeContextCache(makeState, makeContext) { + // non-writeback cache for "context" objects, { state, self/facets } + const readBacking = baseRef => { + const state = makeState(baseRef); + const context = makeContext(baseRef, state); + return context; + }; + const writeBacking = _baseRef => Fail`never called`; + const deleteBacking = _baseRef => Fail`never called`; + return makeCache(readBacking, writeBacking, deleteBacking); +} + +/** + * @typedef {import('@agoric/store/src/patterns/exo-tools.js').ContextProvider } ContextProvider + */ /** - * Make a simple LRU cache of virtual object inner selves. - * - * @param {number} size Maximum number of entries to keep in the cache before - * starting to throw them away. - * @param {(baseRef: string) => object} fetch Function to retrieve an - * object's raw state from the store by its baseRef - * @param {(baseRef: string, rawState: object) => void} store Function to - * store raw object state by its baseRef - * - * @returns {object} An LRU cache of (up to) the given size * - * This cache is part of the virtual object manager and is not intended to be - * used independently; it is exported only for the benefit of test code. + * @param {*} contextCache + * @param {*} getSlotForVal + * @returns {ContextProvider} */ -export function makeCache(size, fetch, store) { - let lruHead; - let lruTail; - let dirtyCount = 0; - const liveTable = new Map(); - - const cache = { - makeRoom() { - while (liveTable.size > size && lruTail) { - // kdebug(`### vo LRU evict ${lruTail.baseRef} (dirty=${lruTail.dirty})`); - liveTable.delete(lruTail.baseRef); - if (lruTail.dirty) { - store(lruTail.baseRef, lruTail.rawState); - lruTail.dirty = false; - dirtyCount -= 1; - } - lruTail.rawState = null; - if (lruTail.prev) { - lruTail.prev.next = undefined; - } else { - lruHead = undefined; - } - const deadEntry = lruTail; - lruTail = lruTail.prev; - deadEntry.next = undefined; - deadEntry.prev = undefined; - } - }, - markDirty(entry) { - if (!entry.dirty) { - entry.dirty = true; - dirtyCount += 1; - } - }, - setSize(newSize) { - if (newSize < size) { - size = newSize; - cache.makeRoom(); - } else { - size = newSize; - } - }, - flush() { - if (dirtyCount > 0) { - let entry = lruTail; - while (entry) { - if (entry.dirty) { - store(entry.baseRef, entry.rawState); - entry.dirty = false; - } - entry = entry.prev; - } - dirtyCount = 0; - } - }, - remember(innerObj) { - if (liveTable.has(innerObj.baseRef)) { - return; - } - cache.makeRoom(); - liveTable.set(innerObj.baseRef, innerObj); - innerObj.prev = undefined; - innerObj.next = lruHead; - if (lruHead) { - lruHead.prev = innerObj; - } - lruHead = innerObj; - if (!lruTail) { - lruTail = innerObj; - } - // kdebug(`### vo LRU remember ${lruHead.baseRef}`); - }, - refresh(innerObj) { - if (innerObj !== lruHead) { - const oldPrev = innerObj.prev; - const oldNext = innerObj.next; - if (oldPrev) { - oldPrev.next = oldNext; - } else { - lruHead = oldNext; - } - if (oldNext) { - oldNext.prev = oldPrev; - } else { - lruTail = oldPrev; - } - innerObj.prev = undefined; - innerObj.next = lruHead; - lruHead.prev = innerObj; - lruHead = innerObj; - // kdebug(`### vo LRU refresh ${lruHead.baseRef}`); - } - }, - lookup(baseRef, load) { - let innerObj = liveTable.get(baseRef); - if (innerObj) { - cache.refresh(innerObj); - } else { - innerObj = { baseRef, rawState: null, repCount: 0 }; - cache.remember(innerObj); - } - if (load && !innerObj.rawState) { - innerObj.rawState = fetch(baseRef); +const makeContextProvider = (contextCache, getSlotForVal) => { + return harden(rep => contextCache.get(getSlotForVal(rep))); +}; + +const makeContextProviderKit = (contextCache, getSlotForVal, facetNames) => { + /** @type { Record } */ + const contextProviderKit = {}; + for (const [index, name] of facetNames.entries()) { + contextProviderKit[name] = rep => { + const vref = getSlotForVal(rep); + const { baseRef, facet } = parseVatSlot(vref); + + // Without this check, an attacker (with access to both cohort1.facetA + // and cohort2.facetB) could effectively forge access to cohort1.facetB + // and cohort2.facetA. They could not forge the identity of those two + // objects, but they could invoke all their equivalent methods, by using + // e.g. cohort1.facetA.foo.apply(cohort2.facetB, [...args]) + Number(facet) === index || Fail`illegal cross-facet access`; + + return harden(contextCache.get(baseRef)); + }; + } + return harden(contextProviderKit); +}; + +function checkAndUpdateFacetiousness( + tag, + desc, + facetNames, + saveDurableKindDescriptor, +) { + // The first time a durable kind gets a definition, the saved descriptor + // will have neither ".unfaceted" nor ".facets", and we must record the + // initial details in the descriptor. + + if (!desc.unfaceted && !desc.facets) { + if (facetNames) { + desc.facets = facetNames; + } else { + desc.unfaceted = true; + } + saveDurableKindDescriptor(desc); + return; + } + + // When a later incarnation redefines the behavior, it must match. + + if (facetNames && desc.unfaceted) { + Fail`defineDurableKindMulti called for unfaceted KindHandle ${tag}`; + } + if (!facetNames && desc.facets) { + Fail`defineDurableKind called for faceted KindHandle ${tag}`; + } + if (facetNames) { + let ok = true; + for (const [idx, facet] of facetNames.entries()) { + if (facet !== desc.facets[idx]) { + ok = false; } - return innerObj; - }, - }; - return cache; + } + if (!ok) { + const orig = desc.facets.join(','); + const newer = facetNames.join(','); + Fail`durable kind "${tag}" facets (${newer}) don't match original definition (${orig})`; + } + } +} + +// The management of single Representatives (i.e. defineKind) is very similar +// to that of a cohort of facets (i.e. defineKindMulti). In this description, +// we use "self/facets" to refer to either 'self' or 'facets', as appropriate +// for the particular Kind. From userspace's perspective, the main difference +// is that single-facet Kinds present self/facets as 'context.self', whereas +// multi-facet Kinds present it as 'context.facets'. + +// makeRepresentative/makeFacets returns the self/facets . This serves several +// purposes: +// +// * it is returned to userspace when making a new VO instance +// * it appears as 'context.self/facets' when VO methods are invoked +// * it is stored in the slotToVal table, specifically: +// * slotToVal.get(baseref).deref() === self/facets +// * (for facets, convertSlotToVal will then extract a single facet) +// * it is registered with our FinalizationRegistry +// * (for facets, the FR must not fire until all cohort members have been +// collected) +// +// Any facet can be passed to valToSlot to learn its vref, from which we learn +// the baseRef, which we pass to contextCache.get to retrieve or create a +// 'context', which will include self/facets and a 'state' object. So either: +// * context = { state, self } +// * context = { state, facets } +// +// Userspace might hold on to a Representative, the `facets` record, the context +// object, or the state object, for an unbounded length of time: beyond a single +// crank/delivery. They might hold on to "context" but drop the Representative, +// etc. They may compare these held objects against newer versions they receive +// in future interactions. They might attempt to put any of these in a +// WeakMap/WeakSet (remembering that they only get the +// VirtualObjectAwareWeakMap/Set form that we give them). None of these actions +// may allow userspace to sense GC. +// +// Userspace could build a GC sensor out of any object with the following +// properties: +// * it has a GC-sensitive lifetime (i.e. created by these two functions) +// * it is reachable from userspace +// * it lacks a vref (else it'd be handled specially by VOAwareWeakMap) +// + +// We must mark such objects as "unweakable" to prevent their use in +// VOAwareWeakMap -based sensors (unweakable keys are held strongly by those +// collections), and we must tie their lifetime to the facets to prevent their +// use in a stash-and-compare-later sensor. We achieve the latter by adding a +// linkToCohort WeakMap entry from every facet to the cohort record. This also +// ensures that the FinalizationRegistry won't see the cohort record go away +// until all the individual facets have been collected. +// +// We only need to do this for multi-facet Kinds; single-facet kinds don't +// have any extra objects for userspace to work with. + +function makeRepresentative(proto) { + const self = { __proto__: proto }; + return harden(self); +} + +function makeFacets(facetNames, proto, linkToCohort, unweakable) { + const facets = {}; // aka context.facets + for (const name of facetNames) { + const facet = { __proto__: proto[name] }; + facets[name] = facet; + linkToCohort.set(facet, facets); + } + unweakable.add(facets); + return harden(facets); } /** @@ -156,15 +265,14 @@ export function makeCache(size, fetch, store) { * of virtual references. * @param {() => number} allocateExportID Function to allocate the next object * export ID for the enclosing vat. - * @param {(val: object) => string} _getSlotForVal A function that returns the + * @param {(val: object) => string} getSlotForVal A function that returns the * object ID (vref) for a given object, if any. their corresponding export * IDs + * @param {(slot: string) => object} requiredValForSlot * @param {*} registerValue Function to register a new slot+value in liveSlot's * various tables * @param {import('@endo/marshal').Serialize} serialize Serializer for this vat * @param {import('@endo/marshal').Unserialize} unserialize Unserializer for this vat - * @param {number} cacheSize How many virtual objects this manager should cache - * in memory. * @param {*} assertAcceptableSyscallCapdataSize Function to check for oversized * syscall params * @@ -188,11 +296,10 @@ export function makeCache(size, fetch, store) { * substitutes for their regular JS analogs in way that should be transparent * to ordinary users of those classes. * - * - `flushCache` will empty the object manager's cache of in-memory object - * instances, writing any changed state to the persistent store. This is - * provided for testing and to ensure that state that should be persisted - * actually is prior to a controlled shutdown; normal code should not use - * this. + * - `flushStateCache` will empty the object manager's cache of in-memory object + * instances, writing any changed state to the persistent store. This should + * be called at the end of each crank, to ensure the syscall trace does not + * depend upon GC of Representatives. * * The `defineKind` functions are made available to user vat code in the * `VatData` global (along with various other storage functions defined @@ -202,53 +309,37 @@ export function makeVirtualObjectManager( syscall, vrm, allocateExportID, - _getSlotForVal, + getSlotForVal, + requiredValForSlot, registerValue, serialize, unserialize, - cacheSize, assertAcceptableSyscallCapdataSize, ) { + // array of Caches that need to be flushed at end-of-crank, two per Kind + // (dataCache, contextCache) + const allCaches = []; + + // WeakMap tieing VO components together, to prevent anyone who + // retains one piece (e.g. the cohort record of facets) from being + // able to observe the comings and goings of representatives by + // hanging onto that piece while the other pieces are GC'd, then + // comparing it to what gets generated when the VO is reconstructed + // by a later import. + const linkToCohort = new WeakMap(); + const canBeDurable = specimen => { const capData = serialize(specimen); return capData.slots.every(vrm.isDurable); }; - const cache = makeCache(cacheSize, fetch, store); - - // WeakMap tieing VO components together, to prevent anyone who retains one - // piece (say, the state object) from being able to observe the comings and - // goings of representatives by hanging onto that piece while the other pieces - // are GC'd, then comparing it to what gets generated when the VO is - // reconstructed by a later import. - const linkToCohort = new WeakMap(); - - /** - * Fetch an object's state from secondary storage. - * - * @param {string} baseRef The baseRef of the object whose state is being - * fetched. - * @returns {*} an object representing the object's stored state. - */ - function fetch(baseRef) { - const rawState = syscall.vatstoreGet(`vom.${baseRef}`); - if (rawState) { - return JSON.parse(rawState); - } else { - return undefined; - } - } - - /** - * Write an object's state to secondary storage. - * - * @param {string} baseRef The baseRef of the object whose state is being - * stored. - * @param {*} rawState A data object representing the state to be written. - */ - function store(baseRef, rawState) { - syscall.vatstoreSet(`vom.${baseRef}`, JSON.stringify(rawState)); - } + // Marker associated to flag objects that should be held onto strongly if + // somebody attempts to use them as keys in a VirtualObjectAwareWeakSet or + // VirtualObjectAwareWeakMap, despite the fact that keys in such collections + // are nominally held onto weakly. This to thwart attempts to observe GC by + // squirreling away a piece of a VO while the rest of the VO gets GC'd and + // then later regenerated. + const unweakable = new WeakSet(); // This is a WeakMap from VO aware weak collections to strong Sets that retain // keys used in the associated collection that should not actually be held @@ -423,59 +514,6 @@ export function makeVirtualObjectManager( configurable: true, }); - /** - * Assess the facetiousness of a value. If the value is an object containing - * only named properties and each such property's value is a function, `obj` - * represents a single facet and 'one' is returned. If each property's value - * is instead an object of facetiousness 'one', `obj` represents multiple - * facets and 'many' is returned. In all other cases `obj` does not represent - * any kind of facet abstraction and 'not' is returned. - * - * @typedef {'one'|'many'|'not'} Facetiousness - * - * @param {*} obj The (alleged) object to be assessed - * @param {boolean} [inner] True if this is being called recursively; no more - * than one level of recursion is allowed. - * - * @returns {Facetiousness} an assessment of the facetiousness of `obj` - */ - function assessFacetiousness(obj, inner) { - if (typeof obj !== 'object') { - return 'not'; - } - let established; - for (const prop of Reflect.ownKeys(obj)) { - const value = obj[prop]; - let current; - if (typeof value === 'function') { - current = 'one'; - } else if ( - !inner && - typeof value === 'object' && - assessFacetiousness(value, true) === 'one' - ) { - if (typeof prop === 'symbol') { - // can't have symbol-named facets - return 'not'; - } - current = 'many'; - } else { - return 'not'; - } - if (!established) { - established = current; - } else if (established !== current) { - return 'not'; - } - } - if (!established) { - // empty objects are methodless Far objects - return 'one'; - } else { - return /** @type {Facetiousness} */ (established); - } - } - /** * @typedef {{ * kindID: string, @@ -486,6 +524,16 @@ export function makeVirtualObjectManager( * }} DurableKindDescriptor */ + /** + * @param {DurableKindDescriptor} durableKindDescriptor + */ + function saveDurableKindDescriptor(durableKindDescriptor) { + syscall.vatstoreSet( + `vom.dkind.${durableKindDescriptor.kindID}`, + JSON.stringify(durableKindDescriptor), + ); + } + /** * Define a new kind of virtual object. * @@ -586,7 +634,7 @@ export function makeVirtualObjectManager( behavior, options = {}, isDurable, - durableKindDescriptor, + durableKindDescriptor = undefined, // only for durables ) { const { finish, @@ -594,274 +642,283 @@ export function makeVirtualObjectManager( thisfulMethods = false, interfaceGuard = undefined, } = options; - let facetNames; - let contextMapTemplate; - let prototypeTemplate; - - harden(stateShape); - stateShape === undefined || - passStyleOf(stateShape) === 'copyRecord' || - assert.fail(X`A stateShape must be a copyRecord: ${q(stateShape)}`); - assertPattern(stateShape); - const serializeSlot = (slotState, prop) => { - if (stateShape !== undefined) { - hasOwnPropertyOf(stateShape, prop) || - assert.fail( - X`State must only have fields described by stateShape: ${q( - ownKeys(stateShape), - )}`, - ); - mustMatch(slotState, stateShape[prop], prop); - } - return serialize(slotState); - }; + let facetNames; // undefined or a list of strings - const unserializeSlot = (slotData, prop) => { - const slotValue = unserialize(slotData); - if (stateShape !== undefined) { - hasOwnPropertyOf(stateShape, prop) || - assert.fail( - X`State only has fields described by stateShape: ${q( - ownKeys(stateShape), - )}`, - ); - mustMatch(slotValue, stateShape[prop]); - } - return slotValue; - }; - - const facetiousness = assessFacetiousness(behavior); - const getContextKit = objectMap( - behavior, - (_v, name) => facet => contextMapTemplate[name].get(facet), - ); - - switch (facetiousness) { + // 'multifaceted' tells us which API was used: define[Durable]Kind + // vs define[Durable]KindMulti. This function checks whether + // 'behavior' has one facet, or many, and must match. + switch (assessFacetiousness(behavior)) { case 'one': { assert(!multifaceted); facetNames = undefined; - contextMapTemplate = new WeakMap(); - prototypeTemplate = defendPrototype( - tag, - self => contextMapTemplate.get(self), - behavior, - thisfulMethods, - interfaceGuard, - ); break; } case 'many': { assert(multifaceted); facetNames = Object.getOwnPropertyNames(behavior).sort(); - contextMapTemplate = objectMap(behavior, () => new WeakMap()); - prototypeTemplate = defendPrototypeKit( - tag, - getContextKit, - behavior, - thisfulMethods, - interfaceGuard, - ); break; } case 'not': { throw Fail`invalid behavior specifier for ${q(tag)}`; } default: { - throw Fail`unexepected facetiousness: ${q(facetiousness)}`; + throw Fail`invalid facetiousness`; } } + // beyond this point, we use 'multifaceted' to switch modes - if (durableKindDescriptor) { - const { unfaceted, facets } = durableKindDescriptor; - if (multifaceted) { - assert( - !unfaceted, - `durable kind "${tag}" originally defined as single-faceted`, - ); - if (facets) { - const m = `durable kind "${tag}" facets don't match original definition`; - assert( - facetNames !== undefined && facets.length === facetNames.length, - m, - ); - for (const [idx, facet] of facetNames.entries()) { - assert(facet === facets[idx], m); - } - } else { - durableKindDescriptor.facets = facetNames; - saveDurableKindDescriptor(durableKindDescriptor); - } - } else { - assert( - !facets, - `durable kind "${tag}" originally defined as multi-faceted`, - ); - if (!unfaceted) { - durableKindDescriptor.unfaceted = true; - saveDurableKindDescriptor(durableKindDescriptor); - } - } + if (isDurable) { + checkAndUpdateFacetiousness( + tag, + durableKindDescriptor, + facetNames, + saveDurableKindDescriptor, + ); } - vrm.registerKind(kindID, reanimate, deleteStoredVO, isDurable); - vrm.rememberFacetNames(kindID, facetNames); - harden(contextMapTemplate); - harden(prototypeTemplate); - - function makeRepresentative(innerSelf, initializing) { - innerSelf.repCount === 0 || - Fail`${innerSelf.baseRef} already has a representative`; - innerSelf.repCount += 1; + harden(stateShape); + stateShape === undefined || + passStyleOf(stateShape) === 'copyRecord' || + Fail`A stateShape must be a copyRecord: ${q(stateShape)}`; + assertPattern(stateShape); - function ensureState() { - if (innerSelf.rawState) { - cache.refresh(innerSelf); - } else { - innerSelf = cache.lookup(innerSelf.baseRef, true); - } - } + let checkStateProperty = _prop => undefined; + /** @type {(value: any, prop: string) => void} */ + let checkStatePropertyValue = (_value, _prop) => undefined; + if (stateShape) { + checkStateProperty = prop => + hasOwnPropertyOf(stateShape, prop) || + Fail`State must only have fields described by stateShape: ${q( + ownKeys(stateShape), + )}`; + checkStatePropertyValue = (value, prop) => { + checkStateProperty(prop); + mustMatch(value, stateShape[prop]); + }; + } + // The dataCache holds both unserialized and still-serialized + // (capdata) contents of the virtual-object state record. + // dataCache[baseRef] -> { capdatas, valueMap } + // valueCD=capdatas[prop], value=valueMap.get(prop) + /** @type { import('./cache.js').Cache<{ capdatas: any, valueMap: Map }>} */ + const dataCache = makeDataCache(syscall); + allCaches.push(dataCache); + + // Behavior functions will receive a 'state' object that provides + // access to their virtualized data, with getters and setters + // backed by the vatstore DB. When those functions are invoked and + // we miss in contextCache, we'll call makeState() and + // makeContext(). The makeState() call might read from the + // vatstore DB if we miss in dataCache. + + // We sample dataCache.get() once each time: + // * makeState() is called, which happens the first time in each crank that + // a method is invoked (and the prototype does getContext) + // * when state.prop is read, invoking the getter + // * when state.prop is written, invoking the setter + // This will cause a syscall.vatstoreGet only once per crank. + + function makeState(baseRef) { const state = {}; - if (!initializing) { - ensureState(); - } - for (const prop of Object.getOwnPropertyNames(innerSelf.rawState)) { + for (const prop of Object.getOwnPropertyNames( + dataCache.get(baseRef).capdatas, + )) { + checkStateProperty(prop); Object.defineProperty(state, prop, { get: () => { - ensureState(); - return unserializeSlot(innerSelf.rawState[prop], prop); + const { valueMap, capdatas } = dataCache.get(baseRef); + if (!valueMap.has(prop)) { + const value = harden(unserialize(capdatas[prop])); + checkStatePropertyValue(value, prop); + valueMap.set(prop, value); + } + return valueMap.get(prop); }, set: value => { - ensureState(); - const before = innerSelf.rawState[prop]; - const after = serializeSlot(value, prop); - assertAcceptableSyscallCapdataSize([after]); + checkStatePropertyValue(value, prop); + const capdata = serialize(value); + assertAcceptableSyscallCapdataSize([capdata]); + const newSlots = capdata.slots; if (isDurable) { - after.slots.forEach((vref, index) => { + newSlots.forEach((vref, index) => { vrm.isDurable(vref) || Fail`value for ${q(prop)} is not durable at slot ${q( index, - )} of ${after}`; + )} of ${capdata}`; }); } - vrm.updateReferenceCounts(before.slots, after.slots); - innerSelf.rawState[prop] = after; - cache.markDirty(innerSelf); + const record = dataCache.get(baseRef); // mutable + const oldSlots = record.capdatas[prop].slots; + vrm.updateReferenceCounts(oldSlots, newSlots); + record.capdatas[prop] = capdata; // modify in place .. + record.valueMap.set(prop, value); + dataCache.set(baseRef, record); // .. but mark as dirty }, enumerable: true, }); } - harden(state); + return harden(state); + } - if (initializing) { - cache.remember(innerSelf); - } - let toHold; - let toExpose; - unweakable.add(state); - if (!facetNames) { - const context = { state }; - // `context` does not need a linkToCohort because it holds the - // facets (which hold the cohort) - unweakable.add(context); - const self = harden({ __proto__: prototypeTemplate }); - context.self = self; - contextMapTemplate.set(self, context); - toHold = self; - toExpose = toHold; - harden(context); + // More specifically, behavior functions receive a "context" + // object as their first argument, with { state, self } or { + // state, facets }. This makeContext() creates one, and is called + // if/when those functions are invoked and the "contextCache" + // misses, in which case the makeContextCache/readBacking function + // will sample dataCache.get, then call both "makeState()" and + // "makeContext". The DB might be read by that dataCache.get. + + function makeContext(baseRef, state) { + // baseRef came from valToSlot, so must be in slotToVal + const val = requiredValForSlot(baseRef); + // val is either 'self' or the facet record + if (multifaceted) { + return harden({ state, facets: val }); } else { - toExpose = {}; - toHold = []; - const facets = {}; - const context = { state, facets }; - for (const name of facetNames) { - const facet = harden({ - __proto__: prototypeTemplate[name], - }); - contextMapTemplate[name].set(facet, context); - facets[name] = facet; - toExpose[name] = facet; - toHold.push(facet); - linkToCohort.set(facet, toHold); - } - unweakable.add(facets); - harden(context); - harden(facets); - harden(toExpose); - harden(toHold); + return harden({ state, self: val }); } - innerSelf.representative = toHold; - linkToCohort.set(state, toHold); - return [toHold, toExpose, state]; } - function reanimate(baseRef) { - // kdebug(`vo reanimate ${baseRef}`); - const innerSelf = cache.lookup(baseRef, false); - const [toHold] = makeRepresentative(innerSelf, false); - return toHold; + // The contextCache holds the {state,self} or {state,facets} "context" + // object, needed by behavior functions. We keep this in a (per-crank) + // cache because creating one requires knowledge of the state property + // names, which requires a DB read. The property names are fixed at + // instance initialization time, so we never write changes to this cache. + + const contextCache = makeContextCache(makeState, makeContext); + allCaches.push(contextCache); + + // defendPrototype/defendPrototypeKit accept a contextProvider function, + // or a contextProviderKit record which maps facet name strings to + // provider functions. It calls the function during invocation of each + // method, and expects to get back the "context" record, either { state, + // self } for single-facet VOs, or { state, facets } for multi-facet + // ones. The provider we use fetches the state data (if not already in the + // cache) at the last minute. This moves any syscalls needed by + // stateCache.get() out of deserialization time (which is sensitive to GC) + // and into method-invocation time (which is not). + + let proto; + if (multifaceted) { + proto = defendPrototypeKit( + tag, + makeContextProviderKit(contextCache, getSlotForVal, facetNames), + behavior, + thisfulMethods, + interfaceGuard, + ); + } else { + proto = defendPrototype( + tag, + makeContextProvider(contextCache, getSlotForVal), + behavior, + thisfulMethods, + interfaceGuard, + ); + } + harden(proto); + + // this builds new Representatives, both when creating a new instance and + // for reanimating an existing one when the old rep gets GCed + + function reanimateVO(_baseRef) { + if (multifaceted) { + return makeFacets(facetNames, proto, linkToCohort, unweakable); + } else { + return makeRepresentative(proto); + } } function deleteStoredVO(baseRef) { let doMoreGC = false; - const rawState = fetch(baseRef); - if (rawState) { - for (const propValue of Object.values(rawState)) { - propValue.slots.forEach(vref => { - doMoreGC = vrm.removeReachableVref(vref) || doMoreGC; - }); - } + const record = dataCache.get(baseRef); + for (const valueCD of Object.values(record.capdatas)) { + valueCD.slots.forEach(vref => { + doMoreGC = vrm.removeReachableVref(vref) || doMoreGC; + }); } - syscall.vatstoreDelete(`vom.${baseRef}`); + dataCache.delete(baseRef); return doMoreGC; } + // Tell the VRM about this Kind. + vrm.registerKind(kindID, reanimateVO, deleteStoredVO, isDurable); + vrm.rememberFacetNames(kindID, facetNames); + function makeNewInstance(...args) { const id = getNextInstanceID(kindID, isDurable); const baseRef = makeBaseRef(kindID, id, isDurable); // kdebug(`vo make ${baseRef}`); const initialData = init ? init(...args) : {}; - if (typeof initialData !== 'object') { + + // catch mistaken use of `() => { foo: 1 }` rather than `() => ({ foo: 1 })` + // (the former being a function with a body having a no-op statement labeled + // "foo" and returning undefined, the latter being a function with a concise + // body that returns an object having a property named "foo"). + typeof initialData === 'object' || Fail`initial data must be object, not ${initialData}`; - // a common mistake is to use `() => {foo:1}`, not `() => ({foo:1})` - } - const rawState = {}; + + // save (i.e. populate the cache) with the initial serialized record + const capdatas = {}; + const valueMap = new Map(); for (const prop of Object.getOwnPropertyNames(initialData)) { - const data = serializeSlot(initialData[prop], prop); - assertAcceptableSyscallCapdataSize([data]); + const value = initialData[prop]; + checkStatePropertyValue(value, prop); + const valueCD = serialize(value); + // TODO: we're only checking the size of one property at a + // time, but the real constraint is the vatstoreSet of the + // aggregate record. We should apply this check to the full + // list of capdatas, plus its likely JSON overhead. + assertAcceptableSyscallCapdataSize([valueCD]); if (isDurable) { - data.slots.forEach(vref => { + valueCD.slots.forEach(vref => { vrm.isDurable(vref) || Fail`value for ${q(prop)} is not durable`; }); } - data.slots.forEach(vrm.addReachableVref); - rawState[prop] = data; + valueCD.slots.forEach(vrm.addReachableVref); + capdatas[prop] = valueCD; + valueMap.set(prop, value); } - const innerSelf = { baseRef, rawState, repCount: 0 }; - const [toHold, toExpose, state] = makeRepresentative(innerSelf, true); - registerValue(baseRef, toHold, Array.isArray(toHold)); - if (finish) { - if (toHold === toExpose) { - finish({ state, self: toExpose }); - } else { - finish({ state, facets: toExpose }); - } + // dataCache contents remain mutable: state setter modifies in-place + dataCache.set(baseRef, { capdatas, valueMap }); + + // make the initial representative or cohort + let val; + if (multifaceted) { + val = makeFacets(facetNames, proto, linkToCohort, unweakable); + } else { + val = makeRepresentative(proto); } - cache.markDirty(innerSelf); - return toExpose; + registerValue(baseRef, val, multifaceted); + finish?.(contextCache.get(baseRef)); + return val; } return makeNewInstance; } let kindIDID; - const kindHandleToID = new WeakMap(); /** @type Map */ const kindIDToDescriptor = new Map(); + const kindHandleToID = new Map(); const definedDurableKinds = new Set(); // kindID + const nextInstanceIDs = new Map(); // kindID -> nextInstanceID + + function reanimateDurableKindID(vobjID) { + const kindID = `${parseVatSlot(vobjID).subid}`; + const raw = syscall.vatstoreGet(`vom.dkind.${kindID}`); + raw || Fail`unknown kind ID ${kindID}`; + const durableKindDescriptor = JSON.parse(raw); + kindIDToDescriptor.set(kindID, durableKindDescriptor); + const kindHandle = Far('kind', {}); + kindHandleToID.set(kindHandle, kindID); + // KindHandles are held strongly for the remainder of the incarnation, so + // their components do not provide GC sensors + return kindHandle; + } function initializeKindHandleKind() { kindIDID = syscall.vatstoreGet('kindIDID'); @@ -872,18 +929,6 @@ export function makeVirtualObjectManager( vrm.registerKind(kindIDID, reanimateDurableKindID, () => null, true); } - const nextInstanceIDs = new Map(); // kindID -> nextInstanceID - - /** - * @param {DurableKindDescriptor} durableKindDescriptor - */ - function saveDurableKindDescriptor(durableKindDescriptor) { - syscall.vatstoreSet( - `vom.dkind.${durableKindDescriptor.kindID}`, - JSON.stringify(durableKindDescriptor), - ); - } - function getNextInstanceID(kindID, isDurable) { assert.typeof(kindID, 'string'); // nextInstanceID is initialized to 1 for brand new kinds, loaded @@ -933,23 +978,6 @@ export function makeVirtualObjectManager( ); } - function reanimateDurableKindID(vobjID) { - const kindID = `${parseVatSlot(vobjID).subid}`; - const raw = syscall.vatstoreGet(`vom.dkind.${kindID}`); - raw || Fail`unknown kind ID ${kindID}`; - const durableKindDescriptor = JSON.parse(raw); - const kindHandle = Far('kind', {}); - linkToCohort.set(Object.getPrototypeOf(kindHandle), kindHandle); - unweakable.add(Object.getPrototypeOf(kindHandle)); - kindHandleToID.set(kindHandle, kindID); - // we load the descriptor (including .nextInstanceID) every time - // the vat makes a new DurableKindHandle representative (during - // deserialization). The handle is held weakly and can be dropped, - // but the KindID-to-descriptor mapping remains in memory. - kindIDToDescriptor.set(kindID, durableKindDescriptor); - return kindHandle; - } - /** * * @param {string} tag @@ -1023,10 +1051,6 @@ export function makeVirtualObjectManager( return maker; } - function setCacheSize(newSize) { - cache.setSize(newSize); - } - function insistAllDurableKindsReconnected() { // identify all user-defined durable kinds by iterating `vom.dkind.*` const missing = []; @@ -1060,6 +1084,12 @@ export function makeVirtualObjectManager( countWeakKeysForCollection, }; + const flushStateCache = () => { + for (const cache of allCaches) { + cache.flush(); + } + }; + return harden({ initializeKindHandleKind, defineKind, @@ -1070,8 +1100,7 @@ export function makeVirtualObjectManager( insistAllDurableKindsReconnected, VirtualObjectAwareWeakMap, VirtualObjectAwareWeakSet, - setCacheSize, - flushCache: cache.flush, + flushStateCache, testHooks, canBeDurable, }); diff --git a/packages/swingset-liveslots/src/virtualReferences.js b/packages/swingset-liveslots/src/virtualReferences.js index b1d3846ae56..33b921cbb30 100644 --- a/packages/swingset-liveslots/src/virtualReferences.js +++ b/packages/swingset-liveslots/src/virtualReferences.js @@ -133,18 +133,8 @@ export function makeVirtualReferenceManager( } } - function getFacetCount(baseRef) { - // Note that this only works if the VDO is in memory - const val = requiredValForSlot(baseRef); - if (Array.isArray(val)) { - return val.length; - } else { - return 1; - } - } - function setExportStatus(vref, exportStatus) { - const { baseRef, facet } = parseVatSlot(vref); + const { baseRef, id, facet } = parseVatSlot(vref); const key = `vom.es.${baseRef}`; const esRaw = syscall.vatstoreGet(key); // If `esRaw` is undefined, it means there's no export status information @@ -155,7 +145,7 @@ export function makeVirtualReferenceManager( // the other hand, if `esRaw` does have a value, the value will be a string // whose length is the facet count. Either way, we will know how many // facets there are. - const es = Array.from(esRaw || 'n'.repeat(getFacetCount(baseRef))); + const es = Array.from(esRaw || 'n'.repeat(getFacetCount(id))); const facetIdx = facet === undefined ? 0 : facet; // The export status of each facet is encoded as: // 's' -> 'recognizable' ('s' for "see"), 'r' -> 'reachable', 'n' -> 'none' @@ -236,6 +226,24 @@ export function makeVirtualReferenceManager( kindInfo.facetNames = facetNames; } + function getFacetNames(kindID) { + return kindInfoTable.get(`${kindID}`).facetNames; + } + + function getFacetCount(kindID) { + const facetNames = getFacetNames(kindID); + return facetNames ? facetNames.length : 1; + } + + function getFacet(kindID, facets, facetIndex) { + const facetName = getFacetNames(kindID)[facetIndex]; + facetName !== undefined || // allow empty-string -named facets + Fail`getFacet missing, ${kindID} [${facetIndex}]`; + const facet = facets[facetName]; + facet || Fail`getFacet missing, ${kindID} [${facetIndex}] ${facetName}`; + return facet; + } + /** * Inquire if a given persistent object kind is a durable kind or not. * @@ -654,6 +662,8 @@ export function makeVirtualReferenceManager( isDurableKind, registerKind, rememberFacetNames, + getFacet, + getFacetNames, reanimate, addReachableVref, removeReachableVref, diff --git a/packages/swingset-liveslots/test/liveslots-helpers.js b/packages/swingset-liveslots/test/liveslots-helpers.js index 626f10d4200..42b53deb5f6 100644 --- a/packages/swingset-liveslots/test/liveslots-helpers.js +++ b/packages/swingset-liveslots/test/liveslots-helpers.js @@ -190,7 +190,6 @@ export async function setupTestLiveslots( syscall, buildRootObject, vatName, - { virtualObjectCacheSize: 0 }, ); async function dispatchMessage(message, ...args) { @@ -219,6 +218,8 @@ export async function setupTestLiveslots( // environment. Nevertheless there's a chance we may be courting some // deeper problem, hence this comment. engineGC(); + engineGC(); + engineGC(); } await dispatch(makeBringOutYourDead()); return rp; diff --git a/packages/swingset-liveslots/test/test-baggage.js b/packages/swingset-liveslots/test/test-baggage.js index 2f3647e0912..e730b839748 100644 --- a/packages/swingset-liveslots/test/test-baggage.js +++ b/packages/swingset-liveslots/test/test-baggage.js @@ -29,7 +29,6 @@ test.serial('exercise baggage', async t => { const baggageVref = fakestore.get('baggageID'); const { subid } = parseVatSlot(baggageVref); const baggageID = Number(subid); - console.log(`baggageID`, baggageID); const kindIDs = JSON.parse(fakestore.get('storeKindIDTable')); // baggage is the first collection created, a scalarDurableMapStore t.is(baggageVref, `o+d${kindIDs.scalarDurableMapStore}/1`); diff --git a/packages/swingset-liveslots/test/test-cache.js b/packages/swingset-liveslots/test/test-cache.js new file mode 100644 index 00000000000..0e8d0f0e9f5 --- /dev/null +++ b/packages/swingset-liveslots/test/test-cache.js @@ -0,0 +1,116 @@ +import test from 'ava'; +import '@endo/init/debug.js'; + +import { makeCache } from '../src/cache.js'; + +test('cache', t => { + const backing = new Map(); + backing.set('key0', 'value0'); + + const log = []; + const readBacking = key => { + log.push(['read', key]); + return backing.get(key); + }; + const writeBacking = (key, value) => { + log.push(['write', key, value]); + backing.set(key, value); + }; + const deleteBacking = key => { + log.push(['delete', key]); + backing.delete(key); + }; + + const c = makeCache(readBacking, writeBacking, deleteBacking); + c.insistClear(); + + // new reads pass through immediately to backing store + t.is(c.get('key0'), 'value0'); + t.deepEqual(log.splice(0), [['read', 'key0']]); + t.is(c.get('key2'), undefined); + t.deepEqual(log.splice(0), [['read', 'key2']]); + + // more reads within the same crank do not + t.is(c.get('key0'), 'value0'); + t.deepEqual(log, []); + + // writes update the cache but do not write through to backing store + c.set('key1', 'value1'); + t.deepEqual(log, []); + + // reads are served from the cache + t.is(c.get('key1'), 'value1'); + t.deepEqual(log, []); + + // new writes update the cache + c.set('key1', 'value2'); + t.deepEqual(log, []); + t.is(c.get('key1'), 'value2'); + t.deepEqual(log, []); + + c.set('key3', 'value3'); + c.set('key2', 'value2'); + t.deepEqual(log, []); + + c.delete('key2'); + t.is(c.get('key2'), undefined); + + c.delete('key4'); // no read from backing store, but schedules a write + t.is(c.get('key4'), undefined); // remembers that it is missing + t.deepEqual(log, []); + + // flush writes/deletes everything dirty, in sorted order + c.flush(); + t.deepEqual(log.splice(0), [ + // key0 is not dirty + ['write', 'key1', 'value2'], + ['delete', 'key2'], + ['write', 'key3', 'value3'], + ['delete', 'key4'], + ]); + c.insistClear(); + + // and now the cache is empty, so reads pass through + t.is(c.get('key3'), 'value3'); + t.deepEqual(log.splice(0), [['read', 'key3']]); + t.is(c.get('key2'), undefined); + t.deepEqual(log.splice(0), [['read', 'key2']]); + t.is(c.get('key0'), 'value0'); + t.deepEqual(log.splice(0), [['read', 'key0']]); + t.is(c.get('key1'), 'value2'); + t.deepEqual(log.splice(0), [['read', 'key1']]); + + // flush sees nothing dirty + c.flush(); + t.deepEqual(log, []); + c.insistClear(); + + t.is(c.get('key0'), 'value0'); + t.deepEqual(log.splice(0), [['read', 'key0']]); + t.throws(() => c.insistClear(), { message: /^cache still has stash/ }); + c.flush(); + t.deepEqual(log, []); + c.insistClear(); + + c.set('key1', 'value3'); + t.deepEqual(log, []); + t.throws(() => c.insistClear(), { message: /^cache still has dirtyKeys/ }); + c.flush(); + t.deepEqual(log.splice(0), [['write', 'key1', 'value3']]); + c.insistClear(); + + // we can delete values that haven't been read in yet + c.delete('key1'); + t.deepEqual(log, []); + c.flush(); + t.deepEqual(log.splice(0), [['delete', 'key1']]); + + // we can delete and overwrite values + c.delete('key3'); + c.set('key3', 'value4'); + c.delete('key3'); + c.set('key3', 'value5'); + t.deepEqual(log, []); + c.flush(); + t.deepEqual(log.splice(0), [['write', 'key3', 'value5']]); +}); diff --git a/packages/swingset-liveslots/test/test-durabilityChecks.js b/packages/swingset-liveslots/test/test-durabilityChecks.js index 8978937647b..29e60067791 100644 --- a/packages/swingset-liveslots/test/test-durabilityChecks.js +++ b/packages/swingset-liveslots/test/test-durabilityChecks.js @@ -6,7 +6,6 @@ import { makeFakeVirtualStuff } from '../tools/fakeVirtualSupport.js'; async function runDurabilityCheckTest(t, relaxDurabilityRules) { const { vom, cm } = makeFakeVirtualStuff({ - cacheSize: 3, relaxDurabilityRules, }); const strict = !relaxDurabilityRules; diff --git a/packages/swingset-liveslots/test/test-facetiousness.js b/packages/swingset-liveslots/test/test-facetiousness.js new file mode 100644 index 00000000000..9440256321f --- /dev/null +++ b/packages/swingset-liveslots/test/test-facetiousness.js @@ -0,0 +1,98 @@ +import '@endo/init/debug.js'; +import test from 'ava'; +import { assessFacetiousness } from '../src/facetiousness.js'; + +const empty = harden({}); + +const single = harden({ + add: (a, b) => a + b, +}); + +const multi = harden({ + incrementer: { + increment: ({ state }) => (state.count += 1), + }, + resetter: { + reset: ({ state }) => (state.count = 0), + }, +}); + +const brokenNested = harden({ + multi, // the "facet" named multi is nested, so broken +}); + +const brokenMixed1 = harden({ + add: (a, b) => a + b, + incrementer: { + increment: ({ state }) => (state.count += 1), + }, +}); + +const brokenMixed2 = harden({ + incrementer: { + increment: ({ state }) => (state.count += 1), + }, + add: (a, b) => a + b, +}); + +const brokenSymbolNamedFacet1 = harden({ + [Symbol.for('resetter')]: { + reset: ({ state }) => (state.count = 0), + }, +}); + +const brokenSymbolNamedFacet2 = harden({ + incrementer: { + increment: ({ state }) => (state.count += 1), + }, + [Symbol.for('resetter')]: { + reset: ({ state }) => (state.count = 0), + }, +}); + +const brokenSymbolNamedFacet3 = harden({ + [Symbol.for('resetter')]: { + reset: ({ state }) => (state.count = 0), + }, + incrementer: { + increment: ({ state }) => (state.count += 1), + }, +}); + +const brokenMultiNonFacet = harden({ + incrementer: { + increment: ({ state }) => (state.count += 1), + }, + data: 4, +}); + +const brokenMixedData1 = harden({ + add: (a, b) => a + b, + data: 4, +}); + +const brokenMixedData2 = harden({ + data: 4, + add: (a, b) => a + b, +}); + +test('facetiousness', t => { + t.is(assessFacetiousness(4), 'not'); + t.is(assessFacetiousness('not'), 'not'); + t.is(assessFacetiousness([]), 'not'); + + t.is(assessFacetiousness(empty), 'one'); + t.is(assessFacetiousness(single), 'one'); + + t.is(assessFacetiousness(multi), 'many'); + + t.is(assessFacetiousness(brokenNested), 'not'); + t.is(assessFacetiousness(brokenMixed1), 'not'); + t.is(assessFacetiousness(brokenMixed2), 'not'); + t.is(assessFacetiousness(brokenSymbolNamedFacet1), 'not'); + t.is(assessFacetiousness(brokenSymbolNamedFacet2), 'not'); + t.is(assessFacetiousness(brokenSymbolNamedFacet3), 'not'); + t.is(assessFacetiousness(brokenMixedData1), 'not'); + t.is(assessFacetiousness(brokenMixedData2), 'not'); + t.is(assessFacetiousness(brokenMultiNonFacet), 'not'); +}); diff --git a/packages/swingset-liveslots/test/test-gc-sensitivity.js b/packages/swingset-liveslots/test/test-gc-sensitivity.js index 0994a2a96d1..4aaf486b63d 100644 --- a/packages/swingset-liveslots/test/test-gc-sensitivity.js +++ b/packages/swingset-liveslots/test/test-gc-sensitivity.js @@ -67,7 +67,7 @@ test.failing('kind handle reanimation', async t => { t.deepEqual(noGCLog, yesGCLog); }); -test.failing('representative reanimation', async t => { +test('representative reanimation', async t => { const { syscall, log } = buildSyscall(); const gcTools = makeMockGC(); diff --git a/packages/swingset-liveslots/test/test-liveslots.js b/packages/swingset-liveslots/test/test-liveslots.js index b871ef31f17..40313fee8b2 100644 --- a/packages/swingset-liveslots/test/test-liveslots.js +++ b/packages/swingset-liveslots/test/test-liveslots.js @@ -667,6 +667,12 @@ test('capdata size limit on syscalls', async t => { key: `vom.vkind.${kid}`, value: `{"kindID":"${kid}","tag":"test"}`, }); + const expectStore = kid => + t.deepEqual(log.shift(), { + type: 'vatstoreSet', + key: `vom.o+v${kid}/1`, + value: `{"x":{"body":"#0","slots":[]}}`, + }); rp = nextRP(); await send('voInitTooManySlots'); @@ -690,6 +696,7 @@ test('capdata size limit on syscalls', async t => { expectFail(); expectVoidReturn(); matchIDCounterSet(t, log); + expectStore(14); t.deepEqual(log, []); rp = nextRP(); @@ -698,6 +705,7 @@ test('capdata size limit on syscalls', async t => { expectFail(); expectVoidReturn(); matchIDCounterSet(t, log); + expectStore(15); t.deepEqual(log, []); rp = nextRP(); diff --git a/packages/swingset-liveslots/test/virtual-objects/test-cross-facet.js b/packages/swingset-liveslots/test/virtual-objects/test-cross-facet.js index cae137ea23d..e77f3030039 100644 --- a/packages/swingset-liveslots/test/virtual-objects/test-cross-facet.js +++ b/packages/swingset-liveslots/test/virtual-objects/test-cross-facet.js @@ -34,10 +34,10 @@ test('forbid cross-facet prototype attack', async t => { thing2.mutable.set(2); t.throws(() => attack1(thing1.mutable, thing2.immutable), { - message: /may only be applied to a valid instance/, + message: /^illegal cross-facet access/, }); t.throws(() => attack2(thing1.mutable, thing2.immutable), { - message: /may only be applied to a valid instance/, + message: /^illegal cross-facet access/, }); t.is(thing1.immutable.get(), 1); t.is(thing2.immutable.get(), 2); diff --git a/packages/swingset-liveslots/test/virtual-objects/test-facets.js b/packages/swingset-liveslots/test/virtual-objects/test-facets.js new file mode 100644 index 00000000000..f604a1dd82c --- /dev/null +++ b/packages/swingset-liveslots/test/virtual-objects/test-facets.js @@ -0,0 +1,23 @@ +import test from 'ava'; +import '@endo/init/debug.js'; + +import { makeFakeVirtualObjectManager } from '../../tools/fakeVirtualSupport.js'; + +test('facets', async t => { + const vom = makeFakeVirtualObjectManager(); + const init = () => ({ value: 0 }); + const behavior = { + mutable: { + set: ({ state }, value) => (state.value = value), + get: ({ state }) => state.value, + getImmutable: ({ facets }) => facets.immutable, + }, + immutable: { + get: ({ state }) => state.value, + }, + }; + const makeThing = vom.defineKindMulti('thing', init, behavior); + const thing1 = makeThing(); + thing1.mutable.set(1); + t.is(thing1.mutable.getImmutable(), thing1.immutable); +}); diff --git a/packages/swingset-liveslots/test/virtual-objects/test-reachable-vrefs.js b/packages/swingset-liveslots/test/virtual-objects/test-reachable-vrefs.js index 09602b2eba5..d772f5fdfd7 100644 --- a/packages/swingset-liveslots/test/virtual-objects/test-reachable-vrefs.js +++ b/packages/swingset-liveslots/test/virtual-objects/test-reachable-vrefs.js @@ -8,8 +8,7 @@ import { makeVatSlot } from '../../src/parseVatSlots.js'; import { makeFakeVirtualStuff } from '../../tools/fakeVirtualSupport.js'; test('VOM tracks reachable vrefs', async t => { - const vomOptions = { cacheSize: 3 }; - const { vom, vrm, cm } = makeFakeVirtualStuff(vomOptions); + const { vom, vrm, cm } = makeFakeVirtualStuff(); const { defineKind } = vom; const { makeScalarBigWeakMapStore } = cm; const weakStore = makeScalarBigWeakMapStore('test'); diff --git a/packages/swingset-liveslots/test/virtual-objects/test-retain-remotable.js b/packages/swingset-liveslots/test/virtual-objects/test-retain-remotable.js index 06636152e83..84035f74d11 100644 --- a/packages/swingset-liveslots/test/virtual-objects/test-retain-remotable.js +++ b/packages/swingset-liveslots/test/virtual-objects/test-retain-remotable.js @@ -55,7 +55,7 @@ function stashRemotableFour(holderMaker) { test('remotables retained by virtualized data', async t => { const gcAndFinalize = makeGcAndFinalize(engineGC); - const vomOptions = { cacheSize: 3, weak: true }; + const vomOptions = { weak: true }; const { vom, cm } = makeFakeVirtualStuff(vomOptions); const { defineKind } = vom; const { makeScalarBigWeakMapStore } = cm; diff --git a/packages/swingset-liveslots/test/virtual-objects/test-virtualObjectCache.js b/packages/swingset-liveslots/test/virtual-objects/test-virtualObjectCache.js deleted file mode 100644 index ded8f932b37..00000000000 --- a/packages/swingset-liveslots/test/virtual-objects/test-virtualObjectCache.js +++ /dev/null @@ -1,157 +0,0 @@ -import test from 'ava'; -import '@endo/init/debug.js'; - -import { makeCache } from '../../src/virtualObjectManager.js'; - -function makeFakeStore() { - const backing = new Map(); - let log = []; - return { - fetch(key) { - const result = backing.get(key); - log.push(['fetch', key, result]); - return result; - }, - store(key, value) { - log.push(['store', key, value]); - backing.set(key, value); - }, - getLog() { - const result = log; - log = []; - return result; - }, - dump() { - const result = []; - for (const entry of backing.entries()) { - result.push(entry); - } - result.sort((e1, e2) => e1[0].localeCompare(e2[0])); - return result; - }, - }; -} - -function makeThing(n) { - // for testing purposes, all we create is the inner self; there's no actual - // object above it - return { - baseRef: `t${n}`, - rawState: `thing #${n}`, - dirty: false, - }; -} - -test('cache overflow and refresh', t => { - const store = makeFakeStore(); - const cache = makeCache(3, store.fetch, store.store); - const things = []; - - for (let i = 0; i < 6; i += 1) { - const thing = makeThing(i); - things.push(thing); - cache.remember(thing); - cache.markDirty(thing); - } - // cache: t5, t4, t3, t2 - - // after initialization - t.is(things[0].rawState, null); - t.is(things[1].rawState, null); - t.is(things[2].rawState, 'thing #2'); - t.is(things[3].rawState, 'thing #3'); - t.is(things[4].rawState, 'thing #4'); - t.is(things[5].rawState, 'thing #5'); - t.deepEqual(store.getLog(), [ - ['store', 't0', 'thing #0'], - ['store', 't1', 'thing #1'], - ]); - - // lookup that refreshes - cache.lookup('t2'); // cache: t2, t5, t4, t3 - t.is(things[5].rawState, 'thing #5'); - t.deepEqual(store.getLog(), []); - - // lookup that has no effect - things[0] = cache.lookup('t0'); // cache: t0, t2, t5, t4 - things[0].rawState = 'changed thing #0'; - cache.markDirty(things[0]); // pretend we changed it - t.is(things[0].rawState, 'changed thing #0'); - t.is(things[3].rawState, null); - t.deepEqual(store.getLog(), [['store', 't3', 'thing #3']]); - - // verify refresh - cache.refresh(things[4]); // cache: t4, t0, t2, t5 - things[1] = cache.lookup('t1'); // cache: t1, t4, t0, t2 - t.is(things[1].rawState, null); - t.is(things[5].rawState, null); - t.deepEqual(store.getLog(), [['store', 't5', 'thing #5']]); - - // verify that everything is there - t.truthy(things[0].dirty); - t.falsy(things[1].dirty); - t.truthy(things[2].dirty); - t.falsy(things[3].dirty); - t.truthy(things[4].dirty); - t.falsy(things[5].dirty); - cache.flush(); // cache: empty - t.falsy(things[0].dirty); - t.falsy(things[1].dirty); - t.falsy(things[2].dirty); - t.falsy(things[3].dirty); - t.falsy(things[4].dirty); - t.falsy(things[5].dirty); - t.is(things[0].rawState, 'changed thing #0'); - t.is(things[1].rawState, null); - t.is(things[2].rawState, 'thing #2'); - t.is(things[3].rawState, null); - t.is(things[4].rawState, 'thing #4'); - t.is(things[5].rawState, null); - t.deepEqual(store.getLog(), [ - ['store', 't2', 'thing #2'], - ['store', 't0', 'changed thing #0'], - ['store', 't4', 'thing #4'], - ]); - t.deepEqual(store.dump(), [ - ['t0', 'changed thing #0'], - ['t1', 'thing #1'], - ['t2', 'thing #2'], - ['t3', 'thing #3'], - ['t4', 'thing #4'], - ['t5', 'thing #5'], - ]); - - // verify that changes get written - things[0] = cache.lookup('t0'); // cache: t0 - things[0].rawState = 'new thing #0'; - things[0].dirty = true; - things[1] = cache.lookup('t1'); // cache: t1, t0 - things[1].rawState = 'new thing #1'; - things[1].dirty = true; - things[2] = cache.lookup('t2'); // cache: t2, t1, t0 - things[2].rawState = 'new thing #2'; - things[2].dirty = true; - things[3] = cache.lookup('t3'); // cache: t3, t2, t1, t0 - things[3].rawState = 'new thing #3'; - things[3].dirty = true; - things[4] = cache.lookup('t4'); // cache: t4, t3, t2, t1 - things[4].rawState = 'new thing #4'; - things[4].dirty = true; - things[5] = cache.lookup('t5'); // cache: t5, t4, t3, t2 - things[5].rawState = 'new thing #5'; - things[5].dirty = true; - t.is(things[0].rawState, null); - t.is(things[5].rawState, 'new thing #5'); - t.deepEqual(store.getLog(), [ - ['store', 't0', 'new thing #0'], - ['store', 't1', 'new thing #1'], - ]); - t.deepEqual(store.dump(), [ - ['t0', 'new thing #0'], - ['t1', 'new thing #1'], - ['t2', 'thing #2'], - ['t3', 'thing #3'], - ['t4', 'thing #4'], - ['t5', 'thing #5'], - ]); -}); diff --git a/packages/swingset-liveslots/test/virtual-objects/test-virtualObjectManager.js b/packages/swingset-liveslots/test/virtual-objects/test-virtualObjectManager.js index 3c6fc42cc25..74cd48b8cdb 100644 --- a/packages/swingset-liveslots/test/virtual-objects/test-virtualObjectManager.js +++ b/packages/swingset-liveslots/test/virtual-objects/test-virtualObjectManager.js @@ -82,8 +82,7 @@ function zotVal(arbitrary, name, tag, count) { test('multifaceted virtual objects', t => { const log = []; - const { defineKindMulti } = makeFakeVirtualObjectManager({ - cacheSize: 0, + const { defineKindMulti, flushStateCache } = makeFakeVirtualObjectManager({ log, }); @@ -125,20 +124,34 @@ test('multifaceted virtual objects', t => { decr.dec(); t.is(decr.getCount(), 1); const other = makeMultiThing('other'); - t.is(log.shift(), `get kindIDID => undefined`); - t.is(log.shift(), `set kindIDID 1`); - t.is(log.shift(), `set vom.vkind.2 {"kindID":"2","tag":"multithing"}`); - t.is(log.shift(), `set vom.${kid}/1 ${multiThingVal('foo', 1)}`); - t.deepEqual(log, []); + + flushStateCache(); + t.deepEqual(log.splice(0), [ + `get kindIDID => undefined`, + `set kindIDID 1`, + `set vom.vkind.2 {"kindID":"2","tag":"multithing"}`, + `set vom.${kid}/1 ${multiThingVal('foo', 1)}`, + `set vom.${kid}/2 ${multiThingVal('other', 0)}`, + ]); + incr.inc(); - t.is(log.shift(), `set vom.${kid}/2 ${multiThingVal('other', 0)}`); - t.is(log.shift(), `get vom.${kid}/1 => ${multiThingVal('foo', 1)}`); + t.deepEqual(log.splice(0), [ + `get vom.${kid}/1 => ${multiThingVal('foo', 1)}`, + ]); + other.decr.dec(); - t.is(log.shift(), `set vom.${kid}/1 ${multiThingVal('foo', 2)}`); - t.is(log.shift(), `get vom.${kid}/2 => ${multiThingVal('other', 0)}`); + t.deepEqual(log.splice(0), [ + `get vom.${kid}/2 => ${multiThingVal('other', 0)}`, + ]); + incr.inc(); - t.is(log.shift(), `set vom.${kid}/2 ${multiThingVal('other', -1)}`); - t.is(log.shift(), `get vom.${kid}/1 => ${multiThingVal('foo', 2)}`); + t.deepEqual(log, []); + + flushStateCache(); + t.deepEqual(log.splice(0), [ + `set vom.${kid}/1 ${multiThingVal('foo', 3)}`, + `set vom.${kid}/2 ${multiThingVal('other', -1)}`, + ]); t.deepEqual(log, []); }); @@ -163,7 +176,7 @@ test('multi-faceted object definition fails with unfaceted behavior', t => { // prettier-ignore test('virtual object operations', t => { const log = []; - const { defineKind, flushCache, dumpStore } = makeFakeVirtualObjectManager({ cacheSize: 3, log }); + const { defineKind, flushStateCache, dumpStore } = makeFakeVirtualObjectManager({ log }); const makeThing = defineKind('thing', initThing, thingBehavior); const tid = 'o+v2'; @@ -177,39 +190,50 @@ test('virtual object operations', t => { ['vom.vkind.3', '{"kindID":"3","tag":"zot"}'], ]); + // note: the "[t1-0].." comments show the expected cache contents, + // with "*" meaning "dirty" + // phase 1: object creations - const thing1 = makeThing('thing-1'); // [t1-0] + const thing1 = makeThing('thing-1'); // [t1-0*] // t1-0: 'thing-1' 0 0 - const thing2 = makeThing('thing-2', 100); // [t2-0 t1-0] + const thing2 = makeThing('thing-2', 100); // [t2-0* t1-0*] // t2-0: 'thing-2' 100 0 - const thing3 = makeThing('thing-3', 200); // [t3-0 t2-0 t1-0] + const thing3 = makeThing('thing-3', 200); // [t3-0* t2-0* t1-0*] // t3-0: 'thing-3' 200 0 - const thing4 = makeThing('thing-4', 300); // [t4-0 t3-0 t2-0 t1-0] + const thing4 = makeThing('thing-4', 300); // [t4-0* t3-0* t2-0* t1-0*] // t4-0: 'thing-4' 300 0 t.is(log.shift(), `get kindIDID => undefined`); t.is(log.shift(), `set kindIDID 1`); t.is(log.shift(), `set vom.vkind.2 {"kindID":"2","tag":"thing"}`); t.is(log.shift(), `set vom.vkind.3 {"kindID":"3","tag":"zot"}`); t.deepEqual(log, []); + flushStateCache(); + t.is(log.shift(), `set vom.${tid}/1 ${thingVal(0, 'thing-1', 0)}`); + t.is(log.shift(), `set vom.${tid}/2 ${thingVal(100, 'thing-2', 0)}`); + t.is(log.shift(), `set vom.${tid}/3 ${thingVal(200, 'thing-3', 0)}`); + t.is(log.shift(), `set vom.${tid}/4 ${thingVal(300, 'thing-4', 0)}`); + t.deepEqual(log, []); - const zot1 = makeZot(23, 'Alice', 'is this on?'); // [z1-0 t4-0 t3-0 t2-0] evict t1-0 + const zot1 = makeZot(23, 'Alice', 'is this on?'); // [z1-0*] // z1-0: 23 'Alice' 'is this on?' 0 - t.is(log.shift(), `set vom.${tid}/1 ${thingVal(0, 'thing-1', 0)}`); // evict t1-0 t.deepEqual(log, []); - const zot2 = makeZot(29, 'Bob', 'what are you saying?'); // [z2-0 z1-0 t4-0 t3-0] evict t2-0 + const zot2 = makeZot(29, 'Bob', 'what are you saying?'); // [z2-0* z1-0*] // z2-0: 29 'Bob' 'what are you saying?' 0 - t.is(log.shift(), `set vom.${tid}/2 ${thingVal(100, 'thing-2', 0)}`); // evict t2-0 t.deepEqual(log, []); - const zot3 = makeZot(47, 'Carol', 'as if...'); // [z3-0 z2-0 z1-0 t4-0] evict t3-0 + const zot3 = makeZot(47, 'Carol', 'as if...'); // [z3-0* z2-0* z1-0*] // z3-0: 47 'Carol' 'as if...' 0 - t.is(log.shift(), `set vom.${tid}/3 ${thingVal(200, 'thing-3', 0)}`); // evict t3-0 t.deepEqual(log, []); - const zot4 = makeZot(66, 'Dave', 'you and what army?'); // [z4-0 z3-0 z2-0 z1-0] evict t4-0 + const zot4 = makeZot(66, 'Dave', 'you and what army?'); // [z4-0* z3-0* z2-0* z1-0*] // z4-0: 66 'Dave' 'you and what army?' 0 - t.is(log.shift(), `set vom.${tid}/4 ${thingVal(300, 'thing-4', 0)}`); // evict t4-0 + t.deepEqual(log, []); + flushStateCache(); + t.is(log.shift(), `set vom.${zid}/1 ${zotVal(23, 'Alice', 'is this on?', 0)}`); // z1-0 + t.is(log.shift(), `set vom.${zid}/2 ${zotVal(29, 'Bob', 'what are you saying?', 0)}`); // z2-0 + t.is(log.shift(), `set vom.${zid}/3 ${zotVal(47, 'Carol', 'as if...', 0)}`); // z3-0 + t.is(log.shift(), `set vom.${zid}/4 ${zotVal(66, 'Dave', 'you and what army?', 0)}`); // z4-0 t.deepEqual(log, []); t.deepEqual(dumpStore(), [ @@ -218,168 +242,179 @@ test('virtual object operations', t => { [`vom.${tid}/2`, thingVal(100, 'thing-2', 0)], // =t2-0 [`vom.${tid}/3`, thingVal(200, 'thing-3', 0)], // =t3-0 [`vom.${tid}/4`, thingVal(300, 'thing-4', 0)], // =t4-0 + [`vom.${zid}/1`, zotVal(23, 'Alice', 'is this on?', 0)], // =z1-0 + [`vom.${zid}/2`, zotVal(29, 'Bob', 'what are you saying?', 0)], // =z2-0 + [`vom.${zid}/3`, zotVal(47, 'Carol', 'as if...', 0)], // =z3-0 + [`vom.${zid}/4`, zotVal(66, 'Dave', 'you and what army?', 0)], // =z4-0 ['vom.vkind.2', '{"kindID":"2","tag":"thing"}'], ['vom.vkind.3', '{"kindID":"3","tag":"zot"}'], ]); // phase 2: first batch-o-stuff - t.is(thing1.inc(), 1); // [t1-1 z4-0 z3-0 z2-0] evict z1-0 - t.is(log.shift(), `set vom.${zid}/1 ${zotVal(23, 'Alice', 'is this on?', 0)}`); // evict z1-0 + // t1-0 -> t1-1 + t.is(thing1.inc(), 1); // [t1-1*] t.is(log.shift(), `get vom.${tid}/1 => ${thingVal(0, 'thing-1', 0)}`); // load t1-0 t.deepEqual(log, []); - // t1-1: 'thing-1' 1 0 - t.is(zot1.sayHello('hello'), 'hello Alice'); // [z1-1 t1-1 z4-0 z3-0] evict z2-0 - t.is(log.shift(), `set vom.${zid}/2 ${zotVal(29, 'Bob', 'what are you saying?', 0)}`); // evict z2-0 + // z1-0 -> z1-1 + t.is(zot1.sayHello('hello'), 'hello Alice'); // [t1-1* z1-1*] t.is(log.shift(), `get vom.${zid}/1 => ${zotVal(23, 'Alice', 'is this on?', 0)}`); // load z1-0 t.deepEqual(log, []); - // z1-1: 23 'Alice' 'is this on?' 1 - t.is(thing1.inc(), 2); // [t1-2 z1-1 z4-0 z3-0] + // t1-1 -> t1-2 (no load, already in cache) + t.is(thing1.inc(), 2); // [t1-2* z1-1*] t.deepEqual(log, []); - // t1-2: 'thing-1' 2 0 - t.is(zot2.sayHello('hi'), 'hi Bob'); // [z2-1 t1-2 z1-1 z4-0] evict z3-0 - t.is(log.shift(), `set vom.${zid}/3 ${zotVal(47, 'Carol', 'as if...', 0)}`); // evict z3-0 + // z2-0 -> z2-1 + t.is(zot2.sayHello('hi'), 'hi Bob'); // [t1-2* z1-1* z2-1*] t.is(log.shift(), `get vom.${zid}/2 => ${zotVal(29, 'Bob', 'what are you saying?', 0)}`); // load z2-0 t.deepEqual(log, []); - // z2-1: 29 'Bob' 'what are you saying?' 1 - t.is(thing1.inc(), 3); // [t1-3 z2-1 z1-1 z4-0] + // t1-2 -> t1-3 + t.is(thing1.inc(), 3); // [t1-3* z1-1* z2-1*] t.deepEqual(log, []); - // t1-3: 'thing-1' 3 0 - t.is(zot3.sayHello('aloha'), 'aloha Carol'); // [z3-1 t1-3 z2-1 z1-1] evict z4-0 - t.is(log.shift(), `set vom.${zid}/4 ${zotVal(66, 'Dave', 'you and what army?', 0)}`); // evict z4-0 + // z3-0 -> z3-1 + t.is(zot3.sayHello('aloha'), 'aloha Carol'); // [t1-3* z1-1* z2-1* z3-1*] t.is(log.shift(), `get vom.${zid}/3 => ${zotVal(47, 'Carol', 'as if...', 0)}`); // load z3-0 t.deepEqual(log, []); - // z3-1: 47 'Carol' 'as if...' 1 - t.is(zot4.sayHello('bonjour'), 'bonjour Dave'); // [z4-1 z3-1 t1-3 z2-1] evict z1-1 - t.is(log.shift(), `set vom.${zid}/1 ${zotVal(23, 'Alice', 'is this on?', 1)}`); // evict z1-1 + // z4-0 -> z4-1 + t.is(zot4.sayHello('bonjour'), 'bonjour Dave'); // [t1-3* z1-1* z2-1* z3-1* z4-1*] t.is(log.shift(), `get vom.${zid}/4 => ${zotVal(66, 'Dave', 'you and what army?', 0)}`); // load z4-0 t.deepEqual(log, []); - // z4-1: 66 'Dave' 'you and what army?' 1 - t.is(zot1.sayHello('hello again'), 'hello again Alice'); // [z1-2 z4-1 z3-1 t1-3] evict z2-1 - t.is(log.shift(), `set vom.${zid}/2 ${zotVal(29, 'Bob', 'what are you saying?', 1)}`); // evict z2-1 - t.is(log.shift(), `get vom.${zid}/1 => ${zotVal(23, 'Alice', 'is this on?', 1)}`); // get z1-1 + // z1-1 -> z1-2 + t.is(zot1.sayHello('hello again'), 'hello again Alice'); // [t1-3* z1-2* z2-1* z3-1* z4-1*] t.deepEqual(log, []); - // z1-2: 23 'Alice' 'is this on?' 2 + // read t2-0 t.is( - thing2.describe(), // [t2-0 z1-2 z4-1 z3-1] evict t1-3 + thing2.describe(), 'thing-2 counter has been reset 0 times and is now 100', ); - t.is(log.shift(), `set vom.${tid}/1 ${thingVal(3, 'thing-1', 0)}`); // evict t1-3 + // [t1-3* t2-0 z1-2* z2-1* z3-1* z4-1*] t.is(log.shift(), `get vom.${tid}/2 => ${thingVal(100, 'thing-2', 0)}`); // load t2-0 t.deepEqual(log, []); + flushStateCache(); + t.is(log.shift(), `set vom.${tid}/1 ${thingVal(3, 'thing-1', 0)}`); // write t1-3 + t.is(log.shift(), `set vom.${zid}/1 ${zotVal(23, 'Alice', 'is this on?', 2)}`); // write z1-2 + t.is(log.shift(), `set vom.${zid}/2 ${zotVal(29, 'Bob', 'what are you saying?', 1)}`); // write z2-1 + t.is(log.shift(), `set vom.${zid}/3 ${zotVal(47, 'Carol', 'as if...', 1)}`); // write z3-1 + t.is(log.shift(), `set vom.${zid}/4 ${zotVal(66, 'Dave', 'you and what army?', 1)}`); // write z4-1 + t.deepEqual(log, []); + t.deepEqual(dumpStore(), [ ['kindIDID', '1'], [`vom.${tid}/1`, thingVal(3, 'thing-1', 0)], // =t1-3 [`vom.${tid}/2`, thingVal(100, 'thing-2', 0)], // =t2-0 [`vom.${tid}/3`, thingVal(200, 'thing-3', 0)], // =t3-0 [`vom.${tid}/4`, thingVal(300, 'thing-4', 0)], // =t4-0 - [`vom.${zid}/1`, zotVal(23, 'Alice', 'is this on?', 1)], // =z1-1 + [`vom.${zid}/1`, zotVal(23, 'Alice', 'is this on?', 2)], // =z1-2 [`vom.${zid}/2`, zotVal(29, 'Bob', 'what are you saying?', 1)], // =z2-1 - [`vom.${zid}/3`, zotVal(47, 'Carol', 'as if...', 0)], // =z3-0 - [`vom.${zid}/4`, zotVal(66, 'Dave', 'you and what army?', 0)], // =z4-0 + [`vom.${zid}/3`, zotVal(47, 'Carol', 'as if...', 1)], // =z3-1 + [`vom.${zid}/4`, zotVal(66, 'Dave', 'you and what army?', 1)], // =z4-1 ['vom.vkind.2', '{"kindID":"2","tag":"thing"}'], ['vom.vkind.3', '{"kindID":"3","tag":"zot"}'], ]); // phase 3: second batch-o-stuff - t.is(thing1.get(), 3); // [t1-3 t2-0 z1-2 z4-1] evict z3-1 - t.is(log.shift(), `set vom.${zid}/3 ${zotVal(47, 'Carol', 'as if...', 1)}`); // evict z3-1 + // read t1-3 + t.is(thing1.get(), 3); // [t1-3] t.is(log.shift(), `get vom.${tid}/1 => ${thingVal(3, 'thing-1', 0)}`); // load t1-3 t.deepEqual(log, []); - t.is(thing1.inc(), 4); // [t1-4 t2-0 z1-2 z4-1] + // t1-3 -> t1-4 + t.is(thing1.inc(), 4); // [t1-4*] t.deepEqual(log, []); - // t1-4: 'thing-1' 4 0 - t.is(thing4.reset(1000), 1); // [t4-1 t1-4 t2-0 z1-2] evict z4-1 - t.is(log.shift(), `set vom.${zid}/4 ${zotVal(66, 'Dave', 'you and what army?', 1)}`); // evict z4-1 + // t4-0 -> t4-1 + t.is(thing4.reset(1000), 1); // [t1-4* t4-1*] t.is(log.shift(), `get vom.${tid}/4 => ${thingVal(300, 'thing-4', 0)}`); // load t4-0 t.deepEqual(log, []); - // t4-1: 'thing-4' 1000 1 - t.is(zot3.rename('Chester'), 'Chester'); // [z3-2 t4-1 t1-4 t2-0] evict z1-2 - t.is(log.shift(), `set vom.${zid}/1 ${zotVal(23, 'Alice', 'is this on?', 2)}`); // evict z1-2 + // z3-1 -> z3-2 + t.is(zot3.rename('Chester'), 'Chester'); // [t1-4* t4-1* z3-2*] t.is(log.shift(), `get vom.${zid}/3 => ${zotVal(47, 'Carol', 'as if...', 1)}`); // load z3-1 t.deepEqual(log, []); - // z3-2: 47 'Chester' 'as if...' 2 - t.is(zot1.getInfo(), 'zot Alice tag=is this on? count=3 arbitrary=23'); // [z1-3 z3-2 t4-1 t1-4] evict t2-0 - // evict t2-0 does nothing because t2 is not dirty + // z1-2 -> z1-3 + t.is(zot1.getInfo(), 'zot Alice tag=is this on? count=3 arbitrary=23'); // [t1-4* t4-1* z1-3* z3-2*] t.is(log.shift(), `get vom.${zid}/1 => ${zotVal(23, 'Alice', 'is this on?', 2)}`); // load z1-2 t.deepEqual(log, []); - // z1-3: 23 'Alice' 'is this on?' 3 - t.is(zot2.getInfo(), 'zot Bob tag=what are you saying? count=2 arbitrary=29'); // [z2-2 z1-3 z3-2 t4-1] evict t1-4 - t.is(log.shift(), `set vom.${tid}/1 ${thingVal(4, 'thing-1', 0)}`); // evict t1-4 + // z2-1 -> z2-2 + t.is(zot2.getInfo(), 'zot Bob tag=what are you saying? count=2 arbitrary=29'); // [t1-4* t4-1* z1-3* z2-2* z3-2*] t.is(log.shift(), `get vom.${zid}/2 => ${zotVal(29, 'Bob', 'what are you saying?', 1)}`); // load z2-1 t.deepEqual(log, []); - // z2-2: 29 'Bob' 'what are you saying?' 1 + // read t2-0 t.is( - thing2.describe(), // [t2-0 z2-2 z1-3 z3-2] evict t4-1 + thing2.describe(), 'thing-2 counter has been reset 0 times and is now 100', ); - t.is(log.shift(), `set vom.${tid}/4 ${thingVal(1000, 'thing-4', 1)}`); // evict t4-1 + // [t1-4* t2-0 t4-1* z1-3* z2-2* z3-2*] t.is(log.shift(), `get vom.${tid}/2 => ${thingVal(100, 'thing-2', 0)}`); // load t2-0 t.deepEqual(log, []); - t.is(zot3.getInfo(), 'zot Chester tag=as if... count=3 arbitrary=47'); // [z3-3 t2-0 z2-2 z1-3] + // z3-2 -> z3-3, already in cache + t.is(zot3.getInfo(), 'zot Chester tag=as if... count=3 arbitrary=47'); + // [t1-4* t2-0 t4-1* z1-3* z2-2* z3-3*] t.deepEqual(log, []); - // z3-3: 47 'Chester' 'as if...' 3 - t.is(zot4.getInfo(), 'zot Dave tag=you and what army? count=2 arbitrary=66'); // [z4-1 z3-3 t2-0 z2-2] evict z1-3 - t.is(log.shift(), `set vom.${zid}/1 ${zotVal(23, 'Alice', 'is this on?', 3)}`); // evict z1-3 + // read z4-1 + t.is(zot4.getInfo(), 'zot Dave tag=you and what army? count=2 arbitrary=66'); + // [t1-4* t2-0 t4-1* z1-3* z2-2* z3-3* z4-1] t.is(log.shift(), `get vom.${zid}/4 => ${zotVal(66, 'Dave', 'you and what army?', 1)}`); // load z4-1 t.deepEqual(log, []); - // z4-2: 66 'Dave' 'you and what army?' 2 - t.is(thing3.inc(), 201); // [t3-1 z4-2 z3-3 t2-0] evict z2-2 - t.is(log.shift(), `set vom.${zid}/2 ${zotVal(29, 'Bob', 'what are you saying?', 2)}`); // evict z2-2 + // t3-0 -> t3-1 + t.is(thing3.inc(), 201); + // [t1-4* t2-0 t3-1* t4-1* z1-3* z2-2* z3-3* z4-1] t.is(log.shift(), `get vom.${tid}/3 => ${thingVal(200, 'thing-3', 0)}`); // load t3-0 t.deepEqual(log, []); - // t3-1: 'thing-3' 201 0 + // read t4-1, already in cache t.is( - thing4.describe(), // [t4-1 t3-1 z4-2 z3-3] evict t2-0 + thing4.describe(), 'thing-4 counter has been reset 1 times and is now 1000', ); - // evict t2-0 does nothing because t2 is not dirty - t.is(log.shift(), `get vom.${tid}/4 => ${thingVal(1000, 'thing-4', 1)}`); // load t4-1 + // [t1-4* t2-0 t3-1* t4-1* z1-3* z2-2* z3-3* z4-1] + t.deepEqual(log, []); + + flushStateCache(); + t.is(log.shift(), `set vom.${tid}/1 ${thingVal(4, 'thing-1', 0)}`); // write t1-4 + // t2-0 is not written because t2 is not dirty + t.is(log.shift(), `set vom.${tid}/3 ${thingVal(201, 'thing-3', 0)}`); // write t3-1 + t.is(log.shift(), `set vom.${tid}/4 ${thingVal(1000, 'thing-4', 1)}`); // write t4-1 + t.is(log.shift(), `set vom.${zid}/1 ${zotVal(23, 'Alice', 'is this on?', 3)}`); // write z1-3 + t.is(log.shift(), `set vom.${zid}/2 ${zotVal(29, 'Bob', 'what are you saying?', 2)}`); // write z2-2 + t.is(log.shift(), `set vom.${zid}/3 ${zotVal(47, 'Chester', 'as if...', 3)}`); // write z3-3 + t.is(log.shift(), `set vom.${zid}/4 ${zotVal(66, 'Dave', 'you and what army?', 2)}`); // write z4-2 t.deepEqual(log, []); t.deepEqual(dumpStore(), [ ['kindIDID', '1'], [`vom.${tid}/1`, thingVal(4, 'thing-1', 0)], // =t1-4 [`vom.${tid}/2`, thingVal(100, 'thing-2', 0)], // =t2-0 - [`vom.${tid}/3`, thingVal(200, 'thing-3', 0)], // =t3-0 + [`vom.${tid}/3`, thingVal(201, 'thing-3', 0)], // =t3-1 [`vom.${tid}/4`, thingVal(1000, 'thing-4', 1)], // =t4-1 [`vom.${zid}/1`, zotVal(23, 'Alice', 'is this on?', 3)], // =z1-3 [`vom.${zid}/2`, zotVal(29, 'Bob', 'what are you saying?', 2)], // =z2-2 - [`vom.${zid}/3`, zotVal(47, 'Carol', 'as if...', 1)], // =z3-1 - [`vom.${zid}/4`, zotVal(66, 'Dave', 'you and what army?', 1)], // =z4-1 + [`vom.${zid}/3`, zotVal(47, 'Chester', 'as if...', 3)], // =z3-3 + [`vom.${zid}/4`, zotVal(66, 'Dave', 'you and what army?', 2)], // =z4-2 ['vom.vkind.2', '{"kindID":"2","tag":"thing"}'], ['vom.vkind.3', '{"kindID":"3","tag":"zot"}'], ]); - // phase 4: flush test - t.is(thing1.inc(), 5); // [t1-5 t4-1 t3-1 z4-2] evict z3-3 - t.is(log.shift(), `set vom.${zid}/3 ${zotVal(47, 'Chester', 'as if...', 3)}`); // evict z3-3 + // phase 4 + // t1-4 -> t1-5 + t.is(thing1.inc(), 5); // [t1-5*] t.is(log.shift(), `get vom.${tid}/1 => ${thingVal(4, 'thing-1', 0)}`); // load t1-4 t.deepEqual(log, []); - // t1-5: 'thing-1' 5 0 - flushCache(); // [] evict z4-2 t3-1 t4-1 t1-5 - t.is(log.shift(), `set vom.${zid}/4 ${zotVal(66, 'Dave', 'you and what army?', 2)}`); // evict z4-2 - t.is(log.shift(), `set vom.${tid}/3 ${thingVal(201, 'thing-3', 0)}`); // evict t3-1 - // evict t4-1 does nothing because t4 is not dirty + flushStateCache(); t.is(log.shift(), `set vom.${tid}/1 ${thingVal(5, 'thing-1', 0)}`); // evict t1-5 t.deepEqual(log, []); @@ -400,10 +435,10 @@ test('virtual object operations', t => { test('symbol named methods', t => { const log = []; - const { defineKind, flushCache, dumpStore } = makeFakeVirtualObjectManager({ - cacheSize: 0, - log, - }); + const { defineKind, flushStateCache, dumpStore } = + makeFakeVirtualObjectManager({ + log, + }); const IncSym = Symbol.for('incsym'); @@ -427,48 +462,45 @@ test('symbol named methods', t => { ]); // phase 1: object creations - const thing1 = makeThing('thing-1'); // [t1-0] + const thing1 = makeThing('thing-1'); // [t1-0*] // t1-0: 'thing-1' 0 0 - const thing2 = makeThing('thing-2', 100); // [t2-0] + const thing2 = makeThing('thing-2', 100); // [t1-0* t2-0*] // t2-0: 'thing-2' 100 0 t.is(log.shift(), `get kindIDID => undefined`); t.is(log.shift(), `set kindIDID 1`); t.is(log.shift(), `set vom.vkind.2 {"kindID":"2","tag":"symthing"}`); - t.is(log.shift(), `set vom.${tid}/1 ${thingVal(0, 'thing-1', 0)}`); // evict t1-0 + t.deepEqual(log, []); + flushStateCache(); + t.is(log.shift(), `set vom.${tid}/1 ${thingVal(0, 'thing-1', 0)}`); // write t1-0 + t.is(log.shift(), `set vom.${tid}/2 ${thingVal(100, 'thing-2', 0)}`); // write t2-0 t.deepEqual(log, []); t.deepEqual(dumpStore(), [ ['kindIDID', '1'], [`vom.${tid}/1`, thingVal(0, 'thing-1', 0)], // =t1-0 + [`vom.${tid}/2`, thingVal(100, 'thing-2', 0)], // =t2-0 ['vom.vkind.2', '{"kindID":"2","tag":"symthing"}'], ]); // phase 2: call symbol-named method on thing1 - t.is(thing1[IncSym](), 1); // [t1-1] evict t2-0 - t.is(log.shift(), `set vom.${tid}/2 ${thingVal(100, 'thing-2', 0)}`); // evict t2-0 + t.is(thing1[IncSym](), 1); // [t1-1*] t.is(log.shift(), `get vom.${tid}/1 => ${thingVal(0, 'thing-1', 0)}`); // load t1-0 t.deepEqual(log, []); + flushStateCache(); + t.is(log.shift(), `set vom.${tid}/1 ${thingVal(1, 'thing-1', 0)}`); // write t1-1 + t.deepEqual(log, []); t.deepEqual(dumpStore(), [ ['kindIDID', '1'], - [`vom.${tid}/1`, thingVal(0, 'thing-1', 0)], // =t1-0 + [`vom.${tid}/1`, thingVal(1, 'thing-1', 0)], // =t1-1 [`vom.${tid}/2`, thingVal(100, 'thing-2', 0)], // =t2-0 ['vom.vkind.2', '{"kindID":"2","tag":"symthing"}'], ]); // phase 3: call symbol-named method on thing2 - t.is(thing2[IncSym](), 101); // [t2-1] evict t1-0 - t.is(log.shift(), `set vom.${tid}/1 ${thingVal(1, 'thing-1', 0)}`); // evict t1-1 + t.is(thing2[IncSym](), 101); // [t2-1*] t.is(log.shift(), `get vom.${tid}/2 => ${thingVal(100, 'thing-2', 0)}`); // load t2-0 t.deepEqual(log, []); - t.deepEqual(dumpStore(), [ - ['kindIDID', '1'], - [`vom.${tid}/1`, thingVal(1, 'thing-1', 0)], // =t1-1 - [`vom.${tid}/2`, thingVal(100, 'thing-2', 0)], // =t2-0 - ['vom.vkind.2', '{"kindID":"2","tag":"symthing"}'], - ]); - - // phase 4: flush cache - flushCache(); // [] evict t2-1 - t.is(log.shift(), `set vom.${tid}/2 ${thingVal(101, 'thing-2', 0)}`); // evict t2-1 + flushStateCache(); + t.is(log.shift(), `set vom.${tid}/2 ${thingVal(101, 'thing-2', 0)}`); // write t2-1 t.deepEqual(log, []); t.deepEqual(dumpStore(), [ ['kindIDID', '1'], @@ -527,10 +559,9 @@ test('durable kind IDs cannot be reused', t => { test('durable kind IDs can be reanimated', t => { const log = []; const { vom, vrm, cm, fakeStuff } = makeFakeVirtualStuff({ - cacheSize: 0, log, }); - const { makeKindHandle, defineDurableKind, flushCache } = vom; + const { makeKindHandle, defineDurableKind, flushStateCache } = vom; const { possibleVirtualObjectDeath } = vrm; const { makeScalarBigMapStore } = cm; const { deleteEntry } = fakeStuff; @@ -587,7 +618,7 @@ test('durable kind IDs can be reanimated', t => { // Make an instance of the new kind, just to be sure it's there makeThing('laterThing'); - flushCache(); + flushStateCache(); t.is( log.shift(), 'set vom.dkind.10 {"kindID":"10","tag":"testkind","nextInstanceID":2,"unfaceted":true}', @@ -598,8 +629,8 @@ test('durable kind IDs can be reanimated', t => { test('virtual object gc', t => { const log = []; - const { vom, vrm, fakeStuff } = makeFakeVirtualStuff({ cacheSize: 3, log }); - const { defineKind } = vom; + const { vom, vrm, fakeStuff } = makeFakeVirtualStuff({ log }); + const { defineKind, flushStateCache } = vom; const { setExportStatus, possibleVirtualObjectDeath } = vrm; const { deleteEntry, dumpStore } = fakeStuff; @@ -629,11 +660,17 @@ test('virtual object gc', t => { for (let i = 1; i <= 9; i += 1) { things.push(makeThing(`thing #${i}`)); } + t.deepEqual(log, []); + flushStateCache(); t.is(log.shift(), `set vom.${tbase}/1 ${minThing('thing #1')}`); t.is(log.shift(), `set vom.${tbase}/2 ${minThing('thing #2')}`); t.is(log.shift(), `set vom.${tbase}/3 ${minThing('thing #3')}`); t.is(log.shift(), `set vom.${tbase}/4 ${minThing('thing #4')}`); t.is(log.shift(), `set vom.${tbase}/5 ${minThing('thing #5')}`); + t.is(log.shift(), `set vom.${tbase}/6 ${minThing('thing #6')}`); + t.is(log.shift(), `set vom.${tbase}/7 ${minThing('thing #7')}`); + t.is(log.shift(), `set vom.${tbase}/8 ${minThing('thing #8')}`); + t.is(log.shift(), `set vom.${tbase}/9 ${minThing('thing #9')}`); t.deepEqual(log, []); t.deepEqual(dumpStore(), [ ['kindIDID', '1'], @@ -643,6 +680,10 @@ test('virtual object gc', t => { [`vom.${tbase}/3`, minThing('thing #3')], [`vom.${tbase}/4`, minThing('thing #4')], [`vom.${tbase}/5`, minThing('thing #5')], + [`vom.${tbase}/6`, minThing('thing #6')], + [`vom.${tbase}/7`, minThing('thing #7')], + [`vom.${tbase}/8`, minThing('thing #8')], + [`vom.${tbase}/9`, minThing('thing #9')], ['vom.vkind.10', '{"kindID":"10","tag":"thing"}'], ['vom.vkind.11', '{"kindID":"11","tag":"ref"}'], ]); @@ -673,9 +714,14 @@ test('virtual object gc', t => { [`vom.${tbase}/3`, minThing('thing #3')], [`vom.${tbase}/4`, minThing('thing #4')], [`vom.${tbase}/5`, minThing('thing #5')], + [`vom.${tbase}/6`, minThing('thing #6')], + [`vom.${tbase}/7`, minThing('thing #7')], + [`vom.${tbase}/8`, minThing('thing #8')], + [`vom.${tbase}/9`, minThing('thing #9')], ['vom.vkind.10', '{"kindID":"10","tag":"thing"}'], ['vom.vkind.11', '{"kindID":"11","tag":"ref"}'], ]); + // drop export -- should delete setExportStatus(`${tbase}/1`, 'recognizable'); t.is(log.shift(), `get vom.es.${tbase}/1 => r`); @@ -686,10 +732,17 @@ test('virtual object gc', t => { t.is(log.shift(), `get vom.rc.${tbase}/1 => undefined`); t.is(log.shift(), `get vom.es.${tbase}/1 => s`); t.is(log.shift(), `get vom.${tbase}/1 => ${thingVal(0, 'thing #1', 0)}`); - t.is(log.shift(), `delete vom.${tbase}/1`); t.is(log.shift(), `delete vom.rc.${tbase}/1`); t.is(log.shift(), `delete vom.es.${tbase}/1`); - t.is(log.shift(), `getNextKey vom.ir.${tbase}/1| => vom.${tbase}/2`); + // This getNextKey is looking for vom.ir records (things which + // recognize the dropped object, to notify them of its + // retirement). It doesn't find any, and getNextKey reports the next + // lexicographic key, which happens to be the vom.${tbase}/1 data + // record itself, because that doesn't get deleted until a flush + t.is(log.shift(), `getNextKey vom.ir.${tbase}/1| => vom.${tbase}/1`); + t.deepEqual(log, []); + flushStateCache(); + t.is(log.shift(), `delete vom.${tbase}/1`); t.deepEqual(log, []); t.deepEqual(dumpStore(), [ ['kindIDID', '1'], @@ -698,6 +751,10 @@ test('virtual object gc', t => { [`vom.${tbase}/3`, minThing('thing #3')], [`vom.${tbase}/4`, minThing('thing #4')], [`vom.${tbase}/5`, minThing('thing #5')], + [`vom.${tbase}/6`, minThing('thing #6')], + [`vom.${tbase}/7`, minThing('thing #7')], + [`vom.${tbase}/8`, minThing('thing #8')], + [`vom.${tbase}/9`, minThing('thing #9')], ['vom.vkind.10', '{"kindID":"10","tag":"thing"}'], ['vom.vkind.11', '{"kindID":"11","tag":"ref"}'], ]); @@ -722,25 +779,37 @@ test('virtual object gc', t => { [`vom.${tbase}/3`, minThing('thing #3')], [`vom.${tbase}/4`, minThing('thing #4')], [`vom.${tbase}/5`, minThing('thing #5')], + [`vom.${tbase}/6`, minThing('thing #6')], + [`vom.${tbase}/7`, minThing('thing #7')], + [`vom.${tbase}/8`, minThing('thing #8')], + [`vom.${tbase}/9`, minThing('thing #9')], ['vom.vkind.10', '{"kindID":"10","tag":"thing"}'], ['vom.vkind.11', '{"kindID":"11","tag":"ref"}'], ]); + // drop local ref -- should delete pretendGC(`${tbase}/2`); t.is(log.shift(), `get vom.rc.${tbase}/2 => undefined`); t.is(log.shift(), `get vom.es.${tbase}/2 => s`); t.is(log.shift(), `get vom.${tbase}/2 => ${thingVal(0, 'thing #2', 0)}`); - t.is(log.shift(), `delete vom.${tbase}/2`); t.is(log.shift(), `delete vom.rc.${tbase}/2`); t.is(log.shift(), `delete vom.es.${tbase}/2`); - t.is(log.shift(), `getNextKey vom.ir.${tbase}/2| => vom.${tbase}/3`); + t.is(log.shift(), `getNextKey vom.ir.${tbase}/2| => vom.${tbase}/2`); t.deepEqual(log, []); + flushStateCache(); + t.is(log.shift(), `delete vom.${tbase}/2`); + t.deepEqual(log, []); + t.deepEqual(dumpStore(), [ ['kindIDID', '1'], skit, [`vom.${tbase}/3`, minThing('thing #3')], [`vom.${tbase}/4`, minThing('thing #4')], [`vom.${tbase}/5`, minThing('thing #5')], + [`vom.${tbase}/6`, minThing('thing #6')], + [`vom.${tbase}/7`, minThing('thing #7')], + [`vom.${tbase}/8`, minThing('thing #8')], + [`vom.${tbase}/9`, minThing('thing #9')], ['vom.vkind.10', '{"kindID":"10","tag":"thing"}'], ['vom.vkind.11', '{"kindID":"11","tag":"ref"}'], ]); @@ -751,16 +820,23 @@ test('virtual object gc', t => { t.is(log.shift(), `get vom.rc.${tbase}/3 => undefined`); t.is(log.shift(), `get vom.es.${tbase}/3 => undefined`); t.is(log.shift(), `get vom.${tbase}/3 => ${thingVal(0, 'thing #3', 0)}`); - t.is(log.shift(), `delete vom.${tbase}/3`); t.is(log.shift(), `delete vom.rc.${tbase}/3`); t.is(log.shift(), `delete vom.es.${tbase}/3`); - t.is(log.shift(), `getNextKey vom.ir.${tbase}/3| => vom.${tbase}/4`); + t.is(log.shift(), `getNextKey vom.ir.${tbase}/3| => vom.${tbase}/3`); + t.deepEqual(log, []); + flushStateCache(); + t.is(log.shift(), `delete vom.${tbase}/3`); t.deepEqual(log, []); + t.deepEqual(dumpStore(), [ ['kindIDID', '1'], skit, [`vom.${tbase}/4`, minThing('thing #4')], [`vom.${tbase}/5`, minThing('thing #5')], + [`vom.${tbase}/6`, minThing('thing #6')], + [`vom.${tbase}/7`, minThing('thing #7')], + [`vom.${tbase}/8`, minThing('thing #8')], + [`vom.${tbase}/9`, minThing('thing #9')], ['vom.vkind.10', '{"kindID":"10","tag":"thing"}'], ['vom.vkind.11', '{"kindID":"11","tag":"ref"}'], ]); @@ -771,7 +847,6 @@ test('virtual object gc', t => { const ref1 = makeRef(things[3]); t.is(log.shift(), `get vom.rc.${tbase}/4 => undefined`); t.is(log.shift(), `set vom.rc.${tbase}/4 1`); - t.is(log.shift(), `set vom.${tbase}/6 ${minThing('thing #6')}`); t.deepEqual(log, []); t.deepEqual(dumpStore(), [ ['kindIDID', '1'], @@ -779,6 +854,9 @@ test('virtual object gc', t => { [`vom.${tbase}/4`, minThing('thing #4')], [`vom.${tbase}/5`, minThing('thing #5')], [`vom.${tbase}/6`, minThing('thing #6')], + [`vom.${tbase}/7`, minThing('thing #7')], + [`vom.${tbase}/8`, minThing('thing #8')], + [`vom.${tbase}/9`, minThing('thing #9')], [`vom.rc.${tbase}/4`, '1'], ['vom.vkind.10', '{"kindID":"10","tag":"thing"}'], ['vom.vkind.11', '{"kindID":"11","tag":"ref"}'], @@ -811,7 +889,6 @@ test('virtual object gc', t => { const ref2 = makeRef(things[4]); t.is(log.shift(), `get vom.rc.${tbase}/5 => undefined`); t.is(log.shift(), `set vom.rc.${tbase}/5 1`); - t.is(log.shift(), `set vom.${tbase}/7 ${minThing('thing #7')}`); t.deepEqual(log, []); t.deepEqual(dumpStore(), [ ['kindIDID', '1'], @@ -822,6 +899,8 @@ test('virtual object gc', t => { [`vom.${tbase}/5`, minThing('thing #5')], [`vom.${tbase}/6`, minThing('thing #6')], [`vom.${tbase}/7`, minThing('thing #7')], + [`vom.${tbase}/8`, minThing('thing #8')], + [`vom.${tbase}/9`, minThing('thing #9')], [`vom.rc.${tbase}/4`, '1'], [`vom.rc.${tbase}/5`, '1'], ['vom.vkind.10', '{"kindID":"10","tag":"thing"}'], @@ -845,7 +924,6 @@ test('virtual object gc', t => { const ref3 = makeRef(things[5]); t.is(log.shift(), `get vom.rc.${tbase}/6 => undefined`); t.is(log.shift(), `set vom.rc.${tbase}/6 1`); - t.is(log.shift(), `set vom.${tbase}/8 ${minThing('thing #8')}`); t.deepEqual(log, []); t.deepEqual(dumpStore(), [ ['kindIDID', '1'], @@ -857,6 +935,7 @@ test('virtual object gc', t => { [`vom.${tbase}/6`, minThing('thing #6')], [`vom.${tbase}/7`, minThing('thing #7')], [`vom.${tbase}/8`, minThing('thing #8')], + [`vom.${tbase}/9`, minThing('thing #9')], [`vom.rc.${tbase}/4`, '1'], [`vom.rc.${tbase}/5`, '1'], [`vom.rc.${tbase}/6`, '1'], @@ -878,6 +957,7 @@ test('virtual object gc', t => { [`vom.${tbase}/6`, minThing('thing #6')], [`vom.${tbase}/7`, minThing('thing #7')], [`vom.${tbase}/8`, minThing('thing #8')], + [`vom.${tbase}/9`, minThing('thing #9')], [`vom.rc.${tbase}/4`, '1'], [`vom.rc.${tbase}/5`, '1'], [`vom.rc.${tbase}/6`, '1'], @@ -887,7 +967,7 @@ test('virtual object gc', t => { }); test('weak store operations', t => { - const { vom, cm } = makeFakeVirtualStuff({ cacheSize: 3 }); + const { vom, cm } = makeFakeVirtualStuff(); const { defineKind } = vom; const { makeScalarBigWeakMapStore } = cm; @@ -934,7 +1014,7 @@ test('virtualized weak collection operations', t => { // collections const { VirtualObjectAwareWeakMap, VirtualObjectAwareWeakSet, defineKind } = - makeFakeVirtualObjectManager({ cacheSize: 3 }); + makeFakeVirtualObjectManager(); const makeThing = defineKind('thing', initThing, thingBehavior); const makeZot = defineKind('zot', initZot, zotBehavior); diff --git a/packages/swingset-liveslots/test/virtual-objects/test-vo-real-gc.js b/packages/swingset-liveslots/test/virtual-objects/test-vo-real-gc.js new file mode 100644 index 00000000000..f4cd4fd9026 --- /dev/null +++ b/packages/swingset-liveslots/test/virtual-objects/test-vo-real-gc.js @@ -0,0 +1,74 @@ +/* global WeakRef */ +import test from 'ava'; +import '@endo/init/debug.js'; + +import { Far } from '@endo/marshal'; +import { kser, kunser } from '../kmarshal.js'; +import { setupTestLiveslots } from '../liveslots-helpers.js'; + +test('virtual object state writes', async t => { + let monitor; + + const initData = { begin: 'ning' }; + const initStateData = { begin: kser(initData.begin) }; + + function buildRootObject(vatPowers) { + const { VatData } = vatPowers; + const { defineKind } = VatData; + const init = () => initData; + const makeThing = defineKind('thing', init, { + // eslint-disable-next-line no-unused-vars + ping: ({ state }) => 0, + }); + const root = Far('root', { + make: () => { + const thing = makeThing(); + monitor = new WeakRef(thing); + return thing; + }, + ping: thing => { + monitor = new WeakRef(thing); + return thing.ping(); + }, + }); + return root; + } + + const tl = await setupTestLiveslots(t, buildRootObject, 'vatA'); + const { v, dispatchMessageSuccessfully } = tl; + v.log.length = 0; + + // creating the VO will create the initial state data, but does not + // require creation of a "context" nor the "state" accessor object + const res = await dispatchMessageSuccessfully('make'); + const thingKSlot = kunser(res); + const kref = thingKSlot.getKref(); // should be o+v10/1 + const vkey = `vom.${kref}`; + const isGet = l => l.type === 'vatstoreGet'; + const isSet = l => l.type === 'vatstoreSet'; + const getReads = log => log.filter(l => isGet(l) && l.key === vkey); + const getWrites = log => log.filter(l => isSet(l) && l.key === vkey); + const getValues = log => getWrites(log).map(l => l.value); + + // the initial data should be written by end-of-crank + t.deepEqual(getReads(v.log), []); + t.deepEqual(JSON.parse(getValues(v.log)[0]), initStateData); + + // 'thing' is exported, but not held in RAM, so the Representative + // should be dropped + t.falsy(monitor.deref()); + + // Invoking a method, on the other hand, *does* require creation of + // "state" and "context", and creation of "state" requires reading + // the state data (to learn the property names). The "ping" method + // does not modify the state data, so there are no vatstore writes + // afterwards. + await dispatchMessageSuccessfully('ping', thingKSlot); + + t.is(getReads(v.log).length, 1); + t.is(getWrites(v.log).length, 0); + + // 'thing' is again dropped by RAM, so it should be dropped. If + // "context" were erroneously retained, it would stick around. + t.falsy(monitor.deref()); +}); diff --git a/packages/swingset-liveslots/test/virtual-objects/test-weakcollections-vref-handling.js b/packages/swingset-liveslots/test/virtual-objects/test-weakcollections-vref-handling.js index fae18509b66..a34c6987c3f 100644 --- a/packages/swingset-liveslots/test/virtual-objects/test-weakcollections-vref-handling.js +++ b/packages/swingset-liveslots/test/virtual-objects/test-weakcollections-vref-handling.js @@ -10,7 +10,7 @@ test('weakMap vref handling', async t => { VirtualObjectAwareWeakSet, registerEntry, deleteEntry, - } = makeFakeVirtualObjectManager({ cacheSize: 3, log }); + } = makeFakeVirtualObjectManager({ log }); function addCListEntry(slot, val) { registerEntry(slot, val); diff --git a/packages/swingset-liveslots/tools/fakeVirtualObjectManager.js b/packages/swingset-liveslots/tools/fakeVirtualObjectManager.js index 54c072dda04..2acee35e1ef 100644 --- a/packages/swingset-liveslots/tools/fakeVirtualObjectManager.js +++ b/packages/swingset-liveslots/tools/fakeVirtualObjectManager.js @@ -11,9 +11,7 @@ import { makeVirtualObjectManager } from '../src/virtualObjectManager.js'; // it *will* execute virtual object manager operations in the same way that the // real one will because underneath it *is* the real one. -export function makeFakeVirtualObjectManager(vrm, fakeStuff, options = {}) { - const { cacheSize = 100 } = options; - +export function makeFakeVirtualObjectManager(vrm, fakeStuff) { const { initializeKindHandleKind, defineKind, @@ -23,17 +21,17 @@ export function makeFakeVirtualObjectManager(vrm, fakeStuff, options = {}) { makeKindHandle, VirtualObjectAwareWeakMap, VirtualObjectAwareWeakSet, - flushCache, + flushStateCache, canBeDurable, } = makeVirtualObjectManager( fakeStuff.syscall, vrm, fakeStuff.allocateExportID, fakeStuff.getSlotForVal, + fakeStuff.requiredValForSlot, fakeStuff.registerEntry, fakeStuff.marshal.serialize, fakeStuff.marshal.unserialize, - cacheSize, fakeStuff.assertAcceptableSyscallCapdataSize, ); @@ -54,7 +52,7 @@ export function makeFakeVirtualObjectManager(vrm, fakeStuff, options = {}) { setValForSlot: fakeStuff.setValForSlot, registerEntry: fakeStuff.registerEntry, deleteEntry: fakeStuff.deleteEntry, - flushCache, + flushStateCache, dumpStore: fakeStuff.dumpStore, }; diff --git a/packages/swingset-liveslots/tools/fakeVirtualSupport.js b/packages/swingset-liveslots/tools/fakeVirtualSupport.js index 6db39bd5cfd..ea6998e74e9 100644 --- a/packages/swingset-liveslots/tools/fakeVirtualSupport.js +++ b/packages/swingset-liveslots/tools/fakeVirtualSupport.js @@ -167,6 +167,12 @@ export function makeFakeLiveSlotsStuff(options = {}) { return d && (weak ? d.deref() : d); } + function requiredValForSlot(slot) { + const val = getValForSlot(slot); + assert(val, `${slot} must have a value`); + return val; + } + function setValForSlot(slot, val) { slotToVal.set(slot, weak ? new WeakRef(val) : val); } @@ -181,13 +187,13 @@ export function makeFakeLiveSlotsStuff(options = {}) { } function convertSlotToVal(slot) { - const { type, virtual, durable, facet, baseRef } = parseVatSlot(slot); + const { type, id, virtual, durable, facet, baseRef } = parseVatSlot(slot); assert.equal(type, 'object'); let val = getValForSlot(baseRef); if (val) { if (virtual || durable) { if (facet !== undefined) { - return val[facet]; + return vrm.getFacet(id, val, facet); } } return val; @@ -196,7 +202,7 @@ export function makeFakeLiveSlotsStuff(options = {}) { if (vrm) { val = vrm.reanimate(slot); if (facet !== undefined) { - val = val[facet]; + return vrm.getFacet(id, val, facet); } } else { assert.fail('fake liveSlots stuff configured without vrm'); @@ -212,9 +218,10 @@ export function makeFakeLiveSlotsStuff(options = {}) { function registerEntry(baseRef, val, valIsCohort) { setValForSlot(baseRef, val); if (valIsCohort) { - for (let i = 0; i < val.length; i += 1) { - valToSlot.set(val[i], `${baseRef}:${i}`); - } + const { id } = parseVatSlot(baseRef); + vrm.getFacetNames(id).forEach((name, index) => { + valToSlot.set(val[name], `${baseRef}:${index}`); + }); } else { valToSlot.set(val, baseRef); } @@ -237,6 +244,7 @@ export function makeFakeLiveSlotsStuff(options = {}) { allocateExportID, allocateCollectionID, getSlotForVal, + requiredValForSlot, getValForSlot, setValForSlot, registerEntry, @@ -296,14 +304,13 @@ export function makeFakeWatchedPromiseManager( */ export function makeFakeVirtualStuff(options = {}) { const actualOptions = { - cacheSize: 3, relaxDurabilityRules: true, ...options, }; const { relaxDurabilityRules } = actualOptions; const fakeStuff = makeFakeLiveSlotsStuff(actualOptions); const vrm = makeFakeVirtualReferenceManager(fakeStuff, relaxDurabilityRules); - const vom = makeFakeVirtualObjectManager(vrm, fakeStuff, actualOptions); + const vom = makeFakeVirtualObjectManager(vrm, fakeStuff); vom.initializeKindHandleKind(); fakeStuff.setVrm(vrm); const cm = makeFakeCollectionManager(vrm, fakeStuff, actualOptions); @@ -315,7 +322,7 @@ export function makeStandaloneFakeVirtualObjectManager(options = {}) { const fakeStuff = makeFakeLiveSlotsStuff(options); const { relaxDurabilityRules = true } = options; const vrm = makeFakeVirtualReferenceManager(fakeStuff, relaxDurabilityRules); - const vom = makeFakeVirtualObjectManager(vrm, fakeStuff, options); + const vom = makeFakeVirtualObjectManager(vrm, fakeStuff); vom.initializeKindHandleKind(); fakeStuff.setVrm(vrm); return vom;