+ Mapbox GL JS debug page
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/util/actor.js b/src/util/actor.js
index 940a0030f0b..f6677c12f1e 100644
--- a/src/util/actor.js
+++ b/src/util/actor.js
@@ -2,12 +2,23 @@
import {bindAll, isWorker, isSafari} from './util';
import window from './window';
+import browser from './browser';
import {serialize, deserialize} from './web_worker_transfer';
import ThrottledInvoker from './throttled_invoker';
import type {Transferable} from '../types/transferable';
import type {Cancelable} from '../types/cancelable';
+// Upper limit on time in ms, the actor allocates towards flushing the task queue per frame
+const MAIN_THREAD_TIME_BUDGET = 1;
+// Upper limit of number of tasks executed in a frame in the main thread
+const MAIN_THREAD_TASK_BUDGET = 10;
+
+// Upper limit on time in ms, the actor allocates towards flushing the task queue per worker tick, see throttled_invoker.js
+const WORKER_THREAD_TIME_BUDGET = 2;
+// Upper limit of number of tasks executed per worker tick as per throttled_invoker.
+const WORKER_THREAD_TASK_BUDGET = 20;
+
/**
* An implementation of the [Actor design pattern](http://en.wikipedia.org/wiki/Actor_model)
* that maintains the relationship between asynchronous tasks and the objects
@@ -30,11 +41,13 @@ class Actor {
cancelCallbacks: { number: Cancelable };
invoker: ThrottledInvoker;
globalScope: any;
+ isWorker: boolean;
constructor(target: any, parent: any, mapId: ?number) {
this.target = target;
this.parent = parent;
this.mapId = mapId;
+ this.isWorker = isWorker();
this.callbacks = {};
this.tasks = {};
this.taskQueue = [];
@@ -42,7 +55,7 @@ class Actor {
bindAll(['receive', 'process'], this);
this.invoker = new ThrottledInvoker(this.process);
this.target.addEventListener('message', this.receive, false);
- this.globalScope = isWorker() ? target : window;
+ this.globalScope = this.isWorker ? target : window;
}
/**
@@ -110,21 +123,9 @@ class Actor {
cancel();
}
} else {
- // In workers, store the tasks that we need to process before actually processing them. This
- // 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. 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 (isWorker()) {
- this.invoker.trigger();
- } else {
- // In the main thread, process messages immediately so that other work does not slip in
- // between getting partial data back from workers.
- this.process();
- }
+ this.invoker.trigger();
}
}
@@ -132,20 +133,31 @@ class Actor {
if (!this.taskQueue.length) {
return;
}
- const id = this.taskQueue.shift();
- const task = this.tasks[id];
- delete this.tasks[id];
- // Schedule another process call if we know there's more to process _before_ invoking the
- // current task. This is necessary so that processing continues even if the current task
- // doesn't execute successfully.
+
+ const timeBudget = this.isWorker ? WORKER_THREAD_TIME_BUDGET : MAIN_THREAD_TIME_BUDGET;
+ const taskBudget = this.isWorker ? WORKER_THREAD_TASK_BUDGET : MAIN_THREAD_TASK_BUDGET;
+
+ const start = browser.now();
+ let taskCtr = 0;
+ while (browser.now() - start < timeBudget && taskCtr < taskBudget && this.taskQueue.length > 0) {
+ this._processQueueTop();
+ taskCtr++;
+ }
+ // We've reached our budget for this frame, defer processing of the rest of the tasks to the next tick,
+ // this allows the deferred tasks to be preempted on slower browsers.
if (this.taskQueue.length) {
this.invoker.trigger();
}
+ }
+
+ _processQueueTop() {
+ const id = this.taskQueue.shift();
+ const task = this.tasks[id];
+ delete this.tasks[id];
if (!task) {
// If the task ID doesn't have associated task data anymore, it was canceled.
return;
}
-
if (task.type === '') {
// The done() function in the counterpart has been called, and we are now
// firing the callback in the originating actor, if there is one.
diff --git a/src/util/throttled_invoker.js b/src/util/throttled_invoker.js
index ca1b97bfce1..f0426a4b7c6 100644
--- a/src/util/throttled_invoker.js
+++ b/src/util/throttled_invoker.js
@@ -1,45 +1,64 @@
// @flow
+import window from './window';
+import {isWorker} from './util';
+const raf = window.requestAnimationFrame ||
+ window.mozRequestAnimationFrame ||
+ window.webkitRequestAnimationFrame ||
+ window.msRequestAnimationFrame;
/**
* Invokes the wrapped function in a non-blocking way when trigger() is called. Invocation requests
* are ignored until the function was actually invoked.
*
+ * On the main thread, this uses requestAnimationFrame so the deferral of tasks is as low-latency with the render loop as possible.
+ * In the WebWorker context, we use a `MessageChannel` to send a message back to the same context, with a fallback to `setTimeout(..,0)`.
+ *
* @private
*/
class ThrottledInvoker {
- _channel: MessageChannel;
+ _channel: ?MessageChannel;
_triggered: boolean;
- _callback: Function
+ _callback: Function;
+ _bindFunc: Function;
+ _isWorker: boolean;
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();
- };
+ // The function actually bound to the callback runner.
+ this._bindFunc = () => {
+ this._triggered = false;
+ this._callback();
+ };
+ this._isWorker = isWorker();
+ if (this._isWorker) {
+ if (typeof MessageChannel !== 'undefined') {
+ this._channel = new MessageChannel();
+ this._channel.port2.onmessage = this._bindFunc;
+ }
}
}
trigger() {
if (!this._triggered) {
this._triggered = true;
- if (this._channel) {
- this._channel.port1.postMessage(true);
+ //Invoker is MessageChannel/setTimeout on worker side
+ if (this._isWorker) {
+ if (this._channel) {
+ this._channel.port1.postMessage(true);
+ } else {
+ setTimeout(this._bindFunc, 0);
+ }
+ // requestAnimationFrame on the Main Thread.
} else {
- setTimeout(() => {
- this._triggered = false;
- this._callback();
- }, 0);
+ raf(this._bindFunc);
}
}
}
remove() {
- delete this._channel;
this._callback = () => {};
+ this._bindFunc = () => {};
}
}