1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 'use strict'; 6 7 /** 8 * @fileoverview TimelineAnalysis summarizes info about the selected slices 9 * to the analysis panel. 10 */ 11 base.require('ui'); 12 base.requireStylesheet('timeline_analysis'); 13 base.exportTo('tracing', function() { 14 15 var AnalysisResults = base.ui.define('div'); 16 17 AnalysisResults.prototype = { 18 __proto__: HTMLDivElement.prototype, 19 20 decorate: function() { 21 }, 22 23 tsRound_: function(ts) { 24 return Math.round(ts * 1000.0) / 1000.0; 25 }, 26 27 appendElement_: function(parent, tagName, opt_text) { 28 var n = parent.ownerDocument.createElement(tagName); 29 parent.appendChild(n); 30 if (opt_text != undefined) 31 n.textContent = opt_text; 32 return n; 33 }, 34 35 appendText_: function(parent, text) { 36 var textElement = parent.ownerDocument.createTextNode(text); 37 parent.appendChild(textNode); 38 return textNode; 39 }, 40 41 appendTableCell_: function(table, row, cellnum, text) { 42 var td = this.appendElement_(row, 'td', text); 43 td.className = table.className + '-col-' + cellnum; 44 return td; 45 }, 46 47 appendTableCellWithTooltip_: function(table, row, cellnum, text, tooltip) { 48 if (tooltip) { 49 var td = this.appendElement_(row, 'td'); 50 td.className = table.className + '-col-' + cellnum; 51 var span = this.appendElement_(td, 'span', text); 52 span.className = 'tooltip'; 53 span.title = tooltip; 54 return td; 55 } else { 56 this.appendTableCell_(table, row, cellnum, text); 57 } 58 }, 59 60 /** 61 * Adds a table with the given className. 62 * @return {HTMLTableElement} The newly created table. 63 */ 64 appendTable: function(className, numColumns) { 65 var table = this.appendElement_(this, 'table'); 66 table.className = className + ' timeline-analysis-table'; 67 table.numColumns = numColumns; 68 return table; 69 }, 70 71 /** 72 * Creates and appends a row to |table| with a left-aligned |label] 73 * header that spans all columns. 74 */ 75 appendTableHeader: function(table, label) { 76 var row = this.appendElement_(table, 'tr'); 77 78 var th = this.appendElement_(row, 'th', label); 79 th.className = 'timeline-analysis-table-header'; 80 }, 81 82 /** 83 * Creates and appends a row to |table| with a left-aligned |label] 84 * in the first column and an optional |opt_text| value in the second 85 * column. 86 */ 87 appendSummaryRow: function(table, label, opt_text) { 88 var row = this.appendElement_(table, 'tr'); 89 row.className = 'timeline-analysis-table-row'; 90 91 this.appendTableCell_(table, row, 0, label); 92 if (opt_text !== undefined) { 93 this.appendTableCell_(table, row, 1, opt_text); 94 for (var i = 2; i < table.numColumns; i++) 95 this.appendTableCell_(table, row, i, ''); 96 } else { 97 for (var i = 1; i < table.numColumns; i++) 98 this.appendTableCell_(table, row, 1, ''); 99 } 100 }, 101 102 /** 103 * Adds a spacing row to spread out results. 104 */ 105 appendSpacingRow: function(table) { 106 var row = this.appendElement_(table, 'tr'); 107 row.className = 'timeline-analysis-table-row'; 108 for (var i = 0; i < table.numColumns; i++) 109 this.appendTableCell_(table, row, i, ' '); 110 }, 111 112 /** 113 * Creates and appends a row to |table| with a left-aligned |label] 114 * in the first column and a millisecvond |time| value in the second 115 * column. 116 */ 117 appendSummaryRowTime: function(table, label, time) { 118 this.appendSummaryRow(table, label, this.tsRound_(time) + ' ms'); 119 }, 120 121 /** 122 * Creates and appends a row to |table| that summarizes one or more slices, 123 * or one or more counters. 124 * The row has a left-aligned |label| in the first column, the |duration| 125 * of the data in the second, the number of |occurrences| in the third. 126 * @param {object} opt_statistics May be undefined, or an object which 127 * contains calculated staistics containing min/max/avg for slices, or 128 * min/max/avg/start/end for counters. 129 */ 130 appendDataRow: function( 131 table, label, opt_duration, opt_occurences, opt_statistics) { 132 133 var tooltip = undefined; 134 if (opt_statistics) { 135 tooltip = 'Min Duration:\u0009' + this.tsRound_(opt_statistics.min) + 136 ' ms \u000DMax Duration:\u0009' + 137 this.tsRound_(opt_statistics.max) + 138 ' ms \u000DAvg Duration:\u0009' + 139 this.tsRound_(opt_statistics.avg) + ' ms'; 140 141 if (opt_statistics.start) { 142 tooltip += '\u000DStart Time:\u0009' + 143 this.tsRound_(opt_statistics.start) + ' ms'; 144 } 145 if (opt_statistics.end) { 146 tooltip += '\u000DEnd Time:\u0009' + 147 this.tsRound_(opt_statistics.end) + ' ms'; 148 } 149 if (opt_statistics.frequency && opt_statistics.frequency_stddev) { 150 tooltip += '\u000DFrequency:\u0009' + 151 this.tsRound_(opt_statistics.frequency) + 152 ' occurrences/s (\u03C3 = ' + 153 this.tsRound_(opt_statistics.frequency_stddev) + ')'; 154 } 155 } 156 157 var row = this.appendElement_(table, 'tr'); 158 row.className = 'timeline-analysis-table-row'; 159 160 this.appendTableCellWithTooltip_(table, row, 0, label, tooltip); 161 162 if (opt_duration !== undefined) { 163 this.appendTableCellWithTooltip_(table, row, 1, 164 this.tsRound_(opt_duration) + ' ms', tooltip); 165 } else { 166 this.appendTableCell_(table, row, 1, ''); 167 } 168 169 if (opt_occurences !== undefined) { 170 this.appendTableCellWithTooltip_(table, row, 2, 171 String(opt_occurences) + ' occurrences', tooltip); 172 173 } else { 174 this.appendTableCell_(table, row, 2, ''); 175 } 176 } 177 }; 178 179 /** 180 * Analyzes the selection, outputting the analysis results into the provided 181 * results object. 182 * 183 * @param {AnalysisResults} results Where the analysis is placed. 184 * @param {TimelineSelection} selection What to analyze. 185 */ 186 function analyzeSelection(results, selection) { 187 188 var sliceHits = selection.getSliceHits(); 189 var counterSampleHits = selection.getCounterSampleHits(); 190 191 if (sliceHits.length == 1) { 192 var slice = sliceHits[0].slice; 193 var table = results.appendTable('timeline-analysis-slice-table', 2); 194 195 results.appendTableHeader(table, 'Selected slice:'); 196 results.appendSummaryRow(table, 'Title', slice.title); 197 198 if (slice.category) 199 results.appendSummaryRow(table, 'Category', slice.category); 200 201 results.appendSummaryRowTime(table, 'Start', slice.start); 202 results.appendSummaryRowTime(table, 'Duration', slice.duration); 203 204 if (slice.durationInUserTime) { 205 results.appendSummaryRowTime( 206 table, 'Duration (U)', slice.durationInUserTime); 207 } 208 209 var n = 0; 210 for (var argName in slice.args) { 211 n += 1; 212 } 213 if (n > 0) { 214 results.appendSummaryRow(table, 'Args'); 215 for (var argName in slice.args) { 216 var argVal = slice.args[argName]; 217 // TODO(sleffler) use span instead? 218 results.appendSummaryRow(table, ' ' + argName, argVal); 219 } 220 } 221 } else if (sliceHits.length > 1) { 222 var tsLo = sliceHits.range.min; 223 var tsHi = sliceHits.range.max; 224 225 // compute total sliceHits duration 226 var titles = sliceHits.map(function(i) { return i.slice.title; }); 227 228 var numTitles = 0; 229 var slicesByTitle = {}; 230 for (var i = 0; i < sliceHits.length; i++) { 231 var slice = sliceHits[i].slice; 232 if (!slicesByTitle[slice.title]) { 233 slicesByTitle[slice.title] = { 234 slices: [] 235 }; 236 numTitles++; 237 } 238 slicesByTitle[slice.title].slices.push(slice); 239 } 240 241 var table; 242 table = results.appendTable('timeline-analysis-slices-table', 3); 243 results.appendTableHeader(table, 'Slices:'); 244 245 var totalDuration = 0; 246 for (var sliceGroupTitle in slicesByTitle) { 247 var sliceGroup = slicesByTitle[sliceGroupTitle]; 248 var duration = 0; 249 var avg = 0; 250 var startOfFirstOccurrence = Number.MAX_VALUE; 251 var startOfLastOccurrence = -Number.MAX_VALUE; 252 var frequencyDetails = undefined; 253 var min = Number.MAX_VALUE; 254 var max = -Number.MAX_VALUE; 255 for (var i = 0; i < sliceGroup.slices.length; i++) { 256 duration += sliceGroup.slices[i].duration; 257 startOfFirstOccurrence = Math.min(sliceGroup.slices[i].start, 258 startOfFirstOccurrence); 259 startOfLastOccurrence = Math.max(sliceGroup.slices[i].start, 260 startOfLastOccurrence); 261 min = Math.min(sliceGroup.slices[i].duration, min); 262 max = Math.max(sliceGroup.slices[i].duration, max); 263 } 264 265 totalDuration += duration; 266 267 if (sliceGroup.slices.length == 0) 268 avg = 0; 269 avg = duration / sliceGroup.slices.length; 270 271 var details = {min: min, 272 max: max, 273 avg: avg, 274 frequency: undefined, 275 frequency_stddev: undefined}; 276 277 // We require at least 3 samples to compute the stddev. 278 var elapsed = startOfLastOccurrence - startOfFirstOccurrence; 279 if (sliceGroup.slices.length > 2 && elapsed > 0) { 280 var numDistances = sliceGroup.slices.length - 1; 281 details.frequency = (1000 * numDistances) / elapsed; 282 283 // Compute the stddev. 284 var sumOfSquaredDistancesToMean = 0; 285 for (var i = 1; i < sliceGroup.slices.length; i++) { 286 var currentFrequency = 1000 / 287 (sliceGroup.slices[i].start - sliceGroup.slices[i - 1].start); 288 var signedDistance = details.frequency - currentFrequency; 289 sumOfSquaredDistancesToMean += signedDistance * signedDistance; 290 } 291 292 details.frequency_stddev = Math.sqrt( 293 sumOfSquaredDistancesToMean / (numDistances - 1)); 294 } 295 results.appendDataRow( 296 table, sliceGroupTitle, duration, sliceGroup.slices.length, 297 details); 298 } 299 results.appendDataRow(table, '*Totals', totalDuration, sliceHits.length); 300 results.appendSpacingRow(table); 301 results.appendSummaryRowTime(table, 'Selection start', tsLo); 302 results.appendSummaryRowTime(table, 'Selection extent', tsHi - tsLo); 303 } 304 305 if (counterSampleHits.length == 1) { 306 var hit = counterSampleHits[0]; 307 var ctr = hit.counter; 308 var sampleIndex = hit.sampleIndex; 309 var values = []; 310 for (var i = 0; i < ctr.numSeries; ++i) 311 values.push(ctr.samples[ctr.numSeries * sampleIndex + i]); 312 313 var table = results.appendTable('timeline-analysis-counter-table', 2); 314 results.appendTableHeader(table, 'Selected counter:'); 315 results.appendSummaryRow(table, 'Title', ctr.name); 316 results.appendSummaryRowTime( 317 table, 'Timestamp', ctr.timestamps[sampleIndex]); 318 319 for (var i = 0; i < ctr.numSeries; i++) 320 results.appendSummaryRow(table, ctr.seriesNames[i], values[i]); 321 } else if (counterSampleHits.length > 1) { 322 var hitsByCounter = {}; 323 for (var i = 0; i < counterSampleHits.length; i++) { 324 var ctr = counterSampleHits[i].counter; 325 if (!hitsByCounter[ctr.guid]) 326 hitsByCounter[ctr.guid] = []; 327 hitsByCounter[ctr.guid].push(counterSampleHits[i]); 328 } 329 330 var table = results.appendTable('timeline-analysis-counter-table', 7); 331 results.appendTableHeader(table, 'Counters:'); 332 for (var id in hitsByCounter) { 333 var hits = hitsByCounter[id]; 334 var ctr = hits[0].counter; 335 var sampleIndices = []; 336 for (var i = 0; i < hits.length; i++) 337 sampleIndices.push(hits[i].sampleIndex); 338 339 var stats = ctr.getSampleStatistics(sampleIndices); 340 for (var i = 0; i < stats.length; i++) { 341 results.appendDataRow( 342 table, ctr.name + ': ' + ctr.seriesNames[i], undefined, 343 undefined, stats[i]); 344 } 345 } 346 } 347 } 348 349 var TimelineAnalysisView = base.ui.define('div'); 350 351 TimelineAnalysisView.prototype = { 352 __proto__: HTMLDivElement.prototype, 353 354 decorate: function() { 355 this.className = 'timeline-analysis'; 356 }, 357 358 set selection(selection) { 359 this.textContent = ''; 360 var results = new AnalysisResults(); 361 analyzeSelection(results, selection); 362 this.appendChild(results); 363 } 364 }; 365 366 return { 367 TimelineAnalysisView: TimelineAnalysisView, 368 analyzeSelection_: analyzeSelection 369 }; 370 }); 371