flamegraph.ts 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  1. import {lastOfArray} from 'sentry/utils';
  2. import {FlamegraphFrame} from 'sentry/utils/profiling/flamegraphFrame';
  3. import {Rect} from './gl/utils';
  4. import {Profile} from './profile/profile';
  5. import {makeFormatter, makeTimelineFormatter} from './units/units';
  6. import {CallTreeNode} from './callTreeNode';
  7. import {Frame} from './frame';
  8. // Intermediary flamegraph data structure for rendering a profile. Constructs a list of frames from a profile
  9. // and appends them to a virtual root. Taken mostly from speedscope with a few modifications. This should get
  10. // removed as we port to our own format for profiles. The general idea is to iterate over profiles while
  11. // keeping an intermediary stack so as to resemble the execution of the program.
  12. export class Flamegraph {
  13. profile: Profile;
  14. frames: FlamegraphFrame[] = [];
  15. profileIndex: number;
  16. inverted?: boolean = false;
  17. leftHeavy?: boolean = false;
  18. depth = 0;
  19. configSpace: Rect = new Rect(0, 0, 0, 0);
  20. root: FlamegraphFrame = {
  21. key: -1,
  22. parent: null,
  23. frame: new Frame({...Frame.Root}),
  24. node: new CallTreeNode(new Frame({...Frame.Root}), null),
  25. depth: -1,
  26. start: 0,
  27. end: 0,
  28. children: [],
  29. };
  30. formatter: (value: number) => string;
  31. timelineFormatter: (value: number) => string;
  32. static Empty(): Flamegraph {
  33. return new Flamegraph(Profile.Empty, 0, {
  34. inverted: false,
  35. leftHeavy: false,
  36. });
  37. }
  38. static From(from: Flamegraph, {inverted = false, leftHeavy = false}): Flamegraph {
  39. return new Flamegraph(from.profile, from.profileIndex, {inverted, leftHeavy});
  40. }
  41. constructor(
  42. profile: Profile,
  43. profileIndex: number,
  44. {
  45. inverted = false,
  46. leftHeavy = false,
  47. configSpace,
  48. }: {configSpace?: Rect; inverted?: boolean; leftHeavy?: boolean} = {}
  49. ) {
  50. this.inverted = inverted;
  51. this.leftHeavy = leftHeavy;
  52. // @TODO check if we can get rid of this profile reference
  53. this.profile = profile;
  54. this.profileIndex = profileIndex;
  55. // If a custom config space is provided, use it and draw the chart in it
  56. this.frames = leftHeavy
  57. ? this.buildLeftHeavyGraph(profile, configSpace ? configSpace.x : 0)
  58. : this.buildCallOrderGraph(profile, configSpace ? configSpace.x : 0);
  59. this.formatter = makeFormatter(profile.unit);
  60. this.timelineFormatter = makeTimelineFormatter(profile.unit);
  61. if (this.profile.duration > 0) {
  62. this.configSpace = new Rect(
  63. configSpace ? configSpace.x : this.profile.startedAt,
  64. 0,
  65. configSpace ? configSpace.width : this.profile.duration,
  66. this.depth
  67. );
  68. } else {
  69. // If the profile duration is 0, set the flamegraph duration
  70. // to 1 second so we can render a placeholder grid
  71. this.configSpace = new Rect(
  72. 0,
  73. 0,
  74. this.profile.unit === 'nanoseconds'
  75. ? 1e9
  76. : this.profile.unit === 'microseconds'
  77. ? 1e6
  78. : this.profile.unit === 'milliseconds'
  79. ? 1e3
  80. : 1,
  81. this.depth
  82. );
  83. }
  84. const weight = this.root.children.reduce(
  85. (acc, frame) => acc + frame.node.totalWeight,
  86. 0
  87. );
  88. this.root.node.addToTotalWeight(weight);
  89. this.root.end = this.root.start + weight;
  90. this.root.frame.addToTotalWeight(weight);
  91. }
  92. buildCallOrderGraph(profile: Profile, offset: number): FlamegraphFrame[] {
  93. const frames: FlamegraphFrame[] = [];
  94. const stack: FlamegraphFrame[] = [];
  95. let idx = 0;
  96. const openFrame = (node: CallTreeNode, value: number) => {
  97. const parent = lastOfArray(stack) ?? this.root;
  98. const frame: FlamegraphFrame = {
  99. key: idx,
  100. frame: node.frame,
  101. node,
  102. parent,
  103. children: [],
  104. depth: 0,
  105. start: offset + value,
  106. end: offset + value,
  107. };
  108. if (parent) {
  109. parent.children.push(frame);
  110. } else {
  111. this.root.children.push(frame);
  112. }
  113. stack.push(frame);
  114. idx++;
  115. };
  116. const closeFrame = (_: CallTreeNode, value: number) => {
  117. const stackTop = stack.pop();
  118. if (!stackTop) {
  119. // This is unreachable because the profile importing logic already checks this
  120. throw new Error('Unbalanced stack');
  121. }
  122. stackTop.end = offset + value;
  123. stackTop.depth = stack.length;
  124. if (stackTop.end - stackTop.start === 0) {
  125. return;
  126. }
  127. frames.unshift(stackTop);
  128. this.depth = Math.max(stackTop.depth, this.depth);
  129. };
  130. profile.forEach(openFrame, closeFrame);
  131. return frames;
  132. }
  133. buildLeftHeavyGraph(profile: Profile, offset: number): FlamegraphFrame[] {
  134. const frames: FlamegraphFrame[] = [];
  135. const stack: FlamegraphFrame[] = [];
  136. const sortTree = (node: CallTreeNode) => {
  137. node.children.sort((a, b) => -(a.totalWeight - b.totalWeight));
  138. node.children.forEach(c => sortTree(c));
  139. };
  140. sortTree(profile.appendOrderTree);
  141. const virtualRoot: FlamegraphFrame = {
  142. key: -1,
  143. frame: CallTreeNode.Root.frame,
  144. node: CallTreeNode.Root,
  145. parent: null,
  146. children: [],
  147. depth: 0,
  148. start: 0,
  149. end: 0,
  150. };
  151. this.root = virtualRoot;
  152. let idx = 0;
  153. const openFrame = (node: CallTreeNode, value: number) => {
  154. const parent = lastOfArray(stack) ?? this.root;
  155. const frame: FlamegraphFrame = {
  156. key: idx,
  157. frame: node.frame,
  158. node,
  159. parent,
  160. children: [],
  161. depth: 0,
  162. start: offset + value,
  163. end: offset + value,
  164. };
  165. if (parent) {
  166. parent.children.push(frame);
  167. } else {
  168. this.root.children.push(frame);
  169. }
  170. stack.push(frame);
  171. idx++;
  172. };
  173. const closeFrame = (_node: CallTreeNode, value: number) => {
  174. const stackTop = stack.pop();
  175. if (!stackTop) {
  176. throw new Error('Unbalanced stack');
  177. }
  178. stackTop.end = offset + value;
  179. stackTop.depth = stack.length;
  180. // Dont draw 0 width frames
  181. if (stackTop.end - stackTop.start === 0) {
  182. return;
  183. }
  184. frames.unshift(stackTop);
  185. this.depth = Math.max(stackTop.depth, this.depth);
  186. };
  187. function visit(node: CallTreeNode, start: number) {
  188. if (!node.frame.isRoot()) {
  189. openFrame(node, start);
  190. }
  191. let childTime = 0;
  192. node.children.forEach(child => {
  193. visit(child, start + childTime);
  194. childTime += child.totalWeight;
  195. });
  196. if (!node.frame.isRoot()) {
  197. closeFrame(node, start + node.totalWeight);
  198. }
  199. }
  200. visit(profile.appendOrderTree, 0);
  201. return frames;
  202. }
  203. findAllMatchingFrames(
  204. frameOrName: FlamegraphFrame | string,
  205. packageName?: string
  206. ): FlamegraphFrame[] {
  207. const matches: FlamegraphFrame[] = [];
  208. if (typeof frameOrName === 'string') {
  209. for (let i = 0; i < this.frames.length; i++) {
  210. if (
  211. this.frames[i].frame.name === frameOrName &&
  212. this.frames[i].frame.image === packageName
  213. ) {
  214. matches.push(this.frames[i]);
  215. }
  216. }
  217. } else {
  218. for (let i = 0; i < this.frames.length; i++) {
  219. if (
  220. this.frames[i].frame.name === frameOrName.node.frame.name &&
  221. this.frames[i].frame.image === frameOrName.node.frame.image
  222. ) {
  223. matches.push(this.frames[i]);
  224. }
  225. }
  226. }
  227. return matches;
  228. }
  229. setConfigSpace(configSpace: Rect): Flamegraph {
  230. this.configSpace = configSpace;
  231. return this;
  232. }
  233. }