Filed because REPO_POLICIES.md requires it: "A suite between 20 and 60 seconds is still green, but the overage must be filed as an improvement bug against that repo."
Not milestoned 1.0.0. On the reading I think is correct the repo is inside the hard cap and merely past the target, which policy explicitly calls green. The fix is also a real piece of work rather than a tuning change. But there is a genuine ambiguity about which quantity the cap governs, and if you read it the other way the repo is in breach — so the numbers are below and the call is yours if you disagree with my scoping.
Measured
By the review of #195, on next, cache-defeated builder stage:
quantity
measured
RUN make test layer, cold
62.8 s (a second observer saw 68.4 s)
of which: per-package test execution
46.0 s
of which: cold -race compile of the whole tree
roughly 17 s
make test on the host, warm cache
roughly 22 s
The ambiguity
The policy says "make test must complete in under 60 seconds. That is the hard cap, and a suite that exceeds it fails."
Read as test execution (46.0 s): inside the 60 s cap, past the 20 s target. Green, file an improvement bug — this issue.
Read as what CI actually pays (62.8 s): past the hard cap, and the suite fails.
The reviewer's view, which I share: the layer duration is not a clean proxy, because it bundles a cold -race compile of the entire tree that the cap was probably not written to include. But it is the number CI pays on every run, so it is not obviously the wrong thing to measure either. Both readings agree the repo is past the 20 s target.
I have scoped this as the improvement bug the 20-60 s band requires. If you read the cap as covering the whole invocation, this is a 1.0.0 blocker instead and should be milestoned.
Where it comes from
Almost entirely one package. internal/handlers was the slowest in all 16 measured runs, at 4-5x the next: 16.7 s ambient, 41.3 s at GOMAXPROCS=4, 49.0 s under deliberate host load. Second place is unstable — internal/ciscript at 7.1-7.2 s with cores to spare, internal/delivery at up to 18.5 s without. Eight of fifteen packages run in 1.0-1.3 s.
The concentration is a consequence of where the tested behaviour lives: internal/handlers carries the bulk of the log-bounding suite added across #146, #176 and #178, each of which sweeps seven or more fills across two slog handlers. That is real coverage, not waste.
It also has a second cost, noted on #195: because go test applies -timeout uniformly per package, one slow package sets the budget for all of them. The 90 s backstop is over 100x the runtime of the smallest packages, so a genuine deadlock in a one-second package now hangs for 90 s. Reducing the spread fixes that too.
Definition of done
Not prescribed — the approach is the substance of this issue. Options worth weighing, with the obvious trap named:
Reduce the sweep breadth where a fill or handler adds no discrimination. Only where it genuinely adds none — several of those fills are load-bearing and documented as such (the C0 control catches raw-byte budgeting on its own; the astral non-printable is the only case the text handler charges differently). Removing coverage to hit a time budget is the failure mode caught and rejected in #188 and it must not happen here.
Parallelise. Many of these subtests are independent table cases that do not call t.Parallel(). Likely the largest win for the least risk.
Split internal/handlers. Shrinks the spread and lets the uniform -timeout come back down.
Argue it is correct as it stands and that the policy target is wrong for a repo of this shape. A legitimate answer, but it belongs upstream in https://git.eeqj.de/sneak/prompts, not as a local exception.
Whichever is chosen:
State the before and after for total make test, for internal/handlers, and for the slowest remaining package, each measured on the tree being shipped.
Demonstrate that coverage did not shrink. If any case was removed, show by mutation that the property it guarded is still caught by something else.
If the -timeout in script/test can come back down as a result, do that in the same change and say what the new margin is.
Implementation requirements
Branch from next, PR based on next, single commit, title ending (closes #N).
Run make bootstrap in a fresh clone before gating. Gate on make check plus the cache-defeated Docker lint path; all linting in Docker, never the host. Clean up every container and image; never run any prune.
Re-measure everything: the figures above predate whatever lands between now and then.
Filed because `REPO_POLICIES.md` requires it: "A suite between 20 and 60 seconds is still green, but the overage must be filed as an improvement bug against that repo."
**Not milestoned 1.0.0.** On the reading I think is correct the repo is inside the hard cap and merely past the target, which policy explicitly calls green. The fix is also a real piece of work rather than a tuning change. But there is a genuine ambiguity about *which quantity* the cap governs, and if you read it the other way the repo is in breach — so the numbers are below and the call is yours if you disagree with my scoping.
## Measured
By the review of https://git.eeqj.de/sneak/webhooker/pulls/195, on `next`, cache-defeated builder stage:
| quantity | measured |
| --- | --- |
| `RUN make test` layer, cold | **62.8 s** (a second observer saw 68.4 s) |
| of which: per-package test execution | **46.0 s** |
| of which: cold `-race` compile of the whole tree | roughly **17 s** |
| `make test` on the host, warm cache | roughly **22 s** |
## The ambiguity
The policy says "`make test` must complete in under 60 seconds. That is the hard cap, and a suite that exceeds it fails."
- Read as **test execution** (46.0 s): inside the 60 s cap, past the 20 s target. Green, file an improvement bug — this issue.
- Read as **what CI actually pays** (62.8 s): past the hard cap, and the suite fails.
The reviewer's view, which I share: the layer duration is not a clean proxy, because it bundles a cold `-race` compile of the entire tree that the cap was probably not written to include. But it is the number CI pays on every run, so it is not obviously the wrong thing to measure either. Both readings agree the repo is past the 20 s target.
I have scoped this as the improvement bug the 20-60 s band requires. If you read the cap as covering the whole invocation, this is a 1.0.0 blocker instead and should be milestoned.
## Where it comes from
Almost entirely one package. `internal/handlers` was the slowest in all 16 measured runs, at 4-5x the next: 16.7 s ambient, 41.3 s at `GOMAXPROCS=4`, 49.0 s under deliberate host load. Second place is unstable — `internal/ciscript` at 7.1-7.2 s with cores to spare, `internal/delivery` at up to 18.5 s without. Eight of fifteen packages run in 1.0-1.3 s.
The concentration is a consequence of where the tested behaviour lives: `internal/handlers` carries the bulk of the log-bounding suite added across https://git.eeqj.de/sneak/webhooker/issues/146, https://git.eeqj.de/sneak/webhooker/issues/176 and https://git.eeqj.de/sneak/webhooker/issues/178, each of which sweeps seven or more fills across two `slog` handlers. That is real coverage, not waste.
It also has a second cost, noted on https://git.eeqj.de/sneak/webhooker/pulls/195: because `go test` applies `-timeout` uniformly per package, one slow package sets the budget for all of them. The 90 s backstop is over 100x the runtime of the smallest packages, so a genuine deadlock in a one-second package now hangs for 90 s. Reducing the spread fixes that too.
## Definition of done
Not prescribed — the approach is the substance of this issue. Options worth weighing, with the obvious trap named:
1. **Reduce the sweep breadth** where a fill or handler adds no discrimination. **Only where it genuinely adds none** — several of those fills are load-bearing and documented as such (the C0 control catches raw-byte budgeting on its own; the astral non-printable is the only case the text handler charges differently). Removing coverage to hit a time budget is the failure mode caught and rejected in https://git.eeqj.de/sneak/webhooker/pulls/188 and it must not happen here.
2. **Parallelise.** Many of these subtests are independent table cases that do not call `t.Parallel()`. Likely the largest win for the least risk.
3. **Split `internal/handlers`.** Shrinks the spread and lets the uniform `-timeout` come back down.
4. **Argue it is correct as it stands** and that the policy target is wrong for a repo of this shape. A legitimate answer, but it belongs upstream in https://git.eeqj.de/sneak/prompts, not as a local exception.
Whichever is chosen:
- State the before and after for total `make test`, for `internal/handlers`, and for the slowest remaining package, each measured on the tree being shipped.
- **Demonstrate that coverage did not shrink.** If any case was removed, show by mutation that the property it guarded is still caught by something else.
- If the `-timeout` in `script/test` can come back down as a result, do that in the same change and say what the new margin is.
## Implementation requirements
- Branch from `next`, PR based on `next`, single commit, title ending ` (closes #N)`.
- Do not modify `TODO.md` (see https://git.eeqj.de/sneak/webhooker/issues/112).
- Run `make bootstrap` in a fresh clone before gating. Gate on `make check` plus the cache-defeated Docker lint path; all linting in Docker, never the host. Clean up every container and image; never run any prune.
- Re-measure everything: the figures above predate whatever lands between now and then.
Fresh measurement from the review of #237, in a cache-defeated container build at host load average ~55 on 48 cores:
make test — 72.1s, 17 packages, zero (cached) lines.
That is over the 60s hard cap in REPO_POLICIES.md, not merely over the 20s target. The reviewer waived it rather than failing that PR, which was the right call: it is pre-existing on next, load-dependent, and that branch added roughly 1.7s of it. Recording the number here so the overage is tracked rather than re-waived per PR.
Related, and probably the actual lever: #225 measured the root cause of the internal/handlers share of this — every newTestApp seeds an admin user and pays an Argon2id hash at 64 MB, so the package's runtime scales with how many fx applications the tests stand up rather than with what they assert. Cutting that is what would bring the whole invocation back under the cap; raising the timeout again would not.
Fresh measurement from the review of https://git.eeqj.de/sneak/webhooker/pulls/237, in a cache-defeated container build at host load average ~55 on 48 cores:
`make test` — **72.1s**, 17 packages, zero `(cached)` lines.
That is over the 60s hard cap in `REPO_POLICIES.md`, not merely over the 20s target. The reviewer waived it rather than failing that PR, which was the right call: it is pre-existing on `next`, load-dependent, and that branch added roughly 1.7s of it. Recording the number here so the overage is tracked rather than re-waived per PR.
Related, and probably the actual lever: https://git.eeqj.de/sneak/webhooker/issues/225 measured the root cause of the `internal/handlers` share of this — every `newTestApp` seeds an admin user and pays an Argon2id hash at 64 MB, so the package's runtime scales with how many fx applications the tests stand up rather than with what they assert. Cutting that is what would bring the whole invocation back under the cap; raising the timeout again would not.
Plan. The figures on this issue predate cheaper test password hashing (#404): script/test's header now records internal/handlers at 8.5s and the slowest package, internal/database, at 15.8s, at host load 44 to 109.
Measure make test in a cache-defeated image build under the host's real load (no synthetic load on this shared host): the whole run, the test execution, and the three slowest packages.
Find why the slowest package is slow, as #225 found the password hash, and fix that cause where it is plain (shared setup, a cost paid per test that need not be); no test case removed, no assertion loosened.
Leave the 90s per-package timeout as it is: it is the org-wide backstop.
The PR states the before and after figures and what, if anything, still keeps the run above the 20s target.
Model: opus-5-5
Plan. The figures on this issue predate cheaper test password hashing (https://git.eeqj.de/sneak/webhooker/pulls/404): `script/test`'s header now records `internal/handlers` at 8.5s and the slowest package, `internal/database`, at 15.8s, at host load 44 to 109.
- Measure `make test` in a cache-defeated image build under the host's real load (no synthetic load on this shared host): the whole run, the test execution, and the three slowest packages.
- Find why the slowest package is slow, as https://git.eeqj.de/sneak/webhooker/issues/225 found the password hash, and fix that cause where it is plain (shared setup, a cost paid per test that need not be); no test case removed, no assertion loosened.
- Leave the 90s per-package timeout as it is: it is the org-wide backstop.
- The PR states the before and after figures and what, if anything, still keeps the run above the 20s target.
Model: opus-5-5
Opened #441. The three retention tests that made internal/database the slowest package spent most of their time seeding rows 500 per insert, which this SQLite driver binds in time growing with the square of the parameter count; they now seed the same rows 50 per insert. No test case or assertion changes. The header of script/test records the new figures, which are in the PR.
Deviation: the 90s timeout in script/test stays, as the plan above says, although the issue body asked for it to come down where possible.
Judgement call: internal/handlers, now the slowest package, has no single cause left to fix, and internal/ciscript's fixed wait is the retry behaviour its test checks, so neither changes.
Model: opus-5-5
Opened https://git.eeqj.de/sneak/webhooker/pulls/441. The three retention tests that made `internal/database` the slowest package spent most of their time seeding rows 500 per insert, which this SQLite driver binds in time growing with the square of the parameter count; they now seed the same rows 50 per insert. No test case or assertion changes. The header of `script/test` records the new figures, which are in the PR.
- Deviation: the 90s timeout in `script/test` stays, as the plan above says, although the issue body asked for it to come down where possible.
- Judgement call: `internal/handlers`, now the slowest package, has no single cause left to fix, and `internal/ciscript`'s fixed wait is the retry behaviour its test checks, so neither changes.
Model: opus-5-5
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Filed because
REPO_POLICIES.mdrequires it: "A suite between 20 and 60 seconds is still green, but the overage must be filed as an improvement bug against that repo."Not milestoned 1.0.0. On the reading I think is correct the repo is inside the hard cap and merely past the target, which policy explicitly calls green. The fix is also a real piece of work rather than a tuning change. But there is a genuine ambiguity about which quantity the cap governs, and if you read it the other way the repo is in breach — so the numbers are below and the call is yours if you disagree with my scoping.
Measured
By the review of #195, on
next, cache-defeated builder stage:RUN make testlayer, cold-racecompile of the whole treemake teston the host, warm cacheThe ambiguity
The policy says "
make testmust complete in under 60 seconds. That is the hard cap, and a suite that exceeds it fails."The reviewer's view, which I share: the layer duration is not a clean proxy, because it bundles a cold
-racecompile of the entire tree that the cap was probably not written to include. But it is the number CI pays on every run, so it is not obviously the wrong thing to measure either. Both readings agree the repo is past the 20 s target.I have scoped this as the improvement bug the 20-60 s band requires. If you read the cap as covering the whole invocation, this is a 1.0.0 blocker instead and should be milestoned.
Where it comes from
Almost entirely one package.
internal/handlerswas the slowest in all 16 measured runs, at 4-5x the next: 16.7 s ambient, 41.3 s atGOMAXPROCS=4, 49.0 s under deliberate host load. Second place is unstable —internal/ciscriptat 7.1-7.2 s with cores to spare,internal/deliveryat up to 18.5 s without. Eight of fifteen packages run in 1.0-1.3 s.The concentration is a consequence of where the tested behaviour lives:
internal/handlerscarries the bulk of the log-bounding suite added across #146, #176 and #178, each of which sweeps seven or more fills across twosloghandlers. That is real coverage, not waste.It also has a second cost, noted on #195: because
go testapplies-timeoutuniformly per package, one slow package sets the budget for all of them. The 90 s backstop is over 100x the runtime of the smallest packages, so a genuine deadlock in a one-second package now hangs for 90 s. Reducing the spread fixes that too.Definition of done
Not prescribed — the approach is the substance of this issue. Options worth weighing, with the obvious trap named:
t.Parallel(). Likely the largest win for the least risk.internal/handlers. Shrinks the spread and lets the uniform-timeoutcome back down.Whichever is chosen:
make test, forinternal/handlers, and for the slowest remaining package, each measured on the tree being shipped.-timeoutinscript/testcan come back down as a result, do that in the same change and say what the new margin is.Implementation requirements
next, PR based onnext, single commit, title ending(closes #N).TODO.md(see #112).make bootstrapin a fresh clone before gating. Gate onmake checkplus the cache-defeated Docker lint path; all linting in Docker, never the host. Clean up every container and image; never run any prune.Fresh measurement from the review of #237, in a cache-defeated container build at host load average ~55 on 48 cores:
make test— 72.1s, 17 packages, zero(cached)lines.That is over the 60s hard cap in
REPO_POLICIES.md, not merely over the 20s target. The reviewer waived it rather than failing that PR, which was the right call: it is pre-existing onnext, load-dependent, and that branch added roughly 1.7s of it. Recording the number here so the overage is tracked rather than re-waived per PR.Related, and probably the actual lever: #225 measured the root cause of the
internal/handlersshare of this — everynewTestAppseeds an admin user and pays an Argon2id hash at 64 MB, so the package's runtime scales with how many fx applications the tests stand up rather than with what they assert. Cutting that is what would bring the whole invocation back under the cap; raising the timeout again would not.Plan. The figures on this issue predate cheaper test password hashing (#404):
script/test's header now recordsinternal/handlersat 8.5s and the slowest package,internal/database, at 15.8s, at host load 44 to 109.make testin a cache-defeated image build under the host's real load (no synthetic load on this shared host): the whole run, the test execution, and the three slowest packages.Model: opus-5-5
Opened #441. The three retention tests that made
internal/databasethe slowest package spent most of their time seeding rows 500 per insert, which this SQLite driver binds in time growing with the square of the parameter count; they now seed the same rows 50 per insert. No test case or assertion changes. The header ofscript/testrecords the new figures, which are in the PR.script/teststays, as the plan above says, although the issue body asked for it to come down where possible.internal/handlers, now the slowest package, has no single cause left to fix, andinternal/ciscript's fixed wait is the retry behaviour its test checks, so neither changes.Model: opus-5-5