Skip to content

Commit 6208a8b

Browse files
committed
fix(webapp): give every chart in a synced group the same bucket width
Flooring only the sparse charts on the queue detail page broke the shared hover crosshair. It is drawn as a recharts ReferenceLine on a category x-axis, so it only appears where the hovered bucket timestamp exists in the other chart's own data: a 10-second timestamp has no match in a 60-second series, so hovering Concurrency drew nothing on Scheduling delay or Throttled. Measured before the fix, hovering Concurrency reached 2 of the 4 other charts; now it reaches 4. Every chart inside each ChartSyncProvider on the page therefore takes the same floor, which is what the Queues list hero row already does. The gauges lose some resolution (a max over a wider bucket is still the same kind of value) in exchange for the crosshair working across the row. Also: a chart whose every plotted value is null still had rows, so it reported itself as having data while rendering its own no-data placeholder, leaving the series legend stranded above it. Presence is now derived from the plotted values rather than the row count.
1 parent 477004f commit 6208a8b

2 files changed

Lines changed: 23 additions & 10 deletions

File tree

  • apps/webapp/app

apps/webapp/app/components/queues/QueueMetricCards.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -220,9 +220,14 @@ export function QueueMetricChart({
220220

221221
// Report data presence so a wrapping card can hide its legend when the chart settles on the
222222
// "no activity" state. Only report once loaded, so the legend stays put while loading.
223+
const hasPlottedData = useMemo(
224+
() => data.some((point) => series.some((s) => point[s.key] != null)),
225+
[data, series]
226+
);
227+
223228
useEffect(() => {
224-
if (!showLoading) onHasDataChange?.(!failed && data.length > 0);
225-
}, [showLoading, failed, data.length, onHasDataChange]);
229+
if (!showLoading) onHasDataChange?.(!failed && hasPlottedData);
230+
}, [showLoading, failed, hasPlottedData, onHasDataChange]);
226231

227232
return (
228233
<Chart.Root

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues_.$queueParam/route.tsx

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -192,12 +192,14 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
192192
const CK_LIVE_LIMIT = 50;
193193

194194
/**
195-
* Bucket floor for this page's event-driven charts (scheduling delay, throttling). Their samples
196-
* only exist when something started or was held back, so at the 10-second width a short range
197-
* picks, most buckets hold nothing and the line reads as a run of zeros. Gauge charts on this page
198-
* (concurrency, queue depth, backlogged keys) carry forward and are left at the natural width.
195+
* Bucket floor for the charts in a synced group. The event-driven series (scheduling delay,
196+
* throttling) need it: their samples only exist when something started or was held back, so at the
197+
* 10-second width a short range picks, most buckets hold nothing and the line reads as a run of
198+
* zeros. The gauges beside them take the same floor because the shared hover crosshair is a
199+
* recharts ReferenceLine on a category x-axis, so it only draws where the hovered bucket
200+
* timestamp exists in the other chart's own data — mixing widths in one group silently drops it.
199201
*/
200-
const SPARSE_CHART_MIN_BUCKET_SECONDS = 60;
202+
const SYNCED_CHART_MIN_BUCKET_SECONDS = 60;
201203

202204
// Whole-queue oldest wait right now: for keyed queues the per-key breakdown carries the oldest
203205
// enqueue time per key, so the queue's oldest is the max wait across keys; otherwise fall back to
@@ -417,6 +419,7 @@ function OverviewCharts({
417419
className="aspect-[2/1]"
418420
query={`SELECT timeBucket() AS t, max(max_running) AS running, max(max_limit) AS limit\nFROM queue_metrics\nGROUP BY t\nORDER BY t`}
419421
fillGaps
422+
minBucketSeconds={SYNCED_CHART_MIN_BUCKET_SECONDS}
420423
ids={ids}
421424
timeRange={timeRange}
422425
queueName={queueName}
@@ -443,6 +446,7 @@ function OverviewCharts({
443446
className="aspect-[2/1]"
444447
query={`SELECT timeBucket() AS t, max(max_queued) AS queued\nFROM queue_metrics\nGROUP BY t\nORDER BY t`}
445448
fillGaps
449+
minBucketSeconds={SYNCED_CHART_MIN_BUCKET_SECONDS}
446450
ids={ids}
447451
timeRange={timeRange}
448452
queueName={queueName}
@@ -462,6 +466,7 @@ function OverviewCharts({
462466
className="aspect-[2/1]"
463467
query={`SELECT timeBucket() AS t,\n deltaSumTimestampMerge(enqueue_delta) AS enqueued,\n deltaSumTimestampMerge(started_delta) AS started\nFROM queue_metrics\nGROUP BY t\nORDER BY t`}
464468
fillGaps
469+
minBucketSeconds={SYNCED_CHART_MIN_BUCKET_SECONDS}
465470
ids={ids}
466471
timeRange={timeRange}
467472
queueName={queueName}
@@ -480,7 +485,7 @@ function OverviewCharts({
480485
className="aspect-[2/1]"
481486
query={`SELECT timeBucket() AS t,\n round(quantilesMerge(0.5, 0.9, 0.95, 0.99)(wait_quantiles)[1]) AS p50,\n round(quantilesMerge(0.5, 0.9, 0.95, 0.99)(wait_quantiles)[3]) AS p95,\n round(quantilesMerge(0.5, 0.9, 0.95, 0.99)(wait_quantiles)[4]) AS p99,\n sum(wait_ms_count) AS samples\nFROM queue_metrics\nGROUP BY t\nORDER BY t`}
482487
fillGaps
483-
minBucketSeconds={SPARSE_CHART_MIN_BUCKET_SECONDS}
488+
minBucketSeconds={SYNCED_CHART_MIN_BUCKET_SECONDS}
484489
sampleCountColumn="samples"
485490
ids={ids}
486491
timeRange={timeRange}
@@ -503,7 +508,7 @@ function OverviewCharts({
503508
className="aspect-[2/1] sm:col-span-2 sm:aspect-[4/1]"
504509
query={`SELECT timeBucket() AS t, sum(throttled_count) AS throttled\nFROM queue_metrics\nGROUP BY t\nORDER BY t`}
505510
fillGaps
506-
minBucketSeconds={SPARSE_CHART_MIN_BUCKET_SECONDS}
511+
minBucketSeconds={SYNCED_CHART_MIN_BUCKET_SECONDS}
507512
ids={ids}
508513
timeRange={timeRange}
509514
queueName={queueName}
@@ -993,6 +998,7 @@ function KeyDrilldown({
993998
className="aspect-[2/1]"
994999
query={`SELECT timeBucket() AS t, max(max_queued) AS queued, max(max_running) AS running\nFROM queue_metrics_by_key\nWHERE ${pin}\nGROUP BY t\nORDER BY t`}
9951000
fillGaps
1001+
minBucketSeconds={SYNCED_CHART_MIN_BUCKET_SECONDS}
9961002
ids={ids}
9971003
timeRange={timeRange}
9981004
queueName={queueName}
@@ -1006,6 +1012,8 @@ function KeyDrilldown({
10061012
title={`Key ${keyName}: throughput`}
10071013
className="aspect-[2/1]"
10081014
query={`SELECT timeBucket() AS t, deltaSumTimestampMerge(started_delta) AS started\nFROM queue_metrics_by_key\nWHERE ${pin}\nGROUP BY t\nORDER BY t`}
1015+
fillGaps
1016+
minBucketSeconds={SYNCED_CHART_MIN_BUCKET_SECONDS}
10091017
ids={ids}
10101018
timeRange={timeRange}
10111019
queueName={queueName}
@@ -1016,7 +1024,7 @@ function KeyDrilldown({
10161024
className="aspect-[2/1]"
10171025
query={`SELECT timeBucket() AS t, if(sum(wait_ms_count) > 0, round(sum(wait_ms_sum) / sum(wait_ms_count)), 0) AS wait, sum(wait_ms_count) AS samples\nFROM queue_metrics_by_key\nWHERE ${pin}\nGROUP BY t\nORDER BY t`}
10181026
fillGaps
1019-
minBucketSeconds={SPARSE_CHART_MIN_BUCKET_SECONDS}
1027+
minBucketSeconds={SYNCED_CHART_MIN_BUCKET_SECONDS}
10201028
sampleCountColumn="samples"
10211029
ids={ids}
10221030
timeRange={timeRange}

0 commit comments

Comments
 (0)