sampledProfile.tsx 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  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 {createFrameIndex} 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 SampledProfile extends Profile {
  10. static FromProfile(
  11. sampledProfile: Profiling.SampledProfile,
  12. frameIndex: ReturnType<typeof createFrameIndex>
  13. ): Profile {
  14. const profile = new SampledProfile({
  15. duration: sampledProfile.endValue - sampledProfile.startValue,
  16. startedAt: sampledProfile.startValue,
  17. endedAt: sampledProfile.endValue,
  18. name: sampledProfile.name,
  19. unit: sampledProfile.unit,
  20. threadId: sampledProfile.threadID,
  21. });
  22. if (sampledProfile.samples.length !== sampledProfile.weights.length) {
  23. throw new Error(
  24. `Expected samples.length (${sampledProfile.samples.length}) to equal weights.length (${sampledProfile.weights.length})`
  25. );
  26. }
  27. for (let i = 0; i < sampledProfile.samples.length; i++) {
  28. const stack = sampledProfile.samples[i];
  29. const weight = sampledProfile.weights[i];
  30. profile.appendSampleWithWeight(
  31. stack.map(n => {
  32. if (!frameIndex[n]) {
  33. throw new Error(`Could not resolve frame ${n} in frame index`);
  34. }
  35. return frameIndex[n];
  36. }),
  37. weight
  38. );
  39. }
  40. return profile.build();
  41. }
  42. appendSampleWithWeight(stack: Frame[], weight: number): void {
  43. // Keep track of discarded samples and ones that may have negative weights
  44. this.trackSampleStats(weight);
  45. // Ignore samples with 0 weight
  46. if (weight === 0) {
  47. return;
  48. }
  49. let node = this.appendOrderTree;
  50. const framesInStack: CallTreeNode[] = [];
  51. for (const frame of stack) {
  52. const last = lastOfArray(node.children);
  53. // Find common frame between two stacks
  54. if (last && !last.isLocked() && last.frame === frame) {
  55. node = last;
  56. } else {
  57. const parent = node;
  58. node = new CallTreeNode(frame, node);
  59. parent.children.push(node);
  60. }
  61. node.addToTotalWeight(weight);
  62. // TODO: This is On^2, because we iterate over all frames in the stack to check if our
  63. // frame is a recursive frame. We could do this in O(1) by keeping a map of frames in the stack
  64. // We check the stack in a top-down order to find the first recursive frame.
  65. let start = framesInStack.length - 1;
  66. while (start >= 0) {
  67. if (framesInStack[start].frame === node.frame) {
  68. // The recursion edge is bidirectional
  69. framesInStack[start].setRecursiveThroughNode(node);
  70. node.setRecursiveThroughNode(framesInStack[start]);
  71. break;
  72. }
  73. start--;
  74. }
  75. framesInStack.push(node);
  76. }
  77. node.addToSelfWeight(weight);
  78. this.minFrameDuration = Math.min(weight, this.minFrameDuration);
  79. // Lock the stack node, so we make sure we dont mutate it in the future.
  80. // The samples should be ordered by timestamp when processed so we should never
  81. // iterate over them again in the future.
  82. for (const child of node.children) {
  83. child.lock();
  84. }
  85. node.frame.addToSelfWeight(weight);
  86. for (const stackNode of framesInStack) {
  87. stackNode.frame.addToTotalWeight(weight);
  88. }
  89. // If node is the same as the previous sample, add the weight to the previous sample
  90. if (node === lastOfArray(this.samples)) {
  91. this.weights[this.weights.length - 1] += weight;
  92. } else {
  93. this.samples.push(node);
  94. this.weights.push(weight);
  95. }
  96. this.rawWeights.push(weight);
  97. }
  98. build(): Profile {
  99. this.duration = Math.max(
  100. this.duration,
  101. this.weights.reduce((a, b) => a + b, 0)
  102. );
  103. // We had no frames with duration > 0, so set min duration to timeline duration
  104. // which effectively disables any zooming on the flamegraphs
  105. if (
  106. this.minFrameDuration === Number.POSITIVE_INFINITY ||
  107. this.minFrameDuration === 0
  108. ) {
  109. this.minFrameDuration = this.duration;
  110. }
  111. return this;
  112. }
  113. }