// Copyright Epic Games, Inc. All Rights Reserved. #include "DerivedDataCache.h" #include "DerivedDataCacheInterface.h" #include "Algo/Accumulate.h" #include "Algo/AllOf.h" #include "Algo/BinarySearch.h" #include "Algo/Sort.h" #include "Async/AsyncWork.h" #include "Async/TaskGraphInterfaces.h" #include "Containers/Map.h" #include "Containers/StringConv.h" #include "DDCCleanup.h" #include "DerivedDataBackendInterface.h" #include "DerivedDataCache.h" #include "DerivedDataCacheMaintainer.h" #include "DerivedDataCachePrivate.h" #include "DerivedDataCacheUsageStats.h" #include "DerivedDataPluginInterface.h" #include "DerivedDataRequestOwner.h" #include "Features/IModularFeatures.h" #include "HAL/ThreadSafeCounter.h" #include "Misc/CommandLine.h" #include "Misc/CoreMisc.h" #include "Misc/ScopeLock.h" #include "ProfilingDebugging/CookStats.h" #include "Serialization/CompactBinary.h" #include "Serialization/CompactBinaryWriter.h" #include "Stats/Stats.h" #include "Stats/StatsMisc.h" #include "String/ParseTokens.h" #include "ZenServerInterface.h" #include DEFINE_STAT(STAT_DDC_NumGets); DEFINE_STAT(STAT_DDC_NumPuts); DEFINE_STAT(STAT_DDC_NumBuilds); DEFINE_STAT(STAT_DDC_NumExist); DEFINE_STAT(STAT_DDC_SyncGetTime); DEFINE_STAT(STAT_DDC_ASyncWaitTime); DEFINE_STAT(STAT_DDC_PutTime); DEFINE_STAT(STAT_DDC_SyncBuildTime); DEFINE_STAT(STAT_DDC_ExistTime); //#define DDC_SCOPE_CYCLE_COUNTER(x) QUICK_SCOPE_CYCLE_COUNTER(STAT_ ## x) #define DDC_SCOPE_CYCLE_COUNTER(x) TRACE_CPUPROFILER_EVENT_SCOPE(x); #if ENABLE_COOK_STATS #include "DerivedDataCacheUsageStats.h" namespace UE::DerivedData::CookStats { // Use to prevent potential divide by zero issues inline double SafeDivide(const int64 Numerator, const int64 Denominator) { return Denominator != 0 ? (double)Numerator / (double)Denominator : 0.0; } // AddCookStats cannot be a lambda because of false positives in static analysis. // See https://developercommunity.visualstudio.com/content/problem/576913/c6244-regression-in-new-lambda-processorpermissive.html static void AddCookStats(FCookStatsManager::AddStatFuncRef AddStat) { PRAGMA_DISABLE_DEPRECATION_WARNINGS; TSharedRef RootNode = GetDerivedDataCacheRef().GatherUsageStats(); PRAGMA_ENABLE_DEPRECATION_WARNINGS; { const FString StatName(TEXT("DDC.Usage")); for (const auto& UsageStatPair : RootNode->ToLegacyUsageMap()) { UsageStatPair.Value.LogStats(AddStat, StatName, UsageStatPair.Key); } } TArray> Nodes; RootNode->ForEachDescendant([&Nodes](TSharedRef Node) { if (Node->Children.IsEmpty()) { Nodes.Add(Node); } }); // Now lets add some summary data to that applies some crazy knowledge of how we set up our DDC. The goal // is to print out the global hit rate, and the hit rate of the local and shared DDC. // This is done by adding up the total get/miss calls the root node receives. // Then we find the FileSystem nodes that correspond to the local and shared cache using some hacky logic to detect a "network drive". // If the DDC graph ever contains more than one local or remote filesystem, this will only find one of them. { const TSharedRef* LocalNode = Nodes.FindByPredicate([](TSharedRef Node) { return Node->GetCacheType() == TEXT("File System") && Node->IsLocal(); }); const TSharedRef* SharedNode = Nodes.FindByPredicate([](TSharedRef Node) { return Node->GetCacheType() == TEXT("File System") && !Node->IsLocal(); }); const TSharedRef* CloudNode = Nodes.FindByPredicate([](TSharedRef Node) { return Node->GetCacheType() == TEXT("Horde Storage"); }); const TSharedRef* ZenLocalNode = Nodes.FindByPredicate([](TSharedRef Node) { return Node->GetCacheType() == TEXT("Zen") && Node->IsLocal(); }); const TSharedRef* ZenRemoteNode = Nodes.FindByPredicate([](TSharedRef Node) { return (Node->GetCacheType() == TEXT("Zen") || Node->GetCacheType() == TEXT("Horde")) && !Node->IsLocal(); }); const FDerivedDataCacheUsageStats& RootStats = RootNode->Stats.CreateConstIterator().Value(); const int64 TotalGetHits = RootStats.GetStats.GetAccumulatedValueAnyThread(FCookStats::CallStats::EHitOrMiss::Hit, FCookStats::CallStats::EStatType::Counter); const int64 TotalGetMisses = RootStats.GetStats.GetAccumulatedValueAnyThread(FCookStats::CallStats::EHitOrMiss::Miss, FCookStats::CallStats::EStatType::Counter); const int64 TotalGets = TotalGetHits + TotalGetMisses; int64 LocalHits = 0; if (LocalNode) { const FDerivedDataCacheUsageStats& Stats = (*LocalNode)->Stats.CreateConstIterator().Value(); LocalHits += Stats.GetStats.GetAccumulatedValueAnyThread(FCookStats::CallStats::EHitOrMiss::Hit, FCookStats::CallStats::EStatType::Counter); } if (ZenLocalNode) { const FDerivedDataCacheUsageStats& Stats = (*ZenLocalNode)->Stats.CreateConstIterator().Value(); LocalHits += Stats.GetStats.GetAccumulatedValueAnyThread(FCookStats::CallStats::EHitOrMiss::Hit, FCookStats::CallStats::EStatType::Counter); } int64 SharedHits = 0; if (SharedNode) { // The shared DDC is only queried if the local one misses (or there isn't one). So it's hit rate is technically const FDerivedDataCacheUsageStats& Stats = (*SharedNode)->Stats.CreateConstIterator().Value(); SharedHits += Stats.GetStats.GetAccumulatedValueAnyThread(FCookStats::CallStats::EHitOrMiss::Hit, FCookStats::CallStats::EStatType::Counter); } if (ZenRemoteNode) { const FDerivedDataCacheUsageStats& Stats = (*ZenRemoteNode)->Stats.CreateConstIterator().Value(); SharedHits += Stats.GetStats.GetAccumulatedValueAnyThread(FCookStats::CallStats::EHitOrMiss::Hit, FCookStats::CallStats::EStatType::Counter); } int64 CloudHits = 0; if (CloudNode) { const FDerivedDataCacheUsageStats& Stats = (*CloudNode)->Stats.CreateConstIterator().Value(); CloudHits += Stats.GetStats.GetAccumulatedValueAnyThread(FCookStats::CallStats::EHitOrMiss::Hit, FCookStats::CallStats::EStatType::Counter); } const int64 TotalPutHits = RootStats.PutStats.GetAccumulatedValueAnyThread(FCookStats::CallStats::EHitOrMiss::Hit, FCookStats::CallStats::EStatType::Counter); const int64 TotalPutMisses = RootStats.PutStats.GetAccumulatedValueAnyThread(FCookStats::CallStats::EHitOrMiss::Miss, FCookStats::CallStats::EStatType::Counter); const int64 TotalPuts = TotalPutHits + TotalPutMisses; AddStat(TEXT("DDC.Summary"), FCookStatsManager::CreateKeyValueArray( TEXT("BackEnd"), FDerivedDataBackend::Get().GetGraphName(), TEXT("HasLocalCache"), LocalNode || ZenLocalNode, TEXT("HasSharedCache"), SharedNode || ZenRemoteNode, TEXT("HasCloudCache"), !!CloudNode, TEXT("HasZenCache"), ZenLocalNode || ZenRemoteNode, TEXT("TotalGetHits"), TotalGetHits, TEXT("TotalGets"), TotalGets, TEXT("TotalGetHitPct"), SafeDivide(TotalGetHits, TotalGets), TEXT("LocalGetHitPct"), SafeDivide(LocalHits, TotalGets), TEXT("SharedGetHitPct"), SafeDivide(SharedHits, TotalGets), TEXT("CloudGetHitPct"), SafeDivide(CloudHits, TotalGets), TEXT("OtherGetHitPct"), SafeDivide((TotalGetHits - LocalHits - SharedHits - CloudHits), TotalGets), TEXT("GetMissPct"), SafeDivide(TotalGetMisses, TotalGets), TEXT("TotalPutHits"), TotalPutHits, TEXT("TotalPuts"), TotalPuts, TEXT("TotalPutHitPct"), SafeDivide(TotalPutHits, TotalPuts), TEXT("PutMissPct"), SafeDivide(TotalPutMisses, TotalPuts) )); } } FCookStatsManager::FAutoRegisterCallback RegisterCookStats(AddCookStats); } #endif void GatherDerivedDataCacheResourceStats(TArray& DDCResourceStats); void GatherDerivedDataCacheSummaryStats(FDerivedDataCacheSummaryStats& DDCSummaryStats); /** Whether we want to verify the DDC (pass in -VerifyDDC on the command line)*/ bool GVerifyDDC = false; namespace UE::DerivedData::Private { class FCacheRecordPolicyShared; } namespace UE::DerivedData { namespace Private::CachePolicy { // TODO: Implement these as Ansi String instead to maximize the most-optimal path, see ParseCachePolicyImpl constexpr ANSICHAR DelimiterChar = ','; constexpr FStringView Delimiter = TEXTVIEW(","); constexpr FStringView None = TEXTVIEW("None"); constexpr FStringView QueryLocal = TEXTVIEW("QueryLocal"); constexpr FStringView QueryRemote = TEXTVIEW("QueryRemote"); constexpr FStringView Query = TEXTVIEW("Query"); constexpr FStringView StoreLocal = TEXTVIEW("StoreLocal"); constexpr FStringView StoreRemote = TEXTVIEW("StoreRemote"); constexpr FStringView Store = TEXTVIEW("Store"); constexpr FStringView SkipMeta = TEXTVIEW("SkipMeta"); constexpr FStringView SkipData = TEXTVIEW("SkipData"); constexpr FStringView PartialRecord = TEXTVIEW("PartialRecord"); constexpr FStringView KeepAlive = TEXTVIEW("KeepAlive"); constexpr FStringView Local = TEXTVIEW("Local"); constexpr FStringView Remote = TEXTVIEW("Remote"); constexpr FStringView Default = TEXTVIEW("Default"); constexpr FStringView Disable = TEXTVIEW("Disable"); const TMap TextToPolicy { {None, ECachePolicy::None}, {QueryLocal, ECachePolicy::QueryLocal}, {QueryRemote, ECachePolicy::QueryRemote}, {Query, ECachePolicy::Query}, {StoreLocal, ECachePolicy::StoreLocal}, {StoreRemote, ECachePolicy::StoreRemote}, {Store, ECachePolicy::Store}, {SkipMeta, ECachePolicy::SkipMeta}, {SkipData, ECachePolicy::SkipData}, {PartialRecord, ECachePolicy::PartialRecord}, {KeepAlive, ECachePolicy::KeepAlive}, {Local, ECachePolicy::Local}, {Remote, ECachePolicy::Remote}, {Default, ECachePolicy::Default}, {Disable, ECachePolicy::Disable} }; using FPolicyTextPair = TPair; const FPolicyTextPair FlagsToString[] { // Order of these Flags is important: we want the aliases before the atomic values, // and the bigger aliases first, to reduce the number of tokens we add { ECachePolicy::Default, Default }, { ECachePolicy::Remote, Remote }, { ECachePolicy::Local, Local }, { ECachePolicy::Store, Store }, { ECachePolicy::Query, Query }, // Order of Atomics doesn't matter, so arbitrarily we list them in enum order { ECachePolicy::QueryLocal, QueryLocal }, { ECachePolicy::QueryRemote, QueryRemote }, { ECachePolicy::StoreLocal, StoreLocal }, { ECachePolicy::StoreRemote, StoreRemote }, { ECachePolicy::SkipMeta, SkipMeta }, { ECachePolicy::SkipData, SkipData }, { ECachePolicy::PartialRecord, PartialRecord }, { ECachePolicy::KeepAlive, KeepAlive }, // None must come at the end of the array, to write out only if no others exist { ECachePolicy::None, None }, }; constexpr ECachePolicy KnownFlags = ECachePolicy::Default | ECachePolicy::SkipMeta | ECachePolicy::SkipData | ECachePolicy::KeepAlive | ECachePolicy::PartialRecord; } // namespace Private::CachePolicy template TStringBuilderBase& AppendToBuilderImpl( TStringBuilderBase& Builder, UE::DerivedData::ECachePolicy Policy) { // Remove any bits we don't recognize; write None if there are not any bits we recognize Policy = Policy & Private::CachePolicy::KnownFlags; for (const Private::CachePolicy::FPolicyTextPair& Pair : Private::CachePolicy::FlagsToString) { if (EnumHasAllFlags(Policy, Pair.Key)) { EnumRemoveFlags(Policy, Pair.Key); Builder << Pair.Value << Private::CachePolicy::DelimiterChar; if (Policy == ECachePolicy::None) { break; } } } Builder.RemoveSuffix(1); // Text will have been added by ECachePolicy::None if not by anything else return Builder; } FAnsiStringBuilderBase& operator<<(FAnsiStringBuilderBase& Builder, UE::DerivedData::ECachePolicy Policy) { return AppendToBuilderImpl(Builder, Policy); } FUtf8StringBuilderBase& operator<<(FUtf8StringBuilderBase& Builder, UE::DerivedData::ECachePolicy Policy) { return AppendToBuilderImpl(Builder, Policy); } FWideStringBuilderBase& operator<<(FWideStringBuilderBase& Builder, UE::DerivedData::ECachePolicy Policy) { return AppendToBuilderImpl(Builder, Policy); } template ECachePolicy ParseCachePolicyImpl(TStringView Text) { checkf(!Text.IsEmpty(), TEXT("Empty string is not valid input to ParseCachePolicy")); ECachePolicy Result = ECachePolicy::None; // TODO: Implement ParseTokens for FAnsiStringView so we can convert to Ansi instead of Wide auto WideText = StringCast(Text.GetData(), Text.Len()); UE::String::ParseTokens(WideText, Private::CachePolicy::DelimiterChar, [&Result](FStringView Token) { const ECachePolicy* TokenPolicy = Private::CachePolicy::TextToPolicy.Find(Token); if (TokenPolicy) { Result |= *TokenPolicy; } }); return Result; } ECachePolicy ParseCachePolicy(FAnsiStringView Text) { return ParseCachePolicyImpl(Text); } ECachePolicy ParseCachePolicy(FUtf8StringView Text) { return ParseCachePolicyImpl(Text); } ECachePolicy ParseCachePolicy(FWideStringView Text) { return ParseCachePolicyImpl(Text); } class Private::FCacheRecordPolicyShared final : public Private::ICacheRecordPolicyShared { public: inline void AddRef() const final { ReferenceCount.fetch_add(1, std::memory_order_relaxed); } inline void Release() const final { if (ReferenceCount.fetch_sub(1, std::memory_order_acq_rel) == 1) { delete this; } } inline TConstArrayView GetValuePolicies() const final { return Values; } inline void AddValuePolicy(const FCacheValuePolicy& Policy) final { Values.Add(Policy); } inline void Build() final { Algo::SortBy(Values, &FCacheValuePolicy::Id); } private: TArray> Values; mutable std::atomic ReferenceCount{0}; }; ECachePolicy FCacheRecordPolicy::GetValuePolicy(const FValueId& Id) const { if (Shared) { if (TConstArrayView Values = Shared->GetValuePolicies(); !Values.IsEmpty()) { if (int32 Index = Algo::BinarySearchBy(Values, Id, &FCacheValuePolicy::Id); Index != INDEX_NONE) { return Values[Index].Policy; } } } return DefaultValuePolicy; } FCacheRecordPolicy FCacheRecordPolicy::Transform(TFunctionRef Op) const { if (IsUniform()) { return Op(RecordPolicy); } FCacheRecordPolicyBuilder Builder(Op(DefaultValuePolicy)); for (const FCacheValuePolicy& Value : GetValuePolicies()) { Builder.AddValuePolicy({Value.Id, Op(Value.Policy)}); } return Builder.Build(); } void FCacheRecordPolicy::Save(FCbWriter& Writer) const { Writer.BeginObject(); { // The RecordPolicy is calculated from the ValuePolicies and does not need to be saved separately. Writer << "DefaultValuePolicy"_ASV << WriteToUtf8String<128>(GetDefaultValuePolicy()); if (!IsUniform()) { // FCacheRecordPolicyBuilder guarantees IsUniform -> non-empty GetValuePolicies. Small size penalty here if not. Writer.BeginArray("ValuePolicies"_ASV); { for (const FCacheValuePolicy& ValuePolicy : GetValuePolicies()) { // FCacheRecordPolicyBuilder is responsible for ensuring that each ValuePolicy != DefaultValuePolicy // If it lets any duplicates through we will incur a small serialization size penalty here Writer.BeginObject(); Writer << "Id"_ASV << ValuePolicy.Id; Writer << "Policy"_ASV << WriteToUtf8String<128>(ValuePolicy.Policy); Writer.EndObject(); } } Writer.EndArray(); } } Writer.EndObject(); } FCacheRecordPolicy FCacheRecordPolicy::Load(FCbObjectView Object, ECachePolicy DefaultPolicy) { FUtf8StringView PolicyText = Object["DefaultValuePolicy"_ASV].AsString(); ECachePolicy DefaultValuePolicy = !PolicyText.IsEmpty() ? ParseCachePolicy(PolicyText) : DefaultPolicy; FCacheRecordPolicyBuilder Builder(DefaultValuePolicy); for (FCbFieldView ValueObjectField : Object["ValuePolicies"_ASV]) { FCbObjectView ValueObject = ValueObjectField.AsObjectView(); const FCbObjectId ValueId = ValueObject["Id"_ASV].AsObjectId(); PolicyText = ValueObject["Policy"_ASV].AsString(); ECachePolicy ValuePolicy = !PolicyText.IsEmpty() ? ParseCachePolicy(PolicyText) : DefaultValuePolicy; // FCacheRecordPolicyBuilder should guarantee that FValueId(ValueId).IsValid and ValuePolicy != DefaultValuePolicy // If it lets any through we will have unused data in the record we create. Builder.AddValuePolicy(ValueId, ValuePolicy); } return Builder.Build(); } void FCacheRecordPolicyBuilder::AddValuePolicy(const FCacheValuePolicy& Policy) { if (!Shared) { Shared = new Private::FCacheRecordPolicyShared; } Shared->AddValuePolicy(Policy); } FCacheRecordPolicy FCacheRecordPolicyBuilder::Build() { FCacheRecordPolicy Policy(BasePolicy); if (Shared) { Shared->Build(); const auto PolicyOr = [](ECachePolicy A, ECachePolicy B) { return A | (B & ~ECachePolicy::SkipData); }; const TConstArrayView Values = Shared->GetValuePolicies(); Policy.RecordPolicy = Algo::TransformAccumulate(Values, &FCacheValuePolicy::Policy, BasePolicy, PolicyOr); Policy.Shared = MoveTemp(Shared); } return Policy; } void ICacheStore::PutValue( const TConstArrayView Requests, IRequestOwner& Owner, FOnCachePutValueComplete&& OnComplete) { if (OnComplete) { for (const FCachePutValueRequest& Request : Requests) { OnComplete({Request.Name, Request.Key, Request.UserData, EStatus::Error}); } } } void ICacheStore::GetValue( const TConstArrayView Requests, IRequestOwner& Owner, FOnCacheGetValueComplete&& OnComplete) { if (OnComplete) { for (const FCacheGetValueRequest& Request : Requests) { OnComplete({Request.Name, Request.Key, {}, Request.UserData, EStatus::Error}); } } } FCachePutResponse FCachePutRequest::MakeResponse(const EStatus Status) const { return {Name, Record.GetKey(), UserData, Status}; } FCacheGetResponse FCacheGetRequest::MakeResponse(const EStatus Status) const { return {Name, FCacheRecordBuilder(Key).Build(), UserData, Status}; } FCachePutValueResponse FCachePutValueRequest::MakeResponse(const EStatus Status) const { return {Name, Key, UserData, Status}; } FCacheGetValueResponse FCacheGetValueRequest::MakeResponse(const EStatus Status) const { return {Name, Key, {}, UserData, Status}; } FCacheGetChunkResponse FCacheGetChunkRequest::MakeResponse(const EStatus Status) const { return {Name, Key, Id, RawOffset, 0, {}, {}, UserData, Status}; } } // UE::DerivedData namespace UE::DerivedData::Private { FQueuedThreadPool* GCacheThreadPool; /** * Implementation of the derived data cache * This API is fully threadsafe **/ class FDerivedDataCache final : public FDerivedDataCacheInterface , public ICache , public ICacheStoreMaintainer , public IDDCCleanup { /** * Async worker that checks the cache backend and if that fails, calls the deriver to build the data and then puts the results to the cache **/ friend class FBuildAsyncWorker; class FBuildAsyncWorker : public FNonAbandonableTask { public: enum EWorkerState : uint32 { WorkerStateNone = 0, WorkerStateRunning = 1 << 0, WorkerStateFinished = 1 << 1, WorkerStateDestroyed = 1 << 2, }; /** * Constructor for async task * @param InDataDeriver plugin to produce cache key and in the event of a miss, return the data. * @param InCacheKey Complete cache key for this data. **/ FBuildAsyncWorker(FDerivedDataPluginInterface* InDataDeriver, const TCHAR* InCacheKey, FStringView InDebugContext, bool bInSynchronousForStats) : bSuccess(false) , bSynchronousForStats(bInSynchronousForStats) , bDataWasBuilt(false) , DataDeriver(InDataDeriver) , CacheKey(InCacheKey) , DebugContext(InDebugContext) { } virtual ~FBuildAsyncWorker() { // Record that the task is destroyed and check that it was not running or destroyed previously. { const uint32 PreviousState = WorkerState.fetch_or(WorkerStateDestroyed, std::memory_order_relaxed); checkf(!(PreviousState & WorkerStateRunning), TEXT("Destroying DDC worker that is still running! Key: %s"), *CacheKey); checkf(!(PreviousState & WorkerStateDestroyed), TEXT("Destroying DDC worker that has been destroyed previously! Key: %s"), *CacheKey); } } /** Async worker that checks the cache backend and if that fails, calls the deriver to build the data and then puts the results to the cache **/ void DoWork() { // Record that the task is running and check that it was not running, finished, or destroyed previously. { const uint32 PreviousState = WorkerState.fetch_or(WorkerStateRunning, std::memory_order_relaxed); checkf(!(PreviousState & WorkerStateRunning), TEXT("Starting DDC worker that is already running! Key: %s"), *CacheKey); checkf(!(PreviousState & WorkerStateFinished), TEXT("Starting DDC worker that is already finished! Key: %s"), *CacheKey); checkf(!(PreviousState & WorkerStateDestroyed), TEXT("Starting DDC worker that has been destroyed! Key: %s"), *CacheKey); } TRACE_CPUPROFILER_EVENT_SCOPE(DDC_DoWork); const int32 NumBeforeDDC = Data.Num(); bool bGetResult; { TRACE_CPUPROFILER_EVENT_SCOPE(DDC_Get); INC_DWORD_STAT(STAT_DDC_NumGets); STAT(double ThisTime = 0); { SCOPE_SECONDS_COUNTER(ThisTime); FLegacyCacheGetRequest LegacyRequest; LegacyRequest.Name = DebugContext; LegacyRequest.Key = FLegacyCacheKey(CacheKey, FDerivedDataBackend::Get().GetMaxKeyLength()); FRequestOwner BlockingOwner(EPriority::Blocking); FDerivedDataBackend::Get().GetRoot().LegacyGet({LegacyRequest}, BlockingOwner, [this, &bGetResult](FLegacyCacheGetResponse&& Response) { bGetResult = Response.Status == EStatus::Ok && Response.Value.GetSize() < MAX_int32; if (bGetResult) { Data = MakeArrayView(static_cast(Response.Value.GetData()), int32(Response.Value.GetSize())); } }); BlockingOwner.Wait(); } INC_FLOAT_STAT_BY(STAT_DDC_SyncGetTime, bSynchronousForStats ? (float)ThisTime : 0.0f); } if (bGetResult) { if(GVerifyDDC && DataDeriver && DataDeriver->IsDeterministic()) { TArray CmpData; DataDeriver->Build(CmpData); const int32 NumInDDC = Data.Num() - NumBeforeDDC; const int32 NumGenerated = CmpData.Num(); bool bMatchesInSize = NumGenerated == NumInDDC; bool bDifferentMemory = true; int32 DifferentOffset = 0; if (bMatchesInSize) { bDifferentMemory = false; for (int32 i = 0; i < NumGenerated; i++) { if (CmpData[i] != Data[i]) { bDifferentMemory = true; DifferentOffset = i; break; } } } if(!bMatchesInSize || bDifferentMemory) { FString ErrMsg = FString::Printf(TEXT("There is a mismatch between the DDC data and the generated data for plugin (%s) for asset (%s). BytesInDDC:%d, BytesGenerated:%d, bDifferentMemory:%d, offset:%d"), DataDeriver->GetPluginName(), *DataDeriver->GetDebugContextString(), NumInDDC, NumGenerated, bDifferentMemory, DifferentOffset); ensureMsgf(false, TEXT("%s"), *ErrMsg); UE_LOG(LogDerivedDataCache, Error, TEXT("%s"), *ErrMsg ); } } check(Data.Num()); bSuccess = true; delete DataDeriver; DataDeriver = NULL; } else if (DataDeriver) { { TRACE_CPUPROFILER_EVENT_SCOPE(DDC_Build); INC_DWORD_STAT(STAT_DDC_NumBuilds); STAT(double ThisTime = 0); { SCOPE_SECONDS_COUNTER(ThisTime); bSuccess = DataDeriver->Build(Data); bDataWasBuilt = true; } INC_FLOAT_STAT_BY(STAT_DDC_SyncBuildTime, bSynchronousForStats ? (float)ThisTime : 0.0f); } delete DataDeriver; DataDeriver = NULL; if (bSuccess) { check(Data.Num()); TRACE_CPUPROFILER_EVENT_SCOPE(DDC_Put); INC_DWORD_STAT(STAT_DDC_NumPuts); STAT(double ThisTime = 0); { SCOPE_SECONDS_COUNTER(ThisTime); FLegacyCachePutRequest LegacyRequest; LegacyRequest.Name = DebugContext; LegacyRequest.Key = FLegacyCacheKey(CacheKey, FDerivedDataBackend::Get().GetMaxKeyLength()); LegacyRequest.Value = FCompositeBuffer(FSharedBuffer::Clone(MakeMemoryView(Data))); FRequestOwner BlockingOwner(EPriority::Blocking); FDerivedDataBackend::Get().GetRoot().LegacyPut({LegacyRequest}, BlockingOwner, [](auto&&){}); BlockingOwner.Wait(); } INC_FLOAT_STAT_BY(STAT_DDC_PutTime, bSynchronousForStats ? (float)ThisTime : 0.0f); } } if (!bSuccess) { Data.Empty(); } FDerivedDataBackend::Get().AddToAsyncCompletionCounter(-1); // Record that the task is finished and check that it was running and not finished or destroyed previously. { const uint32 PreviousState = WorkerState.fetch_xor(WorkerStateRunning | WorkerStateFinished, std::memory_order_relaxed); checkf((PreviousState & WorkerStateRunning), TEXT("Finishing DDC worker that was not running! Key: %s"), *CacheKey); checkf(!(PreviousState & WorkerStateFinished), TEXT("Finishing DDC worker that is already finished! Key: %s"), *CacheKey); checkf(!(PreviousState & WorkerStateDestroyed), TEXT("Finishing DDC worker that has been destroyed! Key: %s"), *CacheKey); } } FORCEINLINE TStatId GetStatId() const { RETURN_QUICK_DECLARE_CYCLE_STAT(FBuildAsyncWorker, STATGROUP_ThreadPoolAsyncTasks); } std::atomic WorkerState{WorkerStateNone}; /** true in the case of a cache hit, otherwise the result of the deriver build call **/ bool bSuccess; /** true if we should record the timing **/ bool bSynchronousForStats; /** true if we had to build the data */ bool bDataWasBuilt; /** Data dervier we are operating on **/ FDerivedDataPluginInterface* DataDeriver; /** Cache key associated with this build **/ FString CacheKey; /** Context from the caller */ FSharedString DebugContext; /** Data to return to caller, later **/ TArray Data; }; public: /** Constructor, called once to cereate a singleton **/ FDerivedDataCache() : CurrentHandle(19248) // we will skip some potential handles to catch errors { if (FPlatformProcess::SupportsMultithreading()) { GCacheThreadPool = FQueuedThreadPool::Allocate(); const int32 ThreadCount = FPlatformMisc::NumberOfIOWorkerThreadsToSpawn(); verify(GCacheThreadPool->Create(ThreadCount, 96 * 1024, TPri_AboveNormal, TEXT("DDC IO ThreadPool"))); } FDerivedDataBackend::Get(); // we need to make sure this starts before we allow us to start CacheStoreMaintainers = IModularFeatures::Get().GetModularFeatureImplementations(FeatureName); GVerifyDDC = FParse::Param(FCommandLine::Get(), TEXT("VerifyDDC")); UE_CLOG(GVerifyDDC, LogDerivedDataCache, Display, TEXT("Items retrieved from the DDC will be verified (-VerifyDDC)")); } /** Destructor, flushes all sync tasks **/ ~FDerivedDataCache() { WaitForQuiescence(true); FScopeLock ScopeLock(&SynchronizationObject); for (TMap*>::TIterator It(PendingTasks); It; ++It) { It.Value()->EnsureCompletion(); delete It.Value(); } PendingTasks.Empty(); } virtual bool GetSynchronous(FDerivedDataPluginInterface* DataDeriver, TArray& OutData, bool* bDataWasBuilt = nullptr) override { DDC_SCOPE_CYCLE_COUNTER(DDC_GetSynchronous); check(DataDeriver); FString CacheKey = FDerivedDataCache::BuildCacheKey(DataDeriver); UE_LOG(LogDerivedDataCache, VeryVerbose, TEXT("GetSynchronous %s from '%s'"), *CacheKey, *DataDeriver->GetDebugContextString()); FAsyncTask PendingTask(DataDeriver, *CacheKey, DataDeriver->GetDebugContextString(), true); AddToAsyncCompletionCounter(1); PendingTask.StartSynchronousTask(); OutData = PendingTask.GetTask().Data; if (bDataWasBuilt) { *bDataWasBuilt = PendingTask.GetTask().bDataWasBuilt; } return PendingTask.GetTask().bSuccess; } virtual uint32 GetAsynchronous(FDerivedDataPluginInterface* DataDeriver) override { DDC_SCOPE_CYCLE_COUNTER(DDC_GetAsynchronous); FScopeLock ScopeLock(&SynchronizationObject); const uint32 Handle = NextHandle(); FString CacheKey = FDerivedDataCache::BuildCacheKey(DataDeriver); UE_LOG(LogDerivedDataCache, VeryVerbose, TEXT("GetAsynchronous %s from '%s', Handle %d"), *CacheKey, *DataDeriver->GetDebugContextString(), Handle); const bool bSync = !DataDeriver->IsBuildThreadsafe(); FAsyncTask* AsyncTask = new FAsyncTask(DataDeriver, *CacheKey, DataDeriver->GetDebugContextString(), bSync); check(!PendingTasks.Contains(Handle)); PendingTasks.Add(Handle,AsyncTask); AddToAsyncCompletionCounter(1); if (!bSync) { AsyncTask->StartBackgroundTask(DataDeriver->GetCustomThreadPool()); } else { AsyncTask->StartSynchronousTask(); } // Must return a valid handle check(Handle != 0); return Handle; } virtual bool PollAsynchronousCompletion(uint32 Handle) override { DDC_SCOPE_CYCLE_COUNTER(DDC_PollAsynchronousCompletion); FAsyncTask* AsyncTask = NULL; { FScopeLock ScopeLock(&SynchronizationObject); AsyncTask = PendingTasks.FindRef(Handle); } check(AsyncTask); return AsyncTask->IsDone(); } virtual void WaitAsynchronousCompletion(uint32 Handle) override { DDC_SCOPE_CYCLE_COUNTER(DDC_WaitAsynchronousCompletion); STAT(double ThisTime = 0); { SCOPE_SECONDS_COUNTER(ThisTime); FAsyncTask* AsyncTask = NULL; { FScopeLock ScopeLock(&SynchronizationObject); AsyncTask = PendingTasks.FindRef(Handle); } check(AsyncTask); AsyncTask->EnsureCompletion(); UE_LOG(LogDerivedDataCache, Verbose, TEXT("WaitAsynchronousCompletion, Handle %d"), Handle); } INC_FLOAT_STAT_BY(STAT_DDC_ASyncWaitTime,(float)ThisTime); } virtual bool GetAsynchronousResults(uint32 Handle, TArray& OutData, bool* bOutDataWasBuilt = nullptr) override { DDC_SCOPE_CYCLE_COUNTER(DDC_GetAsynchronousResults); FAsyncTask* AsyncTask = NULL; { FScopeLock ScopeLock(&SynchronizationObject); PendingTasks.RemoveAndCopyValue(Handle,AsyncTask); } check(AsyncTask); const bool bDataWasBuilt = AsyncTask->GetTask().bDataWasBuilt; if (bOutDataWasBuilt) { *bOutDataWasBuilt = bDataWasBuilt; } if (!AsyncTask->GetTask().bSuccess) { UE_LOG(LogDerivedDataCache, Verbose, TEXT("GetAsynchronousResults, bDataWasBuilt: %d, Handle %d, FAILED"), (int32)bDataWasBuilt, Handle); delete AsyncTask; return false; } UE_LOG(LogDerivedDataCache, Verbose, TEXT("GetAsynchronousResults, bDataWasBuilt: %d, Handle %d, SUCCESS"), (int32)bDataWasBuilt, Handle); OutData = MoveTemp(AsyncTask->GetTask().Data); delete AsyncTask; check(OutData.Num()); return true; } virtual bool GetSynchronous(const TCHAR* CacheKey, TArray& OutData, FStringView DebugContext) override { DDC_SCOPE_CYCLE_COUNTER(DDC_GetSynchronous_Data); UE_LOG(LogDerivedDataCache, VeryVerbose, TEXT("GetSynchronous %s from '%.*s'"), CacheKey, DebugContext.Len(), DebugContext.GetData()); ValidateCacheKey(CacheKey); FAsyncTask PendingTask((FDerivedDataPluginInterface*)NULL, CacheKey, DebugContext, true); AddToAsyncCompletionCounter(1); PendingTask.StartSynchronousTask(); OutData = PendingTask.GetTask().Data; return PendingTask.GetTask().bSuccess; } virtual uint32 GetAsynchronous(const TCHAR* CacheKey, FStringView DebugContext) override { DDC_SCOPE_CYCLE_COUNTER(DDC_GetAsynchronous_Handle); FScopeLock ScopeLock(&SynchronizationObject); const uint32 Handle = NextHandle(); UE_LOG(LogDerivedDataCache, VeryVerbose, TEXT("GetAsynchronous %s from '%.*s', Handle %d"), CacheKey, DebugContext.Len(), DebugContext.GetData(), Handle); ValidateCacheKey(CacheKey); FAsyncTask* AsyncTask = new FAsyncTask((FDerivedDataPluginInterface*)NULL, CacheKey, DebugContext, false); check(!PendingTasks.Contains(Handle)); PendingTasks.Add(Handle, AsyncTask); AddToAsyncCompletionCounter(1); // This request is I/O only, doesn't do any processing, send it to the I/O only thread-pool to avoid wasting worker threads on long I/O waits. AsyncTask->StartBackgroundTask(GCacheThreadPool); return Handle; } virtual void Put(const TCHAR* CacheKey, TArrayView Data, FStringView DebugContext, bool bPutEvenIfExists = false) override { DDC_SCOPE_CYCLE_COUNTER(DDC_Put); UE_LOG(LogDerivedDataCache, VeryVerbose, TEXT("Put %s from '%.*s'"), CacheKey, DebugContext.Len(), DebugContext.GetData()); ValidateCacheKey(CacheKey); STAT(double ThisTime = 0); { SCOPE_SECONDS_COUNTER(ThisTime); FLegacyCachePutRequest LegacyRequest; LegacyRequest.Name = DebugContext; LegacyRequest.Key = FLegacyCacheKey(CacheKey, FDerivedDataBackend::Get().GetMaxKeyLength()); LegacyRequest.Value = FCompositeBuffer(FSharedBuffer::Clone(MakeMemoryView(Data))); FRequestOwner BlockingOwner(EPriority::Blocking); FDerivedDataBackend::Get().GetRoot().LegacyPut({LegacyRequest}, BlockingOwner, [](auto&&){}); BlockingOwner.Wait(); } INC_FLOAT_STAT_BY(STAT_DDC_PutTime,(float)ThisTime); INC_DWORD_STAT(STAT_DDC_NumPuts); } virtual void MarkTransient(const TCHAR* CacheKey) override { ValidateCacheKey(CacheKey); FLegacyCacheDeleteRequest LegacyRequest; LegacyRequest.Key = FLegacyCacheKey(CacheKey, FDerivedDataBackend::Get().GetMaxKeyLength()); LegacyRequest.Name = LegacyRequest.Key.GetFullKey(); LegacyRequest.bTransient = true; FRequestOwner BlockingOwner(EPriority::Blocking); FDerivedDataBackend::Get().GetRoot().LegacyDelete({LegacyRequest}, BlockingOwner, [](auto&&){}); BlockingOwner.Wait(); } virtual bool CachedDataProbablyExists(const TCHAR* CacheKey) override { DDC_SCOPE_CYCLE_COUNTER(DDC_CachedDataProbablyExists); ValidateCacheKey(CacheKey); bool bResult; INC_DWORD_STAT(STAT_DDC_NumExist); STAT(double ThisTime = 0); { SCOPE_SECONDS_COUNTER(ThisTime); FLegacyCacheGetRequest LegacyRequest; LegacyRequest.Key = FLegacyCacheKey(CacheKey, FDerivedDataBackend::Get().GetMaxKeyLength()); LegacyRequest.Name = LegacyRequest.Key.GetFullKey(); LegacyRequest.Policy = ECachePolicy::Query | ECachePolicy::SkipData; FRequestOwner BlockingOwner(EPriority::Blocking); FDerivedDataBackend::Get().GetRoot().LegacyGet({LegacyRequest}, BlockingOwner, [&bResult](FLegacyCacheGetResponse&& Response) { bResult = Response.Status == EStatus::Ok; }); BlockingOwner.Wait(); } INC_FLOAT_STAT_BY(STAT_DDC_ExistTime, (float)ThisTime); return bResult; } virtual TBitArray<> CachedDataProbablyExistsBatch(TConstArrayView CacheKeys) override { TBitArray<> Result(false, CacheKeys.Num()); if (!CacheKeys.IsEmpty()) { DDC_SCOPE_CYCLE_COUNTER(DDC_CachedDataProbablyExistsBatch); INC_DWORD_STAT(STAT_DDC_NumExist); STAT(double ThisTime = 0); { SCOPE_SECONDS_COUNTER(ThisTime); TArray> LegacyRequests; int32 Index = 0; for (const FString& CacheKey : CacheKeys) { FLegacyCacheGetRequest& LegacyRequest = LegacyRequests.AddDefaulted_GetRef(); LegacyRequest.Key = FLegacyCacheKey(CacheKey, FDerivedDataBackend::Get().GetMaxKeyLength()); LegacyRequest.Name = LegacyRequest.Key.GetFullKey(); LegacyRequest.Policy = ECachePolicy::Query | ECachePolicy::SkipData; LegacyRequest.UserData = uint64(Index); ++Index; } FRequestOwner BlockingOwner(EPriority::Blocking); FDerivedDataBackend::Get().GetRoot().LegacyGet(LegacyRequests, BlockingOwner, [&Result](FLegacyCacheGetResponse&& Response) { Result[int32(Response.UserData)] = Response.Status == EStatus::Ok; }); BlockingOwner.Wait(); } INC_FLOAT_STAT_BY(STAT_DDC_ExistTime, (float)ThisTime); } return Result; } virtual bool AllCachedDataProbablyExists(TConstArrayView CacheKeys) override { return CacheKeys.Num() == 0 || CachedDataProbablyExistsBatch(CacheKeys).CountSetBits() == CacheKeys.Num(); } virtual bool TryToPrefetch(TConstArrayView CacheKeys, FStringView DebugContext) override { if (!CacheKeys.IsEmpty()) { DDC_SCOPE_CYCLE_COUNTER(DDC_TryToPrefetch); UE_LOG(LogDerivedDataCache, VeryVerbose, TEXT("TryToPrefetch %d keys including %s from '%.*s'"), CacheKeys.Num(), *CacheKeys[0], DebugContext.Len(), DebugContext.GetData()); TArray> LegacyRequests; int32 Index = 0; const FSharedString Name = DebugContext; for (const FString& CacheKey : CacheKeys) { FLegacyCacheGetRequest& LegacyRequest = LegacyRequests.AddDefaulted_GetRef(); LegacyRequest.Name = Name; LegacyRequest.Key = FLegacyCacheKey(CacheKey, FDerivedDataBackend::Get().GetMaxKeyLength()); LegacyRequest.Policy = ECachePolicy::Default | ECachePolicy::SkipData; LegacyRequest.UserData = uint64(Index); ++Index; } bool bOk = true; FRequestOwner BlockingOwner(EPriority::Blocking); FDerivedDataBackend::Get().GetRoot().LegacyGet(LegacyRequests, BlockingOwner, [&bOk](FLegacyCacheGetResponse&& Response) { bOk &= Response.Status == EStatus::Ok; }); BlockingOwner.Wait(); return bOk; } return true; } void NotifyBootComplete() override { DDC_SCOPE_CYCLE_COUNTER(DDC_NotifyBootComplete); FDerivedDataBackend::Get().NotifyBootComplete(); } void AddToAsyncCompletionCounter(int32 Addend) override { FDerivedDataBackend::Get().AddToAsyncCompletionCounter(Addend); } bool AnyAsyncRequestsRemaining() const override { return FDerivedDataBackend::Get().AnyAsyncRequestsRemaining(); } void WaitForQuiescence(bool bShutdown) override { DDC_SCOPE_CYCLE_COUNTER(DDC_WaitForQuiescence); FDerivedDataBackend::Get().WaitForQuiescence(bShutdown); } /** Get whether a Shared Data Cache is in use */ virtual bool GetUsingSharedDDC() const override { return FDerivedDataBackend::Get().GetUsingSharedDDC(); } virtual const TCHAR* GetGraphName() const override { return FDerivedDataBackend::Get().GetGraphName(); } virtual const TCHAR* GetDefaultGraphName() const override { return FDerivedDataBackend::Get().GetDefaultGraphName(); } void GetDirectories(TArray& OutResults) override { FDerivedDataBackend::Get().GetDirectories(OutResults); } PRAGMA_DISABLE_DEPRECATION_WARNINGS virtual IDDCCleanup* GetCleanup() const override { return const_cast(static_cast(this)); } PRAGMA_ENABLE_DEPRECATION_WARNINGS virtual bool IsFinished() const override { return IsIdle(); } virtual void WaitBetweenDeletes(bool bWait) override { if (!bWait) { BoostPriority(); } } virtual void GatherUsageStats(TMap& UsageStats) override { GatherUsageStats()->GatherLegacyUsageStats(UsageStats, TEXT(" 0")); } PRAGMA_DISABLE_DEPRECATION_WARNINGS virtual TSharedRef GatherUsageStats() const override { return FDerivedDataBackend::Get().GatherUsageStats(); } PRAGMA_ENABLE_DEPRECATION_WARNINGS virtual void GatherResourceStats(TArray& DDCResourceStats) const override { GatherDerivedDataCacheResourceStats(DDCResourceStats); } virtual void GatherSummaryStats(FDerivedDataCacheSummaryStats& DDCSummaryStats) const override { GatherDerivedDataCacheSummaryStats(DDCSummaryStats); } /** Get event delegate for data cache notifications */ virtual FOnDDCNotification& GetDDCNotificationEvent() { return DDCNotificationEvent; } protected: uint32 NextHandle() { return (uint32)CurrentHandle.Increment(); } private: /** * Internal function to build a cache key out of the plugin name, versions and plugin specific info * @param DataDeriver plugin to produce the elements of the cache key. * @return Assembled cache key **/ static FString BuildCacheKey(FDerivedDataPluginInterface* DataDeriver) { FString Result = FDerivedDataCacheInterface::BuildCacheKey(DataDeriver->GetPluginName(), DataDeriver->GetVersionString(), *DataDeriver->GetPluginSpecificCacheKeySuffix()); return Result; } static void ValidateCacheKey(const TCHAR* CacheKey) { checkf(Algo::AllOf(FStringView(CacheKey), IsValidCacheChar), TEXT("Invalid characters in cache key %s. Use SanitizeCacheKey or BuildCacheKey to create valid keys."), CacheKey); } /** Counter used to produce unique handles **/ FThreadSafeCounter CurrentHandle; /** Object used for synchronization via a scoped lock **/ FCriticalSection SynchronizationObject; /** Map of handle to pending task **/ TMap*> PendingTasks; /** Cache notification delegate */ FOnDDCNotification DDCNotificationEvent; public: // ICache Interface void Put( TConstArrayView Requests, IRequestOwner& Owner, FOnCachePutComplete&& OnComplete) final { return FDerivedDataBackend::Get().GetRoot().Put(Requests, Owner, OnComplete ? MoveTemp(OnComplete) : [](auto&&){}); } void Get( TConstArrayView Requests, IRequestOwner& Owner, FOnCacheGetComplete&& OnComplete) final { return FDerivedDataBackend::Get().GetRoot().Get(Requests, Owner, OnComplete ? MoveTemp(OnComplete) : [](auto&&){}); } void PutValue( TConstArrayView Requests, IRequestOwner& Owner, FOnCachePutValueComplete&& OnComplete) final { return FDerivedDataBackend::Get().GetRoot().PutValue(Requests, Owner, OnComplete ? MoveTemp(OnComplete) : [](auto&&){}); } void GetValue( TConstArrayView Requests, IRequestOwner& Owner, FOnCacheGetValueComplete&& OnComplete) final { return FDerivedDataBackend::Get().GetRoot().GetValue(Requests, Owner, OnComplete ? MoveTemp(OnComplete) : [](auto&&){}); } void GetChunks( TConstArrayView Requests, IRequestOwner& Owner, FOnCacheGetChunkComplete&& OnComplete) final { return FDerivedDataBackend::Get().GetRoot().GetChunks(Requests, Owner, OnComplete ? MoveTemp(OnComplete) : [](auto&&){}); } ICacheStoreMaintainer& GetMaintainer() final { return *this; } // ICacheStoreMaintainer Interface bool IsIdle() const final { return Algo::AllOf(CacheStoreMaintainers, &ICacheStoreMaintainer::IsIdle); } void BoostPriority() final { for (ICacheStoreMaintainer* Maintainer : CacheStoreMaintainers) { Maintainer->BoostPriority(); } } private: TArray CacheStoreMaintainers; }; ICache* CreateCache(FDerivedDataCacheInterface** OutLegacyCache) { FDerivedDataCache* Cache = new FDerivedDataCache; if (OutLegacyCache) { *OutLegacyCache = Cache; } return Cache; } } // UE::DerivedData::Private