FlameChart.js 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815
  1. /*
  2. * Copyright (C) 2013 Google Inc. All rights reserved.
  3. *
  4. * Redistribution and use in source and binary forms, with or without
  5. * modification, are permitted provided that the following conditions are
  6. * met:
  7. *
  8. * * Redistributions of source code must retain the above copyright
  9. * notice, this list of conditions and the following disclaimer.
  10. * * Redistributions in binary form must reproduce the above
  11. * copyright notice, this list of conditions and the following disclaimer
  12. * in the documentation and/or other materials provided with the
  13. * distribution.
  14. * * Neither the name of Google Inc. nor the names of its
  15. * contributors may be used to endorse or promote products derived from
  16. * this software without specific prior written permission.
  17. *
  18. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  19. * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  20. * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
  21. * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
  22. * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  23. * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  24. * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  25. * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
  26. * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  27. * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  28. * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  29. */
  30. /**
  31. * @constructor
  32. * @extends {WebInspector.View}
  33. * @param {WebInspector.CPUProfileView} cpuProfileView
  34. */
  35. WebInspector.FlameChart = function(cpuProfileView)
  36. {
  37. WebInspector.View.call(this);
  38. this.registerRequiredCSS("flameChart.css");
  39. this.element.className = "fill";
  40. this.element.id = "cpu-flame-chart";
  41. this._overviewContainer = this.element.createChild("div", "overview-container");
  42. this._overviewGrid = new WebInspector.OverviewGrid("flame-chart");
  43. this._overviewCanvas = this._overviewContainer.createChild("canvas", "flame-chart-overview-canvas");
  44. this._overviewContainer.appendChild(this._overviewGrid.element);
  45. this._overviewCalculator = new WebInspector.FlameChart.OverviewCalculator();
  46. this._overviewGrid.addEventListener(WebInspector.OverviewGrid.Events.WindowChanged, this._onWindowChanged, this);
  47. this._chartContainer = this.element.createChild("div", "chart-container");
  48. this._timelineGrid = new WebInspector.TimelineGrid();
  49. this._chartContainer.appendChild(this._timelineGrid.element);
  50. this._calculator = new WebInspector.FlameChart.Calculator();
  51. this._canvas = this._chartContainer.createChild("canvas");
  52. this._canvas.addEventListener("mousemove", this._onMouseMove.bind(this));
  53. WebInspector.installDragHandle(this._canvas, this._startCanvasDragging.bind(this), this._canvasDragging.bind(this), this._endCanvasDragging.bind(this), "col-resize");
  54. this._cpuProfileView = cpuProfileView;
  55. this._windowLeft = 0.0;
  56. this._windowRight = 1.0;
  57. this._barHeight = 15;
  58. this._minWidth = 2;
  59. this._paddingLeft = 15;
  60. this._canvas.addEventListener("mousewheel", this._onMouseWheel.bind(this), false);
  61. this._canvas.addEventListener("click", this._onClick.bind(this), false);
  62. this._linkifier = new WebInspector.Linkifier();
  63. this._highlightedEntryIndex = -1;
  64. if (!WebInspector.FlameChart._colorGenerator)
  65. WebInspector.FlameChart._colorGenerator = new WebInspector.FlameChart.ColorGenerator();
  66. }
  67. /**
  68. * @constructor
  69. * @implements {WebInspector.TimelineGrid.Calculator}
  70. */
  71. WebInspector.FlameChart.Calculator = function()
  72. {
  73. }
  74. WebInspector.FlameChart.Calculator.prototype = {
  75. /**
  76. * @param {WebInspector.FlameChart} flameChart
  77. */
  78. _updateBoundaries: function(flameChart)
  79. {
  80. this._minimumBoundaries = flameChart._windowLeft * flameChart._timelineData.totalTime;
  81. this._maximumBoundaries = flameChart._windowRight * flameChart._timelineData.totalTime;
  82. this.paddingLeft = flameChart._paddingLeft;
  83. this._width = flameChart._canvas.width - this.paddingLeft;
  84. this._timeToPixel = this._width / this.boundarySpan();
  85. },
  86. /**
  87. * @param {number} time
  88. */
  89. computePosition: function(time)
  90. {
  91. return (time - this._minimumBoundaries) * this._timeToPixel + this.paddingLeft;
  92. },
  93. formatTime: function(value)
  94. {
  95. return WebInspector.UIString("%s\u2009ms", Number.withThousandsSeparator(Math.round(value + this._minimumBoundaries)));
  96. },
  97. maximumBoundary: function()
  98. {
  99. return this._maximumBoundaries;
  100. },
  101. minimumBoundary: function()
  102. {
  103. return this._minimumBoundaries;
  104. },
  105. zeroTime: function()
  106. {
  107. return 0;
  108. },
  109. boundarySpan: function()
  110. {
  111. return this._maximumBoundaries - this._minimumBoundaries;
  112. }
  113. }
  114. /**
  115. * @constructor
  116. * @implements {WebInspector.TimelineGrid.Calculator}
  117. */
  118. WebInspector.FlameChart.OverviewCalculator = function()
  119. {
  120. }
  121. WebInspector.FlameChart.OverviewCalculator.prototype = {
  122. /**
  123. * @param {WebInspector.FlameChart} flameChart
  124. */
  125. _updateBoundaries: function(flameChart)
  126. {
  127. this._minimumBoundaries = 0;
  128. this._maximumBoundaries = flameChart._timelineData.totalTime;
  129. this._xScaleFactor = flameChart._canvas.width / flameChart._timelineData.totalTime;
  130. },
  131. /**
  132. * @param {number} time
  133. */
  134. computePosition: function(time)
  135. {
  136. return (time - this._minimumBoundaries) * this._xScaleFactor;
  137. },
  138. formatTime: function(value)
  139. {
  140. return Number.secondsToString((value + this._minimumBoundaries) / 1000);
  141. },
  142. maximumBoundary: function()
  143. {
  144. return this._maximumBoundaries;
  145. },
  146. minimumBoundary: function()
  147. {
  148. return this._minimumBoundaries;
  149. },
  150. zeroTime: function()
  151. {
  152. return this._minimumBoundaries;
  153. },
  154. boundarySpan: function()
  155. {
  156. return this._maximumBoundaries - this._minimumBoundaries;
  157. }
  158. }
  159. WebInspector.FlameChart.Events = {
  160. SelectedNode: "SelectedNode"
  161. }
  162. /**
  163. * @constructor
  164. */
  165. WebInspector.FlameChart.ColorGenerator = function()
  166. {
  167. this._colorPairs = {};
  168. this._currentColorIndex = 0;
  169. this._colorPairs["(idle)::0"] = this._createPair(0, 50);
  170. this._colorPairs["(program)::0"] = this._createPair(5, 50);
  171. this._colorPairs["(garbage collector)::0"] = this._createPair(10, 50);
  172. }
  173. WebInspector.FlameChart.ColorGenerator.prototype = {
  174. /**
  175. * @param {!string} id
  176. */
  177. _colorPairForID: function(id)
  178. {
  179. var colorPairs = this._colorPairs;
  180. var colorPair = colorPairs[id];
  181. if (!colorPair)
  182. colorPairs[id] = colorPair = this._createPair(++this._currentColorIndex);
  183. return colorPair;
  184. },
  185. /**
  186. * @param {!number} index
  187. * @param {number=} sat
  188. */
  189. _createPair: function(index, sat)
  190. {
  191. var hue = (index * 7 + 12 * (index % 2)) % 360;
  192. if (typeof sat !== "number")
  193. sat = 100;
  194. return {highlighted: "hsla(" + hue + ", " + sat + "%, 33%, 0.7)", normal: "hsla(" + hue + ", " + sat + "%, 66%, 0.7)"}
  195. }
  196. }
  197. /**
  198. * @constructor
  199. * @param {!Object} colorPair
  200. * @param {!number} depth
  201. * @param {!number} duration
  202. * @param {!number} startTime
  203. * @param {Object} node
  204. */
  205. WebInspector.FlameChart.Entry = function(colorPair, depth, duration, startTime, node)
  206. {
  207. this.colorPair = colorPair;
  208. this.depth = depth;
  209. this.duration = duration;
  210. this.startTime = startTime;
  211. this.node = node;
  212. this.selfTime = 0;
  213. }
  214. WebInspector.FlameChart.prototype = {
  215. /**
  216. * @param {!number} timeLeft
  217. * @param {!number} timeRight
  218. */
  219. selectRange: function(timeLeft, timeRight)
  220. {
  221. this._overviewGrid.setWindow(timeLeft / this._totalTime, timeRight / this._totalTime);
  222. },
  223. _onWindowChanged: function(event)
  224. {
  225. this._scheduleUpdate();
  226. },
  227. _startCanvasDragging: function(event)
  228. {
  229. if (!this._timelineData)
  230. return false;
  231. this._isDragging = true;
  232. this._wasDragged = false;
  233. this._dragStartPoint = event.pageX;
  234. this._dragStartWindowLeft = this._windowLeft;
  235. this._dragStartWindowRight = this._windowRight;
  236. return true;
  237. },
  238. _canvasDragging: function(event)
  239. {
  240. var pixelShift = this._dragStartPoint - event.pageX;
  241. var windowShift = pixelShift / this._totalPixels;
  242. var windowLeft = Math.max(0, this._dragStartWindowLeft + windowShift);
  243. if (windowLeft === this._windowLeft)
  244. return;
  245. windowShift = windowLeft - this._dragStartWindowLeft;
  246. var windowRight = Math.min(1, this._dragStartWindowRight + windowShift);
  247. if (windowRight === this._windowRight)
  248. return;
  249. windowShift = windowRight - this._dragStartWindowRight;
  250. this._overviewGrid.setWindow(this._dragStartWindowLeft + windowShift, this._dragStartWindowRight + windowShift);
  251. this._wasDragged = true;
  252. },
  253. _endCanvasDragging: function()
  254. {
  255. this._isDragging = false;
  256. },
  257. _calculateTimelineData: function()
  258. {
  259. if (this._cpuProfileView.samples)
  260. return this._calculateTimelineDataForSamples();
  261. if (this._timelineData)
  262. return this._timelineData;
  263. if (!this._cpuProfileView.profileHead)
  264. return null;
  265. var index = 0;
  266. var entries = [];
  267. function appendReversedArray(toArray, fromArray)
  268. {
  269. for (var i = fromArray.length - 1; i >= 0; --i)
  270. toArray.push(fromArray[i]);
  271. }
  272. var stack = [];
  273. appendReversedArray(stack, this._cpuProfileView.profileHead.children);
  274. var levelOffsets = /** @type {Array.<!number>} */ ([0]);
  275. var levelExitIndexes = /** @type {Array.<!number>} */ ([0]);
  276. var colorGenerator = WebInspector.FlameChart._colorGenerator;
  277. while (stack.length) {
  278. var level = levelOffsets.length - 1;
  279. var node = stack.pop();
  280. var offset = levelOffsets[level];
  281. var colorPair = colorGenerator._colorPairForID(node.functionName + ":" + node.url + ":" + node.lineNumber);
  282. entries.push(new WebInspector.FlameChart.Entry(colorPair, level, node.totalTime, offset, node));
  283. ++index;
  284. levelOffsets[level] += node.totalTime;
  285. if (node.children.length) {
  286. levelExitIndexes.push(stack.length);
  287. levelOffsets.push(offset + node.selfTime / 2);
  288. appendReversedArray(stack, node.children);
  289. }
  290. while (stack.length === levelExitIndexes[levelExitIndexes.length - 1]) {
  291. levelOffsets.pop();
  292. levelExitIndexes.pop();
  293. }
  294. }
  295. this._timelineData = {
  296. entries: entries,
  297. totalTime: this._cpuProfileView.profileHead.totalTime,
  298. }
  299. return this._timelineData;
  300. },
  301. _calculateTimelineDataForSamples: function()
  302. {
  303. if (this._timelineData)
  304. return this._timelineData;
  305. if (!this._cpuProfileView.profileHead)
  306. return null;
  307. var samples = this._cpuProfileView.samples;
  308. var idToNode = this._cpuProfileView._idToNode;
  309. var gcNode = this._cpuProfileView._gcNode;
  310. var samplesCount = samples.length;
  311. var index = 0;
  312. var entries = /** @type {Array.<!WebInspector.FlameChart.Entry>} */ ([]);
  313. var openIntervals = [];
  314. var stackTrace = [];
  315. var colorGenerator = WebInspector.FlameChart._colorGenerator;
  316. for (var sampleIndex = 0; sampleIndex < samplesCount; sampleIndex++) {
  317. var node = idToNode[samples[sampleIndex]];
  318. stackTrace.length = 0;
  319. while (node) {
  320. stackTrace.push(node);
  321. node = node.parent;
  322. }
  323. stackTrace.pop(); // Remove (root) node
  324. var depth = 0;
  325. node = stackTrace.pop();
  326. var intervalIndex;
  327. // GC samples have no stack, so we just put GC node on top of the last recoreded sample.
  328. if (node === gcNode) {
  329. while (depth < openIntervals.length) {
  330. intervalIndex = openIntervals[depth].index;
  331. entries[intervalIndex].duration += 1;
  332. ++depth;
  333. }
  334. // If previous stack is also GC then just continue.
  335. if (openIntervals.length > 0 && openIntervals.peekLast().node === node) {
  336. entries[intervalIndex].selfTime += 1;
  337. continue;
  338. }
  339. }
  340. while (node && depth < openIntervals.length && node === openIntervals[depth].node) {
  341. intervalIndex = openIntervals[depth].index;
  342. entries[intervalIndex].duration += 1;
  343. node = stackTrace.pop();
  344. ++depth;
  345. }
  346. if (depth < openIntervals.length)
  347. openIntervals.length = depth;
  348. if (!node) {
  349. entries[intervalIndex].selfTime += 1;
  350. continue;
  351. }
  352. while (node) {
  353. var colorPair = colorGenerator._colorPairForID(node.functionName + ":" + node.url + ":" + node.lineNumber);
  354. entries.push(new WebInspector.FlameChart.Entry(colorPair, depth, 1, sampleIndex, node));
  355. openIntervals.push({node: node, index: index});
  356. ++index;
  357. node = stackTrace.pop();
  358. ++depth;
  359. }
  360. entries[entries.length - 1].selfTime += 1;
  361. }
  362. this._timelineData = {
  363. entries: entries,
  364. totalTime: samplesCount,
  365. };
  366. return this._timelineData;
  367. },
  368. _onMouseMove: function(event)
  369. {
  370. if (this._isDragging)
  371. return;
  372. var entryIndex = this._coordinatesToEntryIndex(event.offsetX, event.offsetY);
  373. if (this._highlightedEntryIndex === entryIndex)
  374. return;
  375. if (entryIndex === -1 || this._timelineData.entries[entryIndex].node.scriptId === "0")
  376. this._canvas.style.cursor = "default";
  377. else
  378. this._canvas.style.cursor = "pointer";
  379. this._highlightedEntryIndex = entryIndex;
  380. this._scheduleUpdate();
  381. },
  382. _millisecondsToString: function(ms)
  383. {
  384. if (ms === 0)
  385. return "0";
  386. if (ms < 1000)
  387. return WebInspector.UIString("%.1f\u2009ms", ms);
  388. return Number.secondsToString(ms / 1000, true);
  389. },
  390. _prepareHighlightedEntryInfo: function()
  391. {
  392. if (this._isDragging)
  393. return null;
  394. var entry = this._timelineData.entries[this._highlightedEntryIndex];
  395. if (!entry)
  396. return null;
  397. var node = entry.node;
  398. if (!node)
  399. return null;
  400. var entryInfo = [];
  401. function pushEntryInfoRow(title, text)
  402. {
  403. var row = {};
  404. row.title = title;
  405. row.text = text;
  406. entryInfo.push(row);
  407. }
  408. pushEntryInfoRow(WebInspector.UIString("Name"), node.functionName);
  409. if (this._cpuProfileView.samples) {
  410. var rate = this._cpuProfileView.samplesPerMs;
  411. var selfTime = this._millisecondsToString(entry.selfTime / rate);
  412. var totalTime = this._millisecondsToString(entry.duration / rate);
  413. pushEntryInfoRow(WebInspector.UIString("Self time"), selfTime);
  414. pushEntryInfoRow(WebInspector.UIString("Total time"), totalTime);
  415. }
  416. if (node.url)
  417. pushEntryInfoRow(WebInspector.UIString("URL"), node.url + ":" + node.lineNumber);
  418. pushEntryInfoRow(WebInspector.UIString("Aggregated self time"), Number.secondsToString(node.selfTime / 1000, true));
  419. pushEntryInfoRow(WebInspector.UIString("Aggregated total time"), Number.secondsToString(node.totalTime / 1000, true));
  420. return entryInfo;
  421. },
  422. _onClick: function(e)
  423. {
  424. // onClick comes after dragStart and dragEnd events.
  425. // So if there was drag (mouse move) in the middle of that events
  426. // we skip the click. Otherwise we jump to the sources.
  427. if (this._wasDragged)
  428. return;
  429. if (this._highlightedEntryIndex === -1)
  430. return;
  431. var node = this._timelineData.entries[this._highlightedEntryIndex].node;
  432. this.dispatchEventToListeners(WebInspector.FlameChart.Events.SelectedNode, node);
  433. },
  434. _onMouseWheel: function(e)
  435. {
  436. if (e.wheelDeltaY) {
  437. const zoomFactor = 1.1;
  438. const mouseWheelZoomSpeed = 1 / 120;
  439. var zoom = Math.pow(zoomFactor, -e.wheelDeltaY * mouseWheelZoomSpeed);
  440. var overviewReference = (this._pixelWindowLeft + e.offsetX - this._paddingLeft) / this._totalPixels;
  441. this._overviewGrid.zoom(zoom, overviewReference);
  442. } else {
  443. var shift = Number.constrain(-1 * this._windowWidth / 4 * e.wheelDeltaX / 120, -this._windowLeft, 1 - this._windowRight);
  444. this._overviewGrid.setWindow(this._windowLeft + shift, this._windowRight + shift);
  445. }
  446. },
  447. /**
  448. * @param {!number} x
  449. * @param {!number} y
  450. */
  451. _coordinatesToEntryIndex: function(x, y)
  452. {
  453. var timelineData = this._timelineData;
  454. if (!timelineData)
  455. return -1;
  456. var timelineEntries = timelineData.entries;
  457. var cursorTime = (x + this._pixelWindowLeft - this._paddingLeft) * this._pixelToTime;
  458. var cursorLevel = Math.floor((this._canvas.height / window.devicePixelRatio - y) / this._barHeight);
  459. for (var i = 0; i < timelineEntries.length; ++i) {
  460. if (cursorTime < timelineEntries[i].startTime)
  461. return -1;
  462. if (cursorTime < (timelineEntries[i].startTime + timelineEntries[i].duration)
  463. && cursorLevel === timelineEntries[i].depth)
  464. return i;
  465. }
  466. return -1;
  467. },
  468. onResize: function()
  469. {
  470. this._updateOverviewCanvas = true;
  471. this._scheduleUpdate();
  472. },
  473. _drawOverviewCanvas: function(width, height)
  474. {
  475. if (!this._timelineData)
  476. return;
  477. var timelineEntries = this._timelineData.entries;
  478. var drawData = new Uint8Array(width);
  479. var scaleFactor = width / this._totalTime;
  480. var maxStackDepth = 5; // minimum stack depth for the case when we see no activity.
  481. for (var entryIndex = 0; entryIndex < timelineEntries.length; ++entryIndex) {
  482. var entry = timelineEntries[entryIndex];
  483. var start = Math.floor(entry.startTime * scaleFactor);
  484. var finish = Math.floor((entry.startTime + entry.duration) * scaleFactor);
  485. for (var x = start; x < finish; ++x) {
  486. drawData[x] = Math.max(drawData[x], entry.depth + 1);
  487. maxStackDepth = Math.max(maxStackDepth, entry.depth + 1);
  488. }
  489. }
  490. var ratio = window.devicePixelRatio;
  491. var canvasWidth = width * ratio;
  492. var canvasHeight = height * ratio;
  493. this._overviewCanvas.width = canvasWidth;
  494. this._overviewCanvas.height = canvasHeight;
  495. this._overviewCanvas.style.width = width + "px";
  496. this._overviewCanvas.style.height = height + "px";
  497. var context = this._overviewCanvas.getContext("2d");
  498. var yScaleFactor = canvasHeight / (maxStackDepth * 1.1);
  499. context.lineWidth = 1;
  500. context.translate(0.5, 0.5);
  501. context.strokeStyle = "rgba(20,0,0,0.4)";
  502. context.fillStyle = "rgba(214,225,254,0.8)";
  503. context.moveTo(-1, canvasHeight - 1);
  504. if (drawData)
  505. context.lineTo(-1, Math.round(height - drawData[0] * yScaleFactor - 1));
  506. var value;
  507. for (var x = 0; x < width; ++x) {
  508. value = Math.round(canvasHeight - drawData[x] * yScaleFactor - 1);
  509. context.lineTo(x * ratio, value);
  510. }
  511. context.lineTo(canvasWidth + 1, value);
  512. context.lineTo(canvasWidth + 1, canvasHeight - 1);
  513. context.fill();
  514. context.stroke();
  515. context.closePath();
  516. },
  517. /**
  518. * @param {WebInspector.FlameChart.Entry} entry
  519. * @param {AnchorBox} anchorBox
  520. */
  521. _entryToAnchorBox: function(entry, anchorBox)
  522. {
  523. anchorBox.x = Math.floor(entry.startTime * this._timeToPixel) - this._pixelWindowLeft + this._paddingLeft;
  524. anchorBox.y = this._canvas.height / window.devicePixelRatio - (entry.depth + 1) * this._barHeight;
  525. anchorBox.width = Math.max(Math.ceil(entry.duration * this._timeToPixel), this._minWidth);
  526. anchorBox.height = this._barHeight;
  527. if (anchorBox.x < 0) {
  528. anchorBox.width += anchorBox.x;
  529. anchorBox.x = 0;
  530. }
  531. anchorBox.width = Number.constrain(anchorBox.width, 0, this._canvas.width - anchorBox.x);
  532. },
  533. /**
  534. * @param {!number} height
  535. * @param {!number} width
  536. */
  537. draw: function(width, height)
  538. {
  539. var timelineData = this._calculateTimelineData();
  540. if (!timelineData)
  541. return;
  542. var timelineEntries = timelineData.entries;
  543. var ratio = window.devicePixelRatio;
  544. var canvasWidth = width * ratio;
  545. var canvasHeight = height * ratio;
  546. this._canvas.width = canvasWidth;
  547. this._canvas.height = canvasHeight;
  548. this._canvas.style.width = width + "px";
  549. this._canvas.style.height = height + "px";
  550. var barHeight = this._barHeight;
  551. var context = this._canvas.getContext("2d");
  552. context.scale(ratio, ratio);
  553. var visibleTimeLeft = this._timeWindowLeft - this._paddingLeftTime;
  554. var timeWindowRight = this._timeWindowRight;
  555. function forEachEntry(flameChart, callback)
  556. {
  557. var anchorBox = new AnchorBox();
  558. for (var i = 0; i < timelineEntries.length; ++i) {
  559. var entry = timelineEntries[i];
  560. var startTime = entry.startTime;
  561. if (startTime > timeWindowRight)
  562. break;
  563. if ((startTime + entry.duration) < visibleTimeLeft)
  564. continue;
  565. flameChart._entryToAnchorBox(entry, anchorBox);
  566. callback(flameChart, context, entry, anchorBox, flameChart._highlightedEntryIndex === i);
  567. }
  568. }
  569. function drawBar(flameChart, context, entry, anchorBox, highlighted)
  570. {
  571. context.beginPath();
  572. context.rect(anchorBox.x, anchorBox.y, anchorBox.width - 1, anchorBox.height - 1);
  573. var colorPair = entry.colorPair;
  574. context.fillStyle = highlighted ? colorPair.highlighted : colorPair.normal;
  575. context.fill();
  576. }
  577. forEachEntry(this, drawBar);
  578. context.font = (barHeight - 4) + "px " + window.getComputedStyle(this.element, null).getPropertyValue("font-family");
  579. context.textBaseline = "alphabetic";
  580. context.fillStyle = "#333";
  581. this._dotsWidth = context.measureText("\u2026").width;
  582. var textPaddingLeft = 2;
  583. function drawText(flameChart, context, entry, anchorBox, highlighted)
  584. {
  585. var xText = Math.max(0, anchorBox.x);
  586. var widthText = anchorBox.width - textPaddingLeft + anchorBox.x - xText;
  587. var title = flameChart._prepareText(context, entry.node.functionName, widthText);
  588. if (title)
  589. context.fillText(title, xText + textPaddingLeft, anchorBox.y + barHeight - 4);
  590. }
  591. forEachEntry(this, drawText);
  592. var entryInfo = this._prepareHighlightedEntryInfo();
  593. if (entryInfo)
  594. this._printEntryInfo(context, entryInfo, 0, 25, width);
  595. },
  596. _printEntryInfo: function(context, entryInfo, x, y, width)
  597. {
  598. const lineHeight = 18;
  599. const paddingLeft = 10;
  600. const paddingTop = 5;
  601. var maxTitleWidth = 0;
  602. var basicFont = "100% " + window.getComputedStyle(this.element, null).getPropertyValue("font-family");
  603. context.font = "bold " + basicFont;
  604. context.textBaseline = "top";
  605. for (var i = 0; i < entryInfo.length; ++i)
  606. maxTitleWidth = Math.max(maxTitleWidth, context.measureText(entryInfo[i].title).width);
  607. var maxTextWidth = 0;
  608. for (var i = 0; i < entryInfo.length; ++i)
  609. maxTextWidth = Math.max(maxTextWidth, context.measureText(entryInfo[i].text).width);
  610. maxTextWidth = Math.min(maxTextWidth, width - 2 * paddingLeft - maxTitleWidth);
  611. context.beginPath();
  612. context.rect(x, y, maxTitleWidth + maxTextWidth + 5, lineHeight * entryInfo.length + 5);
  613. context.strokeStyle = "rgba(0,0,0,0)";
  614. context.fillStyle = "rgba(254,254,254,0.8)";
  615. context.fill();
  616. context.stroke();
  617. context.fillStyle = "#333";
  618. for (var i = 0; i < entryInfo.length; ++i)
  619. context.fillText(entryInfo[i].title, x + paddingLeft, y + lineHeight * i);
  620. context.font = basicFont;
  621. for (var i = 0; i < entryInfo.length; ++i) {
  622. var text = this._prepareText(context, entryInfo[i].text, maxTextWidth);
  623. context.fillText(text, x + paddingLeft + maxTitleWidth + paddingLeft, y + lineHeight * i);
  624. }
  625. },
  626. _prepareText: function(context, title, maxSize)
  627. {
  628. if (maxSize < this._dotsWidth)
  629. return null;
  630. var titleWidth = context.measureText(title).width;
  631. if (maxSize > titleWidth)
  632. return title;
  633. maxSize -= this._dotsWidth;
  634. var dotRegExp=/[\.\$]/g;
  635. var match = dotRegExp.exec(title);
  636. if (!match) {
  637. var visiblePartSize = maxSize / titleWidth;
  638. var newTextLength = Math.floor(title.length * visiblePartSize) + 1;
  639. var minTextLength = 4;
  640. if (newTextLength < minTextLength)
  641. return null;
  642. var substring;
  643. do {
  644. --newTextLength;
  645. substring = title.substring(0, newTextLength);
  646. } while (context.measureText(substring).width > maxSize);
  647. return title.substring(0, newTextLength) + "\u2026";
  648. }
  649. while (match) {
  650. var substring = title.substring(match.index + 1);
  651. var width = context.measureText(substring).width;
  652. if (maxSize > width)
  653. return "\u2026" + substring;
  654. match = dotRegExp.exec(title);
  655. }
  656. var i = 0;
  657. do {
  658. ++i;
  659. } while (context.measureText(title.substring(0, i)).width < maxSize);
  660. return title.substring(0, i - 1) + "\u2026";
  661. },
  662. _scheduleUpdate: function()
  663. {
  664. if (this._updateTimerId)
  665. return;
  666. this._updateTimerId = setTimeout(this.update.bind(this), 10);
  667. },
  668. _updateBoundaries: function()
  669. {
  670. this._windowLeft = this._overviewGrid.windowLeft();
  671. this._windowRight = this._overviewGrid.windowRight();
  672. this._windowWidth = this._windowRight - this._windowLeft;
  673. this._totalTime = this._timelineData.totalTime;
  674. this._timeWindowLeft = this._windowLeft * this._totalTime;
  675. this._timeWindowRight = this._windowRight * this._totalTime;
  676. this._pixelWindowWidth = this._chartContainer.clientWidth;
  677. this._totalPixels = Math.floor(this._pixelWindowWidth / this._windowWidth);
  678. this._pixelWindowLeft = Math.floor(this._totalPixels * this._windowLeft);
  679. this._pixelWindowRight = Math.floor(this._totalPixels * this._windowRight);
  680. this._timeToPixel = this._totalPixels / this._totalTime;
  681. this._pixelToTime = this._totalTime / this._totalPixels;
  682. this._paddingLeftTime = this._paddingLeft / this._timeToPixel;
  683. },
  684. update: function()
  685. {
  686. this._updateTimerId = 0;
  687. if (!this._timelineData)
  688. this._calculateTimelineData();
  689. if (!this._timelineData)
  690. return;
  691. this._updateBoundaries();
  692. this.draw(this._chartContainer.clientWidth, this._chartContainer.clientHeight);
  693. this._calculator._updateBoundaries(this);
  694. this._overviewCalculator._updateBoundaries(this);
  695. this._timelineGrid.element.style.width = this.element.clientWidth;
  696. this._timelineGrid.updateDividers(this._calculator);
  697. this._overviewGrid.updateDividers(this._overviewCalculator);
  698. if (this._updateOverviewCanvas) {
  699. this._drawOverviewCanvas(this._overviewContainer.clientWidth, this._overviewContainer.clientHeight - 20);
  700. this._updateOverviewCanvas = false;
  701. }
  702. },
  703. __proto__: WebInspector.View.prototype
  704. };