Performance
The numbers behind the 2000-agent claim, with reproduction steps so anyone can verify.
[ Performance ]
2000 agents, single thread, sub-millisecond
Defensible baseline numbers and the exact steps to reproduce them. The data is from the reference workstation; consumer CPUs vary by 2–3×, so re-run on your own hardware and add a row.
Headline numbers
Per-cycle wall time for one full replan pass across N agents, plus per-tick dispatcher cost at the same N.
| Agent count | Replan p99 (ms) | Per-agent p99 (µs) | Dispatcher tick p99 (ms) |
|---|---|---|---|
| 100 | 0.030 | 0.30 | 0.0022 |
| 250 | 0.074 | 0.30 | 0.0053 |
| 500 | 0.130 | 0.26 | 0.0065 |
| 1000 | 0.400 | 0.40 | 0.023 |
| 2000 | 0.375 | 0.19 | 0.052 |
| 4000 | 0.869 | 0.22 | 0.066 |
At 2000 agents, replan + dispatcher together stay well under 1 ms per frame on a single thread. Past the 2000 budget the scaling stays roughly linear: 4000 agents land at 0.87 ms, in line with an O(N) loop.
Reference machine
| Item | Value |
|---|---|
| OS | Windows 11 Pro 10.0.26100 |
| UE version | 5.7 |
| Build config | Development Editor |
| RHI | NullRHI (headless automation) |
Benchmark catalogue
Automation tests live under the IntentForge.Performance.* namespace.
| Test | What it measures | Asserts? |
|---|---|---|
SingleStepPlan | Trivial archetype planner cost | mean < 0.1 ms |
BranchingArchetype | Branching A* search cost | mean < 0.5 ms |
MediumArchetype | 20-action linear chain | mean < 2.0 ms |
ActivityHistoryRingAppend | One RecordActivity at depths 32/128/256 | depth-256 mean < 0.005 ms |
AgentMemoryFootprint | KB per active component at N=1/100/1000 | — |
ManyAgentReplan | Per-cycle wall time for N agents to each replan | N=100 p99 < 50 ms |
PlannerOnlyReplan | Same N sweep, direct Plan() calls | — |
AntiFlapSelectionCost | Momentum + filter + latch overhead vs baseline | — |
WorldStateSetScalarCost | Per-call SetScalar cost across 4 configs | — |
DispatcherTickCost | One dispatcher Tick(0.016) with N components | — |
SensorSampleCost | Per-class Sample() cost (Core sensors only) | — |
Source: Plugins/IntentForge/Source/IntentForgeCore/Private/Tests/PerfBenchmarks_*.cpp.
Reproduce locally
Build the editor target.
"C:\Program Files\Epic Games\UE_5.7\Engine\Build\BatchFiles\Build.bat" \
CrucibleEditor Win64 Development -Project="<path>\Crucible.uproject" -WaitMutexRun the perf subset.
"C:\Program Files\Epic Games\UE_5.7\Engine\Binaries\Win64\UnrealEditor-Cmd.exe" \
"<path>\Crucible.uproject" \
-ExecCmds="Automation RunTests IntentForge.Performance; Quit" \
-unattended -nopause -nullrhiDrop .Performance to run the full IntentForge automation suite.
Compare against the checked-in baseline.
CSV output: <ProjectDir>/Saved/Logs/IntentForgePerf/perf_<UTC-timestamp>.csv —
one row per (benchmark, config, run). Treat
PerformanceBaseline.json
as the source of truth; the tables above are the human-readable view.
A full sweep takes 10–30 seconds on a developer machine.
Anti-flap stack overhead
The anti-flap stack adds runtime cost on
every replan and on every scalar fact write. The
AntiFlapSelectionCost benchmark isolates the momentum dimension by
running the same archetype with GoalMomentumBonus = 0, then with
the default 0.15.
At N=500, 1000 iterations per config, 3 runs:
| Configuration | Mean per cycle (ms) | p99 per cycle (ms) |
|---|---|---|
| Momentum off | 0.0798 | 0.083 |
| Momentum on | 0.0780 | 0.084 |
| Delta | -0.0018 (-2.3%) | +0.001 |
The delta sits inside measurement noise. The momentum bonus is effectively free — one multiplication per candidate goal during ranking, lost in the rest of the selection path.
EMA filter and Schmitt latch costs live inside FWorldState::SetScalar
and only fire when the schema configures them. The
WorldStateSetScalarCost micro-benchmark covers those directly across
four configs (baseline / filter-only / latch-only / both) at K=100
batched calls per timing window.
Memory footprint
AgentMemoryFootprint snapshots process memory before and after
spawning N components on a transient world, force-GCs, snapshots
again, and divides the delta by N.
| N | Total delta (KB) | Per-agent (KB) |
|---|---|---|
| 1 | 8.0 | 8.0 |
| 100 | ~0 | (noise) |
| 1000 | 220 | 0.22 |
The N=100 row falls to zero because process memory snapshots vary by
tens of KB between quiescent reads. N=1000 gives the stable estimate:
roughly 220 bytes per agent including the component, the live
FWorldState, and two history ring buffers at default depths.
Plan on "under 1 KB per agent for the framework data" plus whatever your sensors and executors carry.
Sensor cost
Per-Core-sensor cost for one Sample() call, batched K=100 per timing
window across 5000 windows.
| Sensor class | Mean (µs) | p99 (µs) |
|---|---|---|
GameplayTagOnActor | 0.034 | 0.065 |
DistanceToWorldLocation | 0.055 | 0.095 |
DistanceToReferencedActor | 0.073 | 0.125 |
LineOfSightToActor | 0.208 | 0.357 |
LineOfSightToActor does an actual line trace. The empty-scene case
above is best case — expect 2–5× in a dense scene with collision
geometry.
Perception sensors (AIPerception, BlackboardBool/Float, EnvQuery)
are excluded by design: their cost is dominated by the AI subsystems
they bridge into, not the sensor pattern itself.
Methodology
- Warmup: 10 iterations per config before timing.
- Repeats: 1000 iterations per config (100 at N ≥ 2000). Multi-agent and dispatcher benchmarks do 3 runs and report median p99.
- Percentiles: nearest-rank, no interpolation.
- Lead statistic: p99. A mean that hides a 10× spike every 100 frames is a hung-frame problem.
- GC: full purge between runs.
- Timer:
FPlatformTime::Seconds()(Windows QPC ≈ 100 ns resolution). Sub-microsecond benchmarks batch K=100 calls per window to clear the noise floor.
Caveats
- Single-thread. No async path. The numbers are the full game-thread cost.
- No movement / animation / nav in the test worlds. Real games carry per-actor cost outside the framework.
ManyAgentReplanmeasures per-cycle wall time, not per-plan compute. Coalescing and plan reuse mean the per-cycle work is less than "plan from scratch per agent" — which matches realistic production.AgentMemoryFootprintuses process-global stats. Noisy at the tens-of-KB level — treat as order-of-magnitude.- CPU variance is real. Consumer CPUs differ by 2–3× single-threaded. Your numbers will differ; the shape (linear, bounded tail) is what to look for.
Community runs
Re-run the benchmarks on your own hardware and add a row via PR. If you drop the CSV alongside, the project keeps a per-machine record.
| Contributor | CPU | RAM | OS | UE | p99 replan @ N=2000 | CSV |
|---|---|---|---|---|---|---|
| your row here | e.g. Ryzen 7 7800X3D | 32 GB DDR5-6000 | Win 11 | 5.7 | 0.41 ms | link |