diff --git a/ui/packages/shared/profile/src/ProfileIcicleGraph/index.tsx b/ui/packages/shared/profile/src/ProfileIcicleGraph/index.tsx index a8d13f229d7..04468fff593 100644 --- a/ui/packages/shared/profile/src/ProfileIcicleGraph/index.tsx +++ b/ui/packages/shared/profile/src/ProfileIcicleGraph/index.tsx @@ -13,20 +13,18 @@ import React, {useEffect, useMemo, useState} from 'react'; -import {Table, tableFromIPC} from 'apache-arrow'; import {AnimatePresence, motion} from 'framer-motion'; import {Flamegraph, FlamegraphArrow} from '@parca/client'; import {IcicleGraphSkeleton, useParcaContext, useURLState} from '@parca/components'; import {ProfileType} from '@parca/parser'; -import {capitalizeOnlyFirstLetter, divide, selectQueryParam} from '@parca/utilities'; +import {capitalizeOnlyFirstLetter, divide} from '@parca/utilities'; import {useProfileViewContext} from '../ProfileView/ProfileViewContext'; import DiffLegend from '../components/DiffLegend'; import {IcicleGraph} from './IcicleGraph'; import {FIELD_FUNCTION_NAME, IcicleGraphArrow} from './IcicleGraphArrow'; -import ColorStackLegend from './IcicleGraphArrow/ColorStackLegend'; -import useMappingList, {useFilenamesList} from './IcicleGraphArrow/useMappingList'; +import useMappingList from './IcicleGraphArrow/useMappingList'; const numberFormatter = new Intl.NumberFormat('en-US'); @@ -70,14 +68,8 @@ const ProfileIcicleGraph = function ProfileIcicleGraphNonMemo({ const {onError, authenticationErrorMessage, isDarkMode} = useParcaContext(); const {compareMode} = useProfileViewContext(); const [isLoading, setIsLoading] = useState(true); - const isColorStackLegendEnabled = selectQueryParam('color_stack_legend') === 'true'; - - const table: Table | null = useMemo(() => { - return arrow !== undefined ? tableFromIPC(arrow.record) : null; - }, [arrow]); const mappingsList = useMappingList(metadataMappingFiles); - const filenamesList = useFilenamesList(table); const [storeSortBy = FIELD_FUNCTION_NAME] = useURLState('sort_by'); const [colorBy, setColorBy] = useURLState('color_by'); @@ -89,7 +81,6 @@ const ProfileIcicleGraph = function ProfileIcicleGraphNonMemo({ const [compareAbsolute = compareAbsoluteDefault] = useURLState('compare_absolute'); const isCompareAbsolute = compareAbsolute === 'true'; - const colorByValue = colorBy === undefined || colorBy === '' ? 'binary' : (colorBy as string); const mappingsListCount = useMemo( () => mappingsList.filter(m => m !== '').length, [mappingsList] @@ -231,13 +222,6 @@ const ProfileIcicleGraph = function ProfileIcicleGraphNonMemo({ transition={{duration: 0.5}} > {compareMode ? : null} - {isColorStackLegendEnabled && ( - - )}
<>{icicleGraph}
diff --git a/ui/packages/shared/profile/src/ProfileIcicleGraph/IcicleGraphArrow/ColorStackLegend.tsx b/ui/packages/shared/profile/src/ProfileView/ColorStackLegend.tsx similarity index 96% rename from ui/packages/shared/profile/src/ProfileIcicleGraph/IcicleGraphArrow/ColorStackLegend.tsx rename to ui/packages/shared/profile/src/ProfileView/ColorStackLegend.tsx index ce1de99e639..e0b62eba7c8 100644 --- a/ui/packages/shared/profile/src/ProfileIcicleGraph/IcicleGraphArrow/ColorStackLegend.tsx +++ b/ui/packages/shared/profile/src/ProfileView/ColorStackLegend.tsx @@ -20,8 +20,8 @@ import {useURLState} from '@parca/components'; import {USER_PREFERENCES, useCurrentColorProfile, useUserPreference} from '@parca/hooks'; import {EVERYTHING_ELSE, selectDarkMode, useAppSelector} from '@parca/store'; -import {getMappingColors} from '.'; -import useMappingList from './useMappingList'; +import {getMappingColors} from '../ProfileIcicleGraph/IcicleGraphArrow/'; +import useMappingList from '../ProfileIcicleGraph/IcicleGraphArrow/useMappingList'; interface Props { mappings?: string[]; diff --git a/ui/packages/shared/profile/src/ProfileView/VisualizationPanel.tsx b/ui/packages/shared/profile/src/ProfileView/VisualizationPanel.tsx index 6ea16798af8..a6320933d43 100644 --- a/ui/packages/shared/profile/src/ProfileView/VisualizationPanel.tsx +++ b/ui/packages/shared/profile/src/ProfileView/VisualizationPanel.tsx @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import React, {useState} from 'react'; +import React from 'react'; import {Icon} from '@iconify/react'; import cx from 'classnames'; @@ -26,11 +26,11 @@ interface Props { isMultiPanelView: boolean; handleClosePanel: (dashboardItem: string) => void; dragHandleProps: DraggableProvidedDragHandleProps | null | undefined; - getDashboardItemByType: (props: { - type: string; - isHalfScreen: boolean; - setActionButtons: (actionButtons: JSX.Element) => void; - }) => JSX.Element; + getDashboardItemByType: (props: {type: string; isHalfScreen: boolean}) => JSX.Element; + actionButtons: { + icicle: JSX.Element; + table: JSX.Element; + }; } export const VisualizationPanel = React.memo(function VisualizationPanel({ @@ -39,8 +39,8 @@ export const VisualizationPanel = React.memo(function VisualizationPanel({ handleClosePanel, dragHandleProps, getDashboardItemByType, + actionButtons, }: Props): JSX.Element { - const [actionButtons, setActionButtons] = useState(<>); const {flamegraphHint} = useParcaContext(); return ( @@ -52,14 +52,18 @@ export const VisualizationPanel = React.memo(function VisualizationPanel({ isMultiPanelView && dashboardItem === 'icicle' ? 'items-end gap-x-2' : 'items-end' )} > -
+
-
{actionButtons}
+ {isMultiPanelView ? ( +
+ {actionButtons[dashboardItem as keyof typeof actionButtons]} +
+ ) : null}
); diff --git a/ui/packages/shared/profile/src/ProfileView/index.tsx b/ui/packages/shared/profile/src/ProfileView/index.tsx index 4f63006ca0e..05386226223 100644 --- a/ui/packages/shared/profile/src/ProfileView/index.tsx +++ b/ui/packages/shared/profile/src/ProfileView/index.tsx @@ -11,8 +11,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -import {Profiler, ProfilerProps, useCallback, useEffect, useState} from 'react'; +import {Profiler, ProfilerProps, useCallback, useEffect, useMemo, useState} from 'react'; +import {Table as ArrowTable, tableFromIPC} from 'apache-arrow'; import cx from 'classnames'; import {scaleLinear} from 'd3'; import graphviz from 'graphviz-wasm'; @@ -42,10 +43,17 @@ import {Callgraph} from '../'; import {jsonToDot} from '../Callgraph/utils'; import ProfileIcicleGraph from '../ProfileIcicleGraph'; import {FIELD_FUNCTION_NAME} from '../ProfileIcicleGraph/IcicleGraphArrow'; +import useMappingList, { + useFilenamesList, +} from '../ProfileIcicleGraph/IcicleGraphArrow/useMappingList'; import {ProfileSource} from '../ProfileSource'; import {SourceView} from '../SourceView'; import {Table} from '../Table'; -import VisualisationToolbar from '../components/VisualisationToolbar'; +import VisualisationToolbar, { + IcicleGraphToolbar, + TableToolbar, +} from '../components/VisualisationToolbar'; +import ColorStackLegend from './ColorStackLegend'; import {ProfileViewContextProvider} from './ProfileViewContext'; import {VisualizationPanel} from './VisualizationPanel'; @@ -130,10 +138,22 @@ export const ProfileView = ({ const [graphvizLoaded, setGraphvizLoaded] = useState(false); const [callgraphSVG, setCallgraphSVG] = useState(undefined); const [currentSearchString, setSearchString] = useURLState('search_string'); + const [colorStackLegend] = useURLState('color_stack_legend'); + const [colorBy] = useURLState('color_by'); + + const isColorStackLegendEnabled = colorStackLegend === 'true'; + const colorByValue = colorBy === undefined || colorBy === '' ? 'binary' : (colorBy as string); const isDarkMode = useAppSelector(selectDarkMode); const isMultiPanelView = dashboardItems.length > 1; + const table: ArrowTable | null = useMemo(() => { + return flamegraphData.arrow !== undefined ? tableFromIPC(flamegraphData.arrow.record) : null; + }, [flamegraphData.arrow]); + + const mappingsList = useMappingList(flamegraphData.metadataMappingFiles); + const filenamesList = useFilenamesList(table); + const {perf, profileViewExternalMainActions} = useParcaContext(); useEffect(() => { @@ -195,11 +215,9 @@ export const ProfileView = ({ const getDashboardItemByType = ({ type, isHalfScreen, - setActionButtons, }: { type: string; isHalfScreen: boolean; - setActionButtons: (actionButtons: JSX.Element) => void; }): JSX.Element => { switch (type) { case 'icicle': { @@ -221,13 +239,12 @@ export const ProfileView = ({ filtered={filtered} profileType={profileSource?.ProfileType()} loading={flamegraphData.loading} - setActionButtons={setActionButtons} error={flamegraphData.error} isHalfScreen={isHalfScreen} width={ dimensions?.width !== undefined ? isHalfScreen - ? (dimensions.width - 40) / 2 + ? (dimensions.width - 54) / 2 : dimensions.width - 16 : 0 } @@ -260,10 +277,10 @@ export const ProfileView = ({ data={topTableData.arrow?.record} unit={topTableData.unit} profileType={profileSource?.ProfileType()} - setActionButtons={setActionButtons} currentSearchString={currentSearchString} setSearchString={setSearchString} isHalfScreen={isHalfScreen} + metadataMappingFiles={flamegraphData.metadataMappingFiles} /> ) : ( <> @@ -276,7 +293,6 @@ export const ProfileView = ({ data={sourceData.data} total={total} filtered={filtered} - setActionButtons={setActionButtons} /> ) : ( <> @@ -342,6 +358,28 @@ export const ProfileView = ({ hasProfileSource && (profileViewExternalMainActions === null || profileViewExternalMainActions === undefined); + const clearSelection = useCallback((): void => { + setSearchString?.(''); + }, [setSearchString]); + + const getActionButtonsWithMultiPanelView = (): { + icicle: JSX.Element; + table: JSX.Element; + } => { + return { + icicle: , + table: ( + + ), + }; + }; + return ( @@ -400,6 +438,14 @@ export const ProfileView = ({ groupByLabels={flamegraphData.metadataLabels ?? []} /> + {isColorStackLegendEnabled && ( + + )} +
@@ -429,7 +475,10 @@ export const ProfileView = ({ 'w-full min-h-96', snapshot.isDragging ? 'bg-gray-200 dark:bg-gray-500' - : 'bg-white dark:bg-gray-900' + : 'bg-white dark:bg-gray-900', + isMultiPanelView + ? 'border-2 border-gray-100 dark:border-gray-700 rounded-md p-3' + : '' )} >
)} diff --git a/ui/packages/shared/profile/src/ProfileViewWithData.tsx b/ui/packages/shared/profile/src/ProfileViewWithData.tsx index 5050016e80e..3b81c35b2b0 100644 --- a/ui/packages/shared/profile/src/ProfileViewWithData.tsx +++ b/ui/packages/shared/profile/src/ProfileViewWithData.tsx @@ -81,11 +81,8 @@ export const ProfileViewWithData = ({ profileSource, QueryRequest_ReportType.PROFILE_METADATA, { - skip: !dashboardItems.includes('icicle'), nodeTrimThreshold, groupBy, - invertCallStack, - binaryFrameFilter: undefined, } ); @@ -97,6 +94,7 @@ export const ProfileViewWithData = ({ error: tableError, } = useQuery(queryClient, profileSource, QueryRequest_ReportType.TABLE_ARROW, { skip: !dashboardItems.includes('table'), + binaryFrameFilter, }); const { diff --git a/ui/packages/shared/profile/src/Table/index.tsx b/ui/packages/shared/profile/src/Table/index.tsx index 8f6febf1616..04a4a6c40ba 100644 --- a/ui/packages/shared/profile/src/Table/index.tsx +++ b/ui/packages/shared/profile/src/Table/index.tsx @@ -32,11 +32,36 @@ import { useURLState, } from '@parca/components'; import {type RowRendererProps} from '@parca/components/dist/Table'; +import {useCurrentColorProfile} from '@parca/hooks'; import {ProfileType} from '@parca/parser'; -import {getLastItem, isSearchMatch, valueFormatter} from '@parca/utilities'; +import {isSearchMatch, valueFormatter} from '@parca/utilities'; +import {getFilenameColors, getMappingColors} from '../ProfileIcicleGraph/IcicleGraphArrow/'; +import {colorByColors} from '../ProfileIcicleGraph/IcicleGraphArrow/IcicleGraphNodes'; +import useMappingList, { + useFilenamesList, +} from '../ProfileIcicleGraph/IcicleGraphArrow/useMappingList'; import {useProfileViewContext} from '../ProfileView/ProfileViewContext'; -import {hexifyAddress} from '../utils'; +import { + ColumnName, + DataRow, + DummyRow, + ROW_HEIGHT, + RowName, + addPlusSign, + getCalleeRows, + getCallerRows, + getRowColor, + getScrollTargetIndex, + isFirstSubRow, + isLastSubRow, + isSubRow, + ratioString, + rowBgClassNames, + sizeToBottomStyle, + sizeToHeightStyle, + sizeToWidthStyle, +} from './utils/functions'; import {getTopAndBottomExpandedRowModel} from './utils/topAndBottomExpandedRowModel'; const FIELD_MAPPING_FILE = 'mapping_file'; @@ -51,30 +76,6 @@ const FIELD_CUMULATIVE_DIFF = 'cumulative_diff'; const FIELD_CALLERS = 'callers'; const FIELD_CALLEES = 'callees'; -export interface DataRow { - id: number; - name: string; - flat: bigint; - flatDiff: bigint; - cumulative: bigint; - cumulativeDiff: bigint; - mappingFile: string; - functionSystemName: string; - functionFileName: string; - callers?: DataRow[]; - callees?: DataRow[]; - subRows?: Row[]; - isTopSubRow?: boolean; - isBottomSubRow?: boolean; -} - -interface DummyRow { - size: number; - message?: string; - isTopSubRow?: boolean; - isBottomSubRow?: boolean; -} - export type Row = DataRow | DummyRow; export const isDummyRow = (row: Row): row is DummyRow => { @@ -94,50 +95,9 @@ interface TableProps { setActionButtons?: (buttons: React.JSX.Element) => void; isHalfScreen: boolean; unit?: string; + metadataMappingFiles?: string[]; } -export type ColumnName = - | 'flat' - | 'flatPercentage' - | 'flatDiff' - | 'flatDiffPercentage' - | 'cumulative' - | 'cumulativePercentage' - | 'cumulativeDiff' - | 'cumulativeDiffPercentage' - | 'name' - | 'functionSystemName' - | 'functionFileName' - | 'mappingFile'; - -const rowBgClassNames = (isExpanded: boolean, isSubRow: boolean): Record => { - return { - relative: true, - 'bg-indigo-100 dark:bg-gray-600': isSubRow, - 'bg-indigo-50 dark:bg-gray-700': isExpanded, - }; -}; - -const ROW_HEIGHT = 29; - -const sizeToHeightStyle = (size: number): Record => { - return { - height: `${size * ROW_HEIGHT}px`, - }; -}; - -const sizeToWidthStyle = (size: number): Record => { - return { - width: `${size * ROW_HEIGHT}px`, - }; -}; - -const sizeToBottomStyle = (size: number): Record => { - return { - bottom: `-${size * ROW_HEIGHT}px`, - }; -}; - const CustomRowRenderer = ({ row, usePointerCursor, @@ -266,68 +226,6 @@ const CustomRowRenderer = ({ ); }; -const getCallerRows = (callers: DataRow[]): Row[] => { - if (callers.length === 0) { - return [{size: 3, message: 'No callers.', isTopSubRow: true}]; - } - - const rows = callers.map(row => { - return {...row, isTopSubRow: true}; - }); - if (rows.length >= 3) { - return rows; - } - - return [...rows, {size: 3 - rows.length, message: '', isTopSubRow: true}]; -}; - -const getCalleeRows = (callees: DataRow[]): Row[] => { - if (callees.length === 0) { - return [{size: 3, message: 'No callees.', isBottomSubRow: true}]; - } - - const rows = callees.map(row => { - return {...row, isBottomSubRow: true}; - }); - if (rows.length >= 3) { - return rows; - } - - return [{size: 3 - rows.length, message: '', isBottomSubRow: true}, ...rows]; -}; - -export const getPercentageString = (value: bigint | number, total: bigint | number): string => { - if (total === 0n) { - return '0%'; - } - - const percentage = (Number(value) / Number(total)) * 100; - return `${percentage.toFixed(2)}%`; -}; - -export const getRatioString = (value: bigint | number, total: bigint, filtered: bigint): string => { - if (filtered === 0n) { - return ` ${getPercentageString(value, total)}`; - } - - return `${getPercentageString(value, total)} / ${getPercentageString(value, filtered)}`; -}; - -export const possibleColumns = [ - 'flat', - 'flatPercentage', - 'flatDiff', - 'flatDiffPercentage', - 'cumulative', - 'cumulativePercentage', - 'cumulativeDiff', - 'cumulativeDiffPercentage', - 'name', - 'functionSystemName', - 'functionFileName', - 'mappingFile', -]; - export const Table = React.memo(function Table({ data, total, @@ -338,43 +236,81 @@ export const Table = React.memo(function Table({ setSearchString = () => {}, isHalfScreen, unit, + metadataMappingFiles, }: TableProps): React.JSX.Element { + const currentColorProfile = useCurrentColorProfile(); const [dashboardItems] = useURLState('dashboard_items', { alwaysReturnArray: true, }); const [tableColumns] = useURLState('table_columns', { alwaysReturnArray: true, }); - + const [colorBy, setColorBy] = useURLState('color_by'); const {isDarkMode} = useParcaContext(); const [expanded, setExpanded] = useState({}); const [scrollToIndex, setScrollToIndex] = useState(undefined); const {compareMode} = useProfileViewContext(); - const percentageString = (value: bigint | number, total: bigint | number): string => { - if (total === 0n) { - return '0%'; + const table = useMemo(() => { + if (loading || data == null) { + return null; } - const percentage = (Number(value) / Number(total)) * 100; - return `${percentage.toFixed(2)}%`; - }; + return tableFromIPC(data); + }, [data, loading]); + + const mappingsList = useMappingList(metadataMappingFiles); + const filenamesList = useFilenamesList(table); + const colorByValue = colorBy === undefined || colorBy === '' ? 'binary' : (colorBy as string); + + const mappingsListCount = useMemo( + () => mappingsList.filter(m => m !== '').length, + [mappingsList] + ); - const ratioString = (value: bigint | number): string => { - if (filtered === 0n) { - return ` ${percentageString(value, total)}`; + // If there is only one mapping file, we want to color by filename by default. + useEffect(() => { + if (mappingsListCount === 1 && colorBy !== 'filename') { + setColorBy('filename'); } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [mappingsListCount]); + + const filenameColors = useMemo(() => { + const colors = getFilenameColors(filenamesList, isDarkMode, currentColorProfile); + return colors; + }, [isDarkMode, filenamesList, currentColorProfile]); - return `${percentageString(value, total)} / ${percentageString(value, filtered)}`; + const mappingColors = useMemo(() => { + const colors = getMappingColors(mappingsList, isDarkMode, currentColorProfile); + return colors; + }, [isDarkMode, mappingsList, currentColorProfile]); + + const colorByList = { + filename: filenameColors, + binary: mappingColors, }; + type ColorByKey = keyof typeof colorByList; + + const colorByColors: colorByColors = colorByList[colorByValue as ColorByKey]; + const columnHelper = createColumnHelper(); unit = useMemo(() => unit ?? profileType?.sampleUnit ?? '', [unit, profileType?.sampleUnit]); const columns = useMemo>>(() => { return [ + columnHelper.accessor('color', { + id: 'color', + header: '', + cell: info => { + const color = info.getValue() as string; + return
; + }, + size: 10, + }), columnHelper.accessor('flat', { id: 'flat', header: 'Flat', @@ -392,7 +328,7 @@ export const Table = React.memo(function Table({ if (isDummyRow(info.row.original)) { return ''; } - return ratioString((info as CellContext).getValue()); + return ratioString((info as CellContext).getValue(), total, filtered); }, size: 120, meta: { @@ -418,7 +354,7 @@ export const Table = React.memo(function Table({ if (isDummyRow(info.row.original)) { return ''; } - return ratioString((info as CellContext).getValue()); + return ratioString((info as CellContext).getValue(), total, filtered); }, size: 120, meta: { @@ -443,7 +379,7 @@ export const Table = React.memo(function Table({ if (isDummyRow(info.row.original)) { return ''; } - return ratioString((info as CellContext).getValue()); + return ratioString((info as CellContext).getValue(), total, filtered); }, size: 150, meta: { @@ -469,7 +405,7 @@ export const Table = React.memo(function Table({ if (isDummyRow(info.row.original)) { return ''; } - return ratioString((info as CellContext).getValue()); + return ratioString((info as CellContext).getValue(), total, filtered); }, size: 170, meta: { @@ -503,6 +439,7 @@ export const Table = React.memo(function Table({ const [columnVisibility, setColumnVisibility] = useState(() => { return { + color: true, flat: true, flatPercentage: false, flatDiff: compareMode, @@ -552,37 +489,40 @@ export const Table = React.memo(function Table({ [selectSpan, dashboardItems.length] ); - const onRowDoubleClick = useCallback((row: RowType, rows: Array>) => { - if (isDummyRow(row.original)) { - return; - } - if (!isSubRow(row.original)) { - row.toggleExpanded(); - return; - } - // find the original row for this subrow and toggle it - const newRow = rows.find( - r => - !isDummyRow(r.original) && - !isDummyRow(row.original) && - r.original.name === row.original.name && - !isSubRow(r.original) - ); - const parentRow = rows.find(r => { - const parent = row.getParentRow()!; - if (isDummyRow(parent.original) || isDummyRow(r.original)) { - return false; + const onRowDoubleClick = useCallback( + (row: RowType, rows: Array>) => { + if (isDummyRow(row.original)) { + return; + } + if (!isSubRow(row.original)) { + row.toggleExpanded(); + return; + } + // find the original row for this subrow and toggle it + const newRow = rows.find( + r => + !isDummyRow(r.original) && + !isDummyRow(row.original) && + r.original.name === row.original.name && + !isSubRow(r.original) + ); + const parentRow = rows.find(r => { + const parent = row.getParentRow()!; + if (isDummyRow(parent.original) || isDummyRow(r.original)) { + return false; + } + return r.original.name === parent.original.name; + }); + if (parentRow == null || newRow == null) { + return; } - return r.original.name === parent.original.name; - }); - if (parentRow == null || newRow == null) { - return; - } - newRow.toggleExpanded(); + newRow.toggleExpanded(); - setScrollToIndex(getScrollTargetIndex(rows, parentRow, newRow)); - }, []); + setScrollToIndex(getScrollTargetIndex(rows, parentRow, newRow)); + }, + [setScrollToIndex] + ); const shouldHighlightRow = useCallback( (row: Row) => { @@ -608,14 +548,6 @@ export const Table = React.memo(function Table({ ]; }, [compareMode]); - const table = useMemo(() => { - if (loading || data == null) { - return null; - } - - return tableFromIPC(data); - }, [data, loading]); - const rows: DataRow[] = useMemo(() => { if (table == null || table.numRows === 0) { return []; @@ -644,6 +576,13 @@ export const Table = React.memo(function Table({ return { id: i, + color: getRowColor( + colorByColors, + mappingFileColumn, + i, + functionFileNameColumn, + colorBy as string + ), name: RowName(mappingFileColumn, locationAddressColumn, functionNameColumn, i), flat, flatDiff, @@ -676,7 +615,7 @@ export const Table = React.memo(function Table({ } return rows; - }, [table]); + }, [table, colorByColors, colorBy]); if (loading) { return ( @@ -734,85 +673,4 @@ export const Table = React.memo(function Table({ ); }); -export const addPlusSign = (num: string): string => { - if (num.charAt(0) === '0' || num.charAt(0) === '-') { - return num; - } - - return `+${num}`; -}; - -export const RowName = ( - mappingFileColumn: Vector | null, - locationAddressColumn: Vector | null, - functionNameColumn: Vector | null, - row: number -): string => { - if (mappingFileColumn === null) { - console.error('mapping_file column not found'); - return ''; - } - - const mappingFile: string | null = mappingFileColumn?.get(row); - let mapping = ''; - // Show the last item in the mapping file only if there are more than 1 mappings - if (mappingFile != null && mappingFileColumn.data.length > 1) { - mapping = `[${getLastItem(mappingFile) ?? ''}]`; - } - const functionName: string | null = functionNameColumn?.get(row) ?? ''; - if (functionName !== null && functionName !== '') { - return `${mapping} ${functionName}`; - } - - const address: bigint = locationAddressColumn?.get(row) ?? 0; - - return hexifyAddress(address); -}; - -const getRowsCount = (rows: Array>): number => { - if (rows.length < 6) { - return 6; - } - - return rows.length; -}; - -function getScrollTargetIndex( - rows: Array>, - parentRow: RowType, - newRow: RowType -): number { - const parentIndex = rows.indexOf(parentRow); - const newRowIndex = rows.indexOf(newRow); - let targetIndex = newRowIndex; - if (parentIndex > newRowIndex) { - // Adjusting the number of subs rows to scroll to the main row after expansion. - targetIndex -= getRowsCount(newRow.subRows); - } - if (parentIndex < newRowIndex) { - // If the parent row is above the new row, we need to adjust the number of subrows of the parent. - targetIndex += getRowsCount(parentRow.subRows); - } - if (targetIndex < 0) { - targetIndex = 0; - } - return targetIndex; -} - -function isSubRow(row: Row): boolean { - return row.isTopSubRow === true || row.isBottomSubRow === true; -} - -function isLastSubRow(row: RowType, rows: Array>): boolean { - const index = rows.indexOf(row); - const nextRow = rows[index + 1]; - return nextRow == null || (!isSubRow(nextRow.original) && !nextRow.getIsExpanded()); -} - -function isFirstSubRow(row: RowType, rows: Array>): boolean { - const index = rows.indexOf(row); - const prevRow = rows[index - 1]; - return prevRow == null || (!isSubRow(prevRow.original) && !prevRow.getIsExpanded()); -} - export default Table; diff --git a/ui/packages/shared/profile/src/Table/utils/functions.ts b/ui/packages/shared/profile/src/Table/utils/functions.ts new file mode 100644 index 00000000000..c7d70bdd1cf --- /dev/null +++ b/ui/packages/shared/profile/src/Table/utils/functions.ts @@ -0,0 +1,284 @@ +// Copyright 2022 The Parca Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import {type Row as RowType} from '@tanstack/table-core'; +import {Vector} from 'apache-arrow'; + +import {getLastItem} from '@parca/utilities'; + +import {colorByColors} from '../../ProfileIcicleGraph/IcicleGraphArrow/IcicleGraphNodes'; +import {hexifyAddress} from '../../utils'; + +export interface DataRow { + id: number; + color: string; + name: string; + flat: bigint; + flatDiff: bigint; + cumulative: bigint; + cumulativeDiff: bigint; + mappingFile: string; + functionSystemName: string; + functionFileName: string; + callers?: DataRow[]; + callees?: DataRow[]; + subRows?: Row[]; + isTopSubRow?: boolean; + isBottomSubRow?: boolean; +} + +export interface DummyRow { + size: number; + message?: string; + isTopSubRow?: boolean; + isBottomSubRow?: boolean; +} + +export type Row = DataRow | DummyRow; + +export const addPlusSign = (num: string): string => { + if (num.charAt(0) === '0' || num.charAt(0) === '-') { + return num; + } + + return `+${num}`; +}; + +export const getRowColor = ( + colorByColors: colorByColors, + mappingFileColumn: Vector | null, + row: number, + functionFileNameColumn: Vector | null, + colorBy: string +): string => { + if (mappingFileColumn === null) { + console.error('mapping_file column not found'); + return ''; + } + + const mappingFile: string | null = mappingFileColumn?.get(row); + const functionFileName: string | null = functionFileNameColumn?.get(row); + let mapping = ''; + let filename = ''; + + if (mappingFile != null) { + mapping = `${getLastItem(mappingFile) ?? ''}`; + } + + if (functionFileName != null) { + filename = `${getLastItem(functionFileName) ?? ''}`; + } + + if (colorBy === 'filename') { + return colorByColors[filename === '' ? 'Everything else' : filename]; + } + + return colorByColors[mapping]; +}; + +export const RowName = ( + mappingFileColumn: Vector | null, + locationAddressColumn: Vector | null, + functionNameColumn: Vector | null, + row: number +): string => { + if (mappingFileColumn === null) { + console.error('mapping_file column not found'); + return ''; + } + + const mappingFile: string | null = mappingFileColumn?.get(row); + let mapping = ''; + // Show the last item in the mapping file only if there are more than 1 mappings + if (mappingFile != null && mappingFileColumn.data.length > 1) { + mapping = `[${getLastItem(mappingFile) ?? ''}]`; + } + const functionName: string | null = functionNameColumn?.get(row) ?? ''; + if (functionName !== null && functionName !== '') { + return `${mapping} ${functionName}`; + } + + const address: bigint = locationAddressColumn?.get(row) ?? 0; + + return hexifyAddress(address); +}; + +export const getRowsCount = (rows: Array>): number => { + if (rows.length < 6) { + return 6; + } + + return rows.length; +}; + +export function getScrollTargetIndex( + rows: Array>, + parentRow: RowType, + newRow: RowType +): number { + const parentIndex = rows.indexOf(parentRow); + const newRowIndex = rows.indexOf(newRow); + let targetIndex = newRowIndex; + if (parentIndex > newRowIndex) { + // Adjusting the number of subs rows to scroll to the main row after expansion. + targetIndex -= getRowsCount(newRow.subRows); + } + if (parentIndex < newRowIndex) { + // If the parent row is above the new row, we need to adjust the number of subrows of the parent. + targetIndex += getRowsCount(parentRow.subRows); + } + if (targetIndex < 0) { + targetIndex = 0; + } + return targetIndex; +} + +export function isSubRow(row: Row): boolean { + return row.isTopSubRow === true || row.isBottomSubRow === true; +} + +export function isLastSubRow(row: RowType, rows: Array>): boolean { + const index = rows.indexOf(row); + const nextRow = rows[index + 1]; + return nextRow == null || (!isSubRow(nextRow.original) && !nextRow.getIsExpanded()); +} + +export function isFirstSubRow(row: RowType, rows: Array>): boolean { + const index = rows.indexOf(row); + const prevRow = rows[index - 1]; + return prevRow == null || (!isSubRow(prevRow.original) && !prevRow.getIsExpanded()); +} + +export type ColumnName = + | 'flat' + | 'flatPercentage' + | 'flatDiff' + | 'flatDiffPercentage' + | 'cumulative' + | 'cumulativePercentage' + | 'cumulativeDiff' + | 'cumulativeDiffPercentage' + | 'name' + | 'functionSystemName' + | 'functionFileName' + | 'mappingFile'; + +export const rowBgClassNames = ( + isExpanded: boolean, + isSubRow: boolean +): Record => { + return { + relative: true, + 'bg-indigo-100 dark:bg-gray-600': isSubRow, + 'bg-indigo-50 dark:bg-gray-700': isExpanded, + }; +}; + +export const ROW_HEIGHT = 29; + +export const sizeToHeightStyle = (size: number): Record => { + return { + height: `${size * ROW_HEIGHT}px`, + }; +}; + +export const sizeToWidthStyle = (size: number): Record => { + return { + width: `${size * ROW_HEIGHT}px`, + }; +}; + +export const sizeToBottomStyle = (size: number): Record => { + return { + bottom: `-${size * ROW_HEIGHT}px`, + }; +}; + +export const getCallerRows = (callers: DataRow[]): Row[] => { + if (callers.length === 0) { + return [{size: 3, message: 'No callers.', isTopSubRow: true}]; + } + + const rows = callers.map(row => { + return {...row, isTopSubRow: true}; + }); + if (rows.length >= 3) { + return rows; + } + + return [...rows, {size: 3 - rows.length, message: '', isTopSubRow: true}]; +}; + +export const getCalleeRows = (callees: DataRow[]): Row[] => { + if (callees.length === 0) { + return [{size: 3, message: 'No callees.', isBottomSubRow: true}]; + } + + const rows = callees.map(row => { + return {...row, isBottomSubRow: true}; + }); + if (rows.length >= 3) { + return rows; + } + + return [{size: 3 - rows.length, message: '', isBottomSubRow: true}, ...rows]; +}; + +export const getPercentageString = (value: bigint | number, total: bigint | number): string => { + if (total === 0n) { + return '0%'; + } + + const percentage = (Number(value) / Number(total)) * 100; + return `${percentage.toFixed(2)}%`; +}; + +export const getRatioString = (value: bigint | number, total: bigint, filtered: bigint): string => { + if (filtered === 0n) { + return ` ${getPercentageString(value, total)}`; + } + + return `${getPercentageString(value, total)} / ${getPercentageString(value, filtered)}`; +}; + +export const possibleColumns = [ + 'flat', + 'flatPercentage', + 'flatDiff', + 'flatDiffPercentage', + 'cumulative', + 'cumulativePercentage', + 'cumulativeDiff', + 'cumulativeDiffPercentage', + 'name', + 'functionSystemName', + 'functionFileName', + 'mappingFile', +]; + +export const percentageString = (value: bigint | number, total: bigint | number): string => { + if (total === 0n) { + return '0%'; + } + + const percentage = (Number(value) / Number(total)) * 100; + return `${percentage.toFixed(2)}%`; +}; + +export const ratioString = (value: bigint | number, total: bigint, filtered: bigint): string => { + if (filtered === 0n) { + return ` ${percentageString(value, total)}`; + } + + return `${percentageString(value, total)} / ${percentageString(value, filtered)}`; +}; diff --git a/ui/packages/shared/profile/src/components/ActionButtons/GroupByDropdown.tsx b/ui/packages/shared/profile/src/components/ActionButtons/GroupByDropdown.tsx index 9d6a628be49..65e45b0262b 100644 --- a/ui/packages/shared/profile/src/components/ActionButtons/GroupByDropdown.tsx +++ b/ui/packages/shared/profile/src/components/ActionButtons/GroupByDropdown.tsx @@ -180,7 +180,7 @@ const GroupByDropdown: React.FC = ({ return (
- +