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

Avoid sorting of features if the sort-key is constant #78

Merged
merged 3 commits into from
Mar 30, 2021

Conversation

JannikGM
Copy link
Contributor

This addresses 2 related bugs:

  1. An unevaluated mapbox expression-object was used in a condition (if (expression)) to decide if sorting was necessary, the assumption was probably that the default constant value of undefined would skip sorting. However, the unevaluated mapbox expression is not the value undefined, but an expression-object describing the value and its type. So, even if the mapbox expression was a constant undefined, the check would never be true. This leads to a lot of unnecessary sorting.
    This was already correct for the drawing sort in circles and symbols, and this is where the sortFeaturesByKey variable name originates.
  2. Even where the check was correct, it would explicitly check if the sort-key was a constant with value undefined to skip sorting. However, we also know that the sort-key is irrelevant if it's any constant, not just undefined.

Note that the second optimization would not be possible if cross-layer sorting was implemented (see mapbox/mapbox-gl-js#1349).

I'm unable to provide good performance benchmarks because of #76.

@github-actions
Copy link
Contributor

github-actions bot commented Feb 12, 2021

Bundle size report:

Size Change: +32 B
Total Size Before: 205 kB
Total Size After: 205 kB

Output file Before After Change
maplibre-gl.js 196 kB 196 kB +32 B
maplibre-gl.css 8.8 kB 8.8 kB 0 B
ℹ️ View Details
Source file Before After Change
src/data/bucket/circle_bucket.js 1.12 kB 1.15 kB +33 B
src/data/bucket/fill_bucket.js 1.19 kB 1.22 kB +26 B
src/data/bucket/line_bucket.js 2.53 kB 2.54 kB +12 B
src/render/draw_symbol.js 2.51 kB 2.52 kB +7 B
src/data/bucket/symbol_bucket.js 4.57 kB 4.57 kB -5 B
src/style/pauseable_placement.js 620 B 614 B -6 B

@@ -93,7 +95,8 @@ class CircleBucket<Layer: CircleStyleLayer | HeatmapStyleLayer> implements Bucke

if (!this.layers[0]._featureFilter.filter(new EvaluationParameters(this.zoom), evaluationFeature, canonical)) continue;

const sortKey = circleSortKey ?
const sortKey = sortFeaturesByKey ?
// $FlowFixMe
Copy link
Contributor Author

Choose a reason for hiding this comment

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

nit: Minor annoyance, because flow does not seem to understand that circleSortKey.evaluate can never happen while circleSortKey is null, because sortFeaturesByKey can only be true if circleSortKey was overwritten earlier.

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe circleSortKey && sortFeaturesByKey will do the trick?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It would probably work, but I don't think it helps code readability. Shall I still do it?

Copy link
Contributor

@Joxit Joxit left a comment

Choose a reason for hiding this comment

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

LGTM

@klokan
Copy link
Member

klokan commented Mar 26, 2021

Could you please prepare a test expression (in JSON GL Style?) which is failing here - to clearly demonstrating the bug, please?
Has the bug been reported also on Mapbox repo?

Would it be possible to introduce a test into CI for this use case - to avoid regression in future?

I expect the same problem does not happen GL Native - as all of this is very much related to JavaScript handling of true/false/undefined - but in case there is any relation - please make a ticket proposing fix also into C/C++ parsing code.

@JannikGM
Copy link
Contributor Author

JannikGM commented Mar 28, 2021

Could you please prepare a test expression (in JSON GL Style?) which is failing here - to clearly demonstrating the bug, please?

It's not a bug with any expression in particular, but a logic and performance issue.

We found this issue when we set a sort-key and then wanted to "unset" the sort-key to go back to standard sorting.
We figured we'd have to set it to 0, false, null or undefined - but we saw in our debuggers that mapbox kept sorting features by sort-key regardless.

Even if you just use mapbox normally, it will sort most features by sort-key, even though they all have the same sort-key of undefined.
The condition to disable the sorting for this case was broken; see "1." in PR description.
Because the sort priority is the same for all features, the result of the sort is also useless; see "2." in PR description.

mapbox sorts once while placing objects (in worker), and then (for some features) again while rendering (main-thread).
So this affects CPU usage and performance (also see meta-issue #96).

It's much easier to see this bug in the debugger; but here's a hook so you can spy on all the sorting that maplibre does (without this PR).
With this PR, you should not see any sorting, unless you set demo = 0.

<!DOCTYPE html>
<html>
<head>
  <title>Mapbox GL JS debug page</title>
  <meta charset='utf-8'>
  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
  <link rel='stylesheet' href='http://localhost:9966/dist/maplibre-gl.css' />
  <style>
    body { margin: 0; padding: 0; }
    html, body, #map { height: 100%; }
  </style>
</head>

<body>
<div id='map'></div>

<!--
  Expects that you'll be running maplibre with `yarn run start-debug`.
-->
<script src='http://localhost:9966/dist/maplibre-gl-dev.js'></script>
<script src='http://localhost:9966/debug/access_token_generated.js'></script>

<!--
  We need some hooks in the worker, so here are some helpers to hook them.
  URL will probably change in the future! Right now it's https://gist.github.com/JannikGM/5832e7987499f640c4fb351cbe643883
-->
<script src="https://gist.githack.com/JannikGM/5832e7987499f640c4fb351cbe643883/raw/WorkerHook.js"></script>
<script src="https://gist.githack.com/JannikGM/5832e7987499f640c4fb351cbe643883/raw/WorkerHook-console.js"></script>

<script>


  // Install a hook to report new workers
  workerRun(function() {
    console.log(`Worker created!`);
  });

  // Install a hook to report `Array.sort` in worker
  workerRun(function() {
    const Array_prototype_sort = self.Array.prototype.sort;
    self.Array.prototype.sort = function(...args) {
      if (this[0] && typeof this[0] === 'object' && 'sortKey' in this[0]) {
        console.log(`Worker sorting: ${this.length} items (${this.map((e) => String(e.sortKey))})`);
      }
      return Array_prototype_sort.call(this, ...args);
    };
  });

  // Install a hook to report `Array.sort` on main-thread
  const Array_prototype_sort = self.Array.prototype.sort;
  self.Array.prototype.sort = function(...args) {
    if (this[0] && typeof this[0] === 'object' && 'sortKey' in this[0]) {
      console.log(`Main-Thread sorting: ${this.length} items (${this.map((e) => String(e.sortKey))})`);
    }
    return Array_prototype_sort.call(this, ...args);
  };



  var map = new maplibregl.Map({
    container: 'map',
    style: 'mapbox://styles/mapbox/streets-v11',
    center: [-98, 38.88],
    zoom: 3
  });

  map.on('load', function () {

    // !!! Configure this !!!
    // - 'symbol' sorts in worker and main-thread
    // - 'circle' sorts in worker and main-thread
    // - 'line' sorts in worker only
    // - 'fill' sorts in worker only
    // You will also see some sorting from non-selected types, because of a mapbox bug where `undefined` still sorts.
    const type = 'symbol';
    const demo = 2;

    // Find any matching layer
    var layers = map.getStyle().layers;
    var layerId;
    for (var i = 0; i < layers.length; i++) {
      const layer = layers[i];
      if (layer.type === type) {

        // If the sort-key is constant (= same for all features), then all features have the same sort priority.
        // Therefore, none of these demos (except 0) should require sorting.
        // Because of a mapbox bug, *all* of these are sorting most features.
        // Only exception: This will *not* happen for circles and symbols with `undefined` sort-key: bug not present there!

        if (demo === 0) {
          // This will sort with sort-key (non-constant, so actually requires sorting)
          map.setLayoutProperty(layer.id, `${type}-sort-key`, ["length", ["get", "name"]]);
        } else if (demo === 1) {
          // This will sort with sort-key (constant undefined)
          // So even if you don't do anything, it will sort
        } else if (demo === 2) {
          // This will sort with sort-key (constant 5)
          // So even if you misconfigure your sort-key, it will sort
          map.setLayoutProperty(layer.id, `${type}-sort-key`, 5);
        } else if (demo === 3) {
          // This will sort with sort-key (constant undefined)
          // So even if you try to remove your sort-key, it will sort
          map.setLayoutProperty(layer.id, `${type}-sort-key`, 5);
          map.setLayoutProperty(layer.id, `${type}-sort-key`, undefined);
        }

      }
    }

  });
</script>
</body>
</html>

Has the bug been reported also on Mapbox repo?

At least not by me / us. I doubt it's reported yet.

However, I'm not sure if I'll be allowed to do this, because mapbox has recently become a competitor in the field that our company works in; my contract actually forbids this. I'll have to discuss this with my employer.

We also tried to upstream different changes to mapbox last year (before they were a competitor) and it was wasting too much of our time (no/slow/unsatisfactory review).
We still discussed it briefly again (before finding out they were a competitor now), but primarily because of equally slow PR response by maplibre (also see my criticism here: #102 (comment)) and us wanting to have a community to (quickly) review and test our changes on a larger scale before deployment in our products.

Would it be possible to introduce a test into CI for this use case - to avoid regression in future?

Probably very difficult because it's not necessarily triggered by incorrect API use.
There also shouldn't be any visual impact.

Bug "1." was a trivial coding error; To avoid new errors like this, we need better review to catch logic errors.
More type-safety would have prevented it, too. Considering TypeScript or AssemblyScript could be an option (in my opinion). We also ported our own projects from Flow to TypeScript and it revealed many new issues.

Bug "2." is a logic error / oversight, where they didn't realize that it doesn't matter what the sort-key is, if it's a constant value that's the same for all features.
Better performance related tooling (WebGL / workers / case-specific) to spot unnecessary workloads could have prevented this (also see #76 + consider ecosystem work in Chrome or spector.js).

I did briefly talk about my mapbox-gl-js-debugger bookmarklet on Slack already, which can assist with finding such issues (by making it easier to add hooks to production websites).

I expect the same problem does not happen GL Native - as all of this is very much related to JavaScript handling of true/false/undefined - [...]

I could imagine that the bug also exists there.
What you are describing only affects "1." from PR description, but not "2." (the logic error).

[...] but in case there is any relation - please make a ticket proposing fix also into C/C++ parsing code.

Our company doesn't currently use maplibre-gl-native (so I can't allocate time to it), and most of our performance issues are in maplibre-gl-js.
I'll likely fix or report it in maplibre-gl-native when we start using it (which could be months away).

Someone else should look into it or create an issue if it's important.
Edit: I created an issue in maplibre/maplibre-native#57, but we probably won't fix it anytime soon

Copy link
Member

@nyurik nyurik left a comment

Choose a reason for hiding this comment

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

Looks good from my non-expert-in-this-feature perspective. Please add some text to the changelog so that it will be included in the release notes. Also, do you think it would make sense to add/modify a unit test, or is this just for performance, which would make it very hard to test for it?

@JannikGM
Copy link
Contributor Author

Please add some text to the changelog so that it will be included in the release notes.

Done in separate commit.
I also rebased to latest origin/main (no conflicts).

Also, do you think it would make sense to add/modify a unit test [...]

No; from my previous response:

Would it be possible to introduce a test into CI for this use case - to avoid regression in future?

Probably very difficult because it's not necessarily triggered by incorrect API use.
There also shouldn't be any visual impact.

I also made recommendations how to avoid such bugs in the future.

[...] or is this just for performance, which would make it very hard to test for it?

Purely performance (and real world benefit is probably small because of much larger bottlenecks elsewhere).

Copy link
Member

@nyurik nyurik left a comment

Choose a reason for hiding this comment

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

looks great, thanks!

@nyurik
Copy link
Member

nyurik commented Mar 30, 2021

@JannikGM I have invited you to the devs group, so you should be able to squash-merge it yourself. Also, make sure to edit the commit message when merging, possibly copying the whole or part of the text from the top of the PR. Thanks!

@nyurik nyurik merged commit ba7bfbc into maplibre:main Mar 30, 2021
@JannikGM JannikGM deleted the sort-fix branch March 31, 2021 08:42
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.

4 participants