FlameGraph.js 47 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463
  1. /* This Source Code Form is subject to the terms of the Mozilla Public
  2. * License, v. 2.0. If a copy of the MPL was not distributed with this
  3. * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
  4. "use strict";
  5. const { Task } = require("devtools/shared/task");
  6. const { ViewHelpers, setNamedTimeout } = require("devtools/client/shared/widgets/view-helpers");
  7. const { ELLIPSIS } = require("devtools/shared/l10n");
  8. loader.lazyRequireGetter(this, "defer", "devtools/shared/defer");
  9. loader.lazyRequireGetter(this, "EventEmitter",
  10. "devtools/shared/event-emitter");
  11. loader.lazyRequireGetter(this, "getColor",
  12. "devtools/client/shared/theme", true);
  13. loader.lazyRequireGetter(this, "CATEGORY_MAPPINGS",
  14. "devtools/client/performance/modules/categories", true);
  15. loader.lazyRequireGetter(this, "FrameUtils",
  16. "devtools/client/performance/modules/logic/frame-utils");
  17. loader.lazyRequireGetter(this, "demangle",
  18. "devtools/client/shared/demangle");
  19. loader.lazyRequireGetter(this, "AbstractCanvasGraph",
  20. "devtools/client/shared/widgets/Graphs", true);
  21. loader.lazyRequireGetter(this, "GraphArea",
  22. "devtools/client/shared/widgets/Graphs", true);
  23. loader.lazyRequireGetter(this, "GraphAreaDragger",
  24. "devtools/client/shared/widgets/Graphs", true);
  25. const GRAPH_SRC = "chrome://devtools/content/shared/widgets/graphs-frame.xhtml";
  26. // ms
  27. const GRAPH_RESIZE_EVENTS_DRAIN = 100;
  28. const GRAPH_WHEEL_ZOOM_SENSITIVITY = 0.00035;
  29. const GRAPH_WHEEL_SCROLL_SENSITIVITY = 0.5;
  30. const GRAPH_KEYBOARD_ZOOM_SENSITIVITY = 20;
  31. const GRAPH_KEYBOARD_PAN_SENSITIVITY = 20;
  32. const GRAPH_KEYBOARD_ACCELERATION = 1.05;
  33. const GRAPH_KEYBOARD_TRANSLATION_MAX = 150;
  34. // ms
  35. const GRAPH_MIN_SELECTION_WIDTH = 0.001;
  36. // px
  37. const GRAPH_HORIZONTAL_PAN_THRESHOLD = 10;
  38. const GRAPH_VERTICAL_PAN_THRESHOLD = 30;
  39. const FIND_OPTIMAL_TICK_INTERVAL_MAX_ITERS = 100;
  40. // ms
  41. const TIMELINE_TICKS_MULTIPLE = 5;
  42. // px
  43. const TIMELINE_TICKS_SPACING_MIN = 75;
  44. // px
  45. const OVERVIEW_HEADER_HEIGHT = 16;
  46. const OVERVIEW_HEADER_TEXT_FONT_SIZE = 9;
  47. const OVERVIEW_HEADER_TEXT_FONT_FAMILY = "sans-serif";
  48. // px
  49. const OVERVIEW_HEADER_TEXT_PADDING_LEFT = 6;
  50. const OVERVIEW_HEADER_TEXT_PADDING_TOP = 5;
  51. const OVERVIEW_HEADER_TIMELINE_STROKE_COLOR = "rgba(128, 128, 128, 0.5)";
  52. // px
  53. const FLAME_GRAPH_BLOCK_HEIGHT = 15;
  54. const FLAME_GRAPH_BLOCK_BORDER = 1;
  55. const FLAME_GRAPH_BLOCK_TEXT_FONT_SIZE = 10;
  56. const FLAME_GRAPH_BLOCK_TEXT_FONT_FAMILY = "message-box, Helvetica Neue," +
  57. "Helvetica, sans-serif";
  58. // px
  59. const FLAME_GRAPH_BLOCK_TEXT_PADDING_TOP = 0;
  60. const FLAME_GRAPH_BLOCK_TEXT_PADDING_LEFT = 3;
  61. const FLAME_GRAPH_BLOCK_TEXT_PADDING_RIGHT = 3;
  62. // Large enough number for a diverse pallette.
  63. const PALLETTE_SIZE = 20;
  64. const PALLETTE_HUE_OFFSET = Math.random() * 90;
  65. const PALLETTE_HUE_RANGE = 270;
  66. const PALLETTE_SATURATION = 100;
  67. const PALLETTE_BRIGHTNESS = 55;
  68. const PALLETTE_OPACITY = 0.35;
  69. const COLOR_PALLETTE = Array.from(Array(PALLETTE_SIZE)).map((_, i) => "hsla" +
  70. "(" +
  71. ((PALLETTE_HUE_OFFSET + (i / PALLETTE_SIZE * PALLETTE_HUE_RANGE)) | 0 % 360) +
  72. "," + PALLETTE_SATURATION + "%" +
  73. "," + PALLETTE_BRIGHTNESS + "%" +
  74. "," + PALLETTE_OPACITY +
  75. ")"
  76. );
  77. /**
  78. * A flamegraph visualization. This implementation is responsable only with
  79. * drawing the graph, using a data source consisting of rectangles and
  80. * their corresponding widths.
  81. *
  82. * Example usage:
  83. * let graph = new FlameGraph(node);
  84. * graph.once("ready", () => {
  85. * let data = FlameGraphUtils.createFlameGraphDataFromThread(thread);
  86. * let bounds = { startTime, endTime };
  87. * graph.setData({ data, bounds });
  88. * });
  89. *
  90. * Data source format:
  91. * [
  92. * {
  93. * color: "string",
  94. * blocks: [
  95. * {
  96. * x: number,
  97. * y: number,
  98. * width: number,
  99. * height: number,
  100. * text: "string"
  101. * },
  102. * ...
  103. * ]
  104. * },
  105. * {
  106. * color: "string",
  107. * blocks: [...]
  108. * },
  109. * ...
  110. * {
  111. * color: "string",
  112. * blocks: [...]
  113. * }
  114. * ]
  115. *
  116. * Use `FlameGraphUtils` to convert profiler data (or any other data source)
  117. * into a drawable format.
  118. *
  119. * @param nsIDOMNode parent
  120. * The parent node holding the graph.
  121. * @param number sharpness [optional]
  122. * Defaults to the current device pixel ratio.
  123. */
  124. function FlameGraph(parent, sharpness) {
  125. EventEmitter.decorate(this);
  126. this._parent = parent;
  127. this._ready = defer();
  128. this.setTheme();
  129. AbstractCanvasGraph.createIframe(GRAPH_SRC, parent, iframe => {
  130. this._iframe = iframe;
  131. this._window = iframe.contentWindow;
  132. this._document = iframe.contentDocument;
  133. this._pixelRatio = sharpness || this._window.devicePixelRatio;
  134. let container =
  135. this._container = this._document.getElementById("graph-container");
  136. container.className = "flame-graph-widget-container graph-widget-container";
  137. let canvas = this._canvas = this._document.getElementById("graph-canvas");
  138. canvas.className = "flame-graph-widget-canvas graph-widget-canvas";
  139. let bounds = parent.getBoundingClientRect();
  140. bounds.width = this.fixedWidth || bounds.width;
  141. bounds.height = this.fixedHeight || bounds.height;
  142. iframe.setAttribute("width", bounds.width);
  143. iframe.setAttribute("height", bounds.height);
  144. this._width = canvas.width = bounds.width * this._pixelRatio;
  145. this._height = canvas.height = bounds.height * this._pixelRatio;
  146. this._ctx = canvas.getContext("2d");
  147. this._bounds = new GraphArea();
  148. this._selection = new GraphArea();
  149. this._selectionDragger = new GraphAreaDragger();
  150. this._verticalOffset = 0;
  151. this._verticalOffsetDragger = new GraphAreaDragger(0);
  152. this._keyboardZoomAccelerationFactor = 1;
  153. this._keyboardPanAccelerationFactor = 1;
  154. this._userInputStack = 0;
  155. this._keysPressed = [];
  156. // Calculating text widths is necessary to trim the text inside the blocks
  157. // while the scaling changes (e.g. via scrolling). This is very expensive,
  158. // so maintain a cache of string contents to text widths.
  159. this._textWidthsCache = {};
  160. let fontSize = FLAME_GRAPH_BLOCK_TEXT_FONT_SIZE * this._pixelRatio;
  161. let fontFamily = FLAME_GRAPH_BLOCK_TEXT_FONT_FAMILY;
  162. this._ctx.font = fontSize + "px " + fontFamily;
  163. this._averageCharWidth = this._calcAverageCharWidth();
  164. this._overflowCharWidth = this._getTextWidth(this.overflowChar);
  165. this._onAnimationFrame = this._onAnimationFrame.bind(this);
  166. this._onKeyDown = this._onKeyDown.bind(this);
  167. this._onKeyUp = this._onKeyUp.bind(this);
  168. this._onKeyPress = this._onKeyPress.bind(this);
  169. this._onMouseMove = this._onMouseMove.bind(this);
  170. this._onMouseDown = this._onMouseDown.bind(this);
  171. this._onMouseUp = this._onMouseUp.bind(this);
  172. this._onMouseWheel = this._onMouseWheel.bind(this);
  173. this._onResize = this._onResize.bind(this);
  174. this.refresh = this.refresh.bind(this);
  175. this._window.addEventListener("keydown", this._onKeyDown);
  176. this._window.addEventListener("keyup", this._onKeyUp);
  177. this._window.addEventListener("keypress", this._onKeyPress);
  178. this._window.addEventListener("mousemove", this._onMouseMove);
  179. this._window.addEventListener("mousedown", this._onMouseDown);
  180. this._window.addEventListener("mouseup", this._onMouseUp);
  181. this._window.addEventListener("MozMousePixelScroll", this._onMouseWheel);
  182. let ownerWindow = this._parent.ownerDocument.defaultView;
  183. ownerWindow.addEventListener("resize", this._onResize);
  184. this._animationId =
  185. this._window.requestAnimationFrame(this._onAnimationFrame);
  186. this._ready.resolve(this);
  187. this.emit("ready", this);
  188. });
  189. }
  190. FlameGraph.prototype = {
  191. /**
  192. * Read-only width and height of the canvas.
  193. * @return number
  194. */
  195. get width() {
  196. return this._width;
  197. },
  198. get height() {
  199. return this._height;
  200. },
  201. /**
  202. * Returns a promise resolved once this graph is ready to receive data.
  203. */
  204. ready: function () {
  205. return this._ready.promise;
  206. },
  207. /**
  208. * Destroys this graph.
  209. */
  210. destroy: Task.async(function* () {
  211. yield this.ready();
  212. this._window.removeEventListener("keydown", this._onKeyDown);
  213. this._window.removeEventListener("keyup", this._onKeyUp);
  214. this._window.removeEventListener("keypress", this._onKeyPress);
  215. this._window.removeEventListener("mousemove", this._onMouseMove);
  216. this._window.removeEventListener("mousedown", this._onMouseDown);
  217. this._window.removeEventListener("mouseup", this._onMouseUp);
  218. this._window.removeEventListener("MozMousePixelScroll", this._onMouseWheel);
  219. let ownerWindow = this._parent.ownerDocument.defaultView;
  220. if (ownerWindow) {
  221. ownerWindow.removeEventListener("resize", this._onResize);
  222. }
  223. this._window.cancelAnimationFrame(this._animationId);
  224. this._iframe.remove();
  225. this._bounds = null;
  226. this._selection = null;
  227. this._selectionDragger = null;
  228. this._verticalOffset = null;
  229. this._verticalOffsetDragger = null;
  230. this._keyboardZoomAccelerationFactor = null;
  231. this._keyboardPanAccelerationFactor = null;
  232. this._textWidthsCache = null;
  233. this._data = null;
  234. this.emit("destroyed");
  235. }),
  236. /**
  237. * Makes sure the canvas graph is of the specified width or height, and
  238. * doesn't flex to fit all the available space.
  239. */
  240. fixedWidth: null,
  241. fixedHeight: null,
  242. /**
  243. * How much preliminar drag is necessary to determine the panning direction.
  244. */
  245. horizontalPanThreshold: GRAPH_HORIZONTAL_PAN_THRESHOLD,
  246. verticalPanThreshold: GRAPH_VERTICAL_PAN_THRESHOLD,
  247. /**
  248. * The units used in the overhead ticks. Could be "ms", for example.
  249. * Overwrite this with your own localized format.
  250. */
  251. timelineTickUnits: "",
  252. /**
  253. * Character used when a block's text is overflowing.
  254. * Defaults to an ellipsis.
  255. */
  256. overflowChar: ELLIPSIS,
  257. /**
  258. * Sets the data source for this graph.
  259. *
  260. * @param object data
  261. * An object containing the following properties:
  262. * - data: the data source; see the constructor for more info
  263. * - bounds: the minimum/maximum { start, end }, in ms or px
  264. * - visible: optional, the shown { start, end }, in ms or px
  265. */
  266. setData: function ({ data, bounds, visible }) {
  267. this._data = data;
  268. this.setOuterBounds(bounds);
  269. this.setViewRange(visible || bounds);
  270. },
  271. /**
  272. * Same as `setData`, but waits for this graph to finish initializing first.
  273. *
  274. * @param object data
  275. * The data source. See the constructor for more information.
  276. * @return promise
  277. * A promise resolved once the data is set.
  278. */
  279. setDataWhenReady: Task.async(function* (data) {
  280. yield this.ready();
  281. this.setData(data);
  282. }),
  283. /**
  284. * Gets whether or not this graph has a data source.
  285. * @return boolean
  286. */
  287. hasData: function () {
  288. return !!this._data;
  289. },
  290. /**
  291. * Sets the maximum selection (i.e. the 'graph bounds').
  292. * @param object { start, end }
  293. */
  294. setOuterBounds: function ({ startTime, endTime }) {
  295. this._bounds.start = startTime * this._pixelRatio;
  296. this._bounds.end = endTime * this._pixelRatio;
  297. this._shouldRedraw = true;
  298. },
  299. /**
  300. * Sets the selection and vertical offset (i.e. the 'view range').
  301. * @return number
  302. */
  303. setViewRange: function ({ startTime, endTime }, verticalOffset = 0) {
  304. this._selection.start = startTime * this._pixelRatio;
  305. this._selection.end = endTime * this._pixelRatio;
  306. this._verticalOffset = verticalOffset * this._pixelRatio;
  307. this._shouldRedraw = true;
  308. },
  309. /**
  310. * Gets the maximum selection (i.e. the 'graph bounds').
  311. * @return number
  312. */
  313. getOuterBounds: function () {
  314. return {
  315. startTime: this._bounds.start / this._pixelRatio,
  316. endTime: this._bounds.end / this._pixelRatio
  317. };
  318. },
  319. /**
  320. * Gets the current selection and vertical offset (i.e. the 'view range').
  321. * @return number
  322. */
  323. getViewRange: function () {
  324. return {
  325. startTime: this._selection.start / this._pixelRatio,
  326. endTime: this._selection.end / this._pixelRatio,
  327. verticalOffset: this._verticalOffset / this._pixelRatio
  328. };
  329. },
  330. /**
  331. * Focuses this graph's iframe window.
  332. */
  333. focus: function () {
  334. this._window.focus();
  335. },
  336. /**
  337. * Updates this graph to reflect the new dimensions of the parent node.
  338. *
  339. * @param boolean options.force
  340. * Force redraw everything.
  341. */
  342. refresh: function (options = {}) {
  343. let bounds = this._parent.getBoundingClientRect();
  344. let newWidth = this.fixedWidth || bounds.width;
  345. let newHeight = this.fixedHeight || bounds.height;
  346. // Prevent redrawing everything if the graph's width & height won't change,
  347. // except if force=true.
  348. if (!options.force &&
  349. this._width == newWidth * this._pixelRatio &&
  350. this._height == newHeight * this._pixelRatio) {
  351. this.emit("refresh-cancelled");
  352. return;
  353. }
  354. bounds.width = newWidth;
  355. bounds.height = newHeight;
  356. this._iframe.setAttribute("width", bounds.width);
  357. this._iframe.setAttribute("height", bounds.height);
  358. this._width = this._canvas.width = bounds.width * this._pixelRatio;
  359. this._height = this._canvas.height = bounds.height * this._pixelRatio;
  360. this._shouldRedraw = true;
  361. this.emit("refresh");
  362. },
  363. /**
  364. * Sets the theme via `theme` to either "light" or "dark",
  365. * and updates the internal styling to match. Requires a redraw
  366. * to see the effects.
  367. */
  368. setTheme: function (theme) {
  369. theme = theme || "light";
  370. this.overviewHeaderBackgroundColor = getColor("body-background", theme);
  371. this.overviewHeaderTextColor = getColor("body-color", theme);
  372. // Hard to get a color that is readable across both themes for the text
  373. // on the flames
  374. this.blockTextColor = getColor(theme === "dark" ? "selection-color"
  375. : "body-color", theme);
  376. },
  377. /**
  378. * The contents of this graph are redrawn only when something changed,
  379. * like the data source, or the selection bounds etc. This flag tracks
  380. * if the rendering is "dirty" and needs to be refreshed.
  381. */
  382. _shouldRedraw: false,
  383. /**
  384. * Animation frame callback, invoked on each tick of the refresh driver.
  385. */
  386. _onAnimationFrame: function () {
  387. this._animationId =
  388. this._window.requestAnimationFrame(this._onAnimationFrame);
  389. this._drawWidget();
  390. },
  391. /**
  392. * Redraws the widget when necessary. The actual graph is not refreshed
  393. * every time this function is called, only the cliphead, selection etc.
  394. */
  395. _drawWidget: function () {
  396. if (!this._shouldRedraw) {
  397. return;
  398. }
  399. // Unlike mouse events which are updated as needed in their own respective
  400. // handlers, keyboard events are granular and non-continuous (not even
  401. // "keydown", which is fired with a low frequency). Therefore, to maintain
  402. // animation smoothness, update anything that's controllable via the
  403. // keyboard here, in the animation loop, before any actual drawing.
  404. this._keyboardUpdateLoop();
  405. let ctx = this._ctx;
  406. let canvasWidth = this._width;
  407. let canvasHeight = this._height;
  408. ctx.clearRect(0, 0, canvasWidth, canvasHeight);
  409. let selection = this._selection;
  410. let selectionWidth = selection.end - selection.start;
  411. let selectionScale = canvasWidth / selectionWidth;
  412. this._drawTicks(selection.start, selectionScale);
  413. this._drawPyramid(this._data, this._verticalOffset,
  414. selection.start, selectionScale);
  415. this._drawHeader(selection.start, selectionScale);
  416. // If the user isn't doing anything anymore, it's safe to stop drawing.
  417. // XXX: This doesn't handle cases where we should still be drawing even
  418. // if any input stops (e.g. smooth panning transitions after the user
  419. // finishes input). We don't care about that right now.
  420. if (this._userInputStack == 0) {
  421. this._shouldRedraw = false;
  422. return;
  423. }
  424. if (this._userInputStack < 0) {
  425. throw new Error("The user went back in time from a pyramid.");
  426. }
  427. },
  428. /**
  429. * Performs any necessary changes to the graph's state based on the
  430. * user's input on a keyboard.
  431. */
  432. _keyboardUpdateLoop: function () {
  433. const KEY_CODE_UP = 38;
  434. const KEY_CODE_DOWN = 40;
  435. const KEY_CODE_LEFT = 37;
  436. const KEY_CODE_RIGHT = 39;
  437. const KEY_CODE_W = 87;
  438. const KEY_CODE_A = 65;
  439. const KEY_CODE_S = 83;
  440. const KEY_CODE_D = 68;
  441. let canvasWidth = this._width;
  442. let pressed = this._keysPressed;
  443. let selection = this._selection;
  444. let selectionWidth = selection.end - selection.start;
  445. let selectionScale = canvasWidth / selectionWidth;
  446. let translation = [0, 0];
  447. let isZooming = false;
  448. let isPanning = false;
  449. if (pressed[KEY_CODE_UP] || pressed[KEY_CODE_W]) {
  450. translation[0] += GRAPH_KEYBOARD_ZOOM_SENSITIVITY / selectionScale;
  451. translation[1] -= GRAPH_KEYBOARD_ZOOM_SENSITIVITY / selectionScale;
  452. isZooming = true;
  453. }
  454. if (pressed[KEY_CODE_DOWN] || pressed[KEY_CODE_S]) {
  455. translation[0] -= GRAPH_KEYBOARD_ZOOM_SENSITIVITY / selectionScale;
  456. translation[1] += GRAPH_KEYBOARD_ZOOM_SENSITIVITY / selectionScale;
  457. isZooming = true;
  458. }
  459. if (pressed[KEY_CODE_LEFT] || pressed[KEY_CODE_A]) {
  460. translation[0] -= GRAPH_KEYBOARD_PAN_SENSITIVITY / selectionScale;
  461. translation[1] -= GRAPH_KEYBOARD_PAN_SENSITIVITY / selectionScale;
  462. isPanning = true;
  463. }
  464. if (pressed[KEY_CODE_RIGHT] || pressed[KEY_CODE_D]) {
  465. translation[0] += GRAPH_KEYBOARD_PAN_SENSITIVITY / selectionScale;
  466. translation[1] += GRAPH_KEYBOARD_PAN_SENSITIVITY / selectionScale;
  467. isPanning = true;
  468. }
  469. if (isPanning) {
  470. // Accelerate the left/right selection panning continuously
  471. // while the pan keys are pressed.
  472. this._keyboardPanAccelerationFactor *= GRAPH_KEYBOARD_ACCELERATION;
  473. translation[0] *= this._keyboardPanAccelerationFactor;
  474. translation[1] *= this._keyboardPanAccelerationFactor;
  475. } else {
  476. this._keyboardPanAccelerationFactor = 1;
  477. }
  478. if (isZooming) {
  479. // Accelerate the in/out selection zooming continuously
  480. // while the zoom keys are pressed.
  481. this._keyboardZoomAccelerationFactor *= GRAPH_KEYBOARD_ACCELERATION;
  482. translation[0] *= this._keyboardZoomAccelerationFactor;
  483. translation[1] *= this._keyboardZoomAccelerationFactor;
  484. } else {
  485. this._keyboardZoomAccelerationFactor = 1;
  486. }
  487. if (translation[0] != 0 || translation[1] != 0) {
  488. // Make sure the panning translation speed doesn't end up
  489. // being too high.
  490. let maxTranslation = GRAPH_KEYBOARD_TRANSLATION_MAX / selectionScale;
  491. if (Math.abs(translation[0]) > maxTranslation) {
  492. translation[0] = Math.sign(translation[0]) * maxTranslation;
  493. }
  494. if (Math.abs(translation[1]) > maxTranslation) {
  495. translation[1] = Math.sign(translation[1]) * maxTranslation;
  496. }
  497. this._selection.start += translation[0];
  498. this._selection.end += translation[1];
  499. this._normalizeSelectionBounds();
  500. this.emit("selecting");
  501. }
  502. },
  503. /**
  504. * Draws the overhead header, with time markers and ticks in this graph.
  505. *
  506. * @param number dataOffset, dataScale
  507. * Offsets and scales the data source by the specified amount.
  508. * This is used for scrolling the visualization.
  509. */
  510. _drawHeader: function (dataOffset, dataScale) {
  511. let ctx = this._ctx;
  512. let canvasWidth = this._width;
  513. let headerHeight = OVERVIEW_HEADER_HEIGHT * this._pixelRatio;
  514. ctx.fillStyle = this.overviewHeaderBackgroundColor;
  515. ctx.fillRect(0, 0, canvasWidth, headerHeight);
  516. this._drawTicks(dataOffset, dataScale, {
  517. from: 0,
  518. to: headerHeight,
  519. renderText: true
  520. });
  521. },
  522. /**
  523. * Draws the overhead ticks in this graph in the flame graph area.
  524. *
  525. * @param number dataOffset, dataScale, from, to, renderText
  526. * Offsets and scales the data source by the specified amount.
  527. * from and to determine the Y position of how far the stroke
  528. * should be drawn.
  529. * This is used when scrolling the visualization.
  530. */
  531. _drawTicks: function (dataOffset, dataScale, options) {
  532. let { from, to, renderText } = options || {};
  533. let ctx = this._ctx;
  534. let canvasWidth = this._width;
  535. let canvasHeight = this._height;
  536. let scaledOffset = dataOffset * dataScale;
  537. let fontSize = OVERVIEW_HEADER_TEXT_FONT_SIZE * this._pixelRatio;
  538. let fontFamily = OVERVIEW_HEADER_TEXT_FONT_FAMILY;
  539. let textPaddingLeft = OVERVIEW_HEADER_TEXT_PADDING_LEFT * this._pixelRatio;
  540. let textPaddingTop = OVERVIEW_HEADER_TEXT_PADDING_TOP * this._pixelRatio;
  541. let tickInterval = this._findOptimalTickInterval(dataScale);
  542. ctx.textBaseline = "top";
  543. ctx.font = fontSize + "px " + fontFamily;
  544. ctx.fillStyle = this.overviewHeaderTextColor;
  545. ctx.strokeStyle = OVERVIEW_HEADER_TIMELINE_STROKE_COLOR;
  546. ctx.beginPath();
  547. for (let x = -scaledOffset % tickInterval; x < canvasWidth;
  548. x += tickInterval) {
  549. let lineLeft = x;
  550. let textLeft = lineLeft + textPaddingLeft;
  551. let time = Math.round((x / dataScale + dataOffset) / this._pixelRatio);
  552. let label = time + " " + this.timelineTickUnits;
  553. if (renderText) {
  554. ctx.fillText(label, textLeft, textPaddingTop);
  555. }
  556. ctx.moveTo(lineLeft, from || 0);
  557. ctx.lineTo(lineLeft, to || canvasHeight);
  558. }
  559. ctx.stroke();
  560. },
  561. /**
  562. * Draws the blocks and text in this graph.
  563. *
  564. * @param object dataSource
  565. * The data source. See the constructor for more information.
  566. * @param number verticalOffset
  567. * Offsets the drawing vertically by the specified amount.
  568. * @param number dataOffset, dataScale
  569. * Offsets and scales the data source by the specified amount.
  570. * This is used for scrolling the visualization.
  571. */
  572. _drawPyramid: function (dataSource, verticalOffset, dataOffset, dataScale) {
  573. let ctx = this._ctx;
  574. let fontSize = FLAME_GRAPH_BLOCK_TEXT_FONT_SIZE * this._pixelRatio;
  575. let fontFamily = FLAME_GRAPH_BLOCK_TEXT_FONT_FAMILY;
  576. let visibleBlocksInfo = this._drawPyramidFill(dataSource, verticalOffset,
  577. dataOffset, dataScale);
  578. ctx.textBaseline = "middle";
  579. ctx.font = fontSize + "px " + fontFamily;
  580. ctx.fillStyle = this.blockTextColor;
  581. this._drawPyramidText(visibleBlocksInfo, verticalOffset,
  582. dataOffset, dataScale);
  583. },
  584. /**
  585. * Fills all block inside this graph's pyramid.
  586. * @see FlameGraph.prototype._drawPyramid
  587. */
  588. _drawPyramidFill: function (dataSource, verticalOffset, dataOffset,
  589. dataScale) {
  590. let visibleBlocksInfoStore = [];
  591. let minVisibleBlockWidth = this._overflowCharWidth;
  592. for (let { color, blocks } of dataSource) {
  593. this._drawBlocksFill(
  594. color, blocks, verticalOffset, dataOffset, dataScale,
  595. visibleBlocksInfoStore, minVisibleBlockWidth);
  596. }
  597. return visibleBlocksInfoStore;
  598. },
  599. /**
  600. * Adds the text for all block inside this graph's pyramid.
  601. * @see FlameGraph.prototype._drawPyramid
  602. */
  603. _drawPyramidText: function (blocksInfo, verticalOffset, dataOffset,
  604. dataScale) {
  605. for (let { block, rect } of blocksInfo) {
  606. this._drawBlockText(block, rect, verticalOffset, dataOffset, dataScale);
  607. }
  608. },
  609. /**
  610. * Fills a group of blocks sharing the same style.
  611. *
  612. * @param string color
  613. * The color used as the block's background.
  614. * @param array blocks
  615. * A list of { x, y, width, height } objects visually representing
  616. * all the blocks sharing this particular style.
  617. * @param number verticalOffset
  618. * Offsets the drawing vertically by the specified amount.
  619. * @param number dataOffset, dataScale
  620. * Offsets and scales the data source by the specified amount.
  621. * This is used for scrolling the visualization.
  622. * @param array visibleBlocksInfoStore
  623. * An array to store all the visible blocks into, along with the
  624. * final baked coordinates and dimensions, after drawing them.
  625. * The provided array will be populated.
  626. * @param number minVisibleBlockWidth
  627. * The minimum width of the blocks that will be added into
  628. * the `visibleBlocksInfoStore`.
  629. */
  630. _drawBlocksFill: function (
  631. color, blocks, verticalOffset, dataOffset, dataScale,
  632. visibleBlocksInfoStore, minVisibleBlockWidth) {
  633. let ctx = this._ctx;
  634. let canvasWidth = this._width;
  635. let canvasHeight = this._height;
  636. let scaledOffset = dataOffset * dataScale;
  637. ctx.fillStyle = color;
  638. ctx.beginPath();
  639. for (let block of blocks) {
  640. let { x, y, width, height } = block;
  641. let rectLeft = x * this._pixelRatio * dataScale - scaledOffset;
  642. let rectTop = (y - verticalOffset + OVERVIEW_HEADER_HEIGHT)
  643. * this._pixelRatio;
  644. let rectWidth = width * this._pixelRatio * dataScale;
  645. let rectHeight = height * this._pixelRatio;
  646. // Too far respectively right/left/bottom/top
  647. if (rectLeft > canvasWidth ||
  648. rectLeft < -rectWidth ||
  649. rectTop > canvasHeight ||
  650. rectTop < -rectHeight) {
  651. continue;
  652. }
  653. // Clamp the blocks position to start at 0. Avoid negative X coords,
  654. // to properly place the text inside the blocks.
  655. if (rectLeft < 0) {
  656. rectWidth += rectLeft;
  657. rectLeft = 0;
  658. }
  659. // Avoid drawing blocks that are too narrow.
  660. if (rectWidth <= FLAME_GRAPH_BLOCK_BORDER ||
  661. rectHeight <= FLAME_GRAPH_BLOCK_BORDER) {
  662. continue;
  663. }
  664. ctx.rect(
  665. rectLeft, rectTop,
  666. rectWidth - FLAME_GRAPH_BLOCK_BORDER,
  667. rectHeight - FLAME_GRAPH_BLOCK_BORDER);
  668. // Populate the visible blocks store with this block if the width
  669. // is longer than a given threshold.
  670. if (rectWidth > minVisibleBlockWidth) {
  671. visibleBlocksInfoStore.push({
  672. block: block,
  673. rect: { rectLeft, rectTop, rectWidth, rectHeight }
  674. });
  675. }
  676. }
  677. ctx.fill();
  678. },
  679. /**
  680. * Adds text for a single block.
  681. *
  682. * @param object block
  683. * A single { x, y, width, height, text } object visually representing
  684. * the block containing the text.
  685. * @param object rect
  686. * A single { rectLeft, rectTop, rectWidth, rectHeight } object
  687. * representing the final baked coordinates of the drawn rectangle.
  688. * Think of them as screen-space values, vs. object-space values. These
  689. * differ from the scalars in `block` when the graph is scaled/panned.
  690. * @param number verticalOffset
  691. * Offsets the drawing vertically by the specified amount.
  692. * @param number dataOffset, dataScale
  693. * Offsets and scales the data source by the specified amount.
  694. * This is used for scrolling the visualization.
  695. */
  696. _drawBlockText: function (block, rect, verticalOffset, dataOffset,
  697. dataScale) {
  698. let ctx = this._ctx;
  699. let { text } = block;
  700. let { rectLeft, rectTop, rectWidth, rectHeight } = rect;
  701. let paddingTop = FLAME_GRAPH_BLOCK_TEXT_PADDING_TOP * this._pixelRatio;
  702. let paddingLeft = FLAME_GRAPH_BLOCK_TEXT_PADDING_LEFT * this._pixelRatio;
  703. let paddingRight = FLAME_GRAPH_BLOCK_TEXT_PADDING_RIGHT * this._pixelRatio;
  704. let totalHorizontalPadding = paddingLeft + paddingRight;
  705. // Clamp the blocks position to start at 0. Avoid negative X coords,
  706. // to properly place the text inside the blocks.
  707. if (rectLeft < 0) {
  708. rectWidth += rectLeft;
  709. rectLeft = 0;
  710. }
  711. let textLeft = rectLeft + paddingLeft;
  712. let textTop = rectTop + rectHeight / 2 + paddingTop;
  713. let textAvailableWidth = rectWidth - totalHorizontalPadding;
  714. // Massage the text to fit inside a given width. This clamps the string
  715. // at the end to avoid overflowing.
  716. let fittedText = this._getFittedText(text, textAvailableWidth);
  717. if (fittedText.length < 1) {
  718. return;
  719. }
  720. ctx.fillText(fittedText, textLeft, textTop);
  721. },
  722. /**
  723. * Calculating text widths is necessary to trim the text inside the blocks
  724. * while the scaling changes (e.g. via scrolling). This is very expensive,
  725. * so maintain a cache of string contents to text widths.
  726. */
  727. _textWidthsCache: null,
  728. _overflowCharWidth: null,
  729. _averageCharWidth: null,
  730. /**
  731. * Gets the width of the specified text, for the current context state
  732. * (font size, family etc.).
  733. *
  734. * @param string text
  735. * The text to analyze.
  736. * @return number
  737. * The text width.
  738. */
  739. _getTextWidth: function (text) {
  740. let cachedWidth = this._textWidthsCache[text];
  741. if (cachedWidth) {
  742. return cachedWidth;
  743. }
  744. let metrics = this._ctx.measureText(text);
  745. return (this._textWidthsCache[text] = metrics.width);
  746. },
  747. /**
  748. * Gets an approximate width of the specified text. This is much faster
  749. * than `_getTextWidth`, but inexact.
  750. *
  751. * @param string text
  752. * The text to analyze.
  753. * @return number
  754. * The approximate text width.
  755. */
  756. _getTextWidthApprox: function (text) {
  757. return text.length * this._averageCharWidth;
  758. },
  759. /**
  760. * Gets the average letter width in the English alphabet, for the current
  761. * context state (font size, family etc.). This provides a close enough
  762. * value to use in `_getTextWidthApprox`.
  763. *
  764. * @return number
  765. * The average letter width.
  766. */
  767. _calcAverageCharWidth: function () {
  768. let letterWidthsSum = 0;
  769. // space
  770. let start = 32;
  771. // "z"
  772. let end = 123;
  773. for (let i = start; i < end; i++) {
  774. let char = String.fromCharCode(i);
  775. letterWidthsSum += this._getTextWidth(char);
  776. }
  777. return letterWidthsSum / (end - start);
  778. },
  779. /**
  780. * Massage a text to fit inside a given width. This clamps the string
  781. * at the end to avoid overflowing.
  782. *
  783. * @param string text
  784. * The text to fit inside the given width.
  785. * @param number maxWidth
  786. * The available width for the given text.
  787. * @return string
  788. * The fitted text.
  789. */
  790. _getFittedText: function (text, maxWidth) {
  791. let textWidth = this._getTextWidth(text);
  792. if (textWidth < maxWidth) {
  793. return text;
  794. }
  795. if (this._overflowCharWidth > maxWidth) {
  796. return "";
  797. }
  798. for (let i = 1, len = text.length; i <= len; i++) {
  799. let trimmedText = text.substring(0, len - i);
  800. let trimmedWidth = this._getTextWidthApprox(trimmedText)
  801. + this._overflowCharWidth;
  802. if (trimmedWidth < maxWidth) {
  803. return trimmedText + this.overflowChar;
  804. }
  805. }
  806. return "";
  807. },
  808. /**
  809. * Listener for the "keydown" event on the graph's container.
  810. */
  811. _onKeyDown: function (e) {
  812. ViewHelpers.preventScrolling(e);
  813. const hasModifier = e.ctrlKey || e.shiftKey || e.altKey || e.metaKey;
  814. if (!hasModifier && !this._keysPressed[e.keyCode]) {
  815. this._keysPressed[e.keyCode] = true;
  816. this._userInputStack++;
  817. this._shouldRedraw = true;
  818. }
  819. },
  820. /**
  821. * Listener for the "keyup" event on the graph's container.
  822. */
  823. _onKeyUp: function (e) {
  824. ViewHelpers.preventScrolling(e);
  825. if (this._keysPressed[e.keyCode]) {
  826. this._keysPressed[e.keyCode] = false;
  827. this._userInputStack--;
  828. this._shouldRedraw = true;
  829. }
  830. },
  831. /**
  832. * Listener for the "keypress" event on the graph's container.
  833. */
  834. _onKeyPress: function (e) {
  835. ViewHelpers.preventScrolling(e);
  836. },
  837. /**
  838. * Listener for the "mousemove" event on the graph's container.
  839. */
  840. _onMouseMove: function (e) {
  841. let {mouseX, mouseY} = this._getRelativeEventCoordinates(e);
  842. let canvasWidth = this._width;
  843. let selection = this._selection;
  844. let selectionWidth = selection.end - selection.start;
  845. let selectionScale = canvasWidth / selectionWidth;
  846. let horizDrag = this._selectionDragger;
  847. let vertDrag = this._verticalOffsetDragger;
  848. // Avoid dragging both horizontally and vertically at the same time,
  849. // as this doesn't feel natural. Based on a minimum distance, enable either
  850. // one, and remember the drag direction to offset the mouse coords later.
  851. if (!this._horizontalDragEnabled && !this._verticalDragEnabled) {
  852. let horizDiff = Math.abs(horizDrag.origin - mouseX);
  853. if (horizDiff > this.horizontalPanThreshold) {
  854. this._horizontalDragDirection = Math.sign(horizDrag.origin - mouseX);
  855. this._horizontalDragEnabled = true;
  856. }
  857. let vertDiff = Math.abs(vertDrag.origin - mouseY);
  858. if (vertDiff > this.verticalPanThreshold) {
  859. this._verticalDragDirection = Math.sign(vertDrag.origin - mouseY);
  860. this._verticalDragEnabled = true;
  861. }
  862. }
  863. if (horizDrag.origin != null && this._horizontalDragEnabled) {
  864. let relativeX = mouseX + this._horizontalDragDirection *
  865. this.horizontalPanThreshold;
  866. selection.start = horizDrag.anchor.start +
  867. (horizDrag.origin - relativeX) / selectionScale;
  868. selection.end = horizDrag.anchor.end +
  869. (horizDrag.origin - relativeX) / selectionScale;
  870. this._normalizeSelectionBounds();
  871. this._shouldRedraw = true;
  872. this.emit("selecting");
  873. }
  874. if (vertDrag.origin != null && this._verticalDragEnabled) {
  875. let relativeY = mouseY +
  876. this._verticalDragDirection * this.verticalPanThreshold;
  877. this._verticalOffset = vertDrag.anchor +
  878. (vertDrag.origin - relativeY) / this._pixelRatio;
  879. this._normalizeVerticalOffset();
  880. this._shouldRedraw = true;
  881. this.emit("panning-vertically");
  882. }
  883. },
  884. /**
  885. * Listener for the "mousedown" event on the graph's container.
  886. */
  887. _onMouseDown: function (e) {
  888. let {mouseX, mouseY} = this._getRelativeEventCoordinates(e);
  889. this._selectionDragger.origin = mouseX;
  890. this._selectionDragger.anchor.start = this._selection.start;
  891. this._selectionDragger.anchor.end = this._selection.end;
  892. this._verticalOffsetDragger.origin = mouseY;
  893. this._verticalOffsetDragger.anchor = this._verticalOffset;
  894. this._horizontalDragEnabled = false;
  895. this._verticalDragEnabled = false;
  896. this._canvas.setAttribute("input", "adjusting-view-area");
  897. },
  898. /**
  899. * Listener for the "mouseup" event on the graph's container.
  900. */
  901. _onMouseUp: function () {
  902. this._selectionDragger.origin = null;
  903. this._verticalOffsetDragger.origin = null;
  904. this._horizontalDragEnabled = false;
  905. this._horizontalDragDirection = 0;
  906. this._verticalDragEnabled = false;
  907. this._verticalDragDirection = 0;
  908. this._canvas.removeAttribute("input");
  909. },
  910. /**
  911. * Listener for the "wheel" event on the graph's container.
  912. */
  913. _onMouseWheel: function (e) {
  914. let {mouseX} = this._getRelativeEventCoordinates(e);
  915. let canvasWidth = this._width;
  916. let selection = this._selection;
  917. let selectionWidth = selection.end - selection.start;
  918. let selectionScale = canvasWidth / selectionWidth;
  919. switch (e.axis) {
  920. case e.VERTICAL_AXIS: {
  921. let distFromStart = mouseX;
  922. let distFromEnd = canvasWidth - mouseX;
  923. let vector = e.detail * GRAPH_WHEEL_ZOOM_SENSITIVITY / selectionScale;
  924. selection.start -= distFromStart * vector;
  925. selection.end += distFromEnd * vector;
  926. break;
  927. }
  928. case e.HORIZONTAL_AXIS: {
  929. let vector = e.detail * GRAPH_WHEEL_SCROLL_SENSITIVITY / selectionScale;
  930. selection.start += vector;
  931. selection.end += vector;
  932. break;
  933. }
  934. }
  935. this._normalizeSelectionBounds();
  936. this._shouldRedraw = true;
  937. this.emit("selecting");
  938. },
  939. /**
  940. * Makes sure the start and end points of the current selection
  941. * are withing the graph's visible bounds, and that they form a selection
  942. * wider than the allowed minimum width.
  943. */
  944. _normalizeSelectionBounds: function () {
  945. let boundsStart = this._bounds.start;
  946. let boundsEnd = this._bounds.end;
  947. let selectionStart = this._selection.start;
  948. let selectionEnd = this._selection.end;
  949. if (selectionStart < boundsStart) {
  950. selectionStart = boundsStart;
  951. }
  952. if (selectionEnd < boundsStart) {
  953. selectionStart = boundsStart;
  954. selectionEnd = GRAPH_MIN_SELECTION_WIDTH;
  955. }
  956. if (selectionEnd > boundsEnd) {
  957. selectionEnd = boundsEnd;
  958. }
  959. if (selectionStart > boundsEnd) {
  960. selectionEnd = boundsEnd;
  961. selectionStart = boundsEnd - GRAPH_MIN_SELECTION_WIDTH;
  962. }
  963. if (selectionEnd - selectionStart < GRAPH_MIN_SELECTION_WIDTH) {
  964. let midPoint = (selectionStart + selectionEnd) / 2;
  965. selectionStart = midPoint - GRAPH_MIN_SELECTION_WIDTH / 2;
  966. selectionEnd = midPoint + GRAPH_MIN_SELECTION_WIDTH / 2;
  967. }
  968. this._selection.start = selectionStart;
  969. this._selection.end = selectionEnd;
  970. },
  971. /**
  972. * Makes sure that the current vertical offset is within the allowed
  973. * panning range.
  974. */
  975. _normalizeVerticalOffset: function () {
  976. this._verticalOffset = Math.max(this._verticalOffset, 0);
  977. },
  978. /**
  979. *
  980. * Finds the optimal tick interval between time markers in this graph.
  981. *
  982. * @param number dataScale
  983. * @return number
  984. */
  985. _findOptimalTickInterval: function (dataScale) {
  986. let timingStep = TIMELINE_TICKS_MULTIPLE;
  987. let spacingMin = TIMELINE_TICKS_SPACING_MIN * this._pixelRatio;
  988. let maxIters = FIND_OPTIMAL_TICK_INTERVAL_MAX_ITERS;
  989. let numIters = 0;
  990. if (dataScale > spacingMin) {
  991. return dataScale;
  992. }
  993. while (true) {
  994. let scaledStep = dataScale * timingStep;
  995. if (++numIters > maxIters) {
  996. return scaledStep;
  997. }
  998. if (scaledStep < spacingMin) {
  999. timingStep <<= 1;
  1000. continue;
  1001. }
  1002. return scaledStep;
  1003. }
  1004. },
  1005. /**
  1006. * Gets the offset of this graph's container relative to the owner window.
  1007. *
  1008. * @return object
  1009. * The { left, top } offset.
  1010. */
  1011. _getContainerOffset: function () {
  1012. let node = this._canvas;
  1013. let x = 0;
  1014. let y = 0;
  1015. while ((node = node.offsetParent)) {
  1016. x += node.offsetLeft;
  1017. y += node.offsetTop;
  1018. }
  1019. return { left: x, top: y };
  1020. },
  1021. /**
  1022. * Given a MouseEvent, make it relative to this._canvas.
  1023. * @return object {mouseX,mouseY}
  1024. */
  1025. _getRelativeEventCoordinates: function (e) {
  1026. // For ease of testing, testX and testY can be passed in as the event
  1027. // object.
  1028. if ("testX" in e && "testY" in e) {
  1029. return {
  1030. mouseX: e.testX * this._pixelRatio,
  1031. mouseY: e.testY * this._pixelRatio
  1032. };
  1033. }
  1034. let offset = this._getContainerOffset();
  1035. let mouseX = (e.clientX - offset.left) * this._pixelRatio;
  1036. let mouseY = (e.clientY - offset.top) * this._pixelRatio;
  1037. return {mouseX, mouseY};
  1038. },
  1039. /**
  1040. * Listener for the "resize" event on the graph's parent node.
  1041. */
  1042. _onResize: function () {
  1043. if (this.hasData()) {
  1044. setNamedTimeout(this._uid, GRAPH_RESIZE_EVENTS_DRAIN, this.refresh);
  1045. }
  1046. }
  1047. };
  1048. /**
  1049. * A collection of utility functions converting various data sources
  1050. * into a format drawable by the FlameGraph.
  1051. */
  1052. var FlameGraphUtils = {
  1053. _cache: new WeakMap(),
  1054. /**
  1055. * Create data suitable for use with FlameGraph from a profile's samples.
  1056. * Iterate the profile's samples and keep a moving window of stack traces.
  1057. *
  1058. * @param object thread
  1059. * The raw thread object received from the backend.
  1060. * @param object options
  1061. * Additional supported options,
  1062. * - boolean contentOnly [optional]
  1063. * - boolean invertTree [optional]
  1064. * - boolean flattenRecursion [optional]
  1065. * - string showIdleBlocks [optional]
  1066. * @return object
  1067. * Data source usable by FlameGraph.
  1068. */
  1069. createFlameGraphDataFromThread: function (thread, options = {}, out = []) {
  1070. let cached = this._cache.get(thread);
  1071. if (cached) {
  1072. return cached;
  1073. }
  1074. // 1. Create a map of colors to arrays, representing buckets of
  1075. // blocks inside the flame graph pyramid sharing the same style.
  1076. let buckets = Array.from({ length: PALLETTE_SIZE }, () => []);
  1077. // 2. Populate the buckets by iterating over every frame in every sample.
  1078. let { samples, stackTable, frameTable, stringTable } = thread;
  1079. const SAMPLE_STACK_SLOT = samples.schema.stack;
  1080. const SAMPLE_TIME_SLOT = samples.schema.time;
  1081. const STACK_PREFIX_SLOT = stackTable.schema.prefix;
  1082. const STACK_FRAME_SLOT = stackTable.schema.frame;
  1083. const getOrAddInflatedFrame = FrameUtils.getOrAddInflatedFrame;
  1084. let inflatedFrameCache = FrameUtils.getInflatedFrameCache(frameTable);
  1085. let labelCache = Object.create(null);
  1086. let samplesData = samples.data;
  1087. let stacksData = stackTable.data;
  1088. let flattenRecursion = options.flattenRecursion;
  1089. // Reused objects.
  1090. let mutableFrameKeyOptions = {
  1091. contentOnly: options.contentOnly,
  1092. isRoot: false,
  1093. isLeaf: false,
  1094. isMetaCategoryOut: false
  1095. };
  1096. // Take the timestamp of the first sample as prevTime. 0 is incorrect due
  1097. // to circular buffer wraparound. If wraparound happens, then the first
  1098. // sample will have an incorrect, large duration.
  1099. let prevTime = samplesData.length > 0 ? samplesData[0][SAMPLE_TIME_SLOT]
  1100. : 0;
  1101. let prevFrames = [];
  1102. let sampleFrames = [];
  1103. let sampleFrameKeys = [];
  1104. for (let i = 1; i < samplesData.length; i++) {
  1105. let sample = samplesData[i];
  1106. let time = sample[SAMPLE_TIME_SLOT];
  1107. let stackIndex = sample[SAMPLE_STACK_SLOT];
  1108. let prevFrameKey;
  1109. let stackDepth = 0;
  1110. // Inflate the stack and keep a moving window of call stacks.
  1111. //
  1112. // For reference, see the similar block comment in
  1113. // ThreadNode.prototype._buildInverted.
  1114. //
  1115. // In a similar fashion to _buildInverted, frames are inflated on the
  1116. // fly while stackwalking the stackTable trie. The exact same frame key
  1117. // is computed in both _buildInverted and here.
  1118. //
  1119. // Unlike _buildInverted, which builds a call tree directly, the flame
  1120. // graph inflates the stack into an array, as it maintains a moving
  1121. // window of stacks over time.
  1122. //
  1123. // Like _buildInverted, the various filtering functions are also inlined
  1124. // into stack inflation loop.
  1125. while (stackIndex !== null) {
  1126. let stackEntry = stacksData[stackIndex];
  1127. let frameIndex = stackEntry[STACK_FRAME_SLOT];
  1128. // Fetch the stack prefix (i.e. older frames) index.
  1129. stackIndex = stackEntry[STACK_PREFIX_SLOT];
  1130. // Inflate the frame.
  1131. let inflatedFrame = getOrAddInflatedFrame(inflatedFrameCache,
  1132. frameIndex, frameTable,
  1133. stringTable);
  1134. mutableFrameKeyOptions.isRoot = stackIndex === null;
  1135. mutableFrameKeyOptions.isLeaf = stackDepth === 0;
  1136. let frameKey = inflatedFrame.getFrameKey(mutableFrameKeyOptions);
  1137. // If not skipping the frame, add it to the current level. The (root)
  1138. // node isn't useful for flame graphs.
  1139. if (frameKey !== "" && frameKey !== "(root)") {
  1140. // If the frame is a meta category, use the category label.
  1141. if (mutableFrameKeyOptions.isMetaCategoryOut) {
  1142. frameKey = CATEGORY_MAPPINGS[frameKey].label;
  1143. }
  1144. sampleFrames[stackDepth] = inflatedFrame;
  1145. sampleFrameKeys[stackDepth] = frameKey;
  1146. // If we shouldn't flatten the current frame into the previous one,
  1147. // increment the stack depth.
  1148. if (!flattenRecursion || frameKey !== prevFrameKey) {
  1149. stackDepth++;
  1150. }
  1151. prevFrameKey = frameKey;
  1152. }
  1153. }
  1154. // Uninvert frames in place if needed.
  1155. if (!options.invertTree) {
  1156. sampleFrames.length = stackDepth;
  1157. sampleFrames.reverse();
  1158. sampleFrameKeys.length = stackDepth;
  1159. sampleFrameKeys.reverse();
  1160. }
  1161. // If no frames are available, add a pseudo "idle" block in between.
  1162. let isIdleFrame = false;
  1163. if (options.showIdleBlocks && stackDepth === 0) {
  1164. sampleFrames[0] = null;
  1165. sampleFrameKeys[0] = options.showIdleBlocks;
  1166. stackDepth = 1;
  1167. isIdleFrame = true;
  1168. }
  1169. // Put each frame in a bucket.
  1170. for (let frameIndex = 0; frameIndex < stackDepth; frameIndex++) {
  1171. let key = sampleFrameKeys[frameIndex];
  1172. let prevFrame = prevFrames[frameIndex];
  1173. // Frames at the same location and the same depth will be reused.
  1174. // If there is a block already created, change its width.
  1175. if (prevFrame && prevFrame.frameKey === key) {
  1176. prevFrame.width = (time - prevFrame.startTime);
  1177. } else {
  1178. // Otherwise, create a new block for this frame at this depth,
  1179. // using a simple location based salt for picking a color.
  1180. let hash = this._getStringHash(key);
  1181. let bucket = buckets[hash % PALLETTE_SIZE];
  1182. let label;
  1183. if (isIdleFrame) {
  1184. label = key;
  1185. } else {
  1186. label = labelCache[key];
  1187. if (!label) {
  1188. label = labelCache[key] =
  1189. this._formatLabel(key, sampleFrames[frameIndex]);
  1190. }
  1191. }
  1192. bucket.push(prevFrames[frameIndex] = {
  1193. startTime: prevTime,
  1194. frameKey: key,
  1195. x: prevTime,
  1196. y: frameIndex * FLAME_GRAPH_BLOCK_HEIGHT,
  1197. width: time - prevTime,
  1198. height: FLAME_GRAPH_BLOCK_HEIGHT,
  1199. text: label
  1200. });
  1201. }
  1202. }
  1203. // Previous frames at stack depths greater than the current sample's
  1204. // maximum need to be nullified. It's nonsensical to reuse them.
  1205. prevFrames.length = stackDepth;
  1206. prevTime = time;
  1207. }
  1208. // 3. Convert the buckets into a data source usable by the FlameGraph.
  1209. // This is a simple conversion from a Map to an Array.
  1210. for (let i = 0; i < buckets.length; i++) {
  1211. out.push({ color: COLOR_PALLETTE[i], blocks: buckets[i] });
  1212. }
  1213. this._cache.set(thread, out);
  1214. return out;
  1215. },
  1216. /**
  1217. * Clears the cached flame graph data created for the given source.
  1218. * @param any source
  1219. */
  1220. removeFromCache: function (source) {
  1221. this._cache.delete(source);
  1222. },
  1223. /**
  1224. * Very dumb hashing of a string. Used to pick colors from a pallette.
  1225. *
  1226. * @param string input
  1227. * @return number
  1228. */
  1229. _getStringHash: function (input) {
  1230. const STRING_HASH_PRIME1 = 7;
  1231. const STRING_HASH_PRIME2 = 31;
  1232. let hash = STRING_HASH_PRIME1;
  1233. for (let i = 0, len = input.length; i < len; i++) {
  1234. hash *= STRING_HASH_PRIME2;
  1235. hash += input.charCodeAt(i);
  1236. if (hash > Number.MAX_SAFE_INTEGER / STRING_HASH_PRIME2) {
  1237. return hash;
  1238. }
  1239. }
  1240. return hash;
  1241. },
  1242. /**
  1243. * Takes a frame key and a frame, and returns a string that should be
  1244. * displayed in its flame block.
  1245. *
  1246. * @param string key
  1247. * @param object frame
  1248. * @return string
  1249. */
  1250. _formatLabel: function (key, frame) {
  1251. let { functionName, fileName, line } =
  1252. FrameUtils.parseLocation(key, frame.line);
  1253. let label = FrameUtils.shouldDemangle(functionName) ? demangle(functionName)
  1254. : functionName;
  1255. if (fileName) {
  1256. label += ` (${fileName}${line != null ? (":" + line) : ""})`;
  1257. }
  1258. return label;
  1259. }
  1260. };
  1261. exports.FlameGraph = FlameGraph;
  1262. exports.FlameGraphUtils = FlameGraphUtils;
  1263. exports.PALLETTE_SIZE = PALLETTE_SIZE;
  1264. exports.FLAME_GRAPH_BLOCK_HEIGHT = FLAME_GRAPH_BLOCK_HEIGHT;
  1265. exports.FLAME_GRAPH_BLOCK_TEXT_FONT_SIZE = FLAME_GRAPH_BLOCK_TEXT_FONT_SIZE;
  1266. exports.FLAME_GRAPH_BLOCK_TEXT_FONT_FAMILY = FLAME_GRAPH_BLOCK_TEXT_FONT_FAMILY;