Skip to content

Stop atmos/power consoles from re-scanning sensors and re-running EnsureComp every tick#1392

Merged
fenndragon merged 4 commits intoHardLightSector:masterfrom
Kronyxxx:perf/console-state-thrash-2026-04
Apr 23, 2026
Merged

Stop atmos/power consoles from re-scanning sensors and re-running EnsureComp every tick#1392
fenndragon merged 4 commits intoHardLightSector:masterfrom
Kronyxxx:perf/console-state-thrash-2026-04

Conversation

@Kronyxxx
Copy link
Copy Markdown
Contributor

About the PR

Another small no-gameplay-change perf pass, this time aimed at a few of the
grid-mounted consoles that show up a lot on big ships.

While poking around at where server time goes during a populated round, I
noticed the atmos monitoring console, the atmos alerts computer, and the
power monitoring console all share the same shape of problem: every second
they refresh their UI, and that refresh ends up doing a couple of things
it doesn't really need to do per tick.

The big one is the atmos monitoring console. Its UI refresh walks every
GasPipeSensorComponent in the world to figure out what to show. That's
fine for one console, but a capital-style ship usually has more than one
atmos console open at the same time, and each of them was redoing the
exact same world-wide scan and building the exact same sensor list. This
PR caches that built list per gridUid for the duration of one Update()
pass and lets the rest of the consoles on that grid reuse it. The cache
is dropped at the end of the pass, so behavior across passes is identical.

The other small thing is EnsureComp<NavMapComponent>(gridUid). All three
of these consoles were calling that on the grid every UI refresh, even
though the only thing it actually has to do is run once when the console
gets placed on a grid. This PR moves that call into the existing init /
parent-change handlers so it only runs when a console first lands on a
grid (or moves to a new one), instead of every second per console for the
rest of the round.

Also hoisted the IsUiOpen check on the atmos monitoring console up to
the outer Update() loop so closed consoles don't even pay the
GridUid lookup. It was already the first check inside UpdateUIState,
this just makes it a bit cheaper.

Why / Balance

No gameplay or balance change intended.

  • Same UI payloads get sent to the same consoles at the same cadence.
  • Same NavMapComponent ends up on the same grids — EnsureComp is
    idempotent, so consoles that already had a NavMap in upstream still
    have one, and consoles that re-parent (e.g. a ship docking to a new
    grid) re-run the init path and get one again.
  • Sensor entries shown on the UI are still freshly rebuilt every UI
    refresh window; the cache only deduplicates within a single pass
    across consoles on the same grid.
  • Closed consoles still get re-evaluated on the next pass, same as
    before.

This is purely a "do the same thing, with less repeated server work" pass.

Technical details

  • AtmosMonitoringConsoleSystem
    • Added a per-Update() scratch dictionary
      Dictionary<EntityUid, List<AtmosMonitoringConsoleEntry>> keyed by
      grid. The first console for a given grid in a pass populates it; all
      subsequent consoles on that grid reuse the list. Cleared at the end
      of the pass.
    • Moved the IsUiOpen check up to the Update() loop so closed
      consoles short-circuit before the per-console work starts.
    • Moved EnsureComp<NavMapComponent>(gridUid) into
      InitializeAtmosMonitoringConsole so it runs at console init and
      parent-change, not every UI refresh.
  • AtmosAlertsComputerSystem
    • Moved EnsureComp<NavMapComponent>(gridUid) out of UpdateUIState
      and into InitalizeConsole. (Init runs on ComponentInit,
      EntParentChangedMessage, and OnGridSplit, so any new grid the
      console lands on still gets its NavMap.)
  • PowerMonitoringConsoleSystem
    • Same treatment: added a small EnsureNavMapForConsole helper called
      from OnConsoleInit and OnConsoleParentChanged so the per-tick
      UpdateUIState no longer calls EnsureComp every second per console.

No CVars, no prototypes, no YAML, no shared/client code touched.

How to test

  1. Run dotnet build /property:GenerateFullPaths=true /consoleloggerparameters:'ForceNoAlign;NoSummary'
    and confirm the solution still builds.
  2. Spawn or load a ship with a few atmos pipe sensors and at least two
    atmos monitoring consoles. Open both consoles and confirm both show
    the same sensor list with the same temperature/pressure/gas data as
    before.
  3. Add or remove a pipe sensor on the grid and confirm it appears /
    disappears in the console UI on the next refresh window (~1s), same
    as before.
  4. Open an atmos alerts computer on a grid and confirm the alarms list
    and NavMap regions still render. Trigger an alarm and confirm the
    console state still updates and the appearance still changes.
  5. Open a power monitoring console and confirm the source / battery /
    load lists still populate and update normally, including the
    "rogue power consumer" flag if you spawn a high-draw consumer.
  6. Sanity check that grids that didn't have a NavMapComponent before any
    console was placed still get one as soon as a console lands on the
    grid (e.g. spawn a fresh grid, anchor a console, open the UI — NavMap
    visualisation should work).

Media

N/A — server-side perf only, no visual change.

Breaking changes

None.

Changelog

No changelog. Internal perf cleanup only.

HardLight Perf and others added 4 commits April 23, 2026 20:04
…paths

Round-2 of low-risk, gameplay-neutral perf cleanups.

- Speech/ReplacementAccentSystem: cache compiled (RegexOptions.Compiled)
  word-boundary regexes per replacement word instead of building a fresh
  Regex per word per spoken message. Big win for chat-heavy rounds with
  accents like Bleat/Spanglish/etc.
- Power/BatterySystem: reuse a single scratch List<(EntityUid,float)> for
  PostSync and Update instead of allocating a fresh ValueList sized to
  Count<T>() every tick / every net sync.
- _NF/Cargo/NFCargoSystem.Telepad: replace per-tick LinkedSources.Select(...)
  .ToList() with a reused scratch List<NetEntity> on the partial system.
- _NF/Shipyard/ShipyardGridSaveSystem.Update: lazy-allocate the 'expired'
  list so the common (no-expirations) tick path allocates nothing.
- _Mono/FireControl/FireControlSystem.HasLineOfSight: drop redundant
  .ToList() materialization of the raycast results; use Any() directly.
- _NF/Atmos/GasDepositScannerSystem: drop redundant .ToArray() copy of an
  already-typed GasEntry[] before sending the UI message.
- _HL/ShuttleDeedTracking: gate per-shuttle Debug log interpolation behind
  IsLogLevelEnabled(LogLevel.Debug) so the ToPrettyString call disappears
  when debug logging is off.
- Storage/Material{Storage,Reclaimer}MagnetPickupSystem.ToggleMagnet: drop
  dead 'var query = EntityQueryEnumerator<...>()' allocation that was
  declared but never used.

No behavior changes, no gameplay changes. Build clean, 385 tests pass.
- RadarBlipSystem: stop globally clearing the cached blip-report dictionary
  on every single blip ComponentShutdown. The 75ms TTL is sufficient and
  combat (projectile/torpedo despawns) was thrashing the cache, forcing
  every open radar console to redo a full AssembleBlipsReport every frame.

- ServerCleanupSystem.CleanupFloatingEntities: replace the O(entities * players)
  per-cycle nested scan with spatial bucketing keyed by
  (MapId, floor(x/120), floor(y/120)). At 100-150 CCU this is the difference
  between a several-hundred-millisecond stall and a near-zero pass. Also
  drop the unused _disconnectedPlayers stale-entry .Where().ToList() and
  switch the inner distance check to DistanceSquared.

- ThrusterSystem.Update: replace per-firing-thruster comp.Colliding.ToArray()
  defensive copy with a single reusable scratch list. Capital ships with
  many active burn-fixture thrusters were allocating one List per thruster
  per tick.

Touches HardLight-only code paths; no upstream Robust changes.
Previous fix removed the global _recentRadarReports.Clear() but left a
subtle ordering hole: if BlipRemovalEvent arrived at the client before
a cached GiveBlipsEvent (served within the 75ms TTL), the cached payload
would still contain the dead blip and re-introduce it on the radar for
up to ~300ms (TTL + 225ms throttle).

Iterate each cached report once per shutdown and remove the dead blip's
BlipNetData by Uid. O(consoles * blips_per_report) per shutdown — still
dwarfed by the cache-hit savings during combat — and eliminates the
visual flicker entirely.
…r-tick UI refresh

AtmosMonitoringConsoleSystem.Update used to walk every GasPipeSensorComponent
in the world once per console per second, so a capital ship with multiple
atmos consoles paid that cost N times. Cache the built sensor entry list per
gridUid for the duration of one Update() pass and reuse it for every console
on that grid. Cleared at the end of the pass, so behavior across passes is
identical. Also hoist the IsUiOpen check up to the Update() loop so closed
consoles skip the GridUid lookup entirely.

EnsureComp<NavMapComponent>(gridUid) was being called every UI refresh from
all three consoles (Atmos monitoring, Atmos alerts, Power monitoring). Move
it into the existing init / parent-change handlers so it runs once per
console placement instead of every second per console.

No gameplay change — same UI payloads, same cadence, same NavMap presence.
@fenndragon fenndragon merged commit ba736b8 into HardLightSector:master Apr 23, 2026
13 of 14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants