Files
werkator/docs/prs/2026-10-04-PR#113-capacity-sizing-cpu.md
miandmhoennig f2f23780a2
werkator build succeeded after 01:59
werkator/deploy build succeeded after 00:07
CPU sizing advice asks "did the load queue" and "how busy" separately, its thresholds tested at their boundaries (v1.34.10) (#113)
## The Problem

After PR#112 `CapacitySizing.cpu` had the highest Mutation-CRAP score of Werkator: 33.17 at a complexity of 12 and a mutation score of 0.47.
Unlike the two before it, it failed mostly on its tests: three tests reached it, and none asked for its boundaries.
Whether the load queued in exactly 5 % of the quarter hours, whether a peak of exactly 40 % lets a thread go, whether a sustained 80 % is tight — a changed comparison passed every test.
The tight verdict had no test at all.

Its complexity of 12 was also two questions in one body, asked one after the other:
did the load queue for the threads, and if not, how busy were they.

## Non-Goals

- No change in the advice: the thresholds, the sizes, and the reasons stay as they are.

## The Scenarios

### Feature: CPU sizing advice

#### Scenario#113.01: Without threads or without any CPU history nothing is advised

So that a machine whose CPU was never sampled is not told to shrink.

- **Given** a snapshot with no threads, or a history without the `load1` and `cpuUsed` columns
- **When** the advice is computed
- **Then** the CPU verdict is `unknown`, with the reason `no CPU history`

##### Verified by

- [CapacitySizingUnitTest](../../src/test/kotlin/de/hoennig/werkator/metrics/CapacitySizingUnitTest.kt) — Scenario#113.01

#### Scenario#113.02: A load queuing in exactly 5 % of the quarter hours, or only reaching the threads, buys no thread

So that the waiting share is a limit to exceed, not to reach.

- **Given** two threads, and a load above them in exactly 48 of 960 quarter hours, or a load of exactly 2.0 throughout
- **When** the advice is computed
- **Then** the CPU fits

##### Verified by

- [CapacitySizingUnitTest](../../src/test/kotlin/de/hoennig/werkator/metrics/CapacitySizingUnitTest.kt) — Scenario#113.02

#### Scenario#113.03: An upgrade is sized to the load the quarter hours sustain

So that the advice names the size that stops the queue, not merely one thread more.

- **Given** two threads, and a load of 5.2 in 10 % of the quarter hours
- **When** the advice is computed
- **Then** the CPU is an upgrade to 6 threads, with the busiest use as its peak

##### Verified by

- [CapacitySizingUnitTest](../../src/test/kotlin/de/hoennig/werkator/metrics/CapacitySizingUnitTest.kt) — Scenario#113.03

#### Scenario#113.04: Threads go only when the peak stays below 40 % and a smaller size still holds it

So that a downsize is never advised at the boundary, and never to the size already booked.

- **Given** four threads with a peak of 1.5, four threads with a peak of 1.6, and two threads with a peak of 0.75
- **When** the advice is computed
- **Then** the first is a downsize to 2 with its reason, the second fits, the third is a downsize to 1

##### Verified by

- [CapacitySizingUnitTest](../../src/test/kotlin/de/hoennig/werkator/metrics/CapacitySizingUnitTest.kt) — Scenario#113.04

#### Scenario#113.05: A CPU that sustains 80 % of its threads is tight

So that the watch band starts at 80 %, and its advice holds the sustained use with headroom.

- **Given** two threads sustaining 1.6, 1.59, and 4.0
- **When** the advice is computed
- **Then** 1.6 is tight with 3 threads advised, 1.59 fits, and 4.0 asks for 5

##### Verified by

- [CapacitySizingUnitTest](../../src/test/kotlin/de/hoennig/werkator/metrics/CapacitySizingUnitTest.kt) — Scenario#113.05

#### Scenario#113.06: One CPU column alone is enough to advise

So that a history that lost one column still gets advice from the other.

- **Given** a history with only `load1`, and one with only `cpuUsed`
- **When** the advice is computed
- **Then** the first is judged with a peak of zero, the second without a waiting share

##### Verified by

- [CapacitySizingUnitTest](../../src/test/kotlin/de/hoennig/werkator/metrics/CapacitySizingUnitTest.kt) — Scenario#113.06

## The Solution

`cpu` asks its two questions in two methods:

| Method      | Question                                                                                        |
| ----------- | ----------------------------------------------------------------------------------------------- |
| `cpuWaited` | did the load exceed the threads in more than 5 % of the quarter hours? An upgrade, else nothing |
| `cpuBusy`   | with nothing queued, how busy were the threads? A downsize, tight, or fits                      |

`CpuUsage` carries what both need: the threads, the use per quarter hour, its peak, and the days of the basis.
`cpu` itself keeps the guard for a missing history.

Measured with PIT as the nightly build runs it:

| Method                     | Before | After |
| -------------------------- | ------ | ----- |
| `CapacitySizing.cpu`       | 33.17  | 6     |
| `CapacitySizing.cpuBusy`   | —      | 5.01  |
| `CapacitySizing.cpuWaited` | —      | 4.30  |

`cpu` itself kills 8 of 8 mutants now, and none of the three is `INCONCLUSIVE`.

The gate `maxMutationCrap` falls from 34 to 32: the highest score left is `ForgejoWorkflowExporter.settingNotes` at 31.63.

## Additional Changes

- Version 1.34.10 with its release note.

## Follow-up PRs

- `ForgejoWorkflowExporter.settingNotes` (31.63), `SystemHealth.assess$diskUtilization` (31.28), `GitService.authenticated` (30.12), `SystemMetricsView.from` (29.92), and `CapacitySizing.ram`/`column`/`disk` (26.87, 26.83, 21.19).

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

---------

Co-authored-by: mhoennig <michael@hoennig.de>
Reviewed-on: #113
2026-10-04 19:11:51 +02:00

5.5 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#112 CapacitySizing.cpu had the highest Mutation-CRAP score of Werkator: 33.17 at a complexity of 12 and a mutation score of 0.47. Unlike the two before it, it failed mostly on its tests: three tests reached it, and none asked for its boundaries. Whether the load queued in exactly 5 % of the quarter hours, whether a peak of exactly 40 % lets a thread go, whether a sustained 80 % is tight — a changed comparison passed every test. The tight verdict had no test at all.

Its complexity of 12 was also two questions in one body, asked one after the other: did the load queue for the threads, and if not, how busy were they.

Non-Goals

  • No change in the advice: the thresholds, the sizes, and the reasons stay as they are.

The Scenarios

Feature: CPU sizing advice

Scenario#113.01: Without threads or without any CPU history nothing is advised

So that a machine whose CPU was never sampled is not told to shrink.

  • Given a snapshot with no threads, or a history without the load1 and cpuUsed columns
  • When the advice is computed
  • Then the CPU verdict is unknown, with the reason no CPU history
Verified by

Scenario#113.02: A load queuing in exactly 5 % of the quarter hours, or only reaching the threads, buys no thread

So that the waiting share is a limit to exceed, not to reach.

  • Given two threads, and a load above them in exactly 48 of 960 quarter hours, or a load of exactly 2.0 throughout
  • When the advice is computed
  • Then the CPU fits
Verified by

Scenario#113.03: An upgrade is sized to the load the quarter hours sustain

So that the advice names the size that stops the queue, not merely one thread more.

  • Given two threads, and a load of 5.2 in 10 % of the quarter hours
  • When the advice is computed
  • Then the CPU is an upgrade to 6 threads, with the busiest use as its peak
Verified by

Scenario#113.04: Threads go only when the peak stays below 40 % and a smaller size still holds it

So that a downsize is never advised at the boundary, and never to the size already booked.

  • Given four threads with a peak of 1.5, four threads with a peak of 1.6, and two threads with a peak of 0.75
  • When the advice is computed
  • Then the first is a downsize to 2 with its reason, the second fits, the third is a downsize to 1
Verified by

Scenario#113.05: A CPU that sustains 80 % of its threads is tight

So that the watch band starts at 80 %, and its advice holds the sustained use with headroom.

  • Given two threads sustaining 1.6, 1.59, and 4.0
  • When the advice is computed
  • Then 1.6 is tight with 3 threads advised, 1.59 fits, and 4.0 asks for 5
Verified by

Scenario#113.06: One CPU column alone is enough to advise

So that a history that lost one column still gets advice from the other.

  • Given a history with only load1, and one with only cpuUsed
  • When the advice is computed
  • Then the first is judged with a peak of zero, the second without a waiting share
Verified by

The Solution

cpu asks its two questions in two methods:

Method Question
cpuWaited did the load exceed the threads in more than 5 % of the quarter hours? An upgrade, else nothing
cpuBusy with nothing queued, how busy were the threads? A downsize, tight, or fits

CpuUsage carries what both need: the threads, the use per quarter hour, its peak, and the days of the basis. cpu itself keeps the guard for a missing history.

Measured with PIT as the nightly build runs it:

Method Before After
CapacitySizing.cpu 33.17 6
CapacitySizing.cpuBusy — 5.01
CapacitySizing.cpuWaited — 4.30

cpu itself kills 8 of 8 mutants now, and none of the three is INCONCLUSIVE.

The gate maxMutationCrap falls from 34 to 32: the highest score left is ForgejoWorkflowExporter.settingNotes at 31.63.

Additional Changes

  • Version 1.34.10 with its release note.

Follow-up PRs

  • ForgejoWorkflowExporter.settingNotes (31.63), SystemHealth.assess$diskUtilization (31.28), GitService.authenticated (30.12), SystemMetricsView.from (29.92), and CapacitySizing.ram/column/disk (26.87, 26.83, 21.19).