sampledProfile.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  1. import {CallTreeNode} from 'sentry/utils/profiling/callTreeNode';
  2. import {Frame} from './../frame';
  3. import {Profile} from './profile';
  4. import {createFrameIndex, resolveFlamegraphSamplesProfileIds} from './utils';
  5. function sortStacks(
  6. a: {stack: number[]; weight: number},
  7. b: {stack: number[]; weight: number}
  8. ) {
  9. const max = Math.max(a.stack.length, b.stack.length);
  10. for (let i = 0; i < max; i++) {
  11. if (a.stack[i] === undefined) {
  12. return -1;
  13. }
  14. if (b.stack[i] === undefined) {
  15. return 1;
  16. }
  17. if (a.stack[i] === b.stack[i]) {
  18. continue;
  19. }
  20. return a.stack[i] - b.stack[i];
  21. }
  22. return 0;
  23. }
  24. function stacksWithWeights(
  25. profile: Readonly<Profiling.SampledProfile>,
  26. profileIds: Readonly<string[][]> = [],
  27. frameFilter?: (i: number) => boolean
  28. ) {
  29. return profile.samples.map((stack, index) => {
  30. return {
  31. stack: frameFilter ? stack.filter(frameFilter) : stack,
  32. weight: profile.weights[index],
  33. profileIds: profileIds[index],
  34. };
  35. });
  36. }
  37. function sortSamples(
  38. profile: Readonly<Profiling.SampledProfile>,
  39. profileIds: Readonly<string[][]> = [],
  40. frameFilter?: (i: number) => boolean
  41. ): {stack: number[]; weight: number}[] {
  42. return stacksWithWeights(profile, profileIds, frameFilter).sort(sortStacks);
  43. }
  44. // We should try and remove these as we adopt our own profile format and only rely on the sampled format.
  45. export class SampledProfile extends Profile {
  46. static FromProfile(
  47. sampledProfile: Profiling.SampledProfile,
  48. frameIndex: ReturnType<typeof createFrameIndex>,
  49. options: {
  50. type: 'flamechart' | 'flamegraph';
  51. frameFilter?: (frame: Frame) => boolean;
  52. profileIds?: Readonly<string[]>;
  53. }
  54. ): Profile {
  55. const profile = new SampledProfile({
  56. duration: sampledProfile.endValue - sampledProfile.startValue,
  57. startedAt: sampledProfile.startValue,
  58. endedAt: sampledProfile.endValue,
  59. name: sampledProfile.name,
  60. unit: sampledProfile.unit,
  61. threadId: sampledProfile.threadID,
  62. type: options.type,
  63. });
  64. if (sampledProfile.samples.length !== sampledProfile.weights.length) {
  65. throw new Error(
  66. `Expected samples.length (${sampledProfile.samples.length}) to equal weights.length (${sampledProfile.weights.length})`
  67. );
  68. }
  69. let resolvedProfileIds: string[][] = [];
  70. if (
  71. options.type === 'flamegraph' &&
  72. sampledProfile.samples_profiles &&
  73. options.profileIds
  74. ) {
  75. resolvedProfileIds = resolveFlamegraphSamplesProfileIds(
  76. sampledProfile.samples_profiles,
  77. options.profileIds
  78. );
  79. }
  80. function resolveFrame(index) {
  81. const resolvedFrame = frameIndex[index];
  82. if (!resolvedFrame) {
  83. throw new Error(`Could not resolve frame ${index} in frame index`);
  84. }
  85. return resolvedFrame;
  86. }
  87. const samples =
  88. options.type === 'flamegraph'
  89. ? sortSamples(
  90. sampledProfile,
  91. resolvedProfileIds,
  92. options.frameFilter ? i => options.frameFilter!(resolveFrame(i)) : undefined
  93. )
  94. : stacksWithWeights(sampledProfile);
  95. // We process each sample in the profile while maintaining a resolved stack of frames.
  96. // There is a special case for GC frames where they are appended on top of the previos stack.
  97. // By reusing the stack buffer, we avoid creating a new array for each sample and for GC case,
  98. // also avoid re-resolving the previous stack frames as they are already in the buffer.
  99. // If we encounter multiple consecutive GC frames, we will merge them into a single sample
  100. // and sum their weights so that only one of them is processed.
  101. // After we resolve the stack, we call appendSampleWithWeight with the stack buffer, weight
  102. // and size of the stack to process. The size indicates how many items from the buffer we want
  103. // to process.
  104. const resolvedStack: Frame[] = new Array(256); // stack size limit
  105. let size = 0;
  106. let frame: Frame | null = null;
  107. for (let i = 0; i < samples.length; i++) {
  108. const stack = samples[i].stack;
  109. let weight = samples[i].weight;
  110. const isGCStack =
  111. options.type === 'flamechart' &&
  112. i > 0 &&
  113. // We check for size <= 2 because we have so far only seen node profiles
  114. // where GC is either marked as the root node or is directly under the root node.
  115. // There is a good chance that this logic will at some point live on the backend
  116. // and when that happens, we do not want to enter this case as the GC will already
  117. // be placed at the top of the previous stack and the new stack length will be > 2
  118. stack.length <= 2 &&
  119. frameIndex[stack[stack.length - 1]]?.name === '(garbage collector) [native code]';
  120. if (isGCStack) {
  121. // The next stack we will process will be the previous stack + our new gc frame.
  122. // We write the GC frame on top of the previous stack and set the size to the new stack length.
  123. frame = resolveFrame(stack[stack.length - 1]);
  124. if (frame) {
  125. resolvedStack[samples[i - 1].stack.length] =
  126. frameIndex[stack[stack.length - 1]];
  127. size += 1; // size of previous stack + new gc frame
  128. // Now collect all weights of all the consecutive gc frames and skip the samples
  129. while (
  130. samples[i + 1] &&
  131. // We check for size <= 2 because we have so far only seen node profiles
  132. // where GC is either marked as the root node or is directly under the root node.
  133. // There is a good chance that this logic will at some point live on the backend
  134. // and when that happens, we do not want to enter this case as the GC will already
  135. // be placed at the top of the previous stack and the new stack length will be > 2
  136. samples[i + 1].stack.length <= 2 &&
  137. frameIndex[samples[i + 1].stack[samples[i + 1].stack.length - 1]]?.name ===
  138. '(garbage collector) [native code]'
  139. ) {
  140. weight += samples[++i].weight;
  141. }
  142. }
  143. } else {
  144. size = 0;
  145. // If we are using the current stack, then we need to resolve the frames,
  146. // else the processed frames will be the frames that were previously resolved
  147. for (let j = 0; j < stack.length; j++) {
  148. frame = resolveFrame(stack[j]);
  149. if (!frame) {
  150. continue;
  151. }
  152. resolvedStack[size++] = frame;
  153. }
  154. }
  155. profile.appendSampleWithWeight(resolvedStack, weight, size, resolvedProfileIds[i]);
  156. }
  157. return profile.build();
  158. }
  159. static Example = SampledProfile.FromProfile(
  160. {
  161. startValue: 0,
  162. endValue: 1000,
  163. name: 'Example sampled profile',
  164. threadID: 0,
  165. unit: 'millisecond',
  166. weights: [200, 200, 200, 200, 200],
  167. samples: [
  168. [0, 1, 2],
  169. [0, 1, 2, 3, 4, 5],
  170. [0, 1, 2, 3],
  171. [0, 1, 2, 3, 4],
  172. [0, 1],
  173. ],
  174. type: 'sampled',
  175. },
  176. {
  177. 0: new Frame({key: 0, name: 'f0', is_application: true}),
  178. 1: new Frame({key: 1, name: 'f1', is_application: true}),
  179. 2: new Frame({key: 2, name: 'f2'}),
  180. 3: new Frame({key: 3, name: 'f3'}),
  181. 4: new Frame({key: 4, name: 'f4', is_application: true}),
  182. 5: new Frame({key: 5, name: 'f5'}),
  183. },
  184. {type: 'flamechart'}
  185. );
  186. appendSampleWithWeight(
  187. stack: Frame[],
  188. weight: number,
  189. end: number,
  190. resolvedProfileIds?: string[]
  191. ): void {
  192. // Keep track of discarded samples and ones that may have negative weights
  193. this.trackSampleStats(weight);
  194. // Ignore samples with 0 weight
  195. if (weight === 0) {
  196. return;
  197. }
  198. let node = this.callTree;
  199. const framesInStack: CallTreeNode[] = [];
  200. for (let i = 0; i < end; i++) {
  201. const frame = stack[i];
  202. const last = node.children[node.children.length - 1];
  203. // Find common frame between two stacks
  204. if (last && !last.isLocked() && last.frame === frame) {
  205. node = last;
  206. } else {
  207. const parent = node;
  208. node = new CallTreeNode(frame, node);
  209. parent.children.push(node);
  210. if (resolvedProfileIds) {
  211. this.callTreeNodeProfileIdMap.set(node, resolvedProfileIds);
  212. }
  213. }
  214. node.totalWeight += weight;
  215. // TODO: This is On^2, because we iterate over all frames in the stack to check if our
  216. // frame is a recursive frame. We could do this in O(1) by keeping a map of frames in the stack
  217. // We check the stack in a top-down order to find the first recursive frame.
  218. let start = framesInStack.length - 1;
  219. while (start >= 0) {
  220. if (framesInStack[start].frame === node.frame) {
  221. // The recursion edge is bidirectional
  222. framesInStack[start].recursive = node;
  223. node.recursive = framesInStack[start];
  224. break;
  225. }
  226. start--;
  227. }
  228. framesInStack[i] = node;
  229. }
  230. node.selfWeight += weight;
  231. this.minFrameDuration = Math.min(weight, this.minFrameDuration);
  232. // Lock the stack node, so we make sure we dont mutate it in the future.
  233. // The samples should be ordered by timestamp when processed so we should never
  234. // iterate over them again in the future.
  235. for (const child of node.children) {
  236. child.lock();
  237. }
  238. node.frame.selfWeight += weight;
  239. for (const stackNode of framesInStack) {
  240. stackNode.frame.totalWeight += weight;
  241. stackNode.count++;
  242. }
  243. // If node is the same as the previous sample, add the weight to the previous sample
  244. if (node === this.samples[this.samples.length - 1]) {
  245. this.weights[this.weights.length - 1] += weight;
  246. } else {
  247. this.samples.push(node);
  248. this.weights.push(weight);
  249. }
  250. }
  251. build(): Profile {
  252. this.duration = Math.max(
  253. this.duration,
  254. this.weights.reduce((a, b) => a + b, 0)
  255. );
  256. // We had no frames with duration > 0, so set min duration to timeline duration
  257. // which effectively disables any zooming on the flamegraphs
  258. if (
  259. this.minFrameDuration === Number.POSITIVE_INFINITY ||
  260. this.minFrameDuration === 0
  261. ) {
  262. this.minFrameDuration = this.duration;
  263. }
  264. return this;
  265. }
  266. }