Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use MessageChannel instead of setTimeout to avoid processing delays #8673

Merged
merged 2 commits into from
Aug 22, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;