Files
UnrealEngine/Engine/Source/Runtime/Experimental/IoStore/OnDemand/Private/OnDemandContentInstaller.cpp
Brandyn / Techy fcc1b09210 init
2026-04-04 15:40:51 -05:00

1729 lines
53 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "OnDemandContentInstaller.h"
#include "Algo/Accumulate.h"
#include "Async/UniqueLock.h"
#include "Containers/RingBuffer.h"
#include "Experimental/UnifiedError/CoreErrorTypes.h"
#include "HAL/Platform.h"
#include "IO/IoBuffer.h"
#include "IO/IoChunkId.h"
#include "IO/IoDispatcher.h"
#include "IO/IoStatus.h"
#include "IO/IoStoreOnDemand.h"
#include "IO/OnDemandError.h"
#include "Misc/Timespan.h"
#include "OnDemandHttpThread.h"
#include "OnDemandInstallCache.h"
#include "OnDemandIoStore.h"
#include "OnDemandPackageStoreBackend.h"
#include "Serialization/PackageStore.h"
#include "Statistics.h"
#include "Templates/Overload.h"
#include <atomic>
namespace UE::IoStore
{
namespace CVars
{
#if !UE_BUILD_SHIPPING
static FString IoStoreErrorOnRequest = "";
static FAutoConsoleVariableRef CVar_IoStoreErrorOnRequest(
TEXT("iostore.ErrorOnRequest"),
IoStoreErrorOnRequest,
TEXT("When the request with a debug name partially matching this cvar is found iostore will error with a random error.")
);
FString IoStoreDebugRequest = "";
static FAutoConsoleVariableRef CVar_IoStoreDebugRequest(
TEXT("iostore.DebugRequest"),
IoStoreDebugRequest,
TEXT("When the request with a debug name partially matching this cvar is found iostore will error with a random error.")
);
#endif
static bool bContentInstallerFlushOnIdle = false;
static FAutoConsoleVariableRef CVar_ContentInstallerFlushOnIdle(
TEXT("iostore.ContentInstallerFlushOnIdle"),
bContentInstallerFlushOnIdle,
TEXT("When the content installer runs out of tasks, it will flush any pending iochunks to disk.")
);
static bool bContentInstallerEnableHttpCancel = true;
static FAutoConsoleVariableRef CVar_ContentInstallerEnableHttpCancel(
TEXT("iostore.ContentInstallerEnableHttpCancel"),
bContentInstallerEnableHttpCancel,
TEXT("Enable canceling HTTP requests when an install request is canceled")
);
// Can't safely change this on the fly. Don't call directly
static TAutoConsoleVariable<bool> CVar_EnableConcurrentRequests(
TEXT("iostore.ContentInstallerEnableConcurrentInstallRequests"),
false,
TEXT("Enable the content installer to install chunks for multiple requests concurrently."),
ECVF_SaveForNextBoot
);
static bool EnableConcurentInstallRequests()
{
static bool bEnableConcurentInstallRequests = CVar_EnableConcurrentRequests.GetValueOnAnyThread();
return bEnableConcurentInstallRequests;
}
}
namespace Private
{
////////////////////////////////////////////////////////////////////////////////
int32 SecondsToMillicseconds(double Seconds)
{
return Seconds > 0.0 ? int32(Seconds * 1000.0) : 0;
}
////////////////////////////////////////////////////////////////////////////////
void ResolvePackageDependencies(
const TSet<FPackageId>& PackageIds,
bool bIncludeSoftReferences,
TSet<FPackageId>& OutResolved,
TSet<FPackageId>& OutMissing)
{
TRACE_CPUPROFILER_EVENT_SCOPE(FOnDemandContentInstaller::ResolvePackageDependencies);
TRingBuffer<FPackageId> Queue;
TSet<FPackageId> Visitied;
Visitied.Reserve(PackageIds.Num());
Queue.Reserve(PackageIds.Num());
for (const FPackageId& PackageId : PackageIds)
{
Queue.Add(PackageId);
}
FPackageStore& PackageStore = FPackageStore::Get();
FPackageStoreReadScope _(PackageStore);
while (!Queue.IsEmpty())
{
FPackageId PackageId = Queue.PopFrontValue();
{
FName SourcePackageName;
FPackageId RedirectedToPackageId;
if (PackageStore.GetPackageRedirectInfo(PackageId, SourcePackageName, RedirectedToPackageId))
{
PackageId = RedirectedToPackageId;
}
}
bool bIsAlreadyInSet = false;
Visitied.Add(PackageId, &bIsAlreadyInSet);
if (bIsAlreadyInSet)
{
continue;
}
FPackageStoreEntry PackageStoreEntry;
const EPackageStoreEntryStatus EntryStatus = PackageStore.GetPackageStoreEntry(PackageId, NAME_None, PackageStoreEntry);
if (EntryStatus != EPackageStoreEntryStatus::Missing)
{
OutResolved.Add(PackageId);
for (const FPackageId& ImportedPackageId : PackageStoreEntry.ImportedPackageIds)
{
if (!Visitied.Contains(ImportedPackageId))
{
Queue.Add(ImportedPackageId);
}
}
if (bIncludeSoftReferences)
{
TConstArrayView<FPackageId> SoftReferences;
TConstArrayView<uint32> Indices = PackageStore.GetSoftReferences(PackageId, SoftReferences);
for (uint32 Idx : Indices)
{
const FPackageId& SoftRef = SoftReferences[Idx];
if (!Visitied.Contains(SoftRef))
{
Queue.Add(SoftRef);
}
}
}
}
else
{
OutMissing.Add(PackageId);
}
}
}
////////////////////////////////////////////////////////////////////////////////
void ResolveChunksToInstall(
const TSet<FSharedOnDemandContainer>& Containers,
const TSet<FPackageId>& PackageIds,
bool bIncludeSoftReferences,
bool bIncludeOptionalBulkData,
TArray<FResolvedContainerChunks>& OutResolvedContainerChunks,
TSet<FPackageId>& OutMissing)
{
TRACE_CPUPROFILER_EVENT_SCOPE(FOnDemandContentInstaller::ResolveChunksToInstall);
// For now we always download these required chunks
for (const FSharedOnDemandContainer& Container : Containers)
{
FResolvedContainerChunks& ResolvedChunks = OutResolvedContainerChunks.AddDefaulted_GetRef();
ResolvedChunks.Container = Container;
for (int32 EntryIndex = 0; const FIoChunkId& ChunkId : Container->ChunkIds)
{
switch(ChunkId.GetChunkType())
{
case EIoChunkType::ExternalFile:
case EIoChunkType::ShaderCodeLibrary:
case EIoChunkType::ShaderCode:
{
ResolvedChunks.EntryIndices.Emplace(EntryIndex);
ResolvedChunks.TotalSize += Container->ChunkEntries[EntryIndex].GetDiskSize();
}
default:
break;
}
++EntryIndex;
}
}
auto FindChunkEntry = [&OutResolvedContainerChunks](const FIoChunkId& ChunkId, int32& OutIndex) -> FResolvedContainerChunks*
{
for (FResolvedContainerChunks& ContainerChunks : OutResolvedContainerChunks)
{
if (OutIndex = ContainerChunks.Container->FindChunkEntryIndex(ChunkId); OutIndex != INDEX_NONE)
{
return &ContainerChunks;
}
}
return nullptr;
};
TSet<FPackageId> ResolvedPackageIds;
ResolvePackageDependencies(
PackageIds,
bIncludeSoftReferences,
ResolvedPackageIds,
OutMissing);
// Resolve all chunk entries from the resolved package ID's
for (const FPackageId& PackageId : ResolvedPackageIds)
{
const FIoChunkId PackageChunkId = CreatePackageDataChunkId(PackageId);
int32 EntryIndex = INDEX_NONE;
FResolvedContainerChunks* ResolvedChunks = FindChunkEntry(PackageChunkId, EntryIndex);
if (ResolvedChunks == nullptr)
{
// The chunk resides in a base game container
continue;
}
check(EntryIndex != INDEX_NONE);
FOnDemandContainer& Container = *ResolvedChunks->Container;
ResolvedChunks->EntryIndices.Emplace(EntryIndex);
ResolvedChunks->TotalSize += Container.ChunkEntries[EntryIndex].GetDiskSize();
static constexpr const EIoChunkType RequiredChunkTypes[] =
{
EIoChunkType::BulkData,
EIoChunkType::MemoryMappedBulkData
};
static constexpr const EIoChunkType RequiredAndOptionalChunkTypes[] =
{
EIoChunkType::BulkData,
EIoChunkType::OptionalBulkData,
EIoChunkType::MemoryMappedBulkData
};
TConstArrayView<EIoChunkType> AdditionalChunkTypes = bIncludeOptionalBulkData
? MakeArrayView<const EIoChunkType>(RequiredAndOptionalChunkTypes, UE_ARRAY_COUNT(RequiredAndOptionalChunkTypes))
: MakeArrayView<const EIoChunkType>(RequiredChunkTypes, UE_ARRAY_COUNT(RequiredChunkTypes));
for (EIoChunkType ChunkType : AdditionalChunkTypes)
{
// TODO: For Mutable we need to traverse all possible bulk data chunk indices?
const FIoChunkId ChunkId = CreateBulkDataIoChunkId(PackageId.Value(), 0, 0, ChunkType);
if (ResolvedChunks = FindChunkEntry(ChunkId, EntryIndex); ResolvedChunks != nullptr)
{
check(EntryIndex != INDEX_NONE);
FOnDemandContainer& OtherContainer = *ResolvedChunks->Container;
ResolvedChunks->EntryIndices.Emplace(EntryIndex);
ResolvedChunks->TotalSize += OtherContainer.ChunkEntries[EntryIndex].GetDiskSize();
}
}
}
}
} // namespace Private
////////////////////////////////////////////////////////////////////////////////
uint32 FOnDemandContentInstaller::FRequest::NextSeqNo = 0;
TOptional<UE::UnifiedError::FError> FOnDemandContentInstaller::FRequest::ConsumeError()
{
if (Result.HasError())
{
return TOptional<UE::UnifiedError::FError>(Result.StealError());
}
else if (IsCancelled())
{
return TOptional<UE::UnifiedError::FError>(UE::UnifiedError::Core::CancellationError::MakeError());
}
return TOptional<UE::UnifiedError::FError>();
}
////////////////////////////////////////////////////////////////////////////////
FOnDemandContentInstaller::FOnDemandContentInstaller(FOnDemandIoStore& InIoStore, FOnDemandHttpThread* InHttpClient)
: IoStore(InIoStore)
, HttpClient(InHttpClient)
, InstallerPipe(TEXT("IoStoreOnDemandInstallerPipe"))
{
}
FOnDemandContentInstaller::~FOnDemandContentInstaller()
{
Shutdown();
}
FSharedInternalInstallRequest FOnDemandContentInstaller::EnqueueInstallRequest(
FOnDemandInstallArgs&& Args,
FOnDemandInstallCompleted&& OnCompleted,
FOnDemandInstallProgressed&& OnProgress)
{
FRequest* Request = nullptr;
{
TUniqueLock Lock(Mutex);
Request = RequestAllocator.Construct(MoveTemp(Args), MoveTemp(OnCompleted), MoveTemp(OnProgress));
}
FSharedInternalInstallRequest InstallRequest = MakeShared<FOnDemandInternalInstallRequest, ESPMode::ThreadSafe>(UPTRINT(Request));
Request->AsInstall().Request = InstallRequest;
FOnDemandContentInstallerStats::OnRequestEnqueued();
InstallerPipe.Launch(
TEXT("ProcessIoStoreOnDemandInstallRequest"),
[this, Request] { ProcessInstallRequest(*Request); },
UE::Tasks::ETaskPriority::BackgroundLow);
return InstallRequest;
}
void FOnDemandContentInstaller::EnqueuePurgeRequest(FOnDemandPurgeArgs&& Args, FOnDemandPurgeCompleted&& OnCompleted)
{
{
TUniqueLock Lock(Mutex);
RequestQueue.HeapPush(RequestAllocator.Construct(MoveTemp(Args), MoveTemp(OnCompleted)), RequestSortPredicate);
}
TryExecuteNextRequest();
}
void FOnDemandContentInstaller::EnqueueDefragRequest(FOnDemandDefragArgs&& Args, FOnDemandDefragCompleted&& OnCompleted)
{
{
TUniqueLock Lock(Mutex);
RequestQueue.HeapPush(RequestAllocator.Construct(MoveTemp(Args), MoveTemp(OnCompleted)), RequestSortPredicate);
}
TryExecuteNextRequest();
}
void FOnDemandContentInstaller::EnqueueVerifyRequest(FOnDemandVerifyCacheCompleted&& OnCompleted)
{
{
TUniqueLock Lock(Mutex);
RequestQueue.HeapPush(RequestAllocator.Construct(MoveTemp(OnCompleted)), RequestSortPredicate);
}
TryExecuteNextRequest();
}
void FOnDemandContentInstaller::EnqueueFlushLastAccessRequest(FOnDemandFlushLastAccessCompleted&& OnCompleted)
{
{
TUniqueLock Lock(Mutex);
RequestQueue.HeapPush(RequestAllocator.Construct(MoveTemp(OnCompleted)), RequestSortPredicate);
}
TryExecuteNextRequest();
}
void FOnDemandContentInstaller::CancelInstallRequest(FSharedInternalInstallRequest InstallRequest)
{
InstallerPipe.Launch(
TEXT("CancelIoStoreOnDemandInstallRequest"),
[this, InstallRequest]
{
FRequest* ToComplete = nullptr;
{
if (InstallRequest->InstallerRequest == 0)
{
return;
}
FRequest* Request = reinterpret_cast<FRequest*>(InstallRequest->InstallerRequest);
FRequest::FInstall& Install = Request->AsInstall();
if (Install.bHttpRequestsIssued)
{
if (Request->TryCancel() == false)
{
return;
}
UE_LOG(LogIoStoreOnDemand, Log, TEXT("Cancelling install request, ContentHandle=(%s)"),
*LexToString(Install.Args.ContentHandle));
int32 NumCancelled = 0;
TryCancelHttpRequestsForInstallRequest(Install, NumCancelled);
UE_LOG(LogIoStoreOnDemand, Verbose, TEXT("Cancelled %d HTTP request(s) due to install request cancellation"), NumCancelled);
}
else
{
if (Request->TryCancel() == false)
{
return;
}
UE_LOG(LogIoStoreOnDemand, Log, TEXT("Cancelling install request, ContentHandle=(%s)"),
*LexToString(Install.Args.ContentHandle));
TUniqueLock Lock(Mutex);
if (RequestQueue.Remove(Request) > 0)
{
ToComplete = Request;
RequestQueue.Heapify(RequestSortPredicate);
}
}
}
if (ToComplete != nullptr)
{
CompleteInstallRequest(*ToComplete);
}
});
}
void FOnDemandContentInstaller::UpdateInstallRequestPriority(FSharedInternalInstallRequest InstallRequest, const int32 NewPriority)
{
InstallerPipe.Launch(
TEXT("UpdateIoStoreOnDemandInstallRequestPriority"),
[this, InstallRequest, NewPriority]
{
if (InstallRequest->InstallerRequest == 0)
{
return;
}
FRequest& Request = *reinterpret_cast<FRequest*>(InstallRequest->InstallerRequest);
FRequest::FInstall& Install = Request.AsInstall();
UE_LOG(LogIoStoreOnDemand, Log, TEXT("Updating install request priority, SeqNo=%u, Priority=%d, NewPriority=%d, ContentHandle=(%s)"),
Request.SeqNo, Request.Priority, NewPriority, *LexToString(Install.Args.ContentHandle));
if (Install.bHttpRequestsIssued)
{
// The request has definitely left the RequestQueue
Request.Priority = NewPriority;
for (FChunkHttpRequestHandle& PendingHttpRequest : Install.HttpRequestHandles)
{
const FSharedOnDemandContainer& Container = Install.ResolvedChunks[PendingHttpRequest.ContainerIndex].Container;
const FOnDemandChunkEntry& ChunkEntry = Container->ChunkEntries[PendingHttpRequest.EntryIndex];
const FIoHash& ChunkHash = ChunkEntry.Hash;
for (auto It = PendingChunkDownloads.CreateKeyIterator(ChunkHash); It; ++It)
{
FChunkDownloadRequest& ChunkDownload = It.Value();
if (ChunkDownload.bChunkCanceled)
{
continue;
}
// Use the maximum priority of all requests waiting on this chunk
int32 MaxPriority = std::numeric_limits<int32>::min();
for (FChunkHttpRequestHandle& Handle : ChunkDownload.ChunkRequestHttpHandles)
{
const int32 RequestPriority = Handle.OwnerRequest->Priority;
if (RequestPriority > MaxPriority)
{
MaxPriority = RequestPriority;
}
}
Visit(UE::Overload(
[MaxPriority](FIoHttpRequest& Handle)
{
Handle.UpdatePriorty(MaxPriority);
},
[this, MaxPriority](void* Handle)
{
check(HttpClient != nullptr);
if (Handle != nullptr)
{
HttpClient->ReprioritizeRequest(Handle, MaxPriority);
}
}),
ChunkDownload.HttpHandle);
// Should be a max of one active request for any chunk
break;
}
}
}
else
{
// The request may or may not still be in the RequestQueue
TUniqueLock Lock(Mutex);
Request.Priority = NewPriority;
RequestQueue.Heapify(RequestSortPredicate);
}
});
}
void FOnDemandContentInstaller::ReportAnalytics(TArray<FAnalyticsEventAttribute>& OutAnalyticsArray) const
{
FOnDemandContentInstallerStats::ReportAnalytics(OutAnalyticsArray);
}
bool FOnDemandContentInstaller::CanExecuteRequest(FRequest& Request) const
{
bool bExecute = false;
if (CVars::EnableConcurentInstallRequests())
{
// Allow multiple install requests XOR any other request
bExecute = RunningRequests.IsEmpty() ||
(
Request.IsInstall() &&
(RunningRequests.Num() > 1 || RunningRequests[0]->IsInstall())
);
}
else
{
bExecute = RunningRequests.IsEmpty();
}
return bExecute;
}
void FOnDemandContentInstaller::TryExecuteNextRequest()
{
if (bShuttingDown.load(std::memory_order_relaxed))
{
return;
}
bool bRequestQueueIsEmpty = false;
while (bRequestQueueIsEmpty == false)
{
FRequest* NextRequest = nullptr;
{
TUniqueLock Lock(Mutex);
bRequestQueueIsEmpty = RequestQueue.IsEmpty();
if (bRequestQueueIsEmpty == false)
{
if (CanExecuteRequest(*RequestQueue.HeapTop()))
{
RequestQueue.HeapPop(NextRequest, RequestSortPredicate, EAllowShrinking::No);
RunningRequests.Add(NextRequest);
}
}
}
if (NextRequest == nullptr)
{
break;
}
InstallerPipe.Launch(
TEXT("OnDemandContentInstaller::ExecuteRequest"),
[this, NextRequest] { ExecuteRequest(*NextRequest); },
UE::Tasks::ETaskPriority::BackgroundLow);
// If we aren't doing an install request, we only allow one running request so
// stop trying to dequeue requests.
if (NextRequest->IsInstall() == false)
{
break;
}
}
if (bRequestQueueIsEmpty && UE::IoStore::CVars::bContentInstallerFlushOnIdle)
{
bool bRunningRequestsIsEmpty = false;
{
TUniqueLock Lock(Mutex);
bRunningRequestsIsEmpty = RunningRequests.IsEmpty();
}
if (bRunningRequestsIsEmpty)
{
// Flush any pending chunks, but only if the installer is otherwise idle
InstallerPipe.Launch(
TEXT("OnDemandContentInstaller::Flush"),
[this]
{
bool bExecuteFlush = false;
{
TUniqueLock Lock(Mutex);
bExecuteFlush = RequestQueue.IsEmpty() && RunningRequests.IsEmpty();
}
if (bExecuteFlush)
{
FResult Result = IoStore.InstallCache->Flush();
if (Result.HasError())
{
UE_LOG(LogIoStoreOnDemand, Error, TEXT("Failed to flush pending chunks, Reason %s"), *LexToString(Result.GetError()));
}
}
},
UE::Tasks::ETaskPriority::BackgroundLow);
}
}
}
void FOnDemandContentInstaller::ExecuteRequest(FRequest& Request)
{
struct FVisitor
{
void operator()(FEmptyVariantState& Empty)
{
ensure(false);
}
void operator()(FRequest::FInstall&)
{
Installer.ExecuteInstallRequest(Request);
}
void operator()(FRequest::FPurge&)
{
Installer.ExecutePurgeRequest(Request);
}
void operator()(FRequest::FDefrag&)
{
Installer.ExecuteDefragRequest(Request);
}
void operator()(FRequest::FVerify&)
{
Installer.ExecuteVerifyRequest(Request);
}
void operator()(FRequest::FFlushLastAccess&)
{
Installer.ExectuteFlushLastAccessRequest(Request);
}
FOnDemandContentInstaller& Installer;
FRequest& Request;
};
FVisitor Visitor { .Installer = *this, .Request = Request };
Visit(Visitor, Request.Variant);
}
void FOnDemandContentInstaller::PinCachedChunks(FOnDemandContentInstaller::FRequest::FInstall& InstallRequest, TFunctionRef<void(int32, int32, bool)> OnChunkFound) const
{
const TSharedPtr<FOnDemandInternalContentHandle>& ContentHandle = InstallRequest.Args.ContentHandle.Handle;
// Find all chunks we need to fetch from the resolved chunk(s)
for (int32 ContainerIndex = 0; Private::FResolvedContainerChunks& ResolvedChunks : InstallRequest.ResolvedChunks)
{
TArray<int32, TInlineAllocator<64>> CachedEntryIndices;
for (int32 EntryIndex : ResolvedChunks.EntryIndices)
{
const FOnDemandChunkEntry& Entry = ResolvedChunks.Container->ChunkEntries[EntryIndex];
const bool bCached = IoStore.InstallCache->IsChunkCached(Entry.Hash);
if (bCached)
{
CachedEntryIndices.Add(EntryIndex);
}
OnChunkFound(ContainerIndex, EntryIndex, bCached);
}
// Add references to existing chunk(s)
if (CachedEntryIndices.IsEmpty() == false)
{
TUniqueLock Lock(IoStore.ContainerMutex);
FOnDemandChunkEntryReferences& References = ResolvedChunks.Container->FindOrAddChunkEntryReferences(*ContentHandle);
for (int32 EntryIndex : CachedEntryIndices)
{
References.Indices[EntryIndex] = true;
}
}
ContainerIndex++;
}
}
void FOnDemandContentInstaller::ProcessInstallRequest(FRequest& Request)
{
using namespace UE::IoStore::Private;
TRACE_CPUPROFILER_EVENT_SCOPE(FOnDemandContentInstaller::ProcessInstallRequest);
FRequest::FInstall& InstallRequest = Request.AsInstall();
Request.Priority = InstallRequest.Args.Priority;
UE_LOG(LogIoStoreOnDemand, Verbose, TEXT("Processing install request, SeqNo=%u, Priority=%d, ContentHandle=(%s)"),
Request.SeqNo, Request.Priority, *LexToString(InstallRequest.Args.ContentHandle));
if (InstallRequest.Args.ContentHandle.IsValid() == false)
{
Request.Result = MakeError(UE::UnifiedError::Core::ArgumentError::MakeError(TEXT("ContentHandle"), TEXT("Invalid content handle")));
return CompleteInstallRequest(Request);
}
const TSharedPtr<FOnDemandInternalContentHandle>& ContentHandle = InstallRequest.Args.ContentHandle.Handle;
if (ContentHandle->IoStore.IsValid() == false)
{
// First time this content handle is used
ContentHandle->IoStore = IoStore.AsWeak();
}
TSet<FSharedOnDemandContainer> ContainersForInstallation;
TSet<FPackageId> PackageIdsToInstall;
if (FIoStatus Status = IoStore.GetContainersAndPackagesForInstall(
InstallRequest.Args.MountId,
InstallRequest.Args.TagSets,
InstallRequest.Args.PackageIds,
ContainersForInstallation,
PackageIdsToInstall); !Status.IsOk())
{
Request.Result = MakeError(UE::UnifiedError::Core::ArgumentError::MakeError(TEXT("InstallArgs"), Status.ToString()));
return CompleteInstallRequest(Request);
}
#if !UE_BUILD_SHIPPING
if (!UE::IoStore::CVars::IoStoreErrorOnRequest.IsEmpty())
{
if (FCString::Strstr(*ContentHandle->DebugName, *UE::IoStore::CVars::IoStoreErrorOnRequest) != nullptr ||
FCString::Strstr(*InstallRequest.Args.DebugName, *UE::IoStore::CVars::IoStoreErrorOnRequest) != nullptr)
{
Request.Result = MakeError(
UE::UnifiedError::Core::ArgumentError::MakeError(
TEXT("InstallArgs"),
FString::Printf(TEXT("Debug error requested on debug name %s"), *UE::IoStore::CVars::IoStoreErrorOnRequest)));
return CompleteInstallRequest(Request);
}
}
if (!UE::IoStore::CVars::IoStoreDebugRequest.IsEmpty())
{
if (FCString::Strstr(*ContentHandle->DebugName, *UE::IoStore::CVars::IoStoreDebugRequest) != nullptr ||
FCString::Strstr(*InstallRequest.Args.DebugName, *UE::IoStore::CVars::IoStoreDebugRequest) != nullptr)
{
UE_LOG(LogIoStoreOnDemand, Log, TEXT("Place breakpoint here to debug FOnDemandContentInstaller::ProcessInstallRequest for asset %s"), *ContentHandle->DebugName);
}
}
#endif
if (Request.IsCancelled())
{
return CompleteInstallRequest(Request);
}
TSet<FPackageId> Missing;
const bool bIncludeSoftReferences = EnumHasAnyFlags(InstallRequest.Args.Options, EOnDemandInstallOptions::InstallSoftReferences);
const bool bIncludeOptionalBulkData = EnumHasAnyFlags(InstallRequest.Args.Options, EOnDemandInstallOptions::InstallOptionalBulkData);
Private::ResolveChunksToInstall(
ContainersForInstallation,
PackageIdsToInstall,
bIncludeSoftReferences,
bIncludeOptionalBulkData,
InstallRequest.ResolvedChunks,
Missing);
// Check the other I/O backends for missing package chunks
if (Missing.IsEmpty() == false)
{
FIoDispatcher& IoDispatcher = FIoDispatcher::Get();
TArray<FIoChunkId> MissingChunkIds;
for (const FPackageId& PackageId : Missing)
{
const FIoChunkId ChunkId = CreatePackageDataChunkId(PackageId);
if (IoDispatcher.DoesChunkExist(ChunkId) == false)
{
UE_CLOG(MissingChunkIds.Num() == 0, LogIoStoreOnDemand, Warning, TEXT("Failed to resolve the following chunk(s) for content handle '%s':"),
*LexToString(InstallRequest.Args.ContentHandle));
UE_LOG(LogIoStoreOnDemand, Warning, TEXT("\tChunkId='%s'"), *LexToString(ChunkId));
MissingChunkIds.Add(ChunkId);
}
}
if (MissingChunkIds.IsEmpty() == false && !EnumHasAnyFlags(InstallRequest.Args.Options, EOnDemandInstallOptions::AllowMissingDependencies))
{
Request.Result = MakeError(UE::UnifiedError::IoStoreOnDemand::ChunkMissingError::MakeError(
UE::UnifiedError::IoStoreOnDemand::FChunkMissingErrorContext
{
.ChunkIds = MoveTemp(MissingChunkIds)
}));
return CompleteInstallRequest(Request);
}
}
if (Request.IsCancelled())
{
return CompleteInstallRequest(Request);
}
bool bExecuteRequest = false;
{
TUniqueLock Lock(Mutex);
if (CanExecuteRequest(Request))
{
RunningRequests.Add(&Request);
bExecuteRequest = true;
}
}
if (bExecuteRequest)
{
// Execute immediately, let ExecuteInstallRequest handle chunk pinning
ExecuteInstallRequest(Request);
return;
}
uint64 TotalContentSize = 0;
uint64 TotalInstallSize = 0;
// Pin any already cached chunks while waiting for execution
PinCachedChunks(InstallRequest, [&InstallRequest, &TotalContentSize, &TotalInstallSize](const int32 ContainerIndex, const int32 EntryIndex, const bool bCached)
{
const FOnDemandChunkEntry& Entry = InstallRequest.ResolvedChunks[ContainerIndex].Container->ChunkEntries[EntryIndex];
TotalContentSize += Entry.GetDiskSize();
if (bCached)
{
return;
}
TotalInstallSize += Entry.GetDiskSize();
});
InstallRequest.Progress.TotalContentSize = TotalContentSize;
InstallRequest.Progress.TotalInstallSize = TotalInstallSize;
InstallRequest.Progress.CurrentInstallSize = 0;
if (TotalInstallSize == 0)
{
return CompleteInstallRequest(Request);
}
if (Request.IsCancelled())
{
return CompleteInstallRequest(Request);
}
{
TUniqueLock Lock(Mutex);
RequestQueue.HeapPush(&Request, RequestSortPredicate);
}
}
void FOnDemandContentInstaller::ExecuteInstallRequest(FRequest& Request)
{
check(Request.IsInstall());
check(RunningRequests.Contains(&Request));
FRequest::FInstall& InstallRequest = Request.AsInstall();
check(InstallRequest.HttpRequestHandles.IsEmpty());
UE_LOG(LogIoStoreOnDemand, Verbose, TEXT("Executing install request, SeqNo=%u, Priority=%d, ContentHandle=(%s)"),
Request.SeqNo, Request.Priority, *LexToString(InstallRequest.Args.ContentHandle));
if (Request.IsCancelled())
{
return CompleteInstallRequest(Request);
}
uint64 TotalContentSize = 0;
uint64 TotalInstallSize = 0;
PinCachedChunks(InstallRequest, [&Request, &TotalContentSize, &TotalInstallSize](const int32 ContainerIndex, const int32 EntryIndex, const bool bCached)
{
FRequest::FInstall& InstallRequest = Request.AsInstall();
const FOnDemandChunkEntry& Entry = InstallRequest.ResolvedChunks[ContainerIndex].Container->ChunkEntries[EntryIndex];
TotalContentSize += Entry.GetDiskSize();
if (bCached)
{
return;
}
InstallRequest.HttpRequestHandles.Add(FChunkHttpRequestHandle
{
.OwnerRequest = &Request,
.ContainerIndex = ContainerIndex,
.EntryIndex = EntryIndex
});
TotalInstallSize += Entry.GetDiskSize();
});
// TotalInstallSize may be different now
InstallRequest.Progress.TotalContentSize = TotalContentSize;
InstallRequest.Progress.TotalInstallSize = TotalInstallSize;
InstallRequest.Progress.CurrentInstallSize = 0;
if (InstallRequest.HttpRequestHandles.IsEmpty())
{
return CompleteInstallRequest(Request);
}
if (EnumHasAnyFlags(InstallRequest.Args.Options, EOnDemandInstallOptions::DoNotDownload))
{
Request.Result = MakeError(UE::UnifiedError::Core::ArgumentError::MakeError(TEXT("Options"), TEXT("DoNotDownload flag specified when download required")));
return CompleteInstallRequest(Request);
}
if (CVars::EnableConcurentInstallRequests() == false)
{
// Make sure we have enough space in the cache
uint64 BytesToInstall = 0;
{
TSet<FIoHash> ChunksToInstall;
ChunksToInstall.Reserve(InstallRequest.HttpRequestHandles.Num());
for (FChunkHttpRequestHandle& HttpRequest : InstallRequest.HttpRequestHandles)
{
FSharedOnDemandContainer& Container = InstallRequest.ResolvedChunks[HttpRequest.ContainerIndex].Container;
const FOnDemandChunkEntry& ChunkEntry = Container->ChunkEntries[HttpRequest.EntryIndex];
bool bAlreadyInSet = false;
ChunksToInstall.Add(ChunkEntry.Hash, &bAlreadyInSet);
if (!bAlreadyInSet)
{
BytesToInstall += ChunkEntry.GetDiskSize();
}
}
}
if (Request.Result = IoStore.InstallCache->Purge(BytesToInstall); Request.Result.HasError())
{
return CompleteInstallRequest(Request);
}
}
if (Request.IsCancelled())
{
return CompleteInstallRequest(Request);
}
NotifyInstallProgress(Request);
const bool bUseHttpIoDispatcher = FHttpIoDispatcher::IsInitialized();
check(bUseHttpIoDispatcher || HttpClient != nullptr);
TOptional<FIoHttpBatch> Batch;
if (bUseHttpIoDispatcher)
{
Batch.Emplace(FHttpIoDispatcher::NewBatch());
}
constexpr const int32 HttpRetryCount = 2;
for (FChunkHttpRequestHandle& HttpRequest : InstallRequest.HttpRequestHandles)
{
TRACE_CPUPROFILER_EVENT_SCOPE(FOnDemandContentInstaller::IssueRequest);
FSharedOnDemandContainer& Container = InstallRequest.ResolvedChunks[HttpRequest.ContainerIndex].Container;
FOnDemandChunkInfo ChunkInfo = FOnDemandChunkInfo(Container, Container->ChunkEntries[HttpRequest.EntryIndex]);
const uint8 HttpCategory = 1; // see FOnDemandHttpThread
FChunkDownloadRequest* PendingChunkRequest = nullptr;
for (auto It = PendingChunkDownloads.CreateKeyIterator(ChunkInfo.ChunkEntry().Hash); It; ++It)
{
FChunkDownloadRequest& ChunkDownload = It.Value();
if (ChunkDownload.bChunkCanceled == false)
{
PendingChunkRequest = &ChunkDownload;
break; // Should be a max of one active request for any chunk
}
}
if (PendingChunkRequest)
{
PendingChunkRequest->ChunkRequestHttpHandles.AddHead(&HttpRequest);
}
else
{
const FIoHash& ChunkHash = ChunkInfo.ChunkEntry().Hash;
PendingChunkRequest = &PendingChunkDownloads.Add(ChunkHash);
PendingChunkRequest->RequestId = Request.SeqNo;
PendingChunkRequest->ChunkRequestHttpHandles.AddHead(&HttpRequest);
// Setting HTTP flags to None to not use the HTTP cache
FIoHttpOptions HttpOptions = FIoHttpOptions(Request.Priority, HttpRetryCount, EIoHttpFlags::None);
HttpOptions.SetCategory(HttpCategory);
UE_LOG(LogIoStoreOnDemand, VeryVerbose, TEXT("Created request for chunk: ChunkHash=%s, RequestId=%u"),
*TStringBuilder<64>(InPlace, ChunkHash), PendingChunkRequest->RequestId);
if (bUseHttpIoDispatcher)
{
PendingChunkRequest->HttpHandle.Emplace<FIoHttpRequest>(Batch->Get(
ChunkInfo.HostGroupName(),
ChunkInfo.RelativeUrl(),
FIoHttpHeaders(),
HttpOptions,
ChunkHash,
[this, ChunkHash, RequestId = PendingChunkRequest->RequestId](FIoHttpResponse&& HttpResponse)
{
InstallerPipe.Launch(
TEXT("ProcessIoStoreOnDemandDownloadedChunk"),
[this, ChunkHash, RequestId, HttpResponse = MoveTemp(HttpResponse)]() mutable
{
FIoBuffer Chunk = HttpResponse.GetBody();
ProcessDownloadedChunk(ChunkHash, RequestId, HttpResponse.GetErrorCode(), HttpResponse.GetStatusCode(), MoveTemp(Chunk));
},
UE::Tasks::ETaskPriority::BackgroundLow);
})
);
}
else
{
PendingChunkRequest->HttpHandle.Emplace<void*>(HttpClient->IssueRequest(
MoveTemp(ChunkInfo),
FIoOffsetAndLength(),
Request.Priority,
[this, ChunkHash, RequestId = PendingChunkRequest->RequestId](uint32 HttpStatusCode, FStringView ErrorReason, FIoBuffer&& Chunk)
{
EIoErrorCode CompletionStatus = (HttpStatusCode > 199 && HttpStatusCode < 300) ? EIoErrorCode::Ok : EIoErrorCode::ReadError;
if (ErrorReason.Contains(TEXTVIEW("cancelled"), ESearchCase::IgnoreCase) ||
ErrorReason.Contains(TEXTVIEW("canceled"), ESearchCase::IgnoreCase)) // Yes we spell it both ways (╯°□°)╯︵ ┻━┻
{
CompletionStatus = EIoErrorCode::Cancelled;
}
else
{
UE_CLOG(ErrorReason.IsEmpty() == false, LogIoStoreOnDemand, Warning, TEXT("Failed to download chunk: %.*s"), ErrorReason.Len(), ErrorReason.GetData());
}
InstallerPipe.Launch(
TEXT("ProcessIoStoreOnDemandDownloadedChunk"),
[this, ChunkHash, RequestId, CompletionStatus, HttpStatusCode, Chunk = MoveTemp(Chunk)]() mutable
{
ProcessDownloadedChunk(ChunkHash, RequestId, CompletionStatus, HttpStatusCode, MoveTemp(Chunk));
},
UE::Tasks::ETaskPriority::BackgroundLow);
},
EHttpRequestType::Installed)
);
}
}
}
if (Batch)
{
Batch->Issue();
}
InstallRequest.bHttpRequestsIssued = true;
}
void FOnDemandContentInstaller::ExecutePurgeRequest(FRequest& Request)
{
check(Request.IsPurge());
check(RunningRequests.Num() == 1 && RunningRequests.Contains(&Request));
FRequest::FPurge& PurgeRequest = Request.AsPurge();
const bool bDefrag = EnumHasAnyFlags(PurgeRequest.Args.Options, EOnDemandPurgeOptions::Defrag);
const uint64* BytesToPurge = PurgeRequest.Args.BytesToPurge.GetPtrOrNull();
UE_LOG(LogIoStoreOnDemand, Log, TEXT("Executing purge request, BytesToPurge=%llu, Defrag='%s'"),
BytesToPurge != nullptr ? *BytesToPurge : -1, bDefrag ? TEXT("True") : TEXT("False"));
if (Request.IsCancelled() == false)
{
Request.Result = IoStore.InstallCache->PurgeAllUnreferenced(bDefrag, BytesToPurge);
}
CompletePurgeRequest(Request);
}
void FOnDemandContentInstaller::ExecuteDefragRequest(FRequest& Request)
{
check(Request.IsDefrag());
check(RunningRequests.Num() == 1 && RunningRequests.Contains(&Request));
FRequest::FDefrag& DefragRequest = Request.AsDefrag();
const uint64* BytesToFree = DefragRequest.Args.BytesToFree.GetPtrOrNull();
UE_LOG(LogIoStoreOnDemand, Log, TEXT("Executing defrag request, BytesToFree=%llu"),
BytesToFree != nullptr ? *BytesToFree : -1);
if (Request.IsCancelled() == false)
{
Request.Result = IoStore.InstallCache->DefragAll(BytesToFree);
}
CompleteDefragRequest(Request);
}
void FOnDemandContentInstaller::ExecuteVerifyRequest(FRequest& Request)
{
check(Request.IsVerify());
check(RunningRequests.Num() == 1 && RunningRequests.Contains(&Request));
UE_LOG(LogIoStoreOnDemand, Log, TEXT("Executing verify cache request"));
if (Request.IsCancelled() == false)
{
Request.Result = IoStore.InstallCache->Verify();
}
CompleteVerifyRequest(Request);
}
void FOnDemandContentInstaller::ExectuteFlushLastAccessRequest(FRequest& Request)
{
check(Request.IsFlushLastAccess());
check(RunningRequests.Num() == 1 && RunningRequests.Contains(&Request));
UE_LOG(LogIoStoreOnDemand, Log, TEXT("Executing flush last access request"));
if (Request.IsCancelled() == false)
{
Request.Result = IoStore.InstallCache->FlushLastAccess();
}
CompleteFlushLastAccessRequest(Request);
}
void FOnDemandContentInstaller::ProcessDownloadedChunk(
const FIoHash& ChunkHash, uint32 RequestId, EIoErrorCode InErrorCode, uint32 HttpStatusCode, FIoBuffer&& Chunk)
{
TRACE_CPUPROFILER_EVENT_SCOPE(FOnDemandContentInstaller::ProcessDownloadedChunk);
FResult Result = MakeValue();
const bool bHttpOk = HttpStatusCode > 199 && HttpStatusCode < 300 && Chunk.GetSize() > 0;
if (bHttpOk)
{
const FIoHash VerifyChunkHash = FIoHash::HashBuffer(Chunk.GetView());
if (ChunkHash == VerifyChunkHash)
{
Result = IoStore.InstallCache->PutChunk(MoveTemp(Chunk), ChunkHash);
if (CVars::EnableConcurentInstallRequests())
{
// TODO: Cache needs to track size of blocks in memory so we can quickly compute cache usage
// TODO: if cache is full, Defrag here based on size of the pending http chunks
}
}
else
{
Result = MakeError(UnifiedError::IoStoreOnDemand::ChunkHashError::MakeError(
UnifiedError::IoStoreOnDemand::FChunkHashMismatchErrorContext
{
.ChunkId = FIoChunkId::InvalidChunkId, // Filled in later by OnProcessDownloadedChunkNotifyRequest
.ExpectedHash = ChunkHash,
.ActualHash = VerifyChunkHash
}));
}
}
else
{
Result = MakeError(UnifiedError::IoStoreOnDemand::HttpError::MakeError(HttpStatusCode));
}
const bool bCancelled = (InErrorCode == EIoErrorCode::Cancelled);
bool bFoundRequest = false;
for (auto It = PendingChunkDownloads.CreateKeyIterator(ChunkHash); It; ++It)
{
FChunkDownloadRequest& ChunkRequest = It.Value();
if (ChunkRequest.RequestId != RequestId)
{
continue;
}
static constexpr const TCHAR LogFmt[] = TEXT("ProcessDownloadedChunk: ChunkHash=%s, RequestId=%u, DownloadResult=%s");
if (bCancelled)
{
UE_LOG(LogIoStoreOnDemand, Verbose, LogFmt, *TStringBuilder<64>(InPlace, ChunkHash), ChunkRequest.RequestId, TEXT("Cancelled"));
}
else if (Result.HasError())
{
UE_LOG(LogIoStoreOnDemand, Warning, LogFmt, *TStringBuilder<64>(InPlace, ChunkHash), ChunkRequest.RequestId, *Result.GetError().GetErrorMessage(true).ToString());
}
else
{
UE_LOG(LogIoStoreOnDemand, VeryVerbose, LogFmt, *TStringBuilder<64>(InPlace, ChunkHash), ChunkRequest.RequestId, TEXT("OK"));
}
bFoundRequest = true;
while (FChunkHttpRequestHandle* ChunkHttpRequestHandle = ChunkRequest.ChunkRequestHttpHandles.PopHead())
{
OnProcessDownloadedChunkNotifyRequest(*ChunkHttpRequestHandle, Result, bCancelled);
}
It.RemoveCurrent();
break;
}
check(bFoundRequest);
}
void FOnDemandContentInstaller::OnProcessDownloadedChunkNotifyRequest(
const FChunkHttpRequestHandle& HttpRequest, const FResult& ChunkDownloadResult, bool bChunkCancelled)
{
TRACE_CPUPROFILER_EVENT_SCOPE(FOnDemandContentInstaller::OnProcessDownloadedChunkNotifyRequest);
FRequest& Request = *HttpRequest.OwnerRequest;
FRequest::FInstall& InstallRequest = Request.AsInstall();
FSharedOnDemandContainer& Container = InstallRequest.ResolvedChunks[HttpRequest.ContainerIndex].Container;
const FOnDemandChunkEntry& ChunkEntry = Container->ChunkEntries[HttpRequest.EntryIndex];
const FIoChunkId& ChunkId = Container->ChunkIds[HttpRequest.EntryIndex];
bool bSetErrorOnRequest = false;
if (Request.IsOk())
{
if (!ensureMsgf(!bChunkCancelled, TEXT("Recieved cancelled http callback for active Request %s ! This should never happen."), *InstallRequest.Args.DebugName))
{
bSetErrorOnRequest = true;
Request.Result = ChunkDownloadResult;
}
else if (const UnifiedError::FError* Error = ChunkDownloadResult.TryGetError())
{
bSetErrorOnRequest = true;
// Set the chunk Id from this container if needed
const UnifiedError::IoStoreOnDemand::FChunkHashMismatchErrorContext* ErrorContext =
Error->GetErrorContext<UnifiedError::IoStoreOnDemand::FChunkHashMismatchErrorContext>();
if (ErrorContext)
{
// FError does not deep copy by design.
// We need a different error per-request so we need to re-instance it.
Request.Result = MakeError(UnifiedError::IoStoreOnDemand::ChunkHashError::MakeError(
UnifiedError::IoStoreOnDemand::FChunkHashMismatchErrorContext
{
.ChunkId = ChunkId,
.ExpectedHash = ErrorContext->ExpectedHash,
.ActualHash = ErrorContext->ActualHash
}));
}
else
{
Request.Result = ChunkDownloadResult;
}
}
}
static constexpr const TCHAR LogFmt[] = TEXT("Install progress %.2lf/%.2lf MiB, SeqNo=%u, Priority=%d, ContentHandle=(%s), ChunkId='%s', ChunkSize=%.2lf KiB, DownloadResult=%s");
if (bChunkCancelled)
{
UE_LOG(LogIoStoreOnDemand, Verbose, LogFmt,
double(InstallRequest.Progress.CurrentInstallSize) / 1024.0 / 1024.0, double(InstallRequest.Progress.TotalInstallSize) / 1024.0 / 1024.0,
Request.SeqNo, Request.Priority, *LexToString(InstallRequest.Args.ContentHandle), *LexToString(ChunkId), double(ChunkEntry.GetDiskSize()) / 1024.0,
TEXT("Cancelled"));
}
else if (ChunkDownloadResult.HasError())
{
UE_LOG(LogIoStoreOnDemand, Warning, LogFmt,
double(InstallRequest.Progress.CurrentInstallSize) / 1024.0 / 1024.0, double(InstallRequest.Progress.TotalInstallSize) / 1024.0 / 1024.0,
Request.SeqNo, Request.Priority, *LexToString(InstallRequest.Args.ContentHandle), *LexToString(ChunkId), double(ChunkEntry.GetDiskSize()) / 1024.0,
*ChunkDownloadResult.GetError().GetErrorMessage(true).ToString());
}
else
{
InstallRequest.Progress.CurrentInstallSize += ChunkEntry.GetDiskSize();
UE_LOG(LogIoStoreOnDemand, Verbose, LogFmt,
double(InstallRequest.Progress.CurrentInstallSize) / 1024.0 / 1024.0, double(InstallRequest.Progress.TotalInstallSize) / 1024.0 / 1024.0,
Request.SeqNo, Request.Priority, *LexToString(InstallRequest.Args.ContentHandle), *LexToString(ChunkId), double(ChunkEntry.GetDiskSize()) / 1024.0,
TEXT("OK"));
}
const bool bCompleted = ++InstallRequest.DownloadedChunkCount >= InstallRequest.HttpRequestHandles.Num();
if (bCompleted)
{
CompleteInstallRequest(Request);
}
else
{
NotifyInstallProgress(Request);
// Only try to cancel requests the first time an error is set
if (bSetErrorOnRequest)
{
int32 NumCancelled = 0;
TryCancelHttpRequestsForInstallRequest(InstallRequest, NumCancelled);
UE_CLOG(NumCancelled == 0, LogIoStoreOnDemand, Warning, TEXT("Cancelled %d HTTP request(s) due to install error"), NumCancelled);
}
}
}
void FOnDemandContentInstaller::TryCancelHttpRequestsForInstallRequest(FRequest::FInstall& InstallRequest, int32& OutNumCancelled)
{
int32 NumCancelled = 0;
if (CVars::bContentInstallerEnableHttpCancel == false)
{
OutNumCancelled = NumCancelled;
return;
}
for (FChunkHttpRequestHandle& PendingHttpRequest : InstallRequest.HttpRequestHandles)
{
const FSharedOnDemandContainer& PendingContainer = InstallRequest.ResolvedChunks[PendingHttpRequest.ContainerIndex].Container;
const FOnDemandChunkEntry& PendingChunkEntry = PendingContainer->ChunkEntries[PendingHttpRequest.EntryIndex];
const FIoHash& PendingChunkHash = PendingChunkEntry.Hash;
FChunkDownloadRequest* ChunkDownload = nullptr;
for (auto It = PendingChunkDownloads.CreateKeyIterator(PendingChunkHash); It; ++It)
{
FChunkDownloadRequest& FoundChunkDownload = It.Value();
if (FoundChunkDownload.bChunkCanceled == false)
{
ChunkDownload = &FoundChunkDownload;
break; // Should be a max of one active request for any chunk
}
}
if (ChunkDownload)
{
bool bCancelHttpRequest = true;
for (FChunkHttpRequestHandle& Handle : ChunkDownload->ChunkRequestHttpHandles)
{
if (Handle.OwnerRequest->IsOk())
{
bCancelHttpRequest = false;
break;
}
}
if (bCancelHttpRequest)
{
// No requests are waiting on this chunk, cancel it
Visit(UE::Overload(
[&NumCancelled](FIoHttpRequest& Handle)
{
if (Handle.IsValid())
{
Handle.Cancel();
++NumCancelled;
}
},
[this, &NumCancelled](void* Handle)
{
check(HttpClient != nullptr);
if (Handle != nullptr)
{
HttpClient->CancelRequest(Handle);
++NumCancelled;
}
}),
ChunkDownload->HttpHandle);
ChunkDownload->bChunkCanceled = true;
UE_LOG(LogIoStoreOnDemand, Verbose, TEXT("Cancelled chunk download: hash - %s, Id - %u"), *TStringBuilder<64>(InPlace, PendingChunkHash), ChunkDownload->RequestId);
// ProcessDownloadedChunk will remove the chunk request
}
}
}
OutNumCancelled = NumCancelled;
}
void FOnDemandContentInstaller::NotifyInstallProgress(FRequest& Request)
{
ensure(Request.IsInstall());
FRequest::FInstall& InstallRequest = Request.AsInstall();
if (!InstallRequest.OnProgress)
{
return;
}
const uint64 Cycles = FPlatformTime::Cycles64();
const double SecondsSinceLastProgress = FPlatformTime::ToSeconds64(Cycles - InstallRequest.LastProgressCycles);
if (InstallRequest.bNotifyingProgressOnGameThread.load(std::memory_order_seq_cst) || SecondsSinceLastProgress < .25)
{
return;
}
InstallRequest.LastProgressCycles = Cycles;
//TODO: Remove support for notifying progress on the game thread
FOnDemandInstallProgress Progress = InstallRequest.Progress;
if (EnumHasAnyFlags(InstallRequest.Args.Options, EOnDemandInstallOptions::CallbackOnGameThread))
{
InstallRequest.bNotifyingProgressOnGameThread = true;
ExecuteOnGameThread(
UE_SOURCE_LOCATION,
[&InstallRequest, Progress]()
{
InstallRequest.OnProgress(InstallRequest.Progress);
InstallRequest.bNotifyingProgressOnGameThread = false;
});
}
else
{
InstallRequest.OnProgress(Progress);
}
}
void FOnDemandContentInstaller::DestroyRequest(FRequest& Request)
{
// Call destructor outside the lock to avoid double locking
Request.~FRequest();
TUniqueLock Lock(Mutex);
RequestAllocator.Free(&Request);
}
void FOnDemandContentInstaller::CompleteInstallRequest(FRequest& Request)
{
using namespace UE::IoStore::Private;
FRequest::FInstall& InstallRequest = Request.AsInstall();
#if !UE_BUILD_SHIPPING
TArray<FString> DebugInfo;
#endif
const uint64 ResolvedChunkCount = Algo::TransformAccumulate(
InstallRequest.ResolvedChunks,
[](const Private::FResolvedContainerChunks& ContainerChunks)
{
return ContainerChunks.EntryIndices.Num();
},
uint64(0)
);
// Mark all resolved chunk(s) as referenced by the content handle and notify the package store to update
if (Request.IsOk() && ResolvedChunkCount > 0)
{
{
FOnDemandContentHandle& ContentHandle = InstallRequest.Args.ContentHandle;
TUniqueLock Lock(IoStore.ContainerMutex);
for (FResolvedContainerChunks& ResolvedChunks : InstallRequest.ResolvedChunks)
{
const FSharedOnDemandContainer& Container = ResolvedChunks.Container;
FOnDemandChunkEntryReferences& References = Container->FindOrAddChunkEntryReferences(*ContentHandle.Handle);
for (int32 EntryIndex : ResolvedChunks.EntryIndices)
{
References.Indices[EntryIndex] = true;
#if !UE_BUILD_SHIPPING
if (UE_LOG_ACTIVE(LogIoStoreOnDemand, Verbose))
{
DebugInfo.Add(FString::Printf(TEXT("ID:%s, Size:%d"), *LexToString(Container->ChunkEntries[EntryIndex].Hash), Container->ChunkEntries[EntryIndex].GetDiskSize() ));
}
#endif
}
}
}
IoStore.PackageStoreBackend->NeedsUpdate(EOnDemandPackageStoreUpdateMode::ReferencedPackages);
}
const bool bCancelled = Request.IsCancelled();
const uint64 DurationCycles = FPlatformTime::Cycles64() - Request.StartTimeCycles;
const double DurationInSeconds = FPlatformTime::ToSeconds64(DurationCycles);
const double CacheHitRatio = InstallRequest.Progress.TotalContentSize > 0
? double(InstallRequest.Progress.TotalContentSize - InstallRequest.Progress.TotalInstallSize) / double(InstallRequest.Progress.TotalContentSize)
: 0.0;
#if !UE_BUILD_SHIPPING
FString DebugInfoString = FString::Join(DebugInfo, TEXT(","));
UE_LOG(LogIoStoreOnDemand, Verbose, TEXT("ContentHandle=(%s), ResolvedChunks(%s)"), *LexToString(InstallRequest.Args.ContentHandle), *DebugInfoString);
#endif
UE_LOG(LogIoStoreOnDemand, Verbose, TEXT("Install request completed, Result='%s', SeqNo=%u, Priority=%d, ContentHandle=(%s), ContentSize=%.2lf MiB, InstallSize=%.2lf MiB, CacheHitRatio=%d%%, Duration=%dms"),
*Request.GetErrorString(), Request.SeqNo, Request.Priority, *LexToString(InstallRequest.Args.ContentHandle), double(InstallRequest.Progress.TotalContentSize) / 1024.0 / 1024.0,
double(InstallRequest.Progress.TotalInstallSize) / 1024.0 / 1024.0, int32(CacheHitRatio * 100), Private::SecondsToMillicseconds(DurationInSeconds));
FOnDemandContentInstallerStats::OnRequestCompleted(
Request.Result,
ResolvedChunkCount,
InstallRequest.Progress.TotalContentSize,
static_cast<uint64>(InstallRequest.HttpRequestHandles.Num()),
InstallRequest.Progress.CurrentInstallSize,
CacheHitRatio,
DurationCycles);
FOnDemandInstallResult InstallResult;
InstallResult.Progress = InstallRequest.Progress;
InstallResult.DurationInSeconds = DurationInSeconds;
InstallResult.Error = Request.ConsumeError();
{
TUniqueLock Lock(Mutex);
InstallRequest.Request->InstallerRequest = 0;
RunningRequests.RemoveSingleSwap(&Request);
}
TryExecuteNextRequest();
const FOnDemandInstallRequest::EStatus FinalRequestStatus = bCancelled
? FOnDemandInstallRequest::EStatus::Cancelled
: InstallResult.Error.IsSet()
? FOnDemandInstallRequest::EStatus::Error
: FOnDemandInstallRequest::Ok;
if (!InstallRequest.OnCompleted)
{
InstallRequest.Request->Status.store(FinalRequestStatus);
DestroyRequest(Request);
return;
}
// Do this before the callback in case the callback triggers any further API calls
InstallRequest.Request->Status.store(FOnDemandInstallRequest::PendingCallbacks);
const bool bCallbackOnGameThread = EnumHasAnyFlags(InstallRequest.Args.Options, EOnDemandInstallOptions::CallbackOnGameThread);
if (bCallbackOnGameThread)
{
ExecuteOnGameThread(
UE_SOURCE_LOCATION,
[this, &Request, InstallResult = MoveTemp(InstallResult), FinalRequestStatus]() mutable
{
FRequest::FInstall& InstallRequest = Request.AsInstall();
FOnDemandInstallCompleted OnCompleted = MoveTemp(InstallRequest.OnCompleted);
ensure(InstallRequest.bNotifyingProgressOnGameThread == false);
OnCompleted(MoveTemp(InstallResult));
InstallRequest.Request->Status.store(FinalRequestStatus);
DestroyRequest(Request);
});
}
else
{
FOnDemandInstallCompleted OnCompleted = MoveTemp(InstallRequest.OnCompleted);
OnCompleted(MoveTemp(InstallResult));
InstallRequest.Request->Status.store(FinalRequestStatus);
DestroyRequest(Request);
}
}
void FOnDemandContentInstaller::CompletePurgeRequest(FRequest& Request)
{
check(RunningRequests.Num() == 1 && RunningRequests.Contains(&Request));
const double DurationInSeconds = FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - Request.StartTimeCycles);
UE_LOG(LogIoStoreOnDemand, Log, TEXT("Purge request completed, Result='%s', Duration=%d ms"),
*Request.GetErrorString(), Private::SecondsToMillicseconds(DurationInSeconds));
FOnDemandPurgeResult PurgeResult;
PurgeResult.DurationInSeconds = DurationInSeconds;
PurgeResult.Error = Request.ConsumeError();
const bool bCallbackOnGameThread = EnumHasAnyFlags(Request.AsPurge().Args.Options, EOnDemandPurgeOptions::CallbackOnGameThread);
FOnDemandPurgeCompleted OnCompleted = MoveTemp(Request.AsPurge().OnCompleted);
{
TUniqueLock Lock(Mutex);
RunningRequests.RemoveSingleSwap(&Request);
}
DestroyRequest(Request);
TryExecuteNextRequest();
if (!OnCompleted)
{
return;
}
if (bCallbackOnGameThread)
{
ExecuteOnGameThread(
UE_SOURCE_LOCATION,
[OnCompleted = MoveTemp(OnCompleted), PurgeResult = MoveTemp(PurgeResult)]() mutable
{
OnCompleted(MoveTemp(PurgeResult));
});
}
else
{
OnCompleted(MoveTemp(PurgeResult));
}
}
void FOnDemandContentInstaller::CompleteDefragRequest(FRequest& Request)
{
check(RunningRequests.Num() == 1 && RunningRequests.Contains(&Request));
const double DurationInSeconds = FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - Request.StartTimeCycles);
UE_LOG(LogIoStoreOnDemand, Log, TEXT("Defrag request completed, Result='%s', Duration=%d ms"),
*Request.GetErrorString(), Private::SecondsToMillicseconds(DurationInSeconds));
FOnDemandDefragResult DefragResult;
DefragResult.DurationInSeconds = DurationInSeconds;
DefragResult.Error = Request.ConsumeError();
const bool bCallbackOnGameThread = EnumHasAnyFlags(Request.AsDefrag().Args.Options, EOnDemandDefragOptions::CallbackOnGameThread);
FOnDemandDefragCompleted OnCompleted = MoveTemp(Request.AsDefrag().OnCompleted);
{
TUniqueLock Lock(Mutex);
RunningRequests.RemoveSingleSwap(&Request);
}
DestroyRequest(Request);
TryExecuteNextRequest();
if (!OnCompleted)
{
return;
}
if (bCallbackOnGameThread)
{
ExecuteOnGameThread(
UE_SOURCE_LOCATION,
[OnCompleted = MoveTemp(OnCompleted), DefragResult = MoveTemp(DefragResult)]() mutable
{
OnCompleted(MoveTemp(DefragResult));
});
}
else
{
OnCompleted(MoveTemp(DefragResult));
}
}
void FOnDemandContentInstaller::CompleteVerifyRequest(FRequest& Request)
{
const double DurationInSeconds = FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - Request.StartTimeCycles);
UE_LOG(LogIoStoreOnDemand, Log, TEXT("Verify request completed, Result='%s', Duration=%d ms"),
*Request.GetErrorString(), Private::SecondsToMillicseconds(DurationInSeconds));
FOnDemandVerifyCacheResult VerifyResult;
VerifyResult.DurationInSeconds = DurationInSeconds;
VerifyResult.Error = Request.ConsumeError();
FOnDemandVerifyCacheCompleted OnCompleted = MoveTemp(Request.AsVerify().OnCompleted);
{
TUniqueLock Lock(Mutex);
RunningRequests.RemoveSingleSwap(&Request);
}
DestroyRequest(Request);
TryExecuteNextRequest();
if (!OnCompleted)
{
return;
}
OnCompleted(MoveTemp(VerifyResult));
}
void FOnDemandContentInstaller::CompleteFlushLastAccessRequest(FRequest& Request)
{
const double DurationInSeconds = FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - Request.StartTimeCycles);
UE_LOG(LogIoStoreOnDemand, Log, TEXT("Flush last access request completed, Result='%s', Duration=%d ms"),
*Request.GetErrorString(), Private::SecondsToMillicseconds(DurationInSeconds));
FOnDemandFlushLastAccessResult FlushResult;
FlushResult.DurationInSeconds = DurationInSeconds;
FlushResult.Error = Request.ConsumeError();
FOnDemandFlushLastAccessCompleted OnCompleted = MoveTemp(Request.AsFlushLastAccess().OnCompleted);
{
TUniqueLock Lock(Mutex);
RunningRequests.RemoveSingleSwap(&Request);
}
DestroyRequest(Request);
TryExecuteNextRequest();
if (!OnCompleted)
{
return;
}
OnCompleted(MoveTemp(FlushResult));
}
void FOnDemandContentInstaller::Shutdown()
{
bShuttingDown = true;
const double WaitTimeoutSeconds = 5.0;
const uint64 StartTimeCycles = FPlatformTime::Cycles64();
// Cancel current requests
{
TUniqueLock Lock(Mutex);
for (FRequest* Request : RunningRequests)
{
Request->TryCancel();
}
}
// Wait for the current request(s) to finish
for (;;)
{
double WaitTimeSeconds = FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - StartTimeCycles);
if (WaitTimeSeconds > WaitTimeoutSeconds)
{
UE_LOG(LogIoStoreOnDemand, Warning, TEXT("Content installer shutdown cancelled after %.2lf"), WaitTimeSeconds);
break;
}
InstallerPipe.WaitUntilEmpty(FTimespan::FromSeconds(1.0));
{
TUniqueLock Lock(Mutex);
if (RunningRequests.IsEmpty())
{
break;
}
}
}
{
TUniqueLock Lock(Mutex);
UE_CLOG(RunningRequests.IsEmpty() == false, LogIoStoreOnDemand, Error, TEXT("Content installer has still inflight request(s) while shutting down"));
RunningRequests.Reset();
}
// Cancel all remaining request(s)
for (;;)
{
FRequest* NextRequest = nullptr;
{
TUniqueLock Lock(Mutex);
if (RequestQueue.IsEmpty() == false)
{
RequestQueue.HeapPop(NextRequest, RequestSortPredicate, EAllowShrinking::No);
RunningRequests.Add(NextRequest);
}
}
if (NextRequest == nullptr)
{
break;
}
NextRequest->TryCancel();
ExecuteRequest(*NextRequest);
check(RunningRequests.IsEmpty());
}
}
} // namespace UE::IoStore