Skip to content

feat: color latency percentiles by magnitude#256

Merged
Makisuo merged 3 commits into
mainfrom
feat/latency-percentile-tone-system
Jul 24, 2026
Merged

feat: color latency percentiles by magnitude#256
Makisuo merged 3 commits into
mainfrom
feat/latency-percentile-tone-system

Conversation

@Makisuo

@Makisuo Makisuo commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

What

Adds a shared, percentile-aware magnitude→tone scale in @maple/ui and applies it to every P50/P95/P99 value across web and local-ui — services table, operations/dependencies tabs, service-map hover cards + node metric cells, Cloudflare/PlanetScale infra tables, and the chat result renderers (17 call sites).

Fast values recede and slow ones advance, so a percentile column can be scanned instead of read.

before after
a healthy 40-row services table 120 identical grey numbers reads flat/muted; nothing pops
one slow service looks the same as the rest its p99 goes amber → red first

Why

P50/P95/P99 rendered as flat, uncolored mono text everywhere. Every cell in a percentile column looked identical, so finding the slow service meant reading every number. Error rate already had per-site tone helpers; latency had none.

How it works

packages/ui/src/lib/latency-tone.ts — pure logic + class maps, a sibling of the existing infra/severity-tokens.ts vocabulary.

  • Tone is value ÷ budget(scale), with per-percentile budgets (p50 300ms, avg 500ms, p95 1s, p99 2s, cpu 50ms) rather than one absolute threshold. p99 is expected to exceed p50, so a single threshold would paint every p99 column red; per-budget means a healthy service reads flat across all three.
  • The p95 budget's slow/critical boundaries land exactly on P95_DEGRADED_MS / P95_UNHEALTHY_MS from service-health.ts, so a cell tone can't disagree with the health badge on its own row.
  • The cpu scale exists because Cloudflare Worker CPU time is ~20× tighter than wall-clock; on the p99 budget every worker collapsed to the quiet step.
  • Warm-only 5-step ramp — nothing is colored for being fine: muted → foreground/70 → foreground → severity-warn → severity-error. Reuses existing tokens only; no new design tokens (check:tokens still passes at 148 shared tokens). Tone is never the sole signal — the number carries the meaning and each LatencyValue gets a plain-word title (fast/normal/…/very slow).

packages/ui/src/components/latency-value.tsx<LatencyValue ms={} scale="p95" />, the ergonomic for the ~90% case (format + tone in one span); latencyToneClass() for sites that already own their element (e.g. service-map MetricCell, stat cards).

Judgment calls a reviewer should check

  • A stronger signal wins; the ramp is only the fallback. An open incident/anomaly cause keeps its tone in service-health-section, and the "p95 > 3× this node's own p50" tail check still wins in the service map. Magnitude tone applies only when neither fires.
  • quiet is plain text-muted-foreground, not a dimmer opacity variant (the plan said /60). Eyebrow labels beside these values already sit at text-muted-foreground/60; a value fainter than its own label reads as the label. Confirmed monotonic + legible in both themes via getComputedStyle.
  • One deliberate behavior change: web's percentile cells now route through the @maple/ui formatLatency, which continues past seconds — a 90s p99 renders 1.5min instead of 90.00s. Also deletes the third private copy of formatLatency that lived inside services-table.tsx.

Testing

  • bun typecheck — green across all 32 packages (also re-verified @maple/ui, @maple/web, @maple/local-ui building at this commit in an isolated worktree, i.e. without the unrelated in-flight work that shares my working tree).
  • 10 new boundary tests (apps/web/src/lib/latency-tone.test.ts): every level per scale, the service-health threshold agreement, the budgetMs override, and the 0/NaN/Infinityquiet guard.
  • Rendered the real module through the dev server and screenshotted a 7-service ramp in dark and light — fast rows recede, a slow consumer goes amber, a batch job goes red, and a service at 64/180/410ms stays uniformly neutral despite a 6× spread.

Not done

No authenticated pass over the live UI. Local sign-in needs a password, which I don't enter, so these tones haven't been seen against real warehouse data — only against the real module + real stylesheet. /services and a service's Operations tab are the two views to eyeball.

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

P50/P95/P99 rendered as flat, uncolored mono text everywhere — services
table, operations/dependencies tabs, service map, Cloudflare/PlanetScale
infra tables, chat renderers. Every cell in a percentile column looked
identical, so finding the slow service in a 40-row table meant reading
every number. Error rate already had per-site tone helpers; latency had
none.

Adds one shared scale in @maple/ui and applies it to 17 call sites.

Tone is value ÷ budget(scale), with per-percentile budgets (p50 300ms,
avg 500ms, p95 1s, p99 2s, cpu 50ms) rather than one absolute threshold —
p99 is *expected* to exceed p50, so a single threshold would paint every
p99 column red. A healthy service now reads flat across all three. The
p95 budget's slow/critical boundaries land exactly on P95_DEGRADED_MS /
P95_UNHEALTHY_MS from service-health.ts, so a cell tone cannot disagree
with the health badge on its own row. `cpu` exists because Cloudflare
Worker CPU time is ~20x tighter than wall-clock; on the p99 budget every
worker collapsed to the quiet step.

The ramp is warm-only — nothing is colored for being fine, so fast values
recede and the eye lands on outliers:
  muted → foreground/70 → foreground → severity-warn → severity-error
It reuses existing tokens only; no new design tokens, check:tokens still
passes. The quiet step is plain `text-muted-foreground` rather than a
dimmer opacity variant: eyebrow labels beside these values already sit at
`text-muted-foreground/60`, and a value fainter than its own label reads
as the label instead of the number.

Where a stronger signal already exists it wins, and the ramp is only the
fallback: an open incident/anomaly in service-health-section, and the
"p95 > 3x this node's own p50" tail check in the service map.

Also deletes the third private copy of formatLatency (services-table.tsx)
and routes web's percentile cells onto the @maple/ui formatter, which
continues past seconds — a 90s p99 now renders 1.5min, not 90.00s.

Verified: typecheck green across all 32 packages; 10 new boundary tests
covering every level per scale, the service-health agreement, the
budgetMs override and the 0/NaN/Infinity guard; ramp confirmed monotonic
and legible in both light and dark via getComputedStyle against the live
stylesheet.

Not done: no authenticated pass over the live UI — local sign-in needs a
password, so these tones have not been seen against real warehouse data.
/services and a service's Operations tab are the views to eyeball.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@pullfrog

pullfrog Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Your Pullfrog Router balance is exhausted.

You have a card on file but auto-reload is disabled, so runs paused once your balance went past the overdraft buffer.

Top up balance → · Enable auto-reload →

Pullfrog  | ⚠️ this action is pinned to a commit SHA, which freezes the cleanup step — switch to @v0 or keep the SHA fresh with Dependabot | Rerun failed job ➔View workflow run | via Pullfrog | Using Claude Opus𝕏

`latencyLevel` ran its break-point scan as `for (const [i, brk] of
BREAKS.entries())`. On the service map that function is called three
times per node per frame (120 calls/frame at the bench's 40 nodes), so
it tiers up quickly — and when JavaScriptCore's DFG tier compiles the
iterator-plus-array-destructuring form it hits an assertion in
VirtualRegisterAllocationPhase and SIGTRAPs the whole WebKit content
process.

That took out the webkit-smoke leg of the cross-browser perf gate:
`/service-map-bench` died with "Target page, context or browser has been
closed" on every attempt, while chromium and firefox passed the same
segment. Reproduced locally and confirmed from the macOS crash report
(EXC_BREAKPOINT in JSC::DFG::VirtualRegisterAllocationPhase::run).

Swapping in a plain indexed loop sidesteps it and does less work per
call. Behaviour is unchanged — the 10 boundary tests still pass — and
webkit/chromium/firefox smoke legs are all green locally.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Makisuo
Makisuo merged commit f2dfdcb into main Jul 24, 2026
8 checks passed
@Makisuo
Makisuo deleted the feat/latency-percentile-tone-system branch July 24, 2026 14:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant