profile.spec.tsx 5.1 KB

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