blob: f0defa12518e3ad13660e2cd199527f0711980b8 [file] [log] [blame]
// 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();
}