Files
werkator/docs/prs/2026-10-04-PR#111-metrics-sample-in-steps.md
miandmhoennig c684a7906e
werkator build succeeded after 01:53
werkator/deploy build succeeded after 00:08
One System page sample taken in named steps: host readings, disks with repository size, snapshot, history (v1.34.8) (#111)
## The Problem

After PR#110 `SystemMetricsCollector.sample` had the highest Mutation-CRAP score of Werkator: 35.36 at a complexity of 35.
A score is never below the complexity, so no test could bring it under the limit of 15.

The method did everything one sample of the System page needs in one body:
it read the processors, the memory and swap, the run queue and the stall times,
measured every disk against its budget and reset a disk's series when that budget changed,
built the snapshot, recorded the history, and persisted the state.
Most of its complexity was null handling: each of about twenty `cpu?.usedCores`, `ram?.usedGib`, `reading?.space?.usedBytes` asked again whether its source had been readable.

## Non-Goals

- No change in behaviour, in the snapshot, in the history, or in the persisted state.

## The Scenarios

### Feature: One sample of the System page, taken in named steps

#### Scenario#111.01: A sample still measures what it measured before

So that the System page, the health verdict and the sizing see the same numbers after the refactoring.

- **Given** the existing collector tests: CPU, RAM, swap, swap traffic, load, pressure, OOM kills, disks with quotas, the usage walk, aggregates, persistence, and unreadable sources
- **When** they run against the split `sample`
- **Then** all of them pass unchanged

##### Verified by

- [SystemMetricsCollectorUnitTest](../../src/test/kotlin/de/hoennig/werkator/metrics/SystemMetricsCollectorUnitTest.kt) — all 31 tests, unchanged

## The Solution

`sample` now reads as its domain steps:

| Step            | What it does                                                                                                    |
| --------------- | --------------------------------------------------------------------------------------------------------------- |
| `readHost`      | what the kernel says about the machine: processors, memory and swap, run queue, stalls, OOM kills, swap traffic |
| `sampleStorage` | every monitored disk against its budget, and the repository size from the same usage walk                       |
| `bindSource`    | remembers what a disk was measured against, and restarts its series when that changed (Scenario#16.05)          |
| `diskMetricsOf` | the gauge of one disk; an unreadable disk keeps its key and label                                               |
| `snapshotOf`    | the snapshot the UI and the health verdict read                                                                 |
| `historyValues` | the columns a host reading and a storage sample add to the history                                              |

A host reading that failed now holds its unread value (`CpuLoad.UNREAD`, `RamUsage.UNREAD`, `LoadAverage.UNREAD`) instead of `null`.
A gauge asks for its number, and the question whether the source was readable is answered once, where it is read.
`RunningAggregates` names the running min/max/avg per metric that `sample` kept as a local map and a local function.
`DiskMetrics.unread` is the gauge of a disk not measured, shared with the empty snapshot before the first sample.

Measured with PIT as the nightly build runs it:

| Method                          | Before | After |
| ------------------------------- | ------ | ----- |
| `SystemMetricsCollector.sample` | 35.36  | 3.04  |
| `diskMetricsOf`                 | —      | 4     |
| `RunningAggregates.record`      | —      | 4     |
| `bindSource`                    | —      | 3     |
| `sampleStorage`                 | —      | 3     |
| `StorageSample.historyValues`   | —      | 2     |
| `readHost`                      | —      | 1.13  |
| `snapshotOf`                    | —      | 1     |
| `HostReading.historyValues`     | —      | 1     |

No new method of the collector scores above 4, and none is `INCONCLUSIVE`.

The gate `maxMutationCrap` falls from 36 to 35: the highest score left is `BuildDefinition.applyTo` at 34.

## Additional Changes

- Version 1.34.8 with its release note.

## Follow-up PRs

- `BuildDefinition.applyTo` (34) and `CapacitySizing.cpu` (33.17), each in its own PR.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: mhoennig <michael@hoennig.de>
Reviewed-on: #111
2026-10-04 18:35:09 +02:00

4.3 KiB

WARNING: This document describes only the change applied in this PR. It may already be outdated once the next PR is merged. Historic PR-documentation is not maintained along with new PRs — treat it as a snapshot, not as current documentation.

The Problem

After PR#110 SystemMetricsCollector.sample had the highest Mutation-CRAP score of Werkator: 35.36 at a complexity of 35. A score is never below the complexity, so no test could bring it under the limit of 15.

The method did everything one sample of the System page needs in one body: it read the processors, the memory and swap, the run queue and the stall times, measured every disk against its budget and reset a disk's series when that budget changed, built the snapshot, recorded the history, and persisted the state. Most of its complexity was null handling: each of about twenty cpu?.usedCores, ram?.usedGib, reading?.space?.usedBytes asked again whether its source had been readable.

Non-Goals

  • No change in behaviour, in the snapshot, in the history, or in the persisted state.

The Scenarios

Feature: One sample of the System page, taken in named steps

Scenario#111.01: A sample still measures what it measured before

So that the System page, the health verdict and the sizing see the same numbers after the refactoring.

  • Given the existing collector tests: CPU, RAM, swap, swap traffic, load, pressure, OOM kills, disks with quotas, the usage walk, aggregates, persistence, and unreadable sources
  • When they run against the split sample
  • Then all of them pass unchanged
Verified by

The Solution

sample now reads as its domain steps:

Step What it does
readHost what the kernel says about the machine: processors, memory and swap, run queue, stalls, OOM kills, swap traffic
sampleStorage every monitored disk against its budget, and the repository size from the same usage walk
bindSource remembers what a disk was measured against, and restarts its series when that changed (Scenario#16.05)
diskMetricsOf the gauge of one disk; an unreadable disk keeps its key and label
snapshotOf the snapshot the UI and the health verdict read
historyValues the columns a host reading and a storage sample add to the history

A host reading that failed now holds its unread value (CpuLoad.UNREAD, RamUsage.UNREAD, LoadAverage.UNREAD) instead of null. A gauge asks for its number, and the question whether the source was readable is answered once, where it is read. RunningAggregates names the running min/max/avg per metric that sample kept as a local map and a local function. DiskMetrics.unread is the gauge of a disk not measured, shared with the empty snapshot before the first sample.

Measured with PIT as the nightly build runs it:

Method Before After
SystemMetricsCollector.sample 35.36 3.04
diskMetricsOf — 4
RunningAggregates.record — 4
bindSource — 3
sampleStorage — 3
StorageSample.historyValues — 2
readHost — 1.13
snapshotOf — 1
HostReading.historyValues — 1

No new method of the collector scores above 4, and none is INCONCLUSIVE.

The gate maxMutationCrap falls from 36 to 35: the highest score left is BuildDefinition.applyTo at 34.

Additional Changes

  • Version 1.34.8 with its release note.

Follow-up PRs

  • BuildDefinition.applyTo (34) and CapacitySizing.cpu (33.17), each in its own PR.