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/** 32 * @constructor 33 * @extends {WebInspector.TimelineOverviewBase} 34 * @param {!WebInspector.TimelineModel} model 35 */ 36WebInspector.TimelineFrameOverview = function(model) 37{ 38 WebInspector.TimelineOverviewBase.call(this, model); 39 this.element.id = "timeline-overview-frames"; 40 this.reset(); 41 42 this._outerPadding = 4 * window.devicePixelRatio; 43 this._maxInnerBarWidth = 10 * window.devicePixelRatio; 44 this._topPadding = 6 * window.devicePixelRatio; 45 46 // The below two are really computed by update() -- but let's have something so that windowTimes() is happy. 47 this._actualPadding = 5 * window.devicePixelRatio; 48 this._actualOuterBarWidth = this._maxInnerBarWidth + this._actualPadding; 49 50 this._fillStyles = {}; 51 var categories = WebInspector.TimelinePresentationModel.categories(); 52 for (var category in categories) 53 this._fillStyles[category] = WebInspector.TimelinePresentationModel.createFillStyleForCategory(this._context, this._maxInnerBarWidth, 0, categories[category]); 54 this._frameTopShadeGradient = this._context.createLinearGradient(0, 0, 0, this._topPadding); 55 this._frameTopShadeGradient.addColorStop(0, "rgba(255, 255, 255, 0.9)"); 56 this._frameTopShadeGradient.addColorStop(1, "rgba(255, 255, 255, 0.2)"); 57} 58 59WebInspector.TimelineFrameOverview.prototype = { 60 reset: function() 61 { 62 this._recordsPerBar = 1; 63 /** @type {!Array.<{startTime:number, endTime:number}>} */ 64 this._barTimes = []; 65 this._mainThreadFrames = []; 66 this._backgroundFrames = []; 67 this._framesById = {}; 68 }, 69 70 update: function() 71 { 72 this.resetCanvas(); 73 this._barTimes = []; 74 75 var backgroundFramesHeight = 15; 76 var mainThreadFramesHeight = this._canvas.height - backgroundFramesHeight; 77 const minBarWidth = 4 * window.devicePixelRatio; 78 var frameCount = this._backgroundFrames.length || this._mainThreadFrames.length; 79 var framesPerBar = Math.max(1, frameCount * minBarWidth / this._canvas.width); 80 81 var mainThreadVisibleFrames; 82 var backgroundVisibleFrames; 83 if (this._backgroundFrames.length) { 84 backgroundVisibleFrames = this._aggregateFrames(this._backgroundFrames, framesPerBar); 85 mainThreadVisibleFrames = new Array(backgroundVisibleFrames.length); 86 for (var i = 0; i < backgroundVisibleFrames.length; ++i) { 87 var frameId = backgroundVisibleFrames[i].mainThreadFrameId; 88 mainThreadVisibleFrames[i] = frameId && this._framesById[frameId]; 89 } 90 } else { 91 mainThreadVisibleFrames = this._aggregateFrames(this._mainThreadFrames, framesPerBar); 92 } 93 94 this._context.save(); 95 this._setCanvasWindow(0, backgroundFramesHeight, this._canvas.width, mainThreadFramesHeight); 96 var scale = (mainThreadFramesHeight - this._topPadding) / this._computeTargetFrameLength(mainThreadVisibleFrames); 97 this._renderBars(mainThreadVisibleFrames, scale, mainThreadFramesHeight); 98 this._context.fillStyle = this._frameTopShadeGradient; 99 this._context.fillRect(0, 0, this._canvas.width, this._topPadding); 100 this._drawFPSMarks(scale, mainThreadFramesHeight); 101 this._context.restore(); 102 103 var bottom = backgroundFramesHeight + 0.5; 104 this._context.strokeStyle = "rgba(120, 120, 120, 0.8)"; 105 this._context.beginPath(); 106 this._context.moveTo(0, bottom); 107 this._context.lineTo(this._canvas.width, bottom); 108 this._context.stroke(); 109 110 if (backgroundVisibleFrames) { 111 const targetFPS = 30.0; 112 scale = (backgroundFramesHeight - this._topPadding) / (1.0 / targetFPS); 113 this._renderBars(backgroundVisibleFrames, scale, backgroundFramesHeight); 114 } 115 }, 116 117 /** 118 * @param {!WebInspector.TimelineFrame} frame 119 */ 120 addFrame: function(frame) 121 { 122 var frames; 123 if (frame.isBackground) { 124 frames = this._backgroundFrames; 125 } else { 126 frames = this._mainThreadFrames; 127 this._framesById[frame.id] = frame; 128 } 129 frames.push(frame); 130 }, 131 132 /** 133 * @param {number} x0 134 * @param {number} y0 135 * @param {number} width 136 * @param {number} height 137 */ 138 _setCanvasWindow: function(x0, y0, width, height) 139 { 140 this._context.translate(x0, y0); 141 this._context.beginPath(); 142 this._context.moveTo(0, 0); 143 this._context.lineTo(width, 0); 144 this._context.lineTo(width, height); 145 this._context.lineTo(0, height); 146 this._context.lineTo(0, 0); 147 this._context.clip(); 148 }, 149 150 /** 151 * @param {!Array.<!WebInspector.TimelineFrame>} frames 152 * @param {number} framesPerBar 153 * @return {!Array.<!WebInspector.TimelineFrame>} 154 */ 155 _aggregateFrames: function(frames, framesPerBar) 156 { 157 var visibleFrames = []; 158 for (var barNumber = 0, currentFrame = 0; currentFrame < frames.length; ++barNumber) { 159 var barStartTime = frames[currentFrame].startTime; 160 var longestFrame = null; 161 var longestDuration = 0; 162 163 for (var lastFrame = Math.min(Math.floor((barNumber + 1) * framesPerBar), frames.length); 164 currentFrame < lastFrame; ++currentFrame) { 165 var duration = this._frameDuration(frames[currentFrame]); 166 if (!longestFrame || longestDuration < duration) { 167 longestFrame = frames[currentFrame]; 168 longestDuration = duration; 169 } 170 } 171 var barEndTime = frames[currentFrame - 1].endTime; 172 if (longestFrame) { 173 visibleFrames.push(longestFrame); 174 this._barTimes.push({ startTime: barStartTime, endTime: barEndTime }); 175 } 176 } 177 return visibleFrames; 178 }, 179 180 /** 181 * @param {!WebInspector.TimelineFrame} frame 182 */ 183 _frameDuration: function(frame) 184 { 185 var relatedFrame = frame.mainThreadFrameId && this._framesById[frame.mainThreadFrameId]; 186 return frame.duration + (relatedFrame ? relatedFrame.duration : 0); 187 }, 188 189 /** 190 * @param {!Array.<!WebInspector.TimelineFrame>} frames 191 * @return {number} 192 */ 193 _computeTargetFrameLength: function(frames) 194 { 195 var durations = []; 196 for (var i = 0; i < frames.length; ++i) { 197 if (frames[i]) 198 durations.push(frames[i].duration); 199 } 200 var medianFrameLength = durations.qselect(Math.floor(durations.length / 2)); 201 202 // Optimize appearance for 30fps. However, if at least half frames won't fit at this scale, 203 // fall back to using autoscale. 204 const targetFPS = 30; 205 var result = 1.0 / targetFPS; 206 if (result >= medianFrameLength) 207 return result; 208 209 var maxFrameLength = Math.max.apply(Math, durations); 210 return Math.min(medianFrameLength * 2, maxFrameLength); 211 }, 212 213 /** 214 * @param {!Array.<!WebInspector.TimelineFrame>} frames 215 * @param {number} scale 216 * @param {number} windowHeight 217 */ 218 _renderBars: function(frames, scale, windowHeight) 219 { 220 const maxPadding = 5 * window.devicePixelRatio; 221 this._actualOuterBarWidth = Math.min((this._canvas.width - 2 * this._outerPadding) / frames.length, this._maxInnerBarWidth + maxPadding); 222 this._actualPadding = Math.min(Math.floor(this._actualOuterBarWidth / 3), maxPadding); 223 224 var barWidth = this._actualOuterBarWidth - this._actualPadding; 225 for (var i = 0; i < frames.length; ++i) { 226 if (frames[i]) 227 this._renderBar(this._barNumberToScreenPosition(i), barWidth, windowHeight, frames[i], scale); 228 } 229 }, 230 231 /** 232 * @param {number} n 233 */ 234 _barNumberToScreenPosition: function(n) 235 { 236 return this._outerPadding + this._actualOuterBarWidth * n; 237 }, 238 239 /** 240 * @param {number} scale 241 * @param {number} height 242 */ 243 _drawFPSMarks: function(scale, height) 244 { 245 const fpsMarks = [30, 60]; 246 247 this._context.save(); 248 this._context.beginPath(); 249 this._context.font = (10 * window.devicePixelRatio) + "px " + window.getComputedStyle(this.element, null).getPropertyValue("font-family"); 250 this._context.textAlign = "right"; 251 this._context.textBaseline = "alphabetic"; 252 253 const labelPadding = 4 * window.devicePixelRatio; 254 const baselineHeight = 3 * window.devicePixelRatio; 255 var lineHeight = 12 * window.devicePixelRatio; 256 var labelTopMargin = 0; 257 var labelOffsetY = 0; // Labels are going to be under their grid lines. 258 259 for (var i = 0; i < fpsMarks.length; ++i) { 260 var fps = fpsMarks[i]; 261 // Draw lines one pixel above they need to be, so 60pfs line does not cross most of the frames tops. 262 var y = height - Math.floor(1.0 / fps * scale) - 0.5; 263 var label = WebInspector.UIString("%d\u2009fps", fps); 264 var labelWidth = this._context.measureText(label).width + 2 * labelPadding; 265 var labelX = this._canvas.width; 266 267 if (!i && labelTopMargin < y - lineHeight) 268 labelOffsetY = -lineHeight; // Labels are going to be over their grid lines. 269 var labelY = y + labelOffsetY; 270 if (labelY < labelTopMargin || labelY + lineHeight > height) 271 break; // No space for the label, so no line as well. 272 273 this._context.moveTo(0, y); 274 this._context.lineTo(this._canvas.width, y); 275 276 this._context.fillStyle = "rgba(255, 255, 255, 0.5)"; 277 this._context.fillRect(labelX - labelWidth, labelY, labelWidth, lineHeight); 278 this._context.fillStyle = "black"; 279 this._context.fillText(label, labelX - labelPadding, labelY + lineHeight - baselineHeight); 280 labelTopMargin = labelY + lineHeight; 281 } 282 this._context.strokeStyle = "rgba(60, 60, 60, 0.4)"; 283 this._context.stroke(); 284 this._context.restore(); 285 }, 286 287 /** 288 * @param {number} left 289 * @param {number} width 290 * @param {number} windowHeight 291 * @param {!WebInspector.TimelineFrame} frame 292 * @param {number} scale 293 */ 294 _renderBar: function(left, width, windowHeight, frame, scale) 295 { 296 var categories = Object.keys(WebInspector.TimelinePresentationModel.categories()); 297 if (!categories.length) 298 return; 299 var x = Math.floor(left) + 0.5; 300 width = Math.floor(width); 301 302 for (var i = 0, bottomOffset = windowHeight; i < categories.length; ++i) { 303 var category = categories[i]; 304 var duration = frame.timeByCategory[category]; 305 306 if (!duration) 307 continue; 308 var height = Math.round(duration * scale); 309 var y = Math.floor(bottomOffset - height) + 0.5; 310 311 this._context.save(); 312 this._context.translate(x, 0); 313 this._context.scale(width / this._maxInnerBarWidth, 1); 314 this._context.fillStyle = this._fillStyles[category]; 315 this._context.fillRect(0, y, this._maxInnerBarWidth, Math.floor(height)); 316 this._context.strokeStyle = WebInspector.TimelinePresentationModel.categories()[category].borderColor; 317 this._context.beginPath(); 318 this._context.moveTo(0, y); 319 this._context.lineTo(this._maxInnerBarWidth, y); 320 this._context.stroke(); 321 this._context.restore(); 322 323 bottomOffset -= height; 324 } 325 // Draw a contour for the total frame time. 326 var y0 = Math.floor(windowHeight - frame.duration * scale) + 0.5; 327 var y1 = windowHeight + 0.5; 328 329 this._context.strokeStyle = "rgba(90, 90, 90, 0.3)"; 330 this._context.beginPath(); 331 this._context.moveTo(x, y1); 332 this._context.lineTo(x, y0); 333 this._context.lineTo(x + width, y0); 334 this._context.lineTo(x + width, y1); 335 this._context.stroke(); 336 }, 337 338 /** 339 * @param {number} windowLeft 340 * @param {number} windowRight 341 */ 342 windowTimes: function(windowLeft, windowRight) 343 { 344 if (!this._barTimes.length) 345 return WebInspector.TimelineOverviewBase.prototype.windowTimes.call(this, windowLeft, windowRight); 346 var windowSpan = this._canvas.width; 347 var leftOffset = windowLeft * windowSpan - this._outerPadding + this._actualPadding; 348 var rightOffset = windowRight * windowSpan - this._outerPadding; 349 var firstBar = Math.floor(Math.max(leftOffset, 0) / this._actualOuterBarWidth); 350 var lastBar = Math.min(Math.floor(rightOffset / this._actualOuterBarWidth), this._barTimes.length - 1); 351 if (firstBar >= this._barTimes.length) 352 return {startTime: Infinity, endTime: Infinity}; 353 354 const snapToRightTolerancePixels = 3; 355 return { 356 startTime: this._barTimes[firstBar].startTime, 357 endTime: (rightOffset + snapToRightTolerancePixels > windowSpan) || (lastBar >= this._barTimes.length) ? Infinity : this._barTimes[lastBar].endTime 358 } 359 }, 360 361 /** 362 * @param {number} startTime 363 * @param {number} endTime 364 */ 365 windowBoundaries: function(startTime, endTime) 366 { 367 if (this._barTimes.length === 0) 368 return {left: 0, right: 1}; 369 /** 370 * @param {number} time 371 * @param {{startTime:number, endTime:number}} barTime 372 * @return {number} 373 */ 374 function barStartComparator(time, barTime) 375 { 376 return time - barTime.startTime; 377 } 378 /** 379 * @param {number} time 380 * @param {{startTime:number, endTime:number}} barTime 381 * @return {number} 382 */ 383 function barEndComparator(time, barTime) 384 { 385 // We need a frame where time is in [barTime.startTime, barTime.endTime), so exclude exact matches against endTime. 386 if (time === barTime.endTime) 387 return 1; 388 return time - barTime.endTime; 389 } 390 return { 391 left: this._windowBoundaryFromTime(startTime, barEndComparator), 392 right: this._windowBoundaryFromTime(endTime, barStartComparator) 393 } 394 }, 395 396 /** 397 * @param {number} time 398 * @param {function(number, {startTime:number, endTime:number}):number} comparator 399 */ 400 _windowBoundaryFromTime: function(time, comparator) 401 { 402 if (time === Infinity) 403 return 1; 404 var index = this._firstBarAfter(time, comparator); 405 if (!index) 406 return 0; 407 return (this._barNumberToScreenPosition(index) - this._actualPadding / 2) / this._canvas.width; 408 }, 409 410 /** 411 * @param {number} time 412 * @param {function(number, {startTime:number, endTime:number}):number} comparator 413 */ 414 _firstBarAfter: function(time, comparator) 415 { 416 return insertionIndexForObjectInListSortedByFunction(time, this._barTimes, comparator); 417 }, 418 419 __proto__: WebInspector.TimelineOverviewBase.prototype 420} 421