diff --git a/ui/packages/shared/components/src/hooks/URLState/index.tsx b/ui/packages/shared/components/src/hooks/URLState/index.tsx index 9f0a0b6b490..060ccb9b97b 100644 --- a/ui/packages/shared/components/src/hooks/URLState/index.tsx +++ b/ui/packages/shared/components/src/hooks/URLState/index.tsx @@ -144,7 +144,7 @@ export const useURLStateCustom = ( const [urlValue, setURLValue] = useURLState(param, _options); const val = useMemo(() => { - return parse(urlValue); + return parse(decodeURIComponent(urlValue)); }, [parse, urlValue]); const setVal = useCallback( @@ -157,4 +157,12 @@ export const useURLStateCustom = ( return [val, setVal]; }; +export const JSONSerializer = (val: object): string => { + return JSON.stringify(val, (_, v) => (typeof v === 'bigint' ? v.toString() : v)); +}; + +export const JSONParser = (val: ParamValue): T => { + return JSON.parse(val as string); +}; + export default URLStateContext; diff --git a/ui/packages/shared/profile/src/ProfileIcicleGraph/IcicleGraphArrow/ContextMenu.tsx b/ui/packages/shared/profile/src/ProfileIcicleGraph/IcicleGraphArrow/ContextMenu.tsx index 2638f016365..0c81b8cec7f 100644 --- a/ui/packages/shared/profile/src/ProfileIcicleGraph/IcicleGraphArrow/ContextMenu.tsx +++ b/ui/packages/shared/profile/src/ProfileIcicleGraph/IcicleGraphArrow/ContextMenu.tsx @@ -24,6 +24,7 @@ import {getLastItem} from '@parca/utilities'; import {useGraphTooltip} from '../../GraphTooltipArrow/useGraphTooltip'; import {useGraphTooltipMetaInfo} from '../../GraphTooltipArrow/useGraphTooltipMetaInfo'; import {hexifyAddress, truncateString} from '../../utils'; +import {CurrentPathFrame} from './utils'; interface ContextMenuProps { menuId: string; @@ -36,8 +37,8 @@ interface ContextMenuProps { level: number; compareAbsolute: boolean; trackVisibility: (isVisible: boolean) => void; - curPath: string[]; - setCurPath: (path: string[]) => void; + curPath: CurrentPathFrame[]; + setCurPath: (path: CurrentPathFrame[]) => void; hideMenu: () => void; hideBinary: (binaryToRemove: string) => void; } diff --git a/ui/packages/shared/profile/src/ProfileIcicleGraph/IcicleGraphArrow/IcicleGraphNodes.tsx b/ui/packages/shared/profile/src/ProfileIcicleGraph/IcicleGraphArrow/IcicleGraphNodes.tsx index fe0bbce18cf..0026035e4d6 100644 --- a/ui/packages/shared/profile/src/ProfileIcicleGraph/IcicleGraphArrow/IcicleGraphNodes.tsx +++ b/ui/packages/shared/profile/src/ProfileIcicleGraph/IcicleGraphArrow/IcicleGraphNodes.tsx @@ -33,7 +33,13 @@ import { FIELD_MAPPING_FILE, } from './index'; import useNodeColor from './useNodeColor'; -import {arrowToString, nodeLabel} from './utils'; +import { + CurrentPathFrame, + arrowToString, + getCurrentPathFrameData, + isCurrentPathFrameMatch, + nodeLabel, +} from './utils'; export const RowHeight = 26; @@ -48,11 +54,11 @@ interface IcicleGraphNodesProps { total: bigint; totalWidth: number; level: number; - curPath: string[]; - setCurPath: (path: string[]) => void; + curPath: CurrentPathFrame[]; + setCurPath: (path: CurrentPathFrame[]) => void; setHoveringRow: (row: number | null) => void; setHoveringLevel: (level: number | null) => void; - path: string[]; + path: CurrentPathFrame[]; xScale: (value: bigint) => number; searchString?: string; sortBy: string; @@ -104,7 +110,7 @@ export const IcicleGraphNodes = React.memo(function IcicleGraphNodesNoMemo({ childRows = curPath.length === 0 ? childRows - : childRows.filter(c => nodeLabel(table, c, level, false) === curPath[0]); + : childRows.filter(c => isCurrentPathFrameMatch(table, c, level, curPath[0])); let childrenCumulative = BigInt(0); const childrenElements: ReactNode[] = []; @@ -159,15 +165,15 @@ export interface IcicleNodeProps { y: number; height: number; totalWidth: number; - curPath: string[]; + curPath: CurrentPathFrame[]; level: number; table: Table; row: number; colors: colorByColors; colorBy: string; - path: string[]; + path: CurrentPathFrame[]; total: bigint; - setCurPath: (path: string[]) => void; + setCurPath: (path: CurrentPathFrame[]) => void; setHoveringRow: (row: number | null) => void; setHoveringLevel: (level: number | null) => void; xScale: (value: bigint) => number; @@ -343,8 +349,10 @@ export const IcicleNode = React.memo(function IcicleNodeNoMemo({ const name = useMemo(() => { return isRoot ? 'root' : nodeLabel(table, row, level, binaries.length > 1); }, [table, row, level, isRoot, binaries]); - const nextPath = path.concat([name]); - const isFaded = curPath.length > 0 && name !== curPath[curPath.length - 1]; + const currentPathFrame: CurrentPathFrame = getCurrentPathFrameData(table, row, level); + const nextPath = path.concat([currentPathFrame]); + const isFaded = + curPath.length > 0 && !isCurrentPathFrameMatch(table, row, level, curPath[curPath.length - 1]); const styles = isFaded ? fadedIcicleRectStyles : icicleRectStyles; const nextLevel = level + 1; const nextCurPath = curPath.length === 0 ? [] : curPath.slice(1); diff --git a/ui/packages/shared/profile/src/ProfileIcicleGraph/IcicleGraphArrow/index.tsx b/ui/packages/shared/profile/src/ProfileIcicleGraph/IcicleGraphArrow/index.tsx index b62d0e8aa4d..2b00bec2b2f 100644 --- a/ui/packages/shared/profile/src/ProfileIcicleGraph/IcicleGraphArrow/index.tsx +++ b/ui/packages/shared/profile/src/ProfileIcicleGraph/IcicleGraphArrow/index.tsx @@ -38,7 +38,7 @@ import ContextMenu from './ContextMenu'; import {IcicleChartRootNode} from './IcicleChartRootNode'; import {IcicleNode, RowHeight, colorByColors} from './IcicleGraphNodes'; import {useFilenamesList} from './useMappingList'; -import {arrowToString, extractFeature, extractFilenameFeature} from './utils'; +import {CurrentPathFrame, arrowToString, extractFeature, extractFilenameFeature} from './utils'; export const FIELD_LABELS_ONLY = 'labels_only'; export const FIELD_MAPPING_FILE = 'mapping_file'; @@ -66,8 +66,8 @@ interface IcicleGraphArrowProps { profileType?: ProfileType; profileSource?: ProfileSource; width?: number; - curPath: string[]; - setCurPath: (path: string[]) => void; + curPath: CurrentPathFrame[]; + setCurPath: (path: CurrentPathFrame[]) => void; sortBy: string; flamegraphLoading: boolean; isHalfScreen: boolean; diff --git a/ui/packages/shared/profile/src/ProfileIcicleGraph/IcicleGraphArrow/utils.ts b/ui/packages/shared/profile/src/ProfileIcicleGraph/IcicleGraphArrow/utils.ts index 920df952cec..c443093ffeb 100644 --- a/ui/packages/shared/profile/src/ProfileIcicleGraph/IcicleGraphArrow/utils.ts +++ b/ui/packages/shared/profile/src/ProfileIcicleGraph/IcicleGraphArrow/utils.ts @@ -26,6 +26,8 @@ import {MergedProfileSource, ProfileSource} from '../../ProfileSource'; import {BigIntDuo, hexifyAddress} from '../../utils'; import { FIELD_FUNCTION_NAME, + FIELD_FUNCTION_START_LINE, + FIELD_INLINED, FIELD_LABELS_ONLY, FIELD_LOCATION_ADDRESS, FIELD_MAPPING_FILE, @@ -149,3 +151,52 @@ export const boundsFromProfileSource = (profileSource?: ProfileSource): BigIntDu return [start, end]; }; + +export interface CurrentPathFrame { + functionName: string; + systemName: string; + fileName: string; + lineNumber: number; + address: string; + inlined: boolean; +} + +export const getCurrentPathFrameData = ( + table: Table, + row: number, + _level: number +): CurrentPathFrame => { + const functionName: string | null = arrowToString(table.getChild(FIELD_FUNCTION_NAME)?.get(row)); + const systemName: string | null = arrowToString(table.getChild(FIELD_FUNCTION_NAME)?.get(row)); + const fileName: string | null = arrowToString(table.getChild(FIELD_MAPPING_FILE)?.get(row)); + const lineNumber: bigint = table.getChild(FIELD_FUNCTION_START_LINE)?.get(row) ?? 0n; + const addressBigInt: bigint = table.getChild(FIELD_LOCATION_ADDRESS)?.get(row); + const address = hexifyAddress(addressBigInt); + const inlined: boolean | null = table.getChild(FIELD_INLINED)?.get(row); + + return { + functionName: functionName ?? '', + systemName: systemName ?? '', + fileName: fileName ?? '', + lineNumber: Number(lineNumber), + address: address, + inlined: inlined ?? false, + }; +}; + +export function isCurrentPathFrameMatch( + table: Table, + row: number, + level: number, + b: CurrentPathFrame +): boolean { + const a = getCurrentPathFrameData(table, row, level); + return ( + a.functionName === b.functionName && + a.systemName === b.systemName && + a.fileName === b.fileName && + a.lineNumber === b.lineNumber && + a.address === b.address && + a.inlined === b.inlined + ); +} diff --git a/ui/packages/shared/profile/src/ProfileIcicleGraph/index.tsx b/ui/packages/shared/profile/src/ProfileIcicleGraph/index.tsx index b1607c5e8a0..94431302ab4 100644 --- a/ui/packages/shared/profile/src/ProfileIcicleGraph/index.tsx +++ b/ui/packages/shared/profile/src/ProfileIcicleGraph/index.tsx @@ -28,7 +28,7 @@ import {TimelineGuide} from '../TimelineGuide'; import {IcicleGraph} from './IcicleGraph'; import {FIELD_FUNCTION_NAME, IcicleGraphArrow} from './IcicleGraphArrow'; import useMappingList from './IcicleGraphArrow/useMappingList'; -import {boundsFromProfileSource} from './IcicleGraphArrow/utils'; +import {CurrentPathFrame, boundsFromProfileSource} from './IcicleGraphArrow/utils'; const numberFormatter = new Intl.NumberFormat('en-US'); @@ -44,6 +44,8 @@ interface ProfileIcicleGraphProps { profileSource?: ProfileSource; curPath: string[] | []; setNewCurPath: (path: string[]) => void; + curPathArrow: CurrentPathFrame[] | []; + setNewCurPathArrow: (path: CurrentPathFrame[]) => void; loading: boolean; setActionButtons?: (buttons: React.JSX.Element) => void; error?: any; @@ -64,6 +66,8 @@ const ProfileIcicleGraph = function ProfileIcicleGraphNonMemo({ filtered, curPath, setNewCurPath, + curPathArrow, + setNewCurPathArrow, profileType, loading, error, @@ -191,8 +195,8 @@ const ProfileIcicleGraph = function ProfileIcicleGraphNonMemo({ arrow={arrow} total={total} filtered={filtered} - curPath={curPath} - setCurPath={setNewCurPath} + curPath={curPathArrow} + setCurPath={setNewCurPathArrow} profileType={profileType} sortBy={storeSortBy as string} flamegraphLoading={isLoading} @@ -216,6 +220,8 @@ const ProfileIcicleGraph = function ProfileIcicleGraphNonMemo({ filtered, curPath, setNewCurPath, + curPathArrow, + setNewCurPathArrow, profileType, storeSortBy, isHalfScreen, diff --git a/ui/packages/shared/profile/src/ProfileView/components/DashboardItems/index.tsx b/ui/packages/shared/profile/src/ProfileView/components/DashboardItems/index.tsx index 94ab82074f8..f52faefd147 100644 --- a/ui/packages/shared/profile/src/ProfileView/components/DashboardItems/index.tsx +++ b/ui/packages/shared/profile/src/ProfileView/components/DashboardItems/index.tsx @@ -17,6 +17,7 @@ import {ConditionalWrapper} from '@parca/components'; import Callgraph from '../../../Callgraph'; import ProfileIcicleGraph from '../../../ProfileIcicleGraph'; +import {CurrentPathFrame} from '../../../ProfileIcicleGraph/IcicleGraphArrow/utils'; import {ProfileSource} from '../../../ProfileSource'; import {SourceView} from '../../../SourceView'; import {Table} from '../../../Table'; @@ -42,6 +43,8 @@ interface GetDashboardItemProps { filtered: bigint; curPath: string[]; setNewCurPath: (path: string[]) => void; + curPathArrow: CurrentPathFrame[]; + setNewCurPathArrow: (path: CurrentPathFrame[]) => void; currentSearchString?: string; setSearchString?: (value: string) => void; callgraphSVG?: string; @@ -64,6 +67,8 @@ export const getDashboardItem = ({ filtered, curPath, setNewCurPath, + curPathArrow, + setNewCurPathArrow, currentSearchString, setSearchString, callgraphSVG, @@ -83,6 +88,8 @@ export const getDashboardItem = ({ {}} + curPathArrow={[]} + setNewCurPathArrow={() => {}} arrow={flamechartData?.arrow} total={total} filtered={filtered} diff --git a/ui/packages/shared/profile/src/ProfileView/components/Toolbars/index.tsx b/ui/packages/shared/profile/src/ProfileView/components/Toolbars/index.tsx index 1544c8b9934..59948bed63e 100644 --- a/ui/packages/shared/profile/src/ProfileView/components/Toolbars/index.tsx +++ b/ui/packages/shared/profile/src/ProfileView/components/Toolbars/index.tsx @@ -19,6 +19,7 @@ import {QueryServiceClient} from '@parca/client'; import {Button, UserPreferencesModal} from '@parca/components'; import {ProfileType} from '@parca/parser'; +import {CurrentPathFrame} from '../../../ProfileIcicleGraph/IcicleGraphArrow/utils'; import {ProfileSource} from '../../../ProfileSource'; import {useDashboard} from '../../context/DashboardContext'; import GroupByDropdown from '../ActionButtons/GroupByDropdown'; @@ -37,8 +38,8 @@ export interface VisualisationToolbarProps { profileSource?: ProfileSource; queryClient?: QueryServiceClient; onDownloadPProf: () => void; - curPath: string[]; - setNewCurPath: (path: string[]) => void; + curPath: CurrentPathFrame[]; + setNewCurPath: (path: CurrentPathFrame[]) => void; profileType?: ProfileType; total: bigint; filtered: bigint; @@ -61,8 +62,8 @@ export interface TableToolbarProps { } export interface IcicleGraphToolbarProps { - curPath: string[]; - setNewCurPath: (path: string[]) => void; + curPath: CurrentPathFrame[]; + setNewCurPath: (path: CurrentPathFrame[]) => void; } export const TableToolbar: FC = ({ diff --git a/ui/packages/shared/profile/src/ProfileView/hooks/useVisualizationState.ts b/ui/packages/shared/profile/src/ProfileView/hooks/useVisualizationState.ts index c3500a1c77a..3a899502324 100644 --- a/ui/packages/shared/profile/src/ProfileView/hooks/useVisualizationState.ts +++ b/ui/packages/shared/profile/src/ProfileView/hooks/useVisualizationState.ts @@ -13,13 +13,16 @@ import {useCallback, useState} from 'react'; -import {useURLState} from '@parca/components'; +import {JSONParser, JSONSerializer, useURLState, useURLStateCustom} from '@parca/components'; import {FIELD_FUNCTION_NAME, FIELD_LABELS} from '../../ProfileIcicleGraph/IcicleGraphArrow'; +import {CurrentPathFrame} from '../../ProfileIcicleGraph/IcicleGraphArrow/utils'; export const useVisualizationState = (): { curPath: string[]; setCurPath: (path: string[]) => void; + curPathArrow: CurrentPathFrame[]; + setCurPathArrow: (path: CurrentPathFrame[]) => void; currentSearchString: string | undefined; setSearchString: (searchString: string | undefined) => void; colorStackLegend: string | undefined; @@ -31,6 +34,11 @@ export const useVisualizationState = (): { setGroupByLabels: (labels: string[]) => void; } => { const [curPath, setCurPath] = useState([]); + const [curPathArrow, setCurPathArrow] = useURLStateCustom('cur_path', { + parse: JSONParser, + stringify: JSONSerializer, + defaultValue: '[]', + }); const [currentSearchString, setSearchString] = useURLState('search_string'); const [colorStackLegend] = useURLState('color_stack_legend'); const [colorBy] = useURLState('color_by'); @@ -69,6 +77,8 @@ export const useVisualizationState = (): { return { curPath, setCurPath, + curPathArrow, + setCurPathArrow, currentSearchString, setSearchString, colorStackLegend, diff --git a/ui/packages/shared/profile/src/ProfileView/index.tsx b/ui/packages/shared/profile/src/ProfileView/index.tsx index a7e352d2283..1adeccf9c0d 100644 --- a/ui/packages/shared/profile/src/ProfileView/index.tsx +++ b/ui/packages/shared/profile/src/ProfileView/index.tsx @@ -58,6 +58,8 @@ export const ProfileView = ({ const { curPath, setCurPath, + curPathArrow, + setCurPathArrow, currentSearchString, setSearchString, colorStackLegend, @@ -107,6 +109,8 @@ export const ProfileView = ({ filtered, curPath, setNewCurPath: setCurPath, + curPathArrow, + setNewCurPathArrow: setCurPathArrow, currentSearchString, setSearchString, callgraphSVG, @@ -115,7 +119,7 @@ export const ProfileView = ({ }; const actionButtons = { - icicle: , + icicle: , table: (