Files
sentinel/static/app/views/profiling/differentialFlamegraph.tsx
2024-02-15 17:01:50 +00:00

390 lines
14 KiB
TypeScript

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 {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<boolean>(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<HTMLCanvasElement | null>(null);
const [flamegraphOverlayCanvasRef, setFlamegraphOverlayCanvasRef] =
useState<HTMLCanvasElement | null>(null);
const [flamegraphMiniMapCanvasRef, setFlamegraphMiniMapCanvasRef] =
useState<HTMLCanvasElement | null>(null);
const [flamegraphMiniMapOverlayCanvasRef, setFlamegraphMiniMapOverlayCanvasRef] =
useState<HTMLCanvasElement | null>(null);
const flamegraphCanvas = useMemo(() => {
if (!flamegraphCanvasRef) {
return null;
}
return new FlamegraphCanvas(flamegraphCanvasRef, vec2.fromValues(0, 0));
}, [flamegraphCanvasRef]);
const flamegraphView = useMemo<CanvasView<DifferentialFlamegraphModel> | 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<any>) => {
if (sourceConfigViewChange === flamegraphView) {
flamegraphView.setConfigView(rect.withHeight(flamegraphView.configView.height));
}
canvasPoolManager.draw();
};
const onTransformConfigView = (
mat: mat3,
sourceTransformConfigView: CanvasView<any>
) => {
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 (
<Feature features={['profiling-differential-flamegraph-page']}>
<DifferentialFlamegraphContainer>
<DifferentialFlamegraphToolbar
frameFilter={frameFilterSetting}
onFrameFilterChange={setFrameFilterSetting}
negated={negated}
onNegatedChange={setNegated}
flamegraph={differentialFlamegraph.differentialFlamegraph}
canvasPoolManager={canvasPoolManager}
/>
<DifferentialFlamegraphLayout
minimap={
<FlamegraphZoomViewMinimap
canvasPoolManager={canvasPoolManager}
flamegraph={differentialFlamegraph.differentialFlamegraph}
flamegraphMiniMapCanvas={flamegraphMiniMapCanvas}
flamegraphMiniMapCanvasRef={flamegraphMiniMapCanvasRef}
flamegraphMiniMapOverlayCanvasRef={flamegraphMiniMapOverlayCanvasRef}
flamegraphMiniMapView={flamegraphView}
setFlamegraphMiniMapCanvasRef={setFlamegraphMiniMapCanvasRef}
setFlamegraphMiniMapOverlayCanvasRef={setFlamegraphMiniMapOverlayCanvasRef}
/>
}
flamegraph={
<FlamegraphZoomView
profileGroup={
differentialFlamegraph.afterProfileGroup ?? LOADING_PROFILE_GROUP
}
disableGrid
disableCallOrderSort
disableColorCoding
canvasBounds={flamegraphCanvasBounds}
canvasPoolManager={canvasPoolManager}
flamegraph={differentialFlamegraph.differentialFlamegraph}
flamegraphRenderer={flamegraphRenderer}
flamegraphCanvas={flamegraphCanvas}
flamegraphCanvasRef={flamegraphCanvasRef}
flamegraphOverlayCanvasRef={flamegraphOverlayCanvasRef}
flamegraphView={flamegraphView}
setFlamegraphCanvasRef={setFlamegraphCanvasRef}
setFlamegraphOverlayCanvasRef={setFlamegraphOverlayCanvasRef}
/>
}
flamegraphDrawer={
<DifferentialFlamegraphDrawer
profileGroup={
differentialFlamegraph.afterProfileGroup ?? LOADING_PROFILE_GROUP
}
getFrameColor={getFrameColor}
referenceNode={referenceNode}
rootNodes={rootNodes}
flamegraph={differentialFlamegraph.differentialFlamegraph}
formatDuration={
differentialFlamegraph.differentialFlamegraph
? differentialFlamegraph.differentialFlamegraph.formatter
: noopFormatDuration
}
canvasPoolManager={canvasPoolManager}
canvasScheduler={scheduler}
/>
}
/>
</DifferentialFlamegraphContainer>
</Feature>
);
}
const DifferentialFlamegraphContainer = styled('div')`
display: flex;
flex-direction: column;
flex: 1;
~ footer {
display: none;
}
`;
function DifferentialFlamegraphWithProviders() {
return (
<FlamegraphThemeProvider>
<FlamegraphStateProvider
initialState={{
preferences: {
sorting: 'alphabetical',
view: 'top down',
},
}}
>
<DifferentialFlamegraphView />
</FlamegraphStateProvider>
</FlamegraphThemeProvider>
);
}
export default DifferentialFlamegraphWithProviders;