sentrySampledProfile.tsx 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. import {lastOfArray} from 'sentry/utils';
  2. import {CallTreeNode} from 'sentry/utils/profiling/callTreeNode';
  3. import {Frame} from './../frame';
  4. import {Profile} from './profile';
  5. import {createSentrySampleProfileFrameIndex} from './utils';
  6. // This is a simplified port of speedscope's profile with a few simplifications and some removed functionality.
  7. // head at commit e37f6fa7c38c110205e22081560b99cb89ce885e
  8. // We should try and remove these as we adopt our own profile format and only rely on the sampled format.
  9. export class SentrySampledProfile extends Profile {
  10. static FromProfile(
  11. sampledProfile: Profiling.SentrySampledProfile,
  12. frameIndex: ReturnType<typeof createSentrySampleProfileFrameIndex>
  13. ): Profile {
  14. const {samples, stacks, thread_metadata = {}} = sampledProfile.profile;
  15. const startedAt = parseInt(samples[0].elapsed_since_start_ns, 10);
  16. const endedAt = parseInt(samples[samples.length - 1].elapsed_since_start_ns, 10);
  17. if (Number.isNaN(startedAt) || Number.isNaN(endedAt)) {
  18. throw TypeError('startedAt or endedAt is NaN');
  19. }
  20. const threadId = parseInt(samples[0].thread_id, 10);
  21. const threadName = `thread: ${
  22. thread_metadata[samples[0].thread_id]?.name || threadId
  23. }`;
  24. const profileTransactionName = sampledProfile.transactions?.[0]?.name;
  25. const profile = new SentrySampledProfile({
  26. duration: endedAt - startedAt,
  27. startedAt,
  28. endedAt,
  29. unit: 'nanoseconds',
  30. name: profileTransactionName
  31. ? `${profileTransactionName} (${threadName})`
  32. : threadName,
  33. threadId,
  34. });
  35. let previousSampleWeight = 0;
  36. for (let i = 0; i < samples.length; i++) {
  37. const sample = samples[i];
  38. const stack = stacks[sample.stack_id];
  39. const sampleWeight = parseInt(sample.elapsed_since_start_ns, 10);
  40. profile.appendSampleWithWeight(
  41. stack.map(n => {
  42. if (!frameIndex[n]) {
  43. throw new Error(`Could not resolve frame ${n} in frame index`);
  44. }
  45. return frameIndex[n];
  46. }),
  47. sampleWeight - previousSampleWeight
  48. );
  49. previousSampleWeight = sampleWeight;
  50. }
  51. return profile.build();
  52. }
  53. appendSampleWithWeight(stack: Frame[], weight: number): void {
  54. // Keep track of discarded samples and ones that may have negative weights
  55. this.trackSampleStats(weight);
  56. // Ignore samples with 0 weight
  57. if (weight === 0) {
  58. return;
  59. }
  60. let node = this.appendOrderTree;
  61. const framesInStack: CallTreeNode[] = [];
  62. // frames are ordered outermost -> innermost so we have to iterate backward
  63. for (let i = stack.length - 1; i >= 0; i--) {
  64. const frame = stack[i];
  65. const last = lastOfArray(node.children);
  66. // Find common frame between two stacks
  67. if (last && !last.isLocked() && last.frame === frame) {
  68. node = last;
  69. } else {
  70. const parent = node;
  71. node = new CallTreeNode(frame, node);
  72. parent.children.push(node);
  73. }
  74. node.addToTotalWeight(weight);
  75. // TODO: This is On^2, because we iterate over all frames in the stack to check if our
  76. // frame is a recursive frame. We could do this in O(1) by keeping a map of frames in the stack
  77. // We check the stack in a top-down order to find the first recursive frame.
  78. let start = framesInStack.length - 1;
  79. while (start >= 0) {
  80. if (framesInStack[start].frame === node.frame) {
  81. // The recursion edge is bidirectional
  82. framesInStack[start].setRecursiveThroughNode(node);
  83. node.setRecursiveThroughNode(framesInStack[start]);
  84. break;
  85. }
  86. start--;
  87. }
  88. framesInStack.push(node);
  89. }
  90. node.addToSelfWeight(weight);
  91. this.minFrameDuration = Math.min(weight, this.minFrameDuration);
  92. // Lock the stack node, so we make sure we dont mutate it in the future.
  93. // The samples should be ordered by timestamp when processed so we should never
  94. // iterate over them again in the future.
  95. for (const child of node.children) {
  96. child.lock();
  97. }
  98. node.frame.addToSelfWeight(weight);
  99. for (const stackNode of framesInStack) {
  100. stackNode.frame.addToTotalWeight(weight);
  101. }
  102. // If node is the same as the previous sample, add the weight to the previous sample
  103. if (node === lastOfArray(this.samples)) {
  104. this.weights[this.weights.length - 1] += weight;
  105. } else {
  106. this.samples.push(node);
  107. this.weights.push(weight);
  108. }
  109. }
  110. build(): Profile {
  111. this.duration = Math.max(
  112. this.duration,
  113. this.weights.reduce((a, b) => a + b, 0)
  114. );
  115. // We had no frames with duration > 0, so set min duration to timeline duration
  116. // which effectively disables any zooming on the flamegraphs
  117. if (
  118. this.minFrameDuration === Number.POSITIVE_INFINITY ||
  119. this.minFrameDuration === 0
  120. ) {
  121. this.minFrameDuration = this.duration;
  122. }
  123. return this;
  124. }
  125. }