From ce7fc2b4e5ca9be812f2cdc49ec250fe374308cd Mon Sep 17 00:00:00 2001 From: Lucas Wojciechowski Date: Wed, 9 Mar 2016 16:58:24 -0800 Subject: [PATCH] Make bucket-side style construction faster MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit … allows for buffer-style mismatches at intermediate zoom levels --- bench/buffer/buffer_benchmark.js | 35 +++++++++--- js/data/bucket.js | 44 ++++++--------- js/data/bucket/symbol_bucket.js | 5 ++ js/render/draw_circle.js | 1 - js/source/geojson_source.js | 2 +- js/source/tile.js | 13 +++-- js/source/vector_tile_source.js | 2 +- js/source/worker.js | 61 +++++++++++++++++++-- js/source/worker_tile.js | 4 +- js/style/style_layer.js | 86 ++++++++++++++++-------------- test/js/data/bucket.test.js | 5 +- test/js/data/symbol_bucket.test.js | 8 ++- test/js/source/worker_tile.test.js | 13 ++--- 13 files changed, 180 insertions(+), 99 deletions(-) diff --git a/bench/buffer/buffer_benchmark.js b/bench/buffer/buffer_benchmark.js index 6d045666b65..9ffce175d71 100644 --- a/bench/buffer/buffer_benchmark.js +++ b/bench/buffer/buffer_benchmark.js @@ -8,6 +8,7 @@ var WorkerTile = require('../../js/source/worker_tile'); var ajax = require('../../js/util/ajax'); var Coordinate = require('../../js/geo/coordinate'); var Style = require('../../js/style/style'); +var StyleLayer = require('../../js/style/style_layer'); var util = require('../../js/util/util'); var Evented = require('../../js/util/evented'); var config = require('../../js/util/config'); @@ -259,13 +260,7 @@ function preloadAssets(stylesheet, callback) { function runSample(stylesheet, getGlyphs, getIcons, getTile, callback) { var timeStart = performance.now(); - var layers = {}; - for (var i = 0; i < stylesheet.layers.length; i++) { - var layer = stylesheet.layers[i]; - if (layer.type === 'fill' || layer.type === 'line' || layer.type === 'circle' || layer.type === 'symbol') { - layers[layer.id] = layer; - } - } + var layers = createLayers(stylesheet); util.asyncAll(coordinates, function(coordinate, eachCallback) { var url = 'https://a.tiles.mapbox.com/v4/mapbox.mapbox-terrain-v2,mapbox.mapbox-streets-v6/' + coordinate.zoom + '/' + coordinate.row + '/' + coordinate.column + '.vector.pbf?access_token=' + config.ACCESS_TOKEN; @@ -318,3 +313,29 @@ function asyncTimesSeries(times, work, callback) { callback(); } } + +function createLayers(stylesheet) { + var layers = {}; + var styleLayers = {}; + + // Filter layers and create an id -> layer map + for (var i = 0; i < stylesheet.layers.length; i++) { + var layer = stylesheet.layers[i]; + if (layer.type === 'fill' || layer.type === 'line' || layer.type === 'circle' || layer.type === 'symbol') { + layers[layer.id] = layer; + if (!layer.ref) { + styleLayers[layer.id] = StyleLayer.create(layer); + } + } + } + + // Create an instance of StyleLayer per layer + for (var id in layers) { + var layer = layers[id]; + if (layer.ref) { + styleLayers[layer.id] = StyleLayer.create(layer, styleLayers[layer.ref]); + } + } + + return styleLayers; +} diff --git a/js/data/bucket.js b/js/data/bucket.js index bcb3300891f..02113dffc2c 100644 --- a/js/data/bucket.js +++ b/js/data/bucket.js @@ -2,8 +2,9 @@ var featureFilter = require('feature-filter'); var Buffer = require('./buffer'); -var StyleLayer = require('../style/style_layer'); var util = require('../util/util'); +var assert = require('assert'); +var StyleLayer = require('../style/style_layer'); module.exports = Bucket; @@ -77,11 +78,14 @@ function Bucket(options) { this.minZoom = this.layer.minzoom; this.maxZoom = this.layer.maxzoom; - // TODO make this call more efficient or unnecessary - this.createStyleLayers(options.style); this.attributes = createAttributes(this); this.attributeMap = createAttributeMap(this); + for (var key in this.childLayers) { + assert(this.childLayers[key] instanceof StyleLayer); + } + assert(this.layer instanceof StyleLayer); + if (options.elementGroups) { this.elementGroups = options.elementGroups; this.buffers = util.mapObject(options.buffers, function(options) { @@ -95,6 +99,7 @@ function Bucket(options) { * @private */ Bucket.prototype.populateBuffers = function() { + this.recalculateStyleLayers(); this.createBuffers(); for (var i = 0; i < this.features.length; i++) { @@ -255,39 +260,18 @@ Bucket.prototype.getBufferName = function(shaderName, type) { Bucket.prototype.serialize = function() { return { - layer: this.layer.serialize(), + layerId: this.layer.id, zoom: this.zoom, elementGroups: this.elementGroups, buffers: util.mapObject(this.buffers, function(buffer) { return buffer.serialize(); }), - childLayers: this.childLayers.map(function(layer) { - return layer.serialize(); + childLayerIds: this.childLayers.map(function(layer) { + return layer.id; }) }; }; -// TODO there will be race conditions when the layer passed here has changed -// since it was used to construct the buffers -Bucket.prototype.createStyleLayers = function(style) { - var that = this; - var refLayer = this.layer = create(this.layer); - this.childLayers = this.childLayers.map(create); - - function create(layer) { - if (style) { - return style.getLayer(layer.id); - } else if (!(layer instanceof StyleLayer)) { - layer = StyleLayer.create(layer, refLayer); - layer.cascade({}, {transition: false}); - layer.recalculate(that.zoom, { lastIntegerZoom: Infinity, lastIntegerZoomTime: 0, lastZoom: 0 }); - return layer; - } else { - return layer; - } - } -}; - // TODO use lazy evaluation to get rid of this call Bucket.prototype.createFilter = function() { if (!this.filter) { @@ -297,6 +281,12 @@ Bucket.prototype.createFilter = function() { Bucket.prototype._premultiplyColor = util.premultiply; +var FAKE_ZOOM_HISTORY = { lastIntegerZoom: Infinity, lastIntegerZoomTime: 0, lastZoom: 0 }; +Bucket.prototype.recalculateStyleLayers = function() { + for (var i = 0; i < this.childLayers.length; i++) { + this.childLayers[i].recalculate(this.zoom, FAKE_ZOOM_HISTORY); + } +}; var createVertexAddMethodCache = {}; function createVertexAddMethod(bucket, interfaceName) { diff --git a/js/data/bucket/symbol_bucket.js b/js/data/bucket/symbol_bucket.js index 818a1e0d1b3..d44a745213a 100644 --- a/js/data/bucket/symbol_bucket.js +++ b/js/data/bucket/symbol_bucket.js @@ -114,6 +114,7 @@ SymbolBucket.prototype.shaderInterfaces = { }; SymbolBucket.prototype.populateBuffers = function(collisionTile, stacks, icons) { + this.recalculateStyleLayers(); this.createBuffers(); // To reduce the number of labels that jump around when zooming we need @@ -330,6 +331,8 @@ SymbolBucket.prototype.anchorIsTooClose = function(text, repeatDistance, anchor) }; SymbolBucket.prototype.placeFeatures = function(collisionTile, collisionDebug) { + this.recalculateStyleLayers(); + // Calculate which labels can be shown and when they can be shown and // create the bufers used for rendering. @@ -478,6 +481,7 @@ SymbolBucket.prototype.addSymbols = function(shaderName, quads, scale, keepUprig }; SymbolBucket.prototype.updateIcons = function(icons) { + this.recalculateStyleLayers(); var iconValue = this.layer.layout['icon-image']; if (!iconValue) return; @@ -489,6 +493,7 @@ SymbolBucket.prototype.updateIcons = function(icons) { }; SymbolBucket.prototype.updateFont = function(stacks) { + this.recalculateStyleLayers(); var fontName = this.layer.layout['text-font'], stack = stacks[fontName] = stacks[fontName] || {}; diff --git a/js/render/draw_circle.js b/js/render/draw_circle.js index f32a9165416..aa8feb0e64a 100644 --- a/js/render/draw_circle.js +++ b/js/render/draw_circle.js @@ -37,7 +37,6 @@ function drawCircles(painter, source, layer, coords) { var tile = source.getTile(coord); var bucket = tile.getBucket(layer); if (!bucket) continue; - bucket.createStyleLayers(painter.style); var elementGroups = bucket.elementGroups.circle; if (!elementGroups) continue; diff --git a/js/source/geojson_source.js b/js/source/geojson_source.js index e6b683f67d2..32f7c07d29a 100644 --- a/js/source/geojson_source.js +++ b/js/source/geojson_source.js @@ -195,7 +195,7 @@ GeoJSONSource.prototype = util.inherit(Evented, /** @lends GeoJSONSource.prototy return; } - tile.loadVectorData(data); + tile.loadVectorData(data, this.map.style); if (tile.redoWhenDone) { tile.redoWhenDone = false; diff --git a/js/source/tile.js b/js/source/tile.js index 7bb9db03189..e7e18083f73 100644 --- a/js/source/tile.js +++ b/js/source/tile.js @@ -68,13 +68,13 @@ Tile.prototype = { * @returns {undefined} * @private */ - loadVectorData: function(data) { + loadVectorData: function(data, style) { this.loaded = true; // empty GeoJSON tile if (!data) return; - this.buckets = unserializeBuckets(data.buckets); + this.buckets = unserializeBuckets(data.buckets, style); }, /** @@ -89,7 +89,7 @@ Tile.prototype = { reloadSymbolData: function(data, painter) { if (this.isUnloaded) return; - var newBuckets = unserializeBuckets(data.buckets); + var newBuckets = unserializeBuckets(data.buckets, painter.style); for (var id in newBuckets) { var newBucket = newBuckets[id]; var oldBucket = this.buckets[id]; @@ -151,10 +151,13 @@ Tile.prototype = { } }; -function unserializeBuckets(input) { +function unserializeBuckets(input, style) { var output = {}; for (var i = 0; i < input.length; i++) { - var bucket = Bucket.create(input[i]); + var bucket = Bucket.create(util.extend({ + childLayers: input[i].childLayerIds.map(style.getLayer.bind(style)), + layer: style.getLayer(input[i].layerId) + }, input[i])); output[bucket.id] = bucket; } return output; diff --git a/js/source/vector_tile_source.js b/js/source/vector_tile_source.js index 79b7b0ce7fd..4bc9e969e7a 100644 --- a/js/source/vector_tile_source.js +++ b/js/source/vector_tile_source.js @@ -88,7 +88,7 @@ VectorTileSource.prototype = util.inherit(Evented, { return; } - tile.loadVectorData(data); + tile.loadVectorData(data, this.map.style); if (tile.redoWhenDone) { tile.redoWhenDone = false; diff --git a/js/source/worker.js b/js/source/worker.js index 78e888ce425..a4125cd36bc 100644 --- a/js/source/worker.js +++ b/js/source/worker.js @@ -2,6 +2,7 @@ var Actor = require('../util/actor'); var WorkerTile = require('./worker_tile'); +var StyleLayer = require('../style/style_layer'); var util = require('../util/util'); var ajax = require('../util/ajax'); var vt = require('vector-tile'); @@ -26,14 +27,33 @@ function Worker(self) { util.extend(Worker.prototype, { 'set layers': function(layers) { - this.layers = {}; - for (var i = 0; i < layers.length; i++) { - this.layers[layers[i].id] = layers[i]; - } + this.layers = unserializeLayers(layers); }, 'update layers': function(layers) { - util.extend(this.layers, layers); + var that = this; + var id; + + // Update ref parents + for (id in layers) { + var layer = layers[id]; + if (layer.ref) updateLayer(layer); + } + + // Update ref children + for (id in layers) { + var layer = layers[id]; + if (!layer.ref) updateLayer(layers[id]); + } + + function updateLayer(layer) { + if (that.layers[layer.id]) { + that.layers[layer.id].set(layer); + } else { + that.layers[layer.id] = StyleLayer.create(layer, that.layers[layer.ref]); + } + that.layers[layer.id].cascade({}, {transition: false}); + } }, 'load tile': function(params, callback) { @@ -166,3 +186,34 @@ util.extend(Worker.prototype, { } } }); + +function unserializeLayers(layersArray) { + var layersMap = {}; + var styleLayersMap = {}; + var layer; + var styleLayer; + + // Filter layers and create an id -> layer map + for (var i = 0; i < layersArray.length; i++) { + layer = layersArray[i]; + if (layer.type === 'fill' || layer.type === 'line' || layer.type === 'circle' || layer.type === 'symbol') { + if (layer.ref) { + layersMap[layer.id] = layer; + } else { + styleLayer = StyleLayer.create(layer); + styleLayer.cascade({}, {transition: false}); + styleLayersMap[layer.id] = styleLayer; + } + } + } + + // Create an instance of StyleLayer per layer + for (var id in layersMap) { + layer = layersMap[id]; + styleLayer = StyleLayer.create(layer, styleLayersMap[layer.ref]); + styleLayer.cascade({}, {transition: false}); + styleLayersMap[layer.id] = styleLayer; + } + + return styleLayersMap; +} diff --git a/js/source/worker_tile.js b/js/source/worker_tile.js index f867ef295d6..6cae3a595bd 100644 --- a/js/source/worker_tile.js +++ b/js/source/worker_tile.js @@ -52,7 +52,7 @@ WorkerTile.prototype.parse = function(data, layers, actor, callback) { bucketsById[layer.id] = bucket; if (data.layers) { // vectortile - sourceLayerId = layer['source-layer']; + sourceLayerId = layer.sourceLayer; bucketsBySourceLayer[sourceLayerId] = bucketsBySourceLayer[sourceLayerId] || {}; bucketsBySourceLayer[sourceLayerId][layer.id] = bucket; } @@ -185,7 +185,7 @@ WorkerTile.prototype.parse = function(data, layers, actor, callback) { callback(null, { buckets: buckets.map(function(bucket) { return bucket.serialize(); }), - bucketStats: stats // TODO put this in a separate message? + bucketStats: stats // TODO put this in a separate message }, getTransferables(buckets)); } }; diff --git a/js/style/style_layer.js b/js/style/style_layer.js index 0765637e57a..9996374598b 100644 --- a/js/style/style_layer.js +++ b/js/style/style_layer.js @@ -25,52 +25,56 @@ StyleLayer.create = function(layer, refLayer) { }; function StyleLayer(layer, refLayer) { - this.id = layer.id; - this.ref = layer.ref; - this.metadata = layer.metadata; - this.type = (refLayer || layer).type; - this.source = (refLayer || layer).source; - this.sourceLayer = (refLayer || layer)['source-layer']; - this.minzoom = (refLayer || layer).minzoom; - this.maxzoom = (refLayer || layer).maxzoom; - this.filter = (refLayer || layer).filter; - this.interactive = (refLayer || layer).interactive; - - this.paint = {}; - this.layout = {}; - - this._paintSpecifications = styleSpec['paint_' + this.type]; - this._layoutSpecifications = styleSpec['layout_' + this.type]; - - this._paintTransitions = {}; // {[propertyName]: StyleTransition} - this._paintTransitionOptions = {}; // {[className]: {[propertyName]: { duration:Number, delay:Number }}} - this._paintDeclarations = {}; // {[className]: {[propertyName]: StyleDeclaration}} - this._layoutDeclarations = {}; // {[propertyName]: StyleDeclaration} - - // Resolve paint declarations - for (var key in layer) { - var match = key.match(/^paint(?:\.(.*))?$/); - if (match) { - var klass = match[1] || ''; - for (var paintName in layer[key]) { - this.setPaintProperty(paintName, layer[key][paintName], klass); + this.set(layer, refLayer); +} + +StyleLayer.prototype = util.inherit(Evented, { + + set: function(layer, refLayer) { + this.id = layer.id; + this.ref = layer.ref; + this.metadata = layer.metadata; + this.type = (refLayer || layer).type; + this.source = (refLayer || layer).source; + this.sourceLayer = (refLayer || layer)['source-layer']; + this.minzoom = (refLayer || layer).minzoom; + this.maxzoom = (refLayer || layer).maxzoom; + this.filter = (refLayer || layer).filter; + this.interactive = (refLayer || layer).interactive; + + this.paint = {}; + this.layout = {}; + + this._paintSpecifications = styleSpec['paint_' + this.type]; + this._layoutSpecifications = styleSpec['layout_' + this.type]; + + this._paintTransitions = {}; // {[propertyName]: StyleTransition} + this._paintTransitionOptions = {}; // {[className]: {[propertyName]: { duration:Number, delay:Number }}} + this._paintDeclarations = {}; // {[className]: {[propertyName]: StyleDeclaration}} + this._layoutDeclarations = {}; // {[propertyName]: StyleDeclaration} + + // Resolve paint declarations + for (var key in layer) { + var match = key.match(/^paint(?:\.(.*))?$/); + if (match) { + var klass = match[1] || ''; + for (var paintName in layer[key]) { + this.setPaintProperty(paintName, layer[key][paintName], klass); + } } } - } - // Resolve layout declarations - if (this.ref) { - this._layoutDeclarations = refLayer._layoutDeclarations; - } else { - for (var layoutName in layer.layout) { - this.setLayoutProperty(layoutName, layer.layout[layoutName]); + // Resolve layout declarations + if (this.ref) { + this._layoutDeclarations = refLayer._layoutDeclarations; + } else { + for (var layoutName in layer.layout) { + this.setLayoutProperty(layoutName, layer.layout[layoutName]); + } } - } - - this.recalculateStatic(); -} -StyleLayer.prototype = util.inherit(Evented, { + this.recalculateStatic(); + }, setLayoutProperty: function(name, value) { diff --git a/test/js/data/bucket.test.js b/test/js/data/bucket.test.js index 44f3dab83b0..da0f14f1d55 100644 --- a/test/js/data/bucket.test.js +++ b/test/js/data/bucket.test.js @@ -4,6 +4,7 @@ var test = require('prova'); var Buffer = require('../../../js/data/buffer'); var Bucket = require('../../../js/data/bucket'); var util = require('../../../js/util/util'); +var StyleLayer = require('../../../js/style/style_layer'); test('Bucket', function(t) { @@ -57,8 +58,8 @@ test('Bucket', function(t) { function create() { var Class = createClass(); return new Class({ - layer: { id: 'layerid', type: 'circle' }, - childLayers: [{ id: 'layerid', type: 'circle' }], + layer: new StyleLayer({ id: 'layerid', type: 'circle' }), + childLayers: [new StyleLayer({ id: 'layerid', type: 'circle' })], buffers: {} }); } diff --git a/test/js/data/symbol_bucket.test.js b/test/js/data/symbol_bucket.test.js index 8b0c3a9bb4d..30c02b93ab1 100644 --- a/test/js/data/symbol_bucket.test.js +++ b/test/js/data/symbol_bucket.test.js @@ -8,6 +8,7 @@ var VectorTile = require('vector-tile').VectorTile; var SymbolBucket = require('../../../js/data/bucket/symbol_bucket'); var Collision = require('../../../js/symbol/collision_tile'); var GlyphAtlas = require('../../../js/symbol/glyph_atlas'); +var StyleLayer = require('../../../js/style/style_layer'); // Load a point feature from fixture tile. var vt = new VectorTile(new Protobuf(new Uint8Array(fs.readFileSync(path.join(__dirname, '/../../fixtures/mbsv5-6-18-23.vector.pbf'))))); @@ -27,7 +28,12 @@ test('SymbolBucket', function(t) { var stacks = { 'Test': glyphs }; function bucketSetup() { - var layer = { id: 'test', type: 'symbol', layout: {'text-font': ['Test'] }}; + var layer = new StyleLayer({ + id: 'test', + type: 'symbol', + layout: { 'text-font': ['Test'] } + }); + var bucket = new SymbolBucket({ buffers: buffers, overscaling: 1, diff --git a/test/js/source/worker_tile.test.js b/test/js/source/worker_tile.test.js index a13733beff2..08119ecc72d 100644 --- a/test/js/source/worker_tile.test.js +++ b/test/js/source/worker_tile.test.js @@ -4,6 +4,7 @@ var test = require('prova'); var WorkerTile = require('../../../js/source/worker_tile'); var Wrapper = require('../../../js/source/geojson_wrapper'); var TileCoord = require('../../../js/source/tile_coord'); +var StyleLayer = require('../../../js/style/style_layer'); test('basic', function(t) { var features = [{ @@ -24,13 +25,13 @@ test('basic', function(t) { t.test('basic worker tile', function(t) { var layers = { - test: { + test: new StyleLayer({ id: 'test', source: 'source', type: 'circle', layout: {}, compare: function () { return true; } - } + }) }; tile.parse(new Wrapper(features), layers, {}, function(err, result) { @@ -42,20 +43,20 @@ test('basic', function(t) { t.test('hidden layers', function(t) { var layers = { - 'test': { + 'test': new StyleLayer({ id: 'test', source: 'source', type: 'circle', layout: {}, compare: function () { return true; } - }, - 'test-hidden': { + }), + 'test-hidden': new StyleLayer({ id: 'test-hidden', source: 'source', type: 'fill', layout: { visibility: 'none' }, compare: function () { return true; } - } + }) }; tile.parse(new Wrapper(features), layers, {}, function(err, result) {