Skip to content

Commit ab00152

Browse files
committed
Group together rows by fingerprinting
1 parent 48954c7 commit ab00152

File tree

1 file changed

+129
-10
lines changed

1 file changed

+129
-10
lines changed

extensions/ql-vscode/src/view/compare-performance/ComparePerformance.tsx

Lines changed: 129 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
307318
function 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

314325
interface 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-9a-f]{8}(?![0-9a-f])/g, "");
387+
}
388+
354389
function 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+
543662
interface PredicateRowProps {
544663
renderedName: React.ReactNode;
545664
row: Row;

0 commit comments

Comments
 (0)