--- name: ue-testing-debugging description: "Use when writing or running Unreal Engine tests, adding logging, or profiling a build. Also use when the user mentions 'UE_LOG', 'log category', 'DEFINE_LOG_CATEGORY', 'UE_LOGFMT', 'verbosity', 'check', 'ensure', 'ensureMsgf', 'verify', 'IMPLEMENT_SIMPLE_AUTOMATION_TEST', 'automation test', 'TEST_CLASS', 'CQTest', 'AFunctionalTest', 'latent command', 'Unreal Insights', 'TRACE_CPUPROFILER_EVENT_SCOPE', 'TRACE_BOOKMARK', 'stat unit', 'SCOPE_CYCLE_COUNTER', 'memreport', 'DrawDebugLine', 'UE_VLOG', 'gameplay debugger', 'ShowDebug', 'console command', 'CVar', or 'why is my frame time bad'. For module and target wiring, see ue-module-build-system; for editor tooling, see ue-editor-tools." metadata: version: "2.0.0" engine: "5.8" --- # UE Testing, Logging & Profiling Target engine: **UE 5.8**. APIs below are verified against the 5.8 headers; older forms are listed under "Deprecated — do not use". This skill owns logging, assertions, automation/functional testing, console commands and performance profiling for the repo. Logging, assertions, stats, trace and CVars live in `Core` (`Logging/`, `Misc/AssertionMacros.h`, `Stats/Stats.h`, `ProfilingDebugging/`, `HAL/IConsoleManager.h`). Debug drawing, the Visual Logger and `AHUD::ShowDebug` live in `Engine`. Test frameworks are the developer modules `AutomationController`, `CQTest`, `FunctionalTesting` and `AutomationDriver`; the Gameplay Debugger is the runtime module `GameplayDebugger`. ## Context Read `.agents/ue-project-context.md` if it exists (module names, conventions, enabled plugins, GAS/networking setup). Do not stop if it is missing. Identify the area from the request and the codebase; ask only when two plausible readings would produce different code. | Request is about… | Go to | |---|---| | `UE_LOG`, categories, verbosity, structured logging | [Logging](#logging) | | `check`, `ensure`, `verify`, crash on bad state | [Assertions](#assertions) | | Unit tests, `IMPLEMENT_SIMPLE_AUTOMATION_TEST`, specs, latent commands | [Automation Tests](#automation-tests) | | `TEST_CLASS`, `ASSERT_THAT`, fixture-style tests | [CQTest](#cqtest) | | Tests placed in a map, `AFunctionalTest` | [Functional Tests](#functional-tests) | | Insights, `stat`, cycle counters, CSV, memory, hitches | [Profiling](#profiling) | | `DrawDebug*`, `UE_VLOG`, Gameplay Debugger, `ShowDebug` | [Debug Visualization](#debug-visualization) | | `Exec` functions, `FAutoConsoleCommand`, CVars | [Console Commands and CVars](#console-commands-and-cvars) | ## Logging ### Declaring categories ```cpp #include "Logging/LogMacros.h" // MyGameLogging.h — DECLARE_LOG_CATEGORY_EXTERN(Name, DefaultVerbosity, CompileTimeVerbosity) DECLARE_LOG_CATEGORY_EXTERN(LogMyGame, Log, All); DEFINE_LOG_CATEGORY(LogMyGame); // MyGameLogging.cpp ``` | Macro | Where | Scope | |---|---|---| | `DECLARE_LOG_CATEGORY_EXTERN(Name, Default, CompileTime)` | header | the module and everything that includes it | | `DEFINE_LOG_CATEGORY(Name)` | one `.cpp` | pairs with the `EXTERN` form | | `DEFINE_LOG_CATEGORY_STATIC(Name, Default, CompileTime)` | one `.cpp` | that translation unit only | | `DECLARE_LOG_CATEGORY_CLASS(Name, Default, CompileTime)` | class body | class static; pair with `DEFINE_LOG_CATEGORY_CLASS(Class, Name)` | `DefaultVerbosity` must be `<=` `CompileTimeVerbosity` or the `static_assert` inside the category macro fires. Anything above `CompileTimeVerbosity` is deleted by the compiler, so `DECLARE_LOG_CATEGORY_EXTERN(LogMyGame, Log, Log)` removes every `Verbose`/`VeryVerbose` call site. Monolithic builds can cap all categories at once by defining `COMPILED_IN_MINIMUM_VERBOSITY`. ### Verbosity levels | Level | Console | File | Notes | |---|---|---|---| | `Fatal` | yes | yes | crashes the process, even when logging is disabled | | `Error` | yes | yes | collected by commandlets and the editor; fails a commandlet | | `Warning` | yes | yes | can be promoted to an error | | `Display` | yes | yes | use this when the user must see the line | | `Log` | **no** | yes | file only — the default for development messages | | `Verbose` | no | yes | only once the category is raised to `Verbose` | | `VeryVerbose` | no | yes | only once the category is raised to `VeryVerbose` | `ELogVerbosity::All` is an alias for `VeryVerbose` and is the usual `CompileTimeVerbosity` argument. ### Emitting ```cpp UE_LOG(LogMyGame, Display, TEXT("Player %s spawned"), *PlayerName); UE_LOG(LogMyGame, Warning, TEXT("Inventory full, dropped %s"), *ItemName); UE_LOG(LogMyGame, Error, TEXT("Failed to load asset %s"), *AssetPath); UE_LOG(LogMyGame, Fatal, TEXT("Save data unrecoverable")); // crashes // Condition and arguments are evaluated only when the category is active. UE_CLOG(Health <= 0.f, LogMyGame, Warning, TEXT("%s has zero health"), *GetName()); ``` Wrap an expensive report in `if (UE_LOG_ACTIVE(LogMyGame, Verbose)) { … }` so the work itself is skipped, not just the formatting. ### Structured logging `UE_LOGFMT` emits named fields that survive into Insights and log analysis; use `{Name}` placeholders, not `%s`. ```cpp #include "Logging/StructuredLog.h" // Named fields: order is irrelevant, extra fields are allowed. UE_LOGFMT(LogMyGame, Warning, "Loading '{Name}' failed with error {Error}", ("Name", AssetName), ("Error", ErrorCode), ("Flags", LoadFlags)); // Positional fields: the values must match the placeholders exactly. UE_LOGFMT(LogMyGame, Display, "Spawned {Count} actors in {Seconds}s", SpawnCount, Elapsed); UE_CLOGFMT(bFailed, LogMyGame, Error, "Retry {Attempt} failed", ("Attempt", Attempt)); ``` Field names must match `[A-Za-z0-9_]+` and be unique per event; values serialize through `SerializeForLog` or `operator<<(FCbWriter&, T)`. `UE_LOGFMT` takes at most 16 fields — use `UE_LOGFMT_EX` with `UE_LOGFMT_FIELD` / `UE_LOGFMT_VALUE` beyond that. ### Shipping and Test builds `NO_LOGGING` is `!USE_LOGGING_IN_SHIPPING` in **both** `Test` and `Shipping`, and `0` in `Debug` and `Development`. Under `NO_LOGGING`, `UE_LOG`/`UE_CLOG`/`UE_LOGFMT` collapse to a `Fatal`-only path: every non-fatal call disappears, arguments included. Set `bUseLoggingInShipping = true;` in `MyGame.Target.cs` to define `USE_LOGGING_IN_SHIPPING=1` and get logging back. ### Controlling verbosity at runtime ``` -LogCmds="LogMyGame Verbose, LogNet Warning" # command line, applied at boot Log list [Filter] # every category with its current verbosity Log LogMyGame Verbose # raise one category Log global none # silence everything Log reset # restore defaults ``` From code use `UE_SET_LOG_VERBOSITY(LogMyGame, Verbose)`; from config use `[Core.Log]` in `DefaultEngine.ini`. ## Assertions All in `Misc/AssertionMacros.h`. Choose by *who* is wrong: `check` for programmer error, `ensure` for a recoverable surprise worth reporting, a plain `if` for user or data input. ```cpp check(Component != nullptr); checkf(Index >= 0 && Index < Items.Num(), TEXT("Index %d out of [0,%d)"), Index, Items.Num()); checkNoEntry(); // unreachable branch checkNoReentry(); // must not be entered twice checkSlow(ValidateExpensiveInvariant()); // Debug only verify(Manager->Initialize()); // expression ALWAYS evaluated if (ensure(Component != nullptr)) { Component->Activate(); // ensure() returns the condition } ensureMsgf(Health > 0.f, TEXT("%s has health %.1f"), *GetName(), Health); ensureAlways(bInitialized); // reports every failure, not just the first ``` `ensure`/`ensureMsgf` report **once per call site per session**; `ensureAlways`/`ensureAlwaysMsgf` report every time. All four return the condition, so they compose with `if`. | Build | `DO_CHECK` | `DO_ENSURE` | `DO_GUARD_SLOW` | `NO_LOGGING` | |---|---|---|---|---| | Debug | 1 | 1 | 1 | 0 | | Development | 1 | 1 | 0 | 0 | | Test **and** Shipping | `USE_CHECKS_IN_SHIPPING` | `USE_ENSURES_IN_SHIPPING` | 0 | `!USE_LOGGING_IN_SHIPPING` | `USE_CHECKS_IN_SHIPPING` defaults to `0` and is set by `bUseChecksInShipping` in the target rules, so **Test builds also strip `check` and `ensure` by default**. When `DO_CHECK` is 0, `check(expr)` degrades to `CA_ASSUME(expr)` and the expression is never evaluated; `verify(expr)` still evaluates it and only drops the halt. ## Automation Tests Test sources live in a `Private/Tests` folder of the module under test, or in a dedicated test module. Wrap each test file in `#if WITH_DEV_AUTOMATION_TESTS` (or `WITH_AUTOMATION_TESTS` = `WITH_DEV_AUTOMATION_TESTS || WITH_PERF_AUTOMATION_TESTS`) — the `IMPLEMENT_*` macros alone still compile the class, they only skip registration when `WITH_AUTOMATION_WORKER` is 0 (`Misc/AutomationTest.h:4366`); UBT turns both on for every configuration except Test and Shipping, overridable with `bForceCompileDevelopmentAutomationTests`, `bForceCompilePerformanceAutomationTests` and `bForceDisableAutomationTests` (`UEBuildTarget.cs:6309-6325`). Module and target wiring, the full pattern library and the command lines for running tests are in [references/automation-test-patterns.md](references/automation-test-patterns.md). ```cpp // Source/MyGame/Private/Tests/MyInventoryTest.cpp — whole file inside #if WITH_DEV_AUTOMATION_TESTS #include "Misc/AutomationTest.h" #include "MyInventoryComponent.h" IMPLEMENT_SIMPLE_AUTOMATION_TEST(FMyInventoryAddTest, "MyGame.Inventory.AddItem", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) bool FMyInventoryAddTest::RunTest(const FString& Parameters) { UMyInventoryComponent* Inv = NewObject(GetTransientPackage()); UE_RETURN_ON_ERROR(Inv != nullptr, TEXT("Inventory created")); Inv->AddItem(FName("Sword"), 1); TestEqual(TEXT("Count after add"), Inv->GetItemCount(FName("Sword")), 1); TestTrue(TEXT("Has sword"), Inv->HasItem(FName("Sword"))); TestFalse(TEXT("No axe"), Inv->HasItem(FName("Axe"))); return true; } ``` `IMPLEMENT_COMPLEX_AUTOMATION_TEST` adds `virtual void GetTests(TArray& OutBeautifiedNames, TArray& OutTestCommands) const` and runs `RunTest` once per entry. `IMPLEMENT_CUSTOM_SIMPLE_AUTOMATION_TEST(TClass, TBaseClass, PrettyName, TFlags)` declares a simple test whose base is a shared fixture you derive from `FAutomationTestBase`. ### Flags `EAutomationTestFlags` is an `enum class` with `ENUM_CLASS_FLAGS`, so combine with `|`. Two `static_assert`s enforce **at least one context flag** and **exactly one filter flag**. | Group | Values | |---|---| | Context (≥1 required) | `EditorContext`, `ClientContext`, `ServerContext`, `CommandletContext`, `ProgramContext` | | Filter (exactly 1) | `SmokeFilter`, `EngineFilter`, `ProductFilter`, `PerfFilter`, `StressFilter`, `NegativeFilter` | | Priority | `CriticalPriority`, `HighPriority`, `MediumPriority`, `LowPriority` | | Other | `NonNullRHI`, `RequiresUser`, `Disabled`, `SupportsAutoRTFM` | `EAutomationTestFlags_ApplicationContextMask` covers every context flag in one token. ### Assertions inside a test ```cpp TestEqual(TEXT("Label"), Actual, Expected); // int32/int64/SIZE_T/FName/FText/FString/FStringView… TestEqual(TEXT("Approx"), ActualF, ExpectedF, 0.001f); // float/double/FVector/FRotator/FTransform take a tolerance TestTrue(TEXT("Label"), bCondition); // also TestFalse, TestNotEqual TestNotNull(TEXT("Label"), Ptr); // also TestNull, TestSamePtr, TestNotSamePtr TestValid(TEXT("Label"), SharedOrWeakPtr); // also TestInvalid AddError(FString::Printf(TEXT("Unexpected damage %d"), Damage)); // also AddWarning, AddInfo AddErrorIfFalse(bOk, TEXT("Setup failed")); UE_RETURN_ON_ERROR(Ptr != nullptr, TEXT("Ptr must not be null")); // adds error, returns false // Expect (and swallow) a log line the code under test is supposed to emit. AddExpectedMessage(TEXT("Inventory is full"), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains); ``` The pattern is a regex unless you use the `…Plain` variants (`AddExpectedMessagePlain`, `AddExpectedErrorPlain`). `AddExpectedError(Pattern, MatchType, Occurrences, bIsRegex)` is the shorthand that matches warnings and errors (it forwards `ELogVerbosity::Warning`, which is inclusive); the `AddExpectedMessage` overload without a verbosity matches every severity. ### Latent commands `RunTest` returns before latent commands run. Enqueue them, then let `Update()` return `true` when the step is finished. Enqueuing after `return true;` is unreachable code. ```cpp DEFINE_LATENT_AUTOMATION_COMMAND_ONE_PARAMETER(FMyWaitSecondsCommand, float, Duration); bool FMyWaitSecondsCommand::Update() { return GetCurrentRunTime() >= Duration; } ADD_LATENT_AUTOMATION_COMMAND(FMyWaitSecondsCommand(2.0f)); // inside RunTest, before `return true;` ``` `DEFINE_LATENT_AUTOMATION_COMMAND` through `..._FIVE_PARAMETER` exist. To assert *after* the wait, derive from `IAutomationLatentCommand` and capture the `FAutomationTestBase*`. ### Specs `BEGIN_DEFINE_SPEC(TClass, PrettyName, TFlags)` / member declarations / `END_DEFINE_SPEC(TClass)` declares a BDD-style test whose body is `void TClass::Define()`. Inside `Define()` use `Describe`, `It`, `LatentIt`, `BeforeEach`, `LatentBeforeEach`, `AfterEach` (and `xIt`/`xDescribe` to disable a block). `DEFINE_SPEC(TClass, PrettyName, TFlags)` replaces the pair when the spec needs no members. Worked example in [references/automation-test-patterns.md](references/automation-test-patterns.md). ## CQTest `CQTest` (module `CQTest`) is the fixture-based front end over the same framework. `Assert` is a `FNoDiscardAsserter`, so every assertion goes through `ASSERT_THAT`, which returns from the method on failure. ```cpp #include "CQTest.h" TEST_CLASS(FMyInventoryTests, "MyGame.Inventory") { UMyInventoryComponent* Inventory = nullptr; BEFORE_EACH() { Inventory = NewObject(GetTransientPackage()); } TEST_METHOD(AddItem_WhenEmpty_StoresOne) { Inventory->AddItem(FName("Sword"), 1); ASSERT_THAT(AreEqual(1, Inventory->GetItemCount(FName("Sword")))); } TEST_METHOD_WITH_TAGS(AddItem_WhenFull_Rejects, "[Slow][Inventory]") { Inventory->SetCapacity(0); ASSERT_THAT(IsFalse(Inventory->AddItem(FName("Sword"), 1))); } }; ``` `TEST_CLASS(ClassName, TestDir)` uses `FNoDiscardAsserter` and `DefaultFlags` (`EAutomationTestFlags_ApplicationContextMask | EAutomationTestFlags::ProductFilter`). Variants: `TEST_CLASS_WITH_FLAGS(ClassName, TestDir, Flags)`, `TEST_CLASS_WITH_TAGS`, `TEST_CLASS_WITH_BASE`, `TEST_CLASS_WITH_ASSERTS`. Lifecycle macros are `BEFORE_ALL()`/`AFTER_ALL()` (static, once per fixture) and `BEFORE_EACH()`/`AFTER_EACH()` (override `Setup()`/`TearDown()`). Asserter helpers: `IsTrue`, `IsFalse`, `IsNull`, `IsNotNull`, `AreEqual`, `AreNotEqual`, `AreEqualIgnoreCase`, `AreNotEqualIgnoreCase`, `IsNear(Expected, Actual, Epsilon)` — each also takes a trailing failure message. World, actor and async helpers (`TestCommandBuilder`, `FSpawnHelper`, `FActorTestSpawner`, `FMapTestSpawner`, `TObjectBuilder`, `UTestGameInstance`) are covered in [references/automation-test-patterns.md](references/automation-test-patterns.md). ## Functional Tests `AFunctionalTest` (module `FunctionalTesting`) is an actor placed in a test map. `PrepareTest`, `StartTest`, `IsReady_Implementation` and `OnTimeout` are **protected** virtuals; `FinishTest` and the `Assert*` helpers are public. ```cpp // MyFunctionalTest.h #pragma once #include "CoreMinimal.h" #include "FunctionalTest.h" #include "MyFunctionalTest.generated.h" UCLASS() class MYGAME_API AMyFunctionalTest : public AFunctionalTest { GENERATED_BODY() protected: // PrepareTest() sets TimeLimit / PreparationTimeLimit; IsReady_Implementation() // gates StartTest(); StartTest() asserts and calls FinishTest(). virtual void PrepareTest() override; virtual bool IsReady_Implementation() override; virtual void StartTest() override; private: UPROPERTY(EditInstanceOnly, Category = "MyGame") TObjectPtr TargetActor; }; ``` Each override calls `Super::` first. `FinishTest(EFunctionalTestResult TestResult, const FString& Message)` ends the run; `EFunctionalTestResult` is `Default`, `Invalid`, `Error`, `Running`, `Failed`, `Succeeded`, and `Default` lets the recorded assertions decide. Assertions are `AssertTrue`/`AssertFalse` (`bool Condition, const FString& Message, const UObject* ContextObject = nullptr`), `AssertIsValid(UObject*, const FString&, const UObject* = nullptr)`, `AssertEqual_Int/Float/Bool/Name/Object/Vector/Rotator/Transform/Quat` and `AssertValue_Int/Float/Double/DateTime` (with an `EComparisonMethod`). Also `TimeLimit`, `PreparationTimeLimit`, `SetTimeLimit(float, EFunctionalTestResult)`, `LogMessage(const FString&)` and `StartStep`/`FinishStep`. Full `.cpp` in [references/automation-test-patterns.md](references/automation-test-patterns.md). Run every functional test in the current world with the Blueprint node `UFunctionalTestingManager::RunAllFunctionalTests(WorldContextObject, bNewLog, bRunLooped, FailedTestsReproString)` (the class is `MinimalAPI` and the function is not `FUNCTIONALTESTING_API`, so calling it from another module's C++ does not link), or from the command line: ``` UnrealEditor-Cmd.exe MyGame.uproject -ExecCmds="Automation RunTests MyGame.Functional" -unattended -nopause -testexit="Automation Test Queue Empty" -log ``` `Automation` also accepts `List`, `RunTest`, `RunFilter `, `RunAll`, `SetFilter`, `SetPriority`, `SetMinimumPriority`, `Quit` and `SoftQuit`; chain them with `;`. ## Profiling Full command tables, the CSV and LLM workflows and the bottleneck-triage procedure are in [references/profiling-commands.md](references/profiling-commands.md). ### Unreal Insights ``` # Capture at launch UnrealEditor.exe MyGame.uproject -trace=cpu,frame,bookmark,counters,gpu,loadtime,log -tracehost=127.0.0.1 UnrealEditor.exe MyGame.uproject -trace=cpu,frame -tracefile=MyCapture.utrace # Or from the console, mid-session Trace.File D:/Captures/Run1.utrace cpu,frame,bookmark # or Trace.Send 127.0.0.1 cpu,frame Trace.Enable counters Trace.Bookmark WaveStarted Trace.Stop # also Trace.Pause, Trace.Resume, Trace.Status ``` Channel names drop the `Channel` suffix and lowercase: `cpu`, `gpu`, `frame`, `bookmark`, `counters`, `log`, `loadtime`, `task`, `memalloc`, `callstack`, `module`, `metadata`, `net`, `rhicommands`, `rendercommands`, `slate`, `assetmetadata`, `iostore`, `animation`, `region`, `screenshot`. ### Instrumenting code ```cpp #include "Stats/Stats.h" // DECLARE_*_STAT, SCOPE_CYCLE_COUNTER #include "HAL/PlatformMisc.h" // SCOPED_NAMED_EVENT #include "ProfilingDebugging/CpuProfilerTrace.h" // TRACE_CPUPROFILER_EVENT_SCOPE #include "ProfilingDebugging/MiscTrace.h" // TRACE_BOOKMARK #include "ProfilingDebugging/CountersTrace.h" // TRACE_*_COUNTER, TRACE_INT_VALUE #include "ProfilingDebugging/CsvProfiler.h" // CSV_* #include "ProfilingDebugging/ScopedTimers.h" // FScopedDurationTimer DECLARE_STATS_GROUP(TEXT("MyGame"), STATGROUP_MyGame, STATCAT_Advanced); DECLARE_CYCLE_STAT(TEXT("MyGame Tick"), STAT_MyGameTick, STATGROUP_MyGame); CSV_DEFINE_CATEGORY(MyGame, true); TRACE_DECLARE_INT_COUNTER(MyGameActiveEnemies, TEXT("MyGame/ActiveEnemies")); void UMySubsystem::Tick(float DeltaTime) { SCOPE_CYCLE_COUNTER(STAT_MyGameTick); // stat system + Insights TRACE_CPUPROFILER_EVENT_SCOPE(UMySubsystem::Tick); // Insights only, very cheap CSV_SCOPED_TIMING_STAT(MyGame, SubsystemTick); // one CSV column SCOPED_NAMED_EVENT(MyGame_Tick, FColor::Orange); // also visible to PIX/Razor TRACE_COUNTER_SET(MyGameActiveEnemies, ActiveEnemyCount); CSV_CUSTOM_STAT(MyGame, Enemies, ActiveEnemyCount, ECsvCustomStatOp::Set); } void UMySubsystem::BeginWave(int32 WaveNumber) { TRACE_BOOKMARK(TEXT("WaveStarted_%d"), WaveNumber); // vertical marker in Insights CSV_EVENT(MyGame, TEXT("WaveStarted")); double LoadSeconds = 0.0; { FScopedDurationTimer Timer(LoadSeconds); // accumulates into LoadSeconds LoadWaveAssets(WaveNumber); } UE_LOGFMT(LogMyGame, Display, "Wave {Wave} loaded in {Seconds}s", ("Wave", WaveNumber), ("Seconds", LoadSeconds)); } ``` `QUICK_SCOPE_CYCLE_COUNTER(STAT_MyAdHocScope)` declares a stat inline for a one-off measurement; `TRACE_INT_VALUE(TEXT("MyGame/Spawned"), Count)` and `TRACE_FLOAT_VALUE` emit a counter with no declaration; `SCOPED_NAMED_EVENT_TEXT("Name", FColor::Yellow)` takes a literal and `SCOPED_NAMED_EVENT_F` formats one. ### stat commands `stat unit` first: it splits frame time into Game, Draw, RHIT and GPU. Then drill into the dominant thread with `stat game`, `stat scenerendering`, `stat initviews`, `stat slate`, `stat threading`, `stat streaming`, `stat net`, or `stat MyGame` for your own group. `stat unitgraph` plots the same numbers over time, `stat fps` shows frame rate, `stat hitches` flags spikes, `stat namedevents` emits stat names as profiler events for external tools. ### Memory ``` stat memory / stat memoryplatform # engine categories / OS physical and virtual memreport -full # dump to Saved/Profiling/MemReports obj list class=Texture2D # loaded objects of a class, with sizes ``` Launch with `-LLM` and `-trace=memalloc,callstack,module` for Memory Insights; scope allocations with `LLM_SCOPE(ELLMTag::EngineMisc)` or a custom tag declared via `LLM_DEFINE_TAG`/`LLM_DECLARE_TAG` and entered with `LLM_SCOPE_BYTAG`. On Windows, non-editor builds default to `MallocBinned3`; override with `-binnedmalloc2`, `-binnedmalloc`, `-ansimalloc` or `-stompmalloc`. ### Hitch capture and UObject count `snapshothitches -start` arms an automatic trace snapshot whenever the stats system detects a hitch; `snapshothitches -stop` disarms it. It needs `STATS` and a non-Shipping build, and disables the `Screenshot` channel while armed so a stale screenshot cannot land in the snapshot tail. Compiling with `CSV_TRACK_UOBJECT_COUNT=1` (with CSV profiling enabled) records the live `UObject` count every frame as CSV stat `Total` in category `ObjectCount`, read from `UObjectStats::GetUObjectCount()`. Use it to catch object leaks over a long session without a full memory capture. ## Debug Visualization ### Debug drawing Everything in `DrawDebugHelpers.h` sits behind `ENABLE_DRAW_DEBUG`, which is `UE_ENABLE_DEBUG_DRAWING` = `(!(UE_BUILD_SHIPPING || UE_BUILD_TEST) || WITH_EDITOR)`. In Test or Shipping game builds they become empty inline stubs (`DrawDebugHelpers.h:180-185`; defining `SHIPPING_DRAW_DEBUG_ERROR=1` removes them so stray calls fail to compile), but arguments are still evaluated — wrap expensive call sites in `#if ENABLE_DRAW_DEBUG`. ```cpp #include "DrawDebugHelpers.h" // LifeTime: -1 (default) or 0 draws one frame; > 0 is seconds. // bPersistentLines = true keeps the shape until FlushPersistentDebugLines. UWorld* World = GetWorld(); DrawDebugLine(World, Start, End, FColor::Red, false, 2.0f, 0, 2.0f); DrawDebugPoint(World, Location, 8.0f, FColor::White, false, 2.0f, 0); DrawDebugDirectionalArrow(World, Start, End, 40.0f, FColor::Cyan, false, 2.0f, 0, 2.0f); DrawDebugBox(World, Center, Extent, FQuat::Identity, FColor::Blue, false, 2.0f, 0, 1.0f); DrawDebugSphere(World, Center, 100.0f, 12, FColor::Green, false, 2.0f, 0, 1.0f); DrawDebugCapsule(World, Center, 88.0f, 34.0f, FQuat::Identity, FColor::Yellow, false, 2.0f, 0, 1.0f); DrawDebugString(World, Location, TEXT("Label"), nullptr, FColor::White, 2.0f, true, 1.0f); FlushPersistentDebugLines(World); // and FlushDebugStrings(World) ``` The Blueprint-facing `UKismetSystemLibrary::DrawDebugLine/Arrow/Sphere/Capsule/String` gate themselves, take `FLinearColor` and an `EDrawDebugSceneDepthPriorityGroup`, and are joined by `PrintString(WorldContextObject, InString, bPrintToScreen, bPrintToLog, TextColor, Duration, Key)`. ### Visual Logger `ENABLE_VISUAL_LOG` is `PLATFORM_DESKTOP && !NO_LOGGING && UE_ENABLE_DEBUG_DRAWING`. Open the timeline with **Window > Visual Logger**. ```cpp #include "VisualLogger/VisualLogger.h" UE_VLOG(this, LogMyGame, Log, TEXT("State: %s"), *StateName); UE_VLOG_UELOG(this, LogMyGame, Warning, TEXT("Lost target %s"), *TargetName); // also hits UE_LOG UE_VLOG_LOCATION(this, LogMyGame, Verbose, GetActorLocation(), 20.0f, FColor::Green, TEXT("Here")); UE_VLOG_SEGMENT(this, LogMyGame, Log, PathStart, PathEnd, FColor::Red, TEXT("Path")); UE_VLOG_SPHERE(this, LogMyGame, Verbose, PatrolPoint, 50.0f, FColor::Blue, TEXT("Patrol")); UE_VLOG_ARROW(this, LogMyGame, Log, GetActorLocation(), AimPoint, FColor::Yellow, TEXT("Aim")); // also _BOX, _CAPSULE UE_CVLOG(bIsAlerted, this, LogMyGame, Log, TEXT("Alerted")); // also UE_CVLOG_SEGMENT, UE_CVLOG_LOCATION FVisualLogger::Get().SetIsRecording(true); // and SetIsRecordingToFile / SetIsRecordingToTrace ``` Every macro early-outs on `FVisualLogger::IsRecording()`, so the arguments cost nothing when recording is off. ### Gameplay Debugger and ShowDebug Press `'` in PIE. Register a category from your module's `StartupModule` (add `"GameplayDebugger"` to `PrivateDependencyModuleNames`; wrap the registration and the category class in `#if WITH_GAMEPLAY_DEBUGGER`, which is 0 in Test/Shipping game builds by default — `GameplayDebuggerCategory.h:10-11`, `TargetRules.cs:1125`): ```cpp #include "GameplayDebugger.h" // in FMyGameModule::StartupModule() if (IGameplayDebugger::IsAvailable()) { IGameplayDebugger& Debugger = IGameplayDebugger::Get(); Debugger.RegisterCategory(TEXT("MyGame"), IGameplayDebugger::FOnGetCategory::CreateStatic(&FMyDebuggerCategory::MakeInstance), EGameplayDebuggerCategoryState::EnabledInGame); Debugger.NotifyCategoriesChanged(); } ``` `FMyDebuggerCategory` derives from `FGameplayDebuggerCategory` and overrides `virtual void CollectData(APlayerController* OwnerPC, AActor* DebugActor)` and `virtual void DrawData(APlayerController* OwnerPC, FGameplayDebuggerCanvasContext& CanvasContext)`, feeding them with `AddTextLine` and `AddShape`. Call `UnregisterCategory(TEXT("MyGame"))` in `ShutdownModule`. `AHUD::ShowDebug(FName DebugType)` toggles a named debug page (`ShowDebug AI`, `ShowDebug Physics`); `ShowDebugToggleSubCategory(FName)` toggles a sub-page and `ShowDebugForReticleTargetToggle(TSubclassOf)` retargets it at whatever the reticle is over. Override `virtual void ShowDebugInfo(float& YL, float& YPos)` or bind `AHUD::OnShowDebugInfo` for your own page. The separate `display`, `displayall` and `displayclear` console commands print a named property of an object or class onto the HUD. ## Console Commands and CVars ```cpp // In the AMyPlayerController class body. Exec works on PlayerController, Pawn, HUD, // GameMode, GameState, CheatManager (or a UCheatManagerExtension) and GameInstance. UFUNCTION(Exec) void ToggleGodMode(); ``` ```cpp // MyGameDebug.cpp — console objects register at static init #include "HAL/IConsoleManager.h" static FAutoConsoleCommand GMyDumpStatsCmd( TEXT("MyGame.DumpStats"), TEXT("Dump gameplay stats"), FConsoleCommandDelegate::CreateStatic(&FMyStatsDumper::Dump)); // World + args; prefer this for anything that touches gameplay in PIE. static FAutoConsoleCommandWithWorldAndArgs GMySpawnItemCmd( TEXT("MyGame.SpawnItem"), TEXT("Usage: MyGame.SpawnItem "), FConsoleCommandWithWorldAndArgsDelegate::CreateStatic(&FMySpawner::SpawnFromConsole)); static TAutoConsoleVariable CVarMyDamageScale( TEXT("MyGame.DamageScale"), 1.0f, TEXT("Scales all outgoing damage."), ECVF_Cheat); // FAutoConsoleVariableRef binds a console name to an existing int32/float/bool/FString/FName. static float GMyDrawDistance = 5000.0f; static FAutoConsoleVariableRef CVarMyDrawDistance( TEXT("MyGame.DrawDistance"), GMyDrawDistance, TEXT("Gameplay draw distance."), ECVF_Default); ``` Read a CVar with `GetValueOnGameThread()`, `GetValueOnRenderThread()` or `GetValueOnAnyThread()`. Common flags: `ECVF_Default`, `ECVF_Cheat` (disabled on shipping targets), `ECVF_ReadOnly`, `ECVF_Scalability`, `ECVF_RenderThreadSafe`. When a command's lifetime is tied to an object, register at runtime with `IConsoleManager::Get().RegisterConsoleCommand(Name, Help, Delegate)` and release the returned `IConsoleCommand*` with `IConsoleManager::Get().UnregisterConsoleObject(Cmd)`. ## Deprecated — do not use | Do not emit | Use in 5.8 | Source | |---|---|---| | `UE_TRACE_BOOKMARK(...)` | `TRACE_BOOKMARK(Format, ...)` | `ProfilingDebugging/MiscTrace.h` — no `UE_TRACE_BOOKMARK` exists | | `Trace.Start [Channels]` | `Trace.File [Path] [Channels]` | "'Trace.Start' is being deprecated in favor of 'Trace.File'" in `TraceAuxiliary.cpp` | | `stat startfile` / `stat stopfile` / `stat startfileraw` (`.uestats`) | `Trace.File` + `UnrealInsights.exe` | gated behind `UE_ENABLE_STATS_FILE_DEPRECATED_IN_5_8` (default `0`) in `Stats/StatsFile.h` | | `stat gpu` | `ProfileGPU`, `DumpGPU`, or the per-queue group `stat GPU0_Graphics0` | GPU groups are built as `STATGROUP_%s` from `GPU%d_%s%d` in `RHI/Private/GPUProfiler.cpp` | | Unscoped flag names such as `EditorContext` | scope them: `EAutomationTestFlags::EditorContext` | `EAutomationTestFlags` is an `enum class` in `Misc/AutomationTest.h` | | `EAutomationExpectedErrorFlags::…` as a distinct type | `EAutomationExpectedMessageFlags::…` | the former is now a namespace alias of the latter | | `UE_LOG_REF`, `UE_LOG_CLINKAGE` | `UE_LOG` | marked "DO NOT USE … will be deprecated" in `Logging/LogMacros.h` | | `-csvstatfile=Out.csv` | `-csvCaptureFrames=N` plus `CsvProfile StartFile=` | `CsvProfiler.cpp` parses `csvCaptureFrames=`, `csvCategories=`, `csvMetadata=`, `csvGpuStats`, `csvExecCmds=` and others, but no `csvstatfile` | ## Common Mistakes **Assuming Test builds keep `check`/`ensure`:** `DO_CHECK` and `DO_ENSURE` are `USE_CHECKS_IN_SHIPPING` / `USE_ENSURES_IN_SHIPPING` in **both** Test and Shipping, and both default to `0`. Use `verify()` when the expression must always run, or set `bUseChecksInShipping`. **Logic inside a log or assert:** `UE_LOG(LogMyGame, Log, TEXT("%d"), AdvanceCounter());` stops advancing once `NO_LOGGING` is on, and `check(Init())` stops initializing once `DO_CHECK` is 0. Compute into a local first, then log or assert on it. **Using `Log` and expecting console output:** `Display` prints to the console; `Log` only reaches the log file. "My log line never appears" is almost always a `Log`-verbosity message. **Missing filter flag:** every `IMPLEMENT_*_AUTOMATION_TEST`, `DEFINE_SPEC` and `BEGIN_DEFINE_SPEC` needs exactly one of `SmokeFilter`/`EngineFilter`/`ProductFilter`/`PerfFilter`/`StressFilter`/`NegativeFilter` plus at least one context flag, or the `static_assert` fails to compile. **Dropping a CQTest assertion result:** the asserter methods are `[[nodiscard]]`, so `Assert.IsTrue(x);` warns and keeps running after a failure. Always write `ASSERT_THAT(IsTrue(x));`. **`DrawDebug*` in a Shipping or Test game build:** the calls become no-op stubs but their arguments (traces, string formatting) still run. Wrap the whole block in `#if ENABLE_DRAW_DEBUG`. **Skipping `Super::StartTest()`:** `AFunctionalTest::StartTest` fires the Blueprint `ReceiveStartTest` event, so omitting `Super::` silently disables every Blueprint-authored step in the same test actor. ## Related Skills - `ue-cpp-foundations` — UObject basics, delegates, containers, the UFUNCTION/UPROPERTY specifier tables - `ue-module-build-system` — Build.cs and Target.cs wiring, `bBuildDeveloperTools`, test module setup, plugin layout - `ue-editor-tools` — editor utilities, commandlets and Slate tooling that host test and debug UI - `ue-physics-collision` — trace and sweep queries whose results you visualize with `DrawDebug*` - `ue-ai-navigation` — behaviour trees, EQS and the AI-side Gameplay Debugger categories - `ue-async-threading` — task graph and async work that latent commands and the `task` trace channel observe - `ue-gameplay-framework` — GameMode, PlayerController, HUD and CheatManager, the hosts for `Exec` functions - `ue-materials-rendering` — material instances, parameter collections, render targets and post process