-
Notifications
You must be signed in to change notification settings - Fork 215
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(xsnap): add gcAndFinalize, tests
This changes the xsnap.c run loop to give finalizers a chance to run just after the promise queue drains. With this change, userspace can do a combination of `gc()` and `setImmediate` that lets it provoke a full GC sweep, and wait until finalizers have run. SwingSet will use this during a crank, after userspace has become idle, and before end-of-crank GC processing takes place. This combination is implemented in a function named `gcAndFinalize()`. We copy this function from its normal home in SwingSet so the xsnap.c behavior it depends upon can be tested locally. refs #2660
- Loading branch information
Showing
3 changed files
with
163 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
/* global gc setImmediate */ | ||
|
||
// This is a copy of a utility from packages/SwingSet/src/gc.js, which | ||
// swingset uses to force GC in the middle of a crank. We copy it into xsnap | ||
// to make sure xsnap's gc() and finalizer scheduling will provide what | ||
// swingset needs. | ||
|
||
/* A note on our GC terminology: | ||
* | ||
* We define four states for any JS object to be in: | ||
* | ||
* REACHABLE: There exists a path from some root (live export or top-level | ||
* global) to this object, making it ineligible for collection. Userspace vat | ||
* code has a strong reference to it (and userspace is not given access to | ||
* WeakRef, so it has no weak reference that might be used to get access). | ||
* | ||
* UNREACHABLE: There is no strong reference from a root to the object. | ||
* Userspace vat code has no means to access the object, although liveslots | ||
* might (via a WeakRef). The object is eligible for collection, but that | ||
* collection has not yet happened. The liveslots WeakRef is still alive: if | ||
* it were to call `.deref()`, it would get the object. | ||
* | ||
* COLLECTED: The JS engine has performed enough GC to notice the | ||
* unreachability of the object, and has collected it. The liveslots WeakRef | ||
* is dead: `wr.deref() === undefined`. Neither liveslots nor userspace has | ||
* any way to reach the object, and never will again. A finalizer callback | ||
* has been queued, but not yet executed. | ||
* | ||
* FINALIZED: The JS engine has run the finalizer callback. After this point, | ||
* the object is thoroughly dead and unremembered, and no longer exists in | ||
* one of these four states. | ||
* | ||
* The transition from REACHABLE to UNREACHABLE always happens as a result of | ||
* a message delivery or resolution notification (e.g when userspace | ||
* overwrites a variable, deletes a Map entry, or a callback on the promise | ||
* queue which closed over some objects is retired and deleted). | ||
* | ||
* The transition from UNREACHABLE to COLLECTED can happen spontaneously, as | ||
* the JS engine decides it wants to perform GC. It will also happen | ||
* deliberately if we provoke a GC call with a magic function like `gc()` | ||
* (when Node.js is run with `--expose-gc`, or when XS is configured to | ||
* provide it as a C-level callback). We can force GC, but we cannot prevent | ||
* it from happening at other times. | ||
* | ||
* FinalizationRegistry callbacks are defined to run on their own turn, so | ||
* the transition from COLLECTED to FINALIZED occurs at a turn boundary. | ||
* Node.js appears to schedule these finalizers on the timer/IO queue, not | ||
* the promise/microtask queue. So under Node.js, you need a `setImmediate()` | ||
* or two to ensure that finalizers have had a chance to run. XS is different | ||
* but responds well to similar techniques. | ||
*/ | ||
|
||
/* | ||
* `gcAndFinalize` must be defined in the start compartment. It uses | ||
* platform-specific features to provide a function which provokes a full GC | ||
* operation: all "UNREACHABLE" objects should transition to "COLLECTED" | ||
* before it returns. In addition, `gcAndFinalize()` returns a Promise. This | ||
* Promise will resolve (with `undefined`) after all FinalizationRegistry | ||
* callbacks have executed, causing all COLLECTED objects to transition to | ||
* FINALIZED. If the caller can manage call gcAndFinalize with an empty | ||
* promise queue, then their .then callback will also start with an empty | ||
* promise queue, and there will be minimal uncollected unreachable objects | ||
* in the heap when it begins. | ||
* | ||
* `gcAndFinalize` depends upon platform-specific tools to provoke a GC sweep | ||
* and wait for finalizers to run: a `gc()` function, and `setImmediate`. If | ||
* these tools do not exist, this function will do nothing, and return a | ||
* dummy pre-resolved Promise. | ||
*/ | ||
|
||
export async function gcAndFinalize() { | ||
if (typeof gc !== 'function') { | ||
console.log(`unable to gc(), skipping`); | ||
return; | ||
} | ||
// on Node.js, GC seems to work better if the promise queue is empty first | ||
await new Promise(setImmediate); | ||
// on xsnap, we must do it twice for some reason | ||
await new Promise(setImmediate); | ||
gc(); | ||
// this gives finalizers a chance to run | ||
await new Promise(setImmediate); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
/* global gc FinalizationRegistry WeakRef */ | ||
import test from 'ava'; | ||
|
||
import * as childProcess from 'child_process'; | ||
import * as os from 'os'; | ||
import { xsnap } from '../src/xsnap'; | ||
import { gcAndFinalize } from './gc'; | ||
|
||
function setup() { | ||
const victim = { doomed: 'oh no' }; | ||
const finalized = ['finalizer not called']; | ||
const fr = new FinalizationRegistry(_tag => { | ||
finalized[0] = 'finalizer was called'; | ||
}); | ||
const wr = new WeakRef(victim); | ||
fr.register(victim, 'tag'); | ||
return { finalized, fr, wr }; | ||
} | ||
|
||
async function provokeGC() { | ||
// the transition from REACHABLE to UNREACHABLE happens as soon as setup() | ||
// finishes, and the local 'victim' binding goes out of scope | ||
|
||
// we must retain the FinalizationRegistry to let the callback fire | ||
// eslint-disable-next-line no-unused-vars | ||
const { finalized, fr, wr } = setup(); | ||
|
||
// the transition from UNREACHABLE to COLLECTED can happen at any moment, | ||
// but is far more likely to happen if we force it | ||
await gcAndFinalize(); | ||
// that also moves it from COLLECTED to FINALIZED | ||
const wrState = wr.deref() ? 'weakref is live' : 'weakref is dead'; | ||
const finalizerState = finalized[0]; | ||
return { wrState, finalizerState }; | ||
} | ||
|
||
const xsnapOptions = { | ||
name: 'xsnap test worker', | ||
spawn: childProcess.spawn, | ||
os: os.type(), | ||
stderr: 'inherit', | ||
stdout: 'inherit', | ||
}; | ||
|
||
const decoder = new TextDecoder(); | ||
|
||
function options() { | ||
const messages = []; | ||
async function handleCommand(message) { | ||
messages.push(decoder.decode(message)); | ||
return new Uint8Array(); | ||
} | ||
return { ...xsnapOptions, handleCommand, messages }; | ||
} | ||
|
||
test(`can provoke gc on xsnap`, async t => { | ||
const opts = options(); | ||
const vat = xsnap(opts); | ||
const code = ` | ||
${gcAndFinalize} | ||
${setup} | ||
${provokeGC} | ||
provokeGC().then(data => issueCommand(ArrayBuffer.fromString(JSON.stringify(data)))); | ||
`; | ||
await vat.evaluate(code); | ||
await vat.close(); | ||
t.truthy(opts.messages.length === 1, `xsnap didn't send response`); | ||
const { wrState, finalizerState } = JSON.parse(opts.messages[0]); | ||
// console.log([wrState, finalizerState]); | ||
t.is(wrState, 'weakref is dead'); | ||
t.is(finalizerState, 'finalizer was called'); | ||
}); |