// 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, {LegacyRef, ReactNode, useCallback, useEffect, useMemo, useState} from 'react'; import cx from 'classnames'; import {AnimatePresence, motion} from 'framer-motion'; import {useQueryState} from 'nuqs'; import {useMeasure} from 'react-use'; import {FlamegraphArrow} from '@parca/client'; import {FlameGraphSkeleton, SandwichFlameGraphSkeleton, useParcaContext} from '@parca/components'; import {ProfileType} from '@parca/parser'; import {TEST_IDS, testId} from '@parca/test-utils'; import {capitalizeOnlyFirstLetter, divide} from '@parca/utilities'; import {MergedProfileSource, ProfileSource} from '../ProfileSource'; import DiffLegend from '../ProfileView/components/DiffLegend'; import {useProfileViewContext} from '../ProfileView/context/ProfileViewContext'; import {useProfileMetadata} from '../ProfileView/hooks/useProfileMetadata'; import {useVisualizationState} from '../ProfileView/hooks/useVisualizationState'; import {boolParam} from '../hooks/urlParsers'; import {FlameGraphArrow} from './FlameGraphArrow'; import {CurrentPathFrame} from './FlameGraphArrow/utils'; const numberFormatter = new Intl.NumberFormat('en-US'); export type ResizeHandler = (width: number, height: number) => void; interface ProfileFlameGraphProps { width: number; arrow?: FlamegraphArrow; total: bigint; filtered: bigint; profileType?: ProfileType; profileSource: ProfileSource; curPathArrow: CurrentPathFrame[] | []; setNewCurPathArrow: (path: CurrentPathFrame[]) => void; loading: boolean; setActionButtons?: (buttons: React.JSX.Element) => void; error?: any; isHalfScreen: boolean; metadataMappingFiles?: string[]; metadataLoading?: boolean; isFlameChart?: boolean; isInSandwichView?: boolean; isRenderedAsFlamegraph?: boolean; tooltipId?: string; maxFrameCount?: number; isExpanded?: boolean; zoomControlsRef?: React.RefObject; } const ErrorContent = ({errorMessage}: {errorMessage: string | ReactNode}): JSX.Element => { return (
{errorMessage}
); }; export const validateFlameChartQuery = ( profileSource: MergedProfileSource ): {isValid: boolean; isNonDelta: boolean; isDurationTooLong: boolean} => { const isNonDelta = !profileSource.ProfileType().delta; const duration = profileSource.mergeTo - profileSource.mergeFrom; const isDurationTooLong = duration > 900_000_000_000n; // 15 minutes in nanoseconds return {isValid: !isNonDelta && !isDurationTooLong, isNonDelta, isDurationTooLong}; }; const ProfileFlameGraph = function ProfileFlameGraphNonMemo({ arrow, total, filtered, curPathArrow, setNewCurPathArrow, profileType, loading, error, width, isHalfScreen, metadataMappingFiles, isFlameChart = false, profileSource, isInSandwichView = false, isRenderedAsFlamegraph = false, tooltipId, maxFrameCount, isExpanded = false, metadataLoading = false, zoomControlsRef, }: ProfileFlameGraphProps): JSX.Element { const {onError, authenticationErrorMessage, isDarkMode, flamechartHelpText} = useParcaContext(); const {compareMode} = useProfileViewContext(); const [isLoading, setIsLoading] = useState(true); const [flameChartRef] = useMeasure(); const {colorBy, setColorBy} = useVisualizationState(); // Create local state for paths when in sandwich view to avoid URL updates const [localCurPathArrow, setLocalCurPathArrow] = useState([]); const setCurPathArrowWrapper = useCallback( (path: CurrentPathFrame[]) => { if (isInSandwichView) { setLocalCurPathArrow(path); } else { setNewCurPathArrow(path); } }, [isInSandwichView, setNewCurPathArrow] ); // Determine which paths to use based on isInSandwichView flag const effectiveCurPathArrow = isInSandwichView ? localCurPathArrow : curPathArrow; const {mappingsList, filenamesList} = useProfileMetadata({ flamegraphArrow: arrow, metadataMappingFiles, metadataLoading, colorBy, }); // By default, we want delta profiles (CPU) to be relatively compared. // For non-delta profiles, like goroutines or memory, we want the profiles to be compared absolutely. const compareAbsoluteDefault = profileType?.delta === false ? 'true' : 'false'; const [compareAbsolute] = useQueryState('compare_absolute', boolParam); const isCompareAbsolute = compareAbsolute ?? compareAbsoluteDefault === 'true'; const mappingsListCount = useMemo( () => mappingsList.filter(m => m !== '').length, [mappingsList] ); const [ totalFormatted, totalUnfilteredFormatted, isTrimmed, trimmedFormatted, trimmedPercentage, isFiltered, filteredPercentage, ] = useMemo(() => { if (arrow === undefined) { return ['0', '0', false, '0', '0', false, '0', '0']; } const trimmed: bigint = arrow?.trimmed ?? 0n; const totalUnfiltered = total + filtered; // safeguard against division by zero const totalUnfilteredDivisor = totalUnfiltered > 0 ? totalUnfiltered : 1n; return [ numberFormatter.format(total), numberFormatter.format(totalUnfiltered), trimmed > 0, numberFormatter.format(trimmed), numberFormatter.format(divide(trimmed * 100n, totalUnfilteredDivisor)), filtered > 0, numberFormatter.format(divide(total * 100n, totalUnfilteredDivisor)), ]; }, [arrow, filtered, total]); const loadingState = !loading && arrow !== undefined && metadataMappingFiles !== undefined; // 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]); useEffect(() => { if (loadingState) { setIsLoading(false); } else { setIsLoading(true); } }, [loadingState]); const flameGraph = useMemo(() => { const { isValid: isFlameChartValid, isNonDelta, isDurationTooLong, } = isFlameChart ? validateFlameChartQuery(profileSource as MergedProfileSource) : {isValid: true, isNonDelta: false, isDurationTooLong: false}; const isInvalidFlameChartQuery = isFlameChart && !isFlameChartValid; if (isLoading && !isInvalidFlameChartQuery) { return (
{isRenderedAsFlamegraph ? ( ) : ( )}
); } // Do necessary checks to ensure that flame chart can be rendered for this query. if (isInvalidFlameChartQuery) { if (isNonDelta) { return ( To use the Flame chart, please switch to a Delta profile. {flamechartHelpText ?? null} } /> ); } else if (isDurationTooLong) { return ( Flame chart is unavailable for queries longer than 15 minutes. Please select a point in the metrics graph to continue. {flamechartHelpText ?? null} } /> ); } else { return ( The Flame chart is not available for this query. {flamechartHelpText ?? null} } /> ); } } if (arrow === undefined) return
No data...
; if (total === 0n && !loading) return
Profile has no samples
; if (arrow !== undefined) { return (
}>
); } }, [ isLoading, arrow, total, loading, width, filtered, profileType, isHalfScreen, isDarkMode, isCompareAbsolute, isFlameChart, profileSource, flameChartRef, flamechartHelpText, isRenderedAsFlamegraph, isInSandwichView, effectiveCurPathArrow, setCurPathArrowWrapper, tooltipId, maxFrameCount, isExpanded, mappingsList, filenamesList, colorBy, zoomControlsRef, ]); useEffect(() => { if (isTrimmed) { console.info(`Trimmed ${trimmedFormatted} (${trimmedPercentage}%) too small values.`); } }, [isTrimmed, trimmedFormatted, trimmedPercentage]); if (error != null) { onError?.(error); if (authenticationErrorMessage !== undefined && error.code === 'UNAUTHENTICATED') { return ; } // Check for specific merge errors const errorMessageLower = error.message?.toLowerCase() ?? ''; const isMergeError: boolean = errorMessageLower.includes('failed to merge flame chart records'); const isTimestampError: boolean = errorMessageLower.includes( 'multiple samples for the same timestamp is not allowed' ); if (isMergeError || isTimestampError) { return ( Unable to display overlapping data The selected data contains overlapping samples from multiple nodes or threads that cannot be merged. To view this data, please apply more specific filters:
  • Select a specific node from the node selector
  • Filter by either CPU or thread
} /> ); } return ( {capitalizeOnlyFirstLetter(error.message)} {isFlameChart ? flamechartHelpText ?? null : null} } /> ); } return ( {compareMode ? : null}
<>{flameGraph}
{!isInSandwichView && (

Showing {totalFormatted}{' '} {isFiltered ? ( ({filteredPercentage}%) filtered of {totalUnfilteredFormatted}{' '} ) : ( <> )} values.{' '}

)}
); }; export default ProfileFlameGraph;