Skip to content

Commit

Permalink
ref(profiling): Lift and unify config views (#34520)
Browse files Browse the repository at this point in the history
Previously, we had 2 separate config space/views that had to be kept in sync so
that the flamegraph and the minimap were rendered correctly. This change lifts
the state into the parent component to reduce complexity.
  • Loading branch information
Zylphrex authored May 12, 2022
1 parent 74cf4de commit ad8fec6
Show file tree
Hide file tree
Showing 12 changed files with 1,015 additions and 902 deletions.
31 changes: 11 additions & 20 deletions static/app/components/profiling/boundTooltip.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import {useLayoutEffect, useMemo, useRef, useState} from 'react';
import styled from '@emotion/styled';
import {mat3, vec2} from 'gl-matrix';
import {vec2} from 'gl-matrix';

import {useFlamegraphTheme} from 'sentry/utils/profiling/flamegraph/useFlamegraphTheme';
import {FlamegraphCanvas} from 'sentry/utils/profiling/flamegraphCanvas';
import {FlamegraphView} from 'sentry/utils/profiling/flamegraphView';
import {getContext, measureText, Rect} from 'sentry/utils/profiling/gl/utils';
import {useDevicePixelRatio} from 'sentry/utils/useDevicePixelRatio';

const useCachedMeasure = (string: string, font: string): Rect => {
const cache = useRef<Record<string, Rect>>({});
Expand Down Expand Up @@ -32,15 +33,17 @@ const useCachedMeasure = (string: string, font: string): Rect => {

interface BoundTooltipProps {
bounds: Rect;
configViewToPhysicalSpace: mat3;
cursor: vec2 | null;
flamegraphCanvas: FlamegraphCanvas;
flamegraphView: FlamegraphView;
children?: React.ReactNode;
}

function BoundTooltip({
bounds,
configViewToPhysicalSpace,
flamegraphCanvas,
cursor,
flamegraphView,
children,
}: BoundTooltipProps): React.ReactElement | null {
const tooltipRef = useRef<HTMLDivElement>(null);
Expand All @@ -49,17 +52,6 @@ function BoundTooltip({
tooltipRef.current?.textContent ?? '',
`${flamegraphTheme.SIZES.TOOLTIP_FONT_SIZE}px ${flamegraphTheme.FONTS.FONT}`
);
const devicePixelRatio = useDevicePixelRatio();

const physicalToLogicalSpace = useMemo(
() =>
mat3.fromScaling(
mat3.create(),
vec2.fromValues(1 / devicePixelRatio, 1 / devicePixelRatio)
),
[devicePixelRatio]
);

const [tooltipBounds, setTooltipBounds] = useState<Rect>(Rect.Empty());

useLayoutEffect(() => {
Expand Down Expand Up @@ -87,14 +79,13 @@ function BoundTooltip({
const physicalSpaceCursor = vec2.transformMat3(
vec2.create(),
cursor,

configViewToPhysicalSpace
flamegraphView.fromConfigView(flamegraphCanvas.physicalSpace)
);

const logicalSpaceCursor = vec2.transformMat3(
vec2.create(),
physicalSpaceCursor,
physicalToLogicalSpace
flamegraphCanvas.physicalToLogicalSpace
);

let cursorHorizontalPosition = logicalSpaceCursor[0];
Expand All @@ -110,7 +101,7 @@ function BoundTooltip({
cursorHorizontalPosition -= tooltipBounds.width;
}

return children ? (
return (
<Tooltip
ref={tooltipRef}
style={{
Expand All @@ -123,7 +114,7 @@ function BoundTooltip({
>
{children}
</Tooltip>
) : null;
);
}

const Tooltip = styled('div')`
Expand Down
195 changes: 189 additions & 6 deletions static/app/components/profiling/flamegraph.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {Fragment, ReactElement, useMemo} from 'react';
import {Fragment, ReactElement, useEffect, useMemo, useState} from 'react';
import styled from '@emotion/styled';
import {mat3, vec2} from 'gl-matrix';

import {FlamegraphOptionsMenu} from 'sentry/components/profiling/flamegraphOptionsMenu';
import {FlamegraphSearch} from 'sentry/components/profiling/flamegraphSearch';
Expand All @@ -12,15 +13,20 @@ import {
ProfileDragDropImportProps,
} from 'sentry/components/profiling/profileDragDropImport';
import {ThreadMenuSelector} from 'sentry/components/profiling/threadSelector';
import {CanvasPoolManager} from 'sentry/utils/profiling/canvasScheduler';
import {CanvasPoolManager, CanvasScheduler} from 'sentry/utils/profiling/canvasScheduler';
import {Flamegraph as FlamegraphModel} from 'sentry/utils/profiling/flamegraph';
import {FlamegraphTheme} from 'sentry/utils/profiling/flamegraph/flamegraphTheme';
import {useFlamegraphPreferences} from 'sentry/utils/profiling/flamegraph/useFlamegraphPreferences';
import {useFlamegraphProfiles} from 'sentry/utils/profiling/flamegraph/useFlamegraphProfiles';
import {useFlamegraphTheme} from 'sentry/utils/profiling/flamegraph/useFlamegraphTheme';
import {Rect} from 'sentry/utils/profiling/gl/utils';
import {FlamegraphCanvas} from 'sentry/utils/profiling/flamegraphCanvas';
import {FlamegraphFrame} from 'sentry/utils/profiling/flamegraphFrame';
import {FlamegraphView} from 'sentry/utils/profiling/flamegraphView';
import {Rect, watchForResize} from 'sentry/utils/profiling/gl/utils';
import {ProfileGroup} from 'sentry/utils/profiling/profile/importProfile';
import {Profile} from 'sentry/utils/profiling/profile/profile';
import {useDevicePixelRatio} from 'sentry/utils/useDevicePixelRatio';
import {useMemoWithPrevious} from 'sentry/utils/useMemoWithPrevious';

function getTransactionConfigSpace(profiles: Profile[]): Rect {
const startedAt = Math.min(...profiles.map(p => p.startedAt));
Expand All @@ -33,12 +39,28 @@ interface FlamegraphProps {
}

function Flamegraph(props: FlamegraphProps): ReactElement {
const devicePixelRatio = useDevicePixelRatio();

const flamegraphTheme = useFlamegraphTheme();
const [{sorting, view, xAxis}, dispatch] = useFlamegraphPreferences();
const [{threadId}, dispatchThreadId] = useFlamegraphProfiles();

const [canvasBounds, setCanvasBounds] = useState<Rect>(Rect.Empty());

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();
Expand All @@ -59,7 +81,155 @@ function Flamegraph(props: FlamegraphProps): ReactElement {
? getTransactionConfigSpace(props.profiles.profiles)
: undefined,
});
}, [props.profiles, threadId, sorting, xAxis, view]);
}, [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 we're rendering as a flamegraph has changed, we do not
// want to persist the config view
if (previousView?.flamegraph.profile === newView.flamegraph.profile) {
// 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) => {
flamegraphView.setConfigView(
new Rect(
frame.start,
frame.depth,
frame.end - frame.start,
flamegraphView.configView.height
)
);

canvasPoolManager.draw();
};

scheduler.on('setConfigView', onConfigViewChange);
scheduler.on('transformConfigView', onTransformConfigView);
scheduler.on('resetZoom', onResetZoom);
scheduler.on('zoomIntoFrame', onZoomIntoFrame);

return () => {
scheduler.off('setConfigView', onConfigViewChange);
scheduler.off('transformConfigView', onTransformConfigView);
scheduler.off('resetZoom', onResetZoom);
scheduler.off('zoomIntoFrame', onZoomIntoFrame);
};
}, [canvasPoolManager, flamegraphCanvas, flamegraphView, scheduler]);

useEffect(() => {
canvasPoolManager.registerScheduler(scheduler);
return () => canvasPoolManager.unregisterScheduler(scheduler);
}, [canvasPoolManager, scheduler]);

useEffect(() => {
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,
setCanvasBounds,
]);

return (
<Fragment>
Expand Down Expand Up @@ -90,15 +260,28 @@ function Flamegraph(props: FlamegraphProps): ReactElement {

<FlamegraphZoomViewMinimapContainer height={flamegraphTheme.SIZES.MINIMAP_HEIGHT}>
<FlamegraphZoomViewMinimap
flamegraph={flamegraph}
canvasPoolManager={canvasPoolManager}
flamegraph={flamegraph}
flamegraphMiniMapCanvas={flamegraphMiniMapCanvas}
flamegraphMiniMapCanvasRef={flamegraphMiniMapCanvasRef}
flamegraphMiniMapOverlayCanvasRef={flamegraphMiniMapOverlayCanvasRef}
flamegraphMiniMapView={flamegraphView}
setFlamegraphMiniMapCanvasRef={setFlamegraphMiniMapCanvasRef}
setFlamegraphMiniMapOverlayCanvasRef={setFlamegraphMiniMapOverlayCanvasRef}
/>
</FlamegraphZoomViewMinimapContainer>
<FlamegraphZoomViewContainer>
<ProfileDragDropImport onImport={props.onImport}>
<FlamegraphZoomView
flamegraph={flamegraph}
canvasBounds={canvasBounds}
canvasPoolManager={canvasPoolManager}
flamegraph={flamegraph}
flamegraphCanvas={flamegraphCanvas}
flamegraphCanvasRef={flamegraphCanvasRef}
flamegraphOverlayCanvasRef={flamegraphOverlayCanvasRef}
flamegraphView={flamegraphView}
setFlamegraphCanvasRef={setFlamegraphCanvasRef}
setFlamegraphOverlayCanvasRef={setFlamegraphOverlayCanvasRef}
/>
</ProfileDragDropImport>
</FlamegraphZoomViewContainer>
Expand Down
Loading

0 comments on commit ad8fec6

Please sign in to comment.