Javascript  |  371行  |  12.44 KB

// 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
  };
});