222 lines
13 KiB
Markdown
222 lines
13 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.
|
|
|
|
## Related Links
|
|
|
|
- [PR#40](2026-09-09-PR%2340-system-page-charts-and-disk-usage.md) — brought the advice forward to one day of history, which is what made these three defects visible.
|
|
|
|
## The Problem
|
|
|
|
The first day of sizing advice on the production host asked for roughly a hundred Euro a month of hardware that nobody needs.
|
|
|
|
**CPU: five threads on a machine that copes with two.**
|
|
`CapacitySizing` read the history through one helper that prefers the per-bucket *maxima* over the averages.
|
|
For RAM and disk that is right — a single moment of overflow is the failure being sized against.
|
|
For CPU it made the number a peak of peaks: the highest one-minute run queue inside each quarter hour, and then the 95th percentile of those.
|
|
A compiler that queues four tasks for a minute read as a standing demand for four threads.
|
|
The same history, read as sustained load, asks for three.
|
|
|
|
**RAM: 6.5 GiB, a size that cannot be ordered.**
|
|
The recommendation was the peak divided by a 70 % headroom target, rounded up to half a GiB, and expressed in *visible* memory.
|
|
Memory is bought in whole GiB, and what `/proc/meminfo` reports is a little less than what was booked because the host keeps a slice — 4.80 GiB visible is 5 GiB bought.
|
|
So the advice named neither a bookable size nor the size actually in use.
|
|
|
|
**Disk: an order for 62 GiB of quota because a directory was unpacked once.**
|
|
The growth rate was the difference between the first and the last point of the window, divided by its length.
|
|
The production host's SSD series is flat at 4.89 GiB, steps once to 6.29 when a build environment is unpacked, and is flat again — which that formula reports as "growing 1.21 GiB/day" and, extrapolated over thirty days, as a need for 62 GiB.
|
|
The HDD told the same story from a 0.6 GiB step.
|
|
Both were phrased as "upgrade to N GiB" without naming that the budget is a group quota, which reads as an order for hardware.
|
|
|
|
**And nothing at all between 80 % and 90 %.**
|
|
A resource sitting at 81 % of its budget was reported as `fits`, in the same green as one at 20 %, until it crossed 90 % and turned red.
|
|
The gauges have had a `watch` band at 80 % since the beginning; the advice had no equivalent, so the only thing it could say about a budget worth planning to raise was nothing.
|
|
|
|
**And nothing to do with the answer.**
|
|
Where the advice is correct, acting on it means writing a mail to a service desk and transcribing four numbers by hand.
|
|
|
|
## Non-Goals
|
|
|
|
- No change to the thresholds themselves: 90 % is still an upgrade, 80 % is the watch band the gauges already use, below 40 % is still a downsize, and CPU still needs more than 5 % of the quarter hours waiting to be an upgrade.
|
|
- No change to what is sampled or stored. All three corrections are in how the existing history is read.
|
|
- No swap in the sizing. It never was in it, and the question whether swap pressure should raise the RAM recommendation is left open.
|
|
- The page never sends a mail and never talks to a hosting API. It composes text and hands it to the user's own mail client.
|
|
- Only one hosting is recognised, because only one is deployed to. There is no plugin point for others and no guessing: an unrecognised environment simply offers nothing.
|
|
|
|
## The Scenarios
|
|
|
|
### Feature: sizing CPU
|
|
|
|
#### Background
|
|
|
|
- The quarter-hour tier keeps an average *and* a maximum per bucket; `load1` is the one-minute run queue, `cpuUsed` the busy threads.
|
|
|
|
#### Scenario#42.01: CPU is sized from the load a quarter hour sustained
|
|
|
|
So that a brief queue during a compile does not buy a thread.
|
|
|
|
- **Given** a history whose quarter-hour maxima of `load1` reach 4.6 on a two-thread machine
|
|
- **and** whose quarter-hour averages stay near 2.2
|
|
- **When** the advice is computed
|
|
- **Then** both the share of waiting quarter hours and the recommended size come from the averages
|
|
- **and** the recommendation is three threads, not five
|
|
|
|
##### Verified by
|
|
|
|
- [CapacitySizingTest](../../src/test/kotlin/de/hoennig/werkator/metrics/CapacitySizingTest.kt)
|
|
|
|
### Feature: sizing RAM
|
|
|
|
#### Scenario#42.02: RAM is advised in the size it is bought in
|
|
|
|
So that the recommendation names something that can be ordered.
|
|
|
|
- **Given** 4.80 GiB of visible memory and a peak of 4.40 GiB
|
|
- **When** the advice is computed
|
|
- **Then** the current size is reported as 5 GiB — the visible total rounded up to whole GiB
|
|
- **and** the difference, 0.20 GiB, is treated as the reserve the host keeps
|
|
- **and** the recommendation is the peak plus 20 % headroom plus that reserve, rounded up to whole GiB: 6 GiB
|
|
- **and** the verdict still compares the peak against the *visible* total, because that is what a process can allocate
|
|
|
|
##### Verified by
|
|
|
|
- [CapacitySizingTest](../../src/test/kotlin/de/hoennig/werkator/metrics/CapacitySizingTest.kt)
|
|
|
|
### Feature: sizing a disk
|
|
|
|
#### Scenario#42.03: A one-off step is not growth
|
|
|
|
So that unpacking a build environment does not order a month of it.
|
|
|
|
- **Given** a disk series that is flat, steps up once, and is flat again
|
|
- **When** the advice is computed
|
|
- **Then** the growth rate used is the smaller of the two half-window slopes, which the step leaves at zero
|
|
- **and** the verdict falls back to the peak against the total
|
|
- **Given** less than seven days of history
|
|
- **Then** no trend is extrapolated at all, whatever the points suggest
|
|
- **Given** a series that grows steadily across the whole window, over at least a week
|
|
- **Then** the trend is used as before, and the reason names the budget — "90 % of the 8.00 GiB group quota in 12 days"
|
|
|
|
##### Verified by
|
|
|
|
- [CapacitySizingTest](../../src/test/kotlin/de/hoennig/werkator/metrics/CapacitySizingTest.kt)
|
|
|
|
### Feature: the disk card
|
|
|
|
#### Scenario#42.04: The breakdown button sits where the average sits
|
|
|
|
So that the three gauge cards have one shape.
|
|
|
|
- **Given** a disk card with a breakdown
|
|
- **Then** its button is a pale pie-chart icon in the row of the current value, the slot where CPU and RAM show their average
|
|
- **and** it takes the accent colour on hover and on keyboard focus
|
|
|
|
### Feature: the watch band
|
|
|
|
#### Scenario#42.05: A budget in the watch band is a planned raise, not an alarm
|
|
|
|
So that a quota can be raised before it is urgent.
|
|
|
|
- **Given** a disk whose peak is 81 % of its budget
|
|
- **When** the advice is computed
|
|
- **Then** the verdict is `tight`
|
|
- **and** the recommended size leaves a third free: 10 GiB for a 6.5 GiB peak
|
|
- **and** the chip reads "SSD: watch, raise to 10.00 GiB" in the same yellow the gauges use for watch
|
|
- **Given** a resource in the watch band whose recommended size is the one already booked
|
|
- **Then** the verdict stays `fits`, because there is nothing to raise
|
|
- **Given** a CPU whose busiest single quarter hour was fully used but which sustains a fifth of that
|
|
- **Then** the verdict stays `fits`: the watch band asks the 95th percentile of the sustained load, not the peak
|
|
|
|
##### Verified by
|
|
|
|
- [CapacitySizingTest](../../src/test/kotlin/de/hoennig/werkator/metrics/CapacitySizingTest.kt)
|
|
- [UiViewsTest](../../src/test/kotlin/de/hoennig/werkator/server/UiViewsTest.kt)
|
|
|
|
### Feature: ordering what was recommended
|
|
|
|
#### Background
|
|
|
|
- A Hostsharing Managed Webspace user lives at `/home/pacs/<package>/users/<user>`; resources are ordered by mail from a service desk, not through an API.
|
|
|
|
#### Scenario#42.06: The hosting is recognised, or nothing is offered
|
|
|
|
So that a button that cannot work is never shown.
|
|
|
|
- **Given** a home directory of the shape `/home/pacs/mih09/users/werkator` and a hostname `h68.hostsharing.net`
|
|
- **Then** the hosting is `hostsharing`, the webspace `mih09` and the machine `h68` — the host's own name, not the domain it answers for
|
|
- **Given** any other home directory, or a hostname that cannot be read
|
|
- **Then** there is no hosting, `GET /api/system` reports `null`, and the page shows no order button
|
|
|
|
#### Scenario#42.08: A Container Server is not a Managed Webspace
|
|
|
|
So that the wrong product is never offered the wrong order.
|
|
|
|
- **Given** an instance running as the account `tallyman`
|
|
- **Then** there is no hosting, whatever its home directory looks like
|
|
- **because** a Managed Webspace account is always the package itself or the package with a suffix — `mih09`, `mih09-werkator` — and `tallyman` can be neither
|
|
|
|
##### Verified by
|
|
|
|
- [HostingEnvironmentTest](../../src/test/kotlin/de/hoennig/werkator/server/HostingEnvironmentTest.kt)
|
|
|
|
##### Verified by
|
|
|
|
- [HostingEnvironmentTest](../../src/test/kotlin/de/hoennig/werkator/server/HostingEnvironmentTest.kt)
|
|
|
|
#### Scenario#42.07: The order is chosen, then written as a mail
|
|
|
|
So that the customer decides what to order and sees the text before it is sent.
|
|
|
|
- **Given** an advice of "CPU 3 threads", "RAM 6 GB", "SSD quota 10 GiB" and "HDD quota 5 GiB"
|
|
- **When** the order dialog is opened
|
|
- **Then** each of the four is a checkbox in German
|
|
- **and** the three raises are ticked, the shrink is not
|
|
- **When** the ticked ones are written
|
|
- **Then** the mail client opens with a mail to the service desk
|
|
- **and** its subject is "Auftrag für mih09 auf h68"
|
|
- **and** its body names the webspace and the machine and lists the ticked changes as bullets
|
|
- **and** nothing is sent by the page itself
|
|
|
|
##### Verified by
|
|
|
|
- the pure `orderItems`/`orderMail` in [werkator.js](../../src/main/resources/static/werkator.js) — no JS test harness exists in this repository; verified in the browser against a seeded advice, light and dark
|
|
|
|
## The Solution
|
|
|
|
`averageColumn` joins `column` in `CapacitySizing`: the same shape, but always the averages, and CPU is the only caller.
|
|
The two are documented against each other, because which one a resource uses is the whole difference between sizing for a peak and sizing for a load.
|
|
|
|
RAM gets its own function instead of sharing `byPeak` with the disks.
|
|
It rounds the visible total up to whole GiB to get both the booked size and the reserve, and returns the advice in booked GiB while judging the verdict on the visible total.
|
|
|
|
`growthPerDay` replaces the first-to-last difference with least squares over each half of the window, taking the smaller slope.
|
|
A genuine trend shows in both halves; a step shows in one.
|
|
`disk()` additionally refuses to extrapolate below `RELIABLE_DAYS`, and takes the budget's name so the reason can say what would have to be raised.
|
|
|
|
The headroom target moves from 70 % to 80 % for CPU and RAM, because every recommendation is now rounded up to a bookable size and the two compounded: 30 % headroom plus the rounding bought a whole extra step.
|
|
Disks keep the 70 %, as `DISK_TARGET_RATIO`: memory pressure passes, a full filesystem does not, and what a disk holds only grows between prunings.
|
|
|
|
`SizingVerdict.TIGHT` fills the gap between "fits" and "upgrade" wherever a ratio drives the verdict — RAM and disk from their peak, CPU from the 95th percentile of its sustained utilisation, never from its busiest quarter hour.
|
|
It is only reached when there is actually something to raise, so a resource whose recommended size is the one already booked stays `fits`.
|
|
The chip renders in the amber the gauges use for `watch`, and `UiFormats.adviceLabel` and its JavaScript twin both say "watch, raise to N".
|
|
|
|
On the page the breakdown button moves into the value row as an icon.
|
|
|
|
`HostingEnvironments.detect` answers where the instance runs, and `null` everywhere it cannot act — which is everywhere but a Managed Webspace.
|
|
Two conditions have to hold together: the home directory has the webspace shape, *and* the account name is the package found in that path, or that package with a suffix.
|
|
The path alone is not proof, because Hostsharing's Container Server is a different product that grows differently and must not be offered a webspace order; its account is `tallyman`, which no webspace account can be.
|
|
It is read once per process, because neither value changes while the server runs, and served as `hosting` in `GET /api/system`; `CapacityView.orderable` combines it with "is there anything to change" so the button renders correctly on the first paint rather than appearing on the first poll.
|
|
`orderItems` and `orderMail` in `werkator.js` are pure: the first turns the polled advice into German lines with a default tick, the second turns the ticked ones into subject, body and a `mailto:` link.
|
|
The mail is German because the desk that reads it is; the dialog around it stays in the page's language.
|
|
|
|
## Open Questions
|
|
|
|
- The half-window rule rejects a step, and it also halves the sensitivity to a trend that only started in the second half. With a week's minimum window that seemed the better trade.
|
|
- Whether sustained swap use should raise the RAM recommendation. It does not today, and on the production host 2 GiB of swap are in use while RAM is advised from its own peak alone.
|
|
|
|
## Additional Changes
|
|
|
|
- A CPU is counted in *threads* throughout the UI, the API and the advice, not in cores: `availableProcessors` reports hardware threads, and a Managed Webspace is billed by them. `CapacitySizing.CPU_UNIT` is the one place the word lives.
|
|
- Version 1.4.1 and its release-notes entry, since this is what gets deployed.
|
|
- The `buildenv` slice of the breakdown donut gets a colour of its own; it and `other` were both grey and, being the two largest slices on the production host, sat next to each other indistinguishably.
|