@parca/profile 0.16.379 → 0.16.380

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.
@@ -13,7 +13,16 @@
13
13
 
14
14
  import React, {useCallback, useEffect, useMemo, useState} from 'react';
15
15
 
16
- import {Vector, tableFromIPC} from 'apache-arrow';
16
+ import {flexRender} from '@tanstack/react-table';
17
+ import {
18
+ createColumnHelper,
19
+ type CellContext,
20
+ type ColumnDef,
21
+ type ExpandedState,
22
+ type Row as RowType,
23
+ } from '@tanstack/table-core';
24
+ import {Int64, Vector, tableFromIPC, vectorFromArray} from 'apache-arrow';
25
+ import cx from 'classnames';
17
26
  import {AnimatePresence, motion} from 'framer-motion';
18
27
 
19
28
  import {
@@ -23,6 +32,7 @@ import {
23
32
  useParcaContext,
24
33
  useURLState,
25
34
  } from '@parca/components';
35
+ import {type RowRendererProps} from '@parca/components/dist/Table';
26
36
  import {ProfileType} from '@parca/parser';
27
37
  import {
28
38
  getLastItem,
@@ -35,6 +45,7 @@ import {
35
45
  import {useProfileViewContext} from '../ProfileView/ProfileViewContext';
36
46
  import {hexifyAddress} from '../utils';
37
47
  import ColumnsVisibility from './ColumnsVisibility';
48
+ import {getTopAndBottomExpandedRowModel} from './utils/topAndBottomExpandedRowModel';
38
49
 
39
50
  const FIELD_MAPPING_FILE = 'mapping_file';
40
51
  const FIELD_LOCATION_ADDRESS = 'location_address';
@@ -45,8 +56,11 @@ const FIELD_FLAT = 'flat';
45
56
  const FIELD_FLAT_DIFF = 'flat_diff';
46
57
  const FIELD_CUMULATIVE = 'cumulative';
47
58
  const FIELD_CUMULATIVE_DIFF = 'cumulative_diff';
59
+ const FIELD_CALLERS = 'callers';
60
+ const FIELD_CALLEES = 'callees';
48
61
 
49
- interface row {
62
+ export interface DataRow {
63
+ id: number;
50
64
  name: string;
51
65
  flat: bigint;
52
66
  flatDiff: bigint;
@@ -55,19 +69,26 @@ interface row {
55
69
  mappingFile: string;
56
70
  functionSystemName: string;
57
71
  functionFileName: string;
72
+ callers?: DataRow[];
73
+ callees?: DataRow[];
74
+ subRows?: Row[];
75
+ isTopSubRow?: boolean;
76
+ isBottomSubRow?: boolean;
58
77
  }
59
78
 
60
- export interface ColumnDef {
61
- id: string;
62
- header: string;
63
- accessorKey: string;
64
- footer?: string;
65
- cell?: (info: any) => string | number;
66
- meta?: {align: 'right' | 'left'};
67
- invertSorting?: boolean;
68
- size?: number;
79
+ interface DummyRow {
80
+ size: number;
81
+ message?: string;
82
+ isTopSubRow?: boolean;
83
+ isBottomSubRow?: boolean;
69
84
  }
70
85
 
86
+ export type Row = DataRow | DummyRow;
87
+
88
+ const isDummyRow = (row: Row): row is DummyRow => {
89
+ return 'size' in row;
90
+ };
91
+
71
92
  interface TableProps {
72
93
  data?: Uint8Array;
73
94
  total: bigint;
@@ -80,6 +101,159 @@ interface TableProps {
80
101
  isHalfScreen: boolean;
81
102
  }
82
103
 
104
+ const rowBgClassNames = (isExpanded: boolean, isSubRow: boolean): Record<string, boolean> => {
105
+ return {
106
+ 'bg-indigo-100 dark:bg-gray-600': isSubRow,
107
+ 'bg-indigo-50 dark:bg-gray-700': isExpanded,
108
+ };
109
+ };
110
+
111
+ const ROW_HEIGHT = 29;
112
+
113
+ const sizeToHeightStyle = (size: number): Record<string, string> => {
114
+ return {
115
+ height: `${size * ROW_HEIGHT}px`,
116
+ };
117
+ };
118
+
119
+ const sizeToWidthStyle = (size: number): Record<string, string> => {
120
+ return {
121
+ width: `${size * ROW_HEIGHT}px`,
122
+ };
123
+ };
124
+
125
+ const sizeToTopStyle = (size: number): Record<string, string> => {
126
+ return {
127
+ top: `${size * ROW_HEIGHT + 10}px`,
128
+ };
129
+ };
130
+
131
+ const getCallerLabelWidthStyle = (subRows: Row[]): Record<string, string> => {
132
+ let callerRows = subRows.filter(row => row.isTopSubRow).length;
133
+ if (callerRows < 3) {
134
+ callerRows = 3;
135
+ }
136
+
137
+ return sizeToWidthStyle(callerRows);
138
+ };
139
+
140
+ const getCalleeLabelWidthStyle = (subRows: Row[]): Record<string, string> => {
141
+ let calleeRows = subRows.filter(row => row.isBottomSubRow).length;
142
+ if (calleeRows < 3) {
143
+ calleeRows = 3;
144
+ }
145
+
146
+ return {...sizeToWidthStyle(calleeRows), ...sizeToTopStyle(calleeRows)};
147
+ };
148
+
149
+ const CustomRowRenderer = ({
150
+ row,
151
+ usePointerCursor,
152
+ onRowClick,
153
+ onRowDoubleClick,
154
+ enableHighlighting,
155
+ shouldHighlightRow,
156
+ rows,
157
+ }: RowRendererProps<Row>): React.JSX.Element => {
158
+ const data = row.original;
159
+ const isExpanded = row.getIsExpanded();
160
+ const _isSubRow = isSubRow(data);
161
+ const bgClassNames = rowBgClassNames(isExpanded, _isSubRow);
162
+ if (isDummyRow(data)) {
163
+ return (
164
+ <tr key={row.id} className={cx(bgClassNames)}>
165
+ <td colSpan={100} className={`text-center`} style={sizeToHeightStyle(data.size)}>
166
+ {data.message}
167
+ </td>
168
+ </tr>
169
+ );
170
+ }
171
+
172
+ return (
173
+ <tr
174
+ key={row.id}
175
+ className={cx(
176
+ usePointerCursor === true ? 'cursor-pointer' : 'cursor-auto',
177
+ 'relative',
178
+ bgClassNames,
179
+ {
180
+ 'hover:bg-[#62626212] dark:hover:bg-[#ffffff12] ': !isExpanded && !_isSubRow,
181
+ 'hover:bg-indigo-200 dark:hover:bg-indigo-500': isExpanded || _isSubRow,
182
+ }
183
+ )}
184
+ onClick={onRowClick != null ? () => onRowClick(row.original) : undefined}
185
+ onDoubleClick={onRowDoubleClick != null ? () => onRowDoubleClick(row, rows) : undefined}
186
+ style={
187
+ enableHighlighting !== true || shouldHighlightRow === undefined
188
+ ? undefined
189
+ : {opacity: shouldHighlightRow(row.original) ? 1 : 0.5}
190
+ }
191
+ >
192
+ {row.getVisibleCells().map((cell, idx) => {
193
+ return (
194
+ <td
195
+ key={cell.id}
196
+ className={cx('p-1.5 align-top', {
197
+ /* @ts-expect-error */
198
+ 'text-right': cell.column.columnDef.meta?.align === 'right',
199
+ /* @ts-expect-error */
200
+ 'text-left': cell.column.columnDef.meta?.align === 'left',
201
+ })}
202
+ >
203
+ {idx === 0 && isExpanded ? (
204
+ <>
205
+ <div
206
+ className={`absolute top-0 left-0 bg-white dark:bg-indigo-500 px-1 uppercase -rotate-90 origin-top-left z-10 text-[10px] border-l border-y border-gray-200 dark:border-gray-700 text-left `}
207
+ style={getCallerLabelWidthStyle(row.originalSubRows ?? [])}
208
+ >
209
+ Callers {'->'}
210
+ </div>
211
+ <div
212
+ className={`absolute left-[18px] bg-white dark:bg-indigo-500 px-1 uppercase -rotate-90 origin-bottom-left z-10 text-[10px] border-r border-y border-gray-200 dark:border-gray-700 `}
213
+ style={getCalleeLabelWidthStyle(row.originalSubRows ?? [])}
214
+ >
215
+ {'<-'} Callees
216
+ </div>
217
+ </>
218
+ ) : null}
219
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
220
+ </td>
221
+ );
222
+ })}
223
+ </tr>
224
+ );
225
+ };
226
+
227
+ const getCallerRows = (callers: DataRow[]): Row[] => {
228
+ if (callers.length === 0) {
229
+ return [{size: 3, message: 'No callers.', isTopSubRow: true}];
230
+ }
231
+
232
+ const rows = callers.map(row => {
233
+ return {...row, isTopSubRow: true};
234
+ });
235
+ if (rows.length >= 3) {
236
+ return rows;
237
+ }
238
+
239
+ return [...rows, {size: 3 - rows.length, message: '', isTopSubRow: true}];
240
+ };
241
+
242
+ const getCalleeRows = (callees: DataRow[]): Row[] => {
243
+ if (callees.length === 0) {
244
+ return [{size: 3, message: 'No callees.', isBottomSubRow: true}];
245
+ }
246
+
247
+ const rows = callees.map(row => {
248
+ return {...row, isBottomSubRow: true};
249
+ });
250
+ if (rows.length >= 3) {
251
+ return rows;
252
+ }
253
+
254
+ return [{size: 3 - rows.length, message: '', isBottomSubRow: true}, ...rows];
255
+ };
256
+
83
257
  export const Table = React.memo(function Table({
84
258
  data,
85
259
  total,
@@ -95,6 +269,8 @@ export const Table = React.memo(function Table({
95
269
  const [rawDashboardItems] = useURLState({param: 'dashboard_items'});
96
270
  const [filterByFunctionInput] = useURLState({param: 'filter_by_function'});
97
271
  const {isDarkMode} = useParcaContext();
272
+ const [expanded, setExpanded] = useState<ExpandedState>({});
273
+ const [scrollToIndex, setScrollToIndex] = useState<number | undefined>(undefined);
98
274
 
99
275
  const {compareMode} = useProfileViewContext();
100
276
 
@@ -122,119 +298,154 @@ export const Table = React.memo(function Table({
122
298
  return `${percentageString(value, total)} / ${percentageString(value, filtered)}`;
123
299
  };
124
300
 
125
- const columns = useMemo<ColumnDef[]>(() => {
301
+ const columnHelper = createColumnHelper<Row>();
302
+
303
+ const columns = useMemo<Array<ColumnDef<Row>>>(() => {
126
304
  return [
127
- {
305
+ columnHelper.accessor('flat', {
128
306
  id: 'flat',
129
- accessorKey: 'flat',
130
307
  header: 'Flat',
131
- cell: info => valueFormatter(info.getValue(), profileType?.sampleUnit ?? '', 2),
308
+ cell: info =>
309
+ valueFormatter(
310
+ (info as CellContext<DataRow, bigint>).getValue(),
311
+ profileType?.sampleUnit ?? '',
312
+ 2
313
+ ),
132
314
  size: 80,
133
315
  meta: {
134
316
  align: 'right',
135
317
  },
136
318
  invertSorting: true,
137
- },
138
- {
319
+ }),
320
+ columnHelper.accessor('flat', {
139
321
  id: 'flatPercentage',
140
- accessorKey: 'flat',
141
322
  header: 'Flat (%)',
142
- cell: info => ratioString(info.getValue()),
323
+ cell: info => {
324
+ if (isDummyRow(info.row.original)) {
325
+ return '';
326
+ }
327
+ return ratioString((info as CellContext<DataRow, bigint>).getValue());
328
+ },
143
329
  size: 120,
144
330
  meta: {
145
331
  align: 'right',
146
332
  },
147
333
  invertSorting: true,
148
- },
149
- {
334
+ }),
335
+ columnHelper.accessor('flatDiff', {
150
336
  id: 'flatDiff',
151
- accessorKey: 'flatDiff',
152
337
  header: 'Flat Diff',
153
338
  cell: info =>
154
- addPlusSign(valueFormatter(info.getValue(), profileType?.sampleUnit ?? '', 2)),
339
+ addPlusSign(
340
+ valueFormatter(
341
+ (info as CellContext<DataRow, bigint>).getValue(),
342
+ profileType?.sampleUnit ?? '',
343
+ 2
344
+ )
345
+ ),
155
346
  size: 120,
156
347
  meta: {
157
348
  align: 'right',
158
349
  },
159
350
  invertSorting: true,
160
- },
161
- {
351
+ }),
352
+ columnHelper.accessor('flatDiff', {
162
353
  id: 'flatDiffPercentage',
163
- accessorKey: 'flatDiff',
164
354
  header: 'Flat Diff (%)',
165
- cell: info => ratioString(info.getValue()),
355
+ cell: info => {
356
+ if (isDummyRow(info.row.original)) {
357
+ return '';
358
+ }
359
+ return ratioString((info as CellContext<DataRow, bigint>).getValue());
360
+ },
166
361
  size: 120,
167
362
  meta: {
168
363
  align: 'right',
169
364
  },
170
365
  invertSorting: true,
171
- },
172
- {
366
+ }),
367
+ columnHelper.accessor('cumulative', {
173
368
  id: 'cumulative',
174
- accessorKey: 'cumulative',
175
369
  header: 'Cumulative',
176
- cell: info => valueFormatter(info.getValue(), profileType?.sampleUnit ?? '', 2),
370
+ cell: info =>
371
+ valueFormatter(
372
+ (info as CellContext<DataRow, bigint>).getValue(),
373
+ profileType?.sampleUnit ?? '',
374
+ 2
375
+ ),
177
376
  size: 150,
178
377
  meta: {
179
378
  align: 'right',
180
379
  },
181
380
  invertSorting: true,
182
- },
183
- {
381
+ }),
382
+ columnHelper.accessor('cumulative', {
184
383
  id: 'cumulativePercentage',
185
- accessorKey: 'cumulative',
186
384
  header: 'Cumulative (%)',
187
- cell: info => ratioString(info.getValue()),
385
+ cell: info => {
386
+ if (isDummyRow(info.row.original)) {
387
+ return '';
388
+ }
389
+ return ratioString((info as CellContext<DataRow, bigint>).getValue());
390
+ },
188
391
  size: 150,
189
392
  meta: {
190
393
  align: 'right',
191
394
  },
192
395
  invertSorting: true,
193
- },
194
- {
396
+ }),
397
+ columnHelper.accessor('cumulativeDiff', {
195
398
  id: 'cumulativeDiff',
196
- accessorKey: 'cumulativeDiff',
197
399
  header: 'Cumulative Diff',
198
400
  cell: info =>
199
- addPlusSign(valueFormatter(info.getValue(), profileType?.sampleUnit ?? '', 2)),
401
+ addPlusSign(
402
+ valueFormatter(
403
+ (info as CellContext<DataRow, bigint>).getValue(),
404
+ profileType?.sampleUnit ?? '',
405
+ 2
406
+ )
407
+ ),
200
408
  size: 170,
201
409
  meta: {
202
410
  align: 'right',
203
411
  },
204
412
  invertSorting: true,
205
- },
206
- {
413
+ }),
414
+ columnHelper.accessor('cumulativeDiff', {
207
415
  id: 'cumulativeDiffPercentage',
208
- accessorKey: 'cumulativeDiff',
209
416
  header: 'Cumulative Diff (%)',
210
- cell: info => ratioString(info.getValue()),
417
+ cell: info => {
418
+ if (isDummyRow(info.row.original)) {
419
+ return '';
420
+ }
421
+ return ratioString((info as CellContext<DataRow, bigint>).getValue());
422
+ },
211
423
  size: 170,
212
424
  meta: {
213
425
  align: 'right',
214
426
  },
215
427
  invertSorting: true,
216
- },
217
- {
428
+ }),
429
+ columnHelper.accessor('name', {
218
430
  id: 'name',
219
- accessorKey: 'name',
220
431
  header: 'Name',
221
432
  cell: info => info.getValue(),
222
- },
223
- {
433
+ }),
434
+ columnHelper.accessor('functionSystemName', {
224
435
  id: 'functionSystemName',
225
- accessorKey: 'functionSystemName',
226
436
  header: 'Function System Name',
227
- },
228
- {
437
+ cell: info => info.getValue(),
438
+ }),
439
+ columnHelper.accessor('functionFileName', {
229
440
  id: 'functionFileName',
230
- accessorKey: 'functionFileName',
231
441
  header: 'Function File Name',
232
- },
233
- {
442
+ cell: info => info.getValue(),
443
+ }),
444
+ columnHelper.accessor('mappingFile', {
234
445
  id: 'mappingFile',
235
- accessorKey: 'mappingFile',
236
446
  header: 'Mapping File',
237
- },
447
+ cell: info => info.getValue(),
448
+ }),
238
449
  ];
239
450
  // eslint-disable-next-line react-hooks/exhaustive-deps
240
451
  }, [profileType]);
@@ -273,7 +484,11 @@ export const Table = React.memo(function Table({
273
484
  );
274
485
 
275
486
  const onRowClick = useCallback(
276
- (row: row) => {
487
+ (row: Row) => {
488
+ if (isDummyRow(row)) {
489
+ return;
490
+ }
491
+
277
492
  // If there is only one dashboard item, we don't want to select a span
278
493
  if (dashboardItems.length <= 1) {
279
494
  return;
@@ -283,8 +498,43 @@ export const Table = React.memo(function Table({
283
498
  [selectSpan, dashboardItems.length]
284
499
  );
285
500
 
501
+ const onRowDoubleClick = useCallback((row: RowType<Row>, rows: Array<RowType<Row>>) => {
502
+ if (isDummyRow(row.original)) {
503
+ return;
504
+ }
505
+ if (!isSubRow(row.original)) {
506
+ row.toggleExpanded();
507
+ return;
508
+ }
509
+ // find the original row for this subrow and toggle it
510
+ const newRow = rows.find(
511
+ r =>
512
+ !isDummyRow(r.original) &&
513
+ !isDummyRow(row.original) &&
514
+ r.original.name === row.original.name &&
515
+ !isSubRow(r.original)
516
+ );
517
+ const parentRow = rows.find(r => {
518
+ const parent = row.getParentRow()!;
519
+ if (isDummyRow(parent.original) || isDummyRow(r.original)) {
520
+ return false;
521
+ }
522
+ return r.original.name === parent.original.name;
523
+ });
524
+ if (parentRow == null || newRow == null) {
525
+ return;
526
+ }
527
+
528
+ newRow.toggleExpanded();
529
+
530
+ setScrollToIndex(getScrollTargetIndex(rows, parentRow, newRow));
531
+ }, []);
532
+
286
533
  const shouldHighlightRow = useCallback(
287
- (row: row) => {
534
+ (row: Row) => {
535
+ if (!('name' in row)) {
536
+ return false;
537
+ }
288
538
  const name = row.name;
289
539
  return isSearchMatch(currentSearchString as string, name);
290
540
  },
@@ -350,48 +600,86 @@ export const Table = React.memo(function Table({
350
600
  ];
351
601
  }, [compareMode]);
352
602
 
353
- if (loading)
603
+ const table = useMemo(() => {
604
+ if (loading || data == null) {
605
+ return null;
606
+ }
607
+
608
+ return tableFromIPC(data);
609
+ }, [data, loading]);
610
+
611
+ const rows: DataRow[] = useMemo(() => {
612
+ if (table == null || table.numRows === 0) {
613
+ return [];
614
+ }
615
+
616
+ const flatColumn = table.getChild(FIELD_FLAT);
617
+ const flatDiffColumn = table.getChild(FIELD_FLAT_DIFF);
618
+ const cumulativeColumn = table.getChild(FIELD_CUMULATIVE);
619
+ const cumulativeDiffColumn = table.getChild(FIELD_CUMULATIVE_DIFF);
620
+ const functionNameColumn = table.getChild(FIELD_FUNCTION_NAME);
621
+ const functionSystemNameColumn = table.getChild(FIELD_FUNCTION_SYSTEM_NAME);
622
+ const functionFileNameColumn = table.getChild(FIELD_FUNCTION_FILE_NAME);
623
+ const mappingFileColumn = table.getChild(FIELD_MAPPING_FILE);
624
+ const locationAddressColumn = table.getChild(FIELD_LOCATION_ADDRESS);
625
+ const callersColumn = table.getChild(FIELD_CALLERS);
626
+ const calleesColumn = table.getChild(FIELD_CALLEES);
627
+
628
+ const getRow = (i: number): DataRow => {
629
+ const flat: bigint = flatColumn?.get(i) ?? 0n;
630
+ const flatDiff: bigint = flatDiffColumn?.get(i) ?? 0n;
631
+ const cumulative: bigint = cumulativeColumn?.get(i) ?? 0n;
632
+ const cumulativeDiff: bigint = cumulativeDiffColumn?.get(i) ?? 0n;
633
+ const functionSystemName: string = functionSystemNameColumn?.get(i) ?? '';
634
+ const functionFileName: string = functionFileNameColumn?.get(i) ?? '';
635
+ const mappingFile: string = mappingFileColumn?.get(i) ?? '';
636
+
637
+ return {
638
+ id: i,
639
+ name: RowName(mappingFileColumn, locationAddressColumn, functionNameColumn, i),
640
+ flat,
641
+ flatDiff,
642
+ cumulative,
643
+ cumulativeDiff,
644
+ functionSystemName,
645
+ functionFileName,
646
+ mappingFile,
647
+ };
648
+ };
649
+
650
+ const rows: DataRow[] = [];
651
+ for (let i = 0; i < table.numRows; i++) {
652
+ const row = getRow(i);
653
+ const callerIndices: Vector<Int64> = callersColumn?.get(i) ?? vectorFromArray([]);
654
+ const callers: DataRow[] = Array.from(callerIndices.toArray().values()).map(rowIdx => {
655
+ return getRow(Number(rowIdx));
656
+ });
657
+
658
+ const calleeIndices: Vector<Int64> = calleesColumn?.get(i) ?? vectorFromArray([]);
659
+ const callees: DataRow[] = Array.from(calleeIndices.toArray().values()).map(rowIdx => {
660
+ return getRow(Number(rowIdx));
661
+ });
662
+
663
+ row.callers = callers;
664
+ row.callees = callees;
665
+ row.subRows = [...getCallerRows(callers), ...getCalleeRows(callees)];
666
+
667
+ rows.push(row);
668
+ }
669
+
670
+ return rows;
671
+ }, [table]);
672
+
673
+ if (loading) {
354
674
  return (
355
675
  <div className="overflow-clip h-[700px] min-h-[700px]">
356
676
  <TableSkeleton isHalfScreen={isHalfScreen} isDarkMode={isDarkMode} />
357
677
  </div>
358
678
  );
679
+ }
359
680
 
360
- if (data === undefined) return <div className="mx-auto text-center">Profile has no samples</div>;
361
-
362
- const table = tableFromIPC(data);
363
- if (table.numRows === 0) return <div className="mx-auto text-center">Profile has no samples</div>;
364
-
365
- const flatColumn = table.getChild(FIELD_FLAT);
366
- const flatDiffColumn = table.getChild(FIELD_FLAT_DIFF);
367
- const cumulativeColumn = table.getChild(FIELD_CUMULATIVE);
368
- const cumulativeDiffColumn = table.getChild(FIELD_CUMULATIVE_DIFF);
369
- const functionNameColumn = table.getChild(FIELD_FUNCTION_NAME);
370
- const functionSystemNameColumn = table.getChild(FIELD_FUNCTION_SYSTEM_NAME);
371
- const functionFileNameColumn = table.getChild(FIELD_FUNCTION_FILE_NAME);
372
- const mappingFileColumn = table.getChild(FIELD_MAPPING_FILE);
373
- const locationAddressColumn = table.getChild(FIELD_LOCATION_ADDRESS);
374
-
375
- const rows: row[] = [];
376
- // TODO: Figure out how to only read the data of the columns we need for the virtualized table
377
- for (let i = 0; i < table.numRows; i++) {
378
- const flat: bigint = flatColumn?.get(i) ?? 0n;
379
- const flatDiff: bigint = flatDiffColumn?.get(i) ?? 0n;
380
- const cumulative: bigint = cumulativeColumn?.get(i) ?? 0n;
381
- const cumulativeDiff: bigint = cumulativeDiffColumn?.get(i) ?? 0n;
382
- const functionSystemName: string = functionSystemNameColumn?.get(i) ?? '';
383
- const functionFileName: string = functionFileNameColumn?.get(i) ?? '';
384
- const mappingFile: string = mappingFileColumn?.get(i) ?? '';
385
- rows.push({
386
- name: RowName(mappingFileColumn, locationAddressColumn, functionNameColumn, i),
387
- flat,
388
- flatDiff,
389
- cumulative,
390
- cumulativeDiff,
391
- functionSystemName,
392
- functionFileName,
393
- mappingFile,
394
- });
681
+ if (rows.length === 0) {
682
+ return <div className="mx-auto text-center">Profile has no samples</div>;
395
683
  }
396
684
 
397
685
  return (
@@ -414,6 +702,22 @@ export const Table = React.memo(function Table({
414
702
  enableHighlighting={enableHighlighting}
415
703
  shouldHighlightRow={shouldHighlightRow}
416
704
  usePointerCursor={dashboardItems.length > 1}
705
+ onRowDoubleClick={onRowDoubleClick}
706
+ getSubRows={row => (isDummyRow(row) ? [] : row.subRows ?? [])}
707
+ getCustomExpandedRowModel={getTopAndBottomExpandedRowModel}
708
+ expandedState={expanded}
709
+ onExpandedChange={getNewState => {
710
+ // We only want the new expanded row so passing the exisitng state as empty
711
+ // @ts-expect-error
712
+ let newState = getNewState({});
713
+ if (Object.keys(newState)[0] === Object.keys(expanded)[0]) {
714
+ newState = {};
715
+ }
716
+ setExpanded(newState);
717
+ }}
718
+ CustomRowRenderer={CustomRowRenderer}
719
+ scrollToIndex={scrollToIndex}
720
+ estimatedRowHeight={ROW_HEIGHT}
417
721
  />
418
722
  </div>
419
723
  </div>
@@ -457,4 +761,38 @@ export const RowName = (
457
761
  return hexifyAddress(address);
458
762
  };
459
763
 
764
+ const getRowsCount = (rows: Array<RowType<Row>>): number => {
765
+ if (rows.length < 6) {
766
+ return 6;
767
+ }
768
+
769
+ return rows.length;
770
+ };
771
+
772
+ function getScrollTargetIndex(
773
+ rows: Array<RowType<Row>>,
774
+ parentRow: RowType<Row>,
775
+ newRow: RowType<Row>
776
+ ): number {
777
+ const parentIndex = rows.indexOf(parentRow);
778
+ const newRowIndex = rows.indexOf(newRow);
779
+ let targetIndex = newRowIndex;
780
+ if (parentIndex > newRowIndex) {
781
+ // Adjusting the number of subs rows to scroll to the main row after expansion.
782
+ targetIndex -= getRowsCount(newRow.subRows);
783
+ }
784
+ if (parentIndex < newRowIndex) {
785
+ // If the parent row is above the new row, we need to adjust the number of subrows of the parent.
786
+ targetIndex += getRowsCount(parentRow.subRows);
787
+ }
788
+ if (targetIndex < 0) {
789
+ targetIndex = 0;
790
+ }
791
+ return targetIndex;
792
+ }
793
+
794
+ function isSubRow(row: Row): boolean {
795
+ return row.isTopSubRow === true || row.isBottomSubRow === true;
796
+ }
797
+
460
798
  export default Table;