cesium-native 0.44.2
Loading...
Searching...
No Matches
SharedAssetDepot.h
1#pragma once
2
3#include <CesiumAsync/AsyncSystem.h>
4#include <CesiumAsync/Future.h>
5#include <CesiumAsync/IAssetAccessor.h>
6#include <CesiumUtility/DoublyLinkedList.h>
7#include <CesiumUtility/IDepotOwningAsset.h>
8#include <CesiumUtility/IntrusivePointer.h>
9#include <CesiumUtility/ReferenceCounted.h>
10#include <CesiumUtility/Result.h>
11
12#include <cstddef>
13#include <functional>
14#include <memory>
15#include <mutex>
16#include <optional>
17#include <string>
18#include <unordered_map>
19
20namespace CesiumUtility {
21template <typename T> class SharedAsset;
22}
23
24namespace CesiumAsync {
25
32template <typename TAssetType, typename TAssetKey>
33class CESIUMASYNC_API SharedAssetDepot
35 SharedAssetDepot<TAssetType, TAssetKey>>,
36 public CesiumUtility::IDepotOwningAsset<TAssetType> {
37public:
50 int64_t inactiveAssetSizeLimitBytes = static_cast<int64_t>(16 * 1024 * 1024);
51
68 const AsyncSystem& asyncSystem,
69 const std::shared_ptr<IAssetAccessor>& pAssetAccessor,
70 const TAssetKey& key);
71
79 SharedAssetDepot(std::function<FactorySignature> factory);
80
81 virtual ~SharedAssetDepot();
82
94 const AsyncSystem& asyncSystem,
95 const std::shared_ptr<IAssetAccessor>& pAssetAccessor,
96 const TAssetKey& assetKey);
97
102 size_t getAssetCount() const;
103
108 size_t getActiveAssetCount() const;
109
114 size_t getInactiveAssetCount() const;
115
121
122 // Disable copy
123 void operator=(const SharedAssetDepot<TAssetType, TAssetKey>& other) = delete;
124
125private:
126 struct LockHolder;
127
133 LockHolder lock() const;
134
143 void markDeletionCandidate(const TAssetType& asset, bool threadOwnsDepotLock)
144 override;
145
146 void markDeletionCandidateUnderLock(const TAssetType& asset);
147
156 void unmarkDeletionCandidate(
157 const TAssetType& asset,
158 bool threadOwnsDepotLock) override;
159
160 void unmarkDeletionCandidateUnderLock(const TAssetType& asset);
161
166 struct AssetEntry
167 : public CesiumUtility::ReferenceCountedThreadSafe<AssetEntry> {
168 AssetEntry(TAssetKey&& key_)
169 : CesiumUtility::ReferenceCountedThreadSafe<AssetEntry>(),
170 key(std::move(key_)),
171 pAsset(),
172 maybePendingAsset(),
173 errorsAndWarnings(),
174 sizeInDeletionList(0),
175 deletionListPointers() {}
176
177 AssetEntry(const TAssetKey& key_) : AssetEntry(TAssetKey(key_)) {}
178
182 TAssetKey key;
183
188 std::unique_ptr<TAssetType> pAsset;
189
195 std::optional<SharedFuture<CesiumUtility::ResultPointer<TAssetType>>>
196 maybePendingAsset;
197
203 CesiumUtility::ErrorList errorsAndWarnings;
204
211 int64_t sizeInDeletionList;
212
218
219 CesiumUtility::ResultPointer<TAssetType> toResultUnderLock() const;
220 };
221
222 // Manages the depot's mutex. Also ensures, via IntrusivePointer, that the
223 // depot won't be destroyed while the lock is held.
224 struct LockHolder {
225 LockHolder(
227 ~LockHolder();
228 void unlock();
229
230 private:
231 // These two fields _must_ be declared in this order to guarantee that the
232 // mutex is released before the depot pointer. Releasing the depot pointer
233 // could destroy the depot, and that will be disastrous if the lock is still
234 // held.
236 std::unique_lock<std::mutex> lock;
237 };
238
239 // Maps asset keys to AssetEntry instances. This collection owns the asset
240 // entries.
241 std::unordered_map<TAssetKey, CesiumUtility::IntrusivePointer<AssetEntry>>
242 _assets;
243
244 // Maps asset pointers to AssetEntry instances. The values in this map refer
245 // to instances owned by the _assets map.
246 std::unordered_map<TAssetType*, AssetEntry*> _assetsByPointer;
247
248 // List of assets that are being considered for deletion, in the order that
249 // they became unused.
251 _deletionCandidates;
252
253 // The total amount of memory used by all assets in the _deletionCandidates
254 // list.
255 int64_t _totalDeletionCandidateMemoryUsage;
256
257 // Mutex serializing access to _assets, _assetsByPointer, _deletionCandidates,
258 // and any AssetEntry owned by this depot.
259 mutable std::mutex _mutex;
260
261 // The factory used to create new AssetType instances.
262 std::function<FactorySignature> _factory;
263
264 // This instance keeps a reference to itself whenever it is managing active
265 // assets, preventing it from being destroyed even if all other references to
266 // it are dropped.
268 _pKeepAlive;
269};
270
271template <typename TAssetType, typename TAssetKey>
273 std::function<FactorySignature> factory)
274 : _assets(),
275 _assetsByPointer(),
276 _deletionCandidates(),
277 _totalDeletionCandidateMemoryUsage(0),
278 _mutex(),
279 _factory(std::move(factory)),
280 _pKeepAlive(nullptr) {}
281
282template <typename TAssetType, typename TAssetKey>
284 // Ideally, when the depot is destroyed, all the assets it owns would become
285 // independent assets. But this is extremely difficult to manage in a
286 // thread-safe manner.
287
288 // Since we're in the destructor, we can be sure no one has a reference to
289 // this instance anymore. That means that no other thread can be executing
290 // `getOrCreate`, and no async asset creations are in progress.
291
292 // However, if assets owned by this depot are still alive, then other
293 // threads can still be calling addReference / releaseReference on some of
294 // our assets even while we're running the depot's destructor. Which means
295 // that we can end up in `markDeletionCandidate` at the same time the
296 // destructor is running. And in fact it's possible for a `SharedAsset` with
297 // especially poor timing to call into a `SharedAssetDepot` just after it is
298 // destroyed.
299
300 // To avoid this, we use the _pKeepAlive field to maintain an artificial
301 // reference to this depot whenever it owns live assets. This should keep
302 // this destructor from being called except when all of its assets are also
303 // in the _deletionCandidates list.
304
305 CESIUM_ASSERT(this->_assets.size() == this->_deletionCandidates.size());
306}
307
308template <typename TAssetType, typename TAssetKey>
309SharedFuture<CesiumUtility::ResultPointer<TAssetType>>
311 const AsyncSystem& asyncSystem,
312 const std::shared_ptr<IAssetAccessor>& pAssetAccessor,
313 const TAssetKey& assetKey) {
314 // We need to take care here to avoid two assets starting to load before the
315 // first asset has added an entry and set its maybePendingAsset field.
316 LockHolder lock = this->lock();
317
318 auto existingIt = this->_assets.find(assetKey);
319 if (existingIt != this->_assets.end()) {
320 // We've already loaded (or are loading) an asset with this ID - we can
321 // just use that.
322 const AssetEntry& entry = *existingIt->second;
323 if (entry.maybePendingAsset) {
324 // Asset is currently loading.
325 return *entry.maybePendingAsset;
326 } else {
327 return asyncSystem.createResolvedFuture(entry.toResultUnderLock())
328 .share();
329 }
330 }
331
332 // Calling the factory function while holding the mutex unnecessarily
333 // limits parallelism. It can even lead to a bug in the scenario where the
334 // `thenInWorkerThread` continuation is invoked immediately in the current
335 // thread, before `thenInWorkerThread` itself returns. That would result
336 // in an attempt to lock the mutex recursively, which is not allowed.
337
338 // So we jump through some hoops here to publish "this thread is working
339 // on it", then unlock the mutex, and _then_ actually call the factory
340 // function.
341 Promise<void> promise = asyncSystem.createPromise<void>();
342
343 // We haven't loaded or started to load this asset yet.
344 // Let's do that now.
346 pDepot = this;
347 CesiumUtility::IntrusivePointer<AssetEntry> pEntry = new AssetEntry(assetKey);
348
349 auto future =
350 promise.getFuture()
351 .thenImmediately([pDepot, pEntry, asyncSystem, pAssetAccessor]() {
352 return pDepot->_factory(asyncSystem, pAssetAccessor, pEntry->key);
353 })
354 .catchImmediately([](std::exception&& e) {
358 std::string("Error creating asset: ") + e.what()));
359 })
360 .thenInWorkerThread(
361 [pDepot,
362 pEntry](CesiumUtility::Result<
364 LockHolder lock = pDepot->lock();
365
366 if (result.pValue) {
367 result.pValue->_pDepot = pDepot.get();
368 pDepot->_assetsByPointer[result.pValue.get()] = pEntry.get();
369 }
370
371 // Now that this asset is owned by the depot, we exclusively
372 // control its lifetime with a std::unique_ptr.
373 pEntry->pAsset =
374 std::unique_ptr<TAssetType>(result.pValue.get());
375 pEntry->errorsAndWarnings = std::move(result.errors);
376 pEntry->maybePendingAsset.reset();
377
378 // The asset is initially live because we have an
379 // IntrusivePointer to it right here. So make sure the depot
380 // stays alive, too.
381 pDepot->_pKeepAlive = pDepot;
382
383 return pEntry->toResultUnderLock();
384 });
385
387 std::move(future).share();
388
389 pEntry->maybePendingAsset = sharedFuture;
390
391 [[maybe_unused]] bool added = this->_assets.emplace(assetKey, pEntry).second;
392
393 // Should always be added successfully, because we checked above that the
394 // asset key doesn't exist in the map yet.
395 CESIUM_ASSERT(added);
396
397 // Unlock the mutex and then call the factory function.
398 lock.unlock();
399 promise.resolve();
400
401 return sharedFuture;
402}
403
404template <typename TAssetType, typename TAssetKey>
406 LockHolder lock = this->lock();
407 return this->_assets.size();
408}
409
410template <typename TAssetType, typename TAssetKey>
412 LockHolder lock = this->lock();
413 return this->_assets.size() - this->_deletionCandidates.size();
414}
415
416template <typename TAssetType, typename TAssetKey>
418 LockHolder lock = this->lock();
419 return this->_deletionCandidates.size();
420}
421
422template <typename TAssetType, typename TAssetKey>
423int64_t
425 const {
426 LockHolder lock = this->lock();
427 return this->_totalDeletionCandidateMemoryUsage;
428}
429
430template <typename TAssetType, typename TAssetKey>
431typename SharedAssetDepot<TAssetType, TAssetKey>::LockHolder
433 return LockHolder{this};
434}
435
436template <typename TAssetType, typename TAssetKey>
437void SharedAssetDepot<TAssetType, TAssetKey>::markDeletionCandidate(
438 const TAssetType& asset,
439 bool threadOwnsDepotLock) {
440 if (threadOwnsDepotLock) {
441 this->markDeletionCandidateUnderLock(asset);
442 } else {
443 LockHolder lock = this->lock();
444 this->markDeletionCandidateUnderLock(asset);
445 }
446}
447
448template <typename TAssetType, typename TAssetKey>
449void SharedAssetDepot<TAssetType, TAssetKey>::markDeletionCandidateUnderLock(
450 const TAssetType& asset) {
451 // Verify that the reference count is still zero.
452 // See: https://github.com/CesiumGS/cesium-native/issues/1073
453 if (asset._referenceCount != 0) {
454 return;
455 }
456
457 auto it = this->_assetsByPointer.find(const_cast<TAssetType*>(&asset));
458 CESIUM_ASSERT(it != this->_assetsByPointer.end());
459 if (it == this->_assetsByPointer.end()) {
460 return;
461 }
462
463 CESIUM_ASSERT(it->second != nullptr);
464
465 AssetEntry& entry = *it->second;
466 entry.sizeInDeletionList = asset.getSizeBytes();
467 this->_totalDeletionCandidateMemoryUsage += entry.sizeInDeletionList;
468
469 this->_deletionCandidates.insertAtTail(entry);
470
471 if (this->_totalDeletionCandidateMemoryUsage >
472 this->inactiveAssetSizeLimitBytes) {
473 // Delete the deletion candidates until we're below the limit.
474 while (this->_deletionCandidates.size() > 0 &&
475 this->_totalDeletionCandidateMemoryUsage >
476 this->inactiveAssetSizeLimitBytes) {
477 AssetEntry* pOldEntry = this->_deletionCandidates.head();
478 this->_deletionCandidates.remove(*pOldEntry);
479
480 this->_totalDeletionCandidateMemoryUsage -= pOldEntry->sizeInDeletionList;
481
482 CESIUM_ASSERT(
483 pOldEntry->pAsset == nullptr ||
484 pOldEntry->pAsset->_referenceCount == 0);
485
486 if (pOldEntry->pAsset) {
487 this->_assetsByPointer.erase(pOldEntry->pAsset.get());
488 }
489
490 // This will actually delete the asset.
491 this->_assets.erase(pOldEntry->key);
492 }
493 }
494
495 // If this depot is not managing any live assets, then we no longer need to
496 // keep it alive.
497 if (this->_assets.size() == this->_deletionCandidates.size()) {
498 this->_pKeepAlive.reset();
499 }
500}
501
502template <typename TAssetType, typename TAssetKey>
503void SharedAssetDepot<TAssetType, TAssetKey>::unmarkDeletionCandidate(
504 const TAssetType& asset,
505 bool threadOwnsDepotLock) {
506 if (threadOwnsDepotLock) {
507 this->unmarkDeletionCandidateUnderLock(asset);
508 } else {
509 LockHolder lock = this->lock();
510 this->unmarkDeletionCandidateUnderLock(asset);
511 }
512}
513
514template <typename TAssetType, typename TAssetKey>
515void SharedAssetDepot<TAssetType, TAssetKey>::unmarkDeletionCandidateUnderLock(
516 const TAssetType& asset) {
517 auto it = this->_assetsByPointer.find(const_cast<TAssetType*>(&asset));
518 CESIUM_ASSERT(it != this->_assetsByPointer.end());
519 if (it == this->_assetsByPointer.end()) {
520 return;
521 }
522
523 CESIUM_ASSERT(it->second != nullptr);
524
525 AssetEntry& entry = *it->second;
526 bool isFound = this->_deletionCandidates.contains(entry);
527
528 // The asset won't necessarily be found in the deletionCandidates set.
529 // See: https://github.com/CesiumGS/cesium-native/issues/1073
530 if (isFound) {
531 this->_totalDeletionCandidateMemoryUsage -= entry.sizeInDeletionList;
532 this->_deletionCandidates.remove(entry);
533 }
534
535 // This depot is now managing at least one live asset, so keep it alive.
536 this->_pKeepAlive = this;
537}
538
539template <typename TAssetType, typename TAssetKey>
541SharedAssetDepot<TAssetType, TAssetKey>::AssetEntry::toResultUnderLock() const {
542 // This method is called while the calling thread already owns the depot
543 // mutex. So we must take care not to lock it again, which could happen if
544 // the asset is currently unreferenced and we naively create an
545 // IntrusivePointer for it.
547 if (pAsset) {
548 pAsset->addReference(true);
549 p = pAsset.get();
550 pAsset->releaseReference(true);
551 }
552 return CesiumUtility::ResultPointer<TAssetType>(p, errorsAndWarnings);
553}
554
555template <typename TAssetType, typename TAssetKey>
556SharedAssetDepot<TAssetType, TAssetKey>::LockHolder::LockHolder(
558 : pDepot(pDepot_), lock(pDepot_->_mutex) {}
559
560template <typename TAssetType, typename TAssetKey>
561SharedAssetDepot<TAssetType, TAssetKey>::LockHolder::~LockHolder() = default;
562
563template <typename TAssetType, typename TAssetKey>
564void SharedAssetDepot<TAssetType, TAssetKey>::LockHolder::unlock() {
565 this->lock.unlock();
566}
567
568} // namespace CesiumAsync
A system for managing asynchronous requests and tasks.
Definition AsyncSystem.h:36
Future< T > createResolvedFuture(T &&value) const
Creates a future that is already resolved.
Promise< T > createPromise() const
Create a Promise that can be used at a later time to resolve or reject a Future.
Definition AsyncSystem.h:93
A value that will be available in the future, as produced by AsyncSystem.
Definition Promise.h:12
A promise that can be resolved or rejected by an asynchronous task.
Definition Promise.h:19
void resolve(T &&value) const
Will be called when the task completed successfully.
Definition Promise.h:26
Future< T > getFuture() const
Gets the Future that resolves or rejects when this Promise is resolved or rejected.
Definition Promise.h:62
A depot for CesiumUtility::SharedAsset instances, which are potentially shared between multiple objec...
Definition SharedAsset.h:11
size_t getActiveAssetCount() const
Gets the number of assets owned by this depot that are active, meaning that they are currently being ...
size_t getInactiveAssetCount() const
Gets the number of assets owned by this depot that are inactive, meaning that they are not currently ...
size_t getAssetCount() const
Returns the total number of distinct assets contained in this depot, including both active and inacti...
CesiumAsync::Future< CesiumUtility::ResultPointer< TAssetType > >( const AsyncSystem &asyncSystem, const std::shared_ptr< IAssetAccessor > &pAssetAccessor, const TAssetKey &key) FactorySignature
Signature for the callback function that will be called to fetch and create a new instance of TAssetT...
SharedAssetDepot(std::function< FactorySignature > factory)
Creates a new SharedAssetDepot using the given factory callback to load new assets.
int64_t getInactiveAssetTotalSizeBytes() const
Gets the total bytes used by inactive (unused) assets owned by this depot.
SharedFuture< CesiumUtility::ResultPointer< TAssetType > > getOrCreate(const AsyncSystem &asyncSystem, const std::shared_ptr< IAssetAccessor > &pAssetAccessor, const TAssetKey &assetKey)
Gets an asset from the depot if it already exists, or creates it using the depot's factory if it does...
A value that will be available in the future, as produced by AsyncSystem. Unlike Future,...
Contains the previous and next pointers for an element in a DoublyLinkedList.
An interface representing the depot that owns a SharedAsset. This interface is an implementation deta...
A smart pointer that calls addReference and releaseReference on the controlled object.
void reset()
Reset this pointer to nullptr.
T * get() const noexcept
Returns the internal pointer.
A reference-counted base class, meant to be used with IntrusivePointer.
Classes that support asynchronous operations.
Utility classes for Cesium.
Result< IntrusivePointer< T > > ResultPointer
A convenient shortcut for CesiumUtility::Result<CesiumUtility::IntrusivePointer<T>>.
Definition Result.h:122
STL namespace.
The container to store the error and warning list when loading a tile or glTF content.
Definition ErrorList.h:18
static ErrorList error(std::string errorMessage)
Creates an ErrorList containing a single error.
Holds the result of an operation. If the operation succeeds, it will provide a value....
Definition Result.h:16