continuousFlamegraph.tsx 53 KB

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