Skip to content

Commit 2bdd72f

Browse files
authored
defer generator resumption until start of frame (#319)
* defer generator resumption until start of frame * handle generator error * assign promise if generator.next throws * don’t expose value until it resolves * remove needless try-catch * compute downstream on error * minimize diff * clean and comment * reuse promise
1 parent c035367 commit 2bdd72f

File tree

3 files changed

+223
-49
lines changed

3 files changed

+223
-49
lines changed

src/runtime.js

Lines changed: 93 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export default function Runtime(builtins = new Library, global = window_global)
1616
Object.defineProperties(this, {
1717
_dirty: {value: new Set},
1818
_updates: {value: new Set},
19+
_precomputes: {value: [], writable: true},
1920
_computing: {value: null, writable: true},
2021
_init: {value: null, writable: true},
2122
_modules: {value: new Map},
@@ -34,6 +35,7 @@ Object.defineProperties(Runtime, {
3435
});
3536

3637
Object.defineProperties(Runtime.prototype, {
38+
_precompute: {value: runtime_precompute, writable: true, configurable: true},
3739
_compute: {value: runtime_compute, writable: true, configurable: true},
3840
_computeSoon: {value: runtime_computeSoon, writable: true, configurable: true},
3941
_computeNow: {value: runtime_computeNow, writable: true, configurable: true},
@@ -72,24 +74,32 @@ function runtime_module(define, observer = noop) {
7274
return module;
7375
}
7476

77+
function runtime_precompute(callback) {
78+
this._precomputes.push(callback);
79+
this._compute();
80+
}
81+
7582
function runtime_compute() {
7683
return this._computing || (this._computing = this._computeSoon());
7784
}
7885

7986
function runtime_computeSoon() {
80-
var runtime = this;
81-
return new Promise(function(resolve) {
82-
frame(function() {
83-
resolve();
84-
runtime._disposed || runtime._computeNow();
85-
});
86-
});
87+
return new Promise(frame).then(() => this._disposed ? undefined : this._computeNow());
8788
}
8889

89-
function runtime_computeNow() {
90+
async function runtime_computeNow() {
9091
var queue = [],
9192
variables,
92-
variable;
93+
variable,
94+
precomputes = this._precomputes;
95+
96+
// If there are any paused generators, resume them before computing so they
97+
// can update (if synchronous) before computing downstream variables.
98+
if (precomputes.length) {
99+
this._precomputes = [];
100+
for (const callback of precomputes) callback();
101+
await runtime_defer(3);
102+
}
93103

94104
// Compute the reachability of the transitive closure of dirty variables.
95105
// Any newly-reachable variable must also be recomputed.
@@ -159,6 +169,16 @@ function runtime_computeNow() {
159169
}
160170
}
161171

172+
// We want to give generators, if they’re defined synchronously, a chance to
173+
// update before computing downstream variables. This creates a synchronous
174+
// promise chain of the given depth that we’ll await before recomputing
175+
// downstream variables.
176+
function runtime_defer(depth = 0) {
177+
let p = Promise.resolve();
178+
for (let i = 0; i < depth; ++i) p = p.then(() => {});
179+
return p;
180+
}
181+
162182
function variable_circular(variable) {
163183
const inputs = new Set(variable._inputs);
164184
for (const i of inputs) {
@@ -206,10 +226,21 @@ function variable_compute(variable) {
206226
variable._invalidate();
207227
variable._invalidate = noop;
208228
variable._pending();
209-
var value0 = variable._value,
210-
version = ++variable._version,
211-
invalidation = null,
212-
promise = variable._promise = Promise.all(variable._inputs.map(variable_value)).then(function(inputs) {
229+
230+
const value0 = variable._value;
231+
const version = ++variable._version;
232+
233+
// Lazily-constructed invalidation variable; only constructed if referenced as an input.
234+
let invalidation = null;
235+
236+
// If the variable doesn’t have any inputs, we can optimize slightly.
237+
const promise = variable._promise = (variable._inputs.length
238+
? Promise.all(variable._inputs.map(variable_value)).then(define)
239+
: new Promise(resolve => resolve(variable._definition.call(value0))))
240+
.then(generate);
241+
242+
// Compute the initial value of the variable.
243+
function define(inputs) {
213244
if (variable._version !== version) return;
214245

215246
// Replace any reference to invalidation with the promise, lazily.
@@ -227,64 +258,77 @@ function variable_compute(variable) {
227258
}
228259
}
229260

230-
// Compute the initial value of the variable.
231261
return variable._definition.apply(value0, inputs);
232-
}).then(function(value) {
233-
// If the value is a generator, then retrieve its first value,
234-
// and dispose of the generator if the variable is invalidated.
235-
// Note that the cell may already have been invalidated here,
236-
// in which case we need to terminate the generator immediately!
262+
}
263+
264+
// If the value is a generator, then retrieve its first value, and dispose of
265+
// the generator if the variable is invalidated. Note that the cell may
266+
// already have been invalidated here, in which case we need to terminate the
267+
// generator immediately!
268+
function generate(value) {
237269
if (generatorish(value)) {
238270
if (variable._version !== version) return void value.return();
239271
(invalidation || variable_invalidator(variable)).then(variable_return(value));
240-
return variable_precompute(variable, version, promise, value);
272+
return variable_generate(variable, version, value);
241273
}
242274
return value;
243-
});
244-
promise.then(function(value) {
275+
}
276+
277+
promise.then((value) => {
245278
if (variable._version !== version) return;
246279
variable._value = value;
247280
variable._fulfilled(value);
248-
}, function(error) {
281+
}, (error) => {
249282
if (variable._version !== version) return;
250283
variable._value = undefined;
251284
variable._rejected(error);
252285
});
253286
}
254287

255-
function variable_precompute(variable, version, promise, generator) {
288+
function variable_generate(variable, version, generator) {
289+
const runtime = variable._module._runtime;
290+
291+
// Retrieve the next value from the generator; if successful, invoke the
292+
// specified callback. The returned promise resolves to the yielded value, or
293+
// to undefined if the generator is done.
294+
function compute(onfulfilled) {
295+
return new Promise(resolve => resolve(generator.next())).then(({done, value}) => {
296+
return done ? undefined : (value = Promise.resolve(value), value.then(onfulfilled), value);
297+
});
298+
}
299+
300+
// Retrieve the next value from the generator; if successful, fulfill the
301+
// variable, compute downstream variables, and schedule the next value to be
302+
// pulled from the generator at the start of the next animation frame. If not
303+
// successful, reject the variable, compute downstream variables, and return.
256304
function recompute() {
257-
var promise = new Promise(function(resolve) {
258-
resolve(generator.next());
259-
}).then(function(next) {
260-
return next.done ? undefined : Promise.resolve(next.value).then(function(value) {
261-
if (variable._version !== version) return;
262-
variable_postrecompute(variable, value, promise).then(recompute);
263-
variable._fulfilled(value);
264-
return value;
265-
});
305+
const promise = compute((value) => {
306+
if (variable._version !== version) return;
307+
postcompute(value, promise).then(() => runtime._precompute(recompute));
308+
variable._fulfilled(value);
266309
});
267-
promise.catch(function(error) {
310+
promise.catch((error) => {
268311
if (variable._version !== version) return;
269-
variable_postrecompute(variable, undefined, promise);
312+
postcompute(undefined, promise);
270313
variable._rejected(error);
271314
});
272315
}
273-
return new Promise(function(resolve) {
274-
resolve(generator.next());
275-
}).then(function(next) {
276-
if (next.done) return;
277-
promise.then(recompute);
278-
return next.value;
279-
});
280-
}
281316

282-
function variable_postrecompute(variable, value, promise) {
283-
var runtime = variable._module._runtime;
284-
variable._value = value;
285-
variable._promise = promise;
286-
variable._outputs.forEach(runtime._updates.add, runtime._updates); // TODO Cleaner?
287-
return runtime._compute();
317+
// After the generator fulfills or rejects, set its current value, promise,
318+
// and schedule any downstream variables for update.
319+
function postcompute(value, promise) {
320+
variable._value = value;
321+
variable._promise = promise;
322+
variable._outputs.forEach(runtime._updates.add, runtime._updates);
323+
return runtime._compute();
324+
}
325+
326+
// When retrieving the first value from the generator, the promise graph is
327+
// already established, so we only need to queue the next pull.
328+
return compute(() => {
329+
if (variable._version !== version) return;
330+
runtime._precompute(recompute);
331+
});
288332
}
289333

290334
function variable_error(variable, error) {

test/module/value-test.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,38 @@ tape("module.value(name) supports generators", async test => {
3737
test.deepEqual(await module.value("foo"), 3);
3838
});
3939

40+
tape("module.value(name) supports generators that throw", async test => {
41+
const runtime = new Runtime();
42+
const module = runtime.module();
43+
module.define("foo", [], function*() { yield 1; throw new Error("fooed"); });
44+
module.define("bar", ["foo"], foo => foo);
45+
const [foo1, bar1] = await Promise.all([module.value("foo"), module.value("bar")]);
46+
test.deepEqual(foo1, 1);
47+
test.deepEqual(bar1, 1);
48+
try {
49+
await module.value("foo");
50+
test.fail();
51+
} catch (error) {
52+
test.deepEqual(error.message, "fooed");
53+
}
54+
try {
55+
await module.value("bar");
56+
test.fail();
57+
} catch (error) {
58+
test.deepEqual(error.message, "fooed");
59+
}
60+
});
61+
62+
tape("module.value(name) supports async generators", async test => {
63+
const runtime = new Runtime();
64+
const module = runtime.module();
65+
module.define("foo", [], async function*() { yield 1; yield 2; yield 3; });
66+
test.deepEqual(await module.value("foo"), 1);
67+
test.deepEqual(await module.value("foo"), 2);
68+
test.deepEqual(await module.value("foo"), 3);
69+
test.deepEqual(await module.value("foo"), 3);
70+
});
71+
4072
tape("module.value(name) supports promises", async test => {
4173
const runtime = new Runtime();
4274
const module = runtime.module();

test/variable/define-test.js

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,3 +388,101 @@ tape("variable.define correctly handles globals that throw", async test => {
388388
const foo = module.variable(true).define(["oops"], oops => oops);
389389
test.deepEqual(await valueof(foo), {error: "RuntimeError: oops"});
390390
});
391+
392+
tape("variable.define allows other variables to begin computation before a generator may resume", async test => {
393+
const runtime = new Runtime();
394+
const module = runtime.module();
395+
const main = runtime.module();
396+
let i = 0;
397+
let genIteration = 0;
398+
let valIteration = 0;
399+
const onGenFulfilled = value => {
400+
if (genIteration === 0) {
401+
test.equals(valIteration, 0);
402+
test.equals(value, 1);
403+
test.equals(i, 1);
404+
} else if (genIteration === 1) {
405+
test.equals(valIteration, 1);
406+
test.equals(value, 2);
407+
test.equals(i, 2);
408+
} else if (genIteration === 2) {
409+
test.equals(valIteration, 2);
410+
test.equals(value, 3);
411+
test.equals(i, 3);
412+
} else {
413+
test.fail();
414+
}
415+
genIteration++;
416+
};
417+
const onValFulfilled = value => {
418+
if (valIteration === 0) {
419+
test.equals(genIteration, 1);
420+
test.equals(value, 1);
421+
test.equals(i, 1);
422+
} else if (valIteration === 1) {
423+
test.equals(genIteration, 2);
424+
test.equals(value, 2);
425+
test.equals(i, 2);
426+
} else if (valIteration === 2) {
427+
test.equals(genIteration, 3);
428+
test.equals(value, 3);
429+
test.equals(i, 3);
430+
} else {
431+
test.fail();
432+
}
433+
valIteration++;
434+
};
435+
const gen = module.variable({fulfilled: onGenFulfilled}).define("gen", [], function*() {
436+
i++;
437+
yield i;
438+
i++;
439+
yield i;
440+
i++;
441+
yield i;
442+
});
443+
main.variable().import("gen", module);
444+
const val = main.variable({fulfilled: onValFulfilled}).define("val", ["gen"], i => i);
445+
test.equals(await gen._promise, undefined, "gen cell undefined");
446+
test.equals(await val._promise, undefined, "val cell undefined");
447+
await runtime._compute();
448+
test.equals(await gen._promise, 1, "gen cell 1");
449+
test.equals(await val._promise, 1, "val cell 1");
450+
await runtime._compute();
451+
test.equals(await gen._promise, 2, "gen cell 2");
452+
test.equals(await val._promise, 2, "val cell 2");
453+
await runtime._compute();
454+
test.equals(await gen._promise, 3, "gen cell 3");
455+
test.equals(await val._promise, 3, "val cell 3");
456+
});
457+
458+
tape("variable.define allows other variables to begin computation before a generator may resume", async test => {
459+
const runtime = new Runtime();
460+
const main = runtime.module();
461+
let i = 0;
462+
let j = 0;
463+
const gen = main.variable().define("gen", [], function*() {
464+
i++;
465+
yield i;
466+
i++;
467+
yield i;
468+
i++;
469+
yield i;
470+
});
471+
const val = main.variable(true).define("val", ["gen"], gen => {
472+
j++;
473+
test.equals(gen, j, "gen = j");
474+
test.equals(gen, i, "gen = i");
475+
return gen;
476+
});
477+
test.equals(await gen._promise, undefined, "gen = undefined");
478+
test.equals(await val._promise, undefined, "val = undefined");
479+
await runtime._compute();
480+
test.equals(await gen._promise, 1, "gen cell 1");
481+
test.equals(await val._promise, 1, "val cell 1");
482+
await runtime._compute();
483+
test.equals(await gen._promise, 2, "gen cell 2");
484+
test.equals(await val._promise, 2, "val cell 2");
485+
await runtime._compute();
486+
test.equals(await gen._promise, 3, "gen cell 3");
487+
test.equals(await val._promise, 3, "val cell 3");
488+
});

0 commit comments

Comments
 (0)