jsSelfProfile.tsx 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  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. endedAt - startedAt,
  39. startedAt,
  40. endedAt,
  41. 'JSSelfProfiling',
  42. 'milliseconds',
  43. 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. jsSelfProfile.appendSample(
  61. resolveJSSelfProfilingStack(
  62. profile,
  63. profile.samples[i].stackId,
  64. frameIndex,
  65. profile.samples[i].marker
  66. ),
  67. profile.samples[i].timestamp - profile.samples[i - 1].timestamp
  68. );
  69. }
  70. return jsSelfProfile.build();
  71. }
  72. appendSample(stack: Frame[], weight: number): void {
  73. let node = this.appendOrderTree;
  74. const framesInStack: CallTreeNode[] = [];
  75. for (const frame of stack) {
  76. const last = lastOfArray(node.children);
  77. if (last && !last.isLocked() && last.frame === frame) {
  78. node = last;
  79. } else {
  80. const parent = node;
  81. node = new CallTreeNode(frame, node);
  82. parent.children.push(node);
  83. }
  84. node.addToTotalWeight(weight);
  85. // TODO: This is On^2, because we iterate over all frames in the stack to check if our
  86. // frame is a recursive frame. We could do this in O(1) by keeping a map of frames in the stack
  87. // We check the stack in a top-down order to find the first recursive frame.
  88. let stackHeight = framesInStack.length - 1;
  89. while (stackHeight >= 0) {
  90. if (framesInStack[stackHeight].frame === node.frame) {
  91. // The recursion edge is bidirectional
  92. framesInStack[stackHeight].setRecursive(node);
  93. node.setRecursive(framesInStack[stackHeight]);
  94. break;
  95. }
  96. stackHeight--;
  97. }
  98. framesInStack.push(node);
  99. }
  100. node.addToSelfWeight(weight);
  101. if (weight > 0) {
  102. this.minFrameDuration = Math.min(weight, this.minFrameDuration);
  103. }
  104. // Lock the stack node, so we make sure we dont mutate it in the future.
  105. // The samples should be ordered by timestamp when processed so we should never
  106. // iterate over them again in the future.
  107. for (const child of node.children) {
  108. child.lock();
  109. }
  110. node.frame.addToSelfWeight(weight);
  111. for (const stackNode of framesInStack) {
  112. stackNode.frame.addToTotalWeight(weight);
  113. }
  114. // If node is the same as the previous sample, add the weight to the previous sample
  115. if (node === lastOfArray(this.samples)) {
  116. this.weights[this.weights.length - 1] += weight;
  117. } else {
  118. this.samples.push(node);
  119. this.weights.push(weight);
  120. }
  121. }
  122. build(): JSSelfProfile {
  123. this.duration = Math.max(
  124. this.duration,
  125. this.weights.reduce((a, b) => a + b, 0)
  126. );
  127. // We had no frames with duration > 0, so set min duration to timeline duration
  128. // which effectively disables any zooming on the flamegraphs
  129. if (
  130. this.minFrameDuration === Number.POSITIVE_INFINITY ||
  131. this.minFrameDuration === 0
  132. ) {
  133. this.minFrameDuration = this.duration;
  134. }
  135. return this;
  136. }
  137. }