chromium/chrome/browser/resources/chromeos/power.js

// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import './strings.m.js';

import {sendWithPromise} from 'chrome://resources/ash/common/cr.m.js';
import {loadTimeData} from 'chrome://resources/ash/common/load_time_data.m.js';
import {$} from 'chrome://resources/ash/common/util.js';

const devicePixelRatio = window.devicePixelRatio;

/**
 * Plot a line graph of data versus time on a HTML canvas element.
 *
 * @param {HTMLCanvasElement} plotCanvas The canvas on which the line graph is
 *     drawn.
 * @param {HTMLCanvasElement} legendCanvas The canvas on which the legend for
 *     the line graph is drawn.
 * @param {Array<number>} tData The time (in seconds) in the past when the
 *     corresponding data in plots was sampled.
 * @param {Array<{data: Array<number>, color: string}>} plots An
 *     array of plots to plot on the canvas. The field 'data' of a plot is an
 *     array of samples to be plotted as a line graph with color speficied by
 *     the field 'color'. The elements in the 'data' array are ordered
 *     corresponding to their sampling time in the argument 'tData'. Also, the
 *     number of elements in the 'data' array should be the same as in the time
 *     array 'tData' above.
 * @param {number} yMin Minimum bound of y-axis
 * @param {number} yMax Maximum bound of y-axis.
 * @param {number} yPrecision An integer value representing the number of
 *     digits of precision the y-axis data should be printed with.
 */
function plotLineGraph(
    plotCanvas, legendCanvas, tData, plots, yMin, yMax, yPrecision) {
  const textFont = 12 * devicePixelRatio + 'px Arial';
  const textHeight = 12 * devicePixelRatio;
  const padding = 5 * devicePixelRatio;  // Pixels
  const errorOffsetPixels = 15 * devicePixelRatio;
  const gridColor = '#ccc';
  const plotCtx = plotCanvas.getContext('2d');
  const size = tData.length;

  function drawText(ctx, text, x, y) {
    ctx.font = textFont;
    ctx.fillStyle = '#000';
    ctx.fillText(text, x, y);
  }

  function printErrorText(ctx, text) {
    ctx.clearRect(0, 0, plotCanvas.width, plotCanvas.height);
    drawText(ctx, text, errorOffsetPixels, errorOffsetPixels);
  }

  if (size < 2) {
    printErrorText(
        plotCtx, loadTimeData.getString('notEnoughDataAvailableYet'));
    return;
  }

  for (let count = 0; count < plots.length; count++) {
    if (plots[count].data.length !== size) {
      throw new Error('Mismatch in time and plot data.');
    }
  }

  function valueToString(value) {
    if (Math.abs(value) < 1) {
      return Number(value).toFixed(yPrecision - 1);
    } else {
      return Number(value).toPrecision(yPrecision);
    }
  }

  function getTextWidth(ctx, text) {
    ctx.font = textFont;
    // For now, all text is drawn to the left of vertical lines, or centered.
    // Add a 2 pixel padding so that there is some spacing between the text
    // and the vertical line.
    return Math.round(ctx.measureText(text).width) + 2 * devicePixelRatio;
  }

  function getLegend(text) {
    return ' ' + text + '    ';
  }

  function drawHighlightText(ctx, text, x, y, color) {
    ctx.strokeStyle = '#000';
    ctx.strokeRect(x, y - textHeight, getTextWidth(ctx, text), textHeight);
    ctx.fillStyle = color;
    ctx.fillRect(x, y - textHeight, getTextWidth(ctx, text), textHeight);
    ctx.fillStyle = '#fff';
    ctx.fillText(text, x, y);
  }

  function drawLine(ctx, x1, y1, x2, y2, color) {
    ctx.save();
    ctx.beginPath();
    ctx.moveTo(x1, y1);
    ctx.lineTo(x2, y2);
    ctx.strokeStyle = color;
    ctx.lineWidth = 1 * devicePixelRatio;
    ctx.stroke();
    ctx.restore();
  }

  // The strokeRect method of the 2d context of a plotCanvas draws a bounding
  // rectangle with an offset origin and greater dimensions. Hence, use this
  // function to draw a rect at the desired location with desired dimensions.
  function drawRect(ctx, x, y, width, height, color) {
    const offset = 1 * devicePixelRatio;
    drawLine(ctx, x, y, x + width - offset, y, color);
    drawLine(ctx, x, y, x, y + height - offset, color);
    drawLine(
        ctx, x, y + height - offset, x + width - offset, y + height - offset,
        color);
    drawLine(
        ctx, x + width - offset, y, x + width - offset, y + height - offset,
        color);
  }

  function drawLegend() {
    // Show a legend only if at least one individual plot has a name.
    let valid = false;
    for (let i = 0; i < plots.length; i++) {
      if (plots[i].name != null) {
        valid = true;
        break;
      }
    }
    if (!valid) {
      legendCanvas.hidden = true;
      return;
    }


    const padding = 2 * devicePixelRatio;
    const legendSquareSide = 12 * devicePixelRatio;
    const legendCtx = legendCanvas.getContext('2d');
    let xLoc = padding;
    let yLoc = padding;
    // Adjust the height of the canvas before drawing on it.
    for (let i = 0; i < plots.length; i++) {
      if (plots[i].name == null) {
        continue;
      }
      const legendText = getLegend(plots[i].name);
      xLoc +=
          legendSquareSide + getTextWidth(legendCtx, legendText) + 2 * padding;
      if (i < plots.length - 1) {
        const xLocNext = xLoc +
            getTextWidth(legendCtx, getLegend(plots[i + 1].name)) +
            legendSquareSide;
        if (xLocNext >= legendCanvas.width) {
          xLoc = padding;
          yLoc = yLoc + 2 * padding + textHeight;
        }
      }
    }

    legendCanvas.height = yLoc + textHeight + padding;
    legendCanvas.style.height = legendCanvas.height / devicePixelRatio + 'px';

    xLoc = padding;
    yLoc = padding;
    // Go over the plots again, this time drawing the legends.
    for (let i = 0; i < plots.length; i++) {
      legendCtx.fillStyle = plots[i].color;
      legendCtx.fillRect(xLoc, yLoc, legendSquareSide, legendSquareSide);
      xLoc += legendSquareSide;

      const legendText = getLegend(plots[i].name);
      drawText(legendCtx, legendText, xLoc, yLoc + textHeight - 1);
      xLoc += getTextWidth(legendCtx, legendText) + 2 * padding;

      if (i < plots.length - 1) {
        const xLocNext = xLoc +
            getTextWidth(legendCtx, getLegend(plots[i + 1].name)) +
            legendSquareSide;
        if (xLocNext >= legendCanvas.width) {
          xLoc = padding;
          yLoc = yLoc + 2 * padding + textHeight;
        }
      }
    }
  }

  const yMinStr = valueToString(yMin);
  const yMaxStr = valueToString(yMax);
  const yHalfStr = valueToString((yMax + yMin) / 2);
  const yMinWidth = getTextWidth(plotCtx, yMinStr);
  const yMaxWidth = getTextWidth(plotCtx, yMaxStr);
  const yHalfWidth = getTextWidth(plotCtx, yHalfStr);

  const xMinStr = tData[0];
  const xMaxStr = tData[size - 1];
  const xMinWidth = getTextWidth(plotCtx, xMinStr);
  const xMaxWidth = getTextWidth(plotCtx, xMaxStr);

  const xOrigin =
      padding + Math.max(yMinWidth, yMaxWidth, Math.round(xMinWidth / 2));
  const yOrigin = padding + textHeight;
  let width = plotCanvas.width - xOrigin - Math.floor(xMaxWidth / 2) - padding;
  if (width < size) {
    plotCanvas.width += size - width;
    width = size;
  }
  const height = plotCanvas.height - yOrigin - textHeight - padding;
  const linePlotEndMarkerWidth = 3;

  function drawPlots() {
    // Start fresh.
    plotCtx.clearRect(0, 0, plotCanvas.width, plotCanvas.height);

    // Draw the bounding rectangle.
    drawRect(plotCtx, xOrigin, yOrigin, width, height, gridColor);

    // Draw the x and y bound values.
    drawText(plotCtx, yMaxStr, xOrigin - yMaxWidth, yOrigin + textHeight);
    drawText(plotCtx, yMinStr, xOrigin - yMinWidth, yOrigin + height);
    drawText(
        plotCtx, xMinStr, xOrigin - xMinWidth / 2,
        yOrigin + height + textHeight);
    drawText(
        plotCtx, xMaxStr, xOrigin + width - xMaxWidth / 2,
        yOrigin + height + textHeight);

    // Draw y-level (horizontal) lines.
    drawLine(
        plotCtx, xOrigin + 1, yOrigin + height / 4, xOrigin + width - 2,
        yOrigin + height / 4, gridColor);
    drawLine(
        plotCtx, xOrigin + 1, yOrigin + height / 2, xOrigin + width - 2,
        yOrigin + height / 2, gridColor);
    drawLine(
        plotCtx, xOrigin + 1, yOrigin + 3 * height / 4, xOrigin + width - 2,
        yOrigin + 3 * height / 4, gridColor);

    // Draw half-level value.
    drawText(
        plotCtx, yHalfStr, xOrigin - yHalfWidth,
        yOrigin + height / 2 + textHeight / 2);

    // Draw the plots.
    const yValRange = yMax - yMin;
    for (let count = 0; count < plots.length; count++) {
      const plot = plots[count];
      const yData = plot.data;
      plotCtx.strokeStyle = plot.color;
      plotCtx.lineWidth = 2;
      plotCtx.beginPath();
      let beginPath = true;
      for (let i = 0; i < size; i++) {
        const val = yData[i];
        if (typeof val === 'string') {
          // Stroke the plot drawn so far and begin a fresh plot.
          plotCtx.stroke();
          plotCtx.beginPath();
          beginPath = true;
          continue;
        }
        const xPos = xOrigin + Math.floor(i / (size - 1) * (width - 1));
        const yPos = yOrigin + height - 1 -
            Math.round((val - yMin) / yValRange * (height - 1));
        if (beginPath) {
          plotCtx.moveTo(xPos, yPos);
          // A simple move to does not print anything. Hence, draw a little
          // square here to mark a beginning.
          plotCtx.fillStyle = '#000';
          plotCtx.fillRect(
              xPos - linePlotEndMarkerWidth, yPos - linePlotEndMarkerWidth,
              linePlotEndMarkerWidth * devicePixelRatio,
              linePlotEndMarkerWidth * devicePixelRatio);
          beginPath = false;
        } else {
          plotCtx.lineTo(xPos, yPos);
          if (i === size - 1 || typeof yData[i + 1] === 'string') {
            // Draw a little square to mark an end to go with the start
            // markers from above.
            plotCtx.fillStyle = '#000';
            plotCtx.fillRect(
                xPos - linePlotEndMarkerWidth, yPos - linePlotEndMarkerWidth,
                linePlotEndMarkerWidth * devicePixelRatio,
                linePlotEndMarkerWidth * devicePixelRatio);
          }
        }
      }
      plotCtx.stroke();
    }

    // Paint the missing time intervals with |gridColor|.
    // Pick one of the plots to look for missing time intervals.
    function drawMissingRect(start, end) {
      const xLeft = xOrigin + Math.floor(start / (size - 1) * (width - 1));
      const xRight = xOrigin + Math.floor(end / (size - 1) * (width - 1));
      plotCtx.fillStyle = gridColor;
      // The x offsets below are present so that the blank space starts
      // and ends between two valid samples.
      plotCtx.fillRect(xLeft + 1, yOrigin, xRight - xLeft - 2, height - 1);
    }
    let inMissingInterval = false;
    let intervalStart;
    for (let i = 0; i < size; i++) {
      if (typeof plots[0].data[i] === 'string') {
        if (!inMissingInterval) {
          inMissingInterval = true;
          // The missing interval should actually start from the previous
          // sample.
          intervalStart = Math.max(i - 1, 0);
        }

        if (i === size - 1) {
          // If this is the last sample, just draw missing rect.
          drawMissingRect(intervalStart, i);
        }
      } else if (inMissingInterval) {
        inMissingInterval = false;
        drawMissingRect(intervalStart, i);
      }
    }
  }

  function drawTimeGuide(tDataIndex) {
    const x = xOrigin + tDataIndex / (size - 1) * (width - 1);
    drawLine(plotCtx, x, yOrigin, x, yOrigin + height - 1, '#000');
    drawText(
        plotCtx, tData[tDataIndex],
        x - getTextWidth(plotCtx, tData[tDataIndex]) / 2, yOrigin - 2);

    for (let count = 0; count < plots.length; count++) {
      const yData = plots[count].data;

      // Draw small black square on the plot where the time guide intersects
      // it.
      const val = yData[tDataIndex];
      let yPos;
      let valStr;
      if (typeof val === 'string') {
        yPos = yOrigin + Math.round(height / 2);
        valStr = val;
      } else {
        yPos = yOrigin + height - 1 -
            Math.round((val - yMin) / (yMax - yMin) * (height - 1));
        valStr = valueToString(val);
      }
      plotCtx.fillStyle = '#000';
      plotCtx.fillRect(x - 2, yPos - 2, 4, 4);

      // Draw the val to right of the intersection.
      let yLoc;
      if (yPos - textHeight / 2 < yOrigin) {
        yLoc = yOrigin + textHeight;
      } else if (yPos + textHeight / 2 >= yPos + height) {
        yLoc = yOrigin + height - 1;
      } else {
        yLoc = yPos + textHeight / 2;
      }
      drawHighlightText(plotCtx, valStr, x + 5, yLoc, plots[count].color);
    }
  }

  function onMouseOverOrMove(event) {
    drawPlots();

    const boundingRect = plotCanvas.getBoundingClientRect();
    const x =
        Math.round((event.clientX - boundingRect.left) * devicePixelRatio);
    const y = Math.round((event.clientY - boundingRect.top) * devicePixelRatio);
    if (x < xOrigin || x >= xOrigin + width || y < yOrigin ||
        y >= yOrigin + height) {
      return;
    }

    if (width === size) {
      drawTimeGuide(Math.round(x - xOrigin));
    } else {
      drawTimeGuide(Math.round((x - xOrigin) / (width - 1) * (size - 1)));
    }
  }

  function onMouseOut(event) {
    drawPlots();
  }

  drawLegend();
  drawPlots();
  plotCanvas.addEventListener('mouseover', onMouseOverOrMove);
  plotCanvas.addEventListener('mousemove', onMouseOverOrMove);
  plotCanvas.addEventListener('mouseout', onMouseOut);
}

const sleepSampleInterval = 30 * 1000;  // in milliseconds.
const sleepText = loadTimeData.getString('systemSuspended');
const invalidDataText = loadTimeData.getString('invalidData');
const offlineText = loadTimeData.getString('offlineText');

const plotColors = [
  'Red',
  'Blue',
  'Green',
  'Gold',
  'CadetBlue',
  'LightCoral',
  'LightSlateGray',
  'Peru',
  'DarkRed',
  'LawnGreen',
  'Tan',
];

/**
 * Add canvases for plotting to |plotsDiv|. For every header in |headerArray|,
 * one canvas for the plot and one for its legend are added.
 *
 * @param {Array<string>} headerArray Headers for the different plots to be
 *     added to |plotsDiv|.
 * @param {HTMLElement} plotsDiv The div element into which the canvases
 *     are added.
 * @return {!Object<{plot: !HTMLCanvasElement, legend: !HTMLCanvasElement}>}
 *   Returns an object with the headers as 'keys'. Each element is an object
 *   containing the legend canvas and the plot canvas that have been added to
 *   |plotsDiv|.
 */
function addCanvases(headerArray, plotsDiv) {
  // Remove the contents before adding new ones.
  while (plotsDiv.firstChild != null) {
    plotsDiv.removeChild(plotsDiv.firstChild);
  }
  const width = Math.floor(plotsDiv.getBoundingClientRect().width);
  const canvases = {};
  for (let i = 0; i < headerArray.length; i++) {
    const header = document.createElement('h4');
    header.textContent = headerArray[i];
    plotsDiv.appendChild(header);

    const legendCanvas =
        /** @type {!HTMLCanvasElement} */ (document.createElement('canvas'));
    legendCanvas.width = width * devicePixelRatio;
    legendCanvas.style.width = width + 'px';
    plotsDiv.appendChild(legendCanvas);

    const plotCanvasDiv = document.createElement('div');
    plotCanvasDiv.style.overflow = 'auto';
    plotsDiv.appendChild(plotCanvasDiv);

    const plotCanvas =
        /** @type {!HTMLCanvasElement} */ (document.createElement('canvas'));
    plotCanvas.width = width * devicePixelRatio;
    plotCanvas.height = 200 * devicePixelRatio;
    plotCanvas.style.height = '200px';
    plotCanvasDiv.appendChild(plotCanvas);

    canvases[headerArray[i]] = {plot: plotCanvas, legend: legendCanvas};
  }
  return canvases;
}

/**
 * Add samples in |sampleArray| to individual plots in |plots|. If the system
 * resumed from a sleep/suspend, then "suspended" sleep samples are added to
 * the plot for the sleep duration.
 *
 * @param {Array<{data: Array<number|string>, color: string}>} plots An
 *     array of plots to plot on the canvas. The field 'data' of a plot is an
 *     array of samples to be plotted as a line graph with color speficied by
 *     the field 'color'. The elements in the 'data' array are ordered
 *     corresponding to their sampling time in the argument 'tData'. Also, the
 *     number of elements in the 'data' array should be the same as in the time
 *     array 'tData' below.
 * @param {Array<string>} tData The time (in seconds) in the past when the
 *     corresponding data in plots was sampled.
 * @param {Array<number>} absTime
 * @param {Array<number>} sampleArray The array of samples wherein each
 *     element corresponds to the individual plot in |plots|.
 * @param {number} sampleTime Time in milliseconds since the epoch when the
 *     samples in |sampleArray| were captured.
 * @param {number} previousSampleTime Time in milliseconds since the epoch
 *     when the sample prior to the current sample was captured.
 * @param {Array<{time: number, sleepDuration: number}>} systemResumedArray An
 *     array objects corresponding to system resume events. The 'time' field is
 *     for the time in milliseconds since the epoch when the system resumed. The
 *     'sleepDuration' field is for the time in milliseconds the system spent
 *     in sleep/suspend state.
 */
function addTimeDataSample(
    plots, tData, absTime, sampleArray, sampleTime, previousSampleTime,
    systemResumedArray) {
  for (let i = 0; i < plots.length; i++) {
    if (plots[i].data.length !== tData.length) {
      throw new Error('Mismatch in time and plot data.');
    }
  }

  let time;
  if (tData.length === 0) {
    time = new Date(sampleTime);
    absTime[0] = sampleTime;
    tData[0] = time.toLocaleTimeString();
    for (let i = 0; i < plots.length; i++) {
      plots[i].data[0] = sampleArray[i];
    }
    return;
  }

  for (let i = 0; i < systemResumedArray.length; i++) {
    const resumeTime = systemResumedArray[i].time;
    const sleepDuration = systemResumedArray[i].sleepDuration;
    let sleepStartTime = resumeTime - sleepDuration;
    if (resumeTime < sampleTime) {
      if (sleepStartTime < previousSampleTime) {
        // This can happen if pending callbacks were handled before actually
        // suspending.
        sleepStartTime = previousSampleTime + 1000;
      }
      // Add sleep samples for every |sleepSampleInterval|.
      let sleepSampleTime = sleepStartTime;
      while (sleepSampleTime < resumeTime) {
        time = new Date(sleepSampleTime);
        absTime.push(sleepSampleTime);
        tData.push(time.toLocaleTimeString());
        for (let j = 0; j < plots.length; j++) {
          plots[j].data.push(sleepText);
        }
        sleepSampleTime += sleepSampleInterval;
      }
    }
  }

  time = new Date(sampleTime);
  absTime.push(sampleTime);
  tData.push(time.toLocaleTimeString());
  for (let i = 0; i < plots.length; i++) {
    plots[i].data.push(sampleArray[i]);
  }
}

/**
 * Display the battery charge vs time on a line graph.
 *
 * powerSupplyData:
 * An array of objects with fields representing the battery charge, time when
 * the charge measurement was taken, and whether there was external power
 * connected at that time.
 *
 * systemResumedData:
 * An array objects with fields 'time' and 'sleepDuration'. Each object
 * corresponds to a system resume event. The 'time' field is for the time in
 * milliseconds since the epoch when the system resumed. The 'sleepDuration'
 * field is for the time in milliseconds the system spent in sleep/suspend
 * state.
 *
 * @param {{
 *   powerSupplyData: Array<{time: number,
 *                           batteryPercent: number,
 *                           batteryDischargeRate: number,
 *                           externalPower: number}>,
 *   systemResumedData: Array<{time: ?, sleepDuration: ?}>
 *  }} destructedParams
 */
function showBatteryChargeData({powerSupplyData, systemResumedData}) {
  const chargeTimeData = [];
  const chargeAbsTime = [];
  const chargePlot = [{
    name: loadTimeData.getString('batteryChargePercentageHeader'),
    color: 'Blue',
    data: [],
  }];
  const dischargeRateTimeData = [];
  const dischargeRateAbsTime = [];
  const dischargeRatePlot = [
    {
      name: loadTimeData.getString('dischargeRateLegendText'),
      color: 'Red',
      data: [],
    },
    {
      name: loadTimeData.getString('movingAverageLegendText'),
      color: 'Green',
      data: [],
    },
    {
      name: loadTimeData.getString('binnedAverageLegendText'),
      color: 'Blue',
      data: [],
    },
  ];
  let minDischargeRate = 1000;   // A high unrealistic number to begin with.
  let maxDischargeRate = -1000;  // A low unrealistic number to begin with.
  for (let i = 0; i < powerSupplyData.length; i++) {
    const j = Math.max(i - 1, 0);

    addTimeDataSample(
        chargePlot, chargeTimeData, chargeAbsTime,
        [powerSupplyData[i].batteryPercent], powerSupplyData[i].time,
        powerSupplyData[j].time, systemResumedData);

    const dischargeRate = powerSupplyData[i].batteryDischargeRate;
    const inputSampleCount = $('sample-count-input').value;

    let movingAverage = 0;
    let k = 0;
    for (k = 0; k < inputSampleCount && i - k >= 0; k++) {
      movingAverage += powerSupplyData[i - k].batteryDischargeRate;
    }
    // |k| will be atleast 1 because the 'min' value of the input field is 1.
    movingAverage /= k;

    let binnedAverage = 0;
    for (k = 0; k < inputSampleCount; k++) {
      const currentSampleIndex = i - i % inputSampleCount + k;
      if (currentSampleIndex >= powerSupplyData.length) {
        break;
      }

      binnedAverage += powerSupplyData[currentSampleIndex].batteryDischargeRate;
    }
    binnedAverage /= k;

    minDischargeRate = Math.min(dischargeRate, minDischargeRate);
    maxDischargeRate = Math.max(dischargeRate, maxDischargeRate);
    addTimeDataSample(
        dischargeRatePlot, dischargeRateTimeData, dischargeRateAbsTime,
        [dischargeRate, movingAverage, binnedAverage], powerSupplyData[i].time,
        powerSupplyData[j].time, systemResumedData);
  }
  if (minDischargeRate === maxDischargeRate) {
    // This means that all the samples had the same value. Hence, offset the
    // extremes by a bit so that the plot looks good.
    minDischargeRate -= 1;
    maxDischargeRate += 1;
  }

  const plotsDiv = $('battery-charge-plots-div');

  const canvases = addCanvases(
      [
        loadTimeData.getString('batteryChargePercentageHeader'),
        loadTimeData.getString('batteryDischargeRateHeader'),
      ],
      plotsDiv);

  const batteryChargeCanvases =
      canvases[loadTimeData.getString('batteryChargePercentageHeader')];
  plotLineGraph(
      batteryChargeCanvases['plot'], batteryChargeCanvases['legend'],
      chargeTimeData, chargePlot, 0.00, 100.00, 3);

  const dischargeRateCanvases =
      canvases[loadTimeData.getString('batteryDischargeRateHeader')];
  plotLineGraph(
      dischargeRateCanvases['plot'], dischargeRateCanvases['legend'],
      dischargeRateTimeData, dischargeRatePlot, minDischargeRate,
      maxDischargeRate, 3);
}

/**
 * Shows state occupancy data (CPU idle or CPU freq state occupancy) on a set of
 * plots on the about:power UI.
 *
 * @param {Array<Array<{
 *     time: number,
 *     cpuOnline: boolean,
 *     timeInState: Object<number>}>>} timeInStateData Array of arrays
 *     where each array corresponds to a CPU on the system. The elements of the
 *     individual arrays contain state occupancy samples.
 * @param {Array<{time: ?, sleepDuration: ?}>} systemResumedArray An array
 *     objects with fields 'time' and 'sleepDuration'. Each object corresponds
 *     to a system resume event. The 'time' field is for the time in
 *     milliseconds since the epoch when the system resumed. The 'sleepDuration'
 *     field is for the time in milliseconds the system spent in sleep/suspend
 *     state.
 * @param {string} i18nHeaderString The header string to be displayed with each
 *     plot. For example, CPU idle data will have its own header format, and CPU
 *     freq data will have its header format.
 * @param {?string} unitString This is the string capturing the unit, if any,
 *     for the different states. Note that this is not the unit of the data
 *     being plotted.
 * @param {string} plotsDivId The div element's ID in which the plots should
 *     be added.
 */
function showStateOccupancyData(
    timeInStateData, systemResumedArray, i18nHeaderString, unitString,
    plotsDivId) {
  const cpuPlots = [];
  let tData;
  let absTime;
  for (let cpu = 0; cpu < timeInStateData.length; cpu++) {
    const cpuData = timeInStateData[cpu];
    if (cpuData.length === 0) {
      cpuPlots[cpu] = {plots: [], tData: []};
      continue;
    }
    tData = [];
    absTime = [];
    // Each element of |plots| is an array of samples, one for each of the CPU
    // states. The number of states is dicovered by looking at the first
    // sample for which the CPU is online.
    const plots = [];
    const stateIndexMap = [];
    let stateCount = 0;
    for (let i = 0; i < cpuData.length; i++) {
      if (cpuData[i].cpuOnline) {
        for (const state in cpuData[i].timeInState) {
          let stateName = state;
          if (unitString != null) {
            stateName += ' ' + unitString;
          }
          plots.push(
              {name: stateName, data: [], color: plotColors[stateCount]});
          stateIndexMap.push(state);
          stateCount += 1;
        }
        break;
      }
    }
    // If stateCount is 0, then it means the CPU has been offline
    // throughout. Just add a single plot for such a case.
    if (stateCount === 0) {
      plots.push({name: null, data: [], color: null});
      stateCount = 1;  // Some invalid state!
    }

    // Pass the samples through the function addTimeDataSample to add 'sleep'
    // samples.
    for (let i = 0; i < cpuData.length; i++) {
      const sample = cpuData[i];
      const valArray = [];
      for (let j = 0; j < stateCount; j++) {
        if (sample.cpuOnline) {
          valArray[j] = sample.timeInState[stateIndexMap[j]];
        } else {
          valArray[j] = offlineText;
        }
      }

      const k = Math.max(i - 1, 0);
      addTimeDataSample(
          plots, tData, absTime, valArray, sample.time, cpuData[k].time,
          systemResumedArray);
    }

    // Calculate the percentage occupancy of each state. A valid number is
    // possible only if two consecutive samples are valid/numbers.
    for (let k = 0; k < stateCount; k++) {
      const stateData = plots[k].data;
      // Skip the first sample as there is no previous sample.
      for (let i = stateData.length - 1; i > 0; i--) {
        if (typeof stateData[i] === 'number') {
          if (typeof stateData[i - 1] === 'number') {
            stateData[i] = (stateData[i] - stateData[i - 1]) /
                (absTime[i] - absTime[i - 1]) * 100;
          } else {
            stateData[i] = invalidDataText;
          }
        }
      }
    }

    // Remove the first sample from the time and data arrays.
    tData.shift();
    for (let k = 0; k < stateCount; k++) {
      plots[k].data.shift();
    }
    cpuPlots[cpu] = {plots: plots, tData: tData};
  }

  const headers = [];
  for (let cpu = 0; cpu < timeInStateData.length; cpu++) {
    headers[cpu] =
        'CPU ' + cpu + ' ' + loadTimeData.getString(i18nHeaderString);
  }

  const canvases = addCanvases(headers, $(plotsDivId));
  for (let cpu = 0; cpu < timeInStateData.length; cpu++) {
    const cpuCanvases = canvases[headers[cpu]];
    plotLineGraph(
        cpuCanvases['plot'], cpuCanvases['legend'], cpuPlots[cpu]['tData'],
        cpuPlots[cpu]['plots'], 0, 100, 3);
  }
}

/**
 * @param {{
 *   idleStateData: Array<Array<{
 *     time: number,
 *     cpuOnline: boolean,
 *     timeInState: Object<number>}>>,
 *   systemResumedData: Array<{time: ?, sleepDuration: ?}>
 * }} destructedParams
 */
function showCpuIdleData({idleStateData, systemResumedData}) {
  showStateOccupancyData(
      idleStateData, systemResumedData, 'idleStateOccupancyPercentageHeader',
      null, 'cpu-idle-plots-div');
}

/**
 * @param {{
 *   freqStateData: Array<Array<{
 *     time: number,
 *     cpuOnline: boolean,
 *     timeInState: Object<number>}>>,
 *   systemResumedData: Array<{time: ?, sleepDuration: ?}>
 * }} destructedParams
 */
function showCpuFreqData({freqStateData, systemResumedData}) {
  showStateOccupancyData(
      freqStateData, systemResumedData,
      'frequencyStateOccupancyPercentageHeader', 'MHz', 'cpu-freq-plots-div');
}

function requestBatteryChargeData() {
  sendWithPromise('requestBatteryChargeData').then(showBatteryChargeData);
}

function requestCpuIdleData() {
  sendWithPromise('requestCpuIdleData').then(showCpuIdleData);
}

function requestCpuFreqData() {
  sendWithPromise('requestCpuFreqData').then(showCpuFreqData);
}

/**
 * Return a callback for the 'Show'/'Hide' buttons for each section of the
 * about:power page.
 *
 * @param {string} sectionId The ID of the section which is to be shown or
 *     hidden.
 * @param {string} buttonId The ID of the 'Show'/'Hide' button.
 * @param {Function} requestFunction The function which should be invoked on
 *    'Show' to request for data from chrome.
 * @return {Function} The button callback function.
 */
function showHideCallback(sectionId, buttonId, requestFunction) {
  return function() {
    if ($(sectionId).hidden) {
      $(sectionId).hidden = false;
      $(buttonId).textContent = loadTimeData.getString('hideButton');
      requestFunction();
    } else {
      $(sectionId).hidden = true;
      $(buttonId).textContent = loadTimeData.getString('showButton');
    }
  };
}

document.addEventListener('DOMContentLoaded', function() {
  $('battery-charge-section').hidden = true;
  $('battery-charge-show-button').onclick = showHideCallback(
      'battery-charge-section', 'battery-charge-show-button',
      requestBatteryChargeData);
  $('battery-charge-reload-button').onclick = requestBatteryChargeData;
  $('sample-count-input').onclick = requestBatteryChargeData;

  $('cpu-idle-section').hidden = true;
  $('cpu-idle-show-button').onclick = showHideCallback(
      'cpu-idle-section', 'cpu-idle-show-button', requestCpuIdleData);
  $('cpu-idle-reload-button').onclick = requestCpuIdleData;

  $('cpu-freq-section').hidden = true;
  $('cpu-freq-show-button').onclick = showHideCallback(
      'cpu-freq-section', 'cpu-freq-show-button', requestCpuFreqData);
  $('cpu-freq-reload-button').onclick = requestCpuFreqData;
});