import {useCallback, useLayoutEffect, useMemo, useState} from 'react'; import styled from '@emotion/styled'; import * as Sentry from '@sentry/react'; import type {mat3} from 'gl-matrix'; import {vec2} from 'gl-matrix'; import {addErrorMessage} from 'sentry/actionCreators/indicator'; import Feature from 'sentry/components/acl/feature'; import {DifferentialFlamegraphLayout} from 'sentry/components/profiling/flamegraph/differentialFlamegraphLayout'; import {FlamegraphContextMenu} from 'sentry/components/profiling/flamegraph/flamegraphContextMenu'; import {DifferentialFlamegraphDrawer} from 'sentry/components/profiling/flamegraph/flamegraphDrawer/differentialFlamegraphDrawer'; import {DifferentialFlamegraphToolbar} from 'sentry/components/profiling/flamegraph/flamegraphToolbar/differentialFlamegraphToolbar'; import {FlamegraphZoomView} from 'sentry/components/profiling/flamegraph/flamegraphZoomView'; import {FlamegraphZoomViewMinimap} from 'sentry/components/profiling/flamegraph/flamegraphZoomViewMinimap'; import { CanvasPoolManager, useCanvasScheduler, } from 'sentry/utils/profiling/canvasScheduler'; import {CanvasView} from 'sentry/utils/profiling/canvasView'; import type {DifferentialFlamegraph as DifferentialFlamegraphModel} from 'sentry/utils/profiling/differentialFlamegraph'; import {FlamegraphStateProvider} from 'sentry/utils/profiling/flamegraph/flamegraphStateProvider/flamegraphContextProvider'; import {FlamegraphThemeProvider} from 'sentry/utils/profiling/flamegraph/flamegraphThemeProvider'; import {useFlamegraphPreferences} from 'sentry/utils/profiling/flamegraph/hooks/useFlamegraphPreferences'; import {useFlamegraphProfiles} from 'sentry/utils/profiling/flamegraph/hooks/useFlamegraphProfiles'; import {useFlamegraphTheme} from 'sentry/utils/profiling/flamegraph/useFlamegraphTheme'; import {FlamegraphCanvas} from 'sentry/utils/profiling/flamegraphCanvas'; import type {FlamegraphFrame} from 'sentry/utils/profiling/flamegraphFrame'; import type {Frame} from 'sentry/utils/profiling/frame'; import { computeConfigViewWithStrategy, formatColorForFrame, initializeFlamegraphRenderer, useResizeCanvasObserver, } from 'sentry/utils/profiling/gl/utils'; import {useCurrentProjectFromRouteParam} from 'sentry/utils/profiling/hooks/useCurrentProjectFromRouteParam'; import {useDifferentialFlamegraphModel} from 'sentry/utils/profiling/hooks/useDifferentialFlamegraphModel'; import {useDifferentialFlamegraphQuery} from 'sentry/utils/profiling/hooks/useDifferentialFlamegraphQuery'; import {FlamegraphRenderer2D} from 'sentry/utils/profiling/renderers/flamegraphRenderer2D'; import {FlamegraphRendererWebGL} from 'sentry/utils/profiling/renderers/flamegraphRendererWebGL'; import {Rect} from 'sentry/utils/profiling/speedscope'; import {useLocation} from 'sentry/utils/useLocation'; import usePageFilters from 'sentry/utils/usePageFilters'; import {LOADING_PROFILE_GROUP} from 'sentry/views/profiling/profileGroupProvider'; const noopFormatDuration = () => ''; function applicationFrameOnly(frame: Frame): boolean { return frame.is_application; } function systemFrameOnly(frame: Frame): boolean { return !frame.is_application; } function DifferentialFlamegraphView() { const location = useLocation(); const selection = usePageFilters(); const flamegraphTheme = useFlamegraphTheme(); const {colorCoding} = useFlamegraphPreferences(); const {selectedRoot} = useFlamegraphProfiles(); const [frameFilterSetting, setFrameFilterSetting] = useState< 'application' | 'system' | 'all' >('all'); const frameFilter = frameFilterSetting === 'application' ? applicationFrameOnly : frameFilterSetting === 'system' ? systemFrameOnly : undefined; const project = useCurrentProjectFromRouteParam(); const [negated, setNegated] = useState(false); const canvasPoolManager = useMemo(() => new CanvasPoolManager(), []); const scheduler = useCanvasScheduler(canvasPoolManager); const {before, after} = useDifferentialFlamegraphQuery({ projectID: parseInt((project?.id as string) ?? 0, 10), breakpoint: location.query.breakpoint as unknown as number, environments: selection.selection.environments, fingerprint: location.query.fingerprint as unknown as string, transaction: location.query.transaction as unknown as string, }); const differentialFlamegraph = useDifferentialFlamegraphModel({ before, after, frameFilter, negated, }); const [flamegraphCanvasRef, setFlamegraphCanvasRef] = useState(null); const [flamegraphOverlayCanvasRef, setFlamegraphOverlayCanvasRef] = useState(null); const [flamegraphMiniMapCanvasRef, setFlamegraphMiniMapCanvasRef] = useState(null); const [flamegraphMiniMapOverlayCanvasRef, setFlamegraphMiniMapOverlayCanvasRef] = useState(null); const flamegraphCanvas = useMemo(() => { if (!flamegraphCanvasRef) { return null; } return new FlamegraphCanvas(flamegraphCanvasRef, vec2.fromValues(0, 0)); }, [flamegraphCanvasRef]); const flamegraphView = useMemo | null>( () => { if (!flamegraphCanvas || !differentialFlamegraph.differentialFlamegraph) { return null; } const newView = new CanvasView({ canvas: flamegraphCanvas, model: differentialFlamegraph.differentialFlamegraph, options: { inverted: differentialFlamegraph.differentialFlamegraph.inverted, minWidth: differentialFlamegraph.differentialFlamegraph.profile.minFrameDuration, barHeight: flamegraphTheme.SIZES.BAR_HEIGHT, depthOffset: flamegraphTheme.SIZES.AGGREGATE_FLAMEGRAPH_DEPTH_OFFSET, configSpaceTransform: undefined, }, }); // Find p75 of the graphtree depth and set the view to 3/4 of that const depths: number[] = []; for (const frame of differentialFlamegraph.differentialFlamegraph.frames) { if (frame.children.length > 0) { continue; } depths.push(frame.depth); } if (depths.length > 0) { depths.sort(); const d = depths[Math.floor(depths.length - 1 * 0.75)]; const depth = Math.max(d, 0); newView.setConfigView( newView.configView.withY(depth - (newView.configView.height * 3) / 4) ); } return newView; }, // We skip position.view dependency because it will go into an infinite loop // eslint-disable-next-line react-hooks/exhaustive-deps [differentialFlamegraph.differentialFlamegraph, flamegraphCanvas, flamegraphTheme] ); // Uses a useLayoutEffect to ensure that these top level/global listeners are added before // any of the children components effects actually run. This way we do not lose events // when we register/unregister these top level listeners. useLayoutEffect(() => { if (!flamegraphCanvas || !flamegraphView) { return undefined; } // This code below manages the synchronization of the config views between spans and flamegraph // We do so by listening to the config view change event and then updating the other views accordingly which // allows us to keep the X axis in sync between the two views but keep the Y axis independent const onConfigViewChange = (rect: Rect, sourceConfigViewChange: CanvasView) => { if (sourceConfigViewChange === flamegraphView) { flamegraphView.setConfigView(rect.withHeight(flamegraphView.configView.height)); } canvasPoolManager.draw(); }; const onTransformConfigView = ( mat: mat3, sourceTransformConfigView: CanvasView ) => { if (sourceTransformConfigView === flamegraphView) { flamegraphView.transformConfigView(mat); } canvasPoolManager.draw(); }; const onResetZoom = () => { flamegraphView.resetConfigView(flamegraphCanvas); canvasPoolManager.draw(); }; const onZoomIntoFrame = (frame: FlamegraphFrame, strategy: 'min' | 'exact') => { const newConfigView = computeConfigViewWithStrategy( strategy, flamegraphView.configView, new Rect(frame.start, frame.depth, frame.end - frame.start, 1) ).transformRect(flamegraphView.configSpaceTransform); flamegraphView.setConfigView(newConfigView); canvasPoolManager.draw(); }; scheduler.on('set config view', onConfigViewChange); scheduler.on('transform config view', onTransformConfigView); scheduler.on('reset zoom', onResetZoom); scheduler.on('zoom at frame', onZoomIntoFrame); return () => { scheduler.off('set config view', onConfigViewChange); scheduler.off('transform config view', onTransformConfigView); scheduler.off('reset zoom', onResetZoom); scheduler.off('zoom at frame', onZoomIntoFrame); }; }, [canvasPoolManager, flamegraphCanvas, flamegraphView, scheduler]); const flamegraphCanvases = useMemo(() => { return [flamegraphCanvasRef, flamegraphOverlayCanvasRef]; }, [flamegraphCanvasRef, flamegraphOverlayCanvasRef]); const flamegraphCanvasBounds = useResizeCanvasObserver( flamegraphCanvases, canvasPoolManager, flamegraphCanvas, flamegraphView ); const minimapCanvases = useMemo(() => { return [flamegraphMiniMapCanvasRef, flamegraphMiniMapOverlayCanvasRef]; }, [flamegraphMiniMapCanvasRef, flamegraphMiniMapOverlayCanvasRef]); const flamegraphMiniMapCanvas = useMemo(() => { if (!flamegraphMiniMapCanvasRef) { return null; } return new FlamegraphCanvas(flamegraphMiniMapCanvasRef, vec2.fromValues(0, 0)); }, [flamegraphMiniMapCanvasRef]); useResizeCanvasObserver( minimapCanvases, canvasPoolManager, flamegraphMiniMapCanvas, null ); const flamegraphRenderer = useMemo(() => { if (!flamegraphCanvasRef || !differentialFlamegraph) { return null; } const renderer = initializeFlamegraphRenderer( [FlamegraphRendererWebGL, FlamegraphRenderer2D], [ flamegraphCanvasRef, differentialFlamegraph.differentialFlamegraph, flamegraphTheme, { colorCoding, draw_border: true, }, ] ); if (renderer === null) { Sentry.captureException('Failed to initialize a flamegraph renderer'); addErrorMessage('Failed to initialize renderer'); return null; } return renderer; }, [colorCoding, differentialFlamegraph, flamegraphCanvasRef, flamegraphTheme]); const getFrameColor = useCallback( (frame: FlamegraphFrame) => { if (!flamegraphRenderer) { return ''; } return formatColorForFrame(frame, flamegraphRenderer); }, [flamegraphRenderer] ); const rootNodes = useMemo(() => { return selectedRoot ? [selectedRoot] : differentialFlamegraph.differentialFlamegraph.root.children; }, [selectedRoot, differentialFlamegraph.differentialFlamegraph.root]); const referenceNode = useMemo( () => selectedRoot ? selectedRoot : differentialFlamegraph.differentialFlamegraph.root, [selectedRoot, differentialFlamegraph.differentialFlamegraph.root] ); return ( } flamegraph={ } flamegraphDrawer={ } /> ); } const DifferentialFlamegraphContainer = styled('div')` display: flex; flex-direction: column; flex: 1; ~ footer { display: none; } `; function DifferentialFlamegraphWithProviders() { return ( ); } export default DifferentialFlamegraphWithProviders;