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

Optimize and simplify tile retention logic #6995

Merged
merged 8 commits into from
Jul 31, 2018
Merged

Conversation

mourner
Copy link
Member

@mourner mourner commented Jul 20, 2018

While investigating #6643, I noticed that our tile retention logic (and _findLoadedChildren use in particular) was accidentally quadratic. It didn't manifest with vector tiles because they're big and there are only 6–8 at the time, but with small raster tile layers, where there can be 40–80 tiles loading at the same time, it blows up.

This PR brings down children retention CPU time on a test fly animation from the #6643 case down from 812ms to 37ms (from quadratic to linear). This somewhat reduces jank overall (maybe 20%), but there's still a lot of remaining jank which is almost fully dominated by XHR image requests — I'll explore whether we can do something about it in another PR.

Commits in the PR are self-contained so it's easier to review them one by one, and let's rebase-merge, not squash.

Launch Checklist

  • briefly describe the changes in this PR
  • write tests for all new functionality already covered
  • document any changes to public APIs
  • post benchmark scores
  • manually test the debug page

@mourner mourner requested review from ansis and mollymerp July 20, 2018 10:47
@mourner
Copy link
Member Author

mourner commented Jul 20, 2018

Paint benchmark is not affected because it doesn't exercise small tiles use case:

image

@mourner mourner force-pushed the optimize-tile-retention branch from 1bee1cb to 45bbc09 Compare July 23, 2018 20:59
@hyperknot
Copy link

hyperknot commented Jul 24, 2018

I've deployed to production with the 3 raster PRs included, and I'm getting a large number of "Cannot read property 'clearFadeHold' of undefined" exceptions, because of source_cache.js.

https://sentry.io/share/issue/0c5f7a31a923428ca24a43ad1705d14c/

@mourner
Copy link
Member Author

mourner commented Jul 24, 2018

@hyperknot this is likely a bug in #6995 — will investigate.

@hyperknot
Copy link

This is #6995 :-)

@mourner
Copy link
Member Author

mourner commented Jul 24, 2018

@hyperknot pushed a commit that might fix this, can you try it out?

Copy link
Contributor

@mollymerp mollymerp left a comment

Choose a reason for hiding this comment

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

nice catch @mourner! This looks good to me – just a few nits with the comments.

I'm pretty sure native's update_renderables algorithm doesn't have this accidentally quadratic problem because it doesn't use a findLoadedChildren equivalent, but it might be worth refactoring that algorithm to match the structure of this code to improve readability and comparability in the future.

let found = false;

_retainLoadedChildren(
tiles: {[any]: OverscaledTileID},
Copy link
Contributor

Choose a reason for hiding this comment

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

can the any be replaced with number here?

Copy link
Contributor

Choose a reason for hiding this comment

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

also can the name of this argument be more descriptive like idealTiles or something else to avoid confusion with this._tiles?

Copy link
Member Author

Choose a reason for hiding this comment

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

can the any be replaced with number here?

Yes, but I'd leave this for a separate PR, since improving types for all objects that use tile IDs as keys is a pretty big change. Currently it's either any or string | number throughout the code because when you iterate with for (const id in obj), id is always a string, and converting it everywhere is cumbersome.

// loop through ancestors of the topmost loaded child to see if there's one that needed it
let tileID = topmostLoadedID;
while (tileID.overscaledZ > zoom) {
tileID = tileID.scaledTo(tileID.overscaledZ - 1);
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: can you use topmostLoadedID in the conditional and have tileID be a const?

while (topmostLoadedID.overscaledZ > zoom) {
  const tileID = topmostLoadedID.scaledTo(topmostLoadedID.overscaledZ-1);
  ...

Copy link
Member Author

Choose a reason for hiding this comment

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

No, since tileID loops through parents — changing to the above would do a different thing.

Copy link
Contributor

Choose a reason for hiding this comment

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

oh yeah 🤦‍♀️ its a while loop

// If the drawRasterTile has never seen this tile, then
// tile.fadeEndTime may be unset. In that case, or if
// fadeEndTime is in the future, then this tile is still
// fading in. Find tiles to cross-fade with it.
Copy link
Contributor

Choose a reason for hiding this comment

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

can we modify and keep this comment to explain the continue condition below?

Copy link
Member Author

@mourner mourner Jul 25, 2018

Choose a reason for hiding this comment

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

I felt this would be redundant due to the comment after that. So one would say "skip tiles not loaded or already faded in", and another "do X with tiles that are loaded but still fading in".

}
}

// retain any loaded children of ideal riles up to maxCoveringZoom
Copy link
Contributor

Choose a reason for hiding this comment

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

nit : retain any loaded children of missing ideal riles up to maxCoveringZoom

}
}
// As we ascend up the tile pyramid of the ideal tile, we check whether the parent
// tile has been previously requested (and errored in this case due to the previous conditional)
Copy link
Contributor

Choose a reason for hiding this comment

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

the due to the previous conditional part of this comment no longer makes sense here – can change to and we can infer has errored or is loading because tiles with loaded data are skipped at the beginning of this block or something

if (tile.hasData()) continue;

// The tile we require is not yet loaded or does not exist.
// We are now attempting to load a parent tile if children are not enough.
Copy link
Contributor

Choose a reason for hiding this comment

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

This second line comment can be deleted (parent tile loading doesn't start til L614 and is already commented down there

@hyperknot
Copy link

@mourner sure, I'll do a fresh build tomorrow and push it to production and wait a day to see if any errors are appearing.

@hyperknot
Copy link

OK, I deployed a build from bbbea02 and 323aaf0, I'll report about the results in a few days.

@mourner
Copy link
Member Author

mourner commented Jul 30, 2018

@hyperknot how's it going?

@hyperknot
Copy link

The new build is perfect so far, not a single error related to tiles.

@mourner mourner requested a review from jfirebaugh July 30, 2018 19:15
@@ -324,59 +324,64 @@ class SourceCache extends Evented {
}

/**
* Recursively find children of the given tile (up to maxCoveringZoom) that are already loaded;
* adds found tiles to retain object; returns true if any child is found.
* Retain children of the given set of tiles (up to maxCoveringZoom) that are already loaded;
Copy link
Contributor

Choose a reason for hiding this comment

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

Can this mention the zoom parameter too? It seems to be something like:

Retain children of the given set of tiles that are loaded and have a zoom between zoom (exclusive) and maxCoveringZoom (inclusive).

}
}
}
return found;
}

/**
* Find a loaded parent of the given tile (up to minCoveringZoom);
* adds the found tile to retain object and returns the tile if found
Copy link
Contributor

Choose a reason for hiding this comment

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

This clause no longer applies.

@mourner mourner merged commit 7da29b9 into master Jul 31, 2018
@mourner mourner deleted the optimize-tile-retention branch November 2, 2018 09:04
pirxpilot pushed a commit to pirxpilot/mapbox-gl-js that referenced this pull request Jun 14, 2019
* clean up tile retention logic a bit

* simplify retention logic further

* simplify raster tile retaining logic

* avoid delete when retaining child tiles

* much faster children retention logic

* simplify parent tile retention

* address PR feedback

* clear up source cache comments
pirxpilot pushed a commit to pirxpilot/mapbox-gl-js that referenced this pull request Jun 18, 2019
* clean up tile retention logic a bit

* simplify retention logic further

* simplify raster tile retaining logic

* avoid delete when retaining child tiles

* much faster children retention logic

* simplify parent tile retention

* address PR feedback

* clear up source cache comments
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