Skip to content

Commit 97875c8

Browse files
committed
fix(webapp): keep the throttled readout independent of the chart's bucket width
The Throttled tile's headline is a share of buckets that saw any throttling, not a peak, so the 60-second bucket floor inflated it: a single brief throttle now marked a whole minute instead of ten seconds. On the same seeded data it read 17% before the floor and 85% after, for identical throttle events. The chart keeps the floor, because a readable line was the point of it. The headline now comes from a second query at the range's natural bucket width, so it means what its tooltip says regardless of how the plotted buckets are sized. Tiles declare this via an optional `readout`; the other three measure peaks, which are width-invariant for a max over gauges, so they are unchanged and issue no extra query. An empty query is now a no-op in useMetricResourceQuery, so the hook can be called unconditionally for tiles that have no separate readout.
1 parent 2be18aa commit 97875c8

2 files changed

Lines changed: 57 additions & 18 deletions

File tree

apps/webapp/app/hooks/useMetricResourceQuery.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ function cacheSet(key: string, rows: MetricResourceRow[]) {
5151
* back-navigation to the queues list) shows its last data immediately and revalidates in the
5252
* background rather than flashing a loading skeleton.
5353
*/
54+
/** An empty query means the caller has nothing to ask for, so no request is made. */
5455
export function useMetricResourceQuery(query: string, opts: MetricResourceQueryOptions) {
5556
const {
5657
organizationId,
@@ -101,6 +102,10 @@ export function useMetricResourceQuery(query: string, opts: MetricResourceQueryO
101102
const loadedKeyRef = useRef<string | null>(null);
102103

103104
const load = useCallback(() => {
105+
if (!query) {
106+
setIsLoading(false);
107+
return;
108+
}
104109
abortRef.current?.abort();
105110
const controller = new AbortController();
106111
abortRef.current = controller;

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

Lines changed: 52 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1228,6 +1228,19 @@ type QueueHeaderTile = {
12281228
formatTotal?: (total: number) => string;
12291229
totalClassName?: string;
12301230
};
1231+
/**
1232+
* Optional second query, run at the range's natural bucket width, that owns the headline readout.
1233+
* A readout measured in buckets rather than in values (a share of buckets, not a peak) would
1234+
* otherwise move whenever this tile's bucket floor widens the plotted buckets.
1235+
*/
1236+
readout?: {
1237+
query: string;
1238+
derive: (rows: MetricTileRow[]) => {
1239+
total: number;
1240+
formatTotal?: (total: number) => string;
1241+
totalClassName?: string;
1242+
};
1243+
};
12311244
};
12321245

12331246
function tileNumber(value: number | string | null): number {
@@ -1245,6 +1258,8 @@ function peakOf(points: TilePoint[]): number {
12451258
return points.reduce((max, p) => (p.value === null ? max : Math.max(max, p.value)), 0);
12461259
}
12471260

1261+
const THROTTLED_QUERY = `SELECT timeBucket() AS t,\n sum(throttled_count) AS throttled\nFROM env_metrics\nGROUP BY t\nORDER BY t`;
1262+
12481263
const QUEUE_HEADER_TILES: QueueHeaderTile[] = [
12491264
{
12501265
id: "saturation",
@@ -1327,24 +1342,31 @@ const QUEUE_HEADER_TILES: QueueHeaderTile[] = [
13271342
totalTooltip: "The share of the selected window with at least one blocked dequeue.",
13281343
color: "var(--color-queues)",
13291344
legend: [{ color: "var(--color-warning)", label: "Throttled" }],
1330-
query: `SELECT timeBucket() AS t,\n sum(throttled_count) AS throttled\nFROM env_metrics\nGROUP BY t\nORDER BY t`,
1345+
query: THROTTLED_QUERY,
13311346
derive: (rows) => {
13321347
const points = rows.map((r) => ({
13331348
bucket: tileTimeToMs(r.t),
13341349
value: tileNumber(r.throttled),
13351350
}));
1336-
// Share of the window that saw any throttling. A raw event sum isn't interpretable (it
1337-
// scales with poll rate and window length); the fraction of buckets with a throttle is.
1338-
// The data path fills gaps (zero-fill for this counter), so every bucket in the window is
1339-
// present and `points.length` is the honest denominator.
1340-
const nonzero = points.filter((p) => p.value !== null && p.value > 0).length;
1341-
const pct = points.length > 0 ? Math.round((nonzero / points.length) * 100) : 0;
1342-
return {
1343-
points,
1344-
total: pct,
1345-
formatTotal: (v) => `${v}% of current period`,
1346-
totalClassName: pct > 0 ? "text-warning" : undefined,
1347-
};
1351+
return { points, total: peakOf(points) };
1352+
},
1353+
readout: {
1354+
query: THROTTLED_QUERY,
1355+
/**
1356+
* Share of the window that saw any throttling. A raw event sum isn't interpretable (it
1357+
* scales with poll rate and window length); the fraction of buckets with a throttle is.
1358+
* Gap fill zero-fills this counter, so every bucket in the window is present and the row
1359+
* count is the honest denominator.
1360+
*/
1361+
derive: (rows) => {
1362+
const nonzero = rows.filter((r) => tileNumber(r.throttled) > 0).length;
1363+
const pct = rows.length > 0 ? Math.round((nonzero / rows.length) * 100) : 0;
1364+
return {
1365+
total: pct,
1366+
formatTotal: (v) => `${v}% of current period`,
1367+
totalClassName: pct > 0 ? "text-warning" : undefined,
1368+
};
1369+
},
13481370
},
13491371
},
13501372
];
@@ -1387,17 +1409,27 @@ function QueueEnvMetricChart({
13871409
const project = useProject();
13881410
const environment = useEnvironment();
13891411

1390-
const { rows, showLoading, failed } = useMetricResourceQuery(tile.query, {
1412+
const sharedOptions = {
13911413
organizationId: organization.id,
13921414
projectId: project.id,
13931415
environmentId: environment.id,
13941416
timeRange,
13951417
defaultPeriod: QUEUE_METRICS_DEFAULT_PERIOD,
13961418
fillGaps: true,
1419+
};
1420+
1421+
const { rows, showLoading, failed } = useMetricResourceQuery(tile.query, {
1422+
...sharedOptions,
13971423
minBucketSeconds: HERO_CHART_MIN_BUCKET_SECONDS,
13981424
});
13991425

1400-
const { points, total, formatTotal, totalClassName } = tile.derive(rows);
1426+
const readoutResult = useMetricResourceQuery(tile.readout?.query ?? "", sharedOptions);
1427+
1428+
const derived = tile.derive(rows);
1429+
const points = derived.points;
1430+
const { total, formatTotal, totalClassName } = tile.readout
1431+
? tile.readout.derive(readoutResult.rows)
1432+
: derived;
14011433

14021434
// Same point shape the shared axis/tooltip helpers expect.
14031435
const data = points
@@ -1423,9 +1455,11 @@ function QueueEnvMetricChart({
14231455
// Peak readout lives in the card title (ChartCard has no dedicated value slot). A zero/empty
14241456
// total renders no readout at all (skipping "0% peak", "0 peak", "0" and the p95 "–" placeholder)
14251457
// so the card title stands alone until there's a non-zero value to show.
1426-
const peak = showLoading ? (
1458+
const readoutLoading = tile.readout ? readoutResult.showLoading : showLoading;
1459+
const readoutFailed = tile.readout ? readoutResult.failed : failed;
1460+
const peak = readoutLoading ? (
14271461
<span className="inline-block h-3 w-12 animate-pulse rounded bg-grid-bright" />
1428-
) : failed || total === 0 ? null : formatTotal ? (
1462+
) : readoutFailed || total === 0 ? null : formatTotal ? (
14291463
formatTotal(total)
14301464
) : (
14311465
total.toLocaleString()
@@ -1445,7 +1479,7 @@ function QueueEnvMetricChart({
14451479
/>
14461480
</span>
14471481
{peak != null ? (
1448-
tile.totalTooltip && !showLoading ? (
1482+
tile.totalTooltip && !readoutLoading ? (
14491483
<SimpleTooltip
14501484
button={
14511485
<span

0 commit comments

Comments
 (0)