// 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 React, {useCallback, useEffect, useMemo, useRef} from 'react'; import {RpcError} from '@protobuf-ts/runtime-rpc'; import {tableFromIPC} from '@uwdata/flechette'; import {AnimatePresence, motion} from 'framer-motion'; import {useQueryState} from 'nuqs'; import {useContextMenu} from 'react-contexify'; import {Table as TableComponent, TableSkeleton, useParcaContext} from '@parca/components'; import {useCurrentColorProfile} from '@parca/hooks'; import {ProfileType} from '@parca/parser'; import useMappingList, { useFilenamesList, } from '../ProfileFlameGraph/FlameGraphArrow/useMappingList'; import {useProfileViewContext} from '../ProfileView/context/ProfileViewContext'; import {stringParam} from '../hooks/urlParsers'; import {useColorBy} from '../hooks/useColorBy'; import {useDashboardItems} from '../hooks/useDashboardItems'; import {alignedUint8Array} from '../utils'; import TableContextMenuWrapper, {TableContextMenuWrapperRef} from './TableContextMenuWrapper'; import {useColorManagement} from './hooks/useColorManagement'; import {useTableConfiguration} from './hooks/useTableConfiguration'; import {DataRow, ROW_HEIGHT, RowName, getRowColor} from './utils/functions'; export const FIELD_MAPPING_FILE = 'mapping_file'; export const FIELD_LOCATION_ADDRESS = 'location_address'; export const FIELD_FUNCTION_NAME = 'function_name'; export const FIELD_FUNCTION_SYSTEM_NAME = 'function_system_name'; export const FIELD_FUNCTION_FILE_NAME = 'function_file_name'; export const FIELD_FLAT = 'flat'; export const FIELD_FLAT_DIFF = 'flat_diff'; export const FIELD_CUMULATIVE = 'cumulative'; export const FIELD_CUMULATIVE_DIFF = 'cumulative_diff'; export const FIELD_CALLERS = 'callers'; export const FIELD_CALLEES = 'callees'; export type Row = DataRow; export interface TableProps { data?: Uint8Array; total: bigint; filtered: bigint; profileType?: ProfileType; loading: boolean; setActionButtons?: (buttons: React.JSX.Element) => void; isHalfScreen: boolean; unit?: string; metadataMappingFiles?: string[]; error: RpcError | null; } export const Table = React.memo(function Table({ data, total, filtered, profileType, loading, isHalfScreen, unit, metadataMappingFiles, error, }: TableProps): React.JSX.Element { const currentColorProfile = useCurrentColorProfile(); const {dashboardItems} = useDashboardItems(); const [_, setSandwichFunctionName] = useQueryState('sandwich_function_name', stringParam); const {colorBy, setColorBy} = useColorBy(); const {isDarkMode} = useParcaContext(); const {compareMode} = useProfileViewContext(); const MENU_ID = 'table-context-menu'; const contextMenuRef = useRef(null); const {show} = useContextMenu({ id: MENU_ID, }); const table = useMemo(() => { if (loading || data == null) { return null; } return tableFromIPC(alignedUint8Array(data), {useBigInt: true}); }, [data, loading]); const mappingsList = useMappingList(metadataMappingFiles); const filenamesList = useFilenamesList(table); const mappingsListCount = useMemo( () => mappingsList.filter(m => m !== '').length, [mappingsList] ); // If there is only one mapping file, we want to color by filename by default. useEffect(() => { if (mappingsListCount === 1 && colorBy !== 'filename') { void setColorBy('filename'); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [mappingsListCount]); const {colorByColors} = useColorManagement({ isDarkMode, currentColorProfile, mappingsList, filenamesList, colorBy, }); unit = useMemo(() => unit ?? profileType?.sampleUnit ?? '', [unit, profileType?.sampleUnit]); const tableConfig = useTableConfiguration({ unit, total, filtered, compareMode, }); const {columns, initialSorting, columnVisibility} = tableConfig; const selectSpan = useCallback( (span: string): void => { if (!dashboardItems.includes('flamegraph')) { void setSandwichFunctionName(span.trim()); } }, [setSandwichFunctionName, dashboardItems] ); const onRowClick = useCallback( (row: Row) => { // If there is only one dashboard item, we don't want to select a span if (dashboardItems.length <= 1) { return; } selectSpan(row.name); }, [selectSpan, dashboardItems.length] ); const onRowContextMenu = useCallback( (e: React.MouseEvent, row: Row) => { e.preventDefault(); contextMenuRef.current?.setRow(row, () => { show({ event: e, }); }); }, [show] ); const rows: DataRow[] = useMemo(() => { if (table == null || table.numRows === 0) { return []; } const flatColumn = table.getChild(FIELD_FLAT); const flatDiffColumn = table.getChild(FIELD_FLAT_DIFF); const cumulativeColumn = table.getChild(FIELD_CUMULATIVE); const cumulativeDiffColumn = table.getChild(FIELD_CUMULATIVE_DIFF); const functionNameColumn = table.getChild(FIELD_FUNCTION_NAME); const functionSystemNameColumn = table.getChild(FIELD_FUNCTION_SYSTEM_NAME); const functionFileNameColumn = table.getChild(FIELD_FUNCTION_FILE_NAME); const mappingFileColumn = table.getChild(FIELD_MAPPING_FILE); const locationAddressColumn = table.getChild(FIELD_LOCATION_ADDRESS); const getRow = (i: number): DataRow => { const flat: bigint = flatColumn?.get(i) ?? 0n; const flatDiff: bigint = flatDiffColumn?.get(i) ?? 0n; const cumulative: bigint = cumulativeColumn?.get(i) ?? 0n; const cumulativeDiff: bigint = cumulativeDiffColumn?.get(i) ?? 0n; const functionSystemName: string = functionSystemNameColumn?.get(i) ?? ''; const functionFileName: string = functionFileNameColumn?.get(i) ?? ''; const mappingFile: string = mappingFileColumn?.get(i) ?? ''; return { id: i, colorProperty: { color: getRowColor(colorByColors, mappingFileColumn, i, functionFileNameColumn, colorBy), mappingFile, }, name: RowName(mappingFileColumn, locationAddressColumn, functionNameColumn, i), flat, flatDiff, cumulative, cumulativeDiff, functionSystemName, functionFileName, mappingFile, }; }; const rows: DataRow[] = Array.from({length: table.numRows}, (_, i) => getRow(i)); return rows; }, [table, colorByColors, colorBy]); if (loading) { return (
); } if (error != null) { return
Error: {error.message}
; } if (rows.length === 0) { return
Profile has no samples
; } return (
1} estimatedRowHeight={ROW_HEIGHT} />
); }); export default Table;