diff --git a/AGRARIAN_DEVELOPMENT_ROADMAP.md b/AGRARIAN_DEVELOPMENT_ROADMAP.md index 862a6b5..8f28382 100644 --- a/AGRARIAN_DEVELOPMENT_ROADMAP.md +++ b/AGRARIAN_DEVELOPMENT_ROADMAP.md @@ -462,7 +462,10 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe - [x] Add weather exposure zones if needed. Added native `AAgrarianWeatherExposureZone` volumes with exposure multipliers and temperature offsets, wired survival to apply the strongest overlapping zone after shelter protection, exposed current zone effects on the dev HUD, placed three Ground Zero zones for ridge, coastal-wind, and drainage cooling cases, and added verification for zone placement plus docs. - [x] Add landmark or ruin placeholder. Added a repeatable five-piece Ground Zero ruin cluster using Agrarian-native placeholder stone meshes for a visible MVP point of interest, regenerated the map, documented the pass, and added verification for actor count, native mesh paths, stone material assignment, distinct placement, and roadmap/doc coverage. - [x] Add spawn area with validation that the player spawns above sea level, above terrain by a safe offset, away from water, away from steep slopes, away from dense resource clusters, and with a known safe fallback coordinate. Added safe spawn candidate/fallback selection to the Ground Zero setup script, validates elevation, slope, above-terrain Z offset, freshwater spacing, and resource-cluster spacing before map save, places `AGR_DemoPlayerStart` at the known safe fallback coordinate, and added verification so future terrain/resource/water changes cannot silently invalidate the spawn area. -- [ ] Add performance profiling markers. +- [x] Add performance profiling markers. Added `STATGROUP_Agrarian` cycle stats + and Unreal Insights CPU scopes for authoritative game-state time/weather + ticking, survival ticking, sky-light refresh, weather-audio refresh, foliage + instance mutation, and weather-provider request/parse/fallback work. - [ ] Add navigation support for wildlife. - [ ] Add map boundaries or soft limits. - [ ] Add developer travel command. diff --git a/Docs/TechnicalDesignDocument.md b/Docs/TechnicalDesignDocument.md index c678255..7cfcd08 100644 --- a/Docs/TechnicalDesignDocument.md +++ b/Docs/TechnicalDesignDocument.md @@ -519,6 +519,15 @@ Minimum gate for milestone/demo builds: - current roadmap milestone is marked complete; - known blockers are documented. +## Performance Profiling + +Agrarian gameplay code exposes a dedicated Unreal stat group named +`STATGROUP_Agrarian`. Use `stat Agrarian` during editor or packaged sessions for +game-specific cycle counters, and capture Unreal Insights traces for matching +CPU scopes. The current markers cover authoritative game-state time/weather +ticking, survival ticking, sky-light refresh, weather-audio refresh, foliage +instance mutation, and weather-provider request/parse/fallback work. + ## Near-Term Technical Priorities Next technical foundation work should focus on: diff --git a/Scripts/verify_performance_profiling_markers.py b/Scripts/verify_performance_profiling_markers.py new file mode 100644 index 0000000..d92ba72 --- /dev/null +++ b/Scripts/verify_performance_profiling_markers.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +"""Validate Agrarian Unreal stat and Insights profiling markers.""" + +from pathlib import Path +import sys + +REPO_ROOT = Path(__file__).resolve().parents[1] + +STATS = [ + "STAT_AgrarianGameStateTick", + "STAT_AgrarianSurvivalTick", + "STAT_AgrarianSkyLightingRefresh", + "STAT_AgrarianWeatherAudioRefresh", + "STAT_AgrarianFoliageInstanceMutation", + "STAT_AgrarianWeatherProviderRequest", + "STAT_AgrarianWeatherProviderParse", +] + +SOURCE_MARKERS = { + "Source/AgrarianGame/AgrarianGameState.cpp": [ + "TRACE_CPUPROFILER_EVENT_SCOPE(AgrarianGameStateTick)", + "SCOPE_CYCLE_COUNTER(STAT_AgrarianGameStateTick)", + ], + "Source/AgrarianGame/AgrarianSurvivalComponent.cpp": [ + "TRACE_CPUPROFILER_EVENT_SCOPE(AgrarianSurvivalTick)", + "SCOPE_CYCLE_COUNTER(STAT_AgrarianSurvivalTick)", + ], + "Source/AgrarianGame/AgrarianSkyLightingController.cpp": [ + "TRACE_CPUPROFILER_EVENT_SCOPE(AgrarianSkyLightingRefresh)", + "SCOPE_CYCLE_COUNTER(STAT_AgrarianSkyLightingRefresh)", + ], + "Source/AgrarianGame/AgrarianWeatherAudioController.cpp": [ + "TRACE_CPUPROFILER_EVENT_SCOPE(AgrarianWeatherAudioRefresh)", + "SCOPE_CYCLE_COUNTER(STAT_AgrarianWeatherAudioRefresh)", + ], + "Source/AgrarianGame/AgrarianFoliagePatch.cpp": [ + "TRACE_CPUPROFILER_EVENT_SCOPE(AgrarianFoliageClear)", + "TRACE_CPUPROFILER_EVENT_SCOPE(AgrarianFoliageAddTreeInstance)", + "TRACE_CPUPROFILER_EVENT_SCOPE(AgrarianFoliageAddShrubInstance)", + "TRACE_CPUPROFILER_EVENT_SCOPE(AgrarianFoliageAddGrassInstance)", + "SCOPE_CYCLE_COUNTER(STAT_AgrarianFoliageInstanceMutation)", + ], + "Source/AgrarianGame/AgrarianWeatherProviderSubsystem.cpp": [ + "TRACE_CPUPROFILER_EVENT_SCOPE(AgrarianWeatherProviderRequest)", + "TRACE_CPUPROFILER_EVENT_SCOPE(AgrarianWeatherProviderFallback)", + "TRACE_CPUPROFILER_EVENT_SCOPE(AgrarianWeatherProviderParseOpenMeteo)", + "TRACE_CPUPROFILER_EVENT_SCOPE(AgrarianWeatherProviderParseNoaaNws)", + "SCOPE_CYCLE_COUNTER(STAT_AgrarianWeatherProviderRequest)", + "SCOPE_CYCLE_COUNTER(STAT_AgrarianWeatherProviderParse)", + ], +} + + +def read_text(relative_path: str) -> str: + path = REPO_ROOT / relative_path + if not path.exists(): + raise AssertionError(f"Missing required file: {relative_path}") + return path.read_text(encoding="utf-8") + + +def require(needle: str, haystack: str, context: str) -> None: + if needle not in haystack: + raise AssertionError(f"Missing {needle!r} in {context}") + + +def main() -> int: + errors: list[str] = [] + + try: + header = read_text("Source/AgrarianGame/AgrarianPerformanceStats.h") + source = read_text("Source/AgrarianGame/AgrarianPerformanceStats.cpp") + require("DECLARE_STATS_GROUP(TEXT(\"Agrarian\"), STATGROUP_Agrarian", header, "AgrarianPerformanceStats.h") + for stat in STATS: + require(f"DECLARE_CYCLE_STAT_EXTERN(", header, "AgrarianPerformanceStats.h") + require(stat, header, "AgrarianPerformanceStats.h") + require(f"DEFINE_STAT({stat});", source, "AgrarianPerformanceStats.cpp") + except AssertionError as exc: + errors.append(str(exc)) + + for relative_path, markers in SOURCE_MARKERS.items(): + try: + contents = read_text(relative_path) + require("#include \"AgrarianPerformanceStats.h\"", contents, relative_path) + require("#include \"ProfilingDebugging/CpuProfilerTrace.h\"", contents, relative_path) + for marker in markers: + require(marker, contents, relative_path) + except AssertionError as exc: + errors.append(str(exc)) + + try: + roadmap = read_text("AGRARIAN_DEVELOPMENT_ROADMAP.md") + require("[x] Add performance profiling markers.", roadmap, "AGRARIAN_DEVELOPMENT_ROADMAP.md") + docs = read_text("Docs/TechnicalDesignDocument.md") + require("stat Agrarian", docs, "Docs/TechnicalDesignDocument.md") + require("Unreal Insights", docs, "Docs/TechnicalDesignDocument.md") + except AssertionError as exc: + errors.append(str(exc)) + + if errors: + for error in errors: + print(f"ERROR: {error}", file=sys.stderr) + return 1 + + print("Performance profiling markers are wired.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Source/AgrarianGame/AgrarianFoliagePatch.cpp b/Source/AgrarianGame/AgrarianFoliagePatch.cpp index 93af678..df16eea 100644 --- a/Source/AgrarianGame/AgrarianFoliagePatch.cpp +++ b/Source/AgrarianGame/AgrarianFoliagePatch.cpp @@ -2,8 +2,10 @@ #include "AgrarianFoliagePatch.h" +#include "AgrarianPerformanceStats.h" #include "Components/HierarchicalInstancedStaticMeshComponent.h" #include "Components/SceneComponent.h" +#include "ProfilingDebugging/CpuProfilerTrace.h" namespace { @@ -47,6 +49,9 @@ AAgrarianFoliagePatch::AAgrarianFoliagePatch() void AAgrarianFoliagePatch::ClearFoliage() { + SCOPE_CYCLE_COUNTER(STAT_AgrarianFoliageInstanceMutation); + TRACE_CPUPROFILER_EVENT_SCOPE(AgrarianFoliageClear); + if (TreeInstances) { TreeInstances->ClearInstances(); @@ -65,16 +70,25 @@ void AAgrarianFoliagePatch::ClearFoliage() int32 AAgrarianFoliagePatch::AddTreeInstance(const FTransform& InstanceTransform) { + SCOPE_CYCLE_COUNTER(STAT_AgrarianFoliageInstanceMutation); + TRACE_CPUPROFILER_EVENT_SCOPE(AgrarianFoliageAddTreeInstance); + return TreeInstances ? TreeInstances->AddInstance(InstanceTransform, true) : INDEX_NONE; } int32 AAgrarianFoliagePatch::AddShrubInstance(const FTransform& InstanceTransform) { + SCOPE_CYCLE_COUNTER(STAT_AgrarianFoliageInstanceMutation); + TRACE_CPUPROFILER_EVENT_SCOPE(AgrarianFoliageAddShrubInstance); + return ShrubInstances ? ShrubInstances->AddInstance(InstanceTransform, true) : INDEX_NONE; } int32 AAgrarianFoliagePatch::AddGrassInstance(const FTransform& InstanceTransform) { + SCOPE_CYCLE_COUNTER(STAT_AgrarianFoliageInstanceMutation); + TRACE_CPUPROFILER_EVENT_SCOPE(AgrarianFoliageAddGrassInstance); + return GrassInstances ? GrassInstances->AddInstance(InstanceTransform, true) : INDEX_NONE; } diff --git a/Source/AgrarianGame/AgrarianGameState.cpp b/Source/AgrarianGame/AgrarianGameState.cpp index 49506c0..7e255dd 100644 --- a/Source/AgrarianGame/AgrarianGameState.cpp +++ b/Source/AgrarianGame/AgrarianGameState.cpp @@ -1,7 +1,9 @@ // Copyright Pacificao. All Rights Reserved. #include "AgrarianGameState.h" +#include "AgrarianPerformanceStats.h" #include "Net/UnrealNetwork.h" +#include "ProfilingDebugging/CpuProfilerTrace.h" namespace { @@ -70,6 +72,9 @@ void AAgrarianGameState::BeginPlay() void AAgrarianGameState::Tick(float DeltaSeconds) { + SCOPE_CYCLE_COUNTER(STAT_AgrarianGameStateTick); + TRACE_CPUPROFILER_EVENT_SCOPE(AgrarianGameStateTick); + Super::Tick(DeltaSeconds); if (!HasAuthority()) diff --git a/Source/AgrarianGame/AgrarianPerformanceStats.cpp b/Source/AgrarianGame/AgrarianPerformanceStats.cpp new file mode 100644 index 0000000..3d555d3 --- /dev/null +++ b/Source/AgrarianGame/AgrarianPerformanceStats.cpp @@ -0,0 +1,11 @@ +// Copyright Pacificao. All Rights Reserved. + +#include "AgrarianPerformanceStats.h" + +DEFINE_STAT(STAT_AgrarianGameStateTick); +DEFINE_STAT(STAT_AgrarianSurvivalTick); +DEFINE_STAT(STAT_AgrarianSkyLightingRefresh); +DEFINE_STAT(STAT_AgrarianWeatherAudioRefresh); +DEFINE_STAT(STAT_AgrarianFoliageInstanceMutation); +DEFINE_STAT(STAT_AgrarianWeatherProviderRequest); +DEFINE_STAT(STAT_AgrarianWeatherProviderParse); diff --git a/Source/AgrarianGame/AgrarianPerformanceStats.h b/Source/AgrarianGame/AgrarianPerformanceStats.h new file mode 100644 index 0000000..ff72705 --- /dev/null +++ b/Source/AgrarianGame/AgrarianPerformanceStats.h @@ -0,0 +1,15 @@ +// Copyright Pacificao. All Rights Reserved. + +#pragma once + +#include "Stats/Stats.h" + +DECLARE_STATS_GROUP(TEXT("Agrarian"), STATGROUP_Agrarian, STATCAT_Advanced); + +DECLARE_CYCLE_STAT_EXTERN(TEXT("Game State Tick"), STAT_AgrarianGameStateTick, STATGROUP_Agrarian, AGRARIANGAME_API); +DECLARE_CYCLE_STAT_EXTERN(TEXT("Survival Tick"), STAT_AgrarianSurvivalTick, STATGROUP_Agrarian, AGRARIANGAME_API); +DECLARE_CYCLE_STAT_EXTERN(TEXT("Sky Lighting Refresh"), STAT_AgrarianSkyLightingRefresh, STATGROUP_Agrarian, AGRARIANGAME_API); +DECLARE_CYCLE_STAT_EXTERN(TEXT("Weather Audio Refresh"), STAT_AgrarianWeatherAudioRefresh, STATGROUP_Agrarian, AGRARIANGAME_API); +DECLARE_CYCLE_STAT_EXTERN(TEXT("Foliage Instance Mutation"), STAT_AgrarianFoliageInstanceMutation, STATGROUP_Agrarian, AGRARIANGAME_API); +DECLARE_CYCLE_STAT_EXTERN(TEXT("Weather Provider Request"), STAT_AgrarianWeatherProviderRequest, STATGROUP_Agrarian, AGRARIANGAME_API); +DECLARE_CYCLE_STAT_EXTERN(TEXT("Weather Provider Parse"), STAT_AgrarianWeatherProviderParse, STATGROUP_Agrarian, AGRARIANGAME_API); diff --git a/Source/AgrarianGame/AgrarianSkyLightingController.cpp b/Source/AgrarianGame/AgrarianSkyLightingController.cpp index 33327d5..cf38b30 100644 --- a/Source/AgrarianGame/AgrarianSkyLightingController.cpp +++ b/Source/AgrarianGame/AgrarianSkyLightingController.cpp @@ -3,11 +3,13 @@ #include "AgrarianSkyLightingController.h" #include "AgrarianGameState.h" +#include "AgrarianPerformanceStats.h" #include "Components/DirectionalLightComponent.h" #include "Components/ExponentialHeightFogComponent.h" #include "Components/SceneComponent.h" #include "Components/SkyLightComponent.h" #include "Engine/World.h" +#include "ProfilingDebugging/CpuProfilerTrace.h" AAgrarianSkyLightingController::AAgrarianSkyLightingController() { @@ -48,6 +50,9 @@ void AAgrarianSkyLightingController::Tick(float DeltaSeconds) void AAgrarianSkyLightingController::RefreshSkyLighting() { + SCOPE_CYCLE_COUNTER(STAT_AgrarianSkyLightingRefresh); + TRACE_CPUPROFILER_EVENT_SCOPE(AgrarianSkyLightingRefresh); + const UWorld* World = GetWorld(); const AAgrarianGameState* GameState = World ? World->GetGameState() : nullptr; if (!GameState) diff --git a/Source/AgrarianGame/AgrarianSurvivalComponent.cpp b/Source/AgrarianGame/AgrarianSurvivalComponent.cpp index 96a82da..c9fcae1 100644 --- a/Source/AgrarianGame/AgrarianSurvivalComponent.cpp +++ b/Source/AgrarianGame/AgrarianSurvivalComponent.cpp @@ -2,11 +2,13 @@ #include "AgrarianSurvivalComponent.h" #include "AgrarianGameState.h" +#include "AgrarianPerformanceStats.h" #include "AgrarianShelterActor.h" #include "AgrarianWeatherExposureZone.h" #include "Components/BoxComponent.h" #include "Engine/World.h" #include "Net/UnrealNetwork.h" +#include "ProfilingDebugging/CpuProfilerTrace.h" UAgrarianSurvivalComponent::UAgrarianSurvivalComponent() { @@ -24,6 +26,9 @@ void UAgrarianSurvivalComponent::BeginPlay() void UAgrarianSurvivalComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) { + SCOPE_CYCLE_COUNTER(STAT_AgrarianSurvivalTick); + TRACE_CPUPROFILER_EVENT_SCOPE(AgrarianSurvivalTick); + Super::TickComponent(DeltaTime, TickType, ThisTickFunction); if (!GetOwner() || !GetOwner()->HasAuthority() || !IsAlive()) diff --git a/Source/AgrarianGame/AgrarianWeatherAudioController.cpp b/Source/AgrarianGame/AgrarianWeatherAudioController.cpp index 366f01d..ed0b494 100644 --- a/Source/AgrarianGame/AgrarianWeatherAudioController.cpp +++ b/Source/AgrarianGame/AgrarianWeatherAudioController.cpp @@ -3,9 +3,11 @@ #include "AgrarianWeatherAudioController.h" #include "AgrarianGameState.h" +#include "AgrarianPerformanceStats.h" #include "Components/AudioComponent.h" #include "Components/SceneComponent.h" #include "Engine/World.h" +#include "ProfilingDebugging/CpuProfilerTrace.h" AAgrarianWeatherAudioController::AAgrarianWeatherAudioController() { @@ -47,6 +49,9 @@ void AAgrarianWeatherAudioController::Tick(float DeltaSeconds) void AAgrarianWeatherAudioController::RefreshWeatherAudio(float DeltaSeconds) { + SCOPE_CYCLE_COUNTER(STAT_AgrarianWeatherAudioRefresh); + TRACE_CPUPROFILER_EVENT_SCOPE(AgrarianWeatherAudioRefresh); + const UWorld* World = GetWorld(); const AAgrarianGameState* GameState = World ? World->GetGameState() : nullptr; if (!GameState) diff --git a/Source/AgrarianGame/AgrarianWeatherProviderSubsystem.cpp b/Source/AgrarianGame/AgrarianWeatherProviderSubsystem.cpp index 4f18a98..9e0b730 100644 --- a/Source/AgrarianGame/AgrarianWeatherProviderSubsystem.cpp +++ b/Source/AgrarianGame/AgrarianWeatherProviderSubsystem.cpp @@ -2,11 +2,13 @@ #include "AgrarianWeatherProviderSubsystem.h" #include "AgrarianGameState.h" +#include "AgrarianPerformanceStats.h" #include "Dom/JsonObject.h" #include "Engine/World.h" #include "HttpModule.h" #include "Interfaces/IHttpResponse.h" #include "Kismet/GameplayStatics.h" +#include "ProfilingDebugging/CpuProfilerTrace.h" #include "Serialization/JsonReader.h" #include "Serialization/JsonSerializer.h" @@ -24,6 +26,9 @@ bool UAgrarianWeatherProviderSubsystem::RequestWeatherForActiveGameState() bool UAgrarianWeatherProviderSubsystem::RequestWeatherForTile(FName TileId, float Latitude, float Longitude) { + SCOPE_CYCLE_COUNTER(STAT_AgrarianWeatherProviderRequest); + TRACE_CPUPROFILER_EVENT_SCOPE(AgrarianWeatherProviderRequest); + if (TileId == NAME_None) { return false; @@ -172,6 +177,9 @@ void UAgrarianWeatherProviderSubsystem::ClearWeatherSnapshotCache() bool UAgrarianWeatherProviderSubsystem::ApplyDeterministicFallbackWeather(FName TileId, float Latitude, float Longitude, AAgrarianGameState* GameState) { + SCOPE_CYCLE_COUNTER(STAT_AgrarianWeatherProviderParse); + TRACE_CPUPROFILER_EVENT_SCOPE(AgrarianWeatherProviderFallback); + if (!bEnableDeterministicFallbackWeather || !GameState || !GameState->HasAuthority() || TileId == NAME_None) { return false; @@ -356,6 +364,9 @@ void UAgrarianWeatherProviderSubsystem::OnNoaaNwsGridDataResponse(FHttpRequestPt bool UAgrarianWeatherProviderSubsystem::ParseOpenMeteoForecast(const FString& ResponseContent, FName TileId, float Latitude, float Longitude, FAgrarianWeatherProviderSnapshot& OutSnapshot) const { + SCOPE_CYCLE_COUNTER(STAT_AgrarianWeatherProviderParse); + TRACE_CPUPROFILER_EVENT_SCOPE(AgrarianWeatherProviderParseOpenMeteo); + TSharedPtr RootObject; const TSharedRef> Reader = TJsonReaderFactory<>::Create(ResponseContent); if (!FJsonSerializer::Deserialize(Reader, RootObject) || !RootObject.IsValid()) @@ -427,6 +438,9 @@ bool UAgrarianWeatherProviderSubsystem::ParseOpenMeteoForecast(const FString& Re bool UAgrarianWeatherProviderSubsystem::ParseNoaaNwsGridData(const FString& ResponseContent, FName TileId, float Latitude, float Longitude, FAgrarianWeatherProviderSnapshot& OutSnapshot) const { + SCOPE_CYCLE_COUNTER(STAT_AgrarianWeatherProviderParse); + TRACE_CPUPROFILER_EVENT_SCOPE(AgrarianWeatherProviderParseNoaaNws); + TSharedPtr RootObject; const TSharedRef> Reader = TJsonReaderFactory<>::Create(ResponseContent); if (!FJsonSerializer::Deserialize(Reader, RootObject) || !RootObject.IsValid())