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 var durations = []; 140 for (var i = 0; i < frames.length; ++i) { 141 if (frames[i]) 142 durations.push(frames[i].duration); 143 } 144 var medianFrameLength = durations.qselect(Math.floor(durations.length / 2)); 145 146 // Optimize appearance for 30fps, but leave some space so it's evident when a frame overflows. 147 // However, if at least half frames won't fit at this scale, fall back to using autoscale. 148 const targetFPS = 20; 149 var result = 1000.0 / targetFPS; 150 if (result >= medianFrameLength) 151 return result; 152 153 var maxFrameLength = Math.max.apply(Math, durations); 154 return Math.min(medianFrameLength * 2, maxFrameLength); 155 }, 156 157 /** 158 * @param {!Array.<!WebInspector.TimelineFrame>} frames 159 * @param {number} scale 160 * @param {number} windowHeight 161 */ 162 _renderBars: function(frames, scale, windowHeight) 163 { 164 const maxPadding = 5 * window.devicePixelRatio; 165 this._actualOuterBarWidth = Math.min((this._canvas.width - 2 * this._outerPadding) / frames.length, this._maxInnerBarWidth + maxPadding); 166 this._actualPadding = Math.min(Math.floor(this._actualOuterBarWidth / 3), maxPadding); 167 168 var barWidth = this._actualOuterBarWidth - this._actualPadding; 169 for (var i = 0; i < frames.length; ++i) { 170 if (frames[i]) 171 this._renderBar(this._barNumberToScreenPosition(i), barWidth, windowHeight, frames[i], scale); 172 } 173 }, 174 175 /** 176 * @param {number} n 177 */ 178 _barNumberToScreenPosition: function(n) 179 { 180 return this._outerPadding + this._actualOuterBarWidth * n; 181 }, 182 183 /** 184 * @param {number} scale 185 * @param {number} height 186 */ 187 _drawFPSMarks: function(scale, height) 188 { 189 const fpsMarks = [30, 60]; 190 191 this._context.save(); 192 this._context.beginPath(); 193 this._context.font = (10 * window.devicePixelRatio) + "px " + window.getComputedStyle(this.element, null).getPropertyValue("font-family"); 194 this._context.textAlign = "right"; 195 this._context.textBaseline = "alphabetic"; 196 197 const labelPadding = 4 * window.devicePixelRatio; 198 const baselineHeight = 3 * window.devicePixelRatio; 199 var lineHeight = 12 * window.devicePixelRatio; 200 var labelTopMargin = 0; 201 var labelOffsetY = 0; // Labels are going to be under their grid lines. 202 203 for (var i = 0; i < fpsMarks.length; ++i) { 204 var fps = fpsMarks[i]; 205 // Draw lines one pixel above they need to be, so 60pfs line does not cross most of the frames tops. 206 var y = height - Math.floor(1000.0 / fps * scale) - 0.5; 207 var label = WebInspector.UIString("%d\u2009fps", fps); 208 var labelWidth = this._context.measureText(label).width + 2 * labelPadding; 209 var labelX = this._canvas.width; 210 211 if (!i && labelTopMargin < y - lineHeight) 212 labelOffsetY = -lineHeight; // Labels are going to be over their grid lines. 213 var labelY = y + labelOffsetY; 214 if (labelY < labelTopMargin || labelY + lineHeight > height) 215 break; // No space for the label, so no line as well. 216 217 this._context.moveTo(0, y); 218 this._context.lineTo(this._canvas.width, y); 219 220 this._context.fillStyle = "rgba(255, 255, 255, 0.5)"; 221 this._context.fillRect(labelX - labelWidth, labelY, labelWidth, lineHeight); 222 this._context.fillStyle = "black"; 223 this._context.fillText(label, labelX - labelPadding, labelY + lineHeight - baselineHeight); 224 labelTopMargin = labelY + lineHeight; 225 } 226 this._context.strokeStyle = "rgba(60, 60, 60, 0.4)"; 227 this._context.stroke(); 228 this._context.restore(); 229 }, 230 231 /** 232 * @param {number} left 233 * @param {number} width 234 * @param {number} windowHeight 235 * @param {!WebInspector.TimelineFrame} frame 236 * @param {number} scale 237 */ 238 _renderBar: function(left, width, windowHeight, frame, scale) 239 { 240 var categories = Object.keys(WebInspector.TimelineUIUtils.categories()); 241 var x = Math.floor(left) + 0.5; 242 width = Math.floor(width); 243 244 var totalCPUTime = frame.cpuTime; 245 var normalizedScale = scale; 246 if (totalCPUTime > frame.duration) 247 normalizedScale *= frame.duration / totalCPUTime; 248 249 for (var i = 0, bottomOffset = windowHeight; i < categories.length; ++i) { 250 var category = categories[i]; 251 var duration = frame.timeByCategory[category]; 252 if (!duration) 253 continue; 254 var height = Math.round(duration * normalizedScale); 255 var y = Math.floor(bottomOffset - height) + 0.5; 256 257 this._context.save(); 258 this._context.translate(x, 0); 259 this._context.scale(width / this._maxInnerBarWidth, 1); 260 this._context.fillStyle = this._fillStyles[category]; 261 this._context.fillRect(0, y, this._maxInnerBarWidth, Math.floor(height)); 262 this._context.strokeStyle = WebInspector.TimelineUIUtils.categories()[category].borderColor; 263 this._context.beginPath(); 264 this._context.moveTo(0, y); 265 this._context.lineTo(this._maxInnerBarWidth, y); 266 this._context.stroke(); 267 this._context.restore(); 268 269 bottomOffset -= height; 270 } 271 // Draw a contour for the total frame time. 272 var y0 = Math.floor(windowHeight - frame.duration * scale) + 0.5; 273 var y1 = windowHeight + 0.5; 274 275 this._context.strokeStyle = "rgba(90, 90, 90, 0.3)"; 276 this._context.beginPath(); 277 this._context.moveTo(x, y1); 278 this._context.lineTo(x, y0); 279 this._context.lineTo(x + width, y0); 280 this._context.lineTo(x + width, y1); 281 this._context.stroke(); 282 }, 283 284 /** 285 * @param {number} windowLeft 286 * @param {number} windowRight 287 * @return {!{startTime: number, endTime: number}} 288 */ 289 windowTimes: function(windowLeft, windowRight) 290 { 291 if (!this._barTimes.length) 292 return WebInspector.TimelineOverviewBase.prototype.windowTimes.call(this, windowLeft, windowRight); 293 var windowSpan = this._canvas.width; 294 var leftOffset = windowLeft * windowSpan; 295 var rightOffset = windowRight * windowSpan; 296 var firstBar = Math.floor(Math.max(leftOffset - this._outerPadding + this._actualPadding, 0) / this._actualOuterBarWidth); 297 var lastBar = Math.min(Math.floor(Math.max(rightOffset - this._outerPadding, 0)/ this._actualOuterBarWidth), this._barTimes.length - 1); 298 if (firstBar >= this._barTimes.length) 299 return {startTime: Infinity, endTime: Infinity}; 300 301 const snapTolerancePixels = 3; 302 return { 303 startTime: leftOffset > snapTolerancePixels ? this._barTimes[firstBar].startTime : this._model.minimumRecordTime(), 304 endTime: (rightOffset + snapTolerancePixels > windowSpan) || (lastBar >= this._barTimes.length) ? this._model.maximumRecordTime() : this._barTimes[lastBar].endTime 305 } 306 }, 307 308 /** 309 * @param {number} startTime 310 * @param {number} endTime 311 * @return {!{left: number, right: number}} 312 */ 313 windowBoundaries: function(startTime, endTime) 314 { 315 if (this._barTimes.length === 0) 316 return {left: 0, right: 1}; 317 /** 318 * @param {number} time 319 * @param {!{startTime:number, endTime:number}} barTime 320 * @return {number} 321 */ 322 function barStartComparator(time, barTime) 323 { 324 return time - barTime.startTime; 325 } 326 /** 327 * @param {number} time 328 * @param {!{startTime:number, endTime:number}} barTime 329 * @return {number} 330 */ 331 function barEndComparator(time, barTime) 332 { 333 // We need a frame where time is in [barTime.startTime, barTime.endTime), so exclude exact matches against endTime. 334 if (time === barTime.endTime) 335 return 1; 336 return time - barTime.endTime; 337 } 338 return { 339 left: this._windowBoundaryFromTime(startTime, barEndComparator), 340 right: this._windowBoundaryFromTime(endTime, barStartComparator) 341 } 342 }, 343 344 /** 345 * @param {number} time 346 * @param {function(number, !{startTime:number, endTime:number}):number} comparator 347 */ 348 _windowBoundaryFromTime: function(time, comparator) 349 { 350 if (time === Infinity) 351 return 1; 352 var index = this._firstBarAfter(time, comparator); 353 if (!index) 354 return 0; 355 return (this._barNumberToScreenPosition(index) - this._actualPadding / 2) / this._canvas.width; 356 }, 357 358 /** 359 * @param {number} time 360 * @param {function(number, {startTime:number, endTime:number}):number} comparator 361 */ 362 _firstBarAfter: function(time, comparator) 363 { 364 return insertionIndexForObjectInListSortedByFunction(time, this._barTimes, comparator); 365 }, 366 367 __proto__: WebInspector.TimelineOverviewBase.prototype 368 } 369