Files
UnrealEngine/Engine/Source/Developer/Virtualization/Private/PackageVirtualizationProcess.cpp
2025-05-18 13:04:45 +08:00

552 lines
20 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "PackageVirtualizationProcess.h"
#include "Containers/UnrealString.h"
#include "HAL/FileManager.h"
#include "HAL/PlatformFileManager.h"
#include "HAL/PlatformTime.h"
#include "Internationalization/Internationalization.h"
#include "Misc/PackageName.h"
#include "Misc/Paths.h"
#include "Misc/ScopeExit.h"
#include "Misc/ScopedSlowTask.h"
#include "PackageUtils.h"
#include "Serialization/EditorBulkData.h"
#include "UObject/Linker.h"
#include "UObject/Package.h"
#include "UObject/PackageResourceManager.h"
#include "UObject/PackageTrailer.h"
#include "UObject/UObjectGlobals.h"
#include "Virtualization/VirtualizationSystem.h"
#include "VirtualizationManager.h"
#include "VirtualizationSourceControlUtilities.h"
#include "VirtualizationUtilities.h"
#define LOCTEXT_NAMESPACE "Virtualization"
namespace UE::Virtualization
{
/**
* Implementation of the IPayloadProvider interface so that payloads can be requested on demand
* when they are being virtualized.
*
* This implementation is not optimized. If a package holds many payloads that are all virtualized
* we will end up loading the same trailer over and over, as well as opening the same package file
* for read many times.
*
* So far this has shown to be a rounding error compared to the actual cost of virtualization
* and so implementing any level of caching has been left as a future task.
*
* TODO: Implement a MRU cache for payloads to prevent loading the same payload off disk many
* times for different backends if it will not cause a huge memory spike.
*/
class FWorkspaceDomainPayloadProvider final : public IPayloadProvider
{
public:
FWorkspaceDomainPayloadProvider() = default;
virtual ~FWorkspaceDomainPayloadProvider() = default;
/** Register the payload with it's trailer and package name so that we can access it later as needed */
void RegisterPayload(const FIoHash& PayloadId, uint64 SizeOnDisk, const FString& PackageName)
{
if (!PayloadId.IsZero())
{
PayloadLookupTable.Emplace(PayloadId, FPayloadData(SizeOnDisk, PackageName));
}
}
private:
virtual FCompressedBuffer RequestPayload(const FIoHash& Identifier) override
{
if (Identifier.IsZero())
{
return FCompressedBuffer();
}
const FPayloadData* Data = PayloadLookupTable.Find(Identifier);
if (Data == nullptr)
{
UE_LOG(LogVirtualization, Error, TEXT("FWorkspaceDomainPayloadProvider was unable to find a payload with the identifier '%s'"),
*LexToString(Identifier));
return FCompressedBuffer();
}
TUniquePtr<FArchive> PackageAr = IPackageResourceManager::Get().OpenReadExternalResource(EPackageExternalResource::WorkspaceDomainFile, *Data->PackageName);
if (!PackageAr.IsValid())
{
UE_LOG(LogVirtualization, Error, TEXT("FWorkspaceDomainPayloadProvider was unable to open the package '%s' for reading"),
*Data->PackageName);
return FCompressedBuffer();
}
PackageAr->Seek(PackageAr->TotalSize());
FPackageTrailer Trailer;
if (!Trailer.TryLoadBackwards(*PackageAr))
{
UE_LOG(LogVirtualization, Error, TEXT("FWorkspaceDomainPayloadProvider failed to load the package trailer from the package '%s'"),
*Data->PackageName);
return FCompressedBuffer();
}
FCompressedBuffer Payload = Trailer.LoadLocalPayload(Identifier, *PackageAr);
if (!Payload)
{
UE_LOG(LogVirtualization, Error, TEXT("FWorkspaceDomainPayloadProvider was uanble to load the payload '%s' from the package '%s'"),
*LexToString(Identifier),
*Data->PackageName);
return FCompressedBuffer();
}
if (Identifier != FIoHash(Payload.GetRawHash()))
{
UE_LOG(LogVirtualization, Error, TEXT("FWorkspaceDomainPayloadProvider loaded an incorrect payload from the package '%s'. Expected '%s' Loaded '%s'"),
*Data->PackageName,
*LexToString(Identifier),
*LexToString(Payload.GetRawHash()));
return FCompressedBuffer();
}
return Payload;
}
virtual uint64 GetPayloadSize(const FIoHash& Identifier) override
{
if (Identifier.IsZero())
{
return 0;
}
const FPayloadData* Data = PayloadLookupTable.Find(Identifier);
if (Data != nullptr)
{
return Data->SizeOnDisk;
}
else
{
UE_LOG(LogVirtualization, Error, TEXT("FWorkspaceDomainPayloadProvider was unable to find a payload with the identifier '%s'"),
*LexToString(Identifier));
return 0;
}
}
/* This structure holds additional info about the payload that we might need later */
struct FPayloadData
{
FPayloadData(uint64 InSizeOnDisk, const FString& InPackageName)
: SizeOnDisk(InSizeOnDisk)
, PackageName(InPackageName)
{
}
uint64 SizeOnDisk;
FString PackageName;
};
TMap<FIoHash, FPayloadData> PayloadLookupTable;
};
void VirtualizePackages(TConstArrayView<FString> PackagePaths, EVirtualizationOptions Options, FVirtualizationResult& OutResultInfo)
{
TRACE_CPUPROFILER_EVENT_SCOPE(UE::Virtualization::VirtualizePackages);
IVirtualizationSystem& System = IVirtualizationSystem::Get();
const double StartTime = FPlatformTime::Seconds();
ON_SCOPE_EXIT
{
OutResultInfo.TimeTaken = FPlatformTime::Seconds() - StartTime;
UE_LOG(LogVirtualization, Display, TEXT("Virtualization process complete"));
UE_LOG(LogVirtualization, Verbose, TEXT("Virtualization process took %.3f(s)"), OutResultInfo.TimeTaken);
};
FScopedSlowTask Progress(5.0f, LOCTEXT("Virtualization_Task", "Virtualizing Assets..."));
// Force the task to be visible otherwise it might not be shown if the initial progress frames are too fast
Progress.Visibility = ESlowTaskVisibility::ForceVisible;
Progress.MakeDialog();
// Other systems may have added errors to this array, we need to check so later we can determine if this function added any additional errors.
const int32 NumErrors = OutResultInfo.GetNumErrors();
struct FPackageInfo
{
FPackagePath Path;
FPackageTrailer Trailer;
TArray<FIoHash> LocalPayloads;
/** Index where the FPushRequest for this package can be found */
int32 PayloadIndex = INDEX_NONE;
bool bWasTrailerUpdated = false;
};
UE_LOG(LogVirtualization, Display, TEXT("Considering %d file(s) for virtualization"), PackagePaths.Num());
TArray<FPackageInfo> Packages;
Packages.Reserve(PackagePaths.Num());
Progress.EnterProgressFrame(1.0f);
// From the list of files to submit we need to find all of the valid packages that contain
// local payloads that need to be virtualized.
int64 TotalPackagesFound = 0;
int64 TotalOutOfDatePackages = 0;
int64 TotalPackageTrailersFound = 0;
int64 TotalPayloadsToVirtualize = 0;
TSet<FString> ConsideredPackages;
ConsideredPackages.Reserve(PackagePaths.Num());
for (const FString& AbsoluteFilePath : PackagePaths)
{
FPackagePath PackagePath = FPackagePath::FromLocalPath(AbsoluteFilePath);
bool bIsDuplicate = false;
ConsideredPackages.Add(PackagePath.GetPackageName(), &bIsDuplicate);
if (bIsDuplicate)
{
UE_LOG(LogVirtualization, Verbose, TEXT("Skipping duplicate package entry '%s'"), *AbsoluteFilePath);
continue; // Skip duplicate packages
}
// TODO: How to handle text packages?
if (FPackageName::IsPackageExtension(PackagePath.GetHeaderExtension()) || FPackageName::IsTextPackageExtension(PackagePath.GetHeaderExtension()))
{
TotalPackagesFound++;
FPackageTrailer Trailer;
if (FPackageTrailer::TryLoadFromPackage(PackagePath, Trailer))
{
TotalPackageTrailersFound++;
// The following is not expected to ever happen, currently we give a user facing error but it generally means that the asset is broken somehow.
ensureMsgf(Trailer.GetNumPayloads(EPayloadStorageType::Referenced) == 0, TEXT("Trying to virtualize a package that already contains payload references which the workspace file should not ever contain!"));
if (Trailer.GetNumPayloads(EPayloadStorageType::Referenced) > 0)
{
FText Message = FText::Format(LOCTEXT("Virtualization_PkgHasReferences", "Cannot virtualize the package '{1}' as it has referenced payloads in the trailer"),
FText::FromString(PackagePath.GetDebugName()));
OutResultInfo.AddError(MoveTemp(Message));
return;
}
FPackageInfo PkgInfo;
PkgInfo.Path = MoveTemp(PackagePath);
PkgInfo.Trailer = MoveTemp(Trailer);
PkgInfo.LocalPayloads = PkgInfo.Trailer.GetPayloads(EPayloadFilter::CanVirtualize);
if (!PkgInfo.LocalPayloads.IsEmpty())
{
TotalPayloadsToVirtualize += PkgInfo.LocalPayloads.Num();
Packages.Emplace(MoveTemp(PkgInfo));
}
}
else if(Utils::FindTrailerFailedReason(PackagePath) == Utils::ETrailerFailedReason::OutOfDate)
{
TotalOutOfDatePackages++;
}
}
}
const int32 NumSkippedPackages = PackagePaths.Num() - ConsideredPackages.Num();
ConsideredPackages.Empty();
UE_LOG(LogVirtualization, Display, TEXT("Found %" INT64_FMT " package(s), %" INT64_FMT " of which had payload trailers"), TotalPackagesFound, TotalPackageTrailersFound);
UE_CLOG(NumSkippedPackages > 0, LogVirtualization, Warning, TEXT("Discarded %d duplicate package paths"), NumSkippedPackages);
UE_CLOG(TotalOutOfDatePackages > 0, LogVirtualization, Warning, TEXT("Found %" INT64_FMT " package(s) that are out of date and need resaving"), TotalOutOfDatePackages);
// TODO: Currently not all of the filtering is done as package save time, so some of the local payloads may not get virtualized.
// When/if we move all filtering to package save we can change this log message to state that the local payloads *will* be virtualized.
UE_LOG(LogVirtualization, Display, TEXT("Found %" INT64_FMT " locally stored payload(s) in %d package(s) that maybe need to be virtualized"), TotalPayloadsToVirtualize, Packages.Num());
Progress.EnterProgressFrame(1.0f);
// TODO Optimization: We might want to check for duplicate payloads and remove them at this point
// Build up the info in the payload provider and the final array of payload push requests
FWorkspaceDomainPayloadProvider PayloadProvider;
TArray<Virtualization::FPushRequest> PayloadsToSubmit;
PayloadsToSubmit.Reserve( IntCastChecked<int32>(TotalPayloadsToVirtualize) );
for (FPackageInfo& PackageInfo : Packages)
{
check(!PackageInfo.LocalPayloads.IsEmpty());
PackageInfo.PayloadIndex = PayloadsToSubmit.Num();
for (const FIoHash& PayloadId : PackageInfo.LocalPayloads)
{
const uint64 SizeOnDisk = PackageInfo.Trailer.FindPayloadSizeOnDisk(PayloadId);
PayloadProvider.RegisterPayload(PayloadId, SizeOnDisk, PackageInfo.Path.GetPackageName());
PayloadsToSubmit.Emplace(PayloadId, PayloadProvider, PackageInfo.Path.GetPackageName());
}
}
// TODO: We should be able to do both Cache and Persistent pushes in the same call
// Push payloads to cache storage
Progress.EnterProgressFrame(1.0f);
if(System.IsPushingEnabled(EStorageType::Cache))
{
UE_LOG(LogVirtualization, Display, TEXT("Pushing payload(s) to EStorageType::Cache storage..."));
if (!System.PushData(PayloadsToSubmit, EStorageType::Cache))
{
// Caching is not critical to the process so we only warn if it fails
UE_LOG(LogVirtualization, Warning, TEXT("Failed to push to EStorageType::Cache storage"));
}
int64 TotalPayloadsCached = 0;
for (Virtualization::FPushRequest& Request : PayloadsToSubmit)
{
TotalPayloadsCached += Request.GetResult().WasPushed() ? 1 : 0;
// TODO: This really shouldn't be required, fix when we allow both pushes to be done in the same call
// Reset the status for the persistent storage push
Request.ResetResult();
}
UE_LOG(LogVirtualization, Display, TEXT("Pushed %" INT64_FMT " payload(s) to cached storage"), TotalPayloadsCached);
}
else
{
UE_LOG(LogVirtualization, Display, TEXT("Pushing payload(s) to cached storage is disbled, skipping"));
}
// Push payloads to persistent storage
{
Progress.EnterProgressFrame(1.0f);
UE_LOG(LogVirtualization, Display, TEXT("Pushing payload(s) to EStorageType::Persistent storage..."));
if (!System.PushData(PayloadsToSubmit, EStorageType::Persistent))
{
FText Message = LOCTEXT("Virtualization_PushFailure", "Failed to push payloads");
OutResultInfo.AddError(MoveTemp(Message));
return;
}
int64 TotalPayloadsVirtualized = 0;
for (const Virtualization::FPushRequest& Request : PayloadsToSubmit)
{
TotalPayloadsVirtualized += Request.GetResult().WasPushed() ? 1 : 0;
}
UE_LOG(LogVirtualization, Display, TEXT("Pushed %" INT64_FMT " payload(s) to EStorageType::Persistent storage"), TotalPayloadsVirtualized);
}
// Update the package info for the submitted payloads
for (FPackageInfo& PackageInfo : Packages)
{
for (int32 Index = 0; Index < PackageInfo.LocalPayloads.Num(); ++Index)
{
const Virtualization::FPushRequest& Request = PayloadsToSubmit[PackageInfo.PayloadIndex + Index];
check(Request.GetIdentifier() == PackageInfo.LocalPayloads[Index]);
if (Request.GetResult().IsVirtualized())
{
if (PackageInfo.Trailer.UpdatePayloadAsVirtualized(Request.GetIdentifier()))
{
PackageInfo.bWasTrailerUpdated = true;
}
else
{
FText Message = FText::Format( LOCTEXT("Virtualization_UpdateStatusFailed", "Unable to update the status for the payload '{0}' in the package '{1}'"),
FText::FromString(LexToString(Request.GetIdentifier())),
FText::FromString(PackageInfo.Path.GetDebugName()));
OutResultInfo.AddError(MoveTemp(Message));
return;
}
}
}
}
// If the process has created new errors by this point we should early out before we actually start making changes on disk
if (NumErrors != OutResultInfo.GetNumErrors())
{
return;
}
Progress.EnterProgressFrame(1.0f);
struct FPackageReplacement
{
FPackagePath Path;
FPackageTrailer Trailer;
};
TArray<FPackageReplacement> PackagesToReplace;
PackagesToReplace.Reserve(Packages.Num());
for (FPackageInfo& PackageInfo : Packages)
{
if (PackageInfo.bWasTrailerUpdated)
{
PackagesToReplace.Add({MoveTemp(PackageInfo.Path), MoveTemp(PackageInfo.Trailer)});
}
}
Packages.Empty(); // No longer used, the useful data have been moved to PackagesToReplace
if (PackagesToReplace.IsEmpty())
{
UE_LOG(LogVirtualization, Display, TEXT("No packages need to be updated on disk"));
return;
}
UE_LOG(LogVirtualization, Display, TEXT("%d package(s) had their trailer container modified and need to be updated"), PackagesToReplace.Num());
{
// We need to reset the loader of any loaded package that we want save over on disk so that
// the file lock is relinquished
TRACE_CPUPROFILER_EVENT_SCOPE(UE::Virtualization::VirtualizePackages::ResetLoaders);
UE_LOG(LogVirtualization, Display, TEXT("Detaching loaded packages from disk..."));
int32 NumPackagesReset = 0;
for (const FPackageReplacement& PackageInfo : PackagesToReplace)
{
UPackage* LoadedPackage = FindObjectFast<UPackage>(nullptr, PackageInfo.Path.GetPackageFName());
if (LoadedPackage != nullptr)
{
UE_LOG(LogVirtualization, Verbose, TEXT("Detaching '%s'"), *PackageInfo.Path.GetDebugName());
// TODO: Consider using the batch API
ResetLoadersForSave(LoadedPackage, *PackageInfo.Path.GetLocalFullPath());
NumPackagesReset++;
}
}
UE_LOG(LogVirtualization, Display, TEXT("Reset the loaders of %d package(s)"), NumPackagesReset);
}
if (EnumHasAnyFlags(Options, EVirtualizationOptions::Checkout))
{
TRACE_CPUPROFILER_EVENT_SCOPE(UE::Virtualization::VirtualizePackages::Checkout);
UE_LOG(LogVirtualization, Display, TEXT("Checking out packages from revision control..."));
TArray<FString> FilesToCheckState;
FilesToCheckState.Reserve(PackagesToReplace.Num());
for (const FPackageReplacement& PackageInfo : PackagesToReplace)
{
FilesToCheckState.Add(PackageInfo.Path.GetLocalFullPath());
}
if (!TryCheckoutFiles(FilesToCheckState, OutResultInfo.Errors, &OutResultInfo.CheckedOutPackages))
{
return;
}
UE_LOG(LogVirtualization, Display, TEXT("Checked out %d package(s)"), OutResultInfo.CheckedOutPackages.Num());
}
{
TRACE_CPUPROFILER_EVENT_SCOPE(UE::Virtualization::VirtualizePackages::EnsureWritePermissions);
UE_CLOG(!PackagesToReplace.IsEmpty(), LogVirtualization, Display, TEXT("Checking packages for write access permission..."));
// Now check to see if there are package files that cannot be edited because they are read only
int32 NumSkipped = 0;
for (int32 Index = 0; Index < PackagesToReplace.Num(); ++Index)
{
const FPackageReplacement& PackageInfo = PackagesToReplace[Index];
if (!CanWriteToFile(PackageInfo.Path.GetLocalFullPath()))
{
// Technically the package could have local payloads that won't be virtualized due to filtering or min payload sizes and so the
// following warning is misleading. This will be solved if we move that evaluation to the point of saving a package.
// If not then we probably need to extend QueryPayloadStatuses to test filtering etc as well, then check for potential package
// modification after that.
// Long term, the stand alone tool should be able to request the UnrealEditor relinquish the lock on the package file so this becomes
// less of a problem.
FText Message = FText::Format(LOCTEXT("Virtualization_PkgLocked", "The package file '{0}' has local payloads but is locked for modification and cannot be virtualized, this package will be skipped!"),
FText::FromString(PackageInfo.Path.GetDebugName()));
UE_LOG(LogVirtualization, Warning, TEXT("%s"), *Message.ToString());
PackagesToReplace.RemoveAt(Index--);
NumSkipped++;
}
}
UE_CLOG(NumSkipped > 0, LogVirtualization, Warning, TEXT("Skipped %d package(s)"), NumSkipped);
UE_CLOG(NumSkipped == 0, LogVirtualization, Display, TEXT("All packages have write permission"));
}
{
// Since we had no errors we can now replace all of the packages that were virtualized data with the virtualized replacement file.
TRACE_CPUPROFILER_EVENT_SCOPE(UE::Virtualization::VirtualizePackages::ReplacePackages);
UE_LOG(LogVirtualization, Display, TEXT("Replacing old packages with the virtualized version..."));
int64 OriginalSizeTotal = 0;
int64 ReducedSizeTotal = 0;
for (const FPackageReplacement& PackageInfo : PackagesToReplace)
{
const FString OriginalPackagePath = PackageInfo.Path.GetLocalFullPath();
const FString NewPackagePath = DuplicatePackageWithUpdatedTrailer(OriginalPackagePath, PackageInfo.Trailer, OutResultInfo.Errors);
if (NewPackagePath.IsEmpty())
{
// Duplication failed so skip this package for now. The error will be in OutResultInfo.Errors
continue;
}
const int64 OriginalSize = IFileManager::Get().GetStatData(*OriginalPackagePath).FileSize;
const int64 ReducedSize = IFileManager::Get().GetStatData(*NewPackagePath).FileSize;
if (IFileManager::Get().Move(*OriginalPackagePath, *NewPackagePath))
{
OutResultInfo.VirtualizedPackages.Add(OriginalPackagePath);
OriginalSizeTotal += OriginalSize;
ReducedSizeTotal += ReducedSize;
UE_LOG(LogVirtualization, Verbose, TEXT("Reducing %s: %s -> %s"),
*PackageInfo.Path.GetDebugName(),
*FText::AsMemory(OriginalSize).ToString(),
*FText::AsMemory(ReducedSize).ToString());
}
else
{
FText Message = FText::Format(LOCTEXT("Virtualization_MoveFailed", "Unable to replace the package '{0}' with the virtualized version"),
FText::FromString(PackageInfo.Path.GetDebugName()));
OutResultInfo.AddError(MoveTemp(Message));
continue;
}
}
if (OriginalSizeTotal != ReducedSizeTotal)
{
UE_LOG(LogVirtualization, Display, TEXT("Total Reduction %s (%s -> %s)"),
*FText::AsMemory(OriginalSizeTotal - ReducedSizeTotal).ToString(),
*FText::AsMemory(OriginalSizeTotal).ToString(),
*FText::AsMemory(ReducedSizeTotal).ToString());
}
UE_LOG(LogVirtualization, Display, TEXT("Replaced %d package(s)"), OutResultInfo.VirtualizedPackages.Num());
}
}
} // namespace UE::Virtualization
#undef LOCTEXT_NAMESPACE