// Copyright (c) 2012 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.
'use strict';
/**
* @fileoverview TimelineAnalysis summarizes info about the selected slices
* to the analysis panel.
*/
base.require('ui');
base.requireStylesheet('timeline_analysis');
base.exportTo('tracing', function() {
var AnalysisResults = base.ui.define('div');
AnalysisResults.prototype = {
__proto__: HTMLDivElement.prototype,
decorate: function() {
},
tsRound_: function(ts) {
return Math.round(ts * 1000.0) / 1000.0;
},
appendElement_: function(parent, tagName, opt_text) {
var n = parent.ownerDocument.createElement(tagName);
parent.appendChild(n);
if (opt_text != undefined)
n.textContent = opt_text;
return n;
},
appendText_: function(parent, text) {
var textElement = parent.ownerDocument.createTextNode(text);
parent.appendChild(textNode);
return textNode;
},
appendTableCell_: function(table, row, cellnum, text) {
var td = this.appendElement_(row, 'td', text);
td.className = table.className + '-col-' + cellnum;
return td;
},
appendTableCellWithTooltip_: function(table, row, cellnum, text, tooltip) {
if (tooltip) {
var td = this.appendElement_(row, 'td');
td.className = table.className + '-col-' + cellnum;
var span = this.appendElement_(td, 'span', text);
span.className = 'tooltip';
span.title = tooltip;
return td;
} else {
this.appendTableCell_(table, row, cellnum, text);
}
},
/**
* Adds a table with the given className.
* @return {HTMLTableElement} The newly created table.
*/
appendTable: function(className, numColumns) {
var table = this.appendElement_(this, 'table');
table.className = className + ' timeline-analysis-table';
table.numColumns = numColumns;
return table;
},
/**
* Creates and appends a row to |table| with a left-aligned |label]
* header that spans all columns.
*/
appendTableHeader: function(table, label) {
var row = this.appendElement_(table, 'tr');
var th = this.appendElement_(row, 'th', label);
th.className = 'timeline-analysis-table-header';
},
/**
* Creates and appends a row to |table| with a left-aligned |label]
* in the first column and an optional |opt_text| value in the second
* column.
*/
appendSummaryRow: function(table, label, opt_text) {
var row = this.appendElement_(table, 'tr');
row.className = 'timeline-analysis-table-row';
this.appendTableCell_(table, row, 0, label);
if (opt_text !== undefined) {
this.appendTableCell_(table, row, 1, opt_text);
for (var i = 2; i < table.numColumns; i++)
this.appendTableCell_(table, row, i, '');
} else {
for (var i = 1; i < table.numColumns; i++)
this.appendTableCell_(table, row, 1, '');
}
},
/**
* Adds a spacing row to spread out results.
*/
appendSpacingRow: function(table) {
var row = this.appendElement_(table, 'tr');
row.className = 'timeline-analysis-table-row';
for (var i = 0; i < table.numColumns; i++)
this.appendTableCell_(table, row, i, ' ');
},
/**
* Creates and appends a row to |table| with a left-aligned |label]
* in the first column and a millisecvond |time| value in the second
* column.
*/
appendSummaryRowTime: function(table, label, time) {
this.appendSummaryRow(table, label, this.tsRound_(time) + ' ms');
},
/**
* Creates and appends a row to |table| that summarizes one or more slices,
* or one or more counters.
* The row has a left-aligned |label| in the first column, the |duration|
* of the data in the second, the number of |occurrences| in the third.
* @param {object} opt_statistics May be undefined, or an object which
* contains calculated staistics containing min/max/avg for slices, or
* min/max/avg/start/end for counters.
*/
appendDataRow: function(
table, label, opt_duration, opt_occurences, opt_statistics) {
var tooltip = undefined;
if (opt_statistics) {
tooltip = 'Min Duration:\u0009' + this.tsRound_(opt_statistics.min) +
' ms \u000DMax Duration:\u0009' +
this.tsRound_(opt_statistics.max) +
' ms \u000DAvg Duration:\u0009' +
this.tsRound_(opt_statistics.avg) + ' ms';
if (opt_statistics.start) {
tooltip += '\u000DStart Time:\u0009' +
this.tsRound_(opt_statistics.start) + ' ms';
}
if (opt_statistics.end) {
tooltip += '\u000DEnd Time:\u0009' +
this.tsRound_(opt_statistics.end) + ' ms';
}
if (opt_statistics.frequency && opt_statistics.frequency_stddev) {
tooltip += '\u000DFrequency:\u0009' +
this.tsRound_(opt_statistics.frequency) +
' occurrences/s (\u03C3 = ' +
this.tsRound_(opt_statistics.frequency_stddev) + ')';
}
}
var row = this.appendElement_(table, 'tr');
row.className = 'timeline-analysis-table-row';
this.appendTableCellWithTooltip_(table, row, 0, label, tooltip);
if (opt_duration !== undefined) {
this.appendTableCellWithTooltip_(table, row, 1,
this.tsRound_(opt_duration) + ' ms', tooltip);
} else {
this.appendTableCell_(table, row, 1, '');
}
if (opt_occurences !== undefined) {
this.appendTableCellWithTooltip_(table, row, 2,
String(opt_occurences) + ' occurrences', tooltip);
} else {
this.appendTableCell_(table, row, 2, '');
}
}
};
/**
* Analyzes the selection, outputting the analysis results into the provided
* results object.
*
* @param {AnalysisResults} results Where the analysis is placed.
* @param {TimelineSelection} selection What to analyze.
*/
function analyzeSelection(results, selection) {
var sliceHits = selection.getSliceHits();
var counterSampleHits = selection.getCounterSampleHits();
if (sliceHits.length == 1) {
var slice = sliceHits[0].slice;
var table = results.appendTable('timeline-analysis-slice-table', 2);
results.appendTableHeader(table, 'Selected slice:');
results.appendSummaryRow(table, 'Title', slice.title);
if (slice.category)
results.appendSummaryRow(table, 'Category', slice.category);
results.appendSummaryRowTime(table, 'Start', slice.start);
results.appendSummaryRowTime(table, 'Duration', slice.duration);
if (slice.durationInUserTime) {
results.appendSummaryRowTime(
table, 'Duration (U)', slice.durationInUserTime);
}
var n = 0;
for (var argName in slice.args) {
n += 1;
}
if (n > 0) {
results.appendSummaryRow(table, 'Args');
for (var argName in slice.args) {
var argVal = slice.args[argName];
// TODO(sleffler) use span instead?
results.appendSummaryRow(table, ' ' + argName, argVal);
}
}
} else if (sliceHits.length > 1) {
var tsLo = sliceHits.range.min;
var tsHi = sliceHits.range.max;
// compute total sliceHits duration
var titles = sliceHits.map(function(i) { return i.slice.title; });
var numTitles = 0;
var slicesByTitle = {};
for (var i = 0; i < sliceHits.length; i++) {
var slice = sliceHits[i].slice;
if (!slicesByTitle[slice.title]) {
slicesByTitle[slice.title] = {
slices: []
};
numTitles++;
}
slicesByTitle[slice.title].slices.push(slice);
}
var table;
table = results.appendTable('timeline-analysis-slices-table', 3);
results.appendTableHeader(table, 'Slices:');
var totalDuration = 0;
for (var sliceGroupTitle in slicesByTitle) {
var sliceGroup = slicesByTitle[sliceGroupTitle];
var duration = 0;
var avg = 0;
var startOfFirstOccurrence = Number.MAX_VALUE;
var startOfLastOccurrence = -Number.MAX_VALUE;
var frequencyDetails = undefined;
var min = Number.MAX_VALUE;
var max = -Number.MAX_VALUE;
for (var i = 0; i < sliceGroup.slices.length; i++) {
duration += sliceGroup.slices[i].duration;
startOfFirstOccurrence = Math.min(sliceGroup.slices[i].start,
startOfFirstOccurrence);
startOfLastOccurrence = Math.max(sliceGroup.slices[i].start,
startOfLastOccurrence);
min = Math.min(sliceGroup.slices[i].duration, min);
max = Math.max(sliceGroup.slices[i].duration, max);
}
totalDuration += duration;
if (sliceGroup.slices.length == 0)
avg = 0;
avg = duration / sliceGroup.slices.length;
var details = {min: min,
max: max,
avg: avg,
frequency: undefined,
frequency_stddev: undefined};
// We require at least 3 samples to compute the stddev.
var elapsed = startOfLastOccurrence - startOfFirstOccurrence;
if (sliceGroup.slices.length > 2 && elapsed > 0) {
var numDistances = sliceGroup.slices.length - 1;
details.frequency = (1000 * numDistances) / elapsed;
// Compute the stddev.
var sumOfSquaredDistancesToMean = 0;
for (var i = 1; i < sliceGroup.slices.length; i++) {
var currentFrequency = 1000 /
(sliceGroup.slices[i].start - sliceGroup.slices[i - 1].start);
var signedDistance = details.frequency - currentFrequency;
sumOfSquaredDistancesToMean += signedDistance * signedDistance;
}
details.frequency_stddev = Math.sqrt(
sumOfSquaredDistancesToMean / (numDistances - 1));
}
results.appendDataRow(
table, sliceGroupTitle, duration, sliceGroup.slices.length,
details);
}
results.appendDataRow(table, '*Totals', totalDuration, sliceHits.length);
results.appendSpacingRow(table);
results.appendSummaryRowTime(table, 'Selection start', tsLo);
results.appendSummaryRowTime(table, 'Selection extent', tsHi - tsLo);
}
if (counterSampleHits.length == 1) {
var hit = counterSampleHits[0];
var ctr = hit.counter;
var sampleIndex = hit.sampleIndex;
var values = [];
for (var i = 0; i < ctr.numSeries; ++i)
values.push(ctr.samples[ctr.numSeries * sampleIndex + i]);
var table = results.appendTable('timeline-analysis-counter-table', 2);
results.appendTableHeader(table, 'Selected counter:');
results.appendSummaryRow(table, 'Title', ctr.name);
results.appendSummaryRowTime(
table, 'Timestamp', ctr.timestamps[sampleIndex]);
for (var i = 0; i < ctr.numSeries; i++)
results.appendSummaryRow(table, ctr.seriesNames[i], values[i]);
} else if (counterSampleHits.length > 1) {
var hitsByCounter = {};
for (var i = 0; i < counterSampleHits.length; i++) {
var ctr = counterSampleHits[i].counter;
if (!hitsByCounter[ctr.guid])
hitsByCounter[ctr.guid] = [];
hitsByCounter[ctr.guid].push(counterSampleHits[i]);
}
var table = results.appendTable('timeline-analysis-counter-table', 7);
results.appendTableHeader(table, 'Counters:');
for (var id in hitsByCounter) {
var hits = hitsByCounter[id];
var ctr = hits[0].counter;
var sampleIndices = [];
for (var i = 0; i < hits.length; i++)
sampleIndices.push(hits[i].sampleIndex);
var stats = ctr.getSampleStatistics(sampleIndices);
for (var i = 0; i < stats.length; i++) {
results.appendDataRow(
table, ctr.name + ': ' + ctr.seriesNames[i], undefined,
undefined, stats[i]);
}
}
}
}
var TimelineAnalysisView = base.ui.define('div');
TimelineAnalysisView.prototype = {
__proto__: HTMLDivElement.prototype,
decorate: function() {
this.className = 'timeline-analysis';
},
set selection(selection) {
this.textContent = '';
var results = new AnalysisResults();
analyzeSelection(results, selection);
this.appendChild(results);
}
};
return {
TimelineAnalysisView: TimelineAnalysisView,
analyzeSelection_: analyzeSelection
};
});