@@ -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
12331246function 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+
12481263const 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