// Copyright 2009 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
 * @fileoverview A table for showing the results of performance testing.
 *
 * {@see goog.testing.benchmark} for an easy way to use this functionality.
 *
 * @author attila@google.com (Attila Bodis)
 * @author nicksantos@google.com (Nick Santos)
 */

goog.provide('goog.testing.PerformanceTable');

goog.require('goog.dom');
goog.require('goog.dom.TagName');
goog.require('goog.testing.PerformanceTimer');



/**
 * A UI widget that runs performance tests and displays the results.
 * @param {Element} root The element where the table should be attached.
 * @param {goog.testing.PerformanceTimer=} opt_timer A timer to use for
 *     executing functions and profiling them.
 * @param {number=} opt_precision Number of digits of precision to include in
 *     results.  Defaults to 0.
 * @param {number=} opt_numSamples The number of samples to take. Defaults to 5.
 * @constructor
 * @final
 */
goog.testing.PerformanceTable = function(
    root, opt_timer, opt_precision, opt_numSamples) {
  /**
   * Where the table should be attached.
   * @private {Element}
   */
  this.root_ = root;

  /**
   * Number of digits of precision to include in results.
   * Defaults to 0.
   * @private {number}
   */
  this.precision_ = opt_precision || 0;

  var timer = opt_timer;
  if (!timer) {
    timer = new goog.testing.PerformanceTimer();
    timer.setNumSamples(opt_numSamples || 5);
    timer.setDiscardOutliers(true);
  }

  /**
   * A timer for running the tests.
   * @private {goog.testing.PerformanceTimer}
   */
  this.timer_ = timer;

  this.initRoot_();
};


/**
 * @return {goog.testing.PerformanceTimer} The timer being used.
 */
goog.testing.PerformanceTable.prototype.getTimer = function() {
  return this.timer_;
};


/**
 * Render the initial table.
 * @private
 */
goog.testing.PerformanceTable.prototype.initRoot_ = function() {
  this.root_.innerHTML =
      '<table class="test-results" cellspacing="1">' +
      '  <thead>' +
      '    <tr>' +
      '      <th rowspan="2">Test Description</th>' +
      '      <th rowspan="2">Runs</th>' +
      '      <th colspan="4">Results (ms)</th>' +
      '    </tr>' +
      '    <tr>' +
      '      <th>Average</th>' +
      '      <th>Std Dev</th>' +
      '      <th>Minimum</th>' +
      '      <th>Maximum</th>' +
      '    </tr>' +
      '  </thead>' +
      '  <tbody>' +
      '  </tbody>' +
      '</table>';
};


/**
 * @return {Element} The body of the table.
 * @private
 */
goog.testing.PerformanceTable.prototype.getTableBody_ = function() {
  return this.root_.getElementsByTagName(goog.dom.TagName.TBODY)[0];
};


/**
 * Round to the specified precision.
 * @param {number} num The number to round.
 * @return {string} The rounded number, as a string.
 * @private
 */
goog.testing.PerformanceTable.prototype.round_ = function(num) {
  var factor = Math.pow(10, this.precision_);
  return String(Math.round(num * factor) / factor);
};


/**
 * Run the given function with the performance timer, and show the results.
 * @param {Function} fn The function to run.
 * @param {string=} opt_desc A description to associate with this run.
 */
goog.testing.PerformanceTable.prototype.run = function(fn, opt_desc) {
  this.runTask(
      new goog.testing.PerformanceTimer.Task(/** @type {function()} */ (fn)),
      opt_desc);
};


/**
 * Run the given task with the performance timer, and show the results.
 * @param {goog.testing.PerformanceTimer.Task} task The performance timer task
 *     to run.
 * @param {string=} opt_desc A description to associate with this run.
 */
goog.testing.PerformanceTable.prototype.runTask = function(task, opt_desc) {
  var results = this.timer_.runTask(task);
  this.recordResults(results, opt_desc);
};


/**
 * Record a performance timer results object to the performance table. See
 * {@code goog.testing.PerformanceTimer} for details of the format of this
 * object.
 * @param {Object} results The performance timer results object.
 * @param {string=} opt_desc A description to associate with these results.
 */
goog.testing.PerformanceTable.prototype.recordResults = function(
    results, opt_desc) {
  var average = results['average'];
  var standardDeviation = results['standardDeviation'];
  var isSuspicious = average < 0 || standardDeviation > average * .5;
  var resultsRow = goog.dom.createDom(goog.dom.TagName.TR, null,
      goog.dom.createDom(goog.dom.TagName.TD, 'test-description',
          opt_desc || 'No description'),
      goog.dom.createDom(goog.dom.TagName.TD, 'test-count',
                         String(results['count'])),
      goog.dom.createDom(goog.dom.TagName.TD, 'test-average',
                         this.round_(average)),
      goog.dom.createDom(goog.dom.TagName.TD, 'test-standard-deviation',
                         this.round_(standardDeviation)),
      goog.dom.createDom(goog.dom.TagName.TD, 'test-minimum',
                         String(results['minimum'])),
      goog.dom.createDom(goog.dom.TagName.TD, 'test-maximum',
                         String(results['maximum'])));
  if (isSuspicious) {
    resultsRow.className = 'test-suspicious';
  }
  this.getTableBody_().appendChild(resultsRow);
};


/**
 * Report an error in the table.
 * @param {*} reason The reason for the error.
 */
goog.testing.PerformanceTable.prototype.reportError = function(reason) {
  this.getTableBody_().appendChild(
      goog.dom.createDom(goog.dom.TagName.TR, null,
          goog.dom.createDom(goog.dom.TagName.TD,
                             {'class': 'test-error', 'colSpan': 5},
                             String(reason))));
};
