From 29588a7de8df921dfa1555dc197533ac69e32f9c Mon Sep 17 00:00:00 2001 From: Volodymyr Agafonkin Date: Thu, 27 Apr 2023 15:24:30 +0300 Subject: [PATCH] Upgrade to KDBush v4 & Supercluster v8 for better performance (#12682) * upgrade to KDBush v4 & Supercluster v8 * fix symbol unit test --- flow-typed/kdbush.js | 7 +-- package.json | 4 +- src/style/pauseable_placement.js | 4 +- src/symbol/cross_tile_symbol_index.js | 64 +++++++++++++++------------ src/symbol/placement.js | 16 +++---- test/unit/data/symbol_bucket.test.js | 2 +- yarn.lock | 18 ++++---- 7 files changed, 61 insertions(+), 54 deletions(-) diff --git a/flow-typed/kdbush.js b/flow-typed/kdbush.js index c243f228402..085613eac53 100644 --- a/flow-typed/kdbush.js +++ b/flow-typed/kdbush.js @@ -1,8 +1,9 @@ // @flow strict declare module 'kdbush' { - declare export default class KDBush { - points: Array; - constructor(points: Array, getX: (T) => number, getY: (T) => number, nodeSize?: number, arrayType?: Class<$ArrayBufferView>): KDBush; + declare export default class KDBush { + constructor(numPoints: number, nodeSize?: number, arrayType?: Class<$ArrayBufferView>): KDBush; + add(x: number, y: number): number; + finish(): void; range(minX: number, minY: number, maxX: number, maxY: number): Array; } } diff --git a/package.json b/package.json index 3ad3488b306..72767691a05 100644 --- a/package.json +++ b/package.json @@ -24,13 +24,13 @@ "geojson-vt": "^3.2.1", "gl-matrix": "^3.4.3", "grid-index": "^1.1.0", - "kdbush": "^3.0.0", + "kdbush": "^4.0.1", "murmurhash-js": "^1.0.0", "pbf": "^3.2.1", "potpack": "^2.0.0", "quickselect": "^2.0.0", "rw": "^1.3.3", - "supercluster": "^7.1.5", + "supercluster": "^8.0.0", "tinyqueue": "^2.0.3", "vt-pbf": "^3.1.3" }, diff --git a/src/style/pauseable_placement.js b/src/style/pauseable_placement.js index f5a4c711109..53a44dba0ba 100644 --- a/src/style/pauseable_placement.js +++ b/src/style/pauseable_placement.js @@ -16,7 +16,7 @@ class LayerPlacement { _sortAcrossTiles: boolean; _currentTileIndex: number; _currentPartIndex: number; - _seenCrossTileIDs: { [string | number]: boolean }; + _seenCrossTileIDs: Set; _bucketParts: Array; constructor(styleLayer: SymbolStyleLayer) { @@ -25,7 +25,7 @@ class LayerPlacement { this._currentTileIndex = 0; this._currentPartIndex = 0; - this._seenCrossTileIDs = {}; + this._seenCrossTileIDs = new Set(); this._bucketParts = []; } diff --git a/src/symbol/cross_tile_symbol_index.js b/src/symbol/cross_tile_symbol_index.js index 6e13907bec0..64c25062107 100644 --- a/src/symbol/cross_tile_symbol_index.js +++ b/src/symbol/cross_tile_symbol_index.js @@ -6,7 +6,6 @@ import {SymbolInstanceArray} from '../data/array_types.js'; import KDBush from 'kdbush'; import type Projection from '../geo/projection/projection.js'; -import type {SymbolInstance} from '../data/array_types.js'; import type {OverscaledTileID} from '../source/tile_id.js'; import type SymbolBucket from '../data/bucket/symbol_bucket.js'; import type StyleLayer from '../style/style_layer.js'; @@ -32,40 +31,46 @@ const roundingFactor = 512 / EXTENT / 2; class TileLayerIndex { tileID: OverscaledTileID; bucketInstanceId: number; - index: KDBush<{x: number, y: number, key: number, crossTileID: number}>; + index: KDBush; + keys: Array; + crossTileIDs: Array; constructor(tileID: OverscaledTileID, symbolInstances: SymbolInstanceArray, bucketInstanceId: number) { this.tileID = tileID; this.bucketInstanceId = bucketInstanceId; - const coords = []; - for (let i = 0; i < symbolInstances.length; i++) { - const symbolInstance = symbolInstances.get(i); - const {key, crossTileID} = symbolInstance; - const {x, y} = this.getScaledCoordinates(symbolInstance, tileID); - coords.push({x, y, key, crossTileID}); - } + // create a spatial index for deduplicating symbol instances; // use a low nodeSize because we're optimizing for search performance, not indexing - this.index = new KDBush(coords, p => p.x, p => p.y, 16, Int32Array); - } + this.index = new KDBush(symbolInstances.length, 16, Int32Array); + this.keys = []; + this.crossTileIDs = []; + const tx = tileID.canonical.x * EXTENT; + const ty = tileID.canonical.y * EXTENT; - // Converts the coordinates of the input symbol instance into coordinates that be can compared - // against other symbols in this index. Coordinates are: - // (1) world-based (so after conversion the source tile is irrelevant) - // (2) converted to the z-scale of this TileLayerIndex - // (3) down-sampled by "roundingFactor" from tile coordinate precision in order to be - // more tolerant of small differences between tiles. - getScaledCoordinates(symbolInstance: SymbolInstance, childTileID: OverscaledTileID): {|x: number, y: number|} { - const scale = roundingFactor / Math.pow(2, childTileID.canonical.z - this.tileID.canonical.z); - return { - x: Math.floor((childTileID.canonical.x * EXTENT + symbolInstance.tileAnchorX) * scale), - y: Math.floor((childTileID.canonical.y * EXTENT + symbolInstance.tileAnchorY) * scale) - }; + for (let i = 0; i < symbolInstances.length; i++) { + const {key, crossTileID, tileAnchorX, tileAnchorY} = symbolInstances.get(i); + + // Converts the coordinates of the input symbol instance into coordinates that be can compared + // against other symbols in this index. Coordinates are: + // (1) world-based (so after conversion the source tile is irrelevant) + // (2) converted to the z-scale of this TileLayerIndex + // (3) down-sampled by "roundingFactor" from tile coordinate precision in order to be + // more tolerant of small differences between tiles. + const x = Math.floor((tx + tileAnchorX) * roundingFactor); + const y = Math.floor((ty + tileAnchorY) * roundingFactor); + + this.index.add(x, y); + this.keys.push(key); + this.crossTileIDs.push(crossTileID); + } + this.index.finish(); } findMatches(symbolInstances: SymbolInstanceArray, newTileID: OverscaledTileID, zoomCrossTileIDs: Set) { const tolerance = this.tileID.canonical.z < newTileID.canonical.z ? 1 : Math.pow(2, this.tileID.canonical.z - newTileID.canonical.z); - const symbols = this.index.points; + const scale = roundingFactor / Math.pow(2, newTileID.canonical.z - this.tileID.canonical.z); + const tx = newTileID.canonical.x * EXTENT; + const ty = newTileID.canonical.y * EXTENT; for (let i = 0; i < symbolInstances.length; i++) { const symbolInstance = symbolInstances.get(i); @@ -73,15 +78,16 @@ class TileLayerIndex { // already has a match, skip continue; } - - const {x, y} = this.getScaledCoordinates(symbolInstance, newTileID); + const {key, tileAnchorX, tileAnchorY} = symbolInstance; + const x = Math.floor((tx + tileAnchorX) * scale); + const y = Math.floor((ty + tileAnchorY) * scale); // Return any symbol with the same keys whose coordinates are within 1 // grid unit. (with a 4px grid, this covers a 12px by 12px area) const matchedIds = this.index.range(x - tolerance, y - tolerance, x + tolerance, y + tolerance); for (const id of matchedIds) { - const {key, crossTileID} = symbols[id]; - if (key === symbolInstance.key && !zoomCrossTileIDs.has(crossTileID)) { + const crossTileID = this.crossTileIDs[id]; + if (this.keys[id] === key && !zoomCrossTileIDs.has(crossTileID)) { // Once we've marked ourselves duplicate against this parent symbol, // don't let any other symbols at the same zoom level duplicate against // the same parent (see issue #5993) @@ -201,7 +207,7 @@ class CrossTileSymbolLayerIndex { } removeBucketCrossTileIDs(zoom: string | number, removedBucket: TileLayerIndex) { - for (const {crossTileID} of removedBucket.index.points) { + for (const crossTileID of removedBucket.crossTileIDs) { this.usedCrossTileIDs[zoom].delete(crossTileID); } } diff --git a/src/symbol/placement.js b/src/symbol/placement.js index a9e83fd188f..61a45c59142 100644 --- a/src/symbol/placement.js +++ b/src/symbol/placement.js @@ -388,7 +388,7 @@ export class Placement { } } - placeLayerBucketPart(bucketPart: Object, seenCrossTileIDs: { [string | number]: boolean }, showCollisionBoxes: boolean, updateCollisionBoxIfNecessary: boolean) { + placeLayerBucketPart(bucketPart: Object, seenCrossTileIDs: Set, showCollisionBoxes: boolean, updateCollisionBoxIfNecessary: boolean) { const { bucket, @@ -470,12 +470,12 @@ export class Placement { if (shouldClip) { this.placements[crossTileID] = new JointPlacement(false, false, false, true); - seenCrossTileIDs[crossTileID] = true; + seenCrossTileIDs.add(crossTileID); return; } } - if (seenCrossTileIDs[crossTileID]) return; + if (seenCrossTileIDs.has(crossTileID)) return; if (holdingForFade) { // Mark all symbols from this tile as "not placed", but don't add to seenCrossTileIDs, because we don't // know yet if we have a duplicate in a parent tile that _should_ be placed. @@ -790,7 +790,7 @@ export class Placement { alwaysShowIcon = alwaysShowIcon && (notGlobe || !iconOccluded); this.placements[crossTileID] = new JointPlacement(placeText || alwaysShowText, placeIcon || alwaysShowIcon, offscreen || bucket.justReloaded); - seenCrossTileIDs[crossTileID] = true; + seenCrossTileIDs.add(crossTileID); }; if (zOrderByViewportY) { @@ -917,7 +917,7 @@ export class Placement { } updateLayerOpacities(styleLayer: StyleLayer, tiles: Array) { - const seenCrossTileIDs = {}; + const seenCrossTileIDs = new Set(); for (const tile of tiles) { const symbolBucket = ((tile.getBucket(styleLayer): any): SymbolBucket); if (symbolBucket && tile.latestFeatureIndex && styleLayer.id === symbolBucket.layerIds[0]) { @@ -926,7 +926,7 @@ export class Placement { } } - updateBucketOpacities(bucket: SymbolBucket, seenCrossTileIDs: { [string | number]: boolean }, collisionBoxArray: ?CollisionBoxArray) { + updateBucketOpacities(bucket: SymbolBucket, seenCrossTileIDs: Set, collisionBoxArray: ?CollisionBoxArray) { if (bucket.hasTextData()) bucket.text.opacityVertexArray.clear(); if (bucket.hasIconData()) bucket.icon.opacityVertexArray.clear(); if (bucket.hasIconCollisionBoxData()) bucket.iconCollisionBox.collisionVertexArray.clear(); @@ -971,7 +971,7 @@ export class Placement { numIconVertices } = symbolInstance; - const isDuplicate = seenCrossTileIDs[crossTileID]; + const isDuplicate = seenCrossTileIDs.has(crossTileID); let opacityState = this.opacities[crossTileID]; if (isDuplicate) { @@ -982,7 +982,7 @@ export class Placement { this.opacities[crossTileID] = opacityState; } - seenCrossTileIDs[crossTileID] = true; + seenCrossTileIDs.add(crossTileID); const hasText = numHorizontalGlyphVertices > 0 || numVerticalGlyphVertices > 0; const hasIcon = numIconVertices > 0; diff --git a/test/unit/data/symbol_bucket.test.js b/test/unit/data/symbol_bucket.test.js index 4e4ad0e7769..254b7c113a6 100644 --- a/test/unit/data/symbol_bucket.test.js +++ b/test/unit/data/symbol_bucket.test.js @@ -72,7 +72,7 @@ test('SymbolBucket', (t) => { const parts = []; placement.getBucketParts(parts, layer, tile, false); for (const part of parts) { - placement.placeLayerBucketPart(part, {}, false); + placement.placeLayerBucketPart(part, new Set(), false); } }; const a = placement.collisionIndex.grid.keysLength(); diff --git a/yarn.lock b/yarn.lock index 0e0250a1747..3c19fc4e56f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4735,10 +4735,10 @@ just-extend@^4.0.2: resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-4.2.1.tgz#ef5e589afb61e5d66b24eca749409a8939a8c744" integrity sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg== -kdbush@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/kdbush/-/kdbush-3.0.0.tgz#f8484794d47004cc2d85ed3a79353dbe0abc2bf0" - integrity sha512-hRkd6/XW4HTsA9vjVpY9tuXJYLSlelnkTmVFu4M9/7MIYQtFcHpbugAU7UbOfjOiVSVYl2fqgBuJ32JUmRo5Ew== +kdbush@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/kdbush/-/kdbush-4.0.1.tgz#c411c5571d246c7f012ea5980ea720023d663329" + integrity sha512-RlSWHFOf40nU5Be8dZIzdA8j6Jn9IYskSPSZwkDCooGb16uXeUx/6QZuW+krdEToViiK2OAwfW7QWDHY+kAz0A== kind-of@^6.0.2, kind-of@^6.0.3: version "6.0.3" @@ -7471,12 +7471,12 @@ subarg@^1.0.0: dependencies: minimist "^1.1.0" -supercluster@^7.1.5: - version "7.1.5" - resolved "https://registry.yarnpkg.com/supercluster/-/supercluster-7.1.5.tgz#65a6ce4a037a972767740614c19051b64b8be5a3" - integrity sha512-EulshI3pGUM66o6ZdH3ReiFcvHpM3vAigyK+vcxdjpJyEbIIrtbmBdY23mGgnI24uXiGFvrGq9Gkum/8U7vJWg== +supercluster@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/supercluster/-/supercluster-8.0.0.tgz#021872ea0ad0a4bfaba4be3cd305fd0a510a9670" + integrity sha512-/cyICCWE1LjHwN5vKjBB1OHn7TwSyrnerRSMvcLkDgSIyPLN7KJEqx3upzB3BxK4Efs5JrF37bmqMuaxfH0EmA== dependencies: - kdbush "^3.0.0" + kdbush "^4.0.1" supports-color@^5.3.0: version "5.5.0"