| /* |
| * Copyright (C) 2013 Google Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions are |
| * met: |
| * |
| * * Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * * Redistributions in binary form must reproduce the above |
| * copyright notice, this list of conditions and the following disclaimer |
| * in the documentation and/or other materials provided with the |
| * distribution. |
| * * Neither the name of Google Inc. nor the names of its |
| * contributors may be used to endorse or promote products derived from |
| * this software without specific prior written permission. |
| * |
| * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
| * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
| * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| /** |
| * @constructor |
| * @extends {WebInspector.View} |
| * @param {WebInspector.CPUProfileView} cpuProfileView |
| */ |
| WebInspector.FlameChart = function(cpuProfileView) |
| { |
| WebInspector.View.call(this); |
| this.registerRequiredCSS("flameChart.css"); |
| this.element.className = "fill"; |
| this.element.id = "cpu-flame-chart"; |
| |
| this._overviewContainer = this.element.createChild("div", "overview-container"); |
| this._overviewGrid = new WebInspector.OverviewGrid("flame-chart"); |
| this._overviewCanvas = this._overviewContainer.createChild("canvas", "flame-chart-overview-canvas"); |
| this._overviewContainer.appendChild(this._overviewGrid.element); |
| this._overviewCalculator = new WebInspector.FlameChart.OverviewCalculator(); |
| this._overviewGrid.addEventListener(WebInspector.OverviewGrid.Events.WindowChanged, this._onWindowChanged, this); |
| |
| this._chartContainer = this.element.createChild("div", "chart-container"); |
| this._timelineGrid = new WebInspector.TimelineGrid(); |
| this._chartContainer.appendChild(this._timelineGrid.element); |
| this._calculator = new WebInspector.FlameChart.Calculator(); |
| |
| this._canvas = this._chartContainer.createChild("canvas"); |
| this._canvas.addEventListener("mousemove", this._onMouseMove.bind(this)); |
| WebInspector.installDragHandle(this._canvas, this._startCanvasDragging.bind(this), this._canvasDragging.bind(this), this._endCanvasDragging.bind(this), "col-resize"); |
| |
| this._cpuProfileView = cpuProfileView; |
| this._windowLeft = 0.0; |
| this._windowRight = 1.0; |
| this._barHeight = 15; |
| this._minWidth = 2; |
| this._paddingLeft = 15; |
| this._canvas.addEventListener("mousewheel", this._onMouseWheel.bind(this), false); |
| this.element.addEventListener("click", this._onClick.bind(this), false); |
| this._linkifier = new WebInspector.Linkifier(); |
| this._highlightedEntryIndex = -1; |
| |
| if (!WebInspector.FlameChart._colorGenerator) |
| WebInspector.FlameChart._colorGenerator = new WebInspector.FlameChart.ColorGenerator(); |
| } |
| |
| /** |
| * @constructor |
| * @implements {WebInspector.TimelineGrid.Calculator} |
| */ |
| WebInspector.FlameChart.Calculator = function() |
| { |
| } |
| |
| WebInspector.FlameChart.Calculator.prototype = { |
| /** |
| * @param {WebInspector.FlameChart} flameChart |
| */ |
| _updateBoundaries: function(flameChart) |
| { |
| this._minimumBoundaries = flameChart._windowLeft * flameChart._timelineData.totalTime; |
| this._maximumBoundaries = flameChart._windowRight * flameChart._timelineData.totalTime; |
| this.paddingLeft = flameChart._paddingLeft; |
| this._width = flameChart._canvas.width - this.paddingLeft; |
| this._timeToPixel = this._width / this.boundarySpan(); |
| }, |
| |
| /** |
| * @param {number} time |
| */ |
| computePosition: function(time) |
| { |
| return (time - this._minimumBoundaries) * this._timeToPixel + this.paddingLeft; |
| }, |
| |
| formatTime: function(value) |
| { |
| return WebInspector.UIString("%s\u2009ms", Number.withThousandsSeparator(Math.round(value + this._minimumBoundaries))); |
| }, |
| |
| maximumBoundary: function() |
| { |
| return this._maximumBoundaries; |
| }, |
| |
| minimumBoundary: function() |
| { |
| return this._minimumBoundaries; |
| }, |
| |
| zeroTime: function() |
| { |
| return 0; |
| }, |
| |
| boundarySpan: function() |
| { |
| return this._maximumBoundaries - this._minimumBoundaries; |
| } |
| } |
| |
| /** |
| * @constructor |
| * @implements {WebInspector.TimelineGrid.Calculator} |
| */ |
| WebInspector.FlameChart.OverviewCalculator = function() |
| { |
| } |
| |
| WebInspector.FlameChart.OverviewCalculator.prototype = { |
| /** |
| * @param {WebInspector.FlameChart} flameChart |
| */ |
| _updateBoundaries: function(flameChart) |
| { |
| this._minimumBoundaries = 0; |
| this._maximumBoundaries = flameChart._timelineData.totalTime; |
| this._xScaleFactor = flameChart._canvas.width / flameChart._timelineData.totalTime; |
| }, |
| |
| /** |
| * @param {number} time |
| */ |
| computePosition: function(time) |
| { |
| return (time - this._minimumBoundaries) * this._xScaleFactor; |
| }, |
| |
| formatTime: function(value) |
| { |
| return Number.secondsToString((value + this._minimumBoundaries) / 1000); |
| }, |
| |
| maximumBoundary: function() |
| { |
| return this._maximumBoundaries; |
| }, |
| |
| minimumBoundary: function() |
| { |
| return this._minimumBoundaries; |
| }, |
| |
| zeroTime: function() |
| { |
| return this._minimumBoundaries; |
| }, |
| |
| boundarySpan: function() |
| { |
| return this._maximumBoundaries - this._minimumBoundaries; |
| } |
| } |
| |
| WebInspector.FlameChart.Events = { |
| SelectedNode: "SelectedNode" |
| } |
| |
| /** |
| * @constructor |
| */ |
| WebInspector.FlameChart.ColorGenerator = function() |
| { |
| this._colorPairs = {}; |
| this._currentColorIndex = 0; |
| this._colorPairs["(idle)::0"] = this._createPair(0, 50); |
| this._colorPairs["(program)::0"] = this._createPair(5, 50); |
| this._colorPairs["(garbage collector)::0"] = this._createPair(10, 50); |
| } |
| |
| WebInspector.FlameChart.ColorGenerator.prototype = { |
| /** |
| * @param {!string} id |
| */ |
| _colorPairForID: function(id) |
| { |
| var colorPairs = this._colorPairs; |
| var colorPair = colorPairs[id]; |
| if (!colorPair) |
| colorPairs[id] = colorPair = this._createPair(++this._currentColorIndex); |
| return colorPair; |
| }, |
| |
| /** |
| * @param {!number} index |
| * @param {number=} sat |
| */ |
| _createPair: function(index, sat) |
| { |
| var hue = (index * 7 + 12 * (index % 2)) % 360; |
| if (typeof sat !== "number") |
| sat = 100; |
| return {highlighted: "hsla(" + hue + ", " + sat + "%, 33%, 0.7)", normal: "hsla(" + hue + ", " + sat + "%, 66%, 0.7)"} |
| } |
| } |
| |
| /** |
| * @constructor |
| * @param {!Object} colorPair |
| * @param {!number} depth |
| * @param {!number} duration |
| * @param {!number} startTime |
| * @param {Object} node |
| */ |
| WebInspector.FlameChart.Entry = function(colorPair, depth, duration, startTime, node) |
| { |
| this.colorPair = colorPair; |
| this.depth = depth; |
| this.duration = duration; |
| this.startTime = startTime; |
| this.node = node; |
| this.selfTime = 0; |
| } |
| |
| WebInspector.FlameChart.prototype = { |
| /** |
| * @param {!number} timeLeft |
| * @param {!number} timeRight |
| */ |
| selectRange: function(timeLeft, timeRight) |
| { |
| this._overviewGrid.setWindow(timeLeft / this._totalTime, timeRight / this._totalTime); |
| }, |
| |
| _onWindowChanged: function(event) |
| { |
| this._scheduleUpdate(); |
| }, |
| |
| _startCanvasDragging: function(event) |
| { |
| if (!this._timelineData) |
| return false; |
| this._isDragging = true; |
| this._dragStartPoint = event.pageX; |
| this._dragStartWindowLeft = this._windowLeft; |
| this._dragStartWindowRight = this._windowRight; |
| return true; |
| }, |
| |
| _canvasDragging: function(event) |
| { |
| var pixelShift = this._dragStartPoint - event.pageX; |
| var windowShift = pixelShift / this._totalPixels; |
| |
| var windowLeft = Math.max(0, this._dragStartWindowLeft + windowShift); |
| if (windowLeft === this._windowLeft) |
| return; |
| windowShift = windowLeft - this._dragStartWindowLeft; |
| |
| var windowRight = Math.min(1, this._dragStartWindowRight + windowShift); |
| if (windowRight === this._windowRight) |
| return; |
| windowShift = windowRight - this._dragStartWindowRight; |
| this._overviewGrid.setWindow(this._dragStartWindowLeft + windowShift, this._dragStartWindowRight + windowShift); |
| }, |
| |
| _endCanvasDragging: function() |
| { |
| this._isDragging = false; |
| }, |
| |
| _calculateTimelineData: function() |
| { |
| if (this._cpuProfileView.samples) |
| return this._calculateTimelineDataForSamples(); |
| |
| if (this._timelineData) |
| return this._timelineData; |
| |
| if (!this._cpuProfileView.profileHead) |
| return null; |
| |
| var index = 0; |
| var entries = []; |
| |
| function appendReversedArray(toArray, fromArray) |
| { |
| for (var i = fromArray.length - 1; i >= 0; --i) |
| toArray.push(fromArray[i]); |
| } |
| |
| var stack = []; |
| appendReversedArray(stack, this._cpuProfileView.profileHead.children); |
| |
| var levelOffsets = /** @type {Array.<!number>} */ ([0]); |
| var levelExitIndexes = /** @type {Array.<!number>} */ ([0]); |
| var colorGenerator = WebInspector.FlameChart._colorGenerator; |
| |
| while (stack.length) { |
| var level = levelOffsets.length - 1; |
| var node = stack.pop(); |
| var offset = levelOffsets[level]; |
| |
| var colorPair = colorGenerator._colorPairForID(node.functionName + ":" + node.url + ":" + node.lineNumber); |
| |
| entries.push(new WebInspector.FlameChart.Entry(colorPair, level, node.totalTime, offset, node)); |
| |
| ++index; |
| |
| levelOffsets[level] += node.totalTime; |
| if (node.children.length) { |
| levelExitIndexes.push(stack.length); |
| levelOffsets.push(offset + node.selfTime / 2); |
| appendReversedArray(stack, node.children); |
| } |
| |
| while (stack.length === levelExitIndexes[levelExitIndexes.length - 1]) { |
| levelOffsets.pop(); |
| levelExitIndexes.pop(); |
| } |
| } |
| |
| this._timelineData = { |
| entries: entries, |
| totalTime: this._cpuProfileView.profileHead.totalTime, |
| } |
| |
| return this._timelineData; |
| }, |
| |
| _calculateTimelineDataForSamples: function() |
| { |
| if (this._timelineData) |
| return this._timelineData; |
| |
| if (!this._cpuProfileView.profileHead) |
| return null; |
| |
| var samples = this._cpuProfileView.samples; |
| var idToNode = this._cpuProfileView._idToNode; |
| var gcNode = this._cpuProfileView._gcNode; |
| var samplesCount = samples.length; |
| |
| var index = 0; |
| var entries = /** @type {Array.<!WebInspector.FlameChart.Entry>} */ ([]); |
| |
| var openIntervals = []; |
| var stackTrace = []; |
| var colorGenerator = WebInspector.FlameChart._colorGenerator; |
| for (var sampleIndex = 0; sampleIndex < samplesCount; sampleIndex++) { |
| var node = idToNode[samples[sampleIndex]]; |
| stackTrace.length = 0; |
| while (node) { |
| stackTrace.push(node); |
| node = node.parent; |
| } |
| stackTrace.pop(); // Remove (root) node |
| |
| var depth = 0; |
| node = stackTrace.pop(); |
| var intervalIndex; |
| |
| // GC samples have no stack, so we just put GC node on top of the last recoreded sample. |
| if (node === gcNode) { |
| while (depth < openIntervals.length) { |
| intervalIndex = openIntervals[depth].index; |
| entries[intervalIndex].duration += 1; |
| ++depth; |
| } |
| // If previous stack is also GC then just continue. |
| if (openIntervals.length > 0 && openIntervals.peekLast().node === node) { |
| entries[intervalIndex].selfTime += 1; |
| continue; |
| } |
| } |
| |
| while (node && depth < openIntervals.length && node === openIntervals[depth].node) { |
| intervalIndex = openIntervals[depth].index; |
| entries[intervalIndex].duration += 1; |
| node = stackTrace.pop(); |
| ++depth; |
| } |
| if (depth < openIntervals.length) |
| openIntervals.length = depth; |
| if (!node) { |
| entries[intervalIndex].selfTime += 1; |
| continue; |
| } |
| |
| while (node) { |
| var colorPair = colorGenerator._colorPairForID(node.functionName + ":" + node.url + ":" + node.lineNumber); |
| |
| entries.push(new WebInspector.FlameChart.Entry(colorPair, depth, 1, sampleIndex, node)); |
| openIntervals.push({node: node, index: index}); |
| ++index; |
| |
| node = stackTrace.pop(); |
| ++depth; |
| } |
| entries[entries.length - 1].selfTime += 1; |
| } |
| |
| this._timelineData = { |
| entries: entries, |
| totalTime: samplesCount, |
| }; |
| |
| return this._timelineData; |
| }, |
| |
| _onMouseMove: function(event) |
| { |
| if (this._isDragging) |
| return; |
| |
| var entryIndex = this._coordinatesToEntryIndex(event.offsetX, event.offsetY); |
| |
| if (this._highlightedEntryIndex === entryIndex) |
| return; |
| |
| if (entryIndex === -1 || this._timelineData.entries[entryIndex].node.scriptId === "0") |
| this._canvas.style.cursor = "default"; |
| else |
| this._canvas.style.cursor = "pointer"; |
| |
| this._highlightedEntryIndex = entryIndex; |
| this._scheduleUpdate(); |
| }, |
| |
| _prepareHighlightedEntryInfo: function() |
| { |
| if (this._isDragging) |
| return null; |
| var entry = this._timelineData.entries[this._highlightedEntryIndex]; |
| if (!entry) |
| return null; |
| var node = entry.node; |
| if (!node) |
| return null; |
| |
| var entryInfo = []; |
| function pushEntryInfoRow(title, text) |
| { |
| var row = {}; |
| row.title = title; |
| row.text = text; |
| entryInfo.push(row); |
| } |
| |
| pushEntryInfoRow(WebInspector.UIString("Name"), node.functionName); |
| if (this._cpuProfileView.samples) { |
| pushEntryInfoRow(WebInspector.UIString("Self time"), Number.secondsToString(entry.selfTime / 1000, true)); |
| pushEntryInfoRow(WebInspector.UIString("Total time"), Number.secondsToString(entry.duration / 1000, true)); |
| } |
| if (node.url) |
| pushEntryInfoRow(WebInspector.UIString("URL"), node.url + ":" + node.lineNumber); |
| pushEntryInfoRow(WebInspector.UIString("Aggregated self time"), Number.secondsToString(node.selfTime / 1000, true)); |
| pushEntryInfoRow(WebInspector.UIString("Aggregated total time"), Number.secondsToString(node.totalTime / 1000, true)); |
| return entryInfo; |
| }, |
| |
| _onClick: function(e) |
| { |
| if (this._highlightedEntryIndex === -1) |
| return; |
| var node = this._timelineData.entries[this._highlightedEntryIndex].node; |
| this.dispatchEventToListeners(WebInspector.FlameChart.Events.SelectedNode, node); |
| }, |
| |
| _onMouseWheel: function(e) |
| { |
| if (e.wheelDeltaY) { |
| const zoomFactor = 1.1; |
| const mouseWheelZoomSpeed = 1 / 120; |
| |
| var zoom = Math.pow(zoomFactor, -e.wheelDeltaY * mouseWheelZoomSpeed); |
| var overviewReference = (this._pixelWindowLeft + e.offsetX - this._paddingLeft) / this._totalPixels; |
| this._overviewGrid.zoom(zoom, overviewReference); |
| } else { |
| var shift = Number.constrain(-1 * this._windowWidth / 4 * e.wheelDeltaX / 120, -this._windowLeft, 1 - this._windowRight); |
| this._overviewGrid.setWindow(this._windowLeft + shift, this._windowRight + shift); |
| } |
| }, |
| |
| /** |
| * @param {!number} x |
| * @param {!number} y |
| */ |
| _coordinatesToEntryIndex: function(x, y) |
| { |
| var timelineData = this._timelineData; |
| if (!timelineData) |
| return -1; |
| var timelineEntries = timelineData.entries; |
| var cursorTime = (x + this._pixelWindowLeft - this._paddingLeft) * this._pixelToTime; |
| var cursorLevel = Math.floor((this._canvas.height / window.devicePixelRatio - y) / this._barHeight); |
| |
| for (var i = 0; i < timelineEntries.length; ++i) { |
| if (cursorTime < timelineEntries[i].startTime) |
| return -1; |
| if (cursorTime < (timelineEntries[i].startTime + timelineEntries[i].duration) |
| && cursorLevel === timelineEntries[i].depth) |
| return i; |
| } |
| return -1; |
| }, |
| |
| onResize: function() |
| { |
| this._updateOverviewCanvas = true; |
| this._scheduleUpdate(); |
| }, |
| |
| _drawOverviewCanvas: function(width, height) |
| { |
| if (!this._timelineData) |
| return; |
| |
| var timelineEntries = this._timelineData.entries; |
| |
| var drawData = new Uint8Array(width); |
| var scaleFactor = width / this._totalTime; |
| var maxStackDepth = 5; // minimum stack depth for the case when we see no activity. |
| |
| for (var entryIndex = 0; entryIndex < timelineEntries.length; ++entryIndex) { |
| var entry = timelineEntries[entryIndex]; |
| var start = Math.floor(entry.startTime * scaleFactor); |
| var finish = Math.floor((entry.startTime + entry.duration) * scaleFactor); |
| for (var x = start; x < finish; ++x) { |
| drawData[x] = Math.max(drawData[x], entry.depth + 1); |
| maxStackDepth = Math.max(maxStackDepth, entry.depth + 1); |
| } |
| } |
| |
| var ratio = window.devicePixelRatio; |
| var canvasWidth = width * ratio; |
| var canvasHeight = height * ratio; |
| this._overviewCanvas.width = canvasWidth; |
| this._overviewCanvas.height = canvasHeight; |
| this._overviewCanvas.style.width = width + "px"; |
| this._overviewCanvas.style.height = height + "px"; |
| |
| var context = this._overviewCanvas.getContext("2d"); |
| |
| var yScaleFactor = canvasHeight / (maxStackDepth * 1.1); |
| context.lineWidth = 1; |
| context.translate(0.5, 0.5); |
| context.strokeStyle = "rgba(20,0,0,0.4)"; |
| context.fillStyle = "rgba(214,225,254,0.8)"; |
| context.moveTo(-1, canvasHeight - 1); |
| if (drawData) |
| context.lineTo(-1, Math.round(height - drawData[0] * yScaleFactor - 1)); |
| var value; |
| for (var x = 0; x < width; ++x) { |
| value = Math.round(canvasHeight - drawData[x] * yScaleFactor - 1); |
| context.lineTo(x * ratio, value); |
| } |
| context.lineTo(canvasWidth + 1, value); |
| context.lineTo(canvasWidth + 1, canvasHeight - 1); |
| context.fill(); |
| context.stroke(); |
| context.closePath(); |
| }, |
| |
| /** |
| * @param {WebInspector.FlameChart.Entry} entry |
| * @param {AnchorBox} anchorBox |
| */ |
| _entryToAnchorBox: function(entry, anchorBox) |
| { |
| anchorBox.x = Math.floor(entry.startTime * this._timeToPixel) - this._pixelWindowLeft + this._paddingLeft; |
| anchorBox.y = this._canvas.height / window.devicePixelRatio - (entry.depth + 1) * this._barHeight; |
| anchorBox.width = Math.max(Math.ceil(entry.duration * this._timeToPixel), this._minWidth); |
| anchorBox.height = this._barHeight; |
| if (anchorBox.x < 0) { |
| anchorBox.width += anchorBox.x; |
| anchorBox.x = 0; |
| } |
| anchorBox.width = Number.constrain(anchorBox.width, 0, this._canvas.width - anchorBox.x); |
| }, |
| |
| /** |
| * @param {!number} height |
| * @param {!number} width |
| */ |
| draw: function(width, height) |
| { |
| var timelineData = this._calculateTimelineData(); |
| if (!timelineData) |
| return; |
| var timelineEntries = timelineData.entries; |
| |
| var ratio = window.devicePixelRatio; |
| var canvasWidth = width * ratio; |
| var canvasHeight = height * ratio; |
| this._canvas.width = canvasWidth; |
| this._canvas.height = canvasHeight; |
| this._canvas.style.width = width + "px"; |
| this._canvas.style.height = height + "px"; |
| |
| var barHeight = this._barHeight; |
| |
| var context = this._canvas.getContext("2d"); |
| var textPaddingLeft = 2; |
| context.scale(ratio, ratio); |
| context.font = (barHeight - 4) + "px " + window.getComputedStyle(this.element, null).getPropertyValue("font-family"); |
| context.textBaseline = "alphabetic"; |
| this._dotsWidth = context.measureText("\u2026").width; |
| var visibleTimeLeft = this._timeWindowLeft - this._paddingLeftTime; |
| |
| var anchorBox = new AnchorBox(); |
| for (var i = 0; i < timelineEntries.length; ++i) { |
| var entry = timelineEntries[i]; |
| var startTime = entry.startTime; |
| if (startTime > this._timeWindowRight) |
| break; |
| if ((startTime + entry.duration) < visibleTimeLeft) |
| continue; |
| this._entryToAnchorBox(entry, anchorBox); |
| |
| var colorPair = entry.colorPair; |
| var color; |
| if (this._highlightedEntryIndex === i) |
| color = colorPair.highlighted; |
| else |
| color = colorPair.normal; |
| |
| context.beginPath(); |
| context.rect(anchorBox.x, anchorBox.y, anchorBox.width - 1, anchorBox.height - 1); |
| context.fillStyle = color; |
| context.fill(); |
| |
| var xText = Math.max(0, anchorBox.x); |
| var widthText = anchorBox.width - textPaddingLeft + anchorBox.x - xText; |
| var title = this._prepareText(context, entry.node.functionName, widthText); |
| if (title) { |
| context.fillStyle = "#333"; |
| context.fillText(title, xText + textPaddingLeft, anchorBox.y + barHeight - 4); |
| } |
| } |
| |
| var entryInfo = this._prepareHighlightedEntryInfo(); |
| if (entryInfo) |
| this._printEntryInfo(context, entryInfo, 0, 25, width); |
| }, |
| |
| _printEntryInfo: function(context, entryInfo, x, y, width) |
| { |
| const lineHeight = 18; |
| const paddingLeft = 10; |
| const paddingTop = 5; |
| var maxTitleWidth = 0; |
| var basicFont = "100% " + window.getComputedStyle(this.element, null).getPropertyValue("font-family"); |
| context.font = "bold " + basicFont; |
| context.textBaseline = "top"; |
| for (var i = 0; i < entryInfo.length; ++i) |
| maxTitleWidth = Math.max(maxTitleWidth, context.measureText(entryInfo[i].title).width); |
| |
| var maxTextWidth = 0; |
| for (var i = 0; i < entryInfo.length; ++i) |
| maxTextWidth = Math.max(maxTextWidth, context.measureText(entryInfo[i].text).width); |
| maxTextWidth = Math.min(maxTextWidth, width - 2 * paddingLeft - maxTitleWidth); |
| |
| context.beginPath(); |
| context.rect(x, y, maxTitleWidth + maxTextWidth + 5, lineHeight * entryInfo.length + 5); |
| context.strokeStyle = "rgba(0,0,0,0)"; |
| context.fillStyle = "rgba(254,254,254,0.8)"; |
| context.fill(); |
| context.stroke(); |
| |
| context.fillStyle = "#333"; |
| for (var i = 0; i < entryInfo.length; ++i) |
| context.fillText(entryInfo[i].title, x + paddingLeft, y + lineHeight * i); |
| |
| context.font = basicFont; |
| for (var i = 0; i < entryInfo.length; ++i) { |
| var text = this._prepareText(context, entryInfo[i].text, maxTextWidth); |
| context.fillText(text, x + paddingLeft + maxTitleWidth + paddingLeft, y + lineHeight * i); |
| } |
| }, |
| |
| _prepareText: function(context, title, maxSize) |
| { |
| if (maxSize < this._dotsWidth) |
| return null; |
| var titleWidth = context.measureText(title).width; |
| if (maxSize > titleWidth) |
| return title; |
| maxSize -= this._dotsWidth; |
| var dotRegExp=/[\.\$]/g; |
| var match = dotRegExp.exec(title); |
| if (!match) { |
| var visiblePartSize = maxSize / titleWidth; |
| var newTextLength = Math.floor(title.length * visiblePartSize) + 1; |
| var minTextLength = 4; |
| if (newTextLength < minTextLength) |
| return null; |
| var substring; |
| do { |
| --newTextLength; |
| substring = title.substring(0, newTextLength); |
| } while (context.measureText(substring).width > maxSize); |
| return title.substring(0, newTextLength) + "\u2026"; |
| } |
| while (match) { |
| var substring = title.substring(match.index + 1); |
| var width = context.measureText(substring).width; |
| if (maxSize > width) |
| return "\u2026" + substring; |
| match = dotRegExp.exec(title); |
| } |
| var i = 0; |
| do { |
| ++i; |
| } while (context.measureText(title.substring(0, i)).width < maxSize); |
| return title.substring(0, i - 1) + "\u2026"; |
| }, |
| |
| _scheduleUpdate: function() |
| { |
| if (this._updateTimerId) |
| return; |
| this._updateTimerId = setTimeout(this.update.bind(this), 10); |
| }, |
| |
| _updateBoundaries: function() |
| { |
| this._windowLeft = this._overviewGrid.windowLeft(); |
| this._windowRight = this._overviewGrid.windowRight(); |
| this._windowWidth = this._windowRight - this._windowLeft; |
| |
| this._totalTime = this._timelineData.totalTime; |
| this._timeWindowLeft = this._windowLeft * this._totalTime; |
| this._timeWindowRight = this._windowRight * this._totalTime; |
| |
| this._pixelWindowWidth = this._chartContainer.clientWidth; |
| this._totalPixels = Math.floor(this._pixelWindowWidth / this._windowWidth); |
| this._pixelWindowLeft = Math.floor(this._totalPixels * this._windowLeft); |
| this._pixelWindowRight = Math.floor(this._totalPixels * this._windowRight); |
| |
| this._timeToPixel = this._totalPixels / this._totalTime; |
| this._pixelToTime = this._totalTime / this._totalPixels; |
| this._paddingLeftTime = this._paddingLeft / this._timeToPixel; |
| }, |
| |
| update: function() |
| { |
| this._updateTimerId = 0; |
| if (!this._timelineData) |
| this._calculateTimelineData(); |
| if (!this._timelineData) |
| return; |
| this._updateBoundaries(); |
| this.draw(this._chartContainer.clientWidth, this._chartContainer.clientHeight); |
| this._calculator._updateBoundaries(this); |
| this._overviewCalculator._updateBoundaries(this); |
| this._timelineGrid.element.style.width = this.element.clientWidth; |
| this._timelineGrid.updateDividers(this._calculator); |
| this._overviewGrid.updateDividers(this._overviewCalculator); |
| if (this._updateOverviewCanvas) { |
| this._drawOverviewCanvas(this._overviewContainer.clientWidth, this._overviewContainer.clientHeight - 20); |
| this._updateOverviewCanvas = false; |
| } |
| }, |
| |
| __proto__: WebInspector.View.prototype |
| }; |