-
Notifications
You must be signed in to change notification settings - Fork 225
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
Clear external tileset skeletons from tile tree to save memory usage #1107
base: main
Are you sure you want to change the base?
Conversation
…external-tilesets-2
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.
Thanks @azrogers, this is so good! I tried flying around with Google Photorealistic 3D Tiles in Cesium for Unreal, and memory usage stays extremely steady. Previously I believe it would have gone up quite quickly. I didn't see any crashes or other dodgy behavior either. This will be a major improvement for our users!
Mostly small comments here, but I did notice a couple of cases where I think there's potential for (rare) bad behavior.
I think it's also worth taking a bit of time to think through whether there could be any other gotchas like this, and generally do everything we can to thoroughly test everything. Perhaps bring back the soak test from #1415?
…external-tilesets-2
…um-native into unload-external-tilesets-2
@kring Looks like reversing the direction of iteration, as well as unloading empty tiles, worked great! I'll take a look at that soak test and think of some other ways we could verify correct behavior here. |
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.
A few more small things here. Also please update CHANGES.md.
Bringing the soak test up-to-date (now in CesiumGS/cesium-unreal#1615 for reasons detailed in that PR) did identify some crashes, including issues with |
I realize now that |
It's now tracking most of the tile pointer usages in |
Almost fixed all the issues. First up, the fix for the issue where external tilesets would sometimes be unloaded while some of their children were setTileContent(tile, std::move(pair.result), pair.pRenderResources);
tile.decrementDoNotUnloadCount("TilesetContentManager::loadTileContent done loading"); The issue with these two lines is that, despite the The other fix at least makes sense. Turns out, while The easiest solution is just to make sure we're unloading the entirety of bool Tileset::_clearChildrenRecursively(Tile* pTile) noexcept {
// Iterate through all children, calling this method recursively unless the
// child is still in _loadedTiles. If the child is still in _loadedTiles, or
// any of its children (or children's children and so on) are in _loadedTiles,
// we return false so we don't clear the children prematurely.
for (Tile& child : pTile->getChildren()) {
if (this->_loadedTiles.contains(child) ||
!_clearChildrenRecursively(&child)) {
return false;
}
}
pTile->clearChildren();
return true;
} That way, if there are children that still haven't been unloaded, we will hold off on clearing the children. And if this method returns The remaining issue is a result of this fix - because the external tileset |
On the first ordering issue: you can test this out by checking out 140a7d9 and adding log statements to both |
Do you have an insight into why this matters? Only the main thread should ever look at the If any of those assumptions are violated (i.e., if any Tile property is read or written off the main thread), then that's the real problem. We do, in some cases, create |
Ok, I believe I've finally solved it! Turns out, there's two reasons that we do in fact need to remove children from
I ran some quick comparisons with the tile loading soak test (CesiumGS/cesium-unreal#1615) to show that this does indeed clean up external tilesets: |
For my own reference, I created an inventory of how the two counts in a _doNotUnloadCount
Summary: _tilesStillNotUnloadedCount
Summary: |
Based on the above, I think it's possible to collapse this back down to a single count, Also, let's rename |
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.
This is looking good! A few suggestions that might help clean things up a bit.
Cesium3DTilesSelection/src/Tile.cpp
Outdated
@@ -122,9 +163,19 @@ void Tile::createChildTiles(std::vector<Tile>&& children) { | |||
throw std::runtime_error("Children already created."); | |||
} | |||
|
|||
int32_t prevLoadedContentsCount = this->_tilesStillNotUnloadedCount; |
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 it's possible for any of these children
to have loaded content at this point. I can't think of how they would, at least. Am I mistaken?
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.
It turns out it is possible - in TilesetJsonLoader::createLoader
when we call createChildTiles
with the root tile from parseTilesetJson
. When parseTilesetJson
is called with an implicit tileset, that root tile is created with external content which gives it the ContentLoaded
state. Checking in createChildTiles
if any children have the ContentLoaded
state and ticking up the _doNotUnloadSubtreeCount
for each one solves the issue.
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.
Ah, interesting! Implicit tiling has been a bit of a blind spot for me in reviewing this so far. We should definitely make sure implicit tilesets are still working well in this PR, if you haven't already.
It doesn't need to be part of this PR, but we should eventually support unloading implicit tilesets, too. The rules there are a bit different, though. With an external tileset, once we're sure the entire thing is unused, we can unload all of it. It's all or nothing. However, with implicit tiling, we can recreate explicit tiles for any part of the implicit tree, so it's valid to unload any unused subtree.
Removing |
@kring Successfully got the fix for empty tile handling implemented. I believe that's everything! |
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.
This is looking better and better, but I still have a few comments. Thanks for your patience and ongoing work on this @azrogers!
@@ -185,6 +185,7 @@ option(CESIUM_TESTS_ENABLED "Whether to enable tests" ON) | |||
option(CESIUM_GLM_STRICT_ENABLED "Whether to force strict GLM compile definitions." ON) | |||
option(CESIUM_DISABLE_DEFAULT_ELLIPSOID "Whether to disable the WGS84 default value for ellipsoid parameters across cesium-native." OFF) | |||
option(CESIUM_MSVC_STATIC_RUNTIME_ENABLED "Whether to enable static linking for MSVC runtimes" OFF) | |||
option(CESIUM_DEBUG_TILE_UNLOADING "Whether to enable tracking of tile _doNotUnloadCount modifications for tile unloading debugging." OFF) |
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.
_doNotUnloadSubtreeCount
* | ||
* This function is not supposed to be called by clients. | ||
*/ | ||
int32_t getDoNotUnloadCount() const noexcept { |
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.
getDoNotUnloadSubtreeCount
#include <optional> | ||
#include <span> | ||
#include <string> | ||
#ifdef CESIUM_DEBUG_TILE_UNLOADING | ||
#include <unordered_map> | ||
#endif | ||
#include <vector> |
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.
clang-format won't reorder around an ifdef (for good reason), so let's put the ifdef'd header at the end.
#include <optional> | |
#include <span> | |
#include <string> | |
#ifdef CESIUM_DEBUG_TILE_UNLOADING | |
#include <unordered_map> | |
#endif | |
#include <vector> | |
#include <optional> | |
#include <span> | |
#include <string> | |
#include <vector> | |
#ifdef CESIUM_DEBUG_TILE_UNLOADING | |
#include <unordered_map> | |
#endif |
#include <cstdint> | ||
#include <memory> | ||
#include <stdexcept> | ||
#ifdef CESIUM_DEBUG_TILE_UNLOADING | ||
#include <unordered_map> | ||
#endif | ||
#include <utility> | ||
#include <vector> |
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.
Same thing here.
#include <cstdint> | |
#include <memory> | |
#include <stdexcept> | |
#ifdef CESIUM_DEBUG_TILE_UNLOADING | |
#include <unordered_map> | |
#endif | |
#include <utility> | |
#include <vector> | |
#include <cstdint> | |
#include <memory> | |
#include <stdexcept> | |
#include <utility> | |
#include <vector> | |
#ifdef CESIUM_DEBUG_TILE_UNLOADING | |
#include <unordered_map> | |
#endif |
if (tile.getState() == TileLoadState::ContentLoaded) { | ||
++this->_doNotUnloadSubtreeCount; | ||
} |
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.
This is still to handle the root tile of implicit tilesets, which are constructed with TileExternalContent
, right?
If so, I don't have high confidence it will work correctly in all cases. For one thing, an implicit root need not be at the root tile of the tileset.json (though it often is). Second, this code is only incrementing the count on this tile, not propagating it up the tree.
(*tileIt)->decrementDoNotUnloadSubtreeCount( | ||
"Tileset::_updateLodTransitions in render list"); |
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.
Same here.
(*tileIt)->decrementDoNotUnloadSubtreeCount( | ||
"Tileset::_updateLodTransitions done fading out"); |
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.
And here.
if (!tilesNeedingChildrenCleared.empty()) { | ||
for (Tile* pTileToClear : tilesNeedingChildrenCleared) { | ||
CESIUM_ASSERT(pTileToClear->getDoNotUnloadCount() == 0); | ||
_clearChildrenRecursively(pTileToClear); |
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.
_clearChildrenRecursively(pTileToClear); | |
this->_clearChildrenRecursively(pTileToClear); |
// The root tile marks the beginning of the tiles that were used for rendering | ||
// last frame. By iterating backwards starting from this position, we ensure | ||
// that descendants will always be unloaded before their ancestors. |
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.
Is this change to flip the order of traversal still needed? The problem with it, which I should have realized when I first suggested it, is that we're no longer unloading the least-recently-used tiles first.
I think reverting this will mean that external tileset loading can be delayed by a frame. The tile with external content will often be considered first and deemed not unloadable, but then other tiles will be unloaded, which would allow the tile with external content to be unloaded. But this won't be noticed until the next frame.
I think this is fine. From a performance perspective, this may even be a good thing, because needing to (re-)load external tilesets before loading the content of tiles in the subtree adds quite a bit of latency.
However, it reminds me that external tilesets are currently not counted at all in getTotalDataBytes
. They always have a size of zero. I don't think this is catastrophic, and I don't think it's essential to fix in this PR or in the next release, but we should write an issue and fix it as a followup.
return loadInput.asyncSystem.createResolvedFuture<TileLoadResult>( | ||
TileLoadResult{ | ||
TileEmptyContent{}, | ||
_upAxis, |
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.
_upAxis, | |
this->_upAxis, |
Closes #739. Currently, though the content of tiles are unloaded when no longer used, the "skeletons" - the
Tile
objects - created by loading external tilesets are never unloaded. This can cause memory usage to steadily increase. This change implements a_doNotUnloadCount
number on each tile that tracks situations where the tile's pointers are still in use. When a tile is in a situation where its pointer is being used - the tile is being loaded, for example - it increments this count on the tile and each of its parent tiles, and when the pointer is no longer needed, this counter is decremented up the tree as well. This means we can clear the children of external tilesets when their_doNotUnloadCount
number is 0. This implementation also includes aTileDoNotUnloadCountTracker
class, enabled with theCESIUM_DEBUG_TILE_UNLOADING
switch, that tracks the source of every modification to a tile's_doNotUnloadCount
.