> Historic PR-documentation is not maintained along with new PRs — treat it as a snapshot, not as current documentation. ## The Problem Four methods of the sizing advice are above the Kotlin target of 15, measured by the nightly mutation build of 2026-10-05: | Method | Mutation-CRAP | Complexity | Mutation score | |-------------------------|---------------|------------|----------------| | `CapacitySizing.ram` | 26.87 | 15 | 0.63 | | `CapacitySizing.column` | 26.83 | 6 | 0.17 | | `CapacitySizing.disk` | 21.19 | 16 | 0.73 | | `UiFormats.adviceLabel` | 15.55 | 5 | 0.25 | `ram` and `disk` decide every verdict of their resource in one method each, as `cpu` did before PR#113. `column` and `adviceLabel` are short, but their tests never told the alternatives apart: every test tier carried the same values as averages and as maxima, and no test asked for a `watch` chip. ## Non-Goals - No change in behaviour: every verdict, size and reason stays as it was. - The other groups above 15 follow in their own PRs. ## The Scenarios ### Feature: the sizing reads the right column and words every verdict #### Scenario#123.01: RAM is sized from the quarter-hour maxima where the tier has them, else from the averages So that a moment of overflow, which is what RAM is sized against, is never averaged away, and a tier without maxima still gets advice. - **Given** ten days of history whose RAM averages stay at 2.0 GiB - **When** the maxima hold one quarter hour at 4.4 GiB - **Then** the RAM peak is 4.4 GiB - **When** the tier has no maxima for RAM, or an empty column - **Then** the RAM peak is 2.0 GiB, from the averages - **When** the maxima hold only gaps - **Then** there is no peak at all, and a gap never counts as zero ##### Verified by - [CapacitySizingUnitTest](../../src/test/kotlin/de/hoennig/werkator/metrics/CapacitySizingUnitTest.kt) `RAM is sized from the quarter-hour maxima where the tier has them, else from the averages (Scenario#123.01)` #### Scenario#123.02: the RAM and disk verdicts stay as they were So that splitting the two methods changes no advice a host has been given. - **Given** the RAM and disk histories of the existing scenarios - **When** the advice is derived - **Then** each verdict, recommended size and reason is the same as before ##### Verified by - [CapacitySizingUnitTest](../../src/test/kotlin/de/hoennig/werkator/metrics/CapacitySizingUnitTest.kt): the tests of Scenario#30, #42, #43, #58, #78 and #79, unchanged #### Scenario#123.03: every verdict has its chip, threads whole and GiB with two decimals So that the server-rendered chips read like the ones `werkator.js` renders after the first poll. - **Given** an advice with the verdict `tight`, `downsize` or `upgrade` - **When** its chip is worded - **Then** it reads `watch, raise to …`, `downsize to …` or `upgrade to …` - **and** threads are whole, a fraction cut off, and GiB carry two decimals ##### Verified by - [UiViewsUnitTest](../../src/test/kotlin/de/hoennig/werkator/server/UiViewsUnitTest.kt) `every verdict has its chip, threads whole and GiB with two decimals, as werkator.js words them (Scenario#123.03)`, and `advice chips read like werkator.js adviceLabel (Scenario#30.03)` ## The Solution The pattern of PR#113 for the CPU, applied to RAM and disk. `ram` checks that there is a history, builds a `RamUsage` (peak, usable and booked size, step, stalled share, fill line) and asks two steps: - `ramStalled` raises when memory stalled in enough quarter hours, `null` otherwise; - `ramUnstalled` decides between a downsize and fits. `stalledShare` counts the stalled quarter hours, `null` without a pressure history. `disk` builds a `DiskUsage` (level, peak, total, step, whether to ask, held line) and asks four steps in order, each `null` when it does not apply: - `diskFull`: the level reaches the upgrade ratio; - `diskGrowing`: the projected level reaches it within the horizon; - `diskTight`: the level is in the watch band and needs more than the total; - `diskSlack`: a downsize when the window maximum leaves room, else fits. `column` reads `tier.values[key].orEmpty()` instead of an elvis to an empty list. `adviceLabel` kept three survivors after the new tests: the `!!` on the recommended size compiles to a `checkNotNull` call, and removing it fails the same way one line later. The size is now worded by `adviceSize`, which reads a missing size as `werkator.js` renders it, `0 threads` or `n/a GiB`. No verdict but `fits` and `collecting` comes without a size, so this changes nothing a reader sees. A local PIT run of `CapacitySizing` and `UiFormats` afterwards: | Method | Mutation-CRAP | Complexity | Mutation score | |-------------------------------|---------------|------------|----------------| | `CapacitySizing.ram` | 6.56 | 6 | 0.75 | | `CapacitySizing.ramStalled` | 5.40 | 4 | 0.56 | | `CapacitySizing.ramUnstalled` | 7.33 | 6 | 0.67 | | `CapacitySizing.stalledShare` | 3.33 | 3 | 0.67 | | `CapacitySizing.column` | 6 | 6 | 1.00 | | `CapacitySizing.disk` | 10.27 | 9 | 0.75 | | `CapacitySizing.diskFull` | 2.06 | 2 | 0.75 | | `CapacitySizing.diskGrowing` | 4.20 | 4 | 0.77 | | `CapacitySizing.diskTight` | 3.33 | 3 | 0.67 | | `CapacitySizing.diskSlack` | 5.20 | 5 | 0.80 | | `UiFormats.adviceLabel` | 5 | 5 | 1.00 | | `UiFormats.adviceSize` | 3 | 3 | 1.00 | `UiFormats.repoStatusLine` (16.03) is the one method of these classes still above 15; it belongs to the build lists and follows in its own PR. The project maximum stays `NginxProxyManager.resolveSettings` at 27.44, so the gate stays at 28. ## Additional Changes - Version 1.37.4 with its entry in `releases.html`. ## Prerequisite PRs - PR#113: the same split for the CPU advice. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: mhoennig <michael@hoennig.de> Reviewed-on: #123
125 lines
6.4 KiB
Markdown
125 lines
6.4 KiB
Markdown
# PR#123: RAM and Disk Sizing Advice Split into Their Steps
|
|
|
|
> **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
|
|
|
|
Four methods of the sizing advice are above the Kotlin target of 15, measured by the nightly mutation build of 2026-10-05:
|
|
|
|
| Method | Mutation-CRAP | Complexity | Mutation score |
|
|
|-------------------------|---------------|------------|----------------|
|
|
| `CapacitySizing.ram` | 26.87 | 15 | 0.63 |
|
|
| `CapacitySizing.column` | 26.83 | 6 | 0.17 |
|
|
| `CapacitySizing.disk` | 21.19 | 16 | 0.73 |
|
|
| `UiFormats.adviceLabel` | 15.55 | 5 | 0.25 |
|
|
|
|
`ram` and `disk` decide every verdict of their resource in one method each, as `cpu` did before PR#113.
|
|
`column` and `adviceLabel` are short, but their tests never told the alternatives apart: every test tier carried the same values as averages and as maxima, and no test asked for a `watch` chip.
|
|
|
|
## Non-Goals
|
|
|
|
- No change in behaviour: every verdict, size and reason stays as it was.
|
|
- The other groups above 15 follow in their own PRs.
|
|
|
|
## The Scenarios
|
|
|
|
### Feature: the sizing reads the right column and words every verdict
|
|
|
|
#### Scenario#123.01: RAM is sized from the quarter-hour maxima where the tier has them, else from the averages
|
|
|
|
So that a moment of overflow, which is what RAM is sized against, is never averaged away, and a tier without maxima still gets advice.
|
|
|
|
- **Given** ten days of history whose RAM averages stay at 2.0 GiB
|
|
- **When** the maxima hold one quarter hour at 4.4 GiB
|
|
- **Then** the RAM peak is 4.4 GiB
|
|
- **When** the tier has no maxima for RAM, or an empty column
|
|
- **Then** the RAM peak is 2.0 GiB, from the averages
|
|
- **When** the maxima hold only gaps
|
|
- **Then** there is no peak at all, and a gap never counts as zero
|
|
|
|
##### Verified by
|
|
|
|
- [CapacitySizingUnitTest](../../src/test/kotlin/de/hoennig/werkator/metrics/CapacitySizingUnitTest.kt) `RAM is sized from the quarter-hour maxima where the tier has them, else from the averages (Scenario#123.01)`
|
|
|
|
#### Scenario#123.02: the RAM and disk verdicts stay as they were
|
|
|
|
So that splitting the two methods changes no advice a host has been given.
|
|
|
|
- **Given** the RAM and disk histories of the existing scenarios
|
|
- **When** the advice is derived
|
|
- **Then** each verdict, recommended size and reason is the same as before
|
|
|
|
##### Verified by
|
|
|
|
- [CapacitySizingUnitTest](../../src/test/kotlin/de/hoennig/werkator/metrics/CapacitySizingUnitTest.kt): the tests of Scenario#30, #42, #43, #58, #78 and #79, unchanged
|
|
|
|
#### Scenario#123.03: every verdict has its chip, threads whole and GiB with two decimals
|
|
|
|
So that the server-rendered chips read like the ones `werkator.js` renders after the first poll.
|
|
|
|
- **Given** an advice with the verdict `tight`, `downsize` or `upgrade`
|
|
- **When** its chip is worded
|
|
- **Then** it reads `watch, raise to …`, `downsize to …` or `upgrade to …`
|
|
- **and** threads are whole, a fraction cut off, and GiB carry two decimals
|
|
|
|
##### Verified by
|
|
|
|
- [UiViewsUnitTest](../../src/test/kotlin/de/hoennig/werkator/server/UiViewsUnitTest.kt) `every verdict has its chip, threads whole and GiB with two decimals, as werkator.js words them (Scenario#123.03)`, and `advice chips read like werkator.js adviceLabel (Scenario#30.03)`
|
|
|
|
## The Solution
|
|
|
|
The pattern of PR#113 for the CPU, applied to RAM and disk.
|
|
|
|
`ram` checks that there is a history, builds a `RamUsage` (peak, usable and booked size, step, stalled share, fill line) and asks two steps:
|
|
|
|
- `ramStalled` raises when memory stalled in enough quarter hours, `null` otherwise;
|
|
- `ramUnstalled` decides between a downsize and fits.
|
|
|
|
`stalledShare` counts the stalled quarter hours, `null` without a pressure history.
|
|
|
|
`disk` builds a `DiskUsage` (level, peak, total, step, whether to ask, held line) and asks four steps in order, each `null` when it does not apply:
|
|
|
|
- `diskFull`: the level reaches the upgrade ratio;
|
|
- `diskGrowing`: the projected level reaches it within the horizon;
|
|
- `diskTight`: the level is in the watch band and needs more than the total;
|
|
- `diskSlack`: a downsize when the window maximum leaves room, else fits.
|
|
|
|
`column` reads `tier.values[key].orEmpty()` instead of an elvis to an empty list.
|
|
|
|
`adviceLabel` kept three survivors after the new tests: the `!!` on the recommended size compiles to a `checkNotNull` call, and removing it fails the same way one line later.
|
|
The size is now worded by `adviceSize`, which reads a missing size as `werkator.js` renders it, `0 threads` or `n/a GiB`.
|
|
No verdict but `fits` and `collecting` comes without a size, so this changes nothing a reader sees.
|
|
|
|
A local PIT run of `CapacitySizing` and `UiFormats` afterwards:
|
|
|
|
| Method | Mutation-CRAP | Complexity | Mutation score |
|
|
|-------------------------------|---------------|------------|----------------|
|
|
| `CapacitySizing.ram` | 6.56 | 6 | 0.75 |
|
|
| `CapacitySizing.ramStalled` | 5.40 | 4 | 0.56 |
|
|
| `CapacitySizing.ramUnstalled` | 7.33 | 6 | 0.67 |
|
|
| `CapacitySizing.stalledShare` | 3.33 | 3 | 0.67 |
|
|
| `CapacitySizing.column` | 6 | 6 | 1.00 |
|
|
| `CapacitySizing.disk` | 10.27 | 9 | 0.75 |
|
|
| `CapacitySizing.diskFull` | 2.06 | 2 | 0.75 |
|
|
| `CapacitySizing.diskGrowing` | 4.20 | 4 | 0.77 |
|
|
| `CapacitySizing.diskTight` | 3.33 | 3 | 0.67 |
|
|
| `CapacitySizing.diskSlack` | 5.20 | 5 | 0.80 |
|
|
| `UiFormats.adviceLabel` | 5 | 5 | 1.00 |
|
|
| `UiFormats.adviceSize` | 3 | 3 | 1.00 |
|
|
|
|
`UiFormats.repoStatusLine` (16.03) is the one method of these classes still above 15; it belongs to the build lists and follows in its own PR.
|
|
The project maximum stays `NginxProxyManager.resolveSettings` at 27.44, so the gate stays at 28.
|
|
|
|
## Additional Changes
|
|
|
|
- Version 1.37.4 with its entry in `releases.html`.
|
|
|
|
## Prerequisite PRs
|
|
|
|
- PR#113: the same split for the CPU advice.
|
|
|
|
🤖 Generated with [Claude Code](https://claude.com/claude-code)
|