From a5eb22e6a92f27eefbb8ffa2048c7d470a985432 Mon Sep 17 00:00:00 2001 From: Florent Tapponnier Date: Mon, 13 Jul 2026 19:24:51 +0200 Subject: [PATCH 1/2] spec: missing store blob for a live bench throws instead of caching the miss (bnb-rpc awaiting incident) --- src/lib/spec.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/lib/spec.ts b/src/lib/spec.ts index 7f9ea805..efcd21b9 100644 --- a/src/lib/spec.ts +++ b/src/lib/spec.ts @@ -201,6 +201,16 @@ const loadBenchmarkUnfilteredCached = unstable_cache( // ISR re-render bursts. const stored = await benchFromStore(slug, ""); if (stored) return slimBenchmarkForCache(overlayEditorial(stored, spec)); + if (spec.status === "live") { + // A live spec with no readable blob is a transient store failure + // (proxy timeout, worker mid-write), not a real state. Returning + // undefined would cache the miss for the whole revalidate window: + // home row + /api/stat then serve "Awaiting samples" while the + // bench page reads fine (bnb-rpc incident, 2026-07-13). Throwing + // makes unstable_cache keep the last good entry; cold-cache + // callers already catch and fall back to the draft placeholder. + throw new Error(`store blob missing for live bench ${slug}`); + } return undefined; }, // Version key bumped when Benchmark shape changes so stale cache entries From 213a2ffdf8d962a1a1a7400f76eaec5f38c72f3d Mon Sep 17 00:00:00 2001 From: Florent Tapponnier Date: Mon, 13 Jul 2026 19:59:11 +0200 Subject: [PATCH 2/2] bridge-monitor: sub-50ms buckets + nanosecond precision on Observe Histogram Buckets grew from [50, 100, 200, 500, 1000, 2000, 5000, 10000] to [10, 25, 50, 100, 200, 500, 1000, 2000, 5000, 10000]. Near Intents on solver-cached corridors (HyperCore in particular) returns via a 1Click coordinator fast path in <50ms, and the previous scheme lumped every one of those observations into (0, 50] so histogram_quantile linearly interpolated to ~25ms regardless of true value. The p50 shown on /benchmarks/bridge-quote-latency?chain=HyperCore was therefore a bucket floor, not a measurement. Sub-50 buckets let the real bimodality surface. Also swap Observe(float64(quoteLatency.Milliseconds())) for Observe(float64(quoteLatency.Nanoseconds()) / 1e6) across all 6 per-bridge observers (across, debridge, lifi, mobula, nearintents, relay). int64 Milliseconds() truncates every sub-1ms round-trip to 0, same bug class as the RPC harness fix in PR #1128. Not a hot path here (network RTT dominates), but keeps precision consistent with the new low buckets and cheap to fix while we are already touching each file. Bench YAML methodology + FAQ updated to reflect the new bucket set. --- benchmarks/bridge-quote-latency.yml | 4 ++-- harnesses/bridge-monitor/cmd/monitor/across_bridge.go | 2 +- .../bridge-monitor/cmd/monitor/debridge_bridge.go | 2 +- harnesses/bridge-monitor/cmd/monitor/lifi_bridge.go | 2 +- harnesses/bridge-monitor/cmd/monitor/metrics.go | 10 ++++++++-- harnesses/bridge-monitor/cmd/monitor/mobula_bridge.go | 2 +- .../bridge-monitor/cmd/monitor/nearintents_bridge.go | 2 +- harnesses/bridge-monitor/cmd/monitor/relay_bridge.go | 2 +- 8 files changed, 16 insertions(+), 10 deletions(-) diff --git a/benchmarks/bridge-quote-latency.yml b/benchmarks/bridge-quote-latency.yml index 0e98a1d9..2f494ca3 100644 --- a/benchmarks/bridge-quote-latency.yml +++ b/benchmarks/bridge-quote-latency.yml @@ -54,7 +54,7 @@ methodology: - "Notional sizes: $5, $50, $300 per quote." - "Cadence: full sweep (4 routes × 3 amounts × N bridges) every 5 minutes for 24 hours." - "Region: eu-west. Single point of measurement; multi-region requires running additional monitor instances." - - "Histogram buckets: 50, 100, 200, 500, 1000, 2000, 5000, 10000 ms." + - "Histogram buckets: 10, 25, 50, 100, 200, 500, 1000, 2000, 5000, 10000 ms. Sub-50ms buckets added 2026-07-13 so solver-cached fast paths (Near Intents on HyperCore in particular) surface their true p50 instead of being floored at the interpolation of the (0, 50] bucket." - "Failures (quote_failed, execution_failed, unsupported route) excluded from latency aggregates and counted toward success rate." # Per-corridor leader rewrite deferred: this YAML does not yet declare @@ -89,7 +89,7 @@ faq: - q: "Are intent and relay bridges always faster than aggregators?" a: "On a single-route query, usually. Intent layers like Relay quote against a pool of pre-positioned solvers, so the API is a thin price-discovery call. Aggregators like LI.FI run a route-search graph over N underlying bridges per request, which adds 100 to 400 ms of irreducible work. On multi-hop or rare-corridor queries the aggregator advantage in coverage matters more than the latency gap, and the comparison flips." - q: "How does OpenChainBench measure bridge quote latency?" - a: "The harness issues identical quote requests against every bridge for the same route and notional, every five minutes, from a single eu-west origin. Each request times wall-clock duration from send to last byte received, then publishes a Prometheus histogram (50, 100, 200, 500, 1000, 2000, 5000, 10000 ms buckets). p50, p90 and p99 are derived via `histogram_quantile` over a rolling 24-hour window. Errored quotes (quote_failed, unsupported route, timeout) are excluded from the latency aggregate and counted toward success rate so a fast-but-broken bridge cannot game the headline." + a: "The harness issues identical quote requests against every bridge for the same route and notional, every five minutes, from a single eu-west origin. Each request times wall-clock duration from send to last byte received, then publishes a Prometheus histogram (10, 25, 50, 100, 200, 500, 1000, 2000, 5000, 10000 ms buckets). p50, p90 and p99 are derived via `histogram_quantile` over a rolling 24-hour window. Errored quotes (quote_failed, unsupported route, timeout) are excluded from the latency aggregate and counted toward success rate so a fast-but-broken bridge cannot game the headline." - q: "Does quote latency affect the price the user gets?" a: "Indirectly, yes. A slower quote API leaves a larger window between the moment the price is quoted and the moment the user signs. On volatile corridors the underlying spot can drift in that window, which surfaces as a quoted-vs-realized gap. Intent layers compensate by re-quoting at signing time; aggregators usually pass through a slippage parameter. The fee benchmark on this site (`/benchmarks/bridge-fee`) measures the realized cost; this page measures the latency half of the same flow." diff --git a/harnesses/bridge-monitor/cmd/monitor/across_bridge.go b/harnesses/bridge-monitor/cmd/monitor/across_bridge.go index fa43f673..d7f6cb9b 100644 --- a/harnesses/bridge-monitor/cmd/monitor/across_bridge.go +++ b/harnesses/bridge-monitor/cmd/monitor/across_bridge.go @@ -188,7 +188,7 @@ func (a *AcrossBridge) TestRoute(route TestRoute, amount, amountUsd float64, raw // Latency is only meaningful for quotes that returned a usable route // (same rule as the other bridges). Fast 4xx rejections must not enter // the histogram or they skew the leaderboard. - bridgeQuoteLatency.WithLabelValues(labels...).Observe(float64(quoteLatency.Milliseconds())) + bridgeQuoteLatency.WithLabelValues(labels...).Observe(float64(quoteLatency.Nanoseconds()) / 1e6) bridgeQuoteSuccess.WithLabelValues(labels...).Set(1) // fees.total.amountUsd is Across's own USD valuation of input value minus diff --git a/harnesses/bridge-monitor/cmd/monitor/debridge_bridge.go b/harnesses/bridge-monitor/cmd/monitor/debridge_bridge.go index 2c15fd65..031eb0c3 100644 --- a/harnesses/bridge-monitor/cmd/monitor/debridge_bridge.go +++ b/harnesses/bridge-monitor/cmd/monitor/debridge_bridge.go @@ -121,7 +121,7 @@ func (d *DebridgeBridge) TestRoute(route TestRoute, amount, amountUsd float64, r // (the published methodology measures exactly that). Fast failures, e.g. // Cloudflare 403s answered in 30ms, must not enter the histogram: they // made deBridge look 15x faster the moment its API started rejecting us. - bridgeQuoteLatency.WithLabelValues(labels...).Observe(float64(quoteLatency.Milliseconds())) + bridgeQuoteLatency.WithLabelValues(labels...).Observe(float64(quoteLatency.Nanoseconds()) / 1e6) bridgeQuoteSuccess.WithLabelValues(labels...).Set(1) // Debridge returns input (with opEx ajusté) and output diff --git a/harnesses/bridge-monitor/cmd/monitor/lifi_bridge.go b/harnesses/bridge-monitor/cmd/monitor/lifi_bridge.go index 82c928e1..ba7b9a58 100644 --- a/harnesses/bridge-monitor/cmd/monitor/lifi_bridge.go +++ b/harnesses/bridge-monitor/cmd/monitor/lifi_bridge.go @@ -151,7 +151,7 @@ func (l *LiFiBridge) TestRoute(route TestRoute, amount, amountUsd float64, rawUn // (the published methodology measures exactly that). Fast failures, e.g. // Cloudflare 403s answered in 30ms, must not enter the histogram: they // made deBridge look 15x faster the moment its API started rejecting us. - bridgeQuoteLatency.WithLabelValues(labels...).Observe(float64(quoteLatency.Milliseconds())) + bridgeQuoteLatency.WithLabelValues(labels...).Observe(float64(quoteLatency.Nanoseconds()) / 1e6) bridgeQuoteSuccess.WithLabelValues(labels...).Set(1) inUsd, _ := strconv.ParseFloat(quote.Estimate.FromAmountUSD, 64) diff --git a/harnesses/bridge-monitor/cmd/monitor/metrics.go b/harnesses/bridge-monitor/cmd/monitor/metrics.go index bd42f6c5..5f2fa0f3 100644 --- a/harnesses/bridge-monitor/cmd/monitor/metrics.go +++ b/harnesses/bridge-monitor/cmd/monitor/metrics.go @@ -6,11 +6,17 @@ import ( ) var ( - // Quote latency (time to get quote response) + // Quote latency (time to get bridge quote in ms). Sub-50ms buckets + // (10, 25) added 2026-07-13 because Near Intents on solver-cached + // corridors (HyperCore in particular) returns in <50ms via the 1Click + // coordinator's cached-price fast path, and the previous [50, ...] + // scheme lumped every one of those observations into the (0, 50] + // bucket. histogram_quantile then linearly interpolated to ~25ms + // regardless of the true value, hiding the real bimodality. bridgeQuoteLatency = promauto.NewHistogramVec(prometheus.HistogramOpts{ Name: "bridge_quote_latency_ms", Help: "Latency to get bridge quote in milliseconds", - Buckets: []float64{50, 100, 200, 500, 1000, 2000, 5000, 10000}, + Buckets: []float64{10, 25, 50, 100, 200, 500, 1000, 2000, 5000, 10000}, }, []string{"bridge", "from_chain", "to_chain", "from_token", "to_token", "amount_usd", "region", "chain"}) // Execution latency (broadcast to funds received) diff --git a/harnesses/bridge-monitor/cmd/monitor/mobula_bridge.go b/harnesses/bridge-monitor/cmd/monitor/mobula_bridge.go index 551428d2..4f1ab858 100644 --- a/harnesses/bridge-monitor/cmd/monitor/mobula_bridge.go +++ b/harnesses/bridge-monitor/cmd/monitor/mobula_bridge.go @@ -363,7 +363,7 @@ func (m *MobulaBridge) TestRoute(route TestRoute, amount, amountUsd float64, reg } // Success-only: see debridge_bridge.go, failures must not enter the histogram. - bridgeQuoteLatency.WithLabelValues(labels...).Observe(float64(quoteLatency.Milliseconds())) + bridgeQuoteLatency.WithLabelValues(labels...).Observe(float64(quoteLatency.Nanoseconds()) / 1e6) bridgeQuoteSuccess.WithLabelValues(labels...).Set(1) diff --git a/harnesses/bridge-monitor/cmd/monitor/nearintents_bridge.go b/harnesses/bridge-monitor/cmd/monitor/nearintents_bridge.go index 645f2f0c..2958a8e6 100644 --- a/harnesses/bridge-monitor/cmd/monitor/nearintents_bridge.go +++ b/harnesses/bridge-monitor/cmd/monitor/nearintents_bridge.go @@ -219,7 +219,7 @@ func (n *NearIntentsBridge) TestRoute(route TestRoute, amount, amountUsd float64 // Latency is only meaningful for quotes that returned a usable response // (same rule as the other bridges). Fast 4xx rejections must not enter // the histogram or they skew the leader board. - bridgeQuoteLatency.WithLabelValues(labels...).Observe(float64(quoteLatency.Milliseconds())) + bridgeQuoteLatency.WithLabelValues(labels...).Observe(float64(quoteLatency.Nanoseconds()) / 1e6) bridgeQuoteSuccess.WithLabelValues(labels...).Set(1) inUsd, _ := strconv.ParseFloat(quote.Quote.AmountInUsd, 64) diff --git a/harnesses/bridge-monitor/cmd/monitor/relay_bridge.go b/harnesses/bridge-monitor/cmd/monitor/relay_bridge.go index 708aa052..7350d132 100644 --- a/harnesses/bridge-monitor/cmd/monitor/relay_bridge.go +++ b/harnesses/bridge-monitor/cmd/monitor/relay_bridge.go @@ -188,7 +188,7 @@ func (r *RelayBridge) TestRoute(route TestRoute, amount, amountUsd float64, rawU // (the published methodology measures exactly that). Fast failures, e.g. // Cloudflare 403s answered in 30ms, must not enter the histogram: they // made deBridge look 15x faster the moment its API started rejecting us. - bridgeQuoteLatency.WithLabelValues(labels...).Observe(float64(quoteLatency.Milliseconds())) + bridgeQuoteLatency.WithLabelValues(labels...).Observe(float64(quoteLatency.Nanoseconds()) / 1e6) bridgeQuoteSuccess.WithLabelValues(labels...).Set(1) inUsd, _ := strconv.ParseFloat(quote.Details.CurrencyIn.AmountUsd, 64)