1 /* 2 * Copyright (C) 2012 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.SplitView} 34 * @param {string} title 35 * @param {!WebInspector.TimelineModeViewDelegate} delegate 36 * @param {!WebInspector.TimelineModel} model 37 */ 38 WebInspector.CountersGraph = function(title, delegate, model) 39 { 40 WebInspector.SplitView.call(this, true, false); 41 42 this.element.id = "memory-graphs-container"; 43 44 this._delegate = delegate; 45 this._model = model; 46 this._calculator = new WebInspector.TimelineCalculator(this._model); 47 48 this._graphsContainer = this.mainElement(); 49 this._createCurrentValuesBar(); 50 this._canvasView = new WebInspector.VBoxWithResizeCallback(this._resize.bind(this)); 51 this._canvasView.show(this._graphsContainer); 52 this._canvasContainer = this._canvasView.element; 53 this._canvasContainer.id = "memory-graphs-canvas-container"; 54 this._canvas = this._canvasContainer.createChild("canvas"); 55 this._canvas.id = "memory-counters-graph"; 56 57 this._canvasContainer.addEventListener("mouseover", this._onMouseMove.bind(this), true); 58 this._canvasContainer.addEventListener("mousemove", this._onMouseMove.bind(this), true); 59 this._canvasContainer.addEventListener("mouseout", this._onMouseOut.bind(this), true); 60 this._canvasContainer.addEventListener("click", this._onClick.bind(this), true); 61 // We create extra timeline grid here to reuse its event dividers. 62 this._timelineGrid = new WebInspector.TimelineGrid(); 63 this._canvasContainer.appendChild(this._timelineGrid.dividersElement); 64 65 // Populate sidebar 66 this.sidebarElement().createChild("div", "sidebar-tree sidebar-tree-section").textContent = title; 67 this._counters = []; 68 this._counterUI = []; 69 } 70 71 WebInspector.CountersGraph.prototype = { 72 _createCurrentValuesBar: function() 73 { 74 this._currentValuesBar = this._graphsContainer.createChild("div"); 75 this._currentValuesBar.id = "counter-values-bar"; 76 }, 77 78 /** 79 * @param {string} uiName 80 * @param {string} uiValueTemplate 81 * @param {string} color 82 * @return {!WebInspector.CountersGraph.Counter} 83 */ 84 createCounter: function(uiName, uiValueTemplate, color) 85 { 86 var counter = new WebInspector.CountersGraph.Counter(); 87 this._counters.push(counter); 88 this._counterUI.push(new WebInspector.CountersGraph.CounterUI(this, uiName, uiValueTemplate, color, counter)); 89 return counter; 90 }, 91 92 /** 93 * @return {!WebInspector.View} 94 */ 95 view: function() 96 { 97 return this; 98 }, 99 100 dispose: function() 101 { 102 }, 103 104 reset: function() 105 { 106 for (var i = 0; i < this._counters.length; ++i) { 107 this._counters[i].reset(); 108 this._counterUI[i].reset(); 109 } 110 this.refresh(); 111 }, 112 113 _resize: function() 114 { 115 var parentElement = this._canvas.parentElement; 116 this._canvas.width = parentElement.clientWidth * window.devicePixelRatio; 117 this._canvas.height = parentElement.clientHeight * window.devicePixelRatio; 118 var timelinePaddingLeft = 15; 119 this._calculator.setDisplayWindow(timelinePaddingLeft, this._canvas.width); 120 this.refresh(); 121 }, 122 123 /** 124 * @param {number} startTime 125 * @param {number} endTime 126 */ 127 setWindowTimes: function(startTime, endTime) 128 { 129 this._calculator.setWindow(startTime, endTime); 130 this.scheduleRefresh(); 131 }, 132 133 scheduleRefresh: function() 134 { 135 WebInspector.invokeOnceAfterBatchUpdate(this, this.refresh); 136 }, 137 138 draw: function() 139 { 140 for (var i = 0; i < this._counters.length; ++i) { 141 this._counters[i]._calculateVisibleIndexes(this._calculator); 142 this._counters[i]._calculateXValues(this._canvas.width); 143 } 144 this._clear(); 145 146 for (var i = 0; i < this._counterUI.length; i++) 147 this._counterUI[i]._drawGraph(this._canvas); 148 }, 149 150 /** 151 * @param {!Event} event 152 */ 153 _onClick: function(event) 154 { 155 var x = event.x - this._canvasContainer.totalOffsetLeft(); 156 var minDistance = Infinity; 157 var bestTime; 158 for (var i = 0; i < this._counterUI.length; ++i) { 159 var counterUI = this._counterUI[i]; 160 if (!counterUI.counter.times.length) 161 continue; 162 var index = counterUI._recordIndexAt(x); 163 var distance = Math.abs(x * window.devicePixelRatio - counterUI.counter.x[index]); 164 if (distance < minDistance) { 165 minDistance = distance; 166 bestTime = counterUI.counter.times[index]; 167 } 168 } 169 if (bestTime !== undefined) 170 this._revealRecordAt(bestTime); 171 }, 172 173 /** 174 * @param {number} time 175 */ 176 _revealRecordAt: function(time) 177 { 178 var recordToReveal; 179 /** 180 * @param {!WebInspector.TimelineModel.Record} record 181 * @return {boolean} 182 * @this {WebInspector.CountersGraph} 183 */ 184 function findRecordToReveal(record) 185 { 186 if (!this._model.isVisible(record)) 187 return false; 188 if (record.startTime() <= time && time <= record.endTime()) { 189 recordToReveal = record; 190 return true; 191 } 192 // If there is no record containing the time than use the latest one before that time. 193 if (!recordToReveal || record.endTime() < time && recordToReveal.endTime() < record.endTime()) 194 recordToReveal = record; 195 return false; 196 } 197 this._model.forAllRecords(null, findRecordToReveal.bind(this)); 198 this._delegate.select(recordToReveal ? WebInspector.TimelineSelection.fromRecord(recordToReveal) : null); 199 }, 200 201 /** 202 * @param {!Event} event 203 */ 204 _onMouseOut: function(event) 205 { 206 delete this._markerXPosition; 207 this._clearCurrentValueAndMarker(); 208 }, 209 210 _clearCurrentValueAndMarker: function() 211 { 212 for (var i = 0; i < this._counterUI.length; i++) 213 this._counterUI[i]._clearCurrentValueAndMarker(); 214 }, 215 216 /** 217 * @param {!Event} event 218 */ 219 _onMouseMove: function(event) 220 { 221 var x = event.x - this._canvasContainer.totalOffsetLeft(); 222 this._markerXPosition = x; 223 this._refreshCurrentValues(); 224 }, 225 226 _refreshCurrentValues: function() 227 { 228 if (this._markerXPosition === undefined) 229 return; 230 for (var i = 0; i < this._counterUI.length; ++i) 231 this._counterUI[i].updateCurrentValue(this._markerXPosition); 232 }, 233 234 refresh: function() 235 { 236 this._timelineGrid.updateDividers(this._calculator); 237 this.draw(); 238 this._refreshCurrentValues(); 239 }, 240 241 refreshRecords: function() 242 { 243 }, 244 245 _clear: function() 246 { 247 var ctx = this._canvas.getContext("2d"); 248 ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); 249 }, 250 251 /** 252 * @param {?WebInspector.TimelineModel.Record} record 253 * @param {string=} regex 254 * @param {boolean=} selectRecord 255 */ 256 highlightSearchResult: function(record, regex, selectRecord) 257 { 258 }, 259 260 /** 261 * @param {?WebInspector.TimelineSelection} selection 262 */ 263 setSelection: function(selection) 264 { 265 }, 266 267 __proto__: WebInspector.SplitView.prototype 268 } 269 270 /** 271 * @constructor 272 */ 273 WebInspector.CountersGraph.Counter = function() 274 { 275 this.times = []; 276 this.values = []; 277 } 278 279 WebInspector.CountersGraph.Counter.prototype = { 280 /** 281 * @param {number} time 282 * @param {number} value 283 */ 284 appendSample: function(time, value) 285 { 286 if (this.values.length && this.values.peekLast() === value) 287 return; 288 this.times.push(time); 289 this.values.push(value); 290 }, 291 292 reset: function() 293 { 294 this.times = []; 295 this.values = []; 296 }, 297 298 /** 299 * @param {number} value 300 */ 301 setLimit: function(value) 302 { 303 this._limitValue = value; 304 }, 305 306 /** 307 * @return {!{min: number, max: number}} 308 */ 309 _calculateBounds: function() 310 { 311 var maxValue; 312 var minValue; 313 for (var i = this._minimumIndex; i <= this._maximumIndex; i++) { 314 var value = this.values[i]; 315 if (minValue === undefined || value < minValue) 316 minValue = value; 317 if (maxValue === undefined || value > maxValue) 318 maxValue = value; 319 } 320 minValue = minValue || 0; 321 maxValue = maxValue || 1; 322 if (this._limitValue) { 323 if (maxValue > this._limitValue * 0.5) 324 maxValue = Math.max(maxValue, this._limitValue); 325 minValue = Math.min(minValue, this._limitValue); 326 } 327 return { min: minValue, max: maxValue }; 328 }, 329 330 /** 331 * @param {!WebInspector.TimelineCalculator} calculator 332 */ 333 _calculateVisibleIndexes: function(calculator) 334 { 335 var start = calculator.minimumBoundary(); 336 var end = calculator.maximumBoundary(); 337 338 // Maximum index of element whose time <= start. 339 this._minimumIndex = Number.constrain(this.times.upperBound(start) - 1, 0, this.times.length - 1); 340 341 // Minimum index of element whose time >= end. 342 this._maximumIndex = Number.constrain(this.times.lowerBound(end), 0, this.times.length - 1); 343 344 // Current window bounds. 345 this._minTime = start; 346 this._maxTime = end; 347 }, 348 349 /** 350 * @param {number} width 351 */ 352 _calculateXValues: function(width) 353 { 354 if (!this.values.length) 355 return; 356 357 var xFactor = width / (this._maxTime - this._minTime); 358 359 this.x = new Array(this.values.length); 360 for (var i = this._minimumIndex + 1; i <= this._maximumIndex; i++) 361 this.x[i] = xFactor * (this.times[i] - this._minTime); 362 } 363 } 364 365 /** 366 * @constructor 367 * @param {!WebInspector.CountersGraph} memoryCountersPane 368 * @param {string} title 369 * @param {string} currentValueLabel 370 * @param {string} graphColor 371 * @param {!WebInspector.CountersGraph.Counter} counter 372 */ 373 WebInspector.CountersGraph.CounterUI = function(memoryCountersPane, title, currentValueLabel, graphColor, counter) 374 { 375 this._memoryCountersPane = memoryCountersPane; 376 this.counter = counter; 377 var container = memoryCountersPane.sidebarElement().createChild("div", "memory-counter-sidebar-info"); 378 var swatchColor = graphColor; 379 this._swatch = new WebInspector.SwatchCheckbox(WebInspector.UIString(title), swatchColor); 380 this._swatch.addEventListener(WebInspector.SwatchCheckbox.Events.Changed, this._toggleCounterGraph.bind(this)); 381 container.appendChild(this._swatch.element); 382 this._range = this._swatch.element.createChild("span"); 383 384 this._value = memoryCountersPane._currentValuesBar.createChild("span", "memory-counter-value"); 385 this._value.style.color = graphColor; 386 this.graphColor = graphColor; 387 this.limitColor = WebInspector.Color.parse(graphColor).setAlpha(0.3).toString(WebInspector.Color.Format.RGBA); 388 this.graphYValues = []; 389 this._verticalPadding = 10; 390 391 this._currentValueLabel = currentValueLabel; 392 this._marker = memoryCountersPane._canvasContainer.createChild("div", "memory-counter-marker"); 393 this._marker.style.backgroundColor = graphColor; 394 this._clearCurrentValueAndMarker(); 395 } 396 397 WebInspector.CountersGraph.CounterUI.prototype = { 398 reset: function() 399 { 400 this._range.textContent = ""; 401 }, 402 403 /** 404 * @param {number} minValue 405 * @param {number} maxValue 406 */ 407 setRange: function(minValue, maxValue) 408 { 409 this._range.textContent = WebInspector.UIString("[%.0f:%.0f]", minValue, maxValue); 410 }, 411 412 _toggleCounterGraph: function(event) 413 { 414 this._value.classList.toggle("hidden", !this._swatch.checked); 415 this._memoryCountersPane.refresh(); 416 }, 417 418 /** 419 * @param {number} x 420 * @return {number} 421 */ 422 _recordIndexAt: function(x) 423 { 424 return this.counter.x.upperBound(x * window.devicePixelRatio, null, this.counter._minimumIndex + 1, this.counter._maximumIndex + 1) - 1; 425 }, 426 427 /** 428 * @param {number} x 429 */ 430 updateCurrentValue: function(x) 431 { 432 if (!this.visible() || !this.counter.values.length || !this.counter.x) 433 return; 434 var index = this._recordIndexAt(x); 435 this._value.textContent = WebInspector.UIString(this._currentValueLabel, this.counter.values[index]); 436 var y = this.graphYValues[index] / window.devicePixelRatio; 437 this._marker.style.left = x + "px"; 438 this._marker.style.top = y + "px"; 439 this._marker.classList.remove("hidden"); 440 }, 441 442 _clearCurrentValueAndMarker: function() 443 { 444 this._value.textContent = ""; 445 this._marker.classList.add("hidden"); 446 }, 447 448 /** 449 * @param {!HTMLCanvasElement} canvas 450 */ 451 _drawGraph: function(canvas) 452 { 453 var ctx = canvas.getContext("2d"); 454 var width = canvas.width; 455 var height = canvas.height - 2 * this._verticalPadding; 456 if (height <= 0) { 457 this.graphYValues = []; 458 return; 459 } 460 var originY = this._verticalPadding; 461 var counter = this.counter; 462 var values = counter.values; 463 464 if (!values.length) 465 return; 466 467 var bounds = counter._calculateBounds(); 468 var minValue = bounds.min; 469 var maxValue = bounds.max; 470 this.setRange(minValue, maxValue); 471 472 if (!this.visible()) 473 return; 474 475 var yValues = this.graphYValues; 476 var maxYRange = maxValue - minValue; 477 var yFactor = maxYRange ? height / (maxYRange) : 1; 478 479 ctx.save(); 480 ctx.lineWidth = window.devicePixelRatio; 481 if (ctx.lineWidth % 2) 482 ctx.translate(0.5, 0.5); 483 ctx.beginPath(); 484 var value = values[counter._minimumIndex]; 485 var currentY = Math.round(originY + height - (value - minValue) * yFactor); 486 ctx.moveTo(0, currentY); 487 for (var i = counter._minimumIndex; i <= counter._maximumIndex; i++) { 488 var x = Math.round(counter.x[i]); 489 ctx.lineTo(x, currentY); 490 var currentValue = values[i]; 491 if (typeof currentValue !== "undefined") 492 value = currentValue; 493 currentY = Math.round(originY + height - (value - minValue) * yFactor); 494 ctx.lineTo(x, currentY); 495 yValues[i] = currentY; 496 } 497 yValues.length = i; 498 ctx.lineTo(width, currentY); 499 ctx.strokeStyle = this.graphColor; 500 ctx.stroke(); 501 if (counter._limitValue) { 502 var limitLineY = Math.round(originY + height - (counter._limitValue - minValue) * yFactor); 503 ctx.moveTo(0, limitLineY); 504 ctx.lineTo(width, limitLineY); 505 ctx.strokeStyle = this.limitColor; 506 ctx.stroke(); 507 } 508 ctx.closePath(); 509 ctx.restore(); 510 }, 511 512 /** 513 * @return {boolean} 514 */ 515 visible: function() 516 { 517 return this._swatch.checked; 518 } 519 } 520 521 522 /** 523 * @constructor 524 * @extends {WebInspector.Object} 525 */ 526 WebInspector.SwatchCheckbox = function(title, color) 527 { 528 this.element = document.createElement("div"); 529 this._swatch = this.element.createChild("div", "swatch"); 530 this.element.createChild("span", "title").textContent = title; 531 this._color = color; 532 this.checked = true; 533 534 this.element.addEventListener("click", this._toggleCheckbox.bind(this), true); 535 } 536 537 WebInspector.SwatchCheckbox.Events = { 538 Changed: "Changed" 539 } 540 541 WebInspector.SwatchCheckbox.prototype = { 542 get checked() 543 { 544 return this._checked; 545 }, 546 547 set checked(v) 548 { 549 this._checked = v; 550 if (this._checked) 551 this._swatch.style.backgroundColor = this._color; 552 else 553 this._swatch.style.backgroundColor = ""; 554 }, 555 556 _toggleCheckbox: function(event) 557 { 558 this.checked = !this.checked; 559 this.dispatchEventToListeners(WebInspector.SwatchCheckbox.Events.Changed); 560 }, 561 562 __proto__: WebInspector.Object.prototype 563 } 564