From 9ce8c631d3de19d3bceeb9d2eba7e5c9da1853ea Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Sat, 8 Aug 2026 17:50:13 +0200 Subject: [PATCH] hub: resolve the two artifact dropdowns side by side, not one after the other MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to the fan-out, and the reason for it is worth recording: THE FIRST FIX DID LESS THAN THE ARITHMETIC PREDICTED. Concurrency took the page from 26.2s to ~11-18s, not the ~2s expected, so the gap was chased instead of declared closed. What it found: the slowest single call on the page is not a per-version sha lookup at all, it is the PACKAGE SEARCH (/api/v1/packages/admin?type=generic&q=...), measured in-cluster at 1.1-2.2s each against ~0.24s for a file's metadata. Per-version fan-out cannot touch it — there is one search per package and they ran in series. The two dropdowns are independent, so they now resolve side by side, overlapping both searches and both fan-outs. go test -race clean on the new concurrent paths. Gitea's latency on this box is load-dependent and varies 2-4x between samples, so the CHANGELOG quotes a range rather than a single pair of numbers. --- hub/CHANGELOG.md | 11 +++++++++++ hub/internal/web/server.go | 16 ++++++++++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/hub/CHANGELOG.md b/hub/CHANGELOG.md index 1b1112e..a378868 100644 --- a/hub/CHANGELOG.md +++ b/hub/CHANGELOG.md @@ -30,8 +30,19 @@ reads exactly this value to confirm what they are about to vouch, so a stale one confident wrong answer. The same reasoning `golden_currency_gate.py` already records for the vouched version. With the fan-out, a cold load needs no cache to be fast. +**And the two dropdowns are resolved side by side**, not one after the other. Each was already +internally concurrent, but in series the page still paid the full latency of BOTH package searches — +and that search is the slowest single call here: measured in-cluster at **1.1–2.2 s each**, against +~0.24 s for a file's metadata. + **The cap stays at 20** and now bounds the fan-out too, not just the rendered list. +**MEASURED HONESTLY, because the first fix did less than the arithmetic predicted and that mattered.** +Concurrency alone took 26.2 s to ~11–18 s, not the ~2 s expected. Chasing the gap is what found the +package SEARCH — `/api/v1/packages/admin?type=generic&q=…` — which no amount of per-version fan-out +touches. Gitea's own latency is load-dependent on this box and varies by 2–4× between samples, so +single before/after numbers are not trustworthy and a range is quoted instead. + Tests assert the three things that had to survive: order (and that each sha belongs to its own version), per-version failure isolation, and **the concurrency itself** — a wall-clock assertion plus an in-flight counter, so a fast run cannot be luck. Red-proof: reverting to the serial loop takes diff --git a/hub/internal/web/server.go b/hub/internal/web/server.go index a7faf46..8cf96c5 100644 --- a/hub/internal/web/server.go +++ b/hub/internal/web/server.go @@ -906,6 +906,18 @@ func (s *Server) handleConfiguration(w http.ResponseWriter, r *http.Request) { } ctx := r.Context() + + // The two dropdowns are independent, so they are resolved side by side rather than one after the + // other (v0.100.0). Each is internally concurrent already; doing them in series still cost the + // full latency of BOTH package searches, and that search is the slowest single call on this page + // — measured in-cluster at 1.1–2.2 s each against ~0.24 s for a file's metadata. + var agentChoices, goldenChoices []artifactChoice + var choicesWG sync.WaitGroup + choicesWG.Add(2) + go func() { defer choicesWG.Done(); agentChoices = s.artifactChoices(ctx, pkgAgent, fileAgent) }() + go func() { defer choicesWG.Done(); goldenChoices = s.artifactChoices(ctx, pkgGolden, fileGolden) }() + choicesWG.Wait() + data := map[string]interface{}{ "CSRFToken": csrfToken, "CSRFField": s.csrfField(r), @@ -914,8 +926,8 @@ func (s *Server) handleConfiguration(w http.ResponseWriter, r *http.Request) { "GlobalFloor": s.store.GetGlobalMinControllerVersion(), "FloorRes": s.store.ResolveGlobalFloor(), "Artifacts": s.store.GetArtifactManifest(), - "AgentChoices": s.artifactChoices(ctx, pkgAgent, fileAgent), - "GoldenChoices": s.artifactChoices(ctx, pkgGolden, fileGolden), + "AgentChoices": agentChoices, + "GoldenChoices": goldenChoices, "Flash": r.URL.Query().Get("flash"), } if err := s.templates.ExecuteTemplate(w, "configuration.html", data); err != nil {