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

import re
import sys

import android_commands
import json
import math

# Valid values of result type.
RESULT_TYPES = {'unimportant': 'RESULT ',
                'default': '*RESULT ',
                'informational': '',
                'unimportant-histogram': 'HISTOGRAM ',
                'histogram': '*HISTOGRAM '}


def _EscapePerfResult(s):
  """Escapes |s| for use in a perf result."""
  return re.sub('[\:|=/#&,]', '_', s)


def GeomMeanAndStdDevFromHistogram(histogram_json):
  histogram = json.loads(histogram_json)
  # Handle empty histograms gracefully.
  if not 'buckets' in histogram:
    return 0.0, 0.0
  count = 0
  sum_of_logs = 0
  for bucket in histogram['buckets']:
    if 'high' in bucket:
      bucket['mean'] = (bucket['low'] + bucket['high']) / 2.0
    else:
      bucket['mean'] = bucket['low']
    if bucket['mean'] > 0:
      sum_of_logs += math.log(bucket['mean']) * bucket['count']
      count += bucket['count']

  if count == 0:
    return 0.0, 0.0

  sum_of_squares = 0
  geom_mean = math.exp(sum_of_logs / count)
  for bucket in histogram['buckets']:
    if bucket['mean'] > 0:
      sum_of_squares += (bucket['mean'] - geom_mean) ** 2 * bucket['count']
  return geom_mean, math.sqrt(sum_of_squares / count)


def _MeanAndStdDevFromList(values):
  avg = None
  sd = None
  if len(values) > 1:
    try:
      value = '[%s]' % ','.join([str(v) for v in values])
      avg = sum([float(v) for v in values]) / len(values)
      sqdiffs = [(float(v) - avg) ** 2 for v in values]
      variance = sum(sqdiffs) / (len(values) - 1)
      sd = math.sqrt(variance)
    except ValueError:
      value = ", ".join(values)
  else:
    value = values[0]
  return value, avg, sd


def PrintPages(page_list):
  """Prints list of pages to stdout in the format required by perf tests."""
  print 'Pages: [%s]' % ','.join([_EscapePerfResult(p) for p in page_list])


def PrintPerfResult(measurement, trace, values, units, result_type='default',
                    print_to_stdout=True):
  """Prints numerical data to stdout in the format required by perf tests.

  The string args may be empty but they must not contain any colons (:) or
  equals signs (=).

  Args:
    measurement: A description of the quantity being measured, e.g. "vm_peak".
    trace: A description of the particular data point, e.g. "reference".
    values: A list of numeric measured values.
    units: A description of the units of measure, e.g. "bytes".
    result_type: Accepts values of RESULT_TYPES.
    print_to_stdout: If True, prints the output in stdout instead of returning
        the output to caller.

    Returns:
      String of the formated perf result.
  """
  assert result_type in RESULT_TYPES, 'result type: %s is invalid' % result_type

  trace_name = _EscapePerfResult(trace)

  if result_type in ['unimportant', 'default', 'informational']:
    assert isinstance(values, list)
    assert len(values)
    assert '/' not in measurement
    value, avg, sd = _MeanAndStdDevFromList(values)
    output = '%s%s: %s%s%s %s' % (
        RESULT_TYPES[result_type],
        _EscapePerfResult(measurement),
        trace_name,
        # Do not show equal sign if the trace is empty. Usually it happens when
        # measurement is enough clear to describe the result.
        '= ' if trace_name else '',
        value,
        units)
  else:
    assert(result_type in ['histogram', 'unimportant-histogram'])
    assert isinstance(values, list)
    # The histograms can only be printed individually, there's no computation
    # across different histograms.
    assert len(values) == 1
    value = values[0]
    output = '%s%s: %s= %s' % (
        RESULT_TYPES[result_type],
        _EscapePerfResult(measurement),
        trace_name,
        value)
    avg, sd = GeomMeanAndStdDevFromHistogram(value)

  if avg:
    output += '\nAvg %s: %f%s' % (measurement, avg, units)
  if sd:
    output += '\nSd  %s: %f%s' % (measurement, sd, units)
  if print_to_stdout:
    print output
    sys.stdout.flush()
  return output


class CacheControl(object):
  _DROP_CACHES = '/proc/sys/vm/drop_caches'

  def __init__(self, adb):
    self._adb = adb

  def DropRamCaches(self):
    """Drops the filesystem ram caches for performance testing."""
    self._adb.RunShellCommand('su -c sync')
    self._adb.SetProtectedFileContents(CacheControl._DROP_CACHES, '3')


class PerfControl(object):
  """Provides methods for setting the performance mode of a device."""
  _SCALING_GOVERNOR_FMT = (
      '/sys/devices/system/cpu/cpu%d/cpufreq/scaling_governor')

  def __init__(self, adb):
    self._adb = adb
    kernel_max = self._adb.GetFileContents('/sys/devices/system/cpu/kernel_max',
                                         log_result=False)
    assert kernel_max, 'Unable to find /sys/devices/system/cpu/kernel_max'
    self._kernel_max = int(kernel_max[0])
    self._original_scaling_governor = self._adb.GetFileContents(
      PerfControl._SCALING_GOVERNOR_FMT % 0,
      log_result=False)[0]

  def SetHighPerfMode(self):
    """Sets the highest possible performance mode for the device."""
    self._SetScalingGovernorInternal('performance')

  def SetDefaultPerfMode(self):
    """Sets the performance mode for the device to its default mode."""
    product_model = self._adb.GetProductModel()
    governor_mode = {
                    "GT-I9300" : 'pegasusq',
                    "Galaxy Nexus" : 'interactive',
                    "Nexus 4" : 'ondemand',
                    "Nexus 7" : 'interactive',
                    "Nexus 10": 'interactive'
                    }.get(product_model, 'ondemand')
    self._SetScalingGovernorInternal(governor_mode)

  def RestoreOriginalPerfMode(self):
    """Resets the original performance mode of the device."""
    self._SetScalingGovernorInternal(self._original_scaling_governor)

  def _SetScalingGovernorInternal(self, value):
    for cpu in range(self._kernel_max + 1):
      scaling_governor_file = PerfControl._SCALING_GOVERNOR_FMT % cpu
      if self._adb.FileExistsOnDevice(scaling_governor_file):
        self._adb.SetProtectedFileContents(scaling_governor_file, value)
