flamegraph.tsx 50 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498
  1. import type {ReactElement} from 'react';
  2. import {
  3. Fragment,
  4. useCallback,
  5. useEffect,
  6. useLayoutEffect,
  7. useMemo,
  8. useState,
  9. } from 'react';
  10. import * as Sentry from '@sentry/react';
  11. import {mat3, vec2} from 'gl-matrix';
  12. import {addErrorMessage} from 'sentry/actionCreators/indicator';
  13. import {ProfileDragDropImport} from 'sentry/components/profiling/flamegraph/flamegraphOverlays/profileDragDropImport';
  14. import {FlamegraphOptionsMenu} from 'sentry/components/profiling/flamegraph/flamegraphToolbar/flamegraphOptionsMenu';
  15. import {FlamegraphSearch} from 'sentry/components/profiling/flamegraph/flamegraphToolbar/flamegraphSearch';
  16. import type {FlamegraphThreadSelectorProps} from 'sentry/components/profiling/flamegraph/flamegraphToolbar/flamegraphThreadSelector';
  17. import {FlamegraphThreadSelector} from 'sentry/components/profiling/flamegraph/flamegraphToolbar/flamegraphThreadSelector';
  18. import {FlamegraphToolbar} from 'sentry/components/profiling/flamegraph/flamegraphToolbar/flamegraphToolbar';
  19. import type {FlamegraphViewSelectMenuProps} from 'sentry/components/profiling/flamegraph/flamegraphToolbar/flamegraphViewSelectMenu';
  20. import {FlamegraphViewSelectMenu} from 'sentry/components/profiling/flamegraph/flamegraphToolbar/flamegraphViewSelectMenu';
  21. import {FlamegraphZoomView} from 'sentry/components/profiling/flamegraph/flamegraphZoomView';
  22. import {FlamegraphZoomViewMinimap} from 'sentry/components/profiling/flamegraph/flamegraphZoomViewMinimap';
  23. import {t} from 'sentry/locale';
  24. import type {RequestState} from 'sentry/types/core';
  25. import type {EntrySpans, EventTransaction} from 'sentry/types/event';
  26. import {EntryType} from 'sentry/types/event';
  27. import {defined} from 'sentry/utils';
  28. import {
  29. CanvasPoolManager,
  30. useCanvasScheduler,
  31. } from 'sentry/utils/profiling/canvasScheduler';
  32. import {CanvasView} from 'sentry/utils/profiling/canvasView';
  33. import {Flamegraph as FlamegraphModel} from 'sentry/utils/profiling/flamegraph';
  34. import type {FlamegraphSearch as FlamegraphSearchType} from 'sentry/utils/profiling/flamegraph/flamegraphStateProvider/reducers/flamegraphSearch';
  35. import {useFlamegraphPreferences} from 'sentry/utils/profiling/flamegraph/hooks/useFlamegraphPreferences';
  36. import {useFlamegraphProfiles} from 'sentry/utils/profiling/flamegraph/hooks/useFlamegraphProfiles';
  37. import {useFlamegraphSearch} from 'sentry/utils/profiling/flamegraph/hooks/useFlamegraphSearch';
  38. import {useDispatchFlamegraphState} from 'sentry/utils/profiling/flamegraph/hooks/useFlamegraphState';
  39. import {useFlamegraphZoomPosition} from 'sentry/utils/profiling/flamegraph/hooks/useFlamegraphZoomPosition';
  40. import {useFlamegraphTheme} from 'sentry/utils/profiling/flamegraph/useFlamegraphTheme';
  41. import {FlamegraphCanvas} from 'sentry/utils/profiling/flamegraphCanvas';
  42. import type {ProfileSeriesMeasurement} from 'sentry/utils/profiling/flamegraphChart';
  43. import {FlamegraphChart as FlamegraphChartModel} from 'sentry/utils/profiling/flamegraphChart';
  44. import type {FlamegraphFrame} from 'sentry/utils/profiling/flamegraphFrame';
  45. import {
  46. computeConfigViewWithStrategy,
  47. computeMinZoomConfigViewForFrames,
  48. formatColorForFrame,
  49. initializeFlamegraphRenderer,
  50. useResizeCanvasObserver,
  51. } from 'sentry/utils/profiling/gl/utils';
  52. import type {ProfileGroup} from 'sentry/utils/profiling/profile/importProfile';
  53. import {FlamegraphRenderer2D} from 'sentry/utils/profiling/renderers/flamegraphRenderer2D';
  54. import {FlamegraphRendererWebGL} from 'sentry/utils/profiling/renderers/flamegraphRendererWebGL';
  55. import type {SpanChartNode} from 'sentry/utils/profiling/spanChart';
  56. import {SpanChart} from 'sentry/utils/profiling/spanChart';
  57. import {SpanTree} from 'sentry/utils/profiling/spanTree';
  58. import {Rect} from 'sentry/utils/profiling/speedscope';
  59. import {UIFrames} from 'sentry/utils/profiling/uiFrames';
  60. import type {ProfilingFormatterUnit} from 'sentry/utils/profiling/units/units';
  61. import {formatTo, fromNanoJoulesToWatts} from 'sentry/utils/profiling/units/units';
  62. import {useDevicePixelRatio} from 'sentry/utils/useDevicePixelRatio';
  63. import {useMemoWithPrevious} from 'sentry/utils/useMemoWithPrevious';
  64. import {useProfileGroup} from 'sentry/views/profiling/profileGroupProvider';
  65. import {
  66. useProfileTransaction,
  67. useSetProfiles,
  68. } from 'sentry/views/profiling/profilesProvider';
  69. import {FlamegraphDrawer} from './flamegraphDrawer/flamegraphDrawer';
  70. import {FlamegraphWarnings} from './flamegraphOverlays/FlamegraphWarnings';
  71. import {useViewKeyboardNavigation} from './interactions/useViewKeyboardNavigation';
  72. import {FlamegraphChart} from './flamegraphChart';
  73. import {FlamegraphLayout} from './flamegraphLayout';
  74. import {FlamegraphSpans} from './flamegraphSpans';
  75. import {FlamegraphUIFrames} from './flamegraphUIFrames';
  76. function getMaxConfigSpace(
  77. profileGroup: ProfileGroup,
  78. transaction: EventTransaction | null,
  79. unit: ProfilingFormatterUnit | string
  80. ): Rect {
  81. // We have a transaction, so we should do our best to align the profile
  82. // with the transaction's timeline.
  83. const maxProfileDuration = Math.max(...profileGroup.profiles.map(p => p.duration));
  84. if (transaction) {
  85. // TODO: Adjust the alignment based on the profile's timestamp if it does
  86. // not match the transaction's start timestamp
  87. const transactionDuration = transaction.endTimestamp - transaction.startTimestamp;
  88. // On most platforms, profile duration < transaction duration, however
  89. // there is one beloved platform where that is not true; android.
  90. // Hence, we should take the max of the two to ensure both the transaction
  91. // and profile are fully visible to the user.
  92. const duration = Math.max(
  93. formatTo(transactionDuration, 'seconds', unit),
  94. maxProfileDuration
  95. );
  96. return new Rect(0, 0, duration, 0);
  97. }
  98. // No transaction was found, so best we can do is align it to the starting
  99. // position of the profiles - find the max of profile durations
  100. return new Rect(0, 0, maxProfileDuration, 0);
  101. }
  102. function collectAllSpanEntriesFromTransaction(
  103. transaction: EventTransaction
  104. ): EntrySpans['data'] {
  105. if (!transaction.entries.length) {
  106. return [];
  107. }
  108. const spans = transaction.entries.filter(
  109. (e): e is EntrySpans => e.type === EntryType.SPANS
  110. );
  111. let allSpans: EntrySpans['data'] = [];
  112. for (const span of spans) {
  113. allSpans = allSpans.concat(span.data);
  114. }
  115. return allSpans;
  116. }
  117. type FlamegraphCandidate = {
  118. frame: FlamegraphFrame;
  119. threadId: number;
  120. isActiveThread?: boolean; // this is the thread referred to by the active profile index
  121. };
  122. function findLongestMatchingFrame(
  123. flamegraph: FlamegraphModel,
  124. focusFrame: FlamegraphSearchType['highlightFrames']
  125. ): FlamegraphFrame | null {
  126. if (focusFrame === null) {
  127. return null;
  128. }
  129. let longestFrame: FlamegraphFrame | null = null;
  130. const frames: FlamegraphFrame[] = [...flamegraph.root.children];
  131. while (frames.length > 0) {
  132. const frame = frames.pop()!;
  133. if (
  134. focusFrame.name === frame.frame.name &&
  135. // the image name on a frame is optional treat it the same as the empty string
  136. (focusFrame.package === (frame.frame.package || '') ||
  137. focusFrame.package === (frame.frame.module || '')) &&
  138. (longestFrame?.node?.totalWeight || 0) < frame.node.totalWeight
  139. ) {
  140. longestFrame = frame;
  141. }
  142. for (let i = 0; i < frame.children.length; i++) {
  143. frames.push(frame.children[i]);
  144. }
  145. }
  146. return longestFrame;
  147. }
  148. function computeProfileOffset(
  149. profileStart: string | undefined,
  150. flamegraph: FlamegraphModel,
  151. transaction: RequestState<EventTransaction | null>
  152. ): number {
  153. let offset = flamegraph.profile.startedAt;
  154. const transactionStart =
  155. transaction.type === 'resolved' ? transaction.data?.startTimestamp ?? null : null;
  156. if (defined(transactionStart) && defined(profileStart)) {
  157. const profileStartTimestamp = new Date(profileStart).getTime() / 1e3;
  158. const profileOffset = profileStartTimestamp - transactionStart;
  159. offset += formatTo(profileOffset, 'second', flamegraph.profile.unit);
  160. }
  161. return offset;
  162. }
  163. const LOADING_OR_FALLBACK_FLAMEGRAPH = FlamegraphModel.Empty();
  164. const LOADING_OR_FALLBACK_SPAN_TREE = SpanTree.Empty;
  165. const LOADING_OR_FALLBACK_UIFRAMES = UIFrames.Empty;
  166. const LOADING_OR_FALLBACK_BATTERY_CHART = FlamegraphChartModel.Empty;
  167. const LOADING_OR_FALLBACK_CPU_CHART = FlamegraphChartModel.Empty;
  168. const LOADING_OR_FALLBACK_MEMORY_CHART = FlamegraphChartModel.Empty;
  169. const noopFormatDuration = () => '';
  170. function Flamegraph(): ReactElement {
  171. const devicePixelRatio = useDevicePixelRatio();
  172. const profiledTransaction = useProfileTransaction();
  173. const dispatch = useDispatchFlamegraphState();
  174. const setProfiles = useSetProfiles();
  175. const profileGroup = useProfileGroup();
  176. const flamegraphTheme = useFlamegraphTheme();
  177. const position = useFlamegraphZoomPosition();
  178. const profiles = useFlamegraphProfiles();
  179. const {colorCoding, sorting, view} = useFlamegraphPreferences();
  180. const {highlightFrames} = useFlamegraphSearch();
  181. const {threadId, selectedRoot} = useFlamegraphProfiles();
  182. const [flamegraphCanvasRef, setFlamegraphCanvasRef] =
  183. useState<HTMLCanvasElement | null>(null);
  184. const [flamegraphOverlayCanvasRef, setFlamegraphOverlayCanvasRef] =
  185. useState<HTMLCanvasElement | null>(null);
  186. const [flamegraphMiniMapCanvasRef, setFlamegraphMiniMapCanvasRef] =
  187. useState<HTMLCanvasElement | null>(null);
  188. const [flamegraphMiniMapOverlayCanvasRef, setFlamegraphMiniMapOverlayCanvasRef] =
  189. useState<HTMLCanvasElement | null>(null);
  190. const [spansCanvasRef, setSpansCanvasRef] = useState<HTMLCanvasElement | null>(null);
  191. const [uiFramesCanvasRef, setUIFramesCanvasRef] = useState<HTMLCanvasElement | null>(
  192. null
  193. );
  194. const [batteryChartCanvasRef, setBatteryChartCanvasRef] =
  195. useState<HTMLCanvasElement | null>(null);
  196. const [cpuChartCanvasRef, setCpuChartCanvasRef] = useState<HTMLCanvasElement | null>(
  197. null
  198. );
  199. const [memoryChartCanvasRef, setMemoryChartCanvasRef] =
  200. useState<HTMLCanvasElement | null>(null);
  201. const canvasPoolManager = useMemo(() => new CanvasPoolManager(), []);
  202. const scheduler = useCanvasScheduler(canvasPoolManager);
  203. const hasUIFrames = useMemo(() => {
  204. const platform = profileGroup.metadata.platform;
  205. return platform === 'cocoa' || platform === 'android';
  206. }, [profileGroup.metadata.platform]);
  207. const hasBatteryChart = useMemo(() => {
  208. const platform = profileGroup.metadata.platform;
  209. return platform === 'cocoa' || profileGroup.measurements?.cpu_energy_usage;
  210. }, [profileGroup.metadata.platform, profileGroup.measurements]);
  211. const hasCPUChart = useMemo(() => {
  212. const platform = profileGroup.metadata.platform;
  213. return (
  214. platform === 'cocoa' ||
  215. platform === 'android' ||
  216. platform === 'node' ||
  217. profileGroup.measurements?.cpu_usage
  218. );
  219. }, [profileGroup.metadata.platform, profileGroup.measurements]);
  220. const hasMemoryChart = useMemo(() => {
  221. const platform = profileGroup.metadata.platform;
  222. return (
  223. platform === 'cocoa' ||
  224. platform === 'android' ||
  225. platform === 'node' ||
  226. profileGroup.measurements?.memory_footprint
  227. );
  228. }, [profileGroup.metadata.platform, profileGroup.measurements]);
  229. const profile = useMemo(() => {
  230. return profileGroup.profiles.find(p => p.threadId === threadId);
  231. }, [profileGroup, threadId]);
  232. const spanTree: SpanTree = useMemo(() => {
  233. if (profiledTransaction.type === 'resolved' && profiledTransaction.data) {
  234. return new SpanTree(
  235. profiledTransaction.data,
  236. collectAllSpanEntriesFromTransaction(profiledTransaction.data)
  237. );
  238. }
  239. return LOADING_OR_FALLBACK_SPAN_TREE;
  240. }, [profiledTransaction]);
  241. const spanChart = useMemo(() => {
  242. if (!profile) {
  243. return null;
  244. }
  245. return new SpanChart(spanTree, {
  246. unit: profile.unit,
  247. configSpace: getMaxConfigSpace(
  248. profileGroup,
  249. profiledTransaction.type === 'resolved' ? profiledTransaction.data : null,
  250. profile.unit
  251. ),
  252. });
  253. }, [spanTree, profile, profileGroup, profiledTransaction]);
  254. const flamegraph = useMemo(() => {
  255. if (typeof threadId !== 'number') {
  256. return LOADING_OR_FALLBACK_FLAMEGRAPH;
  257. }
  258. // This could happen if threadId was initialized from query string, but for some
  259. // reason the profile was removed from the list of profiles.
  260. if (!profile) {
  261. return LOADING_OR_FALLBACK_FLAMEGRAPH;
  262. }
  263. // Wait for the transaction to finish loading, regardless of the results.
  264. // Otherwise, the rendered profile will probably shift once the transaction loads.
  265. if (
  266. profiledTransaction.type === 'loading' ||
  267. profiledTransaction.type === 'initial'
  268. ) {
  269. return LOADING_OR_FALLBACK_FLAMEGRAPH;
  270. }
  271. const span = Sentry.withScope(scope => {
  272. scope.setTag('sorting', sorting.split(' ').join('_'));
  273. scope.setTag('view', view.split(' ').join('_'));
  274. return Sentry.startInactiveSpan({
  275. op: 'import',
  276. name: 'flamegraph.constructor',
  277. forceTransaction: true,
  278. });
  279. });
  280. const newFlamegraph = new FlamegraphModel(profile, {
  281. inverted: view === 'bottom up',
  282. sort: sorting,
  283. configSpace: getMaxConfigSpace(
  284. profileGroup,
  285. profiledTransaction.type === 'resolved' ? profiledTransaction.data : null,
  286. profile.unit
  287. ),
  288. });
  289. span?.end();
  290. return newFlamegraph;
  291. }, [profile, profileGroup, profiledTransaction, sorting, threadId, view]);
  292. const profileOffsetFromTransaction = useMemo(
  293. () =>
  294. computeProfileOffset(
  295. profileGroup.metadata.timestamp,
  296. flamegraph,
  297. profiledTransaction
  298. ),
  299. [flamegraph, profiledTransaction, profileGroup.metadata.timestamp]
  300. );
  301. const uiFrames = useMemo(() => {
  302. if (!hasUIFrames) {
  303. return LOADING_OR_FALLBACK_UIFRAMES;
  304. }
  305. return new UIFrames(
  306. {
  307. slow: profileGroup.measurements?.slow_frame_renders,
  308. frozen: profileGroup.measurements?.frozen_frame_renders,
  309. },
  310. {unit: flamegraph.profile.unit},
  311. flamegraph.configSpace.withHeight(1)
  312. );
  313. }, [
  314. profileGroup.measurements,
  315. flamegraph.profile.unit,
  316. flamegraph.configSpace,
  317. hasUIFrames,
  318. ]);
  319. const batteryChart = useMemo(() => {
  320. if (!hasCPUChart) {
  321. return LOADING_OR_FALLBACK_BATTERY_CHART;
  322. }
  323. const measures: ProfileSeriesMeasurement[] = [];
  324. for (const key in profileGroup.measurements) {
  325. if (key === 'cpu_energy_usage') {
  326. measures.push({
  327. ...profileGroup.measurements[key]!,
  328. values: profileGroup.measurements[key]!.values.map(v => {
  329. return {
  330. elapsed_since_start_ns: v.elapsed_since_start_ns,
  331. value: fromNanoJoulesToWatts(v.value, 0.1),
  332. };
  333. }),
  334. // some versions of cocoa send byte so we need to correct it to watt
  335. unit: 'watt',
  336. name: 'CPU energy usage',
  337. });
  338. }
  339. }
  340. return new FlamegraphChartModel(
  341. Rect.From(flamegraph.configSpace),
  342. measures.length > 0 ? measures : [],
  343. flamegraphTheme.COLORS.BATTERY_CHART_COLORS
  344. );
  345. }, [profileGroup.measurements, flamegraph.configSpace, flamegraphTheme, hasCPUChart]);
  346. const CPUChart = useMemo(() => {
  347. if (!hasCPUChart) {
  348. return LOADING_OR_FALLBACK_CPU_CHART;
  349. }
  350. const measures: ProfileSeriesMeasurement[] = [];
  351. for (const key in profileGroup.measurements) {
  352. if (key.startsWith('cpu_usage')) {
  353. const name =
  354. key === 'cpu_usage'
  355. ? 'Average CPU usage'
  356. : `CPU Core ${key.replace('cpu_usage_', '')}`;
  357. measures.push({...profileGroup.measurements[key]!, name});
  358. }
  359. }
  360. return new FlamegraphChartModel(
  361. Rect.From(flamegraph.configSpace),
  362. measures.length > 0 ? measures : [],
  363. flamegraphTheme.COLORS.CPU_CHART_COLORS
  364. );
  365. }, [profileGroup.measurements, flamegraph.configSpace, flamegraphTheme, hasCPUChart]);
  366. const memoryChart = useMemo(() => {
  367. if (!hasMemoryChart) {
  368. return LOADING_OR_FALLBACK_MEMORY_CHART;
  369. }
  370. const measures: ProfileSeriesMeasurement[] = [];
  371. const memory_footprint = profileGroup.measurements?.memory_footprint;
  372. if (memory_footprint) {
  373. measures.push({
  374. ...memory_footprint!,
  375. name: 'Heap Usage',
  376. });
  377. }
  378. const native_memory_footprint = profileGroup.measurements?.memory_native_footprint;
  379. if (native_memory_footprint) {
  380. measures.push({
  381. ...native_memory_footprint!,
  382. name: 'Native Heap Usage',
  383. });
  384. }
  385. return new FlamegraphChartModel(
  386. Rect.From(flamegraph.configSpace),
  387. measures.length > 0 ? measures : [],
  388. flamegraphTheme.COLORS.MEMORY_CHART_COLORS,
  389. {type: 'area'}
  390. );
  391. }, [
  392. profileGroup.measurements,
  393. flamegraph.configSpace,
  394. flamegraphTheme,
  395. hasMemoryChart,
  396. ]);
  397. const flamegraphCanvas = useMemo(() => {
  398. if (!flamegraphCanvasRef) {
  399. return null;
  400. }
  401. return new FlamegraphCanvas(
  402. flamegraphCanvasRef,
  403. vec2.fromValues(0, flamegraphTheme.SIZES.TIMELINE_HEIGHT * devicePixelRatio)
  404. );
  405. }, [devicePixelRatio, flamegraphCanvasRef, flamegraphTheme]);
  406. const flamegraphMiniMapCanvas = useMemo(() => {
  407. if (!flamegraphMiniMapCanvasRef) {
  408. return null;
  409. }
  410. return new FlamegraphCanvas(flamegraphMiniMapCanvasRef, vec2.fromValues(0, 0));
  411. }, [flamegraphMiniMapCanvasRef]);
  412. const spansCanvas = useMemo(() => {
  413. if (!spansCanvasRef) {
  414. return null;
  415. }
  416. return new FlamegraphCanvas(spansCanvasRef, vec2.fromValues(0, 0));
  417. }, [spansCanvasRef]);
  418. const uiFramesCanvas = useMemo(() => {
  419. if (!uiFramesCanvasRef) {
  420. return null;
  421. }
  422. return new FlamegraphCanvas(uiFramesCanvasRef, vec2.fromValues(0, 0));
  423. }, [uiFramesCanvasRef]);
  424. const batteryChartCanvas = useMemo(() => {
  425. if (!batteryChartCanvasRef) {
  426. return null;
  427. }
  428. return new FlamegraphCanvas(batteryChartCanvasRef, vec2.fromValues(0, 0));
  429. }, [batteryChartCanvasRef]);
  430. const cpuChartCanvas = useMemo(() => {
  431. if (!cpuChartCanvasRef) {
  432. return null;
  433. }
  434. return new FlamegraphCanvas(cpuChartCanvasRef, vec2.fromValues(0, 0));
  435. }, [cpuChartCanvasRef]);
  436. const memoryChartCanvas = useMemo(() => {
  437. if (!memoryChartCanvasRef) {
  438. return null;
  439. }
  440. return new FlamegraphCanvas(memoryChartCanvasRef, vec2.fromValues(0, 0));
  441. }, [memoryChartCanvasRef]);
  442. const flamegraphView = useMemoWithPrevious<CanvasView<FlamegraphModel> | null>(
  443. previousView => {
  444. if (!flamegraphCanvas) {
  445. return null;
  446. }
  447. const newView = new CanvasView({
  448. canvas: flamegraphCanvas,
  449. model: flamegraph,
  450. options: {
  451. inverted: flamegraph.inverted,
  452. minWidth: flamegraph.profile.minFrameDuration,
  453. barHeight: flamegraphTheme.SIZES.BAR_HEIGHT,
  454. depthOffset: flamegraphTheme.SIZES.FLAMEGRAPH_DEPTH_OFFSET,
  455. configSpaceTransform: new Rect(profileOffsetFromTransaction, 0, 0, 0),
  456. },
  457. });
  458. if (
  459. // if the profile or the config space of the flamegraph has changed, we do not
  460. // want to persist the config view. This is to avoid a case where the new config space
  461. // is larger than the previous one, meaning the new view could now be zoomed in even
  462. // though the user did not fire any zoom events.
  463. previousView?.model.profile === newView.model.profile &&
  464. previousView.configSpace.equals(newView.configSpace)
  465. ) {
  466. if (
  467. // if we're still looking at the same profile but only a preference other than
  468. // left heavy has changed, we do want to persist the config view
  469. previousView.model.sort === 'left heavy' &&
  470. newView.model.sort === 'left heavy'
  471. ) {
  472. newView.setConfigView(
  473. previousView.configView.withHeight(newView.configView.height)
  474. );
  475. }
  476. }
  477. if (defined(highlightFrames)) {
  478. let frames = flamegraph.findAllMatchingFrames(
  479. highlightFrames.name,
  480. highlightFrames.package
  481. );
  482. if (
  483. !frames.length &&
  484. !highlightFrames.package &&
  485. highlightFrames.name &&
  486. profileGroup.metadata.platform === 'node'
  487. ) {
  488. // there is a chance that the reason we did not find any frames is because
  489. // for node, we try to infer some package from the frontend code.
  490. // If that happens, we'll try and just do a search by name. This logic
  491. // is duplicated in flamegraphZoomView.tsx and should be kept in sync
  492. frames = flamegraph.findAllMatchingFramesBy(highlightFrames.name, ['name']);
  493. }
  494. if (frames.length > 0) {
  495. const rectFrames = frames.map(
  496. f => new Rect(f.start, f.depth, f.end - f.start, 1)
  497. );
  498. const newConfigView = computeMinZoomConfigViewForFrames(
  499. newView.configView,
  500. rectFrames
  501. ).transformRect(newView.configSpaceTransform);
  502. newView.setConfigView(newConfigView);
  503. return newView;
  504. }
  505. }
  506. // Because we render empty flamechart while we fetch the data, we need to make sure
  507. // to have some heuristic when the data is fetched to determine if we should
  508. // initialize the config view to the full view or a predefined value
  509. else if (
  510. !defined(highlightFrames) &&
  511. position.view &&
  512. !position.view.isEmpty() &&
  513. previousView?.model === LOADING_OR_FALLBACK_FLAMEGRAPH
  514. ) {
  515. // We allow min width to be initialize to lower than view.minWidth because
  516. // there is a chance that user zoomed into a span duration which may have been updated
  517. // after the model was loaded (see L320)
  518. newView.setConfigView(position.view, {width: {min: 0}});
  519. }
  520. return newView;
  521. },
  522. // We skip position.view dependency because it will go into an infinite loop
  523. // eslint-disable-next-line react-hooks/exhaustive-deps
  524. [flamegraph, flamegraphCanvas, flamegraphTheme, profileOffsetFromTransaction]
  525. );
  526. const uiFramesView = useMemoWithPrevious<CanvasView<UIFrames> | null>(
  527. _previousView => {
  528. if (!flamegraphView || !flamegraphCanvas || !uiFrames) {
  529. return null;
  530. }
  531. const newView = new CanvasView({
  532. canvas: flamegraphCanvas,
  533. model: uiFrames,
  534. mode: 'stretchToFit',
  535. options: {
  536. inverted: flamegraph.inverted,
  537. minWidth: uiFrames.minFrameDuration,
  538. barHeight: 10,
  539. depthOffset: 0,
  540. configSpaceTransform: new Rect(profileOffsetFromTransaction, 0, 0, 0),
  541. },
  542. });
  543. // Initialize configView to whatever the flamegraph configView is
  544. newView.setConfigView(
  545. flamegraphView.configView.withHeight(newView.configView.height),
  546. {width: {min: 0}}
  547. );
  548. return newView;
  549. },
  550. [flamegraphView, flamegraphCanvas, flamegraph, uiFrames, profileOffsetFromTransaction]
  551. );
  552. const batteryChartView = useMemoWithPrevious<CanvasView<FlamegraphChartModel> | null>(
  553. _previousView => {
  554. if (!flamegraphView || !flamegraphCanvas || !batteryChart || !batteryChartCanvas) {
  555. return null;
  556. }
  557. const newView = new CanvasView({
  558. canvas: flamegraphCanvas,
  559. model: batteryChart,
  560. mode: 'anchorBottom',
  561. options: {
  562. // Invert chart so origin is at bottom left
  563. // corner as opposed to top left
  564. inverted: true,
  565. minWidth: uiFrames.minFrameDuration,
  566. barHeight: 0,
  567. depthOffset: 0,
  568. maxHeight: batteryChart.configSpace.height,
  569. minHeight: batteryChart.configSpace.height,
  570. configSpaceTransform: new Rect(profileOffsetFromTransaction, 0, 0, 0),
  571. },
  572. });
  573. // Compute the total size of the padding and stretch the view. This ensures that
  574. // the total range is rendered and perfectly aligned from top to bottom.
  575. newView.setConfigView(
  576. flamegraphView.configView.withHeight(newView.configView.height),
  577. {
  578. width: {min: 1},
  579. }
  580. );
  581. return newView;
  582. },
  583. [
  584. flamegraphView,
  585. flamegraphCanvas,
  586. batteryChart,
  587. uiFrames.minFrameDuration,
  588. batteryChartCanvas,
  589. profileOffsetFromTransaction,
  590. ]
  591. );
  592. const cpuChartView = useMemoWithPrevious<CanvasView<FlamegraphChartModel> | null>(
  593. _previousView => {
  594. if (!flamegraphView || !flamegraphCanvas || !CPUChart || !cpuChartCanvas) {
  595. return null;
  596. }
  597. const newView = new CanvasView({
  598. canvas: flamegraphCanvas,
  599. model: CPUChart,
  600. mode: 'anchorBottom',
  601. options: {
  602. // Invert chart so origin is at bottom left
  603. // corner as opposed to top left
  604. inverted: true,
  605. minWidth: uiFrames.minFrameDuration,
  606. barHeight: 0,
  607. depthOffset: 0,
  608. maxHeight: CPUChart.configSpace.height,
  609. minHeight: CPUChart.configSpace.height,
  610. configSpaceTransform: new Rect(profileOffsetFromTransaction, 0, 0, 0),
  611. },
  612. });
  613. // Compute the total size of the padding and stretch the view. This ensures that
  614. // the total range is rendered and perfectly aligned from top to bottom.
  615. newView.setConfigView(
  616. flamegraphView.configView.withHeight(newView.configView.height),
  617. {
  618. width: {min: 1},
  619. }
  620. );
  621. return newView;
  622. },
  623. [
  624. flamegraphView,
  625. flamegraphCanvas,
  626. CPUChart,
  627. uiFrames.minFrameDuration,
  628. cpuChartCanvas,
  629. profileOffsetFromTransaction,
  630. ]
  631. );
  632. const memoryChartView = useMemoWithPrevious<CanvasView<FlamegraphChartModel> | null>(
  633. _previousView => {
  634. if (!flamegraphView || !flamegraphCanvas || !memoryChart || !memoryChartCanvas) {
  635. return null;
  636. }
  637. const newView = new CanvasView({
  638. canvas: flamegraphCanvas,
  639. model: memoryChart,
  640. mode: 'anchorBottom',
  641. options: {
  642. // Invert chart so origin is at bottom left
  643. // corner as opposed to top left
  644. inverted: true,
  645. minWidth: uiFrames.minFrameDuration,
  646. barHeight: 0,
  647. depthOffset: 0,
  648. maxHeight: memoryChart.configSpace.height,
  649. minHeight: memoryChart.configSpace.height,
  650. configSpaceTransform: new Rect(profileOffsetFromTransaction, 0, 0, 0),
  651. },
  652. });
  653. // Compute the total size of the padding and stretch the view. This ensures that
  654. // the total range is rendered and perfectly aligned from top to bottom.
  655. newView.setConfigView(
  656. flamegraphView.configView.withHeight(newView.configView.height),
  657. {
  658. width: {min: 1},
  659. }
  660. );
  661. return newView;
  662. },
  663. [
  664. flamegraphView,
  665. flamegraphCanvas,
  666. memoryChart,
  667. uiFrames.minFrameDuration,
  668. memoryChartCanvas,
  669. profileOffsetFromTransaction,
  670. ]
  671. );
  672. const spansView = useMemoWithPrevious<CanvasView<SpanChart> | null>(
  673. _previousView => {
  674. if (!spansCanvas || !spanChart || !flamegraphView) {
  675. return null;
  676. }
  677. const newView = new CanvasView({
  678. canvas: spansCanvas,
  679. model: spanChart,
  680. options: {
  681. inverted: false,
  682. minWidth: spanChart.minSpanDuration,
  683. barHeight: flamegraphTheme.SIZES.SPANS_BAR_HEIGHT,
  684. depthOffset: flamegraphTheme.SIZES.SPANS_DEPTH_OFFSET,
  685. },
  686. });
  687. // Initialize configView to whatever the flamegraph configView is
  688. newView.setConfigView(
  689. flamegraphView.configView.withHeight(newView.configView.height),
  690. {width: {min: 1}}
  691. );
  692. return newView;
  693. },
  694. [spanChart, spansCanvas, flamegraphView, flamegraphTheme.SIZES]
  695. );
  696. // We want to make sure that the views have the same min zoom levels so that
  697. // if you wheel zoom on one, the other one will also zoom to the same level of detail.
  698. // If we dont do this, then at some point during the zoom action the views will
  699. // detach and only one will zoom while the other one will stay at the same zoom level.
  700. useEffect(() => {
  701. const minWidthBetweenViews = Math.min(
  702. flamegraphView?.minWidth ?? Number.MAX_SAFE_INTEGER,
  703. spansView?.minWidth ?? Number.MAX_SAFE_INTEGER,
  704. uiFramesView?.minWidth ?? Number.MAX_SAFE_INTEGER,
  705. cpuChartView?.minWidth ?? Number.MAX_SAFE_INTEGER,
  706. memoryChartView?.minWidth ?? Number.MAX_SAFE_INTEGER,
  707. batteryChartView?.minWidth ?? Number.MAX_SAFE_INTEGER
  708. );
  709. flamegraphView?.setMinWidth?.(minWidthBetweenViews);
  710. spansView?.setMinWidth?.(minWidthBetweenViews);
  711. uiFramesView?.setMinWidth?.(minWidthBetweenViews);
  712. cpuChartView?.setMinWidth?.(minWidthBetweenViews);
  713. memoryChartView?.setMinWidth?.(minWidthBetweenViews);
  714. batteryChartView?.setMinWidth?.(minWidthBetweenViews);
  715. }, [
  716. flamegraphView,
  717. spansView,
  718. uiFramesView,
  719. cpuChartView,
  720. memoryChartView,
  721. batteryChartView,
  722. ]);
  723. // Uses a useLayoutEffect to ensure that these top level/global listeners are added before
  724. // any of the children components effects actually run. This way we do not lose events
  725. // when we register/unregister these top level listeners.
  726. useLayoutEffect(() => {
  727. if (!flamegraphCanvas || !flamegraphView) {
  728. return undefined;
  729. }
  730. // This code below manages the synchronization of the config views between spans and flamegraph
  731. // We do so by listening to the config view change event and then updating the other views accordingly which
  732. // allows us to keep the X axis in sync between the two views but keep the Y axis independent
  733. const onConfigViewChange = (rect: Rect, sourceConfigViewChange: CanvasView<any>) => {
  734. if (sourceConfigViewChange === flamegraphView) {
  735. flamegraphView.setConfigView(rect.withHeight(flamegraphView.configView.height));
  736. if (spansView) {
  737. const beforeY = spansView.configView.y;
  738. spansView.setConfigView(
  739. rect.withHeight(spansView.configView.height).withY(beforeY)
  740. );
  741. }
  742. if (uiFramesView) {
  743. uiFramesView.setConfigView(rect);
  744. }
  745. if (cpuChartView) {
  746. cpuChartView.setConfigView(rect);
  747. }
  748. if (memoryChartView) {
  749. memoryChartView.setConfigView(rect);
  750. }
  751. if (batteryChartView) {
  752. batteryChartView.setConfigView(rect);
  753. }
  754. }
  755. if (sourceConfigViewChange === spansView) {
  756. spansView.setConfigView(rect.withHeight(spansView.configView.height));
  757. const beforeY = flamegraphView.configView.y;
  758. flamegraphView.setConfigView(
  759. rect.withHeight(flamegraphView.configView.height).withY(beforeY)
  760. );
  761. if (uiFramesView) {
  762. uiFramesView.setConfigView(rect);
  763. }
  764. if (cpuChartView) {
  765. cpuChartView.setConfigView(rect);
  766. }
  767. if (memoryChartView) {
  768. memoryChartView.setConfigView(rect);
  769. }
  770. if (batteryChartView) {
  771. batteryChartView.setConfigView(rect);
  772. }
  773. }
  774. canvasPoolManager.draw();
  775. };
  776. const onTransformConfigView = (
  777. mat: mat3,
  778. sourceTransformConfigView: CanvasView<any>
  779. ) => {
  780. if (sourceTransformConfigView === flamegraphView) {
  781. flamegraphView.transformConfigView(mat);
  782. if (spansView) {
  783. const beforeY = spansView.configView.y;
  784. spansView.transformConfigView(mat);
  785. spansView.setConfigView(spansView.configView.withY(beforeY));
  786. }
  787. if (uiFramesView) {
  788. uiFramesView.transformConfigView(mat);
  789. }
  790. if (batteryChartView) {
  791. batteryChartView.transformConfigView(mat);
  792. }
  793. if (cpuChartView) {
  794. cpuChartView.transformConfigView(mat);
  795. }
  796. if (memoryChartView) {
  797. memoryChartView.transformConfigView(mat);
  798. }
  799. }
  800. if (sourceTransformConfigView === spansView) {
  801. spansView.transformConfigView(mat);
  802. const beforeY = flamegraphView.configView.y;
  803. flamegraphView.transformConfigView(mat);
  804. flamegraphView.setConfigView(flamegraphView.configView.withY(beforeY));
  805. if (uiFramesView) {
  806. uiFramesView.transformConfigView(mat);
  807. }
  808. if (batteryChartView) {
  809. batteryChartView.transformConfigView(mat);
  810. }
  811. if (cpuChartView) {
  812. cpuChartView.transformConfigView(mat);
  813. }
  814. if (memoryChartView) {
  815. memoryChartView.transformConfigView(mat);
  816. }
  817. }
  818. if (
  819. sourceTransformConfigView === uiFramesView ||
  820. sourceTransformConfigView === cpuChartView ||
  821. sourceTransformConfigView === memoryChartView ||
  822. sourceTransformConfigView === batteryChartView
  823. ) {
  824. if (flamegraphView) {
  825. const beforeY = flamegraphView.configView.y;
  826. flamegraphView.transformConfigView(mat);
  827. flamegraphView.setConfigView(flamegraphView.configView.withY(beforeY));
  828. }
  829. if (spansView) {
  830. const beforeY = spansView.configView.y;
  831. spansView.transformConfigView(mat);
  832. spansView.setConfigView(spansView.configView.withY(beforeY));
  833. }
  834. if (uiFramesView) {
  835. uiFramesView.transformConfigView(mat);
  836. }
  837. if (batteryChartView) {
  838. batteryChartView.transformConfigView(mat);
  839. }
  840. if (cpuChartView) {
  841. cpuChartView.transformConfigView(mat);
  842. }
  843. if (memoryChartView) {
  844. memoryChartView.transformConfigView(mat);
  845. }
  846. }
  847. canvasPoolManager.draw();
  848. };
  849. const onResetZoom = () => {
  850. flamegraphView.resetConfigView(flamegraphCanvas);
  851. if (spansView && spansCanvas) {
  852. spansView.resetConfigView(spansCanvas);
  853. }
  854. if (uiFramesView && uiFramesCanvas) {
  855. uiFramesView.resetConfigView(uiFramesCanvas);
  856. }
  857. if (batteryChartView && batteryChartCanvas) {
  858. batteryChartView.resetConfigView(batteryChartCanvas);
  859. }
  860. if (cpuChartView && cpuChartCanvas) {
  861. cpuChartView.resetConfigView(cpuChartCanvas);
  862. }
  863. if (memoryChartView && memoryChartCanvas) {
  864. memoryChartView.resetConfigView(memoryChartCanvas);
  865. }
  866. canvasPoolManager.draw();
  867. };
  868. const onZoomIntoFrame = (frame: FlamegraphFrame, strategy: 'min' | 'exact') => {
  869. const newConfigView = computeConfigViewWithStrategy(
  870. strategy,
  871. flamegraphView.configView,
  872. new Rect(frame.start, frame.depth, frame.end - frame.start, 1)
  873. ).transformRect(flamegraphView.configSpaceTransform);
  874. flamegraphView.setConfigView(newConfigView);
  875. if (spansView) {
  876. spansView.setConfigView(newConfigView.withHeight(spansView.configView.height));
  877. }
  878. if (uiFramesView) {
  879. uiFramesView.setConfigView(
  880. newConfigView.withHeight(uiFramesView.configView.height)
  881. );
  882. }
  883. if (batteryChartView) {
  884. batteryChartView.setConfigView(
  885. newConfigView.withHeight(batteryChartView.configView.height)
  886. );
  887. }
  888. if (cpuChartView) {
  889. cpuChartView.setConfigView(
  890. newConfigView.withHeight(cpuChartView.configView.height)
  891. );
  892. }
  893. if (memoryChartView) {
  894. memoryChartView.setConfigView(
  895. newConfigView.withHeight(memoryChartView.configView.height)
  896. );
  897. }
  898. canvasPoolManager.draw();
  899. };
  900. const onZoomIntoSpan = (span: SpanChartNode, strategy: 'min' | 'exact') => {
  901. if (!spansView) {
  902. return;
  903. }
  904. const newConfigView = computeConfigViewWithStrategy(
  905. strategy,
  906. spansView.configView,
  907. new Rect(span.start, span.depth, span.end - span.start, 1)
  908. ).transformRect(spansView.configSpaceTransform);
  909. spansView.setConfigView(newConfigView);
  910. flamegraphView.setConfigView(
  911. newConfigView
  912. .withHeight(flamegraphView.configView.height)
  913. .withY(flamegraphView.configView.y)
  914. );
  915. if (uiFramesView) {
  916. uiFramesView.setConfigView(
  917. newConfigView.withHeight(uiFramesView.configView.height)
  918. );
  919. }
  920. if (batteryChartView) {
  921. batteryChartView.setConfigView(
  922. newConfigView.withHeight(batteryChartView.configView.height)
  923. );
  924. }
  925. if (cpuChartView) {
  926. cpuChartView.setConfigView(
  927. newConfigView.withHeight(cpuChartView.configView.height)
  928. );
  929. }
  930. if (memoryChartView) {
  931. memoryChartView.setConfigView(
  932. newConfigView.withHeight(memoryChartView.configView.height)
  933. );
  934. }
  935. canvasPoolManager.draw();
  936. };
  937. scheduler.on('set config view', onConfigViewChange);
  938. scheduler.on('transform config view', onTransformConfigView);
  939. scheduler.on('reset zoom', onResetZoom);
  940. scheduler.on('zoom at frame', onZoomIntoFrame);
  941. scheduler.on('zoom at span', onZoomIntoSpan);
  942. return () => {
  943. scheduler.off('set config view', onConfigViewChange);
  944. scheduler.off('transform config view', onTransformConfigView);
  945. scheduler.off('reset zoom', onResetZoom);
  946. scheduler.off('zoom at frame', onZoomIntoFrame);
  947. scheduler.off('zoom at span', onZoomIntoSpan);
  948. };
  949. }, [
  950. canvasPoolManager,
  951. flamegraphCanvas,
  952. flamegraphView,
  953. scheduler,
  954. spansCanvas,
  955. spansView,
  956. uiFramesCanvas,
  957. uiFramesView,
  958. cpuChartCanvas,
  959. cpuChartView,
  960. memoryChartCanvas,
  961. memoryChartView,
  962. batteryChartView,
  963. batteryChartCanvas,
  964. ]);
  965. const minimapCanvases = useMemo(() => {
  966. return [flamegraphMiniMapCanvasRef, flamegraphMiniMapOverlayCanvasRef];
  967. }, [flamegraphMiniMapCanvasRef, flamegraphMiniMapOverlayCanvasRef]);
  968. useResizeCanvasObserver(
  969. minimapCanvases,
  970. canvasPoolManager,
  971. flamegraphMiniMapCanvas,
  972. null
  973. );
  974. const spansCanvases = useMemo(() => {
  975. return [spansCanvasRef];
  976. }, [spansCanvasRef]);
  977. const spansCanvasBounds = useResizeCanvasObserver(
  978. spansCanvases,
  979. canvasPoolManager,
  980. spansCanvas,
  981. spansView
  982. );
  983. const uiFramesCanvases = useMemo(() => {
  984. return [uiFramesCanvasRef];
  985. }, [uiFramesCanvasRef]);
  986. const uiFramesCanvasBounds = useResizeCanvasObserver(
  987. uiFramesCanvases,
  988. canvasPoolManager,
  989. uiFramesCanvas,
  990. uiFramesView
  991. );
  992. const batteryChartCanvases = useMemo(() => {
  993. return [batteryChartCanvasRef];
  994. }, [batteryChartCanvasRef]);
  995. const batteryChartCanvasBounds = useResizeCanvasObserver(
  996. batteryChartCanvases,
  997. canvasPoolManager,
  998. batteryChartCanvas,
  999. batteryChartView
  1000. );
  1001. const cpuChartCanvases = useMemo(() => {
  1002. return [cpuChartCanvasRef];
  1003. }, [cpuChartCanvasRef]);
  1004. const cpuChartCanvasBounds = useResizeCanvasObserver(
  1005. cpuChartCanvases,
  1006. canvasPoolManager,
  1007. cpuChartCanvas,
  1008. cpuChartView
  1009. );
  1010. const memoryChartCanvases = useMemo(() => {
  1011. return [memoryChartCanvasRef];
  1012. }, [memoryChartCanvasRef]);
  1013. const memoryChartCanvasBounds = useResizeCanvasObserver(
  1014. memoryChartCanvases,
  1015. canvasPoolManager,
  1016. memoryChartCanvas,
  1017. memoryChartView
  1018. );
  1019. const flamegraphCanvases = useMemo(() => {
  1020. return [flamegraphCanvasRef, flamegraphOverlayCanvasRef];
  1021. }, [flamegraphCanvasRef, flamegraphOverlayCanvasRef]);
  1022. const flamegraphCanvasBounds = useResizeCanvasObserver(
  1023. flamegraphCanvases,
  1024. canvasPoolManager,
  1025. flamegraphCanvas,
  1026. flamegraphView
  1027. );
  1028. const flamegraphRenderer = useMemo(() => {
  1029. if (!flamegraphCanvasRef) {
  1030. return null;
  1031. }
  1032. const renderer = initializeFlamegraphRenderer(
  1033. [FlamegraphRendererWebGL, FlamegraphRenderer2D],
  1034. [
  1035. flamegraphCanvasRef,
  1036. flamegraph,
  1037. flamegraphTheme,
  1038. {
  1039. colorCoding,
  1040. draw_border: true,
  1041. },
  1042. ]
  1043. );
  1044. if (renderer === null) {
  1045. Sentry.captureException('Failed to initialize a flamegraph renderer');
  1046. addErrorMessage('Failed to initialize renderer');
  1047. return null;
  1048. }
  1049. return renderer;
  1050. }, [colorCoding, flamegraph, flamegraphCanvasRef, flamegraphTheme]);
  1051. const getFrameColor = useCallback(
  1052. (frame: FlamegraphFrame) => {
  1053. if (!flamegraphRenderer) {
  1054. return '';
  1055. }
  1056. return formatColorForFrame(frame, flamegraphRenderer);
  1057. },
  1058. [flamegraphRenderer]
  1059. );
  1060. const physicalToConfig =
  1061. flamegraphView && flamegraphCanvas
  1062. ? mat3.invert(
  1063. mat3.create(),
  1064. flamegraphView.fromConfigView(flamegraphCanvas.physicalSpace)
  1065. )
  1066. : mat3.create();
  1067. const configSpacePixel = new Rect(0, 0, 1, 1).transformRect(physicalToConfig);
  1068. // Register keyboard navigation
  1069. useViewKeyboardNavigation(flamegraphView, canvasPoolManager, configSpacePixel.width);
  1070. // referenceNode is passed down to the flamegraphdrawer and is used to determine
  1071. // the weights of each frame. In other words, in case there is no user selected root, then all
  1072. // of the frame weights and timing are relative to the entire profile. If there is a user selected
  1073. // root however, all weights are relative to that sub tree.
  1074. const referenceNode = useMemo(
  1075. () => (selectedRoot ? selectedRoot : flamegraph.root),
  1076. [selectedRoot, flamegraph.root]
  1077. );
  1078. // In case a user selected root is present, we will display that root + its entire sub tree.
  1079. // If no root is selected, we will display the entire sub tree down from the root. We start at
  1080. // root.children because flamegraph.root is a virtual node that we do not want to show in the table.
  1081. const rootNodes = useMemo(() => {
  1082. return selectedRoot ? [selectedRoot] : flamegraph.root.children;
  1083. }, [selectedRoot, flamegraph.root]);
  1084. const onSortingChange: FlamegraphViewSelectMenuProps['onSortingChange'] = useCallback(
  1085. newSorting => {
  1086. dispatch({type: 'set sorting', payload: newSorting});
  1087. },
  1088. [dispatch]
  1089. );
  1090. const onViewChange: FlamegraphViewSelectMenuProps['onViewChange'] = useCallback(
  1091. newView => {
  1092. dispatch({type: 'set view', payload: newView});
  1093. },
  1094. [dispatch]
  1095. );
  1096. const onThreadIdChange: FlamegraphThreadSelectorProps['onThreadIdChange'] = useCallback(
  1097. newThreadId => {
  1098. dispatch({type: 'set thread id', payload: newThreadId});
  1099. },
  1100. [dispatch]
  1101. );
  1102. const onImport = useCallback(
  1103. (p: Profiling.ProfileInput) => {
  1104. setProfiles({type: 'resolved', data: p});
  1105. },
  1106. [setProfiles]
  1107. );
  1108. useEffect(() => {
  1109. if (defined(profiles.threadId)) {
  1110. return;
  1111. }
  1112. const threadID =
  1113. typeof profileGroup.activeProfileIndex === 'number'
  1114. ? profileGroup.profiles[profileGroup.activeProfileIndex]?.threadId
  1115. : null;
  1116. // if the state has a highlight frame specified, then we want to jump to the
  1117. // thread containing it, highlight the frames on the thread, and change the
  1118. // view so it's obvious where it is
  1119. if (highlightFrames) {
  1120. const candidate = profileGroup.profiles.reduce<FlamegraphCandidate | null>(
  1121. (prevCandidate, currentProfile) => {
  1122. // if the previous candidate is the active thread, it always takes priority
  1123. if (prevCandidate?.isActiveThread) {
  1124. return prevCandidate;
  1125. }
  1126. const graph = new FlamegraphModel(currentProfile, {
  1127. inverted: false,
  1128. sort: sorting,
  1129. configSpace: undefined,
  1130. });
  1131. const frame = findLongestMatchingFrame(graph, highlightFrames);
  1132. if (!defined(frame)) {
  1133. return prevCandidate;
  1134. }
  1135. const newScore = frame.node.totalWeight || 0;
  1136. const oldScore = prevCandidate?.frame?.node?.totalWeight || 0;
  1137. // if we find the frame on the active thread, it always takes priority
  1138. if (newScore > 0 && currentProfile.threadId === threadID) {
  1139. return {
  1140. frame,
  1141. threadId: currentProfile.threadId,
  1142. isActiveThread: true,
  1143. };
  1144. }
  1145. return newScore <= oldScore
  1146. ? prevCandidate
  1147. : {
  1148. frame,
  1149. threadId: currentProfile.threadId,
  1150. };
  1151. },
  1152. null
  1153. );
  1154. if (defined(candidate)) {
  1155. dispatch({
  1156. type: 'set thread id',
  1157. payload: candidate.threadId,
  1158. });
  1159. return;
  1160. }
  1161. }
  1162. // fall back case, when we finally load the active profile index from the profile,
  1163. // make sure we update the thread id so that it is show first
  1164. if (defined(threadID)) {
  1165. dispatch({
  1166. type: 'set thread id',
  1167. payload: threadID,
  1168. });
  1169. }
  1170. }, [profileGroup, highlightFrames, profiles.threadId, dispatch, sorting]);
  1171. // A bit unfortunate for now, but the search component accepts a list
  1172. // of model to search through. This will become useful as we build
  1173. // differential flamecharts or start comparing different profiles/charts
  1174. const flamegraphs = useMemo(() => [flamegraph], [flamegraph]);
  1175. const spans = useMemo(() => (spanChart ? [spanChart] : []), [spanChart]);
  1176. return (
  1177. <Fragment>
  1178. <FlamegraphToolbar>
  1179. <FlamegraphThreadSelector
  1180. profileGroup={profileGroup}
  1181. threadId={threadId}
  1182. onThreadIdChange={onThreadIdChange}
  1183. />
  1184. <FlamegraphViewSelectMenu
  1185. view={view}
  1186. sorting={sorting}
  1187. onSortingChange={onSortingChange}
  1188. onViewChange={onViewChange}
  1189. />
  1190. <FlamegraphSearch
  1191. spans={spans}
  1192. flamegraphs={flamegraphs}
  1193. canvasPoolManager={canvasPoolManager}
  1194. />
  1195. <FlamegraphOptionsMenu canvasPoolManager={canvasPoolManager} />
  1196. </FlamegraphToolbar>
  1197. <FlamegraphLayout
  1198. uiFrames={
  1199. hasUIFrames ? (
  1200. <FlamegraphUIFrames
  1201. canvasBounds={uiFramesCanvasBounds}
  1202. canvasPoolManager={canvasPoolManager}
  1203. setUIFramesCanvasRef={setUIFramesCanvasRef}
  1204. uiFramesCanvasRef={uiFramesCanvasRef}
  1205. uiFramesCanvas={uiFramesCanvas}
  1206. uiFramesView={uiFramesView}
  1207. uiFrames={uiFrames}
  1208. />
  1209. ) : null
  1210. }
  1211. batteryChart={
  1212. hasBatteryChart ? (
  1213. <FlamegraphChart
  1214. chartCanvasRef={batteryChartCanvasRef}
  1215. chartCanvas={batteryChartCanvas}
  1216. setChartCanvasRef={setBatteryChartCanvasRef}
  1217. canvasBounds={batteryChartCanvasBounds}
  1218. chartView={batteryChartView}
  1219. canvasPoolManager={canvasPoolManager}
  1220. chart={batteryChart}
  1221. noMeasurementMessage={
  1222. profileGroup.metadata.platform === 'cocoa'
  1223. ? t(
  1224. 'Upgrade to version 8.9.6 of sentry-cocoa SDK to enable battery usage collection'
  1225. )
  1226. : ''
  1227. }
  1228. />
  1229. ) : null
  1230. }
  1231. memoryChart={
  1232. hasMemoryChart ? (
  1233. <FlamegraphChart
  1234. chartCanvasRef={memoryChartCanvasRef}
  1235. chartCanvas={memoryChartCanvas}
  1236. setChartCanvasRef={setMemoryChartCanvasRef}
  1237. canvasBounds={memoryChartCanvasBounds}
  1238. chartView={memoryChartView}
  1239. canvasPoolManager={canvasPoolManager}
  1240. chart={memoryChart}
  1241. noMeasurementMessage={
  1242. profileGroup.metadata.platform === 'cocoa'
  1243. ? t(
  1244. 'Upgrade to version 8.9.6 of sentry-cocoa SDK to enable memory usage collection'
  1245. )
  1246. : profileGroup.metadata.platform === 'node'
  1247. ? t(
  1248. 'Upgrade to version 1.2.0 of @sentry/profiling-node to enable memory usage collection'
  1249. )
  1250. : ''
  1251. }
  1252. />
  1253. ) : null
  1254. }
  1255. cpuChart={
  1256. hasCPUChart ? (
  1257. <FlamegraphChart
  1258. chartCanvasRef={cpuChartCanvasRef}
  1259. chartCanvas={cpuChartCanvas}
  1260. setChartCanvasRef={setCpuChartCanvasRef}
  1261. canvasBounds={cpuChartCanvasBounds}
  1262. chartView={cpuChartView}
  1263. canvasPoolManager={canvasPoolManager}
  1264. chart={CPUChart}
  1265. noMeasurementMessage={
  1266. profileGroup.metadata.platform === 'cocoa'
  1267. ? t(
  1268. 'Upgrade to version 8.9.6 of sentry-cocoa SDK to enable CPU usage collection'
  1269. )
  1270. : profileGroup.metadata.platform === 'node'
  1271. ? t(
  1272. 'Upgrade to version 1.2.0 of @sentry/profiling-node to enable CPU usage collection'
  1273. )
  1274. : ''
  1275. }
  1276. />
  1277. ) : null
  1278. }
  1279. spansTreeDepth={spanChart?.depth}
  1280. spans={
  1281. spanChart ? (
  1282. <FlamegraphSpans
  1283. canvasBounds={spansCanvasBounds}
  1284. spanChart={spanChart}
  1285. spansCanvas={spansCanvas}
  1286. spansCanvasRef={spansCanvasRef}
  1287. setSpansCanvasRef={setSpansCanvasRef}
  1288. canvasPoolManager={canvasPoolManager}
  1289. spansView={spansView}
  1290. />
  1291. ) : null
  1292. }
  1293. minimap={
  1294. <FlamegraphZoomViewMinimap
  1295. canvasPoolManager={canvasPoolManager}
  1296. flamegraph={flamegraph}
  1297. flamegraphMiniMapCanvas={flamegraphMiniMapCanvas}
  1298. flamegraphMiniMapCanvasRef={flamegraphMiniMapCanvasRef}
  1299. flamegraphMiniMapOverlayCanvasRef={flamegraphMiniMapOverlayCanvasRef}
  1300. flamegraphMiniMapView={flamegraphView}
  1301. setFlamegraphMiniMapCanvasRef={setFlamegraphMiniMapCanvasRef}
  1302. setFlamegraphMiniMapOverlayCanvasRef={setFlamegraphMiniMapOverlayCanvasRef}
  1303. />
  1304. }
  1305. flamegraph={
  1306. <ProfileDragDropImport onImport={onImport}>
  1307. <FlamegraphWarnings flamegraph={flamegraph} />
  1308. <FlamegraphZoomView
  1309. profileGroup={profileGroup}
  1310. canvasBounds={flamegraphCanvasBounds}
  1311. canvasPoolManager={canvasPoolManager}
  1312. flamegraph={flamegraph}
  1313. flamegraphRenderer={flamegraphRenderer}
  1314. flamegraphCanvas={flamegraphCanvas}
  1315. flamegraphCanvasRef={flamegraphCanvasRef}
  1316. flamegraphOverlayCanvasRef={flamegraphOverlayCanvasRef}
  1317. flamegraphView={flamegraphView}
  1318. setFlamegraphCanvasRef={setFlamegraphCanvasRef}
  1319. setFlamegraphOverlayCanvasRef={setFlamegraphOverlayCanvasRef}
  1320. />
  1321. </ProfileDragDropImport>
  1322. }
  1323. flamegraphDrawer={
  1324. <FlamegraphDrawer
  1325. profileGroup={profileGroup}
  1326. getFrameColor={getFrameColor}
  1327. referenceNode={referenceNode}
  1328. rootNodes={rootNodes}
  1329. flamegraph={flamegraph}
  1330. formatDuration={flamegraph ? flamegraph.formatter : noopFormatDuration}
  1331. canvasPoolManager={canvasPoolManager}
  1332. canvasScheduler={scheduler}
  1333. />
  1334. }
  1335. />
  1336. </Fragment>
  1337. );
  1338. }
  1339. export {Flamegraph};