4093 lines
144 KiB
C++
4093 lines
144 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "BuildPatchFileConstructor.h"
|
|
#include "IBuildManifestSet.h"
|
|
#include "HAL/UESemaphore.h"
|
|
#include "HAL/PlatformFileManager.h"
|
|
#include "HAL/FileManager.h"
|
|
#include "HAL/RunnableThread.h"
|
|
#include "Hash/xxhash.h"
|
|
#include "Misc/ConfigCacheIni.h"
|
|
#include "Misc/Paths.h"
|
|
#include "Misc/ScopeLock.h"
|
|
#include "Misc/OutputDeviceRedirector.h"
|
|
#include "BuildPatchServicesPrivate.h"
|
|
#include "Interfaces/IBuildInstaller.h"
|
|
#include "Data/ChunkData.h"
|
|
#include "Common/StatsCollector.h"
|
|
#include "Common/SpeedRecorder.h"
|
|
#include "Common/FileSystem.h"
|
|
#include "Compression/CompressionUtil.h"
|
|
#include "Installer/ChunkSource.h"
|
|
#include "Installer/ChunkDbChunkSource.h"
|
|
#include "Installer/InstallChunkSource.h"
|
|
#include "Installer/CloudChunkSource.h"
|
|
#include "Installer/ChunkReferenceTracker.h"
|
|
#include "Installer/InstallerError.h"
|
|
#include "Installer/InstallerAnalytics.h"
|
|
#include "Installer/InstallerSharedContext.h"
|
|
#include "Installer/MessagePump.h"
|
|
#include "Templates/Greater.h"
|
|
#include "BuildPatchUtil.h"
|
|
#include "ProfilingDebugging/CountersTrace.h"
|
|
|
|
using namespace BuildPatchServices;
|
|
|
|
static int32 SleepTimeWhenFileSystemThrottledSeconds = 1;
|
|
static FAutoConsoleVariableRef CVarSleepTimeWhenFileSystemThrottledSeconds(
|
|
TEXT("BuildPatchFileConstructor.SleepTimeWhenFileSystemThrottledSeconds"),
|
|
SleepTimeWhenFileSystemThrottledSeconds,
|
|
TEXT("The amount of time to sleep if the destination filesystem is throttled."),
|
|
ECVF_Default);
|
|
|
|
// This can be overridden by the installation parameters.
|
|
static bool bCVarStallWhenFileSystemThrottled = false;
|
|
static FAutoConsoleVariableRef CVarStallWhenFileSystemThrottled(
|
|
TEXT("BuildPatchFileConstructor.bStallWhenFileSystemThrottled"),
|
|
bCVarStallWhenFileSystemThrottled,
|
|
TEXT("Whether to stall if the file system is throttled"),
|
|
ECVF_Default);
|
|
|
|
static bool bCVarAllowMultipleFilesInFlight = true;
|
|
static FAutoConsoleVariableRef CVarAllowMultipleFilesInFlight(
|
|
TEXT("BuildPatchFileConstructor.bCVarAllowMultipleFilesInFlight"),
|
|
bCVarAllowMultipleFilesInFlight,
|
|
TEXT("Whether to allow multiple files to be constructed at the same time, though still sequentially."),
|
|
ECVF_Default);
|
|
|
|
static int32 CVarDisableResumeBelowMB = 0;
|
|
static FAutoConsoleVariableRef CVarRefDisableResumeBelowMB(
|
|
TEXT("BuildPatchFileConstructor.DisableResumeBelowMB"),
|
|
CVarDisableResumeBelowMB,
|
|
TEXT("If nonzero, installs (not patches) below this size will not create or check any resume data."),
|
|
ECVF_Default);
|
|
|
|
// Helper functions wrapping common code.
|
|
namespace FileConstructorHelpers
|
|
{
|
|
void WaitWhilePaused(FThreadSafeBool& bIsPaused, FThreadSafeBool& bShouldAbort)
|
|
{
|
|
// Wait while paused
|
|
while (bIsPaused && !bShouldAbort)
|
|
{
|
|
FPlatformProcess::Sleep(0.5f);
|
|
}
|
|
}
|
|
|
|
bool CheckRemainingDiskSpace(const FString& InstallDirectory, uint64 RemainingBytesRequired, uint64& OutAvailableDiskSpace)
|
|
{
|
|
bool bContinueConstruction = true;
|
|
uint64 TotalSize = 0;
|
|
OutAvailableDiskSpace = 0;
|
|
if (FPlatformMisc::GetDiskTotalAndFreeSpace(InstallDirectory, TotalSize, OutAvailableDiskSpace))
|
|
{
|
|
if (OutAvailableDiskSpace < RemainingBytesRequired)
|
|
{
|
|
bContinueConstruction = false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// If we can't get the disk space free then the most likely reason is the drive is no longer around...
|
|
bContinueConstruction = false;
|
|
}
|
|
|
|
return bContinueConstruction;
|
|
}
|
|
|
|
uint64 CalculateRequiredDiskSpace(const FBuildPatchAppManifestPtr& CurrentManifest, const FBuildPatchAppManifestRef& BuildManifest, const EInstallMode& InstallMode, const TSet<FString>& InInstallTags)
|
|
{
|
|
// Make tags expected
|
|
TSet<FString> InstallTags = InInstallTags;
|
|
if (InstallTags.Num() == 0)
|
|
{
|
|
BuildManifest->GetFileTagList(InstallTags);
|
|
}
|
|
InstallTags.Add(TEXT(""));
|
|
// Calculate the files that need constructing.
|
|
TSet<FString> TaggedFiles;
|
|
BuildManifest->GetTaggedFileList(InstallTags, TaggedFiles);
|
|
FString DummyString;
|
|
TSet<FString> FilesToConstruct;
|
|
BuildManifest->GetOutdatedFiles(CurrentManifest.Get(), DummyString, TaggedFiles, FilesToConstruct);
|
|
// Count disk space needed by each operation.
|
|
int64 DiskSpaceDeltaPeak = 0;
|
|
if (InstallMode == EInstallMode::DestructiveInstall && CurrentManifest.IsValid())
|
|
{
|
|
// The simplest method will be to run through each high level file operation, tracking peak disk usage delta.
|
|
int64 DiskSpaceDelta = 0;
|
|
|
|
// Loop through all files to be made next, in order.
|
|
// This is sorted coming in and needs to stay in that order to pass BPT test suite
|
|
//FilesToConstruct.Sort(TLess<FString>());
|
|
for (const FString& FileToConstruct : FilesToConstruct)
|
|
{
|
|
// First we would need to make the new file.
|
|
DiskSpaceDelta += BuildManifest->GetFileSize(FileToConstruct);
|
|
if (DiskSpaceDeltaPeak < DiskSpaceDelta)
|
|
{
|
|
DiskSpaceDeltaPeak = DiskSpaceDelta;
|
|
}
|
|
// Then we can remove the current existing file.
|
|
DiskSpaceDelta -= CurrentManifest->GetFileSize(FileToConstruct);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// When not destructive, or no CurrentManifest, we always stage all new and changed files.
|
|
DiskSpaceDeltaPeak = BuildManifest->GetFileSize(FilesToConstruct);
|
|
}
|
|
return FMath::Max<int64>(DiskSpaceDeltaPeak, 0);
|
|
}
|
|
}
|
|
|
|
|
|
struct FAdministrationScope
|
|
{
|
|
ISpeedRecorder::FRecord ActivityRecord;
|
|
IFileConstructorStat* FileConstructorStat;
|
|
FAdministrationScope(IFileConstructorStat* InFileConstructorStat)
|
|
{
|
|
FileConstructorStat = InFileConstructorStat;
|
|
FileConstructorStat->OnBeforeAdminister();
|
|
ActivityRecord.CyclesStart = FStatsCollector::GetCycles();
|
|
}
|
|
~FAdministrationScope()
|
|
{
|
|
ActivityRecord.CyclesEnd = FStatsCollector::GetCycles();
|
|
ActivityRecord.Size = 0;
|
|
FileConstructorStat->OnAfterAdminister(ActivityRecord);
|
|
}
|
|
};
|
|
|
|
struct FReadScope
|
|
{
|
|
ISpeedRecorder::FRecord ActivityRecord;
|
|
IFileConstructorStat* FileConstructorStat;
|
|
FReadScope(IFileConstructorStat* InFileConstructorStat, int64 Size)
|
|
{
|
|
FileConstructorStat = InFileConstructorStat;
|
|
ActivityRecord.CyclesStart = FStatsCollector::GetCycles();
|
|
ActivityRecord.Size = Size;
|
|
FileConstructorStat->OnBeforeRead();
|
|
}
|
|
~FReadScope()
|
|
{
|
|
ActivityRecord.CyclesEnd = FStatsCollector::GetCycles();
|
|
FileConstructorStat->OnAfterRead(ActivityRecord);
|
|
}
|
|
};
|
|
|
|
enum class EConstructionError : uint8
|
|
{
|
|
None = 0,
|
|
CannotCreateFile,
|
|
OutOfDiskSpace,
|
|
FailedInitialSizeCheck,
|
|
MissingChunk,
|
|
SerializeError,
|
|
TrackingError,
|
|
OutboundDataError,
|
|
InternalConsistencyError,
|
|
Aborted,
|
|
MissingFileInfo
|
|
};
|
|
|
|
|
|
// Since we can have more than one file in flight, store state here.
|
|
struct FFileConstructionState
|
|
{
|
|
FGuid ErrorContextGuid;
|
|
EConstructionError ConstructionError = EConstructionError::None;
|
|
int32 CreateFilePlatformLastError = 0;
|
|
|
|
FSHA1 HashState;
|
|
|
|
// If this is true then we didn't actually have to make the file, it was already done or a symlink or something.
|
|
bool bSkippedConstruction = false;
|
|
bool bSuccess = true;
|
|
bool bIsResumedFile = false;
|
|
|
|
// We track how far we are in the file when we write into the write buffer so that
|
|
// we advance progress bars smoothly instead of in huge writebuffer sized chunks.
|
|
UE::FMutex ProgressLock;
|
|
uint64 Progress = 0;
|
|
uint64 LastSeenProgress = 0; // not locked.
|
|
|
|
// Where we started constructing the file. Can be non zero due to resume.
|
|
int64 StartPosition = 0;
|
|
|
|
int32 OutstandingBatches = 0;
|
|
|
|
// Can be nonzero in the first batch due to resume.
|
|
int32 NextChunkPartToRead = 0;
|
|
|
|
// This is null if we are constructing/install in memory.
|
|
TUniquePtr<FArchive> NewFile;
|
|
|
|
// Which index in the chunk reference tracker this file starts at.
|
|
int32 BaseReferenceIndex = 0;
|
|
|
|
int32 ConstructionIndex = -1;
|
|
const FFileManifest* FileManifest;
|
|
const FString& BuildFilename; // name as it is in the manifest, references the ConstructList in the configuration.
|
|
FString NewFilename; // with output path and such
|
|
|
|
FFileConstructionState(const FFileManifest* InFileManifest, const FString& InBuildFilename, FString&& InNewFilename)
|
|
: FileManifest(InFileManifest)
|
|
, BuildFilename(InBuildFilename)
|
|
, NewFilename(InNewFilename)
|
|
{
|
|
if (!InFileManifest)
|
|
{
|
|
bSuccess = false;
|
|
ConstructionError = EConstructionError::MissingFileInfo;
|
|
}
|
|
}
|
|
|
|
void SetAttributes()
|
|
{
|
|
#if PLATFORM_MAC
|
|
if (bSuccess &&
|
|
EnumHasAllFlags(FileManifest->FileMetaFlags, EFileMetaFlags::UnixExecutable))
|
|
{
|
|
// Enable executable permission bit
|
|
struct stat FileInfo;
|
|
if (stat(TCHAR_TO_UTF8(*NewFilename), &FileInfo) == 0)
|
|
{
|
|
bSuccess = chmod(TCHAR_TO_UTF8(*NewFilename), FileInfo.st_mode | S_IXUSR | S_IXGRP | S_IXOTH) == 0;
|
|
if (!bSuccess)
|
|
{
|
|
UE_LOG(LogBuildPatchServices, Error, TEXT("FBuildPatchFileConstructor: Failed to set exec bit %s"), *BuildFilename);
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
#if PLATFORM_ANDROID
|
|
if (bSuccess)
|
|
{
|
|
IFileManager::Get().SetTimeStamp(*NewFilename, FDateTime::UtcNow());
|
|
}
|
|
#endif
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* This struct handles loading and saving of simple resume information, that will allow us to decide which
|
|
* files should be resumed from. It will also check that we are creating the same version and app as we expect to be.
|
|
*/
|
|
struct FResumeData
|
|
{
|
|
public:
|
|
// File system dependency
|
|
const IFileSystem* const FileSystem;
|
|
|
|
// The manifests for the app we are installing
|
|
const IBuildManifestSet* const ManifestSet;
|
|
|
|
// Save the staging directory
|
|
const FString StagingDir;
|
|
|
|
// The filename to the resume data information
|
|
const FString ResumeDataFilename;
|
|
|
|
// The resume ids that we loaded from disk
|
|
TSet<FString> LoadedResumeIds;
|
|
|
|
// The set of files that were started
|
|
TSet<FString> FilesStarted;
|
|
|
|
// The set of files that were completed, determined by expected file size
|
|
TSet<FString> FilesCompleted;
|
|
|
|
// The set of files that exist but are not able to assume resumable
|
|
TSet<FString> FilesIncompatible;
|
|
|
|
// Whether we have any resume data for this install
|
|
bool bHasResumeData = false;
|
|
|
|
// For small installs we may disable resume entirely to mitigate the number of file operations.
|
|
bool bResumeEnabled = false;
|
|
|
|
public:
|
|
|
|
FResumeData(IFileSystem* InFileSystem, IBuildManifestSet* InManifestSet, const FString& InStagingDir, const FString& InResumeDataFilename)
|
|
: FileSystem(InFileSystem)
|
|
, ManifestSet(InManifestSet)
|
|
, StagingDir(InStagingDir)
|
|
, ResumeDataFilename(InResumeDataFilename)
|
|
{
|
|
// Leave resume disabled until initialized;
|
|
}
|
|
|
|
void InitResume()
|
|
{
|
|
bResumeEnabled = true;
|
|
|
|
// Load data from previous resume file
|
|
bHasResumeData = FileSystem->FileExists(*ResumeDataFilename);
|
|
GLog->Logf(TEXT("BuildPatchResumeData file found: %s"), bHasResumeData ? TEXT("true") : TEXT("false"));
|
|
if (bHasResumeData)
|
|
{
|
|
// Grab existing resume metadata.
|
|
const bool bCullEmptyLines = true;
|
|
FString PrevResumeData;
|
|
TArray<FString> PrevResumeDataLines;
|
|
FileSystem->LoadFileToString(*ResumeDataFilename, PrevResumeData);
|
|
PrevResumeData.ParseIntoArrayLines(PrevResumeDataLines, bCullEmptyLines);
|
|
// Grab current resume ids
|
|
const bool bCheckLegacyIds = true;
|
|
TSet<FString> NewResumeIds;
|
|
ManifestSet->GetInstallResumeIds(NewResumeIds, bCheckLegacyIds);
|
|
LoadedResumeIds.Reserve(PrevResumeDataLines.Num());
|
|
// Check if any builds we are installing are a resume from previous run.
|
|
for (FString& PrevResumeDataLine : PrevResumeDataLines)
|
|
{
|
|
PrevResumeDataLine.TrimStartAndEndInline();
|
|
LoadedResumeIds.Add(PrevResumeDataLine);
|
|
if (NewResumeIds.Contains(PrevResumeDataLine))
|
|
{
|
|
bHasResumeData = true;
|
|
GLog->Logf(TEXT("BuildPatchResumeData version matched %s"), *PrevResumeDataLine);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Saves out the resume data
|
|
*/
|
|
void SaveOut(const TSet<FString>& ResumeIds)
|
|
{
|
|
// Save out the patch versions
|
|
if (bResumeEnabled)
|
|
{
|
|
FileSystem->SaveStringToFile(*ResumeDataFilename, FString::Join(ResumeIds, TEXT("\n")));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks whether the file was completed during last install attempt and adds it to FilesCompleted if so
|
|
* @param Filename The filename to check
|
|
*/
|
|
void CheckFile(const FString& Filename)
|
|
{
|
|
// If we had resume data, check if this file might have been resumable
|
|
if (bHasResumeData)
|
|
{
|
|
int64 DiskFileSize;
|
|
const FString FullFilename = StagingDir / Filename;
|
|
const bool bFileExists = FileSystem->GetFileSize(*FullFilename, DiskFileSize);
|
|
const bool bCheckLegacyIds = true;
|
|
TSet<FString> FileResumeIds;
|
|
ManifestSet->GetInstallResumeIdsForFile(Filename, FileResumeIds, bCheckLegacyIds);
|
|
if (LoadedResumeIds.Intersect(FileResumeIds).Num() > 0)
|
|
{
|
|
const FFileManifest* NewFileManifest = ManifestSet->GetNewFileManifest(Filename);
|
|
if (NewFileManifest && bFileExists)
|
|
{
|
|
const uint64 UnsignedDiskFileSize = DiskFileSize;
|
|
if (UnsignedDiskFileSize > 0 && UnsignedDiskFileSize <= NewFileManifest->FileSize)
|
|
{
|
|
FilesStarted.Add(Filename);
|
|
}
|
|
if (UnsignedDiskFileSize == NewFileManifest->FileSize)
|
|
{
|
|
FilesCompleted.Add(Filename);
|
|
}
|
|
if (UnsignedDiskFileSize > NewFileManifest->FileSize)
|
|
{
|
|
FilesIncompatible.Add(Filename);
|
|
}
|
|
}
|
|
}
|
|
else if (bFileExists)
|
|
{
|
|
FilesIncompatible.Add(Filename);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
static FString FormatNumber(uint64 Value) { return FText::AsNumber(Value).ToString(); }
|
|
|
|
|
|
// We need a place to put chunks if we know we're going to need them again after
|
|
// their source retires. We don't want to route everything through here. We also
|
|
// want to be able to optionally overflow to disk, and ideally this would be persistent
|
|
// across resumes so that we don't have to re-download a huge amount of harvested chunks
|
|
// from install files.
|
|
class FChunkBackingStore : IConstructorChunkSource
|
|
{
|
|
FBuildPatchFileConstructor* ParentConstructor = nullptr;
|
|
const FString& InstallDirectory;
|
|
FBuildPatchFileConstructor::FBackingStoreStats& Stats;
|
|
|
|
struct FStoredChunk
|
|
{
|
|
TArray<uint8> ChunkData;
|
|
uint32 ChunkSize = 0;
|
|
int32 NextUsageIndex;
|
|
int32 LastUsageIndex;
|
|
|
|
// Has the data made it into memory yet?
|
|
bool bCommitted = false;
|
|
|
|
// Have we been evicted for memory concerns? We are in UsedEntries if so.
|
|
bool bBackedByDisk = false;
|
|
|
|
// When something is reading/writing to us we mark that so we don't
|
|
// evict during async operations.
|
|
uint16 LockCount = 0;
|
|
};
|
|
|
|
TMap<FGuid, FStoredChunk> StoredChunks;
|
|
uint64 CurrentMemoryLoad = 0;
|
|
uint64 PeakMemoryLoad = 0;
|
|
|
|
int32 ReadCount = 0;
|
|
int32 WriteCount = 0;
|
|
|
|
static constexpr uint64 ChunkStoreMemoryLimitDisabledSentinel = TNumericLimits<uint64>::Max();
|
|
uint64 MaxMemoryBytes = 1 << 30; // 1GB.
|
|
|
|
|
|
// 0 disables
|
|
uint64 MaxDiskSpaceBytes = 1 << 30; // 1GB.
|
|
uint64 AdditionalDiskSpaceHeadroomBytes = 0;
|
|
uint64 InstallationFreeSpaceRequired = 0;
|
|
|
|
// The backing store allocates in 128kb chunks.
|
|
static constexpr uint32 BitsPerEntry = 17;
|
|
struct FBackingStoreFreeSpan
|
|
{
|
|
uint32 StartEntryIndex = 0;
|
|
uint32 EndEntryIndex = 0;
|
|
uint64 Size() const { return EntryCount() << BitsPerEntry; }
|
|
uint64 Offset() const { return (uint64)StartEntryIndex << BitsPerEntry; }
|
|
uint64 EntryCount() const { return EndEntryIndex - StartEntryIndex; }
|
|
};
|
|
struct FBackingStoreUsedSpan
|
|
{
|
|
uint32 StartEntryIndex = 0;
|
|
uint32 EndEntryIndex = 0;
|
|
uint32 UsedBytes = 0;
|
|
FXxHash64 Hash;
|
|
uint64 ReservedSize() const { return EntryCount() << BitsPerEntry; }
|
|
uint64 Offset() const { return (uint64)StartEntryIndex << BitsPerEntry; }
|
|
uint64 EntryCount() const { return EndEntryIndex - StartEntryIndex; }
|
|
};
|
|
|
|
uint32 BackingStoreMaxEntries = 0;
|
|
uint32 BackingStoreEntryCount = 0;
|
|
uint64 BackingStoreUsedSpace = 0;
|
|
uint64 BackingStoreWastedSpace = 0;
|
|
TMap<FGuid, FBackingStoreUsedSpan> UsedDiskSpans;
|
|
TArray<FBackingStoreFreeSpan> FreeDiskSpans;
|
|
uint64 CurrentDiskLoad() const { return (uint64)(BackingStoreEntryCount) << BitsPerEntry; }
|
|
|
|
TUniquePtr<IFileHandle> BackingStoreFileHandle;
|
|
FString BackingStoreFileName;
|
|
|
|
void DumpFreeDiskSpans()
|
|
{
|
|
UE_LOG(LogBuildPatchServices, Display, TEXT("Backing Store (Entries / Max): %d / %d"), BackingStoreEntryCount, BackingStoreMaxEntries);
|
|
UE_LOG(LogBuildPatchServices, Display, TEXT("Dumping Free Disk Spans..."));
|
|
|
|
for (int32 FreeIndex = 0; FreeIndex < FreeDiskSpans.Num(); FreeIndex++)
|
|
{
|
|
UE_LOG(LogBuildPatchServices, Display, TEXT(" %d/%d: %d - %d"), FreeIndex, FreeDiskSpans.Num(), FreeDiskSpans[FreeIndex].StartEntryIndex, FreeDiskSpans[FreeIndex].EndEntryIndex);
|
|
}
|
|
}
|
|
// false means consistency failure
|
|
[[nodiscard]] bool ConsistencyCheck()
|
|
{
|
|
bool bSuccess = true;
|
|
|
|
uint64 UsedEntryReservedBytes = 0;
|
|
uint64 UsedEntryUsedBytes = 0;
|
|
for (TPair<FGuid, FBackingStoreUsedSpan>& Pair : UsedDiskSpans)
|
|
{
|
|
FBackingStoreUsedSpan& Span = Pair.Value;
|
|
UsedEntryReservedBytes += Span.ReservedSize();
|
|
UsedEntryUsedBytes += Span.UsedBytes;
|
|
}
|
|
|
|
uint64 FreeBytes = 0;
|
|
|
|
for (int32 FreeIndex = 0; FreeIndex < FreeDiskSpans.Num(); FreeIndex++)
|
|
{
|
|
FBackingStoreFreeSpan& Span = FreeDiskSpans[FreeIndex];
|
|
|
|
FreeBytes += Span.Size();
|
|
|
|
if (Span.Size() == 0)
|
|
{
|
|
UE_LOG(LogBuildPatchServices, Error, TEXT("BackingStore FreeList Merge Fail: empty entry failed to get deleted"));
|
|
bSuccess = false;
|
|
}
|
|
if (Span.EndEntryIndex > BackingStoreEntryCount)
|
|
{
|
|
UE_LOG(LogBuildPatchServices, Error, TEXT("BackingStore FreeList Merge Fail: entry exceeds backing store size"));
|
|
bSuccess = false;
|
|
}
|
|
if (Span.StartEntryIndex > Span.EndEntryIndex)
|
|
{
|
|
UE_LOG(LogBuildPatchServices, Error, TEXT("BackingStore FreeList Merge Fail: negative sized entry"));
|
|
bSuccess = false;
|
|
}
|
|
|
|
if (FreeIndex)
|
|
{
|
|
if (Span.StartEntryIndex == FreeDiskSpans[FreeIndex-1].EndEntryIndex)
|
|
{
|
|
UE_LOG(LogBuildPatchServices, Error, TEXT("BackingStore FreeList Merge Fail: adjacent entries failed to merge"));
|
|
bSuccess = false;
|
|
}
|
|
if (Span.StartEntryIndex < FreeDiskSpans[FreeIndex-1].EndEntryIndex)
|
|
{
|
|
UE_LOG(LogBuildPatchServices, Error, TEXT("BackingStore FreeList Merge Fail: adjacent entries overlap or are out of order"));
|
|
bSuccess = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!bSuccess)
|
|
{
|
|
DumpFreeDiskSpans();
|
|
}
|
|
|
|
if (UsedEntryReservedBytes != (BackingStoreUsedSpace + BackingStoreWastedSpace) ||
|
|
UsedEntryUsedBytes != BackingStoreUsedSpace ||
|
|
CurrentDiskLoad() - UsedEntryReservedBytes != FreeBytes)
|
|
{
|
|
UE_LOG(LogBuildPatchServices, Error, TEXT("Disk Backing Store Consistency Fail:"));
|
|
UE_LOG(LogBuildPatchServices, Error, TEXT("Actual:"));
|
|
UE_LOG(LogBuildPatchServices, Error, TEXT(" ReservedBytes: %s"), *FormatNumber(UsedEntryReservedBytes));
|
|
UE_LOG(LogBuildPatchServices, Error, TEXT(" WastedBytes: %s"), *FormatNumber(UsedEntryReservedBytes - UsedEntryUsedBytes));
|
|
UE_LOG(LogBuildPatchServices, Error, TEXT(" FreeBytes: %s"), *FormatNumber(FreeBytes));
|
|
UE_LOG(LogBuildPatchServices, Error, TEXT("Expected:"));
|
|
UE_LOG(LogBuildPatchServices, Error, TEXT(" ReservedBytes: %s"), *FormatNumber(BackingStoreUsedSpace + BackingStoreWastedSpace));
|
|
UE_LOG(LogBuildPatchServices, Error, TEXT(" WastedBytes: %s"), *FormatNumber(BackingStoreWastedSpace));
|
|
UE_LOG(LogBuildPatchServices, Error, TEXT(" FreeBytes: %s"), *FormatNumber(CurrentDiskLoad() - BackingStoreUsedSpace - BackingStoreWastedSpace));
|
|
bSuccess = false;
|
|
}
|
|
|
|
return bSuccess;
|
|
}
|
|
|
|
// false means consistency or write failure
|
|
// constructor thread
|
|
[[nodiscard]] bool PageOut(const FGuid& InGuid)
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(BackingStore_PageOut);
|
|
|
|
// InChunk must be valid for paging out at this point!
|
|
FStoredChunk& InChunk = StoredChunks[InGuid];
|
|
|
|
// It's possible we loaded back into memory but are already backed by disk so we can
|
|
// just free.
|
|
if (InChunk.bBackedByDisk)
|
|
{
|
|
InChunk.ChunkData.Empty();
|
|
CurrentMemoryLoad -= InChunk.ChunkSize;
|
|
ParentConstructor->SetChunkLocation(InGuid, EConstructorChunkLocation::DiskOverflow);
|
|
return true;
|
|
}
|
|
|
|
UE_LOG(LogBuildPatchServices, VeryVerbose, TEXT("Paging out: %s"), *WriteToString<40>(InGuid));
|
|
|
|
uint32 EntriesRequired = Align(InChunk.ChunkSize, (1 << BitsPerEntry)) >> BitsPerEntry;
|
|
|
|
int32 SpanIndex = 0;
|
|
while (SpanIndex < FreeDiskSpans.Num() &&
|
|
FreeDiskSpans[SpanIndex].EntryCount() < EntriesRequired)
|
|
{
|
|
SpanIndex++;
|
|
}
|
|
|
|
bool bAppendingToFile = false;
|
|
if (SpanIndex == FreeDiskSpans.Num())
|
|
{
|
|
// Check our disk space limitations.
|
|
uint64 UseMaxDiskSpaceBytes = MaxDiskSpaceBytes;
|
|
|
|
// If we have a headroom value, we want to dynamically adjust
|
|
// our max disk space so that we always leave that amount of disk space free.
|
|
// This is expected to almost always be enabled in order to prevent the backing store from
|
|
// eating into space reserved for the actual installation.
|
|
if (InstallationFreeSpaceRequired || AdditionalDiskSpaceHeadroomBytes)
|
|
{
|
|
uint64 TotalDiskBytes, FreeDiskBytes;
|
|
if (!FPlatformMisc::GetDiskTotalAndFreeSpace(InstallDirectory, TotalDiskBytes, FreeDiskBytes))
|
|
{
|
|
// If we fail to get disk space then disable it since we don't really know what we're doing
|
|
// at that point.
|
|
InstallationFreeSpaceRequired = 0;
|
|
AdditionalDiskSpaceHeadroomBytes = 0;
|
|
}
|
|
else
|
|
{
|
|
// The free space we got is counting any bytes we've already written to disk, so adjust for that.
|
|
uint64 FreeSizeBytesWithoutBackingStore = FreeDiskBytes + CurrentDiskLoad();
|
|
|
|
uint64 HeadroomRequiredBytes = InstallationFreeSpaceRequired + AdditionalDiskSpaceHeadroomBytes;
|
|
|
|
// By default we aren't allowed any space due to headroom limitations.
|
|
uint64 HeadroomRestrictedMaxDiskSpace = 0;
|
|
|
|
// If we have enough space above the headroom, then we can talk.
|
|
if (FreeSizeBytesWithoutBackingStore > HeadroomRequiredBytes)
|
|
{
|
|
HeadroomRestrictedMaxDiskSpace = FreeSizeBytesWithoutBackingStore - HeadroomRequiredBytes;
|
|
if (UseMaxDiskSpaceBytes == 0 || // Note if UseMaxDiskSpaceBytes is 0 then we they have no other limitation.
|
|
HeadroomRestrictedMaxDiskSpace < UseMaxDiskSpaceBytes)
|
|
{
|
|
UseMaxDiskSpaceBytes = HeadroomRestrictedMaxDiskSpace;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Need to expand the backing store.
|
|
if (BackingStoreFileHandle == nullptr ||
|
|
(UseMaxDiskSpaceBytes && (((uint64)BackingStoreEntryCount + EntriesRequired) << BitsPerEntry) > UseMaxDiskSpaceBytes))
|
|
{
|
|
// We can't expand - this fails to page out and gets evicted from the backing store.
|
|
Stats.DiskLostChunkCount++;
|
|
ParentConstructor->SetChunkLocation(InGuid, EConstructorChunkLocation::Cloud);
|
|
CurrentMemoryLoad -= InChunk.ChunkSize;
|
|
InChunk.ChunkData.Empty();
|
|
StoredChunks.Remove(InGuid);
|
|
return true;
|
|
}
|
|
|
|
FBackingStoreFreeSpan& NewSpan = FreeDiskSpans.AddDefaulted_GetRef();
|
|
NewSpan.StartEntryIndex = BackingStoreEntryCount;
|
|
BackingStoreEntryCount += EntriesRequired;
|
|
if (BackingStoreEntryCount > BackingStoreMaxEntries)
|
|
{
|
|
BackingStoreMaxEntries = BackingStoreEntryCount;
|
|
}
|
|
NewSpan.EndEntryIndex = NewSpan.StartEntryIndex + EntriesRequired;
|
|
bAppendingToFile = true;
|
|
|
|
Stats.DiskPeakUsageBytes = CurrentDiskLoad();
|
|
}
|
|
|
|
// SpanIndex is the one we just added or found to reuse
|
|
FBackingStoreFreeSpan& FreeSpan = FreeDiskSpans[SpanIndex];
|
|
FBackingStoreUsedSpan& UsedSpan = UsedDiskSpans.FindOrAdd(InGuid);
|
|
if (!UsedSpan.Hash.IsZero())
|
|
{
|
|
// Can't be paging out twice!
|
|
UE_LOG(LogBuildPatchServices, Error, TEXT("Consistency failure: Backing store used entry already existed for %s"), *WriteToString<40>(InGuid));
|
|
return false;
|
|
}
|
|
|
|
UsedSpan.StartEntryIndex = FreeSpan.StartEntryIndex;
|
|
UsedSpan.EndEntryIndex = UsedSpan.StartEntryIndex + EntriesRequired;
|
|
UsedSpan.UsedBytes = InChunk.ChunkSize;
|
|
|
|
FreeSpan.StartEntryIndex += EntriesRequired;
|
|
|
|
// LONGTERM - we should be using XX has for all chunk consistency checking, so we would have this value already.
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(BSPO_Hash);
|
|
UsedSpan.Hash = FXxHash64::HashBuffer(InChunk.ChunkData.GetData(), InChunk.ChunkData.Num());
|
|
}
|
|
|
|
if (FreeSpan.EndEntryIndex == FreeSpan.StartEntryIndex)
|
|
{
|
|
FreeDiskSpans.RemoveAt(SpanIndex);
|
|
}
|
|
|
|
// Write
|
|
{
|
|
WriteCount++;
|
|
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(BSPO_Write);
|
|
if (!BackingStoreFileHandle->Seek(UsedSpan.Offset()))
|
|
{
|
|
UE_LOG(LogBuildPatchServices, Error, TEXT("Failed to seek disk backing store to %llu"), UsedSpan.Offset());
|
|
return false;
|
|
}
|
|
|
|
if (!BackingStoreFileHandle->Write(InChunk.ChunkData.GetData(), InChunk.ChunkData.Num()))
|
|
{
|
|
UE_LOG(LogBuildPatchServices, Error, TEXT("Failed to write %llu bytes to disk backing store at %llu"), InChunk.ChunkData.Num(), UsedSpan.Offset());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
uint64 Wastage = UsedSpan.ReservedSize() - InChunk.ChunkData.Num();
|
|
|
|
// If we just added to the end of the file, we only wrote the size of the chunk data, not necessarily
|
|
// the size of our reservation, so top off with zeroes.
|
|
if (bAppendingToFile)
|
|
{
|
|
// This is at most a 128kb allocation (1 << BitsPerEntry), but the chunk size currently in use
|
|
// is 3 bytes shy of being perfectly aligned to a multiple of that meaning we only expect to need to write
|
|
// 3 bytes of zeroes. So we make sure we have enough space on the stack for that.
|
|
TArray<uint8, TInlineAllocator<128>> Zeroes;
|
|
Zeroes.AddZeroed(Wastage);
|
|
if (!BackingStoreFileHandle->Write(Zeroes.GetData(), Zeroes.Num()))
|
|
{
|
|
UE_LOG(LogBuildPatchServices, Error, TEXT("Failed to write %llu bytes of zeroes to disk backing store at %llu"), Zeroes.Num(), UsedSpan.Offset());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Done.
|
|
CurrentMemoryLoad -= InChunk.ChunkSize;
|
|
BackingStoreUsedSpace += InChunk.ChunkSize;
|
|
BackingStoreWastedSpace += Wastage;
|
|
InChunk.ChunkData.Empty();
|
|
InChunk.bBackedByDisk = true;
|
|
ParentConstructor->SetChunkLocation(InGuid, EConstructorChunkLocation::DiskOverflow);
|
|
|
|
return ConsistencyCheck();
|
|
}
|
|
|
|
// false means consistency failure
|
|
[[nodiscard]] bool ReleaseBackingStoreEntry(const FGuid& InGuid, FStoredChunk& InChunk)
|
|
{
|
|
// Return the bits back to the free list.
|
|
FBackingStoreFreeSpan NewFreeSpan;
|
|
int32 FreeSpanIndex;
|
|
{
|
|
FBackingStoreUsedSpan& UsedEntry = UsedDiskSpans[InGuid];
|
|
|
|
BackingStoreUsedSpace -= InChunk.ChunkSize;
|
|
BackingStoreWastedSpace -= UsedEntry.ReservedSize() - InChunk.ChunkSize;
|
|
|
|
FreeSpanIndex = Algo::LowerBoundBy(FreeDiskSpans, UsedEntry.StartEntryIndex, &FBackingStoreFreeSpan::StartEntryIndex);
|
|
|
|
NewFreeSpan.StartEntryIndex = UsedEntry.StartEntryIndex;
|
|
NewFreeSpan.EndEntryIndex = UsedEntry.EndEntryIndex;
|
|
|
|
InChunk.bBackedByDisk = false;
|
|
|
|
UsedDiskSpans.Remove(InGuid);
|
|
}
|
|
|
|
// Merge into an adjacent entry without adding and having to do a linear
|
|
// pass to coalesce.
|
|
bool bMerged = false;
|
|
if (FreeSpanIndex < FreeDiskSpans.Num())
|
|
{
|
|
if (NewFreeSpan.EndEntryIndex == FreeDiskSpans[FreeSpanIndex].StartEntryIndex)
|
|
{
|
|
// We are right before to the one after us - just extend them lower
|
|
FreeDiskSpans[FreeSpanIndex].StartEntryIndex = NewFreeSpan.StartEntryIndex;
|
|
bMerged = true;
|
|
|
|
// OK they got merged down, see if they can connect with the one below
|
|
if (FreeSpanIndex > 0)
|
|
{
|
|
if (FreeDiskSpans[FreeSpanIndex - 1].EndEntryIndex == FreeDiskSpans[FreeSpanIndex].StartEntryIndex)
|
|
{
|
|
// We fill a gap, we can connect and remove.
|
|
FreeDiskSpans[FreeSpanIndex - 1].EndEntryIndex = FreeDiskSpans[FreeSpanIndex].EndEntryIndex;
|
|
FreeDiskSpans.RemoveAt(FreeSpanIndex);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!bMerged && FreeSpanIndex > 0) // if we missed, this will be Num(). If we have nothing yet it'll fail this and be fine.
|
|
{
|
|
if (FreeDiskSpans[FreeSpanIndex - 1].EndEntryIndex == NewFreeSpan.StartEntryIndex)
|
|
{
|
|
// We are right after the one before us, extend them farther.
|
|
FreeDiskSpans[FreeSpanIndex - 1].EndEntryIndex = NewFreeSpan.EndEntryIndex;
|
|
bMerged = true;
|
|
|
|
// They got merged up, see if we filled a gap.
|
|
if (FreeSpanIndex < FreeDiskSpans.Num())
|
|
{
|
|
if (FreeDiskSpans[FreeSpanIndex - 1].EndEntryIndex == FreeDiskSpans[FreeSpanIndex].StartEntryIndex)
|
|
{
|
|
FreeDiskSpans[FreeSpanIndex - 1].EndEntryIndex = FreeDiskSpans[FreeSpanIndex].EndEntryIndex;
|
|
FreeDiskSpans.RemoveAt(FreeSpanIndex);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// If we didn't merge, we need to insert
|
|
if (!bMerged)
|
|
{
|
|
FreeDiskSpans.Insert(NewFreeSpan, FreeSpanIndex);
|
|
}
|
|
|
|
// Check and see if the free space is at the end of the file. If so, we can truncate
|
|
// and free up disk space.
|
|
if (FreeDiskSpans.Top().EndEntryIndex == BackingStoreEntryCount)
|
|
{
|
|
uint64 TruncateToSize = (uint64)FreeDiskSpans.Top().StartEntryIndex << BitsPerEntry;
|
|
if (BackingStoreFileHandle->Truncate(TruncateToSize)) // truncate can fail... don't update structures if we can't!
|
|
{
|
|
BackingStoreEntryCount = FreeDiskSpans.Top().StartEntryIndex;
|
|
FreeDiskSpans.Pop();
|
|
}
|
|
}
|
|
|
|
return ConsistencyCheck();
|
|
}
|
|
|
|
[[nodiscard]] bool ReleaseEntryInternal(const FGuid& InGuid, FStoredChunk* InStoredChunk)
|
|
{
|
|
if (InStoredChunk->LockCount == 0)
|
|
{
|
|
UE_LOG(LogBuildPatchServices, Error, TEXT("Consistency Failure: Releasing memory entry that isn't locked! %s"), *WriteToString<40>(InGuid));
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
InStoredChunk->LockCount--;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
public:
|
|
|
|
FChunkBackingStore(FBuildPatchFileConstructor* InParentConstructor, const FString& InInstallDirectory, FBuildPatchFileConstructor::FBackingStoreStats& InStats)
|
|
: ParentConstructor(InParentConstructor)
|
|
, InstallDirectory(InInstallDirectory)
|
|
, Stats(InStats)
|
|
{
|
|
BackingStoreFileName = InParentConstructor->Configuration.BackingStoreDirectory / TEXT("backingstore");
|
|
|
|
bool bUseDiskOverflowStore = true;
|
|
GConfig->GetBool(TEXT("BuildPatchServices"), TEXT("bEnableDiskOverflowStore"), bUseDiskOverflowStore, GEngineIni);
|
|
|
|
if (ParentConstructor->Configuration.bInstallToMemory)
|
|
{
|
|
UE_LOG(LogBuildPatchServices, Display, TEXT("Disabling backing store due to InstallToMemory"));
|
|
bUseDiskOverflowStore = false;
|
|
}
|
|
|
|
// Is there a hard limit on how much disk space we can use?
|
|
// negative = no
|
|
// 0 = disable disk overflow
|
|
int64 MaxDiskMB = 0;
|
|
GConfig->GetInt64(TEXT("BuildPatchServices"), TEXT("DiskOverflowStoreLimitMB"), MaxDiskMB, GEngineIni);
|
|
if (MaxDiskMB < 0)
|
|
{
|
|
MaxDiskSpaceBytes = TNumericLimits<int64>::Max();
|
|
}
|
|
else
|
|
{
|
|
MaxDiskSpaceBytes = MaxDiskMB << 20;
|
|
}
|
|
|
|
// Do we want to always try and keep some disk space available, no matter what our limit is?
|
|
// note that independent of this we try and prevent the disk backing store from eating into space
|
|
// we have reserved for the actual install so they don't compete.
|
|
//
|
|
// this checks the free space after each file and updates the space limit correspondingly.
|
|
int64 AdditionalDiskSpaceHeadroomMB = 0;
|
|
GConfig->GetInt64(TEXT("BuildPatchServices"), TEXT("DiskOverflowStoreAdditionalHeadroomMB"), AdditionalDiskSpaceHeadroomMB, GEngineIni);
|
|
if (AdditionalDiskSpaceHeadroomMB >= 0)
|
|
{
|
|
AdditionalDiskSpaceHeadroomBytes = AdditionalDiskSpaceHeadroomMB << 20;
|
|
}
|
|
|
|
if (bUseDiskOverflowStore)
|
|
{
|
|
IFileManager::Get().MakeDirectory(*InParentConstructor->Configuration.BackingStoreDirectory);
|
|
BackingStoreFileHandle.Reset(FPlatformFileManager::Get().GetPlatformFile().OpenWrite(*BackingStoreFileName, false, true));
|
|
}
|
|
|
|
if (!BackingStoreFileHandle)
|
|
{
|
|
// Prevent any pageouts.
|
|
UE_CLOG(bUseDiskOverflowStore, LogBuildPatchServices, Warning, TEXT("Unable to open disk backing store at %s"), *BackingStoreFileName);
|
|
UE_LOG(LogBuildPatchServices, Warning, TEXT("Disk backing store will be disabled"));
|
|
MaxDiskSpaceBytes = 0;
|
|
}
|
|
|
|
UE_LOG(LogBuildPatchServices, Display, TEXT("DiskOverflowStore is: %s - MaxSize = %s, Additional Headroom = %s"),
|
|
BackingStoreFileHandle ? TEXT("Enabled") : TEXT("Disabled"),
|
|
*FormatNumber(MaxDiskSpaceBytes),
|
|
*FormatNumber(AdditionalDiskSpaceHeadroomBytes)
|
|
);
|
|
|
|
|
|
// Now memory limits.
|
|
uint64 ChunkStoreMemoryLimit = 0;
|
|
|
|
// Check old values and warn/assume
|
|
{
|
|
int32 ChunkStoreMemorySizeChunks;
|
|
const bool bLoadedStoreSize = GConfig->GetInt(TEXT("Portal.BuildPatch"), TEXT("ChunkStoreMemorySize"), ChunkStoreMemorySizeChunks, GEngineIni);
|
|
if (bLoadedStoreSize)
|
|
{
|
|
UE_LOG(LogBuildPatchServices, Warning, TEXT("Outdated memory size limitation found: ChunkStoreMemorySize. Assuming chunk size is 1MB, use ChunkStoreMemorySizeMB instead."));
|
|
ChunkStoreMemoryLimit = (uint64)FMath::Max(ChunkStoreMemorySizeChunks, 0) << 20;
|
|
}
|
|
|
|
int32 CloudChunkStoreMemorySizeChunks = 0;
|
|
int32 InstallChunkStoreMemorySizeChunks = 0;
|
|
const bool bLoadedCloudSize = GConfig->GetInt(TEXT("Portal.BuildPatch"), TEXT("CloudChunkStoreMemorySize"), CloudChunkStoreMemorySizeChunks, GEngineIni);
|
|
const bool bLoadedInstallSize = GConfig->GetInt(TEXT("Portal.BuildPatch"), TEXT("InstallChunkStoreMemorySize"), InstallChunkStoreMemorySizeChunks, GEngineIni);
|
|
if (bLoadedCloudSize || bLoadedInstallSize)
|
|
{
|
|
UE_LOG(LogBuildPatchServices, Warning, TEXT("Outdated memory size limitations found: CloudChunkStoreMemorySize or InstallChunkStoreMemorySize. Assuming chunk size is 1MB."));
|
|
UE_LOG(LogBuildPatchServices, Warning, TEXT("Use ChunkStoreMemorySizeMB and/or ChunkStoreMemoryHeadRoomMB."));
|
|
uint64 OldMemoryLimit = FMath::Max(CloudChunkStoreMemorySizeChunks + InstallChunkStoreMemorySizeChunks, 0);
|
|
OldMemoryLimit <<= 20;
|
|
ChunkStoreMemoryLimit = OldMemoryLimit;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// check current values. We expect this to override anything from above.
|
|
int32 ChunkStoreMemoryLimitMB = 0;
|
|
if (GConfig->GetInt(TEXT("Portal.BuildPatch"), TEXT("ChunkStoreMemorySizeMB"), ChunkStoreMemoryLimitMB, GEngineIni))
|
|
{
|
|
// To be consistent with other limitations, negative disables it
|
|
// 0 is OK - we require locked data to be in memory so we'll go over the limit but it'll be a minimum.
|
|
if (ChunkStoreMemoryLimitMB < 0)
|
|
{
|
|
ChunkStoreMemoryLimit = ChunkStoreMemoryLimitDisabledSentinel;
|
|
}
|
|
else
|
|
{
|
|
ChunkStoreMemoryLimit = (uint64)ChunkStoreMemoryLimitMB << 20;
|
|
}
|
|
}
|
|
|
|
|
|
// Get headroom. Default to 2GB of headroom. If no config is entered then we expect a 0 chunk limit that
|
|
// gets updated off of this default headroom.
|
|
int32 ChunkStoreMemoryHeadRoomMB = 2000;
|
|
if (GConfig->GetInt(TEXT("Portal.BuildPatch"), TEXT("ChunkStoreMemoryHeadRoomMB"), ChunkStoreMemoryHeadRoomMB, GEngineIni))
|
|
{
|
|
if (ChunkStoreMemoryHeadRoomMB < 0)
|
|
{
|
|
// negative disables
|
|
ChunkStoreMemoryHeadRoomMB = -1;
|
|
}
|
|
else if (ChunkStoreMemoryHeadRoomMB < 500)
|
|
{
|
|
UE_LOG(LogBuildPatchServices, Warning, TEXT("ChunkStoreMemoryHeadRoomMB too low (%d), using min (500)"), ChunkStoreMemoryHeadRoomMB);
|
|
ChunkStoreMemoryHeadRoomMB = 500;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
FPlatformMemoryStats MemoryStats = FPlatformMemory::GetStats();
|
|
uint64 AvailableMem = MemoryStats.AvailablePhysical;
|
|
if (ChunkStoreMemoryHeadRoomMB >= 0)
|
|
{
|
|
uint64 RequestedHeadRoom = ((uint64)ChunkStoreMemoryHeadRoomMB << 20);
|
|
|
|
uint64 ProposedChunkStoreMemoryLimit = ChunkStoreMemoryLimit;
|
|
|
|
if (RequestedHeadRoom < AvailableMem)
|
|
{
|
|
uint64 MemoryStoreMem = AvailableMem - RequestedHeadRoom;
|
|
ProposedChunkStoreMemoryLimit = MemoryStoreMem;
|
|
}
|
|
else
|
|
{
|
|
// Cap at available.
|
|
if (ProposedChunkStoreMemoryLimit > AvailableMem)
|
|
{
|
|
ProposedChunkStoreMemoryLimit = AvailableMem;
|
|
}
|
|
}
|
|
|
|
// If there's already a limit requested by the inis, we don't want to make it _smaller_. If there's no
|
|
// limit specified, then use the proposed limit.
|
|
if (ChunkStoreMemoryLimit != ChunkStoreMemoryLimitDisabledSentinel)
|
|
{
|
|
ChunkStoreMemoryLimit = FMath::Max(ChunkStoreMemoryLimit, ProposedChunkStoreMemoryLimit);
|
|
}
|
|
else
|
|
{
|
|
ChunkStoreMemoryLimit = ProposedChunkStoreMemoryLimit;
|
|
}
|
|
}
|
|
|
|
if (ChunkStoreMemoryLimit == ChunkStoreMemoryLimitDisabledSentinel)
|
|
{
|
|
UE_LOG(LogBuildPatchServices, Display, TEXT("ChunkStoreMemoryLimits are disabled"));
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogBuildPatchServices, Display, TEXT("ChunkStoreMemoryLimits: %s using headroom of %s (%s available memory, %s used physical, %s used virtual)"),
|
|
*FormatNumber(ChunkStoreMemoryLimit),
|
|
ChunkStoreMemoryHeadRoomMB >= 0 ? *FormatNumber((uint64)ChunkStoreMemoryHeadRoomMB << 20) : TEXT("<disabled>"),
|
|
*FormatNumber(AvailableMem),
|
|
*FormatNumber(MemoryStats.UsedPhysical),
|
|
*FormatNumber(MemoryStats.UsedVirtual)
|
|
);
|
|
}
|
|
|
|
MaxMemoryBytes = ChunkStoreMemoryLimit;
|
|
Stats.MemoryLimitBytes = MaxMemoryBytes;
|
|
}
|
|
|
|
~FChunkBackingStore()
|
|
{
|
|
if (BackingStoreFileHandle.IsValid())
|
|
{
|
|
BackingStoreFileHandle.Reset(); // have to close the file before we try to delete it.
|
|
if (!FPlatformFileManager::Get().GetPlatformFile().DeleteFile(*BackingStoreFileName))
|
|
{
|
|
UE_LOG(LogBuildPatchServices, Warning, TEXT("Unable to delete disk backing store: %s"), *BackingStoreFileName);
|
|
}
|
|
UE_LOG(LogBuildPatchServices, Verbose, TEXT("BackingStore Reads: %d Writes: %d"), ReadCount, WriteCount);
|
|
}
|
|
}
|
|
|
|
// Set the amount of disk space the installation needs to we can ensure that we don't
|
|
// expand into that space no matter what our config disk space limits are.
|
|
void SetDynamicDiskSpaceHeadroom(uint64 InInstallationFreeSpaceRequired)
|
|
{
|
|
InstallationFreeSpaceRequired = InInstallationFreeSpaceRequired;
|
|
}
|
|
void DisableDynamicDiskSpaceHeadroom()
|
|
{
|
|
InstallationFreeSpaceRequired = 0;
|
|
}
|
|
|
|
// false means consistency failure
|
|
// Constructor thread
|
|
[[nodiscard]] bool DereserveHarvestingEntry(const FGuid& InGuid)
|
|
{
|
|
FStoredChunk* StoredChunk = StoredChunks.Find(InGuid);
|
|
if (StoredChunk)
|
|
{
|
|
if (StoredChunk->bCommitted || StoredChunk->bBackedByDisk)
|
|
{
|
|
UE_LOG(LogBuildPatchServices, Error,
|
|
TEXT("Consistency Failure: deserve memory entry that's uncommitted or paged out! %s, paged out = %d committed = %d"),
|
|
*WriteToString<40>(InGuid), StoredChunk->bBackedByDisk, StoredChunk->bCommitted);
|
|
return false;
|
|
}
|
|
|
|
CurrentMemoryLoad -= StoredChunk->ChunkData.Num();
|
|
StoredChunks.Remove(InGuid);
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogBuildPatchServices, Error, TEXT("Consistency Failure: Cleared memory entry that doesn't exist! %s"), *WriteToString<40>(InGuid));
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// false means consistency failure
|
|
// Constructor thread
|
|
[[nodiscard]] bool ReleaseEntry(const FGuid& InGuid)
|
|
{
|
|
FStoredChunk* StoredChunk = StoredChunks.Find(InGuid);
|
|
if (StoredChunk)
|
|
{
|
|
return ReleaseEntryInternal(InGuid, StoredChunk);
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogBuildPatchServices, Error, TEXT("Consistency Failure: Releasing memory entry that doesn't exist! %s"), *WriteToString<40>(InGuid));
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// false means consistency failure
|
|
// Constructor thread
|
|
[[nodiscard]] bool CommitAndReleaseEntry(const FGuid& InGuid)
|
|
{
|
|
FStoredChunk* StoredChunk = StoredChunks.Find(InGuid);
|
|
if (StoredChunk)
|
|
{
|
|
if (StoredChunk->bCommitted)
|
|
{
|
|
UE_LOG(LogBuildPatchServices, Error, TEXT("Consistency Failure: Committing memory entry that is already committed! %s"), *WriteToString<40>(InGuid));
|
|
return false;
|
|
}
|
|
StoredChunk->bCommitted = true;
|
|
return ReleaseEntryInternal(InGuid, StoredChunk);
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogBuildPatchServices, Error, TEXT("Consistency Failure: Committed memory entry that doesn't exist! %s"), *WriteToString<40>(InGuid));
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// false means consistency failure
|
|
// Constructor thread
|
|
[[nodiscard]] bool LockEntry(const FGuid& InGuid)
|
|
{
|
|
FStoredChunk* StoredChunk = StoredChunks.Find(InGuid);
|
|
if (!StoredChunk)
|
|
{
|
|
UE_LOG(LogBuildPatchServices, Error, TEXT("Consistency Failure: locking memory entry that doesn't exist! %s"), *WriteToString<40>(InGuid));
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
StoredChunk->LockCount++;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Returns an empty view on consistency failure
|
|
// Constructor thread.
|
|
[[nodiscard]] FMutableMemoryView ReserveAndLockEntry(const FGuid& InGuid, uint32 InChunkSize, int32 LastUsageIndex)
|
|
{
|
|
// Note that this function can be called to reserve an entry already in the backing store
|
|
// because we need to read from the disk to memory for a sub-chunk (or otherwise). So it needs
|
|
// to be able to handle reserving on top of paged out chunks.
|
|
|
|
int32 CurrentUsageIndex = ParentConstructor->ChunkReferenceTracker->GetCurrentUsageIndex();
|
|
|
|
if (MaxMemoryBytes != ChunkStoreMemoryLimitDisabledSentinel)
|
|
{
|
|
while (CurrentMemoryLoad + InChunkSize > MaxMemoryBytes)
|
|
{
|
|
// Gotta dump stuff to disk. If we fail to dump to disk we mark the chunk
|
|
// as only available via the cloud source.
|
|
|
|
// Evict the one that is the longest until we use it. Chunks are almost always the same size
|
|
// so we expect this to run once.
|
|
int32 FarthestNextUsage = -1;
|
|
FGuid EvictGuid;
|
|
for (TPair<FGuid, FStoredChunk>& Pair : StoredChunks)
|
|
{
|
|
FStoredChunk& Chunk = Pair.Value;
|
|
|
|
if (Chunk.LockCount || // Actively in use - don't evict.
|
|
!Chunk.ChunkData.Num()) // Not in memory - can't evict.
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (Chunk.NextUsageIndex < CurrentUsageIndex)
|
|
{
|
|
Chunk.NextUsageIndex = ParentConstructor->ChunkReferenceTracker->GetNextUsageForChunk(Pair.Key, Chunk.LastUsageIndex);
|
|
}
|
|
|
|
if (Chunk.NextUsageIndex > FarthestNextUsage)
|
|
{
|
|
FarthestNextUsage = Pair.Value.NextUsageIndex;
|
|
EvictGuid = Pair.Key;
|
|
}
|
|
}
|
|
|
|
if (FarthestNextUsage == -1)
|
|
{
|
|
// This means we can't reserve and also keep our memory requirements.
|
|
//
|
|
// We currently have a minimum memory requirement for construction: Partial or reused chunks
|
|
// are routed through the backing store for holding. Partial because we need a full allocation to decompress
|
|
// the chunk and reuse so we don't have to re-read from disk.
|
|
//
|
|
// For unpatched installs this is minimal as most chunks are able to write directly to
|
|
// the destination buffer. For optimized delta manifests this is not the case as BPT assembles
|
|
// chunks from all over the place - I've seen 80+ chunk references to assemble 16MB of data,
|
|
// resulting in total buffer usage of 80MB + 16MB > 100MB of memory use per batch. With two
|
|
// batches in flight this pushes 200MB total buffer allocation.
|
|
//
|
|
// This is not likely an issue in real life as 200MB isn't much, but it does mean if we have a
|
|
// low memory usage limit we can hit this legitimately.
|
|
//
|
|
// So...we let the reservation continue in violation of our memory constraints and hope we don't OOM.
|
|
//
|
|
// Future work could be:
|
|
// 1. Limit batch creation based on inflight chunk in addition to write buffer size
|
|
// 2. Only allow 1 batch in flight when using an optimized delta + low memory constraints.
|
|
break;
|
|
}
|
|
else
|
|
{
|
|
if (!PageOut(EvictGuid))
|
|
{
|
|
return FMutableMemoryView();
|
|
}
|
|
}
|
|
} // end while over limits
|
|
} // end if memory limits enabled
|
|
FStoredChunk& StoredChunk = StoredChunks.FindOrAdd(InGuid);
|
|
|
|
//
|
|
// It's possible to request a reservation for a chunk already in the backing store when reading from the
|
|
// disk into memory (i.e. with a non-direct read). However it can't already be in memory.
|
|
//
|
|
if (StoredChunk.ChunkData.Num())
|
|
{
|
|
UE_LOG(LogBuildPatchServices, Error, TEXT("Consistency failure: Reserving read space for a chunk already in memory: %s"), *WriteToString<40>(InGuid));
|
|
return FMutableMemoryView();
|
|
}
|
|
else
|
|
{
|
|
StoredChunk.LastUsageIndex = LastUsageIndex;
|
|
StoredChunk.bCommitted = false;
|
|
StoredChunk.ChunkData.AddUninitialized(InChunkSize);
|
|
StoredChunk.ChunkSize = InChunkSize;
|
|
StoredChunk.LockCount = 1;
|
|
CurrentMemoryLoad += InChunkSize;
|
|
if (CurrentMemoryLoad > PeakMemoryLoad)
|
|
{
|
|
PeakMemoryLoad = CurrentMemoryLoad;
|
|
Stats.MemoryPeakUsageBytes = PeakMemoryLoad;
|
|
}
|
|
}
|
|
|
|
FMutableMemoryView ChunkBuffer(StoredChunk.ChunkData.GetData(), StoredChunk.ChunkData.Num());
|
|
return ChunkBuffer;
|
|
}
|
|
|
|
// false means consistency failure
|
|
// Constructor thread
|
|
[[nodiscard]] bool CheckRetirements(int32 CurrentUsageIndex)
|
|
{
|
|
// 6 just chosen because we wouldn't expect a ton of things at once
|
|
// but we could get several. shrug.
|
|
TArray<FGuid, TInlineAllocator<6>> GuidsToDelete;
|
|
|
|
for (TPair<FGuid, FStoredChunk>& Pair : StoredChunks)
|
|
{
|
|
if (Pair.Value.LastUsageIndex < CurrentUsageIndex)
|
|
{
|
|
GuidsToDelete.Add(Pair.Key);
|
|
}
|
|
}
|
|
|
|
for (FGuid& ToDelete : GuidsToDelete)
|
|
{
|
|
FStoredChunk* Chunk = StoredChunks.Find(ToDelete);
|
|
if (Chunk->bBackedByDisk)
|
|
{
|
|
if (!ReleaseBackingStoreEntry(ToDelete, *Chunk))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (Chunk->LockCount)
|
|
{
|
|
UE_LOG(LogBuildPatchServices, Error, TEXT("Consistency Failure: Retiring memory entry with lock count! %s %d"), *WriteToString<40>(ToDelete), Chunk->LockCount);
|
|
return false;
|
|
}
|
|
|
|
if (Chunk->ChunkData.Num())
|
|
{
|
|
if (!Chunk->bCommitted)
|
|
{
|
|
UE_LOG(LogBuildPatchServices, Error, TEXT("Consistency Failure: Retiring memory entry that never got committed! %s"), *WriteToString<40>(ToDelete));
|
|
return false;
|
|
}
|
|
|
|
CurrentMemoryLoad -= Chunk->ChunkSize;
|
|
}
|
|
|
|
StoredChunks.Remove(ToDelete);
|
|
|
|
ParentConstructor->SetChunkLocation(ToDelete, EConstructorChunkLocation::Retired);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// This should only be for paged out chunks... in-memory chunks should be handled directly.
|
|
// Consistency failures in this will pass as data read fails, which will end up redirecting to
|
|
// the cloud. However the post-file consistency check will catch it and fail the install.
|
|
// Constructor thread only for this chunk source
|
|
[[nodiscard]] FRequestProcessFn CreateRequest(const FGuid& DataId, FMutableMemoryView DestinationBuffer, void* UserPtr, FChunkRequestCompleteDelegate CompleteFn)
|
|
{
|
|
// LONGTERM - can we make this fully async once we have a pipe we can prevent reentrancy on or ReadAtOffset API?
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(BackingStoreRead);
|
|
|
|
bool bSuccess = true;
|
|
// This should go to a generate IO dispatch with completion function I think? idk...
|
|
// This acts as any other IO source - we don't know where it's going, it might be going back into memory.
|
|
FStoredChunk* StoredChunk = StoredChunks.Find(DataId);
|
|
bSuccess = StoredChunk != nullptr;
|
|
|
|
if (bSuccess && !StoredChunk->bBackedByDisk)
|
|
{
|
|
UE_LOG(LogBuildPatchServices, Error, TEXT("Consistency Failure: Trying to page in a chunk that isn't paged out! %s"), *WriteToString<40>(DataId));
|
|
bSuccess = false;
|
|
}
|
|
|
|
FBackingStoreUsedSpan* UsedEntry = nullptr;
|
|
if (bSuccess)
|
|
{
|
|
UsedEntry = UsedDiskSpans.Find(DataId);
|
|
if (!UsedEntry)
|
|
{
|
|
UE_LOG(LogBuildPatchServices, Error, TEXT("Consistency Failure: Backing store entry not found for paged out chunk! %s"), *WriteToString<40>(DataId));
|
|
bSuccess = false;
|
|
}
|
|
}
|
|
|
|
if (bSuccess)
|
|
{
|
|
ReadCount++;
|
|
BackingStoreFileHandle->Seek(UsedEntry->Offset());
|
|
if (!BackingStoreFileHandle->Read((uint8*)DestinationBuffer.GetData(), DestinationBuffer.GetSize()))
|
|
{
|
|
UE_LOG(LogBuildPatchServices, Error, TEXT("Consistency Failure: Backing store page-in failed read! %s"), *WriteToString<40>(DataId));
|
|
bSuccess = false;
|
|
}
|
|
}
|
|
|
|
if (bSuccess)
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(BSR_Hash);
|
|
if (FXxHash64::HashBuffer(DestinationBuffer) != UsedEntry->Hash)
|
|
{
|
|
UE_LOG(LogBuildPatchServices, Error, TEXT("Consistency Failure: Backing store page-in failed hash check! %s"), *WriteToString<40>(DataId));
|
|
bSuccess = false;
|
|
}
|
|
}
|
|
|
|
if (!bSuccess)
|
|
{
|
|
Stats.DiskLoadFailureCount++;
|
|
}
|
|
|
|
Stats.DiskChunkLoadCount++;
|
|
CompleteFn.Execute(DataId, false, !bSuccess, UserPtr);
|
|
return [](bool) { return; };
|
|
}
|
|
|
|
// false means consistency failure
|
|
// Constructor thread
|
|
[[nodiscard]] bool CheckNoLocks(bool bIsHarvest)
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(BackingStore_CheckNoLocks);
|
|
// After a file there should be no locked chunks since there are no reads.
|
|
bool bSuccess = true;
|
|
for (TPair<FGuid, FStoredChunk>& Chunk : StoredChunks)
|
|
{
|
|
if (Chunk.Value.LockCount)
|
|
{
|
|
UE_LOG(LogBuildPatchServices, Error, TEXT("Chunk %s locked with count %d after %s!"), *WriteToString<40>(Chunk.Key), Chunk.Value.LockCount, bIsHarvest ? TEXT("harvest") : TEXT("file completion"));
|
|
bSuccess = false;
|
|
}
|
|
}
|
|
|
|
if (bSuccess)
|
|
{
|
|
bSuccess = ConsistencyCheck();
|
|
}
|
|
|
|
return bSuccess;
|
|
}
|
|
|
|
// This is for the disk store - we don't free the page entry until it retires, so it's always available and can
|
|
// be read direct.
|
|
virtual int32 GetChunkUnavailableAt(const FGuid& DataId) const override { return TNumericLimits<int32>::Max(); }
|
|
|
|
FMemoryView GetViewForChunk(const FGuid& DataId) const
|
|
{
|
|
FStoredChunk const* StoredChunk = StoredChunks.Find(DataId);
|
|
if (!StoredChunk)
|
|
{
|
|
return FMemoryView();
|
|
}
|
|
|
|
return FMemoryView(StoredChunk->ChunkData.GetData(), StoredChunk->ChunkData.Num());
|
|
}
|
|
}; // FChunkBackingStore
|
|
|
|
/* FBuildPatchFileConstructor implementation
|
|
*****************************************************************************/
|
|
FBuildPatchFileConstructor::FBuildPatchFileConstructor(
|
|
FFileConstructorConfig InConfiguration, IFileSystem* InFileSystem,
|
|
IConstructorChunkDbChunkSource* InChunkDbChunkSource, IConstructorCloudChunkSource* InCloudChunkSource, IConstructorInstallChunkSource* InInstallChunkSource,
|
|
IChunkReferenceTracker* InChunkReferenceTracker, IInstallerError* InInstallerError, IInstallerAnalytics* InInstallerAnalytics, IMessagePump* InMessagePump,
|
|
IFileConstructorStat* InFileConstructorStat, TMap<FGuid, EConstructorChunkLocation>&& InChunkLocations)
|
|
: Configuration(MoveTemp(InConfiguration))
|
|
, bIsDownloadStarted(false)
|
|
, bInitialDiskSizeCheck(false)
|
|
, bIsPaused(false)
|
|
, bShouldAbort(false)
|
|
, FileSystem(InFileSystem)
|
|
, ChunkDbSource(InChunkDbChunkSource)
|
|
, InstallSource(InInstallChunkSource)
|
|
, CloudSource(InCloudChunkSource)
|
|
, ChunkLocations(MoveTemp(InChunkLocations))
|
|
, ChunkReferenceTracker(InChunkReferenceTracker)
|
|
, InstallerError(InInstallerError)
|
|
, InstallerAnalytics(InInstallerAnalytics)
|
|
, MessagePump(InMessagePump)
|
|
, FileConstructorStat(InFileConstructorStat)
|
|
, TotalJobSize(0)
|
|
, ByteProcessed(0)
|
|
, RequiredDiskSpace(0)
|
|
, AvailableDiskSpace(0)
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(FileConstructor_ctor);
|
|
|
|
bStallWhenFileSystemThrottled = bCVarStallWhenFileSystemThrottled;
|
|
if (Configuration.StallWhenFileSystemThrottled.IsSet())
|
|
{
|
|
UE_LOG(LogBuildPatchServices, Display, TEXT("Overridding StallWhenFileSystemThrottled to: %d, cvar was %d"), Configuration.StallWhenFileSystemThrottled.GetValue(), bStallWhenFileSystemThrottled);
|
|
bStallWhenFileSystemThrottled = Configuration.StallWhenFileSystemThrottled.GetValue();
|
|
}
|
|
|
|
bAllowMultipleFilesInFlight = bCVarAllowMultipleFilesInFlight;
|
|
|
|
BackingStore.Reset(new FChunkBackingStore(this, Configuration.InstallDirectory, BackingStoreStats));
|
|
|
|
// Count initial job size
|
|
ConstructionList.Reserve(Configuration.ConstructList.Num());
|
|
|
|
// Track when we will complete files in the reference chain.
|
|
int32 CurrentPosition = 0;
|
|
FileCompletionPositions.Reserve(Configuration.ConstructList.Num());
|
|
|
|
// The first index after the file is complete.
|
|
// \todo with the construction list now stable across the install, we could
|
|
// key this off a StringView and save the allocs. For another CL...
|
|
TMap<FString, int32> FileRetirementPositions;
|
|
|
|
for (const FString& FileToConstructName : Configuration.ConstructList)
|
|
{
|
|
const FFileManifest* FileManifest = Configuration.ManifestSet->GetNewFileManifest(FileToConstructName);
|
|
|
|
FFileToConstruct FileToConstruct;
|
|
FileToConstruct.FileManifest = FileManifest;
|
|
|
|
// If we are missing the file manifest, we will fail to install when we get to the file. However,
|
|
// we guarantee a 1:1 mapping with the arrays we are filling here so we use invalid data for those
|
|
// slots (which won't get used).
|
|
// Maybe we should fail immediately? Need to review whether we can fail in the constructor or we need
|
|
// to delay until Run().
|
|
if (FileManifest)
|
|
{
|
|
TotalJobSize += FileManifest->FileSize;
|
|
|
|
// We will be advancing the chunk reference tracker by this many chunks.
|
|
int32 AdvanceCount = FileManifest->ChunkParts.Num();
|
|
CurrentPosition += AdvanceCount;
|
|
}
|
|
|
|
FileCompletionPositions.Add(CurrentPosition);
|
|
FileRetirementPositions.Add(FileToConstructName, CurrentPosition);
|
|
ConstructionList.Add(MoveTemp(FileToConstruct));
|
|
}
|
|
|
|
// Let the install source know when we're going to be deleting their sources.
|
|
bool bHasInstallSource = false;
|
|
if (InstallSource)
|
|
{
|
|
InstallSource->SetFileRetirementPositions(MoveTemp(FileRetirementPositions));
|
|
|
|
if (InstallSource->GetAvailableChunks().Num() != 0)
|
|
{
|
|
bHasInstallSource = true;
|
|
|
|
// We need to set up a dependency chain so that files can know when they can start.
|
|
TMap<FStringView, int32> FileToIndexMap;
|
|
FileToIndexMap.Reserve(Configuration.ConstructList.Num());
|
|
for (int32 FileConstructIndex = 0; FileConstructIndex < Configuration.ConstructList.Num(); FileConstructIndex++)
|
|
{
|
|
// The construct list has filenames that match the manifest, and afaict manifest filenames are already normalized.
|
|
FileToIndexMap.Add(Configuration.ConstructList[FileConstructIndex], FileConstructIndex);
|
|
}
|
|
|
|
for (int32 FileConstructIndex = 0; FileConstructIndex < Configuration.ConstructList.Num(); FileConstructIndex++)
|
|
{
|
|
FFileToConstruct& FileToConstruct = ConstructionList[FileConstructIndex];
|
|
if (!FileToConstruct.FileManifest)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
for (const FChunkPart& ChunkPart : FileToConstruct.FileManifest->ChunkParts)
|
|
{
|
|
EConstructorChunkLocation ChunkLocation = ChunkLocations[ChunkPart.Guid];
|
|
if (ChunkLocation == EConstructorChunkLocation::Install)
|
|
{
|
|
InstallSource->EnumerateFilesForChunk(ChunkPart.Guid, [FileConstructIndex, &FileToIndexMap, &FileToConstruct, NormalizedInstallDirectory = &Configuration.InstallDirectory](const FString& ChunkNormalizedInstallDirectory, const FString& NormalizedFilenameContainingChunk)
|
|
{
|
|
if (*NormalizedInstallDirectory != ChunkNormalizedInstallDirectory)
|
|
{
|
|
// We aren't affecting that install source so it's not a dependency.
|
|
return;
|
|
}
|
|
|
|
const int32* DependentFileIndexPtr = FileToIndexMap.Find(NormalizedFilenameContainingChunk);
|
|
if (DependentFileIndexPtr)
|
|
{
|
|
int32 DependentFileIndex = *DependentFileIndexPtr;
|
|
|
|
// If the file is constructed before us then we can't start until it's ready.
|
|
// We only care about the latest file.
|
|
if (DependentFileIndex < FileConstructIndex)
|
|
{
|
|
FileToConstruct.LatestDependentInstallSource = FMath::Max(FileToConstruct.LatestDependentInstallSource, DependentFileIndex);
|
|
}
|
|
}
|
|
}
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
// Create the threads we are allowed to.
|
|
//
|
|
bHasChunkDbSource = ChunkDbSource->GetAvailableChunks().Num() != 0;
|
|
|
|
// Default everything to running synchronously.
|
|
for (int8& ThreadAssignment : ThreadAssignments)
|
|
{
|
|
ThreadAssignment = -1;
|
|
}
|
|
WriteThreadIndex = -1;
|
|
|
|
bool bSpawnAdditionalIOThreads = Configuration.bDefaultSpawnAdditionalIOThreads;
|
|
if (GConfig->GetBool(TEXT("Portal.BuildPatch"), TEXT("ConstructorSpawnAdditionalIOThreads"), bSpawnAdditionalIOThreads, GEngineIni))
|
|
{
|
|
UE_LOG(LogBuildPatchServices, Verbose, TEXT("Got INI ConstructorSpawnAdditionalIOThreads = %d"), bSpawnAdditionalIOThreads);
|
|
}
|
|
|
|
if (Configuration.SpawnAdditionalIOThreads.IsSet())
|
|
{
|
|
bSpawnAdditionalIOThreads = Configuration.SpawnAdditionalIOThreads.Get(bSpawnAdditionalIOThreads);
|
|
UE_LOG(LogBuildPatchServices, Verbose, TEXT("Got override ConstructorSpawnAdditionalIOThreads = %d"), bSpawnAdditionalIOThreads);
|
|
}
|
|
|
|
// For now we have to strictly assign jobs to threads so that we don't accidentally
|
|
// hit the same file handle on multiple threads. Once we have proper ReadAtOffset support
|
|
// we can go nuts (and just use UE::Tasks)
|
|
// LONGTERM try using UE::Pipe and just blasting everything on tasks?
|
|
int32 ThreadCount = 0;
|
|
if (bSpawnAdditionalIOThreads)
|
|
{
|
|
if (!Configuration.bInstallToMemory && !Configuration.bConstructInMemory)
|
|
{
|
|
WriteThreadIndex = 0;
|
|
ThreadCount++;
|
|
}
|
|
|
|
if (bHasInstallSource)
|
|
{
|
|
ThreadAssignments[EConstructorChunkLocation::Install] = ThreadCount;
|
|
ThreadCount++;
|
|
}
|
|
if (bHasChunkDbSource)
|
|
{
|
|
ThreadAssignments[EConstructorChunkLocation::ChunkDb] = ThreadCount;
|
|
ThreadCount++;
|
|
}
|
|
}
|
|
|
|
WakeUpDispatchThreadEvent = FPlatformProcess::GetSynchEventFromPool();
|
|
CloudSource->SetWakeupFunction([WakeUpDispatchThreadEvent = WakeUpDispatchThreadEvent]()
|
|
{
|
|
WakeUpDispatchThreadEvent->Trigger();
|
|
}
|
|
);
|
|
|
|
// Preallocate the arrays so we don't get any movement.
|
|
Threads.SetNum(ThreadCount);
|
|
ThreadWakeups.SetNum(ThreadCount);
|
|
ThreadCompleteEvents.SetNum(ThreadCount);
|
|
ThreadJobPostingLocks.SetNum(ThreadCount); // Default init is fine for these.
|
|
ThreadJobPostings.SetNum(ThreadCount); // Default init is fine for these.
|
|
|
|
for (int32 ThreadIndex = 0; ThreadIndex < ThreadCount; ThreadIndex++)
|
|
{
|
|
Threads[ThreadIndex] = Configuration.SharedContext->CreateThread();
|
|
ThreadWakeups[ThreadIndex] = FPlatformProcess::GetSynchEventFromPool();
|
|
ThreadCompleteEvents[ThreadIndex] = FPlatformProcess::GetSynchEventFromPool();
|
|
|
|
Threads[ThreadIndex]->RunTask([this, ThreadIndex]() { GenericThreadFn(ThreadIndex); });
|
|
}
|
|
|
|
}
|
|
|
|
FBuildPatchFileConstructor::~FBuildPatchFileConstructor()
|
|
{
|
|
// Wait for threads to shut down.
|
|
Abort();
|
|
|
|
|
|
for (int32 ThreadIndex = 0; ThreadIndex < Threads.Num(); ThreadIndex++)
|
|
{
|
|
ThreadCompleteEvents[ThreadIndex]->Wait();
|
|
}
|
|
|
|
for (int32 ThreadIndex = 0; ThreadIndex < Threads.Num(); ThreadIndex++)
|
|
{
|
|
FPlatformProcess::ReturnSynchEventToPool(ThreadWakeups[ThreadIndex]);
|
|
FPlatformProcess::ReturnSynchEventToPool(ThreadCompleteEvents[ThreadIndex]);
|
|
Configuration.SharedContext->ReleaseThread(Threads[ThreadIndex]);
|
|
}
|
|
|
|
FPlatformProcess::ReturnSynchEventToPool(WakeUpDispatchThreadEvent);
|
|
WakeUpDispatchThreadEvent = nullptr;
|
|
}
|
|
|
|
void FBuildPatchFileConstructor::SetPaused(bool bInIsPaused)
|
|
{
|
|
bool bWasPaused = bIsPaused.AtomicSet(bInIsPaused);
|
|
if (bWasPaused && !bInIsPaused)
|
|
{
|
|
// If we unpaused, the dispatch thread might be waiting in an event for us
|
|
// to tell it to unpark.
|
|
WakeUpDispatchThreadEvent->Trigger();
|
|
}
|
|
}
|
|
|
|
void FBuildPatchFileConstructor::Abort()
|
|
{
|
|
bool bAlreadyAborted = bShouldAbort.AtomicSet(true);
|
|
|
|
UE_LOG(LogBuildPatchServices, Verbose, TEXT("Issuing abort (previously aborted: %d)"), bAlreadyAborted)
|
|
|
|
if (bAlreadyAborted)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Make sure to wake up any threads that might be parked so they can bail.
|
|
WakeUpDispatchThreadEvent->Trigger();
|
|
|
|
for (int32 ThreadIndex = 0; ThreadIndex < Threads.Num(); ThreadIndex++)
|
|
{
|
|
ThreadWakeups[ThreadIndex]->Trigger();
|
|
}
|
|
|
|
}
|
|
|
|
void FBuildPatchFileConstructor::SetChunkLocation(const FGuid& InGuid, EConstructorChunkLocation InNewLocation)
|
|
{
|
|
// NOTE - reentrant. We assume a) that ChunkLocations is filled before threading
|
|
// and that b) no guids are used across concurrent actions.
|
|
FRWScopeLock ChunkLock(ChunkLocationsLock, SLT_Write);
|
|
EConstructorChunkLocation* Location = ChunkLocations.Find(InGuid);
|
|
if (!Location)
|
|
{
|
|
UE_LOG(LogBuildPatchServices, Error, TEXT("Consistency failure: setting chunk location for non existent chunk %s"), *WriteToString<40>(InGuid));
|
|
}
|
|
else
|
|
{
|
|
if (*Location != EConstructorChunkLocation::Cloud &&
|
|
InNewLocation == EConstructorChunkLocation::Cloud)
|
|
{
|
|
uint64 ChunkSize = Configuration.ManifestSet->GetDownloadSize(InGuid);
|
|
|
|
UE_LOG(LogBuildPatchServices, VeryVerbose, TEXT("Migrating chunk to cloud: %s, %llu bytes"), *WriteToString<40>(InGuid), ChunkSize);
|
|
|
|
DownloadRequirement += ChunkSize;
|
|
CloudSource->PostRequiredByteCount(DownloadRequirement);
|
|
}
|
|
|
|
*Location = InNewLocation;
|
|
}
|
|
}
|
|
|
|
void FBuildPatchFileConstructor::QueueGenericThreadTask(int32 ThreadIndex, IConstructorChunkSource::FRequestProcessFn&& Task)
|
|
{
|
|
// No thread for this task - run synchronously
|
|
if (ThreadIndex == -1 || ThreadIndex >= Threads.Num())
|
|
{
|
|
Task(false);
|
|
WakeUpDispatchThreadEvent->Trigger();
|
|
return;
|
|
}
|
|
|
|
bool bPosted = false;
|
|
ThreadJobPostingLocks[ThreadIndex].Lock();
|
|
if (!bShouldAbort)
|
|
{
|
|
ThreadJobPostings[ThreadIndex].Add(MoveTemp(Task));
|
|
bPosted = true;
|
|
}
|
|
ThreadJobPostingLocks[ThreadIndex].Unlock();
|
|
ThreadWakeups[ThreadIndex]->Trigger();
|
|
|
|
if (!bPosted)
|
|
{
|
|
// This means we aborted during the queue - make sure to run
|
|
Task(true);
|
|
}
|
|
}
|
|
|
|
|
|
void FBuildPatchFileConstructor::GenericThreadFn(int32 ThreadIndex)
|
|
{
|
|
for (;;)
|
|
{
|
|
ThreadWakeups[ThreadIndex]->Wait();
|
|
|
|
TArray<IConstructorChunkSource::FRequestProcessFn> GrabbedJobs;
|
|
|
|
{
|
|
ThreadJobPostingLocks[ThreadIndex].Lock();
|
|
GrabbedJobs = MoveTemp(ThreadJobPostings[ThreadIndex]);
|
|
ThreadJobPostingLocks[ThreadIndex].Unlock();
|
|
}
|
|
|
|
if (bShouldAbort)
|
|
{
|
|
for (IConstructorChunkSource::FRequestProcessFn& AbortJob : GrabbedJobs)
|
|
{
|
|
AbortJob(true);
|
|
}
|
|
WakeUpDispatchThreadEvent->Trigger();
|
|
break;
|
|
}
|
|
|
|
for (IConstructorChunkSource::FRequestProcessFn& Job : GrabbedJobs)
|
|
{
|
|
Job(false);
|
|
}
|
|
|
|
WakeUpDispatchThreadEvent->Trigger();
|
|
}
|
|
|
|
ThreadCompleteEvents[ThreadIndex]->Trigger();
|
|
}
|
|
|
|
bool FBuildPatchFileConstructor::HarvestChunksForCompletedFile(const FString& CompletedFullPathFileName)
|
|
{
|
|
UE_LOG(LogBuildPatchServices, Verbose, TEXT("Harvesting source: %s"), *CompletedFullPathFileName);
|
|
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(Harvest);
|
|
// We need to grab any chunks from install sources that are no longer available.
|
|
// Anything that's already been loaded is already placed into the memory store appropriately,
|
|
// but anything that _hasn't_ needs to be pulled out.
|
|
TSet<FGuid> FileChunks;
|
|
InstallSource->GetChunksForFile(CompletedFullPathFileName, FileChunks);
|
|
|
|
struct FNeededChunk
|
|
{
|
|
FGuid Id;
|
|
int32 LastUsageIndex;
|
|
int32 NextUsageIndex;
|
|
int32 ChunkSize;
|
|
};
|
|
|
|
TArray<FNeededChunk> ChunksFromFileWeNeed;
|
|
|
|
{
|
|
FRWScopeLock ChunkLock(ChunkLocationsLock, SLT_ReadOnly);
|
|
|
|
int32 CurrentUsageIndex = ChunkReferenceTracker->GetCurrentUsageIndex();
|
|
for (const FGuid& FileChunk : FileChunks)
|
|
{
|
|
EConstructorChunkLocation* Location = ChunkLocations.Find(FileChunk);
|
|
if (Location && *Location == EConstructorChunkLocation::Install)
|
|
{
|
|
int32 LastUsageIndex = 0;
|
|
int32 NextUsageIndex = ChunkReferenceTracker->GetNextUsageForChunk(FileChunk, LastUsageIndex);
|
|
|
|
if (NextUsageIndex == -1 ||
|
|
LastUsageIndex < CurrentUsageIndex)
|
|
{
|
|
// The chunk is no longer needed
|
|
*Location = EConstructorChunkLocation::Retired;
|
|
continue;
|
|
}
|
|
|
|
int32 ChunkSize = Configuration.ManifestSet->GetChunkInfo(FileChunk)->WindowSize;
|
|
|
|
ChunksFromFileWeNeed.Add({FileChunk, LastUsageIndex, NextUsageIndex, ChunkSize});
|
|
}
|
|
}
|
|
}
|
|
|
|
if (ChunksFromFileWeNeed.Num() == 0)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// Try to load all the chunks that are about to go away. If it fails we don't particularly
|
|
// care since we would have fallen back to cloud anyway.
|
|
|
|
// There's some care here - if we just kick off a ton of reads, all those backing store
|
|
// entries are locked during the reads so we have to allocate space and can't page anything
|
|
// out. This is fine if we can load the whole file, but under constrained memory we want to
|
|
// only keep stuff that's going to get used soon - so we load the stuff we _aren't_ going to use
|
|
// soon so it can get paged out. Then we load in batches so we can release the locks and let them
|
|
// page out.
|
|
// LONGTERM - detect this condition and write directly to the disk backing store? Ideally this
|
|
// would be something we can retain across restarts as right now any harvested chunks get lost
|
|
// on abort and cause a download from the cloud source (not chunkdb!)
|
|
Algo::SortBy(ChunksFromFileWeNeed, &FNeededChunk::NextUsageIndex, TGreater<int32>());
|
|
|
|
constexpr int32 HarvestBatchSize = 16 << 20; // 16 MB
|
|
|
|
bool bHarvestSuccess = true;
|
|
for (int32 ChunkIndex = 0; ChunkIndex < ChunksFromFileWeNeed.Num();)
|
|
{
|
|
int32 BatchSize = 0;
|
|
int32 ChunkEndIndex = ChunkIndex + 1;
|
|
for (; ChunkEndIndex < ChunksFromFileWeNeed.Num(); ChunkEndIndex++)
|
|
{
|
|
if (BatchSize + ChunksFromFileWeNeed[ChunkEndIndex].ChunkSize > HarvestBatchSize)
|
|
{
|
|
break;
|
|
}
|
|
BatchSize += ChunksFromFileWeNeed[ChunkEndIndex].ChunkSize;
|
|
}
|
|
|
|
PendingHarvestRequests.store(ChunkEndIndex - ChunkIndex, std::memory_order_release);
|
|
|
|
for (int32 DispatchIndex = ChunkIndex; DispatchIndex < ChunkEndIndex; DispatchIndex++)
|
|
{
|
|
FNeededChunk& Chunk = ChunksFromFileWeNeed[DispatchIndex];
|
|
|
|
SetChunkLocation(Chunk.Id, EConstructorChunkLocation::Memory);
|
|
|
|
FMutableMemoryView Destination = BackingStore->ReserveAndLockEntry(Chunk.Id, Chunk.ChunkSize, Chunk.LastUsageIndex);
|
|
if (Destination.GetSize() == 0)
|
|
{
|
|
// Call the completion function so we decrement the request count,
|
|
// but this is a consistency failure so we can't use the results.
|
|
ChunkHarvestCompletedFn(Chunk.Id, false, false, 0);
|
|
bHarvestSuccess = false;
|
|
}
|
|
|
|
IConstructorChunkSource::FRequestProcessFn HarvestFn = InstallSource->CreateRequest(Chunk.Id, Destination, 0, IConstructorChunkSource::FChunkRequestCompleteDelegate::CreateRaw(this, &FBuildPatchFileConstructor::ChunkHarvestCompletedFn));
|
|
|
|
HarvestFn(false);
|
|
}
|
|
|
|
// The read is synchronous but the verification is not, so we still need to do the wait.
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(HarvestWait);
|
|
WakeUpDispatchThreadEvent->Wait();
|
|
}
|
|
|
|
if (bHarvestSuccess)
|
|
{
|
|
// Unlock any memory store entries
|
|
FRWScopeLock ChunkLock(ChunkLocationsLock, SLT_ReadOnly);
|
|
|
|
for (int32 DispatchIndex = ChunkIndex; DispatchIndex < ChunkEndIndex; DispatchIndex++)
|
|
{
|
|
// The read could have failed - in which case the location was switched to cloud from Memory.
|
|
// We handle this here so we don't have to deal with synchronization in the completion function.
|
|
if (ChunkLocations[ChunksFromFileWeNeed[DispatchIndex].Id] == EConstructorChunkLocation::Cloud)
|
|
{
|
|
if (!BackingStore->DereserveHarvestingEntry(ChunksFromFileWeNeed[DispatchIndex].Id))
|
|
{
|
|
bHarvestSuccess = false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (!BackingStore->CommitAndReleaseEntry(ChunksFromFileWeNeed[DispatchIndex].Id))
|
|
{
|
|
bHarvestSuccess = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (bHarvestSuccess && !bAllowMultipleFilesInFlight)
|
|
{
|
|
bHarvestSuccess = BackingStore->CheckNoLocks(true);
|
|
}
|
|
|
|
ChunkIndex = ChunkEndIndex;
|
|
|
|
if (bShouldAbort || !bHarvestSuccess)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
return bHarvestSuccess;
|
|
}
|
|
|
|
void FBuildPatchFileConstructor::Run()
|
|
{
|
|
FileConstructorStat->OnTotalRequiredUpdated(TotalJobSize);
|
|
|
|
|
|
// We'd really like to have a sense of what each chunk looks like, size-wise, so that
|
|
// we know things like how many downloads we expect per batch.
|
|
// Map of window sizes to counts of that size.
|
|
TMap<uint32, int32> WindowSizes;
|
|
|
|
// lock not requried - no threads yet.
|
|
DownloadRequirement = 0;
|
|
for (const TPair<FGuid, EConstructorChunkLocation>& ChunkLocation : ChunkLocations)
|
|
{
|
|
FChunkInfo const* ChunkInfo = Configuration.ManifestSet->GetChunkInfo(ChunkLocation.Key);
|
|
WindowSizes.FindOrAdd(ChunkInfo->WindowSize)++;
|
|
|
|
if (ChunkLocation.Value == EConstructorChunkLocation::Cloud)
|
|
{
|
|
DownloadRequirement += ChunkInfo->FileSize;
|
|
}
|
|
}
|
|
CloudSource->PostRequiredByteCount(DownloadRequirement);
|
|
|
|
{
|
|
ExpectedChunkSize = 0;
|
|
int32 LargestWindowCount = 0;
|
|
for (TPair<uint32, int32>& Entry : WindowSizes)
|
|
{
|
|
if (Entry.Value > LargestWindowCount)
|
|
{
|
|
LargestWindowCount = Entry.Value;
|
|
ExpectedChunkSize = Entry.Key;
|
|
}
|
|
}
|
|
|
|
if (ExpectedChunkSize)
|
|
{
|
|
UE_LOG(LogBuildPatchServices, Log, TEXT("Expected chunk size: %d count %d"), ExpectedChunkSize, LargestWindowCount);
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogBuildPatchServices, Log, TEXT("Can't find largest chunk size, using 1MB"));
|
|
ExpectedChunkSize = 1 << 20;
|
|
}
|
|
}
|
|
|
|
// We disable resume if we are a fresh install that's below a certain threshold in order to minimize
|
|
// io operations.
|
|
bool bResumeEnabled = true;
|
|
if (!Configuration.ManifestSet->ContainsUpdate())
|
|
{
|
|
int64 DisableResumeBelowBytes = ((int64)Configuration.DisableResumeBelowMB.Get(CVarDisableResumeBelowMB)) << 20;
|
|
if (DisableResumeBelowBytes > TotalJobSize)
|
|
{
|
|
UE_LOG(LogBuildPatchServices, Log, TEXT("Disabling resume: JobSize = %s, Disable = %s, from %s"), *FormatNumber(TotalJobSize), *FormatNumber(DisableResumeBelowBytes), Configuration.DisableResumeBelowMB.IsSet() ? TEXT("config") : TEXT("cvar"));
|
|
bResumeEnabled = false;
|
|
}
|
|
}
|
|
|
|
if (Configuration.bInstallToMemory)
|
|
{
|
|
UE_LOG(LogBuildPatchServices, Log, TEXT("Disabling resume: install to memory"));
|
|
bResumeEnabled = false;
|
|
}
|
|
|
|
const FString ResumeDataFilename = Configuration.MetaDirectory / TEXT("$resumeData");
|
|
FResumeData ResumeData(FileSystem, Configuration.ManifestSet, Configuration.StagingDirectory, ResumeDataFilename);
|
|
|
|
if (bResumeEnabled)
|
|
{
|
|
IFileManager::Get().MakeDirectory(*Configuration.MetaDirectory);
|
|
|
|
// Check for resume data, we need to also look for a legacy resume file to use instead in case we are resuming from an install of previous code version.
|
|
const FString LegacyResumeDataFilename = Configuration.StagingDirectory / TEXT("$resumeData");
|
|
const bool bHasLegacyResumeData = FileSystem->FileExists(*LegacyResumeDataFilename);
|
|
// If we find a legacy resume data file, lets move it first.
|
|
if (bHasLegacyResumeData)
|
|
{
|
|
FileSystem->MoveFile(*ResumeDataFilename, *LegacyResumeDataFilename);
|
|
}
|
|
|
|
ResumeData.InitResume();
|
|
|
|
// Remove incompatible files
|
|
if (ResumeData.bHasResumeData)
|
|
{
|
|
for (const FString& FileToConstruct : Configuration.ConstructList)
|
|
{
|
|
ResumeData.CheckFile(FileToConstruct);
|
|
const bool bFileIncompatible = ResumeData.FilesIncompatible.Contains(FileToConstruct);
|
|
if (bFileIncompatible)
|
|
{
|
|
GLog->Logf(TEXT("FBuildPatchFileConstructor: Deleting incompatible stage file %s"), *FileToConstruct);
|
|
FileSystem->DeleteFile(*(Configuration.StagingDirectory / FileToConstruct));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Save for started versions
|
|
TSet<FString> ResumeIds;
|
|
const bool bCheckLegacyIds = false;
|
|
|
|
Configuration.ManifestSet->GetInstallResumeIds(ResumeIds, bCheckLegacyIds);
|
|
ResumeData.SaveOut(ResumeIds);
|
|
}
|
|
|
|
// Start resume progress at zero or one.
|
|
FileConstructorStat->OnResumeStarted();
|
|
|
|
// While we have files to construct, run.
|
|
ConstructFiles(ResumeData);
|
|
|
|
// Mark resume complete if we didn't have work to do.
|
|
if (!bIsDownloadStarted)
|
|
{
|
|
FileConstructorStat->OnResumeCompleted();
|
|
}
|
|
FileConstructorStat->OnConstructionCompleted();
|
|
}
|
|
|
|
uint64 FBuildPatchFileConstructor::GetRequiredDiskSpace()
|
|
{
|
|
return RequiredDiskSpace.load(std::memory_order_relaxed);
|
|
}
|
|
|
|
uint64 FBuildPatchFileConstructor::GetAvailableDiskSpace()
|
|
{
|
|
return AvailableDiskSpace.load(std::memory_order_relaxed);
|
|
}
|
|
|
|
FBuildPatchFileConstructor::FOnBeforeDeleteFile& FBuildPatchFileConstructor::OnBeforeDeleteFile()
|
|
{
|
|
return BeforeDeleteFileEvent;
|
|
}
|
|
|
|
void FBuildPatchFileConstructor::CountBytesProcessed( const int64& ByteCount )
|
|
{
|
|
ByteProcessed += ByteCount;
|
|
FileConstructorStat->OnProcessedDataUpdated(ByteProcessed);
|
|
}
|
|
|
|
int64 FBuildPatchFileConstructor::GetRemainingBytes()
|
|
{
|
|
// Need the sum of the output sizes of files not yet started.
|
|
// Since this gets called from any thread, construction will continue
|
|
// as we calculate this, but all the structures are stable as long as
|
|
// the constructor is still valid memory.
|
|
|
|
int32 LocalNextIndexToConstruct = NextIndexToConstruct.load(std::memory_order_acquire);
|
|
|
|
uint64 RemainingBytes = 0;
|
|
for (int32 ConstructIndex = LocalNextIndexToConstruct; ConstructIndex < ConstructionList.Num(); ConstructIndex++)
|
|
{
|
|
if (ConstructionList[ConstructIndex].FileManifest)
|
|
{
|
|
RemainingBytes += ConstructionList[ConstructIndex].FileManifest->FileSize;
|
|
}
|
|
}
|
|
|
|
return RemainingBytes;
|
|
}
|
|
|
|
uint64 FBuildPatchFileConstructor::CalculateInProgressDiskSpaceRequired(const FFileManifest& InProgressFileManifest, uint64 InProgressFileAmountWritten)
|
|
{
|
|
if (Configuration.InstallMode == EInstallMode::DestructiveInstall)
|
|
{
|
|
// The simplest method will be to run through each high level file operation, tracking peak disk usage delta.
|
|
|
|
// We know we need enough space to finish writing this file
|
|
uint64 RemainingThisFileSpace = InProgressFileManifest.FileSize - InProgressFileAmountWritten;
|
|
|
|
int64 DiskSpaceDeltaPeak = RemainingThisFileSpace;
|
|
int64 DiskSpaceDelta = RemainingThisFileSpace;
|
|
|
|
// Then we move this file over.
|
|
{
|
|
const FFileManifest* OldFileManifest = Configuration.ManifestSet->GetCurrentFileManifest(InProgressFileManifest.Filename);
|
|
if (OldFileManifest)
|
|
{
|
|
DiskSpaceDelta -= OldFileManifest->FileSize;
|
|
}
|
|
|
|
// We've already accounted for the new file above, so we could be pretty negative if we resumed the file
|
|
// almost at the end and had an existing file we're deleting.
|
|
}
|
|
|
|
// Loop through all files to be made next, in order.
|
|
int32 LocalNextIndexToConstruct = NextIndexToConstruct.load(std::memory_order_acquire);
|
|
for (int32 ConstructionIndex = LocalNextIndexToConstruct; ConstructionIndex < ConstructionList.Num(); ConstructionIndex++)
|
|
{
|
|
const FFileManifest* NewFileManifest = ConstructionList[ConstructionIndex].FileManifest;
|
|
if (!NewFileManifest)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
const FFileManifest* OldFileManifest = Configuration.ManifestSet->GetCurrentFileManifest(Configuration.ConstructList[ConstructionIndex]);
|
|
|
|
// First we would need to make the new file.
|
|
DiskSpaceDelta += NewFileManifest->FileSize;
|
|
if (DiskSpaceDeltaPeak < DiskSpaceDelta)
|
|
{
|
|
DiskSpaceDeltaPeak = DiskSpaceDelta;
|
|
}
|
|
// Then we can remove the current existing file.
|
|
if (OldFileManifest)
|
|
{
|
|
DiskSpaceDelta -= OldFileManifest->FileSize;
|
|
}
|
|
}
|
|
return DiskSpaceDeltaPeak;
|
|
}
|
|
else
|
|
{
|
|
// When not destructive, we always stage all new and changed files.
|
|
uint64 RemainingFilesSpace = GetRemainingBytes();
|
|
uint64 RemainingThisFileSpace = InProgressFileManifest.FileSize - InProgressFileAmountWritten;
|
|
return RemainingFilesSpace + RemainingThisFileSpace;
|
|
}
|
|
}
|
|
|
|
uint64 FBuildPatchFileConstructor::CalculateDiskSpaceRequirementsWithDeleteDuringInstall()
|
|
{
|
|
if (ChunkDbSource == nullptr)
|
|
{
|
|
// invalid use.
|
|
return 0;
|
|
}
|
|
|
|
// These are the sizes at after each file that we _started_ with. This is the size after retirement for the
|
|
// file at those positions.
|
|
TArray<uint64> ChunkDbSizesAtPosition;
|
|
uint64 TotalChunkDbSize = ChunkDbSource->GetChunkDbSizesAtIndexes(FileCompletionPositions, ChunkDbSizesAtPosition);
|
|
|
|
// Strip off the files we've completed.
|
|
int32 CompletedFileCount = NextIndexToConstruct.load(std::memory_order_acquire);
|
|
|
|
// Since we are called after the first file is popped (but before it's actually done), we have one less completed.
|
|
check(CompletedFileCount > 0); // should ALWAYS be at least 1!
|
|
if (CompletedFileCount > 0)
|
|
{
|
|
CompletedFileCount--;
|
|
}
|
|
|
|
uint64 MaxDiskSize = FBuildPatchUtils::CalculateDiskSpaceRequirementsWithDeleteDuringInstall(
|
|
Configuration.ConstructList, CompletedFileCount, Configuration.ManifestSet, ChunkDbSizesAtPosition, TotalChunkDbSize);
|
|
|
|
// Strip off the data we already have on disk.
|
|
uint64 PostDlSize = 0;
|
|
if (MaxDiskSize > TotalChunkDbSize)
|
|
{
|
|
PostDlSize = MaxDiskSize - TotalChunkDbSize;
|
|
}
|
|
|
|
return PostDlSize;
|
|
}
|
|
|
|
struct FBatchState;
|
|
struct FRequestInfo
|
|
{
|
|
struct FRequestSplat
|
|
{
|
|
uint64 DestinationOffset;
|
|
uint32 OffsetInChunk;
|
|
uint32 BytesToCopy;
|
|
};
|
|
|
|
FGuid Guid;
|
|
|
|
// We can do a lot of shortcuts if we are working with the entire chunk
|
|
uint32 ChunkSize = 0;
|
|
|
|
// We could request the same guid multiple times
|
|
// for the same buffer... in this case we want one
|
|
// request but we need to remember to splat it
|
|
// afterwards.
|
|
TArray<FRequestSplat, TInlineAllocator<1>> Splats;
|
|
|
|
// The read goes here - this is usually directly into the write buffer,
|
|
// but we might need to duplicate out of this (and this might be memory
|
|
// store owned if we don't use the whole chunk)
|
|
FMutableMemoryView ReadBuffer;
|
|
|
|
FBatchState* Batch = nullptr;
|
|
|
|
// Splats offset into this.
|
|
FMutableMemoryView DestinationBuffer;
|
|
|
|
FFileConstructionState* File = nullptr;
|
|
|
|
// We can only read direct in some cases.
|
|
bool bReadIntoMemoryStore = false;
|
|
|
|
// Never save back to memory store if it came from it.
|
|
bool bSourceIsMemoryStore = false;
|
|
bool bAborted = false;
|
|
bool bFailedToRead = false;
|
|
bool bLaunchedFallback = false;
|
|
|
|
// Which index in the chunk reference tracker we are requesting.
|
|
int32 ChunkIndexInFile;
|
|
int32 ChunkUnavailableAt = TNumericLimits<int32>::Max();
|
|
};
|
|
|
|
|
|
struct FBatchState
|
|
{
|
|
int32_t BatchId = 0;
|
|
|
|
TMap<FGuid, FRequestInfo> Requests;
|
|
std::atomic_int32_t PendingRequestCount = 0;
|
|
std::atomic_int32_t FailedRequestCount = 0;
|
|
|
|
// Reads for the batch end up here, and will be written to the target
|
|
// file in order.
|
|
FMutableMemoryView BatchBuffer;
|
|
|
|
EConstructionError Error = EConstructionError::None;
|
|
FGuid ErrorContextGuid;
|
|
|
|
FFileConstructionState* OwningFile = nullptr;
|
|
|
|
int32 StartChunkPartIndex = 0;
|
|
int32 ChunkCount = 0;
|
|
|
|
// If true, this batch never reads or writes, it exists to complete the empty file in order.
|
|
bool bIsEmptyFileSentinel = false;
|
|
bool bNeedsWrite = false;
|
|
bool bIsReading = false;
|
|
bool bIsWriting = false;
|
|
|
|
// Set by the completing threads when the batch is done.
|
|
std::atomic<bool> bIsFinished = true;
|
|
|
|
FBatchState()
|
|
{
|
|
static std::atomic_int32_t UniqueBatchId = 1;
|
|
BatchId = UniqueBatchId.fetch_add(1);
|
|
}
|
|
};
|
|
|
|
// Called from basically any thread.
|
|
void FBuildPatchFileConstructor::ChunkHarvestCompletedFn(const FGuid& Guid, bool bAborted, bool bFailedToRead, void* UserPtr)
|
|
{
|
|
if (bFailedToRead)
|
|
{
|
|
// We tell the main thread this failed by setting the location since that's thread
|
|
// safe.
|
|
SetChunkLocation(Guid, EConstructorChunkLocation::Cloud);
|
|
}
|
|
|
|
if (PendingHarvestRequests.fetch_add(-1, std::memory_order_relaxed) == 1)
|
|
{
|
|
WakeUpDispatchThreadEvent->Trigger();
|
|
}
|
|
}
|
|
|
|
// Called from basically any thread.
|
|
void FBuildPatchFileConstructor::RequestCompletedFn(const FGuid& Guid, bool bAborted, bool bFailedToRead, void* UserPtr)
|
|
{
|
|
FRequestInfo* Request = (FRequestInfo*)UserPtr;
|
|
Request->bAborted = bAborted;
|
|
Request->bFailedToRead = bFailedToRead;
|
|
|
|
// If failed but didn't abort and we haven't already kicked the fallback, kick the fallback
|
|
if (!bAborted && bFailedToRead && !Request->bLaunchedFallback)
|
|
{
|
|
Request->bLaunchedFallback = true;
|
|
Request->bFailedToRead = false;
|
|
|
|
// We couldn't read the expected source. This means we need to fall back to the cloud source.
|
|
|
|
// This should be safe because the values already exist in the map and we only
|
|
// ever have 1 request for a Guid active at one time. However if we've already read into the
|
|
// memory store then it's already updated to memory which is where the cloud source will read
|
|
// it to. If it's not reading into the memory store, then we need to remember to grab it
|
|
// from the cloud next time.
|
|
if (!Request->bReadIntoMemoryStore)
|
|
{
|
|
SetChunkLocation(Request->Guid, EConstructorChunkLocation::Cloud);
|
|
}
|
|
else
|
|
{
|
|
// Normally SetChunkLocation will update the download amount, but we aren't actually
|
|
// changing the chunk's location since it's going into the memory store. We do still
|
|
// need to tell the user about the download requirement though:
|
|
uint64 ChunkSize = Configuration.ManifestSet->GetDownloadSize(Guid);
|
|
|
|
ChunkLocationsLock.WriteLock();
|
|
DownloadRequirement += ChunkSize;
|
|
CloudSource->PostRequiredByteCount(DownloadRequirement);
|
|
ChunkLocationsLock.WriteUnlock();
|
|
|
|
}
|
|
|
|
if (bHasChunkDbSource)
|
|
{
|
|
// Only send this message if we have chunk dbs. The theory is if they don't have chunkdbs then they are expecting
|
|
// chunks to download from the cloud. If they provide chunkdbs then they are surprised when chunks come from the cloud.
|
|
MessagePump->SendMessage({FGenericMessage::EType::CloudSourceUsed, Guid});
|
|
}
|
|
|
|
QueueGenericThreadTask(ThreadAssignments[EConstructorChunkLocation::Cloud], CloudSource->CreateRequest(
|
|
Request->Guid,
|
|
Request->ReadBuffer,
|
|
Request,
|
|
IConstructorChunkSource::FChunkRequestCompleteDelegate::CreateRaw(this, &FBuildPatchFileConstructor::RequestCompletedFn)));
|
|
}
|
|
else
|
|
{
|
|
if (bFailedToRead)
|
|
{
|
|
Request->Batch->ErrorContextGuid = Guid;
|
|
Request->Batch->FailedRequestCount.fetch_add(1, std::memory_order_relaxed);
|
|
}
|
|
else if (!bAborted)
|
|
{
|
|
uint64 TotalToDestinationBuffer = 0;
|
|
|
|
if (Request->bReadIntoMemoryStore)
|
|
{
|
|
// If the read went to memory, we need to copy splats. Otherwise it was a single
|
|
// direct read so do nothing.
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(CFFC_Splats);
|
|
for (const FRequestInfo::FRequestSplat& Splat : Request->Splats)
|
|
{
|
|
FMemory::Memcpy(
|
|
(uint8*)Request->DestinationBuffer.GetData() + Splat.DestinationOffset,
|
|
(uint8*)Request->ReadBuffer.GetData() + Splat.OffsetInChunk,
|
|
Splat.BytesToCopy);
|
|
TotalToDestinationBuffer += Splat.BytesToCopy;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Direct means 1 splat
|
|
TotalToDestinationBuffer += Request->Splats[0].BytesToCopy;
|
|
}
|
|
|
|
Request->File->ProgressLock.Lock();
|
|
Request->File->Progress += TotalToDestinationBuffer;
|
|
Request->File->ProgressLock.Unlock();
|
|
}
|
|
|
|
if (Request->Batch->PendingRequestCount.fetch_add(-1, std::memory_order_relaxed) == 1)
|
|
{
|
|
Request->Batch->bIsFinished.store(true, std::memory_order_release);
|
|
WakeUpDispatchThreadEvent->Trigger();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Return a function that writes the data for the batch to the given file.
|
|
IConstructorChunkSource::FRequestProcessFn FBuildPatchFileConstructor::CreateWriteRequest(FArchive* File, FBatchState& Batch)
|
|
{
|
|
return [this, File, Batch = &Batch](bool bIsAbort)
|
|
{
|
|
if (bIsAbort)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Has to be mutable because of the serialize call.
|
|
FMutableMemoryView WriteBuffer = Batch->BatchBuffer;
|
|
|
|
// Manage write limits.
|
|
if (bStallWhenFileSystemThrottled)
|
|
{
|
|
uint64 AvailableBytes = FileSystem->GetAllowedBytesToWriteThrottledStorage(*File->GetArchiveName());
|
|
while (WriteBuffer.GetSize() > AvailableBytes)
|
|
{
|
|
UE_LOG(LogBuildPatchServices, Display, TEXT("Avaliable write bytes to write throttled storage exhausted (%s). Sleeping %ds. Bytes needed: %llu, bytes available: %llu")
|
|
, *File->GetArchiveName(), SleepTimeWhenFileSystemThrottledSeconds, WriteBuffer.GetSize(), AvailableBytes);
|
|
FPlatformProcess::Sleep(SleepTimeWhenFileSystemThrottledSeconds);
|
|
AvailableBytes = FileSystem->GetAllowedBytesToWriteThrottledStorage(*File->GetArchiveName());
|
|
|
|
if (bShouldAbort)
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
FileConstructorStat->OnBeforeWrite();
|
|
ISpeedRecorder::FRecord ActivityRecord;
|
|
ActivityRecord.CyclesStart = FStatsCollector::GetCycles();
|
|
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(WriteThread_Serialize)
|
|
File->Serialize(WriteBuffer.GetData(), WriteBuffer.GetSize());
|
|
}
|
|
|
|
ActivityRecord.Size = WriteBuffer.GetSize();
|
|
ActivityRecord.CyclesEnd = FStatsCollector::GetCycles();
|
|
FileConstructorStat->OnAfterWrite(ActivityRecord);
|
|
|
|
Batch->bIsFinished.store(true, std::memory_order_release);
|
|
};
|
|
}
|
|
|
|
|
|
// Always called on Constructor thread.
|
|
void FBuildPatchFileConstructor::CompleteReadBatch(const FFileManifest& FileManifest, FBatchState& Batch)
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(CFFC_CompleteRead);
|
|
|
|
if (Batch.Error == EConstructionError::None && Batch.FailedRequestCount.load(std::memory_order_acquire))
|
|
{
|
|
Batch.Error = EConstructionError::MissingChunk;
|
|
}
|
|
|
|
UE_LOG(LogBuildPatchServices, Verbose, TEXT("Completing ReadBatch: %d"), Batch.BatchId);
|
|
|
|
// We have to copy the memory source chunks after the reads are done
|
|
// because if we have two buffer's reads queued, the first one could be
|
|
// filling the memory source. If we copy these after we are done, we guarantee
|
|
// that the previous buffer has completed its reads so we know we are
|
|
// working with valid memory.
|
|
if (Batch.Error == EConstructionError::None)
|
|
{
|
|
for (TPair<FGuid, FRequestInfo>& RequestPair : Batch.Requests)
|
|
{
|
|
FRequestInfo& Request = RequestPair.Value;
|
|
|
|
if (Request.bSourceIsMemoryStore)
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(CFFC_DirectCopy);
|
|
// Just copy what we need directly.
|
|
FMemoryView ChunkData = BackingStore->GetViewForChunk(Request.Guid);
|
|
bool bFailedToGetChunk = ChunkData.GetSize() == 0;
|
|
|
|
if (!bFailedToGetChunk)
|
|
{
|
|
uint64 TotalToDestinationBuffer = 0;
|
|
for (const FRequestInfo::FRequestSplat& Splat : Request.Splats)
|
|
{
|
|
FMemory::Memcpy(
|
|
(uint8*)Request.DestinationBuffer.GetData() + Splat.DestinationOffset,
|
|
(uint8*)ChunkData.GetData() + Splat.OffsetInChunk,
|
|
Splat.BytesToCopy);
|
|
TotalToDestinationBuffer += Splat.BytesToCopy;
|
|
}
|
|
|
|
Request.File->ProgressLock.Lock();
|
|
Request.File->Progress += TotalToDestinationBuffer;
|
|
Request.File->ProgressLock.Unlock();
|
|
}
|
|
else
|
|
{
|
|
Batch.Error = EConstructionError::MissingChunk;
|
|
Batch.ErrorContextGuid = Request.Guid;
|
|
}
|
|
|
|
// Mark that we're done with the memory so it can get evicted if necessary.
|
|
if (!BackingStore->ReleaseEntry(Request.Guid))
|
|
{
|
|
Batch.Error = EConstructionError::InternalConsistencyError;
|
|
Batch.ErrorContextGuid = Request.Guid;
|
|
}
|
|
}
|
|
else if (Request.bReadIntoMemoryStore)
|
|
{
|
|
// Commit the read so the memory store knows its safe to evict if necessary.
|
|
if (!BackingStore->CommitAndReleaseEntry(Request.Guid))
|
|
{
|
|
Batch.Error = EConstructionError::InternalConsistencyError;
|
|
Batch.ErrorContextGuid = Request.Guid;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (Batch.Error == EConstructionError::None)
|
|
{
|
|
// Retire the chunks we've used. This has to be in order
|
|
for (int32 i = 0; i < Batch.ChunkCount; i++)
|
|
{
|
|
if (!ChunkReferenceTracker->PopReference(FileManifest.ChunkParts[Batch.StartChunkPartIndex + i].Guid))
|
|
{
|
|
Batch.Error = EConstructionError::TrackingError;
|
|
Batch.ErrorContextGuid = FileManifest.ChunkParts[Batch.StartChunkPartIndex + i].Guid;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (Batch.Error == EConstructionError::None)
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(CFFC_MemRetire);
|
|
// Has to be after the splats because this might free the memory we need!
|
|
if (!BackingStore->CheckRetirements(ChunkReferenceTracker->GetCurrentUsageIndex()))
|
|
{
|
|
Batch.Error = EConstructionError::InternalConsistencyError;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Always called from Constructor thread
|
|
// This return -1 when we run into an error that requires stopping the installation.
|
|
// Note that there could be outstanding reads and we can no longer rely on the completion functions
|
|
// to wake up the dispatch thread (which we must run on), so we can't ever wait if this returns -1.
|
|
// Check Batch.Error for the error on -1 return.
|
|
void FBuildPatchFileConstructor::StartReadBatch(FFileConstructionState& CurrentFile, FBatchState& Batch)
|
|
{
|
|
CurrentFile.OutstandingBatches++;
|
|
Batch.Requests.Reset();
|
|
Batch.StartChunkPartIndex = CurrentFile.NextChunkPartToRead;
|
|
|
|
int32 EndChunkIdx = Batch.StartChunkPartIndex;
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(CFFC_Count);
|
|
uint64 BufferFillLevel = 0;
|
|
for (; EndChunkIdx < CurrentFile.FileManifest->ChunkParts.Num(); ++EndChunkIdx)
|
|
{
|
|
const FChunkPart& ChunkPart = CurrentFile.FileManifest->ChunkParts[EndChunkIdx];
|
|
if (ChunkPart.Size + BufferFillLevel > Batch.BatchBuffer.GetSize())
|
|
{
|
|
break;
|
|
}
|
|
|
|
FRequestInfo& Request = Batch.Requests.FindOrAdd(ChunkPart.Guid);
|
|
Request.Guid = ChunkPart.Guid;
|
|
Request.Batch = &Batch;
|
|
Request.ChunkSize = Configuration.ManifestSet->GetChunkInfo(ChunkPart.Guid)->WindowSize;
|
|
Request.File = &CurrentFile;
|
|
Request.ChunkIndexInFile = EndChunkIdx;
|
|
|
|
FRequestInfo::FRequestSplat& Splat = Request.Splats.AddDefaulted_GetRef();
|
|
Splat.DestinationOffset = BufferFillLevel;
|
|
Splat.OffsetInChunk = ChunkPart.Offset;
|
|
Splat.BytesToCopy = ChunkPart.Size;
|
|
|
|
BufferFillLevel += ChunkPart.Size;
|
|
}
|
|
|
|
// Trim our view to the amount we actually used. The calling code will reclaim the rest.
|
|
Batch.BatchBuffer.LeftInline(BufferFillLevel);
|
|
}
|
|
|
|
Batch.ChunkCount = EndChunkIdx - Batch.StartChunkPartIndex;
|
|
Batch.PendingRequestCount = Batch.Requests.Num();
|
|
Batch.FailedRequestCount = 0;
|
|
|
|
//
|
|
// IMPORTANT!!!
|
|
//
|
|
// We MUST call the completion routine for each request here so that we can know
|
|
// when all outstanding requests are done. If we bail early then we can't know when
|
|
// all requests are done during cancelation/abort. If we hit a consistency error during then
|
|
// we just need to call it with a failure.
|
|
//
|
|
for (TPair<FGuid, FRequestInfo>& RequestPair : Batch.Requests)
|
|
{
|
|
// Can come from:
|
|
// -- memory. This chunk had already been loaded and we needed it again _after it's source expired_.
|
|
// -- chunkdb. async IO + decompress
|
|
// -- cloud. async download + decompress
|
|
// -- disk. we ran out of memory store and had to dump to disk. async IO + maybe decompress
|
|
// -- install. async IO
|
|
|
|
// Note -- most chunks will be used more than once.
|
|
|
|
// sources that expire: note that expired chunks can always be redownloaded via the cloud source
|
|
// -- cloud (they expire immediately, but can be redownloaded)
|
|
// -- install
|
|
|
|
// We aren't necessarily using the whole chunk - e.g. if we are a small file, we'll only
|
|
// be a tiny part of the chunk and the rest will need to be used by the next file. In this case
|
|
// we have to read into a memory store destination so that it can be carried over into the
|
|
// next file.
|
|
|
|
|
|
FRequestInfo& Request = RequestPair.Value;
|
|
Request.DestinationBuffer = Batch.BatchBuffer;
|
|
|
|
|
|
const EConstructorChunkLocation* ChunkLocationPtr = ChunkLocations.Find(Request.Guid);
|
|
if (ChunkLocationPtr == nullptr)
|
|
{
|
|
Batch.Error = EConstructionError::InternalConsistencyError;
|
|
RequestCompletedFn(Request.Guid, true, false, &Request);
|
|
continue;
|
|
}
|
|
EConstructorChunkLocation ChunkLocation = *ChunkLocationPtr;
|
|
|
|
Request.bSourceIsMemoryStore = ChunkLocation == EConstructorChunkLocation::Memory;
|
|
if (Request.bSourceIsMemoryStore)
|
|
{
|
|
// We copy after the reads are done since the memory might not be ready.
|
|
// Make sure we don't evict it in the meantime
|
|
if (!BackingStore->LockEntry(Request.Guid))
|
|
{
|
|
Batch.Error = EConstructionError::InternalConsistencyError;
|
|
RequestCompletedFn(Request.Guid, true, false, &Request);
|
|
continue;
|
|
}
|
|
RequestCompletedFn(Request.Guid, false, false, &Request);
|
|
}
|
|
else
|
|
{
|
|
IConstructorChunkSource* ThisChunkSource = nullptr;
|
|
switch (ChunkLocation)
|
|
{
|
|
case EConstructorChunkLocation::Install: ThisChunkSource = InstallSource; break;
|
|
case EConstructorChunkLocation::Cloud: ThisChunkSource = CloudSource; break;
|
|
case EConstructorChunkLocation::ChunkDb: ThisChunkSource = ChunkDbSource; break;
|
|
case EConstructorChunkLocation::Memory: /* already handled above */ break;
|
|
case EConstructorChunkLocation::DiskOverflow: ThisChunkSource = (IConstructorChunkSource*)BackingStore.Get(); break;
|
|
}
|
|
|
|
if (ThisChunkSource == CloudSource)
|
|
{
|
|
// If we are already downloading from the cloud, then failures shouldn't try to
|
|
// fall back to the cloud.
|
|
Request.bLaunchedFallback = true;
|
|
|
|
if (bHasChunkDbSource)
|
|
{
|
|
// Only send this message if we have chunk dbs. The theory is if they don't have chunkdbs then they are expecting
|
|
// chunks to download from the cloud. If they provide chunkdbs then they are surprised when chunks come from the cloud.
|
|
MessagePump->SendMessage({FGenericMessage::EType::CloudSourceUsed, RequestPair.Value.Guid});
|
|
}
|
|
}
|
|
|
|
// We need to kick a request. The question is whether we can request direct
|
|
// or need to route through the memory store.
|
|
int32 LastUsageIndex = 0;
|
|
ChunkReferenceTracker->GetNextUsageForChunk(Request.Guid, LastUsageIndex);
|
|
Request.ChunkUnavailableAt = ThisChunkSource->GetChunkUnavailableAt(Request.Guid);
|
|
|
|
const bool bNeedsEntireChunk = Request.Splats.Num() == 1 && Request.Splats[0].BytesToCopy == Request.ChunkSize && Request.Splats[0].OffsetInChunk == 0;
|
|
|
|
int32 ThisUsageIndex = CurrentFile.BaseReferenceIndex + Request.ChunkIndexInFile;
|
|
|
|
// Unavailable might be reported before the current usage which would force this to route through
|
|
// memory store even though it could read direct, so only check needed after retirement if we need it again.
|
|
const bool bNeededAfterRetirement = (LastUsageIndex > ThisUsageIndex) && LastUsageIndex >= Request.ChunkUnavailableAt;
|
|
|
|
if (bNeedsEntireChunk && !bNeededAfterRetirement)
|
|
{
|
|
// Read direct.
|
|
Request.ReadBuffer = MakeMemoryView((uint8*)Request.DestinationBuffer.GetData() + Request.Splats[0].DestinationOffset, Request.Splats[0].BytesToCopy);
|
|
}
|
|
else
|
|
{
|
|
// Route through memory store.
|
|
Request.ReadBuffer = BackingStore->ReserveAndLockEntry(Request.Guid, Request.ChunkSize, LastUsageIndex);
|
|
if (Request.ReadBuffer.GetSize() == 0)
|
|
{
|
|
Batch.Error = EConstructionError::InternalConsistencyError;
|
|
RequestCompletedFn(Request.Guid, true, false, &Request);
|
|
continue;
|
|
}
|
|
Request.bReadIntoMemoryStore = true;
|
|
|
|
// Note that when we set this, the next batch read could want this chunk before its read is done.
|
|
// Hence reads for memory sources are done _after_ reads are done, because we retire reads in order,
|
|
// we then know this memory is populated.
|
|
SetChunkLocation(RequestPair.Value.Guid, EConstructorChunkLocation::Memory);
|
|
}
|
|
|
|
|
|
IConstructorChunkSource::FRequestProcessFn RequestProcess = ThisChunkSource->CreateRequest(
|
|
Request.Guid,
|
|
Request.ReadBuffer,
|
|
(void*)&Request,
|
|
IConstructorChunkSource::FChunkRequestCompleteDelegate::CreateRaw(this, &FBuildPatchFileConstructor::RequestCompletedFn));
|
|
|
|
QueueGenericThreadTask(ThreadAssignments[ChunkLocation], MoveTemp(RequestProcess));
|
|
}
|
|
}
|
|
|
|
CurrentFile.NextChunkPartToRead = EndChunkIdx;
|
|
}
|
|
|
|
void FBuildPatchFileConstructor::WakeUpDispatch()
|
|
{
|
|
WakeUpDispatchThreadEvent->Trigger();
|
|
}
|
|
|
|
void FBuildPatchFileConstructor::InitFile(FFileConstructionState& CurrentFile, const FResumeData& ResumeData)
|
|
{
|
|
if (!CurrentFile.bSuccess)
|
|
{
|
|
return;
|
|
}
|
|
|
|
const int64 FileSize = CurrentFile.FileManifest->FileSize;
|
|
|
|
// Check resume status for this file.
|
|
const bool bFilePreviouslyComplete = ResumeData.FilesCompleted.Contains(CurrentFile.BuildFilename);
|
|
|
|
// Construct or skip the file.
|
|
if (bFilePreviouslyComplete)
|
|
{
|
|
CountBytesProcessed(FileSize);
|
|
|
|
UE_LOG(LogBuildPatchServices, Display, TEXT("Skipping completed file %s"), *CurrentFile.BuildFilename);
|
|
// Go through each chunk part, and dereference it from the reference tracker.
|
|
for (const FChunkPart& ChunkPart : CurrentFile.FileManifest->ChunkParts)
|
|
{
|
|
if (!ChunkReferenceTracker->PopReference(ChunkPart.Guid))
|
|
{
|
|
CurrentFile.bSuccess = false;
|
|
CurrentFile.ConstructionError = EConstructionError::TrackingError;
|
|
break;
|
|
}
|
|
}
|
|
|
|
CurrentFile.bSkippedConstruction = true;
|
|
return;
|
|
}
|
|
|
|
if (!CurrentFile.bSuccess &&
|
|
!CurrentFile.FileManifest->SymlinkTarget.IsEmpty())
|
|
{
|
|
#if PLATFORM_MAC
|
|
CurrentFile.bSuccess = true;
|
|
if (!Configuration.bInstallToMemory && !Configuration.bConstructInMemory)
|
|
{
|
|
CurrentFile.bSuccess = symlink(TCHAR_TO_UTF8(*CurrentFile.FileManifest->SymlinkTarget), TCHAR_TO_UTF8(*CurrentFile.NewFilename)) == 0;
|
|
}
|
|
CurrentFile.bSkippedConstruction = true;
|
|
#else
|
|
const bool bSymlinkNotImplemented = false;
|
|
check(bSymlinkNotImplemented);
|
|
CurrentFile.bSuccess = false;
|
|
#endif
|
|
}
|
|
|
|
if (Configuration.bInstallToMemory || Configuration.bConstructInMemory)
|
|
{
|
|
// Allocate the destination buffer. This is most likely where we would run out of memory, but UE doesn't
|
|
// really have any way to gracefully handle OOM the way we might handle running out of disk space.
|
|
TArray64<uint8> OutputFile;
|
|
OutputFile.SetNumUninitialized(CurrentFile.FileManifest->FileSize);
|
|
|
|
MemoryOutputFiles.Add(CurrentFile.BuildFilename, MoveTemp(OutputFile));
|
|
}
|
|
}
|
|
|
|
bool FBuildPatchFileConstructor::HandleInitialDiskSizeCheck(const FFileManifest& FileManifest, int64 StartPosition)
|
|
{
|
|
if (Configuration.bInstallToMemory)
|
|
{
|
|
bInitialDiskSizeCheck = true;
|
|
return true;
|
|
}
|
|
|
|
// Returns false if we failed the disk space check.
|
|
if (!bInitialDiskSizeCheck)
|
|
{
|
|
bInitialDiskSizeCheck = true;
|
|
|
|
// Normal operation can just use the classic calculation
|
|
uint64 LocalDiskSpaceRequired = CalculateInProgressDiskSpaceRequired(FileManifest, StartPosition);
|
|
|
|
// If we are delete-during-install this gets more complicated because we'll be freeing up
|
|
// space as we add.
|
|
if (Configuration.bDeleteChunkDBFilesAfterUse)
|
|
{
|
|
LocalDiskSpaceRequired = CalculateDiskSpaceRequirementsWithDeleteDuringInstall();
|
|
}
|
|
|
|
uint64 LocalDiskSpaceAvailable = 0;
|
|
{
|
|
uint64 TotalSize = 0;
|
|
uint64 AvailableSpace = 0;
|
|
if (FPlatformMisc::GetDiskTotalAndFreeSpace(Configuration.InstallDirectory, TotalSize, AvailableSpace))
|
|
{
|
|
LocalDiskSpaceAvailable = AvailableSpace;
|
|
BackingStore->SetDynamicDiskSpaceHeadroom(LocalDiskSpaceRequired);
|
|
}
|
|
else
|
|
{
|
|
BackingStore->DisableDynamicDiskSpaceHeadroom();
|
|
}
|
|
}
|
|
|
|
UE_LOG(LogBuildPatchServices, Verbose, TEXT("Initial Disk Sizes: Required: %s Available: %s"), *FormatNumber(LocalDiskSpaceRequired), *FormatNumber(LocalDiskSpaceAvailable));
|
|
|
|
AvailableDiskSpace.store(LocalDiskSpaceAvailable, std::memory_order_release);
|
|
RequiredDiskSpace.store(LocalDiskSpaceRequired, std::memory_order_release);
|
|
|
|
if (!FileConstructorHelpers::CheckRemainingDiskSpace(Configuration.InstallDirectory, LocalDiskSpaceRequired, LocalDiskSpaceAvailable))
|
|
{
|
|
UE_LOG(LogBuildPatchServices, Error, TEXT("Out of HDD space. Needs %llu bytes, Free %llu bytes"), LocalDiskSpaceRequired, LocalDiskSpaceAvailable);
|
|
InstallerError->SetError(
|
|
EBuildPatchInstallError::OutOfDiskSpace,
|
|
DiskSpaceErrorCodes::InitialSpaceCheck,
|
|
0,
|
|
BuildPatchServices::GetDiskSpaceMessage(Configuration.InstallDirectory, LocalDiskSpaceRequired, LocalDiskSpaceAvailable));
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void FBuildPatchFileConstructor::ResumeFile(FFileConstructionState& FileToResume)
|
|
{
|
|
if (!FileToResume.bSuccess || FileToResume.bSkippedConstruction)
|
|
{
|
|
return;
|
|
}
|
|
|
|
check(!Configuration.bInstallToMemory);
|
|
check(!Configuration.bConstructInMemory);
|
|
|
|
// We have to read in the existing file so that the hash check can still be done.
|
|
TUniquePtr<FArchive> NewFileReader(IFileManager::Get().CreateFileReader(*FileToResume.NewFilename));
|
|
if (!NewFileReader.IsValid())
|
|
{
|
|
// We don't fail if we can't read in the previous file - we try and rebuild it from scratch.
|
|
// (Note that the likely outcome here is we can't open the file for write either and fail to
|
|
// install - we're only here if we were supposed to be resuming!)
|
|
return;
|
|
}
|
|
|
|
const int32 ReadBufferSize = 4 * 1024 * 1024;
|
|
// Read buffer
|
|
TArray<uint8> ReadBuffer;
|
|
ReadBuffer.SetNumUninitialized(ReadBufferSize);
|
|
|
|
// We don't allow resuming mid-chunk for simplicity, so eat entire chunks until
|
|
// we can't anymore.
|
|
int64 OnDiskSize = NewFileReader->TotalSize();
|
|
int64 ByteCounter = 0;
|
|
for (int32 ChunkPartIdx = 0; ChunkPartIdx < FileToResume.FileManifest->ChunkParts.Num() && !bShouldAbort; ++ChunkPartIdx)
|
|
{
|
|
const FChunkPart& ChunkPart = FileToResume.FileManifest->ChunkParts[ChunkPartIdx];
|
|
const int64 NextBytePosition = ByteCounter + ChunkPart.Size;
|
|
ByteCounter = NextBytePosition;
|
|
if (NextBytePosition <= OnDiskSize)
|
|
{
|
|
// Ensure buffer is large enough
|
|
ReadBuffer.SetNumUninitialized(ChunkPart.Size, EAllowShrinking::No);
|
|
|
|
{
|
|
FReadScope _(FileConstructorStat, ChunkPart.Size);
|
|
NewFileReader->Serialize(ReadBuffer.GetData(), ChunkPart.Size);
|
|
}
|
|
|
|
FileToResume.HashState.Update(ReadBuffer.GetData(), ChunkPart.Size);
|
|
|
|
// Update resume start position
|
|
FileToResume.StartPosition = NextBytePosition;
|
|
FileToResume.NextChunkPartToRead = ChunkPartIdx + 1;
|
|
|
|
// Inform the reference tracker of the chunk part skip
|
|
if (!ChunkReferenceTracker->PopReference(ChunkPart.Guid))
|
|
{
|
|
FileToResume.bSuccess = false;
|
|
FileToResume.ConstructionError = EConstructionError::TrackingError;
|
|
FileToResume.ErrorContextGuid = ChunkPart.Guid;
|
|
break;
|
|
}
|
|
|
|
CountBytesProcessed(ChunkPart.Size);
|
|
FileConstructorStat->OnFileProgress(FileToResume.BuildFilename, NewFileReader->Tell());
|
|
// Wait if paused
|
|
FileConstructorHelpers::WaitWhilePaused(bIsPaused, bShouldAbort);
|
|
}
|
|
else
|
|
{
|
|
// We can't consume any more full chunks from the part list, bail.
|
|
break;
|
|
}
|
|
}
|
|
|
|
NewFileReader->Close();
|
|
FileToResume.bIsResumedFile = true;
|
|
}
|
|
|
|
void FBuildPatchFileConstructor::OpenFileToConstruct(FFileConstructionState& CurrentFile)
|
|
{
|
|
if (!CurrentFile.bSuccess || CurrentFile.bSkippedConstruction || Configuration.bInstallToMemory || Configuration.bConstructInMemory)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Attempt to create the file
|
|
{
|
|
FAdministrationScope _(FileConstructorStat);
|
|
CurrentFile.NewFile = FileSystem->CreateFileWriter(*CurrentFile.NewFilename, CurrentFile.bIsResumedFile ? EWriteFlags::Append : EWriteFlags::None);
|
|
CurrentFile.CreateFilePlatformLastError = FPlatformMisc::GetLastError();
|
|
}
|
|
|
|
CurrentFile.bSuccess = CurrentFile.NewFile != nullptr;
|
|
if (!CurrentFile.bSuccess)
|
|
{
|
|
CurrentFile.ConstructionError = EConstructionError::CannotCreateFile;
|
|
return;
|
|
}
|
|
|
|
// Seek to file write position
|
|
if (CurrentFile.NewFile->Tell() != CurrentFile.StartPosition)
|
|
{
|
|
FAdministrationScope _(FileConstructorStat);
|
|
|
|
// Currently no way of checking if the seek succeeded. If it didn't and further reads succeed, then
|
|
// we can end up with a bad file on disk and not know it as the hash is assuming this worked - requires
|
|
// full load-and-hash verification to find.
|
|
CurrentFile.NewFile->Seek(CurrentFile.StartPosition);
|
|
}
|
|
|
|
CurrentFile.Progress = CurrentFile.StartPosition;
|
|
CurrentFile.LastSeenProgress = CurrentFile.Progress;
|
|
}
|
|
|
|
|
|
void FBuildPatchFileConstructor::CompleteConstructedFile(FFileConstructionState& CurrentFile)
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(Constructor_CompleteFile);
|
|
if (!CurrentFile.bSkippedConstruction)
|
|
{
|
|
if (CurrentFile.NewFile) // we don't have a file when constructing/installing in memory.
|
|
{
|
|
if (CurrentFile.bSuccess && CurrentFile.NewFile->IsError())
|
|
{
|
|
CurrentFile.ConstructionError = EConstructionError::SerializeError;
|
|
CurrentFile.bSuccess = false;
|
|
}
|
|
|
|
if (CurrentFile.bSuccess)
|
|
{
|
|
// Close the file writer
|
|
bool bArchiveSuccess = false;
|
|
{
|
|
FAdministrationScope _(FileConstructorStat);
|
|
bArchiveSuccess = CurrentFile.NewFile->Close();
|
|
CurrentFile.NewFile.Reset();
|
|
}
|
|
|
|
// Check for final success
|
|
if (CurrentFile.bSuccess && !bArchiveSuccess)
|
|
{
|
|
CurrentFile.ConstructionError = EConstructionError::SerializeError;
|
|
CurrentFile.bSuccess = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// We can't check for zero locks if we have multiple files in flight because the other
|
|
// files hold locks.
|
|
if (CurrentFile.bSuccess && !bAllowMultipleFilesInFlight)
|
|
{
|
|
if (!BackingStore->CheckNoLocks(false))
|
|
{
|
|
CurrentFile.bSuccess = false;
|
|
CurrentFile.ConstructionError = EConstructionError::InternalConsistencyError;
|
|
}
|
|
}
|
|
|
|
// Verify the hash for the file that we created
|
|
if (CurrentFile.bSuccess)
|
|
{
|
|
CurrentFile.HashState.Final();
|
|
|
|
FSHAHash HashValue;
|
|
CurrentFile.HashState.GetHash(HashValue.Hash);
|
|
CurrentFile.bSuccess = HashValue == CurrentFile.FileManifest->FileHash;
|
|
if (!CurrentFile.bSuccess)
|
|
{
|
|
CurrentFile.ConstructionError = EConstructionError::OutboundDataError;
|
|
}
|
|
}
|
|
|
|
if (!Configuration.bInstallToMemory && !Configuration.bConstructInMemory)
|
|
{
|
|
CurrentFile.SetAttributes();
|
|
}
|
|
|
|
} // end if we did actual construction work for this file.
|
|
|
|
// If we are destructive, remove the old file.
|
|
if (CurrentFile.bSuccess &&
|
|
Configuration.InstallMode == EInstallMode::DestructiveInstall &&
|
|
!Configuration.bInstallToMemory) // Install to memory never patches/has an existing installation dir.
|
|
{
|
|
FString FileToDelete = Configuration.InstallDirectory / CurrentFile.BuildFilename;
|
|
FPaths::NormalizeFilename(FileToDelete);
|
|
FPaths::CollapseRelativeDirectories(FileToDelete);
|
|
if (FileSystem->FileExists(*FileToDelete))
|
|
{
|
|
if (InstallSource && !HarvestChunksForCompletedFile(FileToDelete))
|
|
{
|
|
CurrentFile.bSuccess = false;
|
|
CurrentFile.ConstructionError = EConstructionError::InternalConsistencyError;
|
|
} // end if install source exists.
|
|
|
|
OnBeforeDeleteFile().Broadcast(FileToDelete);
|
|
{
|
|
// This can take forever due to file system filters. If we throw this on an async
|
|
// job then we can go over our calculated disk space.
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(Delete);
|
|
const bool bRequireExists = false;
|
|
const bool bEvenReadOnly = true;
|
|
IFileManager::Get().Delete(*FileToDelete, bRequireExists, bEvenReadOnly);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (Configuration.bConstructInMemory)
|
|
{
|
|
// Now that we have the entire file ready, we write it in one big pass. This
|
|
// is synchronous to avoid complexity, but we do eat some performance since we
|
|
// aren't overlapping work with these writes.
|
|
|
|
// This is specifically after the deletion of the old installation file in order to
|
|
// manage disk space requirements - we never want to have two copies of the same file (independent
|
|
// of version) on the disk at the same time - avoiding that is the entire reason we added bConstructInMemory!
|
|
|
|
{
|
|
FAdministrationScope _(FileConstructorStat);
|
|
CurrentFile.NewFile = FileSystem->CreateFileWriter(*CurrentFile.NewFilename, EWriteFlags::None);
|
|
CurrentFile.CreateFilePlatformLastError = FPlatformMisc::GetLastError();
|
|
}
|
|
|
|
CurrentFile.bSuccess = CurrentFile.NewFile != nullptr;
|
|
if (!CurrentFile.bSuccess)
|
|
{
|
|
CurrentFile.ConstructionError = EConstructionError::CannotCreateFile;
|
|
}
|
|
else
|
|
{
|
|
TArray64<uint8>& Data = MemoryOutputFiles[CurrentFile.BuildFilename];
|
|
|
|
|
|
FileConstructorStat->OnBeforeWrite();
|
|
ISpeedRecorder::FRecord ActivityRecord;
|
|
ActivityRecord.CyclesStart = FStatsCollector::GetCycles();
|
|
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(MemoryConstruction_Write)
|
|
int64 WritePosition = 0;
|
|
for (;;)
|
|
{
|
|
if (WritePosition == Data.Num())
|
|
{
|
|
break;
|
|
}
|
|
int64 WriteAmount = 4 << 20; // 4mb write chunks.
|
|
if (WritePosition + WriteAmount > Data.Num())
|
|
{
|
|
WriteAmount = Data.Num() - WritePosition;
|
|
}
|
|
|
|
CurrentFile.NewFile->Serialize(Data.GetData() + WritePosition, WriteAmount);
|
|
|
|
WritePosition += WriteAmount;
|
|
}
|
|
}
|
|
|
|
ActivityRecord.Size = Data.Num();
|
|
ActivityRecord.CyclesEnd = FStatsCollector::GetCycles();
|
|
FileConstructorStat->OnAfterWrite(ActivityRecord);
|
|
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(MemoryConstruction_WriteFree)
|
|
Data.Empty();
|
|
}
|
|
|
|
if (CurrentFile.NewFile->IsError())
|
|
{
|
|
CurrentFile.bSuccess = false;
|
|
CurrentFile.ConstructionError = EConstructionError::SerializeError;
|
|
}
|
|
|
|
bool bArchiveSuccess = true;
|
|
{
|
|
FAdministrationScope _(FileConstructorStat);
|
|
bArchiveSuccess = CurrentFile.NewFile->Close();
|
|
CurrentFile.NewFile.Reset();
|
|
}
|
|
|
|
// Check for final success
|
|
if (CurrentFile.bSuccess && !bArchiveSuccess)
|
|
{
|
|
CurrentFile.ConstructionError = EConstructionError::SerializeError;
|
|
CurrentFile.bSuccess = false;
|
|
}
|
|
}
|
|
|
|
CurrentFile.SetAttributes();
|
|
}
|
|
|
|
if (CurrentFile.bSuccess)
|
|
{
|
|
ChunkDbSource->ReportFileCompletion(ChunkReferenceTracker->GetRemainingChunkCount());
|
|
}
|
|
|
|
FileConstructorStat->OnFileCompleted(CurrentFile.BuildFilename, CurrentFile.bSuccess);
|
|
|
|
// Report errors.
|
|
if (!CurrentFile.bSuccess)
|
|
{
|
|
if (!Configuration.bInstallToMemory &&
|
|
CurrentFile.ConstructionError == EConstructionError::SerializeError)
|
|
{
|
|
// Serialize error is our catchall file error right now. This should probably get
|
|
// migrated such that it's when we fail to load an existing chunk (i.e. corruption)
|
|
// but instead that shows up as a missing chunk.
|
|
|
|
// Right now however we get this whenever we fail to write due to our disk space disappearing,
|
|
// and we'd like to report that, so we check and see if we're low on space if it looks like
|
|
// that's the problem.
|
|
|
|
uint64 TotalSize = 0;
|
|
uint64 FreeSize = 0;
|
|
if (FPlatformMisc::GetDiskTotalAndFreeSpace(Configuration.InstallDirectory, TotalSize, FreeSize))
|
|
{
|
|
// Don't bother recalculating our disk space requirements - our original requirement is still
|
|
// valid, the only thing that's changed is someone took our space.
|
|
AvailableDiskSpace.store(FreeSize, std::memory_order_release);
|
|
|
|
// We're responding to an actual failure, which would have happened because we literally weren't able
|
|
// to write our write butter. Because of transient stuff this might not be correct so we double our
|
|
// write buffer size for this check.
|
|
if (FreeSize < ((uint64)MaxWriteBatchSize << 1))
|
|
{
|
|
CurrentFile.ConstructionError = EConstructionError::OutOfDiskSpace;
|
|
}
|
|
else
|
|
{
|
|
// If it looks like we had enough disk space to write the last buffer, then
|
|
// leave it as serialize.
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// If we can't get the free space then likely the disk has disconnected or otherwise had a Bad Error, leave
|
|
// as serialize.
|
|
}
|
|
}
|
|
|
|
const bool bReportAnalytic = InstallerError->HasError() == false;
|
|
switch (CurrentFile.ConstructionError)
|
|
{
|
|
case EConstructionError::OutboundDataError:
|
|
{
|
|
// Only report if the first error
|
|
if (bReportAnalytic)
|
|
{
|
|
InstallerAnalytics->RecordConstructionError(CurrentFile.BuildFilename, INDEX_NONE, TEXT("Serialised Verify Fail"));
|
|
}
|
|
UE_LOG(LogBuildPatchServices, Error, TEXT("FBuildPatchFileConstructor: Verify failed after constructing %s"), *CurrentFile.BuildFilename);
|
|
InstallerError->SetError(EBuildPatchInstallError::FileConstructionFail, ConstructionErrorCodes::OutboundCorrupt);
|
|
break;
|
|
}
|
|
case EConstructionError::OutOfDiskSpace:
|
|
{
|
|
uint64 LocalAvailableDiskSpace = AvailableDiskSpace.load(std::memory_order_acquire);
|
|
uint64 LocalRequiredDiskSpace = RequiredDiskSpace.load(std::memory_order_acquire);
|
|
UE_LOG(LogBuildPatchServices, Error, TEXT("Out of HDD space. Needs %llu bytes, Free %llu bytes"), LocalRequiredDiskSpace, LocalAvailableDiskSpace);
|
|
InstallerError->SetError(
|
|
EBuildPatchInstallError::OutOfDiskSpace,
|
|
DiskSpaceErrorCodes::DuringInstallation,
|
|
0,
|
|
BuildPatchServices::GetDiskSpaceMessage(Configuration.InstallDirectory, LocalRequiredDiskSpace, LocalAvailableDiskSpace));
|
|
break;
|
|
}
|
|
case EConstructionError::CannotCreateFile:
|
|
{
|
|
if (bReportAnalytic)
|
|
{
|
|
InstallerAnalytics->RecordConstructionError(CurrentFile.BuildFilename, CurrentFile.CreateFilePlatformLastError, TEXT("Could Not Create File"));
|
|
}
|
|
UE_LOG(LogBuildPatchServices, Error, TEXT("FBuildPatchFileConstructor: Could not create %s (LastError=%d)"), *CurrentFile.BuildFilename, CurrentFile.CreateFilePlatformLastError);
|
|
InstallerError->SetError(EBuildPatchInstallError::FileConstructionFail, ConstructionErrorCodes::FileCreateFail, CurrentFile.CreateFilePlatformLastError);
|
|
break;
|
|
}
|
|
case EConstructionError::MissingChunk:
|
|
{
|
|
if (bReportAnalytic)
|
|
{
|
|
InstallerAnalytics->RecordConstructionError(CurrentFile.BuildFilename, INDEX_NONE, TEXT("Missing Chunk"));
|
|
}
|
|
|
|
UE_LOG(LogBuildPatchServices, Error, TEXT("FBuildPatchFileConstructor: Failed %s due to missing chunk %s (can be 0000 if unknown)"), *CurrentFile.BuildFilename, *WriteToString<64>(CurrentFile.ErrorContextGuid));
|
|
InstallerError->SetError(EBuildPatchInstallError::FileConstructionFail, ConstructionErrorCodes::MissingChunkData);
|
|
break;
|
|
}
|
|
case EConstructionError::SerializeError:
|
|
{
|
|
if (bReportAnalytic)
|
|
{
|
|
InstallerAnalytics->RecordConstructionError(CurrentFile.BuildFilename, INDEX_NONE, TEXT("Serialization Error"));
|
|
}
|
|
UE_LOG(LogBuildPatchServices, Error, TEXT("FBuildPatchFileConstructor: Failed %s due to serialization error"), *CurrentFile.BuildFilename);
|
|
InstallerError->SetError(EBuildPatchInstallError::FileConstructionFail, ConstructionErrorCodes::SerializationError);
|
|
break;
|
|
}
|
|
case EConstructionError::TrackingError:
|
|
{
|
|
if (bReportAnalytic)
|
|
{
|
|
InstallerAnalytics->RecordConstructionError(CurrentFile.BuildFilename, INDEX_NONE, TEXT("Tracking Error"));
|
|
}
|
|
UE_LOG(LogBuildPatchServices, Error, TEXT("FBuildPatchFileConstructor: Failed %s due to untracked chunk %s (can be 0000 if unknown)"), *CurrentFile.BuildFilename, *WriteToString<64>(CurrentFile.ErrorContextGuid));
|
|
InstallerError->SetError(EBuildPatchInstallError::FileConstructionFail, ConstructionErrorCodes::TrackingError);
|
|
break;
|
|
}
|
|
case EConstructionError::InternalConsistencyError:
|
|
{
|
|
if (bReportAnalytic)
|
|
{
|
|
InstallerAnalytics->RecordConstructionError(CurrentFile.BuildFilename, INDEX_NONE, TEXT("Internal Consistency Error"));
|
|
}
|
|
UE_LOG(LogBuildPatchServices, Error, TEXT("FBuildPatchFileConstructor: Failed %s due to internal consistency checking failure"), *CurrentFile.BuildFilename);
|
|
InstallerError->SetError(EBuildPatchInstallError::FileConstructionFail, ConstructionErrorCodes::InternalConsistencyFailure);
|
|
break;
|
|
}
|
|
case EConstructionError::MissingFileInfo:
|
|
{
|
|
if (bReportAnalytic)
|
|
{
|
|
InstallerAnalytics->RecordConstructionError(CurrentFile.BuildFilename, INDEX_NONE, TEXT("Missing File Manifest"));
|
|
}
|
|
UE_LOG(LogBuildPatchServices, Error, TEXT("FBuildPatchFileConstructor: Missing file manifest for %s"), *CurrentFile.BuildFilename);
|
|
InstallerError->SetError(EBuildPatchInstallError::FileConstructionFail, ConstructionErrorCodes::MissingFileInfo);
|
|
break;
|
|
}
|
|
case EConstructionError::FailedInitialSizeCheck:
|
|
{
|
|
// Error already set back when we had the info.
|
|
break;
|
|
}
|
|
case EConstructionError::Aborted:
|
|
{
|
|
// We don't set errors on abort.
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Delete the staging file if unsuccessful by means of any failure that could leave the file in unknown state.
|
|
switch (CurrentFile.ConstructionError)
|
|
{
|
|
// Errors we expect that we can conceptually resume:
|
|
case EConstructionError::OutOfDiskSpace:
|
|
case EConstructionError::MissingChunk:
|
|
case EConstructionError::Aborted:
|
|
{
|
|
break;
|
|
}
|
|
|
|
// Errors we expect there to be issues with the outbound file:
|
|
case EConstructionError::CannotCreateFile:
|
|
case EConstructionError::SerializeError:
|
|
case EConstructionError::TrackingError:
|
|
case EConstructionError::OutboundDataError:
|
|
case EConstructionError::InternalConsistencyError:
|
|
{
|
|
if (!FileSystem->DeleteFile(*CurrentFile.NewFilename))
|
|
{
|
|
UE_LOG(LogBuildPatchServices, Warning, TEXT("FBuildPatchFileConstructor: Error deleting file: %s (Error Code %i)"), *CurrentFile.NewFilename, FPlatformMisc::GetLastError());
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Stop trying to install further files.
|
|
Abort();
|
|
}
|
|
}
|
|
|
|
void FBuildPatchFileConstructor::StartFile(FFileConstructionState& CurrentFile, const FResumeData& ResumeData)
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(Constructor_StartFile);
|
|
|
|
InitFile(CurrentFile, ResumeData);
|
|
|
|
if (CurrentFile.bSkippedConstruction)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (ResumeData.FilesStarted.Contains(CurrentFile.BuildFilename))
|
|
{
|
|
ResumeFile(CurrentFile);
|
|
|
|
// Generally speaking we only expect there to be one file to resume (as of this writing there
|
|
// is no way for the FilesStarted set to have more than one file), so we update the download requirements
|
|
// after the first one.
|
|
|
|
// Need to sum up the size of all remaining chunks that we need. We can't just look at the
|
|
// chunk locations because we don't know if it's still needed or not.
|
|
TSet<FGuid> RemainingNeededChunks = ChunkReferenceTracker->GetReferencedChunks();
|
|
|
|
uint64 NewDownloadRequirement = 0;
|
|
FRWScopeLock ChunkLock(ChunkLocationsLock, SLT_Write); // write because we're updating the download requirement
|
|
for (FGuid& NeededChunk : RemainingNeededChunks)
|
|
{
|
|
EConstructorChunkLocation* LocationPtr = ChunkLocations.Find(NeededChunk);
|
|
if (LocationPtr && *LocationPtr == EConstructorChunkLocation::Cloud)
|
|
{
|
|
NewDownloadRequirement += Configuration.ManifestSet->GetChunkInfo(NeededChunk)->FileSize;
|
|
}
|
|
}
|
|
|
|
DownloadRequirement = NewDownloadRequirement;
|
|
CloudSource->PostRequiredByteCount(DownloadRequirement);
|
|
}
|
|
|
|
// If we haven't done so yet, make the initial disk space check. We do this after resume
|
|
// so that we know how much to discount from our current file size.
|
|
if (!HandleInitialDiskSizeCheck(*CurrentFile.FileManifest, CurrentFile.StartPosition))
|
|
{
|
|
CurrentFile.bSuccess = false;
|
|
CurrentFile.ConstructionError = EConstructionError::FailedInitialSizeCheck;
|
|
}
|
|
|
|
if (!bIsDownloadStarted)
|
|
{
|
|
bIsDownloadStarted = true;
|
|
FileConstructorStat->OnResumeCompleted();
|
|
}
|
|
|
|
OpenFileToConstruct(CurrentFile);
|
|
|
|
}
|
|
|
|
void FBuildPatchFileConstructor::ConstructFiles(const FResumeData& ResumeData)
|
|
{
|
|
MaxWriteBatchSize = FFileConstructorConfig::DefaultIOBatchSizeMB;
|
|
if (GConfig->GetInt(TEXT("Portal.BuildPatch"), TEXT("ConstructorIOBatchSizeMB"), (int32&)MaxWriteBatchSize, GEngineIni))
|
|
{
|
|
UE_LOG(LogBuildPatchServices, Verbose, TEXT("Got INI ConstructorIOBatchSizeMB = %d"), MaxWriteBatchSize);
|
|
}
|
|
if (Configuration.IOBatchSizeMB.IsSet())
|
|
{
|
|
MaxWriteBatchSize = FMath::Max(1, Configuration.IOBatchSizeMB.Get(MaxWriteBatchSize));
|
|
UE_LOG(LogBuildPatchServices, Verbose, TEXT("Got override ConstructorIOBatchSizeMB = %d"), MaxWriteBatchSize);
|
|
}
|
|
MaxWriteBatchSize <<= 20; // to MB;
|
|
|
|
IOBufferSize = FFileConstructorConfig::DefaultIOBufferSizeMB;
|
|
if (GConfig->GetInt(TEXT("Portal.BuildPatch"), TEXT("ConstructorIOBufferSizeMB"), (int32&)IOBufferSize, GEngineIni))
|
|
{
|
|
UE_LOG(LogBuildPatchServices, Verbose, TEXT("Got INI ConstructorIOBufferSizeMB = %d"), IOBufferSize);
|
|
}
|
|
if (Configuration.IOBufferSizeMB.IsSet())
|
|
{
|
|
IOBufferSize = FMath::Max(1, Configuration.IOBufferSizeMB.Get(IOBufferSize));
|
|
UE_LOG(LogBuildPatchServices, Verbose, TEXT("Got override ConstructorIOBufferSizeMB = %d"), IOBufferSize);
|
|
}
|
|
IOBufferSize <<= 20; // to MB;
|
|
|
|
// This is the buffer we issue reads into and writes out of. We segment it up
|
|
// based on batch sizing.
|
|
TArray<uint8> IOBuffer;
|
|
|
|
// Ensure that our batch size can always make progress (all chunks can fit)
|
|
if (!Configuration.bInstallToMemory &&
|
|
!Configuration.bConstructInMemory)
|
|
{
|
|
uint32 LargestChunkSize = 0;
|
|
for (const FFileToConstruct& FileToConstruct : ConstructionList)
|
|
{
|
|
if (FileToConstruct.FileManifest)
|
|
{
|
|
for (const FChunkPart& ChunkPart : FileToConstruct.FileManifest->ChunkParts)
|
|
{
|
|
if (ChunkPart.Size > LargestChunkSize)
|
|
{
|
|
LargestChunkSize = ChunkPart.Size;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (LargestChunkSize > MaxWriteBatchSize)
|
|
{
|
|
MaxWriteBatchSize = LargestChunkSize;
|
|
UE_LOG(LogBuildPatchServices, Display, TEXT("Increasing batch size to fit any chunk size: %u bytes"), MaxWriteBatchSize);
|
|
|
|
if (MaxWriteBatchSize > IOBufferSize)
|
|
{
|
|
IOBufferSize = MaxWriteBatchSize;
|
|
UE_LOG(LogBuildPatchServices, Display, TEXT("Increasing IO buffer size to fit batch size: %u bytes"), IOBufferSize);
|
|
}
|
|
}
|
|
|
|
IOBuffer.SetNumUninitialized(IOBufferSize);
|
|
}
|
|
|
|
uint64 ConstructStartCycles = FPlatformTime::Cycles64();
|
|
uint64 WriteDoneCycles = 0;
|
|
uint64 ReadDoneCycles = 0;
|
|
uint64 ReadCheckCycles = 0;
|
|
uint64 CloudTickCycles = 0;
|
|
uint64 HashCycles = 0;
|
|
uint64 WaitCycles = 0;
|
|
uint64 WriteStartCycles = 0;
|
|
|
|
// List of files that are currently opened. The last one is the one we are starting reads on,
|
|
// the first one is the one we are currently writing.
|
|
TArray<TUniquePtr<FFileConstructionState>> ActiveFiles;
|
|
|
|
// List of batches in flight. These must be dispatched in order.
|
|
TArray<TUniquePtr<FBatchState>> Batches;
|
|
|
|
struct Range
|
|
{
|
|
uint64 Start=0, End=0;
|
|
};
|
|
TArray<Range, TInlineAllocator<3>> IOBufferFreeBlockList;
|
|
IOBufferFreeBlockList.Add({0, (uint64)IOBuffer.Num()});
|
|
|
|
auto SortAndCoalesceFreeList = [](TArray<Range, TInlineAllocator<3>>& FreeList)
|
|
{
|
|
// Sort the list on Start.
|
|
Algo::SortBy(FreeList, &Range::Start, TLess<uint64>());
|
|
|
|
// go over everything and coalesce anything that's adjacent.
|
|
for (int32 i=0; i<FreeList.Num() - 1; i++)
|
|
{
|
|
if (FreeList[i].End == FreeList[i+1].Start)
|
|
{
|
|
// Extend us.
|
|
FreeList[i].End = FreeList[i+1].End;
|
|
|
|
// Remove them.
|
|
FreeList.RemoveAt(i+1);
|
|
|
|
// Recheck us.
|
|
i--;
|
|
}
|
|
}
|
|
};
|
|
|
|
//
|
|
// The abort handling with this loop is:
|
|
// During the loop, if we see the abort signal, we mark all active files as failed.
|
|
// That prevents any new work from starting, but we have to have the outstanding work
|
|
// complete in order to be thread safe. Once that work completes, we then break out of the loop
|
|
// (bDoneWithFiles will still be true).
|
|
//
|
|
bool bStillProcessingFiles = true;
|
|
for (;bStillProcessingFiles;)
|
|
{
|
|
bool bHasAnyFileFailed = false;
|
|
// Check state. We can continue to run this after a failure has been encountered in order
|
|
// to drain any async tasks so we know we can shut down safely.
|
|
{
|
|
// Is our next write done?
|
|
if (Batches.Num() &&
|
|
((Batches[0]->bIsWriting && Batches[0]->bIsFinished.load(std::memory_order_acquire)) || Batches[0]->bIsEmptyFileSentinel))
|
|
{
|
|
uint64 WriteDoneStartCycles = FPlatformTime::Cycles64();
|
|
|
|
FFileConstructionState* File = Batches[0]->OwningFile;
|
|
File->OutstandingBatches--;
|
|
|
|
if (!Configuration.bInstallToMemory &&
|
|
!Configuration.bConstructInMemory)
|
|
{
|
|
// Could be empty buffer if sentinel
|
|
if (Batches[0]->BatchBuffer.GetSize())
|
|
{
|
|
uint64 BufferBase = (uint64)((uint8*)Batches[0]->BatchBuffer.GetData() - (uint8*)IOBuffer.GetData());
|
|
|
|
IOBufferFreeBlockList.Add({BufferBase, BufferBase + Batches[0]->BatchBuffer.GetSize()});
|
|
SortAndCoalesceFreeList(IOBufferFreeBlockList);
|
|
}
|
|
|
|
if (File->NewFile->IsError())
|
|
{
|
|
File->bSuccess = false;
|
|
|
|
// SerializeError signals the error management code to check for low disk space
|
|
// since we currently have no way of routing out of disk errors from the UE low level
|
|
// writing code.
|
|
File->ConstructionError = EConstructionError::SerializeError;
|
|
}
|
|
}
|
|
|
|
Batches.RemoveAt(0);
|
|
|
|
// If we have nothing further to read and all the file's reads are done,
|
|
// since we only ever have 1 write batch we know it must be done.
|
|
if (File->NextChunkPartToRead == File->FileManifest->ChunkParts.Num() &&
|
|
File->OutstandingBatches == 0)
|
|
{
|
|
CompleteConstructedFile(*File);
|
|
|
|
UE_LOG(LogBuildPatchServices, Verbose, TEXT("Completed file: %s, New ActiveFileCount = %d"), *File->BuildFilename, ActiveFiles.Num() - 1);
|
|
|
|
// Since we write in order, we must be the first in the queue.
|
|
check (ActiveFiles[0].Get() == File);
|
|
|
|
// This frees the file
|
|
ActiveFiles.RemoveAt(0);
|
|
|
|
// We completed the previous file, if there's another active file we need to tell
|
|
// the stats that it has started. If there isn't, we'll fire it off when we start it.
|
|
if (ActiveFiles.Num())
|
|
{
|
|
FileConstructorStat->OnFileStarted(ActiveFiles[0]->BuildFilename, ActiveFiles[0]->FileManifest->FileSize);
|
|
}
|
|
}
|
|
|
|
WriteDoneCycles += FPlatformTime::Cycles64() - WriteDoneStartCycles;
|
|
}
|
|
|
|
// Check for completed reads.
|
|
if (Batches.Num())
|
|
{
|
|
uint64 ReadDoneStartCycles = FPlatformTime::Cycles64();
|
|
|
|
// We have to retire reads in order because later reads could be waiting for data to
|
|
// get placed correctly by the earlier one. e.g. read 1 could be placing in the memory store
|
|
// and read 2 could be wanting to read from that when it retires.
|
|
// Since we queue the batches in order, we can look at the first reading batch and know it's the next one.
|
|
for (TUniquePtr<FBatchState>& Batch : Batches)
|
|
{
|
|
if (Batch->bIsReading)
|
|
{
|
|
if (Batch->bIsFinished.load(std::memory_order_acquire))
|
|
{
|
|
FFileConstructionState* File = Batch->OwningFile;
|
|
|
|
CompleteReadBatch(*File->FileManifest, *Batch.Get());
|
|
Batch->bIsReading = false;
|
|
Batch->bNeedsWrite = true;
|
|
|
|
// Only update the file construction error if it's the first one
|
|
// since errors can cascade and make it not clear what the original problem was.
|
|
if (File->ConstructionError == EConstructionError::None &&
|
|
Batch->Error != EConstructionError::None)
|
|
{
|
|
File->ErrorContextGuid = Batch->ErrorContextGuid;
|
|
File->ConstructionError = Batch->Error;
|
|
File->bSuccess = false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// We have to retire reads in order, so once we hit one that isn't finished we stop.
|
|
break;
|
|
}
|
|
}
|
|
// else - might be a writing batch before the first read.
|
|
}
|
|
|
|
ReadDoneCycles += FPlatformTime::Cycles64() - ReadDoneStartCycles;
|
|
}
|
|
|
|
// If we've aborted, fail the files.
|
|
for (TUniquePtr<FFileConstructionState>& File : ActiveFiles)
|
|
{
|
|
if (File->bSuccess &&
|
|
bShouldAbort)
|
|
{
|
|
File->ConstructionError = EConstructionError::Aborted;
|
|
File->bSuccess = false;
|
|
}
|
|
|
|
bHasAnyFileFailed |= !File->bSuccess;
|
|
}
|
|
|
|
// If we are in a failure case, we want to clear out anything happening with the cloud source.
|
|
// We'll hit this over and over as we "drain", but we need to do that anyway because further failures
|
|
// might try to queue more cloud reads.
|
|
if (bHasAnyFileFailed)
|
|
{
|
|
CloudSource->Abort();
|
|
}
|
|
|
|
if (!bIsPaused && !bHasAnyFileFailed)
|
|
{
|
|
// Can we start another read?
|
|
// LONGTERM - if we are memory constrained and have an install source we can consider only dispatching
|
|
// single reads. This dramatically lowers our memory requirements as install sources often use a lot
|
|
// of small pieces of chunks - and we have to load the entire chunk into memory.
|
|
bool bCheckForAnotherRead = true;
|
|
while (bCheckForAnotherRead)
|
|
{
|
|
double ReadCheckStartCycles = FPlatformTime::Cycles64();
|
|
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(CFFC_ReadCheck);
|
|
|
|
// We only check for another if we got one queued so we fill up the buffer space asap.
|
|
bCheckForAnotherRead = false;
|
|
|
|
bool bNeedsIoBufferSpace = !Configuration.bInstallToMemory && !Configuration.bConstructInMemory;
|
|
|
|
int32 BiggestFreeBlockSize = 0;
|
|
int32 BiggestFreeBlockSlotIndex = 0;
|
|
if (bNeedsIoBufferSpace)
|
|
{
|
|
for (int32 Slot = 0; Slot < IOBufferFreeBlockList.Num(); Slot++)
|
|
{
|
|
if (IOBufferFreeBlockList[Slot].End - IOBufferFreeBlockList[Slot].Start > BiggestFreeBlockSize)
|
|
{
|
|
BiggestFreeBlockSize = IOBufferFreeBlockList[Slot].End - IOBufferFreeBlockList[Slot].Start;
|
|
BiggestFreeBlockSlotIndex = Slot;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!bNeedsIoBufferSpace || BiggestFreeBlockSize > 0)
|
|
{
|
|
// Default to continue to work on the last file.
|
|
FFileConstructionState* FileToStart = nullptr;
|
|
int32 ActiveFileCount = ActiveFiles.Num();
|
|
if (ActiveFileCount && ActiveFiles[ActiveFileCount-1]->NextChunkPartToRead < ActiveFiles[ActiveFileCount-1]->FileManifest->ChunkParts.Num())
|
|
{
|
|
FileToStart = ActiveFiles[ActiveFileCount-1].Get();
|
|
}
|
|
|
|
// No more work do to on active files - is there another to start?
|
|
if (!FileToStart)
|
|
{
|
|
// This is not a race because the only place we ever increment is this thread, this function right
|
|
// below.
|
|
int32 IndexToConstruct = NextIndexToConstruct.load(std::memory_order_acquire);
|
|
bool bAnotherFileExists = IndexToConstruct < ConstructionList.Num();
|
|
|
|
bool bAllowAnotherFile = ActiveFiles.Num() == 0;
|
|
if (!bAllowAnotherFile && bAnotherFileExists)
|
|
{
|
|
if (bAllowMultipleFilesInFlight)
|
|
{
|
|
bAllowAnotherFile = true;
|
|
|
|
// The IOBuffer size (i.e. memory) is what limits how many files can be in flight.
|
|
// For normal operation, this happens implicitly below when we look for a read destination,
|
|
// but for situations where we are writing directly to the destination, we need to explictly
|
|
// govern this.
|
|
if (Configuration.bInstallToMemory || Configuration.bConstructInMemory)
|
|
{
|
|
// For install to memory, we are going to eventually have anything in memory _anyway_,
|
|
// but this also limits how many requests get queued up so we still limit.
|
|
// For construct in memory, if we don't limit here it'll freely allocate the entire
|
|
// installation when we almost certainly don't want that.
|
|
//
|
|
// Note that the IOBuffer isn't actually getting allocated in these situations - but
|
|
// we can use the user provided limitation as a limit on how much to have in flight.
|
|
uint64 TotalActiveData = 0;
|
|
for (const TUniquePtr<FFileConstructionState>& ActiveFile : ActiveFiles)
|
|
{
|
|
const TArray64<uint8>& Data = MemoryOutputFiles[ActiveFile->BuildFilename];
|
|
TotalActiveData += Data.Num();
|
|
}
|
|
|
|
// Be sure to include the size of the next file. We don't get here at all
|
|
// if there are no active files.
|
|
uint64 NeededData = ConstructionList[IndexToConstruct].FileManifest->FileSize;
|
|
|
|
bAllowAnotherFile = (TotalActiveData + NeededData) < IOBufferSize;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
if (bAllowAnotherFile && bAnotherFileExists)
|
|
{
|
|
// Even though another file exists, we might not be able to start it if its dependent
|
|
// files aren't done.
|
|
bool bDelayForDependencies = false;
|
|
if (ConstructionList[IndexToConstruct].LatestDependentInstallSource != -1)
|
|
{
|
|
// Files are in construct order - if the first one is after our last dependency then we know we
|
|
// are safe.
|
|
if (ActiveFileCount && ActiveFiles[0]->ConstructionIndex <= ConstructionList[IndexToConstruct].LatestDependentInstallSource)
|
|
{
|
|
bDelayForDependencies = true;
|
|
}
|
|
}
|
|
|
|
if (!bDelayForDependencies)
|
|
{
|
|
// Now commit this file since we are starting it.
|
|
NextIndexToConstruct.fetch_add(1, std::memory_order_acq_rel);
|
|
|
|
FString StagingFileName = Configuration.StagingDirectory / Configuration.ConstructList[IndexToConstruct];
|
|
TUniquePtr<FFileConstructionState> AnotherFile = MakeUnique<FFileConstructionState>(
|
|
ConstructionList[IndexToConstruct].FileManifest,
|
|
Configuration.ConstructList[IndexToConstruct],
|
|
MoveTemp(StagingFileName));
|
|
|
|
AnotherFile->BaseReferenceIndex = 0;
|
|
if (IndexToConstruct)
|
|
{
|
|
AnotherFile->BaseReferenceIndex = FileCompletionPositions[IndexToConstruct-1];
|
|
}
|
|
|
|
AnotherFile->ConstructionIndex = IndexToConstruct;
|
|
|
|
TRACE_BOOKMARK(TEXT("Starting File: %s [%u]"), *AnotherFile->BuildFilename, AnotherFile->FileManifest->ChunkParts.Num());
|
|
|
|
UE_LOG(LogBuildPatchServices, Verbose, TEXT("Starting File: %s [%s bytes, %d chunks], New ActiveFileCount = %d"),
|
|
*AnotherFile->BuildFilename,
|
|
*FormatNumber(AnotherFile->FileManifest->FileSize),
|
|
AnotherFile->FileManifest->ChunkParts.Num(),
|
|
ActiveFiles.Num() + 1);
|
|
|
|
StartFile(*AnotherFile.Get(), ResumeData);
|
|
|
|
if (AnotherFile->bSkippedConstruction)
|
|
{
|
|
// Nothing else needs to happen with this file - we'll loop around to try for another.
|
|
bCheckForAnotherRead = true;
|
|
}
|
|
else
|
|
{
|
|
// Only start the file if there's not currently an active file - otherwise we
|
|
// do it when the current one finishes.
|
|
if (ActiveFileCount == 0)
|
|
{
|
|
FileConstructorStat->OnFileStarted(AnotherFile->BuildFilename, AnotherFile->FileManifest->FileSize);
|
|
}
|
|
|
|
FileToStart = AnotherFile.Get();
|
|
ActiveFiles.Push(MoveTemp(AnotherFile));
|
|
}
|
|
} // end if not delaying for dependencies
|
|
else
|
|
{
|
|
UE_LOG(LogBuildPatchServices, Verbose, TEXT("Delaying %s due to incomplete dependencies"), *Configuration.ConstructList[IndexToConstruct]);
|
|
}
|
|
|
|
// If we are delaying for dependencies we fall through here with FileToStart = nullptr
|
|
// and nothing happens until we check again when the construct thread is woken up again.
|
|
}
|
|
else
|
|
{
|
|
// If we don't have any files to construct, then we can't start any more reads at all.
|
|
// If there are no active files, then we are done.
|
|
if (ActiveFiles.Num() == 0)
|
|
{
|
|
bStillProcessingFiles = false;
|
|
}
|
|
}
|
|
} // end if we are starting a new file.
|
|
|
|
// It's possible the file failed during creation and we need to start the failure process. We need to
|
|
// set this since we did the scan before here.
|
|
if (FileToStart && !FileToStart->bSuccess)
|
|
{
|
|
bHasAnyFileFailed = true;
|
|
}
|
|
|
|
if (FileToStart && FileToStart->bSuccess)
|
|
{
|
|
if (FileToStart->FileManifest->ChunkParts.Num() == 0)
|
|
{
|
|
// We have a file that will never launch any batches, which means it'll never hit the finalization
|
|
// logic. We can't complete it here because then we are out of order. So we need to inject a placeholder
|
|
// batch that will auto pass the write check and also prevent us from sleeping on an event.
|
|
TUniquePtr<FBatchState> Batch = MakeUnique<FBatchState>();
|
|
Batch->bNeedsWrite = false;
|
|
Batch->bIsReading = false;
|
|
Batch->OwningFile = FileToStart;
|
|
Batch->bIsEmptyFileSentinel = true;
|
|
Batch->bIsFinished.store(false, std::memory_order_release);
|
|
FileToStart->OutstandingBatches++;
|
|
|
|
Batches.Add(MoveTemp(Batch));
|
|
|
|
bCheckForAnotherRead = true;
|
|
}
|
|
else if (Configuration.bInstallToMemory || Configuration.bConstructInMemory)
|
|
{
|
|
// When installing to memory we can start a batch for the entirety of the file.
|
|
TArray64<uint8>& Output = MemoryOutputFiles[FileToStart->BuildFilename];
|
|
|
|
// \todo break up the batches a bit so that the hash overlaps with other
|
|
// work. Right now its a giant hit on the constructor thread.
|
|
|
|
TUniquePtr<FBatchState> Batch = MakeUnique<FBatchState>();
|
|
Batch->bNeedsWrite = true;
|
|
Batch->bIsReading = true;
|
|
Batch->OwningFile = FileToStart;
|
|
Batch->bIsFinished.store(false, std::memory_order_release);
|
|
|
|
Batch->BatchBuffer = FMutableMemoryView(Output.GetData(), FileToStart->FileManifest->FileSize);
|
|
|
|
StartReadBatch(*FileToStart, *Batch.Get());
|
|
|
|
// This should use the entire buffer. If it doesn't we've violated some assumptions here.
|
|
if (Batch->BatchBuffer.GetSize() != FileToStart->FileManifest->FileSize)
|
|
{
|
|
FileToStart->bSuccess = false;
|
|
FileToStart->ConstructionError = EConstructionError::InternalConsistencyError;
|
|
bHasAnyFileFailed = true;
|
|
|
|
UE_LOG(LogBuildPatchServices, Error, TEXT("Memory construction setup failed - batch buffer didn't fill entirely: Expected %llu, got %llu. File %s"),
|
|
FileToStart->FileManifest->FileSize,
|
|
Batch->BatchBuffer.GetSize(),
|
|
*FileToStart->BuildFilename);
|
|
}
|
|
else
|
|
{
|
|
bCheckForAnotherRead = true;
|
|
Batches.Push(MoveTemp(Batch));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
uint32 MaxBufferSize = BiggestFreeBlockSize;
|
|
if (MaxBufferSize > MaxWriteBatchSize)
|
|
{
|
|
MaxBufferSize = MaxWriteBatchSize;
|
|
}
|
|
|
|
// We want to do big batches as much as possible. If we only have space for a single chunk,
|
|
// that's fine if it's a single chunk file, but generally we want to try and favor large batches.
|
|
// If we're at the point where we are worried about this, we have enough outstanding work to keep
|
|
// the pipelines full so we can afford to wait for room.
|
|
uint32 MaxFileBatchSize = 0;
|
|
const TArray<FChunkPart>& FileChunkParts = FileToStart->FileManifest->ChunkParts;
|
|
uint32 NextChunkSize = FileChunkParts[FileToStart->NextChunkPartToRead].Size;
|
|
|
|
for (int32 ChunkPartIndex = FileToStart->NextChunkPartToRead;
|
|
ChunkPartIndex < FileChunkParts.Num();
|
|
ChunkPartIndex++)
|
|
{
|
|
MaxFileBatchSize += FileChunkParts[ChunkPartIndex].Size;
|
|
if (MaxFileBatchSize >= MaxWriteBatchSize)
|
|
{
|
|
// If it's big enough for the max batch size we no longer care.
|
|
MaxFileBatchSize = MaxWriteBatchSize;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// If the file can support a large batch, we want to wait until we have room for a reasonable size.
|
|
// We know we can eventually read the next chunk because during init we sized the buffers such that
|
|
// we could.
|
|
if (NextChunkSize > MaxBufferSize &&
|
|
Batches.Num() == 0)
|
|
{
|
|
FileToStart->bSuccess = false;
|
|
FileToStart->ConstructionError = EConstructionError::InternalConsistencyError;
|
|
UE_LOG(LogBuildPatchServices, Error, TEXT("Chunk size encountered larger than batch buffer size! %u vs %u"), NextChunkSize, MaxBufferSize);
|
|
bHasAnyFileFailed = true;
|
|
|
|
// We'll fail the next conditional below and then start the failure process on the next loop.
|
|
}
|
|
|
|
if (MaxBufferSize >= MaxFileBatchSize)
|
|
{
|
|
TUniquePtr<FBatchState> Batch = MakeUnique<FBatchState>();
|
|
Batch->bNeedsWrite = true;
|
|
Batch->bIsReading = true;
|
|
Batch->OwningFile = FileToStart;
|
|
Batch->BatchBuffer = FMutableMemoryView(IOBuffer.GetData() + IOBufferFreeBlockList[BiggestFreeBlockSlotIndex].Start, MaxBufferSize);
|
|
Batch->bIsFinished.store(false, std::memory_order_release);
|
|
StartReadBatch(*FileToStart, *Batch.Get());
|
|
|
|
// The read might not have used the whole thing, so only consume as much
|
|
// as it needed.
|
|
IOBufferFreeBlockList[BiggestFreeBlockSlotIndex].Start += Batch->BatchBuffer.GetSize();
|
|
if (IOBufferFreeBlockList[BiggestFreeBlockSlotIndex].Start == IOBufferFreeBlockList[BiggestFreeBlockSlotIndex].End)
|
|
{
|
|
// Ate whole thing
|
|
IOBufferFreeBlockList.RemoveAt(BiggestFreeBlockSlotIndex);
|
|
SortAndCoalesceFreeList(IOBufferFreeBlockList);
|
|
}
|
|
|
|
bCheckForAnotherRead = true;
|
|
|
|
UE_LOG(LogBuildPatchServices, VeryVerbose, TEXT("Starting ReadBatch: %d, Chunks=%d, Bytes=%s, Batches=%d"),
|
|
Batch->BatchId,
|
|
Batch->ChunkCount,
|
|
*FormatNumber(Batch->BatchBuffer.GetSize()),
|
|
Batches.Num() + 1
|
|
);
|
|
|
|
Batches.Push(MoveTemp(Batch));
|
|
}
|
|
} // end if file has parts
|
|
} // end if we have a file to read from.
|
|
} // end if free block exists.
|
|
|
|
ReadCheckCycles += FPlatformTime::Cycles64() - ReadCheckStartCycles;
|
|
} // end looping on whether we should start a read.
|
|
|
|
// Can we start a write?
|
|
// We always have to issue writes in order and there can only be one, so it must be the
|
|
// first active file, and the first batch.
|
|
if (Batches.Num() && Batches[0]->bNeedsWrite && !Batches[0]->bIsReading)
|
|
{
|
|
uint64 WriteStartStartCycles = FPlatformTime::Cycles64();
|
|
|
|
FFileConstructionState* FirstFile = ActiveFiles[0].Get();
|
|
if (Configuration.bInstallToMemory || Configuration.bConstructInMemory)
|
|
{
|
|
// We read directly into the output - nothing needs to be done. However we need to
|
|
// catch the write completion logic so we mark bIsFinished and signal our wakeup
|
|
// event so we immediately do another pass
|
|
Batches[0]->bNeedsWrite = false;
|
|
Batches[0]->bIsWriting = true;
|
|
Batches[0]->bIsFinished.store(true, std::memory_order_release);
|
|
WakeUpDispatchThreadEvent->Trigger();
|
|
}
|
|
else
|
|
{
|
|
Batches[0]->bNeedsWrite = false;
|
|
Batches[0]->bIsWriting = true;
|
|
Batches[0]->bIsFinished.store(false, std::memory_order_release);
|
|
|
|
// Launch the write and hash the buffer on this thread.
|
|
UE_LOG(LogBuildPatchServices, VeryVerbose, TEXT("Writing Batch: %d, file %s [%d - %d]"),
|
|
Batches[0]->BatchId,
|
|
*FirstFile->BuildFilename,
|
|
Batches[0]->StartChunkPartIndex,
|
|
Batches[0]->StartChunkPartIndex + Batches[0]->ChunkCount
|
|
);
|
|
|
|
IConstructorChunkSource::FRequestProcessFn WriteFn = CreateWriteRequest(FirstFile->NewFile.Get(), *Batches[0].Get());
|
|
QueueGenericThreadTask(WriteThreadIndex, MoveTemp(WriteFn));
|
|
}
|
|
|
|
uint64 HashStartCycles = FPlatformTime::Cycles64();
|
|
WriteStartCycles += HashStartCycles - WriteStartStartCycles;
|
|
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(CFFC_Hash);
|
|
FirstFile->HashState.Update((const uint8*)Batches[0]->BatchBuffer.GetData(), Batches[0]->BatchBuffer.GetSize());
|
|
}
|
|
HashCycles += FPlatformTime::Cycles64() - HashStartCycles;
|
|
|
|
} // end if checking for write
|
|
} // end not paused
|
|
} // end state check
|
|
|
|
// If the file progress changed since we last saw it, post the update.
|
|
// Note that we want this to update reasonably often but we're about to wait
|
|
// potentially until all reads complete - however the only time things actually
|
|
// take a long time wall-clock wise is when we are downloading, and the cloud
|
|
// source will then prevent us from sleeping too long, so we actually catch these
|
|
// updates.
|
|
// We do this from here to ensure we always increase rather than risk multi thread
|
|
// races.
|
|
if (ActiveFiles.Num())
|
|
{
|
|
// We only post the progress for the first file in the active list - this means
|
|
// that when we finish that file we'll likely jump to the middle progress for the next
|
|
// file, but we don't have a way to post the progress per file.
|
|
FFileConstructionState* File = ActiveFiles[0].Get();
|
|
|
|
uint64 CurrentFileProgress;
|
|
File->ProgressLock.Lock();
|
|
CurrentFileProgress = File->Progress;
|
|
File->ProgressLock.Unlock();
|
|
|
|
if (CurrentFileProgress != File->LastSeenProgress)
|
|
{
|
|
// this updates the overall install progress.
|
|
CountBytesProcessed(CurrentFileProgress - File->LastSeenProgress);
|
|
File->LastSeenProgress = CurrentFileProgress;
|
|
FileConstructorStat->OnFileProgress(File->BuildFilename, CurrentFileProgress);
|
|
}
|
|
}
|
|
|
|
uint32 WaitTimeMs = TNumericLimits<uint32>::Max();
|
|
{
|
|
uint64 CloudTickStartCycles = FPlatformTime::Cycles64();
|
|
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(CFFC_TickCloud);
|
|
|
|
// Max downloads is tricky - the internet makes no guarantees about which of our downloads finishes first. So while
|
|
// we want to have as many outstanding as possible to cover up connection overhead / resends and all that, if we enqueue
|
|
// downloads from several batches, we can end up where the first batch can't complete because it's waiting on a download
|
|
// that isn't finishing due to congestion from the next batch's download. Then we counterintuitively end up with FEWER
|
|
// outstanding downloads because we can't launch more batches due to waiting on the front of this long chain. This is easily reproducible
|
|
// where Insights will show highly out of order completion. This ordering unfortunately scales with the number of
|
|
// outstanding downloads to a certain extent: if you allow 16 downloads then you'll end up waiting on a download
|
|
// 16 issues old - 32 downloads and you'll wait on one 32 issues old. We try to bound this by capping the issued
|
|
// downloads here, and in the cloud source we prevent queues if we get too far ahead of the last download.
|
|
// Note this also gets adjusted by the connection health stuff internal to the cloud source.
|
|
uint32 MaxDownloads = FMath::Max(1U, FMath::DivideAndRoundUp(MaxWriteBatchSize, ExpectedChunkSize));
|
|
|
|
// If we are reading directly, then we don't have to worry about individual batches anymore - but we don't want to wait
|
|
// too long at the end of each file either. We expose IoBuffer as a conceptual limit to how much the user wants going
|
|
// on in these cases, so we use it here as well.
|
|
if (Configuration.bInstallToMemory || Configuration.bConstructInMemory)
|
|
{
|
|
MaxDownloads = FMath::Max(1U, FMath::DivideAndRoundUp(IOBufferSize, ExpectedChunkSize));
|
|
}
|
|
|
|
|
|
// WaitTimeMs is an OUT param
|
|
CloudSource->Tick(!bIsPaused && !bHasAnyFileFailed, WaitTimeMs, MaxDownloads);
|
|
|
|
CloudTickCycles += FPlatformTime::Cycles64() - CloudTickStartCycles;
|
|
}
|
|
|
|
int32 ActiveReadBatches = 0;
|
|
int32 ActiveWriteBatches = 0;
|
|
int32 EmptyFileBatches = 0;
|
|
for (const TUniquePtr<FBatchState>& _Batch : Batches)
|
|
{
|
|
EmptyFileBatches += _Batch->bIsEmptyFileSentinel ? 1 : 0;
|
|
ActiveReadBatches += _Batch->bIsReading ? 1 : 0;
|
|
ActiveWriteBatches += _Batch->bIsWriting ? 1 : 0;
|
|
}
|
|
|
|
// Empty files don't have async jobs.
|
|
bool bAsyncJobExists = ActiveReadBatches || ActiveWriteBatches;
|
|
|
|
if (bHasAnyFileFailed)
|
|
{
|
|
// We can only bail when all our async jobs have completed.
|
|
if (!bAsyncJobExists)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
TRACE_INT_VALUE(TEXT("BPS.FC.ActiveFiles"), ActiveFiles.Num());
|
|
TRACE_INT_VALUE(TEXT("BPS.FC.ActiveReadBatches"), ActiveReadBatches);
|
|
|
|
if (bStillProcessingFiles &&
|
|
bAsyncJobExists)
|
|
{
|
|
if (WaitTimeMs == TNumericLimits<uint32>::Max())
|
|
{
|
|
WaitTimeMs = 15*1000;
|
|
}
|
|
|
|
uint64 WaitStartCycles = FPlatformTime::Cycles64();
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(CFFC_Waiting);
|
|
// We have a bunch of stuff outstanding that will wake us up if something happens.
|
|
WakeUpDispatchThreadEvent->Wait(WaitTimeMs);
|
|
|
|
WaitCycles += FPlatformTime::Cycles64() - WaitStartCycles;
|
|
}
|
|
} // end loop until we complete all the files.
|
|
|
|
// Any remaining active files (due to abort/failure) need to be failed and completed
|
|
// so that errors get reported. We want to report the non-abort failures first, because
|
|
// anything else that was in flight gets reported as an abort and the first error is the
|
|
// one we actually care about.
|
|
if (ActiveFiles.Num())
|
|
{
|
|
for (TUniquePtr<FFileConstructionState>& File : ActiveFiles)
|
|
{
|
|
if (!File->bSuccess && File->ConstructionError != EConstructionError::Aborted)
|
|
{
|
|
CompleteConstructedFile(*File.Get());
|
|
}
|
|
}
|
|
|
|
// Now handle everything else.
|
|
for (TUniquePtr<FFileConstructionState>& File : ActiveFiles)
|
|
{
|
|
if (!File->bSuccess && File->ConstructionError != EConstructionError::Aborted)
|
|
{
|
|
continue; // handled in previous loop.
|
|
}
|
|
|
|
if (File->bSuccess)
|
|
{
|
|
File->bSuccess = false;
|
|
File->ConstructionError = EConstructionError::Aborted;
|
|
}
|
|
CompleteConstructedFile(*File.Get());
|
|
}
|
|
}
|
|
|
|
uint64 ConstructCycles = FPlatformTime::Cycles64() - ConstructStartCycles;
|
|
uint64 UnaccountedForCycles = ConstructCycles - HashCycles - WaitCycles - ReadCheckCycles - ReadDoneCycles - WriteStartCycles - WriteDoneCycles - CloudTickCycles;
|
|
|
|
double ConstructSec = FPlatformTime::ToSeconds64(ConstructCycles);
|
|
UE_LOG(LogBuildPatchServices, Display, TEXT("Construction done: %.2f sec. Hash %.1f%% Wait %.1f%% ReadCheck %.1f%% WriteStart %.1f%% ReadDone %.1f%% WriteDone %.1f%% CloudTick %.1f%% Unaccounted %.1f%%"),
|
|
ConstructSec,
|
|
100.0 * HashCycles / ConstructCycles,
|
|
100.0 * WaitCycles / ConstructCycles,
|
|
100.0 * ReadCheckCycles / ConstructCycles,
|
|
100.0 * WriteStartCycles / ConstructCycles,
|
|
100.0 * ReadDoneCycles / ConstructCycles,
|
|
100.0 * WriteDoneCycles / ConstructCycles,
|
|
100.0 * CloudTickCycles / ConstructCycles,
|
|
100.0 * UnaccountedForCycles / ConstructCycles
|
|
);
|
|
}
|