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