// 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 Analysis summarizes info about the selected slices * to the analysis panel. */ base.require('analysis.util'); base.require('ui'); base.requireStylesheet('timeline_analysis_view'); base.exportTo('tracing', function() { var AnalysisResults = tracing.ui.define('div'); AnalysisResults.prototype = { __proto__: HTMLDivElement.prototype, decorate: function() { }, 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 + ' 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 = '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 = 'analysis-table-row'; this.appendTableCell_(table, row, 0, label); if (opt_text !== undefined) { if (opt_text[0] == '{' && opt_text[opt_text.length - 1] == '}') { // Try to treat the opt_text as json. var value; try { value = JSON.parse(opt_text) } catch(e) { value = undefined; } if (!value === undefined) { this.appendTableCell_(table, row, 1, opt_text); } else { var pretty = JSON.stringify(value, null, ' '); this.appendTableCell_(table, row, 1, pretty); } } else { 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 = '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, tracing.analysis.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' + tracing.analysis.tsRound(opt_statistics.min) + ' ms \u000DMax Duration:\u0009' + tracing.analysis.tsRound(opt_statistics.max) + ' ms \u000DAvg Duration:\u0009' + tracing.analysis.tsRound(opt_statistics.avg) + ' ms (\u03C3 = ' + tracing.analysis.tsRound(opt_statistics.avg_stddev) + ')'; if (opt_statistics.start) { tooltip += '\u000DStart Time:\u0009' + tracing.analysis.tsRound(opt_statistics.start) + ' ms'; } if (opt_statistics.end) { tooltip += '\u000DEnd Time:\u0009' + tracing.analysis.tsRound(opt_statistics.end) + ' ms'; } if (opt_statistics.frequency && opt_statistics.frequency_stddev) { tooltip += '\u000DFrequency:\u0009' + tracing.analysis.tsRound(opt_statistics.frequency) + ' occurrences/s (\u03C3 = ' + tracing.analysis.tsRound(opt_statistics.frequency_stddev) + ')'; } } var row = this.appendElement_(table, 'tr'); row.className = 'analysis-table-row'; this.appendTableCellWithTooltip_(table, row, 0, label, tooltip); if (opt_duration !== undefined) { this.appendTableCellWithTooltip_(table, row, 1, tracing.analysis.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 {Selection} selection What to analyze. */ function analyzeSelection(results, selection) { var sliceHits = selection.getSliceHitsAsSelection(); var counterSampleHits = selection.getCounterSampleHitsAsSelection(); if (sliceHits.length == 1) { var slice = sliceHits[0].slice; var table = results.appendTable('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.bounds.min; var tsHi = sliceHits.bounds.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('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, avg_stddev: undefined, frequency: undefined, frequency_stddev: undefined}; // Compute the stddev of the slice durations. var sumOfSquaredDistancesToMean = 0; for (var i = 0; i < sliceGroup.slices.length; i++) { var signedDistance = details.avg - sliceGroup.slices[i].duration; sumOfSquaredDistancesToMean += signedDistance * signedDistance; } details.avg_stddev = Math.sqrt( sumOfSquaredDistancesToMean / (sliceGroup.slices.length - 1)); // 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. 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('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('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 = tracing.ui.define('div'); TimelineAnalysisView.prototype = { __proto__: HTMLDivElement.prototype, decorate: function() { this.className = 'analysis'; }, set selection(selection) { this.textContent = ''; var results = new AnalysisResults(); analyzeSelection(results, selection); this.appendChild(results); } }; return { TimelineAnalysisView: TimelineAnalysisView, analyzeSelection_: analyzeSelection }; });