-
-
Notifications
You must be signed in to change notification settings - Fork 932
Improve, optimize, and consolidate Tile LRU cache to new ES6 Map #6731
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
base: main
Are you sure you want to change the base?
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #6731 +/- ##
==========================================
+ Coverage 92.40% 92.42% +0.02%
==========================================
Files 288 288
Lines 23775 23761 -14
Branches 5055 5050 -5
==========================================
- Hits 21969 21961 -8
+ Misses 1806 1800 -6 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
| } | ||
|
|
||
| // Updating same key with a new tile - remove old tile and add new one | ||
| this.remove(key); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this call onRemove? I'm honestly asking, I don't know the right answer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe so:
https://github.com/maplibre/maplibre-gl-js/blob/main/src/tile/tile_cache.ts#L82-L85
Eventually on remove would be called for the same id.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
onRemove propagates back to source.unloadTile which then goes back to the worker_source.removeTile. In here, loaded is used to store tiles by uid in the worker. uid is created in tile.js using uniqueId() which is an integer count++ on each function run, so every tile object has a new and unique uid. Even though the new tile may have the same tileID, it will absolutely have a unique uid, which means it still needs to be removed from the this.loaded cache in the worker. In other words, onRemove needs to be called otherwise the tiles would collect in the worker. Do some tracing and let me know if you agree...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I think I know what should be the right approach here in order to get confident in the current solution.
- We need to cover the current cache with tests so that we know exactly how it operates, which callbacks are called when and how it behaves in general (assuming this is not the tests that currently exists, if the tests cover the current functionality then we can skip this step).
- After we have the tests in place, we can simply replace the implementation under the hood - no changes to logic (besides maybe the timers), without even changing the public API (take vs getAndRemove, clear vs reset etc)
- After this is passing and we know that we haven't changed any logic, we can rename stuff.
I think this is the safest way to handle this.
If this is how you did it, then there's very low risk in introducing bugs, as the process above is fairly safe.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've read the cache code start to end and a few questions rose:
- Why does it have an array? Most of the usage is referring to the first element, but not all.
- If the timeout were to be removed, how will this affect the code that listens to onRemove? What will happen if the tile's onRemove is not called when the tile expiries?
- The functionally as covered in the current tests (not those you added, but those that exist) is far from ideal, there's even a comment about how the cache should behave but it's not covered in a test, only in a comment, so not a great start...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you are good with keeping both 'raster' and 'vector' handled in tile_manager, then we could extract all retainment logic into tile_retainment.ts... versus having separate managers. This would cut out about 30% of the file. Just another idea...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you'll need to pass "this" into this file, which is an anti-pattern.
Also regarding the above name I think there are simply two "storage" types: in-view-tiles and out-of-view-tiles, the current cache us the out of view as far as I understand.
But I want to better understand other parts as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you can just pass this._tiles and retain, and change getLoadedTile to tile.hasData(), then you don't need to pass this...
Current cache is tiles that were loaded for a previous view... this._tiles is the current view. As tiles move out of view they are added to the cache...
Covering tiles -> add tile -> check the cache -> remove other this._tiles that aren't covering/substitute -> place them into cache -> repeat...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, this is also my understanding.
I wonder why there is a usage counter for a tile though...
In any case, a first step can be to move these parts to separate files as you suggested, onne for raster and one for vector.
Also renaming _tiles to inViewTiles and cache to outOfViewCache so that is clear why these two variables are needed and how they are different.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
'uses' to me seems like a hack that should be deprecated... if it doesn't work without it then we are missing something...
|
An initial stab at moving some code around. Not a big change overall... |
|
Well I resolved the merge conflicts and merged your split-tile-manager branch but now the 'files changed' page looks polluted. I guess after you merge your branch into main and I update this one it will be easier to read the changes... |
|
I've merged it to main, so you'd have to resolve conflicts again, sorry... |
52c774d to
59be754
Compare
# Conflicts: # src/style/style.ts # src/tile/tile_manager.test.ts # src/tile/tile_manager.ts
|
Got it merged... but still needs a little thought regarding remove/onRemove... |
| } | ||
|
|
||
| this._outOfViewCache.reset(); | ||
| // Clear the tile cache |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this comment is needed...
| } | ||
|
|
||
| this._outOfViewCache.reset(); | ||
| // Clear the tile cache |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not needed...
|
I still think adding tests to current implementation is necessary to make sure we don't miss on a use case, for example maybe the cache array is used to store different wrap instances of a tile on the same key? IDK... |
|
Ok.
|
|
Current implementation, as in the old one, I know I don't fully understand the old behavior, so it's important to make sure we understand it before replacing it. |
Since ES6, Map has made simple in-memory caching far easier, providing fast average-case O(1) access. Our previous tile cache relied heavily on O(n) array operations. This PR replaces that implementation with the existing BoundedLRUCache and wires it into TileCache. I evaluated several npm caching libraries, but none fit well with the specifics of MapLibre’s tile loading model or offered a good balance of size and simplicity.
The most critical operation used in TileManager, getAndRemove, previously incurred three O(n) operations per call. It is now implemented as take, which performs the same behavior in O(1). Additionally, the old tile cache maintained many concurrent timers for tiles sitting idle in the cache. The new cache no longer tracks per-tile timers; instead, expired tiles are evicted/unloaded lazily on access, avoiding the overhead of a large number of active timers.
O(N) Comparison Table
Timers Comparison Table
CHANGELOG.mdunder the## mainsection.