jsSelfProfile.tsx 6.0 KB

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