mirror of
https://github.com/hanzoai/sentinel.git
synced 2026-09-29 22:34:33 +00:00
274 lines
9.2 KiB
TypeScript
274 lines
9.2 KiB
TypeScript
import {Fragment, useMemo} from 'react';
|
|
import styled from '@emotion/styled';
|
|
import type {vec2} from 'gl-matrix';
|
|
|
|
import {BoundTooltip} from 'sentry/components/profiling/boundTooltip';
|
|
import {IconLightning} from 'sentry/icons';
|
|
import {t} from 'sentry/locale';
|
|
import {space} from 'sentry/styles/space';
|
|
import {defined} from 'sentry/utils';
|
|
import {formatPercentage} from 'sentry/utils/formatters';
|
|
import type {CallTreeNode} from 'sentry/utils/profiling/callTreeNode';
|
|
import type {CanvasView} from 'sentry/utils/profiling/canvasView';
|
|
import type {DifferentialFlamegraph} from 'sentry/utils/profiling/differentialFlamegraph';
|
|
import type {Flamegraph} from 'sentry/utils/profiling/flamegraph';
|
|
import type {FlamegraphCanvas} from 'sentry/utils/profiling/flamegraphCanvas';
|
|
import type {FlamegraphFrame} from 'sentry/utils/profiling/flamegraphFrame';
|
|
import {formatColorForFrame} from 'sentry/utils/profiling/gl/utils';
|
|
import type {FlamegraphRenderer} from 'sentry/utils/profiling/renderers/flamegraphRenderer';
|
|
import {Rect} from 'sentry/utils/profiling/speedscope';
|
|
import {relativeChange} from 'sentry/utils/profiling/units/units';
|
|
|
|
export function formatWeightToProfileDuration(
|
|
frame: CallTreeNode,
|
|
flamegraph: Flamegraph
|
|
) {
|
|
return `${Math.round((frame.totalWeight / flamegraph.profile.duration) * 100)}%`;
|
|
}
|
|
|
|
function formatFileNameAndLineColumn(frame: FlamegraphFrame): string | null {
|
|
if (!frame.frame.file) {
|
|
return '<unknown file>';
|
|
}
|
|
if (typeof frame.frame.line === 'number' && typeof frame.frame.column === 'number') {
|
|
return `${frame.frame.file}:${frame.frame.line}:${frame.frame.column}`;
|
|
}
|
|
if (typeof frame.frame.line === 'number') {
|
|
return `${frame.frame.file}:${frame.frame.line}`;
|
|
}
|
|
return `${frame.frame.file}:<unknown line>`;
|
|
}
|
|
|
|
export interface FlamegraphTooltipProps {
|
|
canvasBounds: Rect;
|
|
configSpaceCursor: vec2;
|
|
flamegraph: Flamegraph | DifferentialFlamegraph;
|
|
flamegraphCanvas: FlamegraphCanvas;
|
|
flamegraphRenderer: FlamegraphRenderer;
|
|
flamegraphView: CanvasView<Flamegraph>;
|
|
frame: FlamegraphFrame;
|
|
platform: 'javascript' | 'python' | 'ios' | 'android' | string | undefined;
|
|
disableColorCoding?: boolean;
|
|
}
|
|
|
|
function isDifferentiableFlamegraph(
|
|
flamegraph: Flamegraph | DifferentialFlamegraph
|
|
): flamegraph is DifferentialFlamegraph {
|
|
return 'isDifferentialFlamegraph' in flamegraph && flamegraph.isDifferentialFlamegraph;
|
|
}
|
|
|
|
export function FlamegraphTooltip(props: FlamegraphTooltipProps) {
|
|
const frameInConfigSpace = useMemo<Rect>(() => {
|
|
return new Rect(
|
|
props.frame.start,
|
|
props.frame.end,
|
|
props.frame.end - props.frame.start,
|
|
1
|
|
).transformRect(props.flamegraphView.configSpaceTransform);
|
|
}, [props.flamegraphView, props.frame]);
|
|
|
|
if (
|
|
props.flamegraphRenderer.flamegraph.profile.unit === 'count' &&
|
|
!isDifferentiableFlamegraph(props.flamegraph)
|
|
) {
|
|
return (
|
|
<AggregateFlamegraphTooltip {...props} frameInConfigSpace={frameInConfigSpace} />
|
|
);
|
|
}
|
|
|
|
if (isDifferentiableFlamegraph(props.flamegraph)) {
|
|
return (
|
|
<DifferentialFlamegraphTooltip
|
|
{...props}
|
|
flamegraph={props.flamegraph}
|
|
frameInConfigSpace={frameInConfigSpace}
|
|
/>
|
|
);
|
|
}
|
|
|
|
return <FlamechartTooltip {...props} frameInConfigSpace={frameInConfigSpace} />;
|
|
}
|
|
|
|
interface DifferentialFlamegraphTooltipProps extends FlamegraphTooltipProps {
|
|
flamegraph: DifferentialFlamegraph;
|
|
frameInConfigSpace: Rect;
|
|
}
|
|
function DifferentialFlamegraphTooltip(props: DifferentialFlamegraphTooltipProps) {
|
|
const flamegraph = props.flamegraph as DifferentialFlamegraph;
|
|
const count = useMemo(() => {
|
|
return props.flamegraph.weights.get(props.frame.node);
|
|
}, [props.frame, props.flamegraph]);
|
|
|
|
// A change can only happen if a frame was present in previous and current profiles
|
|
const shouldShowChange = !!count;
|
|
const relative = shouldShowChange ? relativeChange(count.after, count.before) : 0;
|
|
|
|
const formattedChange = shouldShowChange
|
|
? relative === 0
|
|
? t('no change')
|
|
: `${relative > 0 ? '+' : ''}${formatPercentage(relative)}`
|
|
: props.flamegraph.negated
|
|
? t('removed function')
|
|
: t(`new function`);
|
|
|
|
return (
|
|
<BoundTooltip
|
|
bounds={props.canvasBounds}
|
|
cursor={props.configSpaceCursor}
|
|
canvas={props.flamegraphCanvas}
|
|
canvasView={props.flamegraphView}
|
|
>
|
|
<FlamegraphTooltipFrameMainInfo>
|
|
<FlamegraphTooltipColorIndicator
|
|
backgroundColor={formatColorForFrame(props.frame, props.flamegraphRenderer)}
|
|
/>
|
|
{flamegraph.formatter(props.frame.node.totalWeight)}{' '}
|
|
{t('samples, ') + formattedChange}{' '}
|
|
{`(${formatWeightToProfileDuration(props.frame.node, flamegraph)})`}{' '}
|
|
{props.frame.frame.name}
|
|
</FlamegraphTooltipFrameMainInfo>
|
|
<FlamegraphTooltipTimelineInfo>
|
|
{defined(props.frame.frame.file) && (
|
|
<Fragment>
|
|
{t('source')}:{formatFileNameAndLineColumn(props.frame)}
|
|
</Fragment>
|
|
)}
|
|
<FlamegraphTooltipTimelineInfo>
|
|
{props.frame.frame.is_application ? t('application frame') : t('system frame')}
|
|
</FlamegraphTooltipTimelineInfo>
|
|
</FlamegraphTooltipTimelineInfo>
|
|
</BoundTooltip>
|
|
);
|
|
}
|
|
|
|
interface AggregateFlamegraphTooltipProps extends FlamegraphTooltipProps {
|
|
frameInConfigSpace: Rect;
|
|
}
|
|
function AggregateFlamegraphTooltip(props: AggregateFlamegraphTooltipProps) {
|
|
return (
|
|
<BoundTooltip
|
|
bounds={props.canvasBounds}
|
|
cursor={props.configSpaceCursor}
|
|
canvas={props.flamegraphCanvas}
|
|
canvasView={props.flamegraphView}
|
|
>
|
|
<FlamegraphTooltipFrameMainInfo>
|
|
<FlamegraphTooltipColorIndicator
|
|
backgroundColor={formatColorForFrame(props.frame, props.flamegraphRenderer)}
|
|
/>
|
|
{props.flamegraphRenderer.flamegraph.formatter(props.frame.node.totalWeight)}{' '}
|
|
{t('samples') + ' '}
|
|
{`(${formatWeightToProfileDuration(
|
|
props.frame.node,
|
|
props.flamegraphRenderer.flamegraph
|
|
)})`}{' '}
|
|
{props.frame.frame.name}
|
|
</FlamegraphTooltipFrameMainInfo>
|
|
<FlamegraphTooltipTimelineInfo>
|
|
{defined(props.frame.frame.file) && (
|
|
<Fragment>
|
|
{t('source')}:{formatFileNameAndLineColumn(props.frame)}
|
|
</Fragment>
|
|
)}
|
|
<FlamegraphTooltipTimelineInfo>
|
|
{props.frame.frame.is_application ? t('application frame') : t('system frame')}
|
|
</FlamegraphTooltipTimelineInfo>
|
|
</FlamegraphTooltipTimelineInfo>
|
|
</BoundTooltip>
|
|
);
|
|
}
|
|
|
|
interface FlamechartTooltipProps extends FlamegraphTooltipProps {
|
|
frameInConfigSpace: Rect;
|
|
}
|
|
function FlamechartTooltip(props: FlamechartTooltipProps) {
|
|
return (
|
|
<BoundTooltip
|
|
bounds={props.canvasBounds}
|
|
cursor={props.configSpaceCursor}
|
|
canvas={props.flamegraphCanvas}
|
|
canvasView={props.flamegraphView}
|
|
>
|
|
<FlamegraphTooltipFrameMainInfo>
|
|
<FlamegraphTooltipColorIndicator
|
|
backgroundColor={formatColorForFrame(props.frame, props.flamegraphRenderer)}
|
|
/>
|
|
{props.flamegraphRenderer.flamegraph.formatter(props.frame.node.totalWeight)}{' '}
|
|
{`(${formatWeightToProfileDuration(
|
|
props.frame.node,
|
|
props.flamegraphRenderer.flamegraph
|
|
)})`}{' '}
|
|
{props.frame.frame.name}
|
|
</FlamegraphTooltipFrameMainInfo>
|
|
<FlamegraphTooltipTimelineInfo>
|
|
{defined(props.frame.frame.file) && (
|
|
<Fragment>
|
|
{t('source')}:{formatFileNameAndLineColumn(props.frame)}
|
|
</Fragment>
|
|
)}
|
|
<FlamegraphTooltipTimelineInfo>
|
|
{props.frame.frame.is_application ? t('application frame') : t('system frame')}
|
|
</FlamegraphTooltipTimelineInfo>
|
|
</FlamegraphTooltipTimelineInfo>
|
|
<FlamegraphTooltipTimelineInfo>
|
|
{props.flamegraphRenderer.flamegraph.timelineFormatter(
|
|
props.frameInConfigSpace.left
|
|
)}{' '}
|
|
{' \u2014 '}
|
|
{props.flamegraphRenderer.flamegraph.timelineFormatter(
|
|
props.frameInConfigSpace.right
|
|
)}
|
|
{props.frame.frame.inline ? (
|
|
<FlamegraphInlineIndicator>
|
|
<IconLightning width={10} />
|
|
{t('inline frame')}
|
|
</FlamegraphInlineIndicator>
|
|
) : (
|
|
''
|
|
)}
|
|
</FlamegraphTooltipTimelineInfo>
|
|
</BoundTooltip>
|
|
);
|
|
}
|
|
|
|
const FlamegraphInlineIndicator = styled('span')`
|
|
border: 1px solid ${p => p.theme.border};
|
|
border-radius: ${p => p.theme.borderRadius};
|
|
font-size: ${p => p.theme.fontSizeExtraSmall};
|
|
padding: ${space(0.25)} ${space(0.25)};
|
|
line-height: 12px;
|
|
margin: 0 ${space(0.5)};
|
|
align-self: flex-end;
|
|
|
|
svg {
|
|
width: 10px;
|
|
height: 10px;
|
|
transform: translateY(1px);
|
|
}
|
|
`;
|
|
|
|
export const FlamegraphTooltipTimelineInfo = styled('div')`
|
|
color: ${p => p.theme.subText};
|
|
`;
|
|
|
|
export const FlamegraphTooltipFrameMainInfo = styled('div')`
|
|
display: flex;
|
|
align-items: center;
|
|
`;
|
|
|
|
export const FlamegraphTooltipColorIndicator = styled('div')<{
|
|
backgroundColor: React.CSSProperties['backgroundColor'];
|
|
backgroundImage?: React.CSSProperties['backgroundImage'];
|
|
}>`
|
|
width: 12px;
|
|
height: 12px;
|
|
min-width: 12px;
|
|
min-height: 12px;
|
|
border-radius: 2px;
|
|
display: inline-block;
|
|
background-image: ${p => p.backgroundImage};
|
|
background-size: 16px 16px;
|
|
background-color: ${p => p.backgroundColor};
|
|
margin-right: ${space(1)};
|
|
`;
|