22 * License, v. 2.0. If a copy of the MPL was not distributed with this
33 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
44
5- import { Fragment , useState , useEffect } from 'react' ;
5+ import { Fragment , useState , useEffect , useMemo } from 'react' ;
66import { useSelector } from 'react-redux' ;
77
88import { AppHeader } from './AppHeader' ;
@@ -25,11 +25,24 @@ import type {
2525 ConfidenceRating ,
2626 EffectSize ,
2727} from 'firefox-profiler/profile-logic/benchmark/perf-compare-stats' ;
28+ import type { Profile } from 'firefox-profiler/types' ;
29+ import { BucketFlameGraphPair } from './BucketFlameGraphPair' ;
30+ import type { BucketProfileBundle } from './BucketFlameGraphPair' ;
31+ import {
32+ buildDerivedThread ,
33+ getCategoriesForProfile ,
34+ getDefaultCategoryIndex ,
35+ } from 'firefox-profiler/profile-logic/benchmark/bucket-flame-graph-data' ;
36+ import { getBenchmarkInfo } from 'firefox-profiler/profile-logic/benchmark/benchmark-stuff' ;
2837import './BenchmarkCompareViewer.css' ;
2938
3039type ComparisonData = {
3140 baseUrl : string ;
3241 newUrl : string ;
42+ /** The loaded source profiles, retained so we can render flame graphs of
43+ * individual buckets on demand (focusSelf on a bucket's representative func). */
44+ baseProfile : Profile ;
45+ newProfile : Profile ;
3346 overallScore : ScoreComparison ;
3447 suiteScores : ScoreComparison [ ] ;
3548 suiteComparisons : Array < {
@@ -121,6 +134,8 @@ async function computeComparison(
121134 newSuite . buckets ,
122135 baseStats . bucketNames ,
123136 newStats . bucketNames ,
137+ baseStats . bucketFuncs ,
138+ newStats . bucketFuncs ,
124139 baseSuite . iterationCount
125140 ) ;
126141 return [ { suiteName : baseSuite . suiteName , comparisons } ] ;
@@ -130,6 +145,8 @@ async function computeComparison(
130145 return {
131146 baseUrl,
132147 newUrl,
148+ baseProfile,
149+ newProfile,
133150 overallScore,
134151 suiteScores,
135152 suiteComparisons,
@@ -224,10 +241,14 @@ function ScoreTable({
224241 overallScore,
225242 suiteScores,
226243 suiteComparisonsByName,
244+ baseBundle,
245+ newBundle,
227246} : {
228247 overallScore : ScoreComparison ;
229248 suiteScores : ScoreComparison [ ] ;
230249 suiteComparisonsByName : Map < string , BucketComparison [ ] > ;
250+ baseBundle : BucketProfileBundle ;
251+ newBundle : BucketProfileBundle ;
231252} ) {
232253 const [ expanded , setExpanded ] = useState < Set < string > > ( new Set ( ) ) ;
233254 const numSuites = suiteScores . length ;
@@ -313,6 +334,8 @@ function ScoreTable({
313334 label = { row . label }
314335 baseSubtestMean = { row . baseMean }
315336 numSuites = { numSuites }
337+ baseBundle = { baseBundle }
338+ newBundle = { newBundle }
316339 />
317340 </ td >
318341 </ tr >
@@ -330,16 +353,31 @@ function BucketTable({
330353 label,
331354 baseSubtestMean,
332355 numSuites,
356+ baseBundle,
357+ newBundle,
333358} : {
334359 comparisons : BucketComparison [ ] ;
335360 label : string ;
336361 /** When provided together with numSuites, two percent columns are shown
337362 * (Δ% overall and Δ% subtest) instead of the bucket-relative Δ%. */
338363 baseSubtestMean ?: number ;
339364 numSuites ?: number ;
365+ baseBundle : BucketProfileBundle ;
366+ newBundle : BucketProfileBundle ;
340367} ) {
341368 const showSubtestColumns =
342369 baseSubtestMean !== undefined && numSuites !== undefined ;
370+ const columnCount = showSubtestColumns ? 6 : 5 ;
371+
372+ const [ expanded , setExpanded ] = useState < Set < string > > ( new Set ( ) ) ;
373+ const toggle = ( bucketName : string ) => {
374+ setExpanded ( ( prev ) => {
375+ const next = new Set ( prev ) ;
376+ if ( next . has ( bucketName ) ) next . delete ( bucketName ) ;
377+ else next . add ( bucketName ) ;
378+ return next ;
379+ } ) ;
380+ } ;
343381
344382 const significant = comparisons
345383 . filter ( ( c ) => c . confidence !== 'LOW' && c . effectSize !== 'Negligible' )
@@ -403,23 +441,70 @@ function BucketTable({
403441 </ td >
404442 ) ;
405443 }
444+ // A bucket can be expanded if at least one side has a func index.
445+ // (If both are null it's a degenerate "appeared/disappeared with no
446+ // attributable func" case.)
447+ const expandable = c . baseFunc !== null || c . newFunc !== null ;
448+ const isExpanded = expanded . has ( c . bucketName ) ;
406449 return (
407- < tr key = { i } >
408- < td className = "benchmarkCell--bucketName" title = { c . bucketName } >
409- { c . bucketName }
410- </ td >
411- < td className = "benchmarkCell--number" > { c . baseMean . toFixed ( 2 ) } </ td >
412- < td className = "benchmarkCell--number" > { c . newMean . toFixed ( 2 ) } </ td >
413- < td className = "benchmarkCell--number" > { absDiffStr } </ td >
414- { pctCells }
415- </ tr >
450+ < Fragment key = { i } >
451+ < tr
452+ className = {
453+ expandable ? 'benchmarkRow--bucket-expandable' : undefined
454+ }
455+ onClick = { expandable ? ( ) => toggle ( c . bucketName ) : undefined }
456+ >
457+ < td className = "benchmarkCell--bucketName" title = { c . bucketName } >
458+ { expandable && (
459+ < span className = "benchmarkDisclosure" aria-hidden = "true" >
460+ { isExpanded ? '▼' : '▶' }
461+ </ span >
462+ ) }
463+ { c . bucketName }
464+ </ td >
465+ < td className = "benchmarkCell--number" >
466+ { c . baseMean . toFixed ( 2 ) }
467+ </ td >
468+ < td className = "benchmarkCell--number" > { c . newMean . toFixed ( 2 ) } </ td >
469+ < td className = "benchmarkCell--number" > { absDiffStr } </ td >
470+ { pctCells }
471+ </ tr >
472+ { expandable && isExpanded && (
473+ < tr className = "benchmarkRow--bucket-expansion" >
474+ < td colSpan = { columnCount } >
475+ < BucketFlameGraphPair
476+ baseBundle = { baseBundle }
477+ newBundle = { newBundle }
478+ baseFunc = { c . baseFunc }
479+ newFunc = { c . newFunc }
480+ />
481+ </ td >
482+ </ tr >
483+ ) }
484+ </ Fragment >
416485 ) ;
417486 } ) }
418487 </ tbody >
419488 </ table >
420489 ) ;
421490}
422491
492+ /** Build the (profile, derivedThread, categories) bundle once per profile.
493+ * Computing the derived thread is expensive, so we memoize on profile identity
494+ * and reuse the same bundle across every bucket the user expands. */
495+ function makeBucketProfileBundle ( profile : Profile ) : BucketProfileBundle {
496+ const categories = getCategoriesForProfile ( profile ) ;
497+ const defaultCategory = getDefaultCategoryIndex ( categories ) ;
498+ const benchmarkInfo = getBenchmarkInfo ( profile , 'speedometer' ) ;
499+ const thread = buildDerivedThread (
500+ profile ,
501+ benchmarkInfo . threadIndex ,
502+ categories ,
503+ defaultCategory
504+ ) ;
505+ return { profile, thread, categories, defaultCategory } ;
506+ }
507+
423508function ComparisonResults ( { data } : { data : ComparisonData } ) {
424509 const suiteComparisonsByName = new Map (
425510 data . suiteComparisons . map ( ( { suiteName, comparisons } ) => [
@@ -428,6 +513,15 @@ function ComparisonResults({ data }: { data: ComparisonData }) {
428513 ] )
429514 ) ;
430515
516+ const baseBundle = useMemo (
517+ ( ) => makeBucketProfileBundle ( data . baseProfile ) ,
518+ [ data . baseProfile ]
519+ ) ;
520+ const newBundle = useMemo (
521+ ( ) => makeBucketProfileBundle ( data . newProfile ) ,
522+ [ data . newProfile ]
523+ ) ;
524+
431525 return (
432526 < div className = "benchmarkResults" >
433527 < div className = "benchmarkProfileUrls" >
@@ -450,6 +544,8 @@ function ComparisonResults({ data }: { data: ComparisonData }) {
450544 overallScore = { data . overallScore }
451545 suiteScores = { data . suiteScores }
452546 suiteComparisonsByName = { suiteComparisonsByName }
547+ baseBundle = { baseBundle }
548+ newBundle = { newBundle }
453549 />
454550 </ div >
455551 ) ;
0 commit comments