profile.spec.tsx 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  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, in_app: boolean = true) =>
  6. new Frame({name, key, is_application: in_app});
  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. export const _logExpectedStack = (samples: Profile['samples']): string => {
  35. const head = `
  36. Samples follow a top-down chronological order\n\n`;
  37. const tail = `\n
  38. ----------------------->
  39. stack top -> stack bottom`;
  40. const final: string[] = [];
  41. const visit = (node: CallTreeNode, str: string[]) => {
  42. str.push(`${node.frame.name}`);
  43. if (node.parent) {
  44. visit(node.parent, str);
  45. }
  46. };
  47. for (const stackTop of samples) {
  48. const str = [];
  49. visit(stackTop, str);
  50. final.push(str.join(' -> '));
  51. }
  52. return `${head}${final.join('\n')}${tail}`;
  53. };
  54. describe('Profile', () => {
  55. it('Empty profile duration is not infinity', () => {
  56. const profile = Profile.Empty;
  57. expect(profile.duration).toEqual(1000);
  58. expect(profile.minFrameDuration).toEqual(1000);
  59. });
  60. it('forEach - iterates over a single sample', () => {
  61. const profile = new Profile({
  62. duration: 1000,
  63. startedAt: 0,
  64. endedAt: 1000,
  65. name: 'profile',
  66. unit: 'ms',
  67. threadId: 0,
  68. type: 'flamechart',
  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. type: 'flamechart',
  98. });
  99. // Frames
  100. const f0 = f('f0', 0);
  101. const f1 = f('f1', 1);
  102. const f2 = f('f2', 1);
  103. // Call tree nodes
  104. const s0 = c(f0);
  105. const s1 = c(f1);
  106. const s2 = c(f2);
  107. s1.parent = s0;
  108. s2.parent = s1;
  109. profile.samples = [s0, s1, s2];
  110. const {open, close, openSpy, closeSpy, timings} = makeTestingBoilerplate();
  111. profile.forEach(open, close);
  112. expect(timings).toEqual([
  113. ['f0', 'open'],
  114. ['f1', 'open'],
  115. ['f2', 'open'],
  116. ['f2', 'close'],
  117. ['f1', 'close'],
  118. ['f0', 'close'],
  119. ]);
  120. expect(openSpy).toHaveBeenCalledTimes(3);
  121. expect(closeSpy).toHaveBeenCalledTimes(3);
  122. });
  123. it('forEach - closes frames one by one when stack is shared', () => {
  124. const profile = new Profile({
  125. duration: 1000,
  126. startedAt: 0,
  127. endedAt: 1000,
  128. name: 'profile',
  129. unit: 'ms',
  130. threadId: 0,
  131. type: 'flamechart',
  132. });
  133. // Instantiate frames
  134. const f0 = f('f0', 0);
  135. const f1 = f('f1', 1);
  136. const f2 = f('f2', 2);
  137. // Instantiate call tree nodes
  138. const s0 = c(f2);
  139. const s1 = c(f1);
  140. const s2 = c(f0);
  141. profile.samples = [s0, s1, s2];
  142. const {open, close, openSpy, closeSpy, timings} = makeTestingBoilerplate();
  143. profile.forEach(open, close);
  144. expect(timings).toEqual([
  145. ['f2', 'open'],
  146. ['f2', 'close'],
  147. ['f1', 'open'],
  148. ['f1', 'close'],
  149. ['f0', 'open'],
  150. ['f0', 'close'],
  151. ]);
  152. expect(openSpy).toHaveBeenCalledTimes(3);
  153. expect(closeSpy).toHaveBeenCalledTimes(3);
  154. });
  155. // In JS land, the stack can be idle which is not the case in other runtimes, e.g. in mobile
  156. // the program main is always running, so make sure we support "holes" in the samples
  157. it('forEach - supports an idle stack', () => {
  158. const profile = new Profile({
  159. duration: 1000,
  160. startedAt: 0,
  161. endedAt: 1000,
  162. name: 'profile',
  163. unit: 'ms',
  164. threadId: 0,
  165. type: 'flamechart',
  166. });
  167. // Instantiate frames
  168. const f0 = f('f0', 0);
  169. // Instantiate call tree nodes
  170. const s0 = c(f0);
  171. const s1 = c(Frame.Root);
  172. profile.samples = [s0, s1, s0];
  173. const {open, close, openSpy, closeSpy, timings} = makeTestingBoilerplate();
  174. profile.forEach(open, close);
  175. expect(timings).toEqual([
  176. ['f0', 'open'],
  177. ['f0', 'close'],
  178. ['f0', 'open'],
  179. ['f0', 'close'],
  180. ]);
  181. expect(openSpy).toHaveBeenCalledTimes(2);
  182. expect(closeSpy).toHaveBeenCalledTimes(2);
  183. });
  184. });