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

Clear external tileset skeletons from tile tree to save memory usage #1107

Merged
merged 40 commits into from
Feb 27, 2025
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
dd27186
_doNotUnloadCount tracking
azrogers Feb 7, 2025
2aa36b3
Use _doNotUnloadCount to actually unload external tiles
azrogers Feb 7, 2025
d4c14db
Add _loadedTiles to _doNotUnloadCount
azrogers Feb 7, 2025
c5a7fc4
Scratch that - just add tile ID to log
azrogers Feb 7, 2025
e7d1ce5
Add *initiator* tile ID to log
azrogers Feb 7, 2025
3fb4007
Handle removing children in Tileset
azrogers Feb 7, 2025
2b35b26
Inc/dec _doNotUnloadCount from setState
azrogers Feb 7, 2025
101f71c
Alllllmost working
azrogers Feb 7, 2025
db707fd
Final fixes and ifdef for debug tracking
azrogers Feb 10, 2025
ae37451
Merge branch 'main' of github.com:CesiumGS/cesium-native into unload-…
azrogers Feb 10, 2025
3816ef9
Add comment, format
azrogers Feb 10, 2025
0b27e40
RemoveChildren -> RemoveAndClearChildren
azrogers Feb 10, 2025
765e6db
Add [[maybe_unused]] to `reason`
azrogers Feb 10, 2025
7171545
#ifdef around unordered_map
azrogers Feb 10, 2025
38a9f08
Same stuff, different file
azrogers Feb 10, 2025
7f8e023
Merge branch 'main' of github.com:CesiumGS/cesium-native into unload-…
azrogers Feb 13, 2025
7842279
Address review comments
azrogers Feb 13, 2025
7367c77
Merge branch 'unload-external-tilesets-2' of github.com:CesiumGS/cesi…
azrogers Feb 13, 2025
f705ac2
More review comments
azrogers Feb 14, 2025
9c30fc5
Update CHANGES
azrogers Feb 14, 2025
21c03e2
Include Assert.h for clang-tidy
azrogers Feb 14, 2025
140a7d9
Track *most* of the Tile pointers in TilesetHeightQuery
azrogers Feb 14, 2025
9289398
Bizarre fix for ContentLoading issue
azrogers Feb 18, 2025
8aa6cc0
Fix for other _loadedTiles issue
azrogers Feb 18, 2025
b4dd32a
Add *second* counter
azrogers Feb 19, 2025
40f951f
Lots of SPDLOG_INFO
azrogers Feb 20, 2025
b5d4637
The final piece??
azrogers Feb 20, 2025
489a541
Fix test
azrogers Feb 20, 2025
598f49f
Remove pointless check I left in
azrogers Feb 20, 2025
81e5311
Remove _tilesStillNotUnloadedCount, rename _doNotUnloadCount, review …
azrogers Feb 24, 2025
7bc0896
Remove #define left in accidentally
azrogers Feb 24, 2025
c5de73a
Fix tileset height query test failure
azrogers Feb 24, 2025
7517543
Unhackify empty tile handling
azrogers Feb 25, 2025
160fa4d
Merge remote-tracking branch 'origin/main' into unload-external-tiles…
kring Feb 26, 2025
3546686
Address some of the review comments.
azrogers Feb 26, 2025
4ea9154
Change createChildTiles behavior
azrogers Feb 26, 2025
44cf954
Move _doNotUnloadSubtreeCount along with other fields.
kring Feb 27, 2025
2009ee1
Add comment about _doNotUnloadSubtreeCount copying.
kring Feb 27, 2025
3cdf25e
Fix doc statement that is no longer true.
kring Feb 27, 2025
9d757a7
Mark tile failed when load throws an exception.
kring Feb 27, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

- Fixed parsing URIs that have a scheme followed by `:` instead of `://`.
- Fixed decoding of KHR_mesh_quantization normalized values.
- `Tile` children of external tilesets will now be cleared when the external tileset is unloaded, fixing a memory leak that happened as a result of these `Tile` skeletons accumulating over time.

### v0.44.3 - 2025-02-12

Expand Down
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)

if (CESIUM_MSVC_STATIC_RUNTIME_ENABLED)
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
Expand Down
94 changes: 94 additions & 0 deletions Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tile.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,35 @@
#include <optional>
#include <span>
#include <string>
#ifdef CESIUM_DEBUG_TILE_UNLOADING
#include <unordered_map>
#endif
#include <vector>

namespace Cesium3DTilesSelection {
class TilesetContentLoader;

#ifdef CESIUM_DEBUG_TILE_UNLOADING
class TileDoNotUnloadCountTracker {
private:
struct Entry {
std::string reason;
bool increment;
int32_t newCount;
};

public:
static void addEntry(
const uint64_t id,
bool increment,
const std::string& reason,
int32_t newCount);

private:
static std::unordered_map<std::string, std::vector<Entry>> _entries;
};
#endif

/**
* The current state of this tile in the loading process.
*/
Expand Down Expand Up @@ -188,6 +212,13 @@ class CESIUM3DTILESSELECTION_API Tile final {
return std::span<const Tile>(this->_children);
}

/**
* @brief Clears the children of this tile.
*
* This function is not supposed to be called by clients.
*/
void clearChildren() noexcept;

/**
* @brief Assigns the given child tiles to this tile.
*
Expand Down Expand Up @@ -485,7 +516,64 @@ class CESIUM3DTILESSELECTION_API Tile final {
*/
TileLoadState getState() const noexcept;

/**
* @brief Returns the internal count denoting that the tile and its ancestors
* should not be unloaded.
*
* This function is not supposed to be called by clients.
*/
int32_t getDoNotUnloadCount() const noexcept {
return this->_doNotUnloadCount;
}

/**
* @brief Increments the internal count denoting that the tile and its
* ancestors should not be unloaded.
*
* This function is not supposed to be called by clients.
*/
void incrementDoNotUnloadCount(const char* reason) noexcept;

/**
* @brief Decrements the internal count denoting that the tile and its
* ancestors should not be unloaded.
*
* This function is not supposed to be called by clients.
*/
void decrementDoNotUnloadCount(const char* reason) noexcept;

/**
* @brief Marks this tile as having content that has not yet been unloaded,
* preventing a parent external tileset from cleaning up. This count will be
* propagated to any ancestors.
*
* This function is not supposed to be called by clients.
*/
void incrementTilesStillNotUnloadedCount() noexcept;

/**
* @brief Unmarks this tile as having content that has not yet been unloaded,
* allowing a parent external tileset to clean up. This count will be
* propagated to any ancestors.
*
* This function is not supposed to be called by clients.
*/
void decrementTilesStillNotUnloadedCount() noexcept;

/**
* @brief Obtains the number of tiles at or below this tile (that is, the tile
* itself and its children) that still have content that has not yet been
* unloaded, preventing a parent external tileset from cleaning up.
*/
int32_t getTilesStillNotUnloadedCount() const noexcept {
return this->_tilesStillNotUnloadedCount;
}

private:
void incrementDoNotUnloadCount(const std::string& reason) noexcept;

void decrementDoNotUnloadCount(const std::string& reason) noexcept;

struct TileConstructorImpl {};
template <
typename... TileContentArgs,
Expand Down Expand Up @@ -548,6 +636,12 @@ class CESIUM3DTILESSELECTION_API Tile final {
// mapped raster overlay
std::vector<RasterMappedTo3DTile> _rasterTiles;

// Number of existing claims on this tile preventing it and its parent
// external tileset (if any) from being unloaded from the tree.
int32_t _doNotUnloadCount = 0;

int32_t _tilesStillNotUnloadedCount = 0;

friend class TilesetContentManager;
friend class MockTilesetContentManagerTestFixture;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,7 @@ class CESIUM3DTILESSELECTION_API Tileset final {
void _processWorkerThreadLoadQueue();
void _processMainThreadLoadQueue();

void _clearChildrenRecursively(Tile* pTile) noexcept;
void _unloadCachedTiles(double timeBudget) noexcept;
void _markTileVisited(Tile& tile) noexcept;

Expand Down
133 changes: 131 additions & 2 deletions Cesium3DTilesSelection/src/Tile.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,17 @@
#include <CesiumGltf/BufferView.h>
#include <CesiumGltf/Image.h>
#include <CesiumGltf/Model.h>
#include <CesiumUtility/Assert.h>
#include <CesiumUtility/Math.h>

#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <memory>
#include <stdexcept>
#ifdef CESIUM_DEBUG_TILE_UNLOADING
#include <unordered_map>
#endif
#include <utility>
#include <vector>

Expand All @@ -22,6 +26,28 @@ using namespace CesiumUtility;
using namespace std::string_literals;

namespace Cesium3DTilesSelection {
#ifdef CESIUM_DEBUG_TILE_UNLOADING
std::unordered_map<std::string, std::vector<TileDoNotUnloadCountTracker::Entry>>
TileDoNotUnloadCountTracker::_entries;

void TileDoNotUnloadCountTracker::addEntry(
uint64_t id,
bool increment,
const std::string& reason,
int32_t newCount) {
const std::string idString = fmt::format("{:x}", id);
const auto foundIt = TileDoNotUnloadCountTracker::_entries.find(idString);
if (foundIt != TileDoNotUnloadCountTracker::_entries.end()) {
foundIt->second.push_back(Entry{reason, increment, newCount});
} else {
std::vector<Entry> entries{Entry{reason, increment, newCount}};

TileDoNotUnloadCountTracker::_entries.insert(
{idString, std::move(entries)});
}
}
#endif

Tile::Tile(TilesetContentLoader* pLoader) noexcept
: Tile(TileConstructorImpl{}, TileLoadState::Unloaded, pLoader) {}

Expand Down Expand Up @@ -63,7 +89,12 @@ Tile::Tile(
_content{std::forward<TileContentArgs>(args)...},
_pLoader{pLoader},
_loadState{loadState},
_mightHaveLatentChildren{true} {}
_mightHaveLatentChildren{true} {
if (_loadState != TileLoadState::Unloaded && !_content.isUnknownContent() &&
!_content.isEmptyContent()) {
incrementTilesStillNotUnloadedCount();
}
}

Tile::Tile(Tile&& rhs) noexcept
: _pParent(rhs._pParent),
Expand All @@ -80,7 +111,8 @@ Tile::Tile(Tile&& rhs) noexcept
_content(std::move(rhs._content)),
_pLoader{rhs._pLoader},
_loadState{rhs._loadState},
_mightHaveLatentChildren{rhs._mightHaveLatentChildren} {
_mightHaveLatentChildren{rhs._mightHaveLatentChildren},
_tilesStillNotUnloadedCount{rhs._tilesStillNotUnloadedCount} {
// since children of rhs will have the parent pointed to rhs,
// we will reparent them to this tile as rhs will be destroyed after this
for (Tile& tile : this->_children) {
Expand Down Expand Up @@ -112,6 +144,15 @@ Tile& Tile::operator=(Tile&& rhs) noexcept {
this->_pLoader = rhs._pLoader;
this->_loadState = rhs._loadState;
this->_mightHaveLatentChildren = rhs._mightHaveLatentChildren;
this->_tilesStillNotUnloadedCount = rhs._tilesStillNotUnloadedCount;
// We deliberately do *not* copy the _doNotUnloadCount of rhs here.
// This is because the _doNotUnloadCount is a count of instances of the
// *pointer* to the tile, denoting the number of active pointers that would
// be invalidated if the Tile were to be deleted. Because the memory
// location of the tile will have changed as a result of the move operation,
// the new Tile object will not have any pointers referencing it, so copying
// over the count would be incorrect and could result in a Tile not being
// removed when it otherwise should be.
}

return *this;
Expand All @@ -122,9 +163,19 @@ void Tile::createChildTiles(std::vector<Tile>&& children) {
throw std::runtime_error("Children already created.");
}

int32_t prevLoadedContentsCount = this->_tilesStillNotUnloadedCount;
Copy link
Member

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?

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 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.

Copy link
Member

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.

this->_children = std::move(children);
for (Tile& tile : this->_children) {
tile.setParent(this);
this->_tilesStillNotUnloadedCount += tile._tilesStillNotUnloadedCount;
}

// Propagate new loaded contents count up the chain
Tile* pParent = this->_pParent;
while (pParent != nullptr) {
pParent->_tilesStillNotUnloadedCount -= prevLoadedContentsCount;
pParent->_tilesStillNotUnloadedCount += this->_tilesStillNotUnloadedCount;
pParent = pParent->_pParent;
}
}

Expand Down Expand Up @@ -243,4 +294,82 @@ void Tile::setMightHaveLatentChildren(bool mightHaveLatentChildren) noexcept {
this->_mightHaveLatentChildren = mightHaveLatentChildren;
}

void Tile::clearChildren() noexcept {
CESIUM_ASSERT(this->_doNotUnloadCount == 0);
this->_children.clear();
}

void Tile::incrementDoNotUnloadCount(
[[maybe_unused]] const char* reason) noexcept {
#ifdef CESIUM_DEBUG_TILE_UNLOADING
const std::string reasonStr = fmt::format(
"Initiator ID: {:x}, {}",
reinterpret_cast<uint64_t>(this),
reason);
this->incrementDoNotUnloadCount(reasonStr);
#else
this->incrementDoNotUnloadCount(std::string());
#endif
}

void Tile::decrementDoNotUnloadCount(
[[maybe_unused]] const char* reason) noexcept {
#ifdef CESIUM_DEBUG_TILE_UNLOADING
const std::string reasonStr = fmt::format(
"Initiator ID: {:x}, {}",
reinterpret_cast<uint64_t>(this),
reason);
this->decrementDoNotUnloadCount(reasonStr);
#else
this->decrementDoNotUnloadCount(std::string());
#endif
}

void Tile::incrementDoNotUnloadCount(
[[maybe_unused]] const std::string& reason) noexcept {
++this->_doNotUnloadCount;
#ifdef CESIUM_DEBUG_TILE_UNLOADING
TileDoNotUnloadCountTracker::addEntry(
reinterpret_cast<uint64_t>(this),
true,
std::string(reason),
this->_doNotUnloadCount);
#endif
if (this->getParent() != nullptr) {
this->getParent()->incrementDoNotUnloadCount(reason);
}
}

void Tile::decrementDoNotUnloadCount(
[[maybe_unused]] const std::string& reason) noexcept {
CESIUM_ASSERT(this->_doNotUnloadCount > 0);
--this->_doNotUnloadCount;
#ifdef CESIUM_DEBUG_TILE_UNLOADING
TileDoNotUnloadCountTracker::addEntry(
reinterpret_cast<uint64_t>(this),
false,
std::string(reason),
this->_doNotUnloadCount);
#endif
if (this->getParent() != nullptr) {
this->getParent()->decrementDoNotUnloadCount(reason);
}
}

void Tile::incrementTilesStillNotUnloadedCount() noexcept {
CESIUM_ASSERT(this->_tilesStillNotUnloadedCount >= 0);
this->_tilesStillNotUnloadedCount++;
if (this->getParent() != nullptr) {
this->getParent()->incrementTilesStillNotUnloadedCount();
}
}

void Tile::decrementTilesStillNotUnloadedCount() noexcept {
CESIUM_ASSERT(this->_tilesStillNotUnloadedCount > 0);
--this->_tilesStillNotUnloadedCount;
if (this->getParent() != nullptr) {
this->getParent()->decrementTilesStillNotUnloadedCount();
}
}

} // namespace Cesium3DTilesSelection
Loading
Loading