1// Copyright 2014 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5/** 6 * Plot a line graph of data versus time on a HTML canvas element. 7 * 8 * @param {HTMLCanvasElement} plotCanvas The canvas on which the line graph is 9 * drawn. 10 * @param {HTMLCanvasElement} legendCanvas The canvas on which the legend for 11 * the line graph is drawn. 12 * @param {Array.<number>} tData The time (in seconds) in the past when the 13 * corresponding data in plots was sampled. 14 * @param {Array.<{data: Array.<number>, color: string}>} plots An 15 * array of plots to plot on the canvas. The field 'data' of a plot is an 16 * array of samples to be plotted as a line graph with color speficied by 17 * the field 'color'. The elements in the 'data' array are ordered 18 * corresponding to their sampling time in the argument 'tData'. Also, the 19 * number of elements in the 'data' array should be the same as in the time 20 * array 'tData' above. 21 * @param {number} yMin Minimum bound of y-axis 22 * @param {number} yMax Maximum bound of y-axis. 23 * @param {integer} yPrecision An integer value representing the number of 24 * digits of precision the y-axis data should be printed with. 25 */ 26function plotLineGraph( 27 plotCanvas, legendCanvas, tData, plots, yMin, yMax, yPrecision) { 28 var textFont = 12 * devicePixelRatio + 'px Arial'; 29 var textHeight = 12 * devicePixelRatio; 30 var padding = 5 * devicePixelRatio; // Pixels 31 var errorOffsetPixels = 15 * devicePixelRatio; 32 var gridColor = '#ccc'; 33 var plotCtx = plotCanvas.getContext('2d'); 34 var size = tData.length; 35 36 function drawText(ctx, text, x, y) { 37 ctx.font = textFont; 38 ctx.fillStyle = '#000'; 39 ctx.fillText(text, x, y); 40 } 41 42 function printErrorText(ctx, text) { 43 ctx.clearRect(0, 0, plotCanvas.width, plotCanvas.height); 44 drawText(ctx, text, errorOffsetPixels, errorOffsetPixels); 45 } 46 47 if (size < 2) { 48 printErrorText(plotCtx, 49 loadTimeData.getString('notEnoughDataAvailableYet')); 50 return; 51 } 52 53 for (var count = 0; count < plots.length; count++) { 54 if (plots[count].data.length != size) { 55 throw new Error('Mismatch in time and plot data.'); 56 } 57 } 58 59 function valueToString(value) { 60 if (Math.abs(value) < 1) { 61 return Number(value).toFixed(yPrecision - 1); 62 } else { 63 return Number(value).toPrecision(yPrecision); 64 } 65 } 66 67 function getTextWidth(ctx, text) { 68 ctx.font = textFont; 69 // For now, all text is drawn to the left of vertical lines, or centered. 70 // Add a 2 pixel padding so that there is some spacing between the text 71 // and the vertical line. 72 return Math.round(ctx.measureText(text).width) + 2 * devicePixelRatio; 73 } 74 75 function getLegend(text) { 76 return ' ' + text + ' '; 77 } 78 79 function drawHighlightText(ctx, text, x, y, color) { 80 ctx.strokeStyle = '#000'; 81 ctx.strokeRect(x, y - textHeight, getTextWidth(ctx, text), textHeight); 82 ctx.fillStyle = color; 83 ctx.fillRect(x, y - textHeight, getTextWidth(ctx, text), textHeight); 84 ctx.fillStyle = '#fff'; 85 ctx.fillText(text, x, y); 86 } 87 88 function drawLine(ctx, x1, y1, x2, y2, color) { 89 ctx.save(); 90 ctx.beginPath(); 91 ctx.moveTo(x1, y1); 92 ctx.lineTo(x2, y2); 93 ctx.strokeStyle = color; 94 ctx.lineWidth = 1 * devicePixelRatio; 95 ctx.stroke(); 96 ctx.restore(); 97 } 98 99 // The strokeRect method of the 2d context of a plotCanvas draws a bounding 100 // rectangle with an offset origin and greater dimensions. Hence, use this 101 // function to draw a rect at the desired location with desired dimensions. 102 function drawRect(ctx, x, y, width, height, color) { 103 var offset = 1 * devicePixelRatio; 104 drawLine(ctx, x, y, x + width - offset, y, color); 105 drawLine(ctx, x, y, x, y + height - offset, color); 106 drawLine(ctx, x, y + height - offset, x + width - offset, 107 y + height - offset, color); 108 drawLine(ctx, x + width - offset, y, x + width - offset, 109 y + height - offset, color); 110 } 111 112 function drawLegend() { 113 // Show a legend only if at least one individual plot has a name. 114 var valid = false; 115 for (var i = 0; i < plots.length; i++) { 116 if (plots[i].name != null) { 117 valid = true; 118 break; 119 } 120 } 121 if (!valid) { 122 legendCanvas.hidden = true; 123 return; 124 } 125 126 127 var padding = 2 * devicePixelRatio; 128 var legendSquareSide = 12 * devicePixelRatio; 129 var legendCtx = legendCanvas.getContext('2d'); 130 var xLoc = padding; 131 var yLoc = padding; 132 // Adjust the height of the canvas before drawing on it. 133 for (var i = 0; i < plots.length; i++) { 134 if (plots[i].name == null) { 135 continue; 136 } 137 var legendText = getLegend(plots[i].name); 138 xLoc += legendSquareSide + getTextWidth(legendCtx, legendText) + 139 2 * padding; 140 if (i < plots.length - 1) { 141 var xLocNext = xLoc + 142 getTextWidth(legendCtx, getLegend(plots[i + 1].name)) + 143 legendSquareSide; 144 if (xLocNext >= legendCanvas.width) { 145 xLoc = padding; 146 yLoc = yLoc + 2 * padding + textHeight; 147 } 148 } 149 } 150 151 legendCanvas.height = yLoc + textHeight + padding; 152 legendCanvas.style.height = 153 legendCanvas.height / devicePixelRatio + 'px'; 154 155 xLoc = padding; 156 yLoc = padding; 157 // Go over the plots again, this time drawing the legends. 158 for (var i = 0; i < plots.length; i++) { 159 legendCtx.fillStyle = plots[i].color; 160 legendCtx.fillRect(xLoc, yLoc, legendSquareSide, legendSquareSide); 161 xLoc += legendSquareSide; 162 163 var legendText = getLegend(plots[i].name); 164 drawText(legendCtx, legendText, xLoc, yLoc + textHeight - 1); 165 xLoc += getTextWidth(legendCtx, legendText) + 2 * padding; 166 167 if (i < plots.length - 1) { 168 var xLocNext = xLoc + 169 getTextWidth(legendCtx, getLegend(plots[i + 1].name)) + 170 legendSquareSide; 171 if (xLocNext >= legendCanvas.width) { 172 xLoc = padding; 173 yLoc = yLoc + 2 * padding + textHeight; 174 } 175 } 176 } 177 } 178 179 var yMinStr = valueToString(yMin); 180 var yMaxStr = valueToString(yMax); 181 var yHalfStr = valueToString((yMax + yMin) / 2); 182 var yMinWidth = getTextWidth(plotCtx, yMinStr); 183 var yMaxWidth = getTextWidth(plotCtx, yMaxStr); 184 var yHalfWidth = getTextWidth(plotCtx, yHalfStr); 185 186 var xMinStr = tData[0]; 187 var xMaxStr = tData[size - 1]; 188 var xMinWidth = getTextWidth(plotCtx, xMinStr); 189 var xMaxWidth = getTextWidth(plotCtx, xMaxStr); 190 191 var xOrigin = padding + Math.max(yMinWidth, 192 yMaxWidth, 193 Math.round(xMinWidth / 2)); 194 var yOrigin = padding + textHeight; 195 var width = plotCanvas.width - xOrigin - Math.floor(xMaxWidth / 2) - padding; 196 if (width < size) { 197 plotCanvas.width += size - width; 198 width = size; 199 } 200 var height = plotCanvas.height - yOrigin - textHeight - padding; 201 var linePlotEndMarkerWidth = 3; 202 203 function drawPlots() { 204 // Start fresh. 205 plotCtx.clearRect(0, 0, plotCanvas.width, plotCanvas.height); 206 207 // Draw the bounding rectangle. 208 drawRect(plotCtx, xOrigin, yOrigin, width, height, gridColor); 209 210 // Draw the x and y bound values. 211 drawText(plotCtx, yMaxStr, xOrigin - yMaxWidth, yOrigin + textHeight); 212 drawText(plotCtx, yMinStr, xOrigin - yMinWidth, yOrigin + height); 213 drawText(plotCtx, 214 xMinStr, 215 xOrigin - xMinWidth / 2, 216 yOrigin + height + textHeight); 217 drawText(plotCtx, 218 xMaxStr, 219 xOrigin + width - xMaxWidth / 2, 220 yOrigin + height + textHeight); 221 222 // Draw y-level (horizontal) lines. 223 drawLine(plotCtx, 224 xOrigin + 1, yOrigin + height / 4, 225 xOrigin + width - 2, yOrigin + height / 4, 226 gridColor); 227 drawLine(plotCtx, 228 xOrigin + 1, yOrigin + height / 2, 229 xOrigin + width - 2, yOrigin + height / 2, gridColor); 230 drawLine(plotCtx, 231 xOrigin + 1, yOrigin + 3 * height / 4, 232 xOrigin + width - 2, yOrigin + 3 * height / 4, 233 gridColor); 234 235 // Draw half-level value. 236 drawText(plotCtx, 237 yHalfStr, 238 xOrigin - yHalfWidth, 239 yOrigin + height / 2 + textHeight / 2); 240 241 // Draw the plots. 242 var yValRange = yMax - yMin; 243 for (var count = 0; count < plots.length; count++) { 244 var plot = plots[count]; 245 var yData = plot.data; 246 plotCtx.strokeStyle = plot.color; 247 plotCtx.lineWidth = 2; 248 plotCtx.beginPath(); 249 var beginPath = true; 250 for (var i = 0; i < size; i++) { 251 var val = yData[i]; 252 if (typeof val === 'string') { 253 // Stroke the plot drawn so far and begin a fresh plot. 254 plotCtx.stroke(); 255 plotCtx.beginPath(); 256 beginPath = true; 257 continue; 258 } 259 var xPos = xOrigin + Math.floor(i / (size - 1) * (width - 1)); 260 var yPos = yOrigin + height - 1 - 261 Math.round((val - yMin) / yValRange * (height - 1)); 262 if (beginPath) { 263 plotCtx.moveTo(xPos, yPos); 264 // A simple move to does not print anything. Hence, draw a little 265 // square here to mark a beginning. 266 plotCtx.fillStyle = '#000'; 267 plotCtx.fillRect(xPos - linePlotEndMarkerWidth, 268 yPos - linePlotEndMarkerWidth, 269 linePlotEndMarkerWidth * devicePixelRatio, 270 linePlotEndMarkerWidth * devicePixelRatio); 271 beginPath = false; 272 } else { 273 plotCtx.lineTo(xPos, yPos); 274 if (i === size - 1 || typeof yData[i + 1] === 'string') { 275 // Draw a little square to mark an end to go with the start 276 // markers from above. 277 plotCtx.fillStyle = '#000'; 278 plotCtx.fillRect(xPos - linePlotEndMarkerWidth, 279 yPos - linePlotEndMarkerWidth, 280 linePlotEndMarkerWidth * devicePixelRatio, 281 linePlotEndMarkerWidth * devicePixelRatio); 282 } 283 } 284 } 285 plotCtx.stroke(); 286 } 287 288 // Paint the missing time intervals with |gridColor|. 289 // Pick one of the plots to look for missing time intervals. 290 function drawMissingRect(start, end) { 291 var xLeft = xOrigin + Math.floor(start / (size - 1) * (width - 1)); 292 var xRight = xOrigin + Math.floor(end / (size - 1) * (width - 1)); 293 plotCtx.fillStyle = gridColor; 294 // The x offsets below are present so that the blank space starts 295 // and ends between two valid samples. 296 plotCtx.fillRect(xLeft + 1, yOrigin, xRight - xLeft - 2, height - 1); 297 } 298 var inMissingInterval = false; 299 var intervalStart; 300 for (var i = 0; i < size; i++) { 301 if (typeof plots[0].data[i] === 'string') { 302 if (!inMissingInterval) { 303 inMissingInterval = true; 304 // The missing interval should actually start from the previous 305 // sample. 306 intervalStart = Math.max(i - 1, 0); 307 } 308 309 if (i == size - 1) { 310 // If this is the last sample, just draw missing rect. 311 drawMissingRect(intervalStart, i); 312 } 313 } else if (inMissingInterval) { 314 inMissingInterval = false; 315 drawMissingRect(intervalStart, i); 316 } 317 } 318 } 319 320 function drawTimeGuide(tDataIndex) { 321 var x = xOrigin + tDataIndex / (size - 1) * (width - 1); 322 drawLine(plotCtx, x, yOrigin, x, yOrigin + height - 1, '#000'); 323 drawText(plotCtx, 324 tData[tDataIndex], 325 x - getTextWidth(plotCtx, tData[tDataIndex]) / 2, 326 yOrigin - 2); 327 328 for (var count = 0; count < plots.length; count++) { 329 var yData = plots[count].data; 330 331 // Draw small black square on the plot where the time guide intersects 332 // it. 333 var val = yData[tDataIndex]; 334 var yPos, valStr; 335 if (typeof val === 'string') { 336 yPos = yOrigin + Math.round(height / 2); 337 valStr = val; 338 } else { 339 yPos = yOrigin + height - 1 - 340 Math.round((val - yMin) / (yMax - yMin) * (height - 1)); 341 valStr = valueToString(val); 342 } 343 plotCtx.fillStyle = '#000'; 344 plotCtx.fillRect(x - 2, yPos - 2, 4, 4); 345 346 // Draw the val to right of the intersection. 347 var yLoc; 348 if (yPos - textHeight / 2 < yOrigin) { 349 yLoc = yOrigin + textHeight; 350 } else if (yPos + textHeight / 2 >= yPos + height) { 351 yLoc = yOrigin + height - 1; 352 } else { 353 yLoc = yPos + textHeight / 2; 354 } 355 drawHighlightText(plotCtx, valStr, x + 5, yLoc, plots[count].color); 356 } 357 } 358 359 function onMouseOverOrMove(event) { 360 drawPlots(); 361 362 var boundingRect = plotCanvas.getBoundingClientRect(); 363 var x = Math.round((event.clientX - boundingRect.left) * devicePixelRatio); 364 var y = Math.round((event.clientY - boundingRect.top) * devicePixelRatio); 365 if (x < xOrigin || x >= xOrigin + width || 366 y < yOrigin || y >= yOrigin + height) { 367 return; 368 } 369 370 if (width == size) { 371 drawTimeGuide(x - xOrigin); 372 } else { 373 drawTimeGuide(Math.round((x - xOrigin) / (width - 1) * (size - 1))); 374 } 375 } 376 377 function onMouseOut(event) { 378 drawPlots(); 379 } 380 381 drawLegend(); 382 drawPlots(); 383 plotCanvas.addEventListener('mouseover', onMouseOverOrMove); 384 plotCanvas.addEventListener('mousemove', onMouseOverOrMove); 385 plotCanvas.addEventListener('mouseout', onMouseOut); 386} 387 388var sleepSampleInterval = 30 * 1000; // in milliseconds. 389var sleepText = loadTimeData.getString('systemSuspended'); 390var invalidDataText = loadTimeData.getString('invalidData'); 391var offlineText = loadTimeData.getString('offlineText'); 392 393var plotColors = ['Red', 'Blue', 'Green', 'Gold', 'CadetBlue', 'LightCoral', 394 'LightSlateGray', 'Peru', 'DarkRed', 'LawnGreen', 'Tan']; 395 396/** 397 * Add canvases for plotting to |plotsDiv|. For every header in |headerArray|, 398 * one canvas for the plot and one for its legend are added. 399 * 400 * @param {Array.<string>} headerArray Headers for the different plots to be 401 * added to |plotsDiv|. 402 * @param {HTMLDivElement} plotsDiv The div element into which the canvases 403 * are added. 404 * @return {<string>: {plotCanvas: <HTMLCanvasElement>, 405 * legendCanvas: <HTMLCanvasElement>} Returns an object 406 * with the headers as 'keys'. Each element is an object containing the 407 * legend canvas and the plot canvas that have been added to |plotsDiv|. 408 */ 409function addCanvases(headerArray, plotsDiv) { 410 // Remove the contents before adding new ones. 411 while (plotsDiv.firstChild != null) { 412 plotsDiv.removeChild(plotsDiv.firstChild); 413 } 414 var width = Math.floor(plotsDiv.getBoundingClientRect().width); 415 var canvases = {}; 416 for (var i = 0; i < headerArray.length; i++) { 417 var header = document.createElement('h4'); 418 header.textContent = headerArray[i]; 419 plotsDiv.appendChild(header); 420 421 var legendCanvas = document.createElement('canvas'); 422 legendCanvas.width = width * devicePixelRatio; 423 legendCanvas.style.width = width + 'px'; 424 plotsDiv.appendChild(legendCanvas); 425 426 var plotCanvasDiv = document.createElement('div'); 427 plotCanvasDiv.style.overflow = 'auto'; 428 plotsDiv.appendChild(plotCanvasDiv); 429 430 plotCanvas = document.createElement('canvas'); 431 plotCanvas.width = width * devicePixelRatio; 432 plotCanvas.height = 200 * devicePixelRatio; 433 plotCanvas.style.height = '200px'; 434 plotCanvasDiv.appendChild(plotCanvas); 435 436 canvases[headerArray[i]] = {plot: plotCanvas, legend: legendCanvas}; 437 } 438 return canvases; 439} 440 441/** 442 * Add samples in |sampleArray| to individual plots in |plots|. If the system 443 * resumed from a sleep/suspend, then "suspended" sleep samples are added to 444 * the plot for the sleep duration. 445 * 446 * @param {Array.<{data: Array.<number>, color: string}>} plots An 447 * array of plots to plot on the canvas. The field 'data' of a plot is an 448 * array of samples to be plotted as a line graph with color speficied by 449 * the field 'color'. The elements in the 'data' array are ordered 450 * corresponding to their sampling time in the argument 'tData'. Also, the 451 * number of elements in the 'data' array should be the same as in the time 452 * array 'tData' below. 453 * @param {Array.<number>} tData The time (in seconds) in the past when the 454 * corresponding data in plots was sampled. 455 * @param {Array.<number>} sampleArray The array of samples wherein each 456 * element corresponds to the individual plot in |plots|. 457 * @param {number} sampleTime Time in milliseconds since the epoch when the 458 * samples in |sampleArray| were captured. 459 * @param {number} previousSampleTime Time in milliseconds since the epoch 460 * when the sample prior to the current sample was captured. 461 * @param {Array.<{time: number, sleepDuration: number}>} systemResumedArray An 462 * array objects corresponding to system resume events. The 'time' field is 463 * for the time in milliseconds since the epoch when the system resumed. The 464 * 'sleepDuration' field is for the time in milliseconds the system spent 465 * in sleep/suspend state. 466 */ 467function addTimeDataSample(plots, tData, absTime, sampleArray, 468 sampleTime, previousSampleTime, 469 systemResumedArray) { 470 for (var i = 0; i < plots.length; i++) { 471 if (plots[i].data.length != tData.length) { 472 throw new Error('Mismatch in time and plot data.'); 473 } 474 } 475 476 var time; 477 if (tData.length == 0) { 478 time = new Date(sampleTime); 479 absTime[0] = sampleTime; 480 tData[0] = time.toLocaleTimeString(); 481 for (var i = 0; i < plots.length; i++) { 482 plots[i].data[0] = sampleArray[i]; 483 } 484 return; 485 } 486 487 for (var i = 0; i < systemResumedArray.length; i++) { 488 var resumeTime = systemResumedArray[i].time; 489 var sleepDuration = systemResumedArray[i].sleepDuration; 490 var sleepStartTime = resumeTime - sleepDuration; 491 if (resumeTime < sampleTime) { 492 if (sleepStartTime < previousSampleTime) { 493 // This can happen if pending callbacks were handled before actually 494 // suspending. 495 sleepStartTime = previousSampleTime + 1000; 496 } 497 // Add sleep samples for every |sleepSampleInterval|. 498 var sleepSampleTime = sleepStartTime; 499 while (sleepSampleTime < resumeTime) { 500 time = new Date(sleepSampleTime); 501 absTime.push(sleepSampleTime); 502 tData.push(time.toLocaleTimeString()); 503 for (var j = 0; j < plots.length; j++) { 504 plots[j].data.push(sleepText); 505 } 506 sleepSampleTime += sleepSampleInterval; 507 } 508 } 509 } 510 511 time = new Date(sampleTime); 512 absTime.push(sampleTime); 513 tData.push(time.toLocaleTimeString()); 514 for (var i = 0; i < plots.length; i++) { 515 plots[i].data.push(sampleArray[i]); 516 } 517} 518 519/** 520 * Display the battery charge vs time on a line graph. 521 * 522 * @param {Array.<{time: number, 523 * batteryPercent: number, 524 * batteryDischargeRate: number, 525 * externalPower: number}>} powerSupplyArray An array of objects 526 * with fields representing the battery charge, time when the charge 527 * measurement was taken, and whether there was external power connected at 528 * that time. 529 * @param {Array.<{time: ?, sleepDuration: ?}>} systemResumedArray An array 530 * objects with fields 'time' and 'sleepDuration'. Each object corresponds 531 * to a system resume event. The 'time' field is for the time in 532 * milliseconds since the epoch when the system resumed. The 'sleepDuration' 533 * field is for the time in milliseconds the system spent in sleep/suspend 534 * state. 535 */ 536function showBatteryChargeData(powerSupplyArray, systemResumedArray) { 537 var chargeTimeData = []; 538 var chargeAbsTime = []; 539 var chargePlot = [ 540 { 541 name: loadTimeData.getString('batteryChargePercentageHeader'), 542 color: 'Blue', 543 data: [] 544 } 545 ]; 546 var dischargeRateTimeData = []; 547 var dischargeRateAbsTime = []; 548 var dischargeRatePlot = [ 549 { 550 name: loadTimeData.getString('dischargeRateLegendText'), 551 color: 'Red', 552 data: [] 553 }, 554 { 555 name: loadTimeData.getString('movingAverageLegendText'), 556 color: 'Green', 557 data: [] 558 }, 559 { 560 name: loadTimeData.getString('binnedAverageLegendText'), 561 color: 'Blue', 562 data: [] 563 } 564 ]; 565 var minDischargeRate = 1000; // A high unrealistic number to begin with. 566 var maxDischargeRate = -1000; // A low unrealistic number to begin with. 567 for (var i = 0; i < powerSupplyArray.length; i++) { 568 var j = Math.max(i - 1, 0); 569 570 addTimeDataSample(chargePlot, 571 chargeTimeData, 572 chargeAbsTime, 573 [powerSupplyArray[i].batteryPercent], 574 powerSupplyArray[i].time, 575 powerSupplyArray[j].time, 576 systemResumedArray); 577 578 var dischargeRate = powerSupplyArray[i].batteryDischargeRate; 579 var inputSampleCount = $('sample-count-input').value; 580 581 var movingAverage = 0; 582 var k = 0; 583 for (k = 0; k < inputSampleCount && i - k >= 0; k++) { 584 movingAverage += powerSupplyArray[i - k].batteryDischargeRate; 585 } 586 // |k| will be atleast 1 because the 'min' value of the input field is 1. 587 movingAverage /= k; 588 589 var binnedAverage = 0; 590 for (k = 0; k < inputSampleCount; k++) { 591 var currentSampleIndex = i - i % inputSampleCount + k; 592 if (currentSampleIndex >= powerSupplyArray.length) { 593 break; 594 } 595 596 binnedAverage += 597 powerSupplyArray[currentSampleIndex].batteryDischargeRate; 598 } 599 binnedAverage /= k; 600 601 minDischargeRate = Math.min(dischargeRate, minDischargeRate); 602 maxDischargeRate = Math.max(dischargeRate, maxDischargeRate); 603 addTimeDataSample(dischargeRatePlot, 604 dischargeRateTimeData, 605 dischargeRateAbsTime, 606 [dischargeRate, movingAverage, binnedAverage], 607 powerSupplyArray[i].time, 608 powerSupplyArray[j].time, 609 systemResumedArray); 610 } 611 if (minDischargeRate == maxDischargeRate) { 612 // This means that all the samples had the same value. Hence, offset the 613 // extremes by a bit so that the plot looks good. 614 minDischargeRate -= 1; 615 maxDischargeRate += 1; 616 } 617 618 plotsDiv = $('battery-charge-plots-div'); 619 620 canvases = addCanvases( 621 [loadTimeData.getString('batteryChargePercentageHeader'), 622 loadTimeData.getString('batteryDischargeRateHeader')], 623 plotsDiv); 624 625 batteryChargeCanvases = canvases[ 626 loadTimeData.getString('batteryChargePercentageHeader')]; 627 plotLineGraph( 628 batteryChargeCanvases['plot'], 629 batteryChargeCanvases['legend'], 630 chargeTimeData, 631 chargePlot, 632 0.00, 633 100.00, 634 3); 635 636 dischargeRateCanvases = canvases[ 637 loadTimeData.getString('batteryDischargeRateHeader')]; 638 plotLineGraph( 639 dischargeRateCanvases['plot'], 640 dischargeRateCanvases['legend'], 641 dischargeRateTimeData, 642 dischargeRatePlot, 643 minDischargeRate, 644 maxDischargeRate, 645 3); 646} 647 648/** 649 * Shows state occupancy data (CPU idle or CPU freq state occupancy) on a set of 650 * plots on the about:power UI. 651 * 652 * @param {Array.<{Array.<{ 653 * time: number, 654 * cpuOnline:boolean, 655 * timeInState: {<string>: number}>}>} timeInStateData Array of arrays 656 * where each array corresponds to a CPU on the system. The elements of the 657 * individual arrays contain state occupancy samples. 658 * @param {Array.<{time: ?, sleepDuration: ?}>} systemResumedArray An array 659 * objects with fields 'time' and 'sleepDuration'. Each object corresponds 660 * to a system resume event. The 'time' field is for the time in 661 * milliseconds since the epoch when the system resumed. The 'sleepDuration' 662 * field is for the time in milliseconds the system spent in sleep/suspend 663 * state. 664 * @param {string} i18nHeaderString The header string to be displayed with each 665 * plot. For example, CPU idle data will have its own header format, and CPU 666 * freq data will have its header format. 667 * @param {string} unitString This is the string capturing the unit, if any, 668 * for the different states. Note that this is not the unit of the data 669 * being plotted. 670 * @param {HTMLDivElement} plotsDivId The div element in which the plots should 671 * be added. 672 */ 673function showStateOccupancyData(timeInStateData, 674 systemResumedArray, 675 i18nHeaderString, 676 unitString, 677 plotsDivId) { 678 var cpuPlots = []; 679 for (var cpu = 0; cpu < timeInStateData.length; cpu++) { 680 var cpuData = timeInStateData[cpu]; 681 if (cpuData.length == 0) { 682 cpuPlots[cpu] = {plots: [], tData: []}; 683 continue; 684 } 685 tData = []; 686 absTime = []; 687 // Each element of |plots| is an array of samples, one for each of the CPU 688 // states. The number of states is dicovered by looking at the first 689 // sample for which the CPU is online. 690 var plots = []; 691 var stateIndexMap = []; 692 var stateCount = 0; 693 for (var i = 0; i < cpuData.length; i++) { 694 if (cpuData[i].cpuOnline) { 695 for (var state in cpuData[i].timeInState) { 696 var stateName = state; 697 if (unitString != null) { 698 stateName += ' ' + unitString; 699 } 700 plots.push({ 701 name: stateName, 702 data: [], 703 color: plotColors[stateCount] 704 }); 705 stateIndexMap.push(state); 706 stateCount += 1; 707 } 708 break; 709 } 710 } 711 // If stateCount is 0, then it means the CPU has been offline 712 // throughout. Just add a single plot for such a case. 713 if (stateCount == 0) { 714 plots.push({ 715 name: null, 716 data: [], 717 color: null 718 }); 719 stateCount = 1; // Some invalid state! 720 } 721 722 // Pass the samples through the function addTimeDataSample to add 'sleep' 723 // samples. 724 for (var i = 0; i < cpuData.length; i++) { 725 var sample = cpuData[i]; 726 var valArray = []; 727 for (var j = 0; j < stateCount; j++) { 728 if (sample.cpuOnline) { 729 valArray[j] = sample.timeInState[stateIndexMap[j]]; 730 } else { 731 valArray[j] = offlineText; 732 } 733 } 734 735 var k = Math.max(i - 1, 0); 736 addTimeDataSample(plots, 737 tData, 738 absTime, 739 valArray, 740 sample.time, 741 cpuData[k].time, 742 systemResumedArray); 743 } 744 745 // Calculate the percentage occupancy of each state. A valid number is 746 // possible only if two consecutive samples are valid/numbers. 747 for (var k = 0; k < stateCount; k++) { 748 var stateData = plots[k].data; 749 // Skip the first sample as there is no previous sample. 750 for (var i = stateData.length - 1; i > 0; i--) { 751 if (typeof stateData[i] === 'number') { 752 if (typeof stateData[i - 1] === 'number') { 753 stateData[i] = (stateData[i] - stateData[i - 1]) / 754 (absTime[i] - absTime[i - 1]) * 100; 755 } else { 756 stateData[i] = invalidDataText; 757 } 758 } 759 } 760 } 761 762 // Remove the first sample from the time and data arrays. 763 tData.shift(); 764 for (var k = 0; k < stateCount; k++) { 765 plots[k].data.shift(); 766 } 767 cpuPlots[cpu] = {plots: plots, tData: tData}; 768 } 769 770 headers = []; 771 for (var cpu = 0; cpu < timeInStateData.length; cpu++) { 772 headers[cpu] = 773 'CPU ' + cpu + ' ' + loadTimeData.getString(i18nHeaderString); 774 } 775 776 canvases = addCanvases(headers, $(plotsDivId)); 777 for (var cpu = 0; cpu < timeInStateData.length; cpu++) { 778 cpuCanvases = canvases[headers[cpu]]; 779 plotLineGraph(cpuCanvases['plot'], 780 cpuCanvases['legend'], 781 cpuPlots[cpu]['tData'], 782 cpuPlots[cpu]['plots'], 783 0, 784 100, 785 3); 786 } 787} 788 789function showCpuIdleData(idleStateData, systemResumedArray) { 790 showStateOccupancyData(idleStateData, 791 systemResumedArray, 792 'idleStateOccupancyPercentageHeader', 793 null, 794 'cpu-idle-plots-div'); 795} 796 797function showCpuFreqData(freqStateData, systemResumedArray) { 798 showStateOccupancyData(freqStateData, 799 systemResumedArray, 800 'frequencyStateOccupancyPercentageHeader', 801 'MHz', 802 'cpu-freq-plots-div'); 803} 804 805function requestBatteryChargeData() { 806 chrome.send('requestBatteryChargeData'); 807} 808 809function requestCpuIdleData() { 810 chrome.send('requestCpuIdleData'); 811} 812 813function requestCpuFreqData() { 814 chrome.send('requestCpuFreqData'); 815} 816 817/** 818 * Return a callback for the 'Show'/'Hide' buttons for each section of the 819 * about:power page. 820 * 821 * @param {string} sectionId The ID of the section which is to be shown or 822 * hidden. 823 * @param {string} buttonId The ID of the 'Show'/'Hide' button. 824 * @param {function} requestFunction The function which should be invoked on 825 * 'Show' to request for data from chrome. 826 * @return {function} The button callback function. 827 */ 828function showHideCallback(sectionId, buttonId, requestFunction) { 829 return function() { 830 if ($(sectionId).hidden) { 831 $(sectionId).hidden = false; 832 $(buttonId).textContent = loadTimeData.getString('hideButton'); 833 requestFunction(); 834 } else { 835 $(sectionId).hidden = true; 836 $(buttonId).textContent = loadTimeData.getString('showButton'); 837 } 838 } 839} 840 841var powerUI = { 842 showBatteryChargeData: showBatteryChargeData, 843 showCpuIdleData: showCpuIdleData, 844 showCpuFreqData: showCpuFreqData 845}; 846 847document.addEventListener('DOMContentLoaded', function() { 848 $('battery-charge-section').hidden = true; 849 $('battery-charge-show-button').onclick = showHideCallback( 850 'battery-charge-section', 851 'battery-charge-show-button', 852 requestBatteryChargeData); 853 $('battery-charge-reload-button').onclick = requestBatteryChargeData; 854 $('sample-count-input').onclick = requestBatteryChargeData; 855 856 $('cpu-idle-section').hidden = true; 857 $('cpu-idle-show-button').onclick = showHideCallback( 858 'cpu-idle-section', 'cpu-idle-show-button', requestCpuIdleData); 859 $('cpu-idle-reload-button').onclick = requestCpuIdleData; 860 861 $('cpu-freq-section').hidden = true; 862 $('cpu-freq-show-button').onclick = showHideCallback( 863 'cpu-freq-section', 'cpu-freq-show-button', requestCpuFreqData); 864 $('cpu-freq-reload-button').onclick = requestCpuFreqData; 865}); 866