// Copyright Epic Games, Inc. All Rights Reserved. #include "Commandlets/DiffAssetRegistriesCommandlet.h" #include "AssetData.h" #include "UObject/Class.h" #include "PlatformInfo.h" #include "Misc/Paths.h" #include "HAL/FileManager.h" #include "Serialization/ArrayReader.h" #include "Misc/FileHelper.h" #include "Internationalization/Regex.h" DEFINE_LOG_CATEGORY(LogDiffAssets); void UDiffAssetRegistriesCommandlet::PopulateChangelistMap(const FString& Branch, const FString& CL, bool bEnginePackages) { FString FilePattern = FString::Printf(TEXT("%s%s@*.p4cache"), *FPaths::DiffDir(), *Branch); FString FileName = FString::Printf(TEXT("%s%s@%s.p4cache"), *FPaths::DiffDir(), *Branch, *CL); TArray CacheFiles; IFileManager::Get().FindFiles(CacheFiles, *FilePattern, true, false); // search through the list of cache files for the newest one we can use int32 CLNum; LexTryParseString(CLNum, *CL); int32 BestCLNum = 0; FString BestCache; for (TArray::TConstIterator It(CacheFiles); It; ++It) { FString TestCL; FPaths::GetBaseFilename(*It).Split(TEXT("@"), nullptr, &TestCL); int32 TestCLNum; LexTryParseString(TestCLNum, *TestCL); if (TestCLNum <= CLNum && TestCLNum > BestCLNum) { BestCLNum = TestCLNum; BestCache = *It; } } FArchive* CacheFile = nullptr; // read in the baseline from the newest one if (BestCLNum > 0) { CacheFile = IFileManager::Get().CreateFileReader(*(FPaths::DiffDir() / BestCache)); *CacheFile << AssetPathToChangelist; delete CacheFile; } FString CLRange = CL; // grab the newer file lists, or all of them if we had no best one, and merge them in if (BestCLNum < CLNum) { if (BestCLNum > 0) CLRange = FString::Printf(TEXT("%d,%d"), BestCLNum, CLNum); // skip the game packages if we're doing engine packages only if (!bEnginePackages) { FillChangelists(Branch, CLRange, P4GameBasePath, P4GameAssetPath); } FillChangelists(Branch, CLRange, P4EngineBasePath, P4EngineAssetPath); } // save out the new table if (BestCLNum != CLNum) { CacheFile = IFileManager::Get().CreateFileWriter(*FileName); *CacheFile << AssetPathToChangelist; delete CacheFile; } } int32 UDiffAssetRegistriesCommandlet::Main(const FString& FullCommandLine) { UE_LOG(LogDiffAssets, Display, TEXT("--------------------------------------------------------------------------------------------")); UE_LOG(LogDiffAssets, Display, TEXT("Running DiffAssetRegistries Commandlet")); TArray Tokens; TArray Switches; TMap Params; ParseCommandLine(*FullCommandLine, Tokens, Switches, Params); DiffChunkID = -1; bIsVerbose = Switches.Contains(TEXT("VERBOSE")); { bSaveCSV = Switches.Contains(TEXT("CSV")); FString CSVName; bSaveCSV |= FParse::Value(*FullCommandLine, TEXT("CSVName="), CSVName); FString CSVPath; bSaveCSV |= FParse::Value(*FullCommandLine, TEXT("CSVPath="), CSVPath); if (bSaveCSV) { if (CSVFilename.IsEmpty() && !CSVName.IsEmpty()) { CSVFilename = FString::Printf(TEXT("%s%s"), *FPaths::DiffDir(), *CSVName); } if (CSVFilename.IsEmpty()) { CSVFilename = CSVPath; } if (CSVFilename.IsEmpty()) { CSVFilename = FPaths::Combine(*FPaths::DiffDir(), TEXT("AssetChanges.csv")); } if (FPaths::GetExtension(CSVFilename).IsEmpty()) { CSVFilename += TEXT(".csv"); } } } // options to ignore small changes/sizes FParse::Value(*FullCommandLine, TEXT("MinChanges="), MinChangeCount); FParse::Value(*FullCommandLine, TEXT("MinChangeSize="), MinChangeSizeMB); FParse::Value(*FullCommandLine, TEXT("ChunkID="), DiffChunkID); FParse::Value(*FullCommandLine, TEXT("WarnPercentage="), WarnPercentage); FParse::Value(*FullCommandLine, TEXT("WarnSizeMin="), WarnSizeMinMB); FParse::Value(*FullCommandLine, TEXT("WarnTotalChangedSize="), WarnTotalChangedSizeMB); FString OldPath; FString NewPath; const bool bUseSourceGuid = Switches.Contains(TEXT("SOURCEGUID")); const bool bConsistency = Switches.Contains(TEXT("CONSISTENCY")); const bool bEnginePackages = Switches.Contains(TEXT("ENGINEPACKAGES")); bGroupByChunk = Switches.Contains(TEXT("GROUPBYCHUNK")); FString SortOrder; FParse::Value(*FullCommandLine, TEXT("Sort="), SortOrder); if (SortOrder == TEXT("name")) { ReportedFileOrder = SortOrder::ByName; } else if (SortOrder == TEXT("size")) { ReportedFileOrder = SortOrder::BySize; } else if (SortOrder == TEXT("class")) { ReportedFileOrder = SortOrder::ByClass; } else if (SortOrder == TEXT("change")) { ReportedFileOrder = SortOrder::ByChange; } FString Branch; FString CL; FString Spec; FParse::Value(*FullCommandLine, TEXT("platform="), TargetPlatform); if (TargetPlatform.IsEmpty()) { UE_LOG(LogDiffAssets, Error, TEXT("No platform specified on the commandline use \"-platform=\".")); } auto FindAssetRegistryPath = [&](const FString& PathVal, FString& OutPath) { for (const FString& SearchPath: AssetRegistrySearchPath) { FString FinalSearchPath = SearchPath; FinalSearchPath.ReplaceInline(TEXT("[buildversion]"), *PathVal); FinalSearchPath.ReplaceInline(TEXT("[platform]"), *TargetPlatform); if (IFileManager::Get().FileExists(*FinalSearchPath)) { OutPath = FinalSearchPath; return true; } } return false; }; // const TCHAR* AssetRegistrySubPath = TEXT("/Metadata/DevelopmentAssetRegistry.bin"); const FString* OldPathVal = Params.Find(FString(TEXT("OldPath"))); if (OldPathVal) { FindAssetRegistryPath(*OldPathVal, OldPath); } else { UE_LOG(LogDiffAssets, Error, TEXT("No old path specified \"-oldpath=<>\", use full path to asset registry or build version.")); return -1; } const FString* NewPathVal = Params.Find(FString(TEXT("NewPath"))); if (NewPathVal) { FindAssetRegistryPath(*NewPathVal, NewPath); if (!RegexBranchCL.IsEmpty()) { const FRegexPattern CLPattern(RegexBranchCL); FRegexMatcher CLMatcher(CLPattern, NewPath); if (CLMatcher.FindNext()) { Branch = CLMatcher.GetCaptureGroup(1); CL = CLMatcher.GetCaptureGroup(2); } } } else { UE_LOG(LogDiffAssets, Error, TEXT("No new path specified \"-newpath=<>\", use full path to asset registry or build version.")); return -1; } bMatchChangelists = false; if (FParse::Value(*FullCommandLine, TEXT("Branch="), Spec)) { FString NewBranch, NewCL; bMatchChangelists = true; Spec.Split(TEXT("@"), &NewBranch, &NewCL); bMatchChangelists = true; PopulateChangelistMap(NewBranch, NewCL, bEnginePackages); } else if (Switches.Contains(TEXT("CHANGELISTS"))) { bMatchChangelists = true; PopulateChangelistMap(Branch, CL, bEnginePackages); } if (OldPath.IsEmpty()) { UE_LOG(LogDiffAssets, Error, TEXT("Unable to locate AssetRegistry.bin for supplied oldpath (%s), use full path to asset registry or build version."), **OldPathVal); return -1; } if (NewPath.IsEmpty()) { UE_LOG(LogDiffAssets, Error, TEXT("Unable to locate AssetRegistry.bin for supplied newpath (%s), use full path to asset registry or build version."), **NewPathVal); return -1; } FPaths::NormalizeFilename(NewPath); FPaths::NormalizeFilename(OldPath); // try to discern platform /*if (NewPath.Contains(AssetRegistrySubPath)) { FString NewPlatformDir = NewPath.Left(NewPath.Find(AssetRegistrySubPath)); FString PlatformPath = FPaths::GetCleanFilename(NewPlatformDir); for (const FPlatformInfo& PlatformInfo : PlatformInfo::GetPlatformInfoArray()) { if (PlatformPath == PlatformInfo.TargetPlatformName.ToString()) { TargetPlatform = PlatformPath; break; } } }*/ if (bConsistency) { ConsistencyCheck(OldPath, NewPath); } else { DiffAssetRegistries(OldPath, NewPath, bUseSourceGuid, bEnginePackages); } UE_LOG(LogDiffAssets, Display, TEXT("Successfully finished running DiffAssetRegistries Commandlet")); UE_LOG(LogDiffAssets, Display, TEXT("--------------------------------------------------------------------------------------------")); return 0; } void UDiffAssetRegistriesCommandlet::FillChangelists(FString Branch, FString CL, FString BasePath, FString AssetPath) { TArray Results; int32 ReturnCode = 0; if (LaunchP4(TEXT("files ") + P4Repository + Branch + BasePath + TEXT("....uasset@") + CL, Results, ReturnCode)) { if (ReturnCode == 0) { for (const FString& Result: Results) { FString DepotPathName; FString ExtraInfoAfterPound; if (Result.Split(TEXT("#"), &DepotPathName, &ExtraInfoAfterPound)) { FString PostContentPath; if (DepotPathName.Split(BasePath, nullptr, &PostContentPath)) { if (!PostContentPath.IsEmpty() && !PostContentPath.StartsWith(TEXT("Cinematics")) && !PostContentPath.StartsWith(TEXT("Developers")) && !PostContentPath.StartsWith(TEXT("Maps/Test_Maps"))) { const FString PostContentPathWithoutExtension = FPaths::GetBaseFilename(PostContentPath, false); const FString FullPackageName = AssetPath + PostContentPathWithoutExtension; TArray Chunks; ExtraInfoAfterPound.ParseIntoArray(Chunks, TEXT(" "), true); int32 Changelist; LexTryParseString(Changelist, *Chunks[4]); if (Changelist) { int32& Entry = AssetPathToChangelist.FindOrAdd(*FullPackageName); if (Changelist > Entry) Entry = Changelist; } } } } } } } if (LaunchP4(TEXT("files ") + P4Repository + Branch + BasePath + TEXT("....umap@") + CL, Results, ReturnCode)) { if (ReturnCode == 0) { for (const FString& Result: Results) { FString DepotPathName; FString ExtraInfoAfterPound; if (Result.Split(TEXT("#"), &DepotPathName, &ExtraInfoAfterPound)) { FString PostContentPath; if (DepotPathName.Split(BasePath, nullptr, &PostContentPath)) { if (!PostContentPath.IsEmpty() && !PostContentPath.StartsWith(TEXT("Cinematics")) && !PostContentPath.StartsWith(TEXT("Developers")) && !PostContentPath.StartsWith(TEXT("Maps/Test_Maps"))) { const FString PostContentPathWithoutExtension = FPaths::GetBaseFilename(PostContentPath, false); const FString FullPackageName = AssetPath + PostContentPathWithoutExtension; TArray Chunks; ExtraInfoAfterPound.ParseIntoArray(Chunks, TEXT(" "), true); int32 Changelist; LexTryParseString(Changelist, *Chunks[4]); if (Changelist) { int32& Entry = AssetPathToChangelist.FindOrAdd(*FullPackageName); if (Changelist > Entry) Entry = Changelist; } } } } } } } } void rescale(int bytes, double& value, int& exp) { value = bytes; value = fabs(value); while (value > 1024.0) { value /= 1024.0; exp++; } } void UDiffAssetRegistriesCommandlet::ConsistencyCheck(const FString& OldPath, const FString& NewPath) { { FArrayReader SerializedAssetData; if (!IFileManager::Get().FileExists(*OldPath)) { UE_LOG(LogDiffAssets, Error, TEXT("File '%s' does not exist."), *OldPath); return; } if (!FFileHelper::LoadFileToArray(SerializedAssetData, *OldPath)) { UE_LOG(LogDiffAssets, Error, TEXT("Failed to load file '%s'."), *OldPath); return; } if (!OldState.Load(SerializedAssetData)) { UE_LOG(LogDiffAssets, Error, TEXT("Failed to parse file '%s' as asset registry."), *OldPath); return; } } { FArrayReader SerializedAssetData; if (!IFileManager::Get().FileExists(*NewPath)) { UE_LOG(LogDiffAssets, Error, TEXT("File '%s' does not exist."), *NewPath); return; } if (!FFileHelper::LoadFileToArray(SerializedAssetData, *NewPath)) { UE_LOG(LogDiffAssets, Error, TEXT("Failed to load file '%s'."), *NewPath); return; } if (!OldState.Load(SerializedAssetData)) { UE_LOG(LogDiffAssets, Error, TEXT("Failed to parse file '%s' as asset registry."), *NewPath); return; } } UE_LOG(LogDiffAssets, Display, TEXT("Comparing asset registries '%s' and '%s'."), *OldPath, *NewPath); UE_LOG(LogDiffAssets, Display, TEXT("Source vs Cooked Consistency Diff")); if (bIsVerbose) { UE_LOG(LogDiffAssets, Display, TEXT("Cooked files that differ, where source guids do not:")); } // We're looking for packages that the Cooked check says are modified, but that the Guid check says are not // We're ignoring new packages for this, as those are obviously going to change TSet GuidModified, CookModified; TSet New; for (const TPair& Pair: NewState.GetAssetPackageDataMap()) { FName Name = Pair.Key; const FAssetPackageData* Data = Pair.Value; const FAssetPackageData* PrevData = OldState.GetAssetPackageData(Name); if (!PrevData) { New.Add(Name); } else { PRAGMA_DISABLE_DEPRECATION_WARNINGS if (Data->PackageGuid != PrevData->PackageGuid) PRAGMA_ENABLE_DEPRECATION_WARNINGS { GuidModified.Add(Name); } if (Data->CookedHash != PrevData->CookedHash) { CookModified.Add(Name); } } } // recurse through the referencer lists to fill out GuidModified TArray Recurse = GuidModified.Array(); for (int32 RecurseIndex = 0; RecurseIndex < Recurse.Num(); RecurseIndex++) { FName Package = Recurse[RecurseIndex]; TArray Referencers; NewState.GetReferencers(Package, Referencers, UE::AssetRegistry::EDependencyCategory::Package, UE::AssetRegistry::EDependencyQuery::Hard); for (const FAssetIdentifier& Referencer: Referencers) { FName ReferencerPackage = Referencer.PackageName; if (!New.Contains(ReferencerPackage) && !GuidModified.Contains(ReferencerPackage)) { GuidModified.Add(ReferencerPackage); Recurse.Add(ReferencerPackage); } } } int64 changes = 0; int64 change_bytes = 0; // find all entries of CookModified that do not exist in GuidModified const TMap& PackageMap = NewState.GetAssetPackageDataMap(); for (FName const& Package: CookModified) { const FAssetPackageData* Data = PackageMap[Package]; if (!GuidModified.Contains(Package)) { ++changes; change_bytes += Data->DiskSize; if (bIsVerbose) { UE_LOG(LogDiffAssets, Display, TEXT("%s : %d bytes"), *Package.ToString(), Data->DiskSize); } } } double change_value = 0.0; int change_exp = 0; rescale(change_bytes, change_value, change_exp); UE_LOG(LogDiffAssets, Display, TEXT("Summary:")); UE_LOG(LogDiffAssets, Display, TEXT("%d nondeterministic cooks, %8.3f %cB"), changes, change_value, " KMGTP"[change_exp]); } bool UDiffAssetRegistriesCommandlet::IsInRelevantChunk(FAssetRegistryState& InRegistryState, FName InAssetPath) { if (DiffChunkID == -1) { return true; } TArrayView Assets = InRegistryState.GetAssetsByPackageName(InAssetPath); if (Assets.Num() && Assets[0]->ChunkIDs.Num()) { return Assets[0]->ChunkIDs.Contains(DiffChunkID); } return true; } FName UDiffAssetRegistriesCommandlet::GetClassName(FAssetRegistryState& InRegistryState, FName InAssetPath) { if (AssetPathToClassName.Contains(InAssetPath) == false) { TArrayView Assets = InRegistryState.GetAssetsByPackageName(InAssetPath); FName NewName = NAME_None; if (Assets.Num() > 0) { NewName = Assets[0]->AssetClass; } else { if (InAssetPath.ToString().StartsWith(TEXT("/Script/"))) { NewName = NAME_Class; } } if (NewName == NAME_None) { UE_LOG(LogDiffAssets, Log, TEXT("Unable to find class type of asset %s"), *InAssetPath.ToString()); } AssetPathToClassName.Add(InAssetPath) = NewName; } return AssetPathToClassName[InAssetPath]; } TArray UDiffAssetRegistriesCommandlet::GetAssetChunks(FAssetRegistryState& InRegistryState, FName InAssetPath) { if (ChunkIdByAssetPath.Contains(InAssetPath) == false) { TArrayView Assets = InRegistryState.GetAssetsByPackageName(InAssetPath); if (Assets.Num() > 0 && Assets[0]->ChunkIDs.Num() > 0) { if (Assets[0]->ChunkIDs.Num() > 1) { UE_LOG(LogDiffAssets, Log, TEXT("Multiple ChunkIds for asset %s"), *InAssetPath.ToString()); } for (int32 id: Assets[0]->ChunkIDs) { ChangesByChunk.FindOrAdd(id).IncludedAssets.Add(InAssetPath); ChunkIdByAssetPath.Add(InAssetPath, id); } } else { UE_LOG(LogDiffAssets, Log, TEXT("Unable to find chunk ids of asset %s"), *InAssetPath.ToString()); ChunkIdByAssetPath.Add(InAssetPath, -1); } } TArray ChunkIds; ChunkIdByAssetPath.MultiFind(InAssetPath, ChunkIds); return ChunkIds; } void UDiffAssetRegistriesCommandlet::RecordAdd(FName InAssetPath, const FAssetPackageData& InNewData) { FChangeInfo AssetChange; ++AssetChange.Adds; if (InNewData.DiskSize > 0) { AssetChange.AddedBytes += InNewData.DiskSize; } FName ClassName = GetClassName(NewState, InAssetPath); TArray ChunkIds = GetAssetChunks(NewState, InAssetPath); int changelist = AssetPathToChangelist.FindOrAdd(*InAssetPath.ToString()); ChangeInfoByAsset.FindOrAdd(InAssetPath) = AssetChange; ChangeSummaryByClass.FindOrAdd(ClassName) += AssetChange; ChangeSummaryByChangelist.FindOrAdd(changelist) += AssetChange; for (int32 ChunkId: ChunkIds) { ChangesByChunk.FindOrAdd(ChunkId).ChangesByClass.FindOrAdd(ClassName) += AssetChange; } ChangeSummary += AssetChange; } void UDiffAssetRegistriesCommandlet::RecordEdit(FName InAssetPath, const FAssetPackageData& InNewData, const FAssetPackageData& InOldData) { FChangeInfo AssetChange; if (InNewData.DiskSize > 0) { ++AssetChange.Changes; AssetChange.ChangedBytes += InNewData.DiskSize; } FName ClassName = GetClassName(NewState, InAssetPath); TArray ChunkIds = GetAssetChunks(NewState, InAssetPath); int changelist = AssetPathToChangelist.FindOrAdd(*InAssetPath.ToString()); ChangeInfoByAsset.FindOrAdd(InAssetPath) = AssetChange; ChangeSummaryByClass.FindOrAdd(ClassName) += AssetChange; ChangeSummaryByChangelist.FindOrAdd(changelist) += AssetChange; for (int32 ChunkId: ChunkIds) { ChangesByChunk.FindOrAdd(ChunkId).ChangesByClass.FindOrAdd(ClassName) += AssetChange; } ChangeSummary += AssetChange; } void UDiffAssetRegistriesCommandlet::RecordDelete(FName InAssetPath, const FAssetPackageData& InData) { FChangeInfo AssetChange; ++AssetChange.Deletes; if (InData.DiskSize >= 0) { AssetChange.DeletedBytes += InData.DiskSize; } FName ClassName = GetClassName(OldState, InAssetPath); TArray ChunkIds = GetAssetChunks(NewState, InAssetPath); ChangeInfoByAsset.FindOrAdd(InAssetPath) = AssetChange; ChangeSummaryByClass.FindOrAdd(ClassName) += AssetChange; for (int32 ChunkId: ChunkIds) { ChangesByChunk.FindOrAdd(ChunkId).ChangesByClass.FindOrAdd(ClassName) += AssetChange; } ChangeSummary += AssetChange; } void UDiffAssetRegistriesCommandlet::RecordNoChange(FName InAssetPath, const FAssetPackageData& InData) { FChangeInfo AssetChange; AssetChange.Unchanged++; if (InData.DiskSize >= 0) { AssetChange.UnchangedBytes += InData.DiskSize; } FName ClassName = GetClassName(NewState, InAssetPath); TArray ChunkIds = GetAssetChunks(NewState, InAssetPath); ChangeInfoByAsset.FindOrAdd(InAssetPath) = AssetChange; ChangeSummaryByClass.FindOrAdd(ClassName) += AssetChange; for (int32 ChunkId: ChunkIds) { ChangesByChunk.FindOrAdd(ChunkId).ChangesByClass.FindOrAdd(ClassName) += AssetChange; } ChangeSummary += AssetChange; } void UDiffAssetRegistriesCommandlet::SummarizeDeterminism() { TArray AssetPaths; ChangeInfoByAsset.GetKeys(AssetPaths); for (const FName& AssetPath: AssetPaths) { const FChangeInfo& ChangeInfo = ChangeInfoByAsset[AssetPath]; char classification; // classify the asset change by the flags int32 flags = AssetPathFlags.FindOrAdd(AssetPath); { bool hash = (flags & EAssetFlags::HashChange) != 0; bool guid = (flags & EAssetFlags::GuidChange) != 0; bool dephash = (flags & EAssetFlags::DepHashChange) != 0; bool depguid = (flags & EAssetFlags::DepGuidChange) != 0; if (!hash) classification = 'x'; // shouldn't see this in here, no binary change else { if (guid) classification = 'e'; // explicit edit else if (dephash & depguid) classification = 'd'; // dependency edit else if (dephash & !depguid) classification = 'n'; // nondeterministic dependency else classification = 'c'; // nondeterministic } } TArray ChunkIds = GetAssetChunks(NewState, AssetPath); FName ClassName = GetClassName(NewState, AssetPath); if (classification == 'c') { NondeterministicSummary += ChangeInfo; for (int32 ChunkId: ChunkIds) { ChangesByChunk.FindOrAdd(ChunkId).Determinism.FindOrAdd(ClassName).AddDirect(ChangeInfo); } DeterminismByClass.FindOrAdd(ClassName).AddDirect(ChangeInfo); } else if (classification == 'n') { IndirectNondeterministicSummary += ChangeInfo; for (int32 ChunkId: ChunkIds) { ChangesByChunk.FindOrAdd(ChunkId).Determinism.FindOrAdd(ClassName).AddIndirect(ChangeInfo); } DeterminismByClass.FindOrAdd(ClassName).AddIndirect(ChangeInfo); } } } void UDiffAssetRegistriesCommandlet::LogChangedFiles(FArchive* CSVFile, FString const& OldPath, FString const& NewPath) { if (!bIsVerbose && !bSaveCSV) { return; } TArray AssetPaths; ChangeInfoByAsset.GetKeys(AssetPaths); // sort by size if (ReportedFileOrder == SortOrder::BySize) { // Sort by size of change AssetPaths.Sort([this](const FName& Lhs, const FName& Rhs) { return ChangeInfoByAsset[Lhs].GetTotalChangeSize() > ChangeInfoByAsset[Rhs].GetTotalChangeSize(); }); } // Sort by class type then size size else if (ReportedFileOrder == SortOrder::ByClass) { AssetPaths.Sort([this](const FName& Lhs, const FName& Rhs) { FString LhsName = GetClassName(NewState, Lhs).ToString(); FString RhsName = GetClassName(NewState, Rhs).ToString(); if (LhsName != RhsName) { return LhsName < RhsName; } return ChangeInfoByAsset[Lhs].GetTotalChangeSize() > ChangeInfoByAsset[Rhs].GetTotalChangeSize(); }); } // sort by change type then size else if (ReportedFileOrder == SortOrder::ByChange) { AssetPaths.Sort([this](const FName& Lhs, const FName& Rhs) { int32 LHSChanges = ChangeInfoByAsset[Lhs].GetChangeFlags(); int32 RHSChanges = ChangeInfoByAsset[Rhs].GetChangeFlags(); if (LHSChanges != RHSChanges) { return LHSChanges > RHSChanges; } // sort by size return ChangeInfoByAsset[Lhs].GetTotalChangeSize() > ChangeInfoByAsset[Rhs].GetTotalChangeSize(); }); } // sort by name else if (ReportedFileOrder == SortOrder::ByName) { AssetPaths.Sort([this](const FName& Lhs, const FName& Rhs) { return Lhs.ToString() < Rhs.ToString(); }); } if (CSVFile) { CSVFile->Logf(TEXT("Type Key")); CSVFile->Logf(TEXT("a, file added")); CSVFile->Logf(TEXT("r, file removed")); CSVFile->Logf(TEXT("e, explicit edit (this file specifically has been modified)")); CSVFile->Logf(TEXT("d, dependency edit (this file is different likely because a dependency has also been changed)")); CSVFile->Logf(TEXT("n, indirect non deterministic (a dependency file changed but wasn't changed directly (Indicates the dependency was either non determinisitc or another indirect non deterministic file))")); CSVFile->Logf(TEXT("c, non deterministic (the hashes for all dependencies are the same but this file is not)")); CSVFile->Logf(TEXT("x, no binary change (shouldn't ever happen)")); CSVFile->Logf(TEXT("")); CSVFile->Logf(TEXT("Modification,Name,Class,NewSize,OldSize,Changelist,Chunk")); UE_LOG(LogDiffAssets, Display, TEXT("Saving CSV results to %s"), *CSVFilename); } for (const FName& AssetPath: AssetPaths) { const FChangeInfo& ChangeInfo = ChangeInfoByAsset[AssetPath]; int Changelist = bMatchChangelists ? AssetPathToChangelist.FindOrAdd(*AssetPath.ToString()) : 0; FName ClassName; if (ChangeInfo.Deletes) { ClassName = GetClassName(OldState, AssetPath); } else { ClassName = GetClassName(NewState, AssetPath); } auto GetChunkIDString = [this, AssetPath = AssetPath](FAssetRegistryState& State) { TArray ChunkIds = GetAssetChunks(State, AssetPath); FString ChunkIdString = FString::JoinBy(ChunkIds, TEXT(" & "), [](int32 Value) { return FString::Printf(TEXT("%d"), Value); }); return ChunkIdString; }; if (ChangeInfo.Adds) { if (CSVFile) { CSVFile->Logf(TEXT("a,%s,%s,%d,0,%d,%s"), *AssetPath.ToString(), *ClassName.ToString(), ChangeInfo.AddedBytes, Changelist, *GetChunkIDString(NewState)); } if (bIsVerbose) { UE_LOG(LogDiffAssets, Display, TEXT("a %s : (Class=%s,NewSize=%d bytes)"), *AssetPath.ToString(), *ClassName.ToString(), ChangeInfo.AddedBytes); } } else if (ChangeInfo.Changes) { const FAssetPackageData* PrevData = OldState.GetAssetPackageData(AssetPath); char classification; // classify the asset change by the flags int32 flags = AssetPathFlags.FindOrAdd(AssetPath); { bool hash = (flags & EAssetFlags::HashChange) != 0; bool guid = (flags & EAssetFlags::GuidChange) != 0; bool dephash = (flags & EAssetFlags::DepHashChange) != 0; bool depguid = (flags & EAssetFlags::DepGuidChange) != 0; if (!hash) classification = 'x'; // shouldn't see this in here, no binary change else { if (guid) classification = 'e'; // explicit edit else if (dephash & depguid) classification = 'd'; // dependency edit else if (dephash & !depguid) classification = 'n'; // nondeterministic dependency else classification = 'c'; // nondeterministic } } if (CSVFile) { CSVFile->Logf(TEXT("%c,%s,%s,%d,%d,%d,%s"), classification, *AssetPath.ToString(), *ClassName.ToString(), ChangeInfo.ChangedBytes, PrevData->DiskSize, Changelist, *GetChunkIDString(NewState)); } if (bIsVerbose) { UE_LOG(LogDiffAssets, Display, TEXT("%c %s : (Class=%s,NewSize=%d bytes,OldSize=%d bytes)"), classification, *AssetPath.ToString(), *ClassName.ToString(), ChangeInfo.ChangedBytes, PrevData->DiskSize); } // UE_LOG(LogDiffAssets, Display, TEXT("Source file changed: %s"), (flags & EAssetFlags::GuidChange) ? TEXT("true") : TEXT("false")); /*if ((flags & EAssetFlags::GuidChange) && bMatchChangelists) { UE_LOG(LogDiffAssets, Display, TEXT("Last change: %d"), Changelist); }*/ #if 0 TArray Dependencies; NewState.GetDependencies(AssetPath, Dependencies, EAssetRegistryDependencyType::Hard); if (Dependencies.Num() > 0) { UE_LOG(LogDiffAssets, Display, TEXT("Dependency changes:")); for (const FAssetIdentifier& Dependency : Dependencies) { if (AssetPathToSourceChanged.FindOrAdd(Dependency.PackageName)) { UE_LOG(LogDiffAssets, Display, TEXT(" %s: %d"), *Dependency.PackageName.ToString(), AssetPathToChangelist.FindOrAdd(Dependency.PackageName)); } } } #endif } else if (ChangeInfo.Deletes) { const FAssetPackageData* PrevData = OldState.GetAssetPackageData(AssetPath); if (CSVFile) { CSVFile->Logf(TEXT("r,%s,%s,0,%d,0,0"), *AssetPath.ToString(), *ClassName.ToString(), PrevData->DiskSize); } if (bIsVerbose) { UE_LOG(LogDiffAssets, Display, TEXT("r %s : (Class=%s,OldSize=%d bytes)"), *AssetPath.ToString(), *ClassName.ToString(), PrevData->DiskSize); } } } } void UDiffAssetRegistriesCommandlet::LogClassSummary(FArchive* CSVFile, const FString& HeaderPrefix, const TMap& InChangeInfoByAsset, bool bDoWarnings, TMap DeterminismInfo) { const float InvToMB = 1.0 / (1024 * 1024); FString HeaderSpacer = (HeaderPrefix.Len() ? TEXT(" ") : TEXT("")); // show class totals first TArray ClassNames; InChangeInfoByAsset.GetKeys(ClassNames); // Sort keys by the desired order if (ReportedFileOrder == SortOrder::ByName || ReportedFileOrder == SortOrder::ByClass) { ClassNames.Sort([](const FName& Lhs, const FName& Rhs) { return Lhs.ToString() < Rhs.ToString(); }); } else // Default to size for everything else for class list { // sort by size of changes (number can also be a big impact on patch size but depends on datalayout and patch algo..) ClassNames.Sort([this, InChangeInfoByAsset](const FName& Lhs, const FName& Rhs) { const FChangeInfo& LHSChanges = InChangeInfoByAsset[Lhs]; const FChangeInfo& RHSChanges = InChangeInfoByAsset[Rhs]; return LHSChanges.GetTotalChangeSize() > RHSChanges.GetTotalChangeSize(); }); } // Overall Class Summary bool bChangesPastThreshold = false; for (FName ClassName: ClassNames) { const FChangeInfo& Changes = InChangeInfoByAsset[ClassName]; if (Changes.GetTotalChangeSize() == 0) { continue; } if (Changes.GetTotalChangeCount() < MinChangeCount || Changes.GetTotalChangeSize() < (MinChangeSizeMB * 1024 * 1024)) { continue; } else if (!bChangesPastThreshold) { // We'll need to display the header, since this is the first row that has sufficient changes if (CSVFile) { FString ExtraNondeterminismHeader = TEXT(""); if (DeterminismInfo.Num()) { ExtraNondeterminismHeader = TEXT(",DirectNondeterministicCount,DirectNondeterministicSize,IndirectNondeterministicCount,IndirectNondeterministicSize"); } CSVFile->Logf(TEXT("")); CSVFile->Logf(TEXT("%s%sClass Summary"), *HeaderPrefix, *HeaderSpacer); CSVFile->Logf(TEXT("Name,Percentage,TotalCount,TotalSize,Adds,AddedSize,Changes,ChangesSize,Deletes,DeletedSize,Unchanged,UnchangedSize%s"), *ExtraNondeterminismHeader); } bChangesPastThreshold = true; } if (CSVFile) { FString ExtraNondeterminismData = TEXT(""); if (DeterminismInfo.Num()) { int64 DirectCount = 0; int64 IndirectCount = 0; int64 DirectSize = 0; int64 IndirectSize = 0; if (DeterminismInfo.Contains(ClassName)) { int64 TotalCount = Changes.GetTotalChangeCount(); DirectCount = DeterminismInfo[ClassName].DirectCount; IndirectCount = DeterminismInfo[ClassName].IndirectCount; DirectSize = DeterminismInfo[ClassName].DirectSize; IndirectSize = DeterminismInfo[ClassName].IndirectSize; } ExtraNondeterminismData = FString::Printf(TEXT(",%lld,%lld,%lld,%lld"), DirectCount, DirectSize, IndirectCount, IndirectSize); } CSVFile->Logf(TEXT("%s,%0.02f,%lld,%lld,%lld,%lld,%lld,%lld,%lld,%lld,%lld,%lld%s"), *ClassName.ToString(), Changes.GetChangePercentage() * 100.0, Changes.GetTotalChangeCount(), Changes.GetTotalChangeSize(), Changes.Adds, Changes.AddedBytes, Changes.Changes, Changes.ChangedBytes, Changes.Deletes, Changes.DeletedBytes, Changes.Unchanged, Changes.UnchangedBytes, *ExtraNondeterminismData); } // log summary & change UE_LOG(LogDiffAssets, Display, TEXT("%s%sClass Summary: "), *HeaderPrefix, *HeaderSpacer); UE_LOG(LogDiffAssets, Display, TEXT("%s: %.02f%% changes (%.02f MB Total)"), *ClassName.ToString(), Changes.GetChangePercentage() * 100.0, Changes.GetTotalChangeSize() * InvToMB); if (Changes.Adds) { UE_LOG(LogDiffAssets, Display, TEXT("\t%d packages added, %8.3f MB"), Changes.Adds, Changes.AddedBytes * InvToMB); } if (Changes.Changes) { UE_LOG(LogDiffAssets, Display, TEXT("\t%d packages modified, %8.3f MB"), Changes.Changes, Changes.ChangedBytes * InvToMB); } if (Changes.Deletes) { UE_LOG(LogDiffAssets, Display, TEXT("\t%d packages removed, %8.3f MB"), Changes.Deletes, Changes.DeletedBytes * InvToMB); } UE_LOG(LogDiffAssets, Display, TEXT("\t%d packages unchanged, %8.3f MB"), Changes.Unchanged, Changes.UnchangedBytes * InvToMB); // Warn on a certain % of changes if that's enabled if (bDoWarnings && Changes.Changes >= 10 && (WarnPercentage > 0 || WarnSizeMinMB > 0) && Changes.ChangedBytes * InvToMB >= WarnSizeMinMB && Changes.GetChangePercentage() * 100.0 > WarnPercentage) { UE_LOG(LogDiffAssets, Warning, TEXT("\t%s Assets for %s are %.02f%% changed. (%.02f MB of data)"), *TargetPlatform, *ClassName.ToString(), Changes.GetChangePercentage() * 100.0, Changes.ChangedBytes * InvToMB); } } // If we didn't find any changes of sufficient size, note as much instead of the header if (!bChangesPastThreshold) { FString SummaryName = (HeaderPrefix.Len() ? FString::Printf(TEXT("%s: "), *HeaderPrefix) : TEXT("")); if (CSVFile) { CSVFile->Logf(TEXT("")); CSVFile->Logf(TEXT("%sNo classes had changes past change threshold"), *SummaryName); } UE_LOG(LogDiffAssets, Display, TEXT("%sNo classes had changes past thresholds"), *SummaryName); } } void UDiffAssetRegistriesCommandlet::DiffAssetRegistries(const FString& OldPath, const FString& NewPath, bool bUseSourceGuid, bool bEnginePackagesOnly) { { FArrayReader SerializedAssetData; if (!IFileManager::Get().FileExists(*OldPath)) { UE_LOG(LogDiffAssets, Error, TEXT("File '%s' does not exist."), *OldPath); return; } if (!FFileHelper::LoadFileToArray(SerializedAssetData, *OldPath)) { UE_LOG(LogDiffAssets, Error, TEXT("Failed to load file '%s'."), *OldPath); return; } if (!OldState.Load(SerializedAssetData)) { UE_LOG(LogDiffAssets, Error, TEXT("Failed to parse file '%s' as asset registry."), *OldPath); return; } } { FArrayReader SerializedAssetData; if (!IFileManager::Get().FileExists(*NewPath)) { UE_LOG(LogDiffAssets, Error, TEXT("File '%s' does not exist."), *NewPath); return; } if (!FFileHelper::LoadFileToArray(SerializedAssetData, *NewPath)) { UE_LOG(LogDiffAssets, Error, TEXT("Failed to load file '%s'."), *NewPath); return; } if (!NewState.Load(SerializedAssetData)) { UE_LOG(LogDiffAssets, Error, TEXT("Failed to parse file '%s' as asset registry."), *NewPath); return; } } int64 newtotal = 0; int64 oldtotal = 0; int64 newuncooked = 0; int64 olduncooked = 0; int64 newassets = 0; int64 oldassets = 0; UE_LOG(LogDiffAssets, Display, TEXT("Comparing asset registries '%s' and '%s'."), *OldPath, *NewPath); if (bUseSourceGuid) { UE_LOG(LogDiffAssets, Display, TEXT("Source Package Diff")); } else { UE_LOG(LogDiffAssets, Display, TEXT("Cooked Package Diff")); } if (bIsVerbose) { UE_LOG(LogDiffAssets, Display, TEXT("Package changes:")); } TSet Modified; TSet New; if (bUseSourceGuid) { for (const TPair& Pair: NewState.GetAssetPackageDataMap()) { FName Name = Pair.Key; FString NameString = Name.ToString(); if (bEnginePackagesOnly && !NameString.StartsWith(TEXT("/Engine/"))) { continue; } if (!IsInRelevantChunk(NewState, Name)) { continue; } const FAssetPackageData* Data = Pair.Value; const FAssetPackageData* PrevData = OldState.GetAssetPackageData(Name); if (Data->DiskSize < 0) { newuncooked++; } newassets += NewState.GetAssetsByPackageName(Name).Num(); if (!PrevData) { New.Add(Name); RecordAdd(Name, *Data); } PRAGMA_DISABLE_DEPRECATION_WARNINGS else if (Data->PackageGuid != PrevData->PackageGuid) PRAGMA_ENABLE_DEPRECATION_WARNINGS { Modified.Add(Name); } else { RecordNoChange(Name, *Data); } ++newtotal; } TArray Recurse = Modified.Array(); for (int32 RecurseIndex = 0; RecurseIndex < Recurse.Num(); RecurseIndex++) { FName Package = Recurse[RecurseIndex]; TArray Referencers; NewState.GetReferencers(Package, Referencers, UE::AssetRegistry::EDependencyCategory::Package, UE::AssetRegistry::EDependencyQuery::Hard); for (const FAssetIdentifier& Referencer: Referencers) { FName ReferencerPackage = Referencer.PackageName; if (!New.Contains(ReferencerPackage) && !Modified.Contains(ReferencerPackage)) { Modified.Add(ReferencerPackage); Recurse.Add(ReferencerPackage); } } } const TMap& PackageMap = NewState.GetAssetPackageDataMap(); for (FName const& Package: Modified) { const FAssetPackageData* Data = PackageMap[Package]; const FAssetPackageData* PrevData = OldState.GetAssetPackageData(Package); RecordEdit(Package, *Data, *PrevData); } } else { for (const TPair& Pair: NewState.GetAssetPackageDataMap()) { FName Name = Pair.Key; if (bEnginePackagesOnly && !Name.ToString().StartsWith(TEXT("/Engine/"))) { continue; } if (!IsInRelevantChunk(NewState, Name)) { continue; } const FAssetPackageData* Data = Pair.Value; const FAssetPackageData* PrevData = OldState.GetAssetPackageData(Name); if (Data->DiskSize < 0) { newuncooked++; } newassets += NewState.GetAssetsByPackageName(Name).Num(); if (!PrevData) { RecordAdd(Name, *Data); AssetPathFlags.FindOrAdd(Name) |= EAssetFlags::Add; } else if (Data->CookedHash != PrevData->CookedHash) { RecordEdit(Name, *Data, *PrevData); AssetPathFlags.FindOrAdd(Name) |= EAssetFlags::HashChange; PRAGMA_DISABLE_DEPRECATION_WARNINGS if (Data->PackageGuid != PrevData->PackageGuid) PRAGMA_ENABLE_DEPRECATION_WARNINGS { AssetPathFlags.FindOrAdd(Name) |= EAssetFlags::GuidChange; } } else { RecordNoChange(Name, *Data); } newtotal++; } } for (const TPair& Pair: OldState.GetAssetPackageDataMap()) { FName Name = Pair.Key; FString NameString = Name.ToString(); if (bEnginePackagesOnly && !NameString.StartsWith(TEXT("/Engine/"))) { continue; } if (!IsInRelevantChunk(OldState, Name)) { continue; } const FAssetPackageData* PrevData = Pair.Value; const FAssetPackageData* Data = NewState.GetAssetPackageData(Name); if (PrevData->DiskSize < 0) { olduncooked++; } oldassets += OldState.GetAssetsByPackageName(Name).Num(); if (!Data) { RecordDelete(Name, *PrevData); AssetPathFlags.FindOrAdd(Name) |= EAssetFlags::Remove; } oldtotal++; } // Propagate hash/guid changes down through referencers { TArray Recurse; AssetPathFlags.GetKeys(Recurse); for (int32 RecurseIndex = 0; RecurseIndex < Recurse.Num(); RecurseIndex++) { FName Package = Recurse[RecurseIndex]; TArray Referencers; // grab the hash/guid change flags, shift up to the dependency ones int32 PackageFlags = AssetPathFlags.FindOrAdd(Package); int32 NewFlags = (PackageFlags & 0x0C) << 2; // If we have a dependency chain like C -> B -> A, and A changes, this does not // necessarily cause B's binary representation to change, but it can still impact C. // We must propagate the dependency change flags too, otherwise C will be marked as // non-deterministic when this happens. NewFlags |= PackageFlags & (EAssetFlags::DepGuidChange | EAssetFlags::DepHashChange); // don't bother touching anything if this asset or its dependencies didn't change if (NewFlags) { NewState.GetReferencers(Package, Referencers, UE::AssetRegistry::EDependencyCategory::Package, UE::AssetRegistry::EDependencyQuery::Hard); for (const FAssetIdentifier& Referencer: Referencers) { FName RefPackage = Referencer.PackageName; // merge new dependency flags in, add to the list if something changed int32& RefFlags = AssetPathFlags.FindOrAdd(RefPackage); int32 OldFlags = RefFlags; RefFlags |= NewFlags; if (RefFlags != OldFlags) { Recurse.Add(RefPackage); } } } } } FArchive* CSVFile = nullptr; if (bSaveCSV) { CSVFile = IFileManager::Get().CreateFileWriter(*CSVFilename); CSVFile->Logf(TEXT("old,%s"), *OldPath); CSVFile->Logf(TEXT("new,%s"), *NewPath); CSVFile->Logf(TEXT("")); } SummarizeDeterminism(); LogChangedFiles(CSVFile, OldPath, NewPath); // start summary UE_LOG(LogDiffAssets, Display, TEXT("Summary:")); UE_LOG(LogDiffAssets, Display, TEXT("Old AssetRegistry: %s"), *OldPath); UE_LOG(LogDiffAssets, Display, TEXT("%d packages total, %d uncooked, %d cooked assets"), oldtotal, olduncooked, oldassets); UE_LOG(LogDiffAssets, Display, TEXT("New AssetRegistry: %s"), *NewPath); const float InvToMB = 1.0 / (1024 * 1024); // Overall Class Summary // Actually do the warnings in this run, since it's the overall one LogClassSummary(CSVFile, TEXT("Overall"), ChangeSummaryByClass, true, DeterminismByClass); // Chunk-by-chunk class summaries if (bGroupByChunk) { TArray ChunkIDs; ChangesByChunk.GetKeys(ChunkIDs); ChunkIDs.Sort(); for (int32 ChunkID: ChunkIDs) { FString ChunkHeader = (ChunkID == -1) ? TEXT("Untagged") : FString::Printf(TEXT("Chunk %d"), ChunkID); if (ChangesByChunk[ChunkID].ChangesByClass.Num()) { LogClassSummary(CSVFile, *ChunkHeader, ChangesByChunk[ChunkID].ChangesByClass, false, ChangesByChunk[ChunkID].Determinism); } } } #if 0 TArray Changelists; ChangeSummaryByChangelist.GetKeys(Changelists); Changelists.Sort([this](const int32& Lhs, const int32& Rhs) { const FChangeInfo& LHSChanges = ChangeSummaryByChangelist[Lhs]; const FChangeInfo& RHSChanges = ChangeSummaryByChangelist[Rhs]; return LHSChanges.GetTotalChangeSize() > RHSChanges.GetTotalChangeSize(); }); for (int32 Changelist : Changelists) { const FChangeInfo& Changes = ChangeSummaryByChangelist[Changelist]; if (Changes.GetTotalChangeSize() == 0) { continue; } if (Changes.GetTotalChangeCount() < MinChangeCount || Changes.GetTotalChangeSize() < (MinChangeSizeMB*1024*1024)) { continue; } if (Changelist) { UE_LOG(LogDiffAssets, Display, TEXT("%d: %.02f%% changes (%.02f MB Total)"), Changelist, Changes.GetChangePercentage() * 100.0, Changes.GetTotalChangeSize() * InvToMB); } else { UE_LOG(LogDiffAssets, Display, TEXT("Unattributable (nondeterministic?): %.02f%% changes (%.02f MB Total)"), Changes.GetChangePercentage() * 100.0, Changes.GetTotalChangeSize() * InvToMB); } if (Changes.Adds) { UE_LOG(LogDiffAssets, Display, TEXT("\t%d packages added, %8.3f MB"), Changes.Adds, Changes.AddedBytes * InvToMB); } if (Changes.Changes) { UE_LOG(LogDiffAssets, Display, TEXT("\t%d packages modified, %8.3f MB"), Changes.Changes, Changes.ChangedBytes * InvToMB); } if (Changes.Deletes) { UE_LOG(LogDiffAssets, Display, TEXT("\t%d packages removed, %8.3f MB"), Changes.Deletes, Changes.DeletedBytes * InvToMB); } } #endif // these are parsed by scripts, so please don't modify :) UE_LOG(LogDiffAssets, Display, TEXT("%d total packages, %d uncooked, %d cooked assets"), newtotal, newuncooked, newassets); UE_LOG(LogDiffAssets, Display, TEXT("%d total unchanged, %8.3f MB"), ChangeSummary.Unchanged, ChangeSummary.UnchangedBytes * InvToMB); UE_LOG(LogDiffAssets, Display, TEXT("%d total packages added, %8.3f MB"), ChangeSummary.Adds, ChangeSummary.AddedBytes * InvToMB); UE_LOG(LogDiffAssets, Display, TEXT("%d total packages modified, %8.3f MB"), ChangeSummary.Changes, ChangeSummary.ChangedBytes * InvToMB); UE_LOG(LogDiffAssets, Display, TEXT("%d total packages removed, %8.3f MB"), ChangeSummary.Deletes, ChangeSummary.DeletedBytes * InvToMB); UE_LOG(LogDiffAssets, Display, TEXT("Nondeterministic summary:")); UE_LOG(LogDiffAssets, Display, TEXT("direct %d total packages modified, %8.3f MB"), NondeterministicSummary.Changes, NondeterministicSummary.ChangedBytes * InvToMB); UE_LOG(LogDiffAssets, Display, TEXT("indirect %d total packages modified, %8.3f MB"), IndirectNondeterministicSummary.Changes, IndirectNondeterministicSummary.ChangedBytes * InvToMB); // Warn when meeting or exceeding a certain total changed size, if that's enabled if (WarnTotalChangedSizeMB > 0 && ChangeSummary.ChangedBytes * InvToMB >= WarnTotalChangedSizeMB) { UE_LOG(LogDiffAssets, Warning, TEXT("Total Changed Bytes exceeded %d MB! (%8.3f MB)"), WarnTotalChangedSizeMB, ChangeSummary.ChangedBytes * InvToMB); } if (CSVFile) { CSVFile->Logf(TEXT("")); CSVFile->Logf(TEXT("Summary")); CSVFile->Logf(TEXT("total,%d"), newtotal); CSVFile->Logf(TEXT("unchanged,%lld,%lld"), ChangeSummary.Unchanged, ChangeSummary.UnchangedBytes); CSVFile->Logf(TEXT("added,%lld,%lld"), ChangeSummary.Adds, ChangeSummary.AddedBytes); CSVFile->Logf(TEXT("modified,%lld,%lld"), ChangeSummary.Changes, ChangeSummary.ChangedBytes); CSVFile->Logf(TEXT("removed,%lld,%lld"), ChangeSummary.Deletes, ChangeSummary.DeletedBytes); CSVFile->Logf(TEXT("direct non-deterministic,%lld,%lld"), NondeterministicSummary.Changes, NondeterministicSummary.ChangedBytes); CSVFile->Logf(TEXT("indirect non-deterministic,%lld,%lld"), IndirectNondeterministicSummary.Changes, IndirectNondeterministicSummary.ChangedBytes); delete CSVFile; } } bool UDiffAssetRegistriesCommandlet::LaunchP4(const FString& Args, TArray& Output, int32& OutReturnCode) const { void* PipeRead = nullptr; void* PipeWrite = nullptr; verify(FPlatformProcess::CreatePipe(PipeRead, PipeWrite)); bool bInvoked = false; OutReturnCode = -1; FString StringOutput; FProcHandle ProcHandle = FPlatformProcess::CreateProc(TEXT("p4.exe"), *Args, false, true, true, nullptr, 0, nullptr, PipeWrite); if (ProcHandle.IsValid()) { while (FPlatformProcess::IsProcRunning(ProcHandle)) { FString ThisRead = FPlatformProcess::ReadPipe(PipeRead); StringOutput += ThisRead; // Re-enable waits if constant pipe-querying is somehow damaging, but keep the wait SMALL // if (ThisRead.Len() <= 0) // { // FPlatformProcess::Sleep(0.001f); // } } StringOutput += FPlatformProcess::ReadPipe(PipeRead); FPlatformProcess::GetProcReturnCode(ProcHandle, &OutReturnCode); bInvoked = true; } else { UE_LOG(LogDiffAssets, Error, TEXT("Failed to launch p4.")); } FPlatformProcess::ClosePipe(PipeRead, PipeWrite); StringOutput.ParseIntoArrayLines(Output); return bInvoked; }