sentrySampledProfile.spec.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517
  1. import merge from 'lodash/merge';
  2. import {DeepPartial} from 'sentry/types/utils';
  3. import {Frame} from '../frame';
  4. import {makeTestingBoilerplate} from './profile.spec';
  5. import {SentrySampledProfile} from './sentrySampledProfile';
  6. import {createSentrySampleProfileFrameIndex} from './utils';
  7. export const makeSentrySampledProfile = (
  8. profile?: DeepPartial<Profiling.SentrySampledProfile>
  9. ) => {
  10. return merge(
  11. {
  12. event_id: '1',
  13. version: '1',
  14. os: {
  15. name: 'iOS',
  16. version: '16.0',
  17. build_number: '19H253',
  18. },
  19. device: {
  20. architecture: 'arm64e',
  21. is_emulator: false,
  22. locale: 'en_US',
  23. manufacturer: 'Apple',
  24. model: 'iPhone14,3',
  25. },
  26. timestamp: '2022-09-01T09:45:00.000Z',
  27. platform: 'cocoa',
  28. profile: {
  29. samples: [
  30. {
  31. stack_id: 0,
  32. thread_id: '0',
  33. elapsed_since_start_ns: 0,
  34. },
  35. {
  36. stack_id: 1,
  37. thread_id: '0',
  38. elapsed_since_start_ns: 1000,
  39. },
  40. ],
  41. frames: [
  42. {
  43. function: 'main',
  44. instruction_addr: '',
  45. lineno: 1,
  46. colno: 1,
  47. file: 'main.c',
  48. },
  49. {
  50. function: 'foo',
  51. instruction_addr: '',
  52. lineno: 2,
  53. colno: 2,
  54. file: 'main.c',
  55. },
  56. ],
  57. stacks: [[1, 0], [0]],
  58. },
  59. transaction: {
  60. id: '',
  61. name: 'foo',
  62. active_thread_id: 0,
  63. trace_id: '1',
  64. },
  65. },
  66. profile
  67. ) as Profiling.SentrySampledProfile;
  68. };
  69. describe('SentrySampledProfile', () => {
  70. it('constructs a profile', () => {
  71. const sampledProfile: Profiling.SentrySampledProfile = makeSentrySampledProfile();
  72. const profile = SentrySampledProfile.FromProfile(
  73. sampledProfile,
  74. createSentrySampleProfileFrameIndex(sampledProfile.profile.frames, 'javascript'),
  75. {type: 'flamechart'}
  76. );
  77. const {open, close, timings} = makeTestingBoilerplate();
  78. profile.forEach(open, close);
  79. expect(profile.duration).toBe(1000);
  80. expect(timings).toEqual([
  81. ['main', 'open'],
  82. ['foo', 'open'],
  83. ['foo', 'close'],
  84. ['main', 'close'],
  85. ]);
  86. expect(profile.startedAt).toEqual(0);
  87. expect(profile.endedAt).toEqual(1000);
  88. });
  89. it('tracks discarded samples', () => {
  90. const sampledProfile = makeSentrySampledProfile({
  91. transaction: {
  92. id: '',
  93. name: 'foo',
  94. active_thread_id: 1,
  95. trace_id: '1',
  96. },
  97. profile: {
  98. samples: [
  99. {
  100. stack_id: 0,
  101. elapsed_since_start_ns: 1000,
  102. thread_id: '0',
  103. },
  104. {
  105. stack_id: 0,
  106. elapsed_since_start_ns: 1000,
  107. thread_id: '0',
  108. },
  109. ],
  110. thread_metadata: {
  111. '0': {
  112. name: 'bar',
  113. },
  114. },
  115. },
  116. });
  117. const profile = SentrySampledProfile.FromProfile(
  118. sampledProfile,
  119. createSentrySampleProfileFrameIndex(sampledProfile.profile.frames, 'javascript'),
  120. {type: 'flamechart'}
  121. );
  122. expect(profile.stats.discardedSamplesCount).toBe(2);
  123. });
  124. it('tracks negative samples', () => {
  125. const sampledProfile = makeSentrySampledProfile({
  126. transaction: {
  127. id: '',
  128. name: 'foo',
  129. active_thread_id: 1,
  130. trace_id: '1',
  131. },
  132. profile: {
  133. samples: [
  134. {
  135. stack_id: 0,
  136. elapsed_since_start_ns: 1000,
  137. thread_id: '0',
  138. },
  139. {
  140. stack_id: 0,
  141. elapsed_since_start_ns: -1000,
  142. thread_id: '0',
  143. },
  144. ],
  145. thread_metadata: {
  146. '0': {
  147. name: 'bar',
  148. },
  149. },
  150. },
  151. });
  152. const profile = SentrySampledProfile.FromProfile(
  153. sampledProfile,
  154. createSentrySampleProfileFrameIndex(sampledProfile.profile.frames, 'javascript'),
  155. {type: 'flamechart'}
  156. );
  157. expect(profile.stats.negativeSamplesCount).toBe(1);
  158. });
  159. it('tracks raw weights', () => {
  160. const sampledProfile = makeSentrySampledProfile({
  161. transaction: {
  162. id: '',
  163. name: 'foo',
  164. active_thread_id: 1,
  165. trace_id: '1',
  166. },
  167. profile: {
  168. samples: [
  169. {
  170. stack_id: 0,
  171. elapsed_since_start_ns: 1000,
  172. thread_id: '0',
  173. },
  174. {
  175. stack_id: 0,
  176. elapsed_since_start_ns: 2000,
  177. thread_id: '0',
  178. },
  179. {
  180. stack_id: 0,
  181. elapsed_since_start_ns: 3000,
  182. thread_id: '0',
  183. },
  184. ],
  185. thread_metadata: {
  186. '0': {
  187. name: 'bar',
  188. },
  189. },
  190. },
  191. });
  192. const profile = SentrySampledProfile.FromProfile(
  193. sampledProfile,
  194. createSentrySampleProfileFrameIndex(sampledProfile.profile.frames, 'javascript'),
  195. {type: 'flamechart'}
  196. );
  197. expect(profile.rawWeights.length).toBe(2);
  198. });
  199. it('derives a profile name from the transaction.name and thread_id', () => {
  200. const sampledProfile = makeSentrySampledProfile({
  201. transaction: {
  202. id: '',
  203. name: 'foo',
  204. active_thread_id: 1,
  205. trace_id: '1',
  206. },
  207. profile: {
  208. samples: [
  209. {
  210. stack_id: 0,
  211. elapsed_since_start_ns: 1000,
  212. thread_id: '0',
  213. },
  214. ],
  215. thread_metadata: {
  216. '0': {
  217. name: 'bar',
  218. },
  219. },
  220. },
  221. });
  222. const profile = SentrySampledProfile.FromProfile(
  223. sampledProfile,
  224. createSentrySampleProfileFrameIndex(sampledProfile.profile.frames, 'javascript'),
  225. {type: 'flamechart'}
  226. );
  227. expect(profile.name).toBe('bar');
  228. expect(profile.threadId).toBe(0);
  229. });
  230. it('derives a profile name from just thread_id', () => {
  231. const sampledProfile = makeSentrySampledProfile({
  232. platform: 'python',
  233. profile: {
  234. samples: [
  235. {
  236. stack_id: 0,
  237. elapsed_since_start_ns: 1000,
  238. thread_id: '0',
  239. },
  240. ],
  241. thread_metadata: {},
  242. },
  243. });
  244. const profile = SentrySampledProfile.FromProfile(
  245. sampledProfile,
  246. createSentrySampleProfileFrameIndex(sampledProfile.profile.frames, 'javascript'),
  247. {type: 'flamechart'}
  248. );
  249. expect(profile.name).toBe('');
  250. expect(profile.threadId).toBe(0);
  251. });
  252. it('derives a profile name from just thread name', () => {
  253. const sampledProfile = makeSentrySampledProfile({
  254. profile: {
  255. samples: [
  256. {
  257. stack_id: 0,
  258. elapsed_since_start_ns: 1000,
  259. thread_id: '0',
  260. },
  261. ],
  262. thread_metadata: {
  263. '0': {
  264. name: 'foo',
  265. },
  266. },
  267. },
  268. });
  269. const profile = SentrySampledProfile.FromProfile(
  270. sampledProfile,
  271. createSentrySampleProfileFrameIndex(sampledProfile.profile.frames, 'javascript'),
  272. {type: 'flamechart'}
  273. );
  274. expect(profile.name).toBe('foo');
  275. expect(profile.threadId).toBe(0);
  276. });
  277. it('derives a coca profile name from active thread id', () => {
  278. const sampledProfile = makeSentrySampledProfile({
  279. profile: {
  280. samples: [
  281. {
  282. stack_id: 0,
  283. elapsed_since_start_ns: 1000,
  284. thread_id: '0',
  285. },
  286. ],
  287. thread_metadata: {},
  288. },
  289. });
  290. const profile = SentrySampledProfile.FromProfile(
  291. sampledProfile,
  292. createSentrySampleProfileFrameIndex(sampledProfile.profile.frames, 'javascript'),
  293. {type: 'flamechart'}
  294. );
  295. expect(profile.name).toBe('com.apple.main-thread');
  296. expect(profile.threadId).toBe(0);
  297. });
  298. it('derives a coca profile name from queue label', () => {
  299. const sampledProfile = makeSentrySampledProfile({
  300. profile: {
  301. samples: [
  302. {
  303. stack_id: 0,
  304. elapsed_since_start_ns: 1000,
  305. thread_id: '1',
  306. queue_address: '0x000000016bec7180',
  307. },
  308. ],
  309. thread_metadata: {},
  310. queue_metadata: {
  311. '0x000000016bec7180': {label: 'sentry-http-transport'},
  312. },
  313. },
  314. });
  315. const profile = SentrySampledProfile.FromProfile(
  316. sampledProfile,
  317. createSentrySampleProfileFrameIndex(sampledProfile.profile.frames, 'javascript'),
  318. {type: 'flamechart'}
  319. );
  320. expect(profile.name).toBe('sentry-http-transport');
  321. expect(profile.threadId).toBe(1);
  322. });
  323. it('derives a coca profile name from queue label thats main thread', () => {
  324. const sampledProfile = makeSentrySampledProfile({
  325. transaction: {
  326. id: '',
  327. name: 'foo',
  328. active_thread_id: 1,
  329. trace_id: '1',
  330. },
  331. profile: {
  332. samples: [
  333. {
  334. stack_id: 0,
  335. elapsed_since_start_ns: 1000,
  336. thread_id: '1',
  337. queue_address: '0x000000016bec7180',
  338. },
  339. ],
  340. thread_metadata: {},
  341. queue_metadata: {
  342. '0x000000016bec7180': {label: 'com.apple.main-thread'},
  343. },
  344. },
  345. });
  346. const profile = SentrySampledProfile.FromProfile(
  347. sampledProfile,
  348. createSentrySampleProfileFrameIndex(sampledProfile.profile.frames, 'javascript'),
  349. {type: 'flamechart'}
  350. );
  351. expect(profile.name).toBe('com.apple.main-thread');
  352. expect(profile.threadId).toBe(1);
  353. });
  354. it('derives a coca profile name from queue label thats not main thread', () => {
  355. const sampledProfile = makeSentrySampledProfile({
  356. transaction: {
  357. id: '',
  358. name: 'foo',
  359. active_thread_id: 0,
  360. trace_id: '1',
  361. },
  362. profile: {
  363. samples: [
  364. {
  365. stack_id: 0,
  366. elapsed_since_start_ns: 1000,
  367. thread_id: '1',
  368. queue_address: '0x000000016bec7180',
  369. },
  370. ],
  371. thread_metadata: {},
  372. queue_metadata: {
  373. '0x000000016bec7180': {label: 'com.apple.main-thread'},
  374. },
  375. },
  376. });
  377. const profile = SentrySampledProfile.FromProfile(
  378. sampledProfile,
  379. createSentrySampleProfileFrameIndex(sampledProfile.profile.frames, 'javascript'),
  380. {type: 'flamechart'}
  381. );
  382. expect(profile.name).toBe('');
  383. expect(profile.threadId).toBe(1);
  384. });
  385. it('flamegraph tracks node occurrences', () => {
  386. const sampledProfile = makeSentrySampledProfile({
  387. transaction: {
  388. id: '',
  389. name: 'foo',
  390. active_thread_id: 1,
  391. trace_id: '1',
  392. },
  393. profile: {
  394. samples: [
  395. {
  396. stack_id: 0,
  397. elapsed_since_start_ns: 1000,
  398. thread_id: '0',
  399. },
  400. {
  401. stack_id: 1,
  402. elapsed_since_start_ns: 2000,
  403. thread_id: '0',
  404. },
  405. {
  406. stack_id: 0,
  407. elapsed_since_start_ns: 3000,
  408. thread_id: '0',
  409. },
  410. ],
  411. thread_metadata: {
  412. '0': {
  413. name: 'bar',
  414. },
  415. },
  416. // Frame 0 occurs 3 times, frame 1 occurs once
  417. stacks: [[0], [1, 0], [0]],
  418. frames: [{function: 'f0'}, {function: 'f1'}, {function: 'f2'}],
  419. },
  420. });
  421. const profile = SentrySampledProfile.FromProfile(
  422. sampledProfile,
  423. createSentrySampleProfileFrameIndex(sampledProfile.profile.frames, 'javascript'),
  424. {type: 'flamegraph'}
  425. );
  426. expect(profile.callTree.children[0].count).toBe(2);
  427. expect(profile.callTree.children[0].children[0].count).toBe(1);
  428. });
  429. it('filters frames', () => {
  430. const sampledProfile = makeSentrySampledProfile({
  431. transaction: {
  432. id: '',
  433. name: 'foo',
  434. active_thread_id: 1,
  435. trace_id: '1',
  436. },
  437. profile: {
  438. samples: [
  439. {
  440. stack_id: 0,
  441. elapsed_since_start_ns: 1000,
  442. thread_id: '0',
  443. },
  444. {
  445. stack_id: 0,
  446. elapsed_since_start_ns: 2000,
  447. thread_id: '0',
  448. },
  449. ],
  450. thread_metadata: {
  451. '0': {
  452. name: 'bar',
  453. },
  454. },
  455. stacks: [[1, 0]],
  456. frames: [{function: 'f0'}, {function: 'f1'}],
  457. },
  458. });
  459. const profile = SentrySampledProfile.FromProfile(
  460. sampledProfile,
  461. createSentrySampleProfileFrameIndex(sampledProfile.profile.frames, 'javascript'),
  462. {
  463. type: 'flamegraph',
  464. frameFilter: frame => frame.name === 'f0',
  465. }
  466. );
  467. expect(profile.callTree.frame).toBe(Frame.Root);
  468. expect(profile.callTree.children).toHaveLength(1);
  469. expect(profile.callTree.children[0].frame.name).toEqual('f0');
  470. // the f1 frame is filtered out, so the f0 frame has no children
  471. expect(profile.callTree.children[0].children).toHaveLength(0);
  472. });
  473. });