@@ -27,6 +27,7 @@ import {
2727 TableHeaderCell ,
2828 TableRow ,
2929} from "~/components/primitives/Table" ;
30+ import { TabButton , TabContainer } from "~/components/primitives/Tabs" ;
3031import { engine } from "~/v3/runEngine.server" ;
3132import { TimeFilter } from "~/components/runs/v3/SharedFilters" ;
3233import { useSearchParams } from "~/hooks/useSearchParam" ;
@@ -114,13 +115,26 @@ export default function Page() {
114115 const plan = useCurrentPlan ( ) ;
115116 const maxPeriodDays = plan ?. v3Subscription ?. plan ?. limits ?. queryPeriodDays ?. number ;
116117
117- const { value } = useSearchParams ( ) ;
118+ const { value, replace } = useSearchParams ( ) ;
118119 const timeRange : TimeRangeParams = {
119120 period : value ( "period" ) ?? null ,
120121 from : value ( "from" ) ?? null ,
121122 to : value ( "to" ) ?? null ,
122123 } ;
123124
125+ // The Concurrency keys tab exists only for queues with key activity: live keys in the
126+ // ckIndex, or nonzero CK history in the selected range (one cached scalar query decides).
127+ const { rows : gateRows , showLoading : gateLoading } = useQueueMetric (
128+ `SELECT max(max_ck_backlogged) AS peak_keys, max(max_ck_wait_ms) AS peak_wait\nFROM queue_metrics` ,
129+ { ids, timeRange, queueName : fullName }
130+ ) ;
131+ const gateRow = gateRows [ 0 ] ;
132+ const hasHistory = gateRow
133+ ? toNumber ( gateRow . peak_keys ) > 0 || toNumber ( gateRow . peak_wait ) > 0
134+ : false ;
135+ const showKeysTab = ckBreakdown . keys . length > 0 || ( ! gateLoading && hasHistory ) ;
136+ const view = value ( "view" ) === "keys" && showKeysTab ? "keys" : "overview" ;
137+
124138 return (
125139 < PageContainer >
126140 < NavBar >
@@ -145,63 +159,101 @@ export default function Page() {
145159 />
146160 </ div >
147161
148- < QueueDetailChartCard
149- title = "Concurrency"
150- query = { `SELECT timeBucket() AS t, max(max_running) AS running, max(max_limit) AS limit\nFROM queue_metrics\nGROUP BY t\nORDER BY t` }
151- fillGaps
152- ids = { ids }
153- timeRange = { timeRange }
154- queueName = { fullName }
155- series = { [
156- { key : "running" , label : "Running" , color : COLORS . running } ,
157- { key : "limit" , label : "Limit" , color : COLORS . limit } ,
158- ] }
159- />
160- < QueueDetailChartCard
161- title = "Queue depth (backlog)"
162- query = { `SELECT timeBucket() AS t, max(max_queued) AS queued\nFROM queue_metrics\nGROUP BY t\nORDER BY t` }
163- fillGaps
164- ids = { ids }
165- timeRange = { timeRange }
166- queueName = { fullName }
167- series = { [ { key : "queued" , label : "Queued" , color : COLORS . queued } ] }
168- />
169- < QueueDetailChartCard
170- title = "Scheduling delay"
171- 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\nFROM queue_metrics\nGROUP BY t\nORDER BY t` }
172- fillGaps
173- ids = { ids }
174- timeRange = { timeRange }
175- queueName = { fullName }
176- valueFormat = { formatWaitMs }
177- series = { [
178- { key : "p50" , label : "p50" , color : COLORS . p50 } ,
179- { key : "p95" , label : "p95" , color : COLORS . p95 } ,
180- { key : "p99" , label : "p99" , color : COLORS . p99 } ,
181- ] }
182- />
183- < QueueDetailChartCard
184- title = "Throttled buckets"
185- query = { `SELECT timeBucket() AS t, sum(throttled_count) AS throttled\nFROM queue_metrics\nGROUP BY t\nORDER BY t` }
186- fillGaps
187- ids = { ids }
188- timeRange = { timeRange }
189- queueName = { fullName }
190- series = { [ { key : "throttled" , label : "Throttled" , color : COLORS . throttled } ] }
191- />
192- < ConcurrencyKeysSection
193- breakdown = { ckBreakdown }
194- loadedAt = { loadedAt }
195- ids = { ids }
196- timeRange = { timeRange }
197- queueName = { fullName }
198- />
162+ { showKeysTab && (
163+ < TabContainer >
164+ < TabButton
165+ isActive = { view === "overview" }
166+ layoutId = "queue-detail-view"
167+ onClick = { ( ) => replace ( { view : undefined , key : undefined } ) }
168+ >
169+ Overview
170+ </ TabButton >
171+ < TabButton
172+ isActive = { view === "keys" }
173+ layoutId = "queue-detail-view"
174+ onClick = { ( ) => replace ( { view : "keys" } ) }
175+ >
176+ Concurrency keys
177+ </ TabButton >
178+ </ TabContainer >
179+ ) }
180+
181+ { view === "keys" ? (
182+ < ConcurrencyKeysView
183+ breakdown = { ckBreakdown }
184+ loadedAt = { loadedAt }
185+ ids = { ids }
186+ timeRange = { timeRange }
187+ queueName = { fullName }
188+ />
189+ ) : (
190+ < OverviewCharts ids = { ids } timeRange = { timeRange } queueName = { fullName } />
191+ ) }
199192 </ div >
200193 </ PageBody >
201194 </ PageContainer >
202195 ) ;
203196}
204197
198+ function OverviewCharts ( {
199+ ids,
200+ timeRange,
201+ queueName,
202+ } : {
203+ ids : Ids ;
204+ timeRange : TimeRangeParams ;
205+ queueName : string ;
206+ } ) {
207+ return (
208+ < >
209+ < QueueDetailChartCard
210+ title = "Concurrency"
211+ query = { `SELECT timeBucket() AS t, max(max_running) AS running, max(max_limit) AS limit\nFROM queue_metrics\nGROUP BY t\nORDER BY t` }
212+ fillGaps
213+ ids = { ids }
214+ timeRange = { timeRange }
215+ queueName = { queueName }
216+ series = { [
217+ { key : "running" , label : "Running" , color : COLORS . running } ,
218+ { key : "limit" , label : "Limit" , color : COLORS . limit } ,
219+ ] }
220+ />
221+ < QueueDetailChartCard
222+ title = "Queue depth (backlog)"
223+ query = { `SELECT timeBucket() AS t, max(max_queued) AS queued\nFROM queue_metrics\nGROUP BY t\nORDER BY t` }
224+ fillGaps
225+ ids = { ids }
226+ timeRange = { timeRange }
227+ queueName = { queueName }
228+ series = { [ { key : "queued" , label : "Queued" , color : COLORS . queued } ] }
229+ />
230+ < QueueDetailChartCard
231+ title = "Scheduling delay"
232+ 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\nFROM queue_metrics\nGROUP BY t\nORDER BY t` }
233+ fillGaps
234+ ids = { ids }
235+ timeRange = { timeRange }
236+ queueName = { queueName }
237+ valueFormat = { formatWaitMs }
238+ series = { [
239+ { key : "p50" , label : "p50" , color : COLORS . p50 } ,
240+ { key : "p95" , label : "p95" , color : COLORS . p95 } ,
241+ { key : "p99" , label : "p99" , color : COLORS . p99 } ,
242+ ] }
243+ />
244+ < QueueDetailChartCard
245+ title = "Throttled buckets"
246+ query = { `SELECT timeBucket() AS t, sum(throttled_count) AS throttled\nFROM queue_metrics\nGROUP BY t\nORDER BY t` }
247+ fillGaps
248+ ids = { ids }
249+ timeRange = { timeRange }
250+ queueName = { queueName }
251+ series = { [ { key : "throttled" , label : "Throttled" , color : COLORS . throttled } ] }
252+ />
253+ </ >
254+ ) ;
255+ }
256+
205257type CkBreakdown = {
206258 totalBackloggedKeys : number ;
207259 keys : Array < {
@@ -212,9 +264,7 @@ type CkBreakdown = {
212264 } > ;
213265} ;
214266
215- // Rendered only for queues with concurrency-key activity: live keys in the ckIndex, or
216- // nonzero CK history in the selected range (one cached scalar query decides).
217- function ConcurrencyKeysSection ( {
267+ function ConcurrencyKeysView ( {
218268 breakdown,
219269 loadedAt,
220270 ids,
@@ -227,20 +277,27 @@ function ConcurrencyKeysSection({
227277 timeRange : TimeRangeParams ;
228278 queueName : string ;
229279} ) {
230- const { rows, showLoading } = useQueueMetric (
231- `SELECT max(max_ck_backlogged) AS peak_keys, max(max_ck_wait_ms) AS peak_wait\nFROM queue_metrics` ,
232- { ids, timeRange, queueName }
233- ) ;
234- const row = rows [ 0 ] ;
235- const hasHistory = row ? toNumber ( row . peak_keys ) > 0 || toNumber ( row . peak_wait ) > 0 : false ;
236- const hasLive = breakdown . keys . length > 0 ;
237-
238- if ( ! hasLive && ( showLoading || ! hasHistory ) ) return null ;
239-
240280 return (
241281 < >
282+ < GroupedKeyChartCard
283+ title = "Backlog by key"
284+ rankExpr = "max(max_queued)"
285+ seriesExpr = "max(max_queued)"
286+ fillGaps
287+ ids = { ids }
288+ timeRange = { timeRange }
289+ queueName = { queueName }
290+ />
291+ < GroupedKeyChartCard
292+ title = "Throughput by key (started)"
293+ rankExpr = "deltaSumTimestampMerge(started_delta)"
294+ seriesExpr = "deltaSumTimestampMerge(started_delta)"
295+ ids = { ids }
296+ timeRange = { timeRange }
297+ queueName = { queueName }
298+ />
242299 < QueueDetailChartCard
243- title = "Concurrency keys with backlog "
300+ title = "Keys with queued runs (count) "
244301 query = { `SELECT timeBucket() AS t, max(max_ck_backlogged) AS keys\nFROM queue_metrics\nGROUP BY t\nORDER BY t` }
245302 fillGaps
246303 ids = { ids }
@@ -249,7 +306,7 @@ function ConcurrencyKeysSection({
249306 series = { [ { key : "keys" , label : "Keys" , color : COLORS . ckKeys } ] }
250307 />
251308 < QueueDetailChartCard
252- title = "Most-starved key wait"
309+ title = "Most-starved key wait (max across all keys) "
253310 query = { `SELECT timeBucket() AS t, max(max_ck_wait_ms) AS wait\nFROM queue_metrics\nGROUP BY t\nORDER BY t` }
254311 fillGaps
255312 ids = { ids }
@@ -258,7 +315,6 @@ function ConcurrencyKeysSection({
258315 valueFormat = { formatWaitMs }
259316 series = { [ { key : "wait" , label : "Max wait" , color : COLORS . ckWait } ] }
260317 />
261- < TopKeysChartCard ids = { ids } timeRange = { timeRange } queueName = { queueName } />
262318 < KeyStatsTable
263319 breakdown = { breakdown }
264320 loadedAt = { loadedAt }
@@ -286,45 +342,49 @@ const KEY_SERIES_COLORS = [
286342 "#84CC16" ,
287343] ;
288344
289- // Two-step top-N: rank keys by peak backlog over the range, then chart those keys as
290- // grouped series (the per-key table is activity-bound, so ranking is a cheap scan).
291- function TopKeysChartCard ( {
292- ids,
293- timeRange,
294- queueName,
295- } : {
345+ type GroupedKeyChartProps = {
346+ title : string ;
347+ /** Aggregate expression ranking keys over the whole range (top 8 charted). */
348+ rankExpr : string ;
349+ /** Aggregate expression charted per (bucket, key). */
350+ seriesExpr : string ;
351+ fillGaps ?: boolean ;
352+ valueFormat ?: ( value : number ) => string ;
296353 ids : Ids ;
297354 timeRange : TimeRangeParams ;
298355 queueName : string ;
299- } ) {
356+ } ;
357+
358+ // Two-step top-N: rank keys over the range, then chart those keys as grouped series
359+ // (the per-key table is activity-bound, so ranking is a cheap scan).
360+ function GroupedKeyChartCard ( props : GroupedKeyChartProps ) {
300361 const { rows, showLoading, failed } = useQueueMetric (
301- `SELECT concurrency_key, max(max_queued) AS peak\nFROM queue_metrics_by_key\nGROUP BY concurrency_key\nORDER BY peak DESC\nLIMIT 8` ,
302- { ids, timeRange, queueName }
362+ `SELECT concurrency_key, ${ props . rankExpr } AS peak\nFROM queue_metrics_by_key\nGROUP BY concurrency_key\nORDER BY peak DESC\nLIMIT 8` ,
363+ { ids : props . ids , timeRange : props . timeRange , queueName : props . queueName }
303364 ) ;
304365 const keys = useMemo (
305366 ( ) => rows . filter ( ( r ) => toNumber ( r . peak ) > 0 ) . map ( ( r ) => String ( r . concurrency_key ) ) ,
306367 [ rows ]
307368 ) ;
308369
309370 if ( showLoading || failed || keys . length === 0 ) return null ;
310- return < TopKeysSeries keys = { keys } ids = { ids } timeRange = { timeRange } queueName = { queueName } /> ;
371+ return < GroupedKeySeries keys = { keys } { ... props } /> ;
311372}
312373
313- function TopKeysSeries ( {
374+ function GroupedKeySeries ( {
314375 keys,
376+ title,
377+ seriesExpr,
378+ fillGaps,
379+ valueFormat,
315380 ids,
316381 timeRange,
317382 queueName,
318- } : {
319- keys : string [ ] ;
320- ids : Ids ;
321- timeRange : TimeRangeParams ;
322- queueName : string ;
323- } ) {
383+ } : GroupedKeyChartProps & { keys : string [ ] } ) {
324384 const inList = keys . map ( ( k ) => `'${ trqlString ( k ) } '` ) . join ( ", " ) ;
325385 const { rows, showLoading, failed } = useQueueMetric (
326- `SELECT timeBucket() AS t, concurrency_key, max(max_queued) AS queued \nFROM queue_metrics_by_key\nWHERE concurrency_key IN (${ inList } )\nGROUP BY t, concurrency_key\nORDER BY t` ,
327- { ids, timeRange, queueName, fillGaps : true }
386+ `SELECT timeBucket() AS t, concurrency_key, ${ seriesExpr } AS v \nFROM queue_metrics_by_key\nWHERE concurrency_key IN (${ inList } )\nGROUP BY t, concurrency_key\nORDER BY t` ,
387+ { ids, timeRange, queueName, fillGaps }
328388 ) ;
329389
330390 const data = useMemo ( ( ) => {
@@ -337,7 +397,7 @@ function TopKeysSeries({
337397 point = { bucket } as { bucket : number } & Record < string , number > ;
338398 buckets . set ( bucket , point ) ;
339399 }
340- point [ String ( r . concurrency_key ) ] = toNumber ( r . queued ) ;
400+ point [ String ( r . concurrency_key ) ] = toNumber ( r . v ) ;
341401 }
342402 return [ ...buckets . values ( ) ] . sort ( ( a , b ) => a . bucket - b . bucket ) ;
343403 } , [ rows ] ) ;
@@ -358,7 +418,7 @@ function TopKeysSeries({
358418
359419 return (
360420 < div className = "h-64" >
361- < ChartCard title = "Top keys by backlog" >
421+ < ChartCard title = { title } >
362422 < Chart . Root
363423 config = { chartConfig }
364424 data = { data }
@@ -370,7 +430,9 @@ function TopKeysSeries({
370430 < Chart . Line
371431 lineType = "monotone"
372432 xAxisProps = { { tickFormatter } }
433+ yAxisProps = { valueFormat ? { tickFormatter : ( v : number ) => valueFormat ( v ) } : undefined }
373434 tooltipLabelFormatter = { tooltipLabelFormatter }
435+ tooltipValueFormatter = { valueFormat }
374436 />
375437 </ Chart . Root >
376438 </ ChartCard >
0 commit comments