// Copyright Epic Games, Inc. All Rights Reserved. #include "AnalysisCache.h" #include "Algo/Count.h" #include "Algo/Find.h" #include "Async/MappedFileHandle.h" #include "Containers/StringView.h" #include "GenericPlatform/GenericPlatformFile.h" #include "HAL/PlatformFileManager.h" #include "Logging/LogMacros.h" #include "Memory/SharedBuffer.h" #include "Misc/CommandLine.h" #include "Misc/Paths.h" #include "Misc/PathViews.h" #include "Serialization/BufferArchive.h" #include "Serialization/BufferWriter.h" #include "Serialization/CompactBinaryPackage.h" #include "Serialization/CompactBinaryWriter.h" #include "Serialization/MemoryReader.h" DEFINE_LOG_CATEGORY_STATIC(LogAnalysisCache, Log, All); namespace TraceServices { ////////////////////////////////////////////////////////////////////// FAnalysisCache::FFileContents::FFileContents(const TCHAR* FilePath) : CacheFilePath(FilePath) { IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); if (FParse::Param(FCommandLine::Get(), TEXT("disableanalysiscache"))) { UE_LOG(LogAnalysisCache, Display, TEXT("Putting cache in transient mode.")); bTransientMode = true; return; } // Opening the file we can encounter one of 3 scenarios: // 1. File does not exist, create on first save // 2. File exist, we can read the contents // 3. File exist but we could not open the file for reading. Multiple processes are competing. Put the cache in transient mode if (const int64 FileSize = PlatformFile.FileSize(*CacheFilePath); FileSize > 0) { if (const TUniquePtr File(PlatformFile.OpenRead(*CacheFilePath)); File.IsValid()) { if (const bool Result = Load(); !Result) { UE_LOG(LogAnalysisCache, Error, TEXT("Failed to open cache file table of contents.")); bTransientMode = true; //todo: Recover by deleting file? } // Additional sanity check. A common error scenario is that Insights crashed after writing block but before // committing them to the table of contents. Detect that scenario here. uint32 MinimalExpectedSizePerVersion[] = {0, ReservedSizeV1, ReservedSizeV2}; if (Version < UE_ARRAY_COUNT(MinimalExpectedSizePerVersion) && FileSize > MinimalExpectedSizePerVersion[Version] && Blocks.IsEmpty()) { UE_LOG(LogAnalysisCache, Error, TEXT("Cache file has written several blocks but table of contents contains no blocks. This is likely caused by abnormal program termination. Please delete \"%s\". Putting cache in transient mode."), *CacheFilePath); IndexEntries.Empty(); bTransientMode = true; return; } UE_LOG(LogAnalysisCache, VeryVerbose, TEXT("Cache contains %d blocks:"), Blocks.Num()); UE_LOG(LogAnalysisCache, VeryVerbose, TEXT(" %10s %10s %13s %13s %13s"), TEXT("Cache index"), TEXT("Block index"), TEXT("Offset"), TEXT("Uncompressed"), TEXT("Compressed")); for (const FFileContents::FBlockEntry& Block : Blocks) { UE_LOG(LogAnalysisCache, VeryVerbose, TEXT(" %10d %10d %10" UINT64_FMT " kb %10" UINT64_FMT " kb %10" UINT64_FMT " kb"), GetCacheId(Block.BlockKey), GetBlockIndex(Block.BlockKey), Block.Offset / 1024, Block.UncompressedSize / 1024, Block.CompressedSize / 1024); } //todo: At this point we can check the file size and if it doesn't match we can trim unknown segments. } else { // Unable to open for read. Most likely this is because another instance is using the file UE_LOG(LogAnalysisCache, Warning, TEXT("Unable to read the cache file %s, possibly already open in another session. Putting cache in transient mode."), *CacheFilePath); bTransientMode = true; } } // If we haven't established a version use current if (!Version) { Version = CurrentVersion; } } ////////////////////////////////////////////////////////////////////// FAnalysisCache::FFileContents::~FFileContents() { // Save the table of contents. if (!Blocks.IsEmpty()) { if (const bool Result = Save(); !Result ) { UE_LOG(LogAnalysisCache, Error, TEXT("Failed to update cache files table of contents.")); } } } ////////////////////////////////////////////////////////////////////// FCacheId FAnalysisCache::FFileContents::GetId(const TCHAR* Name, uint16 Flags) { const FIndexEntry* Entry = Algo::FindByPredicate(IndexEntries, [Name](const FIndexEntry& Entry){ return Entry.Name.Equals(Name); }); if (Entry) { return Entry->Id; } // Name was not previously registered, create new entry const uint32 NewId = IndexEntries.Num() + 1; FIndexEntry& NewEntry = IndexEntries.AddZeroed_GetRef(); NewEntry.Name = Name; NewEntry.Id = NewId; NewEntry.Flags = uint32(Flags); return NewId; } ////////////////////////////////////////////////////////////////////// uint16 FAnalysisCache::FFileContents::GetFlags(FCacheId InId) { const FIndexEntry* Entry = Algo::FindBy(IndexEntries, InId, &FIndexEntry::Id); if (Entry) { return uint16(Entry->Flags & 0xffff); } return 0; } ////////////////////////////////////////////////////////////////////// FMutableMemoryView FAnalysisCache::FFileContents::GetUserData(FCacheId InId) { FIndexEntry* Entry = Algo::FindBy(IndexEntries, InId, &FIndexEntry::Id); if (Entry) { return FMutableMemoryView(Entry->UserData, UserDataSize); } return FMutableMemoryView(); } ////////////////////////////////////////////////////////////////////// bool FAnalysisCache::FFileContents::Save() { IFileHandle* File = GetFileHandleForWrite(); if (!File) { return true; } File->Seek(0); check(Version > 0); // Version should always be set here switch(Version) { case 1: return SaveVersion1(File); case 2: return SaveVersion2(File); } return false; } ////////////////////////////////////////////////////////////////////// void FAnalysisCache::FFileContents::SaveVersion1Index(FCbWriter& Writer) { Writer.BeginArray(ANSITEXTVIEW("Index")); for (auto Entry : IndexEntries) { Writer.BeginObject(); Writer << ANSITEXTVIEW("N") << Entry.Name; Writer << ANSITEXTVIEW("I") << Entry.Id; Writer << ANSITEXTVIEW("F") << Entry.Flags; Writer.AddBinary(ANSITEXTVIEW("UD"), &Entry.UserData, UserDataSize); Writer.EndObject(); } Writer.EndArray(); Writer.BeginArray(ANSITEXTVIEW("Blocks")); for (const FBlockEntry& Entry : Blocks) { Writer.AddBinary(&Entry, sizeof(FBlockEntry)); } Writer.EndArray(); } ////////////////////////////////////////////////////////////////////// bool FAnalysisCache::FFileContents::SaveVersion1(IFileHandle* File) { FCbWriter Writer; Writer.BeginObject(); Writer << "Version" << 1; SaveVersion1Index(Writer); Writer.EndObject(); FCbPackage Package(Writer.Save().AsObject()); FUniqueBuffer Buffer = FUniqueBuffer::Alloc(ReservedSizeV1); FBufferWriter BufferWriter(Buffer.GetData(), Buffer.GetSize()); Package.Save(BufferWriter); return File->Write((uint8*)Buffer.GetData(), Buffer.GetSize()); } ////////////////////////////////////////////////////////////////////// bool FAnalysisCache::FFileContents::SaveVersion2(IFileHandle* File) { check (Version != 0); // Header ANSICHAR Magic[] = "UC"; File->Write((uint8*)Magic,2); File->Write((uint8*)&Version, 4); checkSlow(File->Tell() == IndexOffset); // Write the index FCbWriter Writer; Writer.BeginObject(); SaveVersion1Index(Writer); Writer.EndObject(); FCbPackage Package(Writer.Save().AsObject()); FUniqueBuffer Buffer = FUniqueBuffer::Alloc(ReservedSizeV2); FBufferWriter BufferWriter(Buffer.GetData(), Buffer.GetSize()); Package.Save(BufferWriter); return File->Write((uint8*)Buffer.GetData(), Buffer.GetSize()); } ////////////////////////////////////////////////////////////////////// bool FAnalysisCache::FFileContents::Load() { IFileHandle* File = GetFileHandleForRead(); if (!File) { return true; } // Read header Version = ReadHeader(File); if (Version > CurrentVersion) { UE_LOG(LogAnalysisCache, Warning, TEXT("Cache file of unknown version (%u), cannot load."), Version); return false; } UE_LOG(LogAnalysisCache, Display, TEXT("Loading cache file (version %u)."), Version); switch(Version) { case 1: return LoadVersion1(File); case 2: return LoadVersion2(File); } return false; } ////////////////////////////////////////////////////////////////////// uint32 FAnalysisCache::FFileContents::ReadHeader(IFileHandle* File) { uint32 Version = 0; uint8 Header[IndexOffset]; File->Seek(0); File->Read((uint8*)&Header, IndexOffset); if (Header[0] != 'U' || Header[1] != 'C') { // Version 1 lacked a magic header Version = 1; File->Seek(0); } else { // Expect version number after magic Version = *(uint32*)&Header[2]; } return Version; } ////////////////////////////////////////////////////////////////////// bool FAnalysisCache::FFileContents::WriteHeader(IFileHandle* File, uint32 Version) { bool bSuccess = true; File->Seek(0); ANSICHAR Magic[] = "UC"; bSuccess &= File->Write((uint8*)Magic,2); bSuccess &= File->Write((uint8*)&Version, 4); checkSlow(File->Tell() == IndexOffset); return bSuccess; } ////////////////////////////////////////////////////////////////////// void FAnalysisCache::FFileContents::LoadVersion1Index(const FCbPackage& Package) { FCbArrayView IndexArray = Package.GetObject().Find(ANSITEXTVIEW("Index")).AsArrayView(); IndexEntries.Reserve(static_cast(IndexArray.Num())); for (FCbFieldView IndexEntry : IndexArray) { FCbObjectView IndexEntryObj = IndexEntry.AsObjectView(); FUtf8StringView NameView = IndexEntryObj[ANSITEXTVIEW("N")].AsString(); const uint32 Id = IndexEntryObj[ANSITEXTVIEW("I")].AsUInt32(); const uint32 Flags = IndexEntryObj[ANSITEXTVIEW("F")].AsUInt32(); FMemoryView UserData = IndexEntryObj[ANSITEXTVIEW("UD")].AsBinaryView(); FIndexEntry& Entry = IndexEntries.AddZeroed_GetRef(); Entry.Name = FString(NameView); Entry.Id = Id; Entry.Flags = Flags; FMutableMemoryView UserDataDst(&Entry.UserData, UserDataSize); FMutableMemoryView Remainder = UserDataDst.CopyFrom(UserData); check(Remainder.GetSize() == 0); } FCbArrayView BlockArray = Package.GetObject().Find(ANSITEXTVIEW("Blocks")).AsArrayView(); Blocks.Reserve(static_cast(BlockArray.Num())); for (FCbFieldView BlockEntryView : BlockArray) { FBlockEntry& Block = Blocks.AddZeroed_GetRef(); FMutableMemoryView BlockView = FMutableMemoryView(&Block, sizeof(FBlockEntry)); BlockView.CopyFrom(BlockEntryView.AsBinaryView()); } } ////////////////////////////////////////////////////////////////////// bool FAnalysisCache::FFileContents::LoadVersion1(IFileHandle* File) { FUniqueBuffer Buffer = FUniqueBuffer::Alloc(ReservedSizeV1); File->Read((uint8*)Buffer.GetData(), Buffer.GetSize()); FMemoryReaderView Ar(MakeArrayView((uint8*)Buffer.GetData(), IntCastChecked(Buffer.GetSize()))); FCbPackage Package; if (!Package.TryLoad(Ar)) { return false; } Version = Package.GetObject().Find("Version").AsUInt32(); LoadVersion1Index(Package); return true; } ////////////////////////////////////////////////////////////////////// bool FAnalysisCache::FFileContents::LoadVersion2(IFileHandle* File) { FUniqueBuffer Buffer = FUniqueBuffer::Alloc(ReservedSizeV2); File->Read((uint8*)Buffer.GetData(), Buffer.GetSize()); FMemoryReaderView Ar(MakeArrayView((uint8*)Buffer.GetData(), IntCastChecked(Buffer.GetSize()))); FCbPackage Package; if (!Package.TryLoad(Ar)) { return false; } LoadVersion1Index(Package); return true; } ////////////////////////////////////////////////////////////////////// uint64 FAnalysisCache::FFileContents::UpdateBlock(FMemoryView Block, BlockKeyType BlockKey) { IFileHandle* File = GetFileHandleForWrite(); if (!File) { return 0; } const int32 EntryIndex = Algo::BinarySearchBy(Blocks, BlockKey, [&](const FBlockEntry& InEntry) { return InEntry.BlockKey; }); const FIoHash CurrentHash = FIoHash::HashBuffer(Block); if (EntryIndex != INDEX_NONE) { FBlockEntry& Entry = Blocks[EntryIndex]; // todo: This only works if size hasn't changed! if (CurrentHash != Entry.Hash) { //todo: Add compression File->Seek(Entry.Offset); if(!File->Write((const uint8*)Block.GetData(), Block.GetSize())) { UE_LOG(LogAnalysisCache, Error, TEXT("Failed to update block 0x%x at offset %" UINT64_FMT " kb"), BlockKey, Entry.Offset / 1024); return 0; } Entry.Hash = CurrentHash; return Block.GetSize(); } } else { // Write to end of file and add to blocks array const bool bSeekResult = File->SeekFromEnd(0); check(bSeekResult); const uint64 Offset = File->Tell(); checkf(Offset >= ReservedSizeV2, TEXT("Offset (%" UINT64_FMT ") is larger than reserved size (%u)"), Offset, ReservedSizeV2); if(!File->Write((const uint8*) Block.GetData(), Block.GetSize())) { UE_LOG(LogAnalysisCache, Error, TEXT("Failed to update block 0x%x at offset %" UINT64_FMT " kb"), BlockKey, Offset / 1024); return 0; } // Insert entry in blocks list and sort array Blocks.Emplace(FBlockEntry{BlockKey, 0, Offset, 0, Block.GetSize(), CurrentHash}); Algo::SortBy(Blocks, [](const FBlockEntry& Entry){ return Entry.BlockKey; }); return Block.GetSize(); } return 0; } ////////////////////////////////////////////////////////////////////// uint64 FAnalysisCache::FFileContents::LoadBlock(FMutableMemoryView Block, BlockKeyType BlockKey) { IFileHandle* File = GetFileHandleForRead(); if (!File) { return 0; } const int32 EntryIndex = Algo::BinarySearchBy(Blocks, BlockKey, [&](const FBlockEntry& InEntry) { return InEntry.BlockKey; }); if (EntryIndex == INDEX_NONE) { UE_LOG(LogAnalysisCache, Error, TEXT("Trying to load unknown block 0x%x."), BlockKey); return 0; } const FBlockEntry& Entry = Blocks[EntryIndex]; check(Entry.UncompressedSize <= Block.GetSize()); if (!File->Seek(Entry.Offset)) { UE_LOG(LogAnalysisCache, Error, TEXT("Block 0x%x was located on an invalid offset %" UINT64_FMT " kb."), BlockKey, Entry.Offset / 1024); return 0; } //todo: Add compression if (!File->Read((uint8*)Block.GetData(), Entry.UncompressedSize)) { UE_LOG(LogAnalysisCache, Error, TEXT("Unable to read block 0x%x on offset %" UINT64_FMT " kb with size %" UINT64_FMT " kb."), BlockKey, Entry.Offset / 1024, Entry.UncompressedSize / 1024); return 0; } return Block.GetSize(); } ////////////////////////////////////////////////////////////////////// IFileHandle* FAnalysisCache::FFileContents::GetFileHandleForWrite() { if (bTransientMode) { return nullptr; } if (CacheFileWrite.IsValid()) { return CacheFileWrite.Get(); } IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); const bool bCreated = !PlatformFile.FileExists(*CacheFilePath); CacheFileWrite = TUniquePtr(PlatformFile.OpenWrite(*CacheFilePath, true, true)); if (!CacheFileWrite.IsValid()) { // Unable to open for write. Most likely this is because another instance is using the file UE_LOG(LogAnalysisCache, Warning, TEXT("Unable to write to the cache file %s, possibly already open in another session. Putting cache in transient mode."), *CacheFilePath); bTransientMode = true; return nullptr; } if (bCreated) { // Save the table of contents to reserve space Save(); } return CacheFileWrite.Get(); } ////////////////////////////////////////////////////////////////////// IFileHandle* FAnalysisCache::FFileContents::GetFileHandleForRead() { if (bTransientMode) { return nullptr; } if (CacheFile.IsValid()) { return CacheFile.Get(); } IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); CacheFile = TUniquePtr(PlatformFile.OpenRead(*CacheFilePath, true)); if (!CacheFile.IsValid()) { // Unable to open for read. Most likely this is because another instance is using the file UE_LOG(LogAnalysisCache, Warning, TEXT("Unable to read the cache file %s, possibly already open in another session. Putting cache in transient mode."), *CacheFilePath); bTransientMode = true; return nullptr; } return CacheFile.Get(); } ////////////////////////////////////////////////////////////////////// FAnalysisCache::FAnalysisCache(const TCHAR* Path) : Stats() { // Find the cache file path. // todo: This will need to be refined as we move away from files // We expect to receive the full path to the session file FString CacheFilePath(Path); CacheFilePath = FPaths::SetExtension(CacheFilePath, TEXT(".ucache")); Contents = MakeUnique(*CacheFilePath); // Build a dictionary of number of blocks per cache id. for (const FFileContents::FBlockEntry& Block : Contents->Blocks) { const uint32 CacheId = GetCacheId(Block.BlockKey); IndexBlockCount.FindOrAdd(CacheId)++; } } ///////////////////////////////////////////////////////////////////// FAnalysisCache::~FAnalysisCache() { // Remove all references to cached block, forcing them to write. CachedBlocks.Empty(); // Delete file contents wrapper Contents.Reset(); UE_LOG(LogAnalysisCache, Display, TEXT("Closing analysis cache, %0.2f MiB read, %0.2f MiB written."), double(Stats.BytesRead) / (1024.0 * 1024.0), double(Stats.BytesWritten) / (1024.0 * 1024.0)); } ///////////////////////////////////////////////////////////////////// uint32 FAnalysisCache::GetCacheId(const TCHAR* Name, uint16 Flags) { return Contents->GetId(Name, Flags); } ///////////////////////////////////////////////////////////////////// FMutableMemoryView FAnalysisCache::GetUserData(FCacheId CacheId) { return Contents->GetUserData(CacheId); } ///////////////////////////////////////////////////////////////////// FSharedBuffer FAnalysisCache::CreateBlocks(FCacheId CacheId, uint32 BlockCount) { const uint32 BlockIndex = IndexBlockCount.FindOrAdd(CacheId); const BlockKeyType BlockKey = CreateBlockKey(CacheId, BlockIndex); // Allocate memory and make the shared buffer with freeing callback. const uint64 TotalBytes = IAnalysisCache::BlockSizeBytes * BlockCount; void* Block = FMemory::Malloc(TotalBytes, BlockAlignment); FMemory::Memzero(Block, TotalBytes); FSharedBuffer Blocks = FSharedBuffer::TakeOwnership(Block, TotalBytes, [this, CacheId, BlockIndex](void* InBlock, uint64 Size) { ReleaseBlocks((uint8*)InBlock, CacheId, BlockIndex, Size); }); // Increment the block count IndexBlockCount[CacheId] += BlockCount; // Add the blocks into our internal caching mechanism if (!(Contents->GetFlags(CacheId) & ECacheFlags_NoGlobalCaching)) { check(!CachedBlocks.Contains(BlockKey)); CachedBlocks.Add(BlockKey, FSharedBuffer::MakeView(Blocks.GetView(), Blocks)); } return Blocks; } ///////////////////////////////////////////////////////////////////// FSharedBuffer FAnalysisCache::GetBlocks(FCacheId CacheId, uint32 BlockIndexStart, uint32 BlockCount) { const BlockKeyType CacheBlockKey = CreateBlockKey(CacheId, BlockIndexStart); const uint32 ExistingBlockCount = IndexBlockCount.FindOrAdd(CacheId); if (BlockIndexStart >= ExistingBlockCount || (BlockIndexStart + BlockCount) > ExistingBlockCount) { UE_LOG(LogAnalysisCache, Error, TEXT("Block range %u to %u is invalid for cache id %u."), BlockIndexStart, BlockIndexStart + BlockCount, CacheId); return FSharedBuffer(); } // Look in our currently help block cache // todo: Cached blocks are only keyed on first block index. What if a different range is requested? Overlap. if (FSharedBuffer* Block = CachedBlocks.Find(CacheBlockKey)) { return *Block; } // Allocate a contiguous chunk of memory that fits all the blocks const uint64 TotalBytes = IAnalysisCache::BlockSizeBytes * BlockCount; uint8* BlockBuffer = (uint8*)FMemory::Malloc(TotalBytes, BlockAlignment); for (uint32 Block = 0; Block < BlockCount; ++Block) { const BlockKeyType BlockKey = CreateBlockKey(CacheId, BlockIndexStart + Block); const FMutableMemoryView BlockView(BlockBuffer + (Block*IAnalysisCache::BlockSizeBytes), IAnalysisCache::BlockSizeBytes); const uint64 BytesRead = Contents->LoadBlock(BlockView, BlockKey); Stats.BytesRead += BytesRead; } // Take ownership of memory and register freeing callback. FSharedBuffer Blocks = FSharedBuffer::TakeOwnership(BlockBuffer, TotalBytes, [this, CacheId, BlockIndexStart](void* Block, uint64 Size) { ReleaseBlocks((uint8*)Block, CacheId, BlockIndexStart, Size); }); // Add the blocks into our internal caching mechanism if (!(Contents->GetFlags(CacheId) & ECacheFlags_NoGlobalCaching)) { CachedBlocks.Add(CacheBlockKey, Blocks); } return Blocks; } ///////////////////////////////////////////////////////////////////// void FAnalysisCache::ReleaseBlocks(uint8* BlockBuffer, FCacheId CacheId, uint32 BlockIndexStart, uint64 Size) { const uint32 BlockCount = IntCastChecked(Size / IAnalysisCache::BlockSizeBytes); for (uint32 Block = 0; Block < BlockCount; ++Block) { const void* BlockStart = BlockBuffer + (Block * IAnalysisCache::BlockSizeBytes); const FMemoryView BlockView = FMemoryView(BlockStart, IAnalysisCache::BlockSizeBytes); const BlockKeyType BlockKey = CreateBlockKey(CacheId, BlockIndexStart + Block); const uint64 BytesWritten = Contents->UpdateBlock(BlockView, BlockKey); Stats.BytesWritten += BytesWritten; } } ///////////////////////////////////////////////////////////////////// } //namespace TraceServices