mirror of
https://github.com/hanzoai/sentinel.git
synced 2026-09-29 22:34:33 +00:00
1482 lines
50 KiB
TypeScript
1482 lines
50 KiB
TypeScript
import type {ReactElement} from 'react';
|
|
import {
|
|
Fragment,
|
|
useCallback,
|
|
useEffect,
|
|
useLayoutEffect,
|
|
useMemo,
|
|
useState,
|
|
} from 'react';
|
|
import * as Sentry from '@sentry/react';
|
|
import {mat3, vec2} from 'gl-matrix';
|
|
|
|
import {addErrorMessage} from 'sentry/actionCreators/indicator';
|
|
import {ProfileDragDropImport} from 'sentry/components/profiling/flamegraph/flamegraphOverlays/profileDragDropImport';
|
|
import {FlamegraphOptionsMenu} from 'sentry/components/profiling/flamegraph/flamegraphToolbar/flamegraphOptionsMenu';
|
|
import {FlamegraphSearch} from 'sentry/components/profiling/flamegraph/flamegraphToolbar/flamegraphSearch';
|
|
import type {FlamegraphThreadSelectorProps} from 'sentry/components/profiling/flamegraph/flamegraphToolbar/flamegraphThreadSelector';
|
|
import {FlamegraphThreadSelector} from 'sentry/components/profiling/flamegraph/flamegraphToolbar/flamegraphThreadSelector';
|
|
import {FlamegraphToolbar} from 'sentry/components/profiling/flamegraph/flamegraphToolbar/flamegraphToolbar';
|
|
import type {FlamegraphViewSelectMenuProps} from 'sentry/components/profiling/flamegraph/flamegraphToolbar/flamegraphViewSelectMenu';
|
|
import {FlamegraphViewSelectMenu} from 'sentry/components/profiling/flamegraph/flamegraphToolbar/flamegraphViewSelectMenu';
|
|
import {FlamegraphZoomView} from 'sentry/components/profiling/flamegraph/flamegraphZoomView';
|
|
import {FlamegraphZoomViewMinimap} from 'sentry/components/profiling/flamegraph/flamegraphZoomViewMinimap';
|
|
import {t} from 'sentry/locale';
|
|
import type {EventTransaction, RequestState} from 'sentry/types';
|
|
import {EntryType} from 'sentry/types';
|
|
import type {EntrySpans} from 'sentry/types/event';
|
|
import {defined} from 'sentry/utils';
|
|
import {
|
|
CanvasPoolManager,
|
|
useCanvasScheduler,
|
|
} from 'sentry/utils/profiling/canvasScheduler';
|
|
import {CanvasView} from 'sentry/utils/profiling/canvasView';
|
|
import {Flamegraph as FlamegraphModel} from 'sentry/utils/profiling/flamegraph';
|
|
import type {FlamegraphSearch as FlamegraphSearchType} from 'sentry/utils/profiling/flamegraph/flamegraphStateProvider/reducers/flamegraphSearch';
|
|
import {useFlamegraphPreferences} from 'sentry/utils/profiling/flamegraph/hooks/useFlamegraphPreferences';
|
|
import {useFlamegraphProfiles} from 'sentry/utils/profiling/flamegraph/hooks/useFlamegraphProfiles';
|
|
import {useFlamegraphSearch} from 'sentry/utils/profiling/flamegraph/hooks/useFlamegraphSearch';
|
|
import {useDispatchFlamegraphState} from 'sentry/utils/profiling/flamegraph/hooks/useFlamegraphState';
|
|
import {useFlamegraphZoomPosition} from 'sentry/utils/profiling/flamegraph/hooks/useFlamegraphZoomPosition';
|
|
import {useFlamegraphTheme} from 'sentry/utils/profiling/flamegraph/useFlamegraphTheme';
|
|
import {FlamegraphCanvas} from 'sentry/utils/profiling/flamegraphCanvas';
|
|
import type {ProfileSeriesMeasurement} from 'sentry/utils/profiling/flamegraphChart';
|
|
import {FlamegraphChart as FlamegraphChartModel} from 'sentry/utils/profiling/flamegraphChart';
|
|
import type {FlamegraphFrame} from 'sentry/utils/profiling/flamegraphFrame';
|
|
import {
|
|
computeConfigViewWithStrategy,
|
|
computeMinZoomConfigViewForFrames,
|
|
formatColorForFrame,
|
|
initializeFlamegraphRenderer,
|
|
useResizeCanvasObserver,
|
|
} from 'sentry/utils/profiling/gl/utils';
|
|
import type {ProfileGroup} from 'sentry/utils/profiling/profile/importProfile';
|
|
import {FlamegraphRenderer2D} from 'sentry/utils/profiling/renderers/flamegraphRenderer2D';
|
|
import {FlamegraphRendererWebGL} from 'sentry/utils/profiling/renderers/flamegraphRendererWebGL';
|
|
import type {SpanChartNode} from 'sentry/utils/profiling/spanChart';
|
|
import {SpanChart} from 'sentry/utils/profiling/spanChart';
|
|
import {SpanTree} from 'sentry/utils/profiling/spanTree';
|
|
import {Rect} from 'sentry/utils/profiling/speedscope';
|
|
import {UIFrames} from 'sentry/utils/profiling/uiFrames';
|
|
import type {ProfilingFormatterUnit} from 'sentry/utils/profiling/units/units';
|
|
import {formatTo, fromNanoJoulesToWatts} from 'sentry/utils/profiling/units/units';
|
|
import {useDevicePixelRatio} from 'sentry/utils/useDevicePixelRatio';
|
|
import {useMemoWithPrevious} from 'sentry/utils/useMemoWithPrevious';
|
|
import {useProfileGroup} from 'sentry/views/profiling/profileGroupProvider';
|
|
import {
|
|
useProfileTransaction,
|
|
useSetProfiles,
|
|
} from 'sentry/views/profiling/profilesProvider';
|
|
|
|
import {FlamegraphDrawer} from './flamegraphDrawer/flamegraphDrawer';
|
|
import {FlamegraphWarnings} from './flamegraphOverlays/FlamegraphWarnings';
|
|
import {useViewKeyboardNavigation} from './interactions/useViewKeyboardNavigation';
|
|
import {FlamegraphChart} from './flamegraphChart';
|
|
import {FlamegraphLayout} from './flamegraphLayout';
|
|
import {FlamegraphSpans} from './flamegraphSpans';
|
|
import {FlamegraphUIFrames} from './flamegraphUIFrames';
|
|
|
|
function getMaxConfigSpace(
|
|
profileGroup: ProfileGroup,
|
|
transaction: EventTransaction | null,
|
|
unit: ProfilingFormatterUnit | string
|
|
): Rect {
|
|
// We have a transaction, so we should do our best to align the profile
|
|
// with the transaction's timeline.
|
|
const maxProfileDuration = Math.max(...profileGroup.profiles.map(p => p.duration));
|
|
if (transaction) {
|
|
// TODO: Adjust the alignment based on the profile's timestamp if it does
|
|
// not match the transaction's start timestamp
|
|
const transactionDuration = transaction.endTimestamp - transaction.startTimestamp;
|
|
// On most platforms, profile duration < transaction duration, however
|
|
// there is one beloved platform where that is not true; android.
|
|
// Hence, we should take the max of the two to ensure both the transaction
|
|
// and profile are fully visible to the user.
|
|
const duration = Math.max(
|
|
formatTo(transactionDuration, 'seconds', unit),
|
|
maxProfileDuration
|
|
);
|
|
return new Rect(0, 0, duration, 0);
|
|
}
|
|
|
|
// No transaction was found, so best we can do is align it to the starting
|
|
// position of the profiles - find the max of profile durations
|
|
return new Rect(0, 0, maxProfileDuration, 0);
|
|
}
|
|
|
|
function collectAllSpanEntriesFromTransaction(
|
|
transaction: EventTransaction
|
|
): EntrySpans['data'] {
|
|
if (!transaction.entries.length) {
|
|
return [];
|
|
}
|
|
|
|
const spans = transaction.entries.filter(
|
|
(e): e is EntrySpans => e.type === EntryType.SPANS
|
|
);
|
|
|
|
let allSpans: EntrySpans['data'] = [];
|
|
|
|
for (const span of spans) {
|
|
allSpans = allSpans.concat(span.data);
|
|
}
|
|
|
|
return allSpans;
|
|
}
|
|
|
|
type FlamegraphCandidate = {
|
|
frame: FlamegraphFrame;
|
|
threadId: number;
|
|
isActiveThread?: boolean; // this is the thread referred to by the active profile index
|
|
};
|
|
|
|
function findLongestMatchingFrame(
|
|
flamegraph: FlamegraphModel,
|
|
focusFrame: FlamegraphSearchType['highlightFrames']
|
|
): FlamegraphFrame | null {
|
|
if (focusFrame === null) {
|
|
return null;
|
|
}
|
|
|
|
let longestFrame: FlamegraphFrame | null = null;
|
|
|
|
const frames: FlamegraphFrame[] = [...flamegraph.root.children];
|
|
while (frames.length > 0) {
|
|
const frame = frames.pop()!;
|
|
if (
|
|
focusFrame.name === frame.frame.name &&
|
|
// the image name on a frame is optional treat it the same as the empty string
|
|
(focusFrame.package === (frame.frame.package || '') ||
|
|
focusFrame.package === (frame.frame.module || '')) &&
|
|
(longestFrame?.node?.totalWeight || 0) < frame.node.totalWeight
|
|
) {
|
|
longestFrame = frame;
|
|
}
|
|
|
|
for (let i = 0; i < frame.children.length; i++) {
|
|
frames.push(frame.children[i]);
|
|
}
|
|
}
|
|
|
|
return longestFrame;
|
|
}
|
|
|
|
function computeProfileOffset(
|
|
flamegraph: FlamegraphModel,
|
|
transaction: RequestState<EventTransaction | null>
|
|
): number {
|
|
let offset = flamegraph.profile.startedAt;
|
|
|
|
const transactionStart =
|
|
transaction.type === 'resolved' ? transaction.data?.startTimestamp ?? null : null;
|
|
|
|
const profileStart = flamegraph.profile.timestamp;
|
|
|
|
if (defined(transactionStart) && defined(profileStart)) {
|
|
offset += formatTo(
|
|
profileStart - transactionStart,
|
|
'second',
|
|
flamegraph.profile.unit
|
|
);
|
|
}
|
|
|
|
return offset;
|
|
}
|
|
|
|
const LOADING_OR_FALLBACK_FLAMEGRAPH = FlamegraphModel.Empty();
|
|
const LOADING_OR_FALLBACK_SPAN_TREE = SpanTree.Empty;
|
|
const LOADING_OR_FALLBACK_UIFRAMES = UIFrames.Empty;
|
|
const LOADING_OR_FALLBACK_BATTERY_CHART = FlamegraphChartModel.Empty;
|
|
const LOADING_OR_FALLBACK_CPU_CHART = FlamegraphChartModel.Empty;
|
|
const LOADING_OR_FALLBACK_MEMORY_CHART = FlamegraphChartModel.Empty;
|
|
|
|
const noopFormatDuration = () => '';
|
|
|
|
function Flamegraph(): ReactElement {
|
|
const devicePixelRatio = useDevicePixelRatio();
|
|
const profiledTransaction = useProfileTransaction();
|
|
const dispatch = useDispatchFlamegraphState();
|
|
|
|
const setProfiles = useSetProfiles();
|
|
const profileGroup = useProfileGroup();
|
|
|
|
const flamegraphTheme = useFlamegraphTheme();
|
|
const position = useFlamegraphZoomPosition();
|
|
const profiles = useFlamegraphProfiles();
|
|
const {colorCoding, sorting, view} = useFlamegraphPreferences();
|
|
const {highlightFrames} = useFlamegraphSearch();
|
|
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 [spansCanvasRef, setSpansCanvasRef] = useState<HTMLCanvasElement | null>(null);
|
|
const [uiFramesCanvasRef, setUIFramesCanvasRef] = useState<HTMLCanvasElement | null>(
|
|
null
|
|
);
|
|
|
|
const [batteryChartCanvasRef, setBatteryChartCanvasRef] =
|
|
useState<HTMLCanvasElement | null>(null);
|
|
const [cpuChartCanvasRef, setCpuChartCanvasRef] = useState<HTMLCanvasElement | null>(
|
|
null
|
|
);
|
|
const [memoryChartCanvasRef, setMemoryChartCanvasRef] =
|
|
useState<HTMLCanvasElement | null>(null);
|
|
|
|
const canvasPoolManager = useMemo(() => new CanvasPoolManager(), []);
|
|
const scheduler = useCanvasScheduler(canvasPoolManager);
|
|
|
|
const hasUIFrames = useMemo(() => {
|
|
const platform = profileGroup.metadata.platform;
|
|
return platform === 'cocoa' || platform === 'android';
|
|
}, [profileGroup.metadata.platform]);
|
|
|
|
const hasBatteryChart = useMemo(() => {
|
|
const platform = profileGroup.metadata.platform;
|
|
return platform === 'cocoa';
|
|
}, [profileGroup.metadata.platform]);
|
|
|
|
const hasCPUChart = useMemo(() => {
|
|
const platform = profileGroup.metadata.platform;
|
|
return platform === 'cocoa' || platform === 'android' || platform === 'node';
|
|
}, [profileGroup.metadata.platform]);
|
|
|
|
const hasMemoryChart = useMemo(() => {
|
|
const platform = profileGroup.metadata.platform;
|
|
return platform === 'cocoa' || platform === 'android' || platform === 'node';
|
|
}, [profileGroup.metadata.platform]);
|
|
|
|
const profile = useMemo(() => {
|
|
return profileGroup.profiles.find(p => p.threadId === threadId);
|
|
}, [profileGroup, threadId]);
|
|
|
|
const spanTree: SpanTree = useMemo(() => {
|
|
if (profiledTransaction.type === 'resolved' && profiledTransaction.data) {
|
|
return new SpanTree(
|
|
profiledTransaction.data,
|
|
collectAllSpanEntriesFromTransaction(profiledTransaction.data)
|
|
);
|
|
}
|
|
|
|
return LOADING_OR_FALLBACK_SPAN_TREE;
|
|
}, [profiledTransaction]);
|
|
|
|
const spanChart = useMemo(() => {
|
|
if (!profile) {
|
|
return null;
|
|
}
|
|
|
|
return new SpanChart(spanTree, {
|
|
unit: profile.unit,
|
|
configSpace: getMaxConfigSpace(
|
|
profileGroup,
|
|
profiledTransaction.type === 'resolved' ? profiledTransaction.data : null,
|
|
profile.unit
|
|
),
|
|
});
|
|
}, [spanTree, profile, profileGroup, profiledTransaction]);
|
|
|
|
const flamegraph = useMemo(() => {
|
|
if (typeof threadId !== 'number') {
|
|
return LOADING_OR_FALLBACK_FLAMEGRAPH;
|
|
}
|
|
|
|
// This could happen if threadId was initialized from query string, but for some
|
|
// reason the profile was removed from the list of profiles.
|
|
if (!profile) {
|
|
return LOADING_OR_FALLBACK_FLAMEGRAPH;
|
|
}
|
|
|
|
// Wait for the transaction to finish loading, regardless of the results.
|
|
// Otherwise, the rendered profile will probably shift once the transaction loads.
|
|
if (
|
|
profiledTransaction.type === 'loading' ||
|
|
profiledTransaction.type === 'initial'
|
|
) {
|
|
return LOADING_OR_FALLBACK_FLAMEGRAPH;
|
|
}
|
|
|
|
const transaction = Sentry.startTransaction({
|
|
op: 'import',
|
|
name: 'flamegraph.constructor',
|
|
});
|
|
|
|
transaction.setTag('sorting', sorting.split(' ').join('_'));
|
|
transaction.setTag('view', view.split(' ').join('_'));
|
|
|
|
const newFlamegraph = new FlamegraphModel(profile, {
|
|
inverted: view === 'bottom up',
|
|
sort: sorting,
|
|
configSpace: getMaxConfigSpace(
|
|
profileGroup,
|
|
profiledTransaction.type === 'resolved' ? profiledTransaction.data : null,
|
|
profile.unit
|
|
),
|
|
});
|
|
transaction.finish();
|
|
|
|
return newFlamegraph;
|
|
}, [profile, profileGroup, profiledTransaction, sorting, threadId, view]);
|
|
|
|
const profileOffsetFromTransaction = useMemo(
|
|
() => computeProfileOffset(flamegraph, profiledTransaction),
|
|
[flamegraph, profiledTransaction]
|
|
);
|
|
|
|
const uiFrames = useMemo(() => {
|
|
if (!hasUIFrames) {
|
|
return LOADING_OR_FALLBACK_UIFRAMES;
|
|
}
|
|
return new UIFrames(
|
|
{
|
|
slow: profileGroup.measurements?.slow_frame_renders,
|
|
frozen: profileGroup.measurements?.frozen_frame_renders,
|
|
},
|
|
{unit: flamegraph.profile.unit},
|
|
flamegraph.configSpace.withHeight(1)
|
|
);
|
|
}, [
|
|
profileGroup.measurements,
|
|
flamegraph.profile.unit,
|
|
flamegraph.configSpace,
|
|
hasUIFrames,
|
|
]);
|
|
|
|
const batteryChart = useMemo(() => {
|
|
if (!hasCPUChart) {
|
|
return LOADING_OR_FALLBACK_BATTERY_CHART;
|
|
}
|
|
|
|
const measures: ProfileSeriesMeasurement[] = [];
|
|
|
|
for (const key in profileGroup.measurements) {
|
|
if (key === 'cpu_energy_usage') {
|
|
measures.push({
|
|
...profileGroup.measurements[key]!,
|
|
values: profileGroup.measurements[key]!.values.map(v => {
|
|
return {
|
|
elapsed_since_start_ns: v.elapsed_since_start_ns,
|
|
value: fromNanoJoulesToWatts(v.value, 0.1),
|
|
};
|
|
}),
|
|
// some versions of cocoa send byte so we need to correct it to watt
|
|
unit: 'watt',
|
|
name: 'CPU energy usage',
|
|
});
|
|
}
|
|
}
|
|
|
|
return new FlamegraphChartModel(
|
|
Rect.From(flamegraph.configSpace),
|
|
measures.length > 0 ? measures : [],
|
|
flamegraphTheme.COLORS.BATTERY_CHART_COLORS
|
|
);
|
|
}, [profileGroup.measurements, flamegraph.configSpace, flamegraphTheme, hasCPUChart]);
|
|
|
|
const CPUChart = useMemo(() => {
|
|
if (!hasCPUChart) {
|
|
return LOADING_OR_FALLBACK_CPU_CHART;
|
|
}
|
|
|
|
const measures: ProfileSeriesMeasurement[] = [];
|
|
|
|
for (const key in profileGroup.measurements) {
|
|
if (key.startsWith('cpu_usage')) {
|
|
const name =
|
|
key === 'cpu_usage'
|
|
? 'Average CPU usage'
|
|
: `CPU Core ${key.replace('cpu_usage_', '')}`;
|
|
measures.push({...profileGroup.measurements[key]!, name});
|
|
}
|
|
}
|
|
|
|
return new FlamegraphChartModel(
|
|
Rect.From(flamegraph.configSpace),
|
|
measures.length > 0 ? measures : [],
|
|
flamegraphTheme.COLORS.CPU_CHART_COLORS
|
|
);
|
|
}, [profileGroup.measurements, flamegraph.configSpace, flamegraphTheme, hasCPUChart]);
|
|
|
|
const memoryChart = useMemo(() => {
|
|
if (!hasMemoryChart) {
|
|
return LOADING_OR_FALLBACK_MEMORY_CHART;
|
|
}
|
|
|
|
const measures: ProfileSeriesMeasurement[] = [];
|
|
|
|
const memory_footprint = profileGroup.measurements?.memory_footprint;
|
|
if (memory_footprint) {
|
|
measures.push({
|
|
...memory_footprint!,
|
|
name: 'Heap Usage',
|
|
});
|
|
}
|
|
|
|
const native_memory_footprint = profileGroup.measurements?.memory_native_footprint;
|
|
if (native_memory_footprint) {
|
|
measures.push({
|
|
...native_memory_footprint!,
|
|
name: 'Native Heap Usage',
|
|
});
|
|
}
|
|
|
|
return new FlamegraphChartModel(
|
|
Rect.From(flamegraph.configSpace),
|
|
measures.length > 0 ? measures : [],
|
|
flamegraphTheme.COLORS.MEMORY_CHART_COLORS,
|
|
{type: 'area'}
|
|
);
|
|
}, [
|
|
profileGroup.measurements,
|
|
flamegraph.configSpace,
|
|
flamegraphTheme,
|
|
hasMemoryChart,
|
|
]);
|
|
|
|
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 spansCanvas = useMemo(() => {
|
|
if (!spansCanvasRef) {
|
|
return null;
|
|
}
|
|
return new FlamegraphCanvas(spansCanvasRef, vec2.fromValues(0, 0));
|
|
}, [spansCanvasRef]);
|
|
|
|
const uiFramesCanvas = useMemo(() => {
|
|
if (!uiFramesCanvasRef) {
|
|
return null;
|
|
}
|
|
return new FlamegraphCanvas(uiFramesCanvasRef, vec2.fromValues(0, 0));
|
|
}, [uiFramesCanvasRef]);
|
|
|
|
const batteryChartCanvas = useMemo(() => {
|
|
if (!batteryChartCanvasRef) {
|
|
return null;
|
|
}
|
|
return new FlamegraphCanvas(batteryChartCanvasRef, vec2.fromValues(0, 0));
|
|
}, [batteryChartCanvasRef]);
|
|
|
|
const cpuChartCanvas = useMemo(() => {
|
|
if (!cpuChartCanvasRef) {
|
|
return null;
|
|
}
|
|
return new FlamegraphCanvas(cpuChartCanvasRef, vec2.fromValues(0, 0));
|
|
}, [cpuChartCanvasRef]);
|
|
|
|
const memoryChartCanvas = useMemo(() => {
|
|
if (!memoryChartCanvasRef) {
|
|
return null;
|
|
}
|
|
return new FlamegraphCanvas(memoryChartCanvasRef, vec2.fromValues(0, 0));
|
|
}, [memoryChartCanvasRef]);
|
|
|
|
const flamegraphView = useMemoWithPrevious<CanvasView<FlamegraphModel> | null>(
|
|
previousView => {
|
|
if (!flamegraphCanvas) {
|
|
return null;
|
|
}
|
|
|
|
const newView = new CanvasView({
|
|
canvas: flamegraphCanvas,
|
|
model: flamegraph,
|
|
options: {
|
|
inverted: flamegraph.inverted,
|
|
minWidth: flamegraph.profile.minFrameDuration,
|
|
barHeight: flamegraphTheme.SIZES.BAR_HEIGHT,
|
|
depthOffset: flamegraphTheme.SIZES.FLAMEGRAPH_DEPTH_OFFSET,
|
|
configSpaceTransform: new Rect(profileOffsetFromTransaction, 0, 0, 0),
|
|
},
|
|
});
|
|
|
|
if (
|
|
// 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.
|
|
previousView?.model.profile === newView.model.profile &&
|
|
previousView.configSpace.equals(newView.configSpace)
|
|
) {
|
|
if (
|
|
// 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
|
|
previousView.model.sort === 'left heavy' &&
|
|
newView.model.sort === 'left heavy'
|
|
) {
|
|
newView.setConfigView(
|
|
previousView.configView.withHeight(newView.configView.height)
|
|
);
|
|
}
|
|
}
|
|
|
|
if (defined(highlightFrames)) {
|
|
let frames = flamegraph.findAllMatchingFrames(
|
|
highlightFrames.name,
|
|
highlightFrames.package
|
|
);
|
|
|
|
if (
|
|
!frames.length &&
|
|
!highlightFrames.package &&
|
|
highlightFrames.name &&
|
|
profileGroup.metadata.platform === 'node'
|
|
) {
|
|
// there is a chance that the reason we did not find any frames is because
|
|
// for node, we try to infer some package from the frontend code.
|
|
// If that happens, we'll try and just do a search by name. This logic
|
|
// is duplicated in flamegraphZoomView.tsx and should be kept in sync
|
|
frames = flamegraph.findAllMatchingFramesBy(highlightFrames.name, ['name']);
|
|
}
|
|
|
|
if (frames.length > 0) {
|
|
const rectFrames = frames.map(
|
|
f => new Rect(f.start, f.depth, f.end - f.start, 1)
|
|
);
|
|
const newConfigView = computeMinZoomConfigViewForFrames(
|
|
newView.configView,
|
|
rectFrames
|
|
).transformRect(newView.configSpaceTransform);
|
|
newView.setConfigView(newConfigView);
|
|
return newView;
|
|
}
|
|
}
|
|
|
|
// Because we render empty flamechart while we fetch the data, we need to make sure
|
|
// to have some heuristic when the data is fetched to determine if we should
|
|
// initialize the config view to the full view or a predefined value
|
|
else if (
|
|
!defined(highlightFrames) &&
|
|
position.view &&
|
|
!position.view.isEmpty() &&
|
|
previousView?.model === LOADING_OR_FALLBACK_FLAMEGRAPH
|
|
) {
|
|
// We allow min width to be initialize to lower than view.minWidth because
|
|
// there is a chance that user zoomed into a span duration which may have been updated
|
|
// after the model was loaded (see L320)
|
|
newView.setConfigView(position.view, {width: {min: 0}});
|
|
}
|
|
|
|
return newView;
|
|
},
|
|
|
|
// We skip position.view dependency because it will go into an infinite loop
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
[flamegraph, flamegraphCanvas, flamegraphTheme, profileOffsetFromTransaction]
|
|
);
|
|
|
|
const uiFramesView = useMemoWithPrevious<CanvasView<UIFrames> | null>(
|
|
_previousView => {
|
|
if (!flamegraphView || !flamegraphCanvas || !uiFrames) {
|
|
return null;
|
|
}
|
|
|
|
const newView = new CanvasView({
|
|
canvas: flamegraphCanvas,
|
|
model: uiFrames,
|
|
mode: 'stretchToFit',
|
|
options: {
|
|
inverted: flamegraph.inverted,
|
|
minWidth: uiFrames.minFrameDuration,
|
|
barHeight: 10,
|
|
depthOffset: 0,
|
|
configSpaceTransform: new Rect(profileOffsetFromTransaction, 0, 0, 0),
|
|
},
|
|
});
|
|
|
|
// Initialize configView to whatever the flamegraph configView is
|
|
newView.setConfigView(
|
|
flamegraphView.configView.withHeight(newView.configView.height),
|
|
{width: {min: 0}}
|
|
);
|
|
|
|
return newView;
|
|
},
|
|
[flamegraphView, flamegraphCanvas, flamegraph, uiFrames, profileOffsetFromTransaction]
|
|
);
|
|
|
|
const batteryChartView = useMemoWithPrevious<CanvasView<FlamegraphChartModel> | null>(
|
|
_previousView => {
|
|
if (!flamegraphView || !flamegraphCanvas || !batteryChart || !batteryChartCanvas) {
|
|
return null;
|
|
}
|
|
|
|
const newView = new CanvasView({
|
|
canvas: flamegraphCanvas,
|
|
model: batteryChart,
|
|
mode: 'anchorBottom',
|
|
options: {
|
|
// Invert chart so origin is at bottom left
|
|
// corner as opposed to top left
|
|
inverted: true,
|
|
minWidth: uiFrames.minFrameDuration,
|
|
barHeight: 0,
|
|
depthOffset: 0,
|
|
maxHeight: batteryChart.configSpace.height,
|
|
minHeight: batteryChart.configSpace.height,
|
|
configSpaceTransform: new Rect(profileOffsetFromTransaction, 0, 0, 0),
|
|
},
|
|
});
|
|
|
|
// Compute the total size of the padding and stretch the view. This ensures that
|
|
// the total range is rendered and perfectly aligned from top to bottom.
|
|
newView.setConfigView(
|
|
flamegraphView.configView.withHeight(newView.configView.height),
|
|
{
|
|
width: {min: 1},
|
|
}
|
|
);
|
|
|
|
return newView;
|
|
},
|
|
[
|
|
flamegraphView,
|
|
flamegraphCanvas,
|
|
batteryChart,
|
|
uiFrames.minFrameDuration,
|
|
batteryChartCanvas,
|
|
profileOffsetFromTransaction,
|
|
]
|
|
);
|
|
|
|
const cpuChartView = useMemoWithPrevious<CanvasView<FlamegraphChartModel> | null>(
|
|
_previousView => {
|
|
if (!flamegraphView || !flamegraphCanvas || !CPUChart || !cpuChartCanvas) {
|
|
return null;
|
|
}
|
|
|
|
const newView = new CanvasView({
|
|
canvas: flamegraphCanvas,
|
|
model: CPUChart,
|
|
mode: 'anchorBottom',
|
|
options: {
|
|
// Invert chart so origin is at bottom left
|
|
// corner as opposed to top left
|
|
inverted: true,
|
|
minWidth: uiFrames.minFrameDuration,
|
|
barHeight: 0,
|
|
depthOffset: 0,
|
|
maxHeight: CPUChart.configSpace.height,
|
|
minHeight: CPUChart.configSpace.height,
|
|
configSpaceTransform: new Rect(profileOffsetFromTransaction, 0, 0, 0),
|
|
},
|
|
});
|
|
|
|
// Compute the total size of the padding and stretch the view. This ensures that
|
|
// the total range is rendered and perfectly aligned from top to bottom.
|
|
newView.setConfigView(
|
|
flamegraphView.configView.withHeight(newView.configView.height),
|
|
{
|
|
width: {min: 1},
|
|
}
|
|
);
|
|
|
|
return newView;
|
|
},
|
|
[
|
|
flamegraphView,
|
|
flamegraphCanvas,
|
|
CPUChart,
|
|
uiFrames.minFrameDuration,
|
|
cpuChartCanvas,
|
|
profileOffsetFromTransaction,
|
|
]
|
|
);
|
|
|
|
const memoryChartView = useMemoWithPrevious<CanvasView<FlamegraphChartModel> | null>(
|
|
_previousView => {
|
|
if (!flamegraphView || !flamegraphCanvas || !memoryChart || !memoryChartCanvas) {
|
|
return null;
|
|
}
|
|
|
|
const newView = new CanvasView({
|
|
canvas: flamegraphCanvas,
|
|
model: memoryChart,
|
|
mode: 'anchorBottom',
|
|
options: {
|
|
// Invert chart so origin is at bottom left
|
|
// corner as opposed to top left
|
|
inverted: true,
|
|
minWidth: uiFrames.minFrameDuration,
|
|
barHeight: 0,
|
|
depthOffset: 0,
|
|
maxHeight: memoryChart.configSpace.height,
|
|
minHeight: memoryChart.configSpace.height,
|
|
configSpaceTransform: new Rect(profileOffsetFromTransaction, 0, 0, 0),
|
|
},
|
|
});
|
|
|
|
// Compute the total size of the padding and stretch the view. This ensures that
|
|
// the total range is rendered and perfectly aligned from top to bottom.
|
|
newView.setConfigView(
|
|
flamegraphView.configView.withHeight(newView.configView.height),
|
|
{
|
|
width: {min: 1},
|
|
}
|
|
);
|
|
|
|
return newView;
|
|
},
|
|
[
|
|
flamegraphView,
|
|
flamegraphCanvas,
|
|
memoryChart,
|
|
uiFrames.minFrameDuration,
|
|
memoryChartCanvas,
|
|
profileOffsetFromTransaction,
|
|
]
|
|
);
|
|
|
|
const spansView = useMemoWithPrevious<CanvasView<SpanChart> | null>(
|
|
_previousView => {
|
|
if (!spansCanvas || !spanChart || !flamegraphView) {
|
|
return null;
|
|
}
|
|
|
|
const newView = new CanvasView({
|
|
canvas: spansCanvas,
|
|
model: spanChart,
|
|
options: {
|
|
inverted: false,
|
|
minWidth: spanChart.minSpanDuration,
|
|
barHeight: flamegraphTheme.SIZES.SPANS_BAR_HEIGHT,
|
|
depthOffset: flamegraphTheme.SIZES.SPANS_DEPTH_OFFSET,
|
|
},
|
|
});
|
|
|
|
// Initialize configView to whatever the flamegraph configView is
|
|
newView.setConfigView(
|
|
flamegraphView.configView.withHeight(newView.configView.height),
|
|
{width: {min: 1}}
|
|
);
|
|
|
|
return newView;
|
|
},
|
|
[spanChart, spansCanvas, flamegraphView, flamegraphTheme.SIZES]
|
|
);
|
|
|
|
// We want to make sure that the views have the same min zoom levels so that
|
|
// if you wheel zoom on one, the other one will also zoom to the same level of detail.
|
|
// If we dont do this, then at some point during the zoom action the views will
|
|
// detach and only one will zoom while the other one will stay at the same zoom level.
|
|
useEffect(() => {
|
|
const minWidthBetweenViews = Math.min(
|
|
flamegraphView?.minWidth ?? Number.MAX_SAFE_INTEGER,
|
|
spansView?.minWidth ?? Number.MAX_SAFE_INTEGER,
|
|
uiFramesView?.minWidth ?? Number.MAX_SAFE_INTEGER,
|
|
cpuChartView?.minWidth ?? Number.MAX_SAFE_INTEGER,
|
|
memoryChartView?.minWidth ?? Number.MAX_SAFE_INTEGER,
|
|
batteryChartView?.minWidth ?? Number.MAX_SAFE_INTEGER
|
|
);
|
|
|
|
flamegraphView?.setMinWidth?.(minWidthBetweenViews);
|
|
spansView?.setMinWidth?.(minWidthBetweenViews);
|
|
uiFramesView?.setMinWidth?.(minWidthBetweenViews);
|
|
cpuChartView?.setMinWidth?.(minWidthBetweenViews);
|
|
memoryChartView?.setMinWidth?.(minWidthBetweenViews);
|
|
batteryChartView?.setMinWidth?.(minWidthBetweenViews);
|
|
}, [
|
|
flamegraphView,
|
|
spansView,
|
|
uiFramesView,
|
|
cpuChartView,
|
|
memoryChartView,
|
|
batteryChartView,
|
|
]);
|
|
|
|
// 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));
|
|
|
|
if (spansView) {
|
|
const beforeY = spansView.configView.y;
|
|
spansView.setConfigView(
|
|
rect.withHeight(spansView.configView.height).withY(beforeY)
|
|
);
|
|
}
|
|
if (uiFramesView) {
|
|
uiFramesView.setConfigView(rect);
|
|
}
|
|
if (cpuChartView) {
|
|
cpuChartView.setConfigView(rect);
|
|
}
|
|
if (memoryChartView) {
|
|
memoryChartView.setConfigView(rect);
|
|
}
|
|
if (batteryChartView) {
|
|
batteryChartView.setConfigView(rect);
|
|
}
|
|
}
|
|
|
|
if (sourceConfigViewChange === spansView) {
|
|
spansView.setConfigView(rect.withHeight(spansView.configView.height));
|
|
const beforeY = flamegraphView.configView.y;
|
|
flamegraphView.setConfigView(
|
|
rect.withHeight(flamegraphView.configView.height).withY(beforeY)
|
|
);
|
|
if (uiFramesView) {
|
|
uiFramesView.setConfigView(rect);
|
|
}
|
|
if (cpuChartView) {
|
|
cpuChartView.setConfigView(rect);
|
|
}
|
|
if (memoryChartView) {
|
|
memoryChartView.setConfigView(rect);
|
|
}
|
|
if (batteryChartView) {
|
|
batteryChartView.setConfigView(rect);
|
|
}
|
|
}
|
|
|
|
canvasPoolManager.draw();
|
|
};
|
|
|
|
const onTransformConfigView = (
|
|
mat: mat3,
|
|
sourceTransformConfigView: CanvasView<any>
|
|
) => {
|
|
if (sourceTransformConfigView === flamegraphView) {
|
|
flamegraphView.transformConfigView(mat);
|
|
if (spansView) {
|
|
const beforeY = spansView.configView.y;
|
|
spansView.transformConfigView(mat);
|
|
spansView.setConfigView(spansView.configView.withY(beforeY));
|
|
}
|
|
if (uiFramesView) {
|
|
uiFramesView.transformConfigView(mat);
|
|
}
|
|
if (batteryChartView) {
|
|
batteryChartView.transformConfigView(mat);
|
|
}
|
|
if (cpuChartView) {
|
|
cpuChartView.transformConfigView(mat);
|
|
}
|
|
if (memoryChartView) {
|
|
memoryChartView.transformConfigView(mat);
|
|
}
|
|
}
|
|
|
|
if (sourceTransformConfigView === spansView) {
|
|
spansView.transformConfigView(mat);
|
|
const beforeY = flamegraphView.configView.y;
|
|
flamegraphView.transformConfigView(mat);
|
|
flamegraphView.setConfigView(flamegraphView.configView.withY(beforeY));
|
|
if (uiFramesView) {
|
|
uiFramesView.transformConfigView(mat);
|
|
}
|
|
if (batteryChartView) {
|
|
batteryChartView.transformConfigView(mat);
|
|
}
|
|
if (cpuChartView) {
|
|
cpuChartView.transformConfigView(mat);
|
|
}
|
|
if (memoryChartView) {
|
|
memoryChartView.transformConfigView(mat);
|
|
}
|
|
}
|
|
|
|
if (
|
|
sourceTransformConfigView === uiFramesView ||
|
|
sourceTransformConfigView === cpuChartView ||
|
|
sourceTransformConfigView === memoryChartView ||
|
|
sourceTransformConfigView === batteryChartView
|
|
) {
|
|
if (flamegraphView) {
|
|
const beforeY = flamegraphView.configView.y;
|
|
flamegraphView.transformConfigView(mat);
|
|
flamegraphView.setConfigView(flamegraphView.configView.withY(beforeY));
|
|
}
|
|
|
|
if (spansView) {
|
|
const beforeY = spansView.configView.y;
|
|
spansView.transformConfigView(mat);
|
|
spansView.setConfigView(spansView.configView.withY(beforeY));
|
|
}
|
|
if (uiFramesView) {
|
|
uiFramesView.transformConfigView(mat);
|
|
}
|
|
if (batteryChartView) {
|
|
batteryChartView.transformConfigView(mat);
|
|
}
|
|
if (cpuChartView) {
|
|
cpuChartView.transformConfigView(mat);
|
|
}
|
|
if (memoryChartView) {
|
|
memoryChartView.transformConfigView(mat);
|
|
}
|
|
}
|
|
|
|
canvasPoolManager.draw();
|
|
};
|
|
|
|
const onResetZoom = () => {
|
|
flamegraphView.resetConfigView(flamegraphCanvas);
|
|
if (spansView && spansCanvas) {
|
|
spansView.resetConfigView(spansCanvas);
|
|
}
|
|
if (uiFramesView && uiFramesCanvas) {
|
|
uiFramesView.resetConfigView(uiFramesCanvas);
|
|
}
|
|
if (batteryChartView && batteryChartCanvas) {
|
|
batteryChartView.resetConfigView(batteryChartCanvas);
|
|
}
|
|
if (cpuChartView && cpuChartCanvas) {
|
|
cpuChartView.resetConfigView(cpuChartCanvas);
|
|
}
|
|
if (memoryChartView && memoryChartCanvas) {
|
|
memoryChartView.resetConfigView(memoryChartCanvas);
|
|
}
|
|
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);
|
|
if (spansView) {
|
|
spansView.setConfigView(newConfigView.withHeight(spansView.configView.height));
|
|
}
|
|
if (uiFramesView) {
|
|
uiFramesView.setConfigView(
|
|
newConfigView.withHeight(uiFramesView.configView.height)
|
|
);
|
|
}
|
|
if (batteryChartView) {
|
|
batteryChartView.setConfigView(
|
|
newConfigView.withHeight(batteryChartView.configView.height)
|
|
);
|
|
}
|
|
if (cpuChartView) {
|
|
cpuChartView.setConfigView(
|
|
newConfigView.withHeight(cpuChartView.configView.height)
|
|
);
|
|
}
|
|
if (memoryChartView) {
|
|
memoryChartView.setConfigView(
|
|
newConfigView.withHeight(memoryChartView.configView.height)
|
|
);
|
|
}
|
|
canvasPoolManager.draw();
|
|
};
|
|
|
|
const onZoomIntoSpan = (span: SpanChartNode, strategy: 'min' | 'exact') => {
|
|
if (!spansView) {
|
|
return;
|
|
}
|
|
|
|
const newConfigView = computeConfigViewWithStrategy(
|
|
strategy,
|
|
spansView.configView,
|
|
new Rect(span.start, span.depth, span.end - span.start, 1)
|
|
).transformRect(spansView.configSpaceTransform);
|
|
|
|
spansView.setConfigView(newConfigView);
|
|
flamegraphView.setConfigView(
|
|
newConfigView
|
|
.withHeight(flamegraphView.configView.height)
|
|
.withY(flamegraphView.configView.y)
|
|
);
|
|
if (uiFramesView) {
|
|
uiFramesView.setConfigView(
|
|
newConfigView.withHeight(uiFramesView.configView.height)
|
|
);
|
|
}
|
|
if (batteryChartView) {
|
|
batteryChartView.setConfigView(
|
|
newConfigView.withHeight(batteryChartView.configView.height)
|
|
);
|
|
}
|
|
if (cpuChartView) {
|
|
cpuChartView.setConfigView(
|
|
newConfigView.withHeight(cpuChartView.configView.height)
|
|
);
|
|
}
|
|
if (memoryChartView) {
|
|
memoryChartView.setConfigView(
|
|
newConfigView.withHeight(memoryChartView.configView.height)
|
|
);
|
|
}
|
|
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);
|
|
scheduler.on('zoom at span', onZoomIntoSpan);
|
|
|
|
return () => {
|
|
scheduler.off('set config view', onConfigViewChange);
|
|
scheduler.off('transform config view', onTransformConfigView);
|
|
scheduler.off('reset zoom', onResetZoom);
|
|
scheduler.off('zoom at frame', onZoomIntoFrame);
|
|
scheduler.off('zoom at span', onZoomIntoSpan);
|
|
};
|
|
}, [
|
|
canvasPoolManager,
|
|
flamegraphCanvas,
|
|
flamegraphView,
|
|
scheduler,
|
|
spansCanvas,
|
|
spansView,
|
|
uiFramesCanvas,
|
|
uiFramesView,
|
|
cpuChartCanvas,
|
|
cpuChartView,
|
|
memoryChartCanvas,
|
|
memoryChartView,
|
|
batteryChartView,
|
|
batteryChartCanvas,
|
|
]);
|
|
|
|
const minimapCanvases = useMemo(() => {
|
|
return [flamegraphMiniMapCanvasRef, flamegraphMiniMapOverlayCanvasRef];
|
|
}, [flamegraphMiniMapCanvasRef, flamegraphMiniMapOverlayCanvasRef]);
|
|
|
|
useResizeCanvasObserver(
|
|
minimapCanvases,
|
|
canvasPoolManager,
|
|
flamegraphMiniMapCanvas,
|
|
null
|
|
);
|
|
|
|
const spansCanvases = useMemo(() => {
|
|
return [spansCanvasRef];
|
|
}, [spansCanvasRef]);
|
|
|
|
const spansCanvasBounds = useResizeCanvasObserver(
|
|
spansCanvases,
|
|
canvasPoolManager,
|
|
spansCanvas,
|
|
spansView
|
|
);
|
|
|
|
const uiFramesCanvases = useMemo(() => {
|
|
return [uiFramesCanvasRef];
|
|
}, [uiFramesCanvasRef]);
|
|
|
|
const uiFramesCanvasBounds = useResizeCanvasObserver(
|
|
uiFramesCanvases,
|
|
canvasPoolManager,
|
|
uiFramesCanvas,
|
|
uiFramesView
|
|
);
|
|
|
|
const batteryChartCanvases = useMemo(() => {
|
|
return [batteryChartCanvasRef];
|
|
}, [batteryChartCanvasRef]);
|
|
|
|
const batteryChartCanvasBounds = useResizeCanvasObserver(
|
|
batteryChartCanvases,
|
|
canvasPoolManager,
|
|
batteryChartCanvas,
|
|
batteryChartView
|
|
);
|
|
|
|
const cpuChartCanvases = useMemo(() => {
|
|
return [cpuChartCanvasRef];
|
|
}, [cpuChartCanvasRef]);
|
|
|
|
const cpuChartCanvasBounds = useResizeCanvasObserver(
|
|
cpuChartCanvases,
|
|
canvasPoolManager,
|
|
cpuChartCanvas,
|
|
cpuChartView
|
|
);
|
|
|
|
const memoryChartCanvases = useMemo(() => {
|
|
return [memoryChartCanvasRef];
|
|
}, [memoryChartCanvasRef]);
|
|
const memoryChartCanvasBounds = useResizeCanvasObserver(
|
|
memoryChartCanvases,
|
|
canvasPoolManager,
|
|
memoryChartCanvas,
|
|
memoryChartView
|
|
);
|
|
|
|
const flamegraphCanvases = useMemo(() => {
|
|
return [flamegraphCanvasRef, flamegraphOverlayCanvasRef];
|
|
}, [flamegraphCanvasRef, flamegraphOverlayCanvasRef]);
|
|
|
|
const flamegraphCanvasBounds = useResizeCanvasObserver(
|
|
flamegraphCanvases,
|
|
canvasPoolManager,
|
|
flamegraphCanvas,
|
|
flamegraphView
|
|
);
|
|
|
|
const flamegraphRenderer = useMemo(() => {
|
|
if (!flamegraphCanvasRef) {
|
|
return null;
|
|
}
|
|
|
|
const renderer = initializeFlamegraphRenderer(
|
|
[FlamegraphRendererWebGL, FlamegraphRenderer2D],
|
|
[
|
|
flamegraphCanvasRef,
|
|
flamegraph,
|
|
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, flamegraph, flamegraphCanvasRef, flamegraphTheme]);
|
|
|
|
const getFrameColor = useCallback(
|
|
(frame: FlamegraphFrame) => {
|
|
if (!flamegraphRenderer) {
|
|
return '';
|
|
}
|
|
return formatColorForFrame(frame, flamegraphRenderer);
|
|
},
|
|
[flamegraphRenderer]
|
|
);
|
|
|
|
const physicalToConfig =
|
|
flamegraphView && flamegraphCanvas
|
|
? mat3.invert(
|
|
mat3.create(),
|
|
flamegraphView.fromConfigView(flamegraphCanvas.physicalSpace)
|
|
)
|
|
: mat3.create();
|
|
|
|
const configSpacePixel = new Rect(0, 0, 1, 1).transformRect(physicalToConfig);
|
|
|
|
// Register keyboard navigation
|
|
useViewKeyboardNavigation(flamegraphView, canvasPoolManager, configSpacePixel.width);
|
|
|
|
// referenceNode is passed down to the flamegraphdrawer 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]);
|
|
|
|
const onSortingChange: FlamegraphViewSelectMenuProps['onSortingChange'] = useCallback(
|
|
newSorting => {
|
|
dispatch({type: 'set sorting', payload: newSorting});
|
|
},
|
|
[dispatch]
|
|
);
|
|
|
|
const onViewChange: FlamegraphViewSelectMenuProps['onViewChange'] = useCallback(
|
|
newView => {
|
|
dispatch({type: 'set view', payload: newView});
|
|
},
|
|
[dispatch]
|
|
);
|
|
|
|
const onThreadIdChange: FlamegraphThreadSelectorProps['onThreadIdChange'] = useCallback(
|
|
newThreadId => {
|
|
dispatch({type: 'set thread id', payload: newThreadId});
|
|
},
|
|
[dispatch]
|
|
);
|
|
|
|
const onImport = useCallback(
|
|
(p: Profiling.ProfileInput) => {
|
|
setProfiles({type: 'resolved', data: p});
|
|
},
|
|
[setProfiles]
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (defined(profiles.threadId)) {
|
|
return;
|
|
}
|
|
const threadID =
|
|
typeof profileGroup.activeProfileIndex === 'number'
|
|
? profileGroup.profiles[profileGroup.activeProfileIndex]?.threadId
|
|
: null;
|
|
|
|
// if the state has a highlight frame specified, then we want to jump to the
|
|
// thread containing it, highlight the frames on the thread, and change the
|
|
// view so it's obvious where it is
|
|
if (highlightFrames) {
|
|
const candidate = profileGroup.profiles.reduce<FlamegraphCandidate | null>(
|
|
(prevCandidate, currentProfile) => {
|
|
// if the previous candidate is the active thread, it always takes priority
|
|
if (prevCandidate?.isActiveThread) {
|
|
return prevCandidate;
|
|
}
|
|
|
|
const graph = new FlamegraphModel(currentProfile, {
|
|
inverted: false,
|
|
sort: sorting,
|
|
configSpace: undefined,
|
|
});
|
|
|
|
const frame = findLongestMatchingFrame(graph, highlightFrames);
|
|
|
|
if (!defined(frame)) {
|
|
return prevCandidate;
|
|
}
|
|
|
|
const newScore = frame.node.totalWeight || 0;
|
|
const oldScore = prevCandidate?.frame?.node?.totalWeight || 0;
|
|
|
|
// if we find the frame on the active thread, it always takes priority
|
|
if (newScore > 0 && currentProfile.threadId === threadID) {
|
|
return {
|
|
frame,
|
|
threadId: currentProfile.threadId,
|
|
isActiveThread: true,
|
|
};
|
|
}
|
|
|
|
return newScore <= oldScore
|
|
? prevCandidate
|
|
: {
|
|
frame,
|
|
threadId: currentProfile.threadId,
|
|
};
|
|
},
|
|
null
|
|
);
|
|
|
|
if (defined(candidate)) {
|
|
dispatch({
|
|
type: 'set thread id',
|
|
payload: candidate.threadId,
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
|
|
// fall back case, when we finally load the active profile index from the profile,
|
|
// make sure we update the thread id so that it is show first
|
|
if (defined(threadID)) {
|
|
dispatch({
|
|
type: 'set thread id',
|
|
payload: threadID,
|
|
});
|
|
}
|
|
}, [profileGroup, highlightFrames, profiles.threadId, dispatch, sorting]);
|
|
|
|
// A bit unfortunate for now, but the search component accepts a list
|
|
// of model to search through. This will become useful as we build
|
|
// differential flamecharts or start comparing different profiles/charts
|
|
const flamegraphs = useMemo(() => [flamegraph], [flamegraph]);
|
|
const spans = useMemo(() => (spanChart ? [spanChart] : []), [spanChart]);
|
|
|
|
return (
|
|
<Fragment>
|
|
<FlamegraphToolbar>
|
|
<FlamegraphThreadSelector
|
|
profileGroup={profileGroup}
|
|
threadId={threadId}
|
|
onThreadIdChange={onThreadIdChange}
|
|
/>
|
|
<FlamegraphViewSelectMenu
|
|
view={view}
|
|
sorting={sorting}
|
|
onSortingChange={onSortingChange}
|
|
onViewChange={onViewChange}
|
|
/>
|
|
<FlamegraphSearch
|
|
spans={spans}
|
|
flamegraphs={flamegraphs}
|
|
canvasPoolManager={canvasPoolManager}
|
|
/>
|
|
<FlamegraphOptionsMenu canvasPoolManager={canvasPoolManager} />
|
|
</FlamegraphToolbar>
|
|
|
|
<FlamegraphLayout
|
|
uiFrames={
|
|
hasUIFrames ? (
|
|
<FlamegraphUIFrames
|
|
canvasBounds={uiFramesCanvasBounds}
|
|
canvasPoolManager={canvasPoolManager}
|
|
setUIFramesCanvasRef={setUIFramesCanvasRef}
|
|
uiFramesCanvasRef={uiFramesCanvasRef}
|
|
uiFramesCanvas={uiFramesCanvas}
|
|
uiFramesView={uiFramesView}
|
|
uiFrames={uiFrames}
|
|
/>
|
|
) : null
|
|
}
|
|
batteryChart={
|
|
hasBatteryChart ? (
|
|
<FlamegraphChart
|
|
chartCanvasRef={batteryChartCanvasRef}
|
|
chartCanvas={batteryChartCanvas}
|
|
setChartCanvasRef={setBatteryChartCanvasRef}
|
|
canvasBounds={batteryChartCanvasBounds}
|
|
chartView={batteryChartView}
|
|
canvasPoolManager={canvasPoolManager}
|
|
chart={batteryChart}
|
|
noMeasurementMessage={
|
|
profileGroup.metadata.platform === 'cocoa'
|
|
? t(
|
|
'Upgrade to version 8.9.6 of sentry-cocoa SDK to enable battery usage collection'
|
|
)
|
|
: ''
|
|
}
|
|
/>
|
|
) : null
|
|
}
|
|
memoryChart={
|
|
hasMemoryChart ? (
|
|
<FlamegraphChart
|
|
chartCanvasRef={memoryChartCanvasRef}
|
|
chartCanvas={memoryChartCanvas}
|
|
setChartCanvasRef={setMemoryChartCanvasRef}
|
|
canvasBounds={memoryChartCanvasBounds}
|
|
chartView={memoryChartView}
|
|
canvasPoolManager={canvasPoolManager}
|
|
chart={memoryChart}
|
|
noMeasurementMessage={
|
|
profileGroup.metadata.platform === 'cocoa'
|
|
? t(
|
|
'Upgrade to version 8.9.6 of sentry-cocoa SDK to enable memory usage collection'
|
|
)
|
|
: profileGroup.metadata.platform === 'node'
|
|
? t(
|
|
'Upgrade to version 1.2.0 of @sentry/profiling-node to enable memory usage collection'
|
|
)
|
|
: ''
|
|
}
|
|
/>
|
|
) : null
|
|
}
|
|
cpuChart={
|
|
hasCPUChart ? (
|
|
<FlamegraphChart
|
|
chartCanvasRef={cpuChartCanvasRef}
|
|
chartCanvas={cpuChartCanvas}
|
|
setChartCanvasRef={setCpuChartCanvasRef}
|
|
canvasBounds={cpuChartCanvasBounds}
|
|
chartView={cpuChartView}
|
|
canvasPoolManager={canvasPoolManager}
|
|
chart={CPUChart}
|
|
noMeasurementMessage={
|
|
profileGroup.metadata.platform === 'cocoa'
|
|
? t(
|
|
'Upgrade to version 8.9.6 of sentry-cocoa SDK to enable CPU usage collection'
|
|
)
|
|
: profileGroup.metadata.platform === 'node'
|
|
? t(
|
|
'Upgrade to version 1.2.0 of @sentry/profiling-node to enable CPU usage collection'
|
|
)
|
|
: ''
|
|
}
|
|
/>
|
|
) : null
|
|
}
|
|
spansTreeDepth={spanChart?.depth}
|
|
spans={
|
|
spanChart ? (
|
|
<FlamegraphSpans
|
|
canvasBounds={spansCanvasBounds}
|
|
spanChart={spanChart}
|
|
spansCanvas={spansCanvas}
|
|
spansCanvasRef={spansCanvasRef}
|
|
setSpansCanvasRef={setSpansCanvasRef}
|
|
canvasPoolManager={canvasPoolManager}
|
|
spansView={spansView}
|
|
/>
|
|
) : null
|
|
}
|
|
minimap={
|
|
<FlamegraphZoomViewMinimap
|
|
canvasPoolManager={canvasPoolManager}
|
|
flamegraph={flamegraph}
|
|
flamegraphMiniMapCanvas={flamegraphMiniMapCanvas}
|
|
flamegraphMiniMapCanvasRef={flamegraphMiniMapCanvasRef}
|
|
flamegraphMiniMapOverlayCanvasRef={flamegraphMiniMapOverlayCanvasRef}
|
|
flamegraphMiniMapView={flamegraphView}
|
|
setFlamegraphMiniMapCanvasRef={setFlamegraphMiniMapCanvasRef}
|
|
setFlamegraphMiniMapOverlayCanvasRef={setFlamegraphMiniMapOverlayCanvasRef}
|
|
/>
|
|
}
|
|
flamegraph={
|
|
<ProfileDragDropImport onImport={onImport}>
|
|
<FlamegraphWarnings flamegraph={flamegraph} />
|
|
<FlamegraphZoomView
|
|
profileGroup={profileGroup}
|
|
canvasBounds={flamegraphCanvasBounds}
|
|
canvasPoolManager={canvasPoolManager}
|
|
flamegraph={flamegraph}
|
|
flamegraphRenderer={flamegraphRenderer}
|
|
flamegraphCanvas={flamegraphCanvas}
|
|
flamegraphCanvasRef={flamegraphCanvasRef}
|
|
flamegraphOverlayCanvasRef={flamegraphOverlayCanvasRef}
|
|
flamegraphView={flamegraphView}
|
|
setFlamegraphCanvasRef={setFlamegraphCanvasRef}
|
|
setFlamegraphOverlayCanvasRef={setFlamegraphOverlayCanvasRef}
|
|
/>
|
|
</ProfileDragDropImport>
|
|
}
|
|
flamegraphDrawer={
|
|
<FlamegraphDrawer
|
|
profileGroup={profileGroup}
|
|
getFrameColor={getFrameColor}
|
|
referenceNode={referenceNode}
|
|
rootNodes={rootNodes}
|
|
flamegraph={flamegraph}
|
|
formatDuration={flamegraph ? flamegraph.formatter : noopFormatDuration}
|
|
canvasPoolManager={canvasPoolManager}
|
|
canvasScheduler={scheduler}
|
|
/>
|
|
}
|
|
/>
|
|
</Fragment>
|
|
);
|
|
}
|
|
|
|
export {Flamegraph};
|