sentrySampledProfile.tsx 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298
  1. import moment from 'moment';
  2. import {defined, lastOfArray} from 'sentry/utils';
  3. import {CallTreeNode} from 'sentry/utils/profiling/callTreeNode';
  4. import {Frame} from './../frame';
  5. import {Profile} from './profile';
  6. import {createSentrySampleProfileFrameIndex} from './utils';
  7. type WeightedSample = Profiling.SentrySampledProfile['profile']['samples'][0] & {
  8. weight: number;
  9. };
  10. function sortSentrySampledProfileSamples(
  11. samples: Readonly<WeightedSample[]>,
  12. stacks: Profiling.SentrySampledProfile['profile']['stacks'],
  13. frames: Profiling.SentrySampledProfile['profile']['frames'],
  14. frameFilter?: (i: number) => boolean
  15. ) {
  16. const frameIds = [...Array(frames.length).keys()].sort((a, b) => {
  17. const frameA = frames[a];
  18. const frameB = frames[b];
  19. if (defined(frameA.function) && defined(frameB.function)) {
  20. // sort alphabetically first
  21. const ret = frameA.function.localeCompare(frameB.function);
  22. if (ret !== 0) {
  23. return ret;
  24. }
  25. // break ties using the line number
  26. if (defined(frameA.lineno) && defined(frameB.lineno)) {
  27. return frameA.lineno - frameB.lineno;
  28. }
  29. if (defined(frameA.lineno)) {
  30. return -1;
  31. }
  32. if (defined(frameB.lineno)) {
  33. return 1;
  34. }
  35. } else if (defined(frameA.function)) {
  36. // if only frameA is defined, it goes first
  37. return -1;
  38. } else if (defined(frameB.function)) {
  39. // if only frameB is defined, it goes first
  40. return 1;
  41. }
  42. // if neither functions are defined, they're treated as equal
  43. return 0;
  44. });
  45. const framesMapping = frameIds.reduce((acc, frameId, idx) => {
  46. acc[frameId] = idx;
  47. return acc;
  48. }, {});
  49. return [...samples].sort((a, b) => {
  50. // same stack id, these are the same
  51. if (a.stack_id === b.stack_id) {
  52. return 0;
  53. }
  54. const stackA = frameFilter
  55. ? stacks[a.stack_id].filter(frameFilter)
  56. : stacks[a.stack_id];
  57. const stackB = frameFilter
  58. ? stacks[b.stack_id].filter(frameFilter)
  59. : stacks[b.stack_id];
  60. const minDepth = Math.min(stackA.length, stackB.length);
  61. for (let i = 0; i < minDepth; i++) {
  62. // we iterate from the end of each stack because that's where the main function is
  63. const frameIdA = stackA[stackA.length - i - 1];
  64. const frameIdB = stackB[stackB.length - i - 1];
  65. // same frame id, so check the next frame in the stack
  66. if (frameIdA === frameIdB) {
  67. continue;
  68. }
  69. const frameIdxA = framesMapping[frameIdA];
  70. const frameIdxB = framesMapping[frameIdB];
  71. // same frame idx, so check the next frame in the stack
  72. if (frameIdxA === frameIdxB) {
  73. continue;
  74. }
  75. return frameIdxA - frameIdxB;
  76. }
  77. // if all frames up to the depth of the shorter stack matches,
  78. // then the deeper stack goes first
  79. return stackB.length - stackA.length;
  80. });
  81. }
  82. export class SentrySampledProfile extends Profile {
  83. static FromProfile(
  84. sampledProfile: Profiling.SentrySampledProfile,
  85. frameIndex: ReturnType<typeof createSentrySampleProfileFrameIndex>,
  86. options: {
  87. type: 'flamechart' | 'flamegraph';
  88. frameFilter?: (frame: Frame) => boolean;
  89. }
  90. ): Profile {
  91. const weightedSamples: WeightedSample[] = sampledProfile.profile.samples.map(
  92. (sample, i) => {
  93. // falling back to the current sample timestamp has the effect
  94. // of giving the last sample a weight of 0
  95. const nextSample = sampledProfile.profile.samples[i + 1] ?? sample;
  96. return {
  97. ...sample,
  98. weight: nextSample.elapsed_since_start_ns - sample.elapsed_since_start_ns,
  99. };
  100. }
  101. );
  102. function resolveFrame(index): Frame {
  103. const resolvedFrame = frameIndex[index];
  104. if (!resolvedFrame) {
  105. throw new Error(`Could not resolve frame ${index} in frame index`);
  106. }
  107. return resolvedFrame;
  108. }
  109. const {frames, stacks} = sampledProfile.profile;
  110. const samples =
  111. options.type === 'flamegraph'
  112. ? sortSentrySampledProfileSamples(
  113. weightedSamples,
  114. stacks,
  115. frames,
  116. options.frameFilter ? i => options.frameFilter!(resolveFrame(i)) : undefined
  117. )
  118. : weightedSamples;
  119. const startedAt = samples[0].elapsed_since_start_ns;
  120. const endedAt = samples[samples.length - 1].elapsed_since_start_ns;
  121. if (Number.isNaN(startedAt) || Number.isNaN(endedAt)) {
  122. throw TypeError('startedAt or endedAt is NaN');
  123. }
  124. const {threadId, threadName} = getThreadData(sampledProfile);
  125. const profile = new SentrySampledProfile({
  126. // .unix() only has second resolution
  127. timestamp: moment(sampledProfile.timestamp).valueOf() / 1000,
  128. duration: endedAt - startedAt,
  129. startedAt,
  130. endedAt,
  131. unit: 'nanoseconds',
  132. name: threadName,
  133. threadId,
  134. type: options.type,
  135. });
  136. for (let i = 0; i < samples.length; i++) {
  137. const sample = samples[i];
  138. let stack = stacks[sample.stack_id].map(resolveFrame);
  139. if (options.frameFilter) {
  140. stack = stack.filter(frame => options.frameFilter!(frame));
  141. }
  142. profile.appendSampleWithWeight(stack, sample.weight);
  143. }
  144. return profile.build();
  145. }
  146. appendSampleWithWeight(stack: Frame[], weight: number): void {
  147. // Keep track of discarded samples and ones that may have negative weights
  148. this.trackSampleStats(weight);
  149. // Ignore samples with 0 weight
  150. if (weight === 0) {
  151. return;
  152. }
  153. let node = this.callTree;
  154. const framesInStack: CallTreeNode[] = [];
  155. // frames are ordered outermost -> innermost so we have to iterate backward
  156. for (let i = stack.length - 1; i >= 0; i--) {
  157. const frame = stack[i];
  158. const last = lastOfArray(node.children);
  159. // Find common frame between two stacks
  160. if (last && !last.isLocked() && last.frame === frame) {
  161. node = last;
  162. } else {
  163. const parent = node;
  164. node = new CallTreeNode(frame, node);
  165. parent.children.push(node);
  166. }
  167. node.totalWeight += weight;
  168. // TODO: This is On^2, because we iterate over all frames in the stack to check if our
  169. // frame is a recursive frame. We could do this in O(1) by keeping a map of frames in the stack
  170. // We check the stack in a top-down order to find the first recursive frame.
  171. let start = framesInStack.length - 1;
  172. while (start >= 0) {
  173. if (framesInStack[start].frame === node.frame) {
  174. // The recursion edge is bidirectional
  175. framesInStack[start].recursive = node;
  176. node.recursive = framesInStack[start];
  177. break;
  178. }
  179. start--;
  180. }
  181. framesInStack.push(node);
  182. }
  183. node.selfWeight += weight;
  184. this.minFrameDuration = Math.min(weight, this.minFrameDuration);
  185. // Lock the stack node, so we make sure we dont mutate it in the future.
  186. // The samples should be ordered by timestamp when processed so we should never
  187. // iterate over them again in the future.
  188. for (const child of node.children) {
  189. child.lock();
  190. }
  191. node.frame.selfWeight += weight;
  192. for (const stackNode of framesInStack) {
  193. stackNode.frame.totalWeight += weight;
  194. stackNode.count++;
  195. }
  196. // If node is the same as the previous sample, add the weight to the previous sample
  197. if (node === lastOfArray(this.samples)) {
  198. this.weights[this.weights.length - 1] += weight;
  199. } else {
  200. this.samples.push(node);
  201. this.weights.push(weight);
  202. }
  203. }
  204. build(): Profile {
  205. this.duration = Math.max(
  206. this.duration,
  207. this.weights.reduce((a, b) => a + b, 0)
  208. );
  209. // We had no frames with duration > 0, so set min duration to timeline duration
  210. // which effectively disables any zooming on the flamegraphs
  211. if (
  212. this.minFrameDuration === Number.POSITIVE_INFINITY ||
  213. this.minFrameDuration === 0
  214. ) {
  215. this.minFrameDuration = this.duration;
  216. }
  217. return this;
  218. }
  219. }
  220. const COCOA_MAIN_THREAD = 'com.apple.main-thread';
  221. function getThreadData(profile: Profiling.SentrySampledProfile): {
  222. threadId: number;
  223. threadName: string;
  224. } {
  225. const {samples, queue_metadata = {}, thread_metadata = {}} = profile.profile;
  226. const sample = samples[0];
  227. const threadId = parseInt(sample.thread_id, 10);
  228. const threadName = thread_metadata?.[threadId]?.name;
  229. if (threadName) {
  230. return {threadId, threadName};
  231. }
  232. // cocoa has a queue address that we fall back to to try to get a thread name
  233. // is this the only platform string to check for?
  234. if (profile.platform === 'cocoa') {
  235. // only the active thread should get the main thread name
  236. if (threadId === profile.transaction.active_thread_id) {
  237. return {threadId, threadName: COCOA_MAIN_THREAD};
  238. }
  239. const queueName =
  240. sample.queue_address && queue_metadata?.[sample.queue_address]?.label;
  241. // if a queue has the main thread name, we discard it
  242. if (queueName && queueName !== COCOA_MAIN_THREAD) {
  243. return {threadId, threadName: queueName};
  244. }
  245. }
  246. return {threadId, threadName: ''};
  247. }