@@ -304,11 +304,22 @@ interface Row {
304304 diff : number ;
305305}
306306
307+ /**
308+ * A set of predicates that have been grouped together because their names have the same fingerprint.
309+ */
310+ interface RowGroup {
311+ name : string ;
312+ rows : Row [ ] ;
313+ before : Optional < number > ;
314+ after : Optional < number > ;
315+ diff : number ;
316+ }
317+
307318function getSortOrder ( sortOrder : "delta" | "absDelta" ) {
308319 if ( sortOrder === "absDelta" ) {
309- return orderBy ( ( row : Row ) => - Math . abs ( row . diff ) ) ;
320+ return orderBy ( ( row : { diff : number } ) => - Math . abs ( row . diff ) ) ;
310321 }
311- return orderBy ( ( row : Row ) => row . diff ) ;
322+ return orderBy ( ( row : { diff : number } ) => row . diff ) ;
312323}
313324
314325interface Metric {
@@ -351,6 +362,30 @@ function metricGetOptional(
351362 return isPresent ( value ) ? metric . get ( value ) : value ;
352363}
353364
365+ function addOptionals ( a : Optional < number > , b : Optional < number > ) {
366+ if ( isPresent ( a ) && isPresent ( b ) ) {
367+ return a + b ;
368+ }
369+ if ( isPresent ( a ) ) {
370+ return a ;
371+ }
372+ if ( isPresent ( b ) ) {
373+ return b ;
374+ }
375+ if ( a === b ) {
376+ return a ; // If absent for the same reason, preserve that reason
377+ }
378+ return 0 ; // Otherwise collapse to zero
379+ }
380+
381+ /**
382+ * Returns a "fingerprint" from the given name, which is used to group together similar names.
383+ */
384+ export function getNameFingerprint ( name : string ) {
385+ // For now just remove the hash from the name. We identify this as a '#' followed by exactly 8 hexadecimal characters.
386+ return name . replace ( / # [ 0 - 9 a - f ] { 8 } (? ! [ 0 - 9 a - f ] ) / g, "" ) ;
387+ }
388+
354389function Chevron ( { expanded } : { expanded : boolean } ) {
355390 return < Codicon name = { expanded ? "chevron-down" : "chevron-right" } /> ;
356391}
@@ -451,9 +486,40 @@ function ComparePerformanceWithData(props: {
451486 return { totalBefore, totalAfter, totalDiff } ;
452487 } , [ rows , metric ] ) ;
453488
454- const rowNames = useMemo (
455- ( ) => abbreviateRANames ( rows . map ( ( row ) => row . name ) ) ,
456- [ rows ] ,
489+ const rowGroups = useMemo ( ( ) => {
490+ const groupedRows = new Map < string , Row [ ] > ( ) ;
491+ for ( const row of rows ) {
492+ const fingerprint = getNameFingerprint ( row . name ) ;
493+ const rows = groupedRows . get ( fingerprint ) ;
494+ if ( rows ) {
495+ rows . push ( row ) ;
496+ } else {
497+ groupedRows . set ( fingerprint , [ row ] ) ;
498+ }
499+ }
500+ return Array . from ( groupedRows . entries ( ) )
501+ . map ( ( [ fingerprint , rows ] ) => {
502+ const before = rows
503+ . map ( ( row ) => metricGetOptional ( metric , row . before ) )
504+ . reduce ( addOptionals ) ;
505+ const after = rows
506+ . map ( ( row ) => metricGetOptional ( metric , row . after ) )
507+ . reduce ( addOptionals ) ;
508+ return {
509+ name : rows . length === 1 ? rows [ 0 ] . name : fingerprint ,
510+ before,
511+ after,
512+ diff :
513+ ( isPresent ( after ) ? after : 0 ) - ( isPresent ( before ) ? before : 0 ) ,
514+ rows,
515+ } satisfies RowGroup ;
516+ } )
517+ . sort ( getSortOrder ( sortOrder ) ) ;
518+ } , [ rows , metric , sortOrder ] ) ;
519+
520+ const rowGroupNames = useMemo (
521+ ( ) => abbreviateRANames ( rowGroups . map ( ( group ) => group . name ) ) ,
522+ [ rowGroups ] ,
457523 ) ;
458524
459525 return (
@@ -511,11 +577,11 @@ function ComparePerformanceWithData(props: {
511577 </ HeaderTR >
512578 </ thead >
513579 </ Table >
514- { rows . map ( ( row , rowIndex ) => (
515- < PredicateRow
516- key = { rowIndex }
517- renderedName = { rowNames [ rowIndex ] }
518- row = { row }
580+ { rowGroups . map ( ( rowGroup , rowGroupIndex ) => (
581+ < PredicateRowGroup
582+ key = { rowGroupIndex }
583+ renderedName = { rowGroupNames [ rowGroupIndex ] }
584+ rowGroup = { rowGroup }
519585 comparison = { comparison }
520586 metric = { metric }
521587 />
@@ -540,6 +606,59 @@ function ComparePerformanceWithData(props: {
540606 ) ;
541607}
542608
609+ interface PredicateRowGroupProps {
610+ renderedName : React . ReactNode ;
611+ rowGroup : RowGroup ;
612+ comparison : boolean ;
613+ metric : Metric ;
614+ }
615+
616+ function PredicateRowGroup ( props : PredicateRowGroupProps ) {
617+ const { renderedName, rowGroup, comparison, metric } = props ;
618+ const [ isExpanded , setExpanded ] = useState ( false ) ;
619+ const rowNames = useMemo (
620+ ( ) => abbreviateRANames ( rowGroup . rows . map ( ( row ) => row . name ) ) ,
621+ [ rowGroup ] ,
622+ ) ;
623+ if ( rowGroup . rows . length === 1 ) {
624+ return < PredicateRow row = { rowGroup . rows [ 0 ] } { ...props } /> ;
625+ }
626+ return (
627+ < Table className = { isExpanded ? "expanded" : "" } >
628+ < tbody >
629+ < PredicateTR
630+ className = { isExpanded ? "expanded" : "" }
631+ key = { "main" }
632+ onClick = { ( ) => setExpanded ( ! isExpanded ) }
633+ >
634+ < ChevronCell >
635+ < Chevron expanded = { isExpanded } />
636+ </ ChevronCell >
637+ { comparison && renderOptionalValue ( rowGroup . before ) }
638+ { renderOptionalValue ( rowGroup . after ) }
639+ { comparison && renderDelta ( rowGroup . diff , metric . unit ) }
640+ < NameCell >
641+ { renderedName } ({ rowGroup . rows . length } predicates)
642+ </ NameCell >
643+ </ PredicateTR >
644+ { isExpanded &&
645+ rowGroup . rows . map ( ( row , rowIndex ) => (
646+ < tr key = { rowIndex } >
647+ < td colSpan = { 5 } >
648+ < PredicateRow
649+ renderedName = { rowNames [ rowIndex ] }
650+ row = { row }
651+ comparison = { comparison }
652+ metric = { metric }
653+ />
654+ </ td >
655+ </ tr >
656+ ) ) }
657+ </ tbody >
658+ </ Table >
659+ ) ;
660+ }
661+
543662interface PredicateRowProps {
544663 renderedName : React . ReactNode ;
545664 row : Row ;
0 commit comments