Files
EscapistsReintroduced/docs/hq-performance-measurements.md
T
jakub 6a667d8deb Capture current mission, AI, map and administration development
Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
2026-10-13 22:52:53 +02:00

154 lines
9.5 KiB
Markdown

# Runtime HQ measurements
The mod records read-only server telemetry by default. Publish this source and
run a normal multiplayer session. No AMP integration or external service is
required. Add `-errNoPerf` to the game server startup arguments to disable all
collection (useful for measuring instrumentation overhead).
Measurements go to **`rereintroduced-performance.log`** in the server's current
engine log directory (`$logs:`, alongside `console.log`). Both `ERR PERF START`
and the measurement rows are written only to this separate file. Mission restarts
append new run IDs; a new engine log directory gets its own performance file.
The file is closed after each write batch. If it cannot be opened, a single error
per mission is reported in the main log and opening is retried after 60 seconds;
measurements never fall back to the console. Pending statistics are retained across
open failures, but are lost if the final shutdown write also fails.
Each window is
60 seconds of real elapsed time, with up to 26 aggregate rows. Clean mission
shutdown also writes a partial final window. Crashes can lose the unfinished
window. Every row includes a run ID, window ID, elapsed time and start/end mission
phase. Windows may span phase transitions or changes in player/AI population.
```sh
python3 tools/parse_hq_performance.py /path/to/rereintroduced-performance.log
python3 tools/parse_hq_performance.py /path/to/rereintroduced-performance.log --csv > /tmp/hq-performance.csv
```
Identical repeated rows are deduplicated; conflicting duplicates and malformed
rows are errors. CSV retains individual windows and the `over50` / `over100`
threshold counts. The text summary combines only windows where the metric was
observed; compare matching windows in CSV for attribution rather than comparing
different stages of a mission.
Older console logs remain compatible with the parser.
## What is measured
| Metric | Meaning |
|---|---|
| `mission_tick_ms` | Whole scheduled mission tick, including early returns and terminal transitions |
| `maintenance_ms` | Command maintenance, including continuing aircraft |
| `civilians_ms` | Civilian traffic tick |
| `forces_ms` | Force-ledger snapshot and virtual-force update |
| `observers_ms` | Connected player observer/camera updates |
| `hq_tick_ms` | Whole tactical HQ tick, including all HQ scopes below |
| `listeners_ms` | Mission tick event listeners |
| `hq_prep_ms` | Belief memory, equipment and special-skill maintenance |
| `hq_hunters_ms` | Hunter fleet management |
| `hq_garrisons_ms` | Objective defence and town/object worker scheduling (before the slicing fix this also included all town/object work) |
| `garrison_work_ms` | Independent background town/object discovery, placement or creation slice; outside the mission/HQ tick |
| `performance_flush_ms` | Duration of the preceding successful non-final telemetry write batch, recorded in the following window |
| `hq_convoys_ms` | Mounted intercept, convoy management and automatic dispatch |
| `hq_bodies_ms` | Body investigation/recovery budgeting |
| `hq_radio_index_ms` | Five-second radio/inventory indexing pass, only when run |
| `hq_reports_ms` | Contact expiry, local combat updates, radio delivery and report collection |
| `hq_planner_ms` | Five-second convoy, supply, mounted-intercept and squad assignment planning pass, only when run |
| `hq_support_ms` | Mines, resupply, logistics, mortars, assault, airfield and recovery ticks |
| `hq_journal_ms` | HQ journal flushing |
| `update_interval_ms` | Wall-clock gaps between game OnUpdate callbacks |
| `engine_sim_sample_ms` | Engine-reported total simulation time sampled approximately once per second |
| `engine_frame_sample_ms`, `engine_fps_sample` | Engine-reported frame time/FPS sampled at the same cadence |
| `active_ai_sample`, `players_sample` | Native active AI and connected player counts, approximately once per second |
| `hq_groups_sample` | Registered HQ unit records, once per HQ tick; not a soldier count |
Durations use `System.GetTickCount()` with **1 ms resolution**. Rows contain count,
sum, minimum, maximum, zero-valued count and counts above 50/100 in that metric's
units. Threshold counts are intended for duration metrics, especially update
intervals. A zero duration means below the available clock resolution; it is not
proof that an operation is free. Averages can show fractions of milliseconds
because they aggregate many quantized samples. This is elapsed wall time, not
process CPU time, and synchronous native calls inside a scope contribute to it.
`mission_tick_ms` contains `hq_tick_ms`, which contains `hq_planner_ms` and the
other HQ scopes. `garrison_work_ms` is separate background work, not a child of
those ticks. The last telemetry flush cannot report its own duration if no later
window is written. **Do not sum parents and children.** Uninstrumented mission work
and collection overhead also contribute to parent scopes. The profiler uses
fixed-size accumulators, does no entity/inventory scanning, and makes no network
requests. It adds one clock read per server OnUpdate and boundary reads around
the slow scheduled scopes; reporting itself also costs time.
`update_interval_ms` includes scheduling, deliberate FPS caps and stalls; it is
not engine CPU cost. ProfileData averaging/window semantics are engine-defined;
these are sampled engine values, not raw per-frame timings or percentiles.
`SimulationTimeMs` is **not AI-only time**. Do not subtract script timings from it
to claim a native-AI cost: sampling, timing domains and asynchronous work differ.
The public API has not established an AI-only timer. Combat, pathfinding and
physics performed outside these script calls are not attributed to HQ scopes.
## Using the result for external planning
Collect comparable windows with players present, including prison, hunting and
combat. Check `hq_planner_ms` sum divided by the window's elapsed seconds:
this gives planner wall milliseconds per real second. Compare its peaks with the
whole HQ/mission tick, engine simulation samples and update stalls. Correlation
with active AI is useful evidence, not proof of causation.
If planning is consistently tiny while frame stalls grow with AI population,
moving the existing planner externally is unlikely to provide much relief. If
planning spikes are substantial, an external planner may deserve a controlled
comparison. Even then it cannot remove all measured planner work: current
planning includes local world checks and applying orders. Serialization,
validation and service latency would add costs. No speedup estimate is emitted.
Collection runs only for the current server-owned Escape game mode, including
idle/preparation periods so those can be identified. It resets on a new mode,
closes on StopServices and remains disabled on clients. There are no changes to
AI decisions, frequencies, simulation settings or ownership.
Implementation was reviewed without launching the game or automated tests, per
the user's request. Native compilation and live telemetry remain to be verified
after publication.
## Memory investigation, 11 October 2026
`ERR MEMORY` rows now share `rereintroduced-performance.log`. They record native
`System.MemoryAllocationKB()` at mode construction, once per minute, on mode
stop and when world cleanup is requested. These are engine allocation estimates,
not OS RSS. Cleanup-requested is **before** engine teardown; compare with the
next mode-created reading rather than interpreting it as memory after cleanup.
Rows include UTC Unix time, phase, players, active AI and live mission-owned root
entities. Root entities are not a world-wide entity count (children/inventory are
excluded). Collection does not query or stream terrain or walk inventories.
`-errNoPerf` disables these rows too.
For Linux RSS, use `tools/sample_server_memory.py --output /tmp/escape-memory.jsonl`
on the host. It samples all Reforger processes once per minute for 24 hours,
including PID/start identity, RSS/anonymous/file memory, available host RAM,
container limits, cache usage and OOM counters. It performs no game calls or
server changes. Output rotates at 2 MiB with one previous file. Container
`memory.current` includes file cache and must not be treated as process RSS.
RKS2 evidence: the kernel killed PID 251447 on 10 October at 15:00 UTC with
15,439,164 KiB anonymous RSS, following several scenario reloads. On 11 October
at 09:40 UTC the fresh PID 305274 had 7,520,580 KiB RSS. A second Reforger PID
246717 used 2,996,396 KiB. The host has 19,526 MiB total RAM and no swap; the
containers reported `memory.max=max`, not a private 19 GiB reservation for RKS2.
These snapshots confirm host memory pressure, **not** its precise allocation
source or a proven leak in a particular mission subsystem.
Implemented mitigations: defer distant background population until the first
connection (the prison admission scene is still built), then let admitted native
preparation finish even if players disconnect; suppress new/replacement recon
planes when no players are connected; release unfinished garrison resource
handles on stop and catalog bookkeeping at shutdown/world cleanup. Equipment
probabilities and native catalog initialization are unchanged. Native Arsenal
InitData already loads/pins resources before the lottery reads GetItemResource;
removing that getter from the lottery would not prove any memory saving.
A read-only host sampler was started on 11 October for 24 hours, writing
`/tmp/escape-memory-20261011.jsonl`. The mission changes still need publication.
Native 1.8 Workbench compilation passed; a comparable multi-round live memory
soak is required before claiming the recurring OOM is resolved.