# Derived values

What the collector computes beside the raw samples: memory pressure, per-packet PMU cost, packets per interrupt, the burst flag, and when each is withheld.

Source: https://docs.jmrp.io/mikroscope/sinks/derive/

The collector's derive stage computes values beside the raw samples. For each: its
inputs, when it is withheld rather than written, and where each sink puts it. The
discrete events the same stage raises are on [Detection
rules](https://docs.jmrp.io/mikroscope/sinks/detections/).

## Where values are derived

The derive stage exists for the derivations that need state across samples, that join
the kernel tier to the API tier, or that must be computed once so every sink that carries
them carries the same numbers (which sinks have a place for them is in the table below) —
none of which a dashboard query does, and none of which the agent should pay for. The agent
ships raw tick deltas and never divides; the collector may. Nothing in this stage runs
on the router.

Two rules govern it:

- **A derived value is written beside its inputs, never instead of them**, so the store
  can recompute it if the derivation is later found wrong.
- **A detection is a discrete event** on the timeline, a "look here" — never a continuous
  series and never a verdict.

## Per kernel sample

### `mem_pressure`

The allocator's own escalation ladder as one ordinal. Every input is already shipped and
plotted on its own; what the ordinal adds is that the ladder is ordered, so one series
says how bad it got. The highest rung reached in the sample's `/proc/vmstat` deltas wins:

| Value | Rung                                        | From the sample's `/proc/vmstat` deltas |
| ----- | ------------------------------------------- | --------------------------------------- |
| 0     | none                                        | none of the below                       |
| 1     | kswapd scanned                              | `pgscan_kswapd` > 0                     |
| 2     | direct reclaim: a thread scanned itself     | `pgscan_direct` > 0                     |
| 3     | an allocation stalled or a page swapped out | `allocstall` > 0 or `pswpout` > 0       |
| 4     | the OOM killer ran                          | `oom_kill` > 0                          |

### Per-packet PMU ratios

`cycles_per_packet`, `instructions_per_packet` and `cache_misses_per_packet`: PMU counts
summed over cores, divided by the packets softnet processed in the sample,
summed over cores: the forwarding cost of the router in the one unit that lets two
configurations be compared. A ruleset change that halves cycles per packet is a real
win; one that halves busy time while traffic also halved is not. No such comparison
between two configurations is recorded yet
([not tested](https://docs.jmrp.io/mikroscope/about/status/#not-tested)).

Absent without a PMU, in a sample with no packets, and in a sample with a counter reset
(`suspect`), where a delta is a lower bound and not a measurement.

### `packets_per_irq`

Packets processed per device interrupt — every `/proc/interrupts` row but the timer and
the inter-processor interrupts — which is the NAPI coalescing depth. The device count is
the sample's total interrupts minus the timer rows and the `IPI` rows present in its
top-K.

Absent in a sample with no packets or with a counter reset (`suspect`), and when the timer
row is not in the sample's top-K, because then the device count cannot be separated from
the total.

### `burst`

A dropped packet, or more softnet squeezes than that CPU usually has — above its trailing
90th percentile **and** at least 3 — in a sample whose packet count was at or below its
trailing median. It is the kernel's own evidence of a burst shorter than the sample
interval, which is the only way this tool can see inside one. The flag is true when any
CPU qualifies.

The trailing baselines span ten seconds of wall clock at any rate: 100 samples at 10 Hz,
500 at 50 Hz, 1 000 at 100 Hz, bounded between 10 and 2 000 samples. No sample is flagged
until its CPU has at least ten samples of history.

Why a percentile and not "any squeeze": squeezes can be a router's background. On the
router measured, about 11.2 % of per-CPU samples carry one squeeze
and 1.2 % exactly two, so "any squeeze" would flag the device's
norm several times a minute. The baseline is the device's own: on another board or
traffic mix its percentile is that device's, and whether ten seconds is the right span
there is not measured ([Tested on](https://docs.jmrp.io/mikroscope/about/status/#not-tested)).

Three flagged samples on one CPU within 60 s is what raises the `microburst`
[detection](https://docs.jmrp.io/mikroscope/sinks/detections/#microburst); one flagged sample stays a data
point.

### `suspect`

True when the agent reported a counter reset in the sample: a counter that went backwards
without a 32-bit wrap. The raw row is kept; the per-packet values above — the three PMU
ratios and `packets_per_irq` — are withheld.

## Per counter poll

On each API round that carried the per-port counters, the collector computes, over the
deltas since the previous poll, the fast-path share of the traffic each interface hands
the CPU: of the bytes that reached the CPU on that interface, the part RouterOS counted
through the fast path rather than the slow path. The denominator is not the same counter
on every interface, because RouterOS does not count the same thing on every type — the
first counter present in both polls wins:

| Interface                                     | `fp_rx_share` is                             | Why that denominator                                                                                                             |
| --------------------------------------------- | -------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| a switch port (`ether…`, `sfp-sfpplus1`)      | Δ`fp-rx-byte` / Δ`driver-rx-byte`            | its `rx-byte` is the wire total, including frames the switch chip forwarded in hardware; `driver-rx-byte` is what reached the CPU |
| a software interface (bridge, VLAN, PPPoE)    | Δ`fp-rx-byte` / Δ`rx-byte` (or `rx-bytes`)   | it has no driver counters, and its `rx-byte` is already what the CPU sent and received                                           |

`fp_tx_share` is the same quotient in the other direction — Δ`fp-tx-byte` over
Δ`driver-tx-byte`, Δ`tx-byte` or Δ`tx-bytes` — and is withheld while `fp-tx-byte` has never counted, below.
Each share is capped at 1, with the four byte deltas written on the same row; `rx_bytes`
and `tx_bytes` there are the share's denominators, not the wire totals.

**It is not a share of the wire.** A frame the switch chip forwarded in hardware is in
neither number: it never reached the CPU. The CPU's share of a port's wire bytes —
`driver-rx-byte` against `rx-byte` — is a different question, and the stacked "Where a
port's receive bytes went" dashboard panel answers it from the raw counters; the derive
stage does not compute it.

A switch port whose `fp-rx-byte` equals its `driver-rx-byte` reads about 100 %: every
byte the port hands the CPU is counted at the fast-path-capable driver, so the port's line
says little. The share informs on software interfaces, such as a bridge or a PPPoE
client ([measured](https://docs.jmrp.io/mikroscope/about/status/#campaign-counters-2026-09-16)).

**The tx share is withheld while `fp-tx-byte` has never counted.** A router can leave
`fp-tx-byte` at 0 on every interface after hundreds of GB transmitted: that reads as a
counter RouterOS does not maintain there rather than as a fast path that forwarded
nothing, and a share computed from it would be a fabricated 0 %. While an interface's
cumulative `fp-tx-byte` is 0, the collector writes no `fp_tx_share` for it, and its
`tx_bytes` and `fp_tx_bytes` are 0. No tx share has been produced from live counters
yet, and why RouterOS leaves the counter at 0 is not established
([not tested](https://docs.jmrp.io/mikroscope/about/status/#not-tested)).

A share is also absent for a direction that moved no bytes, whose counter went backwards,
or whose fast-path counter the port does not report, and the whole row is absent when
neither direction's denominator could be differenced. The first poll of each interface
seeds and writes nothing.

A delta that could not be computed — its counter absent, gone backwards, or withheld with
the tx share — is written as 0 beside the absent share: the InfluxDB and SQL rows and the
Elasticsearch `fastpath` object carry `rx_bytes`, `fp_rx_bytes`, `tx_bytes` and
`fp_tx_bytes` unconditionally. This is read from the code
([`internal/derive/derive.go`](https://github.com/jmrplens/mikroscope/blob/main/internal/derive/derive.go)) and not observed
([Tested on](https://docs.jmrp.io/mikroscope/about/status/#not-tested)). In that case read the share, not the
delta.

The share is per poll, and on an interface moving a few packets it swings between 0 and
100 % from one poll to the next, so the panel
"Fast-path share of the traffic each interface hands the CPU" weights it by bytes per bin
— the bin's fast-path bytes over the bin's bytes — rather than plotting the per-poll
value. The Prometheus form of that panel is the collector's per-poll gauge and keeps the
noise.

## Storage per sink

| Sink                            | Beside kernel samples                                                                           | Beside counter polls                                      |
| ------------------------------- | ----------------------------------------------------------------------------------------------- | --------------------------------------------------------- |
| InfluxDB, Telegraf, stdout `lp` | `mikroscope_derived`: `mem_pressure`, `burst`, `suspect`, and the ratios that could be computed | `mikroscope_derived_iface{interface}`                     |
| SQL (`--sql`, `--postgres`)     | `mikroscope_derived`, ratios NULL where not computed                                            | `mikroscope_derived_iface`                                |
| file, stdout `json`             | a `{"derived":…}` line after its sample                                                         | not written; the raw counters are in the `{"api":…}` line |
| Prometheus                      | `mikroscope_derived_*` gauges for the newest sample; `mikroscope_collector_bursts_total`        | `mikroscope_derived_fastpath_share{interface,direction}`  |
| OTLP                            | `mikroscope.derived.*` gauges                                                                   | `mikroscope.derived.fastpath_share{interface,direction}`  |
| Graphite                        | `derived.*` paths                                                                               | `api.iface.<if>.fp_rx_share`, `fp_tx_share`               |
| Elasticsearch                   | `derived` on the kernel document                                                                | `fastpath` on the API document                            |
| Loki                            | —                                                                                               | —                                                         |

Three sinks spell the kernel-side names their own way, and carry less. Prometheus writes
`mikroscope_derived_memory_pressure`, `mikroscope_derived_cycles_per_packet`,
`mikroscope_derived_instructions_per_packet`, `mikroscope_derived_cache_misses_per_packet`
and `mikroscope_derived_packets_per_interrupt` for the newest sample; `burst` reaches it
only as the counter `mikroscope_collector_bursts_total`, and `suspect` not at all. OTLP
writes `mikroscope.derived.memory_pressure`, `mikroscope.derived.cycles_per_packet`,
`mikroscope.derived.instructions_per_packet`, `mikroscope.derived.cache_misses_per_packet`
and `mikroscope.derived.packets_per_irq`. Graphite writes `derived.mem_pressure`,
`derived.cycles_per_packet`, `derived.instructions_per_packet`,
`derived.cache_misses_per_packet` and `derived.packets_per_irq`. Neither OTLP nor Graphite
carries `burst` or `suspect`. In all three, a ratio that could not be computed is left out
rather than written as 0.

## See also

- [Detection rules](https://docs.jmrp.io/mikroscope/sinks/detections/): the eleven rules the same stage runs, and what each
  one may not claim.
- [RouterOS API tier](https://docs.jmrp.io/mikroscope/sinks/api-tier/): the port counters the fast-path share is
  computed from.
- [Resolution limits](https://docs.jmrp.io/mikroscope/limits/): why the PMU is the resolution below
  the jiffie.
- [Packet flood](https://docs.jmrp.io/mikroscope/playbooks/packet-flood/): softnet drops and squeezes on a real
  router.
