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