Web worker for amp-bind#7436
Conversation
8819c6e to
ac6d200
Compare
|
/to @cramforce @jridgewell /cc @kmh287 PTAL, working on tests now. |
3cff9b2 to
6db62be
Compare
cramforce
left a comment
There was a problem hiding this comment.
Just a few comments. This is exciting!
|
|
||
| this.evaluator_ = new BindEvaluator(); | ||
| const parseErrors = this.evaluator_.setBindings(bindings); | ||
| // Parse on web worker if experiment is enabled. |
There was a problem hiding this comment.
Are you making this an experiment to not disrupt your other development, for easy benchmarking, or is there a different reason?
There was a problem hiding this comment.
Mostly to reduce risk of breakages as we start talking to publishers (e.g. #7329). I can remove this shortly after verifying correctness in prod.
extensions/amp-bind/0.1/bind-impl.js
Outdated
| const {property, expressionString, previousResult} = | ||
| boundProperty; | ||
|
|
||
| // Rewrite attribute value if necessary. |
There was a problem hiding this comment.
Definitely fine for now. The URL API may be exposed to workers.
There was a problem hiding this comment.
The problem is that parseUrl uses document.createElement, which IIUC is unavailable to workers.
To workaround, we could either change this implementation to not use DOM or even use Worker#postMessage to delegate this to main page. Thoughts?
There was a problem hiding this comment.
Ohh I see, thanks. Add a TODO here.
src/web-worker/amp-worker.js
Outdated
| */ | ||
| sendMessage_(method, args) { | ||
| const promise = new Promise((resolve, reject) => { | ||
| if (!this.messages_[method]) { |
There was a problem hiding this comment.
This works, but it might be simpler to have a map of outstanding messages with a simple running index that you increment for each message.
src/web-worker/amp-worker.js
Outdated
| } | ||
| } | ||
| if (empty) { | ||
| delete this.messages_[method]; |
There was a problem hiding this comment.
As explained above, this is simpler with a running index and one map independent of method name.
|
|
||
| let returnValue; | ||
|
|
||
| switch (method) { |
There was a problem hiding this comment.
Please add TODO and file issue to add error reporting to this.
We can slightly generify the code in src/service-worker/error-reporting.js
| } else { | ||
| throw new Error(`${method}: BindEvaluator is not initialized.`); | ||
| } | ||
| break; |
dreamofabear
left a comment
There was a problem hiding this comment.
Thanks for the quick review.
|
|
||
| this.evaluator_ = new BindEvaluator(); | ||
| const parseErrors = this.evaluator_.setBindings(bindings); | ||
| // Parse on web worker if experiment is enabled. |
There was a problem hiding this comment.
Mostly to reduce risk of breakages as we start talking to publishers (e.g. #7329). I can remove this shortly after verifying correctness in prod.
src/web-worker/amp-worker.js
Outdated
| */ | ||
| sendMessage_(method, args) { | ||
| const promise = new Promise((resolve, reject) => { | ||
| if (!this.messages_[method]) { |
src/web-worker/amp-worker.js
Outdated
| } | ||
| } | ||
| if (empty) { | ||
| delete this.messages_[method]; |
|
|
||
| let returnValue; | ||
|
|
||
| switch (method) { |
| } else { | ||
| throw new Error(`${method}: BindEvaluator is not initialized.`); | ||
| } | ||
| break; |
extensions/amp-bind/0.1/bind-impl.js
Outdated
| const {property, expressionString, previousResult} = | ||
| boundProperty; | ||
|
|
||
| // Rewrite attribute value if necessary. |
There was a problem hiding this comment.
The problem is that parseUrl uses document.createElement, which IIUC is unavailable to workers.
To workaround, we could either change this implementation to not use DOM or even use Worker#postMessage to delegate this to main page. Thoughts?
e0a7719 to
290d318
Compare
| return invokeWebWorker(this.win_, 'bind.initialize', [bindings]); | ||
| } else { | ||
| this.evaluator_ = new BindEvaluator(); | ||
| const parseErrors = this.evaluator_.setBindings(bindings); |
There was a problem hiding this comment.
No need to wrap this in a promise instance, just return parseErrors.
| } | ||
|
|
||
| dev().fine(TAG, `Initialized ${bindings.length} bindings from ` + | ||
| `${boundElements.length} elements.`); |
extensions/amp-bind/0.1/bind-impl.js
Outdated
| const {property, expressionString, previousResult} = | ||
| boundProperty; | ||
|
|
||
| // Rewrite attribute value if necessary. |
src/web-worker/amp-worker.js
Outdated
| return Promise.reject(`Experiment "${TAG}" is disabled.`); | ||
| } | ||
| if (!win.Worker) { | ||
| return Promise.reject('Worker not supported in window: ' + win); |
There was a problem hiding this comment.
The + win will generate a useless "[object Window]" string.
| var opts = Object.assign({}, options); | ||
|
|
||
| return compileJs('./src/web-worker/', 'web-worker.js', './dist/', { | ||
| toName: 'ww.max.js', |
There was a problem hiding this comment.
This will require special handling on our servers. /cc @vitaliybl
There was a problem hiding this comment.
Good to know, I'll also reach out.
src/web-worker/amp-worker.js
Outdated
| * @private | ||
| */ | ||
| sendMessage_(method, args) { | ||
| const promise = new Promise((resolve, reject) => { |
| const {method, returnValue, id} = | ||
| /** @type {FromWorkerMessageDef} */ (event.data); | ||
|
|
||
| const message = this.messages_[id]; |
There was a problem hiding this comment.
I hate this id based on messages.length. Let's use a monotonic AmpWorker#id, increasing on every message. Then do a binary search when we receive a message. (We're already doing O(n) on every message receipt)
There was a problem hiding this comment.
Splice completed messages from the array and binary search to find them? Seems more complex and slower, I don't see the benefit.
...hate leads to suffering, suffering leads to the dark side.
😄
There was a problem hiding this comment.
We have a never ending array this way...
There was a problem hiding this comment.
Only while messages are in flight which shouldn't be an issue for current functionality.
There was a problem hiding this comment.
Changed this to use a counter per Malte's comment above.
src/web-worker/amp-worker.js
Outdated
| let empty = true; | ||
| for (let i = 0; i < this.messages_.length && empty; i++) { | ||
| if (this.messages_[i] !== undefined) { | ||
| empty = false; |
There was a problem hiding this comment.
The termination condition contains && empty and @camelburrito hates break.
There was a problem hiding this comment.
Should be a while loop, then. 😛
src/web-worker/amp-worker.js
Outdated
| } | ||
| } | ||
| if (empty) { | ||
| this.messages_ = []; |
There was a problem hiding this comment.
Nit: message.length = 0 will truncate and save the array instance.
|
|
||
| // TODO(choumx): Add error reporting. | ||
| switch (method) { | ||
| case 'bind.initialize': |
There was a problem hiding this comment.
Nit: I dislike massive switch statements. Can we split this these into functions (we can keep the switch, then), or split them into functions and use some sort of map from "method" -> function.
There was a problem hiding this comment.
Point taken. With only two cases at the moment, using a <string, Function> map is less readable IMO. Besides, this file will need to be significantly refactored to support other use cases in the future anyways.
There was a problem hiding this comment.
I mean something like:
switch (method) {
case "whatever":
returnValue = whatever(...);
break;
case "other":
returnValue = other(...);
break;
}| return invokeWebWorker(this.win_, 'bind.initialize', [bindings]); | ||
| } else { | ||
| this.evaluator_ = new BindEvaluator(); | ||
| const parseErrors = this.evaluator_.setBindings(bindings); |
| } | ||
|
|
||
| dev().fine(TAG, `Initialized ${bindings.length} bindings from ` + | ||
| `${boundElements.length} elements.`); |
extensions/amp-bind/0.1/bind-impl.js
Outdated
| const {property, expressionString, previousResult} = | ||
| boundProperty; | ||
|
|
||
| // Rewrite attribute value if necessary. |
There was a problem hiding this comment.
Ohh I see, thanks. Add a TODO here.
src/web-worker/amp-worker.js
Outdated
| return Promise.reject(`Experiment "${TAG}" is disabled.`); | ||
| } | ||
| if (!win.Worker) { | ||
| return Promise.reject('Worker not supported in window: ' + win); |
src/web-worker/amp-worker.js
Outdated
| * @private | ||
| */ | ||
| sendMessage_(method, args) { | ||
| const promise = new Promise((resolve, reject) => { |
| const {method, returnValue, id} = | ||
| /** @type {FromWorkerMessageDef} */ (event.data); | ||
|
|
||
| const message = this.messages_[id]; |
There was a problem hiding this comment.
Splice completed messages from the array and binary search to find them? Seems more complex and slower, I don't see the benefit.
...hate leads to suffering, suffering leads to the dark side.
😄
src/web-worker/amp-worker.js
Outdated
| let empty = true; | ||
| for (let i = 0; i < this.messages_.length && empty; i++) { | ||
| if (this.messages_[i] !== undefined) { | ||
| empty = false; |
There was a problem hiding this comment.
The termination condition contains && empty and @camelburrito hates break.
src/web-worker/amp-worker.js
Outdated
| } | ||
| } | ||
| if (empty) { | ||
| this.messages_ = []; |
|
|
||
| // TODO(choumx): Add error reporting. | ||
| switch (method) { | ||
| case 'bind.initialize': |
There was a problem hiding this comment.
Point taken. With only two cases at the moment, using a <string, Function> map is less readable IMO. Besides, this file will need to be significantly refactored to support other use cases in the future anyways.
| var opts = Object.assign({}, options); | ||
|
|
||
| return compileJs('./src/web-worker/', 'web-worker.js', './dist/', { | ||
| toName: 'ww.max.js', |
There was a problem hiding this comment.
Good to know, I'll also reach out.
| break; | ||
|
|
||
| case 'bind.evaluate': | ||
| if (evaluator_) { |
There was a problem hiding this comment.
Do web workers terminate? If so, evaluator_ could be undefined even if it was initialized previously.
There was a problem hiding this comment.
I don't think they do. But we'll find out via error reporting.
There was a problem hiding this comment.
As far as I can tell, they only terminate on request.
src/web-worker/amp-worker.js
Outdated
| * Array of in-flight messages pending response from worker. | ||
| * @const @private {!Array<(PendingMessageDef|undefined)>} | ||
| */ | ||
| this.messages_ = []; |
There was a problem hiding this comment.
Sorry, missed this earlier: This should be an object. Makes the deletion after messages are done so much easier.
src/web-worker/amp-worker.js
Outdated
| */ | ||
| sendMessage_(method, args) { | ||
| return new Promise((resolve, reject) => { | ||
| this.messages_[this.counter_] = {method, resolve, reject}; |
There was a problem hiding this comment.
While we don't need to be thread safe, this is still dangerous if anything throws an exception.
Something like const messageIndex = this.counter_++ would be more obvious, I think.
| */ | ||
| this.digestQueuedAfterScan_ = false; | ||
|
|
||
| /** @const @private {boolean} */ |
There was a problem hiding this comment.
We also need to check that web workers are supported. Might be better to export a function from the amp-worker.js module.
There was a problem hiding this comment.
Looks like web workers are available for all browsers we care about, so I don't think we should bother supporting the fallback case. It's still useful to check this in amp-worker.js so we can reject invokeWebWorker with a meaningful error message.
src/web-worker/amp-worker.js
Outdated
|
|
||
| /** | ||
| * Array of in-flight messages pending response from worker. | ||
| * @const @private {!Object<number, (PendingMessageDef|undefined)>} |
There was a problem hiding this comment.
No need for the |undefined.
| sendMessage_(method, args) { | ||
| return new Promise((resolve, reject) => { | ||
| const id = this.counter_++; | ||
| this.messages_[id] = {method, resolve, reject}; |
There was a problem hiding this comment.
May be useful for returning warnings or errors to the caller. Error reporting for the worker is next: #7453
| let returnValue; | ||
|
|
||
| // TODO(choumx): Add error reporting. | ||
| switch (method) { |
There was a problem hiding this comment.
Let's wrap this in a try-catch, then post back an error if something goes wrong.
| fakeWin = {Worker: fakeWorkerClass}; | ||
|
|
||
| toggleExperiment(fakeWin, 'web-worker', true); | ||
| isExperimentOnStub = sandbox.stub(experiments, 'isExperimentOn'); |
There was a problem hiding this comment.
This is supppppperrrrr icky, and not valid ES6. import * as experiments means experiments is not an mutable object.
Why was this changed?
There was a problem hiding this comment.
Test was failing due to a recent change in the implementation of toggleExperiment and figured stubbing is less brittle. Is there no good way to stub exported methods?
Reverted and changed to avoid the source of the original error.
There was a problem hiding this comment.
You have to export a setter method from the module:
export function method() {}
export function setMethodForTesting(fn) {
method = fn;
}There was a problem hiding this comment.
Ew. 😄
Thanks for the review.
* initial commit for bind worker * move URL rewriting to bind service, add promise api * clean up, fix lint and type errors * install AmpWorker service * add web-worker experiment * add worker experiment fallback * type check web-worker.js * comments and tweaks * mark postMessage with OK * fix dep check and tests * add tests for amp-worker.js * malte's comments * move typedefs to separate file to avoid browserify from bundling polyfills into worker * fix test and type error * justin's comments * malte's comment * increment counter first * justin's comments * fix unit test * fix unit test in a different way
* initial commit for bind worker * move URL rewriting to bind service, add promise api * clean up, fix lint and type errors * install AmpWorker service * add web-worker experiment * add worker experiment fallback * type check web-worker.js * comments and tweaks * mark postMessage with OK * fix dep check and tests * add tests for amp-worker.js * malte's comments * move typedefs to separate file to avoid browserify from bundling polyfills into worker * fix test and type error * justin's comments * malte's comment * increment counter first * justin's comments * fix unit test * fix unit test in a different way
Fixes #6910, partial for #6199.
Workerin a new entry point compiled todist/ww.jsAmpWorkerclass that wraps theWorkerwith a Promise-based APIamp-bindto parse and evaluate expressions on background thread