flamegraph.ts 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  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 the profile duration is 0, set the flamegraph duration
  62. // to 1 second so we can render a placeholder grid
  63. this.configSpace = new Rect(
  64. 0,
  65. 0,
  66. this.profile.unit === 'nanoseconds'
  67. ? 1e9
  68. : this.profile.unit === 'microseconds'
  69. ? 1e6
  70. : this.profile.unit === 'milliseconds'
  71. ? 1e3
  72. : 1,
  73. this.depth
  74. );
  75. if (this.profile.duration) {
  76. this.configSpace = new Rect(
  77. configSpace ? configSpace.x : this.profile.startedAt,
  78. 0,
  79. configSpace ? configSpace.width : this.profile.duration,
  80. this.depth
  81. );
  82. }
  83. const weight = this.root.children.reduce(
  84. (acc, frame) => acc + frame.node.totalWeight,
  85. 0
  86. );
  87. this.root.node.addToTotalWeight(weight);
  88. this.root.end = this.root.start + weight;
  89. this.root.frame.addToTotalWeight(weight);
  90. }
  91. buildCallOrderGraph(profile: Profile, offset: number): FlamegraphFrame[] {
  92. const frames: FlamegraphFrame[] = [];
  93. const stack: FlamegraphFrame[] = [];
  94. let idx = 0;
  95. const openFrame = (node: CallTreeNode, value: number) => {
  96. const parent = lastOfArray(stack);
  97. const frame: FlamegraphFrame = {
  98. key: idx,
  99. frame: node.frame,
  100. node,
  101. parent,
  102. children: [],
  103. depth: 0,
  104. start: offset + value,
  105. end: offset + value,
  106. };
  107. if (parent) {
  108. parent.children.push(frame);
  109. } else {
  110. this.root.children.push(frame);
  111. }
  112. stack.push(frame);
  113. idx++;
  114. };
  115. const closeFrame = (_: CallTreeNode, value: number) => {
  116. const stackTop = stack.pop();
  117. if (!stackTop) {
  118. // This is unreachable because the profile importing logic already checks this
  119. throw new Error('Unbalanced stack');
  120. }
  121. stackTop.end = offset + value;
  122. stackTop.depth = stack.length;
  123. if (stackTop.end - stackTop.start === 0) {
  124. return;
  125. }
  126. frames.unshift(stackTop);
  127. this.depth = Math.max(stackTop.depth, this.depth);
  128. };
  129. profile.forEach(openFrame, closeFrame);
  130. return frames;
  131. }
  132. buildLeftHeavyGraph(profile: Profile, offset: number): FlamegraphFrame[] {
  133. const frames: FlamegraphFrame[] = [];
  134. const stack: FlamegraphFrame[] = [];
  135. const sortTree = (node: CallTreeNode) => {
  136. node.children.sort((a, b) => -(a.totalWeight - b.totalWeight));
  137. node.children.forEach(c => sortTree(c));
  138. };
  139. sortTree(profile.appendOrderTree);
  140. const virtualRoot: FlamegraphFrame = {
  141. key: -1,
  142. frame: CallTreeNode.Root.frame,
  143. node: CallTreeNode.Root,
  144. parent: null,
  145. children: [],
  146. depth: 0,
  147. start: 0,
  148. end: 0,
  149. };
  150. this.root = virtualRoot;
  151. let idx = 0;
  152. const openFrame = (node: CallTreeNode, value: number) => {
  153. const parent = lastOfArray(stack);
  154. const frame: FlamegraphFrame = {
  155. key: idx,
  156. frame: node.frame,
  157. node,
  158. parent,
  159. children: [],
  160. depth: 0,
  161. start: offset + value,
  162. end: offset + value,
  163. };
  164. if (parent) {
  165. parent.children.push(frame);
  166. } else {
  167. this.root.children.push(frame);
  168. }
  169. stack.push(frame);
  170. idx++;
  171. };
  172. const closeFrame = (_node: CallTreeNode, value: number) => {
  173. const stackTop = stack.pop();
  174. if (!stackTop) {
  175. throw new Error('Unbalanced stack');
  176. }
  177. stackTop.end = offset + value;
  178. stackTop.depth = stack.length;
  179. // Dont draw 0 width frames
  180. if (stackTop.end - stackTop.start === 0) {
  181. return;
  182. }
  183. frames.unshift(stackTop);
  184. this.depth = Math.max(stackTop.depth, this.depth);
  185. };
  186. function visit(node: CallTreeNode, start: number) {
  187. if (!node.frame.isRoot()) {
  188. openFrame(node, start);
  189. }
  190. let childTime = 0;
  191. node.children.forEach(child => {
  192. visit(child, start + childTime);
  193. childTime += child.totalWeight;
  194. });
  195. if (!node.frame.isRoot()) {
  196. closeFrame(node, start + node.totalWeight);
  197. }
  198. }
  199. visit(profile.appendOrderTree, 0);
  200. return frames;
  201. }
  202. setConfigSpace(configSpace: Rect): Flamegraph {
  203. this.configSpace = configSpace;
  204. return this;
  205. }
  206. }