The SSD's one moment at 90 % kept a 16 GiB quota on `upgrade` for the thirty days it stayed in the window; the HDD's three imports on three days read as "growing 0.41 GiB/day". A disk is now judged by the 95th percentile of the last week's quarter-hour maxima, the growth is the median of the daily deltas, and the projection starts from the level. The window maximum is still named and still vetoes a downsize. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Co-authored-by: mhoennig <michael@hoennig.de> Reviewed-on: #78
111 lines
6.6 KiB
Markdown
111 lines
6.6 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
|
|
|
|
The System page of the production host advised an upgrade for both disks on 2026-09-17, and both verdicts were wrong.
|
|
|
|
The SSD read `upgrade | peak 14.46 of 16.00 GiB (90 %) in 9 d`.
|
|
That peak was one quarter hour out of 885, the moment the old per-repository Gradle caches and the new shared one lay side by side during the migration of PR#76.
|
|
The disk held 8.4 GiB before and 11 GiB after.
|
|
A disk was sized by the highest quarter hour of the 30-day window, so that one moment would have kept the quota on `upgrade` for a month.
|
|
A one-off — an accident, a deliberate action of one afternoon — must not ask for a raise, least of all for thirty days.
|
|
|
|
The HDD read `upgrade | growing 0.41 GiB/day, 90 % of the 20.00 GiB group quota in 22 days` at 45 % of its quota.
|
|
The growth was three steps: a rootfs imported on 09.09, Testcontainers images on 11.09, another rootfs on 16.09, and six days of nothing in between.
|
|
The trend estimate of PR#42 took the smaller regression slope of the two window halves, which catches one step and not three.
|
|
The projection also started from the window peak, 8.97 GiB, while the disk held 7.05 GiB.
|
|
|
|
## Non-Goals
|
|
|
|
- No change to RAM and CPU sizing.
|
|
- No change to the health verdict; the live gauges keep showing the moment.
|
|
- No change to the thresholds (`UPGRADE_RATIO`, `WATCH_RATIO`, `DOWNSIZE_RATIO`) or to the bookable steps.
|
|
|
|
## The Scenarios
|
|
|
|
### Feature: a disk is sized by the level it holds and the growth it shows on most days
|
|
|
|
#### Background
|
|
|
|
- The *level* of a disk is the 95th percentile of the quarter-hour maxima of the last seven days — the percentile the CPU watch band has used since PR#42.
|
|
A quarter hour is a moment; a level is what the disk holds in more than a twentieth of the week.
|
|
- The *growth* of a disk is the median of its daily deltas over the last seven days.
|
|
A step is a delta on one day; a trend is a delta on most days.
|
|
- The *peak* stays the highest quarter hour of the whole window.
|
|
It is named in the reason, and it can veto a downsize, but it cannot ask for a raise.
|
|
|
|
#### Scenario#78.01: A moment is not a level
|
|
|
|
- **Given** a 16 GiB quota holding 8.4 GiB for ten days, and one quarter hour at 14.46 GiB
|
|
- **When** the disk is sized
|
|
- **Then** the verdict is `fits`
|
|
- **and** the reason names both: `holds 8.40 of 16.00 GiB (53 %), peak 14.46 (90 %) in 10 d`
|
|
- **But** a disk at 85 % throughout the last two days is `tight`, because a level held is a level
|
|
|
|
##### Verified by
|
|
|
|
- [CapacitySizingTest — one quarter hour at 90 % is a moment, not a level: the production SSD of 2026-09-17](../../src/test/kotlin/de/hoennig/werkator/metrics/CapacitySizingTest.kt)
|
|
- [CapacitySizingTest — a level held for two days is a level](../../src/test/kotlin/de/hoennig/werkator/metrics/CapacitySizingTest.kt)
|
|
|
|
#### Scenario#78.02: The window maximum is named and vetoes a downsize, nothing more
|
|
|
|
- **Given** a disk that held 93 % for two days a week ago and 50 % since
|
|
- **When** the disk is sized
|
|
- **Then** the verdict is `fits`, the reason names the peak
|
|
- **Given** a disk at 25 % with one quarter hour at 60 %
|
|
- **Then** the verdict is `fits`, not `downsize`: a moment cannot prove the slack a shrink needs
|
|
|
|
##### Verified by
|
|
|
|
- [CapacitySizingTest — a level the disk no longer holds is named, not judged](../../src/test/kotlin/de/hoennig/werkator/metrics/CapacitySizingTest.kt)
|
|
- [CapacitySizingTest — a moment vetoes a downsize the level alone would allow](../../src/test/kotlin/de/hoennig/werkator/metrics/CapacitySizingTest.kt)
|
|
|
|
#### Scenario#78.03: Steps are not growth
|
|
|
|
- **Given** a 20 GiB quota with steps of 1.9, 1.6 and 1.8 GiB on three of ten days and no change on the others
|
|
- **When** the disk is sized
|
|
- **Then** the verdict is `fits` and no growth is reported
|
|
|
|
##### Verified by
|
|
|
|
- [CapacitySizingTest — three steps in nine days are not growth: the production HDD of 2026-09-17](../../src/test/kotlin/de/hoennig/werkator/metrics/CapacitySizingTest.kt)
|
|
|
|
#### Scenario#78.04: Growth is projected from the level
|
|
|
|
- **Given** a disk growing 0.1 GiB on every day and one quarter hour at 88 % on the first day
|
|
- **When** the disk is sized
|
|
- **Then** the projection starts from the level, not from that moment, and the verdict is `fits`
|
|
- **Given** fewer than four daily deltas
|
|
- **Then** no growth is reported at all
|
|
|
|
##### Verified by
|
|
|
|
- [CapacitySizingTest — growth is what most days show, and it is projected from the level, not from a moment](../../src/test/kotlin/de/hoennig/werkator/metrics/CapacitySizingTest.kt)
|
|
- [CapacitySizingTest — a trend needs a majority of the week's days](../../src/test/kotlin/de/hoennig/werkator/metrics/CapacitySizingTest.kt)
|
|
- [CapacitySizingTest — disk growing towards 90 % within thirty days is an upgrade even below the peak threshold](../../src/test/kotlin/de/hoennig/werkator/metrics/CapacitySizingTest.kt)
|
|
|
|
## The Solution
|
|
|
|
`CapacitySizing.disk` no longer goes through `byPeak`.
|
|
It reads the level with `sustainedLevel` — `SUSTAINED_PERCENTILE` (0.95) of the maxima column of the last `DISK_LEVEL_DAYS` (7) — and judges `upgrade` and `tight` from it; the recommendation is the level with a third free, rounded to the bookable step.
|
|
The window maximum is still computed, named in the reason (`holds … , peak …`), and used for the downsize test as before: slack is proven against the worst moment, a raise against the level.
|
|
|
|
`growthPerDay` takes the level at the end of each of the last seven days from the quarter-hour averages, forms the daily deltas, and returns their median; fewer than `MIN_TREND_DAYS` (4) deltas or a negative median count as no growth.
|
|
The projection is the level plus thirty days of that growth, and the days left are counted from the level.
|
|
The two-halves regression and `MIN_TREND_POINTS` are gone.
|
|
|
|
CPU's two percentile calls now use the shared `SUSTAINED_PERCENTILE` instead of a literal.
|
|
|
|
The level is read from the last week rather than the whole window on purpose: what a disk held three weeks ago and has since been cleaned up is not what it holds, and the verdict should follow a cleanup within the week, not within the month.
|
|
|
|
## Open Questions
|
|
|
|
- A disk that grows on most days and is pruned once a month shows its growth phase as a trend; whether retention pruning needs to be accounted for is left until it shows on a host.
|
|
|
|
## Additional Changes
|
|
|
|
- Release notes and version bump to v1.21.1.
|
|
- `AGENTS.md` invariant and the architecture skill describe the disk rule.
|