123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374 |
- import {
- Fragment,
- ReactElement,
- useCallback,
- useEffect,
- useLayoutEffect,
- useMemo,
- useState,
- } from 'react';
- import {mat3, vec2} from 'gl-matrix';
- import {FlamegraphOptionsMenu} from 'sentry/components/profiling/flamegraphOptionsMenu';
- import {FlamegraphSearch} from 'sentry/components/profiling/flamegraphSearch';
- import {FlamegraphToolbar} from 'sentry/components/profiling/flamegraphToolbar';
- import {FlamegraphViewSelectMenu} from 'sentry/components/profiling/flamegraphViewSelectMenu';
- import {FlamegraphZoomView} from 'sentry/components/profiling/flamegraphZoomView';
- import {FlamegraphZoomViewMinimap} from 'sentry/components/profiling/flamegraphZoomViewMinimap';
- import {FrameStack} from 'sentry/components/profiling/FrameStack/frameStack';
- import {
- ProfileDragDropImport,
- ProfileDragDropImportProps,
- } from 'sentry/components/profiling/profileDragDropImport';
- import {ThreadMenuSelector} from 'sentry/components/profiling/threadSelector';
- import {CanvasPoolManager, CanvasScheduler} from 'sentry/utils/profiling/canvasScheduler';
- import {Flamegraph as FlamegraphModel} from 'sentry/utils/profiling/flamegraph';
- import {useFlamegraphPreferences} from 'sentry/utils/profiling/flamegraph/hooks/useFlamegraphPreferences';
- import {useFlamegraphProfiles} from 'sentry/utils/profiling/flamegraph/hooks/useFlamegraphProfiles';
- import {useDispatchFlamegraphState} from 'sentry/utils/profiling/flamegraph/hooks/useFlamegraphState';
- import {useFlamegraphTheme} from 'sentry/utils/profiling/flamegraph/useFlamegraphTheme';
- import {FlamegraphCanvas} from 'sentry/utils/profiling/flamegraphCanvas';
- import {FlamegraphFrame} from 'sentry/utils/profiling/flamegraphFrame';
- import {FlamegraphView} from 'sentry/utils/profiling/flamegraphView';
- import {
- computeConfigViewWithStategy,
- formatColorForFrame,
- Rect,
- watchForResize,
- } from 'sentry/utils/profiling/gl/utils';
- import {ProfileGroup} from 'sentry/utils/profiling/profile/importProfile';
- import {FlamegraphRenderer} from 'sentry/utils/profiling/renderers/flamegraphRenderer';
- import {formatTo, ProfilingFormatterUnit} from 'sentry/utils/profiling/units/units';
- import {useDevicePixelRatio} from 'sentry/utils/useDevicePixelRatio';
- import {useMemoWithPrevious} from 'sentry/utils/useMemoWithPrevious';
- import {FlamegraphWarnings} from './FlamegraphWarnings';
- import {ProfilingFlamechartLayout} from './profilingFlamechartLayout';
- function getTransactionConfigSpace(
- profileGroup: ProfileGroup,
- startedAt: number,
- unit: ProfilingFormatterUnit | string
- ): Rect {
- const duration = profileGroup.metadata.durationNS;
- // If durationNs is present, use it
- if (typeof duration === 'number') {
- return new Rect(startedAt, 0, formatTo(duration, 'nanoseconds', unit), 0);
- }
- // else fallback to Math.max of profile durations
- const maxProfileDuration = Math.max(...profileGroup.profiles.map(p => p.duration));
- return new Rect(startedAt, 0, maxProfileDuration, 0);
- }
- const noopFormatDuration = () => '';
- interface FlamegraphProps {
- onImport: ProfileDragDropImportProps['onImport'];
- profiles: ProfileGroup;
- }
- function Flamegraph(props: FlamegraphProps): ReactElement {
- const [canvasBounds, setCanvasBounds] = useState<Rect>(Rect.Empty());
- const devicePixelRatio = useDevicePixelRatio();
- const dispatch = useDispatchFlamegraphState();
- const flamegraphTheme = useFlamegraphTheme();
- const {sorting, view, xAxis} = useFlamegraphPreferences();
- const {threadId, selectedRoot} = useFlamegraphProfiles();
- 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 canvasPoolManager = useMemo(() => new CanvasPoolManager(), []);
- const scheduler = useMemo(() => new CanvasScheduler(), []);
- const flamegraph = useMemo(() => {
- if (typeof threadId !== 'number') {
- return FlamegraphModel.Empty();
- }
- // This could happen if threadId was initialized from query string, but for some
- // reason the profile was removed from the list of profiles.
- const profile = props.profiles.profiles.find(p => p.threadId === threadId);
- if (!profile) {
- return FlamegraphModel.Empty();
- }
- return new FlamegraphModel(profile, threadId, {
- inverted: view === 'bottom up',
- leftHeavy: sorting === 'left heavy',
- configSpace:
- xAxis === 'transaction'
- ? getTransactionConfigSpace(props.profiles, profile.startedAt, profile.unit)
- : undefined,
- });
- }, [props.profiles, sorting, threadId, view, xAxis]);
- const flamegraphCanvas = useMemo(() => {
- if (!flamegraphCanvasRef) {
- return null;
- }
- return new FlamegraphCanvas(
- flamegraphCanvasRef,
- vec2.fromValues(0, flamegraphTheme.SIZES.TIMELINE_HEIGHT * devicePixelRatio)
- );
- }, [devicePixelRatio, flamegraphCanvasRef, flamegraphTheme]);
- const flamegraphMiniMapCanvas = useMemo(() => {
- if (!flamegraphMiniMapCanvasRef) {
- return null;
- }
- return new FlamegraphCanvas(flamegraphMiniMapCanvasRef, vec2.fromValues(0, 0));
- }, [flamegraphMiniMapCanvasRef]);
- const flamegraphView = useMemoWithPrevious<FlamegraphView | null>(
- previousView => {
- if (!flamegraphCanvas) {
- return null;
- }
- const newView = new FlamegraphView({
- canvas: flamegraphCanvas,
- flamegraph,
- theme: flamegraphTheme,
- });
- // if the profile or the config space of the flamegraph has changed, we do not
- // want to persist the config view. This is to avoid a case where the new config space
- // is larger than the previous one, meaning the new view could now be zoomed in even
- // though the user did not fire any zoom events.
- if (
- previousView?.flamegraph.profile === newView.flamegraph.profile &&
- previousView.configSpace.equals(newView.configSpace)
- ) {
- // if we're still looking at the same profile but only a preference other than
- // left heavy has changed, we do want to persist the config view
- if (previousView.flamegraph.leftHeavy === newView.flamegraph.leftHeavy) {
- newView.setConfigView(
- previousView.configView.withHeight(newView.configView.height)
- );
- }
- }
- return newView;
- },
- [flamegraph, flamegraphCanvas, flamegraphTheme]
- );
- useEffect(() => {
- if (!flamegraphCanvas || !flamegraphView) {
- return undefined;
- }
- const onConfigViewChange = (rect: Rect) => {
- flamegraphView.setConfigView(rect);
- canvasPoolManager.draw();
- };
- const onTransformConfigView = (mat: mat3) => {
- flamegraphView.transformConfigView(mat);
- canvasPoolManager.draw();
- };
- const onResetZoom = () => {
- flamegraphView.resetConfigView(flamegraphCanvas);
- canvasPoolManager.draw();
- };
- const onZoomIntoFrame = (frame: FlamegraphFrame, strategy: 'min' | 'exact') => {
- const newConfigView = computeConfigViewWithStategy(
- strategy,
- flamegraphView.configView,
- new Rect(frame.start, frame.depth, frame.end - frame.start, 1)
- );
- 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]);
- useEffect(() => {
- canvasPoolManager.registerScheduler(scheduler);
- return () => canvasPoolManager.unregisterScheduler(scheduler);
- }, [canvasPoolManager, scheduler]);
- useLayoutEffect(() => {
- if (
- !flamegraphView ||
- !flamegraphCanvas ||
- !flamegraphMiniMapCanvas ||
- !flamegraphCanvasRef ||
- !flamegraphOverlayCanvasRef ||
- !flamegraphMiniMapCanvasRef ||
- !flamegraphMiniMapOverlayCanvasRef
- ) {
- return undefined;
- }
- const flamegraphObserver = watchForResize(
- [flamegraphCanvasRef, flamegraphOverlayCanvasRef],
- () => {
- const bounds = flamegraphCanvasRef.getBoundingClientRect();
- setCanvasBounds(new Rect(bounds.x, bounds.y, bounds.width, bounds.height));
- flamegraphCanvas.initPhysicalSpace();
- flamegraphView.resizeConfigSpace(flamegraphCanvas);
- canvasPoolManager.drawSync();
- }
- );
- const flamegraphMiniMapObserver = watchForResize(
- [flamegraphMiniMapCanvasRef, flamegraphMiniMapOverlayCanvasRef],
- () => {
- flamegraphMiniMapCanvas.initPhysicalSpace();
- canvasPoolManager.drawSync();
- }
- );
- return () => {
- flamegraphObserver.disconnect();
- flamegraphMiniMapObserver.disconnect();
- };
- }, [
- canvasPoolManager,
- flamegraphCanvas,
- flamegraphCanvasRef,
- flamegraphMiniMapCanvas,
- flamegraphMiniMapCanvasRef,
- flamegraphMiniMapOverlayCanvasRef,
- flamegraphOverlayCanvasRef,
- flamegraphView,
- ]);
- const flamegraphRenderer = useMemo(() => {
- if (!flamegraphCanvasRef) {
- return null;
- }
- return new FlamegraphRenderer(flamegraphCanvasRef, flamegraph, flamegraphTheme, {
- draw_border: true,
- });
- }, [flamegraph, flamegraphCanvasRef, flamegraphTheme]);
- const getFrameColor = useCallback(
- (frame: FlamegraphFrame) => {
- if (!flamegraphRenderer) {
- return '';
- }
- return formatColorForFrame(frame, flamegraphRenderer);
- },
- [flamegraphRenderer]
- );
- // referenceNode is passed down to the frameStack and is used to determine
- // the weights of each frame. In other words, in case there is no user selected root, then all
- // of the frame weights and timing are relative to the entire profile. If there is a user selected
- // root however, all weights are relative to that sub tree.
- const referenceNode = useMemo(
- () => (selectedRoot ? selectedRoot : flamegraph.root),
- [selectedRoot, flamegraph.root]
- );
- // In case a user selected root is present, we will display that root + it's entire sub tree.
- // If no root is selected, we will display the entire sub tree down from the root. We start at
- // root.children because flamegraph.root is a virtual node that we do not want to show in the table.
- const rootNodes = useMemo(() => {
- return selectedRoot ? [selectedRoot] : flamegraph.root.children;
- }, [selectedRoot, flamegraph.root]);
- return (
- <Fragment>
- <FlamegraphToolbar>
- <ThreadMenuSelector
- profileGroup={props.profiles}
- threadId={threadId}
- onThreadIdChange={newThreadId =>
- dispatch({type: 'set thread id', payload: newThreadId})
- }
- />
- <FlamegraphViewSelectMenu
- view={view}
- sorting={sorting}
- onSortingChange={s => {
- dispatch({type: 'set sorting', payload: s});
- }}
- onViewChange={v => {
- dispatch({type: 'set view', payload: v});
- }}
- />
- <FlamegraphSearch
- flamegraphs={[flamegraph]}
- canvasPoolManager={canvasPoolManager}
- />
- <FlamegraphOptionsMenu canvasPoolManager={canvasPoolManager} />
- </FlamegraphToolbar>
- <ProfilingFlamechartLayout
- minimap={
- <FlamegraphZoomViewMinimap
- canvasPoolManager={canvasPoolManager}
- flamegraph={flamegraph}
- flamegraphMiniMapCanvas={flamegraphMiniMapCanvas}
- flamegraphMiniMapCanvasRef={flamegraphMiniMapCanvasRef}
- flamegraphMiniMapOverlayCanvasRef={flamegraphMiniMapOverlayCanvasRef}
- flamegraphMiniMapView={flamegraphView}
- setFlamegraphMiniMapCanvasRef={setFlamegraphMiniMapCanvasRef}
- setFlamegraphMiniMapOverlayCanvasRef={setFlamegraphMiniMapOverlayCanvasRef}
- />
- }
- flamechart={
- <ProfileDragDropImport onImport={props.onImport}>
- <FlamegraphWarnings flamegraph={flamegraph} />
- <FlamegraphZoomView
- flamegraphRenderer={flamegraphRenderer}
- canvasBounds={canvasBounds}
- canvasPoolManager={canvasPoolManager}
- flamegraph={flamegraph}
- flamegraphCanvas={flamegraphCanvas}
- flamegraphCanvasRef={flamegraphCanvasRef}
- flamegraphOverlayCanvasRef={flamegraphOverlayCanvasRef}
- flamegraphView={flamegraphView}
- setFlamegraphCanvasRef={setFlamegraphCanvasRef}
- setFlamegraphOverlayCanvasRef={setFlamegraphOverlayCanvasRef}
- />
- </ProfileDragDropImport>
- }
- frameStack={
- <FrameStack
- profileGroup={props.profiles}
- flamegraph={flamegraph}
- referenceNode={referenceNode}
- rootNodes={rootNodes}
- getFrameColor={getFrameColor}
- formatDuration={flamegraph ? flamegraph.formatter : noopFormatDuration}
- canvasPoolManager={canvasPoolManager}
- />
- }
- />
- </Fragment>
- );
- }
- export {Flamegraph};
|