sampledProfile.spec.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423
  1. import {SampledProfile} from 'sentry/utils/profiling/profile/sampledProfile';
  2. import {createFrameIndex} from 'sentry/utils/profiling/profile/utils';
  3. import {Frame} from '../frame';
  4. import {firstCallee, makeTestingBoilerplate} from './profile.spec';
  5. describe('SampledProfile', () => {
  6. it('imports the base properties', () => {
  7. const trace: Profiling.SampledProfile = {
  8. name: 'profile',
  9. startValue: 0,
  10. endValue: 1000,
  11. unit: 'milliseconds',
  12. threadID: 0,
  13. type: 'sampled',
  14. weights: [],
  15. samples: [],
  16. };
  17. const profile = SampledProfile.FromProfile(trace, createFrameIndex('mobile', []), {
  18. type: 'flamechart',
  19. });
  20. expect(profile.duration).toBe(1000);
  21. expect(profile.name).toBe(trace.name);
  22. expect(profile.threadId).toBe(trace.threadID);
  23. expect(profile.startedAt).toBe(0);
  24. expect(profile.endedAt).toBe(1000);
  25. });
  26. it('tracks discarded samples', () => {
  27. const trace: Profiling.SampledProfile = {
  28. name: 'profile',
  29. startValue: 0,
  30. endValue: 1000,
  31. unit: 'milliseconds',
  32. threadID: 0,
  33. type: 'sampled',
  34. weights: [0],
  35. samples: [[0]],
  36. };
  37. const profile = SampledProfile.FromProfile(
  38. trace,
  39. createFrameIndex('mobile', [{name: 'f0'}]),
  40. {type: 'flamechart'}
  41. );
  42. expect(profile.stats.discardedSamplesCount).toBe(1);
  43. });
  44. it('tracks negative samples', () => {
  45. const trace: Profiling.SampledProfile = {
  46. name: 'profile',
  47. startValue: 0,
  48. endValue: 1000,
  49. unit: 'milliseconds',
  50. threadID: 0,
  51. type: 'sampled',
  52. weights: [0, -1],
  53. samples: [[0], [0]],
  54. };
  55. const profile = SampledProfile.FromProfile(
  56. trace,
  57. createFrameIndex('mobile', [{name: 'f0'}]),
  58. {type: 'flamechart'}
  59. );
  60. expect(profile.stats.negativeSamplesCount).toBe(1);
  61. });
  62. it('tracks raw weights', () => {
  63. const trace: Profiling.SampledProfile = {
  64. name: 'profile',
  65. startValue: 0,
  66. endValue: 1000,
  67. unit: 'milliseconds',
  68. threadID: 0,
  69. type: 'sampled',
  70. weights: [0, 10, 20],
  71. samples: [[0], [0], []],
  72. };
  73. const profile = SampledProfile.FromProfile(
  74. trace,
  75. createFrameIndex('mobile', [{name: 'f0'}]),
  76. {type: 'flamechart'}
  77. );
  78. expect(profile.rawWeights.length).toBe(2);
  79. });
  80. it('rebuilds the stack', () => {
  81. const trace: Profiling.SampledProfile = {
  82. name: 'profile',
  83. startValue: 0,
  84. endValue: 1000,
  85. unit: 'milliseconds',
  86. threadID: 0,
  87. type: 'sampled',
  88. weights: [1, 1],
  89. samples: [
  90. [0, 1],
  91. [0, 1],
  92. ],
  93. };
  94. const {open, close, openSpy, closeSpy, timings} = makeTestingBoilerplate();
  95. const profile = SampledProfile.FromProfile(
  96. trace,
  97. createFrameIndex('mobile', [{name: 'f0'}, {name: 'f1'}]),
  98. {type: 'flamechart'}
  99. );
  100. profile.forEach(open, close);
  101. expect(timings).toEqual([
  102. ['f0', 'open'],
  103. ['f1', 'open'],
  104. ['f1', 'close'],
  105. ['f0', 'close'],
  106. ]);
  107. expect(openSpy).toHaveBeenCalledTimes(2);
  108. expect(closeSpy).toHaveBeenCalledTimes(2);
  109. const root = firstCallee(profile.callTree);
  110. expect(root.totalWeight).toEqual(2);
  111. expect(firstCallee(root).totalWeight).toEqual(2);
  112. expect(root.selfWeight).toEqual(0);
  113. expect(firstCallee(root).selfWeight).toEqual(2);
  114. });
  115. it('marks direct recursion', () => {
  116. const trace: Profiling.SampledProfile = {
  117. name: 'profile',
  118. startValue: 0,
  119. endValue: 1000,
  120. unit: 'milliseconds',
  121. threadID: 0,
  122. type: 'sampled',
  123. weights: [1],
  124. samples: [[0, 0]],
  125. };
  126. const profile = SampledProfile.FromProfile(
  127. trace,
  128. createFrameIndex('mobile', [{name: 'f0'}, {name: 'f1'}]),
  129. {type: 'flamechart'}
  130. );
  131. expect(!!firstCallee(firstCallee(profile.callTree)).recursive).toBe(true);
  132. });
  133. it('marks indirect recursion', () => {
  134. const trace: Profiling.SampledProfile = {
  135. name: 'profile',
  136. startValue: 0,
  137. endValue: 1000,
  138. unit: 'milliseconds',
  139. threadID: 0,
  140. type: 'sampled',
  141. weights: [1],
  142. samples: [[0, 1, 0]],
  143. };
  144. const profile = SampledProfile.FromProfile(
  145. trace,
  146. createFrameIndex('mobile', [{name: 'f0'}, {name: 'f1'}]),
  147. {type: 'flamechart'}
  148. );
  149. expect(!!firstCallee(firstCallee(firstCallee(profile.callTree))).recursive).toBe(
  150. true
  151. );
  152. });
  153. it('tracks minFrameDuration', () => {
  154. const trace: Profiling.SampledProfile = {
  155. name: 'profile',
  156. startValue: 0,
  157. endValue: 1000,
  158. unit: 'milliseconds',
  159. threadID: 0,
  160. type: 'sampled',
  161. weights: [0.5, 2],
  162. samples: [
  163. [0, 1],
  164. [0, 2],
  165. ],
  166. };
  167. const profile = SampledProfile.FromProfile(
  168. trace,
  169. createFrameIndex('mobile', [{name: 'f0'}, {name: 'f1'}, {name: 'f2'}]),
  170. {type: 'flamechart'}
  171. );
  172. expect(profile.minFrameDuration).toBe(0.5);
  173. });
  174. it('places garbage collector calls on top of previous stack for node', () => {
  175. const trace: Profiling.SampledProfile = {
  176. name: 'profile',
  177. startValue: 0,
  178. endValue: 1000,
  179. unit: 'milliseconds',
  180. threadID: 0,
  181. type: 'sampled',
  182. weights: [1, 3],
  183. samples: [
  184. [0, 1],
  185. [0, 2],
  186. ],
  187. };
  188. const profile = SampledProfile.FromProfile(
  189. trace,
  190. createFrameIndex('node', [
  191. {name: 'f0'},
  192. {name: 'f1'},
  193. {name: '(garbage collector)'},
  194. ]),
  195. {type: 'flamechart'}
  196. );
  197. // GC gets places on top of the previous stack and the weight is updated
  198. expect(profile.callTree.children[0].children[0].frame.name).toBe('f1 [native code]');
  199. // The total weight of the previous top is now the weight of the GC call + the weight of the previous top
  200. expect(profile.callTree.children[0].children[0].frame.totalWeight).toBe(4);
  201. expect(profile.callTree.children[0].children[0].children[0].frame.name).toBe(
  202. '(garbage collector) [native code]'
  203. );
  204. // The self weight of the GC call is only the weight of the GC call
  205. expect(profile.callTree.children[0].children[0].children[0].frame.selfWeight).toBe(3);
  206. });
  207. it('places garbage collector calls on top of previous stack and skips stack', () => {
  208. const trace: Profiling.SampledProfile = {
  209. name: 'profile',
  210. startValue: 0,
  211. endValue: 1000,
  212. unit: 'milliseconds',
  213. threadID: 0,
  214. type: 'sampled',
  215. weights: [1, 1, 1, 1],
  216. samples: [
  217. [0, 1],
  218. [0, 2],
  219. [0, 2],
  220. [0, 1],
  221. ],
  222. };
  223. const profile = SampledProfile.FromProfile(
  224. trace,
  225. createFrameIndex('node', [
  226. {name: 'f0'},
  227. {name: 'f1'},
  228. {name: '(garbage collector)'},
  229. ]),
  230. {type: 'flamechart'}
  231. );
  232. expect(profile.weights).toEqual([1, 2, 1]);
  233. });
  234. it('does not place garbage collector calls on top of previous stack for node', () => {
  235. const trace: Profiling.SampledProfile = {
  236. name: 'profile',
  237. startValue: 0,
  238. endValue: 1000,
  239. unit: 'milliseconds',
  240. threadID: 0,
  241. type: 'sampled',
  242. weights: [1, 2],
  243. samples: [
  244. [0, 1, 3],
  245. [0, 1, 2],
  246. ],
  247. };
  248. const profile = SampledProfile.FromProfile(
  249. trace,
  250. createFrameIndex('node', [
  251. {name: 'f0'},
  252. {name: 'f1'},
  253. {name: '(garbage collector)'},
  254. {name: 'f2'},
  255. ]),
  256. {type: 'flamechart'}
  257. );
  258. expect(profile.callTree.children[0].children[0].children.length).toBe(2);
  259. expect(profile.callTree.children[0].children[0].children[0].frame.name).toBe(
  260. 'f2 [native code]'
  261. );
  262. expect(profile.callTree.children[0].children[0].children[1].frame.name).toBe(
  263. '(garbage collector) [native code]'
  264. );
  265. });
  266. it('merges consecutive garbage collector calls on top of previous stack for node', () => {
  267. const trace: Profiling.SampledProfile = {
  268. name: 'profile',
  269. startValue: 0,
  270. endValue: 1000,
  271. unit: 'milliseconds',
  272. threadID: 0,
  273. type: 'sampled',
  274. weights: [1, 2, 2, 2],
  275. samples: [
  276. [0, 1],
  277. [0, 2],
  278. [0, 2],
  279. [0, 2],
  280. ],
  281. };
  282. const profile = SampledProfile.FromProfile(
  283. trace,
  284. createFrameIndex('node', [
  285. {name: 'f0'},
  286. {name: 'f1'},
  287. {name: '(garbage collector)'},
  288. ]),
  289. {type: 'flamechart'}
  290. );
  291. // There are no other children than the GC call meaning merge happened
  292. expect(profile.callTree.children[0].children[0].children[1]).toBe(undefined);
  293. expect(profile.callTree.children[0].children[0].children[0].frame.selfWeight).toBe(6);
  294. });
  295. it('flamegraph tracks node occurrences', () => {
  296. const trace: Profiling.SampledProfile = {
  297. name: 'profile',
  298. startValue: 0,
  299. endValue: 1000,
  300. unit: 'milliseconds',
  301. threadID: 0,
  302. type: 'sampled',
  303. weights: [1, 1, 1],
  304. samples: [[0], [0, 1], [0]],
  305. };
  306. const profile = SampledProfile.FromProfile(
  307. trace,
  308. createFrameIndex('node', [{name: 'f0'}, {name: 'f1'}, {name: 'f2'}]),
  309. {type: 'flamechart'}
  310. );
  311. expect(profile.callTree.children[0].count).toBe(3);
  312. expect(profile.callTree.children[0].children[0].count).toBe(1);
  313. });
  314. it('filters frames', () => {
  315. const trace: Profiling.SampledProfile = {
  316. name: 'profile',
  317. startValue: 0,
  318. endValue: 1000,
  319. unit: 'milliseconds',
  320. threadID: 0,
  321. type: 'sampled',
  322. weights: [1, 1],
  323. samples: [
  324. [0, 1],
  325. [0, 1],
  326. ],
  327. };
  328. const profile = SampledProfile.FromProfile(
  329. trace,
  330. createFrameIndex('mobile', [{name: 'f0'}, {name: 'f1'}]),
  331. {
  332. type: 'flamegraph',
  333. frameFilter: frame => {
  334. return frame.name === 'f0';
  335. },
  336. }
  337. );
  338. expect(profile.callTree.frame).toBe(Frame.Root);
  339. expect(profile.callTree.children).toHaveLength(1);
  340. expect(profile.callTree.children[0].frame.name).toEqual('f0');
  341. // the f1 frame is filtered out, so the f0 frame has no children
  342. expect(profile.callTree.children[0].children).toHaveLength(0);
  343. });
  344. it('aggregates durations for flamegraph', () => {
  345. const trace: Profiling.SampledProfile = {
  346. name: 'profile',
  347. startValue: 0,
  348. endValue: 1000,
  349. unit: 'milliseconds',
  350. threadID: 0,
  351. type: 'sampled',
  352. weights: [1, 1],
  353. sample_durations_ns: [10, 5],
  354. samples: [
  355. [0, 1],
  356. [0, 2],
  357. ],
  358. };
  359. const profile = SampledProfile.FromProfile(
  360. trace,
  361. createFrameIndex('mobile', [{name: 'f0'}, {name: 'f1'}, {name: 'f2'}]),
  362. {
  363. type: 'flamegraph',
  364. }
  365. );
  366. expect(profile.callTree.children[0].frame.name).toBe('f0');
  367. expect(profile.callTree.children[0].aggregate_duration_ns).toBe(15);
  368. expect(profile.callTree.children[0].children[0].aggregate_duration_ns).toBe(10);
  369. expect(profile.callTree.children[0].children[1].aggregate_duration_ns).toBe(5);
  370. expect(profile.callTree.children[0].frame.aggregateDuration).toBe(15);
  371. expect(profile.callTree.children[0].children[0].frame.aggregateDuration).toBe(10);
  372. expect(profile.callTree.children[0].children[1].frame.aggregateDuration).toBe(5);
  373. });
  374. });