profile.spec.tsx 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. import {CallTreeNode} from 'sentry/utils/profiling/callTreeNode';
  2. import {Frame} from 'sentry/utils/profiling/frame';
  3. import {Profile} from 'sentry/utils/profiling/profile/profile';
  4. // Test utils to keep the tests code dry
  5. export const f = (name: string, key: number) =>
  6. new Frame({name, key, is_application: false});
  7. export const c = (fr: Frame) => new CallTreeNode(fr, null);
  8. export const firstCallee = (node: CallTreeNode) => node.children[0];
  9. export const nthCallee = (node: CallTreeNode, n: number) => {
  10. const child = node.children[n];
  11. if (!child) {
  12. throw new Error('Child not found');
  13. }
  14. return child;
  15. };
  16. export const makeTestingBoilerplate = () => {
  17. const timings: [Frame['name'], string][] = [];
  18. const openSpy = jest.fn();
  19. const closeSpy = jest.fn();
  20. // We need to wrap the spy fn because they are not allowed to reference external variables
  21. const open = (node, value) => {
  22. timings.push([node.frame.name, 'open']);
  23. openSpy(node, value);
  24. };
  25. // We need to wrap the spy fn because they are not allowed to reference external variables
  26. const close = (node, val) => {
  27. timings.push([node.frame.name, 'close']);
  28. closeSpy(node, val);
  29. };
  30. return {open, close, timings, openSpy, closeSpy};
  31. };
  32. // Since it's easy to make mistakes or accidentally assign parents to the wrong nodes, this utility fn
  33. // will format the stack samples as a tree string so it's more human friendly.
  34. // @ts-ignore this is a helper fn
  35. export const _logExpectedStack = (samples: Profile['samples']): string => {
  36. const head = `
  37. Samples follow a top-down chronological order\n\n`;
  38. const tail = `\n
  39. ----------------------->
  40. stack top -> stack bottom`;
  41. const final: string[] = [];
  42. const visit = (node: CallTreeNode, str: string[]) => {
  43. str.push(`${node.frame.name}`);
  44. if (node.parent) {
  45. visit(node.parent, str);
  46. }
  47. };
  48. for (const stackTop of samples) {
  49. const str = [];
  50. visit(stackTop, str);
  51. final.push(str.join(' -> '));
  52. }
  53. return `${head}${final.join('\n')}${tail}`;
  54. };
  55. describe('Profile', () => {
  56. it('Empty profile duration is not infinity', () => {
  57. const profile = Profile.Empty;
  58. expect(profile.duration).toEqual(1000);
  59. expect(profile.minFrameDuration).toEqual(1000);
  60. });
  61. it('forEach - iterates over a single sample', () => {
  62. const profile = new Profile({
  63. duration: 1000,
  64. startedAt: 0,
  65. endedAt: 1000,
  66. name: 'profile',
  67. unit: 'ms',
  68. threadId: 0,
  69. });
  70. // Frames
  71. const f0 = f('f0', 0);
  72. const f1 = f('f1', 1);
  73. // Call tree nodes
  74. const s0 = c(f0);
  75. const s1 = c(f1);
  76. s0.parent = s1;
  77. profile.samples = [s0];
  78. const {open, close, openSpy, closeSpy, timings} = makeTestingBoilerplate();
  79. profile.forEach(open, close);
  80. expect(timings).toEqual([
  81. ['f1', 'open'],
  82. ['f0', 'open'],
  83. ['f0', 'close'],
  84. ['f1', 'close'],
  85. ]);
  86. expect(openSpy).toHaveBeenCalledTimes(2);
  87. expect(closeSpy).toHaveBeenCalledTimes(2);
  88. });
  89. it('forEach - opens new frames when stack is shared', () => {
  90. const profile = new Profile({
  91. duration: 1000,
  92. startedAt: 0,
  93. endedAt: 1000,
  94. name: 'profile',
  95. unit: 'ms',
  96. threadId: 0,
  97. });
  98. // Frames
  99. const f0 = f('f0', 0);
  100. const f1 = f('f1', 1);
  101. const f2 = f('f2', 1);
  102. // Call tree nodes
  103. const s0 = c(f0);
  104. const s1 = c(f1);
  105. const s2 = c(f2);
  106. s1.parent = s0;
  107. s2.parent = s1;
  108. profile.samples = [s0, s1, s2];
  109. const {open, close, openSpy, closeSpy, timings} = makeTestingBoilerplate();
  110. profile.forEach(open, close);
  111. expect(timings).toEqual([
  112. ['f0', 'open'],
  113. ['f1', 'open'],
  114. ['f2', 'open'],
  115. ['f2', 'close'],
  116. ['f1', 'close'],
  117. ['f0', 'close'],
  118. ]);
  119. expect(openSpy).toHaveBeenCalledTimes(3);
  120. expect(closeSpy).toHaveBeenCalledTimes(3);
  121. });
  122. it('forEach - closes frames one by one when stack is shared', () => {
  123. const profile = new Profile({
  124. duration: 1000,
  125. startedAt: 0,
  126. endedAt: 1000,
  127. name: 'profile',
  128. unit: 'ms',
  129. threadId: 0,
  130. });
  131. // Instantiate frames
  132. const f0 = f('f0', 0);
  133. const f1 = f('f1', 1);
  134. const f2 = f('f2', 2);
  135. // Instantiate call tree nodes
  136. const s0 = c(f2);
  137. const s1 = c(f1);
  138. const s2 = c(f0);
  139. profile.samples = [s0, s1, s2];
  140. const {open, close, openSpy, closeSpy, timings} = makeTestingBoilerplate();
  141. profile.forEach(open, close);
  142. expect(timings).toEqual([
  143. ['f2', 'open'],
  144. ['f2', 'close'],
  145. ['f1', 'open'],
  146. ['f1', 'close'],
  147. ['f0', 'open'],
  148. ['f0', 'close'],
  149. ]);
  150. expect(openSpy).toHaveBeenCalledTimes(3);
  151. expect(closeSpy).toHaveBeenCalledTimes(3);
  152. });
  153. // In JS land, the stack can be idle which is not the case in other runtimes, e.g. in mobile
  154. // the program main is always running, so make sure we support "holes" in the samples
  155. it('forEach - supports an idle stack', () => {
  156. const profile = new Profile({
  157. duration: 1000,
  158. startedAt: 0,
  159. endedAt: 1000,
  160. name: 'profile',
  161. unit: 'ms',
  162. threadId: 0,
  163. });
  164. // Instantiate frames
  165. const f0 = f('f0', 0);
  166. // Instantiate call tree nodes
  167. const s0 = c(f0);
  168. const s1 = c(Frame.Root);
  169. profile.samples = [s0, s1, s0];
  170. const {open, close, openSpy, closeSpy, timings} = makeTestingBoilerplate();
  171. profile.forEach(open, close);
  172. expect(timings).toEqual([
  173. ['f0', 'open'],
  174. ['f0', 'close'],
  175. ['f0', 'open'],
  176. ['f0', 'close'],
  177. ]);
  178. expect(openSpy).toHaveBeenCalledTimes(2);
  179. expect(closeSpy).toHaveBeenCalledTimes(2);
  180. });
  181. });