# Copyright 2013 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.

import optparse
import re
import time

from metrics import v8_object_stats
from telemetry.page import page_measurement

# V8 statistics counter names. These can be retrieved using
# v8_object_stats.V8ObjectStatsMetric.GetV8StatsTable.
_V8_BYTES_COMMITTED = [
    'V8.MemoryNewSpaceBytesCommitted',
    'V8.MemoryOldPointerSpaceBytesCommitted',
    'V8.MemoryOldDataSpaceBytesCommitted',
    'V8.MemoryCodeSpaceBytesCommitted',
    'V8.MemoryMapSpaceBytesCommitted',
    'V8.MemoryCellSpaceBytesCommitted',
    'V8.MemoryPropertyCellSpaceBytesCommitted',
    'V8.MemoryLoSpaceBytesCommitted',
]
_V8_BYTES_USED = [
    'V8.MemoryNewSpaceBytesUsed',
    'V8.MemoryOldPointerSpaceBytesUsed',
    'V8.MemoryOldDataSpaceBytesUsed',
    'V8.MemoryCodeSpaceBytesUsed',
    'V8.MemoryMapSpaceBytesUsed',
    'V8.MemoryCellSpaceBytesUsed',
    'V8.MemoryPropertyCellSpaceBytesUsed',
    'V8.MemoryLoSpaceBytesUsed',
]
_V8_MEMORY_ALLOCATED = [
    'V8.OsMemoryAllocated',
]


class Endure(page_measurement.PageMeasurement):
  def __init__(self):
    super(Endure, self).__init__('endure')
    # Browser object, saved so that browser.memory_stats can be accessed.
    self._browser = None

    # Dict of trace name to lists of y-values, for making summary values.
    self._y_values = {}

    # Time of test start and last sample, in seconds since the epoch.
    self._start_time = None
    self._last_sample_time = 0

    # Number of page repetitions currently, and at the last sample.
    self._iterations = 0
    self._last_sample_iterations = 0

    # Interval between stats sampling. One of these attributes will be set when
    # the perf stats interval option is parsed. The other shall remain as None.
    self._interval_seconds = None
    self._interval_iterations = None

  def AddCommandLineOptions(self, parser):
    # TODO(tdu): When ProcessCommandLine is added to replace this method,
    # move the logic in _ParseIntervalOption there to ProcessCommandLine.
    group = optparse.OptionGroup(parser, 'Endure options')
    group.add_option('--perf-stats-interval',
                     dest='perf_stats_interval',
                     default='20s',
                     type='string',
                     help='Interval between sampling of statistics, either in '
                          'seconds (specified by appending \'s\') or in number '
                          'of iterations')
    parser.add_option_group(group)

  def DidStartBrowser(self, browser):
    """Saves the Browser object. Called after the browser is started."""
    self._browser = browser

  def CustomizeBrowserOptions(self, options):
    """Adds extra command-line options to the browser."""
    v8_object_stats.V8ObjectStatsMetric.CustomizeBrowserOptions(options)

  def CanRunForPage(self, page):
    """Checks whether a page has the required 'endure' property."""
    return hasattr(page, 'endure')

  def WillRunPageRepeats(self, page):
    """Set-up before starting a new page."""
    # Reset the starting time for each new page.
    self._start_time = time.time()

    # Prefix the page name so it can be picked up by the buildbot script that
    # parses Endure output.
    if page.name and not page.display_name.startswith('endure_'):
      page.name = 'endure_' + page.name

  def MeasurePage(self, page, tab, results):
    """Takes a sample and adds a result if enough time has passed."""
    # Parse the interval option, setting either or seconds or iterations.
    # This is done here because self.options is not set when any of the above
    # methods are run.
    self._ParseIntervalOption()

    # Check whether the sample interval is specified in seconds or iterations,
    # and take a sample if it's time.
    self._iterations += 1
    if self._interval_seconds:
      now = time.time()
      seconds_elapsed = int(round(now - self._last_sample_time))
      # Note: the time since last sample must be at least as many seconds
      # as specified; it will usually be more, it will never be less.
      if seconds_elapsed >= self._interval_seconds:
        total_seconds = int(round(now - self._start_time))
        self._SampleStats(tab, results, seconds=total_seconds)
        self._last_sample_time = now
    else:
      iterations_elapsed = self._iterations - self._last_sample_iterations
      if iterations_elapsed >= self._interval_iterations:
        self._SampleStats(tab, results, iterations=self._iterations)
        self._last_sample_iterations = self._iterations

  def _ParseIntervalOption(self):
    """Parses the --perf-stats-interval option that was passed in."""
    if self._interval_seconds or self._interval_iterations:
      return
    interval = self.options.perf_stats_interval
    match = re.match('([0-9]+)([sS]?)$', interval)
    assert match, ('Invalid value for --perf-stats-interval: %s' % interval)
    if match.group(2):
      self._interval_seconds = int(match.group(1))
    else:
      self._interval_iterations = int(match.group(1))
    assert self._interval_seconds or self._interval_iterations

  def _SampleStats(self, tab, results, seconds=None, iterations=None):
    """Records information and add it to the results."""

    def AddPoint(trace_name, units_y, value_y, chart_name=None):
      """Adds one data point to the results object."""
      if seconds:
        results.Add(trace_name + '_X', 'seconds', seconds,
                    data_type='unimportant', chart_name=chart_name)
      else:
        assert iterations, 'Neither seconds nor iterations given.'
        results.Add(trace_name + '_X', 'iterations', iterations,
                    data_type='unimportant', chart_name=chart_name)
      # Add the result as 'unimportant' because we want it to be unmonitored
      # by default.
      results.Add(trace_name + '_Y', units_y, value_y, data_type='unimportant',
                  chart_name=chart_name)
      # Save the value in a list so that summary stats can be calculated.
      if trace_name not in self._y_values:
        self._y_values[trace_name] = {
            'units': units_y,
            'chart_name': chart_name,
            'values': [],
        }
      self._y_values[trace_name]['values'].append(value_y)

    # DOM nodes and event listeners
    dom_stats = tab.dom_stats
    dom_node_count = dom_stats['node_count']
    event_listener_count = dom_stats['event_listener_count']
    AddPoint('dom_nodes', 'count', dom_node_count, chart_name='object_counts')
    AddPoint('event_listeners', 'count', event_listener_count,
             chart_name='object_counts')

    # Browser and renderer virtual memory stats
    memory_stats = self._browser.memory_stats
    def BrowserVMStats(statistic_name):
      """Get VM stats from the Browser object in KB."""
      return memory_stats[statistic_name].get('VM', 0) / 1024.0
    AddPoint('browser_vm', 'KB', BrowserVMStats('Browser'),
             chart_name='vm_stats')
    AddPoint('renderer_vm', 'KB', BrowserVMStats('Renderer'),
             chart_name='vm_stats')
    AddPoint('gpu_vm', 'KB', BrowserVMStats('Gpu'), chart_name='vm_stats')

    # V8 stats
    def V8StatsSum(counters):
      """Given a list of V8 counter names, get the sum of the values in KB."""
      stats = v8_object_stats.V8ObjectStatsMetric.GetV8StatsTable(tab, counters)
      return sum(stats.values()) / 1024.0
    AddPoint('v8_memory_committed', 'KB', V8StatsSum(_V8_BYTES_COMMITTED),
             chart_name='v8_counter_stats')
    AddPoint('v8_memory_used', 'KB', V8StatsSum(_V8_BYTES_USED),
             chart_name='v8_counter_stats')
    AddPoint('v8_memory_allocated', 'KB', V8StatsSum(_V8_MEMORY_ALLOCATED),
             chart_name='v8_counter_stats')

  def DidRunTest(self, browser, results):
    """Adds summary results (single number for one test run)."""
    # Keep track of total test run length in terms of seconds and page
    # repetitions, so that test run length can be known regardless of
    # whether it is specified in seconds or iterations.
    results.AddSummary('total_iterations', 'iterations', self._iterations,
                       data_type='unimportant')
    results.AddSummary('total_time', 'seconds', time.time() - self._start_time,
                       data_type='unimportant')

    # Add summary stats which could be monitored for anomalies.
    for trace_name in self._y_values:
      units = self._y_values[trace_name]['units']
      chart_name = self._y_values[trace_name]['chart_name']
      values = self._y_values[trace_name]['values']
      results.AddSummary(trace_name + '_max', units, max(values),
                         chart_name=chart_name)

