Skip to content

Switch attribution on and off without a restart - #53

Merged
Pixnop merged 1 commit into
devfrom
feat/hot-toggle
Sep 6, 2026
Merged

Pixnop merged 1 commit into
devfrom
feat/hot-toggle

Conversation

@Pixnop

@Pixnop Pixnop commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

A hosting provider running Pulse asked whether the profiler can be switched without restarting the
server. It could not, and that is the wrong shape for a profiler: the moment you want per-mod
attribution is while the server is struggling, and a restart erases what you wanted to look at.

/pulse fixes it. Four subcommands, all behind controlserver so admins and a panel console can
run them:

  • /pulse attribution on starts the duty cycle on whatever BurstTicks and IntervalSeconds are
    in force, with the first burst one interval later.
  • /pulse attribution off stops it and puts the engine's frame profiler back down on the next tick.
    A burst in progress is dropped rather than published half-measured.
  • /pulse attribution status reports the cycle in use, the ticks profiled so far, and whether it is
    inside a burst right now.
  • /pulse reload re-reads pulse.json through the same LoadModConfig and ConfigUpgrade path
    startup uses, applies the Attribution block live, and names the keys whose value in the file has
    drifted from what the server is running, since those still need a restart. A file that does not
    parse changes nothing and the reply carries the parse error.

on and off never write pulse.json, and both replies say so. A ten minute look should not become
permanent because somebody forgot to turn it off.

What made this safe

The four families and the frame profiler priming are now registered whether or not
Attribution.Enabled is set, and that is the load-bearing part rather than a tidy-up.
FrameProfilerUtil.End dereferences the root range that only Begin creates, and ServerMain
calls End outside the try/catch guarding the tick. Flipping the flag part-way through a tick on a
profiler that has never completed one is a NullReferenceException that takes the process down.
Priming at RunGame, before the tick loop exists, guarantees the root is there, and the duty cycle
already refuses to touch the flag until PrevRootEntry proves it. Arming a server that never asks
for attribution costs two profiled ticks at boot and four instruments nothing records into, and an
instrument with no measurement is not a series: an idle server serves the exposition it always did,
which the new scenario asserts before switching anything on.

TickAttribution takes a new duty cycle at runtime by restarting the cycle rather than adjusting it
in place. Switching off mid-burst therefore drops the half-folded sample instead of publishing it,
and switching back on discards the stale tree the profiler left behind exactly the way a normal
burst start does.

Tests

217 green, up from 195: 14 unit tests on the reply text and the restart-key comparison, 6 more on the
mutable duty cycle, and two Atlas scenarios. The first boots with attribution off, asserts nothing
attribution-shaped is on the wire, runs /pulse attribution on through
IWorldSession.ExecuteCommand (a console caller with every privilege, the same shape ServerMain
builds for a console line), waits out a real burst, finds Pulse in its own numbers, then switches
off and checks both that FrameProfiler.Enabled went back down and that no further ticks accrue over
the next 300. The second rewrites pulse.json under the running server three times: a new
BurstTicks that reload applies, a changed Port that it can only name, and a file that is not JSON
at all, which leaves everything as it was.

tools/mutation-check.sh gains three mutations and reports 44/44 killed. The reply string is a
format string with the parse error as a message parameter rather than interpolated, because the
engine runs a command reply through string.Format on its way to whoever asked and a JSON error
quoting a stray brace would blow that up.

Per-mod tick attribution was config plus restart, which is the wrong shape
for a profiler: the moment you want it is while the server is struggling,
and restarting erases what you wanted to look at.

/pulse attribution on and off drive the duty cycle on the running server
and never write pulse.json, so a ten minute look does not become permanent
by accident. /pulse attribution status says what is running and what it has
measured. /pulse reload re-reads the file through the same loader startup
uses, applies the Attribution block live, and names the keys whose values
have drifted from what the server is running, because those still need a
restart. All of it behind controlserver, so a panel console can drive it.

The four families and the frame profiler priming are now registered whether
or not Attribution.Enabled is set. That is what makes a later switch-on safe
rather than merely likely to work: FrameProfilerUtil.End dereferences a root
range that only Begin creates, and enabling the flag part-way through a tick
on a profiler that has never completed one takes the server down. Priming
before the tick loop guarantees the root exists, and PrevRootEntry being
non-null is the proof the duty cycle checks. The cost is two profiled ticks
at boot and four instruments nothing records into, and an instrument with no
measurement is not a series.

TickAttribution takes a new duty cycle at runtime by restarting the cycle,
so switching off mid-burst drops the half-folded sample instead of
publishing it and switching back on discards the stale tree the same way a
normal burst start does.
@Pixnop
Pixnop merged commit 24d4896 into dev Sep 6, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant