cesium-native 0.64.0
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
34
38 std::shared_ptr<IAssetAccessor> pAssetAccessor;
39};
40
53template <
54 typename TAssetType,
55 typename TAssetKey,
56 typename TContext = SharedAssetContext>
57class CESIUMASYNC_API SharedAssetDepot
59 SharedAssetDepot<TAssetType, TAssetKey, TContext>>,
60 public CesiumUtility::IDepotOwningAsset<TAssetType> {
61public:
74 std::atomic<int64_t> inactiveAssetSizeLimitBytes =
75 static_cast<int64_t>(16 * 1024 * 1024);
76
93 const TContext& context,
94 const TAssetKey& key);
95
103 SharedAssetDepot(std::function<FactorySignature> factory);
104
105 virtual ~SharedAssetDepot();
106
116 getOrCreate(const TContext& context, const TAssetKey& assetKey);
117
132 bool invalidate(const TAssetKey& assetKey);
133
149 bool invalidate(TAssetType& asset);
150
155 size_t getAssetCount() const;
156
161 size_t getActiveAssetCount() const;
162
167 size_t getInactiveAssetCount() const;
168
174
175 // Disable copy
176 void operator=(
178
179private:
180 struct LockHolder;
181
187 LockHolder lock() const;
188
197 void markDeletionCandidate(const TAssetType& asset, bool threadOwnsDepotLock)
198 override;
199
200 void markDeletionCandidateUnderLock(const TAssetType& asset);
201
210 void unmarkDeletionCandidate(
211 const TAssetType& asset,
212 bool threadOwnsDepotLock) override;
213
214 void unmarkDeletionCandidateUnderLock(const TAssetType& asset);
215
222 bool invalidateUnderLock(LockHolder&& lock, const TAssetKey& assetKey);
223
228 struct AssetEntry
229 : public CesiumUtility::ReferenceCountedThreadSafe<AssetEntry> {
230 AssetEntry(TAssetKey&& key_)
231 : CesiumUtility::ReferenceCountedThreadSafe<AssetEntry>(),
232 key(std::move(key_)),
233 pAsset(),
234 maybePendingAsset(),
235 errorsAndWarnings(),
236 sizeInDeletionList(0),
237 deletionListPointers() {}
238
239 AssetEntry(const TAssetKey& key_) : AssetEntry(TAssetKey(key_)) {}
240
244 TAssetKey key;
245
250 std::unique_ptr<TAssetType> pAsset;
251
257 std::optional<SharedFuture<CesiumUtility::ResultPointer<TAssetType>>>
258 maybePendingAsset;
259
265 CesiumUtility::ErrorList errorsAndWarnings;
266
273 int64_t sizeInDeletionList;
274
280
281 CesiumUtility::ResultPointer<TAssetType> toResultUnderLock() const;
282 };
283
284 // Manages the depot's mutex. Also ensures, via IntrusivePointer, that the
285 // depot won't be destroyed while the lock is held.
286 struct LockHolder {
287 LockHolder(
288 const CesiumUtility::IntrusivePointer<const SharedAssetDepot>& pDepot);
289 ~LockHolder();
290 void unlock();
291
292 private:
293 // These two fields _must_ be declared in this order to guarantee that the
294 // mutex is released before the depot pointer. Releasing the depot pointer
295 // could destroy the depot, and that will be disastrous if the lock is still
296 // held.
297 CesiumUtility::IntrusivePointer<const SharedAssetDepot> pDepot;
298 std::unique_lock<std::mutex> lock;
299 };
300
301 // Maps asset keys to AssetEntry instances. This collection owns the asset
302 // entries.
303 std::unordered_map<TAssetKey, CesiumUtility::IntrusivePointer<AssetEntry>>
304 _assets;
305
306 // Maps asset pointers to AssetEntry instances. The values in this map refer
307 // to instances owned by the _assets map.
308 std::unordered_map<TAssetType*, AssetEntry*> _assetsByPointer;
309
310 // List of assets that are being considered for deletion, in the order that
311 // they became unused.
313 _deletionCandidates;
314
315 // The total amount of memory used by all assets in the _deletionCandidates
316 // list.
317 int64_t _totalDeletionCandidateMemoryUsage;
318
319 // The number of assets that have been invalidated but that have not been
320 // deleted yet. Such assets hold a pointer to the depot, so the depot must be
321 // kept alive for their entire lifetime.
322 int64_t _liveInvalidatedAssets;
323
324 // Mutex serializing access to _assets, _assetsByPointer, _deletionCandidates,
325 // and any AssetEntry owned by this depot.
326 mutable std::mutex _mutex;
327
328 // The factory used to create new AssetType instances.
329 std::function<FactorySignature> _factory;
330
331 // This instance keeps a reference to itself whenever it is managing active
332 // assets, preventing it from being destroyed even if all other references to
333 // it are dropped.
334 CesiumUtility::IntrusivePointer<
335 SharedAssetDepot<TAssetType, TAssetKey, TContext>>
336 _pKeepAlive;
337};
338
339template <typename TAssetType, typename TAssetKey, typename TContext>
341 std::function<FactorySignature> factory)
342 : _assets(),
343 _assetsByPointer(),
344 _deletionCandidates(),
345 _totalDeletionCandidateMemoryUsage(0),
346 _liveInvalidatedAssets(0),
347 _mutex(),
348 _factory(std::move(factory)),
349 _pKeepAlive(nullptr) {}
350
351template <typename TAssetType, typename TAssetKey, typename TContext>
352SharedAssetDepot<TAssetType, TAssetKey, TContext>::~SharedAssetDepot() {
353 // Ideally, when the depot is destroyed, all the assets it owns would become
354 // independent assets. But this is extremely difficult to manage in a
355 // thread-safe manner.
356
357 // Since we're in the destructor, we can be sure no one has a reference to
358 // this instance anymore. That means that no other thread can be executing
359 // `getOrCreate`, and no async asset creations are in progress.
360
361 // However, if assets owned by this depot are still alive, then other
362 // threads can still be calling addReference / releaseReference on some of
363 // our assets even while we're running the depot's destructor. Which means
364 // that we can end up in `markDeletionCandidate` at the same time the
365 // destructor is running. And in fact it's possible for a `SharedAsset` with
366 // especially poor timing to call into a `SharedAssetDepot` just after it is
367 // destroyed.
368
369 // To avoid this, we use the _pKeepAlive field to maintain an artificial
370 // reference to this depot whenever it owns live assets. This should keep
371 // this destructor from being called except when all of its assets are also
372 // in the _deletionCandidates list.
373
374 CESIUM_ASSERT(this->_liveInvalidatedAssets == 0);
375 CESIUM_ASSERT(this->_assets.size() == this->_deletionCandidates.size());
376}
377
378template <typename TAssetType, typename TAssetKey, typename TContext>
379SharedFuture<CesiumUtility::ResultPointer<TAssetType>>
381 const TContext& context,
382 const TAssetKey& assetKey) {
383 // We need to take care here to avoid two assets starting to load before the
384 // first asset has added an entry and set its maybePendingAsset field.
385 LockHolder lock = this->lock();
386
387 auto existingIt = this->_assets.find(assetKey);
388 if (existingIt != this->_assets.end()) {
389 // We've already loaded (or are loading) an asset with this ID - we can
390 // just use that.
391 const AssetEntry& entry = *existingIt->second;
392 if (entry.maybePendingAsset) {
393 // Asset is currently loading.
394 return *entry.maybePendingAsset;
395 } else {
396 return context.asyncSystem.createResolvedFuture(entry.toResultUnderLock())
397 .share();
398 }
399 }
400
401 // Calling the factory function while holding the mutex unnecessarily
402 // limits parallelism. It can even lead to a bug in the scenario where the
403 // `thenInWorkerThread` continuation is invoked immediately in the current
404 // thread, before `thenInWorkerThread` itself returns. That would result
405 // in an attempt to lock the mutex recursively, which is not allowed.
406
407 // So we jump through some hoops here to publish "this thread is working
408 // on it", then unlock the mutex, and _then_ actually call the factory
409 // function.
410 Promise<void> promise = context.asyncSystem.template createPromise<void>();
411
412 // We haven't loaded or started to load this asset yet.
413 // Let's do that now.
416 pDepot = this;
417 CesiumUtility::IntrusivePointer<AssetEntry> pEntry = new AssetEntry(assetKey);
418
419 auto future =
420 promise.getFuture()
421 .thenImmediately([pDepot, pEntry, context]() {
422 return pDepot->_factory(context, pEntry->key);
423 })
424 .thenInWorkerThread(
425 [pDepot,
426 pEntry](CesiumUtility::Result<
428 LockHolder lock = pDepot->lock();
429
430 if (result.pValue) {
431 result.pValue->_pDepot = pDepot.get();
432 pDepot->_assetsByPointer[result.pValue.get()] = pEntry.get();
433 }
434
435 // Now that this asset is owned by the depot, we exclusively
436 // control its lifetime with a std::unique_ptr.
437 pEntry->pAsset =
438 std::unique_ptr<TAssetType>(result.pValue.get());
439 pEntry->errorsAndWarnings = std::move(result.errors);
440 pEntry->maybePendingAsset.reset();
441
442 // The asset is initially live because we have an
443 // IntrusivePointer to it right here. So make sure the depot
444 // stays alive, too.
445 pDepot->_pKeepAlive = pDepot;
446
447 return pEntry->toResultUnderLock();
448 })
449 .catchImmediately([pDepot, pEntry](std::exception&& e) {
450 // This asset has failed _with an exception_. We don't want to cache
451 // this type of error.
452 {
453 LockHolder lock = pDepot->lock();
454 pDepot->_assets.erase(pEntry->key);
455 }
456
460 std::string("Exception while creating asset: ") +
461 e.what()));
462 });
463
465 std::move(future).share();
466
467 pEntry->maybePendingAsset = sharedFuture;
468
469 [[maybe_unused]] bool added = this->_assets.emplace(assetKey, pEntry).second;
470
471 // Should always be added successfully, because we checked above that the
472 // asset key doesn't exist in the map yet.
473 CESIUM_ASSERT(added);
474
475 // Unlock the mutex and then call the factory function.
476 lock.unlock();
477 promise.resolve();
478
479 return sharedFuture;
480}
481
482template <typename TAssetType, typename TAssetKey, typename TContext>
484 const TAssetKey& assetKey) {
485 LockHolder lock = this->lock();
486 return this->invalidateUnderLock(std::move(lock), assetKey);
487}
488
489template <typename TAssetType, typename TAssetKey, typename TContext>
491 TAssetType& asset) {
492 LockHolder lock = this->lock();
493
494 auto it = this->_assetsByPointer.find(&asset);
495 if (it == this->_assetsByPointer.end())
496 return false;
497
498 AssetEntry* pEntry = it->second;
499 CESIUM_ASSERT(pEntry);
500
501 return this->invalidateUnderLock(std::move(lock), pEntry->key);
502}
503
504template <typename TAssetType, typename TAssetKey, typename TContext>
505size_t
507 LockHolder lock = this->lock();
508 return this->_assets.size();
509}
510
511template <typename TAssetType, typename TAssetKey, typename TContext>
512size_t
514 LockHolder lock = this->lock();
515 return this->_assets.size() - this->_deletionCandidates.size();
516}
517
518template <typename TAssetType, typename TAssetKey, typename TContext>
519size_t
521 const {
522 LockHolder lock = this->lock();
523 return this->_deletionCandidates.size();
524}
525
526template <typename TAssetType, typename TAssetKey, typename TContext>
529 LockHolder lock = this->lock();
530 return this->_totalDeletionCandidateMemoryUsage;
531}
532
533template <typename TAssetType, typename TAssetKey, typename TContext>
534typename SharedAssetDepot<TAssetType, TAssetKey, TContext>::LockHolder
535SharedAssetDepot<TAssetType, TAssetKey, TContext>::lock() const {
536 return LockHolder{this};
537}
538
539template <typename TAssetType, typename TAssetKey, typename TContext>
540void SharedAssetDepot<TAssetType, TAssetKey, TContext>::markDeletionCandidate(
541 const TAssetType& asset,
542 bool threadOwnsDepotLock) {
543 if (threadOwnsDepotLock) {
544 this->markDeletionCandidateUnderLock(asset);
545 } else {
546 LockHolder lock = this->lock();
547 this->markDeletionCandidateUnderLock(asset);
548 }
549}
550
551template <typename TAssetType, typename TAssetKey, typename TContext>
553 markDeletionCandidateUnderLock(const TAssetType& asset) {
554 if (asset._isInvalidated) {
555 // This asset is no longer tracked by the depot, so delete it.
556 --this->_liveInvalidatedAssets;
557 delete &asset;
558
559 // If this depot is not managing any live assets, then we no longer need to
560 // keep it alive.
561 if (this->_assets.size() == this->_deletionCandidates.size() &&
562 this->_liveInvalidatedAssets == 0) {
563 this->_pKeepAlive.reset();
564 }
565
566 return;
567 }
568
569 // Verify that the reference count is still zero.
570 // See: https://github.com/CesiumGS/cesium-native/issues/1073
571 if (asset._referenceCount != 0) {
572 return;
573 }
574
575 auto it = this->_assetsByPointer.find(const_cast<TAssetType*>(&asset));
576 CESIUM_ASSERT(it != this->_assetsByPointer.end());
577 if (it == this->_assetsByPointer.end()) {
578 return;
579 }
580
581 CESIUM_ASSERT(it->second != nullptr);
582
583 AssetEntry& entry = *it->second;
584 entry.sizeInDeletionList = asset.getSizeBytes();
585 this->_totalDeletionCandidateMemoryUsage += entry.sizeInDeletionList;
586
587 this->_deletionCandidates.insertAtTail(entry);
588
589 if (this->_totalDeletionCandidateMemoryUsage >
590 this->inactiveAssetSizeLimitBytes) {
591 // Delete the deletion candidates until we're below the limit.
592 while (this->_deletionCandidates.size() > 0 &&
593 this->_totalDeletionCandidateMemoryUsage >
594 this->inactiveAssetSizeLimitBytes) {
595 AssetEntry* pOldEntry = this->_deletionCandidates.head();
596 this->_deletionCandidates.remove(*pOldEntry);
597
598 this->_totalDeletionCandidateMemoryUsage -= pOldEntry->sizeInDeletionList;
599
600 CESIUM_ASSERT(
601 pOldEntry->pAsset == nullptr ||
602 pOldEntry->pAsset->_referenceCount == 0);
603
604 if (pOldEntry->pAsset) {
605 this->_assetsByPointer.erase(pOldEntry->pAsset.get());
606 }
607
608 // This will actually delete the asset.
609 this->_assets.erase(pOldEntry->key);
610 }
611 }
612
613 // If this depot is not managing any live assets, then we no longer need to
614 // keep it alive.
615 if (this->_assets.size() == this->_deletionCandidates.size() &&
616 this->_liveInvalidatedAssets == 0) {
617 this->_pKeepAlive.reset();
618 }
619}
620
621template <typename TAssetType, typename TAssetKey, typename TContext>
622void SharedAssetDepot<TAssetType, TAssetKey, TContext>::unmarkDeletionCandidate(
623 const TAssetType& asset,
624 bool threadOwnsDepotLock) {
625 if (threadOwnsDepotLock) {
626 this->unmarkDeletionCandidateUnderLock(asset);
627 } else {
628 LockHolder lock = this->lock();
629 this->unmarkDeletionCandidateUnderLock(asset);
630 }
631}
632
633template <typename TAssetType, typename TAssetKey, typename TContext>
635 unmarkDeletionCandidateUnderLock(const TAssetType& asset) {
636 // This asset better not already be invalidated. That would imply this asset
637 // was resurrected after its reference count hit zero. This should only be
638 // possible if the asset depot returned a pointer to the asset, which it
639 // will not do for one that is invalidated.
640 CESIUM_ASSERT(!asset._isInvalidated);
641
642 auto it = this->_assetsByPointer.find(const_cast<TAssetType*>(&asset));
643 CESIUM_ASSERT(it != this->_assetsByPointer.end());
644 if (it == this->_assetsByPointer.end()) {
645 return;
646 }
647
648 CESIUM_ASSERT(it->second != nullptr);
649
650 AssetEntry& entry = *it->second;
651 bool isFound = this->_deletionCandidates.contains(entry);
652
653 // The asset won't necessarily be found in the deletionCandidates set.
654 // See: https://github.com/CesiumGS/cesium-native/issues/1073
655 if (isFound) {
656 this->_totalDeletionCandidateMemoryUsage -= entry.sizeInDeletionList;
657 this->_deletionCandidates.remove(entry);
658 }
659
660 // This depot is now managing at least one live asset, so keep it alive.
661 this->_pKeepAlive = this;
662}
663
664template <typename TAssetType, typename TAssetKey, typename TContext>
665bool SharedAssetDepot<TAssetType, TAssetKey, TContext>::invalidateUnderLock(
666 LockHolder&& lock,
667 const TAssetKey& assetKey) {
668 auto it = this->_assets.find(assetKey);
669 if (it == this->_assets.end())
670 return false;
671
672 AssetEntry* pEntry = it->second.get();
673 CESIUM_ASSERT(pEntry);
674
675 // This will remove the asset from the deletion candidates list, if it's
676 // there.
678 pEntry->toResultUnderLock();
679
680 bool wasInvalidated = false;
681
682 if (assetResult.pValue) {
683 if (!assetResult.pValue->_isInvalidated) {
684 wasInvalidated = true;
685 assetResult.pValue->_isInvalidated = true;
686 ++this->_liveInvalidatedAssets;
687 }
688 this->_assetsByPointer.erase(assetResult.pValue.get());
689 }
690
691 // Detach the asset from the AssetEntry, so that its lifetime is controlled by
692 // reference counting.
693 pEntry->pAsset.release();
694
695 // Remove the asset entry. This won't immediately delete the asset, because
696 // `assetResult` above still holds a reference to it. But once that goes out
697 // of scope, too, the asset _may_ be destroyed.
698 this->_assets.erase(it);
699
700 // Unlock the mutex before allowing `assetResult` to go out of scope. When it
701 // goes out of scope, the asset may be destroyed. If it is, that would cause
702 // us to try to re-enter the lock, which is not allowed.
703 lock.unlock();
704
705 return wasInvalidated;
706}
707
708template <typename TAssetType, typename TAssetKey, typename TContext>
711 toResultUnderLock() const {
712 // This method is called while the calling thread already owns the depot
713 // mutex. So we must take care not to lock it again, which could happen if
714 // the asset is currently unreferenced and we naively create an
715 // IntrusivePointer for it.
716 CesiumUtility::IntrusivePointer<TAssetType> p = nullptr;
717 if (pAsset) {
718 pAsset->addReference(true);
719 p = pAsset.get();
720 pAsset->releaseReference(true);
721 }
722 return CesiumUtility::ResultPointer<TAssetType>(p, errorsAndWarnings);
723}
724
725template <typename TAssetType, typename TAssetKey, typename TContext>
727 const CesiumUtility::IntrusivePointer<const SharedAssetDepot>& pDepot_)
728 : pDepot(pDepot_), lock(pDepot_->_mutex) {}
729
730template <typename TAssetType, typename TAssetKey, typename TContext>
731SharedAssetDepot<TAssetType, TAssetKey, TContext>::LockHolder::~LockHolder() =
732 default;
733
734template <typename TAssetType, typename TAssetKey, typename TContext>
735void SharedAssetDepot<TAssetType, TAssetKey, TContext>::LockHolder::unlock() {
736 this->lock.unlock();
737}
738
739} // namespace CesiumAsync
A system for managing asynchronous requests and tasks.
Definition AsyncSystem.h:37
A value that will be available in the future, as produced by AsyncSystem.
Definition Future.h:29
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...
SharedAssetDepot(std::function< FactorySignature > factory)
Creates a new SharedAssetDepot using the given factory callback to load new assets.
CesiumAsync::Future< CesiumUtility::ResultPointer< TAssetType > >( const TContext &context, const TAssetKey &key) FactorySignature
Signature for the callback function that will be called to fetch and create a new instance of TAssetT...
int64_t getInactiveAssetTotalSizeBytes() const
Gets the total bytes used by inactive (unused) assets owned by this depot.
size_t getActiveAssetCount() const
Gets the number of assets owned by this depot that are active, meaning that they are currently being ...
bool invalidate(TAssetType &asset)
Invalidates the previously-cached asset, so that the next call to getOrCreate will create the asset i...
SharedFuture< CesiumUtility::ResultPointer< TAssetType > > getOrCreate(const TContext &context, const TAssetKey &assetKey)
Gets an asset from the depot if it already exists, or creates it using the depot's factory if it does...
size_t getInactiveAssetCount() const
Gets the number of assets owned by this depot that are inactive, meaning that they are not currently ...
bool invalidate(const TAssetKey &assetKey)
Invalidates the previously-cached asset with the given key, so that the next call to getOrCreate will...
size_t getAssetCount() const
Returns the total number of distinct assets contained in this depot, including both active and inacti...
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.
An asset that is potentially shared between multiple objects, such as an image shared between multipl...
Definition SharedAsset.h:55
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
DoublyLinkedListAdvanced< T, T, Pointers > DoublyLinkedList
An intrusive doubly-linked list.
ReferenceCounted< T, true > ReferenceCountedThreadSafe
A reference-counted base class, meant to be used with IntrusivePointer. The reference count is thread...
STL namespace.
The default context passed to SharedAssetDepot factory functions.
std::shared_ptr< IAssetAccessor > pAssetAccessor
The asset accessor.
AsyncSystem asyncSystem
The async system.
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