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

79 lines
4.3 KiB
Markdown

> **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
- [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.