cesium-native 0.43.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
32template <typename TAssetType, typename TAssetKey>
33class CESIUMASYNC_API SharedAssetDepot
35 SharedAssetDepot<TAssetType, TAssetKey>>,
36 public CesiumUtility::IDepotOwningAsset<TAssetType> {
37public:
50 int64_t inactiveAssetSizeLimitBytes = 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
122private:
123 struct LockHolder;
124
125 // Disable copy
126 void operator=(const SharedAssetDepot<TAssetType, TAssetKey>& other) = delete;
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 auto it = this->_assetsByPointer.find(const_cast<TAssetType*>(&asset));
452 CESIUM_ASSERT(it != this->_assetsByPointer.end());
453 if (it == this->_assetsByPointer.end()) {
454 return;
455 }
456
457 CESIUM_ASSERT(it->second != nullptr);
458
459 AssetEntry& entry = *it->second;
460 entry.sizeInDeletionList = asset.getSizeBytes();
461 this->_totalDeletionCandidateMemoryUsage += entry.sizeInDeletionList;
462
463 this->_deletionCandidates.insertAtTail(entry);
464
465 if (this->_totalDeletionCandidateMemoryUsage >
466 this->inactiveAssetSizeLimitBytes) {
467 // Delete the deletion candidates until we're below the limit.
468 while (this->_deletionCandidates.size() > 0 &&
469 this->_totalDeletionCandidateMemoryUsage >
470 this->inactiveAssetSizeLimitBytes) {
471 AssetEntry* pOldEntry = this->_deletionCandidates.head();
472 this->_deletionCandidates.remove(*pOldEntry);
473
474 this->_totalDeletionCandidateMemoryUsage -= pOldEntry->sizeInDeletionList;
475
476 CESIUM_ASSERT(
477 pOldEntry->pAsset == nullptr ||
478 pOldEntry->pAsset->_referenceCount == 0);
479
480 if (pOldEntry->pAsset) {
481 this->_assetsByPointer.erase(pOldEntry->pAsset.get());
482 }
483
484 // This will actually delete the asset.
485 this->_assets.erase(pOldEntry->key);
486 }
487 }
488
489 // If this depot is not managing any live assets, then we no longer need to
490 // keep it alive.
491 if (this->_assets.size() == this->_deletionCandidates.size()) {
492 this->_pKeepAlive.reset();
493 }
494}
495
496template <typename TAssetType, typename TAssetKey>
497void SharedAssetDepot<TAssetType, TAssetKey>::unmarkDeletionCandidate(
498 const TAssetType& asset,
499 bool threadOwnsDepotLock) {
500 if (threadOwnsDepotLock) {
501 this->unmarkDeletionCandidateUnderLock(asset);
502 } else {
503 LockHolder lock = this->lock();
504 this->unmarkDeletionCandidateUnderLock(asset);
505 }
506}
507
508template <typename TAssetType, typename TAssetKey>
509void SharedAssetDepot<TAssetType, TAssetKey>::unmarkDeletionCandidateUnderLock(
510 const TAssetType& asset) {
511 auto it = this->_assetsByPointer.find(const_cast<TAssetType*>(&asset));
512 CESIUM_ASSERT(it != this->_assetsByPointer.end());
513 if (it == this->_assetsByPointer.end()) {
514 return;
515 }
516
517 CESIUM_ASSERT(it->second != nullptr);
518
519 AssetEntry& entry = *it->second;
520 bool isFound = this->_deletionCandidates.contains(entry);
521
522 CESIUM_ASSERT(isFound);
523
524 if (isFound) {
525 this->_totalDeletionCandidateMemoryUsage -= entry.sizeInDeletionList;
526 this->_deletionCandidates.remove(entry);
527 }
528
529 // This depot is now managing at least one live asset, so keep it alive.
530 this->_pKeepAlive = this;
531}
532
533template <typename TAssetType, typename TAssetKey>
535SharedAssetDepot<TAssetType, TAssetKey>::AssetEntry::toResultUnderLock() const {
536 // This method is called while the calling thread already owns the depot
537 // mutex. So we must take care not to lock it again, which could happen if
538 // the asset is currently unreferenced and we naively create an
539 // IntrusivePointer for it.
541 if (pAsset) {
542 pAsset->addReference(true);
543 p = pAsset.get();
544 pAsset->releaseReference(true);
545 }
546 return CesiumUtility::ResultPointer<TAssetType>(p, errorsAndWarnings);
547}
548
549template <typename TAssetType, typename TAssetKey>
550SharedAssetDepot<TAssetType, TAssetKey>::LockHolder::LockHolder(
552 : pDepot(pDepot_), lock(pDepot_->_mutex) {}
553
554template <typename TAssetType, typename TAssetKey>
555SharedAssetDepot<TAssetType, TAssetKey>::LockHolder::~LockHolder() = default;
556
557template <typename TAssetType, typename TAssetKey>
558void SharedAssetDepot<TAssetType, TAssetKey>::LockHolder::unlock() {
559 this->lock.unlock();
560}
561
562} // 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:11
A promise that can be resolved or rejected by an asynchronous task.
Definition Promise.h:18
void resolve(T &&value) const
Will be called when the task completed successfully.
Definition Promise.h:25
Future< T > getFuture() const
Gets the Future that resolves or rejects when this Promise is resolved or rejected.
Definition Promise.h:61
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:17
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