123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335 |
- import type {CSSProperties} from 'react';
- import {Fragment, useCallback, useEffect, useMemo, useRef, useState} from 'react';
- import styled from '@emotion/styled';
- import {mat3, vec2} from 'gl-matrix';
- import {t} from 'sentry/locale';
- import type {RequestState} from 'sentry/types/core';
- import type {CanvasPoolManager} from 'sentry/utils/profiling/canvasScheduler';
- import {useCanvasScheduler} from 'sentry/utils/profiling/canvasScheduler';
- import type {CanvasView} from 'sentry/utils/profiling/canvasView';
- import {useFlamegraphTheme} from 'sentry/utils/profiling/flamegraph/useFlamegraphTheme';
- import type {FlamegraphCanvas} from 'sentry/utils/profiling/flamegraphCanvas';
- import type {FlamegraphChart as FlamegraphChartModel} from 'sentry/utils/profiling/flamegraphChart';
- import {
- getConfigViewTranslationBetweenVectors,
- getPhysicalSpacePositionFromOffset,
- transformMatrixBetweenRect,
- } from 'sentry/utils/profiling/gl/utils';
- import {FlamegraphChartRenderer} from 'sentry/utils/profiling/renderers/chartRenderer';
- import type {Rect} from 'sentry/utils/profiling/speedscope';
- import {formatTo, type ProfilingFormatterUnit} from 'sentry/utils/profiling/units/units';
- import {useCanvasScroll} from './interactions/useCanvasScroll';
- import {useCanvasZoomOrScroll} from './interactions/useCanvasZoomOrScroll';
- import {useInteractionViewCheckPoint} from './interactions/useInteractionViewCheckPoint';
- import {useWheelCenterZoom} from './interactions/useWheelCenterZoom';
- import {
- CollapsibleTimelineLoadingIndicator,
- CollapsibleTimelineMessage,
- } from './collapsibleTimeline';
- import {FlamegraphChartTooltip} from './flamegraphChartTooltip';
- interface FlamegraphChartProps {
- canvasBounds: Rect;
- canvasPoolManager: CanvasPoolManager;
- chart: FlamegraphChartModel | null;
- chartCanvas: FlamegraphCanvas | null;
- chartCanvasRef: HTMLCanvasElement | null;
- chartView: CanvasView<FlamegraphChartModel> | null;
- configViewUnit: ProfilingFormatterUnit;
- noMeasurementMessage: string | undefined;
- setChartCanvasRef: (ref: HTMLCanvasElement | null) => void;
- status: RequestState<any>['type'];
- }
- export function FlamegraphChart({
- status,
- chart,
- canvasPoolManager,
- chartView,
- chartCanvas,
- chartCanvasRef,
- setChartCanvasRef,
- canvasBounds,
- noMeasurementMessage,
- configViewUnit,
- }: FlamegraphChartProps) {
- const theme = useFlamegraphTheme();
- const scheduler = useCanvasScheduler(canvasPoolManager);
- const [configSpaceCursor, setConfigSpaceCursor] = useState<vec2 | null>(null);
- const [startInteractionVector, setStartInteractionVector] = useState<vec2 | null>(null);
- const [lastInteraction, setLastInteraction] = useState<
- 'pan' | 'click' | 'zoom' | 'scroll' | 'select' | 'resize' | null
- >(null);
- const configSpaceCursorRef = useRef<vec2 | null>(null);
- configSpaceCursorRef.current = configSpaceCursor;
- const chartRenderer = useMemo(() => {
- if (!chartCanvasRef || !chart) {
- return null;
- }
- return new FlamegraphChartRenderer(chartCanvasRef, chart, theme);
- }, [chartCanvasRef, chart, theme]);
- const drawchart = useCallback(() => {
- if (!chartCanvas || !chart || !chartView || !chartRenderer) {
- return;
- }
- const configViewToPhysicalSpaceTransform = transformMatrixBetweenRect(
- chartView.configView,
- chartCanvas.physicalSpace
- );
- const offsetPhysicalSpace = chartCanvas.physicalSpace
- // shrink the chart height by the padding to pad the top of chart
- .withHeight(chartCanvas.physicalSpace.height - theme.SIZES.CHART_PX_PADDING);
- const physicalSpaceToOffsetPhysicalSpaceTransform = transformMatrixBetweenRect(
- chartCanvas.physicalSpace,
- offsetPhysicalSpace
- );
- const configToPhysicalSpace = mat3.create();
- mat3.multiply(
- configToPhysicalSpace,
- physicalSpaceToOffsetPhysicalSpaceTransform,
- configViewToPhysicalSpaceTransform
- );
- mat3.multiply(
- configToPhysicalSpace,
- transformMatrixBetweenRect(chartView.configView, chartCanvas.physicalSpace),
- chartView.configSpaceTransform
- );
- mat3.multiply(
- configToPhysicalSpace,
- chartCanvas.physicalSpace.invertYTransform(),
- configToPhysicalSpace
- );
- chartRenderer.draw(
- chartView.toOriginConfigView(chartView.configView),
- configToPhysicalSpace, // this
- chartView.fromTransformedConfigView(chartCanvas.logicalSpace),
- configSpaceCursorRef
- );
- }, [chart, chartCanvas, chartRenderer, chartView, theme]);
- useEffect(() => {
- drawchart();
- }, [drawchart, configSpaceCursor]);
- useEffect(() => {
- scheduler.registerBeforeFrameCallback(drawchart);
- scheduler.draw();
- return () => {
- scheduler.unregisterBeforeFrameCallback(drawchart);
- };
- }, [drawchart, scheduler]);
- const onMouseDrag = useCallback(
- (evt: React.MouseEvent<HTMLCanvasElement>) => {
- if (!chartCanvas || !chartView || !startInteractionVector) {
- return;
- }
- const configDelta = getConfigViewTranslationBetweenVectors(
- evt.nativeEvent.offsetX,
- evt.nativeEvent.offsetY,
- startInteractionVector,
- chartView,
- chartCanvas
- );
- if (!configDelta) {
- return;
- }
- canvasPoolManager.dispatch('transform config view', [configDelta, chartView]);
- setStartInteractionVector(
- getPhysicalSpacePositionFromOffset(
- evt.nativeEvent.offsetX,
- evt.nativeEvent.offsetY
- )
- );
- },
- [chartCanvas, chartView, startInteractionVector, canvasPoolManager]
- );
- const onCanvasMouseMove = useCallback(
- (evt: React.MouseEvent<HTMLCanvasElement>) => {
- if (!chartCanvas || !chartView) {
- return;
- }
- const configSpaceMouse = chartView.getTransformedConfigViewCursor(
- vec2.fromValues(evt.nativeEvent.offsetX, evt.nativeEvent.offsetY),
- chartCanvas
- );
- setConfigSpaceCursor(configSpaceMouse);
- if (startInteractionVector) {
- onMouseDrag(evt);
- setLastInteraction('pan');
- } else {
- setLastInteraction(null);
- }
- },
- [chartCanvas, chartView, onMouseDrag, startInteractionVector]
- );
- const onMapCanvasMouseUp = useCallback(() => {
- setConfigSpaceCursor(null);
- setLastInteraction(null);
- }, []);
- useEffect(() => {
- window.addEventListener('mouseup', onMapCanvasMouseUp);
- return () => {
- window.removeEventListener('mouseup', onMapCanvasMouseUp);
- };
- }, [onMapCanvasMouseUp]);
- const onWheelCenterZoom = useWheelCenterZoom(chartCanvas, chartView, canvasPoolManager);
- const onCanvasScroll = useCanvasScroll(chartCanvas, chartView, canvasPoolManager);
- useCanvasZoomOrScroll({
- setConfigSpaceCursor,
- setLastInteraction,
- handleWheel: onWheelCenterZoom,
- handleScroll: onCanvasScroll,
- canvas: chartCanvasRef,
- });
- useInteractionViewCheckPoint({
- view: chartView,
- lastInteraction,
- });
- // When a user click anywhere outside the spans, clear cursor and selected node
- useEffect(() => {
- const onClickOutside = (evt: MouseEvent) => {
- if (!chartCanvasRef || chartCanvasRef.contains(evt.target as Node)) {
- return;
- }
- setConfigSpaceCursor(null);
- };
- document.addEventListener('click', onClickOutside);
- return () => {
- document.removeEventListener('click', onClickOutside);
- };
- });
- const onCanvasMouseLeave = useCallback(() => {
- setConfigSpaceCursor(null);
- setStartInteractionVector(null);
- setLastInteraction(null);
- }, []);
- const onCanvasMouseDown = useCallback((evt: React.MouseEvent<HTMLCanvasElement>) => {
- setLastInteraction('click');
- setStartInteractionVector(
- getPhysicalSpacePositionFromOffset(evt.nativeEvent.offsetX, evt.nativeEvent.offsetY)
- );
- }, []);
- const onCanvasMouseUp = useCallback(
- (evt: React.MouseEvent<HTMLCanvasElement>) => {
- evt.preventDefault();
- evt.stopPropagation();
- if (!chartView) {
- return;
- }
- if (!configSpaceCursor) {
- setLastInteraction(null);
- setStartInteractionVector(null);
- return;
- }
- setLastInteraction(null);
- setStartInteractionVector(null);
- },
- [configSpaceCursor, chartView]
- );
- const isInsufficientDuration = useMemo(() => {
- if (!chart) {
- return false;
- }
- return formatTo(chart?.configSpace.width, configViewUnit, 'millisecond') < 200;
- }, [chart, configViewUnit]);
- let message: string | undefined;
- if (chart) {
- if (
- isInsufficientDuration &&
- (chart.status === 'insufficient data' || chart.status === 'empty metrics')
- ) {
- message = t('Profile duration was too short to collect enough metrics');
- } else if (!isInsufficientDuration && chart.status === 'insufficient data') {
- message =
- noMeasurementMessage ||
- t(
- 'Profile failed to collect a sufficient amount of measurements to render a chart'
- );
- } else if (chart.status === 'no metrics') {
- message = noMeasurementMessage || t('Profile has no measurements');
- } else if (chart.status === 'empty metrics') {
- message = t('Profile has empty measurements');
- }
- }
- return (
- <Fragment>
- <Canvas
- ref={setChartCanvasRef}
- onMouseMove={onCanvasMouseMove}
- onMouseLeave={onCanvasMouseLeave}
- onMouseUp={onCanvasMouseUp}
- onMouseDown={onCanvasMouseDown}
- cursor={lastInteraction === 'pan' ? 'grabbing' : 'default'}
- />
- {configSpaceCursor && chartRenderer && chartCanvas && chartView && chart ? (
- <FlamegraphChartTooltip
- chart={chart}
- configSpaceCursor={configSpaceCursor}
- chartCanvas={chartCanvas}
- chartView={chartView}
- chartRenderer={chartRenderer}
- canvasBounds={canvasBounds}
- configViewUnit={configViewUnit}
- />
- ) : null}
- {/* transaction loads after profile, so we want to show loading even if it's in initial state */}
- {status === 'loading' || status === 'initial' ? (
- <CollapsibleTimelineLoadingIndicator />
- ) : status === 'resolved' && chart?.status !== 'ok' ? (
- <CollapsibleTimelineMessage>{message}</CollapsibleTimelineMessage>
- ) : null}
- </Fragment>
- );
- }
- const Canvas = styled('canvas')<{cursor?: CSSProperties['cursor']}>`
- width: 100%;
- height: 100%;
- position: absolute;
- left: 0;
- top: 0;
- user-select: none;
- cursor: ${p => p.cursor};
- `;
|