From ab17e01c124b6be7e69ba7e4ed5885289204bc89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Konstantin=20K=C3=A4fer?= Date: Thu, 22 Aug 2019 10:56:38 +0200 Subject: [PATCH 1/2] use MessageChannel instead of setTimeout to avoid processing delays --- debug/animate.html | 71 ++++++++++++++++++++++++++++++++++++++++++++++ src/util/actor.js | 22 ++++++++------ 2 files changed, 85 insertions(+), 8 deletions(-) create mode 100644 debug/animate.html diff --git a/debug/animate.html b/debug/animate.html new file mode 100644 index 00000000000..1430c31cacf --- /dev/null +++ b/debug/animate.html @@ -0,0 +1,71 @@ + + + + Mapbox GL JS debug page + + + + + + + +
+ + + + + + + \ No newline at end of file diff --git a/src/util/actor.js b/src/util/actor.js index 227f15f3b4f..471a7df93a6 100644 --- a/src/util/actor.js +++ b/src/util/actor.js @@ -26,7 +26,8 @@ class Actor { tasks: { number: any }; taskQueue: Array; cancelCallbacks: { number: Cancelable }; - taskTimeout: ?TimeoutID; + channel: MessageChannel; + triggeredProcessing: boolean; static taskId: number; @@ -37,9 +38,11 @@ class Actor { this.callbacks = {}; this.tasks = {}; this.taskQueue = []; - this.taskTimeout = null; this.cancelCallbacks = {}; + this.channel = new MessageChannel(); + this.triggeredProcessing = false; bindAll(['receive', 'process'], this); + this.channel.port2.onmessage = this.process; this.target.addEventListener('message', this.receive, false); } @@ -108,18 +111,20 @@ class Actor { // is necessary because we want to keep receiving messages, and in particular, // messages. Some tasks may take a while in the worker thread, so before // executing the next task in our queue, postMessage preempts this and - // messages can be processed. + // messages can be processed. We're using a MessageChannel object to get throttle the + // process() flow to one at a time. this.tasks[id] = data; this.taskQueue.push(id); - if (!this.taskTimeout) { - this.taskTimeout = setTimeout(this.process, 0); + if (!this.triggeredProcessing) { + this.triggeredProcessing = true; + this.channel.port1.postMessage(true); } } } process() { - // Reset the timeout ID so that we know that no process call is scheduled in the future yet. - this.taskTimeout = null; + // Reset the flag so that we know that no process call is scheduled for the future yet. + this.triggeredProcessing = false; if (!this.taskQueue.length) { return; } @@ -130,7 +135,8 @@ class Actor { // current task. This is necessary so that processing continues even if the current task // doesn't execute successfully. if (this.taskQueue.length) { - this.taskTimeout = setTimeout(this.process, 0); + this.triggeredProcessing = true; + this.channel.port1.postMessage(true); } if (!task) { // If the task ID doesn't have associated task data anymore, it was canceled. From 241618f3e39144d1ca88c1247095024d15b90d47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Konstantin=20K=C3=A4fer?= Date: Thu, 22 Aug 2019 14:42:25 +0200 Subject: [PATCH 2/2] move invocation throttling to ThrottledInvoker class also adds a fallback for environments that don't support MessageChannel --- src/util/actor.js | 18 +++++---------- src/util/throttled_invoker.js | 41 +++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 13 deletions(-) create mode 100644 src/util/throttled_invoker.js diff --git a/src/util/actor.js b/src/util/actor.js index 471a7df93a6..af626c8ec30 100644 --- a/src/util/actor.js +++ b/src/util/actor.js @@ -2,6 +2,7 @@ import { bindAll } from './util'; import { serialize, deserialize } from './web_worker_transfer'; +import ThrottledInvoker from './throttled_invoker'; import type {Transferable} from '../types/transferable'; import type {Cancelable} from '../types/cancelable'; @@ -26,8 +27,7 @@ class Actor { tasks: { number: any }; taskQueue: Array; cancelCallbacks: { number: Cancelable }; - channel: MessageChannel; - triggeredProcessing: boolean; + invoker: ThrottledInvoker; static taskId: number; @@ -39,10 +39,8 @@ class Actor { this.tasks = {}; this.taskQueue = []; this.cancelCallbacks = {}; - this.channel = new MessageChannel(); - this.triggeredProcessing = false; bindAll(['receive', 'process'], this); - this.channel.port2.onmessage = this.process; + this.invoker = new ThrottledInvoker(this.process); this.target.addEventListener('message', this.receive, false); } @@ -115,16 +113,11 @@ class Actor { // process() flow to one at a time. this.tasks[id] = data; this.taskQueue.push(id); - if (!this.triggeredProcessing) { - this.triggeredProcessing = true; - this.channel.port1.postMessage(true); - } + this.invoker.trigger(); } } process() { - // Reset the flag so that we know that no process call is scheduled for the future yet. - this.triggeredProcessing = false; if (!this.taskQueue.length) { return; } @@ -135,8 +128,7 @@ class Actor { // current task. This is necessary so that processing continues even if the current task // doesn't execute successfully. if (this.taskQueue.length) { - this.triggeredProcessing = true; - this.channel.port1.postMessage(true); + this.invoker.trigger(); } if (!task) { // If the task ID doesn't have associated task data anymore, it was canceled. diff --git a/src/util/throttled_invoker.js b/src/util/throttled_invoker.js new file mode 100644 index 00000000000..0fc3a614dae --- /dev/null +++ b/src/util/throttled_invoker.js @@ -0,0 +1,41 @@ +// @flow + +/** + * Invokes the wrapped function in a non-blocking way when trigger() is called. Invocation requests + * are ignored until the function was actually invoked. + * + * @private + */ +class ThrottledInvoker { + _channel: MessageChannel; + _triggered: boolean; + _callback: Function + + constructor(callback: Function) { + this._callback = callback; + this._triggered = false; + if (typeof MessageChannel !== 'undefined') { + this._channel = new MessageChannel(); + this._channel.port2.onmessage = () => { + this._triggered = false; + this._callback(); + }; + } + } + + trigger() { + if (!this._triggered) { + this._triggered = true; + if (this._channel) { + this._channel.port1.postMessage(true); + } else { + setTimeout(() => { + this._triggered = false; + this._callback(); + }, 0); + } + } + } +} + +export default ThrottledInvoker;