diff --git a/bench/benchmarks/buffer.js b/bench/benchmarks/buffer.js
index 61d0b13ce9c..0b8f1a8fd61 100644
--- a/bench/benchmarks/buffer.js
+++ b/bench/benchmarks/buffer.js
@@ -91,14 +91,14 @@ function preloadAssets(stylesheet, callback) {
style.on('load', function() {
function getGlyphs(params, callback) {
- style['get glyphs'](params, function(err, glyphs) {
+ style['get glyphs'](0, params, function(err, glyphs) {
assets.glyphs[JSON.stringify(params)] = glyphs;
callback(err, glyphs);
});
}
function getIcons(params, callback) {
- style['get icons'](params, function(err, icons) {
+ style['get icons'](0, params, function(err, icons) {
assets.icons[JSON.stringify(params)] = icons;
callback(err, icons);
});
@@ -185,10 +185,10 @@ var createLayerFamiliesCacheValue;
function createLayerFamilies(layers) {
if (layers !== createLayerFamiliesCacheKey) {
var worker = new Worker({addEventListener: function() {} });
- worker['set layers'](layers);
+ worker['set layers'](0, layers);
createLayerFamiliesCacheKey = layers;
- createLayerFamiliesCacheValue = worker.layerFamilies;
+ createLayerFamiliesCacheValue = worker.layerFamilies[0];
}
return createLayerFamiliesCacheValue;
}
diff --git a/bench/benchmarks/map_load.js b/bench/benchmarks/map_load.js
new file mode 100644
index 00000000000..01de2faefe4
--- /dev/null
+++ b/bench/benchmarks/map_load.js
@@ -0,0 +1,40 @@
+'use strict';
+
+var Evented = require('../../js/util/evented');
+var util = require('../../js/util/util');
+var formatNumber = require('../lib/format_number');
+
+module.exports = function(options) {
+ var evented = util.extend({}, Evented);
+
+ var mapsOnPage = 6;
+
+ evented.fire('log', { message: 'Creating ' + mapsOnPage + ' maps' });
+
+ var loaded = 0;
+ var start = Date.now();
+ for (var i = 0; i < mapsOnPage; i++) {
+ var map = options.createMap({});
+ map.on('load', onload.bind(null, map));
+ map.on('error', function (err) {
+ evented.fire('error', err);
+ });
+ }
+
+ function onload () {
+ if (++loaded >= mapsOnPage) {
+ var duration = Date.now() - start;
+ evented.fire('end', {
+ message: formatNumber(duration) + ' ms, loaded ' + mapsOnPage + ' maps.',
+ score: duration
+ });
+ done();
+ }
+ }
+
+ function done () {
+ }
+
+ return evented;
+};
+
diff --git a/bench/index.js b/bench/index.js
index 85381648229..b1f34add9be 100644
--- a/bench/index.js
+++ b/bench/index.js
@@ -195,6 +195,7 @@ var BenchmarksView = React.createClass({
});
var benchmarks = {
+ 'load-multiple-maps': require('./benchmarks/map_load'),
buffer: require('./benchmarks/buffer'),
fps: require('./benchmarks/fps'),
'frame-duration': require('./benchmarks/frame_duration'),
diff --git a/debug/shared_workers.html b/debug/shared_workers.html
new file mode 100644
index 00000000000..e36330e7fa7
--- /dev/null
+++ b/debug/shared_workers.html
@@ -0,0 +1,122 @@
+
+
+
+ Mapbox GL JS debug page
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Waiting for data...
+
+
+
+
+
+
+
+
+
diff --git a/js/global_worker_pool.js b/js/global_worker_pool.js
new file mode 100644
index 00000000000..3391b3e74ce
--- /dev/null
+++ b/js/global_worker_pool.js
@@ -0,0 +1,16 @@
+'use strict';
+var WorkerPool = require('./util/worker_pool');
+
+var globalWorkerPool;
+
+/**
+ * Creates (if necessary) and returns the single, global WorkerPool instance
+ * to be shared across each Map
+ * @private
+ */
+module.exports = function getGlobalWorkerPool () {
+ if (!globalWorkerPool) {
+ globalWorkerPool = new WorkerPool();
+ }
+ return globalWorkerPool;
+};
diff --git a/js/mapbox-gl.js b/js/mapbox-gl.js
index efebf4e3ce0..55b042995c6 100644
--- a/js/mapbox-gl.js
+++ b/js/mapbox-gl.js
@@ -13,6 +13,8 @@ mapboxgl.Attribution = require('./ui/control/attribution');
mapboxgl.Popup = require('./ui/popup');
mapboxgl.Marker = require('./ui/marker');
+mapboxgl.WorkerPool = require('./util/worker_pool');
+
mapboxgl.Style = require('./style/style');
mapboxgl.Source = require('./source/source');
diff --git a/js/source/worker.js b/js/source/worker.js
index b79a30747a9..09e0afbea32 100644
--- a/js/source/worker.js
+++ b/js/source/worker.js
@@ -15,29 +15,28 @@ function Worker(self) {
this.self = self;
this.actor = new Actor(self, this);
- // simple accessor object for passing to WorkerSources
- var styleLayers = {
- getLayers: function () { return this.layers; }.bind(this),
- getLayerFamilies: function () { return this.layerFamilies; }.bind(this)
- };
+ this.layers = {};
+ this.layerFamilies = {};
- this.workerSources = {
- vector: new VectorTileWorkerSource(this.actor, styleLayers),
- geojson: new GeoJSONWorkerSource(this.actor, styleLayers)
+ this.workerSourceTypes = {
+ vector: VectorTileWorkerSource,
+ geojson: GeoJSONWorkerSource
};
+ // [mapId][sourceType] => worker source instance
+ this.workerSources = {};
+
this.self.registerWorkerSource = function (name, WorkerSource) {
- if (this.workerSources[name]) {
+ if (this.workerSourceTypes[name]) {
throw new Error('Worker source with name "' + name + '" already registered.');
}
- this.workerSources[name] = new WorkerSource(this.actor, styleLayers);
+ this.workerSourceTypes[name] = WorkerSource;
}.bind(this);
}
util.extend(Worker.prototype, {
- 'set layers': function(layers) {
- this.layers = {};
- var that = this;
+ 'set layers': function(mapId, layers) {
+ var styleLayers = this.layers[mapId] = {};
// Filter layers and create an id -> layer map
var childLayerIndicies = [];
@@ -60,20 +59,21 @@ util.extend(Worker.prototype, {
function setLayer(serializedLayer) {
var styleLayer = StyleLayer.create(
serializedLayer,
- serializedLayer.ref && that.layers[serializedLayer.ref]
+ serializedLayer.ref && styleLayers[serializedLayer.ref]
);
styleLayer.updatePaintTransitions({}, {transition: false});
- that.layers[styleLayer.id] = styleLayer;
+ styleLayers[styleLayer.id] = styleLayer;
}
- this.layerFamilies = createLayerFamilies(this.layers);
+ this.layerFamilies[mapId] = createLayerFamilies(this.layers[mapId]);
},
- 'update layers': function(layers) {
- var that = this;
+ 'update layers': function(mapId, layers) {
var id;
var layer;
+ var styleLayers = this.layers[mapId];
+
// Update ref parents
for (id in layers) {
layer = layers[id];
@@ -87,41 +87,41 @@ util.extend(Worker.prototype, {
}
function updateLayer(layer) {
- var refLayer = that.layers[layer.ref];
- if (that.layers[layer.id]) {
- that.layers[layer.id].set(layer, refLayer);
+ var refLayer = styleLayers[layer.ref];
+ if (styleLayers[layer.id]) {
+ styleLayers[layer.id].set(layer, refLayer);
} else {
- that.layers[layer.id] = StyleLayer.create(layer, refLayer);
+ styleLayers[layer.id] = StyleLayer.create(layer, refLayer);
}
- that.layers[layer.id].updatePaintTransitions({}, {transition: false});
+ styleLayers[layer.id].updatePaintTransitions({}, {transition: false});
}
- this.layerFamilies = createLayerFamilies(this.layers);
+ this.layerFamilies[mapId] = createLayerFamilies(this.layers[mapId]);
},
- 'load tile': function(params, callback) {
+ 'load tile': function(mapId, params, callback) {
var type = params.type || 'vector';
- this.workerSources[type].loadTile(params, callback);
+ this.getWorkerSource(mapId, type).loadTile(params, callback);
},
- 'reload tile': function(params, callback) {
+ 'reload tile': function(mapId, params, callback) {
var type = params.type || 'vector';
- this.workerSources[type].reloadTile(params, callback);
+ this.getWorkerSource(mapId, type).reloadTile(params, callback);
},
- 'abort tile': function(params) {
+ 'abort tile': function(mapId, params) {
var type = params.type || 'vector';
- this.workerSources[type].abortTile(params);
+ this.getWorkerSource(mapId, type).abortTile(params);
},
- 'remove tile': function(params) {
+ 'remove tile': function(mapId, params) {
var type = params.type || 'vector';
- this.workerSources[type].removeTile(params);
+ this.getWorkerSource(mapId, type).removeTile(params);
},
- 'redo placement': function(params, callback) {
+ 'redo placement': function(mapId, params, callback) {
var type = params.type || 'vector';
- this.workerSources[type].redoPlacement(params, callback);
+ this.getWorkerSource(mapId, type).redoPlacement(params, callback);
},
/**
@@ -130,13 +130,28 @@ util.extend(Worker.prototype, {
* function taking `(name, workerSourceObject)`.
* @private
*/
- 'load worker source': function(params, callback) {
+ 'load worker source': function(map, params, callback) {
try {
this.self.importScripts(params.url);
callback();
} catch (e) {
callback(e);
}
+ },
+
+ getWorkerSource: function(mapId, type) {
+ if (!this.workerSources[mapId])
+ this.workerSources[mapId] = {};
+ if (!this.workerSources[mapId][type]) {
+ // simple accessor object for passing to WorkerSources
+ var styleLayers = {
+ getLayers: function () { return this.layers[mapId]; }.bind(this),
+ getLayerFamilies: function () { return this.layerFamilies[mapId]; }.bind(this)
+ };
+ this.workerSources[mapId][type] = new this.workerSourceTypes[type](this.actor, styleLayers);
+ }
+
+ return this.workerSources[mapId][type];
}
});
diff --git a/js/style/style.js b/js/style/style.js
index 3e950d14f78..d6c2941108e 100644
--- a/js/style/style.js
+++ b/js/style/style.js
@@ -19,12 +19,13 @@ var QueryFeatures = require('../source/query_features');
var SourceCache = require('../source/source_cache');
var styleSpec = require('./style_spec');
var StyleFunction = require('./style_function');
+var getWorkerPool = require('../global_worker_pool');
module.exports = Style;
-function Style(stylesheet, animationLoop, workerCount) {
+function Style(stylesheet, animationLoop) {
this.animationLoop = animationLoop || new AnimationLoop();
- this.dispatcher = new Dispatcher(workerCount || 1, this);
+ this.dispatcher = new Dispatcher(getWorkerPool(), this);
this.spriteAtlas = new SpriteAtlas(1024, 1024);
this.lineAtlas = new LineAtlas(256, 512);
@@ -751,7 +752,7 @@ Style.prototype = util.inherit(Evented, {
// Callbacks from web workers
- 'get sprite json': function(params, callback) {
+ 'get sprite json': function(_, params, callback) {
var sprite = this.sprite;
if (sprite.loaded()) {
callback(null, { sprite: sprite.data, retina: sprite.retina });
@@ -762,7 +763,7 @@ Style.prototype = util.inherit(Evented, {
}
},
- 'get icons': function(params, callback) {
+ 'get icons': function(_, params, callback) {
var sprite = this.sprite;
var spriteAtlas = this.spriteAtlas;
if (sprite.loaded()) {
@@ -776,7 +777,7 @@ Style.prototype = util.inherit(Evented, {
}
},
- 'get glyphs': function(params, callback) {
+ 'get glyphs': function(_, params, callback) {
var stacks = params.stacks,
remaining = Object.keys(stacks).length,
allGlyphs = {};
diff --git a/js/ui/map.js b/js/ui/map.js
index 0ef10a8c5a9..4c73aec5946 100755
--- a/js/ui/map.js
+++ b/js/ui/map.js
@@ -53,8 +53,7 @@ var defaultOptions = {
failIfMajorPerformanceCaveat: false,
preserveDrawingBuffer: false,
- trackResize: true,
- workerCount: Math.max(browser.hardwareConcurrency - 1, 1)
+ trackResize: true
};
/**
@@ -115,7 +114,6 @@ var defaultOptions = {
* @param {number} [options.zoom=0] The initial zoom level of the map. If `zoom` is not specified in the constructor options, Mapbox GL JS will look for it in the map's style object. If it is not specified in the style, either, it will default to `0`.
* @param {number} [options.bearing=0] The initial bearing (rotation) of the map, measured in degrees counter-clockwise from north. If `bearing` is not specified in the constructor options, Mapbox GL JS will look for it in the map's style object. If it is not specified in the style, either, it will default to `0`.
* @param {number} [options.pitch=0] The initial pitch (tilt) of the map, measured in degrees away from the plane of the screen (0-60). If `pitch` is not specified in the constructor options, Mapbox GL JS will look for it in the map's style object. If it is not specified in the style, either, it will default to `0`.
- * @param {number} [options.workerCount=navigator.hardwareConcurrency - 1] The number of WebWorkers that Mapbox GL JS should use to process vector tile data.
* @example
* var map = new mapboxgl.Map({
* container: 'map',
@@ -129,15 +127,10 @@ var Map = module.exports = function(options) {
options = util.extend({}, defaultOptions, options);
- if (options.workerCount < 1) {
- throw new Error('workerCount must an integer greater than or equal to 1.');
- }
-
this._interactive = options.interactive;
this._failIfMajorPerformanceCaveat = options.failIfMajorPerformanceCaveat;
this._preserveDrawingBuffer = options.preserveDrawingBuffer;
this._trackResize = options.trackResize;
- this._workerCount = options.workerCount;
this._bearingSnap = options.bearingSnap;
if (typeof options.container === 'string') {
@@ -647,7 +640,7 @@ util.extend(Map.prototype, /** @lends Map.prototype */{
} else if (style instanceof Style) {
this.style = style;
} else {
- this.style = new Style(style, this.animationLoop, this._workerCount);
+ this.style = new Style(style, this.animationLoop);
}
this.style
diff --git a/js/util/actor.js b/js/util/actor.js
index dc09086cfeb..9aefb0298de 100644
--- a/js/util/actor.js
+++ b/js/util/actor.js
@@ -10,11 +10,13 @@ module.exports = Actor;
*
* @param {WebWorker} target
* @param {WebWorker} parent
+ * @param {string|number} mapId A unique identifier for the Map instance using this Actor.
* @private
*/
-function Actor(target, parent) {
+function Actor(target, parent, mapId) {
this.target = target;
this.parent = parent;
+ this.mapId = mapId;
this.callbacks = {};
this.callbackID = 0;
this.receive = this.receive.bind(this);
@@ -32,17 +34,19 @@ Actor.prototype.receive = function(message) {
if (callback) callback(data.error || null, data.data);
} else if (typeof data.id !== 'undefined' && this.parent[data.type]) {
// data.type == 'load tile', 'remove tile', etc.
- this.parent[data.type](data.data, done.bind(this));
- } else if (typeof data.id !== 'undefined' && this.parent.workerSources) {
+ this.parent[data.type](data.mapId, data.data, done.bind(this));
+ } else if (typeof data.id !== 'undefined' && this.parent.getWorkerSource) {
// data.type == sourcetype.method
var keys = data.type.split('.');
- this.parent.workerSources[keys[0]][keys[1]](data.data, done.bind(this));
+ var workerSource = this.parent.getWorkerSource(data.mapId, keys[0]);
+ workerSource[keys[1]](data.data, done.bind(this));
} else {
this.parent[data.type](data.data);
}
function done(err, data, buffers) {
this.postMessage({
+ mapId: this.mapId,
type: '',
id: String(id),
error: err ? String(err) : null,
@@ -52,9 +56,9 @@ Actor.prototype.receive = function(message) {
};
Actor.prototype.send = function(type, data, callback, buffers) {
- var id = null;
- if (callback) this.callbacks[id = this.callbackID++] = callback;
- this.postMessage({ type: type, id: String(id), data: data }, buffers);
+ var id = callback ? this.mapId + ':' + this.callbackID++ : null;
+ if (callback) this.callbacks[id] = callback;
+ this.postMessage({ mapId: this.mapId, type: type, id: String(id), data: data }, buffers);
};
/**
diff --git a/js/util/dispatcher.js b/js/util/dispatcher.js
index 7ca9bd7c367..d45a562acc8 100644
--- a/js/util/dispatcher.js
+++ b/js/util/dispatcher.js
@@ -2,7 +2,6 @@
var util = require('./util');
var Actor = require('./actor');
-var WebWorker = require('./web_worker');
module.exports = Dispatcher;
@@ -13,12 +12,15 @@ module.exports = Dispatcher;
* @interface Dispatcher
* @private
*/
-function Dispatcher(length, parent) {
+function Dispatcher(workerPool, parent) {
+ this.workerPool = workerPool;
this.actors = [];
this.currentActor = 0;
- for (var i = 0; i < length; i++) {
- var worker = new WebWorker();
- var actor = new Actor(worker, parent);
+ this.id = util.uniqueId();
+ var workers = this.workerPool.acquire(this.id);
+ for (var i = 0; i < workers.length; i++) {
+ var worker = workers[i];
+ var actor = new Actor(worker, parent, this.id);
actor.name = "Worker " + i;
this.actors.push(actor);
}
@@ -65,9 +67,8 @@ Dispatcher.prototype = {
},
remove: function() {
- for (var i = 0; i < this.actors.length; i++) {
- this.actors[i].target.terminate();
- }
+ this.workerPool.release(this.id);
this.actors = [];
}
};
+
diff --git a/js/util/worker_pool.js b/js/util/worker_pool.js
new file mode 100644
index 00000000000..3ac9e1b0e0f
--- /dev/null
+++ b/js/util/worker_pool.js
@@ -0,0 +1,38 @@
+'use strict';
+
+var assert = require('assert');
+var WebWorker = require('./web_worker');
+var browser = require('./browser');
+
+module.exports = WorkerPool;
+
+WorkerPool.WORKER_COUNT = Math.max(browser.hardwareConcurrency - 1, 1);
+
+function WorkerPool() {
+ this.workerCount = WorkerPool.WORKER_COUNT;
+ this.active = {};
+}
+
+WorkerPool.prototype = {
+ acquire: function (mapId) {
+ if (!this.workers) {
+ assert(typeof this.workerCount === 'number');
+ this.workers = [];
+ while (this.workers.length < this.workerCount) {
+ this.workers.push(new WebWorker());
+ }
+ }
+
+ this.active[mapId] = true;
+ return this.workers.slice();
+ },
+
+ release: function (mapId) {
+ delete this.active[mapId];
+ if (Object.keys(this.active).length === 0) {
+ this.workers.forEach(function (w) { w.terminate(); });
+ this.workers = null;
+ }
+ }
+};
+
diff --git a/test/js/source/worker.test.js b/test/js/source/worker.test.js
index 66d04dcc708..45c6c3946c9 100644
--- a/test/js/source/worker.test.js
+++ b/test/js/source/worker.test.js
@@ -26,7 +26,7 @@ test('before', function(t) {
test('load tile', function(t) {
t.test('calls callback on error', function(t) {
var worker = new Worker(_self);
- worker['load tile']({
+ worker['load tile'](0, {
source: 'source',
uid: 0,
url: 'http://localhost:2900/error'
@@ -42,25 +42,25 @@ test('load tile', function(t) {
test('set layers', function(t) {
var worker = new Worker(_self);
- worker['set layers']([
+ worker['set layers'](0, [
{ id: 'one', type: 'circle', paint: { 'circle-color': 'red' } },
{ id: 'two', type: 'circle', paint: { 'circle-color': 'green' } },
{ id: 'three', ref: 'two', type: 'circle', paint: { 'circle-color': 'blue' } }
]);
- t.equal(worker.layers.one.id, 'one');
- t.equal(worker.layers.two.id, 'two');
- t.equal(worker.layers.three.id, 'three');
+ t.equal(worker.layers[0].one.id, 'one');
+ t.equal(worker.layers[0].two.id, 'two');
+ t.equal(worker.layers[0].three.id, 'three');
- t.equal(worker.layers.one.getPaintProperty('circle-color'), 'red');
- t.equal(worker.layers.two.getPaintProperty('circle-color'), 'green');
- t.equal(worker.layers.three.getPaintProperty('circle-color'), 'blue');
+ t.equal(worker.layers[0].one.getPaintProperty('circle-color'), 'red');
+ t.equal(worker.layers[0].two.getPaintProperty('circle-color'), 'green');
+ t.equal(worker.layers[0].three.getPaintProperty('circle-color'), 'blue');
- t.equal(worker.layerFamilies.one.length, 1);
- t.equal(worker.layerFamilies.one[0].id, 'one');
- t.equal(worker.layerFamilies.two.length, 2);
- t.equal(worker.layerFamilies.two[0].id, 'two');
- t.equal(worker.layerFamilies.two[1].id, 'three');
+ t.equal(worker.layerFamilies[0].one.length, 1);
+ t.equal(worker.layerFamilies[0].one[0].id, 'one');
+ t.equal(worker.layerFamilies[0].two.length, 2);
+ t.equal(worker.layerFamilies[0].two[0].id, 'two');
+ t.equal(worker.layerFamilies[0].two[1].id, 'three');
t.end();
});
@@ -68,21 +68,21 @@ test('set layers', function(t) {
test('update layers', function(t) {
var worker = new Worker(_self);
- worker['set layers']([
+ worker['set layers'](0, [
{ id: 'one', type: 'circle', paint: { 'circle-color': 'red' } },
{ id: 'two', type: 'circle', paint: { 'circle-color': 'green' } },
{ id: 'three', ref: 'two', type: 'circle', paint: { 'circle-color': 'blue' } }
]);
- worker['update layers']({
+ worker['update layers'](0, {
one: { id: 'one', type: 'circle', paint: { 'circle-color': 'cyan' } },
two: { id: 'two', type: 'circle', paint: { 'circle-color': 'magenta' } },
three: { id: 'three', ref: 'two', type: 'circle', paint: { 'circle-color': 'yellow' } }
});
- t.equal(worker.layers.one.getPaintProperty('circle-color'), 'cyan');
- t.equal(worker.layers.two.getPaintProperty('circle-color'), 'magenta');
- t.equal(worker.layers.three.getPaintProperty('circle-color'), 'yellow');
+ t.equal(worker.layers[0].one.getPaintProperty('circle-color'), 'cyan');
+ t.equal(worker.layers[0].two.getPaintProperty('circle-color'), 'magenta');
+ t.equal(worker.layers[0].three.getPaintProperty('circle-color'), 'yellow');
t.end();
});
@@ -96,7 +96,46 @@ test('redo placement', function(t) {
};
});
- worker['redo placement']({type: 'test', mapbox: true});
+ worker['redo placement'](0, {type: 'test', mapbox: true});
+});
+
+test('update layers isolates different instances\' data', function(t) {
+ var worker = new Worker(_self);
+
+ worker['set layers'](0, [
+ { id: 'one', type: 'circle', paint: { 'circle-color': 'red' } },
+ { id: 'two', type: 'circle', paint: { 'circle-color': 'green' } },
+ { id: 'three', ref: 'two', type: 'circle', paint: { 'circle-color': 'blue' } }
+ ]);
+
+ worker['set layers'](1, [
+ { id: 'one', type: 'circle', paint: { 'circle-color': 'red' } },
+ { id: 'two', type: 'circle', paint: { 'circle-color': 'green' } },
+ { id: 'three', ref: 'two', type: 'circle', paint: { 'circle-color': 'blue' } }
+ ]);
+
+ worker['update layers'](1, {
+ one: { id: 'one', type: 'circle', paint: { 'circle-color': 'cyan' } },
+ two: { id: 'two', type: 'circle', paint: { 'circle-color': 'magenta' } },
+ three: { id: 'three', ref: 'two', type: 'circle', paint: { 'circle-color': 'yellow' } }
+ });
+
+ t.equal(worker.layers[0].one.id, 'one');
+ t.equal(worker.layers[0].two.id, 'two');
+ t.equal(worker.layers[0].three.id, 'three');
+
+ t.equal(worker.layers[0].one.getPaintProperty('circle-color'), 'red');
+ t.equal(worker.layers[0].two.getPaintProperty('circle-color'), 'green');
+ t.equal(worker.layers[0].three.getPaintProperty('circle-color'), 'blue');
+
+ t.equal(worker.layerFamilies[0].one.length, 1);
+ t.equal(worker.layerFamilies[0].one[0].id, 'one');
+ t.equal(worker.layerFamilies[0].two.length, 2);
+ t.equal(worker.layerFamilies[0].two[0].id, 'two');
+ t.equal(worker.layerFamilies[0].two[1].id, 'three');
+
+
+ t.end();
});
test('after', function(t) {
diff --git a/test/js/style/style.test.js b/test/js/style/style.test.js
index 44a9890b1e5..cb44c3b1cb2 100644
--- a/test/js/style/style.test.js
+++ b/test/js/style/style.test.js
@@ -131,54 +131,6 @@ test('Style', function(t) {
});
});
- t.test('registers WorkerSource for custom sources', function (t) {
- function MySourceType () {}
- MySourceType.workerSourceURL = 'my-worker-source.js';
- function LaterSourceType () {}
- LaterSourceType.workerSourceURL = 'later-worker-source.js';
- function WorkerlessSourceType () {}
- var _types = { 'my-source-type': MySourceType, 'workerless': WorkerlessSourceType };
-
- var expected = [
- { name: 'my-source-type', url: 'my-worker-source.js' },
- { name: 'later-source-type', url: 'later-worker-source.js' }
- ];
-
- t.plan(2 * expected.length);
-
- function Dispatcher () {}
- Dispatcher.prototype = {
- broadcast: function (type, params, callback) {
- if (type === 'load worker source') {
- var exp = expected.shift();
- t.equal(params.name, exp.name);
- t.equal(params.url, exp.url);
- setTimeout(callback, 0);
- }
- }
- };
-
- var Style = proxyquire('../../../js/style/style', {
- '../source/source': {
- getType: function (name) { return _types[name]; },
- setType: function () {},
- getCustomTypeNames: function () { return Object.keys(_types); },
- on: function (type, handler) {
- if (type === '_add') {
- setTimeout(function () {
- _types['later-source-type'] = LaterSourceType;
- handler({ name: 'later-source-type' });
- });
- }
- },
- off: function () {}
- },
- '../util/dispatcher': Dispatcher
- });
-
- new Style(createStyleJSON());
- });
-
t.end();
});
@@ -1227,9 +1179,56 @@ test('Style#query*Features', function(t) {
t.end();
});
-test('Style creates correct number of workers', function(t) {
- var style = new Style(createStyleJSON(), null, 3);
- t.equal(style.dispatcher.actors.length, 3);
- t.ok(style);
+test('Style#addSourceType', function (t) {
+ var _types = { 'existing': function () {} };
+ var Style = proxyquire('../../../js/style/style', {
+ '../source/source': {
+ getType: function (name) { return _types[name]; },
+ setType: function (name, create) { _types[name] = create; }
+ }
+ });
+
+ t.test('adds factory function', function (t) {
+ var style = new Style(createStyleJSON());
+ var SourceType = function () {};
+
+ // expect no call to load worker source
+ style.dispatcher.broadcast = function (type) {
+ if (type === 'load worker source') {
+ t.fail();
+ }
+ };
+
+ style.addSourceType('foo', SourceType, function () {
+ t.equal(_types['foo'], SourceType);
+ t.end();
+ });
+ });
+
+ t.test('triggers workers to load worker source code', function (t) {
+ var style = new Style(createStyleJSON());
+ var SourceType = function () {};
+ SourceType.workerSourceURL = 'worker-source.js';
+
+ style.dispatcher.broadcast = function (type, params) {
+ if (type === 'load worker source') {
+ t.equal(_types['bar'], SourceType);
+ t.equal(params.name, 'bar');
+ t.equal(params.url, 'worker-source.js');
+ t.end();
+ }
+ };
+
+ style.addSourceType('bar', SourceType, function (err) { t.error(err); });
+ });
+
+ t.test('refuses to add new type over existing name', function (t) {
+ var style = new Style(createStyleJSON());
+ style.addSourceType('existing', function () {}, function (err) {
+ t.ok(err);
+ t.end();
+ });
+ });
+
t.end();
});
diff --git a/test/js/ui/map.test.js b/test/js/ui/map.test.js
index dfe0aad5cd7..b5c16182cf8 100755
--- a/test/js/ui/map.test.js
+++ b/test/js/ui/map.test.js
@@ -6,7 +6,6 @@ var window = require('../../../js/util/browser').window;
var Map = require('../../../js/ui/map');
var Style = require('../../../js/style/style');
var LngLat = require('../../../js/geo/lng_lat');
-var browser = require('../../../js/util/browser');
var sinon = require('sinon');
var proxyquire = require('proxyquire');
@@ -1033,17 +1032,6 @@ test('Map', function(t) {
t.end();
});
- t.test('workerCount option', function(t) {
- var map = createMap({ style: createStyle() });
- t.equal(map.style.dispatcher.actors.length, browser.hardwareConcurrency - 1, 'workerCount defaults to hardwareConcurrency - 1');
- map = createMap({ style: createStyle(), workerCount: 3 });
- t.equal(map.style.dispatcher.actors.length, 3, 'workerCount option is used');
- t.throws(function () {
- createMap({ workerCount: 0 });
- });
- t.end();
- });
-
test('render stabilizes', function (t) {
var style = createStyle();
style.sources.mapbox = {
diff --git a/test/js/util/actor.test.js b/test/js/util/actor.test.js
new file mode 100644
index 00000000000..df02df69c24
--- /dev/null
+++ b/test/js/util/actor.test.js
@@ -0,0 +1,36 @@
+'use strict';
+
+var test = require('tap').test;
+var proxyquire = require('proxyquire');
+var Actor = require('../../../js/util/actor');
+
+test('Actor', function (t) {
+ t.test('forwards resopnses to correct callback', function (t) {
+ var WebWorker = proxyquire('../../../js/util/web_worker', {
+ '../source/worker': function Worker(self) {
+ this.self = self;
+ this.actor = new Actor(self, this);
+ this.test = function (mapId, params, callback) {
+ setTimeout(callback, 0, null, params);
+ };
+ }
+ });
+
+ var worker = new WebWorker();
+
+ var m1 = new Actor(worker, {}, 'map-1');
+ var m2 = new Actor(worker, {}, 'map-2');
+
+ t.plan(4);
+ m1.send('test', { value: 1729 }, function (err, response) {
+ t.error(err);
+ t.same(response, { value: 1729 });
+ });
+ m2.send('test', { value: 4104 }, function (err, response) {
+ t.error(err);
+ t.same(response, { value: 4104 });
+ });
+ });
+
+ t.end();
+});
diff --git a/test/js/util/dispatcher.test.js b/test/js/util/dispatcher.test.js
new file mode 100644
index 00000000000..328ce02a01a
--- /dev/null
+++ b/test/js/util/dispatcher.test.js
@@ -0,0 +1,52 @@
+'use strict';
+
+var test = require('tap').test;
+var proxyquire = require('proxyquire');
+var Dispatcher = require('../../../js/util/dispatcher');
+var WebWorker = require('../../../js/util/web_worker');
+var WorkerPool = require('../../../js/util/worker_pool');
+
+test('Dispatcher', function (t) {
+ t.test('requests and releases workers from pool', function (t) {
+ var dispatcher;
+ var workers = [new WebWorker(), new WebWorker()];
+
+ var releaseCalled = [];
+ var workerPool = {
+ acquire: function () {
+ return workers;
+ },
+ release: function (id) {
+ releaseCalled.push(id);
+ }
+ };
+
+ dispatcher = new Dispatcher(workerPool, {});
+ t.same(dispatcher.actors.map(function (actor) { return actor.target; }), workers);
+ dispatcher.remove();
+ t.equal(dispatcher.actors.length, 0, 'actors discarded');
+ t.same(releaseCalled, [dispatcher.id]);
+
+ t.end();
+ });
+
+ test('creates Actors with unique map id', function (t) {
+ var Dispatcher = proxyquire('../../../js/util/dispatcher', { './actor': Actor });
+
+ var ids = [];
+ function Actor (target, parent, mapId) { ids.push(mapId); }
+
+ var previousWorkerCount = WorkerPool.WORKER_COUNT;
+ WorkerPool.WORKER_COUNT = 1;
+
+ var workerPool = new WorkerPool();
+ var dispatchers = [new Dispatcher(workerPool, {}), new Dispatcher(workerPool, {})];
+ t.same(ids, dispatchers.map(function (d) { return d.id; }));
+
+ WorkerPool.WORKER_COUNT = previousWorkerCount;
+ t.end();
+ });
+
+ t.end();
+});
+
diff --git a/test/js/util/worker_pool.test.js b/test/js/util/worker_pool.test.js
new file mode 100644
index 00000000000..a1a0a9bb8ee
--- /dev/null
+++ b/test/js/util/worker_pool.test.js
@@ -0,0 +1,58 @@
+'use strict';
+
+var test = require('tap').test;
+var proxyquire = require('proxyquire');
+var WorkerPool = require('../../../js/util/worker_pool');
+
+test('WorkerPool', function (t) {
+ t.test('.WORKER_COUNT', function (t) {
+ var WorkerPool = proxyquire('../../../js/util/worker_pool', {
+ './browser': { hardwareConcurrency: 15 }
+ });
+ t.equal(WorkerPool.WORKER_COUNT, 14);
+
+ WorkerPool.WORKER_COUNT = 4;
+ t.end();
+ });
+
+ t.test('#acquire', function (t) {
+ // make sure we're actually creating some workers
+ t.ok(WorkerPool.WORKER_COUNT > 0);
+
+ var pool = new WorkerPool();
+
+ t.notOk(pool.workers);
+ var workers1 = pool.acquire('map-1');
+ var workers2 = pool.acquire('map-2');
+ t.equal(workers1.length, WorkerPool.WORKER_COUNT);
+ t.equal(workers2.length, WorkerPool.WORKER_COUNT);
+
+ // check that the two different dispatchers' workers arrays correspond
+ workers1.forEach(function (w, i) { t.equal(w, workers2[i]); });
+ t.end();
+ });
+
+ t.test('#release', function (t) {
+ var pool = new WorkerPool();
+ pool.acquire('map-1');
+ var workers = pool.acquire('map-2');
+ var terminated = 0;
+ workers.forEach(function (w) {
+ w.terminate = function () { terminated += 1; };
+ });
+
+ pool.release('map-2');
+ t.comment('keeps workers if a dispatcher is still active');
+ t.equal(terminated, 0);
+ t.ok(pool.workers.length > 0);
+
+ t.comment('terminates workers if no dispatchers are active');
+ pool.release('map-1');
+ t.equal(terminated, WorkerPool.WORKER_COUNT);
+ t.notOk(pool.workers);
+
+ t.end();
+ });
+
+ t.end();
+});