TimelineOverviewPane.js 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891
  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.TimelineModel} model
  34. */
  35. WebInspector.TimelineOverviewPane = function(model)
  36. {
  37. WebInspector.View.call(this);
  38. this.element.id = "timeline-overview-panel";
  39. this._windowStartTime = 0;
  40. this._windowEndTime = Infinity;
  41. this._eventDividers = [];
  42. this._model = model;
  43. this._topPaneSidebarElement = document.createElement("div");
  44. this._topPaneSidebarElement.id = "timeline-overview-sidebar";
  45. var overviewTreeElement = document.createElement("ol");
  46. overviewTreeElement.className = "sidebar-tree";
  47. this._topPaneSidebarElement.appendChild(overviewTreeElement);
  48. this.element.appendChild(this._topPaneSidebarElement);
  49. var topPaneSidebarTree = new TreeOutline(overviewTreeElement);
  50. this._overviewItems = {};
  51. this._overviewItems[WebInspector.TimelineOverviewPane.Mode.Events] = new WebInspector.SidebarTreeElement("timeline-overview-sidebar-events",
  52. WebInspector.UIString("Events"));
  53. if (Capabilities.timelineSupportsFrameInstrumentation) {
  54. this._overviewItems[WebInspector.TimelineOverviewPane.Mode.Frames] = new WebInspector.SidebarTreeElement("timeline-overview-sidebar-frames",
  55. WebInspector.UIString("Frames"));
  56. }
  57. this._overviewItems[WebInspector.TimelineOverviewPane.Mode.Memory] = new WebInspector.SidebarTreeElement("timeline-overview-sidebar-memory",
  58. WebInspector.UIString("Memory"));
  59. for (var mode in this._overviewItems) {
  60. var item = this._overviewItems[mode];
  61. item.onselect = this.setMode.bind(this, mode);
  62. topPaneSidebarTree.appendChild(item);
  63. }
  64. this._overviewGrid = new WebInspector.OverviewGrid("timeline");
  65. this.element.appendChild(this._overviewGrid.element);
  66. var separatorElement = document.createElement("div");
  67. separatorElement.id = "timeline-overview-separator";
  68. this.element.appendChild(separatorElement);
  69. this._innerSetMode(WebInspector.TimelineOverviewPane.Mode.Events);
  70. var categories = WebInspector.TimelinePresentationModel.categories();
  71. for (var category in categories)
  72. categories[category].addEventListener(WebInspector.TimelineCategory.Events.VisibilityChanged, this._onCategoryVisibilityChanged, this);
  73. this._overviewCalculator = new WebInspector.TimelineOverviewCalculator();
  74. model.addEventListener(WebInspector.TimelineModel.Events.RecordAdded, this._onRecordAdded, this);
  75. model.addEventListener(WebInspector.TimelineModel.Events.RecordsCleared, this._reset, this);
  76. this._overviewGrid.addEventListener(WebInspector.OverviewGrid.Events.WindowChanged, this._onWindowChanged, this);
  77. }
  78. WebInspector.TimelineOverviewPane.Mode = {
  79. Events: "Events",
  80. Frames: "Frames",
  81. Memory: "Memory"
  82. };
  83. WebInspector.TimelineOverviewPane.Events = {
  84. ModeChanged: "ModeChanged",
  85. WindowChanged: "WindowChanged"
  86. };
  87. WebInspector.TimelineOverviewPane.prototype = {
  88. wasShown: function()
  89. {
  90. this._update();
  91. },
  92. onResize: function()
  93. {
  94. this._update();
  95. },
  96. setMode: function(newMode)
  97. {
  98. if (this._currentMode === newMode)
  99. return;
  100. this._innerSetMode(newMode);
  101. this.dispatchEventToListeners(WebInspector.TimelineOverviewPane.Events.ModeChanged, this._currentMode);
  102. this._update();
  103. },
  104. _innerSetMode: function(newMode)
  105. {
  106. if (this._overviewControl)
  107. this._overviewControl.detach();
  108. this._currentMode = newMode;
  109. this._overviewControl = this._createOverviewControl();
  110. this._overviewControl.show(this._overviewGrid.element);
  111. this._overviewItems[this._currentMode].revealAndSelect(false);
  112. },
  113. /**
  114. * @return {WebInspector.TimelineOverviewBase|null}
  115. */
  116. _createOverviewControl: function()
  117. {
  118. switch (this._currentMode) {
  119. case WebInspector.TimelineOverviewPane.Mode.Events:
  120. return new WebInspector.TimelineEventOverview(this._model);
  121. case WebInspector.TimelineOverviewPane.Mode.Frames:
  122. return new WebInspector.TimelineFrameOverview(this._model);
  123. case WebInspector.TimelineOverviewPane.Mode.Memory:
  124. return new WebInspector.TimelineMemoryOverview(this._model);
  125. }
  126. throw new Error("Invalid overview mode: " + this._currentMode);
  127. },
  128. _onCategoryVisibilityChanged: function(event)
  129. {
  130. this._overviewControl.categoryVisibilityChanged();
  131. },
  132. _update: function()
  133. {
  134. delete this._refreshTimeout;
  135. this._updateWindow();
  136. this._overviewCalculator.setWindow(this._model.minimumRecordTime(), this._model.maximumRecordTime());
  137. this._overviewCalculator.setDisplayWindow(0, this._overviewGrid.clientWidth());
  138. this._overviewControl.update();
  139. this._overviewGrid.updateDividers(this._overviewCalculator);
  140. this._updateEventDividers();
  141. },
  142. _updateEventDividers: function()
  143. {
  144. var records = this._eventDividers;
  145. this._overviewGrid.removeEventDividers();
  146. var dividers = [];
  147. for (var i = 0; i < records.length; ++i) {
  148. var record = records[i];
  149. var positions = this._overviewCalculator.computeBarGraphPercentages(record);
  150. var dividerPosition = Math.round(positions.start * 10);
  151. if (dividers[dividerPosition])
  152. continue;
  153. var divider = WebInspector.TimelinePresentationModel.createEventDivider(record.type);
  154. divider.style.left = positions.start + "%";
  155. dividers[dividerPosition] = divider;
  156. }
  157. this._overviewGrid.addEventDividers(dividers);
  158. },
  159. /**
  160. * @param {number} width
  161. */
  162. sidebarResized: function(width)
  163. {
  164. this._overviewGrid.element.style.left = width + "px";
  165. this._topPaneSidebarElement.style.width = width + "px";
  166. this._update();
  167. },
  168. /**
  169. * @param {WebInspector.TimelineFrame} frame
  170. */
  171. addFrame: function(frame)
  172. {
  173. this._overviewControl.addFrame(frame);
  174. this._scheduleRefresh();
  175. },
  176. /**
  177. * @param {WebInspector.TimelineFrame} frame
  178. */
  179. zoomToFrame: function(frame)
  180. {
  181. var frameOverview = /** @type WebInspector.TimelineFrameOverview */ (this._overviewControl);
  182. var window = frameOverview.framePosition(frame);
  183. if (!window)
  184. return;
  185. this._overviewGrid.setWindowPosition(window.start, window.end);
  186. },
  187. _onRecordAdded: function(event)
  188. {
  189. var record = event.data;
  190. var eventDividers = this._eventDividers;
  191. function addEventDividers(record)
  192. {
  193. if (WebInspector.TimelinePresentationModel.isEventDivider(record))
  194. eventDividers.push(record);
  195. }
  196. WebInspector.TimelinePresentationModel.forAllRecords([record], addEventDividers);
  197. this._scheduleRefresh();
  198. },
  199. _reset: function()
  200. {
  201. this._windowStartTime = 0;
  202. this._windowEndTime = Infinity;
  203. this._overviewCalculator.reset();
  204. this._overviewGrid.reset();
  205. this._eventDividers = [];
  206. this._overviewGrid.updateDividers(this._overviewCalculator);
  207. this._overviewControl.reset();
  208. this._update();
  209. },
  210. windowStartTime: function()
  211. {
  212. return this._windowStartTime || this._model.minimumRecordTime();
  213. },
  214. windowEndTime: function()
  215. {
  216. return this._windowEndTime < Infinity ? this._windowEndTime : this._model.maximumRecordTime();
  217. },
  218. windowLeft: function()
  219. {
  220. return this._overviewGrid.windowLeft();
  221. },
  222. windowRight: function()
  223. {
  224. return this._overviewGrid.windowRight();
  225. },
  226. _onWindowChanged: function()
  227. {
  228. if (this._ignoreWindowChangedEvent)
  229. return;
  230. var times = this._overviewControl.windowTimes(this.windowLeft(), this.windowRight());
  231. this._windowStartTime = times.startTime;
  232. this._windowEndTime = times.endTime;
  233. this.dispatchEventToListeners(WebInspector.TimelineOverviewPane.Events.WindowChanged);
  234. },
  235. /**
  236. * @param {Number} left
  237. * @param {Number} right
  238. */
  239. setWindowTimes: function(left, right)
  240. {
  241. this._windowStartTime = left;
  242. this._windowEndTime = right;
  243. this._updateWindow();
  244. },
  245. _updateWindow: function()
  246. {
  247. var offset = this._model.minimumRecordTime();
  248. var timeSpan = this._model.maximumRecordTime() - offset;
  249. var left = this._windowStartTime ? (this._windowStartTime - offset) / timeSpan : 0;
  250. var right = this._windowEndTime < Infinity ? (this._windowEndTime - offset) / timeSpan : 1;
  251. this._ignoreWindowChangedEvent = true;
  252. this._overviewGrid.setWindow(left, right);
  253. this._ignoreWindowChangedEvent = false;
  254. },
  255. _scheduleRefresh: function()
  256. {
  257. if (this._refreshTimeout)
  258. return;
  259. if (!this.isShowing())
  260. return;
  261. this._refreshTimeout = setTimeout(this._update.bind(this), 300);
  262. },
  263. __proto__: WebInspector.View.prototype
  264. }
  265. /**
  266. * @constructor
  267. * @implements {WebInspector.TimelineGrid.Calculator}
  268. */
  269. WebInspector.TimelineOverviewCalculator = function()
  270. {
  271. }
  272. WebInspector.TimelineOverviewCalculator.prototype = {
  273. /**
  274. * @param {number} time
  275. */
  276. computePosition: function(time)
  277. {
  278. return (time - this._minimumBoundary) / this.boundarySpan() * this._workingArea + this.paddingLeft;
  279. },
  280. computeBarGraphPercentages: function(record)
  281. {
  282. var start = (WebInspector.TimelineModel.startTimeInSeconds(record) - this._minimumBoundary) / this.boundarySpan() * 100;
  283. var end = (WebInspector.TimelineModel.endTimeInSeconds(record) - this._minimumBoundary) / this.boundarySpan() * 100;
  284. return {start: start, end: end};
  285. },
  286. /**
  287. * @param {number=} minimum
  288. * @param {number=} maximum
  289. */
  290. setWindow: function(minimum, maximum)
  291. {
  292. this._minimumBoundary = minimum >= 0 ? minimum : undefined;
  293. this._maximumBoundary = maximum >= 0 ? maximum : undefined;
  294. },
  295. /**
  296. * @param {number} paddingLeft
  297. * @param {number} clientWidth
  298. */
  299. setDisplayWindow: function(paddingLeft, clientWidth)
  300. {
  301. this._workingArea = clientWidth - paddingLeft;
  302. this.paddingLeft = paddingLeft;
  303. },
  304. reset: function()
  305. {
  306. this.setWindow();
  307. },
  308. formatTime: function(value)
  309. {
  310. return Number.secondsToString(value);
  311. },
  312. maximumBoundary: function()
  313. {
  314. return this._maximumBoundary;
  315. },
  316. minimumBoundary: function()
  317. {
  318. return this._minimumBoundary;
  319. },
  320. zeroTime: function()
  321. {
  322. return this._minimumBoundary;
  323. },
  324. boundarySpan: function()
  325. {
  326. return this._maximumBoundary - this._minimumBoundary;
  327. }
  328. }
  329. /**
  330. * @constructor
  331. * @extends {WebInspector.View}
  332. * @param {WebInspector.TimelineModel} model
  333. */
  334. WebInspector.TimelineOverviewBase = function(model)
  335. {
  336. WebInspector.View.call(this);
  337. this._model = model;
  338. this._canvas = this.element.createChild("canvas", "fill");
  339. }
  340. WebInspector.TimelineOverviewBase.prototype = {
  341. update: function() { },
  342. reset: function() { },
  343. categoryVisibilityChanged: function() { },
  344. /**
  345. * @param {WebInspector.TimelineFrame} frame
  346. */
  347. addFrame: function(frame) { },
  348. /**
  349. * @param {number} windowLeft
  350. * @param {number} windowRight
  351. */
  352. windowTimes: function(windowLeft, windowRight)
  353. {
  354. var absoluteMin = this._model.minimumRecordTime();
  355. var absoluteMax = this._model.maximumRecordTime();
  356. return {
  357. startTime: absoluteMin + (absoluteMax - absoluteMin) * windowLeft,
  358. endTime: absoluteMin + (absoluteMax - absoluteMin) * windowRight
  359. };
  360. },
  361. __proto__: WebInspector.View.prototype
  362. }
  363. /**
  364. * @constructor
  365. * @extends {WebInspector.TimelineOverviewBase}
  366. * @param {WebInspector.TimelineModel} model
  367. */
  368. WebInspector.TimelineMemoryOverview = function(model)
  369. {
  370. WebInspector.TimelineOverviewBase.call(this, model);
  371. this.element.id = "timeline-overview-memory";
  372. this.element.classList.add("fill");
  373. this._maxHeapSizeLabel = this.element.createChild("div", "max memory-graph-label");
  374. this._minHeapSizeLabel = this.element.createChild("div", "min memory-graph-label");
  375. }
  376. WebInspector.TimelineMemoryOverview.prototype = {
  377. reset: function()
  378. {
  379. this._maxHeapSizeLabel.textContent = "";
  380. this._minHeapSizeLabel.textContent = "";
  381. },
  382. update: function()
  383. {
  384. const yPadding = 5;
  385. this._canvas.width = this.element.clientWidth;
  386. this._canvas.height = this.element.clientHeight - yPadding;
  387. var records = this._model.records;
  388. if (!records.length)
  389. return;
  390. const lowerOffset = 3;
  391. var maxUsedHeapSize = 0;
  392. var minUsedHeapSize = 100000000000;
  393. var minTime = this._model.minimumRecordTime();
  394. var maxTime = this._model.maximumRecordTime();;
  395. WebInspector.TimelinePresentationModel.forAllRecords(records, function(r) {
  396. maxUsedHeapSize = Math.max(maxUsedHeapSize, r.usedHeapSize || maxUsedHeapSize);
  397. minUsedHeapSize = Math.min(minUsedHeapSize, r.usedHeapSize || minUsedHeapSize);
  398. });
  399. minUsedHeapSize = Math.min(minUsedHeapSize, maxUsedHeapSize);
  400. var width = this._canvas.width;
  401. var height = this._canvas.height - lowerOffset;
  402. var xFactor = width / (maxTime - minTime);
  403. var yFactor = height / (maxUsedHeapSize - minUsedHeapSize);
  404. var histogram = new Array(width);
  405. WebInspector.TimelinePresentationModel.forAllRecords(records, function(r) {
  406. if (!r.usedHeapSize)
  407. return;
  408. var x = Math.round((WebInspector.TimelineModel.endTimeInSeconds(r) - minTime) * xFactor);
  409. var y = Math.round((r.usedHeapSize - minUsedHeapSize) * yFactor);
  410. histogram[x] = Math.max(histogram[x] || 0, y);
  411. });
  412. var ctx = this._canvas.getContext("2d");
  413. this._clear(ctx);
  414. // +1 so that the border always fit into the canvas area.
  415. height = height + 1;
  416. ctx.beginPath();
  417. var initialY = 0;
  418. for (var k = 0; k < histogram.length; k++) {
  419. if (histogram[k]) {
  420. initialY = histogram[k];
  421. break;
  422. }
  423. }
  424. ctx.moveTo(0, height - initialY);
  425. for (var x = 0; x < histogram.length; x++) {
  426. if (!histogram[x])
  427. continue;
  428. ctx.lineTo(x, height - histogram[x]);
  429. }
  430. ctx.lineWidth = 0.5;
  431. ctx.strokeStyle = "rgba(20,0,0,0.8)";
  432. ctx.stroke();
  433. ctx.fillStyle = "rgba(214,225,254, 0.8);";
  434. ctx.lineTo(width, 60);
  435. ctx.lineTo(0, 60);
  436. ctx.lineTo(0, height - initialY);
  437. ctx.fill();
  438. ctx.closePath();
  439. this._maxHeapSizeLabel.textContent = Number.bytesToString(maxUsedHeapSize);
  440. this._minHeapSizeLabel.textContent = Number.bytesToString(minUsedHeapSize);
  441. },
  442. _clear: function(ctx)
  443. {
  444. ctx.fillStyle = "rgba(255,255,255,0.8)";
  445. ctx.fillRect(0, 0, this._canvas.width, this._canvas.height);
  446. },
  447. __proto__: WebInspector.TimelineOverviewBase.prototype
  448. }
  449. /**
  450. * @constructor
  451. * @extends {WebInspector.TimelineOverviewBase}
  452. * @param {WebInspector.TimelineModel} model
  453. */
  454. WebInspector.TimelineEventOverview = function(model)
  455. {
  456. WebInspector.TimelineOverviewBase.call(this, model);
  457. this.element.id = "timeline-overview-events";
  458. this._context = this._canvas.getContext("2d");
  459. this._fillStyles = {};
  460. var categories = WebInspector.TimelinePresentationModel.categories();
  461. for (var category in categories)
  462. this._fillStyles[category] = WebInspector.TimelinePresentationModel.createFillStyleForCategory(this._context, 0, WebInspector.TimelineEventOverview._innerStripHeight, categories[category]);
  463. this._disabledCategoryFillStyle = WebInspector.TimelinePresentationModel.createFillStyle(this._context, 0, WebInspector.TimelineEventOverview._innerStripHeight,
  464. "rgb(218, 218, 218)", "rgb(170, 170, 170)", "rgb(143, 143, 143)");
  465. this._disabledCategoryBorderStyle = "rgb(143, 143, 143)";
  466. }
  467. /** @const */
  468. WebInspector.TimelineEventOverview._canvasHeight = 60;
  469. /** @const */
  470. WebInspector.TimelineEventOverview._numberOfStrips = 3;
  471. /** @const */
  472. WebInspector.TimelineEventOverview._stripHeight = Math.round(WebInspector.TimelineEventOverview._canvasHeight / WebInspector.TimelineEventOverview._numberOfStrips);
  473. /** @const */
  474. WebInspector.TimelineEventOverview._stripPadding = 4;
  475. /** @const */
  476. WebInspector.TimelineEventOverview._innerStripHeight = WebInspector.TimelineEventOverview._stripHeight - 2 * WebInspector.TimelineEventOverview._stripPadding;
  477. WebInspector.TimelineEventOverview.prototype = {
  478. update: function()
  479. {
  480. // Use real world, 1:1 coordinates in canvas. This will also take care of clearing it.
  481. this._canvas.width = this.element.parentElement.clientWidth;
  482. this._canvas.height = WebInspector.TimelineEventOverview._canvasHeight;
  483. var timeOffset = this._model.minimumRecordTime();
  484. var timeSpan = this._model.maximumRecordTime() - timeOffset;
  485. var scale = this._canvas.width / timeSpan;
  486. var lastBarByGroup = [];
  487. this._context.fillStyle = "rgba(0, 0, 0, 0.05)";
  488. for (var i = 1; i < WebInspector.TimelineEventOverview._numberOfStrips; i += 2)
  489. this._context.fillRect(0.5, i * WebInspector.TimelineEventOverview._stripHeight + 0.5, this._canvas.width, WebInspector.TimelineEventOverview._stripHeight);
  490. function appendRecord(record)
  491. {
  492. if (record.type === WebInspector.TimelineModel.RecordType.BeginFrame)
  493. return;
  494. var recordStart = Math.floor((WebInspector.TimelineModel.startTimeInSeconds(record) - timeOffset) * scale);
  495. var recordEnd = Math.ceil((WebInspector.TimelineModel.endTimeInSeconds(record) - timeOffset) * scale);
  496. var category = WebInspector.TimelinePresentationModel.categoryForRecord(record);
  497. if (category.overviewStripGroupIndex < 0)
  498. return;
  499. var bar = lastBarByGroup[category.overviewStripGroupIndex];
  500. // This bar may be merged with previous -- so just adjust the previous bar.
  501. const barsMergeThreshold = 2;
  502. if (bar && bar.category === category && bar.end + barsMergeThreshold >= recordStart) {
  503. if (recordEnd > bar.end)
  504. bar.end = recordEnd;
  505. return;
  506. }
  507. if (bar)
  508. this._renderBar(bar.start, bar.end, bar.category);
  509. lastBarByGroup[category.overviewStripGroupIndex] = { start: recordStart, end: recordEnd, category: category };
  510. }
  511. WebInspector.TimelinePresentationModel.forAllRecords(this._model.records, appendRecord.bind(this));
  512. for (var i = 0; i < lastBarByGroup.length; ++i) {
  513. if (lastBarByGroup[i])
  514. this._renderBar(lastBarByGroup[i].start, lastBarByGroup[i].end, lastBarByGroup[i].category);
  515. }
  516. },
  517. categoryVisibilityChanged: function()
  518. {
  519. this.update();
  520. },
  521. _renderBar: function(begin, end, category)
  522. {
  523. var x = begin + 0.5;
  524. var y = category.overviewStripGroupIndex * WebInspector.TimelineEventOverview._stripHeight + WebInspector.TimelineEventOverview._stripPadding + 0.5;
  525. var width = Math.max(end - begin, 1);
  526. this._context.save();
  527. this._context.translate(x, y);
  528. this._context.fillStyle = category.hidden ? this._disabledCategoryFillStyle : this._fillStyles[category.name];
  529. this._context.fillRect(0, 0, width, WebInspector.TimelineEventOverview._innerStripHeight);
  530. this._context.strokeStyle = category.hidden ? this._disabledCategoryBorderStyle : category.borderColor;
  531. this._context.strokeRect(0, 0, width, WebInspector.TimelineEventOverview._innerStripHeight);
  532. this._context.restore();
  533. },
  534. __proto__: WebInspector.TimelineOverviewBase.prototype
  535. }
  536. /**
  537. * @constructor
  538. * @extends {WebInspector.TimelineOverviewBase}
  539. * @param {WebInspector.TimelineModel} model
  540. */
  541. WebInspector.TimelineFrameOverview = function(model)
  542. {
  543. WebInspector.TimelineOverviewBase.call(this, model);
  544. this._canvas.classList.add("timeline-frame-overview-bars");
  545. this.reset();
  546. this._outerPadding = 4;
  547. this._maxInnerBarWidth = 10;
  548. // The below two are really computed by update() -- but let's have something so that windowTimes() is happy.
  549. this._actualPadding = 5;
  550. this._actualOuterBarWidth = this._maxInnerBarWidth + this._actualPadding;
  551. this._context = this._canvas.getContext("2d");
  552. this._fillStyles = {};
  553. var categories = WebInspector.TimelinePresentationModel.categories();
  554. for (var category in categories)
  555. this._fillStyles[category] = WebInspector.TimelinePresentationModel.createFillStyleForCategory(this._context, this._maxInnerBarWidth, 0, categories[category]);
  556. }
  557. WebInspector.TimelineFrameOverview.prototype = {
  558. reset: function()
  559. {
  560. this._recordsPerBar = 1;
  561. this._barTimes = [];
  562. this._frames = [];
  563. },
  564. update: function()
  565. {
  566. const minBarWidth = 4;
  567. this._framesPerBar = Math.max(1, this._frames.length * minBarWidth / this.element.clientWidth);
  568. this._barTimes = [];
  569. var visibleFrames = this._aggregateFrames(this._framesPerBar);
  570. const paddingTop = 4;
  571. // Optimize appearance for 30fps. However, if at least half frames won't fit at this scale,
  572. // fall back to using autoscale.
  573. const targetFPS = 30;
  574. var fullBarLength = 1.0 / targetFPS;
  575. if (fullBarLength < this._medianFrameLength)
  576. fullBarLength = Math.min(this._medianFrameLength * 2, this._maxFrameLength);
  577. var scale = (this._canvas.clientHeight - paddingTop) / fullBarLength;
  578. this._renderBars(visibleFrames, scale);
  579. },
  580. /**
  581. * @param {WebInspector.TimelineFrame} frame
  582. */
  583. addFrame: function(frame)
  584. {
  585. this._frames.push(frame);
  586. },
  587. framePosition: function(frame)
  588. {
  589. var frameNumber = this._frames.indexOf(frame);
  590. if (frameNumber < 0)
  591. return;
  592. var barNumber = Math.floor(frameNumber / this._framesPerBar);
  593. var firstBar = this._framesPerBar > 1 ? barNumber : Math.max(barNumber - 1, 0);
  594. var lastBar = this._framesPerBar > 1 ? barNumber : Math.min(barNumber + 1, this._barTimes.length - 1);
  595. return {
  596. start: Math.ceil(this._barNumberToScreenPosition(firstBar) - this._actualPadding / 2),
  597. end: Math.floor(this._barNumberToScreenPosition(lastBar + 1) - this._actualPadding / 2)
  598. }
  599. },
  600. /**
  601. * @param {number} framesPerBar
  602. */
  603. _aggregateFrames: function(framesPerBar)
  604. {
  605. var visibleFrames = [];
  606. var durations = [];
  607. this._maxFrameLength = 0;
  608. for (var barNumber = 0, currentFrame = 0; currentFrame < this._frames.length; ++barNumber) {
  609. var barStartTime = this._frames[currentFrame].startTime;
  610. var longestFrame = null;
  611. for (var lastFrame = Math.min(Math.floor((barNumber + 1) * framesPerBar), this._frames.length);
  612. currentFrame < lastFrame; ++currentFrame) {
  613. if (!longestFrame || longestFrame.duration < this._frames[currentFrame].duration)
  614. longestFrame = this._frames[currentFrame];
  615. }
  616. var barEndTime = this._frames[currentFrame - 1].endTime;
  617. if (longestFrame) {
  618. this._maxFrameLength = Math.max(this._maxFrameLength, longestFrame.duration);
  619. visibleFrames.push(longestFrame);
  620. this._barTimes.push({ startTime: barStartTime, endTime: barEndTime });
  621. durations.push(longestFrame.duration);
  622. }
  623. }
  624. this._medianFrameLength = durations.qselect(Math.floor(durations.length / 2));
  625. return visibleFrames;
  626. },
  627. /**
  628. * @param {Array.<WebInspector.TimelineFrame>} frames
  629. * @param {number} scale
  630. */
  631. _renderBars: function(frames, scale)
  632. {
  633. // Use real world, 1:1 coordinates in canvas. This will also take care of clearing it.
  634. this._canvas.width = this._canvas.clientWidth;
  635. this._canvas.height = this._canvas.clientHeight;
  636. const maxPadding = 5;
  637. this._actualOuterBarWidth = Math.min((this._canvas.width - 2 * this._outerPadding) / frames.length, this._maxInnerBarWidth + maxPadding);
  638. this._actualPadding = Math.min(Math.floor(this._actualOuterBarWidth / 3), maxPadding);
  639. var barWidth = this._actualOuterBarWidth - this._actualPadding;
  640. for (var i = 0; i < frames.length; ++i)
  641. this._renderBar(this._barNumberToScreenPosition(i), barWidth, frames[i], scale);
  642. this._drawFPSMarks(scale);
  643. },
  644. /**
  645. * @param {number} n
  646. */
  647. _barNumberToScreenPosition: function(n)
  648. {
  649. return this._outerPadding + this._actualOuterBarWidth * n;
  650. },
  651. /**
  652. * @param {number} scale
  653. */
  654. _drawFPSMarks: function(scale)
  655. {
  656. const fpsMarks = [30, 60];
  657. this._context.save();
  658. this._context.beginPath();
  659. this._context.font = "9px monospace";
  660. this._context.textAlign = "right";
  661. this._context.textBaseline = "top";
  662. const labelPadding = 2;
  663. var lineHeight = 12;
  664. var labelTopMargin = 0;
  665. for (var i = 0; i < fpsMarks.length; ++i) {
  666. var fps = fpsMarks[i];
  667. // Draw lines one pixel above they need to be, so 60pfs line does not cross most of the frames tops.
  668. var y = this._canvas.height - Math.floor(1.0 / fps * scale) - 0.5;
  669. var label = fps + " FPS ";
  670. var labelWidth = this._context.measureText(label).width;
  671. var labelX = this._canvas.width;
  672. var labelY;
  673. if (labelTopMargin < y - lineHeight)
  674. labelY = y - lineHeight;
  675. else if (y + lineHeight < this._canvas.height)
  676. labelY = y;
  677. else
  678. break; // No space for the label, so no line as well.
  679. this._context.moveTo(0, y);
  680. this._context.lineTo(this._canvas.width, y);
  681. this._context.fillStyle = "rgba(255, 255, 255, 0.75)";
  682. this._context.fillRect(labelX - labelWidth - labelPadding, labelY, labelWidth + 2 * labelPadding, lineHeight);
  683. this._context.fillStyle = "rgb(0, 0, 0)";
  684. this._context.fillText(label, labelX, labelY);
  685. labelTopMargin = labelY + lineHeight;
  686. }
  687. this._context.strokeStyle = "rgb(51, 51, 51)";
  688. this._context.stroke();
  689. this._context.restore();
  690. },
  691. _renderBar: function(left, width, frame, scale)
  692. {
  693. var categories = Object.keys(WebInspector.TimelinePresentationModel.categories());
  694. if (!categories.length)
  695. return;
  696. var x = Math.floor(left) + 0.5;
  697. width = Math.floor(width);
  698. for (var i = 0, bottomOffset = this._canvas.height; i < categories.length; ++i) {
  699. var category = categories[i];
  700. var duration = frame.timeByCategory[category];
  701. if (!duration)
  702. continue;
  703. var height = duration * scale;
  704. var y = Math.floor(bottomOffset - height) + 0.5;
  705. this._context.save();
  706. this._context.translate(x, 0);
  707. this._context.scale(width / this._maxInnerBarWidth, 1);
  708. this._context.fillStyle = this._fillStyles[category];
  709. this._context.fillRect(0, y, this._maxInnerBarWidth, Math.floor(height));
  710. this._context.restore();
  711. this._context.strokeStyle = WebInspector.TimelinePresentationModel.categories()[category].borderColor;
  712. this._context.strokeRect(x, y, width, Math.floor(height));
  713. bottomOffset -= height - 1;
  714. }
  715. // Draw a contour for the rest of frame time that we did not instrument.
  716. var nonCPUTime = frame.duration - frame.cpuTime;
  717. var y0 = Math.floor(bottomOffset - nonCPUTime * scale) + 0.5;
  718. var y1 = Math.floor(bottomOffset) + 0.5;
  719. this._context.strokeStyle = "rgb(90, 90, 90)";
  720. this._context.beginPath();
  721. this._context.moveTo(x, y1);
  722. this._context.lineTo(x, y0);
  723. this._context.lineTo(x + width, y0);
  724. this._context.lineTo(x + width, y1);
  725. this._context.stroke();
  726. },
  727. windowTimes: function(windowLeft, windowRight)
  728. {
  729. var windowSpan = this.element.clientWidth;
  730. var leftOffset = windowLeft * windowSpan - this._outerPadding + this._actualPadding;
  731. var rightOffset = windowRight * windowSpan - this._outerPadding;
  732. var bars = this.element.children;
  733. var firstBar = Math.floor(Math.max(leftOffset, 0) / this._actualOuterBarWidth);
  734. var lastBar = Math.min(Math.floor(rightOffset / this._actualOuterBarWidth), this._barTimes.length - 1);
  735. const snapToRightTolerancePixels = 3;
  736. return {
  737. startTime: firstBar >= this._barTimes.length ? Infinity : this._barTimes[firstBar].startTime,
  738. endTime: rightOffset + snapToRightTolerancePixels > windowSpan ? Infinity : this._barTimes[lastBar].endTime
  739. }
  740. },
  741. __proto__: WebInspector.TimelineOverviewBase.prototype
  742. }
  743. /**
  744. * @param {WebInspector.TimelineOverviewPane} pane
  745. * @constructor
  746. * @implements {WebInspector.TimelinePresentationModel.Filter}
  747. */
  748. WebInspector.TimelineWindowFilter = function(pane)
  749. {
  750. this._pane = pane;
  751. }
  752. WebInspector.TimelineWindowFilter.prototype = {
  753. /**
  754. * @param {!WebInspector.TimelinePresentationModel.Record} record
  755. * @return {boolean}
  756. */
  757. accept: function(record)
  758. {
  759. return record.lastChildEndTime >= this._pane._windowStartTime && record.startTime <= this._pane._windowEndTime;
  760. }
  761. }