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

Upgrade to KDBush v4 & Supercluster v8 for better performance #12682

Merged
merged 2 commits into from
Apr 27, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
7 changes: 4 additions & 3 deletions flow-typed/kdbush.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
// @flow strict
declare module 'kdbush' {
declare export default class KDBush<T> {
points: Array<T>;
constructor(points: Array<T>, getX: (T) => number, getY: (T) => number, nodeSize?: number, arrayType?: Class<$ArrayBufferView>): KDBush<T>;
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<number>;
}
}
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
4 changes: 2 additions & 2 deletions src/style/pauseable_placement.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class LayerPlacement {
_sortAcrossTiles: boolean;
_currentTileIndex: number;
_currentPartIndex: number;
_seenCrossTileIDs: { [string | number]: boolean };
_seenCrossTileIDs: Set<number>;
_bucketParts: Array<BucketPart>;

constructor(styleLayer: SymbolStyleLayer) {
Expand All @@ -25,7 +25,7 @@ class LayerPlacement {

this._currentTileIndex = 0;
this._currentPartIndex = 0;
this._seenCrossTileIDs = {};
this._seenCrossTileIDs = new Set();
this._bucketParts = [];
}

Expand Down
64 changes: 35 additions & 29 deletions src/symbol/cross_tile_symbol_index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -32,56 +31,63 @@ 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<number>;
crossTileIDs: Array<number>;

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<number>) {
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);
if (symbolInstance.crossTileID) {
// 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)
Expand Down Expand Up @@ -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);
}
}
Expand Down
16 changes: 8 additions & 8 deletions src/symbol/placement.js
Original file line number Diff line number Diff line change
Expand Up @@ -388,7 +388,7 @@ export class Placement {
}
}

placeLayerBucketPart(bucketPart: Object, seenCrossTileIDs: { [string | number]: boolean }, showCollisionBoxes: boolean, updateCollisionBoxIfNecessary: boolean) {
placeLayerBucketPart(bucketPart: Object, seenCrossTileIDs: Set<number>, showCollisionBoxes: boolean, updateCollisionBoxIfNecessary: boolean) {

const {
bucket,
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -917,7 +917,7 @@ export class Placement {
}

updateLayerOpacities(styleLayer: StyleLayer, tiles: Array<Tile>) {
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]) {
Expand All @@ -926,7 +926,7 @@ export class Placement {
}
}

updateBucketOpacities(bucket: SymbolBucket, seenCrossTileIDs: { [string | number]: boolean }, collisionBoxArray: ?CollisionBoxArray) {
updateBucketOpacities(bucket: SymbolBucket, seenCrossTileIDs: Set<number>, collisionBoxArray: ?CollisionBoxArray) {
if (bucket.hasTextData()) bucket.text.opacityVertexArray.clear();
if (bucket.hasIconData()) bucket.icon.opacityVertexArray.clear();
if (bucket.hasIconCollisionBoxData()) bucket.iconCollisionBox.collisionVertexArray.clear();
Expand Down Expand Up @@ -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) {
Expand All @@ -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;
Expand Down
18 changes: 9 additions & 9 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yaml 2.1.1 / yarn.lock

Total vulnerabilities: 1

Critical: 0 High: 0 Medium: 1 Low: 0
Vulnerability IDSeverityCVSSFixed inStatus
CVE-2023-2251 MEDIUM MEDIUM 4 2.2.2 Open

mourner marked this conversation as resolved.
Show resolved Hide resolved

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

spdx-license-ids 3.0.12 / yarn.lock

MEDIUM  Noncompliant License (CC0-1.0)

This package contains a license that is not OSI-approved.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

spdx-exceptions 2.3.0 / yarn.lock

MEDIUM  Noncompliant License (CC-BY-3.0)

This package contains a license that is not OSI-approved.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

path-scurry 1.7.0 / yarn.lock

MEDIUM  Noncompliant License (BlueOak-1.0.0)

This package contains a license that is not OSI-approved.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

opener 1.5.2 / yarn.lock

MEDIUM  Noncompliant License (WTFPL)

This package contains a license that is not OSI-approved.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mdn-data 2.0.30 / yarn.lock

MEDIUM  Noncompliant License (CC0-1.0)

This package contains a license that is not OSI-approved.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mdn-data 2.0.28 / yarn.lock

MEDIUM  Noncompliant License (CC0-1.0)

This package contains a license that is not OSI-approved.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

caniuse-lite 1.0.30001406 / yarn.lock

MEDIUM  Noncompliant License (CC-BY-4.0)

This package contains a license that is not OSI-approved.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

caniuse-lite 1.0.30001384 / yarn.lock

MEDIUM  Noncompliant License (CC-BY-4.0)

This package contains a license that is not OSI-approved.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

type-fest 0.8.1 / yarn.lock

MEDIUM  Noncompliant License (CC0-1.0)

This package contains a license that is not OSI-approved.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

type-fest 0.6.0 / yarn.lock

MEDIUM  Noncompliant License (CC0-1.0)

This package contains a license that is not OSI-approved.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

type-fest 0.21.3 / yarn.lock

MEDIUM  Noncompliant License (CC0-1.0)

This package contains a license that is not OSI-approved.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

type-fest 0.20.2 / yarn.lock

MEDIUM  Noncompliant License (CC0-1.0)

This package contains a license that is not OSI-approved.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

type-fest 0.18.1 / yarn.lock

MEDIUM  Noncompliant License (CC0-1.0)

This package contains a license that is not OSI-approved.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

type-fest 0.12.0 / yarn.lock

MEDIUM  Noncompliant License (CC0-1.0)

This package contains a license that is not OSI-approved.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jackspeak 2.1.0 / yarn.lock

MEDIUM  Noncompliant License (BlueOak-1.0.0)

This package contains a license that is not OSI-approved.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

expand-template 2.0.3 / yarn.lock

MEDIUM  Noncompliant License (WTFPL)

This package contains a license that is not OSI-approved.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@csstools/selector-specificity 2.0.2 / yarn.lock

MEDIUM  Noncompliant License (CC0-1.0)

This package contains a license that is not OSI-approved.

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"
Expand Down Expand Up @@ -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"
Expand Down
Loading