Chromium Code Reviews| Index: Source/devtools/front_end/TimelinePowerOverview.js |
| diff --git a/Source/devtools/front_end/TimelinePowerOverview.js b/Source/devtools/front_end/TimelinePowerOverview.js |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..e942a5298759b02484f77a06997bed029029128f |
| --- /dev/null |
| +++ b/Source/devtools/front_end/TimelinePowerOverview.js |
| @@ -0,0 +1,374 @@ |
| +// Copyright 2014 The Chromium Authors. All rights reserved. |
| +// Use of this source code is governed by a BSD-style license that can be |
| +// found in the LICENSE file. |
| + |
| +/** |
| + * @constructor |
| + * @extends {WebInspector.TimelineOverviewBase} |
|
pfeldman
2014/03/11 12:24:02
For now, you can add
startTimeline and stopTimel
Pan
2014/03/18 10:16:48
Done.
|
| + * @param {!WebInspector.TimelineModel} model |
| + */ |
| +WebInspector.TimelinePowerOverview = function(model) |
|
pfeldman
2014/03/11 12:24:02
Yes, perfect. You should have your own overview ty
|
| +{ |
| + WebInspector.TimelineOverviewBase.call(this, model); |
| + this.element.id = "timeline-overview-power"; |
| + this._canvas.addEventListener("mousemove", this._onMouseMove.bind(this)); |
| + this._powerEventsHistogram = []; |
| + this._maxPower = 0; |
| + this._minPower = 0; |
| + this._yFactor = 0; |
| + this._powerDrawHeight = 0; |
| + this._windowStartTime = 0; |
| + this._windowEndTime = Infinity; |
| +} |
| + |
| +WebInspector.TimelinePowerOverview.prototype = { |
| + windowChanged: function(startTime, endTime) |
|
pfeldman
2014/03/11 12:24:02
Please annotate every method.
|
| + { |
| + this._windowStartTime = startTime; |
| + this._windowEndTime = endTime; |
| + this._drawEnergyMark(startTime, endTime); |
| + }, |
| + |
| + _drawEnergyMark: function(startTime, endTime) { |
| + // Clear previous power marks firstly. |
| + this._restorePowerMarker(); |
|
pfeldman
2014/03/11 12:24:02
Lets think if we can unify this with memory graph
Pan
2014/03/12 13:11:20
Hi @pfeldman,
thanks for your help, do you mean it
|
| + |
| + // Clear previous energy consumption text. |
| + this._restoreImageUnderMarker(this._energyTextSaver); |
| + |
| + var startX = Math.round(this._xFactor * (startTime - this._model.minimumRecordTime())); |
| + var endX = Math.round(this._xFactor * (endTime- this._model.minimumRecordTime())); |
| + var energy = this._calculateEnergyInSelectedRegion(startX, endX); |
| + |
| + if (!energy) |
| + return; |
| + |
| + // Draw energy consumption text. |
| + var label = this._createTextLabel(WebInspector.UIString("%.2f\u2009joule", energy)); |
| + var textXPos = (startX + endX) > label.width ? Math.round((startX + endX - label.width) / 2) : Math.round((startX + endX) / 2); |
| + var textYPos = Math.round(this._powerDrawHeight / 2); |
| + this._context.beginPath(); |
| + this._energyTextSaver = this._saveRectImageUnderMarker(textXPos, textYPos - label.height, label.width, label.height + 2); |
| + this._context.strokeStyle = "black"; |
| + this._context.fillStyle = "black"; |
| + this._context.fillText(label.name, textXPos, textYPos); |
| + this._context.stroke(); |
| + }, |
| + |
| + _calculateEnergyInSelectedRegion: function(startX, endX) { |
| + |
| + // If pos is on the power event, just the prev and next will be same to pos |
| + function findAdjacents(pos, histograms) { |
| + var prevPowerEventIndex; |
| + var nextPowerEventIndex; |
| + // Find previous power event. |
| + for(var i = pos; i >= 0; i--) { |
| + if (histograms[i]) { |
| + prevPowerEventIndex = i; |
| + break; |
| + } |
| + } |
| + // Find next power event. |
| + for(var i = pos; i < histograms.length; i++) { |
| + if (histograms[i]) { |
| + nextPowerEventIndex= i; |
| + break; |
| + } |
| + } |
| + // As pos may fall into a blank region, which only have next or prev. in this case we will return 0 timespan and xSpan. |
| + return { |
| + prev : prevPowerEventIndex, |
| + next : nextPowerEventIndex, |
| + timeSpan : (!prevPowerEventIndex || !nextPowerEventIndex) ? 0 : (histograms[nextPowerEventIndex].startTime - histograms[prevPowerEventIndex].startTime) / 1000, // in seconds |
| + xSpan : (!prevPowerEventIndex || !nextPowerEventIndex) ? 0 : (nextPowerEventIndex - prevPowerEventIndex), |
| + power : (!nextPowerEventIndex) ? 0 : histograms[nextPowerEventIndex].value |
| + }; |
| + } |
| + |
| + var energy = 0; |
| + var startAdjancents = findAdjacents(startX, this._powerEventsHistogram); |
| + var endAdjancents = findAdjacents(endX, this._powerEventsHistogram); |
| + |
| + // Calculate energy consumption of sliced power events. |
| + if (startAdjancents.prev !== endAdjancents.prev) { // Cross at least one power event. |
| + if (startAdjancents.timeSpan) |
| + energy += (startAdjancents.next - startX) / startAdjancents.xSpan * startAdjancents.timeSpan * startAdjancents.power; |
| + |
| + if (endAdjancents.timeSpan) |
| + energy += (endX- endAdjancents.prev) / endAdjancents.xSpan * endAdjancents.timeSpan * endAdjancents.power; |
| + |
| + } else { // Start and end are in the same power event duration. |
| + energy += (endX - startX) / startAdjancents.xSpan * startAdjancents.timeSpan * endAdjancents.power; |
| + } |
| + |
| + // Calculate energy consumption of internal power events. |
| + var prevX = -1; |
| + for(var i = startX; i <= endX; i++) { |
| + if (!this._powerEventsHistogram[i]) |
| + continue; |
| + if (prevX !== -1) { |
| + var timeSpan = (this._powerEventsHistogram[i].startTime- this._powerEventsHistogram[prevX].startTime) / 1000; |
| + energy += timeSpan * this._powerEventsHistogram[i].value; |
| + } |
| + prevX = i; |
| + } |
| + |
| + return energy; |
| + }, |
| + |
| + _onMouseMove: function(event) |
| + { |
| + this._restorePowerMarker(); |
| + var powerValue; |
| + var index = Math.round(event.offsetX); |
| + var isInBlankArea = true; |
| + for (var i = 0; i < index + 1; i++) { |
| + if (this._powerEventsHistogram[i]) { |
| + isInBlankArea = false; |
| + break; |
| + } |
| + } |
| + if (isInBlankArea) |
| + return; |
| + |
| + for (var i = index + 1; i < this._powerEventsHistogram.length; i++) { |
| + if (this._powerEventsHistogram[i]) { |
| + powerValue = this._powerEventsHistogram[i].value; |
| + break; |
| + } |
| + } |
| + |
| + var xPos = event.offsetX; |
| + var yPos = this._powerDrawHeight - Math.round(this._yFactor * (powerValue - this._minPower)); |
| + |
| + this._drawPowerMark(xPos, yPos, powerValue); |
| + }, |
| + |
| + update: function() |
| + { |
| + this.resetCanvas(); |
| + delete this._powerPointSaver; |
| + delete this._powerTextSaver; |
| + delete this._energyTextSaver; |
| + |
| + this._powerEventsHistogram = []; |
| + this._yFactor = 0; |
| + |
| + var records = this._model.records(); |
| + if (!records.length) |
| + return; |
| + |
| + this._resetPowerBoundaries(records); |
| + |
| + var histogram = this._prepareHistograms(records); |
| + |
| + var height = this._powerDrawHeight; |
| + // Draw power gridline and scales. |
| + this._drawPowerGridline(this._minPower, this._maxPower, height); |
| + |
| + // Draw power graph. |
| + // As we only have power samples in timestamp, we should eliminated the first event as we don't know its start point. |
| + var initialX = 0; |
| + var initialY = 0; |
| + var previousX = 0; |
| + for (var k = 0; k < histogram.length; k++) { |
| + var value = histogram[k]; |
| + if (value !== undefined) { |
| + initialX = k; |
| + previousX = k; |
| + initialY = value; |
| + break; |
| + } |
| + } |
| + |
| + var ctx = this._context; |
| + ctx.beginPath(); |
| + var isFirst = true; |
| + |
| + for (var x = previousX + 1; x < histogram.length; x++) { |
| + if (histogram[x] === undefined) |
| + continue; |
| + var currentY = height - histogram[x]; |
| + if (!isFirst) { |
| + ctx.lineTo(previousX, currentY); |
| + } else { |
| + ctx.moveTo(initialX, currentY); |
| + isFirst = false; |
| + } |
| + ctx.lineTo(x, currentY); |
| + previousX = x; |
| + } |
| + |
| + ctx.lineWidth = 0.5; |
| + ctx.strokeStyle = "rgba(20,0,0,0.8)"; |
| + ctx.stroke(); |
| + |
| + ctx.fillStyle = "rgba(255,192,0, 0.8);"; |
| + ctx.lineTo(previousX, this._canvas.height); |
| + ctx.lineTo(initialX, this._canvas.height); |
| + ctx.lineTo(initialX, height - initialY); |
| + ctx.fill(); |
| + ctx.closePath(); |
| + |
| + // Draw energy for a selected time period. |
| + if (this._windowStartTime > 0 && this._windowEndTime < Infinity && this._windowStartTime < this._windowEndTime) |
| + this._drawEnergyMark(this._windowStartTime, this._windowEndTime); |
| + }, |
| + |
| + _resetPowerBoundaries: function(allRecords) |
| + { |
| + var maxPower = 0; |
| + var minPower = 100000000000; |
| + var minTime = this._model.minimumRecordTime(); |
| + this._model.forAllRecords(function(r) { |
| + record = r._record; |
| + if (record.type !== WebInspector.TimelineModel.RecordType.SoC_Package) |
| + return; |
| + maxPower = Math.max(maxPower, record.value); |
| + minPower = Math.min(minPower, record.value); |
| + }); |
| + minPower = Math.min(minPower, maxPower); |
| + this._maxPower = maxPower; |
| + this._minPower = minPower; |
| + }, |
| + |
| + _prepareHistograms: function(allRecords) |
| + { |
| + const lowerOffset = 3; |
| + var width = this._canvas.width; |
| + var height = this._canvas.height - lowerOffset; |
| + var minTime = this._model.minimumRecordTime(); |
| + var maxTime = this._model.maximumRecordTime(); |
| + var xFactor = width / (maxTime - minTime); |
| + var yFactor = height / (this._maxPower - this._minPower); |
| + this._xFactor = xFactor; |
| + this._yFactor = yFactor; |
| + |
| + var histogram = new Array(width); |
| + var powerEventsHistogram = new Array(width); |
| + var minPower = this._minPower; |
| + this._model.forAllRecords(function(r) { |
| + record = r._record; |
| + if (record.type !== WebInspector.TimelineModel.RecordType.SoC_Package) |
| + return; |
| + var x = Math.round((record.startTime - minTime) * xFactor); |
| + var y = Math.round((record.value- minPower ) * yFactor); |
| + histogram[x] = Math.max(histogram[x] || 0, y); |
| + powerEventsHistogram[x] = record; |
| + }); |
| + |
| + this._powerEventsHistogram = powerEventsHistogram; |
| + |
| + // +1 so that the border always fit into the canvas area. |
| + this._powerDrawHeight = height + 1; |
| + |
| + return histogram; |
| + }, |
| + |
| + _drawPowerGridline: function(minPower, maxPower, height) |
| + { |
| + var ctx = this._context; |
| + var width = this._canvas.width; |
| + var yFactor = (maxPower - minPower) / height; |
| + const labelPadding = 4 * window.devicePixelRatio; |
| + |
| + ctx.strokeStyle = "rgba(128,128,128,0.5)"; |
| + var gridHeight = height / 4; |
| + var yPositions = []; |
| + var powerScales = []; |
| + |
| + for (var i = 1; i < 4; i++) { |
| + var yPos = Math.round(gridHeight * i) - 0.5; |
| + var powerScale = yFactor * (height - yPos) + minPower; |
| + |
| + //draw gridline |
| + ctx.beginPath(); |
| + ctx.moveTo(0, yPos); |
| + ctx.lineTo(width, yPos); |
| + ctx.stroke(); |
| + |
| + //draw scale for gridline |
| + if (!powerScale) |
| + continue; |
| + ctx.beginPath(); |
| + var label = this._createTextLabel(WebInspector.UIString("%.2f\u2009watt", powerScale)); |
| + ctx.save(); |
| + this._context.fillStyle = "black"; |
| + ctx.fillText(label.name, 2, yPos - 2); |
| + ctx.stroke(); |
| + ctx.restore(); |
| + } |
| + }, |
| + |
| + _restorePowerMarker: function() |
| + { |
| + this._restoreImageUnderMarker(this._powerPointSaver); |
| + delete this._powerPointSaver; |
| + |
| + this._restoreImageUnderMarker(this._powerTextSaver); |
| + delete this._powerTextSaver; |
| + }, |
| + |
| + _drawPowerMark: function(xPointPos, yPointPos, powerValue) |
| + { |
| + // Draw power point. |
| + const radius = 2; |
| + this._powerPointSaver = this._saveArcImageUnderMarker(xPointPos, yPointPos, radius); |
| + this._context.beginPath(); |
| + this._context.arc(xPointPos, yPointPos, radius, Math.PI*2, false) |
| + this._context.strokeStyle = "red"; |
| + this._context.fillStyle = "red"; |
| + this._context.fill(); |
| + this._context.stroke(); |
| + |
| + // Draw power text. |
| + var label = this._createTextLabel(WebInspector.UIString("%.2f\u2009watt", powerValue)); |
| + var textYPos = yPointPos - 3 - label.height > 0 ? yPointPos - 3: yPointPos + 3 + label.height; |
| + var textXPos = xPointPos + label.width > this._canvas.width ? this._canvas.width - label.width : xPointPos; |
| + this._context.beginPath(); |
| + this._powerTextSaver = this._saveRectImageUnderMarker(textXPos, textYPos - label.height, label.width, label.height); |
| + this._context.fillText(label.name, textXPos, textYPos); |
| + this._context.stroke(); |
| + }, |
| + |
| + _createTextLabel: function(labelName) |
| + { |
| + const labelPadding = 4 * window.devicePixelRatio; |
| + var labelWidth = this._context.measureText(labelName).width + 2 * labelPadding; |
| + var contextFont = this._context.font; |
| + var labelHeight = contextFont.substr(0, contextFont.indexOf("px ")) - 0; |
| + return { name : labelName, |
| + width : labelWidth, |
| + height : labelHeight |
| + }; |
| + }, |
| + |
| + _saveArcImageUnderMarker: function(x, y, radius) |
| + { |
| + const w = radius + 1; |
| + var imageData = this._context.getImageData(x - w, y - w, 2 * w, 2 * w); |
| + return { |
| + x: x - w, |
| + y: y - w, |
| + imageData: imageData |
| + }; |
| + }, |
| + |
| + _saveRectImageUnderMarker: function(x, y, w, h) |
| + { |
| + var imageData = this._context.getImageData(x, y, w, h); |
| + return { |
| + x: x, |
| + y: y, |
| + imageData: imageData |
| + }; |
| + }, |
| + |
| + _restoreImageUnderMarker: function(imageUnderMarker) |
| + { |
| + if (imageUnderMarker) |
| + this._context.putImageData(imageUnderMarker.imageData, imageUnderMarker.x, imageUnderMarker.y); |
| + }, |
| + |
| + __proto__: WebInspector.TimelineOverviewBase.prototype |
| +} |