flamegraph.ts 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. import {lastOfArray} from 'sentry/utils';
  2. import {Rect} from './gl/utils';
  3. import {Profile} from './profile/profile';
  4. import {makeFormatter, makeTimelineFormatter} from './units/units';
  5. import {CallTreeNode} from './callTreeNode';
  6. import {FlamegraphFrame} from './flamegraphFrame';
  7. export class Flamegraph {
  8. profile: Profile;
  9. frames: FlamegraphFrame[] = [];
  10. roots: FlamegraphFrame[] = [];
  11. profileIndex: number;
  12. inverted?: boolean = false;
  13. leftHeavy?: boolean = false;
  14. depth = 0;
  15. configSpace: Rect = new Rect(0, 0, 0, 0);
  16. formatter: (value: number) => string;
  17. timelineFormatter: (value: number) => string;
  18. frameIndex: Record<string, FlamegraphFrame> = {};
  19. static Empty(): Flamegraph {
  20. return new Flamegraph(Profile.Empty(), 0, {
  21. inverted: false,
  22. leftHeavy: false,
  23. });
  24. }
  25. static From(from: Flamegraph, {inverted = false, leftHeavy = false}): Flamegraph {
  26. return new Flamegraph(from.profile, from.profileIndex, {inverted, leftHeavy});
  27. }
  28. constructor(
  29. profile: Profile,
  30. profileIndex: number,
  31. {
  32. inverted = false,
  33. leftHeavy = false,
  34. configSpace,
  35. }: {configSpace?: Rect; inverted?: boolean; leftHeavy?: boolean} = {}
  36. ) {
  37. this.inverted = inverted;
  38. this.leftHeavy = leftHeavy;
  39. // @TODO check if we can get rid of this profile reference
  40. this.profile = profile;
  41. this.profileIndex = profileIndex;
  42. this.roots = [];
  43. // If a custom config space is provided, use it and draw the chart in it
  44. this.frames = leftHeavy
  45. ? this.buildLeftHeavyGraph(profile, configSpace ? configSpace.x : 0)
  46. : this.buildCallOrderGraph(profile, configSpace ? configSpace.x : 0);
  47. this.formatter = makeFormatter(profile.unit);
  48. this.timelineFormatter = makeTimelineFormatter(profile.unit);
  49. // If the profile duration is 0, set the flamegraph duration
  50. // to 1 second so we can render a placeholder grid
  51. this.configSpace = new Rect(
  52. 0,
  53. 0,
  54. this.profile.unit === 'nanoseconds'
  55. ? 1e9
  56. : this.profile.unit === 'microseconds'
  57. ? 1e6
  58. : this.profile.unit === 'milliseconds'
  59. ? 1e3
  60. : 1,
  61. this.depth
  62. );
  63. if (this.profile.duration) {
  64. this.configSpace = new Rect(
  65. configSpace ? configSpace.x : this.profile.startedAt,
  66. 0,
  67. configSpace ? configSpace.width : this.profile.duration,
  68. this.depth
  69. );
  70. }
  71. }
  72. buildCallOrderGraph(profile: Profile, offset: number): FlamegraphFrame[] {
  73. const frames: FlamegraphFrame[] = [];
  74. const stack: FlamegraphFrame[] = [];
  75. let idx = 0;
  76. const openFrame = (node: CallTreeNode, value: number) => {
  77. const parent = lastOfArray(stack);
  78. const frame: FlamegraphFrame = {
  79. key: idx,
  80. frame: node.frame,
  81. node,
  82. parent,
  83. children: [],
  84. depth: 0,
  85. start: offset + value,
  86. end: offset + value,
  87. };
  88. if (parent) {
  89. parent.children.push(frame);
  90. }
  91. stack.push(frame);
  92. if (stack.length === 1) {
  93. this.roots.push(frame);
  94. }
  95. idx++;
  96. };
  97. const closeFrame = (_: CallTreeNode, value: number) => {
  98. const stackTop = stack.pop();
  99. if (!stackTop) {
  100. // This is unreachable because the profile importing logic already checks this
  101. throw new Error('Unbalanced stack');
  102. }
  103. stackTop.end = offset + value;
  104. stackTop.depth = stack.length;
  105. if (stackTop.end - stackTop.start === 0) {
  106. return;
  107. }
  108. frames.unshift(stackTop);
  109. this.depth = Math.max(stackTop.depth, this.depth);
  110. };
  111. profile.forEach(openFrame, closeFrame);
  112. return frames;
  113. }
  114. buildLeftHeavyGraph(profile: Profile, offset: number): FlamegraphFrame[] {
  115. const frames: FlamegraphFrame[] = [];
  116. const stack: FlamegraphFrame[] = [];
  117. const sortTree = (node: CallTreeNode) => {
  118. node.children.sort((a, b) => -(a.totalWeight - b.totalWeight));
  119. node.children.forEach(c => sortTree(c));
  120. };
  121. sortTree(profile.appendOrderTree);
  122. let idx = 0;
  123. const openFrame = (node: CallTreeNode, value: number) => {
  124. const parent = lastOfArray(stack);
  125. const frame: FlamegraphFrame = {
  126. key: idx,
  127. frame: node.frame,
  128. node,
  129. parent,
  130. children: [],
  131. depth: 0,
  132. start: offset + value,
  133. end: offset + value,
  134. };
  135. if (parent) {
  136. parent.children.push(frame);
  137. }
  138. stack.push(frame);
  139. if (stack.length === 1) {
  140. this.roots.push(frame);
  141. }
  142. idx++;
  143. };
  144. const closeFrame = (_node: CallTreeNode, value: number) => {
  145. const stackTop = stack.pop();
  146. if (!stackTop) {
  147. throw new Error('Unbalanced stack');
  148. }
  149. stackTop.end = offset + value;
  150. stackTop.depth = stack.length;
  151. // Dont draw 0 width frames
  152. if (stackTop.end - stackTop.start === 0) {
  153. return;
  154. }
  155. frames.unshift(stackTop);
  156. this.depth = Math.max(stackTop.depth, this.depth);
  157. };
  158. function visit(node: CallTreeNode, start: number) {
  159. if (!node.frame.isRoot()) {
  160. openFrame(node, start);
  161. }
  162. let childTime = 0;
  163. node.children.forEach(child => {
  164. visit(child, start + childTime);
  165. childTime += child.totalWeight;
  166. });
  167. if (!node.frame.isRoot()) {
  168. closeFrame(node, start + node.totalWeight);
  169. }
  170. }
  171. visit(profile.appendOrderTree, 0);
  172. return frames;
  173. }
  174. setConfigSpace(configSpace: Rect): Flamegraph {
  175. this.configSpace = configSpace;
  176. return this;
  177. }
  178. }