Skip to content

Commit

Permalink
Use MessageChannel instead of setTimeout to avoid processing delays (#…
Browse files Browse the repository at this point in the history
…8673)

* use MessageChannel instead of setTimeout to avoid processing delays

* move invocation throttling to ThrottledInvoker class

also adds a fallback for environments that don't support MessageChannel
  • Loading branch information
kkaefer authored and mourner committed Aug 22, 2019
1 parent 4eb7d4e commit c4e323b
Show file tree
Hide file tree
Showing 3 changed files with 119 additions and 9 deletions.
71 changes: 71 additions & 0 deletions debug/animate.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<!DOCTYPE html>
<html>
<head>
<title>Mapbox GL JS debug page</title>
<meta charset='utf-8'>
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<link rel='stylesheet' href='../dist/mapbox-gl.css' />
<style>
#map { width: 764px; height: 400px; }
</style>
</head>

<body>
<div id='map'></div>

<script src='../dist/mapbox-gl-dev.js'></script>
<script src='access_token_generated.js'></script>
<script>

var map = window.map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/streets-v11',
center: [0, 0],
zoom: 2
});

var radius = 20;

function pointOnCircle(angle) {
return {
"type": "Point",
"coordinates": [
Math.cos(angle) * radius,
Math.sin(angle) * radius
]
};
}

map.on('load', function () {
// Add a source and layer displaying a point which will be animated in a circle.
map.addSource('point', {
"type": "geojson",
"data": pointOnCircle(0)
});

map.addLayer({
"id": "point",
"source": "point",
"type": "circle",
"paint": {
"circle-radius": 10,
"circle-color": "#007cbf"
}
});

function animateMarker(timestamp) {
// Update the data to a new position based on the animation timestamp. The
// divisor in the expression `timestamp / 1000` controls the animation speed.
map.getSource('point').setData(pointOnCircle(timestamp / 1000));

// Request the next frame of the animation.
requestAnimationFrame(animateMarker);
}

// Start the animation.
animateMarker(0);
});
</script>

</body>
</html>
16 changes: 7 additions & 9 deletions src/util/actor.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -26,7 +27,7 @@ class Actor {
tasks: { number: any };
taskQueue: Array<number>;
cancelCallbacks: { number: Cancelable };
taskTimeout: ?TimeoutID;
invoker: ThrottledInvoker;

static taskId: number;

Expand All @@ -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);
}

Expand Down Expand Up @@ -108,18 +109,15 @@ class Actor {
// is necessary because we want to keep receiving messages, and in particular,
// <cancel> messages. Some tasks may take a while in the worker thread, so before
// executing the next task in our queue, postMessage preempts this and <cancel>
// 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;
}
Expand All @@ -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.
Expand Down
41 changes: 41 additions & 0 deletions src/util/throttled_invoker.js
Original file line number Diff line number Diff line change
@@ -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;

0 comments on commit c4e323b

Please sign in to comment.