Skip to content

Commit

Permalink
cache the collection schema, only read from vatstore once per crank
Browse files Browse the repository at this point in the history
This changes the collectionManager to keep the per-collection
metadata (keyShape, valueShape, label) in a cache. Like the VOM's
dataCache, this `schemaCache` is populated at most once per crank, and
flushed at the end of every crank. This removes the GC-sensitive
syscalls that used to fetch the metadata when userspace caused a
collection to be reanimated. Instead, the cache is read when userspace
invokes a collection method that needs the data (most of them, to
validate keys or values), regardless of whether the collection
Representative remains in RAM or not.

It also changes the serialized form of the schemata to be an object
like `{ keyShape, valueShape }` instead of an array like `[keyShape,
valueShape]`. If the constraints are missing, the object is empty,
which is smaller to serialize. I'm also thinking this might make it
more extensible.

closes #6360
  • Loading branch information
warner committed Apr 10, 2023
1 parent 901cc26 commit 900986e
Show file tree
Hide file tree
Showing 7 changed files with 145 additions and 93 deletions.
8 changes: 4 additions & 4 deletions packages/SwingSet/test/virtualObjects/test-representatives.js
Original file line number Diff line number Diff line change
Expand Up @@ -454,19 +454,19 @@ test('virtual object gc', async t => {
[`${v}.vs.vc.1.|entryCount`]: '0',
[`${v}.vs.vc.1.|label`]: 'baggage',
[`${v}.vs.vc.1.|nextOrdinal`]: '1',
[`${v}.vs.vc.1.|schemata`]: vstr([M.string()]),
[`${v}.vs.vc.1.|schemata`]: vstr({ keyShape: M.string() }),
[`${v}.vs.vc.2.|entryCount`]: '0',
[`${v}.vs.vc.2.|label`]: 'promiseRegistrations',
[`${v}.vs.vc.2.|nextOrdinal`]: '1',
[`${v}.vs.vc.2.|schemata`]: vstr([M.scalar()]),
[`${v}.vs.vc.2.|schemata`]: vstr({ keyShape: M.scalar() }),
[`${v}.vs.vc.3.|entryCount`]: '0',
[`${v}.vs.vc.3.|label`]: 'promiseWatcherByKind',
[`${v}.vs.vc.3.|nextOrdinal`]: '1',
[`${v}.vs.vc.3.|schemata`]: vstr([M.scalar()]),
[`${v}.vs.vc.3.|schemata`]: vstr({ keyShape: M.scalar() }),
[`${v}.vs.vc.4.|entryCount`]: '0',
[`${v}.vs.vc.4.|label`]: 'watchedPromises',
[`${v}.vs.vc.4.|nextOrdinal`]: '1',
[`${v}.vs.vc.4.|schemata`]: vstr([M.and(M.scalar(), M.string())]),
[`${v}.vs.vc.4.|schemata`]: vstr({ keyShape: M.and(M.scalar(), M.string()) }),
[`${v}.vs.vom.es.o+v10/3`]: 'r',
[`${v}.vs.vom.o+v10/2`]: `{"label":${vstr('thing #2')}}`,
[`${v}.vs.vom.o+v10/3`]: `{"label":${vstr('thing #3')}}`,
Expand Down
189 changes: 109 additions & 80 deletions packages/swingset-liveslots/src/collectionManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
enumerateKeysStartEnd,
enumerateKeysWithPrefix,
} from './vatstore-iterators.js';
import { makeCache } from './cache.js';

// XXX TODO: The following key length limit was put in place due to limitations
// in LMDB. With the move away from LMDB, it is no longer relevant, but I'm
Expand All @@ -46,6 +47,58 @@ function throwNotDurable(value, slotIndex, serializedValue) {
Fail`value is not durable: ${value} at slot ${q(slotIndex)} of ${serializedValue.body}`;
}

function prefixc(collectionID, dbEntryKey) {
return `vc.${collectionID}.${dbEntryKey}`;
}

/**
* @typedef {object} SchemaCacheValue
* @property {Pattern} keyShape
* @property {Pattern} valueShape
* @property {string} label
* @property {object} schemataCapData
*/

/*
* Build a cache that holds the schema for each collection.
*
* The cache maps collectionID to { keyShape, valueShape, label,
* schemataCapData }. These are initialized when the collection is
* first constructed, and never modified afterwards. The values live
* in the vatstore, inside two keys, one for the [keyShape,
* valueShape] schemata, another for the label.
*/
function makeSchemaCache(syscall, unserialize) {
/** @type {(collectionID: string) => SchemaCacheValue} */
const readBacking = collectionID => {
// this is only called once per crank
const schemataKey = prefixc(collectionID, '|schemata');
const schemataValue = syscall.vatstoreGet(schemataKey);
const schemataCapData = JSON.parse(schemataValue);
const { keyShape, valueShape } = unserialize(schemataCapData);
const labelKey = prefixc(collectionID, '|label');
const label = syscall.vatstoreGet(labelKey);
return harden({ keyShape, valueShape, label, schemataCapData });
};
/** @type {(collectionID: string, value: SchemaCacheValue) => void } */
const writeBacking = (collectionID, value) => {
const { label, schemataCapData } = value;
const schemataKey = prefixc(collectionID, '|schemata');
const schemataValue = JSON.stringify(schemataCapData);
syscall.vatstoreSet(schemataKey, schemataValue);
const labelKey = prefixc(collectionID, '|label');
syscall.vatstoreSet(labelKey, label);
};
/** @type {(collectionID: string) => void} */
const deleteBacking = collectionID => {
const schemataKey = prefixc(collectionID, '|schemata');
const labelKey = prefixc(collectionID, '|label');
syscall.vatstoreDelete(schemataKey);
syscall.vatstoreDelete(labelKey);
};
return makeCache(readBacking, writeBacking, deleteBacking);
}

export function makeCollectionManager(
syscall,
vrm,
Expand All @@ -60,6 +113,9 @@ export function makeCollectionManager(
) {
const storeKindIDToName = new Map();

/** @type { import('./cache.js').Cache<SchemaCacheValue>} */
const schemaCache = makeSchemaCache(syscall, unserialize);

const storeKindInfo = {
scalarMapStore: {
hasWeakKeys: false,
Expand Down Expand Up @@ -119,10 +175,6 @@ export function makeCollectionManager(
},
};

function prefixc(collectionID, dbEntryKey) {
return `vc.${collectionID}.${dbEntryKey}`;
}

function initializeStoreKindInfo() {
let storeKindIDs = {};
const rawTable = syscall.vatstoreGet('storeKindIDTable');
Expand Down Expand Up @@ -181,35 +233,33 @@ export function makeCollectionManager(
}
vrm.setDeleteCollectionEntry(deleteCollectionEntry);

function summonCollectionInternal(
_initial,
label,
collectionID,
kindName,
keyShape = M.any(),
valueShape,
) {
function summonCollectionInternal(_initial, collectionID, kindName) {
assert.typeof(kindName, 'string');
const kindInfo = storeKindInfo[kindName];
kindInfo || Fail`unknown collection kind ${kindName}`;
const { hasWeakKeys, durable } = kindInfo;
const getSchema = () => schemaCache.get(collectionID);
const dbKeyPrefix = `vc.${collectionID}.`;
let currentGenerationNumber = 0;

const invalidKeyTypeMsg = `invalid key type for collection ${q(label)}`;
const invalidValueTypeMsg = `invalid value type for collection ${q(label)}`;
const makeInvalidKeyTypeMsg = label =>
`invalid key type for collection ${q(label)}`;
const makeInvalidValueTypeMsg = label =>
`invalid value type for collection ${q(label)}`;

const serializeValue = value => {
const { valueShape, label } = getSchema();
if (valueShape !== undefined) {
mustMatch(value, valueShape, invalidValueTypeMsg);
mustMatch(value, valueShape, makeInvalidValueTypeMsg(label));
}
return serialize(value);
};

const unserializeValue = data => {
const { valueShape, label } = getSchema();
const value = unserialize(data);
if (valueShape !== undefined) {
mustMatch(value, valueShape, invalidValueTypeMsg);
mustMatch(value, valueShape, makeInvalidValueTypeMsg(label));
}
return value;
};
Expand Down Expand Up @@ -277,6 +327,7 @@ export function makeCollectionManager(
}

function has(key) {
const { keyShape } = getSchema();
if (!matches(key, keyShape)) {
return false;
}
Expand All @@ -288,7 +339,8 @@ export function makeCollectionManager(
}

function get(key) {
mustMatch(key, keyShape, invalidKeyTypeMsg);
const { keyShape, label } = getSchema();
mustMatch(key, keyShape, makeInvalidKeyTypeMsg(label));
if (passStyleOf(key) === 'remotable' && getOrdinal(key) === undefined) {
throw Fail`key ${key} not found in collection ${q(label)}`;
}
Expand All @@ -310,7 +362,8 @@ export function makeCollectionManager(
}

const doInit = (key, value, precheckedHas) => {
mustMatch(key, keyShape, invalidKeyTypeMsg);
const { keyShape, label } = getSchema();
mustMatch(key, keyShape, makeInvalidKeyTypeMsg(label));
precheckedHas ||
!has(key) ||
Fail`key ${key} already registered in collection ${q(label)}`;
Expand Down Expand Up @@ -350,7 +403,8 @@ export function makeCollectionManager(
};

function set(key, value) {
mustMatch(key, keyShape, invalidKeyTypeMsg);
const { keyShape, label } = getSchema();
mustMatch(key, keyShape, makeInvalidKeyTypeMsg(label));
const after = serializeValue(harden(value));
assertAcceptableSyscallCapdataSize([after]);
if (durable) {
Expand All @@ -369,7 +423,8 @@ export function makeCollectionManager(
}

function deleteInternal(key) {
mustMatch(key, keyShape, invalidKeyTypeMsg);
const { keyShape, label } = getSchema();
mustMatch(key, keyShape, makeInvalidKeyTypeMsg(label));
if (passStyleOf(key) === 'remotable' && getOrdinal(key) === undefined) {
throw Fail`key ${key} not found in collection ${q(label)}`;
}
Expand Down Expand Up @@ -580,23 +635,9 @@ export function makeCollectionManager(
};
}

function summonCollection(
initial,
label,
collectionID,
kindName,
keyShape,
valueShape,
) {
function summonCollection(initial, collectionID, kindName) {
const hasWeakKeys = storeKindInfo[kindName].hasWeakKeys;
const raw = summonCollectionInternal(
initial,
label,
collectionID,
kindName,
keyShape,
valueShape,
);
const raw = summonCollectionInternal(initial, collectionID, kindName);

const { has, get, init, addToSet, set, delete: del } = raw;
const weakMethods = {
Expand Down Expand Up @@ -640,41 +681,40 @@ export function makeCollectionManager(
const kindName = storeKindIDToName.get(`${id}`);
kindName || Fail`unknown kind ID ${id}`;
const collectionID = `${subid}`;
const collection = summonCollectionInternal(
false,
'test',
collectionID,
kindName,
);
const collection = summonCollectionInternal(false, collectionID, kindName);
return collection.sizeInternal();
}

function deleteCollection(vobjID) {
const { id, subid } = parseVatSlot(vobjID);
const kindName = storeKindIDToName.get(`${id}`);
const collectionID = `${subid}`;
const collection = summonCollectionInternal(
false,
'GC',
collectionID,
kindName,
);
const collection = summonCollectionInternal(false, collectionID, kindName);

const doMoreGC = collection.clearInternal(true);
for (const dbKey of enumerateKeysWithPrefix(syscall, prefixc(subid, '|'))) {
for (const dbKey of enumerateKeysWithPrefix(
syscall,
prefixc(collectionID, '|'),
)) {
// these two keys are owned by schemaCache, and will be deleted
// when schemaCache is flushed
if (dbKey.endsWith('|schemata') || dbKey.endsWith('|label')) {
continue;
}
// but we must still delete the other keys (|nextOrdinal and
// |entryCount)
syscall.vatstoreDelete(dbKey);
}
schemaCache.delete(collectionID);
return doMoreGC;
}

function makeCollection(label, kindName, isDurable, keyShape, valueShape) {
assert.typeof(label, 'string');
assert(storeKindInfo[kindName]);
assertPattern(keyShape);
const schemata = [keyShape];
if (valueShape) {
assertPattern(valueShape);
schemata.push(valueShape);
}
const collectionID = `${allocateCollectionID()}`;
const kindID = obtainStoreKindID(kindName);
Expand All @@ -685,23 +725,21 @@ export function makeCollectionManager(
if (!hasWeakKeys) {
syscall.vatstoreSet(prefixc(collectionID, '|entryCount'), '0');
}
syscall.vatstoreSet(
prefixc(collectionID, '|schemata'),
JSON.stringify(serialize(harden(schemata))),

const schemata = {}; // don't populate 'undefined', keep it small
if (keyShape !== undefined) {
schemata.keyShape = keyShape;
}
if (valueShape !== undefined) {
schemata.valueShape = valueShape;
}
const schemataCapData = serialize(harden(schemata));
schemaCache.set(
collectionID,
harden({ keyShape, valueShape, label, schemataCapData }),
);
syscall.vatstoreSet(prefixc(collectionID, '|label'), label);

return [
vobjID,
summonCollection(
true,
label,
collectionID,
kindName,
keyShape,
valueShape,
),
];

return [vobjID, summonCollection(true, collectionID, kindName)];
}

function collectionToMapStore(collection) {
Expand Down Expand Up @@ -899,19 +937,7 @@ export function makeCollectionManager(
const { id, subid } = parseVatSlot(vobjID);
const collectionID = `${subid}`;
const kindName = storeKindIDToName.get(`${id}`);
const rawSchemata = JSON.parse(
syscall.vatstoreGet(prefixc(subid, '|schemata')),
);
const [keyShape, valueShape] = unserialize(rawSchemata);
const label = syscall.vatstoreGet(prefixc(subid, '|label'));
return summonCollection(
false,
label,
collectionID,
kindName,
keyShape,
valueShape,
);
return summonCollection(false, collectionID, kindName);
}

function reanimateMapStore(vobjID) {
Expand Down Expand Up @@ -998,13 +1024,16 @@ export function makeCollectionManager(
const makeScalarBigWeakSetStore = (label = 'weakSet', options = {}) =>
makeBigWeakSetStore(label, narrowKeyShapeOption(M.scalar(), options));

const flushSchemaCache = () => schemaCache.flush();

return harden({
initializeStoreKindInfo,
makeScalarBigMapStore,
makeScalarBigWeakMapStore,
makeScalarBigSetStore,
makeScalarBigWeakSetStore,
provideBaggage,
flushSchemaCache,
testHooks,
});
}
5 changes: 4 additions & 1 deletion packages/swingset-liveslots/src/liveslots.js
Original file line number Diff line number Diff line change
Expand Up @@ -1511,7 +1511,9 @@ function build(

async function bringOutYourDead() {
await scanForDeadObjects();
// now flush all the vatstore changes (deletions) we made
// Now flush all the vatstore changes (deletions and refcounts) we
// made. dispatch() calls afterDispatchActions() automatically for
// most methods, but not bringOutYourDead().
// eslint-disable-next-line no-use-before-define
afterDispatchActions();
}
Expand All @@ -1530,6 +1532,7 @@ function build(
*/
function afterDispatchActions() {
flushIDCounters();
collectionManager.flushSchemaCache();
vom.flushStateCache();
}

Expand Down
2 changes: 1 addition & 1 deletion packages/swingset-liveslots/test/test-gc-sensitivity.js
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ test('representative reanimation', async t => {
t.deepEqual(noGCLog, yesGCLog);
});

test.failing('collection reanimation', async t => {
test('collection reanimation', async t => {
const { syscall, log } = buildSyscall();
const gcTools = makeMockGC();

Expand Down
Loading

0 comments on commit 900986e

Please sign in to comment.