@parca/profile 0.16.444 → 0.16.445

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/dist/ProfileIcicleGraph/index.d.ts.map +1 -1
  3. package/dist/ProfileIcicleGraph/index.js +3 -11
  4. package/dist/ProfileView/ColorStackLegend.d.ts.map +1 -0
  5. package/dist/{ProfileIcicleGraph/IcicleGraphArrow → ProfileView}/ColorStackLegend.js +2 -2
  6. package/dist/ProfileView/VisualizationPanel.d.ts +4 -1
  7. package/dist/ProfileView/VisualizationPanel.d.ts.map +1 -1
  8. package/dist/ProfileView/VisualizationPanel.js +4 -6
  9. package/dist/ProfileView/index.d.ts.map +1 -1
  10. package/dist/ProfileView/index.js +33 -10
  11. package/dist/ProfileViewWithData.d.ts.map +1 -1
  12. package/dist/ProfileViewWithData.js +1 -3
  13. package/dist/Table/index.d.ts +2 -29
  14. package/dist/Table/index.d.ts.map +1 -1
  15. package/dist/Table/index.js +52 -159
  16. package/dist/Table/utils/functions.d.ts +49 -0
  17. package/dist/Table/utils/functions.d.ts.map +1 -0
  18. package/dist/Table/utils/functions.js +181 -0
  19. package/dist/components/ActionButtons/GroupByDropdown.js +1 -1
  20. package/dist/components/ActionButtons/SortByDropdown.d.ts +3 -0
  21. package/dist/components/ActionButtons/SortByDropdown.d.ts.map +1 -0
  22. package/dist/components/ActionButtons/SortByDropdown.js +49 -0
  23. package/dist/components/VisualisationToolbar/MultiLevelDropdown.d.ts.map +1 -1
  24. package/dist/components/VisualisationToolbar/MultiLevelDropdown.js +3 -27
  25. package/dist/components/VisualisationToolbar/TableColumnsDropdown.d.ts.map +1 -1
  26. package/dist/components/VisualisationToolbar/TableColumnsDropdown.js +3 -1
  27. package/dist/components/VisualisationToolbar/index.d.ts +11 -0
  28. package/dist/components/VisualisationToolbar/index.d.ts.map +1 -1
  29. package/dist/components/VisualisationToolbar/index.js +13 -6
  30. package/dist/styles.css +1 -1
  31. package/package.json +2 -2
  32. package/src/ProfileIcicleGraph/index.tsx +2 -18
  33. package/src/{ProfileIcicleGraph/IcicleGraphArrow → ProfileView}/ColorStackLegend.tsx +2 -2
  34. package/src/ProfileView/VisualizationPanel.tsx +13 -10
  35. package/src/ProfileView/index.tsx +59 -9
  36. package/src/ProfileViewWithData.tsx +1 -3
  37. package/src/Table/index.tsx +121 -263
  38. package/src/Table/utils/functions.ts +284 -0
  39. package/src/components/ActionButtons/GroupByDropdown.tsx +1 -1
  40. package/src/components/ActionButtons/SortByDropdown.tsx +84 -0
  41. package/src/components/VisualisationToolbar/MultiLevelDropdown.tsx +7 -30
  42. package/src/components/VisualisationToolbar/TableColumnsDropdown.tsx +3 -1
  43. package/src/components/VisualisationToolbar/index.tsx +103 -58
  44. package/dist/ProfileIcicleGraph/IcicleGraphArrow/ColorStackLegend.d.ts.map +0 -1
  45. /package/dist/{ProfileIcicleGraph/IcicleGraphArrow → ProfileView}/ColorStackLegend.d.ts +0 -0
@@ -32,11 +32,36 @@ import {
32
32
  useURLState,
33
33
  } from '@parca/components';
34
34
  import {type RowRendererProps} from '@parca/components/dist/Table';
35
+ import {useCurrentColorProfile} from '@parca/hooks';
35
36
  import {ProfileType} from '@parca/parser';
36
- import {getLastItem, isSearchMatch, valueFormatter} from '@parca/utilities';
37
+ import {isSearchMatch, valueFormatter} from '@parca/utilities';
37
38
 
39
+ import {getFilenameColors, getMappingColors} from '../ProfileIcicleGraph/IcicleGraphArrow/';
40
+ import {colorByColors} from '../ProfileIcicleGraph/IcicleGraphArrow/IcicleGraphNodes';
41
+ import useMappingList, {
42
+ useFilenamesList,
43
+ } from '../ProfileIcicleGraph/IcicleGraphArrow/useMappingList';
38
44
  import {useProfileViewContext} from '../ProfileView/ProfileViewContext';
39
- import {hexifyAddress} from '../utils';
45
+ import {
46
+ ColumnName,
47
+ DataRow,
48
+ DummyRow,
49
+ ROW_HEIGHT,
50
+ RowName,
51
+ addPlusSign,
52
+ getCalleeRows,
53
+ getCallerRows,
54
+ getRowColor,
55
+ getScrollTargetIndex,
56
+ isFirstSubRow,
57
+ isLastSubRow,
58
+ isSubRow,
59
+ ratioString,
60
+ rowBgClassNames,
61
+ sizeToBottomStyle,
62
+ sizeToHeightStyle,
63
+ sizeToWidthStyle,
64
+ } from './utils/functions';
40
65
  import {getTopAndBottomExpandedRowModel} from './utils/topAndBottomExpandedRowModel';
41
66
 
42
67
  const FIELD_MAPPING_FILE = 'mapping_file';
@@ -51,30 +76,6 @@ const FIELD_CUMULATIVE_DIFF = 'cumulative_diff';
51
76
  const FIELD_CALLERS = 'callers';
52
77
  const FIELD_CALLEES = 'callees';
53
78
 
54
- export interface DataRow {
55
- id: number;
56
- name: string;
57
- flat: bigint;
58
- flatDiff: bigint;
59
- cumulative: bigint;
60
- cumulativeDiff: bigint;
61
- mappingFile: string;
62
- functionSystemName: string;
63
- functionFileName: string;
64
- callers?: DataRow[];
65
- callees?: DataRow[];
66
- subRows?: Row[];
67
- isTopSubRow?: boolean;
68
- isBottomSubRow?: boolean;
69
- }
70
-
71
- interface DummyRow {
72
- size: number;
73
- message?: string;
74
- isTopSubRow?: boolean;
75
- isBottomSubRow?: boolean;
76
- }
77
-
78
79
  export type Row = DataRow | DummyRow;
79
80
 
80
81
  export const isDummyRow = (row: Row): row is DummyRow => {
@@ -94,50 +95,9 @@ interface TableProps {
94
95
  setActionButtons?: (buttons: React.JSX.Element) => void;
95
96
  isHalfScreen: boolean;
96
97
  unit?: string;
98
+ metadataMappingFiles?: string[];
97
99
  }
98
100
 
99
- export type ColumnName =
100
- | 'flat'
101
- | 'flatPercentage'
102
- | 'flatDiff'
103
- | 'flatDiffPercentage'
104
- | 'cumulative'
105
- | 'cumulativePercentage'
106
- | 'cumulativeDiff'
107
- | 'cumulativeDiffPercentage'
108
- | 'name'
109
- | 'functionSystemName'
110
- | 'functionFileName'
111
- | 'mappingFile';
112
-
113
- const rowBgClassNames = (isExpanded: boolean, isSubRow: boolean): Record<string, boolean> => {
114
- return {
115
- relative: true,
116
- 'bg-indigo-100 dark:bg-gray-600': isSubRow,
117
- 'bg-indigo-50 dark:bg-gray-700': isExpanded,
118
- };
119
- };
120
-
121
- const ROW_HEIGHT = 29;
122
-
123
- const sizeToHeightStyle = (size: number): Record<string, string> => {
124
- return {
125
- height: `${size * ROW_HEIGHT}px`,
126
- };
127
- };
128
-
129
- const sizeToWidthStyle = (size: number): Record<string, string> => {
130
- return {
131
- width: `${size * ROW_HEIGHT}px`,
132
- };
133
- };
134
-
135
- const sizeToBottomStyle = (size: number): Record<string, string> => {
136
- return {
137
- bottom: `-${size * ROW_HEIGHT}px`,
138
- };
139
- };
140
-
141
101
  const CustomRowRenderer = ({
142
102
  row,
143
103
  usePointerCursor,
@@ -266,68 +226,6 @@ const CustomRowRenderer = ({
266
226
  );
267
227
  };
268
228
 
269
- const getCallerRows = (callers: DataRow[]): Row[] => {
270
- if (callers.length === 0) {
271
- return [{size: 3, message: 'No callers.', isTopSubRow: true}];
272
- }
273
-
274
- const rows = callers.map(row => {
275
- return {...row, isTopSubRow: true};
276
- });
277
- if (rows.length >= 3) {
278
- return rows;
279
- }
280
-
281
- return [...rows, {size: 3 - rows.length, message: '', isTopSubRow: true}];
282
- };
283
-
284
- const getCalleeRows = (callees: DataRow[]): Row[] => {
285
- if (callees.length === 0) {
286
- return [{size: 3, message: 'No callees.', isBottomSubRow: true}];
287
- }
288
-
289
- const rows = callees.map(row => {
290
- return {...row, isBottomSubRow: true};
291
- });
292
- if (rows.length >= 3) {
293
- return rows;
294
- }
295
-
296
- return [{size: 3 - rows.length, message: '', isBottomSubRow: true}, ...rows];
297
- };
298
-
299
- export const getPercentageString = (value: bigint | number, total: bigint | number): string => {
300
- if (total === 0n) {
301
- return '0%';
302
- }
303
-
304
- const percentage = (Number(value) / Number(total)) * 100;
305
- return `${percentage.toFixed(2)}%`;
306
- };
307
-
308
- export const getRatioString = (value: bigint | number, total: bigint, filtered: bigint): string => {
309
- if (filtered === 0n) {
310
- return ` ${getPercentageString(value, total)}`;
311
- }
312
-
313
- return `${getPercentageString(value, total)} / ${getPercentageString(value, filtered)}`;
314
- };
315
-
316
- export const possibleColumns = [
317
- 'flat',
318
- 'flatPercentage',
319
- 'flatDiff',
320
- 'flatDiffPercentage',
321
- 'cumulative',
322
- 'cumulativePercentage',
323
- 'cumulativeDiff',
324
- 'cumulativeDiffPercentage',
325
- 'name',
326
- 'functionSystemName',
327
- 'functionFileName',
328
- 'mappingFile',
329
- ];
330
-
331
229
  export const Table = React.memo(function Table({
332
230
  data,
333
231
  total,
@@ -338,43 +236,81 @@ export const Table = React.memo(function Table({
338
236
  setSearchString = () => {},
339
237
  isHalfScreen,
340
238
  unit,
239
+ metadataMappingFiles,
341
240
  }: TableProps): React.JSX.Element {
241
+ const currentColorProfile = useCurrentColorProfile();
342
242
  const [dashboardItems] = useURLState<string[]>('dashboard_items', {
343
243
  alwaysReturnArray: true,
344
244
  });
345
245
  const [tableColumns] = useURLState<string[]>('table_columns', {
346
246
  alwaysReturnArray: true,
347
247
  });
348
-
248
+ const [colorBy, setColorBy] = useURLState('color_by');
349
249
  const {isDarkMode} = useParcaContext();
350
250
  const [expanded, setExpanded] = useState<ExpandedState>({});
351
251
  const [scrollToIndex, setScrollToIndex] = useState<number | undefined>(undefined);
352
252
 
353
253
  const {compareMode} = useProfileViewContext();
354
254
 
355
- const percentageString = (value: bigint | number, total: bigint | number): string => {
356
- if (total === 0n) {
357
- return '0%';
255
+ const table = useMemo(() => {
256
+ if (loading || data == null) {
257
+ return null;
358
258
  }
359
259
 
360
- const percentage = (Number(value) / Number(total)) * 100;
361
- return `${percentage.toFixed(2)}%`;
362
- };
260
+ return tableFromIPC(data);
261
+ }, [data, loading]);
262
+
263
+ const mappingsList = useMappingList(metadataMappingFiles);
264
+ const filenamesList = useFilenamesList(table);
265
+ const colorByValue = colorBy === undefined || colorBy === '' ? 'binary' : (colorBy as string);
266
+
267
+ const mappingsListCount = useMemo(
268
+ () => mappingsList.filter(m => m !== '').length,
269
+ [mappingsList]
270
+ );
363
271
 
364
- const ratioString = (value: bigint | number): string => {
365
- if (filtered === 0n) {
366
- return ` ${percentageString(value, total)}`;
272
+ // If there is only one mapping file, we want to color by filename by default.
273
+ useEffect(() => {
274
+ if (mappingsListCount === 1 && colorBy !== 'filename') {
275
+ setColorBy('filename');
367
276
  }
277
+ // eslint-disable-next-line react-hooks/exhaustive-deps
278
+ }, [mappingsListCount]);
279
+
280
+ const filenameColors = useMemo(() => {
281
+ const colors = getFilenameColors(filenamesList, isDarkMode, currentColorProfile);
282
+ return colors;
283
+ }, [isDarkMode, filenamesList, currentColorProfile]);
368
284
 
369
- return `${percentageString(value, total)} / ${percentageString(value, filtered)}`;
285
+ const mappingColors = useMemo(() => {
286
+ const colors = getMappingColors(mappingsList, isDarkMode, currentColorProfile);
287
+ return colors;
288
+ }, [isDarkMode, mappingsList, currentColorProfile]);
289
+
290
+ const colorByList = {
291
+ filename: filenameColors,
292
+ binary: mappingColors,
370
293
  };
371
294
 
295
+ type ColorByKey = keyof typeof colorByList;
296
+
297
+ const colorByColors: colorByColors = colorByList[colorByValue as ColorByKey];
298
+
372
299
  const columnHelper = createColumnHelper<Row>();
373
300
 
374
301
  unit = useMemo(() => unit ?? profileType?.sampleUnit ?? '', [unit, profileType?.sampleUnit]);
375
302
 
376
303
  const columns = useMemo<Array<ColumnDef<Row>>>(() => {
377
304
  return [
305
+ columnHelper.accessor('color', {
306
+ id: 'color',
307
+ header: '',
308
+ cell: info => {
309
+ const color = info.getValue() as string;
310
+ return <div className="w-4 h-4 rounded-[4px]" style={{backgroundColor: color}} />;
311
+ },
312
+ size: 10,
313
+ }),
378
314
  columnHelper.accessor('flat', {
379
315
  id: 'flat',
380
316
  header: 'Flat',
@@ -392,7 +328,7 @@ export const Table = React.memo(function Table({
392
328
  if (isDummyRow(info.row.original)) {
393
329
  return '';
394
330
  }
395
- return ratioString((info as CellContext<DataRow, bigint>).getValue());
331
+ return ratioString((info as CellContext<DataRow, bigint>).getValue(), total, filtered);
396
332
  },
397
333
  size: 120,
398
334
  meta: {
@@ -418,7 +354,7 @@ export const Table = React.memo(function Table({
418
354
  if (isDummyRow(info.row.original)) {
419
355
  return '';
420
356
  }
421
- return ratioString((info as CellContext<DataRow, bigint>).getValue());
357
+ return ratioString((info as CellContext<DataRow, bigint>).getValue(), total, filtered);
422
358
  },
423
359
  size: 120,
424
360
  meta: {
@@ -443,7 +379,7 @@ export const Table = React.memo(function Table({
443
379
  if (isDummyRow(info.row.original)) {
444
380
  return '';
445
381
  }
446
- return ratioString((info as CellContext<DataRow, bigint>).getValue());
382
+ return ratioString((info as CellContext<DataRow, bigint>).getValue(), total, filtered);
447
383
  },
448
384
  size: 150,
449
385
  meta: {
@@ -469,7 +405,7 @@ export const Table = React.memo(function Table({
469
405
  if (isDummyRow(info.row.original)) {
470
406
  return '';
471
407
  }
472
- return ratioString((info as CellContext<DataRow, bigint>).getValue());
408
+ return ratioString((info as CellContext<DataRow, bigint>).getValue(), total, filtered);
473
409
  },
474
410
  size: 170,
475
411
  meta: {
@@ -503,6 +439,7 @@ export const Table = React.memo(function Table({
503
439
 
504
440
  const [columnVisibility, setColumnVisibility] = useState(() => {
505
441
  return {
442
+ color: true,
506
443
  flat: true,
507
444
  flatPercentage: false,
508
445
  flatDiff: compareMode,
@@ -552,37 +489,40 @@ export const Table = React.memo(function Table({
552
489
  [selectSpan, dashboardItems.length]
553
490
  );
554
491
 
555
- const onRowDoubleClick = useCallback((row: RowType<Row>, rows: Array<RowType<Row>>) => {
556
- if (isDummyRow(row.original)) {
557
- return;
558
- }
559
- if (!isSubRow(row.original)) {
560
- row.toggleExpanded();
561
- return;
562
- }
563
- // find the original row for this subrow and toggle it
564
- const newRow = rows.find(
565
- r =>
566
- !isDummyRow(r.original) &&
567
- !isDummyRow(row.original) &&
568
- r.original.name === row.original.name &&
569
- !isSubRow(r.original)
570
- );
571
- const parentRow = rows.find(r => {
572
- const parent = row.getParentRow()!;
573
- if (isDummyRow(parent.original) || isDummyRow(r.original)) {
574
- return false;
492
+ const onRowDoubleClick = useCallback(
493
+ (row: RowType<Row>, rows: Array<RowType<Row>>) => {
494
+ if (isDummyRow(row.original)) {
495
+ return;
496
+ }
497
+ if (!isSubRow(row.original)) {
498
+ row.toggleExpanded();
499
+ return;
500
+ }
501
+ // find the original row for this subrow and toggle it
502
+ const newRow = rows.find(
503
+ r =>
504
+ !isDummyRow(r.original) &&
505
+ !isDummyRow(row.original) &&
506
+ r.original.name === row.original.name &&
507
+ !isSubRow(r.original)
508
+ );
509
+ const parentRow = rows.find(r => {
510
+ const parent = row.getParentRow()!;
511
+ if (isDummyRow(parent.original) || isDummyRow(r.original)) {
512
+ return false;
513
+ }
514
+ return r.original.name === parent.original.name;
515
+ });
516
+ if (parentRow == null || newRow == null) {
517
+ return;
575
518
  }
576
- return r.original.name === parent.original.name;
577
- });
578
- if (parentRow == null || newRow == null) {
579
- return;
580
- }
581
519
 
582
- newRow.toggleExpanded();
520
+ newRow.toggleExpanded();
583
521
 
584
- setScrollToIndex(getScrollTargetIndex(rows, parentRow, newRow));
585
- }, []);
522
+ setScrollToIndex(getScrollTargetIndex(rows, parentRow, newRow));
523
+ },
524
+ [setScrollToIndex]
525
+ );
586
526
 
587
527
  const shouldHighlightRow = useCallback(
588
528
  (row: Row) => {
@@ -608,14 +548,6 @@ export const Table = React.memo(function Table({
608
548
  ];
609
549
  }, [compareMode]);
610
550
 
611
- const table = useMemo(() => {
612
- if (loading || data == null) {
613
- return null;
614
- }
615
-
616
- return tableFromIPC(data);
617
- }, [data, loading]);
618
-
619
551
  const rows: DataRow[] = useMemo(() => {
620
552
  if (table == null || table.numRows === 0) {
621
553
  return [];
@@ -644,6 +576,13 @@ export const Table = React.memo(function Table({
644
576
 
645
577
  return {
646
578
  id: i,
579
+ color: getRowColor(
580
+ colorByColors,
581
+ mappingFileColumn,
582
+ i,
583
+ functionFileNameColumn,
584
+ colorBy as string
585
+ ),
647
586
  name: RowName(mappingFileColumn, locationAddressColumn, functionNameColumn, i),
648
587
  flat,
649
588
  flatDiff,
@@ -676,7 +615,7 @@ export const Table = React.memo(function Table({
676
615
  }
677
616
 
678
617
  return rows;
679
- }, [table]);
618
+ }, [table, colorByColors, colorBy]);
680
619
 
681
620
  if (loading) {
682
621
  return (
@@ -734,85 +673,4 @@ export const Table = React.memo(function Table({
734
673
  );
735
674
  });
736
675
 
737
- export const addPlusSign = (num: string): string => {
738
- if (num.charAt(0) === '0' || num.charAt(0) === '-') {
739
- return num;
740
- }
741
-
742
- return `+${num}`;
743
- };
744
-
745
- export const RowName = (
746
- mappingFileColumn: Vector | null,
747
- locationAddressColumn: Vector | null,
748
- functionNameColumn: Vector | null,
749
- row: number
750
- ): string => {
751
- if (mappingFileColumn === null) {
752
- console.error('mapping_file column not found');
753
- return '';
754
- }
755
-
756
- const mappingFile: string | null = mappingFileColumn?.get(row);
757
- let mapping = '';
758
- // Show the last item in the mapping file only if there are more than 1 mappings
759
- if (mappingFile != null && mappingFileColumn.data.length > 1) {
760
- mapping = `[${getLastItem(mappingFile) ?? ''}]`;
761
- }
762
- const functionName: string | null = functionNameColumn?.get(row) ?? '';
763
- if (functionName !== null && functionName !== '') {
764
- return `${mapping} ${functionName}`;
765
- }
766
-
767
- const address: bigint = locationAddressColumn?.get(row) ?? 0;
768
-
769
- return hexifyAddress(address);
770
- };
771
-
772
- const getRowsCount = (rows: Array<RowType<Row>>): number => {
773
- if (rows.length < 6) {
774
- return 6;
775
- }
776
-
777
- return rows.length;
778
- };
779
-
780
- function getScrollTargetIndex(
781
- rows: Array<RowType<Row>>,
782
- parentRow: RowType<Row>,
783
- newRow: RowType<Row>
784
- ): number {
785
- const parentIndex = rows.indexOf(parentRow);
786
- const newRowIndex = rows.indexOf(newRow);
787
- let targetIndex = newRowIndex;
788
- if (parentIndex > newRowIndex) {
789
- // Adjusting the number of subs rows to scroll to the main row after expansion.
790
- targetIndex -= getRowsCount(newRow.subRows);
791
- }
792
- if (parentIndex < newRowIndex) {
793
- // If the parent row is above the new row, we need to adjust the number of subrows of the parent.
794
- targetIndex += getRowsCount(parentRow.subRows);
795
- }
796
- if (targetIndex < 0) {
797
- targetIndex = 0;
798
- }
799
- return targetIndex;
800
- }
801
-
802
- function isSubRow(row: Row): boolean {
803
- return row.isTopSubRow === true || row.isBottomSubRow === true;
804
- }
805
-
806
- function isLastSubRow(row: RowType<Row>, rows: Array<RowType<Row>>): boolean {
807
- const index = rows.indexOf(row);
808
- const nextRow = rows[index + 1];
809
- return nextRow == null || (!isSubRow(nextRow.original) && !nextRow.getIsExpanded());
810
- }
811
-
812
- function isFirstSubRow(row: RowType<Row>, rows: Array<RowType<Row>>): boolean {
813
- const index = rows.indexOf(row);
814
- const prevRow = rows[index - 1];
815
- return prevRow == null || (!isSubRow(prevRow.original) && !prevRow.getIsExpanded());
816
- }
817
-
818
676
  export default Table;