// 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} from 'react'; import {tableFromIPC} from '@uwdata/flechette'; import {AnimatePresence, motion} from 'framer-motion'; import {useQueryState} from 'nuqs'; import {Item, Menu, useContextMenu} from 'react-contexify'; import {Source} from '@parca/client'; import {SourceSkeleton, useParcaContext, type ProfileData} from '@parca/components'; import {ExpandOnHover} from '../GraphTooltipArrow/ExpandOnHoverValue'; import {stringParam} from '../hooks/urlParsers'; import {alignedUint8Array, truncateStringReverse} from '../utils'; import {Highlighter, profileAwareRenderer, type LineDataLookup} from './Highlighter'; import useLineRange from './useSelectedLineRange'; interface SourceViewProps { loading: boolean; data?: Source; total: bigint; filtered: bigint; setActionButtons?: (buttons: JSX.Element) => void; } const MENU_ID = 'source-view-context-menu'; export const SourceView = React.memo(function SourceView({ data, loading, total, filtered, setActionButtons, }: SourceViewProps): JSX.Element { const [sourceFileName] = useQueryState('source_filename', stringParam); const {isDarkMode, sourceViewContextMenuItems = []} = useParcaContext(); const sourceCode = useMemo(() => { if (data === undefined) { return ['']; } // To use the array index as line number return ['', ...data.source.split('\n')]; }, [data]); const {show} = useContextMenu({ id: MENU_ID, }); const {startLine, endLine} = useLineRange(); const sourceTable = useMemo(() => { if (data === undefined) { return null; } const table = tableFromIPC(alignedUint8Array(data.record), {useBigInt: true}); return { numRows: table.numRows, lineNumbers: table.getChild('line_number'), cumulative: table.getChild('cumulative'), flat: table.getChild('flat'), }; }, [data]); const getLineData: LineDataLookup = useCallback( (lineNumber: number) => { if (sourceTable === null || sourceTable.lineNumbers === null) { return undefined; } const {numRows, lineNumbers, cumulative, flat} = sourceTable; let lo = 0; let hi = numRows - 1; while (lo <= hi) { const mid = (lo + hi) >>> 1; const midVal = Number(lineNumbers.get(mid)); if (midVal === lineNumber) { return { cumulative: (cumulative?.get(mid) as bigint) ?? 0n, flat: (flat?.get(mid) as bigint) ?? 0n, }; } if (midVal < lineNumber) { lo = mid + 1; } else { hi = mid - 1; } } return undefined; }, [sourceTable] ); const getProfileDataForLine = useCallback( (line: number): ProfileData | undefined => { const data = getLineData(line); if (data === undefined) { return undefined; } if (data.cumulative === 0n && data.flat === 0n) { return undefined; } return { line, cumulative: Number(data.cumulative), flat: Number(data.flat), }; }, [getLineData] ); const [selectedCode, profileData] = useMemo(() => { if (startLine === -1 && endLine === -1) { return ['', []]; } if (startLine === endLine) { const profileData: ProfileData[] = []; const profileDataForLine = getProfileDataForLine(startLine); if (profileDataForLine != null) { profileData.push(profileDataForLine); } return [sourceCode[startLine - 1], profileData]; } let code = ''; const profileData: ProfileData[] = []; for (let i = startLine; i <= endLine; i++) { code += sourceCode[i] + '\n'; const profileDataForLine = getProfileDataForLine(i); if (profileDataForLine != null) { profileData.push(profileDataForLine); } } return [code, profileData]; }, [startLine, endLine, sourceCode, getProfileDataForLine]); useEffect(() => { setActionButtons?.(