@parca/profile 0.16.267 → 0.16.269

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.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,14 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [0.16.269](https://github.com/parca-dev/parca/compare/@parca/profile@0.16.268...@parca/profile@0.16.269) (2023-10-04)
7
+
8
+ **Note:** Version bump only for package @parca/profile
9
+
10
+ ## 0.16.268 (2023-10-01)
11
+
12
+ **Note:** Version bump only for package @parca/profile
13
+
6
14
  ## [0.16.267](https://github.com/parca-dev/parca/compare/@parca/profile@0.16.266...@parca/profile@0.16.267) (2023-09-24)
7
15
 
8
16
  **Note:** Version bump only for package @parca/profile
@@ -54,10 +54,11 @@ const TooltipMetaInfo = ({ table,
54
54
  // total,
55
55
  // totalUnfiltered,
56
56
  onCopy, row, navigateTo, }) => {
57
- const { labelPairs, functionFilename, file, openFile, isSourceAvailable, locationAddress, mappingFile, mappingBuildID, } = useGraphTooltipMetaInfo({ table, row, navigateTo });
57
+ const { labelPairs, functionFilename, file, openFile, isSourceAvailable, locationAddress, mappingFile, mappingBuildID, inlined, } = useGraphTooltipMetaInfo({ table, row, navigateTo });
58
58
  const { enableSourcesView } = useParcaContext();
59
59
  const labels = labelPairs.map((l) => (_jsx("span", { className: "mr-3 inline-block rounded-lg bg-gray-200 px-2 py-1 text-xs font-bold text-gray-700 dark:bg-gray-700 dark:text-gray-400", children: `${l[0]}="${l[1]}"` }, l[0])));
60
60
  const isMappingBuildIDAvailable = mappingBuildID !== null && mappingBuildID !== '';
61
- return (_jsxs(_Fragment, { children: [_jsxs("tr", { children: [_jsx("td", { className: "w-1/4", children: "File" }), _jsx("td", { className: "w-3/4 break-all", children: functionFilename === '' ? (_jsx(NoData, {})) : (_jsxs("div", { className: "flex gap-4", children: [_jsx(CopyToClipboard, { onCopy: onCopy, text: file, children: _jsx("button", { className: "cursor-pointer whitespace-nowrap text-left", children: _jsx(ExpandOnHover, { value: file, displayValue: truncateStringReverse(file, 30) }) }) }), _jsxs("div", { className: cx('flex gap-2', { hidden: enableSourcesView === false }), children: [_jsx("div", { "data-tooltip-id": "open-source-button-help", "data-tooltip-content": "There is no source code uploaded for this build", children: _jsx(Button, { variant: 'neutral', onClick: () => openFile(), className: "shrink-0", disabled: !isSourceAvailable, children: "open" }) }), !isSourceAvailable ? _jsx(Tooltip, { id: "open-source-button-help" }) : null] })] })) })] }), _jsxs("tr", { children: [_jsx("td", { className: "w-1/4", children: "Address" }), _jsx("td", { className: "w-3/4 break-all", children: locationAddress === 0n ? (_jsx(NoData, {})) : (_jsx(CopyToClipboard, { onCopy: onCopy, text: hexifyAddress(locationAddress), children: _jsx("button", { className: "cursor-pointer", children: hexifyAddress(locationAddress) }) })) })] }), _jsxs("tr", { children: [_jsx("td", { className: "w-1/4", children: "Binary" }), _jsx("td", { className: "w-3/4 break-all", children: mappingFile === null ? (_jsx(NoData, {})) : (_jsx(CopyToClipboard, { onCopy: onCopy, text: mappingFile, children: _jsx("button", { className: "cursor-pointer", children: getLastItem(mappingFile) }) })) })] }), _jsxs("tr", { children: [_jsx("td", { className: "w-1/4", children: "Build Id" }), _jsx("td", { className: "w-3/4 break-all", children: !isMappingBuildIDAvailable ? (_jsx(NoData, {})) : (_jsx(CopyToClipboard, { onCopy: onCopy, text: mappingBuildID, children: _jsx("button", { className: "cursor-pointer", children: truncateString(getLastItem(mappingBuildID), 28) }) })) })] }), labelPairs.length > 0 && (_jsxs("tr", { children: [_jsx("td", { className: "w-1/4", children: "Labels" }), _jsx("td", { className: "w-3/4 break-all", children: labels })] }))] }));
61
+ const inlinedText = inlined === null ? 'merged' : inlined ? 'yes' : 'no';
62
+ return (_jsxs(_Fragment, { children: [_jsxs("tr", { children: [_jsx("td", { className: "w-1/4", children: "File" }), _jsx("td", { className: "w-3/4 break-all", children: functionFilename === '' ? (_jsx(NoData, {})) : (_jsxs("div", { className: "flex gap-4", children: [_jsx(CopyToClipboard, { onCopy: onCopy, text: file, children: _jsx("button", { className: "cursor-pointer whitespace-nowrap text-left", children: _jsx(ExpandOnHover, { value: file, displayValue: truncateStringReverse(file, 30) }) }) }), _jsxs("div", { className: cx('flex gap-2', { hidden: enableSourcesView === false }), children: [_jsx("div", { "data-tooltip-id": "open-source-button-help", "data-tooltip-content": "There is no source code uploaded for this build", children: _jsx(Button, { variant: 'neutral', onClick: () => openFile(), className: "shrink-0", disabled: !isSourceAvailable, children: "open" }) }), !isSourceAvailable ? _jsx(Tooltip, { id: "open-source-button-help" }) : null] })] })) })] }), _jsxs("tr", { children: [_jsx("td", { className: "w-1/4", children: "Address" }), _jsx("td", { className: "w-3/4 break-all", children: locationAddress === 0n ? (_jsx(NoData, {})) : (_jsx(CopyToClipboard, { onCopy: onCopy, text: hexifyAddress(locationAddress), children: _jsx("button", { className: "cursor-pointer", children: hexifyAddress(locationAddress) }) })) })] }), _jsxs("tr", { children: [_jsx("td", { className: "w-1/4", children: "Inlined" }), _jsx("td", { className: "w-3/4 break-all", children: _jsx(CopyToClipboard, { onCopy: onCopy, text: inlinedText, children: _jsx("button", { className: "cursor-pointer", children: inlinedText }) }) })] }), _jsxs("tr", { children: [_jsx("td", { className: "w-1/4", children: "Binary" }), _jsx("td", { className: "w-3/4 break-all", children: mappingFile === null ? (_jsx(NoData, {})) : (_jsx(CopyToClipboard, { onCopy: onCopy, text: mappingFile, children: _jsx("button", { className: "cursor-pointer", children: getLastItem(mappingFile) }) })) })] }), _jsxs("tr", { children: [_jsx("td", { className: "w-1/4", children: "Build Id" }), _jsx("td", { className: "w-3/4 break-all", children: !isMappingBuildIDAvailable ? (_jsx(NoData, {})) : (_jsx(CopyToClipboard, { onCopy: onCopy, text: mappingBuildID, children: _jsx("button", { className: "cursor-pointer", children: truncateString(getLastItem(mappingBuildID), 28) }) })) })] }), labelPairs.length > 0 && (_jsxs("tr", { children: [_jsx("td", { className: "w-1/4", children: "Labels" }), _jsx("td", { className: "w-3/4 break-all", children: labels })] }))] }));
62
63
  };
63
64
  export default GraphTooltipArrowContent;
@@ -48,16 +48,17 @@ export const DockedGraphTooltip = ({ table, unit, total, totalUnfiltered, row, l
48
48
  row,
49
49
  level,
50
50
  });
51
- const { labelPairs, functionFilename, file, openFile, isSourceAvailable, locationAddress, mappingFile, mappingBuildID, } = useGraphTooltipMetaInfo({ table, row: row ?? 0, navigateTo });
51
+ const { labelPairs, functionFilename, file, openFile, isSourceAvailable, locationAddress, mappingFile, mappingBuildID, inlined, } = useGraphTooltipMetaInfo({ table, row: row ?? 0, navigateTo });
52
52
  const [_, setIsDocked] = useUserPreference(USER_PREFERENCES.GRAPH_METAINFO_DOCKED.key);
53
53
  if (graphTooltipData === null) {
54
54
  return _jsx(_Fragment, {});
55
55
  }
56
56
  const { name, cumulativeText, diffText, diff } = graphTooltipData;
57
57
  const labels = labelPairs.map((l) => (_jsx("span", { className: "mr-3 inline-block rounded-lg bg-gray-200 px-2 py-1 text-xs font-bold text-gray-700 dark:bg-gray-700 dark:text-gray-400", children: `${l[0]}="${l[1]}"` }, l[0])));
58
+ const inlinedText = inlined === null ? 'merged' : inlined ? 'yes' : 'no';
58
59
  const addressText = locationAddress !== 0n ? hexifyAddress(locationAddress) : 'unknown';
59
60
  const fileText = functionFilename !== '' ? file : 'Not available';
60
61
  return (_jsx("div", { className: "fixed bottom-0 z-20 overflow-hidden rounded-t-lg border-l border-r border-t border-gray-400 bg-white bg-opacity-90 px-8 py-3 dark:border-gray-600 dark:bg-black dark:bg-opacity-80", style: { width }, children: _jsxs("div", { className: "flex flex-col gap-4", children: [_jsxs("div", { className: "flex justify-between gap-4", children: [row === 0 ? (_jsx("p", { children: "root" })) : (_jsx(_Fragment, { children: name !== '' ? (_jsx(CopyToClipboard, { onCopy: onCopy, text: name, children: _jsx("button", { className: "cursor-pointer text-left", children: name }) })) : (_jsx(_Fragment, { children: locationAddress !== 0n ? (_jsx(CopyToClipboard, { onCopy: onCopy, text: hexifyAddress(locationAddress), children: _jsx("button", { className: "cursor-pointer text-left", children: hexifyAddress(locationAddress) }) })) : (_jsx("p", { children: "unknown" })) })) })), _jsx(IconButton, { onClick: () => setIsDocked(false), icon: "mdi:dock-window", title: "Undock MetaInfo Panel" })] }), _jsxs("div", { className: "flex justify-between gap-3", children: [_jsx(InfoSection, { title: "Cumulative", value: cumulativeText, onCopy: onCopy, copyText: cumulativeText, minWidth: "w-44" }), diff !== 0n ? (_jsx(InfoSection, { title: "Diff", value: diffText, onCopy: onCopy, copyText: diffText, minWidth: "w-44" })) : null, _jsx(InfoSection, { title: "File", value: _jsxs("div", { className: "flex gap-2", children: [_jsx(ExpandOnHover, { value: fileText, displayValue: truncateStringReverse(fileText, 45) }), _jsxs("div", { className: cx('flex items-center gap-2', {
61
62
  hidden: enableSourcesView === false || functionFilename === '',
62
- }), children: [_jsx("div", { "data-tooltip-id": "open-source-button-help", "data-tooltip-content": "There is no source code uploaded for this build", children: _jsx(Button, { variant: 'neutral', onClick: () => openFile(), className: "shrink-0", disabled: !isSourceAvailable, children: "open" }) }), !isSourceAvailable ? _jsx(Tooltip, { id: "open-source-button-help" }) : null] })] }), onCopy: onCopy, copyText: file, minWidth: 'w-[460px]' }), _jsx(InfoSection, { title: "Address", value: addressText, onCopy: onCopy, copyText: addressText, minWidth: "w-44" }), _jsx(InfoSection, { title: "Binary", value: (mappingFile != null ? getLastItem(mappingFile) : null) ?? 'Not available', onCopy: onCopy, copyText: mappingFile ?? 'Not available', minWidth: "w-44" }), _jsx(InfoSection, { title: "Build ID", value: truncateString(getLastItem(mappingBuildID) ?? 'Not available', 28), onCopy: onCopy, copyText: mappingBuildID ?? 'Not available' })] }), _jsx("div", { children: _jsx("div", { className: "flex h-5 gap-1", children: labels }) }), _jsx("span", { className: "mx-2 block text-xs text-gray-500", children: isCopied ? 'Copied!' : 'Hold shift and click on a value to copy.' })] }) }));
63
+ }), children: [_jsx("div", { "data-tooltip-id": "open-source-button-help", "data-tooltip-content": "There is no source code uploaded for this build", children: _jsx(Button, { variant: 'neutral', onClick: () => openFile(), className: "shrink-0", disabled: !isSourceAvailable, children: "open" }) }), !isSourceAvailable ? _jsx(Tooltip, { id: "open-source-button-help" }) : null] })] }), onCopy: onCopy, copyText: file, minWidth: 'w-[460px]' }), _jsx(InfoSection, { title: "Address", value: addressText, onCopy: onCopy, copyText: addressText, minWidth: "w-44" }), _jsx(InfoSection, { title: "Inlined", value: inlinedText, onCopy: onCopy, copyText: inlinedText, minWidth: "w-44" }), _jsx(InfoSection, { title: "Binary", value: (mappingFile != null ? getLastItem(mappingFile) : null) ?? 'Not available', onCopy: onCopy, copyText: mappingFile ?? 'Not available', minWidth: "w-44" }), _jsx(InfoSection, { title: "Build ID", value: truncateString(getLastItem(mappingBuildID) ?? 'Not available', 28), onCopy: onCopy, copyText: mappingBuildID ?? 'Not available' })] }), _jsx("div", { children: _jsx("div", { className: "flex h-5 gap-1", children: labels }) }), _jsx("span", { className: "mx-2 block text-xs text-gray-500", children: isCopied ? 'Copied!' : 'Hold shift and click on a value to copy.' })] }) }));
63
64
  };
@@ -14,6 +14,7 @@ interface GraphTooltipMetaInfoData {
14
14
  locationAddress: bigint;
15
15
  mappingFile: string | null;
16
16
  mappingBuildID: string | null;
17
+ inlined: boolean | null;
17
18
  }
18
19
  export declare const useGraphTooltipMetaInfo: ({ table, row, navigateTo, }: Props) => GraphTooltipMetaInfoData;
19
20
  export {};
@@ -12,7 +12,7 @@
12
12
  // limitations under the License.
13
13
  import { QueryRequest_ReportType } from '@parca/client';
14
14
  import { useParcaContext, useURLState } from '@parca/components';
15
- import { FIELD_FUNCTION_FILE_NAME, FIELD_FUNCTION_START_LINE, FIELD_LOCATION_ADDRESS, FIELD_LOCATION_LINE, FIELD_MAPPING_BUILD_ID, FIELD_MAPPING_FILE, } from '../../ProfileIcicleGraph/IcicleGraphArrow';
15
+ import { FIELD_FUNCTION_FILE_NAME, FIELD_FUNCTION_START_LINE, FIELD_INLINED, FIELD_LOCATION_ADDRESS, FIELD_LOCATION_LINE, FIELD_MAPPING_BUILD_ID, FIELD_MAPPING_FILE, } from '../../ProfileIcicleGraph/IcicleGraphArrow';
16
16
  import { arrowToString } from '../../ProfileIcicleGraph/IcicleGraphArrow/utils';
17
17
  import { useProfileViewContext } from '../../ProfileView/ProfileViewContext';
18
18
  import { useQuery } from '../../useQuery';
@@ -20,6 +20,7 @@ export const useGraphTooltipMetaInfo = ({ table, row, navigateTo, }) => {
20
20
  const mappingFile = arrowToString(table.getChild(FIELD_MAPPING_FILE)?.get(row));
21
21
  const mappingBuildID = arrowToString(table.getChild(FIELD_MAPPING_BUILD_ID)?.get(row));
22
22
  const locationAddress = table.getChild(FIELD_LOCATION_ADDRESS)?.get(row) ?? 0n;
23
+ const inlined = table.getChild(FIELD_INLINED)?.get(row);
23
24
  const locationLine = table.getChild(FIELD_LOCATION_LINE)?.get(row) ?? 0n;
24
25
  const functionFilename = arrowToString(table.getChild(FIELD_FUNCTION_FILE_NAME)?.get(row)) ?? '';
25
26
  const functionStartLine = table.getChild(FIELD_FUNCTION_START_LINE)?.get(row) ?? 0n;
@@ -90,5 +91,6 @@ export const useGraphTooltipMetaInfo = ({ table, row, navigateTo, }) => {
90
91
  locationAddress,
91
92
  mappingBuildID,
92
93
  mappingFile,
94
+ inlined,
93
95
  };
94
96
  };
@@ -6,6 +6,7 @@ export declare const FIELD_MAPPING_FILE = "mapping_file";
6
6
  export declare const FIELD_MAPPING_BUILD_ID = "mapping_build_id";
7
7
  export declare const FIELD_LOCATION_ADDRESS = "location_address";
8
8
  export declare const FIELD_LOCATION_LINE = "location_line";
9
+ export declare const FIELD_INLINED = "inlined";
9
10
  export declare const FIELD_FUNCTION_NAME = "function_name";
10
11
  export declare const FIELD_FUNCTION_FILE_NAME = "function_file_name";
11
12
  export declare const FIELD_FUNCTION_START_LINE = "function_startline";
@@ -27,6 +27,7 @@ export const FIELD_MAPPING_FILE = 'mapping_file';
27
27
  export const FIELD_MAPPING_BUILD_ID = 'mapping_build_id';
28
28
  export const FIELD_LOCATION_ADDRESS = 'location_address';
29
29
  export const FIELD_LOCATION_LINE = 'location_line';
30
+ export const FIELD_INLINED = 'inlined';
30
31
  export const FIELD_FUNCTION_NAME = 'function_name';
31
32
  export const FIELD_FUNCTION_FILE_NAME = 'function_file_name';
32
33
  export const FIELD_FUNCTION_START_LINE = 'function_startline';
@@ -19,7 +19,7 @@ import { USER_PREFERENCES, useUserPreference } from '@parca/hooks';
19
19
  import { capitalizeOnlyFirstLetter, divide, selectQueryParam, } from '@parca/utilities';
20
20
  import DiffLegend from '../components/DiffLegend';
21
21
  import IcicleGraph from './IcicleGraph';
22
- import IcicleGraphArrow, { FIELD_CUMULATIVE, FIELD_DIFF, FIELD_FUNCTION_NAME, FIELD_LABELS, } from './IcicleGraphArrow';
22
+ import IcicleGraphArrow, { FIELD_CUMULATIVE, FIELD_DIFF, FIELD_FUNCTION_FILE_NAME, FIELD_FUNCTION_NAME, FIELD_LABELS, FIELD_LOCATION_ADDRESS, FIELD_MAPPING_FILE, } from './IcicleGraphArrow';
23
23
  const numberFormatter = new Intl.NumberFormat('en-US');
24
24
  const ErrorContent = ({ errorMessage }) => {
25
25
  return _jsx("div", { className: "flex justify-center p-10", children: errorMessage });
@@ -129,6 +129,24 @@ const groupByOptions = [
129
129
  description: 'Stacktraces are grouped by pprof labels.',
130
130
  disabled: false,
131
131
  },
132
+ {
133
+ value: FIELD_FUNCTION_FILE_NAME,
134
+ label: 'Filename',
135
+ description: 'Stacktraces are grouped by filenames.',
136
+ disabled: false,
137
+ },
138
+ {
139
+ value: FIELD_LOCATION_ADDRESS,
140
+ label: 'Address',
141
+ description: 'Stacktraces are grouped by addresses.',
142
+ disabled: false,
143
+ },
144
+ {
145
+ value: FIELD_MAPPING_FILE,
146
+ label: 'Binary',
147
+ description: 'Stacktraces are grouped by binaries.',
148
+ disabled: false,
149
+ },
132
150
  ];
133
151
  const GroupByDropdown = ({ groupBy, toggleGroupBy, }) => {
134
152
  const label = groupBy.length === 0
@@ -142,7 +142,7 @@ export const ProfileView = ({ total, filtered, flamegraphData, topTableData, cal
142
142
  dimensions?.width !== undefined ? (_jsx(Callgraph, { data: callgraphData.data, svgString: callgraphSVG, sampleUnit: sampleUnit, width: isHalfScreen ? dimensions?.width / 2 : dimensions?.width })) : (_jsx(_Fragment, {}));
143
143
  }
144
144
  case 'table': {
145
- return topTableData != null ? (_jsx(Table, { loading: topTableData.loading, data: topTableData.arrow?.record, sampleUnit: sampleUnit, navigateTo: navigateTo, setActionButtons: setActionButtons, currentSearchString: currentSearchString })) : (_jsx(_Fragment, {}));
145
+ return topTableData != null ? (_jsx(Table, { total: total, filtered: filtered, loading: topTableData.loading, data: topTableData.arrow?.record, sampleUnit: sampleUnit, navigateTo: navigateTo, setActionButtons: setActionButtons, currentSearchString: currentSearchString })) : (_jsx(_Fragment, {}));
146
146
  }
147
147
  case 'source': {
148
148
  return sourceData != null ? (_jsx(SourceView, { loading: sourceData.loading, data: sourceData.data, total: total, filtered: filtered, setActionButtons: setActionButtons })) : (_jsx(_Fragment, {}));
@@ -1,8 +1,10 @@
1
1
  import React from 'react';
2
- import { Table as ArrowTable } from 'apache-arrow';
2
+ import { Vector } from 'apache-arrow';
3
3
  import { type NavigateFunction } from '@parca/utilities';
4
4
  interface TableProps {
5
5
  data?: Uint8Array;
6
+ total: bigint;
7
+ filtered: bigint;
6
8
  sampleUnit: string;
7
9
  navigateTo?: NavigateFunction;
8
10
  loading: boolean;
@@ -10,5 +12,5 @@ interface TableProps {
10
12
  setActionButtons?: (buttons: React.JSX.Element) => void;
11
13
  }
12
14
  export declare const Table: React.NamedExoticComponent<TableProps>;
13
- export declare const RowName: (table: ArrowTable, row: number) => string;
15
+ export declare const RowName: (mappingFileColumn: Vector | null, locationAddressColumn: Vector | null, functionNameColumn: Vector | null, row: number) => string;
14
16
  export default Table;
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  // Copyright 2022 The Parca Authors
3
3
  // Licensed under the Apache License, Version 2.0 (the "License");
4
4
  // you may not use this file except in compliance with the License.
@@ -11,8 +11,9 @@ import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
11
11
  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
12
  // See the License for the specific language governing permissions and
13
13
  // limitations under the License.
14
- import React, { useCallback, useEffect, useMemo } from 'react';
15
- import { createColumnHelper } from '@tanstack/react-table';
14
+ import React, { Fragment, useCallback, useEffect, useMemo, useState } from 'react';
15
+ import { Menu, Transition } from '@headlessui/react';
16
+ import { Icon } from '@iconify/react';
16
17
  import { tableFromIPC } from 'apache-arrow';
17
18
  import { Button, Table as TableComponent, useURLState } from '@parca/components';
18
19
  import { getLastItem, isSearchMatch, parseParams, valueFormatter, } from '@parca/utilities';
@@ -20,12 +21,13 @@ import { hexifyAddress } from '../utils';
20
21
  const FIELD_MAPPING_FILE = 'mapping_file';
21
22
  const FIELD_LOCATION_ADDRESS = 'location_address';
22
23
  const FIELD_FUNCTION_NAME = 'function_name';
24
+ const FIELD_FUNCTION_SYSTEM_NAME = 'function_system_name';
25
+ const FIELD_FUNCTION_FILE_NAME = 'function_file_name';
23
26
  const FIELD_FLAT = 'flat';
24
27
  const FIELD_FLAT_DIFF = 'flat_diff';
25
28
  const FIELD_CUMULATIVE = 'cumulative';
26
29
  const FIELD_CUMULATIVE_DIFF = 'cumulative_diff';
27
- const columnHelper = createColumnHelper();
28
- export const Table = React.memo(function Table({ data, sampleUnit: unit, navigateTo, loading, currentSearchString, setActionButtons, }) {
30
+ export const Table = React.memo(function Table({ data, total, filtered, sampleUnit: unit, navigateTo, loading, currentSearchString, setActionButtons, }) {
29
31
  const router = parseParams(window?.location.search);
30
32
  const [rawDashboardItems] = useURLState({ param: 'dashboard_items' });
31
33
  const [rawcompareMode] = useURLState({ param: 'compare_a' });
@@ -37,51 +39,149 @@ export const Table = React.memo(function Table({ data, sampleUnit: unit, navigat
37
39
  }
38
40
  return ['icicle'];
39
41
  }, [rawDashboardItems]);
42
+ const percentageString = (value, total) => {
43
+ if (total === 0n) {
44
+ return '0%';
45
+ }
46
+ const percentage = (Number(value) / Number(total)) * 100;
47
+ return `${percentage.toFixed(2)}%`;
48
+ };
49
+ const ratioString = (value) => {
50
+ if (filtered === 0n) {
51
+ return ` ${percentageString(value, total)}`;
52
+ }
53
+ return `${percentageString(value, total)} / ${percentageString(value, filtered)}`;
54
+ };
40
55
  const columns = useMemo(() => {
41
- const cols = [
42
- columnHelper.accessor('flat', {
43
- header: () => 'Flat',
56
+ return [
57
+ {
58
+ id: 'flat',
59
+ accessorKey: 'flat',
60
+ header: 'Flat',
44
61
  cell: info => valueFormatter(info.getValue(), unit, 2),
45
62
  size: 80,
46
63
  meta: {
47
64
  align: 'right',
48
65
  },
49
66
  invertSorting: true,
50
- }),
51
- columnHelper.accessor('flatDiff', {
52
- header: () => 'Flat Diff',
67
+ },
68
+ {
69
+ id: 'flatPercentage',
70
+ accessorKey: 'flat',
71
+ header: 'Flat (%)',
72
+ cell: info => ratioString(info.getValue()),
73
+ size: 120,
74
+ meta: {
75
+ align: 'right',
76
+ },
77
+ invertSorting: true,
78
+ },
79
+ {
80
+ id: 'flatDiff',
81
+ accessorKey: 'flatDiff',
82
+ header: 'Flat Diff',
53
83
  cell: info => addPlusSign(valueFormatter(info.getValue(), unit, 2)),
54
84
  size: 120,
55
85
  meta: {
56
86
  align: 'right',
57
87
  },
58
88
  invertSorting: true,
59
- }),
60
- columnHelper.accessor('cumulative', {
61
- header: () => 'Cumulative',
89
+ },
90
+ {
91
+ id: 'flatDiffPercentage',
92
+ accessorKey: 'flatDiff',
93
+ header: 'Flat Diff (%)',
94
+ cell: info => ratioString(info.getValue()),
95
+ size: 120,
96
+ meta: {
97
+ align: 'right',
98
+ },
99
+ invertSorting: true,
100
+ },
101
+ {
102
+ id: 'cumulative',
103
+ accessorKey: 'cumulative',
104
+ header: 'Cumulative',
62
105
  cell: info => valueFormatter(info.getValue(), unit, 2),
63
- size: 130,
106
+ size: 150,
64
107
  meta: {
65
108
  align: 'right',
66
109
  },
67
110
  invertSorting: true,
68
- }),
69
- columnHelper.accessor('cumulativeDiff', {
70
- header: () => 'Cumulative Diff',
111
+ },
112
+ {
113
+ id: 'cumulativePercentage',
114
+ accessorKey: 'cumulative',
115
+ header: 'Cumulative (%)',
116
+ cell: info => ratioString(info.getValue()),
117
+ size: 150,
118
+ meta: {
119
+ align: 'right',
120
+ },
121
+ invertSorting: true,
122
+ },
123
+ {
124
+ id: 'cumulativeDiff',
125
+ accessorKey: 'cumulativeDiff',
126
+ header: 'Cumulative Diff',
71
127
  cell: info => addPlusSign(valueFormatter(info.getValue(), unit, 2)),
72
128
  size: 170,
73
129
  meta: {
74
130
  align: 'right',
75
131
  },
76
132
  invertSorting: true,
77
- }),
78
- columnHelper.accessor('name', {
79
- header: () => _jsx("span", { className: "text-left", children: "Name" }),
133
+ },
134
+ {
135
+ id: 'cumulativeDiffPercentage',
136
+ accessorKey: 'cumulativeDiff',
137
+ header: 'Cumulative Diff (%)',
138
+ cell: info => ratioString(info.getValue()),
139
+ size: 170,
140
+ meta: {
141
+ align: 'right',
142
+ },
143
+ invertSorting: true,
144
+ },
145
+ {
146
+ id: 'name',
147
+ accessorKey: 'name',
148
+ header: 'Name',
80
149
  cell: info => info.getValue(),
81
- }),
150
+ },
151
+ {
152
+ id: 'functionSystemName',
153
+ accessorKey: 'functionSystemName',
154
+ header: 'Function System Name',
155
+ },
156
+ {
157
+ id: 'functionFileName',
158
+ accessorKey: 'functionFileName',
159
+ header: 'Function File Name',
160
+ },
161
+ {
162
+ id: 'mappingFile',
163
+ accessorKey: 'mappingFile',
164
+ header: 'Mapping File',
165
+ },
82
166
  ];
83
- return cols;
167
+ // eslint-disable-next-line react-hooks/exhaustive-deps
84
168
  }, [unit]);
169
+ const [columnVisibility, setColumnVisibility] = useState(() => {
170
+ return {
171
+ flat: true,
172
+ flatPercentage: false,
173
+ flatDiff: compareMode,
174
+ flatDiffPercentage: false,
175
+ cumulative: true,
176
+ cumulativePercentage: false,
177
+ cumulativeDiff: compareMode,
178
+ cumulativeDiffPercentage: false,
179
+ name: true,
180
+ functionSystemName: false,
181
+ functionFileName: false,
182
+ mappingFile: false,
183
+ };
184
+ });
85
185
  const selectSpan = useCallback((span) => {
86
186
  if (navigateTo != null) {
87
187
  navigateTo('/', {
@@ -116,8 +216,17 @@ export const Table = React.memo(function Table({ data, sampleUnit: unit, navigat
116
216
  if (setActionButtons === undefined) {
117
217
  return;
118
218
  }
119
- setActionButtons(dashboardItems.length > 1 ? (_jsx(Button, { color: "neutral", onClick: clearSelection, className: "w-auto", variant: "neutral", disabled: currentSearchString === undefined || currentSearchString.length === 0, children: "Clear selection" })) : (_jsx(_Fragment, {})));
120
- }, [dashboardItems, clearSelection, currentSearchString, setActionButtons]);
219
+ setActionButtons(_jsxs(_Fragment, { children: [_jsx(ColumnsVisibility, { columns: columns, visibility: columnVisibility, setVisibility: (id, visible) => {
220
+ setColumnVisibility({ ...columnVisibility, [id]: visible });
221
+ } }), dashboardItems.length > 1 && (_jsx(Button, { color: "neutral", onClick: clearSelection, className: "w-auto", variant: "neutral", disabled: currentSearchString === undefined || currentSearchString.length === 0, children: "Clear selection" }))] }));
222
+ }, [
223
+ dashboardItems,
224
+ clearSelection,
225
+ currentSearchString,
226
+ setActionButtons,
227
+ columns,
228
+ columnVisibility,
229
+ ]);
121
230
  const initialSorting = useMemo(() => {
122
231
  return [
123
232
  {
@@ -126,27 +235,22 @@ export const Table = React.memo(function Table({ data, sampleUnit: unit, navigat
126
235
  },
127
236
  ];
128
237
  }, [compareMode]);
129
- const columnVisibility = useMemo(() => {
130
- // TODO: Make this configurable via the UI and add more columns.
131
- return {
132
- flat: true,
133
- flatDiff: compareMode,
134
- cumulative: true,
135
- cumulativeDiff: compareMode,
136
- name: true,
137
- };
138
- }, [compareMode]);
139
238
  if (loading)
140
239
  return _jsx("div", { className: "mx-auto text-center", children: "Loading..." });
141
240
  if (data === undefined)
142
241
  return _jsx("div", { className: "mx-auto text-center", children: "Profile has no samples" });
143
242
  const table = tableFromIPC(data);
243
+ if (table.numRows === 0)
244
+ return _jsx("div", { className: "mx-auto text-center", children: "Profile has no samples" });
144
245
  const flatColumn = table.getChild(FIELD_FLAT);
145
246
  const flatDiffColumn = table.getChild(FIELD_FLAT_DIFF);
146
247
  const cumulativeColumn = table.getChild(FIELD_CUMULATIVE);
147
248
  const cumulativeDiffColumn = table.getChild(FIELD_CUMULATIVE_DIFF);
148
- if (table.numRows === 0)
149
- return _jsx("div", { className: "mx-auto text-center", children: "Profile has no samples" });
249
+ const functionNameColumn = table.getChild(FIELD_FUNCTION_NAME);
250
+ const functionSystemNameColumn = table.getChild(FIELD_FUNCTION_SYSTEM_NAME);
251
+ const functionFileNameColumn = table.getChild(FIELD_FUNCTION_FILE_NAME);
252
+ const mappingFileColumn = table.getChild(FIELD_MAPPING_FILE);
253
+ const locationAddressColumn = table.getChild(FIELD_LOCATION_ADDRESS);
150
254
  const rows = [];
151
255
  // TODO: Figure out how to only read the data of the columns we need for the virtualized table
152
256
  for (let i = 0; i < table.numRows; i++) {
@@ -154,24 +258,34 @@ export const Table = React.memo(function Table({ data, sampleUnit: unit, navigat
154
258
  const flatDiff = flatDiffColumn?.get(i) ?? 0n;
155
259
  const cumulative = cumulativeColumn?.get(i) ?? 0n;
156
260
  const cumulativeDiff = cumulativeDiffColumn?.get(i) ?? 0n;
261
+ const functionSystemName = functionSystemNameColumn?.get(i) ?? '';
262
+ const functionFileName = functionFileNameColumn?.get(i) ?? '';
263
+ const mappingFile = mappingFileColumn?.get(i) ?? '';
157
264
  rows.push({
158
- name: RowName(table, i),
265
+ name: RowName(mappingFileColumn, locationAddressColumn, functionNameColumn, i),
159
266
  flat,
160
267
  flatDiff,
161
268
  cumulative,
162
269
  cumulativeDiff,
270
+ functionSystemName,
271
+ functionFileName,
272
+ mappingFile,
163
273
  });
164
274
  }
165
275
  return (_jsx("div", { className: "relative", children: _jsx("div", { className: "font-robotoMono h-[80vh] w-full", children: _jsx(TableComponent, { data: rows, columns: columns, initialSorting: initialSorting, columnVisibility: columnVisibility, onRowClick: onRowClick, enableHighlighting: enableHighlighting, shouldHighlightRow: shouldHighlightRow, usePointerCursor: dashboardItems.length > 1 }) }) }));
166
276
  });
277
+ const ColumnsVisibility = ({ columns, visibility, setVisibility, }) => {
278
+ return (_jsx("div", { children: _jsxs(Menu, { as: "div", className: "relative text-left", children: [_jsx("div", { children: _jsxs(Menu.Button, { className: "relative w-full cursor-default rounded-md border bg-white py-2 pl-3 pr-10 text-left text-sm shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 dark:border-gray-600 dark:bg-gray-900 sm:text-sm", children: [_jsx("span", { className: "ml-3 block overflow-x-hidden text-ellipsis", children: "Columns" }), _jsx("span", { className: "pointer-events-none absolute inset-y-0 right-0 ml-3 flex items-center pr-2 text-gray-400", children: _jsx(Icon, { icon: "heroicons:chevron-down-20-solid", "aria-hidden": "true" }) })] }) }), _jsx(Transition, { as: Fragment, leave: "transition ease-in duration-100", leaveFrom: "opacity-100", leaveTo: "opacity-0", children: _jsx(Menu.Items, { className: "absolute left-0 z-10 mt-1 min-w-[400px] overflow-auto rounded-md bg-gray-50 py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none dark:border-gray-600 dark:bg-gray-900 dark:ring-white dark:ring-opacity-20 sm:text-sm", children: _jsx("div", { className: "p-4", children: _jsx("fieldset", { children: _jsx("div", { className: "space-y-5", children: columns.map(col => (_jsxs("div", { className: "relative flex items-start", children: [_jsx("div", { className: "flex h-6 items-center", children: _jsx("input", { id: col.id, name: col.id, type: "checkbox", className: "h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600", checked: visibility[col.id ?? ''] ?? false, onChange: () => {
279
+ setVisibility(col.id ?? '', !visibility[col.id ?? '']);
280
+ } }) }), _jsx("div", { className: "ml-3 text-sm leading-6", children: _jsx("label", { htmlFor: col.id, className: "font-medium text-gray-900", children: col.header }) })] }, col.id))) }) }) }) }) })] }) }));
281
+ };
167
282
  const addPlusSign = (num) => {
168
283
  if (num.charAt(0) === '0' || num.charAt(0) === '-') {
169
284
  return num;
170
285
  }
171
286
  return `+${num}`;
172
287
  };
173
- export const RowName = (table, row) => {
174
- const mappingFileColumn = table.getChild(FIELD_MAPPING_FILE);
288
+ export const RowName = (mappingFileColumn, locationAddressColumn, functionNameColumn, row) => {
175
289
  if (mappingFileColumn === null) {
176
290
  console.error('mapping_file column not found');
177
291
  return '';
@@ -182,11 +296,11 @@ export const RowName = (table, row) => {
182
296
  if (mappingFile != null && mappingFileColumn.data.length > 1) {
183
297
  mapping = `[${getLastItem(mappingFile) ?? ''}]`;
184
298
  }
185
- const functionName = table.getChild(FIELD_FUNCTION_NAME)?.get(row) ?? '';
299
+ const functionName = functionNameColumn?.get(row) ?? '';
186
300
  if (functionName !== null && functionName !== '') {
187
301
  return `${mapping} ${functionName}`;
188
302
  }
189
- const address = table.getChild(FIELD_LOCATION_ADDRESS)?.get(row) ?? 0;
303
+ const address = locationAddressColumn?.get(row) ?? 0;
190
304
  return hexifyAddress(address);
191
305
  };
192
306
  export default Table;
package/dist/styles.css CHANGED
@@ -1 +1 @@
1
- /*! tailwindcss v3.2.4 | MIT License | https://tailwindcss.com*/*,:after,:before{border:0 solid #e5e7eb;box-sizing:border-box}:after,:before{--tw-content:""}html{-webkit-text-size-adjust:100%;font-feature-settings:normal;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4}body{line-height:inherit;margin:0}hr{border-top-width:1px;color:inherit;height:0}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{border-collapse:collapse;border-color:inherit;text-indent:0}button,input,optgroup,select,textarea{color:inherit;font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{color:#9ca3af;opacity:1}input::placeholder,textarea::placeholder{color:#9ca3af;opacity:1}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{height:auto;max-width:100%}[hidden]{display:none}*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.pointer-events-none{pointer-events:none}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.-inset-2{bottom:-.5rem;left:-.5rem;right:-.5rem;top:-.5rem}.inset-y-0{bottom:0;top:0}.left-\[25px\]{left:25px}.left-0{left:0}.top-\[-46px\]{top:-46px}.right-0{right:0}.bottom-0{bottom:0}.z-50{z-index:50}.z-10{z-index:10}.z-20{z-index:20}.m-auto{margin:auto}.m-2{margin:.5rem}.mx-auto{margin-left:auto;margin-right:auto}.mx-2{margin-left:.5rem;margin-right:.5rem}.my-2{margin-bottom:.5rem;margin-top:.5rem}.my-20{margin-bottom:5rem;margin-top:5rem}.my-6{margin-bottom:1.5rem;margin-top:1.5rem}.my-4{margin-bottom:1rem;margin-top:1rem}.mr-3{margin-right:.75rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.ml-2{margin-left:.5rem}.ml-3{margin-left:.75rem}.mb-2{margin-bottom:.5rem}.mb-4{margin-bottom:1rem}.mr-6{margin-right:1.5rem}.mr-1{margin-right:.25rem}.mb-1{margin-bottom:.25rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-8{margin-top:2rem}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-fit{height:-moz-fit-content;height:fit-content}.h-10{height:2.5rem}.h-96{height:24rem}.h-6{height:1.5rem}.h-4{height:1rem}.h-full{height:100%}.h-1{height:.25rem}.h-\[80vh\]{height:80vh}.h-5{height:1.25rem}.max-h-\[400px\]{max-height:400px}.min-h-52{min-height:13rem}.min-h-48{min-height:12rem}.min-h-\[200px\]{min-height:200px}.w-full{width:100%}.w-auto{width:auto}.w-1\/4{width:25%}.w-3\/4{width:75%}.w-\[500px\]{width:500px}.w-4{width:1rem}.w-40{width:10rem}.w-3{width:.75rem}.w-5{width:1.25rem}.w-7{width:1.75rem}.w-9{width:2.25rem}.w-11{width:2.75rem}.w-\[52px\]{width:52px}.w-\[68px\]{width:68px}.w-\[76px\]{width:76px}.w-\[84px\]{width:84px}.w-\[92px\]{width:92px}.w-\[100px\]{width:100px}.w-\[108px\]{width:108px}.w-\[116px\]{width:116px}.w-8{width:2rem}.w-44{width:11rem}.w-\[460px\]{width:460px}.w-1\/5{width:20%}.w-11\/12{width:91.666667%}.w-1\/12{width:8.333333%}.w-16{width:4rem}.w-fit{width:-moz-fit-content;width:fit-content}.w-\[420px\]{width:420px}.min-w-\[300px\]{min-width:300px}.min-w-\[400px\]{min-width:400px}.max-w-\[500px\]{max-width:500px}.max-w-\[300px\]{max-width:300px}.max-w-md{max-width:28rem}.flex-1{flex:1 1 0%}.shrink-0{flex-shrink:0}.shrink{flex-shrink:1}.flex-grow-0{flex-grow:0}.flex-grow{flex-grow:1}.table-auto{table-layout:auto}.table-fixed{table-layout:fixed}.translate-y-1{--tw-translate-y:0.25rem}.translate-y-0,.translate-y-1{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-y-0{--tw-translate-y:0px}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.cursor-pointer{cursor:pointer}.cursor-default{cursor:default}.cursor-not-allowed{cursor:not-allowed}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-row{flex-direction:row}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.content-start{align-content:flex-start}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.justify-start{justify-content:flex-start}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-4{gap:1rem}.gap-2{gap:.5rem}.gap-1{gap:.25rem}.gap-3{gap:.75rem}.gap-x-2{-moz-column-gap:.5rem;column-gap:.5rem}.space-y-5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(1.25rem*var(--tw-space-y-reverse));margin-top:calc(1.25rem*(1 - var(--tw-space-y-reverse)))}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-scroll{overflow:scroll}.overflow-x-hidden{overflow-x:hidden}.text-ellipsis{text-overflow:ellipsis}.whitespace-normal{white-space:normal}.whitespace-nowrap{white-space:nowrap}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-none{border-radius:0}.rounded-t-lg{border-top-left-radius:.5rem;border-top-right-radius:.5rem}.rounded-l{border-bottom-left-radius:.25rem;border-top-left-radius:.25rem}.rounded-r{border-bottom-right-radius:.25rem;border-top-right-radius:.25rem}.border{border-width:1px}.border-r{border-right-width:1px}.border-l{border-left-width:1px}.border-t{border-top-width:1px}.border-r-0{border-right-width:0}.border-l-0{border-left-width:0}.border-gray-300{--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity))}.border-red-400{--tw-border-opacity:1;border-color:rgb(248 113 113/var(--tw-border-opacity))}.border-gray-200{--tw-border-opacity:1;border-color:rgb(229 231 235/var(--tw-border-opacity))}.border-gray-400{--tw-border-opacity:1;border-color:rgb(156 163 175/var(--tw-border-opacity))}.border-r-gray-200{--tw-border-opacity:1;border-right-color:rgb(229 231 235/var(--tw-border-opacity))}.border-l-amber-900{--tw-border-opacity:1;border-left-color:rgb(120 53 15/var(--tw-border-opacity))}.bg-gray-50{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity))}.bg-gray-200{--tw-bg-opacity:1;background-color:rgb(229 231 235/var(--tw-bg-opacity))}.bg-indigo-600{--tw-bg-opacity:1;background-color:rgb(79 70 229/var(--tw-bg-opacity))}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.bg-red-100{--tw-bg-opacity:1;background-color:rgb(254 226 226/var(--tw-bg-opacity))}.bg-black{--tw-bg-opacity:1;background-color:rgb(0 0 0/var(--tw-bg-opacity))}.bg-yellow-200{--tw-bg-opacity:1;background-color:rgb(254 240 138/var(--tw-bg-opacity))}.bg-yellow-700{--tw-bg-opacity:1;background-color:rgb(161 98 7/var(--tw-bg-opacity))}.bg-gray-800{--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity))}.bg-inherit{background-color:inherit}.bg-opacity-90{--tw-bg-opacity:0.9}.fill-transparent{fill:transparent}.fill-current{fill:currentColor}.stroke-gray-300{stroke:#d1d5db}.stroke-white{stroke:#fff}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-10{padding:2.5rem}.p-4{padding:1rem}.p-1{padding:.25rem}.px-2{padding-left:.5rem;padding-right:.5rem}.py-1{padding-bottom:.25rem;padding-top:.25rem}.py-2{padding-bottom:.5rem;padding-top:.5rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-4{padding-bottom:1rem;padding-top:1rem}.px-4{padding-left:1rem;padding-right:1rem}.py-3{padding-bottom:.75rem;padding-top:.75rem}.px-3{padding-left:.75rem;padding-right:.75rem}.py-0{padding-bottom:0;padding-top:0}.px-1{padding-left:.25rem;padding-right:.25rem}.px-8{padding-left:2rem;padding-right:2rem}.pr-0{padding-right:0}.pl-3{padding-left:.75rem}.pr-9{padding-right:2.25rem}.pt-2{padding-top:.5rem}.pb-4{padding-bottom:1rem}.pb-2{padding-bottom:.5rem}.pr-10{padding-right:2.5rem}.pr-2{padding-right:.5rem}.pl-1{padding-left:.25rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xs{font-size:.75rem;line-height:1rem}.text-base{font-size:1rem;line-height:1.5rem}.text-xl{font-size:1.25rem}.text-lg,.text-xl{line-height:1.75rem}.text-lg{font-size:1.125rem}.font-semibold{font-weight:600}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-normal{font-weight:400}.capitalize{text-transform:capitalize}.leading-6{line-height:1.5rem}.leading-5{line-height:1.25rem}.text-gray-700{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.text-red-700{--tw-text-opacity:1;color:rgb(185 28 28/var(--tw-text-opacity))}.text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.text-indigo-600{--tw-text-opacity:1;color:rgb(79 70 229/var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity:1;color:rgb(17 24 39/var(--tw-text-opacity))}.text-black{--tw-text-opacity:1;color:rgb(0 0 0/var(--tw-text-opacity))}.\!text-indigo-600{--tw-text-opacity:1!important;color:rgb(79 70 229/var(--tw-text-opacity))!important}.opacity-100{opacity:1}.opacity-0{opacity:0}.opacity-90{opacity:.9}.opacity-50{opacity:.5}.shadow-\[0_0_10px_2px_rgba\(0\2c 0\2c 0\2c 0\.3\)\]{--tw-shadow:0 0 10px 2px rgba(0,0,0,.3);--tw-shadow-colored:0 0 10px 2px var(--tw-shadow-color)}.shadow-\[0_0_10px_2px_rgba\(0\2c 0\2c 0\2c 0\.3\)\],.shadow-lg{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1);--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color)}.shadow-sm{--tw-shadow:0 1px 2px 0 rgba(0,0,0,.05);--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color)}.shadow,.shadow-sm{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow{--tw-shadow:0 1px 3px 0 rgba(0,0,0,.1),0 1px 2px -1px rgba(0,0,0,.1);--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color)}.ring-1{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.ring-black{--tw-ring-opacity:1;--tw-ring-color:rgb(0 0 0/var(--tw-ring-opacity))}.ring-opacity-5{--tw-ring-opacity:0.05}.blur{--tw-blur:blur(8px)}.blur,.invert{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.invert{--tw-invert:invert(100%)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-duration:.15s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1)}.duration-100{transition-duration:.1s}.duration-200{transition-duration:.2s}.duration-150{transition-duration:.15s}.ease-in{transition-timing-function:cubic-bezier(.4,0,1,1)}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}.hover\:whitespace-normal:hover{white-space:normal}.focus\:border-indigo-500:focus{--tw-border-opacity:1;border-color:rgb(99 102 241/var(--tw-border-opacity))}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-1:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.focus\:ring-indigo-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(99 102 241/var(--tw-ring-opacity))}.focus\:ring-indigo-600:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(79 70 229/var(--tw-ring-opacity))}.group:hover .group-hover\:flex{display:flex}[class~=theme-dark] .dark\:border{border-width:1px}[class~=theme-dark] .dark\:border-gray-500{--tw-border-opacity:1;border-color:rgb(107 114 128/var(--tw-border-opacity))}[class~=theme-dark] .dark\:border-gray-600{--tw-border-opacity:1;border-color:rgb(75 85 99/var(--tw-border-opacity))}[class~=theme-dark] .dark\:border-gray-700{--tw-border-opacity:1;border-color:rgb(55 65 81/var(--tw-border-opacity))}[class~=theme-dark] .dark\:border-r-gray-700{--tw-border-opacity:1;border-right-color:rgb(55 65 81/var(--tw-border-opacity))}[class~=theme-dark] .dark\:bg-gray-900{--tw-bg-opacity:1;background-color:rgb(17 24 39/var(--tw-bg-opacity))}[class~=theme-dark] .dark\:bg-gray-800{--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity))}[class~=theme-dark] .dark\:bg-gray-700{--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity))}[class~=theme-dark] .dark\:bg-gray-500{--tw-bg-opacity:1;background-color:rgb(107 114 128/var(--tw-bg-opacity))}[class~=theme-dark] .dark\:bg-yellow-700{--tw-bg-opacity:1;background-color:rgb(161 98 7/var(--tw-bg-opacity))}[class~=theme-dark] .dark\:bg-black{--tw-bg-opacity:1;background-color:rgb(0 0 0/var(--tw-bg-opacity))}[class~=theme-dark] .dark\:bg-opacity-80{--tw-bg-opacity:0.8}[class~=theme-dark] .dark\:stroke-gray-500{stroke:#6b7280}[class~=theme-dark] .dark\:stroke-gray-700{stroke:#374151}[class~=theme-dark] .dark\:text-gray-300{--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity))}[class~=theme-dark] .dark\:text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}[class~=theme-dark] .dark\:text-gray-50{--tw-text-opacity:1;color:rgb(249 250 251/var(--tw-text-opacity))}[class~=theme-dark] .dark\:\!text-indigo-400{--tw-text-opacity:1!important;color:rgb(129 140 248/var(--tw-text-opacity))!important}[class~=theme-dark] .dark\:ring-white{--tw-ring-opacity:1;--tw-ring-color:rgb(255 255 255/var(--tw-ring-opacity))}[class~=theme-dark] .dark\:ring-opacity-20{--tw-ring-opacity:0.2}@media (min-width:640px){.sm\:inline{display:inline}.sm\:text-sm{font-size:.875rem;line-height:1.25rem}}
1
+ /*! tailwindcss v3.2.4 | MIT License | https://tailwindcss.com*/*,:after,:before{border:0 solid #e5e7eb;box-sizing:border-box}:after,:before{--tw-content:""}html{-webkit-text-size-adjust:100%;font-feature-settings:normal;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4}body{line-height:inherit;margin:0}hr{border-top-width:1px;color:inherit;height:0}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{border-collapse:collapse;border-color:inherit;text-indent:0}button,input,optgroup,select,textarea{color:inherit;font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{color:#9ca3af;opacity:1}input::placeholder,textarea::placeholder{color:#9ca3af;opacity:1}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{height:auto;max-width:100%}[hidden]{display:none}*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.pointer-events-none{pointer-events:none}.visible{visibility:visible}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.-inset-2{bottom:-.5rem;left:-.5rem;right:-.5rem;top:-.5rem}.inset-y-0{bottom:0;top:0}.left-\[25px\]{left:25px}.left-0{left:0}.top-\[-46px\]{top:-46px}.right-0{right:0}.bottom-0{bottom:0}.z-50{z-index:50}.z-10{z-index:10}.z-20{z-index:20}.m-auto{margin:auto}.m-2{margin:.5rem}.mx-auto{margin-left:auto;margin-right:auto}.mx-2{margin-left:.5rem;margin-right:.5rem}.my-2{margin-bottom:.5rem;margin-top:.5rem}.my-20{margin-bottom:5rem;margin-top:5rem}.my-6{margin-bottom:1.5rem;margin-top:1.5rem}.my-4{margin-bottom:1rem;margin-top:1rem}.mr-3{margin-right:.75rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.ml-2{margin-left:.5rem}.ml-3{margin-left:.75rem}.mb-2{margin-bottom:.5rem}.mb-4{margin-bottom:1rem}.mr-6{margin-right:1.5rem}.mr-1{margin-right:.25rem}.mb-1{margin-bottom:.25rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-8{margin-top:2rem}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-fit{height:-moz-fit-content;height:fit-content}.h-10{height:2.5rem}.h-96{height:24rem}.h-6{height:1.5rem}.h-4{height:1rem}.h-full{height:100%}.h-1{height:.25rem}.h-\[80vh\]{height:80vh}.h-5{height:1.25rem}.max-h-\[400px\]{max-height:400px}.min-h-52{min-height:13rem}.min-h-48{min-height:12rem}.min-h-\[200px\]{min-height:200px}.w-full{width:100%}.w-auto{width:auto}.w-1\/4{width:25%}.w-3\/4{width:75%}.w-\[500px\]{width:500px}.w-4{width:1rem}.w-40{width:10rem}.w-3{width:.75rem}.w-5{width:1.25rem}.w-7{width:1.75rem}.w-9{width:2.25rem}.w-11{width:2.75rem}.w-\[52px\]{width:52px}.w-\[68px\]{width:68px}.w-\[76px\]{width:76px}.w-\[84px\]{width:84px}.w-\[92px\]{width:92px}.w-\[100px\]{width:100px}.w-\[108px\]{width:108px}.w-\[116px\]{width:116px}.w-8{width:2rem}.w-44{width:11rem}.w-\[460px\]{width:460px}.w-1\/5{width:20%}.w-11\/12{width:91.666667%}.w-1\/12{width:8.333333%}.w-16{width:4rem}.w-fit{width:-moz-fit-content;width:fit-content}.w-\[420px\]{width:420px}.min-w-\[300px\]{min-width:300px}.min-w-\[400px\]{min-width:400px}.max-w-\[500px\]{max-width:500px}.max-w-\[300px\]{max-width:300px}.max-w-md{max-width:28rem}.flex-1{flex:1 1 0%}.shrink-0{flex-shrink:0}.shrink{flex-shrink:1}.flex-grow-0{flex-grow:0}.flex-grow{flex-grow:1}.table-auto{table-layout:auto}.table-fixed{table-layout:fixed}.translate-y-1{--tw-translate-y:0.25rem}.translate-y-0,.translate-y-1{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-y-0{--tw-translate-y:0px}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.cursor-pointer{cursor:pointer}.cursor-default{cursor:default}.cursor-not-allowed{cursor:not-allowed}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-row{flex-direction:row}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.content-start{align-content:flex-start}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.justify-start{justify-content:flex-start}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-4{gap:1rem}.gap-2{gap:.5rem}.gap-1{gap:.25rem}.gap-3{gap:.75rem}.gap-x-2{-moz-column-gap:.5rem;column-gap:.5rem}.space-y-5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(1.25rem*var(--tw-space-y-reverse));margin-top:calc(1.25rem*(1 - var(--tw-space-y-reverse)))}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-scroll{overflow:scroll}.overflow-x-hidden{overflow-x:hidden}.text-ellipsis{text-overflow:ellipsis}.whitespace-normal{white-space:normal}.whitespace-nowrap{white-space:nowrap}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-none{border-radius:0}.rounded-t-lg{border-top-left-radius:.5rem;border-top-right-radius:.5rem}.rounded-l{border-bottom-left-radius:.25rem;border-top-left-radius:.25rem}.rounded-r{border-bottom-right-radius:.25rem;border-top-right-radius:.25rem}.border{border-width:1px}.border-r{border-right-width:1px}.border-l{border-left-width:1px}.border-t{border-top-width:1px}.border-r-0{border-right-width:0}.border-l-0{border-left-width:0}.border-gray-300{--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity))}.border-red-400{--tw-border-opacity:1;border-color:rgb(248 113 113/var(--tw-border-opacity))}.border-gray-200{--tw-border-opacity:1;border-color:rgb(229 231 235/var(--tw-border-opacity))}.border-gray-400{--tw-border-opacity:1;border-color:rgb(156 163 175/var(--tw-border-opacity))}.border-r-gray-200{--tw-border-opacity:1;border-right-color:rgb(229 231 235/var(--tw-border-opacity))}.border-l-amber-900{--tw-border-opacity:1;border-left-color:rgb(120 53 15/var(--tw-border-opacity))}.bg-gray-50{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity))}.bg-gray-200{--tw-bg-opacity:1;background-color:rgb(229 231 235/var(--tw-bg-opacity))}.bg-indigo-600{--tw-bg-opacity:1;background-color:rgb(79 70 229/var(--tw-bg-opacity))}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.bg-red-100{--tw-bg-opacity:1;background-color:rgb(254 226 226/var(--tw-bg-opacity))}.bg-black{--tw-bg-opacity:1;background-color:rgb(0 0 0/var(--tw-bg-opacity))}.bg-yellow-200{--tw-bg-opacity:1;background-color:rgb(254 240 138/var(--tw-bg-opacity))}.bg-yellow-700{--tw-bg-opacity:1;background-color:rgb(161 98 7/var(--tw-bg-opacity))}.bg-gray-800{--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity))}.bg-inherit{background-color:inherit}.bg-opacity-90{--tw-bg-opacity:0.9}.fill-transparent{fill:transparent}.fill-current{fill:currentColor}.stroke-gray-300{stroke:#d1d5db}.stroke-white{stroke:#fff}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-10{padding:2.5rem}.p-4{padding:1rem}.p-1{padding:.25rem}.px-2{padding-left:.5rem;padding-right:.5rem}.py-1{padding-bottom:.25rem;padding-top:.25rem}.py-2{padding-bottom:.5rem;padding-top:.5rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-4{padding-bottom:1rem;padding-top:1rem}.px-4{padding-left:1rem;padding-right:1rem}.py-3{padding-bottom:.75rem;padding-top:.75rem}.px-3{padding-left:.75rem;padding-right:.75rem}.py-0{padding-bottom:0;padding-top:0}.px-1{padding-left:.25rem;padding-right:.25rem}.px-8{padding-left:2rem;padding-right:2rem}.pr-0{padding-right:0}.pl-3{padding-left:.75rem}.pr-9{padding-right:2.25rem}.pt-2{padding-top:.5rem}.pb-4{padding-bottom:1rem}.pb-2{padding-bottom:.5rem}.pr-10{padding-right:2.5rem}.pr-2{padding-right:.5rem}.pl-1{padding-left:.25rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xs{font-size:.75rem;line-height:1rem}.text-base{font-size:1rem;line-height:1.5rem}.text-xl{font-size:1.25rem}.text-lg,.text-xl{line-height:1.75rem}.text-lg{font-size:1.125rem}.font-semibold{font-weight:600}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-normal{font-weight:400}.capitalize{text-transform:capitalize}.leading-6{line-height:1.5rem}.leading-5{line-height:1.25rem}.text-gray-700{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.text-red-700{--tw-text-opacity:1;color:rgb(185 28 28/var(--tw-text-opacity))}.text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.text-indigo-600{--tw-text-opacity:1;color:rgb(79 70 229/var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity:1;color:rgb(17 24 39/var(--tw-text-opacity))}.text-black{--tw-text-opacity:1;color:rgb(0 0 0/var(--tw-text-opacity))}.\!text-indigo-600{--tw-text-opacity:1!important;color:rgb(79 70 229/var(--tw-text-opacity))!important}.opacity-100{opacity:1}.opacity-0{opacity:0}.opacity-90{opacity:.9}.opacity-50{opacity:.5}.shadow-\[0_0_10px_2px_rgba\(0\2c 0\2c 0\2c 0\.3\)\]{--tw-shadow:0 0 10px 2px rgba(0,0,0,.3);--tw-shadow-colored:0 0 10px 2px var(--tw-shadow-color)}.shadow-\[0_0_10px_2px_rgba\(0\2c 0\2c 0\2c 0\.3\)\],.shadow-lg{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1);--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color)}.shadow-sm{--tw-shadow:0 1px 2px 0 rgba(0,0,0,.05);--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color)}.shadow,.shadow-sm{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow{--tw-shadow:0 1px 3px 0 rgba(0,0,0,.1),0 1px 2px -1px rgba(0,0,0,.1);--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color)}.ring-1{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.ring-black{--tw-ring-opacity:1;--tw-ring-color:rgb(0 0 0/var(--tw-ring-opacity))}.ring-opacity-5{--tw-ring-opacity:0.05}.blur{--tw-blur:blur(8px)}.blur,.invert{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.invert{--tw-invert:invert(100%)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-duration:.15s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1)}.duration-100{transition-duration:.1s}.duration-200{transition-duration:.2s}.duration-150{transition-duration:.15s}.ease-in{transition-timing-function:cubic-bezier(.4,0,1,1)}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}.hover\:whitespace-normal:hover{white-space:normal}.focus\:border-indigo-500:focus{--tw-border-opacity:1;border-color:rgb(99 102 241/var(--tw-border-opacity))}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-1:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.focus\:ring-indigo-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(99 102 241/var(--tw-ring-opacity))}.focus\:ring-indigo-600:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(79 70 229/var(--tw-ring-opacity))}.group:hover .group-hover\:flex{display:flex}[class~=theme-dark] .dark\:border{border-width:1px}[class~=theme-dark] .dark\:border-gray-500{--tw-border-opacity:1;border-color:rgb(107 114 128/var(--tw-border-opacity))}[class~=theme-dark] .dark\:border-gray-600{--tw-border-opacity:1;border-color:rgb(75 85 99/var(--tw-border-opacity))}[class~=theme-dark] .dark\:border-gray-700{--tw-border-opacity:1;border-color:rgb(55 65 81/var(--tw-border-opacity))}[class~=theme-dark] .dark\:border-r-gray-700{--tw-border-opacity:1;border-right-color:rgb(55 65 81/var(--tw-border-opacity))}[class~=theme-dark] .dark\:bg-gray-900{--tw-bg-opacity:1;background-color:rgb(17 24 39/var(--tw-bg-opacity))}[class~=theme-dark] .dark\:bg-gray-800{--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity))}[class~=theme-dark] .dark\:bg-gray-700{--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity))}[class~=theme-dark] .dark\:bg-gray-500{--tw-bg-opacity:1;background-color:rgb(107 114 128/var(--tw-bg-opacity))}[class~=theme-dark] .dark\:bg-yellow-700{--tw-bg-opacity:1;background-color:rgb(161 98 7/var(--tw-bg-opacity))}[class~=theme-dark] .dark\:bg-black{--tw-bg-opacity:1;background-color:rgb(0 0 0/var(--tw-bg-opacity))}[class~=theme-dark] .dark\:bg-opacity-80{--tw-bg-opacity:0.8}[class~=theme-dark] .dark\:stroke-gray-500{stroke:#6b7280}[class~=theme-dark] .dark\:stroke-gray-700{stroke:#374151}[class~=theme-dark] .dark\:text-gray-300{--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity))}[class~=theme-dark] .dark\:text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}[class~=theme-dark] .dark\:text-gray-50{--tw-text-opacity:1;color:rgb(249 250 251/var(--tw-text-opacity))}[class~=theme-dark] .dark\:\!text-indigo-400{--tw-text-opacity:1!important;color:rgb(129 140 248/var(--tw-text-opacity))!important}[class~=theme-dark] .dark\:ring-white{--tw-ring-opacity:1;--tw-ring-color:rgb(255 255 255/var(--tw-ring-opacity))}[class~=theme-dark] .dark\:ring-opacity-20{--tw-ring-opacity:0.2}@media (min-width:640px){.sm\:inline{display:inline}.sm\:text-sm{font-size:.875rem;line-height:1.25rem}}
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@parca/profile",
3
- "version": "0.16.267",
3
+ "version": "0.16.269",
4
4
  "description": "Profile viewing libraries",
5
5
  "dependencies": {
6
6
  "@parca/client": "^0.16.89",
7
- "@parca/components": "^0.16.203",
7
+ "@parca/components": "^0.16.204",
8
8
  "@parca/dynamicsize": "^0.16.55",
9
9
  "@parca/hooks": "^0.0.26",
10
10
  "@parca/parser": "^0.16.58",
@@ -49,5 +49,5 @@
49
49
  "access": "public",
50
50
  "registry": "https://registry.npmjs.org/"
51
51
  },
52
- "gitHead": "c6b1ff5d57dc4efb8234d72e83af233e27d131f7"
52
+ "gitHead": "d29477a629bfdc95ed3cfd7ebece9e0cd9be3e6f"
53
53
  }
@@ -179,6 +179,7 @@ const TooltipMetaInfo = ({
179
179
  locationAddress,
180
180
  mappingFile,
181
181
  mappingBuildID,
182
+ inlined,
182
183
  } = useGraphTooltipMetaInfo({table, row, navigateTo});
183
184
  const {enableSourcesView} = useParcaContext();
184
185
 
@@ -194,6 +195,7 @@ const TooltipMetaInfo = ({
194
195
  );
195
196
 
196
197
  const isMappingBuildIDAvailable = mappingBuildID !== null && mappingBuildID !== '';
198
+ const inlinedText = inlined === null ? 'merged' : inlined ? 'yes' : 'no';
197
199
 
198
200
  return (
199
201
  <>
@@ -241,6 +243,14 @@ const TooltipMetaInfo = ({
241
243
  )}
242
244
  </td>
243
245
  </tr>
246
+ <tr>
247
+ <td className="w-1/4">Inlined</td>
248
+ <td className="w-3/4 break-all">
249
+ <CopyToClipboard onCopy={onCopy} text={inlinedText}>
250
+ <button className="cursor-pointer">{inlinedText}</button>
251
+ </CopyToClipboard>
252
+ </td>
253
+ </tr>
244
254
  <tr>
245
255
  <td className="w-1/4">Binary</td>
246
256
  <td className="w-3/4 break-all">
@@ -105,6 +105,7 @@ export const DockedGraphTooltip = ({
105
105
  locationAddress,
106
106
  mappingFile,
107
107
  mappingBuildID,
108
+ inlined,
108
109
  } = useGraphTooltipMetaInfo({table, row: row ?? 0, navigateTo});
109
110
 
110
111
  const [_, setIsDocked] = useUserPreference(USER_PREFERENCES.GRAPH_METAINFO_DOCKED.key);
@@ -126,6 +127,7 @@ export const DockedGraphTooltip = ({
126
127
  )
127
128
  );
128
129
 
130
+ const inlinedText = inlined === null ? 'merged' : inlined ? 'yes' : 'no';
129
131
  const addressText = locationAddress !== 0n ? hexifyAddress(locationAddress) : 'unknown';
130
132
  const fileText = functionFilename !== '' ? file : 'Not available';
131
133
 
@@ -223,6 +225,13 @@ export const DockedGraphTooltip = ({
223
225
  copyText={addressText}
224
226
  minWidth="w-44"
225
227
  />
228
+ <InfoSection
229
+ title="Inlined"
230
+ value={inlinedText}
231
+ onCopy={onCopy}
232
+ copyText={inlinedText}
233
+ minWidth="w-44"
234
+ />
226
235
  <InfoSection
227
236
  title="Binary"
228
237
  value={(mappingFile != null ? getLastItem(mappingFile) : null) ?? 'Not available'}
@@ -20,6 +20,7 @@ import type {NavigateFunction} from '@parca/utilities';
20
20
  import {
21
21
  FIELD_FUNCTION_FILE_NAME,
22
22
  FIELD_FUNCTION_START_LINE,
23
+ FIELD_INLINED,
23
24
  FIELD_LOCATION_ADDRESS,
24
25
  FIELD_LOCATION_LINE,
25
26
  FIELD_MAPPING_BUILD_ID,
@@ -45,6 +46,7 @@ interface GraphTooltipMetaInfoData {
45
46
  locationAddress: bigint;
46
47
  mappingFile: string | null;
47
48
  mappingBuildID: string | null;
49
+ inlined: boolean | null;
48
50
  }
49
51
 
50
52
  export const useGraphTooltipMetaInfo = ({
@@ -57,6 +59,7 @@ export const useGraphTooltipMetaInfo = ({
57
59
  table.getChild(FIELD_MAPPING_BUILD_ID)?.get(row)
58
60
  );
59
61
  const locationAddress: bigint = table.getChild(FIELD_LOCATION_ADDRESS)?.get(row) ?? 0n;
62
+ const inlined: boolean | null = table.getChild(FIELD_INLINED)?.get(row);
60
63
  const locationLine: bigint = table.getChild(FIELD_LOCATION_LINE)?.get(row) ?? 0n;
61
64
  const functionFilename: string =
62
65
  arrowToString(table.getChild(FIELD_FUNCTION_FILE_NAME)?.get(row)) ?? '';
@@ -149,5 +152,6 @@ export const useGraphTooltipMetaInfo = ({
149
152
  locationAddress,
150
153
  mappingBuildID,
151
154
  mappingFile,
155
+ inlined,
152
156
  };
153
157
  };
@@ -44,6 +44,7 @@ export const FIELD_MAPPING_FILE = 'mapping_file';
44
44
  export const FIELD_MAPPING_BUILD_ID = 'mapping_build_id';
45
45
  export const FIELD_LOCATION_ADDRESS = 'location_address';
46
46
  export const FIELD_LOCATION_LINE = 'location_line';
47
+ export const FIELD_INLINED = 'inlined';
47
48
  export const FIELD_FUNCTION_NAME = 'function_name';
48
49
  export const FIELD_FUNCTION_FILE_NAME = 'function_file_name';
49
50
  export const FIELD_FUNCTION_START_LINE = 'function_startline';
@@ -31,8 +31,11 @@ import IcicleGraph from './IcicleGraph';
31
31
  import IcicleGraphArrow, {
32
32
  FIELD_CUMULATIVE,
33
33
  FIELD_DIFF,
34
+ FIELD_FUNCTION_FILE_NAME,
34
35
  FIELD_FUNCTION_NAME,
35
36
  FIELD_LABELS,
37
+ FIELD_LOCATION_ADDRESS,
38
+ FIELD_MAPPING_FILE,
36
39
  } from './IcicleGraphArrow';
37
40
 
38
41
  const numberFormatter = new Intl.NumberFormat('en-US');
@@ -303,6 +306,24 @@ const groupByOptions = [
303
306
  description: 'Stacktraces are grouped by pprof labels.',
304
307
  disabled: false,
305
308
  },
309
+ {
310
+ value: FIELD_FUNCTION_FILE_NAME,
311
+ label: 'Filename',
312
+ description: 'Stacktraces are grouped by filenames.',
313
+ disabled: false,
314
+ },
315
+ {
316
+ value: FIELD_LOCATION_ADDRESS,
317
+ label: 'Address',
318
+ description: 'Stacktraces are grouped by addresses.',
319
+ disabled: false,
320
+ },
321
+ {
322
+ value: FIELD_MAPPING_FILE,
323
+ label: 'Binary',
324
+ description: 'Stacktraces are grouped by binaries.',
325
+ disabled: false,
326
+ },
306
327
  ];
307
328
 
308
329
  const GroupByDropdown = ({
@@ -295,6 +295,8 @@ export const ProfileView = ({
295
295
  case 'table': {
296
296
  return topTableData != null ? (
297
297
  <Table
298
+ total={total}
299
+ filtered={filtered}
298
300
  loading={topTableData.loading}
299
301
  data={topTableData.arrow?.record}
300
302
  sampleUnit={sampleUnit}
@@ -11,10 +11,12 @@
11
11
  // See the License for the specific language governing permissions and
12
12
  // limitations under the License.
13
13
 
14
- import React, {useCallback, useEffect, useMemo} from 'react';
14
+ import React, {Fragment, useCallback, useEffect, useMemo, useState} from 'react';
15
15
 
16
- import {createColumnHelper, type ColumnDef} from '@tanstack/react-table';
17
- import {Table as ArrowTable, tableFromIPC} from 'apache-arrow';
16
+ import {Menu, Transition} from '@headlessui/react';
17
+ import {Icon} from '@iconify/react';
18
+ import {type VisibilityState} from '@tanstack/react-table';
19
+ import {Vector, tableFromIPC} from 'apache-arrow';
18
20
 
19
21
  import {Button, Table as TableComponent, useURLState} from '@parca/components';
20
22
  import {
@@ -30,23 +32,39 @@ import {hexifyAddress} from '../utils';
30
32
  const FIELD_MAPPING_FILE = 'mapping_file';
31
33
  const FIELD_LOCATION_ADDRESS = 'location_address';
32
34
  const FIELD_FUNCTION_NAME = 'function_name';
35
+ const FIELD_FUNCTION_SYSTEM_NAME = 'function_system_name';
36
+ const FIELD_FUNCTION_FILE_NAME = 'function_file_name';
33
37
  const FIELD_FLAT = 'flat';
34
38
  const FIELD_FLAT_DIFF = 'flat_diff';
35
39
  const FIELD_CUMULATIVE = 'cumulative';
36
40
  const FIELD_CUMULATIVE_DIFF = 'cumulative_diff';
37
41
 
38
- const columnHelper = createColumnHelper<row>();
39
-
40
42
  interface row {
41
43
  name: string;
42
44
  flat: bigint;
43
45
  flatDiff: bigint;
44
46
  cumulative: bigint;
45
47
  cumulativeDiff: bigint;
48
+ mappingFile: string;
49
+ functionSystemName: string;
50
+ functionFileName: string;
51
+ }
52
+
53
+ interface ColumnDef {
54
+ id: string;
55
+ header: string;
56
+ accessorKey: string;
57
+ footer?: string;
58
+ cell?: (info: any) => string | number;
59
+ meta?: {align: 'right' | 'left'};
60
+ invertSorting?: boolean;
61
+ size?: number;
46
62
  }
47
63
 
48
64
  interface TableProps {
49
65
  data?: Uint8Array;
66
+ total: bigint;
67
+ filtered: bigint;
50
68
  sampleUnit: string;
51
69
  navigateTo?: NavigateFunction;
52
70
  loading: boolean;
@@ -56,6 +74,8 @@ interface TableProps {
56
74
 
57
75
  export const Table = React.memo(function Table({
58
76
  data,
77
+ total,
78
+ filtered,
59
79
  sampleUnit: unit,
60
80
  navigateTo,
61
81
  loading,
@@ -76,52 +96,155 @@ export const Table = React.memo(function Table({
76
96
  return ['icicle'];
77
97
  }, [rawDashboardItems]);
78
98
 
79
- const columns = useMemo(() => {
80
- const cols: Array<ColumnDef<row, any>> = [
81
- columnHelper.accessor('flat', {
82
- header: () => 'Flat',
99
+ const percentageString = (value: bigint | number, total: bigint | number): string => {
100
+ if (total === 0n) {
101
+ return '0%';
102
+ }
103
+
104
+ const percentage = (Number(value) / Number(total)) * 100;
105
+ return `${percentage.toFixed(2)}%`;
106
+ };
107
+
108
+ const ratioString = (value: bigint | number): string => {
109
+ if (filtered === 0n) {
110
+ return ` ${percentageString(value, total)}`;
111
+ }
112
+
113
+ return `${percentageString(value, total)} / ${percentageString(value, filtered)}`;
114
+ };
115
+
116
+ const columns = useMemo<ColumnDef[]>(() => {
117
+ return [
118
+ {
119
+ id: 'flat',
120
+ accessorKey: 'flat',
121
+ header: 'Flat',
83
122
  cell: info => valueFormatter(info.getValue(), unit, 2),
84
123
  size: 80,
85
124
  meta: {
86
125
  align: 'right',
87
126
  },
88
127
  invertSorting: true,
89
- }),
90
- columnHelper.accessor('flatDiff', {
91
- header: () => 'Flat Diff',
128
+ },
129
+ {
130
+ id: 'flatPercentage',
131
+ accessorKey: 'flat',
132
+ header: 'Flat (%)',
133
+ cell: info => ratioString(info.getValue()),
134
+ size: 120,
135
+ meta: {
136
+ align: 'right',
137
+ },
138
+ invertSorting: true,
139
+ },
140
+ {
141
+ id: 'flatDiff',
142
+ accessorKey: 'flatDiff',
143
+ header: 'Flat Diff',
92
144
  cell: info => addPlusSign(valueFormatter(info.getValue(), unit, 2)),
93
145
  size: 120,
94
146
  meta: {
95
147
  align: 'right',
96
148
  },
97
149
  invertSorting: true,
98
- }),
99
- columnHelper.accessor('cumulative', {
100
- header: () => 'Cumulative',
150
+ },
151
+ {
152
+ id: 'flatDiffPercentage',
153
+ accessorKey: 'flatDiff',
154
+ header: 'Flat Diff (%)',
155
+ cell: info => ratioString(info.getValue()),
156
+ size: 120,
157
+ meta: {
158
+ align: 'right',
159
+ },
160
+ invertSorting: true,
161
+ },
162
+ {
163
+ id: 'cumulative',
164
+ accessorKey: 'cumulative',
165
+ header: 'Cumulative',
101
166
  cell: info => valueFormatter(info.getValue(), unit, 2),
102
- size: 130,
167
+ size: 150,
103
168
  meta: {
104
169
  align: 'right',
105
170
  },
106
171
  invertSorting: true,
107
- }),
108
- columnHelper.accessor('cumulativeDiff', {
109
- header: () => 'Cumulative Diff',
172
+ },
173
+ {
174
+ id: 'cumulativePercentage',
175
+ accessorKey: 'cumulative',
176
+ header: 'Cumulative (%)',
177
+ cell: info => ratioString(info.getValue()),
178
+ size: 150,
179
+ meta: {
180
+ align: 'right',
181
+ },
182
+ invertSorting: true,
183
+ },
184
+ {
185
+ id: 'cumulativeDiff',
186
+ accessorKey: 'cumulativeDiff',
187
+ header: 'Cumulative Diff',
110
188
  cell: info => addPlusSign(valueFormatter(info.getValue(), unit, 2)),
111
189
  size: 170,
112
190
  meta: {
113
191
  align: 'right',
114
192
  },
115
193
  invertSorting: true,
116
- }),
117
- columnHelper.accessor('name', {
118
- header: () => <span className="text-left">Name</span>,
194
+ },
195
+ {
196
+ id: 'cumulativeDiffPercentage',
197
+ accessorKey: 'cumulativeDiff',
198
+ header: 'Cumulative Diff (%)',
199
+ cell: info => ratioString(info.getValue()),
200
+ size: 170,
201
+ meta: {
202
+ align: 'right',
203
+ },
204
+ invertSorting: true,
205
+ },
206
+ {
207
+ id: 'name',
208
+ accessorKey: 'name',
209
+ header: 'Name',
119
210
  cell: info => info.getValue(),
120
- }),
211
+ },
212
+ {
213
+ id: 'functionSystemName',
214
+ accessorKey: 'functionSystemName',
215
+ header: 'Function System Name',
216
+ },
217
+ {
218
+ id: 'functionFileName',
219
+ accessorKey: 'functionFileName',
220
+ header: 'Function File Name',
221
+ },
222
+ {
223
+ id: 'mappingFile',
224
+ accessorKey: 'mappingFile',
225
+ header: 'Mapping File',
226
+ },
121
227
  ];
122
- return cols;
228
+ // eslint-disable-next-line react-hooks/exhaustive-deps
123
229
  }, [unit]);
124
230
 
231
+ const [columnVisibility, setColumnVisibility] = useState(() => {
232
+ return {
233
+ flat: true,
234
+ flatPercentage: false,
235
+ flatDiff: compareMode,
236
+ flatDiffPercentage: false,
237
+ cumulative: true,
238
+ cumulativePercentage: false,
239
+ cumulativeDiff: compareMode,
240
+ cumulativeDiffPercentage: false,
241
+ name: true,
242
+ functionSystemName: false,
243
+ functionFileName: false,
244
+ mappingFile: false,
245
+ };
246
+ });
247
+
125
248
  const selectSpan = useCallback(
126
249
  (span: string): void => {
127
250
  if (navigateTo != null) {
@@ -179,21 +302,35 @@ export const Table = React.memo(function Table({
179
302
  return;
180
303
  }
181
304
  setActionButtons(
182
- dashboardItems.length > 1 ? (
183
- <Button
184
- color="neutral"
185
- onClick={clearSelection}
186
- className="w-auto"
187
- variant="neutral"
188
- disabled={currentSearchString === undefined || currentSearchString.length === 0}
189
- >
190
- Clear selection
191
- </Button>
192
- ) : (
193
- <></>
194
- )
305
+ <>
306
+ <ColumnsVisibility
307
+ columns={columns}
308
+ visibility={columnVisibility}
309
+ setVisibility={(id, visible) => {
310
+ setColumnVisibility({...columnVisibility, [id]: visible});
311
+ }}
312
+ />
313
+ {dashboardItems.length > 1 && (
314
+ <Button
315
+ color="neutral"
316
+ onClick={clearSelection}
317
+ className="w-auto"
318
+ variant="neutral"
319
+ disabled={currentSearchString === undefined || currentSearchString.length === 0}
320
+ >
321
+ Clear selection
322
+ </Button>
323
+ )}
324
+ </>
195
325
  );
196
- }, [dashboardItems, clearSelection, currentSearchString, setActionButtons]);
326
+ }, [
327
+ dashboardItems,
328
+ clearSelection,
329
+ currentSearchString,
330
+ setActionButtons,
331
+ columns,
332
+ columnVisibility,
333
+ ]);
197
334
 
198
335
  const initialSorting = useMemo(() => {
199
336
  return [
@@ -204,27 +341,21 @@ export const Table = React.memo(function Table({
204
341
  ];
205
342
  }, [compareMode]);
206
343
 
207
- const columnVisibility = useMemo(() => {
208
- // TODO: Make this configurable via the UI and add more columns.
209
- return {
210
- flat: true,
211
- flatDiff: compareMode,
212
- cumulative: true,
213
- cumulativeDiff: compareMode,
214
- name: true,
215
- };
216
- }, [compareMode]);
217
-
218
344
  if (loading) return <div className="mx-auto text-center">Loading...</div>;
219
345
  if (data === undefined) return <div className="mx-auto text-center">Profile has no samples</div>;
220
346
 
221
347
  const table = tableFromIPC(data);
348
+ if (table.numRows === 0) return <div className="mx-auto text-center">Profile has no samples</div>;
349
+
222
350
  const flatColumn = table.getChild(FIELD_FLAT);
223
351
  const flatDiffColumn = table.getChild(FIELD_FLAT_DIFF);
224
352
  const cumulativeColumn = table.getChild(FIELD_CUMULATIVE);
225
353
  const cumulativeDiffColumn = table.getChild(FIELD_CUMULATIVE_DIFF);
226
-
227
- if (table.numRows === 0) return <div className="mx-auto text-center">Profile has no samples</div>;
354
+ const functionNameColumn = table.getChild(FIELD_FUNCTION_NAME);
355
+ const functionSystemNameColumn = table.getChild(FIELD_FUNCTION_SYSTEM_NAME);
356
+ const functionFileNameColumn = table.getChild(FIELD_FUNCTION_FILE_NAME);
357
+ const mappingFileColumn = table.getChild(FIELD_MAPPING_FILE);
358
+ const locationAddressColumn = table.getChild(FIELD_LOCATION_ADDRESS);
228
359
 
229
360
  const rows: row[] = [];
230
361
  // TODO: Figure out how to only read the data of the columns we need for the virtualized table
@@ -233,12 +364,18 @@ export const Table = React.memo(function Table({
233
364
  const flatDiff: bigint = flatDiffColumn?.get(i) ?? 0n;
234
365
  const cumulative: bigint = cumulativeColumn?.get(i) ?? 0n;
235
366
  const cumulativeDiff: bigint = cumulativeDiffColumn?.get(i) ?? 0n;
367
+ const functionSystemName: string = functionSystemNameColumn?.get(i) ?? '';
368
+ const functionFileName: string = functionFileNameColumn?.get(i) ?? '';
369
+ const mappingFile: string = mappingFileColumn?.get(i) ?? '';
236
370
  rows.push({
237
- name: RowName(table, i),
371
+ name: RowName(mappingFileColumn, locationAddressColumn, functionNameColumn, i),
238
372
  flat,
239
373
  flatDiff,
240
374
  cumulative,
241
375
  cumulativeDiff,
376
+ functionSystemName,
377
+ functionFileName,
378
+ mappingFile,
242
379
  });
243
380
  }
244
381
 
@@ -260,6 +397,68 @@ export const Table = React.memo(function Table({
260
397
  );
261
398
  });
262
399
 
400
+ const ColumnsVisibility = ({
401
+ columns,
402
+ visibility,
403
+ setVisibility,
404
+ }: {
405
+ columns: ColumnDef[];
406
+ visibility: VisibilityState;
407
+ setVisibility: (id: string, visible: boolean) => void;
408
+ }): React.JSX.Element => {
409
+ return (
410
+ <div>
411
+ <Menu as="div" className="relative text-left">
412
+ <div>
413
+ <Menu.Button className="relative w-full cursor-default rounded-md border bg-white py-2 pl-3 pr-10 text-left text-sm shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 dark:border-gray-600 dark:bg-gray-900 sm:text-sm">
414
+ <span className="ml-3 block overflow-x-hidden text-ellipsis">Columns</span>
415
+ <span className="pointer-events-none absolute inset-y-0 right-0 ml-3 flex items-center pr-2 text-gray-400">
416
+ <Icon icon="heroicons:chevron-down-20-solid" aria-hidden="true" />
417
+ </span>
418
+ </Menu.Button>
419
+ </div>
420
+
421
+ <Transition
422
+ as={Fragment}
423
+ leave="transition ease-in duration-100"
424
+ leaveFrom="opacity-100"
425
+ leaveTo="opacity-0"
426
+ >
427
+ <Menu.Items className="absolute left-0 z-10 mt-1 min-w-[400px] overflow-auto rounded-md bg-gray-50 py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none dark:border-gray-600 dark:bg-gray-900 dark:ring-white dark:ring-opacity-20 sm:text-sm">
428
+ <div className="p-4">
429
+ <fieldset>
430
+ <div className="space-y-5">
431
+ {columns.map(col => (
432
+ <div key={col.id} className="relative flex items-start">
433
+ <div className="flex h-6 items-center">
434
+ <input
435
+ id={col.id}
436
+ name={col.id}
437
+ type="checkbox"
438
+ className="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600"
439
+ checked={visibility[col.id ?? ''] ?? false}
440
+ onChange={() => {
441
+ setVisibility(col.id ?? '', !visibility[col.id ?? '']);
442
+ }}
443
+ />
444
+ </div>
445
+ <div className="ml-3 text-sm leading-6">
446
+ <label htmlFor={col.id} className="font-medium text-gray-900">
447
+ {col.header}
448
+ </label>
449
+ </div>
450
+ </div>
451
+ ))}
452
+ </div>
453
+ </fieldset>
454
+ </div>
455
+ </Menu.Items>
456
+ </Transition>
457
+ </Menu>
458
+ </div>
459
+ );
460
+ };
461
+
263
462
  const addPlusSign = (num: string): string => {
264
463
  if (num.charAt(0) === '0' || num.charAt(0) === '-') {
265
464
  return num;
@@ -268,8 +467,12 @@ const addPlusSign = (num: string): string => {
268
467
  return `+${num}`;
269
468
  };
270
469
 
271
- export const RowName = (table: ArrowTable, row: number): string => {
272
- const mappingFileColumn = table.getChild(FIELD_MAPPING_FILE);
470
+ export const RowName = (
471
+ mappingFileColumn: Vector | null,
472
+ locationAddressColumn: Vector | null,
473
+ functionNameColumn: Vector | null,
474
+ row: number
475
+ ): string => {
273
476
  if (mappingFileColumn === null) {
274
477
  console.error('mapping_file column not found');
275
478
  return '';
@@ -281,12 +484,12 @@ export const RowName = (table: ArrowTable, row: number): string => {
281
484
  if (mappingFile != null && mappingFileColumn.data.length > 1) {
282
485
  mapping = `[${getLastItem(mappingFile) ?? ''}]`;
283
486
  }
284
- const functionName: string | null = table.getChild(FIELD_FUNCTION_NAME)?.get(row) ?? '';
487
+ const functionName: string | null = functionNameColumn?.get(row) ?? '';
285
488
  if (functionName !== null && functionName !== '') {
286
489
  return `${mapping} ${functionName}`;
287
490
  }
288
491
 
289
- const address: bigint = table.getChild(FIELD_LOCATION_ADDRESS)?.get(row) ?? 0;
492
+ const address: bigint = locationAddressColumn?.get(row) ?? 0;
290
493
 
291
494
  return hexifyAddress(address);
292
495
  };