> **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 - `docs/plan/20-build-duration-tracking.md` — the plan step this implements; its Scope Decision 2 is revised here, see *Additional Changes*. ## The Problem The duration of a single build has been recorded since the bash predecessor — `build-results.tsv` carried a `duration(MM:SS)` column, and today the History view, the JSON API and the CLI all show it. What has never existed is the comparison of those numbers with each other. A single number answers "how long did the last run take", never "is this build getting slower". A build that used to take six minutes and now takes nine is the first visible sign of trouble, usually well before anything fails, and at that point the per-build figure still looks perfectly ordinary. ## Non-Goals - No phase breakdown. Where the time inside a build goes is Step 14's subject; this is the whole execution time. - No stored time series. The durations are already recorded; deriving the trend on read keeps the result schema untouched and stays correct as retention prunes old runs. - No alerting. The trend is shown; pushing it to Gitea or a webhook is a separate decision. - No CLI output. `status --history` already prints the per-build duration, and a chart is not a thing the console needs. ## The Scenarios ### Feature: how long a build has been taking #### Background - Every `BuildResult` carries `branch`, the definition it ran (`build`), and the measured `duration` from `runningSince` — the queue wait is not part of it. - The trend window is the last 30 days. #### Scenario#49.01: The trend belongs to the build definition, not to the branch So that a series exists at all — a branch lives for days and leaves two or three measurements. - **Given** builds of the definition `default` on `main` and on three feature branches - **Then** they form one trend, pooled, not four - **and** a second definition `jvm` forms its own trend - **and** the default definition is listed first, the named ones after it in alphabetical order - **Given** the points of a trend - **Then** they are chronological, oldest first, and the latest point is the newest build ##### Verified by - [BuildDurationTrendsTest](../../src/test/kotlin/de/hoennig/werkator/build/BuildDurationTrendsTest.kt) - [BuildsApiControllerTest](../../src/test/kotlin/de/hoennig/werkator/server/BuildsApiControllerTest.kt) #### Scenario#49.02: The window is described by its median So that one unusual run does not colour a whole month. - **Given** a window of build times - **Then** the trend reports the median, the fastest and the slowest run - **When** a single build takes ten times as long as the others - **Then** the median is unmoved — which an average would not be - **Given** the History page - **Then** it shows one card per definition, with the series as a line, the median as a reference, and the figures beside it - **and** the branches view shows none: the trend belongs to the history ##### Verified by - [BuildDurationTrendsTest](../../src/test/kotlin/de/hoennig/werkator/build/BuildDurationTrendsTest.kt) - [UiControllerTest](../../src/test/kotlin/de/hoennig/werkator/server/UiControllerTest.kt) - [UiViewsTest](../../src/test/kotlin/de/hoennig/werkator/server/UiViewsTest.kt) #### Scenario#49.03: A build getting slower says so in words So that the page states the finding instead of leaving it to be read off a line. - **Given** a latest build a quarter or more above the median - **Then** the card is marked `slower` and turns amber - **Given** a latest build within five percent of the median - **Then** the card reads *at the median* — ordinary scatter is not a finding - **Given** any other distance - **Then** it is named as a percentage over or under the median, worded identically by the server and by the script ##### Verified by - [BuildDurationTrendsTest](../../src/test/kotlin/de/hoennig/werkator/build/BuildDurationTrendsTest.kt) - [UiViewsTest](../../src/test/kotlin/de/hoennig/werkator/server/UiViewsTest.kt) #### Scenario#49.04: Only what actually measures a build time counts - **Given** a failed, interrupted or cancelled build - **Then** it is left out — its duration is how long it took to fail, which answers a different question - **Given** a build that never left the queue, or one older than the window - **Then** it is left out as well - **Given** a definition with fewer than five runs in the window - **Then** it has no trend at all, and the page says why ##### Verified by - [BuildDurationTrendsTest](../../src/test/kotlin/de/hoennig/werkator/build/BuildDurationTrendsTest.kt) #### Scenario#49.05: A private repository has no visible shape - **Given** a private repository and a visitor without the control token - **When** the trend is requested - **Then** the answer is empty, not redacted — how often a build runs, when, and how long it takes is itself what must not be learned ##### Verified by - [BuildsApiControllerTest](../../src/test/kotlin/de/hoennig/werkator/server/BuildsApiControllerTest.kt) ## The Solution `BuildDurationTrends.of` is the whole computation, pure over the recorded history and a `now`: filter to successful builds with a measured duration inside the window, group by definition, sort by time, and describe each group by its median, min, max, and the latest run's ratio to the median. The level (`ok`/`slower`) is decided there and travels through the API, so the server-rendered card and the script-rendered one can never disagree about which build is worth looking at. The median rather than the average is the load-bearing choice. Pooling branches into one line means a branch that redefined its build, or a run against a cold cache, lands in the same series as everything else; an average would carry such a run for weeks, while the middle value shrugs it off. That is what makes the pooling affordable. The page draws the series with the x axis counting builds, not time. The question is whether the build is getting slower from run to run; spacing by time would squeeze a busy week against the right-hand edge and stretch a quiet one across the card. ## Open Questions - The threshold is a quarter above the median, and five percent is the band that counts as *at the median*. Both are guesses at what feels like a finding; only use will tell. - The window is a constant, not a config key. A repository that builds twice a month has no trend under it — visible as the "not enough runs" note, which is at least honest about why. - A build that has been slow for the whole window looks normal, because the median moved with it. Catching a slow drift needs a comparison against a period further back, which this does not do. ## Additional Changes - `docs/plan/20-build-duration-tracking.md`: Scope Decision 2 is replaced. It grouped by `name`, giving every branch its own line; the reason it gave — a lagging branch pollutes a shared average — is now answered by the median instead of by the grouping. The step is ticked off in `docs/plan/README.md`. - Version 1.7.0 and its release-notes entry. It assumes PR#48 (build timeout, 1.6.0) lands first; if the order changes, the number does.