flamegraph.tsx 52 KB

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