Skip to content

Commit

Permalink
feat: load virtual objects when accessed, not when deserialized
Browse files Browse the repository at this point in the history
  • Loading branch information
FUDCo committed Apr 25, 2021
1 parent 1b7d921 commit 5e659e6
Show file tree
Hide file tree
Showing 5 changed files with 294 additions and 63 deletions.
32 changes: 23 additions & 9 deletions packages/SwingSet/src/kernel/virtualObjectManager.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { assert, details as X, quote as q } from '@agoric/assert';
import { parseVatSlot } from '../parseVatSlots';
// import { kdebug } from './kdebug';

const initializationsInProgress = new WeakSet();

Expand Down Expand Up @@ -37,6 +38,7 @@ export function makeCache(size, fetch, store) {
refreshCount += 1;
}
}
// kdebug(`### vo LRU evict ${lruTail.vobjID} (dirty=${lruTail.dirty})`);
liveTable.delete(lruTail.vobjID);
if (lruTail.dirty) {
store(lruTail.vobjID, lruTail.rawData);
Expand Down Expand Up @@ -72,6 +74,7 @@ export function makeCache(size, fetch, store) {
if (!lruTail) {
lruTail = innerObj;
}
// kdebug(`### vo LRU remember ${lruHead.vobjID}`);
},
refresh(innerObj) {
if (innerObj !== lruHead) {
Expand All @@ -91,16 +94,20 @@ export function makeCache(size, fetch, store) {
innerObj.next = lruHead;
lruHead.prev = innerObj;
lruHead = innerObj;
// kdebug(`### vo LRU refresh ${lruHead.vobjID}`);
}
},
lookup(vobjID) {
lookup(vobjID, load) {
let innerObj = liveTable.get(vobjID);
if (innerObj) {
cache.refresh(innerObj);
} else {
innerObj = { vobjID, rawData: fetch(vobjID) };
innerObj = { vobjID, rawData: null };
cache.remember(innerObj);
}
if (load && !innerObj.rawData) {
innerObj.rawData = fetch(vobjID);
}
return innerObj;
},
};
Expand Down Expand Up @@ -321,8 +328,9 @@ export function makeVirtualObjectManager(
* representative.
*
* @returns {*} a maker function that can be called to manufacture new
* instance of this kind of object. The parameters of the maker function
* are those of the `init` method provided by the `instanceKitMaker` function.
* instances of this kind of object. The parameters of the maker function
* are those of the `init` method provided by the `instanceKitMaker`
* function.
*
* Notes on theory of operation:
*
Expand Down Expand Up @@ -372,20 +380,21 @@ export function makeVirtualObjectManager(
* memory. If it is not, it is loaded from persistent storage, the
* corresponding inner self is made to point at it, and then the inner self is
* placed at the head of the LRU cache (causing the least recently used inner
* self to fall off the end of the cache). If it *is* memory, it is promoted
* to the head of the LRU cache but the contents of the cache remains
* self to fall off the end of the cache). If it *is* in memory, it is
* promoted to the head of the LRU cache but the contents of the cache remains
* unchanged. When an inner self falls off the end of the LRU, its reference
* to the state is nulled out and the object holding the state becomes garbage
* collectable.
*/
function makeKind(instanceKitMaker) {
const kindID = `${allocateExportID()}`;
let nextInstanceID = 1;
const propertyNames = new Set();

function makeRepresentative(innerSelf, initializing) {
function ensureState() {
if (!innerSelf.rawData) {
innerSelf = cache.lookup(innerSelf.vobjID);
innerSelf = cache.lookup(innerSelf.vobjID, true);
}
}

Expand All @@ -394,7 +403,7 @@ export function makeVirtualObjectManager(
!initializationsInProgress.has(target),
`object is still being initialized`,
);
for (const prop of Object.getOwnPropertyNames(innerSelf.rawData)) {
for (const prop of propertyNames) {
Object.defineProperty(target, prop, {
get: () => {
ensureState();
Expand Down Expand Up @@ -427,7 +436,8 @@ export function makeVirtualObjectManager(
}

function reanimate(vobjID) {
return makeRepresentative(cache.lookup(vobjID), false).self;
// kdebug(`vo reanimate ${vobjID}`);
return makeRepresentative(cache.lookup(vobjID, false), false).self;
}
kindTable.set(kindID, reanimate);

Expand All @@ -438,6 +448,7 @@ export function makeVirtualObjectManager(
const initialData = {};
initializationsInProgress.add(initialData);
const innerSelf = { vobjID, rawData: initialData };
// kdebug(`vo make ${vobjID}`);
// prettier-ignore
const { self: initialRepresentative, init } =
makeRepresentative(innerSelf, true);
Expand All @@ -455,6 +466,9 @@ export function makeVirtualObjectManager(
}
}
innerSelf.rawData = rawData;
for (const prop of Object.getOwnPropertyNames(initialData)) {
propertyNames.add(prop);
}
innerSelf.wrapData(initialData);
innerSelf.dirty = true;
return initialRepresentative;
Expand Down
187 changes: 184 additions & 3 deletions packages/SwingSet/test/virtualObjects/test-representatives.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
/* global __dirname */
// eslint-disable-next-line import/order
import { test } from '../../tools/prepare-test-env-ava';

// eslint-disable-next-line import/order
import path from 'path';
import { buildVatController } from '../../src/index';

import { initSwingStore } from '@agoric/swing-store-simple';
import {
buildVatController,
initializeSwingset,
makeSwingsetController,
} from '../../src/index';
import makeNextLog from '../make-nextlog';

function capdata(body, slots = []) {
Expand Down Expand Up @@ -34,7 +40,7 @@ test('virtual object representatives', async t => {
},
};

const c = await buildVatController(config);
const c = await buildVatController(config, []);
const nextLog = makeNextLog(c);

await c.run();
Expand Down Expand Up @@ -151,3 +157,178 @@ test('virtual object representatives', async t => {
]);
t.deepEqual(c.kpResolution(rz2), capdata('"overflow"'));
});

test('exercise cache', async t => {
const config = {
bootstrap: 'bootstrap',
vats: {
bootstrap: {
sourceSpec: path.resolve(__dirname, 'vat-representative-bootstrap.js'),
creationOptions: {
virtualObjectCacheSize: 3,
},
},
},
};

const log = [];

const storage = initSwingStore().storage;
function vsKey(key) {
return key.match(/^\w+\.vs\./);
}
const loggingStorage = {
has: key => storage.has(key),
getKeys: (start, end) => storage.getKeys(start, end),
get(key) {
const result = storage.get(key);
if (vsKey(key)) {
log.push(['get', key, result]);
}
return result;
},
set(key, value) {
if (vsKey(key)) {
log.push(['set', key, value]);
}
storage.set(key, value);
},
delete(key) {
if (vsKey(key)) {
log.push(['delete', key]);
}
storage.delete(key);
},
};

const bootstrapResult = await initializeSwingset(config, [], loggingStorage);
const c = await makeSwingsetController(loggingStorage, {});

const nextLog = makeNextLog(c);

await c.run();
t.deepEqual(c.kpResolution(bootstrapResult), capargs('bootstrap done'));

async function doSimple(method, what, ...args) {
let sendArgs;
if (what) {
const whatArg = {
'@qclass': 'slot',
iface: 'Alleged: thing',
index: 0,
};
sendArgs = capargs([whatArg, ...args], [what]);
} else {
sendArgs = capargs(args);
}
const r = c.queueToVatExport('bootstrap', 'o+0', method, sendArgs);
await c.run();
t.is(c.kpStatus(r), 'fulfilled');
t.deepEqual(nextLog(), []);
return r;
}

async function make(name, holdIt, expect) {
const r = await doSimple('makeThing', null, name, holdIt);
const result = c.kpResolution(r);
t.deepEqual(result, slot0('thing', expect));
return result.slots[0];
}
async function read(what, expect) {
const r = await doSimple('readThing', what);
t.deepEqual(c.kpResolution(r), capargs(expect));
}
async function readHeld(expect) {
const r = await doSimple('readHeldThing', null);
t.deepEqual(c.kpResolution(r), capargs(expect));
}
async function write(what, newName) {
await doSimple('writeThing', what, newName);
}
async function writeHeld(newName) {
await doSimple('writeHeldThing', null, newName);
}
async function forgetHeld() {
await doSimple('forgetHeldThing', null);
}
async function hold(what) {
await doSimple('holdThing', what);
}
function thingID(num) {
return `v1.vs.o+1/${num}`;
}
function thingVal(name) {
return JSON.stringify({
name: capdata(JSON.stringify(name)),
});
}

// 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';

// init cache - []
await make('thing1', true, T1); // make t1 - [t1]
await make('thing2', false, T2); // make t2 - [t2 t1]
await read(T1, 'thing1'); // refresh t1 - [t1 t2]
await read(T2, 'thing2'); // refresh t2 - [t2 t1]
await readHeld('thing1'); // cache unchanged - [t2 t1]

await make('thing3', false, T3); // make t3 - [t3 t2 t1]
await make('thing4', false, T4); // make t4 - [t4 t3 t2 t1]
t.deepEqual(log, []);
await make('thing5', false, T5); // evict t1, make t5 - [t5 t4 t3 t2] t1
t.deepEqual(log.shift(), ['set', thingID(1), thingVal('thing1')]);
await make('thing6', false, T6); // evict t2, make t6 - [t6 t5 t4 t3] t2
t.deepEqual(log.shift(), ['set', thingID(2), thingVal('thing2')]);
await make('thing7', false, T7); // evict t3, make t7 - [t7 t6 t5 t4]
t.deepEqual(log.shift(), ['set', thingID(3), thingVal('thing3')]);
await make('thing8', false, T8); // evict t4, make t8 - [t8 t7 t6 t5]
t.deepEqual(log.shift(), ['set', thingID(4), thingVal('thing4')]);

await read(T2, 'thing2'); // reanimate t2, evict t5 - [t2 t8 t7 t6]
t.deepEqual(log.shift(), ['get', thingID(2), thingVal('thing2')]);
t.deepEqual(log.shift(), ['set', thingID(5), thingVal('thing5')]);
await readHeld('thing1'); // reanimate t1, evict t6 - [t1 t2 t8 t7]
t.deepEqual(log.shift(), ['get', thingID(1), thingVal('thing1')]);
t.deepEqual(log.shift(), ['set', thingID(6), thingVal('thing6')]);

await write(T2, 'thing2 updated'); // refresh t2 - [t2 t1 t8 t7]
await writeHeld('thing1 updated'); // cache unchanged - [t2 t1 t8 t7]

await read(T8, 'thing8'); // refresh t8 - [t8 t2 t1 t7]
await read(T7, 'thing7'); // refresh t7 - [t7 t8 t2 t1]
t.deepEqual(log, []);
await read(T6, 'thing6'); // reanimate t6, evict t1 - [t6 t7 t8 t2]
t.deepEqual(log.shift(), ['get', thingID(6), thingVal('thing6')]);
t.deepEqual(log.shift(), ['set', thingID(1), thingVal('thing1 updated')]);
await read(T5, 'thing5'); // reanimate t5, evict t2 - [t5 t6 t7 t8]
t.deepEqual(log.shift(), ['get', thingID(5), thingVal('thing5')]);
t.deepEqual(log.shift(), ['set', thingID(2), thingVal('thing2 updated')]);
await read(T4, 'thing4'); // reanimate t4, evict t8 - [t4 t5 t6 t7]
t.deepEqual(log.shift(), ['get', thingID(4), thingVal('thing4')]);
t.deepEqual(log.shift(), ['set', thingID(8), thingVal('thing8')]);
await read(T3, 'thing3'); // reanimate t3, evict t7 - [t3 t4 t5 t6]
t.deepEqual(log.shift(), ['get', thingID(3), thingVal('thing3')]);
t.deepEqual(log.shift(), ['set', thingID(7), thingVal('thing7')]);

await read(T2, 'thing2 updated'); // reanimate t2, evict t6 - [t2 t3 t4 t5]
t.deepEqual(log.shift(), ['get', thingID(2), thingVal('thing2 updated')]);
await readHeld('thing1 updated'); // reanimate t1, evict t5 - [t1 t2 t3 t4]
t.deepEqual(log.shift(), ['get', thingID(1), thingVal('thing1 updated')]);

await forgetHeld(); // cache unchanged - [t1 t2 t3 t4]
await hold(T8); // cache unchanged - [t1 t2 t3 t4]
t.deepEqual(log, []);
await read(T7, 'thing7'); // reanimate t7, evict t4 - [t7 t1 t2 t3]
t.deepEqual(log.shift(), ['get', thingID(7), thingVal('thing7')]);
await writeHeld('thing8 updated'); // reanimate t8, evict t3 - [t8 t7 t1 t2]
t.deepEqual(log.shift(), ['get', thingID(8), thingVal('thing8')]);
t.deepEqual(log, []);
});
25 changes: 7 additions & 18 deletions packages/SwingSet/test/virtualObjects/test-virtualObjectCache.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,23 +72,18 @@ test('cache overflow and refresh', t => {

// lookup that has no effect
things[0] = cache.lookup('t0'); // cache: t0, t2, t5, t4
things[0].rawData = 'changed thing #0';
things[0].dirty = true; // pretend we changed it
t.is(things[0].rawData, 'thing #0');
t.is(things[0].rawData, 'changed thing #0');
t.is(things[3].rawData, null);
t.deepEqual(store.getLog(), [
['fetch', 't0', 'thing #0'],
['store', 't3', 'thing #3'],
]);
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].rawData, 'thing #1');
t.is(things[1].rawData, null);
t.is(things[5].rawData, null);
t.deepEqual(store.getLog(), [
['fetch', 't1', 'thing #1'],
['store', 't5', 'thing #5'],
]);
t.deepEqual(store.getLog(), [['store', 't5', 'thing #5']]);

// verify that everything is there
cache.flush(); // cache: empty
Expand All @@ -100,11 +95,11 @@ test('cache overflow and refresh', t => {
t.is(things[5].rawData, null);
t.deepEqual(store.getLog(), [
['store', 't2', 'thing #2'],
['store', 't0', 'thing #0'],
['store', 't0', 'changed thing #0'],
['store', 't4', 'thing #4'],
]);
t.deepEqual(store.dump(), [
['t0', 'thing #0'],
['t0', 'changed thing #0'],
['t1', 'thing #1'],
['t2', 'thing #2'],
['t3', 'thing #3'],
Expand Down Expand Up @@ -134,13 +129,7 @@ test('cache overflow and refresh', t => {
t.is(things[0].rawData, null);
t.is(things[5].rawData, 'new thing #5');
t.deepEqual(store.getLog(), [
['fetch', 't0', 'thing #0'],
['fetch', 't1', 'thing #1'],
['fetch', 't2', 'thing #2'],
['fetch', 't3', 'thing #3'],
['fetch', 't4', 'thing #4'],
['store', 't0', 'new thing #0'],
['fetch', 't5', 'thing #5'],
['store', 't1', 'new thing #1'],
]);
t.deepEqual(store.dump(), [
Expand Down
Loading

0 comments on commit 5e659e6

Please sign in to comment.