Add Agrarian performance profiling markers

This commit is contained in:
2026-05-16 11:01:46 -07:00
parent e50745dbdd
commit 82463f3b99
11 changed files with 196 additions and 1 deletions
+4 -1
View File
@@ -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.
+9
View File
@@ -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:
@@ -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())
@@ -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;
}
@@ -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())
@@ -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);
@@ -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);
@@ -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<AAgrarianGameState>() : nullptr;
if (!GameState)
@@ -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())
@@ -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<AAgrarianGameState>() : nullptr;
if (!GameState)
@@ -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<FJsonObject> RootObject;
const TSharedRef<TJsonReader<>> 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<FJsonObject> RootObject;
const TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(ResponseContent);
if (!FJsonSerializer::Deserialize(Reader, RootObject) || !RootObject.IsValid())