Skip to content

Commit

Permalink
Batch mode for efficiently mutating Style instances
Browse files Browse the repository at this point in the history
Mutation operations on a Style instance are designed to be atomic.
Depending on the operation, there is a significant amount of work that
must be preformed to flush and recreate state. This work is wasted when
multiple mutation methods are invokes sequentially.

Batching these mutations allows deferring this work until the batch is
complete and deduping the work.

    map.batch(function(batch) {
        batch.addLayer(layer1);
        batch.addLayer(layer2);
        ...
        batch.addLayer(layerN);
    });

Methods on the batch object provided to the work function mirror methods
on Map and Style, and include:
- addLayer
- removeLayer
- setPaintProperty
- setLayoutProperty
- setFilter
- addSource
- removeSource

Internally, calls to four methods are deferred:
- style._groupLayers (will only fire once)
- style._broadcastLayers (will only fire once)
- style._reloadSource (will fire once per source)
- style.fire (each call will fire)

A casual benchmark shows the time to add 80 layers dropping from ~800 ms
to ~20 ms when run as a batch.

Issue: #1341
  • Loading branch information
scothis committed Jun 30, 2015
1 parent bebddc6 commit 6628608
Show file tree
Hide file tree
Showing 6 changed files with 318 additions and 99 deletions.
144 changes: 45 additions & 99 deletions js/style/style.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use strict';

var Evented = require('../util/evented');
var Source = require('../source/source');
var styleBatch = require('./style_batch');
var StyleLayer = require('./style_layer');
var ImageSprite = require('./image_sprite');
var GlyphSource = require('../symbol/glyph_source');
Expand Down Expand Up @@ -215,28 +215,20 @@ Style.prototype = util.inherit(Evented, {
zh.lastZoom = z;
},

/**
* Apply multiple style mutations in a batch
* @param {function} work Function which accepts the StyleBatch interface
* @private
*/
batch: function(work) {
styleBatch(this, work);
},

addSource: function(id, source) {
if (!this._loaded) {
throw new Error('Style is not done loading');
}
if (this.sources[id] !== undefined) {
throw new Error('There is already a source with this ID');
}
source = Source.create(source);
this.sources[id] = source;
source.id = id;
source.style = this;
source.dispatcher = this.dispatcher;
source.glyphAtlas = this.glyphAtlas;
source
.on('load', this._forwardSourceEvent)
.on('error', this._forwardSourceEvent)
.on('change', this._forwardSourceEvent)
.on('tile.add', this._forwardTileEvent)
.on('tile.load', this._forwardTileEvent)
.on('tile.error', this._forwardTileEvent)
.on('tile.remove', this._forwardTileEvent);
this.fire('source.add', {source: source});
this.batch(function(batch) {
batch.addSource(id, source);
});

return this;
},

Expand All @@ -248,23 +240,10 @@ Style.prototype = util.inherit(Evented, {
* @private
*/
removeSource: function(id) {
if (!this._loaded) {
throw new Error('Style is not done loading');
}
if (this.sources[id] === undefined) {
throw new Error('There is no source with this ID');
}
var source = this.sources[id];
delete this.sources[id];
source
.off('load', this._forwardSourceEvent)
.off('error', this._forwardSourceEvent)
.off('change', this._forwardSourceEvent)
.off('tile.add', this._forwardTileEvent)
.off('tile.load', this._forwardTileEvent)
.off('tile.error', this._forwardTileEvent)
.off('tile.remove', this._forwardTileEvent);
this.fire('source.remove', {source: source});
this.batch(function(batch) {
batch.removeSource(id);
});

return this;
},

Expand All @@ -288,26 +267,10 @@ Style.prototype = util.inherit(Evented, {
* @private
*/
addLayer: function(layer, before) {
if (!this._loaded) {
throw new Error('Style is not done loading');
}
if (this._layers[layer.id] !== undefined) {
throw new Error('There is already a layer with this ID');
}
if (!(layer instanceof StyleLayer)) {
layer = new StyleLayer(layer, this.stylesheet.constants || {});
}
this._layers[layer.id] = layer;
this._order.splice(before ? this._order.indexOf(before) : Infinity, 0, layer.id);
layer.resolveLayout();
layer.resolveReference(this._layers);
layer.resolvePaint();
this._groupLayers();
this._broadcastLayers();
if (layer.source) {
this.sources[layer.source].reload();
}
this.fire('layer.add', {layer: layer});
this.batch(function(batch) {
batch.addLayer(layer, before);
});

return this;
},

Expand All @@ -319,23 +282,10 @@ Style.prototype = util.inherit(Evented, {
* @private
*/
removeLayer: function(id) {
if (!this._loaded) {
throw new Error('Style is not done loading');
}
var layer = this._layers[id];
if (layer === undefined) {
throw new Error('There is no layer with this ID');
}
for (var i in this._layers) {
if (this._layers[i].ref === id) {
this.removeLayer(i);
}
}
delete this._layers[id];
this._order.splice(this._order.indexOf(id), 1);
this._groupLayers();
this._broadcastLayers();
this.fire('layer.remove', {layer: layer});
this.batch(function(batch) {
batch.removeLayer(id);
});

return this;
},

Expand Down Expand Up @@ -366,14 +316,11 @@ Style.prototype = util.inherit(Evented, {
},

setFilter: function(layer, filter) {
if (!this._loaded) {
throw new Error('Style is not done loading');
}
layer = this.getReferentLayer(layer);
layer.filter = filter;
this._broadcastLayers();
this.sources[layer.source].reload();
this.fire('change');
this.batch(function(batch) {
batch.setFilter(layer, filter);
});

return this;
},

/**
Expand All @@ -387,16 +334,11 @@ Style.prototype = util.inherit(Evented, {
},

setLayoutProperty: function(layer, name, value) {
if (!this._loaded) {
throw new Error('Style is not done loading');
}
layer = this.getReferentLayer(layer);
layer.setLayoutProperty(name, value);
this._broadcastLayers();
if (layer.source) {
this.sources[layer.source].reload();
}
this.fire('change');
this.batch(function(batch) {
batch.setLayoutProperty(layer, name, value);
});

return this;
},

/**
Expand All @@ -411,11 +353,11 @@ Style.prototype = util.inherit(Evented, {
},

setPaintProperty: function(layer, name, value, klass) {
if (!this._loaded) {
throw new Error('Style is not done loading');
}
this.getLayer(layer).setPaintProperty(name, value, klass);
this.fire('change');
this.batch(function(batch) {
batch.setPaintProperty(layer, name, value, klass);
});

return this;
},

getPaintProperty: function(layer, name, klass) {
Expand Down Expand Up @@ -455,6 +397,10 @@ Style.prototype = util.inherit(Evented, {
this.dispatcher.remove();
},

_reloadSource: function(id) {
this.sources[id].reload();
},

_updateSources: function(transform) {
for (var id in this.sources) {
this.sources[id].update(transform);
Expand Down
170 changes: 170 additions & 0 deletions js/style/style_batch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
'use strict';

var Source = require('../source/source');
var StyleLayer = require('./style_layer');

function styleBatch(style, work) {
if (!style._loaded) {
throw new Error('Style is not done loading');
}

var batch = Object.create(styleBatch.prototype);

batch._style = style;
batch._defer = {
groupLayers: false,
broadcastLayers: false,
sources: {},
events: []
};

work(batch);

// call once if called
if (batch._defer.groupLayers) batch._style._groupLayers();
if (batch._defer.broadcastLayers) batch._style._broadcastLayers();

// reload sources
Object.keys(batch._defer.sources).forEach(function(sourceId) {
batch._style._reloadSource(sourceId);
});

// re-fire events
batch._defer.events.forEach(function(args) {
batch._style.fire.apply(batch._style, args);
});
}

styleBatch.prototype = {

addLayer: function(layer, before) {
if (this._style._layers[layer.id] !== undefined) {
throw new Error('There is already a layer with this ID');
}
if (!(layer instanceof StyleLayer)) {
layer = new StyleLayer(layer, this._style.stylesheet.constants || {});
}
this._style._layers[layer.id] = layer;
this._style._order.splice(before ? this._style._order.indexOf(before) : Infinity, 0, layer.id);
layer.resolveLayout();
layer.resolveReference(this._style._layers);
layer.resolvePaint();
this._groupLayers();
this._broadcastLayers();
if (layer.source) {
this._reloadSource(layer.source);
}
this.fire('layer.add', {layer: layer});

return this;
},

removeLayer: function(id) {
var layer = this._style._layers[id];
if (layer === undefined) {
throw new Error('There is no layer with this ID');
}
for (var i in this._style._layers) {
if (this._style._layers[i].ref === id) {
this.removeLayer(i);
}
}
delete this._style._layers[id];
this._style._order.splice(this._style._order.indexOf(id), 1);
this._groupLayers();
this._broadcastLayers();
this.fire('layer.remove', {layer: layer});

return this;
},

setPaintProperty: function(layer, name, value, klass) {
this._style.getLayer(layer).setPaintProperty(name, value, klass);
this.fire('change');

return this;
},

setLayoutProperty: function(layer, name, value) {
layer = this._style.getReferentLayer(layer);
layer.setLayoutProperty(name, value);
this._broadcastLayers();
if (layer.source) {
this._reloadSource(layer.source);
}
this.fire('change');

return this;
},

setFilter: function(layer, filter) {
layer = this._style.getReferentLayer(layer);
layer.filter = filter;
this._broadcastLayers();
this._reloadSource(layer.source);
this.fire('change');

return this;
},

addSource: function(id, source) {
if (!this._style._loaded) {
throw new Error('Style is not done loading');
}
if (this._style.sources[id] !== undefined) {
throw new Error('There is already a source with this ID');
}
source = Source.create(source);
this._style.sources[id] = source;
source.id = id;
source.style = this._style;
source.dispatcher = this._style.dispatcher;
source.glyphAtlas = this._style.glyphAtlas;
source
.on('load', this._style._forwardSourceEvent)
.on('error', this._style._forwardSourceEvent)
.on('change', this._style._forwardSourceEvent)
.on('tile.add', this._style._forwardTileEvent)
.on('tile.load', this._style._forwardTileEvent)
.on('tile.error', this._style._forwardTileEvent)
.on('tile.remove', this._style._forwardTileEvent);
this.fire('source.add', {source: source});

return this;
},

removeSource: function(id) {
if (this._style.sources[id] === undefined) {
throw new Error('There is no source with this ID');
}
var source = this._style.sources[id];
delete this._style.sources[id];
source
.off('load', this._style._forwardSourceEvent)
.off('error', this._style._forwardSourceEvent)
.off('change', this._style._forwardSourceEvent)
.off('tile.add', this._style._forwardTileEvent)
.off('tile.load', this._style._forwardTileEvent)
.off('tile.error', this._style._forwardTileEvent)
.off('tile.remove', this._style._forwardTileEvent);
this.fire('source.remove', {source: source});

return this;
},

_groupLayers: function() {
this._defer.groupLayers = true;
},
_broadcastLayers: function() {
this._defer.broadcastLayers = true;
},
_reloadSource: function(sourceId) {
this._defer.sources[sourceId] = true;
},
fire: function() {
this._defer.events.push(arguments);
}

};

module.exports = styleBatch;
Loading

0 comments on commit 6628608

Please sign in to comment.