blob: 27d9e78240a0a7a2a89be12a037d25864c0ef53d [file] [log] [blame]
<!DOCTYPE html>
<!--
Copyright 2016 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.
-->
<link rel="import" href="/tracing/base/math/range.html">
<link rel="import" href="/tracing/base/math/statistics.html">
<link rel="import" href="/tracing/base/unit_scale.html">
<link rel="import" href="/tracing/importer/find_input_expectations.html">
<link rel="import" href="/tracing/metrics/metric_registry.html">
<link rel="import" href="/tracing/metrics/system_health/loading_metric.html">
<link rel="import" href="/tracing/value/histogram.html">
<script>
'use strict';
tr.exportTo('tr.metrics.sh', function() {
// If the power series doesn't cover the entire Chrome trace, then
// the results from Chrome tracing metrics will likely be inaccurate,
// so we don't report them. However, we allow the power series bounds
// to be up to 1 ms inside the Chrome trace and still count as
// covering the Chrome trace. This is to allow for small deviations
// due to clock sync latency and the finite sampling rate of the
// BattOr.
var CHROME_POWER_GRACE_PERIOD_MS = 1;
/**
* Creates an empty histogram to hold data for a given time interval.
*
* @returns {Object} An object of the form:
*
* {
* perSecond {boolean}: Whether the data for this time interval is given
* as per second, If not, it's given as an integral over the
* whole interval.
* energy {tr.v.Histogram}: Histogram giving energy use (i.e. energy in J
* if perSecond = False, power in W if perSecond = True)
* }
*/
function createEmptyHistogram_(interval, histograms) {
if (interval.perSecond) {
return {
perSecond: true,
energy: createPowerHistogram_(
histograms, interval.name, interval.description)
};
}
return {
perSecond: false,
energy: createEnergyHistogram_(
histograms, interval.name, interval.description)
};
}
function createHistograms_(data, interval, histograms) {
if (data.histograms[interval.name] === undefined)
data.histograms[interval.name] = createEmptyHistogram_(interval,
histograms);
if (data.histograms[interval.name].perSecond) {
for (var sample of data.model.device.powerSeries.getSamplesWithinRange(
interval.bounds.min, interval.bounds.max))
data.histograms[interval.name].energy.addSample(sample.powerInW);
} else {
var energyInJ = data.model.device.powerSeries.getEnergyConsumedInJ(
interval.bounds.min, interval.bounds.max);
data.histograms[interval.name].energy.addSample(energyInJ);
}
}
/**
* Returns the intervals of time between navigation event and time to
* interactive.
*/
function getNavigationTTIIntervals_(model) {
var chromeHelper = model.getOrCreateHelper(
tr.model.helpers.ChromeModelHelper);
var intervals = [];
for (var rendererHelper of Object.values(chromeHelper.rendererHelpers)) {
var samples = tr.metrics.sh.collectLoadingMetricsForRenderer(
rendererHelper).firstInteractiveSamples;
for (var sample of samples) {
var info = sample.diagnostics['Navigation infos'].value;
intervals.push(tr.b.math.Range.fromExplicitRange(
info.start, info.interactive));
}
}
return intervals.sort((x, y) => x.min - y.min);
}
/**
* Creates a histogram suitable for energy data.
*/
function createEnergyHistogram_(histograms, histogramName, description) {
var histogram = new tr.v.Histogram(`${histogramName}:energy`,
tr.b.Unit.byName.energyInJoules_smallerIsBetter);
histogram.customizeSummaryOptions({
avg: false,
count: false,
max: true,
min: true,
std: false,
sum: true,
});
histogram.description = 'Energy consumed in ' + description;
histograms.addHistogram(histogram);
return histogram;
}
/**
* Creates a histogram suitable for power data.
*/
function createPowerHistogram_(histograms, histogramName, description) {
var histogram = new tr.v.Histogram(`${histogramName}:power`,
tr.b.Unit.byName.powerInWatts_smallerIsBetter);
histogram.customizeSummaryOptions({
avg: true,
count: false,
max: true,
min: true,
std: false,
sum: false,
});
histogram.description = 'Energy consumption rate for ' + description;
histograms.addHistogram(histogram);
return histogram;
}
/**
* Generates the set of time intervals that metrics should be run over.
* Time intervals include each UE (for UE-based metrics), the whole
* story (for the full-story metric), etc. Each time interval is given
* in the following form:
*
* {
* bounds {tr.b.math.Range}: Boundaries of the time interval.
* name {string}: Name of this interval. Used to generate the
* metric names.
* description {string}: Human readable description of the interval.
* Used to generate the metric descriptions.
* perSecond {boolean}: Whether metrics over this interval should be
* reported as per-second values (e.g. power). If not, integrated values
* over the whole interval (e.g. energy) are reported.
* }
*
*/
function* computeTimeIntervals_(model) {
var chromeHelper = model.getOrCreateHelper(
tr.model.helpers.ChromeModelHelper);
var powerSeries = model.device.powerSeries;
if (powerSeries === undefined ||
powerSeries.samples.length === 0) {
return;
}
// Output the full story power metrics, which exists regardless of
// the presence of a Chrome trace.
yield {
bounds: model.bounds,
name: 'story',
description: 'user story',
perSecond: true
};
var chromeBounds = computeChromeBounds_(model);
if (chromeBounds.isEmpty) return;
var powerSeriesBoundsWithGracePeriod = tr.b.math.Range.fromExplicitRange(
powerSeries.bounds.min - CHROME_POWER_GRACE_PERIOD_MS,
powerSeries.bounds.max + CHROME_POWER_GRACE_PERIOD_MS);
if (!powerSeriesBoundsWithGracePeriod.containsRangeExclusive(
chromeBounds)) {
return;
}
// If Chrome bounds are good and the power trace covers the Chrome bounds,
// then output the Chrome specific metrics (loading and RAIL stages). Note
// that only the part of the time interval that overlaps the Chrome bounds
// should be included.
for (var interval of getRailStageIntervals_(model)) {
yield {
bounds: interval.bounds.findIntersection(chromeBounds),
name: interval.name,
description: interval.description,
perSecond: interval.perSecond
};
}
for (var interval of getLoadingIntervals_(model, chromeBounds)) {
yield {
bounds: interval.bounds.findIntersection(chromeBounds),
name: interval.name,
description: interval.description,
perSecond: interval.perSecond
};
}
}
/**
* Gets a list of time intervals for the RAIL stages. Each RAIL stage
* generates a time interval with the name equal to the name of the RAIL
* stage except made into lower case and with spaces replaced bu underscores
* e.g. "Scroll Animation" --> "scroll_animation". Each time interval is given
* in the following form:
*
* {
* bounds {tr.b.math.Range}: Boundaries of the time interval.
* name {string}: Name of this interval. Used to generate the
* metric names.
* description {string}: Human readable description of the interval.
* Used to generate the metric descriptions.
* perSecond {boolean}: Whether metrics over this interval should be
* reported as per-second values (e.g. power). If not, integrated values
* over the whole interval (e.g. energy) are reported.
* }
*
*/
function* getRailStageIntervals_(model) {
for (var exp of model.userModel.expectations) {
var histogramName = exp.title.toLowerCase().replace(' ', '_');
var energyHist = undefined;
if (histogramName.includes('response')) {
yield {
bounds: tr.b.math.Range.fromExplicitRange(exp.start, exp.end),
name: histogramName,
description: 'RAIL stage ' + histogramName,
perSecond: false
};
} else if (histogramName.includes('animation') ||
histogramName.includes('idle')) {
yield {
bounds: tr.b.math.Range.fromExplicitRange(exp.start, exp.end),
name: histogramName,
description: 'RAIL stage ' + histogramName,
perSecond: true
};
}
}
}
/**
* Gets a list of time intervals for the RAIL stages. Each RAIL stage
* generates a time interval with the name equal to the name of the RAIL
* stage except made into lower case and with spaces replaced bu underscores
* e.g. "Scroll Animation" --> "scroll_animation". Each time interval is given
* in the following form:
*
* {
* bounds {tr.b.math.Range}: Boundaries of the time interval.
* name {string}: Name of this interval. Used to generate the
* metric names.
* description {string}: Human readable description of the interval.
* Used to generate the metric descriptions.
* perSecond {boolean}: Whether metrics over this interval should be
* reported as per-second values (e.g. power). If not, integrated values
* over the whole interval (e.g. energy) are reported.
* }
*
*/
function* getLoadingIntervals_(model, chromeBounds) {
var ttiIntervals = getNavigationTTIIntervals_(model);
var lastLoadTime = undefined;
for (var ttiInterval of ttiIntervals) {
yield {
bounds: ttiInterval,
name: 'load',
description: 'page loads',
perSecond: false
};
lastLoadTime = lastLoadTime === undefined ? ttiInterval.max :
Math.max(lastLoadTime, ttiInterval.max);
}
if (lastLoadTime !== undefined) {
yield {
bounds: tr.b.math.Range.fromExplicitRange(
lastLoadTime, chromeBounds.max),
name: 'after_load',
description: 'period after load',
perSecond: true
};
}
}
/**
* @returns {tr.b.math.Range} The boundaries of the Chrome portion of the
* trace.
*/
function computeChromeBounds_(model) {
var chromeBounds = new tr.b.math.Range();
var chromeHelper = model.getOrCreateHelper(
tr.model.helpers.ChromeModelHelper);
if (chromeHelper === undefined) return chromeBounds;
for (var helper of chromeHelper.browserHelpers) {
if (helper.mainThread) {
chromeBounds.addRange(helper.mainThread.bounds);
}
}
for (var pid in chromeHelper.rendererHelpers) {
if (chromeHelper.rendererHelpers[pid].mainThread) {
chromeBounds.addRange(
chromeHelper.rendererHelpers[pid].mainThread.bounds);
}
}
return chromeBounds;
}
/**
* Adds the power histograms to the histogram set.
*
* Each power histogram is based on a specific time interval, and is named as
* follows:
*
* - [time_interval_name]:power, which contains a sample for each power
* sample data point during any time interval with the given type. Each
* sample represents the power draw during the period covered by that
* power sample.
*
* - [time_interval_name]:energy, which contains a sample for each time
* interval with the given type present in the trace. Each sample
* represents the total energy used over that time interval.
*
* The time intervals are as follows:
*
* - "story": The time interval covering the entire user story. There is
* always exactly one "story" interval.
*
* - "load" : The time interval covered by a page load, from navigationStart
* to timeToInteractive. There is one "load" interval for each page load
* in the trace.
*
* - "after_load": The time interval from the timeToInteractive of the last
* load event to the end of the trace.
*
* - "[user_expectation_type]" : Each Response, Animation, or Idle
* UE in the trace generates a time interval whose name is that of the UE,
* converted to lower case and with underscores in place of spaces.
* For instance, if there are three "Scroll Response" UEs in the trace,
* then there will be three "scroll_response" time intervals, so the
* histogram scroll_response:energy will contain three samples.
*
* Note that each time interval type only generates ONE of the "power" or
* "energy" histograms. Power histograms are generated for time intervals
* that represent events that occur over a period of time. This includes
* the following intervals
*
* - "story"
* - "after_load"
* - Any Animation or Idle UE
*
* For instance, "the energy it takes to play a video"
* does not have meaning because it depends on how long the video
* is; thus a more meaningful metric is power. In contrast, energy histograms
* are generated for time intervals that represent particular tasks
* which must be completed. This includes the following intervals:
*
* - "load"
* - Any Response UE
*
* For instance, if a change causes a response to take longer to process, then
* we want to count that as taking the energy over a longer period of time.
*/
function powerMetric(histograms, model) {
var data = {
model: model,
histograms: {}
};
for (var interval of computeTimeIntervals_(model))
createHistograms_(data, interval, histograms);
}
tr.metrics.MetricRegistry.register(powerMetric);
return {
powerMetric
};
});
</script>