Tier 2: whole-node statistics, read on the tick.
Three groups, each sampled at most once per tick no matter how many watches read from it.
vm: :memory
Everything :erlang.memory/0 returns: :total, :processes,
:processes_used, :system, :atom, :atom_used, :binary, :code,
:ets. All in bytes.
vm: :system
:process_count, :port_count, :atom_count, :run_queue, :reductions.
:reductions has a side effect
Reading it calls :erlang.statistics(:reductions), which resets the VM's
"reductions since last call" counter for the whole node. If something else
in your system reads that value, declare this watch knowing it will see
zeros. Kepler's own budget accounting deliberately avoids it and uses
per-process reductions instead.
vm: :scheduler
:utilization, as a fraction between 0.0 and 1.0. Declaring it turns on the
VM's scheduler_wall_time flag, which has a small but real steady-state
cost — that is why it is opt-in rather than always reported. The first tick
after startup reports no data, since a utilization needs two samples.
Summary
Types
@type state() :: %{scheduler: [tuple()] | nil}
Carried between ticks so cumulative counters can be differenced.
Functions
@spec initial_state() :: state()
An empty sampling state.
@spec sample([Kepler.Watch.t()], state()) :: {%{required(atom()) => number() | :no_data}, state()}
Samples every VM watch.
Each group is read once and shared, so ten memory watches cost one
:erlang.memory/0.