chromium/chrome/browser/resources/chromeos/arc_overview_tracing/arc_overview_tracing_ui.js

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

/**
 * @fileoverview Overview Tracing UI.
 */

/**
 * @type {Array<string>}.
 * List of available colors to be used in charts. Each model is associated with
 * the same color in all charts.
 */
const chartColors = [
  '#e6194B',
  '#3cb44b',
  '#4363d8',
  '#f58231',
  '#911eb4',
  '#42d4f4',
  '#f032e6',
  '#469990',
  '#9A6324',
  '#800000',
  '#808000',
  '#000075',
];

/**
 * @type {Array<Object>}.
 * Array of models to display.
 */
const models = [];

/**
 * @type {Array<string>}.
 * Array of taken colors and it is used to prevent several models are displayed
 * in the same color.
 */
const takenColors = [];

/**
 * Maps model to the associated color.
 */
const modelColors = new Map();

/**
 * Frame time based on 60 FPS.
 */
const targetFrameTime = 16667;

function initializeOverviewUi() {
  initializeUi(8 /* zoomLevel */, function() {
    // Update function.
    refreshModels();
  });
}

/**
 * Helper that calculates overall frequency of events.
 *
 * TODO(matvore): Use |information.(app|perceived)_fps| and delete this function
 * once https://crrev.com/c/5554144 has been around for a while.
 *
 * @param {Events} events events to analyze.
 * @param {number} duration duration of analyzed period.
 */
function calculateFPS(events, duration) {
  let eventCount = 0;
  let index = events.getFirstEvent();
  while (index >= 0) {
    ++eventCount;
    index = events.getNextEvent(index, 1 /* direction */);
  }
  // Duration in micro-seconds.
  return eventCount * 1000000 / duration;
}

/**
 * Helper that calculates render quality and commit deviation. This follows the
 * calculation in |ArcAppPerformanceTracingSession|.
 *
 * @param {Object} model model to process.
 */
function calculateAppRenderQualityAndCommitDeviation(model) {
  const deltas = createDeltaEvents(getGraphicsEvents(model, kExoSurfaceCommit));

  let vsyncErrorDeviationAccumulator = 0.0;
  // Frame delta in microseconds.
  for (let i = 0; i < deltas.events.length; i++) {
    const displayFramesPassed =
        Math.round(deltas.events[i][2] / targetFrameTime);
    const vsyncError =
        deltas.events[i][2] - displayFramesPassed * targetFrameTime;
    vsyncErrorDeviationAccumulator += (vsyncError * vsyncError);
  }
  const commitDeviation =
      Math.sqrt(vsyncErrorDeviationAccumulator / deltas.events.length);

  // Sort by time delta.
  deltas.events.sort(function(a, b) {
    return a[2] - b[2];
  });

  if (deltas.events.length < 3) {
    return [
      0.0 /* % */, 0.0, /* ms */
    ];
  }

  // Get 10% and 90% indices.
  const lowerPosition = Math.round(deltas.events.length / 10);
  const upperPosition = deltas.events.length - 1 - lowerPosition;
  const renderQuality =
      deltas.events[lowerPosition][2] / deltas.events[upperPosition][2];

  return [
    renderQuality * 100.0 /* convert to % */,
    commitDeviation * 0.001, /* mcs to ms */
  ];
}

/**
 * Gets model title as an traced app name. If no information is available it
 * returns default name for app.
 *
 * @param {Object} model model to process.
 */
function getModelTitle(model) {
  return model.information.title ? model.information.title : 'Unknown app';
}

/**
 * Creates view that describes particular model. It shows all relevant
 * information and allows remove the model from the view.
 *
 * @param {Object} model model to process.
 */
function addModelHeader(model) {
  const header = $('arc-overview-tracing-model-template').cloneNode(true);
  header.hidden = false;
  const totalPowerElement =
      header.getElementsByClassName('arc-tracing-app-power-total')[0];
  const cpuPowerElement =
      header.getElementsByClassName('arc-tracing-app-power-cpu')[0];
  const gpuPowerElement =
      header.getElementsByClassName('arc-tracing-app-power-gpu')[0];
  const memoryPowerElement =
      header.getElementsByClassName('arc-tracing-app-power-memory')[0];
  totalPowerElement.parentNode.style.display = 'none';

  if (model.information.icon) {
    header.getElementsByClassName('arc-tracing-app-icon')[0].src =
        'data:image/png;base64,' + model.information.icon;
  }
  header.getElementsByClassName('arc-tracing-app-title')[0].textContent =
      getModelTitle(model);
  const date = model.information.timestamp ?
      new Date(model.information.timestamp).toLocaleString() :
      'Unknown date';
  header.getElementsByClassName('arc-tracing-app-date')[0].textContent = date;
  const duration = (model.information.duration * 0.000001).toFixed(2);
  header.getElementsByClassName('arc-tracing-app-duration')[0].textContent =
      duration;
  const platform = model.information.platform ? model.information.platform :
                                                'Unknown platform';
  header.getElementsByClassName('arc-tracing-app-platform')[0].textContent =
      platform;

  function setFPS(cssClass, type) {
    const fps = calculateFPS(
        getGraphicsEvents(model, type), model.information.duration);
    header.getElementsByClassName(cssClass)[0].textContent = fps.toFixed(2);
  }
  setFPS('arc-tracing-app-fps', kExoSurfaceCommit);
  setFPS('arc-tracing-perceived-fps', kChromeOSPresentationDone);

  const renderQualityAndCommitDeviation =
      calculateAppRenderQualityAndCommitDeviation(model);
  header.getElementsByClassName('arc-tracing-app-render-quality')[0]
      .textContent = renderQualityAndCommitDeviation[0].toFixed(1) + '%';
  header.getElementsByClassName('arc-tracing-app-commit-deviation')[0]
      .textContent = renderQualityAndCommitDeviation[1].toFixed(2) + 'ms';

  const cpuPower = getAveragePower(model, 10 /* kCpuPower */);
  const gpuPower = getAveragePower(model, 11 /* kGpuPower */);
  const memoryPower = getAveragePower(model, 12 /* kMemoryPower */);
  if (cpuPower !== -1 && gpuPower !== -1 && memoryPower !== -1) {
    totalPowerElement.parentNode.style.display = 'block';
    totalPowerElement.textContent =
        (cpuPower + gpuPower + memoryPower).toFixed(2);
    cpuPowerElement.textContent = cpuPower.toFixed(2);
    gpuPowerElement.textContent = gpuPower.toFixed(2);
    memoryPowerElement.textContent = memoryPower.toFixed(2);
  }

  // Handler to remove model from the view.
  header.getElementsByClassName('arc-tracing-close-button')[0].onclick =
      function() {
    removeModel(model);
  };

  header.getElementsByClassName('arc-tracing-dot')[0].style.backgroundColor =
      modelColors.get(model);

  $('arc-overview-tracing-models').appendChild(header);
}

/**
 * Helper that extracts graphics events of a given type.
 *
 * @param {object} model source model whose graphics events to filter
 * @param {number} eventType type of event to extract
 */
function getGraphicsEvents(model, eventType) {
  const events = [];
  const extractor = new Events(model.graphics_events, eventType);
  let index = extractor.getFirstEvent();
  while (index >= 0) {
    events.push(extractor.events[index]);
    index = extractor.getNextEvent(index, 1 /* direction */);
  }

  return new Events(events, eventType);
}

/**
 * Helper that analyzes power events of particular type, calculates overall
 * energy consumption and returns average power between first and last event.
 *
 * @param {object} model source model to analyze.
 * @param {number} eventType event type to match particular power counter
 * @returns {number} average power in watts or -1 in case no events found.
 */
function getAveragePower(model, eventType) {
  const events = new Events(model.system.memory, eventType);
  let lastTimestamp = 0;
  let totalEnergy = 0;
  let index = events.getFirstEvent();
  while (index >= 0) {
    const timestamp = events.events[index][1];
    totalEnergy +=
        events.events[index][2] * (timestamp - lastTimestamp) * 0.001;
    lastTimestamp = timestamp;
    index = events.getNextEvent(index, 1 /* direction */);
  }

  if (!lastTimestamp) {
    return -1;
  }

  return totalEnergy / lastTimestamp;
}

/**
 * Creates events as a smoothed event frequency.
 *
 * @param events source events to analyze.
 * @param {number} duration duration to analyze in microseconds.
 * @param {windowSize} window size to smooth values.
 * @param {step} step to generate next results in microseconds.
 */
function createFPSEvents(events, duration, windowSize, step) {
  const fpsEvents = [];
  let timestamp = 0;
  let index = events.getFirstEvent();
  while (timestamp < duration) {
    let windowFromTimestamp = timestamp - windowSize / 2;
    let windowToTimestamp = timestamp + windowSize / 2;
    // Clamp ranges.
    if (windowToTimestamp > duration) {
      windowFromTimestamp = duration - windowSize;
      windowToTimestamp = duration;
    }
    if (windowFromTimestamp < 0) {
      windowFromTimestamp = 0;
      windowToTimestamp = windowSize;
    }
    while (index >= 0 && events.events[index][1] < windowFromTimestamp) {
      index = events.getNextEvent(index, 1 /* direction */);
    }
    let frames = 0;
    let scanIndex = index;
    while (scanIndex >= 0 && events.events[scanIndex][1] < windowToTimestamp) {
      ++frames;
      scanIndex = events.getNextEvent(scanIndex, 1 /* direction */);
    }
    frames = frames * 1000000 / windowSize;
    fpsEvents.push([0 /* type does not matter */, timestamp, frames]);
    timestamp = timestamp + step;
  }

  return new Events(fpsEvents, 0, 0);
}

/**
 * Creates events as a time difference between events.
 *
 * @param events source events to analyze.
 */
function createDeltaEvents(events) {
  const timeEvents = [];
  const timestamp = 0;
  let lastIndex = events.getFirstEvent();
  while (lastIndex >= 0) {
    const index = events.getNextEvent(lastIndex, 1 /* direction */);
    if (index < 0) {
      break;
    }
    const delta = events.events[index][1] - events.events[lastIndex][1];
    timeEvents.push(
        [0 /* type does not mattter */, events.events[index][1], delta]);
    lastIndex = index;
  }

  return new Events(timeEvents, 0, 0);
}

/**
 * Creates view that shows CPU frequency.
 *
 * @param {HTMLElement} parent container for the newly created view.
 * @param {number} resolution scale of the chart in microseconds per pixel.
 * @param {number} duration length of the chart in microseconds.
 */
function addCPUFrequencyView(parent, resolution, duration) {
  // Range from 0 to 3GHz
  // 50MHz  1 pixel resolution
  const bands = createChart(
      parent, 'CPU Frequency' /* title */, resolution, duration,
      60 /* height */, 5 /* gridLinesCount */);
  const attributesTemplate =
      Object.assign({}, valueAttributes[9 /* kCpuFrequency */]);
  for (i = 0; i < models.length; i++) {
    const attributes = Object.assign({}, attributesTemplate);
    attributes.color = modelColors.get(models[i]);
    bands.addChartSources(
        [new Events(models[i].system.memory, 9 /* kCpuFrequency */)],
        true /* smooth */, attributes);
  }
}

/**
 * Creates view that shows CPU temperature.
 *
 * @param {HTMLElement} parent container for the newly created view.
 * @param {number} resolution scale of the chart in microseconds per pixel.
 * @param {number} duration length of the chart in microseconds.
 */
function addCPUTempView(parent, resolution, duration) {
  // Range from 20 to 100 celsius
  // 2 celsius 1 pixel resolution
  const bands = createChart(
      parent, 'CPU Temperature' /* title */, resolution, duration,
      40 /* height */, 3 /* gridLinesCount */);
  const attributesTemplate =
      Object.assign({}, valueAttributes[8 /* kCpuTemperature */]);
  attributesTemplate.minValue = 20000;
  attributesTemplate.maxValue = 100000;
  for (i = 0; i < models.length; i++) {
    const attributes = Object.assign({}, attributesTemplate);
    attributes.color = modelColors.get(models[i]);
    bands.addChartSources(
        [new Events(models[i].system.memory, 8 /* kCpuTemperature */)],
        true /* smooth */, attributes);
  }
}

/**
 * Creates view that shows GPU frequency.
 *
 * @param {HTMLElement} parent container for the newly created view.
 * @param {number} resolution scale of the chart in microseconds per pixel.
 * @param {number} duration length of the chart in microseconds.
 */
function addGPUFrequencyView(parent, resolution, duration) {
  // Range from 300MHz to 1GHz
  // 14MHz  1 pixel resolution
  const bands = createChart(
      parent, 'GPU Frequency' /* title */, resolution, duration,
      50 /* height */, 4 /* gridLinesCount */);
  const attributesTemplate =
      Object.assign({}, valueAttributes[7 /* kGpuFrequency */]);
  attributesTemplate.minValue = 300;   // Mhz
  attributesTemplate.maxValue = 1000;  // Mhz
  for (i = 0; i < models.length; i++) {
    const attributes = Object.assign({}, attributesTemplate);
    attributes.color = modelColors.get(models[i]);
    bands.addChartSources(
        [new Events(models[i].system.memory, 7 /* kGpuFrequency */)],
        false /* smooth */, attributes);
  }
}

/**
 * Creates view that shows FPS based on a sequence of swap or commit events.
 *
 * @param {HTMLElement} parent container for the newly created view.
 * @param {number} resolution scale of the chart in microseconds per pixel.
 * @param {number} duration length of the chart in microseconds.
 * @param {string} title the title of the view
 * @param {number} eventType type of the event whose rate to track
 */
function addFPSView(parent, resolution, duration, title, eventType) {
  // FPS range from 10 to 70.
  // 1 fps 1 pixel resolution.
  const bands = createChart(
      parent, title, resolution, duration, 60 /* height */,
      5 /* gridLinesCount */);

  const exportFrameTimes = function(event) {
    // To prevent further handling.
    event.stopPropagation();

    let content = '';
    let fileName = '';
    const modelEvents = [];
    for (i = 0; i < models.length; i++) {
      if (i > 0) {
        content += ',';
      }
      content += models[i].information.title;

      const events = getGraphicsEvents(models[i], eventType);
      modelEvents.push(createDeltaEvents(events));
    }
    fileName = content.replace(',', '_') + '_frame_times.csv';
    content += '\n';
    let index = 0;
    while (true) {
      let line = '';
      let dataExists = false;
      for (i = 0; i < models.length; i++) {
        if (i > 0) {
          line += ',';
        }
        if (modelEvents[i].events.length <= index) {
          continue;
        }
        line += (modelEvents[i].events[index][2] * 0.001).toFixed(2);  // In ms.
        dataExists = true;
      }
      if (!dataExists) {
        break;
      }
      content += line;
      content += '\n';
      index++;
    }

    const contentType = 'text/csv';
    const a = document.createElement('a');
    const blob = new Blob([content], {'type': contentType});
    a.href = window.URL.createObjectURL(blob);
    a.download = fileName;
    a.click();
  };

  bands.createTitleInput(
      'button', 'Export frame times', false, exportFrameTimes);

  const attributesTemplate =
      {maxValue: 70, minValue: 10, name: 'fps', scale: 1.0, width: 1.0};
  for (i = 0; i < models.length; i++) {
    const attributes = Object.assign({}, attributesTemplate);
    const events = getGraphicsEvents(models[i], eventType);
    const fpsEvents = createFPSEvents(
        events, duration, 200000 /* windowSize, 0.2s */, targetFrameTime);
    attributes.color = modelColors.get(models[i]);
    bands.addChartSources([fpsEvents], true /* smooth */, attributes);
  }
}

/**
 * Creates view that shows FPS histograms based on app and ChromeOS updates.
 *
 * @param {HTMLElement} parent container for the newly created view.
 * @param {HTMLElement} anchor insert point. View will be added after this.
 * @param {boolean} timeBasedView set to true if histograms are frame times
 *                                based. Otherwise historams contain frame
 *                                count.
 * @param {array[number]} eventTypes array of numeric event types corresponding
 *                                   with frame shown events in each histogram
 */
function addFPSHistograms(parent, anchor, timeBasedView, eventTypes) {
  // Define the width of each bar based on number of models in view.
  let barWidth;
  if (models.length === 1) {
    barWidth = 20;
  } else if (models.length === 2) {
    barWidth = 15;
  } else if (models.length === 3) {
    barWidth = 12;
  } else {
    barWidth = 10;
  }

  const titleYOffset = 12;
  const titleXOffset = 5;
  const titleWidth = 40;
  // Centers of baskets. Main points are 60/0.5 60/1.0, 60/1.5 ...
  const basketFPSs = [90, 60, 40, 30, 24, 20, 17, 15, 12, 10, 8, 6, 4, 2];
  // Minimums of baskets set manually to avoid fractional FPS in output.
  const basketMinFPSs = [70, 50, 35, 26, 22, 19, 16, 13, 11, 9, 7, 5, 3, 0];
  const basketGap = 4;
  const barGap = 2;
  const fullBarsWidth = barWidth * models.length + barGap * (models.length - 1);
  const fullBasketsWidth =
      fullBarsWidth * basketFPSs.length + basketGap * (basketFPSs.length - 1);
  const fullSectionWidth = titleXOffset + titleWidth + fullBasketsWidth;

  // Both for App and for ChromeOS view.
  const totalWidth = fullSectionWidth * 2;

  const title = timeBasedView ? 'SPF Histograms' : 'FPS Histograms';
  const bands = createChart(
      parent, title, 1 /* resolution */, totalWidth /* duration */,
      80 /* height */, 5 /* gridLinesCount */, anchor);

  const viewHandler = function(event, timeBasedView) {
    // To prevent further handling.
    event.stopPropagation();
    // TODO (b:238656897): make it more robust.
    // Section consists of 2 elements: header and SVG view.
    parent.removeChild(anchor.nextSibling);
    parent.removeChild(anchor.nextSibling);
    addFPSHistograms(parent, anchor, timeBasedView, eventTypes);
  };

  const viewHandlerTimeBasedView = function(event) {
    viewHandler(event, true);
  };
  const viewHandlerCountView = function(event) {
    viewHandler(event, false);
  };

  bands.createTitleInput(
      'radio', 'Frame count', !timeBasedView, viewHandlerCountView);
  bands.createTitleInput(
      'radio', 'Frame time', timeBasedView, viewHandlerTimeBasedView);

  bands.addChartText('App', titleXOffset, titleYOffset, 'start' /* anchor */);
  bands.addChartText(
      'Perceived', titleXOffset + fullSectionWidth, titleYOffset,
      'start' /* anchor */);

  const fpsBandYOffset = 80;

  for (let t = 0; t < eventTypes.length; ++t) {
    // Create band with FPSs.
    for (let i = 0; i < basketFPSs.length; ++i) {
      const x = fullSectionWidth * t         // Section offset
          + titleXOffset + titleWidth        // Title offset
          + (fullBarsWidth + basketGap) * i  // Bars begin for this basket.
          + fullBarsWidth * 0.5;             // Center of bars.
      bands.addChartText(
          basketFPSs[i].toString(), x, fpsBandYOffset, 'middle' /* anchor */);
    }

    for (let m = 0; m < models.length; ++m) {
      const events = getGraphicsEvents(models[m], eventTypes[t]);
      // Presort deltas between frames. Fastest one goes first.
      const deltas = createDeltaEvents(events);
      if (deltas.events.length === 0) {
        // Nothing to display.
        continue;
      }

      // Fastest frame goes first.
      deltas.events.sort(function(a, b) {
        return a[2] - b[2];  // Delta between frames.
      });

      // Calculate basket values.
      let index = 0;
      let lastIndex = 0;
      let basketIndex = 0;
      // Keep frame count for baskets.
      const basketCountValues = Array(basketFPSs.length).fill(0);
      // Keep total time of frames for basket.
      const basketTimeValues = Array(basketFPSs.length).fill(0);

      // Maximum basket in context of frame count.
      let basketCountValueMax = 0;
      // Maximum basket in context of total frame times.
      let basketTimeValueMax = 0;

      let totalTime = 0;
      let basketTime = 0;

      while (true) {
        if (index < deltas.events.length) {
          const frameTimeSeconds = deltas.events[index][2] * 0.000001;
          const fps = 1.0 / frameTimeSeconds;
          if (fps > basketMinFPSs[basketIndex]) {
            // This frame is in the current basket.
            basketTime += frameTimeSeconds;
            totalTime += frameTimeSeconds;
            ++index;
            continue;
          }
        }

        // Fill up current basket.
        basketCountValues[basketIndex] = index - lastIndex;
        basketTimeValues[basketIndex] = basketTime;

        // Update maximums.
        if (basketCountValues[basketIndex] > basketCountValueMax) {
          basketCountValueMax = basketCountValues[basketIndex];
        }
        if (basketTimeValues[basketIndex] > basketTimeValueMax) {
          basketTimeValueMax = basketTimeValues[basketIndex];
        }

        // Go the next basket or stop if this was last one.
        ++basketIndex;
        if (basketIndex === basketFPSs.length) {
          break;
        }

        // Reset counters for the next basket.
        basketTime = 0;
        lastIndex = index;
      }

      // Create bars
      const maxBarHeight = 60;
      const barYBase = 60;
      const color = modelColors.get(models[m]);

      let barX = fullSectionWidth * t  // Section offset
          + titleXOffset + titleWidth  // Title offset
          + (barWidth + barGap) * m;   // Model offset of first bar.
      for (let b = 0; b < basketFPSs.length; ++b) {
        let tooltip = '';
        let barHeight = 0;
        if (timeBasedView) {
          barHeight = maxBarHeight * basketTimeValues[b] / basketTimeValueMax;
          let basketInfo = '';
          if (b === 0) {
            basketInfo =
                'Frame time <= ' + (1000.0 / basketMinFPSs[b]).toFixed(1) +
                'ms.';
          } else if (b !== basketFPSs.length - 1) {
            basketInfo = 'Frame time range (' +
                (1000.0 / basketMinFPSs[b - 1]).toFixed(1) + '..' +
                (1000.0 / basketMinFPSs[b]).toFixed(1) + '] ms.';
          } else {
            basketInfo = 'Frame time > ' +
                (1000.0 / basketMinFPSs[b - 1]).toFixed(1) + ' ms.';
          }
          const percent = 100.0 * basketTimeValues[b] / totalTime;
          tooltip = basketInfo + '\n' + percent.toFixed(1) + '% (' +
              +basketTimeValues[b].toFixed(1).toString() + ' of ' +
              totalTime.toFixed(1).toString() + ' sec)';
        } else {
          barHeight = maxBarHeight * basketCountValues[b] / basketCountValueMax;
          let basketInfo = '';
          if (b === 0) {
            basketInfo = 'Frame FPS >= ' + basketMinFPSs[b].toString();
          } else {
            basketInfo = 'Frame FPS range (' + basketMinFPSs[b - 1].toString() +
                '..' + basketMinFPSs[b] + '].';
          }
          const percent = 100.0 * basketCountValues[b] / deltas.events.length;
          tooltip = basketInfo + '\n' + percent.toFixed(1) + '% (' +
              +basketCountValues[b].toString() + ' of ' +
              deltas.events.length.toString() + ' frames)';
        }
        bands.addChartBar(
            barX, barYBase - barHeight, barWidth, barHeight, color);
        bands.addChartTooltip(
            barX, barYBase - maxBarHeight, barWidth, maxBarHeight, tooltip,
            190 /* width */, 40 /* height */);
        barX += (fullBarsWidth + basketGap);
      }
    }
  }
}

/**
 * Creates view that shows commit time delta for app or swap time delta for
 * ChromeOS updates.
 *
 * @param {HTMLElement} parent container for the newly created view.
 * @param {number} resolution scale of the chart in microseconds per pixel.
 * @param {number} duration length of the chart in microseconds.
 * @param {string} title the title of the view
 * @param {number} eventType type of event whose rate to track
 * @param {number} jankEventType type of event indicating a jank (optional)
 */
function addDeltaView(
    parent, resolution, duration, title, eventType, jankEventType) {
  // time range from 0 to 67ms. 66.67ms is for 15 FPS.
  // 1 ms 1 pixel resolution.  Each grid lines correspond 1/120 FPS time update.
  const bands = createChart(
      parent, title, resolution, duration, 67 /* height */,
      7 /* gridLinesCount */);
  const attributesTemplate = {
    maxValue: 67000,  // microseconds
    minValue: 0,
    name: 'ms',
    scale: 1.0 / 1000.0,
    width: 1.0,
  };
  for (i = 0; i < models.length; i++) {
    const attributes = Object.assign({}, attributesTemplate);
    const events = getGraphicsEvents(models[i], eventType);
    const timeEvents = createDeltaEvents(events);
    attributes.color = modelColors.get(models[i]);
    bands.addChartSources([timeEvents], false /* smooth */, attributes);
    if (jankEventType) {
      // Offset each model's janks at a different y position, avoiding max and
      // min positions (0 or 1), as these are awkward when the models are few.
      const y = (i + 1) / (models.length + 1);
      bands.addGlobal(
          getGraphicsEvents(models[i], jankEventType), 'circle',
          attributes.color, y);
    }
  }
}

/**
 * TODO(b/182801299): kernel support was removed for non-root process.
 * Not using feature for now to prevent confusing users.
 *
 * Creates power view for the particular counter.
 *
 * @param {HTMLElement} parent container for the newly created chart.
 * @param {string} title of the chart.
 * @param {number} resolution scale of the chart in microseconds per pixel.
 * @param {number} duration length of the chart in microseconds.
 * @param {number} eventType event type to match particular power counter.
 */
function addPowerView(parent, title, resolution, duration, eventType) {
  let bands = null;
  const attributesTemplate = {
    maxValue: 10000,
    minValue: 0,
    name: 'watts',
    scale: 1.0 / 1000,
    width: 1.0,
  };
  for (i = 0; i < models.length; i++) {
    const events = new Events(models[i].system.memory, eventType, eventType);
    if (events.getFirstEvent() < 0) {
      continue;
    }
    if (bands === null) {
      // power range from 0 to 10000 milli-watts.
      // 200 milli-watts 1 pixel resolution. Each grid line 2 watts
      bands = createChart(
          parent, title, resolution, duration, 50 /* height */,
          4 /* gridLinesCount */);
    }
    const attributes = Object.assign({}, attributesTemplate);
    attributes.color = modelColors.get(models[i]);
    bands.addChartSources([events], false /* smooth */, attributes);
  }
}

/**
 * Refreshes view, remove all content and creates new one from all available
 * models.
 */
function refreshModels() {
  // Clear previous content.
  $('arc-event-bands').textContent = '';
  $('arc-overview-tracing-models').textContent = '';

  if (models.length === 0) {
    return;
  }

  // Microseconds per pixel. 100% zoom corresponds to 100 mcs per pixel.
  const resolution = zooms[zoomLevel];
  const parent = $('arc-event-bands');

  let duration = 0;
  for (i = 0; i < models.length; i++) {
    duration = Math.max(duration, models[i].information.duration);
  }

  for (i = 0; i < models.length; i++) {
    addModelHeader(models[i]);
  }

  addCPUFrequencyView(parent, resolution, duration);
  addCPUTempView(parent, resolution, duration);
  addGPUFrequencyView(parent, resolution, duration);

  addFPSView(parent, resolution, duration, 'App FPS', kExoSurfaceCommit);
  addDeltaView(
      parent, resolution, duration, 'App commit time', kExoSurfaceCommit,
      kExoSurfaceCommitJank);
  addDeltaView(
      parent, resolution, duration, 'ChromeOS swap time', kChromeOSSwapDone,
      kChromeOSSwapJank);
  addFPSView(
      parent, resolution, duration, 'Perceived FPS', kChromeOSPresentationDone);
  addDeltaView(
      parent, resolution, duration, 'Perceived swap time',
      kChromeOSPresentationDone, kChromeOSPerceivedJank);

  addFPSHistograms(
      parent, parent.lastChild, false /* timeBasedView */,
      [kExoSurfaceCommit, kChromeOSPresentationDone]);
  addPowerView(
      parent, 'Package power constraint', resolution, duration,
      13 /* eventType */);
  addPowerView(parent, 'CPU Power', resolution, duration, 10 /* eventType */);
  addPowerView(parent, 'GPU Power', resolution, duration, 11 /* eventType */);
  addPowerView(
      parent, 'Memory Power', resolution, duration, 12 /* eventType */);
}

/**
 * Assigns color for the model. Tries to be persistent in different runs. It
 * uses timestamp as a source for hash that points to the ideal color. If that
 * color is already taken for another chart, it scans all possible colors and
 * selects the first available. If nothing helps, pink color as assigned as a
 * fallback.
 *
 * @param model model to assign color to.
 */
function setModelColor(model) {
  // Try to assing color bound to timestamp.
  if (model.information && model.information.timestamp) {
    const color = chartColors[
        Math.trunc(model.information.timestamp * 0.001) % chartColors.length];
    if (!takenColors.includes(color)) {
      modelColors.set(model, color);
      takenColors.push(color);
      return;
    }
  }
  // Just find avaiable.
  for (let i = 0; i < chartColors.length; i++) {
    if (!takenColors.includes(chartColors[i])) {
      modelColors.set(model, chartColors[i]);
      takenColors.push(chartColors[i]);
      return;
    }
  }

  // Nothing helps.
  modelColors.set(model, '#ffc0cb');
}

/**
 * Adds model to the view and refreshes everything.
 *
 * @param {object} model to add.
 */
function addModel(model) {
  models.push(model);

  setModelColor(model);
  const graphicsEvents = [];
  function mergeEvents(es) {
    graphicsEvents.push(...es);
  }

  mergeEvents(model.chrome.global_events);
  model.chrome.buffers.forEach(mergeEvents);
  delete model.chrome;

  model.views.forEach(function(v) {
    mergeEvents(v.global_events);
    v.buffers.forEach(mergeEvents);
  });
  delete model.views;

  // Sort by timestamp.
  graphicsEvents.sort(function(a, b) {
    return a[1] - b[1];
  });
  model.graphics_events = graphicsEvents;

  refreshModels();
}

/**
 * Removes model from the view and refreshes everything.
 *
 * @param {object} model to add.
 */
function removeModel(model) {
  let index = models.indexOf(model);
  if (index === -1) {
    return;
  }

  models.splice(index, 1);

  index = takenColors.indexOf(modelColors.get(model));
  if (index !== -1) {
    takenColors.splice(index, 1);
  }

  modelColors.delete(model);

  refreshModels();
}