// 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* 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& 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&& 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 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& 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& 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& 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 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(); default: checkNoEntry(); return TUniquePtr(); } } TUniquePtr 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(); default: checkNoEntry(); return TUniquePtr(); } } 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 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> ClassFalsePositiveCounts = GetClassStatusSummary( EPackageStatus::DeclaredUnmodified_FoundModified_FalsePositive); int32 NumClassesPrinted = 0; constexpr int32 MaxNumClassesPrinted = 25; for (const TPair>& 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 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* Messages = PackageMessageMap.Find(PackageName); if (!Status || !Messages) { return; } int32& TotalCount = TotalStatusCounts.FindOrAdd(Status->Status, 0); TArray& 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& 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 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 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(Message.Verbosity); Ar << Verbosity << Message.Text; if (Ar.IsLoading()) { Message.Verbosity = static_cast(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& Pair : PackageStatusMap) { StatusCounts[Pair.Value.Status]++; } return StatusCounts; } TMap> FIncrementalValidatePackageWriter::GetClassStatusSummary(EPackageStatus PackageStatus) { TMap> Result; for (const TPair>>& Pair : ClassStatusSummary) { const TArray* Packages = Pair.Value.Find(PackageStatus); if (Packages) { Result.Add(Pair.Key, *Packages); } } Result.ValueSort([](const TArray& A, const TArray& B) { return A.Num() > B.Num(); }); return Result; }