eventedProfile.tsx 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. import {lastOfArray} from 'sentry/utils';
  2. import {CallTreeNode} from 'sentry/utils/profiling/callTreeNode';
  3. import {Frame} from 'sentry/utils/profiling/frame';
  4. import {Profile} from './profile';
  5. import {createFrameIndex} from './utils';
  6. export class EventedProfile extends Profile {
  7. appendOrderStack: CallTreeNode[] = [this.appendOrderTree];
  8. stack: Frame[] = [];
  9. lastValue = 0;
  10. static FromProfile(
  11. eventedProfile: Profiling.EventedProfile,
  12. frameIndex: ReturnType<typeof createFrameIndex>
  13. ): EventedProfile {
  14. const profile = new EventedProfile({
  15. duration: eventedProfile.endValue - eventedProfile.startValue,
  16. startedAt: eventedProfile.startValue,
  17. endedAt: eventedProfile.endValue,
  18. name: eventedProfile.name,
  19. unit: eventedProfile.unit,
  20. threadId: eventedProfile.threadID,
  21. });
  22. // If frames are offset, we need to set lastValue to profile start, so that delta between
  23. // samples is correctly offset by the start value.
  24. profile.lastValue = Math.max(0, eventedProfile.startValue);
  25. for (const event of eventedProfile.events) {
  26. const frame = frameIndex[event.frame];
  27. if (!frame) {
  28. throw new Error(`Cannot retrieve event: ${event.frame} from frame index`);
  29. }
  30. switch (event.type) {
  31. // Open a new frame
  32. case 'O': {
  33. profile.enterFrame(frame, event.at);
  34. break;
  35. }
  36. // Close a frame
  37. case 'C': {
  38. profile.leaveFrame(frame, event.at);
  39. break;
  40. }
  41. default: {
  42. throw new TypeError(`Unknown event type ${event.type}`);
  43. }
  44. }
  45. }
  46. return profile.build();
  47. }
  48. addWeightToFrames(weight: number): void {
  49. const weightDelta = weight - this.lastValue;
  50. for (const frame of this.stack) {
  51. frame.addToTotalWeight(weightDelta);
  52. }
  53. const top = lastOfArray(this.stack);
  54. if (top) {
  55. top.addToSelfWeight(weight);
  56. }
  57. }
  58. addWeightsToNodes(value: number) {
  59. const delta = value - this.lastValue;
  60. for (const node of this.appendOrderStack) {
  61. node.addToTotalWeight(delta);
  62. }
  63. const stackTop = lastOfArray(this.appendOrderStack);
  64. if (stackTop) {
  65. stackTop.addToSelfWeight(delta);
  66. }
  67. }
  68. recordRawWeight(at: number) {
  69. const weight = at - this.lastValue;
  70. if (weight > 0) {
  71. this.rawWeights.push(weight);
  72. }
  73. }
  74. enterFrame(frame: Frame, at: number): void {
  75. this.addWeightToFrames(at);
  76. this.addWeightsToNodes(at);
  77. this.recordRawWeight(at);
  78. const lastTop = lastOfArray(this.appendOrderStack);
  79. if (lastTop) {
  80. const sampleDelta = at - this.lastValue;
  81. if (sampleDelta < 0) {
  82. throw new Error(
  83. 'Sample delta cannot be negative, samples may be corrupt or out of order'
  84. );
  85. }
  86. // If the sample timestamp is not the same as the same as of previous frame,
  87. // we can deduce that this is a new sample and need to push it on the stack
  88. if (sampleDelta > 0) {
  89. this.samples.push(lastTop);
  90. this.weights.push(sampleDelta);
  91. }
  92. const last = lastOfArray(lastTop.children);
  93. let node: CallTreeNode;
  94. if (last && !last.isLocked() && last.frame === frame) {
  95. node = last;
  96. } else {
  97. node = new CallTreeNode(frame, lastTop);
  98. lastTop.children.push(node);
  99. }
  100. // TODO: This is On^2, because we iterate over all frames in the stack to check if our
  101. // frame is a recursive frame. We could do this in O(1) by keeping a map of frames in the stack with their respective indexes
  102. // We check the stack in a top-down order to find the first recursive frame.
  103. let start = this.appendOrderStack.length - 1;
  104. while (start >= 0) {
  105. if (this.appendOrderStack[start].frame === node.frame) {
  106. // The recursion edge is bidirectional
  107. this.appendOrderStack[start].setRecursiveThroughNode(node);
  108. node.setRecursiveThroughNode(this.appendOrderStack[start]);
  109. break;
  110. }
  111. start--;
  112. }
  113. this.appendOrderStack.push(node);
  114. }
  115. this.stack.push(frame);
  116. this.lastValue = at;
  117. }
  118. leaveFrame(_event: Frame, at: number): void {
  119. this.addWeightToFrames(at);
  120. this.addWeightsToNodes(at);
  121. this.trackSampleStats(at);
  122. this.recordRawWeight(at);
  123. const leavingStackTop = this.appendOrderStack.pop();
  124. if (leavingStackTop === undefined) {
  125. throw new Error('Unbalanced stack');
  126. }
  127. // Lock the stack node, so we make sure we dont mutate it in the future.
  128. // The samples should be ordered by timestamp when processed so we should never
  129. // iterate over them again in the future.
  130. leavingStackTop.lock();
  131. const sampleDelta = at - this.lastValue;
  132. if (sampleDelta > 0) {
  133. this.samples.push(leavingStackTop);
  134. this.weights.push(sampleDelta);
  135. // Keep track of the minFrameDuration
  136. this.minFrameDuration = Math.min(sampleDelta, this.minFrameDuration);
  137. }
  138. this.stack.pop();
  139. this.lastValue = at;
  140. }
  141. build(): EventedProfile {
  142. if (this.appendOrderStack.length > 1) {
  143. throw new Error('Unbalanced append order stack');
  144. }
  145. this.duration = Math.max(
  146. this.duration,
  147. this.weights.reduce((a, b) => a + b, 0)
  148. );
  149. // We had no frames with duration > 0, so set min duration to timeline duration
  150. // which effectively disables any zooming on the flamegraphs
  151. if (
  152. this.minFrameDuration === Number.POSITIVE_INFINITY ||
  153. this.minFrameDuration === 0
  154. ) {
  155. this.minFrameDuration = this.duration;
  156. }
  157. return this;
  158. }
  159. }