eventedProfile.spec.tsx 10 KB

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