Files
UnrealEngine/Engine/Source/Editor/UnrealEd/Private/Cooker/IncrementalValidatePackageWriter.cpp
2025-05-18 13:04:45 +08:00

1260 lines
42 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "Cooker/IncrementalValidatePackageWriter.h"
#include "Cooker/CompactBinaryTCP.h"
#include "Cooker/CookGenerationHelper.h"
#include "Cooker/CookPackageData.h"
#include "Cooker/MPCollector.h"
#include "CookOnTheSide/CookLog.h"
#include "CookOnTheSide/CookOnTheFlyServer.h"
#include "HAL/FileManager.h"
#include "Logging/LogMacros.h"
#include "Misc/CommandLine.h"
#include "Misc/ConfigCacheIni.h"
#include "Misc/OutputDeviceHelper.h"
#include "Misc/Parse.h"
#include "Misc/Paths.h"
#include "Serialization/NameAsStringProxyArchive.h"
#include "Templates/UniquePtr.h"
DEFINE_LOG_CATEGORY_STATIC(LogIncrementalValidate, Log, All);
constexpr FStringView IncrementalValidateFilename(TEXTVIEW("IncrementalValidate.bin"));
class FIncrementalValidateMPCollector : public UE::Cook::IMPCollector
{
public:
FIncrementalValidateMPCollector(FIncrementalValidatePackageWriter* InOwner) : Owner(InOwner) {}
virtual FGuid GetMessageType() const { return MessageType; }
virtual const TCHAR* GetDebugName() const { return TEXT("IncrementalValidateMPCollector"); }
virtual void ServerTick(UE::Cook::FMPCollectorServerTickContext& Context) override;
virtual void ClientTickPackage(UE::Cook::FMPCollectorClientTickPackageContext& Context) override;
virtual void ServerTickPackage(UE::Cook::FMPCollectorServerTickPackageContext& Context);
virtual void ClientReceiveMessage(UE::Cook::FMPCollectorClientMessageContext& Context, FCbObjectView Message) override;
virtual void ServerReceiveMessage(UE::Cook::FMPCollectorServerMessageContext& Context, FCbObjectView Message) override;
public:
static FGuid MessageType;
private:
enum class EMessageSubtype : uint8
{
ServerToClient_WorkerStartup,
ClientToServer_ReplIsAnotherSaveNeeded,
ServerToClient_ReplUpdatePackageModificationStatus,
Invalid
};
bool TryWritePackageStatus(FCbWriter& Writer, FName PackageName);
void ReadAndSyncPackageStatus(FCbObjectView Message, FName PackageName);
FIncrementalValidatePackageWriter* Owner;
};
FCbWriter& operator<<(FCbWriter& Writer, const FIncrementalValidatePackageWriter::FMessage& Message)
{
Writer.BeginObject();
Writer << "Text" << Message.Text;
static_assert(sizeof(Message.Verbosity) == sizeof(uint8));
Writer << "Verbosity" << (uint8)Message.Verbosity;
Writer.EndObject();
return Writer;
}
bool LoadFromCompactBinary(FCbFieldView Field, FIncrementalValidatePackageWriter::FMessage& Message)
{
bool bOk = !Field.HasError();
if (bOk && LoadFromCompactBinary(Field["Text"], Message.Text))
{
uint8 Verbosity = ELogVerbosity::NumVerbosity;
bOk = LoadFromCompactBinary(Field["Verbosity"], Verbosity) && Verbosity < ELogVerbosity::NumVerbosity;
if (bOk)
{
Message.Verbosity = (ELogVerbosity::Type)Verbosity;
}
}
if (!bOk)
{
Message = FIncrementalValidatePackageWriter::FMessage();
}
return bOk;
}
FCbWriter& operator<<(FCbWriter& Writer, FIncrementalValidatePackageWriter::EPackageStatus Status)
{
Writer << (uint8)Status;
return Writer;
}
bool LoadFromCompactBinary(FCbFieldView Field, FIncrementalValidatePackageWriter::EPackageStatus& Status)
{
uint8 StatusInteger = (uint8)FIncrementalValidatePackageWriter::EPackageStatus::Count;
if (!LoadFromCompactBinary(Field, StatusInteger))
{
UE_LOG(LogIncrementalValidate, Error, TEXT("Failed to deserialize package status."));
}
else if (StatusInteger >= (uint8)FIncrementalValidatePackageWriter::EPackageStatus::Count)
{
UE_LOG(LogIncrementalValidate, Error, TEXT("Unexpected package status deserialized: %d"), StatusInteger);
}
else
{
Status = (FIncrementalValidatePackageWriter::EPackageStatus)StatusInteger;
return true;
}
return false;
}
FArchive& operator<<(FArchive& Ar, FIncrementalValidatePackageWriter::FPackageStatusInfo& Info)
{
Ar << Info.Status;
bool bHasAssetClass = Info.AssetClass.IsValid();
Ar << bHasAssetClass;
if (bHasAssetClass)
{
Ar << Info.AssetClass;
}
return Ar;
}
FCbWriter& operator<<(FCbWriter& Writer, const FIncrementalValidatePackageWriter::FPackageStatusInfo& Info)
{
Writer.BeginArray();
Writer << Info.Status;
if (Info.AssetClass.IsValid())
{
Writer << Info.AssetClass;
}
Writer.EndArray();
return Writer;
}
bool LoadFromCompactBinary(FCbFieldView Field, FIncrementalValidatePackageWriter::FPackageStatusInfo& Info)
{
FCbArrayView ArrayField = Field.AsArrayView();
if (ArrayField.Num() < 1)
{
return false;
}
FCbFieldViewIterator Iter = ArrayField.CreateViewIterator();
bool bOk = true;
bOk = LoadFromCompactBinary(*(Iter++), Info.Status) & bOk;
if (ArrayField.Num() >= 2)
{
bOk = LoadFromCompactBinary(*(Iter++), Info.AssetClass) & bOk;
}
return bOk;
}
void FIncrementalValidateMPCollector::ServerTick(UE::Cook::FMPCollectorServerTickContext& Context)
{
static_assert(sizeof(EMessageSubtype) == sizeof(uint8));
if (Context.GetEventType() == UE::Cook::FMPCollectorServerTickContext::EServerEventType::WorkerStartup)
{
FCbWriter Writer;
Writer.BeginObject();
Writer << "MessageSubtype" << (uint8)EMessageSubtype::ServerToClient_WorkerStartup;
Writer << "PackageStatusMap" << Owner->PackageStatusMap;
Writer << "PackageMessageMap" << Owner->PackageMessageMap;
Writer.EndObject();
Context.AddMessage(Writer.Save().AsObject());
}
}
void FIncrementalValidateMPCollector::ClientTickPackage(UE::Cook::FMPCollectorClientTickPackageContext& Context)
{
static_assert(sizeof(EMessageSubtype) == sizeof(uint8));
FCbWriter Writer;
Writer.BeginObject();
Writer << "MessageSubtype" << (uint8)EMessageSubtype::ClientToServer_ReplIsAnotherSaveNeeded;
const FName PackageName = Context.GetPackageName();
if (PackageName.IsNone())
{
UE_LOG(LogCook, Error, TEXT("Context does not contain a valid package name."))
// It's safe to continue because TryWritePackageStatus will return false if the name is none.
// The error is logged here to make the call site clear
}
if (TryWritePackageStatus(Writer, PackageName))
{
Writer.EndObject();
Context.AddMessage(Writer.Save().AsObject());
}
}
void FIncrementalValidateMPCollector::ServerTickPackage(UE::Cook::FMPCollectorServerTickPackageContext& Context)
{
static_assert(sizeof(EMessageSubtype) == sizeof(uint8));
FCbWriter Writer;
Writer.BeginObject();
Writer << "MessageSubtype" << (uint8)EMessageSubtype::ServerToClient_ReplUpdatePackageModificationStatus;
const FName PackageName = Context.GetPackageName();
if (PackageName.IsNone())
{
UE_LOG(LogCook, Error, TEXT("Context does not contain a valid package name."))
// It's safe to continue because TryWritePackageStatus will return false if the name is none.
// The error is logged here to make the call site clear
}
if (TryWritePackageStatus(Writer, PackageName))
{
Writer.EndObject();
Context.AddMessage(Writer.Save().AsObject());
}
}
void FIncrementalValidateMPCollector::ClientReceiveMessage(UE::Cook::FMPCollectorClientMessageContext& Context, FCbObjectView Message)
{
uint8 MessageSubtypeAsInteger = (uint8)EMessageSubtype::Invalid;
if (LoadFromCompactBinary(Message["MessageSubtype"], MessageSubtypeAsInteger))
{
switch ((EMessageSubtype)MessageSubtypeAsInteger)
{
case EMessageSubtype::ServerToClient_WorkerStartup:
{
bool bOk = LoadFromCompactBinary(Message["PackageStatusMap"], Owner->PackageStatusMap);
bOk = bOk && LoadFromCompactBinary(Message["PackageMessageMap"], Owner->PackageMessageMap);
check(bOk); // If we fail this, we will fail to get anything right during the rest of the validation. Better to terminate quickly.
break;
}
case EMessageSubtype::ServerToClient_ReplUpdatePackageModificationStatus:
{
FName PackageName = Context.GetPackageName();
if (!PackageName.IsNone())
{
ReadAndSyncPackageStatus(Message, PackageName);
}
else
{
UE_LOG(LogCook, Error, TEXT("Cannot process ServerToClient_ReplUpdatePackageModificationStatus without a valid package name in the current context."));
}
break;
}
default:
{
UE_LOG(LogCook, Error, TEXT("Unexpected message type: %d"), MessageSubtypeAsInteger);
break;
}
};
}
}
void FIncrementalValidateMPCollector::ServerReceiveMessage(UE::Cook::FMPCollectorServerMessageContext& Context, FCbObjectView Message)
{
uint8 MessageSubtypeAsInteger = (uint8)EMessageSubtype::Invalid;
if (LoadFromCompactBinary(Message["MessageSubtype"], MessageSubtypeAsInteger))
{
FName PackageName = Context.GetPackageName();
if (PackageName.IsNone())
{
UE_LOG(LogCook, Error, TEXT("Cannot process messages on server without a valid package name in the current context."));
}
else if (MessageSubtypeAsInteger == (uint8)EMessageSubtype::ClientToServer_ReplIsAnotherSaveNeeded)
{
ReadAndSyncPackageStatus(Message, PackageName);
Owner->MarkPackageCompletedOnDirector(PackageName, Context.GetWorkerId());
}
else
{
UE_LOG(LogCook, Error, TEXT("Unexpected message received. MessageSubtype == %d"), MessageSubtypeAsInteger);
}
}
else
{
UE_LOG(LogCook, Error, TEXT("Invalid message received. No MessageSubtype field available."));
}
}
FGuid FIncrementalValidateMPCollector::MessageType(TEXT("5E56C5D96F3B455E9452C15ADA601A71"));
bool FIncrementalValidateMPCollector::TryWritePackageStatus(FCbWriter& Writer, FName PackageName)
{
const FIncrementalValidatePackageWriter::FPackageStatusInfo* PackageStatus
= Owner->PackageStatusMap.Find(PackageName);
if (PackageStatus && PackageStatus->Status != FIncrementalValidatePackageWriter::EPackageStatus::NotYetProcessed)
{
Writer << "Status" << *PackageStatus;
const TArray<FIncrementalValidatePackageWriter::FMessage>* Messages = Owner->PackageMessageMap.Find(PackageName);
if (Messages != nullptr)
{
Writer << "MessageArray" << *Messages;
}
return true;
}
return false;
}
void FIncrementalValidateMPCollector::ReadAndSyncPackageStatus(FCbObjectView Message, FName PackageName)
{
FIncrementalValidatePackageWriter::FPackageStatusInfo Info;
bool bOk = LoadFromCompactBinary(Message["Status"], Info);
if (bOk)
{
Owner->PackageStatusMap.FindOrAdd(PackageName) = MoveTemp(Info);
if (Message.FindView("MessageArray").HasValue())
{
TArray<FIncrementalValidatePackageWriter::FMessage>& MessageArray = Owner->PackageMessageMap.FindOrAdd(PackageName);
bOk = LoadFromCompactBinary(Message["MessageArray"], MessageArray);
}
}
if (!bOk)
{
UE_LOG(LogCook, Error, TEXT("Invalid message received in ReadAndSyncPackageStatus. Failed to load Info from Message[\"Status\"] for package \"%s\""),
*PackageName.ToString());
}
}
FIncrementalValidatePackageWriter::FIncrementalValidatePackageWriter(UCookOnTheFlyServer& InCOTFS,
TUniquePtr<ICookedPackageWriter>&& InInner, EPhase InPhase, const FString& ResolvedMetadataPath,
UE::Cook::FDeterminismManager* InDeterminismManager)
: FDiffPackageWriter(MoveTemp(InInner), InDeterminismManager)
, MetadataPath(ResolvedMetadataPath)
, COTFS(InCOTFS)
, Phase(InPhase)
{
COTFS.RegisterCollector(new FIncrementalValidateMPCollector(this));
Indent = FCString::Spc(FOutputDeviceHelper::FormatLogLine(ELogVerbosity::Warning,
LogIncrementalValidate.GetCategoryName(), TEXT(""), GPrintLogTimes).Len());
TArray<FString> IncrementalValidatePackageIgnoreList;
GConfig->GetArray(TEXT("IncrementalValidate"), TEXT("PackageIgnoreList"),
IncrementalValidatePackageIgnoreList, GEditorIni);
PackageIgnoreList.Reserve(IncrementalValidatePackageIgnoreList.Num());
for (const FString& PackageName : IncrementalValidatePackageIgnoreList)
{
PackageIgnoreList.Add(FName(FStringView(PackageName)));
}
LoggingSoftMaximum = -1;
GConfig->GetValue(TEXT("IncrementalValidate"), TEXT("LoggingSoftMaximum"), LoggingSoftMaximum, GEditorIni);
}
void FIncrementalValidatePackageWriter::BeginPackage(const FBeginPackageInfo& Info)
{
bPackageFirstPass = true;
switch (Phase)
{
case EPhase::AllInOnePhase:
if (GetPackageStatus(Info.PackageName) == EPackageStatus::DeclaredUnmodified_NotYetProcessed)
{
// Save to memory and look for diffs before saving it out to disk
SaveAction = ESaveAction::CheckForDiffs;
Super::BeginPackage(Info);
}
else
{
// Not incrementally skippable, so we expect it to change. No need to look for diffs, just save to disk.
if (bReadOnly)
{
SaveAction = ESaveAction::IgnoreResults;
}
else
{
SaveAction = ESaveAction::SaveToInner;
Inner->BeginPackage(Info);
}
}
break;
case EPhase::Phase1:
SaveAction = ESaveAction::CheckForDiffs;
Super::BeginPackage(Info);
break;
case EPhase::Phase2:
{
EPackageStatus PackageStatus = GetPackageStatus(Info.PackageName);
if (PackageStatus == EPackageStatus::DeclaredUnmodified_ConfirmedUnmodified ||
PackageStatus == EPackageStatus::DeclaredUnmodified_FoundModified_OnIgnoreList)
{
// Already saved in Phase 1; no need to diff it or save it now
SaveAction = ESaveAction::IgnoreResults;
}
else if (PackageStatus == EPackageStatus::DeclaredUnmodified_FoundModified_IndeterminismOrFalsePositive)
{
SaveAction = ESaveAction::CheckForDiffs;
Super::BeginPackage(Info);
}
else
{
// This is an IncrementallyModified package. It was found during Phase1 to be modified and would in a
// normal incremental cook be resaved rather than incrementally skipped. Resave it as normal.
SaveAction = ESaveAction::SaveToInner;
Inner->BeginPackage(Info);
}
break;
}
default:
checkNoEntry();
break;
}
}
void FIncrementalValidatePackageWriter::CommitPackage(FCommitPackageInfo&& Info)
{
switch (SaveAction)
{
case ESaveAction::CheckForDiffs:
Super::CommitPackage(MoveTemp(Info));
break;
case ESaveAction::SaveToInner:
Inner->CommitPackage(MoveTemp(Info));
break;
case ESaveAction::IgnoreResults:
break;
default:
checkNoEntry();
break;
}
}
void FIncrementalValidatePackageWriter::WritePackageData(const FPackageInfo& Info, FLargeMemoryWriter& ExportsArchive,
const TArray<FFileRegion>& FileRegions)
{
switch (SaveAction)
{
case ESaveAction::CheckForDiffs:
Super::WritePackageData(Info, ExportsArchive, FileRegions);
break;
case ESaveAction::SaveToInner:
Inner->WritePackageData(Info, ExportsArchive, FileRegions);
break;
case ESaveAction::IgnoreResults:
break;
default:
checkNoEntry();
break;
}
}
void FIncrementalValidatePackageWriter::WriteBulkData(const FBulkDataInfo& Info, const FIoBuffer& BulkData,
const TArray<FFileRegion>& FileRegions)
{
switch (SaveAction)
{
case ESaveAction::CheckForDiffs:
Super::WriteBulkData(Info, BulkData, FileRegions);
break;
case ESaveAction::SaveToInner:
Inner->WriteBulkData(Info, BulkData, FileRegions);
break;
case ESaveAction::IgnoreResults:
break;
default:
checkNoEntry();
break;
}
}
void FIncrementalValidatePackageWriter::WriteAdditionalFile(const FAdditionalFileInfo& Info, const FIoBuffer& FileData)
{
switch (SaveAction)
{
case ESaveAction::CheckForDiffs:
Super::WriteAdditionalFile(Info, FileData);
break;
case ESaveAction::SaveToInner:
Inner->WriteAdditionalFile(Info, FileData);
break;
case ESaveAction::IgnoreResults:
break;
default:
checkNoEntry();
break;
}
}
void FIncrementalValidatePackageWriter::WriteLinkerAdditionalData(const FLinkerAdditionalDataInfo& Info,
const FIoBuffer& Data, const TArray<FFileRegion>& FileRegions)
{
switch (SaveAction)
{
case ESaveAction::CheckForDiffs:
Super::WriteLinkerAdditionalData(Info, Data, FileRegions);
break;
case ESaveAction::SaveToInner:
Inner->WriteLinkerAdditionalData(Info, Data, FileRegions);
break;
case ESaveAction::IgnoreResults:
break;
default:
checkNoEntry();
break;
}
}
void FIncrementalValidatePackageWriter::WritePackageTrailer(const FPackageTrailerInfo& Info, const FIoBuffer& Data)
{
switch (SaveAction)
{
case ESaveAction::CheckForDiffs:
Super::WritePackageTrailer(Info, Data);
break;
case ESaveAction::SaveToInner:
Inner->WritePackageTrailer(Info, Data);
break;
case ESaveAction::IgnoreResults:
break;
default:
checkNoEntry();
break;
}
}
int64 FIncrementalValidatePackageWriter::GetExportsFooterSize()
{
switch (SaveAction)
{
case ESaveAction::CheckForDiffs:
return Super::GetExportsFooterSize();
case ESaveAction::SaveToInner:
return Inner->GetExportsFooterSize();
case ESaveAction::IgnoreResults:
return 0;
default:
checkNoEntry();
return 0;
}
}
TUniquePtr<FLargeMemoryWriter> FIncrementalValidatePackageWriter::CreateLinkerArchive(FName PackageName, UObject* Asset, uint16 MultiOutputIndex)
{
if (Asset)
{
PackageStatusMap.FindOrAdd(PackageName).AssetClass = Asset->GetClass()->GetClassPathName();
}
switch (SaveAction)
{
case ESaveAction::CheckForDiffs:
return Super::CreateLinkerArchive(PackageName, Asset, MultiOutputIndex);
case ESaveAction::SaveToInner:
return Inner->CreateLinkerArchive(PackageName, Asset, MultiOutputIndex);
case ESaveAction::IgnoreResults:
return MakeUnique<FLargeMemoryWriter>();
default:
checkNoEntry();
return TUniquePtr<FLargeMemoryWriter>();
}
}
TUniquePtr<FLargeMemoryWriter> FIncrementalValidatePackageWriter::CreateLinkerExportsArchive(FName PackageName, UObject* Asset, uint16 MultiOutputIndex)
{
switch (SaveAction)
{
case ESaveAction::CheckForDiffs:
return Super::CreateLinkerExportsArchive(PackageName, Asset, MultiOutputIndex);
case ESaveAction::SaveToInner:
return Inner->CreateLinkerExportsArchive(PackageName, Asset, MultiOutputIndex);
case ESaveAction::IgnoreResults:
return MakeUnique<FLargeMemoryWriter>();
default:
checkNoEntry();
return TUniquePtr<FLargeMemoryWriter>();
}
}
bool FIncrementalValidatePackageWriter::IsPreSaveCompleted() const
{
return !bPackageFirstPass;
}
ICookedPackageWriter::FCookCapabilities FIncrementalValidatePackageWriter::GetCookCapabilities() const
{
FCookCapabilities Result = Super::GetCookCapabilities();
Result.bReadOnly = bReadOnly;
Result.bOverridesPackageModificationStatus = true;
return Result;
}
void FIncrementalValidatePackageWriter::Initialize(const FCookInfo& CookInfo)
{
switch (Phase)
{
case EPhase::AllInOnePhase:
if (CookInfo.bFullBuild)
{
UE_CLOG(COTFS.GetCookMode() != ECookMode::CookWorker,
LogIncrementalValidate, Display,
TEXT("The cook is running non-incrementally. All packages are reported \"modified\" and will be resaved as in a normal cook."));
bReadOnly = false;
}
else
{
bReadOnly = !FParse::Param(FCommandLine::Get(), TEXT("IncrementalValidateAllowWrite"));
}
break;
case EPhase::Phase1:
if (CookInfo.bFullBuild)
{
UE_CLOG(COTFS.GetCookMode() != ECookMode::CookWorker,
LogIncrementalValidate, Display,
TEXT("The cook is running non-incrementally. All packages are reported \"modified\" and will be resaved during the final IncrementalValidate phase."));
}
bReadOnly = false;
break;
case EPhase::Phase2:
if (CookInfo.bFullBuild)
{
UE_CLOG(COTFS.GetCookMode() != ECookMode::CookWorker,
LogIncrementalValidate, Display,
TEXT("The cook is running non-incrementally. Packages that were incrementally skipped and found valid will be resaved anyway."));
}
bReadOnly = false;
break;
default:
checkNoEntry();
break;
}
Super::Initialize(CookInfo);
}
void FIncrementalValidatePackageWriter::UpdatePackageModificationStatus(FName PackageName, bool bIncrementallyUnmodified,
bool& bInOutShouldIncrementallySkip)
{
// We need to not skip previously cooked generator packages, if they were modified and we're read only we still
// need to cook them so we can investigate their generated packages. Look up whether they were a generator in
// the previous cook results.
bool bKnownGenerator = false;
if (bIncrementallyUnmodified)
{
// GenerationHelpers were created for previously cooked generators by UCOTFS::PopulateCookedPackages.
UE::Cook::FPackageData* PackageData = COTFS.PackageDatas->FindPackageDataByPackageName(PackageName);
if (PackageData)
{
TRefCountPtr<UE::Cook::FGenerationHelper> GenerationHelper = PackageData->GetGenerationHelper();
if (GenerationHelper)
{
bKnownGenerator = true;
}
}
}
switch (Phase)
{
case EPhase::AllInOnePhase:
// Save the input value for bIncrementallyUnmodified, and report skippable for the modified if possible
if (bIncrementallyUnmodified)
{
SetPackageStatus(PackageName, EPackageStatus::DeclaredUnmodified_NotYetProcessed);
bInOutShouldIncrementallySkip = false;
}
else
{
SetPackageStatus(PackageName, EPackageStatus::DeclaredModified_WillNotVerify);
if (!bKnownGenerator && bReadOnly)
{
bInOutShouldIncrementallySkip = true;
}
}
break;
case EPhase::Phase1:
// Save the incrementally unmodified packages to verify their diffs. Skip the non-generator incrementally
// modified packages. Do not skip generator packages; we need to save them to test their generated packages.
bInOutShouldIncrementallySkip = !bIncrementallyUnmodified && !bKnownGenerator;
if (!bIncrementallyUnmodified)
{
SetPackageStatus(PackageName, EPackageStatus::DeclaredModified_WillNotVerify);
}
else
{
SetPackageStatus(PackageName, EPackageStatus::DeclaredUnmodified_NotYetProcessed);
}
break;
case EPhase::Phase2:
{
// Ignore the Unmodified flag from this cook phase. Skip the packages that were found to
// be IncrementallyValidated from Phase1. Save the packages that phase1 found modified; this phase
// is responsible for getting those resaved. Reexecute save for the packages that were IncrementalFailed
// from phase1, so we can test whether they are indeterministic. Always save generators so we can
// test their generated packages.
EPackageStatus PackageStatus = GetPackageStatus(PackageName);
bInOutShouldIncrementallySkip =
(PackageStatus == EPackageStatus::DeclaredUnmodified_ConfirmedUnmodified
|| PackageStatus == EPackageStatus::DeclaredUnmodified_FoundModified_OnIgnoreList)
&& !bKnownGenerator;
break;
}
default:
checkNoEntry();
break;
}
bool bInnerIncrementallyUnmodified = bInOutShouldIncrementallySkip;
bool bInnerInOutShouldIncrementallySkip = bInOutShouldIncrementallySkip;
Inner->UpdatePackageModificationStatus(PackageName, bInnerIncrementallyUnmodified, bInnerInOutShouldIncrementallySkip);
checkf(bInnerInOutShouldIncrementallySkip == bInnerIncrementallyUnmodified,
TEXT("FIncrementalValidatePackageWriter is not supported with an Inner that modifies bInOutShouldIncrementallySkip."));
}
void FIncrementalValidatePackageWriter::BeginCook(const FCookInfo& Info)
{
switch (Phase)
{
case EPhase::AllInOnePhase:
if (bReadOnly)
{
UE_CLOG(COTFS.GetCookMode() != ECookMode::CookWorker,
LogIncrementalValidate, Display,
TEXT("-IncrementalValidateAllowWrite not present, read-only mode. Running -diffonly on all packages that were found to be incrementally unmodified."));
}
else
{
UE_CLOG(COTFS.GetCookMode() != ECookMode::CookWorker,
LogIncrementalValidate, Display,
TEXT("-IncrementalValidateAllowWrite is present, writable mode. Resaving packages as in a normal cook, but also running -diffonly on all packages that were found to be incrementally unmodified."));
}
if (Info.bFullBuild)
{
UE_CLOG(COTFS.GetCookMode() != ECookMode::CookWorker,
LogIncrementalValidate, Error,
TEXT("IncrementalValidate was bypassed on this run; it is a full cook and all packages are marked incrementally modified."));
}
break;
case EPhase::Phase1:
UE_CLOG(COTFS.GetCookMode() != ECookMode::CookWorker,
LogIncrementalValidate, Display,
TEXT("Phase1: running -diffonly and a resave on all packages discovered to be incrementally unmodified."));
if (Info.bFullBuild)
{
UE_CLOG(COTFS.GetCookMode() != ECookMode::CookWorker,
LogIncrementalValidate, Error,
TEXT("IncrementalValidate was bypassed on this run; it is a full cook and all packages are marked incrementally modified."));
}
break;
case EPhase::Phase2:
{
Load();
FStatusCounts StatusCounts = CountPackagesByStatus();
UE_CLOG(COTFS.GetCookMode() != ECookMode::CookWorker,
LogIncrementalValidate, Display,
TEXT("Phase2: %d packages were found during Phase1 to be incrementally unmodified but had differences. "
"Running -diffonly on them again to check whether the differences are due to indeterminism or to FalsePositiveIncrementalSkips."),
StatusCounts[EPackageStatus::DeclaredUnmodified_FoundModified_IndeterminismOrFalsePositive]);
UE_CLOG(COTFS.GetCookMode() != ECookMode::CookWorker,
LogIncrementalValidate, Display,
TEXT("%d packages were found during Phase1 to be modified or new and will be resaved."),
StatusCounts[EPackageStatus::DeclaredModified_WillNotVerify]);
break;
}
default:
checkNoEntry();
break;
}
Super::BeginCook(Info);
}
void FIncrementalValidatePackageWriter::EndCook(const FCookInfo& Info)
{
Super::EndCook(Info);
FStatusCounts StatusCounts = CountPackagesByStatus();
switch (Phase)
{
case EPhase::AllInOnePhase:
{
int32 DetectedUnmodified = StatusCounts[EPackageStatus::DeclaredUnmodified_ConfirmedUnmodified]
+ StatusCounts[EPackageStatus::DeclaredUnmodified_FoundModified_FalsePositive]
+ StatusCounts[EPackageStatus::DeclaredUnmodified_FoundModified_OnIgnoreList];
UE_CLOG(COTFS.GetCookMode() != ECookMode::CookWorker,
LogIncrementalValidate, Display,
TEXT("Modified: %d. DetectedUnmodified: %d. ValidatedUnmodified: %d. IncrementalSkipFalsePositive: %d."),
StatusCounts[EPackageStatus::DeclaredModified_WillNotVerify],
DetectedUnmodified,
StatusCounts[EPackageStatus::DeclaredUnmodified_ConfirmedUnmodified]
+ StatusCounts[EPackageStatus::DeclaredUnmodified_FoundModified_OnIgnoreList],
StatusCounts[EPackageStatus::DeclaredUnmodified_FoundModified_FalsePositive]);
FString Message = FString::Printf(TEXT("Packages Incrementally Skipped: %d: IncrementalSkipFalsePositive: %d."),
DetectedUnmodified,
StatusCounts[EPackageStatus::DeclaredUnmodified_FoundModified_FalsePositive]);
if (StatusCounts[EPackageStatus::DeclaredUnmodified_FoundModified_FalsePositive] > 0)
{
TStringBuilder<1024> MessageWithDiagnostics;
MessageWithDiagnostics << Message;
TMap<FTopLevelAssetPath, TArray<FName>> ClassFalsePositiveCounts = GetClassStatusSummary(
EPackageStatus::DeclaredUnmodified_FoundModified_FalsePositive);
int32 NumClassesPrinted = 0;
constexpr int32 MaxNumClassesPrinted = 25;
for (const TPair<FTopLevelAssetPath, TArray<FName>>& Pair : ClassFalsePositiveCounts)
{
MessageWithDiagnostics << TEXT("\n\t") << WriteToString<256>(Pair.Key) << TEXT(": ") << Pair.Value.Num();
int32 NumPackagesPrinted = 0;
constexpr int32 MaxNumPackagesPrinted = 10;
for (FName PackageName : Pair.Value)
{
MessageWithDiagnostics << TEXT("\n\t\t") << PackageName;
if (++NumPackagesPrinted >= MaxNumPackagesPrinted)
{
break;
}
}
if (Pair.Value.Num() > NumPackagesPrinted)
{
MessageWithDiagnostics << TEXT("\n\t\t...");
}
if (++NumClassesPrinted >= MaxNumClassesPrinted)
{
break;
}
}
if (ClassFalsePositiveCounts.Num() > NumClassesPrinted)
{
MessageWithDiagnostics << TEXT("\n\t...");
}
UE_CLOG(COTFS.GetCookMode() != ECookMode::CookWorker,
LogIncrementalValidate, Error, TEXT("%s"), *MessageWithDiagnostics);
}
else if (StatusCounts[EPackageStatus::DeclaredUnmodified_FoundModified_OnIgnoreList] > 0)
{
Message = FString::Printf(TEXT("Packages Incrementally Skipped: %d: IncrementalSkipFalsePositive (Ignored): %d."),
DetectedUnmodified,
StatusCounts[EPackageStatus::DeclaredUnmodified_FoundModified_OnIgnoreList]);
UE_CLOG(COTFS.GetCookMode() != ECookMode::CookWorker,
LogIncrementalValidate, Warning, TEXT("%s"), *Message);
}
else
{
UE_CLOG(COTFS.GetCookMode() != ECookMode::CookWorker,
LogIncrementalValidate, Display, TEXT("%s"), *Message);
}
break;
}
case EPhase::Phase1:
{
int32 DetectedUnmodified = StatusCounts[EPackageStatus::DeclaredUnmodified_ConfirmedUnmodified]
+ StatusCounts[EPackageStatus::DeclaredUnmodified_FoundModified_IndeterminismOrFalsePositive]
+ StatusCounts[EPackageStatus::DeclaredUnmodified_FoundModified_OnIgnoreList];
UE_CLOG(COTFS.GetCookMode() != ECookMode::CookWorker,
LogIncrementalValidate, Display,
TEXT("Modified: %d. DetectedUnmodified: %d. ValidatedUnmodified: %d. IncrementalSkipFalsePositiveOrIndeterminism: %d."),
StatusCounts[EPackageStatus::DeclaredModified_WillNotVerify],
DetectedUnmodified,
StatusCounts[EPackageStatus::DeclaredUnmodified_ConfirmedUnmodified]
+ StatusCounts[EPackageStatus::DeclaredUnmodified_FoundModified_OnIgnoreList],
StatusCounts[EPackageStatus::DeclaredUnmodified_FoundModified_IndeterminismOrFalsePositive]);
Save();
break;
}
case EPhase::Phase2:
{
int32 DetectedUnmodified = StatusCounts[EPackageStatus::DeclaredUnmodified_ConfirmedUnmodified]
+ StatusCounts[EPackageStatus::DeclaredUnmodified_FoundModified_IndeterminismOrFalsePositive]
+ StatusCounts[EPackageStatus::DeclaredUnmodified_FoundModified_Indeterminism]
+ StatusCounts[EPackageStatus::DeclaredUnmodified_FoundModified_FalsePositive]
+ StatusCounts[EPackageStatus::DeclaredUnmodified_FoundModified_OnIgnoreList];
UE_CLOG(COTFS.GetCookMode() != ECookMode::CookWorker,
LogIncrementalValidate, Display,
TEXT("Modified: %d. DetectedUnmodified: %d. ValidatedUnmodified: %d. Indeterminism: %d. IncrementalSkipFalsePositive: %d."),
StatusCounts[EPackageStatus::DeclaredModified_WillNotVerify],
DetectedUnmodified,
StatusCounts[EPackageStatus::DeclaredUnmodified_ConfirmedUnmodified]
+ StatusCounts[EPackageStatus::DeclaredUnmodified_FoundModified_OnIgnoreList],
StatusCounts[EPackageStatus::DeclaredUnmodified_FoundModified_Indeterminism],
StatusCounts[EPackageStatus::DeclaredUnmodified_FoundModified_FalsePositive]
);
FString Message = FString::Printf(TEXT("Packages Incrementally Skipped: %d: IncrementalSkipFalsePositive: %d."),
DetectedUnmodified,
StatusCounts[EPackageStatus::DeclaredUnmodified_FoundModified_FalsePositive]);
if (StatusCounts[EPackageStatus::DeclaredUnmodified_FoundModified_FalsePositive] > 0)
{
UE_CLOG(COTFS.GetCookMode() != ECookMode::CookWorker,
LogIncrementalValidate, Error, TEXT("%s"), *Message);
}
else if (StatusCounts[EPackageStatus::DeclaredUnmodified_FoundModified_OnIgnoreList] > 0)
{
Message = FString::Printf(TEXT("Packages Incrementally Skipped: %d: IncrementalSkipFalsePositive (Ignored): %d."),
DetectedUnmodified,
StatusCounts[EPackageStatus::DeclaredUnmodified_FoundModified_FalsePositive]);
UE_CLOG(COTFS.GetCookMode() != ECookMode::CookWorker,
LogIncrementalValidate, Warning, TEXT("%s"), *Message);
}
else
{
UE_CLOG(COTFS.GetCookMode() != ECookMode::CookWorker,
LogIncrementalValidate, Display, TEXT("%s"), *Message);
}
break;
}
default:
checkNoEntry();
break;
}
}
void FIncrementalValidatePackageWriter::UpdateSaveArguments(FSavePackageArgs& SaveArgs)
{
switch (SaveAction)
{
case ESaveAction::CheckForDiffs:
Super::UpdateSaveArguments(SaveArgs);
break;
case ESaveAction::SaveToInner:
Inner->UpdateSaveArguments(SaveArgs);
break;
case ESaveAction::IgnoreResults:
break;
default:
checkNoEntry();
break;
}
}
bool FIncrementalValidatePackageWriter::IsAnotherSaveNeeded(FSavePackageResultStruct& PreviousResult, FSavePackageArgs& SaveArgs)
{
bool bResult = IsAnotherSaveNeededInternal(PreviousResult, SaveArgs);
if (!bResult && COTFS.GetCookMode() != ECookMode::CookWorker)
{
MarkPackageCompletedOnDirector(BeginInfo.PackageName, UE::Cook::FWorkerId::Local());
}
return bResult;
}
bool FIncrementalValidatePackageWriter::IsAnotherSaveNeededInternal(FSavePackageResultStruct& PreviousResult,
FSavePackageArgs& SaveArgs)
{
bPackageFirstPass = false;
checkf(!Inner->IsAnotherSaveNeeded(PreviousResult, SaveArgs),
TEXT("FIncrementalValidatePackageWriter does not support an Inner that needs multiple saves."));
if (PreviousResult == ESavePackageResult::Timeout)
{
return false;
}
switch (SaveAction)
{
case ESaveAction::CheckForDiffs:
break;
case ESaveAction::SaveToInner:
// The SaveToInner pass, if present, is the last pass in a phase
return false;
case ESaveAction::IgnoreResults:
// The IgnoreResults pass, if present, is the last pass in a phase
return false;
default:
checkNoEntry();
break;
}
switch (Phase)
{
case EPhase::AllInOnePhase:
check(GetPackageStatus(BeginInfo.PackageName) == EPackageStatus::DeclaredUnmodified_NotYetProcessed); // Otherwise we would have set SaveAction=SaveToInner or IgnoreResults and early exited above
if (Super::IsAnotherSaveNeeded(PreviousResult, SaveArgs))
{
return true;
}
else
{
// Once our superclass has finished looking for differences, finish it off and start a SaveToInner pass
FCommitPackageInfo CommitInfo;
CommitInfo.Status = IPackageWriter::ECommitStatus::Success;
CommitInfo.PackageName = BeginInfo.PackageName;
CommitInfo.WriteOptions = EWriteOptions::None;
Super::CommitPackage(MoveTemp(CommitInfo));
if (bIsDifferent && !bNewPackage)
{
if (!PackageIgnoreList.Contains(BeginInfo.PackageName))
{
SetPackageStatus(BeginInfo.PackageName, EPackageStatus::DeclaredUnmodified_FoundModified_FalsePositive);
}
else
{
SetPackageStatus(BeginInfo.PackageName, EPackageStatus::DeclaredUnmodified_FoundModified_OnIgnoreList);
}
}
else if (!bNewPackage)
{
SetPackageStatus(BeginInfo.PackageName, EPackageStatus::DeclaredUnmodified_ConfirmedUnmodified);
}
else
{
SetPackageStatus(BeginInfo.PackageName, EPackageStatus::DeclaredModified_WillNotVerify);
}
if (bReadOnly)
{
SaveAction = ESaveAction::IgnoreResults;
return false;
}
else
{
Inner->BeginPackage(BeginInfo);
SaveAction = ESaveAction::SaveToInner;
return true;
}
}
case EPhase::Phase1:
if (Super::IsAnotherSaveNeeded(PreviousResult, SaveArgs))
{
return true;
}
else if (bIsDifferent && !bNewPackage)
{
// If our superclass FDiffPackageWriter found differences, when it finishes the saves it wants to do,
// Finish it off and start a SaveToInner pass
FCommitPackageInfo CommitInfo;
CommitInfo.Status = IPackageWriter::ECommitStatus::Success;
CommitInfo.PackageName = BeginInfo.PackageName;
CommitInfo.WriteOptions = EWriteOptions::None;
Super::CommitPackage(MoveTemp(CommitInfo));
Inner->BeginPackage(BeginInfo);
SaveAction = ESaveAction::SaveToInner;
if (!PackageIgnoreList.Contains(BeginInfo.PackageName))
{
// Mark that the incremental validation failed if it was not already marked by log or warning messages.
// We need to record it for an indeterminism test
SetPackageStatus(BeginInfo.PackageName, EPackageStatus::DeclaredUnmodified_FoundModified_IndeterminismOrFalsePositive);
}
else
{
SetPackageStatus(BeginInfo.PackageName, EPackageStatus::DeclaredUnmodified_FoundModified_OnIgnoreList);
}
return true;
}
else if (!bNewPackage)
{
// No differences found, so finish off the superclass's save during CommitPackage, without doing a
// SaveToInner pass
// Mark that the incremental validation passed
SetPackageStatus(BeginInfo.PackageName, EPackageStatus::DeclaredUnmodified_ConfirmedUnmodified);
TArray<FMessage> Messages;
PackageMessageMap.RemoveAndCopyValue(BeginInfo.PackageName, Messages);
for (FMessage& Message : Messages)
{
// If no differences were detected, we should not have logged any warning or error messages
check(Message.Verbosity > ELogVerbosity::Warning);
}
return false;
}
else
{
// New packages need to be resaved in Phase2; for our purposes they are equivalent
// to a package that the IncrementalCook detected as modified.
// Do not add an entry for it in our results for incremental packages, and do not resave it in this pass
SetPackageStatus(BeginInfo.PackageName, EPackageStatus::DeclaredModified_WillNotVerify);
return false;
}
case EPhase::Phase2:
LogIncrementalDifferences();
// No need to do the Super's second diff pass to find callstacks - just knowing whether differences exist is enough.
// No need to save package to disk; for these packages (packages found to be incrementally unmodified during
// Phase1) they were already resaved during Phase1
return false;
default:
checkNoEntry();
return false;
}
}
void FIncrementalValidatePackageWriter::OnDiffWriterMessage(ELogVerbosity::Type Verbosity, FStringView Message)
{
PackageMessageMap.FindOrAdd(BeginInfo.PackageName).Add(FMessage{ FString(Message), Verbosity });
}
void FIncrementalValidatePackageWriter::MarkPackageCompletedOnDirector(FName PackageName,
UE::Cook::FWorkerId WorkerId)
{
FPackageStatusInfo* Status = PackageStatusMap.Find(PackageName);
TArray<FMessage>* Messages = PackageMessageMap.Find(PackageName);
if (!Status || !Messages)
{
return;
}
int32& TotalCount = TotalStatusCounts.FindOrAdd(Status->Status, 0);
TArray<FName>& ClassStatusArray = ClassStatusSummary.FindOrAdd(Status->AssetClass).FindOrAdd(Status->Status);
++TotalCount;
ClassStatusArray.Add(PackageName);
if (LoggingSoftMaximum >= 0 && TotalCount > LoggingSoftMaximum && ClassStatusArray.Num() > 1)
{
// Suppress reported logs for this package
return;
}
switch (Phase)
{
case EPhase::AllInOnePhase:
for (FMessage& Message : *Messages)
{
FMsg::Logf(__FILE__, __LINE__, LogIncrementalValidate.GetCategoryName(), Message.Verbosity,
TEXT("%s"), *ResolveText(Message.Text));
}
break;
case EPhase::Phase1:
break;
case EPhase::Phase2:
break;
default:
checkNoEntry();
break;
}
}
void FIncrementalValidatePackageWriter::LogIncrementalDifferences()
{
// This function is called during Phase2 for packages that had differences during Phase1.
// It is called immediately after first-pass package save that is run by our super class FDiffPackageWriter, which
// compared it against the version that was written to disk during Phase1. If there are differences
// now from Phase1, then this package has a determinism issue. We log that information at display rather
// than Warning because this cookmode only logs warnings for IncrementalSkipFalsePositives.
bool bHasDeterminismIssue = bIsDifferent;
if (bHasDeterminismIssue)
{
UE_LOG(LogIncrementalValidate, Display, TEXT("Could not validate %s because it has a non-deterministic save."),
*BeginInfo.PackageName.ToString());
SetPackageStatus(BeginInfo.PackageName, EPackageStatus::DeclaredUnmodified_FoundModified_Indeterminism);
return;
}
// Otherwise, no determinism issues, so the differences indicate a bug in Diff Package
SetPackageStatus(BeginInfo.PackageName, EPackageStatus::DeclaredUnmodified_FoundModified_FalsePositive);
FMsg::Logf(__FILE__, __LINE__, LogIncrementalValidate.GetCategoryName(), ELogVerbosity::Warning,
TEXT("IncrementalSkipFalsePositive package %s."), *BeginInfo.PackageName.ToString());
TArray<FMessage>& Messages = PackageMessageMap.FindOrAdd(BeginInfo.PackageName);
for (const FMessage& Message : Messages)
{
FMsg::Logf(__FILE__, __LINE__, LogIncrementalValidate.GetCategoryName(), Message.Verbosity,
TEXT("%s"), *ResolveText(Message.Text));
}
}
void FIncrementalValidatePackageWriter::Save()
{
FString IncrementalValidatePath = GetIncrementalValidatePath();
TUniquePtr<FArchive> DiskArchive(IFileManager::Get().CreateFileWriter(*IncrementalValidatePath));
if (!DiskArchive)
{
UE_LOG(LogIncrementalValidate, Error,
TEXT("Could not write to file %s. This file is needed to store results for the -IncrementalValidate cook."),
*IncrementalValidatePath);
return;
}
FNameAsStringProxyArchive Ar(*DiskArchive);
Serialize(Ar);
}
void FIncrementalValidatePackageWriter::Load()
{
FString IncrementalValidatePath = GetIncrementalValidatePath();
TUniquePtr<FArchive> DiskArchive(IFileManager::Get().CreateFileReader(*IncrementalValidatePath));
if (!DiskArchive)
{
UE_LOG(LogIncrementalValidate, Fatal,
TEXT("Could not load file %s. This file is required and should have been written by the -IncrementalValidatePhase1 cook."),
*IncrementalValidatePath);
return;
}
FNameAsStringProxyArchive Ar(*DiskArchive);
Serialize(Ar);
if (Ar.IsError())
{
UE_LOG(LogIncrementalValidate, Fatal, TEXT("Corrupt file %s"), *IncrementalValidatePath);
}
}
void FIncrementalValidatePackageWriter::Serialize(FArchive& Ar)
{
constexpr int32 LatestVersion = 0;
int32 Version = LatestVersion;
Ar << Version;
if (Ar.IsLoading() && Version != LatestVersion)
{
Ar.SetError();
return;
}
Ar << PackageStatusMap;
Ar << PackageMessageMap;
}
FArchive& operator<<(FArchive& Ar, FIncrementalValidatePackageWriter::FMessage& Message)
{
uint8 Verbosity = static_cast<uint8>(Message.Verbosity);
Ar << Verbosity << Message.Text;
if (Ar.IsLoading())
{
Message.Verbosity = static_cast<ELogVerbosity::Type>(Verbosity);
}
return Ar;
}
FString FIncrementalValidatePackageWriter::GetIncrementalValidatePath() const
{
return FPaths::Combine(MetadataPath, FString(IncrementalValidateFilename));
}
FIncrementalValidatePackageWriter::EPackageStatus FIncrementalValidatePackageWriter::GetPackageStatus(FName PackageName) const
{
if (const FPackageStatusInfo* Info = PackageStatusMap.Find(PackageName))
{
return Info->Status;
}
return EPackageStatus::NotYetProcessed;
}
void FIncrementalValidatePackageWriter::SetPackageStatus(FName PackageName, EPackageStatus NewStatus)
{
FPackageStatusInfo& Info = PackageStatusMap.FindOrAdd(PackageName);
Info.Status = NewStatus;
switch (NewStatus)
{
case EPackageStatus::DeclaredUnmodified_ConfirmedUnmodified: [[fallthrough]];
case EPackageStatus::DeclaredModified_WillNotVerify:
// For the non-error cases, clear the AssetClass so that we don't waste bandwidth or diskspace to store it.
Info.AssetClass = FTopLevelAssetPath();
break;
default:
break;
}
}
FIncrementalValidatePackageWriter::FStatusCounts FIncrementalValidatePackageWriter::CountPackagesByStatus()
{
FIncrementalValidatePackageWriter::FStatusCounts StatusCounts;
for (const TPair<FName,FPackageStatusInfo>& Pair : PackageStatusMap)
{
StatusCounts[Pair.Value.Status]++;
}
return StatusCounts;
}
TMap<FTopLevelAssetPath, TArray<FName>> FIncrementalValidatePackageWriter::GetClassStatusSummary(EPackageStatus PackageStatus)
{
TMap<FTopLevelAssetPath, TArray<FName>> Result;
for (const TPair<FTopLevelAssetPath, TMap<EPackageStatus, TArray<FName>>>& Pair : ClassStatusSummary)
{
const TArray<FName>* Packages = Pair.Value.Find(PackageStatus);
if (Packages)
{
Result.Add(Pair.Key, *Packages);
}
}
Result.ValueSort([](const TArray<FName>& A, const TArray<FName>& B)
{
return A.Num() > B.Num();
});
return Result;
}