Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(liveslots): retain WeakRefs to voAware collections #7552

Merged
merged 4 commits into from
Apr 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/swingset-liveslots/src/liveslots.js
Original file line number Diff line number Diff line change
Expand Up @@ -650,6 +650,7 @@ function build(
getSlotForVal,
requiredValForSlot,
FinalizationRegistry,
WeakRef,
addToPossiblyDeadSet,
addToPossiblyRetiredSet,
relaxDurabilityRules,
Expand Down
4 changes: 2 additions & 2 deletions packages/swingset-liveslots/src/virtualObjectManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -418,7 +418,7 @@ export const makeVirtualObjectManager = (
actualWeakMaps.set(this, new WeakMap());
const vmap = new Map();
virtualObjectMaps.set(this, vmap);
vrm.droppedCollectionRegistry.register(this, {
vrm.registerDroppedCollection(this, {
collectionDeleter: voAwareWeakMapDeleter,
vmap,
});
Expand Down Expand Up @@ -496,7 +496,7 @@ export const makeVirtualObjectManager = (
const vset = new Set();
virtualObjectSets.set(this, vset);

vrm.droppedCollectionRegistry.register(this, {
vrm.registerDroppedCollection(this, {
collectionDeleter: voAwareWeakSetDeleter,
vset,
});
Expand Down
26 changes: 24 additions & 2 deletions packages/swingset-liveslots/src/virtualReferences.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import {
* converts an object ID (vref) to an object.
* @param {*} FinalizationRegistry Powerful JavaScript intrinsic normally denied
* by SES
* @param {*} WeakRef Powerful JavaScript intrinsic normally denied
* by SES
* @param {*} addToPossiblyDeadSet Function to record objects whose deaths
* should be reinvestigated
* @param {*} addToPossiblyRetiredSet Function to record dead objects whose
Expand All @@ -29,13 +31,25 @@ export function makeVirtualReferenceManager(
getSlotForVal,
requiredValForSlot,
FinalizationRegistry,
WeakRef,
addToPossiblyDeadSet,
addToPossiblyRetiredSet,
relaxDurabilityRules,
) {
const droppedCollectionRegistry = new FinalizationRegistry(
finalizeDroppedCollection,
);
// Our JS engine is configured to treat WeakRefs as strong during
// organic (non-forced GC), to minimize execution variation. To
// prevent FinalizationRegistry callbacks from varying this way, we
// must maintain WeakRefs to everything therein. The WeakRef is
// retained by the FR "heldValue" context record, next to the
// descriptor, and is thus released when the FR fires.

function registerDroppedCollection(target, descriptor) {
const wr = new WeakRef(target);
droppedCollectionRegistry.register(target, { descriptor, wr });
}

/**
* Check if a virtual object is unreachable via virtual-data references.
Expand Down Expand Up @@ -636,7 +650,13 @@ export function makeVirtualReferenceManager(
}
}

function finalizeDroppedCollection(descriptor) {
function finalizeDroppedCollection({ descriptor }) {
// the 'wr' WeakRef will be dropped about now
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So little tangent on what optimizations engines are allowed or not. Technically, since wr is never observed by the program, the JS engine is allowed to not keep it in the heldValue, which means that the WeakRef would actually not exist in the first place.

If we wanted to be as close to the spec rules as possible, we would need to actually try to deref the weakref and assert it's empty (or even better, log the target if not empty, causing an actual observation). Obviously no engine goes that far in its optimizations, certainly not XS, and the destructuring dropping the wr is in fact completely sufficient.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wow, the engine is allowed to inspect the callback's code to determine that? hardcore.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The engine is allowed to do anything as long as it's not observable to the program. If a property of an object is never used, it doesn't have to store it ;) But in practice engines are not omniscient enough.

//
// note: technically, the engine is allowed to inspect this
// callback, observe that 'wr' is not extracted, and then not
// retain it in the first place (back in
// registerDroppedCollection), but no engine goes that far
descriptor.collectionDeleter(descriptor);
}

Expand Down Expand Up @@ -666,6 +686,8 @@ export function makeVirtualReferenceManager(
getReachableRefCount,
countCollectionsForWeakKey,

// don't harden() the mock FR, that will break it
getDroppedCollectionRegistry: () => droppedCollectionRegistry,
remotableRefCounts,
vrefRecognizers,
kindInfoTable,
Expand All @@ -680,7 +702,7 @@ export function makeVirtualReferenceManager(
}

return harden({
droppedCollectionRegistry,
registerDroppedCollection,
isDurable,
isDurableKind,
registerKind,
Expand Down
5 changes: 5 additions & 0 deletions packages/swingset-liveslots/test/mock-gc.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ export function makeMockGC() {
log(` kill done`);
}

function weakRefFor(val) {
return valToWeakRef.get(val);
}

const mockFinalizationRegistryProto = {
register(val, context) {
log(`FR.register(context=${context})`);
Expand Down Expand Up @@ -95,6 +99,7 @@ export function makeMockGC() {
WeakRef: mockWeakRef,
FinalizationRegistry: mockFinalizationRegistry,
kill,
weakRefFor,
getAllFRs,
flushAllFRs,
waitUntilQuiescent,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import test from 'ava';
import '@endo/init/debug.js';
import { Far } from '@endo/marshal';
import { makeLiveSlots } from '../src/liveslots.js';
import { kser } from './kmarshal.js';
import { buildSyscall } from './liveslots-helpers.js';
import { makeStartVat } from './util.js';
import { makeMockGC } from './mock-gc.js';

test('droppedCollectionWeakRefs', async t => {
const { syscall } = buildSyscall();
const gcTools = makeMockGC();
let myVOAwareWeakMap;
let myVOAwareWeakSet;

// In XS, WeakRefs are treated as strong references except for
// forced GC, which reduces our sensitivity to GC timing (which is
// more likely to change under small upgrades of the engine). We'd
// like this improvement for objects tracked by
// FinalizationRegistries too. Liveslots has two FRs,
// `vreffedObjectRegistry` (whose entries all have WeakRefs in
// slotToVal), and `droppedCollectionRegistry` (in the VRM).
//
// `droppedCollectionRegistry` tracks the VO-aware replacements for
// WeakMap/Set that we impose on userspace, and these do not have
// vref identities, so they will never appear in slotToVal or
// valToSlot, so we need to create new WeakRefs to trigger the
// retain-under-organic-GC behavior. Those WeakRefs are held in the
// FR context/"heldValue" until it fires.

function buildRootObject(vatPowers) {
const { WeakMap, WeakSet } = vatPowers;
// creating a WeakMap/Set should put it in droppedCollectionWeakRefs
myVOAwareWeakMap = new WeakMap();
myVOAwareWeakSet = new WeakSet();
return Far('root', {});
}

const makeNS = () => ({ buildRootObject });
const ls = makeLiveSlots(syscall, 'vatA', {}, {}, gcTools, undefined, makeNS);
const { dispatch, testHooks } = ls;
await dispatch(makeStartVat(kser()));

const wmWeakRef = gcTools.weakRefFor(myVOAwareWeakMap);
const wsWeakRef = gcTools.weakRefFor(myVOAwareWeakSet);

// we snoop inside our mock FinalizationRegistry to get the context
const fr = testHooks.getDroppedCollectionRegistry();
t.is(fr.registry.get(myVOAwareWeakMap).wr, wmWeakRef);
t.is(fr.registry.get(myVOAwareWeakSet).wr, wsWeakRef);

gcTools.kill(myVOAwareWeakMap);
gcTools.flushAllFRs();
t.falsy(fr.registry.has(myVOAwareWeakMap));
t.truthy(fr.registry.has(myVOAwareWeakSet)); // not dead yet

gcTools.kill(myVOAwareWeakSet);
gcTools.flushAllFRs();
t.falsy(fr.registry.has(myVOAwareWeakMap));
t.falsy(fr.registry.has(myVOAwareWeakSet)); // dead now
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/* global FinalizationRegistry */
/* global FinalizationRegistry WeakRef */
import test from 'ava';
import '@endo/init/debug.js';

Expand All @@ -16,6 +16,7 @@ function makeVRM() {
getSlotForVal,
requiredValForSlot,
FinalizationRegistry,
WeakRef,
addToPossiblyDeadSet,
addToPossiblyRetiredSet,
false,
Expand Down
18 changes: 17 additions & 1 deletion packages/swingset-liveslots/tools/fakeVirtualSupport.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/* global WeakRef */
/* eslint-disable max-classes-per-file */
import { makeMarshal } from '@endo/marshal';
import { assert } from '@agoric/assert';
import { parseVatSlot } from '../src/parseVatSlots.js';
Expand All @@ -19,6 +20,18 @@ class FakeFinalizationRegistry {
unregister(_unregisterToken) {}
}

class FakeWeakRef {
constructor(target) {
this.target = target;
}

deref() {
return this.target; // strong ref
}
}

const RealWeakRef = WeakRef;

export function makeFakeLiveSlotsStuff(options = {}) {
let vrm;
function setVrm(vrmToUse) {
Expand All @@ -31,6 +44,7 @@ export function makeFakeLiveSlotsStuff(options = {}) {
weak = false,
log,
FinalizationRegistry = FakeFinalizationRegistry,
WeakRef = FakeWeakRef, // VRM uses this
addToPossiblyDeadSet = () => {},
addToPossiblyRetiredSet = () => {},
} = options;
Expand Down Expand Up @@ -174,7 +188,7 @@ export function makeFakeLiveSlotsStuff(options = {}) {
}

function setValForSlot(slot, val) {
slotToVal.set(slot, weak ? new WeakRef(val) : val);
slotToVal.set(slot, weak ? new RealWeakRef(val) : val);
}

function convertValToSlot(val) {
Expand Down Expand Up @@ -255,6 +269,7 @@ export function makeFakeLiveSlotsStuff(options = {}) {
marshal,
deleteEntry,
FinalizationRegistry,
WeakRef,
addToPossiblyDeadSet,
addToPossiblyRetiredSet,
dumpStore,
Expand All @@ -273,6 +288,7 @@ export function makeFakeVirtualReferenceManager(
fakeStuff.getSlotForVal,
fakeStuff.getValForSlot,
fakeStuff.FinalizationRegistry,
fakeStuff.WeakRef,
fakeStuff.addToPossiblyDeadSet,
fakeStuff.addToPossiblyRetiredSet,
relaxDurabilityRules,
Expand Down