// Portage UE 5.5 - lot debug. Voir LPDebugSubsystem.h. #include "LPDebugSubsystem.h" #include "LifePlay.h" #if !UE_BUILD_SHIPPING #include "Chaos/TriangleMeshImplicitObject.h" #include "Components/PrimitiveComponent.h" #include "Components/StaticMeshComponent.h" #include "Engine/Engine.h" #include "Engine/World.h" #include "EngineUtils.h" #include "HAL/FileManager.h" #include "HAL/IConsoleManager.h" #include "HAL/PlatformMemory.h" #include "Misc/CommandLine.h" #include "Misc/CoreDelegates.h" #include "Misc/FileHelper.h" #include "Misc/Parse.h" #include "Misc/Paths.h" #include "PhysicsEngine/BodySetup.h" #include "ProfilingDebugging/MiscTrace.h" namespace { FAutoConsoleCommand* MemSnapshotCommand = nullptr; FAutoConsoleCommand* CollisionDumpCommand = nullptr; FAutoConsoleCommand* BookmarkCommand = nullptr; FAutoConsoleCommand* HitchSummaryCommand = nullptr; ULPDebugSubsystem* CommandOwner = nullptr; ULPDebugSubsystem* FindDebugSubsystem() { return CommandOwner; } UWorld* FindPlayWorld() { if (!GEngine) { return nullptr; } if (UWorld* Play = GEngine->GetCurrentPlayWorld()) { return Play; } for (const FWorldContext& Context : GEngine->GetWorldContexts()) { UWorld* World = Context.World(); if (World && (World->WorldType == EWorldType::Game || World->WorldType == EWorldType::PIE)) { return World; } } return nullptr; } FString DebugDir() { const FString Dir = FPaths::ProjectSavedDir() / TEXT("Profiling/LPDebug"); IFileManager::Get().MakeDirectory(*Dir, true); return Dir; } void WriteMemSnapshot() { const FString Stamp = FDateTime::Now().ToString(TEXT("%Y%m%d_%H%M%S")); const FString Path = DebugDir() / FString::Printf(TEXT("MemSnapshot_%s.txt"), *Stamp); const FPlatformMemoryStats Stats = FPlatformMemory::GetStats(); TArray Lines; Lines.Add(FString::Printf(TEXT("When=%s"), *FDateTime::Now().ToString())); Lines.Add(FString::Printf(TEXT("UsedPhysicalMB=%.1f"), Stats.UsedPhysical / (1024.0 * 1024.0))); Lines.Add(FString::Printf(TEXT("PeakUsedPhysicalMB=%.1f"), Stats.PeakUsedPhysical / (1024.0 * 1024.0))); Lines.Add(FString::Printf(TEXT("AvailablePhysicalMB=%.1f"), Stats.AvailablePhysical / (1024.0 * 1024.0))); Lines.Add(FString::Printf(TEXT("UsedVirtualMB=%.1f"), Stats.UsedVirtual / (1024.0 * 1024.0))); UWorld* World = FindPlayWorld(); if (GEngine) { const bool bReport = GEngine->Exec(World, TEXT("memreport")); Lines.Add(FString::Printf(TEXT("memreport=%s"), bReport ? TEXT("ok") : TEXT("echec"))); } const FString ReportDir = FPaths::ProjectSavedDir() / TEXT("Profiling/MemReports"); TArray Reports; IFileManager::Get().FindFilesRecursive(Reports, *ReportDir, TEXT("*.memreport"), true, false); if (Reports.Num() > 0) { Reports.Sort(); Lines.Add(FString::Printf(TEXT("NewestMemReport=%s"), *Reports.Last())); } Lines.Add(TEXT("PhysicsMB=voir LLM (colonne Physics) ou le MemReport ; ce fichier ne remplace pas la session B")); FFileHelper::SaveStringArrayToFile(Lines, *Path); UE_LOG(LogLifePlay, Display, TEXT("LPDebug MemSnapshot : %s"), *Path); } struct FCollisionOwner { FString Actor; FString ClassName; FString Component; FString Profile; int64 Triangles = 0; int32 TriMeshes = 0; int32 Simple = 0; int32 Capsules = 0; }; struct FCollisionTally { FString WorldName; int32 Prim = 0; int32 Simulating = 0; int32 QueryOnly = 0; int32 Persons = 0; int32 Players = 0; int32 Street = 0; int32 UniqueBodies = 0; int32 UniqueSimple = 0; int32 UniqueTriMeshes = 0; int32 UniqueCapsules = 0; int64 Triangles = 0; TMap ByClass; TMap TrisByClass; TArray Owners; }; int64 CountTriangles(const UBodySetup* Body) { int64 Count = 0; if (!Body) { return 0; } for (const Chaos::FTriangleMeshImplicitObjectPtr& Mesh : Body->TriMeshGeometries) { if (Mesh) { Count += Mesh->Elements().GetNumTriangles(); } } return Count; } FString ComponentLabel(const UPrimitiveComponent* Prim) { if (const UStaticMeshComponent* MeshComp = Cast(Prim)) { if (const UStaticMesh* Mesh = MeshComp->GetStaticMesh()) { return Mesh->GetName(); } } return Prim ? Prim->GetName() : FString(); } FCollisionTally GatherCollision() { FCollisionTally Tally; UWorld* World = FindPlayWorld(); Tally.WorldName = World ? World->GetName() : TEXT("(aucune)"); if (!World) { return Tally; } TSet Bodies; for (TActorIterator It(World); It; ++It) { const FString ClassName = It->GetClass()->GetName(); TInlineComponentArray Comps; It->GetComponents(Comps); for (UPrimitiveComponent* Prim : Comps) { if (!Prim || !Prim->IsCollisionEnabled()) { continue; } ++Tally.Prim; Tally.ByClass.FindOrAdd(ClassName)++; if (ClassName.StartsWith(TEXT("Person_C"))) { ++Tally.Persons; } else if (ClassName.StartsWith(TEXT("PC_C"))) { ++Tally.Players; } else if (ClassName.Contains(TEXT("StreetMap"))) { ++Tally.Street; } if (Prim->IsSimulatingPhysics()) { ++Tally.Simulating; } if (Prim->GetCollisionEnabled() == ECollisionEnabled::QueryOnly) { ++Tally.QueryOnly; } UBodySetup* Body = Prim->GetBodySetup(); if (!Body || Bodies.Contains(Body)) { continue; } Bodies.Add(Body); const int64 Tris = CountTriangles(Body); const int32 Capsules = Body->AggGeom.SphylElems.Num(); const int32 Simple = Body->AggGeom.GetElementCount(); Tally.UniqueBodies++; Tally.UniqueSimple += Simple; Tally.UniqueCapsules += Capsules; Tally.UniqueTriMeshes += Body->TriMeshGeometries.Num(); Tally.Triangles += Tris; Tally.TrisByClass.FindOrAdd(ClassName) += Tris; if (Tris > 0 || Simple > 0) { FCollisionOwner& Owner = Tally.Owners.AddDefaulted_GetRef(); Owner.Actor = It->GetName(); Owner.ClassName = ClassName; Owner.Component = ComponentLabel(Prim); Owner.Profile = Prim->GetCollisionProfileName().ToString(); Owner.Triangles = Tris; Owner.TriMeshes = Body->TriMeshGeometries.Num(); Owner.Simple = Simple; Owner.Capsules = Capsules; } } } Tally.Owners.Sort([](const FCollisionOwner& A, const FCollisionOwner& B) { return A.Triangles > B.Triangles; }); return Tally; } FString WriteCollisionDump(const TCHAR* Reason, const FCollisionTally* Existing = nullptr) { const FCollisionTally Tally = Existing ? *Existing : GatherCollision(); const FString Stamp = FDateTime::Now().ToString(TEXT("%Y%m%d_%H%M%S")); const FString Path = DebugDir() / FString::Printf(TEXT("CollisionDump_%s.txt"), *Stamp); TArray Lines; Lines.Add(FString::Printf(TEXT("When=%s"), *FDateTime::Now().ToString())); Lines.Add(FString::Printf(TEXT("Reason=%s"), Reason)); Lines.Add(FString::Printf(TEXT("World=%s"), *Tally.WorldName)); Lines.Add(FString::Printf(TEXT("CollisionPrimitives=%d"), Tally.Prim)); Lines.Add(FString::Printf(TEXT("PersonPrimitives=%d"), Tally.Persons)); Lines.Add(FString::Printf(TEXT("PlayerPrimitives=%d"), Tally.Players)); Lines.Add(FString::Printf(TEXT("StreetMapPrimitives=%d"), Tally.Street)); Lines.Add(FString::Printf(TEXT("Simulating=%d"), Tally.Simulating)); Lines.Add(FString::Printf(TEXT("QueryOnly=%d"), Tally.QueryOnly)); Lines.Add(FString::Printf(TEXT("UniqueBodySetups=%d"), Tally.UniqueBodies)); Lines.Add(FString::Printf(TEXT("UniqueSimpleShapes=%d"), Tally.UniqueSimple)); Lines.Add(FString::Printf(TEXT("UniqueCapsules=%d"), Tally.UniqueCapsules)); Lines.Add(FString::Printf(TEXT("UniqueTriMeshes=%d"), Tally.UniqueTriMeshes)); Lines.Add(FString::Printf(TEXT("Triangles=%lld"), Tally.Triangles)); Lines.Add(TEXT("TopClasses:")); TArray> Classes; for (const TPair& Pair : Tally.ByClass) { Classes.Add(Pair); } Classes.Sort([](const TPair& A, const TPair& B) { return A.Value > B.Value; }); for (int32 i = 0; i < Classes.Num() && i < 15; ++i) { const int64 Tris = Tally.TrisByClass.FindRef(Classes[i].Key); Lines.Add(FString::Printf(TEXT(" %d %s triangles=%lld"), Classes[i].Value, *Classes[i].Key, Tris)); } Lines.Add(TEXT("OwnersByTriangles:")); for (int32 i = 0; i < Tally.Owners.Num() && i < 12; ++i) { const FCollisionOwner& Owner = Tally.Owners[i]; Lines.Add(FString::Printf( TEXT(" triangles=%lld trimesh=%d simple=%d capsules=%d profile=%s %s %s %s"), Owner.Triangles, Owner.TriMeshes, Owner.Simple, Owner.Capsules, *Owner.Profile, *Owner.ClassName, *Owner.Actor, *Owner.Component)); } if (Tally.WorldName == TEXT("(aucune)")) { Lines.Add(TEXT("Note=monde de jeu absent (appeler la commande une fois la partie chargee)")); } Lines.Add(TEXT("Switch=-LPCollisionLog pour echantillonner apres le chargement, pas au ExecCmds de demarrage")); FFileHelper::SaveStringArrayToFile(Lines, *Path); UE_LOG(LogLifePlay, Display, TEXT("LPDebug collision : %d primitives, %d trimesh, %lld triangles (%s) -> %s"), Tally.Prim, Tally.UniqueTriMeshes, Tally.Triangles, Reason, *Path); TRACE_BOOKMARK(TEXT("LP Collision %lld tris"), Tally.Triangles); return Path; } void WriteBookmark(const TArray& Args) { const FString Text = Args.Num() > 0 ? FString::Join(Args, TEXT(" ")) : TEXT("manuel"); UE_LOG(LogLifePlay, Display, TEXT("LPDebug bookmark : %s"), *Text); TRACE_BOOKMARK(TEXT("LP %s"), *Text); } } void ULPDebugSubsystem::Initialize(FSubsystemCollectionBase& Collection) { Super::Initialize(Collection); if (!MemSnapshotCommand) { MemSnapshotCommand = new FAutoConsoleCommand( TEXT("LifePlay.MemSnapshot"), TEXT("Ecrit Saved/Profiling/LPDebug/MemSnapshot_*.txt et lance memreport."), FConsoleCommandDelegate::CreateLambda([]() { WriteMemSnapshot(); })); CollisionDumpCommand = new FAutoConsoleCommand( TEXT("LifePlay.CollisionDump"), TEXT("Ecrit un releve des primitives de collision du monde courant."), FConsoleCommandDelegate::CreateLambda([]() { WriteCollisionDump(TEXT("console")); })); BookmarkCommand = new FAutoConsoleCommand( TEXT("LifePlay.Bookmark"), TEXT("Signet Insights (TRACE_BOOKMARK) et ligne LogLifePlay. Argument : texte du signet."), FConsoleCommandWithArgsDelegate::CreateStatic(&WriteBookmark)); HitchSummaryCommand = new FAutoConsoleCommand( TEXT("LifePlay.HitchSummary"), TEXT("Ecrit le bilan courant des a-coups (-LPHitchLog)."), FConsoleCommandDelegate::CreateLambda([]() { if (ULPDebugSubsystem* Sub = FindDebugSubsystem()) { Sub->LogHitchSummary(TEXT("console")); } else { UE_LOG(LogLifePlay, Display, TEXT("LPDebug hitch : sous-systeme absent")); } })); CommandOwner = this; } bHitchLog = FParse::Param(FCommandLine::Get(), TEXT("LPHitchLog")); HitchMs = 50.f; FParse::Value(FCommandLine::Get(), TEXT("LPHitchMs="), HitchMs); bCollisionLog = FParse::Param(FCommandLine::Get(), TEXT("LPCollisionLog")); if (bHitchLog || bCollisionLog) { StartSeconds = FPlatformTime::Seconds(); LastFrameSeconds = StartSeconds; } if (bHitchLog) { UE_LOG(LogLifePlay, Display, TEXT("LPDebug hitch : actif, seuil %.1f ms"), HitchMs); } else { UE_LOG(LogLifePlay, Display, TEXT("LPDebug hitch : inactif")); } if (bCollisionLog) { const FString Stamp = FDateTime::Now().ToString(TEXT("%Y%m%d_%H%M%S")); CollisionTrackPath = DebugDir() / FString::Printf(TEXT("CollisionTrack_%s.txt"), *Stamp); FFileHelper::SaveStringToFile( TEXT("t\tworld\tprim\tpersons\tplayers\tstreet\ttrimesh\ttriangles\n"), *CollisionTrackPath); NextCollisionSeconds = FPlatformTime::Seconds() + 2.0; UE_LOG(LogLifePlay, Display, TEXT("LPDebug collision : actif, premier echantillon dans 2 s -> %s"), *CollisionTrackPath); } else { UE_LOG(LogLifePlay, Display, TEXT("LPDebug collision : inactif")); } if (bHitchLog || bCollisionLog) { FCoreDelegates::OnEndFrame.AddUObject(this, &ULPDebugSubsystem::OnEndFrame); } } void ULPDebugSubsystem::Deinitialize() { if (bHitchLog) { LogHitchSummary(TEXT("fin")); } if (bCollisionLog) { LogCollisionSummary(TEXT("fin")); } if (bHitchLog || bCollisionLog) { FCoreDelegates::OnEndFrame.RemoveAll(this); } if (CommandOwner == this) { CommandOwner = nullptr; } Super::Deinitialize(); } void ULPDebugSubsystem::OnEndFrame() { const double Now = FPlatformTime::Seconds(); if (bCollisionLog && Now >= NextCollisionSeconds) { NextCollisionSeconds = Now + 2.0; SampleCollision(); } if (!bHitchLog) { return; } const double DeltaMs = (Now - LastFrameSeconds) * 1000.0; LastFrameSeconds = Now; if (DeltaMs >= HitchMs) { ++HitchCount; MaxHitchMs = FMath::Max(MaxHitchMs, DeltaMs); if (HitchLines < 100 || (HitchCount % 25) == 0) { ++HitchLines; UE_LOG(LogLifePlay, Warning, TEXT("LPDebug hitch : %.1f ms (seuil %.1f) #%d"), DeltaMs, HitchMs, HitchCount); TRACE_BOOKMARK(TEXT("LP Hitch %.1f ms"), DeltaMs); } } if (!bLoggedTwoMinuteSummary && (Now - StartSeconds) >= 120.0) { bLoggedTwoMinuteSummary = true; LogHitchSummary(TEXT("2 min")); } } void ULPDebugSubsystem::LogHitchSummary(const TCHAR* When) const { const double Elapsed = StartSeconds > 0.0 ? FPlatformTime::Seconds() - StartSeconds : 0.0; UE_LOG(LogLifePlay, Display, TEXT("LPDebug hitch : %d hitch(s) en %.1f s (seuil %.1f ms), max %.1f ms, %s"), HitchCount, Elapsed, HitchMs, MaxHitchMs, When); } void ULPDebugSubsystem::SampleCollision() { const FCollisionTally Tally = GatherCollision(); ++CollisionSamples; PeakPrim = FMath::Max(PeakPrim, Tally.Prim); PeakTriangles = FMath::Max(PeakTriangles, Tally.Triangles); const double Elapsed = StartSeconds > 0.0 ? FPlatformTime::Seconds() - StartSeconds : FPlatformTime::Seconds(); const FString Line = FString::Printf( TEXT("%.1f\t%s\t%d\t%d\t%d\t%d\t%d\t%lld\n"), Elapsed, *Tally.WorldName, Tally.Prim, Tally.Persons, Tally.Players, Tally.Street, Tally.UniqueTriMeshes, Tally.Triangles); FFileHelper::SaveStringToFile(Line, *CollisionTrackPath, FFileHelper::EEncodingOptions::AutoDetect, &IFileManager::Get(), FILEWRITE_Append); UE_LOG(LogLifePlay, Display, TEXT("LPDebug collision : %.0f s %s prim=%d pnj=%d trimesh=%d triangles=%lld"), Elapsed, *Tally.WorldName, Tally.Prim, Tally.Persons, Tally.UniqueTriMeshes, Tally.Triangles); const bool bGrew = Tally.Triangles >= LastDumpedTriangles + 1000 || Tally.UniqueTriMeshes > LastDumpedTriMeshes; if (CollisionDumps < 4 && Tally.Triangles > 0 && bGrew) { WriteCollisionDump(TEXT("suivi"), &Tally); ++CollisionDumps; LastDumpedTriangles = Tally.Triangles; LastDumpedTriMeshes = Tally.UniqueTriMeshes; } } void ULPDebugSubsystem::LogCollisionSummary(const TCHAR* When) const { if (CollisionDumps == 0 && PeakPrim > 0) { WriteCollisionDump(TEXT("fin")); } UE_LOG(LogLifePlay, Display, TEXT("LPDebug collision : %d echantillon(s), %d dump(s), max %d primitives, %lld triangles, %s"), CollisionSamples, CollisionDumps, PeakPrim, PeakTriangles, When); } #else void ULPDebugSubsystem::Initialize(FSubsystemCollectionBase& Collection) { Super::Initialize(Collection); } void ULPDebugSubsystem::Deinitialize() { Super::Deinitialize(); } void ULPDebugSubsystem::OnEndFrame() { } void ULPDebugSubsystem::LogHitchSummary(const TCHAR* When) const { (void)When; } void ULPDebugSubsystem::SampleCollision() { } void ULPDebugSubsystem::LogCollisionSummary(const TCHAR* When) const { (void)When; } #endif