@@ -44,6 +44,7 @@ export function ComparePage() {
4444 diffFilter ?: string
4545 filter ?: string
4646 filterRegex ?: string
47+ gasBuckets ?: string
4748 }
4849
4950 // Backward-compat redirect: ?a=X&b=Y → ?runs=X,Y
@@ -120,7 +121,7 @@ export function ComparePage() {
120121 ? Math . min ( parseInt ( search . tableBase , 10 ) || 0 , runIds . length - 1 )
121122 : 'best'
122123
123- const tableSortBy = ( search . sort ?? 'order' ) as 'order' | 'name' | 'avgValue' | `run-${number } `
124+ const tableSortBy = ( search . sort ?? 'order' ) as 'order' | 'name' | 'gasUsed' | ' avgValue' | `run-${number } `
124125 const tableSortDir = ( search . sortDir === 'desc' ? 'desc' : 'asc' ) as 'asc' | 'desc'
125126 const diffFilter = ( search . diffFilter === 'faster' || search . diffFilter === 'slower' ? search . diffFilter : 'all' ) as 'all' | 'faster' | 'slower'
126127 const testFilter = search . filter ?? ''
@@ -129,27 +130,83 @@ export function ComparePage() {
129130 const [ chartZoom , setChartZoom ] = useState ( { start : 0 , end : 100 } )
130131 const [ chartType , setChartType ] = useState < ChartType > ( 'line' )
131132
132- const testNameFilter = useMemo ( ( ) => {
133- if ( ! testFilter ) return undefined
134- if ( testFilterRegex ) {
135- try {
136- const re = new RegExp ( testFilter , 'i' )
137- return ( name : string ) => re . test ( name )
138- } catch {
139- return undefined
133+ // ─── Gas bucket filter ─────────────────────────────────────────
134+ // Parse the currently-selected gas buckets from the URL. Empty set
135+ // means "show all" (no filtering). Values are in millions (e.g.
136+ // "30,60" means the 30M and 60M buckets).
137+ const GAS_BUCKET_STEP = 30_000_000 // 30M gas per bucket
138+
139+ const selectedGasBuckets = useMemo ( ( ) => {
140+ if ( ! search . gasBuckets ) return new Set < number > ( )
141+ return new Set (
142+ search . gasBuckets . split ( ',' ) . map ( ( s ) => parseInt ( s , 10 ) * 1_000_000 ) . filter ( ( n ) => ! isNaN ( n ) ) ,
143+ )
144+ } , [ search . gasBuckets ] )
145+
146+ // Compute per-test gas used from the first run that has results.
147+ // Used both for the available-buckets list and for the filter.
148+ const testGasMap = useMemo ( ( ) => {
149+ const map = new Map < string , number > ( )
150+ // Use the first result that's loaded — tests are typically the
151+ // same across compared runs (same suite).
152+ const result = resultQueries . find ( ( q ) => q . data ) ?. data
153+ if ( ! result ) return map
154+ for ( const [ name , entry ] of Object . entries ( result . tests ) ) {
155+ const step = entry . steps ?. test
156+ if ( step ) {
157+ map . set ( name , step . aggregated . gas_used_total )
140158 }
141159 }
142- const q = testFilter . toLowerCase ( )
143- return ( name : string ) => name . toLowerCase ( ) . includes ( q )
144- } , [ testFilter , testFilterRegex ] )
160+ return map
161+ } , [ resultQueries ] )
162+
163+ // Available gas buckets sorted ascending. Shown as chips.
164+ const availableGasBuckets = useMemo ( ( ) => {
165+ const buckets = new Set < number > ( )
166+ for ( const gas of testGasMap . values ( ) ) {
167+ buckets . add ( Math . round ( gas / GAS_BUCKET_STEP ) * GAS_BUCKET_STEP )
168+ }
169+ return [ ...buckets ] . sort ( ( a , b ) => a - b )
170+ } , [ testGasMap , GAS_BUCKET_STEP ] )
171+
172+ const testNameFilter = useMemo ( ( ) => {
173+ // Combine text/regex filter with gas-bucket filter.
174+ const textFn = ( ( ) => {
175+ if ( ! testFilter ) return undefined
176+ if ( testFilterRegex ) {
177+ try {
178+ const re = new RegExp ( testFilter , 'i' )
179+ return ( name : string ) => re . test ( name )
180+ } catch {
181+ return undefined
182+ }
183+ }
184+ const q = testFilter . toLowerCase ( )
185+ return ( name : string ) => name . toLowerCase ( ) . includes ( q )
186+ } ) ( )
187+
188+ const gasFn = selectedGasBuckets . size > 0
189+ ? ( name : string ) => {
190+ const gas = testGasMap . get ( name )
191+ if ( gas === undefined ) return false
192+ const bucket = Math . round ( gas / GAS_BUCKET_STEP ) * GAS_BUCKET_STEP
193+ return selectedGasBuckets . has ( bucket )
194+ }
195+ : undefined
196+
197+ if ( ! textFn && ! gasFn ) return undefined
198+ if ( textFn && ! gasFn ) return textFn
199+ if ( ! textFn && gasFn ) return gasFn
200+ return ( name : string ) => textFn ! ( name ) && gasFn ! ( name )
201+ } , [ testFilter , testFilterRegex , selectedGasBuckets , testGasMap , GAS_BUCKET_STEP ] )
145202
146203 const updateSearch = useCallback ( ( patch : Record < string , string | undefined > ) => {
147204 navigate ( {
148205 to : '/compare' ,
149- search : { runs : search . runs , steps : search . steps , baseline : search . baseline , labels : search . labels , tableBase : search . tableBase , sort : search . sort , sortDir : search . sortDir , diffFilter : search . diffFilter , filter : search . filter , filterRegex : search . filterRegex , ...patch } ,
206+ search : { runs : search . runs , steps : search . steps , baseline : search . baseline , labels : search . labels , tableBase : search . tableBase , sort : search . sort , sortDir : search . sortDir , diffFilter : search . diffFilter , filter : search . filter , filterRegex : search . filterRegex , gasBuckets : search . gasBuckets , ...patch } ,
150207 replace : true ,
151208 } )
152- } , [ navigate , search . runs , search . steps , search . baseline , search . labels , search . tableBase , search . sort , search . sortDir , search . diffFilter , search . filter , search . filterRegex ] )
209+ } , [ navigate , search . runs , search . steps , search . baseline , search . labels , search . tableBase , search . sort , search . sortDir , search . diffFilter , search . filter , search . filterRegex , search . gasBuckets ] )
153210
154211 const setBaselineIdx = useCallback ( ( idx : number ) => {
155212 updateSearch ( { baseline : idx > 0 ? String ( idx ) : undefined } )
@@ -173,6 +230,31 @@ export function ComparePage() {
173230 updateSearch ( { filterRegex : enabled ? '1' : undefined } )
174231 } , [ updateSearch ] )
175232
233+ const setGasBuckets = useCallback (
234+ ( buckets : Set < number > ) => {
235+ if ( buckets . size === 0 ) {
236+ updateSearch ( { gasBuckets : undefined } )
237+ } else {
238+ const sorted = [ ...buckets ] . sort ( ( a , b ) => a - b )
239+ updateSearch ( { gasBuckets : sorted . map ( ( v ) => String ( v / 1_000_000 ) ) . join ( ',' ) } )
240+ }
241+ } ,
242+ [ updateSearch ] ,
243+ )
244+
245+ const toggleGasBucket = useCallback (
246+ ( bucket : number ) => {
247+ const next = new Set ( selectedGasBuckets )
248+ if ( next . has ( bucket ) ) {
249+ next . delete ( bucket )
250+ } else {
251+ next . add ( bucket )
252+ }
253+ setGasBuckets ( next )
254+ } ,
255+ [ selectedGasBuckets , setGasBuckets ] ,
256+ )
257+
176258 // Handle backward-compat redirect in progress
177259 if ( search . a && search . b && ! search . runs ) {
178260 return < LoadingState message = "Redirecting..." />
@@ -216,7 +298,7 @@ export function ComparePage() {
216298
217299 return (
218300 < div className = "flex flex-col gap-6" >
219- < StickyRunBar runs = { runs } sentinelRef = { headerRef } labelMode = { labelMode } onLabelModeChange = { setLabelMode } testFilter = { testFilter } testFilterRegex = { testFilterRegex } onTestFilterChange = { setTestFilter } onTestFilterRegexChange = { setTestFilterRegex } />
301+ < StickyRunBar runs = { runs } sentinelRef = { headerRef } labelMode = { labelMode } onLabelModeChange = { setLabelMode } testFilter = { testFilter } testFilterRegex = { testFilterRegex } onTestFilterChange = { setTestFilter } onTestFilterRegexChange = { setTestFilterRegex } availableGasBuckets = { availableGasBuckets } selectedGasBuckets = { selectedGasBuckets } onToggleGasBucket = { toggleGasBucket } onClearGasBuckets = { ( ) => setGasBuckets ( new Set ( ) ) } />
220302
221303 { /* Breadcrumb */ }
222304 < div className = "flex min-w-0 items-center gap-2 text-sm/6 text-gray-500 dark:text-gray-400" >
@@ -316,6 +398,36 @@ export function ComparePage() {
316398 .*
317399 </ button >
318400 </ div >
401+ { availableGasBuckets . length > 1 && (
402+ < div className = "flex items-center gap-1.5" >
403+ < span > Gas:</ span >
404+ < div className = "flex flex-wrap gap-1" >
405+ < button
406+ onClick = { ( ) => setGasBuckets ( new Set ( ) ) }
407+ className = { `rounded-xs px-2 py-0.5 text-xs/5 font-medium transition-colors ${
408+ selectedGasBuckets . size === 0
409+ ? 'bg-gray-800 text-white dark:bg-gray-200 dark:text-gray-900'
410+ : 'bg-gray-100 text-gray-500 hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-400 dark:hover:bg-gray-600'
411+ } `}
412+ >
413+ All
414+ </ button >
415+ { availableGasBuckets . map ( ( bucket ) => (
416+ < button
417+ key = { bucket }
418+ onClick = { ( ) => toggleGasBucket ( bucket ) }
419+ className = { `rounded-xs px-2 py-0.5 text-xs/5 font-medium transition-colors ${
420+ selectedGasBuckets . has ( bucket )
421+ ? 'bg-gray-800 text-white dark:bg-gray-200 dark:text-gray-900'
422+ : 'bg-gray-100 text-gray-500 hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-400 dark:hover:bg-gray-600'
423+ } `}
424+ >
425+ { Math . round ( bucket / 1_000_000 ) } M
426+ </ button >
427+ ) ) }
428+ </ div >
429+ </ div >
430+ ) }
319431 </ div >
320432
321433 { suiteMismatch && (
@@ -336,7 +448,7 @@ export function ComparePage() {
336448 } } />
337449 </ div >
338450
339- < MetricsComparison runs = { runs } stepFilter = { stepFilter } baselineIdx = { baselineIdx } onBaselineChange = { setBaselineIdx } labelMode = { labelMode } />
451+ < MetricsComparison runs = { runs } stepFilter = { stepFilter } baselineIdx = { baselineIdx } onBaselineChange = { setBaselineIdx } labelMode = { labelMode } testNameFilter = { testNameFilter } />
340452
341453 { allResults && (
342454 < MGasComparisonChart runs = { runs } suiteTests = { suite ?. tests } stepFilter = { stepFilter } labelMode = { labelMode } testNameFilter = { testNameFilter } zoomRange = { sharedZoom ? chartZoom : undefined } onZoomChange = { sharedZoom ? setChartZoom : undefined } chartType = { chartType } />
0 commit comments