profile.tsx 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. import {lastOfArray} from 'sentry/utils';
  2. import {CallTreeNode} from '../callTreeNode';
  3. import {Frame} from '../frame';
  4. interface ProfileStats {
  5. discardedSamplesCount: number;
  6. negativeSamplesCount: number;
  7. }
  8. // This is a simplified port of speedscope's profile with a few simplifications and some removed functionality + some added functionality.
  9. // head at commit e37f6fa7c38c110205e22081560b99cb89ce885e
  10. // We should try and remove these as we adopt our own profile format and only rely on the sampled format.
  11. export class Profile {
  12. // Duration of the profile
  13. duration: number;
  14. // Started at ts of the profile - varies between implementations of the profiler.
  15. // For JS self profiles, this is the time origin (https://www.w3.org/TR/hr-time-2/#dfn-time-origin), for others it's epoch time
  16. startedAt: number;
  17. // Ended at ts of the profile - varies between implementations of the profiler.
  18. // For JS self profiles, this is the time origin (https://www.w3.org/TR/hr-time-2/#dfn-time-origin), for others it's epoch time
  19. endedAt: number;
  20. threadId: number;
  21. // Unit in which the timings are reported in
  22. unit = 'microseconds';
  23. // Name of the profile
  24. name = 'Unknown';
  25. appendOrderTree: CallTreeNode = new CallTreeNode(Frame.Root, null);
  26. framesInStack: Set<Profiling.Event['frame']> = new Set();
  27. // Min duration of the profile
  28. minFrameDuration = Number.POSITIVE_INFINITY;
  29. samples: CallTreeNode[] = [];
  30. weights: number[] = [];
  31. rawWeights: number[] = [];
  32. stats: ProfileStats = {
  33. discardedSamplesCount: 0,
  34. negativeSamplesCount: 0,
  35. };
  36. constructor({
  37. duration,
  38. startedAt,
  39. endedAt,
  40. name,
  41. unit,
  42. threadId,
  43. }: {
  44. duration: number;
  45. endedAt: number;
  46. name: string;
  47. startedAt: number;
  48. threadId: number;
  49. unit: string;
  50. }) {
  51. this.threadId = threadId;
  52. this.duration = duration;
  53. this.startedAt = startedAt;
  54. this.endedAt = endedAt;
  55. this.name = name;
  56. this.unit = unit;
  57. }
  58. static Empty = new Profile({
  59. duration: 1000,
  60. startedAt: 0,
  61. endedAt: 1000,
  62. name: 'Empty Profile',
  63. unit: 'milliseconds',
  64. threadId: 0,
  65. }).build();
  66. isEmpty(): boolean {
  67. return this === Profile.Empty;
  68. }
  69. trackSampleStats(duration: number) {
  70. // Keep track of discarded samples and ones that may have negative weights
  71. if (duration === 0) {
  72. this.stats.discardedSamplesCount++;
  73. }
  74. if (duration < 0) {
  75. this.stats.negativeSamplesCount++;
  76. }
  77. }
  78. forEach(
  79. openFrame: (node: CallTreeNode, value: number) => void,
  80. closeFrame: (node: CallTreeNode, value: number) => void
  81. ): void {
  82. let prevStack: CallTreeNode[] = [];
  83. let value = 0;
  84. let sampleIndex = 0;
  85. for (const stackTop of this.samples) {
  86. let top: CallTreeNode | null = stackTop;
  87. while (top && !top.isRoot() && prevStack.indexOf(top) === -1) {
  88. top = top.parent;
  89. }
  90. while (prevStack.length > 0 && lastOfArray(prevStack) !== top) {
  91. const node = prevStack.pop()!;
  92. closeFrame(node, value);
  93. }
  94. const toOpen: CallTreeNode[] = [];
  95. let node: CallTreeNode | null = stackTop;
  96. while (node && !node.isRoot() && node !== top) {
  97. toOpen.unshift(node);
  98. node = node.parent;
  99. }
  100. for (const toOpenNode of toOpen) {
  101. openFrame(toOpenNode, value);
  102. }
  103. prevStack = prevStack.concat(toOpen);
  104. value += this.weights[sampleIndex++];
  105. }
  106. for (let i = prevStack.length - 1; i >= 0; i--) {
  107. closeFrame(prevStack[i], value);
  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. }