// Copyright Epic Games, Inc. All Rights Reserved. #include "CoreTypes.h" #include "Containers/StringView.h" #include "DerivedDataBackendInterface.h" #include "DerivedDataCachePrivate.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 { 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); ILegacyCacheStore* CreateHttpCacheStore( const TCHAR* NodeName, const TCHAR* ServiceUrl, bool bResolveHostCanonicalName, const TCHAR* Namespace, const TCHAR* StructuredNamespace, const TCHAR* OAuthProvider, const TCHAR* OAuthClientId, const TCHAR* OAuthData, const TCHAR* OAuthScope, const FDerivedDataBackendInterface::ESpeedClass* ForceSpeedClass, EBackendLegacyMode LegacyMode, bool bReadOnly); 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); ILegacyCacheStore* CreateZenCacheStore(const TCHAR* NodeName, const TCHAR* ServiceUrl, const TCHAR* Namespace); /** * 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)) , UnountPakCommand( TEXT("DDC.UnmountPak"), *LOCTEXT("CommandText_DDCUnmountPak", "Unmounts read-only pak file").ToString(), FConsoleCommandWithArgsDelegate::CreateRaw(this, &FDerivedDataBackendGraph::UnmountPakCommandHandler)) { 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(); } if (GraphName == TEXT("None")) { UE_LOG(LogDerivedDataCache, Display, TEXT("Requested cache graph of 'None'. Every cache operation will fail.")); } else { if (!GraphName.IsEmpty() && GraphName != TEXT("Default")) { RootNode = ParseNode(TEXT("Root"), 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 using the requested graph settings (%s). "), TEXT("Reverting to the default graph."), *GraphName); } } if (!RootNode.Key) { // Try to use the default graph. GraphName = FApp::IsEngineInstalled() ? TEXT("InstalledDerivedDataBackendGraph") : TEXT("DerivedDataBackendGraph"); FString Entry; if (!GConfig->GetString(*GraphName, TEXT("Root"), Entry, GEngineIni) || !Entry.Len()) { UE_LOG(LogDerivedDataCache, Fatal, TEXT("Unable to create cache graph using the default graph settings (%s) ini=%s."), *GraphName, *GEngineIni); } RootNode = ParseNode(TEXT("Root"), GEngineIni, *GraphName, ParsedNodes); if (!RootNode.Key) { UE_LOG(LogDerivedDataCache, Fatal, TEXT("Unable to create cache graph using the default graph settings (%s) ini=%s. ") TEXT("At least one cache store in the graph must be available."), *GraphName, *GEngineIni); } } } // 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; } } 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("Http")) { ParsedNode = ParseHttpCache(*NodeName, *Entry, IniFilename, IniSection); } else if (NodeType == TEXT("Zen")) { ParsedNode = MakeTuple(ParseZenCache(*NodeName, *Entry), ECacheStoreFlags::Local | ECacheStoreFlags::Remote | ECacheStoreFlags::Query | ECacheStoreFlags::Store); } 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 FDerivedDataBackendInterface::FBackendDebugOptions DebugOptions; if (FDerivedDataBackendInterface::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-