rossoctl / rossoctl/cortex

feature: latency whiskers hide the slow requests — no max, p95 or p99, and the chart's top is the highest whisker

Open
#1,026 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

enhancement
Dominant language
Go
Stars
13
Forks
40
Avg merge
12h 17m
Merged PRs (30d)
156

Description

Problem

The latency view in abctl's Usage pane (u, then m to latency) draws mean-with-whiskers — mean, / ±1σ. That answers "what is typical, and how much does it vary", but it gives the operator no way to see the slow requests, which is usually why they opened a latency chart in the first place.

Concretely: there is currently an event with duration 120.25s (11,185 tokens, against api.anthropic.com), and the latency chart's box and whiskers only reach about 22s. The slowest request in the window is not merely off to one side of the chart — it is above the top of the frame, and nothing on screen says so.

Why it happens

The chart's ceiling is the highest +1σ cap, not the highest observed duration — authbridge/cmd/abctl/tui/usage_whiskers.go:

var peak float64
for _, r := range rows {
    if r.hi > peak {      // r.hi == mean + stddev
        peak = r.hi
    }
}

Everything is scaled from that peak: the axis gridline labels, whiskerCell's row positions, and the clamp if n > plotRows { n = plotRows }. So the top of the plot is the tallest whisker, by construction.

The deeper cause is that no maximum is recorded anywhere in the pipeline. The accumulator keeps only a sum, a sum of squares and a count (authbridge/authlib/usage/usage.go):

latSum   float64 // milliseconds
latSumSq float64 // milliseconds squared, for stddev
latN     int64

and the wire type exposes exactly three latency fields — LatMeanMs, LatStdDevMs, LatSamples. There is no max, no percentile, and no histogram, in the aggregator, on the wire, or in the renderer. The value row printed under each whisker (renderLatencyValues) shows the mean, so the 120.25s request has no numeric representation in this view either.

Why +1σ is not a stand-in for the max

Latency is right-skewed, so a single slow request is diluted into both the mean and σ — and the more traffic shares its bucket, the further the chart's top drifts below the real maximum. Taking the reported 120.25s outlier plus some number of ordinary ~2s requests in the same bucket:

requests in bucket mean σ chart ceiling (mean+σ) actual max
2 61.12s 59.12s 120.25s 120.25s
10 13.82s 35.48s 49.30s 120.25s
20 7.91s 25.77s 33.68s 120.25s
40 4.96s 18.46s 23.42s 120.25s

At ~40 requests the ceiling lands at 23.4s, which matches the ~22s being observed. Under a normal distribution +1σ covers only ~84% of samples; for a heavy tail it is far less, so the whisker cap should not be read as "the worst case" — and today there is nothing else to read instead.

This makes the chart actively misleading rather than merely incomplete: an operator scanning it concludes the workload's worst latency is ~22s when one request took 5.5x that.

Proposed solution

Record and surface the tail. Roughly:

  1. Aggregatorauthbridge/authlib/usage/usage.go has one place where a duration is observed (b.latSum += ms; b.latSumSq += ms*ms; b.latN++), which is where a running max belongs. A true max is one if ms > b.latMax and costs nothing.
  2. Wire — add to usage.Bucket as omitempty, so an older proxy simply omits it and the client falls back to today's behaviour.
  3. Foldsnapshot.go merges buckets; the max folds as a max-of-maxes, not weighted the way the mean is.
  4. Renderer — scale peak to include the max, and give it its own glyph so it is distinguishable from the ±1σ caps. The per-bucket value row could show mean and max rather than mean alone.
  5. README — the Panes section currently documents only //.

Percentiles (p95/p99) are the better statistic — a true max is a single sample and jumps around between refreshes — but they need either reservoir sampling or histogram buckets in the aggregator, which is a substantially larger change than a running max. A reasonable path is max first (cheap, exact, and immediately fixes the misleading ceiling), then p95/p99 if the histogram is judged worth its memory. Worth deciding deliberately, since the wire format is involved either way.

Additional context
  • The 22s-vs-120.25s figures above are from a live session; the table is the arithmetic that reproduces them, not measured data.
  • Related: b (breakdown) is already unavailable for latency because the aggregator holds no per-label latency — the same "latency stats are thinner than the count stats" theme.
  • Not a regression; the chart has behaved this way since the whisker renderer was introduced.

Assisted-By: Claude (Anthropic AI) noreply@anthropic.com

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Read authbridge/authlib/usage/usage.go and the usage.Bucket and snapshot.go paths to trace latency aggregation and folding, then inspect authbridge/cmd/abctl/tui/usage_whiskers.go and the README Panes section. Done means the maximum is preserved through the wire and snapshot, included in chart scaling with a distinct glyph, represented in the value row, and documented, while older proxies retain the existing fallback.

Written by the indexing model from the issue text.

Assessment

Tech stack
go
Domain
cli, observability
Issue type
Feature
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.