jsSelfProfile.tsx 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. import {lastOfArray} from 'sentry/utils';
  2. import {CallTreeNode} from 'sentry/utils/profiling/callTreeNode';
  3. import {Frame} from 'sentry/utils/profiling/frame';
  4. import {stackMarkerToHumanReadable} from './../formatters/stackMarkerToHumanReadable';
  5. import {resolveJSSelfProfilingStack} from './../jsSelfProfiling';
  6. import {Profile} from './profile';
  7. import {createFrameIndex} from './utils';
  8. export class JSSelfProfile extends Profile {
  9. static FromProfile(
  10. profile: JSSelfProfiling.Trace,
  11. frameIndex: ReturnType<typeof createFrameIndex>
  12. ): JSSelfProfile {
  13. // In the case of JSSelfProfiling, we need to index the abstract marker frames
  14. // as they will otherwise not be present in the ProfilerStack.
  15. const markers: JSSelfProfiling.Marker[] = [
  16. 'gc',
  17. 'layout',
  18. 'other',
  19. 'paint',
  20. 'script',
  21. 'style',
  22. ];
  23. for (const marker of markers) {
  24. frameIndex[marker] = new Frame(
  25. {
  26. key: marker,
  27. name: stackMarkerToHumanReadable(marker),
  28. line: undefined,
  29. column: undefined,
  30. is_application: false,
  31. },
  32. 'web'
  33. );
  34. }
  35. const startedAt = profile.samples[0].timestamp;
  36. const endedAt = lastOfArray(profile.samples).timestamp;
  37. const jsSelfProfile = new JSSelfProfile({
  38. duration: endedAt - startedAt,
  39. startedAt,
  40. endedAt,
  41. name: 'JSSelfProfiling',
  42. unit: 'milliseconds',
  43. threadId: 0,
  44. });
  45. // Because JS self profiling takes an initial sample when we call new Profiler(),
  46. // it means that the first sample weight will always be zero. We want to append the sample with 0 weight,
  47. // because the 2nd sample may part of the first sample's stack. This way we keep the most information we can of the stack trace
  48. jsSelfProfile.appendSample(
  49. resolveJSSelfProfilingStack(
  50. profile,
  51. profile.samples[0].stackId,
  52. frameIndex,
  53. profile.samples[0].marker
  54. ),
  55. 0
  56. );
  57. // We start at stack 1, because we've already appended stack 0 above. The weight of each sample is the
  58. // difference between the current sample and the previous one.
  59. for (let i = 1; i < profile.samples.length; i++) {
  60. // When gc is triggered, the stack may be indicated as empty. In that case, the thread was not idle
  61. // and we should append gc to the top of the previous stack.
  62. // https://github.com/WICG/js-self-profiling/issues/59
  63. if (profile.samples[i].marker === 'gc') {
  64. jsSelfProfile.appendSample(
  65. resolveJSSelfProfilingStack(
  66. profile,
  67. // use the previous sample
  68. profile.samples[i - 1].stackId,
  69. frameIndex,
  70. profile.samples[i].marker
  71. ),
  72. profile.samples[i].timestamp - profile.samples[i - 1].timestamp
  73. );
  74. } else {
  75. jsSelfProfile.appendSample(
  76. resolveJSSelfProfilingStack(
  77. profile,
  78. profile.samples[i].stackId,
  79. frameIndex,
  80. profile.samples[i].marker
  81. ),
  82. profile.samples[i].timestamp - profile.samples[i - 1].timestamp
  83. );
  84. }
  85. }
  86. return jsSelfProfile.build();
  87. }
  88. appendSample(stack: Frame[], weight: number): void {
  89. this.trackSampleStats(weight);
  90. let node = this.appendOrderTree;
  91. const framesInStack: CallTreeNode[] = [];
  92. for (const frame of stack) {
  93. const last = lastOfArray(node.children);
  94. if (last && !last.isLocked() && last.frame === frame) {
  95. node = last;
  96. } else {
  97. const parent = node;
  98. node = new CallTreeNode(frame, node);
  99. parent.children.push(node);
  100. }
  101. node.addToTotalWeight(weight);
  102. // TODO: This is On^2, because we iterate over all frames in the stack to check if our
  103. // frame is a recursive frame. We could do this in O(1) by keeping a map of frames in the stack
  104. // We check the stack in a top-down order to find the first recursive frame.
  105. let stackHeight = framesInStack.length - 1;
  106. while (stackHeight >= 0) {
  107. if (framesInStack[stackHeight].frame === node.frame) {
  108. // The recursion edge is bidirectional
  109. framesInStack[stackHeight].setRecursiveThroughNode(node);
  110. node.setRecursiveThroughNode(framesInStack[stackHeight]);
  111. break;
  112. }
  113. stackHeight--;
  114. }
  115. framesInStack.push(node);
  116. }
  117. node.addToSelfWeight(weight);
  118. if (weight > 0) {
  119. this.minFrameDuration = Math.min(weight, this.minFrameDuration);
  120. }
  121. // Lock the stack node, so we make sure we dont mutate it in the future.
  122. // The samples should be ordered by timestamp when processed so we should never
  123. // iterate over them again in the future.
  124. for (const child of node.children) {
  125. child.lock();
  126. }
  127. node.frame.addToSelfWeight(weight);
  128. for (const stackNode of framesInStack) {
  129. stackNode.frame.addToTotalWeight(weight);
  130. }
  131. // If node is the same as the previous sample, add the weight to the previous sample
  132. if (node === lastOfArray(this.samples)) {
  133. this.weights[this.weights.length - 1] += weight;
  134. } else {
  135. this.samples.push(node);
  136. this.weights.push(weight);
  137. }
  138. }
  139. build(): JSSelfProfile {
  140. this.duration = Math.max(
  141. this.duration,
  142. this.weights.reduce((a, b) => a + b, 0)
  143. );
  144. // We had no frames with duration > 0, so set min duration to timeline duration
  145. // which effectively disables any zooming on the flamegraphs
  146. if (
  147. this.minFrameDuration === Number.POSITIVE_INFINITY ||
  148. this.minFrameDuration === 0
  149. ) {
  150. this.minFrameDuration = this.duration;
  151. }
  152. return this;
  153. }
  154. }