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..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,7 +27,7 @@ class Actor { tasks: { number: any }; taskQueue: Array; cancelCallbacks: { number: Cancelable }; - taskTimeout: ?TimeoutID; + invoker: ThrottledInvoker; static taskId: number; @@ -37,9 +38,9 @@ class Actor { this.callbacks = {}; this.tasks = {}; this.taskQueue = []; - this.taskTimeout = null; this.cancelCallbacks = {}; bindAll(['receive', 'process'], this); + this.invoker = new ThrottledInvoker(this.process); this.target.addEventListener('message', this.receive, false); } @@ -108,18 +109,15 @@ 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); - } + this.invoker.trigger(); } } process() { - // Reset the timeout ID so that we know that no process call is scheduled in the future yet. - this.taskTimeout = null; if (!this.taskQueue.length) { return; } @@ -130,7 +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.taskTimeout = setTimeout(this.process, 0); + 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;