| OLD | NEW |
| (Empty) |
| 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); | |
| 40 this.element.id = "timeline-overview-frames"; | |
| 41 this._model = model; | |
| 42 this._frameModel = frameModel; | |
| 43 this.reset(); | |
| 44 | |
| 45 this._outerPadding = 4 * window.devicePixelRatio; | |
| 46 this._maxInnerBarWidth = 10 * window.devicePixelRatio; | |
| 47 this._topPadding = 6 * window.devicePixelRatio; | |
| 48 | |
| 49 // The below two are really computed by update() -- but let's have something
so that windowTimes() is happy. | |
| 50 this._actualPadding = 5 * window.devicePixelRatio; | |
| 51 this._actualOuterBarWidth = this._maxInnerBarWidth + this._actualPadding; | |
| 52 | |
| 53 this._fillStyles = {}; | |
| 54 var categories = WebInspector.TimelineUIUtils.categories(); | |
| 55 for (var category in categories) | |
| 56 this._fillStyles[category] = WebInspector.TimelineUIUtils.createFillStyl
eForCategory(this._context, this._maxInnerBarWidth, 0, categories[category]); | |
| 57 | |
| 58 this._frameTopShadeGradient = this._context.createLinearGradient(0, 0, 0, th
is._topPadding); | |
| 59 this._frameTopShadeGradient.addColorStop(0, "rgba(255, 255, 255, 0.9)"); | |
| 60 this._frameTopShadeGradient.addColorStop(1, "rgba(255, 255, 255, 0.2)"); | |
| 61 | |
| 62 this.element.addEventListener("mousemove", this._onMouseMove.bind(this), fal
se); | |
| 63 this.element.addEventListener("mouseout", this._onMouseOut.bind(this), false
); | |
| 64 } | |
| 65 | |
| 66 WebInspector.TimelineFrameOverview.Events = { | |
| 67 SelectionChanged: "SelectionChanged" | |
| 68 } | |
| 69 | |
| 70 WebInspector.TimelineFrameOverview.prototype = { | |
| 71 /** | |
| 72 * @override | |
| 73 */ | |
| 74 reset: function() | |
| 75 { | |
| 76 /** @type {!Array<!{startTime:number, endTime:number}>} */ | |
| 77 this._barTimes = []; | |
| 78 /** @type {!Array<!WebInspector.TimelineFrame>} */ | |
| 79 this._visibleFrames = []; | |
| 80 this._selectedBarIndex = null; | |
| 81 this._activeBarIndex = null; | |
| 82 }, | |
| 83 | |
| 84 /** | |
| 85 * @override | |
| 86 */ | |
| 87 update: function() | |
| 88 { | |
| 89 this.resetCanvas(); | |
| 90 this._barTimes = []; | |
| 91 | |
| 92 var minBarWidth = 4 * window.devicePixelRatio; | |
| 93 var frames = this._frameModel.frames(); | |
| 94 var framesPerBar = Math.max(1, frames.length * minBarWidth / this._canva
s.width); | |
| 95 this._visibleFrames = this._aggregateFrames(frames, framesPerBar); | |
| 96 this._scale = (this._canvas.height - this._topPadding) / this._computeTa
rgetFrameLength(this._visibleFrames); | |
| 97 var maxPadding = 5 * window.devicePixelRatio; | |
| 98 this._actualOuterBarWidth = Math.min((this._canvas.width - 2 * this._out
erPadding) / this._visibleFrames.length, this._maxInnerBarWidth + maxPadding); | |
| 99 this._actualPadding = Math.min(Math.floor(this._actualOuterBarWidth / 3)
, maxPadding); | |
| 100 | |
| 101 this._context.save(); | |
| 102 for (var i = this._visibleFrames.length - 1; i >= 0; --i) | |
| 103 this._drawBar(i); | |
| 104 this._drawTopShadeGradient(); | |
| 105 this._drawFPSMarks(); | |
| 106 this._drawSelection(); | |
| 107 this._context.restore(); | |
| 108 }, | |
| 109 | |
| 110 /** | |
| 111 * @param {?WebInspector.TimelineSelection} selection | |
| 112 */ | |
| 113 select: function(selection) | |
| 114 { | |
| 115 var oldSelectionIndex = this._selectedBarIndex; | |
| 116 var frame = selection && selection.type() === WebInspector.TimelineSelec
tion.Type.Frame ? /** @type {!WebInspector.TimelineFrame} */ (selection.object()
) : null; | |
| 117 var index = frame ? this._visibleFrames.indexOf(frame) : -1; | |
| 118 this._selectedBarIndex = index >= 0 ? index : null; | |
| 119 if (this._selectedBarIndex === oldSelectionIndex) | |
| 120 return; | |
| 121 if (typeof oldSelectionIndex === "number") | |
| 122 this._redrawBar(oldSelectionIndex); | |
| 123 this._drawSelection(); | |
| 124 }, | |
| 125 | |
| 126 /** | |
| 127 * @override | |
| 128 * @param {!Event} event | |
| 129 * @return {boolean} | |
| 130 */ | |
| 131 onClick: function(event) | |
| 132 { | |
| 133 var barIndex = this._screenPositionToBarIndex(event.clientX); | |
| 134 if (barIndex < 0 || barIndex >= this._visibleFrames.length) | |
| 135 return false; | |
| 136 var selection = WebInspector.TimelineSelection.fromFrame(this._visibleFr
ames[barIndex]); | |
| 137 this.dispatchEventToListeners(WebInspector.TimelineFrameOverview.Events.
SelectionChanged, selection); | |
| 138 return true; | |
| 139 }, | |
| 140 | |
| 141 /** | |
| 142 * @param {!Event} event | |
| 143 */ | |
| 144 _onMouseMove: function(event) | |
| 145 { | |
| 146 var barIndex = this._screenPositionToBarIndex(event.clientX); | |
| 147 if (barIndex < 0 || barIndex >= this._visibleFrames.length) | |
| 148 barIndex = null; | |
| 149 this._setActiveBarIndex(barIndex); | |
| 150 }, | |
| 151 | |
| 152 /** | |
| 153 * @param {!Event} event | |
| 154 */ | |
| 155 _onMouseOut: function(event) | |
| 156 { | |
| 157 this._setActiveBarIndex(null); | |
| 158 }, | |
| 159 | |
| 160 /** | |
| 161 * @param {?number} index | |
| 162 */ | |
| 163 _setActiveBarIndex: function(index) | |
| 164 { | |
| 165 if (this._activeBarIndex === index) | |
| 166 return; | |
| 167 var oldActveBarIndex = this._activeBarIndex; | |
| 168 this._activeBarIndex = index; | |
| 169 if (typeof oldActveBarIndex === "number") | |
| 170 this._redrawBar(oldActveBarIndex); | |
| 171 if (typeof this._activeBarIndex === "number") | |
| 172 this._redrawBar(this._activeBarIndex); | |
| 173 }, | |
| 174 | |
| 175 /** | |
| 176 * @param {number} index | |
| 177 */ | |
| 178 _redrawBar: function(index) | |
| 179 { | |
| 180 this._context.save(); | |
| 181 this._context.beginPath(); | |
| 182 var left = this._barIndexToScreenPosition(index) - this._actualPadding; | |
| 183 var right = Math.ceil(left + this._actualOuterBarWidth); | |
| 184 this._context.rect(left, 0, right - left + 1, this._canvas.height); | |
| 185 this._context.fillStyle = "rgb(255, 255, 255)"; | |
| 186 this._context.clip(); | |
| 187 this._context.fill(); | |
| 188 if (index > 0) | |
| 189 this._drawBar(index - 1); | |
| 190 if (index + 1 < this._visibleFrames.length) | |
| 191 this._drawBar(index + 1); | |
| 192 this._drawBar(index); | |
| 193 this._drawTopShadeGradient(); | |
| 194 this._drawFPSMarks(); | |
| 195 if (typeof this._selectedBarIndex === "number") | |
| 196 this._drawSelection(); | |
| 197 this._context.restore(); | |
| 198 }, | |
| 199 | |
| 200 /** | |
| 201 * @param {!Array.<!WebInspector.TimelineFrame>} frames | |
| 202 * @param {number} framesPerBar | |
| 203 * @return {!Array.<!WebInspector.TimelineFrame>} | |
| 204 */ | |
| 205 _aggregateFrames: function(frames, framesPerBar) | |
| 206 { | |
| 207 var visibleFrames = []; | |
| 208 for (var barIndex = 0, currentFrame = 0; currentFrame < frames.length; +
+barIndex) { | |
| 209 var barStartTime = frames[currentFrame].startTime; | |
| 210 var longestFrame = null; | |
| 211 var longestDuration; | |
| 212 | |
| 213 for (var lastFrame = Math.min(Math.floor((barIndex + 1) * framesPerB
ar), frames.length); | |
| 214 currentFrame < lastFrame; ++currentFrame) { | |
| 215 var frame = frames[currentFrame]; | |
| 216 var duration = frame.idle ? 0 : frame.duration; // Only consider
idle frames if there are no regular frames. | |
| 217 if (!longestFrame || longestDuration < duration) { | |
| 218 longestFrame = frame; | |
| 219 longestDuration = duration; | |
| 220 } | |
| 221 } | |
| 222 var barEndTime = frames[currentFrame - 1].endTime; | |
| 223 if (longestFrame) { | |
| 224 visibleFrames.push(longestFrame); | |
| 225 this._barTimes.push({ startTime: barStartTime, endTime: barEndTi
me }); | |
| 226 } | |
| 227 } | |
| 228 return visibleFrames; | |
| 229 }, | |
| 230 | |
| 231 /** | |
| 232 * @param {!Array.<!WebInspector.TimelineFrame>} frames | |
| 233 * @return {number} | |
| 234 */ | |
| 235 _computeTargetFrameLength: function(frames) | |
| 236 { | |
| 237 var targetFPS = 20; | |
| 238 var result = 1000.0 / targetFPS; | |
| 239 if (!frames.length) | |
| 240 return result; | |
| 241 | |
| 242 var durations = frames.select("duration"); | |
| 243 var medianFrameLength = durations.qselect(Math.floor(durations.length /
2)); | |
| 244 | |
| 245 // Optimize appearance for 30fps, but leave some space so it's evident w
hen a frame overflows. | |
| 246 // However, if at least half frames won't fit at this scale, fall back t
o using autoscale. | |
| 247 if (result >= medianFrameLength) | |
| 248 return result; | |
| 249 | |
| 250 var maxFrameLength = Math.max.apply(Math, durations); | |
| 251 return Math.min(medianFrameLength * 2, maxFrameLength); | |
| 252 }, | |
| 253 | |
| 254 /** | |
| 255 * @param {number} n | |
| 256 */ | |
| 257 _barIndexToScreenPosition: function(n) | |
| 258 { | |
| 259 return this._outerPadding + this._actualOuterBarWidth * n; | |
| 260 }, | |
| 261 | |
| 262 /** | |
| 263 * @param {number} clientX | |
| 264 */ | |
| 265 _screenPositionToBarIndex: function(clientX) | |
| 266 { | |
| 267 var x = (clientX - this.element.totalOffsetLeft()) * window.devicePixelR
atio; | |
| 268 return Math.floor((x - this._outerPadding) / this._actualOuterBarWidth); | |
| 269 }, | |
| 270 | |
| 271 _drawTopShadeGradient: function() | |
| 272 { | |
| 273 this._context.fillStyle = this._frameTopShadeGradient; | |
| 274 this._context.fillRect(0, 0, this._canvas.width, this._topPadding); | |
| 275 }, | |
| 276 | |
| 277 _drawFPSMarks: function() | |
| 278 { | |
| 279 var fpsMarks = [30, 60]; | |
| 280 | |
| 281 this._context.save(); | |
| 282 this._context.beginPath(); | |
| 283 this._context.font = (10 * window.devicePixelRatio) + "px " + window.get
ComputedStyle(this.element, null).getPropertyValue("font-family"); | |
| 284 this._context.textAlign = "right"; | |
| 285 this._context.textBaseline = "alphabetic"; | |
| 286 | |
| 287 var labelPadding = 4 * window.devicePixelRatio; | |
| 288 var baselineHeight = 3 * window.devicePixelRatio; | |
| 289 var lineHeight = 12 * window.devicePixelRatio; | |
| 290 var labelTopMargin = 0; | |
| 291 var labelOffsetY = 0; // Labels are going to be under their grid lines. | |
| 292 | |
| 293 for (var i = 0; i < fpsMarks.length; ++i) { | |
| 294 var fps = fpsMarks[i]; | |
| 295 // Draw lines one pixel above they need to be, so 60pfs line does no
t cross most of the frames tops. | |
| 296 var y = this._canvas.height - Math.floor(1000.0 / fps * this._scale)
- 0.5; | |
| 297 var label = WebInspector.UIString("%d\u2009fps", fps); | |
| 298 var labelWidth = this._context.measureText(label).width + 2 * labelP
adding; | |
| 299 var labelX = this._canvas.width; | |
| 300 | |
| 301 if (!i && labelTopMargin < y - lineHeight) | |
| 302 labelOffsetY = -lineHeight; // Labels are going to be over their
grid lines. | |
| 303 var labelY = y + labelOffsetY; | |
| 304 if (labelY < labelTopMargin || labelY + lineHeight > this._canvas.he
ight) | |
| 305 break; // No space for the label, so no line as well. | |
| 306 | |
| 307 this._context.moveTo(0, y); | |
| 308 this._context.lineTo(this._canvas.width, y); | |
| 309 | |
| 310 this._context.fillStyle = "rgba(255, 255, 255, 0.5)"; | |
| 311 this._context.fillRect(labelX - labelWidth, labelY, labelWidth, line
Height); | |
| 312 this._context.fillStyle = "black"; | |
| 313 this._context.fillText(label, labelX - labelPadding, labelY + lineHe
ight - baselineHeight); | |
| 314 labelTopMargin = labelY + lineHeight; | |
| 315 } | |
| 316 this._context.strokeStyle = "rgba(60, 60, 60, 0.4)"; | |
| 317 this._context.stroke(); | |
| 318 this._context.restore(); | |
| 319 }, | |
| 320 | |
| 321 /** | |
| 322 * @param {number} index | |
| 323 */ | |
| 324 _drawBar: function(index) | |
| 325 { | |
| 326 var left = this._barIndexToScreenPosition(index); | |
| 327 var frame = this._visibleFrames[index]; | |
| 328 var categories = Object.keys(WebInspector.TimelineUIUtils.categories()); | |
| 329 var windowHeight = this._canvas.height; | |
| 330 var width = Math.floor(this._actualOuterBarWidth - this._actualPadding); | |
| 331 | |
| 332 var x = Math.floor(left) + 0.5; | |
| 333 | |
| 334 var totalCPUTime = frame.cpuTime; | |
| 335 var normalizedScale = this._scale; | |
| 336 if (totalCPUTime > frame.duration) | |
| 337 normalizedScale *= frame.duration / totalCPUTime; | |
| 338 | |
| 339 for (var i = 0, bottomOffset = windowHeight; i < categories.length; ++i)
{ | |
| 340 var category = categories[i]; | |
| 341 var duration = frame.timeByCategory[category]; | |
| 342 if (!duration) | |
| 343 continue; | |
| 344 var height = Math.round(duration * normalizedScale); | |
| 345 var y = Math.floor(bottomOffset - height) + 0.5; | |
| 346 | |
| 347 this._context.save(); | |
| 348 this._context.translate(x, 0); | |
| 349 this._context.scale(width / this._maxInnerBarWidth, 1); | |
| 350 this._context.fillStyle = this._fillStyles[category]; | |
| 351 this._context.fillRect(0, y, this._maxInnerBarWidth, Math.floor(heig
ht)); | |
| 352 this._context.strokeStyle = WebInspector.TimelineUIUtils.categories(
)[category].borderColor; | |
| 353 this._context.beginPath(); | |
| 354 this._context.moveTo(0, y); | |
| 355 this._context.lineTo(this._maxInnerBarWidth, y); | |
| 356 this._context.stroke(); | |
| 357 this._context.restore(); | |
| 358 | |
| 359 bottomOffset -= height; | |
| 360 } | |
| 361 // Skip outline for idle frames, unless frame is selected. | |
| 362 if (frame.idle && index !== this._activeBarIndex) | |
| 363 return; | |
| 364 | |
| 365 // Draw a contour for the total frame time. | |
| 366 var y0 = frame.idle ? bottomOffset + 0.5 : Math.floor(windowHeight - fra
me.duration * this._scale) + 0.5; | |
| 367 var y1 = windowHeight + 0.5; | |
| 368 | |
| 369 this._context.strokeStyle = index === this._activeBarIndex ? "rgba(0, 0,
0, 0.6)" : "rgba(90, 90, 90, 0.2)"; | |
| 370 this._context.beginPath(); | |
| 371 this._context.moveTo(x, y1); | |
| 372 this._context.lineTo(x, y0); | |
| 373 this._context.lineTo(x + width, y0); | |
| 374 this._context.lineTo(x + width, y1); | |
| 375 this._context.stroke(); | |
| 376 }, | |
| 377 | |
| 378 _drawSelection: function() | |
| 379 { | |
| 380 if (typeof this._selectedBarIndex !== "number") | |
| 381 return; | |
| 382 var left = this._barIndexToScreenPosition(this._selectedBarIndex); | |
| 383 var width = Math.floor(this._actualOuterBarWidth - this._actualPadding); | |
| 384 var triangleHeight = 4 * window.devicePixelRatio; | |
| 385 this._context.save(); | |
| 386 this._context.beginPath(); | |
| 387 this._context.moveTo(left, 0); | |
| 388 this._context.lineTo(left + width, 0); | |
| 389 this._context.lineTo(left + width / 2, triangleHeight); | |
| 390 this._context.closePath(); | |
| 391 this._context.fillStyle = "black"; | |
| 392 this._context.fill(); | |
| 393 this._context.restore(); | |
| 394 }, | |
| 395 | |
| 396 /** | |
| 397 * @override | |
| 398 * @param {number} windowLeft | |
| 399 * @param {number} windowRight | |
| 400 * @return {!{startTime: number, endTime: number}} | |
| 401 */ | |
| 402 windowTimes: function(windowLeft, windowRight) | |
| 403 { | |
| 404 if (!this._barTimes.length) | |
| 405 return WebInspector.TimelineOverviewBase.prototype.windowTimes.call(
this, windowLeft, windowRight); | |
| 406 var windowSpan = this._canvas.width; | |
| 407 var leftOffset = windowLeft * windowSpan; | |
| 408 var rightOffset = windowRight * windowSpan; | |
| 409 var firstBar = Math.floor(Math.max(leftOffset - this._outerPadding + thi
s._actualPadding, 0) / this._actualOuterBarWidth); | |
| 410 var lastBar = Math.min(Math.floor(Math.max(rightOffset - this._outerPadd
ing, 0)/ this._actualOuterBarWidth), this._barTimes.length - 1); | |
| 411 if (firstBar >= this._barTimes.length) | |
| 412 return {startTime: Infinity, endTime: Infinity}; | |
| 413 | |
| 414 var snapTolerancePixels = 3; | |
| 415 return { | |
| 416 startTime: leftOffset > snapTolerancePixels ? this._barTimes[firstBa
r].startTime : this._model.minimumRecordTime(), | |
| 417 endTime: (rightOffset + snapTolerancePixels > windowSpan) || (lastBa
r >= this._barTimes.length) ? this._model.maximumRecordTime() : this._barTimes[l
astBar].endTime | |
| 418 }; | |
| 419 }, | |
| 420 | |
| 421 /** | |
| 422 * @override | |
| 423 * @param {number} startTime | |
| 424 * @param {number} endTime | |
| 425 * @return {!{left: number, right: number}} | |
| 426 */ | |
| 427 windowBoundaries: function(startTime, endTime) | |
| 428 { | |
| 429 if (this._barTimes.length === 0) | |
| 430 return {left: 0, right: 1}; | |
| 431 /** | |
| 432 * @param {number} time | |
| 433 * @param {!{startTime:number, endTime:number}} barTime | |
| 434 * @return {number} | |
| 435 */ | |
| 436 function barStartComparator(time, barTime) | |
| 437 { | |
| 438 return time - barTime.startTime; | |
| 439 } | |
| 440 /** | |
| 441 * @param {number} time | |
| 442 * @param {!{startTime:number, endTime:number}} barTime | |
| 443 * @return {number} | |
| 444 */ | |
| 445 function barEndComparator(time, barTime) | |
| 446 { | |
| 447 // We need a frame where time is in [barTime.startTime, barTime.endT
ime), so exclude exact matches against endTime. | |
| 448 if (time === barTime.endTime) | |
| 449 return 1; | |
| 450 return time - barTime.endTime; | |
| 451 } | |
| 452 return { | |
| 453 left: this._windowBoundaryFromTime(startTime, barEndComparator), | |
| 454 right: this._windowBoundaryFromTime(endTime, barStartComparator) | |
| 455 }; | |
| 456 }, | |
| 457 | |
| 458 /** | |
| 459 * @param {number} time | |
| 460 * @param {function(number, !{startTime:number, endTime:number}):number} com
parator | |
| 461 */ | |
| 462 _windowBoundaryFromTime: function(time, comparator) | |
| 463 { | |
| 464 if (time === Infinity) | |
| 465 return 1; | |
| 466 var index = this._firstBarAfter(time, comparator); | |
| 467 if (!index) | |
| 468 return 0; | |
| 469 return (this._barIndexToScreenPosition(index) - this._actualPadding / 2)
/ this._canvas.width; | |
| 470 }, | |
| 471 | |
| 472 /** | |
| 473 * @param {number} time | |
| 474 * @param {function(number, {startTime:number, endTime:number}):number} comp
arator | |
| 475 */ | |
| 476 _firstBarAfter: function(time, comparator) | |
| 477 { | |
| 478 return insertionIndexForObjectInListSortedByFunction(time, this._barTime
s, comparator); | |
| 479 }, | |
| 480 | |
| 481 __proto__: WebInspector.TimelineOverviewBase.prototype | |
| 482 } | |
| OLD | NEW |