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.View} 34 * @param {WebInspector.CPUProfileView} cpuProfileView 35 */ 36 WebInspector.FlameChart = function(cpuProfileView) 37 { 38 WebInspector.View.call(this); 39 this.registerRequiredCSS("flameChart.css"); 40 this.element.className = "fill"; 41 this.element.id = "cpu-flame-chart"; 42 43 this._overviewContainer = this.element.createChild("div", "overview-container"); 44 this._overviewGrid = new WebInspector.OverviewGrid("flame-chart"); 45 this._overviewCanvas = this._overviewContainer.createChild("canvas", "flame-chart-overview-canvas"); 46 this._overviewContainer.appendChild(this._overviewGrid.element); 47 this._overviewCalculator = new WebInspector.FlameChart.OverviewCalculator(); 48 this._overviewGrid.addEventListener(WebInspector.OverviewGrid.Events.WindowChanged, this._onWindowChanged, this); 49 50 this._chartContainer = this.element.createChild("div", "chart-container"); 51 this._timelineGrid = new WebInspector.TimelineGrid(); 52 this._chartContainer.appendChild(this._timelineGrid.element); 53 this._calculator = new WebInspector.FlameChart.Calculator(); 54 55 this._canvas = this._chartContainer.createChild("canvas"); 56 this._canvas.addEventListener("mousemove", this._onMouseMove.bind(this)); 57 WebInspector.installDragHandle(this._canvas, this._startCanvasDragging.bind(this), this._canvasDragging.bind(this), this._endCanvasDragging.bind(this), "col-resize"); 58 59 this._cpuProfileView = cpuProfileView; 60 this._windowLeft = 0.0; 61 this._windowRight = 1.0; 62 this._barHeight = 15; 63 this._minWidth = 2; 64 this._paddingLeft = 15; 65 this._canvas.addEventListener("mousewheel", this._onMouseWheel.bind(this), false); 66 this.element.addEventListener("click", this._onClick.bind(this), false); 67 this._linkifier = new WebInspector.Linkifier(); 68 this._highlightedEntryIndex = -1; 69 70 if (!WebInspector.FlameChart._colorGenerator) 71 WebInspector.FlameChart._colorGenerator = new WebInspector.FlameChart.ColorGenerator(); 72 } 73 74 /** 75 * @constructor 76 * @implements {WebInspector.TimelineGrid.Calculator} 77 */ 78 WebInspector.FlameChart.Calculator = function() 79 { 80 } 81 82 WebInspector.FlameChart.Calculator.prototype = { 83 /** 84 * @param {WebInspector.FlameChart} flameChart 85 */ 86 _updateBoundaries: function(flameChart) 87 { 88 this._minimumBoundaries = flameChart._windowLeft * flameChart._timelineData.totalTime; 89 this._maximumBoundaries = flameChart._windowRight * flameChart._timelineData.totalTime; 90 this.paddingLeft = flameChart._paddingLeft; 91 this._width = flameChart._canvas.width - this.paddingLeft; 92 this._timeToPixel = this._width / this.boundarySpan(); 93 }, 94 95 /** 96 * @param {number} time 97 */ 98 computePosition: function(time) 99 { 100 return (time - this._minimumBoundaries) * this._timeToPixel + this.paddingLeft; 101 }, 102 103 formatTime: function(value) 104 { 105 return WebInspector.UIString("%s\u2009ms", Number.withThousandsSeparator(Math.round(value + this._minimumBoundaries))); 106 }, 107 108 maximumBoundary: function() 109 { 110 return this._maximumBoundaries; 111 }, 112 113 minimumBoundary: function() 114 { 115 return this._minimumBoundaries; 116 }, 117 118 zeroTime: function() 119 { 120 return 0; 121 }, 122 123 boundarySpan: function() 124 { 125 return this._maximumBoundaries - this._minimumBoundaries; 126 } 127 } 128 129 /** 130 * @constructor 131 * @implements {WebInspector.TimelineGrid.Calculator} 132 */ 133 WebInspector.FlameChart.OverviewCalculator = function() 134 { 135 } 136 137 WebInspector.FlameChart.OverviewCalculator.prototype = { 138 /** 139 * @param {WebInspector.FlameChart} flameChart 140 */ 141 _updateBoundaries: function(flameChart) 142 { 143 this._minimumBoundaries = 0; 144 this._maximumBoundaries = flameChart._timelineData.totalTime; 145 this._xScaleFactor = flameChart._canvas.width / flameChart._timelineData.totalTime; 146 }, 147 148 /** 149 * @param {number} time 150 */ 151 computePosition: function(time) 152 { 153 return (time - this._minimumBoundaries) * this._xScaleFactor; 154 }, 155 156 formatTime: function(value) 157 { 158 return Number.secondsToString((value + this._minimumBoundaries) / 1000); 159 }, 160 161 maximumBoundary: function() 162 { 163 return this._maximumBoundaries; 164 }, 165 166 minimumBoundary: function() 167 { 168 return this._minimumBoundaries; 169 }, 170 171 zeroTime: function() 172 { 173 return this._minimumBoundaries; 174 }, 175 176 boundarySpan: function() 177 { 178 return this._maximumBoundaries - this._minimumBoundaries; 179 } 180 } 181 182 WebInspector.FlameChart.Events = { 183 SelectedNode: "SelectedNode" 184 } 185 186 /** 187 * @constructor 188 */ 189 WebInspector.FlameChart.ColorGenerator = function() 190 { 191 this._colorPairs = {}; 192 this._currentColorIndex = 0; 193 this._colorPairs["(idle)::0"] = this._createPair(0, 50); 194 this._colorPairs["(program)::0"] = this._createPair(5, 50); 195 this._colorPairs["(garbage collector)::0"] = this._createPair(10, 50); 196 } 197 198 WebInspector.FlameChart.ColorGenerator.prototype = { 199 /** 200 * @param {!string} id 201 */ 202 _colorPairForID: function(id) 203 { 204 var colorPairs = this._colorPairs; 205 var colorPair = colorPairs[id]; 206 if (!colorPair) 207 colorPairs[id] = colorPair = this._createPair(++this._currentColorIndex); 208 return colorPair; 209 }, 210 211 /** 212 * @param {!number} index 213 * @param {number=} sat 214 */ 215 _createPair: function(index, sat) 216 { 217 var hue = (index * 7 + 12 * (index % 2)) % 360; 218 if (typeof sat !== "number") 219 sat = 100; 220 return {highlighted: "hsla(" + hue + ", " + sat + "%, 33%, 0.7)", normal: "hsla(" + hue + ", " + sat + "%, 66%, 0.7)"} 221 } 222 } 223 224 /** 225 * @constructor 226 * @param {!Object} colorPair 227 * @param {!number} depth 228 * @param {!number} duration 229 * @param {!number} startTime 230 * @param {Object} node 231 */ 232 WebInspector.FlameChart.Entry = function(colorPair, depth, duration, startTime, node) 233 { 234 this.colorPair = colorPair; 235 this.depth = depth; 236 this.duration = duration; 237 this.startTime = startTime; 238 this.node = node; 239 this.selfTime = 0; 240 } 241 242 WebInspector.FlameChart.prototype = { 243 /** 244 * @param {!number} timeLeft 245 * @param {!number} timeRight 246 */ 247 selectRange: function(timeLeft, timeRight) 248 { 249 this._overviewGrid.setWindow(timeLeft / this._totalTime, timeRight / this._totalTime); 250 }, 251 252 _onWindowChanged: function(event) 253 { 254 this._scheduleUpdate(); 255 }, 256 257 _startCanvasDragging: function(event) 258 { 259 if (!this._timelineData) 260 return false; 261 this._isDragging = true; 262 this._dragStartPoint = event.pageX; 263 this._dragStartWindowLeft = this._windowLeft; 264 this._dragStartWindowRight = this._windowRight; 265 return true; 266 }, 267 268 _canvasDragging: function(event) 269 { 270 var pixelShift = this._dragStartPoint - event.pageX; 271 var windowShift = pixelShift / this._totalPixels; 272 273 var windowLeft = Math.max(0, this._dragStartWindowLeft + windowShift); 274 if (windowLeft === this._windowLeft) 275 return; 276 windowShift = windowLeft - this._dragStartWindowLeft; 277 278 var windowRight = Math.min(1, this._dragStartWindowRight + windowShift); 279 if (windowRight === this._windowRight) 280 return; 281 windowShift = windowRight - this._dragStartWindowRight; 282 this._overviewGrid.setWindow(this._dragStartWindowLeft + windowShift, this._dragStartWindowRight + windowShift); 283 }, 284 285 _endCanvasDragging: function() 286 { 287 this._isDragging = false; 288 }, 289 290 _calculateTimelineData: function() 291 { 292 if (this._cpuProfileView.samples) 293 return this._calculateTimelineDataForSamples(); 294 295 if (this._timelineData) 296 return this._timelineData; 297 298 if (!this._cpuProfileView.profileHead) 299 return null; 300 301 var index = 0; 302 var entries = []; 303 304 function appendReversedArray(toArray, fromArray) 305 { 306 for (var i = fromArray.length - 1; i >= 0; --i) 307 toArray.push(fromArray[i]); 308 } 309 310 var stack = []; 311 appendReversedArray(stack, this._cpuProfileView.profileHead.children); 312 313 var levelOffsets = /** @type {Array.<!number>} */ ([0]); 314 var levelExitIndexes = /** @type {Array.<!number>} */ ([0]); 315 var colorGenerator = WebInspector.FlameChart._colorGenerator; 316 317 while (stack.length) { 318 var level = levelOffsets.length - 1; 319 var node = stack.pop(); 320 var offset = levelOffsets[level]; 321 322 var colorPair = colorGenerator._colorPairForID(node.functionName + ":" + node.url + ":" + node.lineNumber); 323 324 entries.push(new WebInspector.FlameChart.Entry(colorPair, level, node.totalTime, offset, node)); 325 326 ++index; 327 328 levelOffsets[level] += node.totalTime; 329 if (node.children.length) { 330 levelExitIndexes.push(stack.length); 331 levelOffsets.push(offset + node.selfTime / 2); 332 appendReversedArray(stack, node.children); 333 } 334 335 while (stack.length === levelExitIndexes[levelExitIndexes.length - 1]) { 336 levelOffsets.pop(); 337 levelExitIndexes.pop(); 338 } 339 } 340 341 this._timelineData = { 342 entries: entries, 343 totalTime: this._cpuProfileView.profileHead.totalTime, 344 } 345 346 return this._timelineData; 347 }, 348 349 _calculateTimelineDataForSamples: function() 350 { 351 if (this._timelineData) 352 return this._timelineData; 353 354 if (!this._cpuProfileView.profileHead) 355 return null; 356 357 var samples = this._cpuProfileView.samples; 358 var idToNode = this._cpuProfileView._idToNode; 359 var gcNode = this._cpuProfileView._gcNode; 360 var samplesCount = samples.length; 361 362 var index = 0; 363 var entries = /** @type {Array.<!WebInspector.FlameChart.Entry>} */ ([]); 364 365 var openIntervals = []; 366 var stackTrace = []; 367 var colorGenerator = WebInspector.FlameChart._colorGenerator; 368 for (var sampleIndex = 0; sampleIndex < samplesCount; sampleIndex++) { 369 var node = idToNode[samples[sampleIndex]]; 370 stackTrace.length = 0; 371 while (node) { 372 stackTrace.push(node); 373 node = node.parent; 374 } 375 stackTrace.pop(); // Remove (root) node 376 377 var depth = 0; 378 node = stackTrace.pop(); 379 var intervalIndex; 380 381 // GC samples have no stack, so we just put GC node on top of the last recoreded sample. 382 if (node === gcNode) { 383 while (depth < openIntervals.length) { 384 intervalIndex = openIntervals[depth].index; 385 entries[intervalIndex].duration += 1; 386 ++depth; 387 } 388 // If previous stack is also GC then just continue. 389 if (openIntervals.length > 0 && openIntervals.peekLast().node === node) { 390 entries[intervalIndex].selfTime += 1; 391 continue; 392 } 393 } 394 395 while (node && depth < openIntervals.length && node === openIntervals[depth].node) { 396 intervalIndex = openIntervals[depth].index; 397 entries[intervalIndex].duration += 1; 398 node = stackTrace.pop(); 399 ++depth; 400 } 401 if (depth < openIntervals.length) 402 openIntervals.length = depth; 403 if (!node) { 404 entries[intervalIndex].selfTime += 1; 405 continue; 406 } 407 408 while (node) { 409 var colorPair = colorGenerator._colorPairForID(node.functionName + ":" + node.url + ":" + node.lineNumber); 410 411 entries.push(new WebInspector.FlameChart.Entry(colorPair, depth, 1, sampleIndex, node)); 412 openIntervals.push({node: node, index: index}); 413 ++index; 414 415 node = stackTrace.pop(); 416 ++depth; 417 } 418 entries[entries.length - 1].selfTime += 1; 419 } 420 421 this._timelineData = { 422 entries: entries, 423 totalTime: samplesCount, 424 }; 425 426 return this._timelineData; 427 }, 428 429 _onMouseMove: function(event) 430 { 431 if (this._isDragging) 432 return; 433 434 var entryIndex = this._coordinatesToEntryIndex(event.offsetX, event.offsetY); 435 436 if (this._highlightedEntryIndex === entryIndex) 437 return; 438 439 if (entryIndex === -1 || this._timelineData.entries[entryIndex].node.scriptId === "0") 440 this._canvas.style.cursor = "default"; 441 else 442 this._canvas.style.cursor = "pointer"; 443 444 this._highlightedEntryIndex = entryIndex; 445 this._scheduleUpdate(); 446 }, 447 448 _prepareHighlightedEntryInfo: function() 449 { 450 if (this._isDragging) 451 return null; 452 var entry = this._timelineData.entries[this._highlightedEntryIndex]; 453 if (!entry) 454 return null; 455 var node = entry.node; 456 if (!node) 457 return null; 458 459 var entryInfo = []; 460 function pushEntryInfoRow(title, text) 461 { 462 var row = {}; 463 row.title = title; 464 row.text = text; 465 entryInfo.push(row); 466 } 467 468 pushEntryInfoRow(WebInspector.UIString("Name"), node.functionName); 469 if (this._cpuProfileView.samples) { 470 pushEntryInfoRow(WebInspector.UIString("Self time"), Number.secondsToString(entry.selfTime / 1000, true)); 471 pushEntryInfoRow(WebInspector.UIString("Total time"), Number.secondsToString(entry.duration / 1000, true)); 472 } 473 if (node.url) 474 pushEntryInfoRow(WebInspector.UIString("URL"), node.url + ":" + node.lineNumber); 475 pushEntryInfoRow(WebInspector.UIString("Aggregated self time"), Number.secondsToString(node.selfTime / 1000, true)); 476 pushEntryInfoRow(WebInspector.UIString("Aggregated total time"), Number.secondsToString(node.totalTime / 1000, true)); 477 return entryInfo; 478 }, 479 480 _onClick: function(e) 481 { 482 if (this._highlightedEntryIndex === -1) 483 return; 484 var node = this._timelineData.entries[this._highlightedEntryIndex].node; 485 this.dispatchEventToListeners(WebInspector.FlameChart.Events.SelectedNode, node); 486 }, 487 488 _onMouseWheel: function(e) 489 { 490 if (e.wheelDeltaY) { 491 const zoomFactor = 1.1; 492 const mouseWheelZoomSpeed = 1 / 120; 493 494 var zoom = Math.pow(zoomFactor, -e.wheelDeltaY * mouseWheelZoomSpeed); 495 var overviewReference = (this._pixelWindowLeft + e.offsetX - this._paddingLeft) / this._totalPixels; 496 this._overviewGrid.zoom(zoom, overviewReference); 497 } else { 498 var shift = Number.constrain(-1 * this._windowWidth / 4 * e.wheelDeltaX / 120, -this._windowLeft, 1 - this._windowRight); 499 this._overviewGrid.setWindow(this._windowLeft + shift, this._windowRight + shift); 500 } 501 }, 502 503 /** 504 * @param {!number} x 505 * @param {!number} y 506 */ 507 _coordinatesToEntryIndex: function(x, y) 508 { 509 var timelineData = this._timelineData; 510 if (!timelineData) 511 return -1; 512 var timelineEntries = timelineData.entries; 513 var cursorTime = (x + this._pixelWindowLeft - this._paddingLeft) * this._pixelToTime; 514 var cursorLevel = Math.floor((this._canvas.height / window.devicePixelRatio - y) / this._barHeight); 515 516 for (var i = 0; i < timelineEntries.length; ++i) { 517 if (cursorTime < timelineEntries[i].startTime) 518 return -1; 519 if (cursorTime < (timelineEntries[i].startTime + timelineEntries[i].duration) 520 && cursorLevel === timelineEntries[i].depth) 521 return i; 522 } 523 return -1; 524 }, 525 526 onResize: function() 527 { 528 this._updateOverviewCanvas = true; 529 this._scheduleUpdate(); 530 }, 531 532 _drawOverviewCanvas: function(width, height) 533 { 534 if (!this._timelineData) 535 return; 536 537 var timelineEntries = this._timelineData.entries; 538 539 var drawData = new Uint8Array(width); 540 var scaleFactor = width / this._totalTime; 541 var maxStackDepth = 5; // minimum stack depth for the case when we see no activity. 542 543 for (var entryIndex = 0; entryIndex < timelineEntries.length; ++entryIndex) { 544 var entry = timelineEntries[entryIndex]; 545 var start = Math.floor(entry.startTime * scaleFactor); 546 var finish = Math.floor((entry.startTime + entry.duration) * scaleFactor); 547 for (var x = start; x < finish; ++x) { 548 drawData[x] = Math.max(drawData[x], entry.depth + 1); 549 maxStackDepth = Math.max(maxStackDepth, entry.depth + 1); 550 } 551 } 552 553 var ratio = window.devicePixelRatio; 554 var canvasWidth = width * ratio; 555 var canvasHeight = height * ratio; 556 this._overviewCanvas.width = canvasWidth; 557 this._overviewCanvas.height = canvasHeight; 558 this._overviewCanvas.style.width = width + "px"; 559 this._overviewCanvas.style.height = height + "px"; 560 561 var context = this._overviewCanvas.getContext("2d"); 562 563 var yScaleFactor = canvasHeight / (maxStackDepth * 1.1); 564 context.lineWidth = 1; 565 context.translate(0.5, 0.5); 566 context.strokeStyle = "rgba(20,0,0,0.4)"; 567 context.fillStyle = "rgba(214,225,254,0.8)"; 568 context.moveTo(-1, canvasHeight - 1); 569 if (drawData) 570 context.lineTo(-1, Math.round(height - drawData[0] * yScaleFactor - 1)); 571 var value; 572 for (var x = 0; x < width; ++x) { 573 value = Math.round(canvasHeight - drawData[x] * yScaleFactor - 1); 574 context.lineTo(x * ratio, value); 575 } 576 context.lineTo(canvasWidth + 1, value); 577 context.lineTo(canvasWidth + 1, canvasHeight - 1); 578 context.fill(); 579 context.stroke(); 580 context.closePath(); 581 }, 582 583 /** 584 * @param {WebInspector.FlameChart.Entry} entry 585 * @param {AnchorBox} anchorBox 586 */ 587 _entryToAnchorBox: function(entry, anchorBox) 588 { 589 anchorBox.x = Math.floor(entry.startTime * this._timeToPixel) - this._pixelWindowLeft + this._paddingLeft; 590 anchorBox.y = this._canvas.height / window.devicePixelRatio - (entry.depth + 1) * this._barHeight; 591 anchorBox.width = Math.max(Math.ceil(entry.duration * this._timeToPixel), this._minWidth); 592 anchorBox.height = this._barHeight; 593 if (anchorBox.x < 0) { 594 anchorBox.width += anchorBox.x; 595 anchorBox.x = 0; 596 } 597 anchorBox.width = Number.constrain(anchorBox.width, 0, this._canvas.width - anchorBox.x); 598 }, 599 600 /** 601 * @param {!number} height 602 * @param {!number} width 603 */ 604 draw: function(width, height) 605 { 606 var timelineData = this._calculateTimelineData(); 607 if (!timelineData) 608 return; 609 var timelineEntries = timelineData.entries; 610 611 var ratio = window.devicePixelRatio; 612 var canvasWidth = width * ratio; 613 var canvasHeight = height * ratio; 614 this._canvas.width = canvasWidth; 615 this._canvas.height = canvasHeight; 616 this._canvas.style.width = width + "px"; 617 this._canvas.style.height = height + "px"; 618 619 var barHeight = this._barHeight; 620 621 var context = this._canvas.getContext("2d"); 622 var textPaddingLeft = 2; 623 context.scale(ratio, ratio); 624 context.font = (barHeight - 4) + "px " + window.getComputedStyle(this.element, null).getPropertyValue("font-family"); 625 context.textBaseline = "alphabetic"; 626 this._dotsWidth = context.measureText("\u2026").width; 627 var visibleTimeLeft = this._timeWindowLeft - this._paddingLeftTime; 628 629 var anchorBox = new AnchorBox(); 630 for (var i = 0; i < timelineEntries.length; ++i) { 631 var entry = timelineEntries[i]; 632 var startTime = entry.startTime; 633 if (startTime > this._timeWindowRight) 634 break; 635 if ((startTime + entry.duration) < visibleTimeLeft) 636 continue; 637 this._entryToAnchorBox(entry, anchorBox); 638 639 var colorPair = entry.colorPair; 640 var color; 641 if (this._highlightedEntryIndex === i) 642 color = colorPair.highlighted; 643 else 644 color = colorPair.normal; 645 646 context.beginPath(); 647 context.rect(anchorBox.x, anchorBox.y, anchorBox.width - 1, anchorBox.height - 1); 648 context.fillStyle = color; 649 context.fill(); 650 651 var xText = Math.max(0, anchorBox.x); 652 var widthText = anchorBox.width - textPaddingLeft + anchorBox.x - xText; 653 var title = this._prepareText(context, entry.node.functionName, widthText); 654 if (title) { 655 context.fillStyle = "#333"; 656 context.fillText(title, xText + textPaddingLeft, anchorBox.y + barHeight - 4); 657 } 658 } 659 660 var entryInfo = this._prepareHighlightedEntryInfo(); 661 if (entryInfo) 662 this._printEntryInfo(context, entryInfo, 0, 25, width); 663 }, 664 665 _printEntryInfo: function(context, entryInfo, x, y, width) 666 { 667 const lineHeight = 18; 668 const paddingLeft = 10; 669 const paddingTop = 5; 670 var maxTitleWidth = 0; 671 var basicFont = "100% " + window.getComputedStyle(this.element, null).getPropertyValue("font-family"); 672 context.font = "bold " + basicFont; 673 context.textBaseline = "top"; 674 for (var i = 0; i < entryInfo.length; ++i) 675 maxTitleWidth = Math.max(maxTitleWidth, context.measureText(entryInfo[i].title).width); 676 677 var maxTextWidth = 0; 678 for (var i = 0; i < entryInfo.length; ++i) 679 maxTextWidth = Math.max(maxTextWidth, context.measureText(entryInfo[i].text).width); 680 maxTextWidth = Math.min(maxTextWidth, width - 2 * paddingLeft - maxTitleWidth); 681 682 context.beginPath(); 683 context.rect(x, y, maxTitleWidth + maxTextWidth + 5, lineHeight * entryInfo.length + 5); 684 context.strokeStyle = "rgba(0,0,0,0)"; 685 context.fillStyle = "rgba(254,254,254,0.8)"; 686 context.fill(); 687 context.stroke(); 688 689 context.fillStyle = "#333"; 690 for (var i = 0; i < entryInfo.length; ++i) 691 context.fillText(entryInfo[i].title, x + paddingLeft, y + lineHeight * i); 692 693 context.font = basicFont; 694 for (var i = 0; i < entryInfo.length; ++i) { 695 var text = this._prepareText(context, entryInfo[i].text, maxTextWidth); 696 context.fillText(text, x + paddingLeft + maxTitleWidth + paddingLeft, y + lineHeight * i); 697 } 698 }, 699 700 _prepareText: function(context, title, maxSize) 701 { 702 if (maxSize < this._dotsWidth) 703 return null; 704 var titleWidth = context.measureText(title).width; 705 if (maxSize > titleWidth) 706 return title; 707 maxSize -= this._dotsWidth; 708 var dotRegExp=/[\.\$]/g; 709 var match = dotRegExp.exec(title); 710 if (!match) { 711 var visiblePartSize = maxSize / titleWidth; 712 var newTextLength = Math.floor(title.length * visiblePartSize) + 1; 713 var minTextLength = 4; 714 if (newTextLength < minTextLength) 715 return null; 716 var substring; 717 do { 718 --newTextLength; 719 substring = title.substring(0, newTextLength); 720 } while (context.measureText(substring).width > maxSize); 721 return title.substring(0, newTextLength) + "\u2026"; 722 } 723 while (match) { 724 var substring = title.substring(match.index + 1); 725 var width = context.measureText(substring).width; 726 if (maxSize > width) 727 return "\u2026" + substring; 728 match = dotRegExp.exec(title); 729 } 730 var i = 0; 731 do { 732 ++i; 733 } while (context.measureText(title.substring(0, i)).width < maxSize); 734 return title.substring(0, i - 1) + "\u2026"; 735 }, 736 737 _scheduleUpdate: function() 738 { 739 if (this._updateTimerId) 740 return; 741 this._updateTimerId = setTimeout(this.update.bind(this), 10); 742 }, 743 744 _updateBoundaries: function() 745 { 746 this._windowLeft = this._overviewGrid.windowLeft(); 747 this._windowRight = this._overviewGrid.windowRight(); 748 this._windowWidth = this._windowRight - this._windowLeft; 749 750 this._totalTime = this._timelineData.totalTime; 751 this._timeWindowLeft = this._windowLeft * this._totalTime; 752 this._timeWindowRight = this._windowRight * this._totalTime; 753 754 this._pixelWindowWidth = this._chartContainer.clientWidth; 755 this._totalPixels = Math.floor(this._pixelWindowWidth / this._windowWidth); 756 this._pixelWindowLeft = Math.floor(this._totalPixels * this._windowLeft); 757 this._pixelWindowRight = Math.floor(this._totalPixels * this._windowRight); 758 759 this._timeToPixel = this._totalPixels / this._totalTime; 760 this._pixelToTime = this._totalTime / this._totalPixels; 761 this._paddingLeftTime = this._paddingLeft / this._timeToPixel; 762 }, 763 764 update: function() 765 { 766 this._updateTimerId = 0; 767 if (!this._timelineData) 768 this._calculateTimelineData(); 769 if (!this._timelineData) 770 return; 771 this._updateBoundaries(); 772 this.draw(this._chartContainer.clientWidth, this._chartContainer.clientHeight); 773 this._calculator._updateBoundaries(this); 774 this._overviewCalculator._updateBoundaries(this); 775 this._timelineGrid.element.style.width = this.element.clientWidth; 776 this._timelineGrid.updateDividers(this._calculator); 777 this._overviewGrid.updateDividers(this._overviewCalculator); 778 if (this._updateOverviewCanvas) { 779 this._drawOverviewCanvas(this._overviewContainer.clientWidth, this._overviewContainer.clientHeight - 20); 780 this._updateOverviewCanvas = false; 781 } 782 }, 783 784 __proto__: WebInspector.View.prototype 785 }; 786