Skip to content

Conversation

@bradymadden97
Copy link
Owner

@bradymadden97 bradymadden97 commented Sep 23, 2025

Running some benchmarks:

let X = 10 // 100, 1000, 2000, 3000, 10_000, etc.
export class CrossTileSymbolIndexX extends Benchmark {

    bench() {
        const index = new Index();

        const mainID = new OverscaledTileID(6, 0, 6, 8, 8);
        const childID = new OverscaledTileID(7, 0, 7, 16, 16);

        const mainInstances: any[] = [];
        const childInstances: any[] = [];

        for (let i = 0; i < X; i++) {
            mainInstances.push(makeSymbolInstance(0, 0, ''));
            childInstances.push(makeSymbolInstance(0, 0, ''));
        }
        const mainTile = makeTile(mainID, mainInstances);
        const childTile = makeTile(childID, childInstances);
        index.addLayer(styleLayer as any, [mainTile], 0);
        index.addLayer(styleLayer as any, [childTile], 0);
    }
};
Iterations
Maplibre 5.7 Palantir Internal Fork Maplibre 5.7 + PR Speedup
1k Screenshot 2025-09-23 at 4 06 32 PM Screenshot 2025-09-23 at 4 52 58 PM Screenshot 2025-09-23 at 4 25 17 PM 5.7: 233x

Pal: 20x
2k Screenshot 2025-09-23 at 4 17 36 PM Screenshot 2025-09-23 at 4 54 35 PM Not run, see 3,000 See 3,000
3k Screenshot 2025-09-23 at 4 22 41 PM Screenshot 2025-09-23 at 4 54 51 PM Screenshot 2025-09-23 at 4 24 17 PM 5.7: 661x

Pal: 55x
10k Times out / never finishes Screenshot 2025-09-23 at 4 57 15 PM Screenshot 2025-09-23 at 4 25 24 PM 5.7: n/a

Pal: 111x

Also tested zooming in and out on a map with 20k symbols:

<!DOCTYPE html>
<html lang="en">

<head>
    <title>MapLibre with 10,000 Symbols</title>
    <meta property="og:description" content="MapLibre with 10,000 Symbols." />
    <meta charset='utf-8'>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel='stylesheet' href='../../dist/maplibre-gl.css' />
    <script src='../../dist/maplibre-gl-dev.js'></script>
    <style>
        body {
            margin: 0;
            padding: 0;
        }

        html,
        body,
        #map {
            height: 100%;
        }
    </style>
</head>

<body>
    <div id="map"></div>
    <script>
        const map = new maplibregl.Map({
            container: "map",
            style: 'https://tiles.openfreemap.org/styles/bright',
            center: [-74.5, 40],
            zoom: 2,
        })

        function generateRandomPoints(count, rand) {
            const features = []
            const bounds = {
                west: -75.5 + (rand ? 10 : 0),
                east: -73.5 + (rand ? 10 : 0),
                south: 39.5,
                north: 41.5,
            }

            for (let i = 0; i < count; i++) {
                const lng = bounds.west + Math.random() * (bounds.east - bounds.west)
                const lat = bounds.south + Math.random() * (bounds.north - bounds.south)

                features.push({
                    type: "Feature",
                    geometry: {
                        type: "Point",
                        coordinates: [lng, lat],
                    },
                })
            }

            return {
                type: "FeatureCollection",
                features: features,
            }
        }

        map.on("load", async () => {
            const pointData = generateRandomPoints(10000)
            const pointData2 = generateRandomPoints(10000, true)

            map.addSource("random-points", {
                type: "geojson",
                data: pointData,
            })

            map.addSource("random-points-2", {
                type: "geojson",
                data: pointData2,
            })

            map.addLayer({
                id: "symbols",
                type: "symbol",
                source: "random-points",
                layout: {
                    "icon-image": "marker",
                    "icon-allow-overlap": true,
                },
            })

            map.addLayer({
                id: "symbols-2",
                type: "symbol",
                source: "random-points-2",
                layout: {
                    "icon-image": "marker",
                    "icon-allow-overlap": true,
                },
                paint: {
                    "icon-color": "red",

                }
            })
        })

        // Add geolocate control to the map.
        map.addControl(
            new maplibregl.GeolocateControl({
                positionOptions: {
                    enableHighAccuracy: true
                },
                trackUserLocation: true
            })
        );
    </script>
</body>

</html>

Launch Checklist

  • Confirm your changes do not include backports from Mapbox projects (unless with compliant license) - if you are not sure about this, please ask!
  • Briefly describe the changes in this PR.
  • Link to related issues.
  • Include before/after visuals or gifs if this PR includes visual changes.
  • Write tests for all new functionality.
  • Document any changes to public APIs.
  • Post benchmark scores.
  • Add an entry to CHANGELOG.md under the ## main section.

@bradymadden97 bradymadden97 changed the title opt Optimize cross tile symbol index Sep 23, 2025
for (const [coordinateId, symbolInstancesAtCoordinate] of coordinateMap.entries()) {
const x = coordinateId >>> 16;
const y = coordinateId % (1 << 16);
const indexes = entry.index.range(
Copy link
Owner Author

@bradymadden97 bradymadden97 Sep 23, 2025

Choose a reason for hiding this comment

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

Running the range query on large indexes for N symbol instances was the slow path.

The main optimization here is to instead group all the unique coordinates from the N symbol instances (which is typically significantly less than N), and running the range query once per unique coordinate.

Then try and pair as many of the range query entries with a symbol instance as possible.


for (const [coordinateId, symbolInstancesAtCoordinate] of coordinateMap.entries()) {
const x = coordinateId >>> 16;
const y = coordinateId % (1 << 16);
Copy link

@braeden braeden Sep 24, 2025

Choose a reason for hiding this comment

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

If I'm understanding correctly -- it's probably faster to mask this back out coordinateId & 0xFFFF vs modulo.

Copy link
Owner Author

Choose a reason for hiding this comment

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

Weirdly I tried & 0xFFFF originally and it was slower in profiles. But worth playing around with a few options.

@bradymadden97 bradymadden97 force-pushed the bmadden/cross-tile-symbol-index-optimization branch from 358ed03 to efaaea5 Compare October 31, 2025 17:32
@bradymadden97
Copy link
Owner Author

PR'd against maplibre here: maplibre#6641

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants