// Copyright Epic Games, Inc. All Rights Reserved. #include "CoreTypes.h" #include "Containers/StringView.h" #include "DerivedDataBackendInterface.h" #include "DerivedDataCacheMethod.h" #include "DerivedDataCachePrivate.h" #include "DerivedDataCacheReplay.h" #include "DerivedDataCacheStore.h" #include "DerivedDataCacheUsageStats.h" #include "DerivedDataRequestOwner.h" #include "HAL/CriticalSection.h" #include "HAL/FileManager.h" #include "HAL/IConsoleManager.h" #include "HAL/PlatformFileManager.h" #include "HAL/ThreadSafeCounter.h" #include "Internationalization/FastDecimalFormat.h" #include "Math/BasicMathExpressionEvaluator.h" #include "Math/UnitConversion.h" #include "MemoryCacheStore.h" #include "Misc/App.h" #include "Misc/CommandLine.h" #include "Misc/ConfigCacheIni.h" #include "Misc/EngineBuildSettings.h" #include "Misc/Guid.h" #include "Misc/Paths.h" #include "Misc/ScopeLock.h" #include "Misc/StringBuilder.h" #include "Modules/ModuleManager.h" #include "PakFileCacheStore.h" #include "ProfilingDebugging/CookStats.h" #include "Serialization/CompactBinaryPackage.h" #include "String/Find.h" #include DEFINE_LOG_CATEGORY(LogDerivedDataCache); LLM_DEFINE_TAG(UntaggedDDCResult); LLM_DEFINE_TAG(DDCBackend); #define MAX_BACKEND_KEY_LENGTH (120) #define LOCTEXT_NAMESPACE "DerivedDataBackendGraph" static TAutoConsoleVariable GDerivedDataCacheGraphName( TEXT("DDC.Graph"), TEXT("Default"), TEXT("Name of the graph to use for the Derived Data Cache."), ECVF_ReadOnly); namespace UE::DerivedData::Private { static std::atomic GAsyncTaskCounter; int32 AddToAsyncTaskCounter(int32 Addend) { return GAsyncTaskCounter.fetch_add(Addend); } } // UE::DerivedData::Private namespace UE::DerivedData { ILegacyCacheStore* CreateCacheStoreAsync(ILegacyCacheStore* InnerBackend, ECacheStoreFlags InnerFlags, IMemoryCacheStore* MemoryCache); ILegacyCacheStore* CreateCacheStoreHierarchy(ICacheStoreOwner*& OutOwner, IMemoryCacheStore* MemoryCache); ILegacyCacheStore* CreateCacheStoreThrottle(ILegacyCacheStore* InnerCache, uint32 LatencyMS, uint32 MaxBytesPerSecond); ILegacyCacheStore* CreateCacheStoreVerify(ILegacyCacheStore* InnerCache, bool bPutOnError); ILegacyCacheStore* CreateFileSystemCacheStore(const TCHAR* CacheDirectory, const TCHAR* Params, const TCHAR* AccessLogFileName, ECacheStoreFlags& OutFlags); TTuple CreateHttpCacheStore(const TCHAR* NodeName, const TCHAR* Config); IMemoryCacheStore* CreateMemoryCacheStore(const TCHAR* Name, int64 MaxCacheSize, bool bCanBeDisabled); IPakFileCacheStore* CreatePakFileCacheStore(const TCHAR* Filename, bool bWriting, bool bCompressed); ILegacyCacheStore* CreateS3CacheStore(const TCHAR* RootManifestPath, const TCHAR* BaseUrl, const TCHAR* Region, const TCHAR* CanaryObjectKey, const TCHAR* CachePath); TTuple CreateZenCacheStore(const TCHAR* NodeName, const TCHAR* Config); ILegacyCacheStore* TryCreateCacheStoreReplay(ILegacyCacheStore* InnerCache); /** * This class is used to create a singleton that represents the derived data cache hierarchy and all of the wrappers necessary * ideally this would be data driven and the backends would be plugins... */ class FDerivedDataBackendGraph final : public FDerivedDataBackend { public: using FParsedNode = TPair; using FParsedNodeMap = TMap; /** * constructor, builds the cache tree */ FDerivedDataBackendGraph() : RootCache(nullptr) , MemoryCache(nullptr) , BootCache(nullptr) , WritePakCache(nullptr) , AsyncNode(nullptr) , VerifyNode(nullptr) , Hierarchy(nullptr) , bUsingSharedDDC(false) , bIsShuttingDown(false) , MountPakCommand( TEXT("DDC.MountPak"), *LOCTEXT("CommandText_DDCMountPak", "Mounts read-only pak file").ToString(), FConsoleCommandWithArgsDelegate::CreateRaw(this, &FDerivedDataBackendGraph::MountPakCommandHandler)) , UnmountPakCommand( TEXT("DDC.UnmountPak"), *LOCTEXT("CommandText_DDCUnmountPak", "Unmounts read-only pak file").ToString(), FConsoleCommandWithArgsDelegate::CreateRaw(this, &FDerivedDataBackendGraph::UnmountPakCommandHandler)) , LoadReplayCommand( TEXT("DDC.LoadReplay"), *LOCTEXT("CommandText_DDCLoadReplay", "Loads a cache replay file created by -DDC-ReplaySave=").ToString(), FConsoleCommandWithArgsDelegate::CreateRaw(this, &FDerivedDataBackendGraph::LoadReplayCommandHandler)) { check(!StaticGraph); StaticGraph = this; check(IsInGameThread()); // we pretty much need this to be initialized from the main thread...it uses GConfig, etc check(GConfig && GConfig->IsReadyForUse()); bHasDefaultDebugOptions = FBackendDebugOptions::ParseFromTokens(DefaultDebugOptions, TEXT("All"), FCommandLine::Get()); RootCache = nullptr; FParsedNodeMap ParsedNodes; FParsedNode RootNode(nullptr, ECacheStoreFlags::None); // Create the graph using ini settings. The string "default" forwards creation to use the default graph. if (!FParse::Value(FCommandLine::Get(), TEXT("-DDC="), GraphName)) { GraphName = GDerivedDataCacheGraphName.GetValueOnGameThread(); } // A DDC graph of "None" is used by build worker programs that use the DDC build code paths but avoid use of the DDC cache // code paths. Unfortunately the cache must currently be instantiated as part of initializing the build code, so we have // to disable the cache portion by injecting "-DDC=None" to the commandline args during process startup. This mode is not // compatible with use in the editor/commandlets which is not written to operate with a non-functional cache layer. This // can lead to confusion when people attempt to use "-DDC=None" in the editor expecting it to behave like "-DDC=Cold". // To avoid this confusion (and until we can use the build without the cache) it is restricted to use in programs only // and not the editor. #if IS_PROGRAM if (GraphName == TEXT("None")) { UE_LOG(LogDerivedDataCache, Display, TEXT("Requested cache graph of 'None'. Every cache operation will fail.")); } else #endif { const TCHAR* const RootName = TEXT("Root"); if (!GraphName.IsEmpty() && GraphName != TEXT("Default")) { RootNode = ParseNode(RootName, GEngineIni, *GraphName, ParsedNodes); if (!RootNode.Key) { // Destroy any cache stores that have been created. ParsedNodes.Empty(); DestroyCreatedBackends(); UE_LOG(LogDerivedDataCache, Warning, TEXT("Unable to create cache graph '%s'. Reverting to the default graph."), *GraphName); } } if (!RootNode.Key) { // Try to use the default graph. GraphName = FApp::IsEngineInstalled() ? TEXT("InstalledDerivedDataBackendGraph") : TEXT("DerivedDataBackendGraph"); RootNode = ParseNode(RootName, GEngineIni, *GraphName, ParsedNodes); if (!RootNode.Key) { FString Entry; if (!GConfig->DoesSectionExist(*GraphName, GEngineIni)) { UE_LOG(LogDerivedDataCache, Fatal, TEXT("Unable to create default cache graph '%s' because its config section is missing in the '%s' config."), *GraphName, *GEngineIni); } else if (!GConfig->GetString(*GraphName, RootName, Entry, GEngineIni) || !Entry.Len()) { UE_LOG(LogDerivedDataCache, Fatal, TEXT("Unable to create default cache graph '%s' because the root node '%s' is missing."), *GraphName, RootName); } else { UE_LOG(LogDerivedDataCache, Fatal, TEXT("Unable to create default cache graph '%s' because no cache store was available."), *GraphName); } } } } // Hierarchy must exist in the graph. if (!Hierarchy) { ILegacyCacheStore* HierarchyStore = CreateCacheStoreHierarchy(Hierarchy, GetMemoryCache()); if (RootNode.Key) { Hierarchy->Add(RootNode.Key, RootNode.Value); } CreatedNodes.AddUnique(HierarchyStore); RootNode.Key = HierarchyStore; } // Async must exist in the graph. if (!AsyncNode) { AsyncNode = CreateCacheStoreAsync(RootNode.Key, RootNode.Value, GetMemoryCache()); CreatedNodes.AddUnique(AsyncNode); RootNode.Key = AsyncNode; } // Create a Verify node when using -DDC-Verify[=Type1[@Rate2][+Type2[@Rate2]...]]. if (!VerifyNode) { FString VerifyArg; if (FParse::Value(FCommandLine::Get(), TEXT("-DDC-Verify="), VerifyArg) || FParse::Param(FCommandLine::Get(), TEXT("DDC-Verify"))) { VerifyNode = CreateCacheStoreVerify(RootNode.Key, /*bPutOnError*/ false); CreatedNodes.AddUnique(VerifyNode); RootNode.Key = VerifyNode; } } // Create a Replay node when requested on the command line. if (ILegacyCacheStore* ReplayNode = TryCreateCacheStoreReplay(RootNode.Key)) { CreatedNodes.AddUnique(ReplayNode); RootNode.Key = ReplayNode; } if (MaxKeyLength == 0) { MaxKeyLength = MAX_BACKEND_KEY_LENGTH; } RootCache = RootNode.Key; } /** * Helper function to get the value of parsed bool as the return value **/ bool GetParsedBool( const TCHAR* Stream, const TCHAR* Match ) const { bool bValue = 0; FParse::Bool( Stream, Match, bValue ); return bValue; } /** * Parses backend graph node from ini settings * * @param NodeName Name of the node to parse * @param IniFilename Ini filename * @param IniSection Section in the ini file containing the graph definition * @param InParsedNodes Map of parsed nodes and their names to be able to find already parsed nodes * @return Derived data backend interface instance created from ini settings */ FParsedNode ParseNode(const FString& NodeName, const FString& IniFilename, const TCHAR* IniSection, FParsedNodeMap& InParsedNodes) { if (const FParsedNode* ParsedNode = InParsedNodes.Find(NodeName)) { UE_LOG(LogDerivedDataCache, Warning, TEXT("Node %s was referenced more than once in the graph. Nodes may not be shared."), *NodeName); return *ParsedNode; } FParsedNode ParsedNode(nullptr, ECacheStoreFlags::None); FString Entry; if (GConfig->GetString(IniSection, *NodeName, Entry, IniFilename)) { Entry.TrimStartInline(); Entry.RemoveFromStart(TEXT("(")); Entry.RemoveFromEnd(TEXT(")")); FString NodeType; if (FParse::Value(*Entry, TEXT("Type="), NodeType)) { if (NodeType == TEXT("FileSystem")) { ParsedNode = ParseDataCache(*NodeName, *Entry); } else if (NodeType == TEXT("Boot")) { UE_LOG(LogDerivedDataCache, Display, TEXT("Boot nodes are deprecated. Please remove the Boot node from the cache graph.")); if (BootCache == nullptr) { BootCache = ParseBootCache(*NodeName, *Entry); ParsedNode = MakeTuple(BootCache, ECacheStoreFlags::Local | ECacheStoreFlags::Query | ECacheStoreFlags::Store); } else { UE_LOG(LogDerivedDataCache, Warning, TEXT("Unable to create %s Boot cache because only one Boot node is supported."), *NodeName); } } else if (NodeType == TEXT("Memory")) { ParsedNode = MakeTuple(ParseMemoryCache(*NodeName, *Entry), ECacheStoreFlags::Local | ECacheStoreFlags::Query | ECacheStoreFlags::Store); } else if (NodeType == TEXT("Hierarchical")) { ParsedNode = ParseHierarchyNode(*NodeName, *Entry, IniFilename, IniSection, InParsedNodes); } else if (NodeType == TEXT("KeyLength")) { if (MaxKeyLength == 0) { ParsedNode = ParseKeyLength(*NodeName, *Entry, IniFilename, IniSection, InParsedNodes); } else { UE_LOG(LogDerivedDataCache, Warning, TEXT("Unable to create %s KeyLength node because only one KeyLength node is supported."), *NodeName); } } else if (NodeType == TEXT("AsyncPut")) { if (AsyncNode == nullptr) { ParsedNode = ParseAsyncNode(*NodeName, *Entry, IniFilename, IniSection, InParsedNodes); AsyncNode = ParsedNode.Key; } else { UE_LOG(LogDerivedDataCache, Warning, TEXT("Unable to create %s AsyncPut because only one AsyncPut node is supported."), *NodeName); } } else if (NodeType == TEXT("Verify")) { if (VerifyNode == nullptr) { ParsedNode = ParseVerify(*NodeName, *Entry, IniFilename, IniSection, InParsedNodes); VerifyNode = ParsedNode.Key; } else { UE_LOG(LogDerivedDataCache, Warning, TEXT("Unable to create %s Verify because only one Verify node is supported."), *NodeName); } } else if (NodeType == TEXT("ReadPak")) { ParsedNode = MakeTuple(ParsePak(*NodeName, *Entry, /*bWriting*/ false), ECacheStoreFlags::Local | ECacheStoreFlags::Query | ECacheStoreFlags::StopStore); } else if (NodeType == TEXT("WritePak")) { ParsedNode = MakeTuple(ParsePak(*NodeName, *Entry, /*bWriting*/ true), ECacheStoreFlags::Local | ECacheStoreFlags::Store); } else if (NodeType == TEXT("S3")) { ParsedNode = MakeTuple(ParseS3Cache(*NodeName, *Entry), ECacheStoreFlags::Local | ECacheStoreFlags::Query | ECacheStoreFlags::StopStore); } else if (NodeType == TEXT("Cloud") || NodeType == TEXT("Http")) { ParsedNode = CreateHttpCacheStore(*NodeName, *Entry); } else if (NodeType == TEXT("Zen")) { ParsedNode = CreateZenCacheStore(*NodeName, *Entry); } if (ParsedNode.Key) { // Add a throttling layer if parameters are found uint32 LatencyMS = 0; FParse::Value(*Entry, TEXT("LatencyMS="), LatencyMS); uint32 MaxBytesPerSecond = 0; FParse::Value(*Entry, TEXT("MaxBytesPerSecond="), MaxBytesPerSecond); if (LatencyMS != 0 || MaxBytesPerSecond != 0) { CreatedNodes.AddUnique(ParsedNode.Key); ParsedNode = MakeTuple(CreateCacheStoreThrottle(ParsedNode.Key, LatencyMS, MaxBytesPerSecond), ParsedNode.Value); } } } } if (ParsedNode.Key) { // Store this node so that we don't require any order of adding nodes InParsedNodes.Add(NodeName, ParsedNode); // Keep references to all created nodes. CreatedNodes.AddUnique(ParsedNode.Key); // Parse any debug options for this node. E.g. -DDC--MissRate FBackendDebugOptions DebugOptions; if (FBackendDebugOptions::ParseFromTokens(DebugOptions, *NodeName, FCommandLine::Get())) { if (!ParsedNode.Key->LegacyDebugOptions(DebugOptions)) { UE_LOG(LogDerivedDataCache, Warning, TEXT("%s: Node is ignoring one or more -DDC-%s-