// Copyright Epic Games, Inc. All Rights Reserved. #include "Installer/InstallChunkSource.h" #include "Algo/Transform.h" #include "Async/Mutex.h" #include "BuildPatchHash.h" #include "Common/FileSystem.h" #include "Common/StatsCollector.h" #include "Core/BlockStructure.h" #include "HAL/FileManager.h" #include "Misc/Paths.h" #include "Serialization/MemoryReader.h" #include "Tasks/Task.h" DEFINE_LOG_CATEGORY_STATIC(LogInstallChunkSource, Log, All); namespace BuildPatchServices { static FSHAHash GetShaHashForDataSet(const void* ChunkData, const uint32 ChunkSize) { FSHAHash ShaHashCheck; FSHA1::HashBuffer(ChunkData, ChunkSize, ShaHashCheck.Hash); return ShaHashCheck; } class FInstallChunkSource : public IConstructorInstallChunkSource { public: FInstallChunkSource(IFileSystem* FileSystem, IInstallChunkSourceStat* InInstallChunkSourceStat, const TMultiMap& InInstallationSources, const TSet& ChunksThatWillBeNeeded); ~FInstallChunkSource(); virtual FRequestProcessFn CreateRequest(const FGuid& DataId, FMutableMemoryView DestinationBuffer, void* UserPtr, FChunkRequestCompleteDelegate CompleteFn) override; virtual const TSet& GetAvailableChunks() const override { return AvailableInBuilds; } virtual void OnBeforeDeleteFile(const FString& FilePath) override { // Make sure we close our handle before the deletion occurs. // With multiple files in flight in the constructor we can be deleting a file at the same time as we are // reading chunks for other files, which means we can hit OpenedFileHandles from multiple threads. UE::TUniqueLock _(OpenedFileHandlesLock); { TUniquePtr* File = OpenedFileHandles.Find(FilePath); if (File) { if ((*File)->FileHandleLock.TryLock() == false) { // We can't ever be trying to delete a file when a read is active on it, // as this means that a file is trying to read from it after harvesting has // completed, which should ensure no chunks are read. UE_LOG(LogInstallChunkSource, Error, TEXT("Critical error: trying to delete %s while read lock is held."), *FilePath); // We don't want to just crash, so take the lock in order to wait for the // other thread to finish. Then we hope that nothing else is queued :grimacing: (*File)->FileHandleLock.Lock(); } OpenedFileHandles.Remove(FilePath); } } } virtual int32 GetChunkUnavailableAt(const FGuid& DataId) const override; virtual void SetFileRetirementPositions(TMap&& InFileRetirementPositions) override { FileRetirementPositions = MoveTemp(InFileRetirementPositions); } virtual void GetChunksForFile(const FString& FilePath, TSet& OutChunks) const override; virtual void EnumerateFilesForChunk(const FGuid& DataId, TUniqueFunction&& Callback) const override { const FString* FoundInstallDirectory; const FBuildPatchAppManifest* FoundInstallManifest; FindChunkLocation(DataId, &FoundInstallDirectory, &FoundInstallManifest); const TArray* ChunkSource = ChunkSources.Find(DataId); if (FoundInstallDirectory == nullptr || FoundInstallManifest == nullptr || ChunkSource == nullptr) { return; } // The installation directory starts off normalized by then appends a directory which // might be empty, leaving a trailing slash. Rather than chase down all possibilities we // just re normalize. FString NormalizedInstallDirectory = *FoundInstallDirectory; FPaths::NormalizeDirectoryName(NormalizedInstallDirectory); for (const FChunkSourceDetails& ChunkDetails : *ChunkSource) { // afaict the file manifest filename is normalized because in the manifest builder it // generates it from file spans, which are created in directorybuildstreamer which makes them // relative, and internal to that function they are normalized. Callback(NormalizedInstallDirectory, ChunkDetails.InstalledFileManifest->Filename); } } private: void FindChunkLocation(const FGuid& DataId, const FString** FoundInstallDirectory, const FBuildPatchAppManifest** FoundInstallManifest) const; private: IFileSystem* FileSystem; IInstallChunkSourceStat* InstallChunkSourceStat; // Storage of enumerated chunks. TSet AvailableInBuilds; TArray> InstallationSources; struct FOpenedFile { FOpenedFile(TUniquePtr&& InFileHandle) : FileHandle(MoveTemp(InFileHandle)) {} TUniquePtr FileHandle; UE::FMutex FileHandleLock; }; UE::FMutex OpenedFileHandlesLock; TMap> OpenedFileHandles; // The index (ChunkReferenceTracker->GetCurrentUsageIndex) at which our files will get deleted due to destructive install to make room for // the new file. TMap FileRetirementPositions; // Contains the info to build the chunk from various pieces strewn across the installation. struct FChunkSourceDetails { const FFileManifest* InstalledFileManifest; uint32 OffsetInChunk; uint32 SizeOfChunkPart; uint64 OffsetInInstalledFile; }; // Contains the list of pieces to assemble chunks from, for each chunk. TMap> ChunkSources; }; FInstallChunkSource::FInstallChunkSource(IFileSystem* InFileSystem, IInstallChunkSourceStat* InInstallChunkSourceStat, const TMultiMap& InInstallationSources, const TSet& ChunksThatWillBeNeeded) : FileSystem(InFileSystem) , InstallChunkSourceStat(InInstallChunkSourceStat) { TRACE_CPUPROFILER_EVENT_SCOPE(InstallChunkSource_ctor); // InstallationSources is a map of installation directory to the manifest that was installed there. for (const TPair& InstallationSourcePair : InInstallationSources) { const FString& InstallationDirectory = InstallationSourcePair.Key; const FBuildPatchAppManifestRef& InstalledManifest = InstallationSourcePair.Value; // Patches are only allowed to pull chunks from the install of the same tags, otherwise they must be // supplied via another source. However we can't get the tag list at this point; all we have is a list of // "registered installations" from the caller, so we get the full list of files and filter based on chunks we care // about. This may result in us attempting to pull chunks from files outside our tag if it can be serviced // by a file in a different install tag, however if the file doesn't exist, we'll continue to check other // files until we presumably find the correct file in our own tag. TSet InstalledFiles; InstalledManifest->GetFileList(InstalledFiles); struct FChunkAssemblyInfo { TArray SourceDetails; bool bComplete; }; // Find the chunks we can get from these files. TMap ChunkStructures; TMap ChunkAssemblyInfos; int32 MaxChunkStructures = 0; for (const FString& InstalledFileName : InstalledFiles) { const FFileManifest* InstalledFileManifest = InstalledManifest->GetFileManifest(InstalledFileName); if (!InstalledFileManifest) { // Should never happen? UE_LOG(LogInstallChunkSource, Error, TEXT("Missing file manifest from file provided from manifest? %s"), *InstalledFileName); continue; } bool bExistenceCheckPassed = false; uint32 ChunkPartSize = 0; uint64 FileOffset = 0; for (int32 ChunkPartIndex = 0; ChunkPartIndex < InstalledFileManifest->ChunkParts.Num(); ChunkPartIndex++, FileOffset += ChunkPartSize) { const FChunkPart& ChunkPart = InstalledFileManifest->ChunkParts[ChunkPartIndex]; ChunkPartSize = ChunkPart.Size; if (!ChunksThatWillBeNeeded.Contains(ChunkPart.Guid)) { continue; } // We do the file existence check _after_ we know we need the chunk so that we avoid // hitting the disk for every single file in the manifest when the tag might only be installing // 5 of them. if (!bExistenceCheckPassed) { int64 SizeOnDisk = 0; bool bGotFileSizeSuccessfully = InFileSystem->GetFileSize(*(InstallationDirectory / InstalledFileName), SizeOnDisk); uint64 ExpectedSize = InstalledManifest->GetFileSize(InstalledFileName); if (bGotFileSizeSuccessfully && (uint64)SizeOnDisk == ExpectedSize) { bExistenceCheckPassed = true; } else { // We can't get any chunks from this file, so bail. break; } } // If we get the full chunk here, we don't need to assemble anything. FChunkAssemblyInfo& ChunkAssemblyInfo = ChunkAssemblyInfos.FindOrAdd(ChunkPart.Guid); if (ChunkAssemblyInfo.SourceDetails.Num() == 0) { // First time we've seen this chunk - check if we are getting it all at once (often) const FChunkInfo* ChunkInfoPtr = InstalledManifest->GetChunkInfo(ChunkPart.Guid); if (!ChunkInfoPtr) { UE_LOG(LogInstallChunkSource, Display, TEXT("Missing chunk info for guid: %s"), *LexToString(ChunkPart.Guid)); continue; } if (ChunkPart.Offset == 0 && ChunkInfoPtr->WindowSize == ChunkPart.Size) { // Got the whole thing, don't need to fill out the chunk parts. FChunkSourceDetails NewInfo; NewInfo.InstalledFileManifest = InstalledFileManifest; NewInfo.OffsetInInstalledFile = FileOffset; NewInfo.OffsetInChunk = ChunkPart.Offset; NewInfo.SizeOfChunkPart = ChunkPart.Size; ChunkAssemblyInfo.SourceDetails.Add(NewInfo); ChunkAssemblyInfo.bComplete = true; continue; } // If it's just a part of the chunk we fall through } if (ChunkAssemblyInfo.bComplete) { // We already have all we need to make this chunk, anything else is redundant. continue; } FBlockStructure& PartialStructure = ChunkStructures.FindOrAdd(ChunkPart.Guid); PartialStructure.Add(ChunkPart.Offset, ChunkPart.Size, ESearchDir::FromEnd); if (ChunkStructures.Num() > MaxChunkStructures) { MaxChunkStructures = ChunkStructures.Num(); } FChunkSourceDetails PartInfo; PartInfo.InstalledFileManifest = InstalledFileManifest; PartInfo.OffsetInInstalledFile = FileOffset; PartInfo.OffsetInChunk = ChunkPart.Offset; PartInfo.SizeOfChunkPart = ChunkPart.Size; ChunkAssemblyInfo.SourceDetails.Add(PartInfo); const FChunkInfo* ChunkInfoPtr = InstalledManifest->GetChunkInfo(ChunkPart.Guid); if (PartialStructure.GetHead() == PartialStructure.GetTail() && PartialStructure.GetHead()->GetSize() == ChunkInfoPtr->WindowSize) { // Done with this chunk, free the structures space. ChunkAssemblyInfo.bComplete = true; ChunkStructures.Remove(ChunkPart.Guid); } } } // Move the full chunks we have available over to our internal structures. uint32 IncompleteChunks = 0; uint32 ChunksInAnotherSource = 0; uint32 CompleteChunks = 0; for (TPair& ChunkAssemblyPair : ChunkAssemblyInfos) { if (!ChunkAssemblyPair.Value.bComplete) { IncompleteChunks++; continue; } TArray& ChunkSource = ChunkSources.FindOrAdd(ChunkAssemblyPair.Key); if (ChunkSource.Num()) { ChunksInAnotherSource++; continue; } ChunkSource = MoveTemp(ChunkAssemblyPair.Value.SourceDetails); CompleteChunks++; AvailableInBuilds.Add(ChunkAssemblyPair.Key); } UE_LOG(LogInstallChunkSource, Verbose, TEXT("Got %u complete chunks, %u incomplete chunks, %u repeat chunks"), CompleteChunks, IncompleteChunks, ChunksInAnotherSource); if (CompleteChunks) { InstallationSources.Add(InstallationSourcePair); } } UE_LOG(LogInstallChunkSource, Log, TEXT("Useful Sources:%d. Available Chunks:%d."), InstallationSources.Num(), AvailableInBuilds.Num()); } FInstallChunkSource::~FInstallChunkSource() { } void FInstallChunkSource::GetChunksForFile(const FString& FilePath, TSet& OutChunks) const { const FFileManifest* FileManifest = nullptr; for (const TPair& Pair : InstallationSources) { if (FilePath.StartsWith(Pair.Key)) { FString BuildRelativeFilePath = FilePath; FPaths::MakePathRelativeTo(BuildRelativeFilePath, *(Pair.Key / TEXT(""))); FileManifest = Pair.Value->GetFileManifest(BuildRelativeFilePath); break; } } if (FileManifest != nullptr) { for (const TPair>& ChunkSource : ChunkSources) { bool bNeedsThisFile = false; for (const FChunkSourceDetails& Details : ChunkSource.Value) { if (Details.InstalledFileManifest == FileManifest) { bNeedsThisFile = true; break; } } if (bNeedsThisFile) { OutChunks.Add(ChunkSource.Key); } } } } IConstructorChunkSource::FRequestProcessFn FInstallChunkSource::CreateRequest(const FGuid& DataId, FMutableMemoryView DestinationBuffer, void* UserPtr, FChunkRequestCompleteDelegate CompleteFn) { const FString* FoundInstallDirectory; const FBuildPatchAppManifest* FoundInstallManifest; FindChunkLocation(DataId, &FoundInstallDirectory, &FoundInstallManifest); if (FoundInstallDirectory == nullptr || FoundInstallManifest == nullptr) { CompleteFn.Execute(DataId, false, true, UserPtr); return [](bool) {return;}; } // This request can be called from multiple threads, including on the same source file. return [this, FoundInstallManifest, FoundInstallDirectory = *FoundInstallDirectory, DataId, DestinationBuffer, UserPtr, CompleteFn](bool bIsAborted) { ISpeedRecorder::FRecord ActivityRecord; ActivityRecord.CyclesStart = FStatsCollector::GetCycles(); InstallChunkSourceStat->OnLoadStarted(DataId); TRACE_CPUPROFILER_EVENT_SCOPE(InstallRead); if (bIsAborted) { ActivityRecord.CyclesEnd = ActivityRecord.CyclesStart; InstallChunkSourceStat->OnLoadComplete(DataId, IInstallChunkSourceStat::ELoadResult::Aborted, ActivityRecord); CompleteFn.Execute(DataId, true, false, UserPtr); return; } const TArray* ChunkSource = ChunkSources.Find(DataId); const FChunkInfo* ChunkInfoPtr = FoundInstallManifest->GetChunkInfo(DataId); if (!ChunkInfoPtr || !ChunkSource) { ActivityRecord.CyclesEnd = FStatsCollector::GetCycles(); InstallChunkSourceStat->OnLoadComplete(DataId, IInstallChunkSourceStat::ELoadResult::MissingPartInfo, ActivityRecord); CompleteFn.Execute(DataId, false, true, UserPtr); return; } FBlockStructure ChunkBlocks; IInstallChunkSourceStat::ELoadResult Result = IInstallChunkSourceStat::ELoadResult::Success; bool bLoadedWholeChunk = false; for (int32 FileChunkPartsIdx = 0; FileChunkPartsIdx < ChunkSource->Num(); ++FileChunkPartsIdx) { if (bLoadedWholeChunk) { // The manifest gave us more chunk parts than we needed to generate the full chunk. This shouldn't happen, // and so conceptually is an error, but since we have all the data we can technically proceed. // This seems to happen with some regularity - need to understand why. //UE_LOG(LogInstallChunkSource, Display, TEXT("Chunk %s had more chunk sources than necessary to re-assemble"), *WriteToString<40>(DataId)); break; } const FChunkSourceDetails& FileChunkPart = ChunkSource->operator[](FileChunkPartsIdx); // Validate the chunk can load into the destination. uint32 ChunkEndLocation = FileChunkPart.OffsetInChunk + FileChunkPart.SizeOfChunkPart; if (ChunkEndLocation > DestinationBuffer.GetSize()) { // The chunk metadata tried to assemble larger than the chunk itself - error UE_LOG(LogInstallChunkSource, Error, TEXT("Chunk %s assembled larger than the actual chunk size (chunk wanted end %u vs buffer size %llu"), *WriteToString<40>(DataId), ChunkEndLocation, DestinationBuffer.GetSize()); Result = IInstallChunkSourceStat::ELoadResult::InvalidChunkParts; break; } FString FullFilename = FoundInstallDirectory / FileChunkPart.InstalledFileManifest->Filename; // We use the internal FArchive pointer so that we don't have to hold the file handle // lock over the read - the uniqueptr might move around, but the managed pointer will not. // We do know that our specific file won't get deleted until we are done, but it's possible that // multiple threads are here looking for the same file. FOpenedFile* OpenedFile = nullptr; { OpenedFileHandlesLock.Lock(); TUniquePtr* OpenedFilePtr = OpenedFileHandles.Find(FullFilename); if (OpenedFilePtr == nullptr) { TRACE_CPUPROFILER_EVENT_SCOPE(Install_OpenSource); TUniquePtr NewReader = FileSystem->CreateFileReader(*FullFilename); if (NewReader.IsValid()) { TUniquePtr NewOpenedFile = MakeUnique(MoveTemp(NewReader)); OpenedFileHandles.Add(FullFilename, MoveTemp(NewOpenedFile)); OpenedFilePtr = OpenedFileHandles.Find(FullFilename); } } if (OpenedFilePtr) { OpenedFile = OpenedFilePtr->Get(); } OpenedFileHandlesLock.Unlock(); } if (OpenedFile == nullptr) { UE_LOG(LogInstallChunkSource, Error, TEXT("Failed to open: %s on chunk %s"), *FileChunkPart.InstalledFileManifest->Filename, *LexToString(DataId)); Result = IInstallChunkSourceStat::ELoadResult::OpenFileFail; break; } { UE::TUniqueLock HandleLock(OpenedFile->FileHandleLock); TRACE_CPUPROFILER_EVENT_SCOPE(Install_Serialize); OpenedFile->FileHandle->Seek(FileChunkPart.OffsetInInstalledFile); OpenedFile->FileHandle->Serialize((uint8*)DestinationBuffer.GetData() + FileChunkPart.OffsetInChunk, FileChunkPart.SizeOfChunkPart); ActivityRecord.Size += FileChunkPart.SizeOfChunkPart; FBlockStructure NewChunk; NewChunk.Add(FileChunkPart.OffsetInChunk, FileChunkPart.SizeOfChunkPart); if (NewChunk.Intersect(ChunkBlocks).GetHead() != nullptr) { // This used to be allowed but in advance of multi threaded reading we want to make sure this // doesn't happen anymore (already shouldn't be...) OpenedFile->FileHandleLock.Unlock(); UE_LOG(LogInstallChunkSource, Error, TEXT("Chunk %s had overlapping chunk parts"), *WriteToString<40>(DataId)); Result = IInstallChunkSourceStat::ELoadResult::InvalidChunkParts; break; } ChunkBlocks.Add(FileChunkPart.OffsetInChunk, FileChunkPart.SizeOfChunkPart); } // The expectation is that we get the full chunk only once we've assembled all of the parts // provided by the manifest, so the last iteration should set this to true. If it isn't the last // iteration, then we'll hit the faux-error case at the top of the loop. bLoadedWholeChunk = ChunkBlocks.GetHead() && ChunkBlocks.GetHead() == ChunkBlocks.GetTail() && ChunkBlocks.GetHead()->GetSize() == ChunkInfoPtr->WindowSize; } if (!bLoadedWholeChunk) { if (Result == IInstallChunkSourceStat::ELoadResult::Success) { // If we failed without hitting a different case, then we just didn't have enough parts. Result = IInstallChunkSourceStat::ELoadResult::InvalidChunkParts; } ActivityRecord.CyclesEnd = FStatsCollector::GetCycles(); InstallChunkSourceStat->OnLoadComplete(DataId, IInstallChunkSourceStat::ELoadResult::MissingPartInfo, ActivityRecord); CompleteFn.Execute(DataId, false, !bLoadedWholeChunk, UserPtr); return; } // We set this here because it is used to compute the IO speeds, however we can't call OnLoadComplete because we don't know // the hash result yet. ActivityRecord.CyclesEnd = FStatsCollector::GetCycles(); // Check chunk hash. UE::Tasks::Launch(TEXT("Install_Hash"), [DataId, UserPtr, DestinationBuffer, FoundInstallManifest, CompleteFn, ActivityRecord, InstallChunkSourceStat=InstallChunkSourceStat]() { IInstallChunkSourceStat::ELoadResult Result = IInstallChunkSourceStat::ELoadResult::Success; { TRACE_CPUPROFILER_EVENT_SCOPE(InstallHash); FSHAHash ChunkShaHash; uint64 ChunkRollingHash = 0; if (FoundInstallManifest->GetChunkShaHash(DataId, ChunkShaHash)) { if (GetShaHashForDataSet(DestinationBuffer.GetData(), DestinationBuffer.GetSize()) != ChunkShaHash) { Result = IInstallChunkSourceStat::ELoadResult::HashCheckFailed; } } else if (FoundInstallManifest->GetChunkHash(DataId, ChunkRollingHash)) { if (FRollingHash::GetHashForDataSet((const uint8*)DestinationBuffer.GetData(), DestinationBuffer.GetSize()) != ChunkRollingHash) { Result = IInstallChunkSourceStat::ELoadResult::HashCheckFailed; } } else { Result = IInstallChunkSourceStat::ELoadResult::MissingHashInfo; } } InstallChunkSourceStat->OnLoadComplete( DataId, Result, ActivityRecord); CompleteFn.Execute(DataId, false, Result != IInstallChunkSourceStat::ELoadResult::Success, UserPtr); } ); }; } int32 FInstallChunkSource::GetChunkUnavailableAt(const FGuid& DataId) const { if (FileRetirementPositions.Num() == 0) { // If we aren't destructive then it's always available. return TNumericLimits::Max(); } const FString* FoundInstallDirectory; const FBuildPatchAppManifest* FoundInstallManifest; FindChunkLocation(DataId, &FoundInstallDirectory, &FoundInstallManifest); if (FoundInstallDirectory == nullptr || FoundInstallManifest == nullptr) { return TNumericLimits::Max(); } // This chunk is no longer available as soon as the first file containing a part is complete (if destructive install) int32 ChunkUnavailableAt = TNumericLimits::Max(); const TArray* ChunkSource = ChunkSources.Find(DataId); if (ChunkSource) { for (const FChunkSourceDetails& Part : *ChunkSource) { const int32* FirstIndexAfterFile = FileRetirementPositions.Find(Part.InstalledFileManifest->Filename); if (FirstIndexAfterFile && *FirstIndexAfterFile < ChunkUnavailableAt) { ChunkUnavailableAt = *FirstIndexAfterFile; } } } return ChunkUnavailableAt; } void FInstallChunkSource::FindChunkLocation(const FGuid& DataId, const FString** FoundInstallDirectory, const FBuildPatchAppManifest** FoundInstallManifest) const { uint64 ChunkHash; *FoundInstallDirectory = nullptr; *FoundInstallManifest = nullptr; for (const TPair& Pair : InstallationSources) { // GetChunkHash can be used as a check for whether this manifest references this chunk. if (Pair.Value->GetChunkHash(DataId, ChunkHash)) { *FoundInstallDirectory = &Pair.Key; *FoundInstallManifest = &Pair.Value.Get(); return; } } } IConstructorInstallChunkSource* IConstructorInstallChunkSource::CreateInstallSource(IFileSystem* FileSystem, IInstallChunkSourceStat* InstallChunkSourceStat, const TMultiMap& InstallationSources, const TSet& ChunksThatWillBeNeeded) { return new FInstallChunkSource(FileSystem, InstallChunkSourceStat, InstallationSources, ChunksThatWillBeNeeded); } const TCHAR* ToString(const IInstallChunkSourceStat::ELoadResult& LoadResult) { switch(LoadResult) { case IInstallChunkSourceStat::ELoadResult::Success: return TEXT("Success"); case IInstallChunkSourceStat::ELoadResult::MissingHashInfo: return TEXT("MissingHashInfo"); case IInstallChunkSourceStat::ELoadResult::MissingPartInfo: return TEXT("MissingPartInfo"); case IInstallChunkSourceStat::ELoadResult::OpenFileFail: return TEXT("OpenFileFail"); case IInstallChunkSourceStat::ELoadResult::HashCheckFailed: return TEXT("HashCheckFailed"); case IInstallChunkSourceStat::ELoadResult::Aborted: return TEXT("Aborted"); case IInstallChunkSourceStat::ELoadResult::InvalidChunkParts: return TEXT("InvalidChunkParts"); default: return TEXT("Unknown"); } } }