# 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 Queue
import datetime
import logging
import re
import threading

from pylib import perf_tests_helper


# Log marker containing SurfaceTexture timestamps.
_SURFACE_TEXTURE_TIMESTAMPS_MESSAGE = 'SurfaceTexture update timestamps'
_SURFACE_TEXTURE_TIMESTAMP_RE = '\d+'


class SurfaceStatsCollector(object):
  """Collects surface stats for a SurfaceView from the output of SurfaceFlinger.

  Args:
    adb: the adb connection to use.
  """
  def __init__(self, adb, trace_tag):
    self._adb = adb
    self._trace_tag = trace_tag
    self._collector_thread = None
    self._use_legacy_method = False
    self._surface_before = None
    self._get_data_event = None
    self._data_queue = None
    self._stop_event = None

  def __enter__(self):
    assert not self._collector_thread

    if self._ClearSurfaceFlingerLatencyData():
      self._get_data_event = threading.Event()
      self._stop_event = threading.Event()
      self._data_queue = Queue.Queue()
      self._collector_thread = threading.Thread(target=self._CollectorThread)
      self._collector_thread.start()
    else:
      self._use_legacy_method = True
      self._surface_before = self._GetSurfaceStatsLegacy()

  def __exit__(self, *args):
    self._PrintPerfResults()
    if self._collector_thread:
      self._stop_event.set()
      self._collector_thread.join()
      self._collector_thread = None

  def _PrintPerfResults(self):
    if self._use_legacy_method:
      surface_after = self._GetSurfaceStatsLegacy()
      td = surface_after['timestamp'] - self._surface_before['timestamp']
      seconds = td.seconds + td.microseconds / 1e6
      frame_count = (surface_after['page_flip_count'] -
                     self._surface_before['page_flip_count'])
    else:
      assert self._collector_thread
      (seconds, latencies) = self._GetDataFromThread()
      if not seconds or not len(latencies):
        logging.warning('Surface stat data is empty')
        return

      frame_count = len(latencies)
      jitter_count = 0
      last_latency = latencies[0]
      for latency in latencies[1:]:
        if latency > last_latency:
          jitter_count = jitter_count + 1
        last_latency = latency

      perf_tests_helper.PrintPerfResult(
          'surface_latencies', 'surface_latencies' + self._trace_tag,
          latencies, '')
      perf_tests_helper.PrintPerfResult(
          'peak_jitter', 'peak_jitter' + self._trace_tag, [max(latencies)], '')
      perf_tests_helper.PrintPerfResult(
          'jitter_percent', 'jitter_percent' + self._trace_tag,
          [jitter_count * 100.0 / frame_count], 'percent')

    print 'SurfaceMonitorTime: %fsecs' % seconds
    perf_tests_helper.PrintPerfResult(
        'avg_surface_fps', 'avg_surface_fps' + self._trace_tag,
        [int(round(frame_count / seconds))], 'fps')

  def _CollectorThread(self):
    last_timestamp = 0
    first_timestamp = 0
    latencies = []
    retries = 0
    has_collected_data = False

    while not self._stop_event.is_set():
      self._get_data_event.wait(1)
      try:
        (t, last_timestamp) = self._GetSurfaceFlingerLatencyData(last_timestamp,
                                                                 latencies)
        if (t, last_timestamp) == (None, None):
          retries += 1
          if retries < 3:
            continue
          if has_collected_data:
            # Some data has already been collected, but either the app
            # was closed or there's no new data. Signal the main thread and
            # wait.
            self._data_queue.put((None, None))
            self._stop_event.wait()
            break
          raise Exception('Unable to get surface flinger latency data')

        has_collected_data = True

        if not first_timestamp:
          first_timestamp = t

        if self._get_data_event.is_set():
          self._get_data_event.clear()
          self._data_queue.put(((last_timestamp - first_timestamp) / 1e9,
                                latencies))
          latencies = []
          first_timestamp = 0
      except Exception as e:
        # On any error, before aborting, put the exception into _data_queue to
        # prevent the main thread from waiting at _data_queue.get() infinitely.
        self._data_queue.put(e)
        raise

  def _GetDataFromThread(self):
    self._get_data_event.set()
    ret = self._data_queue.get()
    if isinstance(ret, Exception):
      raise ret
    return ret

  def _ClearSurfaceFlingerLatencyData(self):
    """Clears the SurfaceFlinger latency data.

    Returns:
      True if SurfaceFlinger latency is supported by the device, otherwise
      False.
    """
    # The command returns nothing if it is supported, otherwise returns many
    # lines of result just like 'dumpsys SurfaceFlinger'.
    results = self._adb.RunShellCommand(
        'dumpsys SurfaceFlinger --latency-clear SurfaceView')
    return not len(results)

  def _GetSurfaceFlingerLatencyData(self, previous_timestamp, latencies):
    """Returns collected SurfaceFlinger latency data.

    Args:
      previous_timestamp: The timestamp returned from the previous call or 0.
          Only data after this timestamp will be returned.
      latencies: A list to receive latency data. The latencies are integers
          each of which is the number of refresh periods of each frame.

    Returns:
      A tuple containing:
      - The timestamp of the beginning of the first frame (ns),
      - The timestamp of the end of the last frame (ns).
      The tuple may be (None, None) if there was no data collected (for example,
      if the app was closed before the collector thread has finished).
    """
    # adb shell dumpsys SurfaceFlinger --latency <window name>
    # prints some information about the last 128 frames displayed in
    # that window.
    # The data returned looks like this:
    # 16954612
    # 7657467895508   7657482691352   7657493499756
    # 7657484466553   7657499645964   7657511077881
    # 7657500793457   7657516600576   7657527404785
    # (...)
    #
    # The first line is the refresh period (here 16.95 ms), it is followed
    # by 128 lines w/ 3 timestamps in nanosecond each:
    # A) when the app started to draw
    # B) the vsync immediately preceding SF submitting the frame to the h/w
    # C) timestamp immediately after SF submitted that frame to the h/w
    #
    # The difference between the 1st and 3rd timestamp is the frame-latency.
    # An interesting data is when the frame latency crosses a refresh period
    # boundary, this can be calculated this way:
    #
    # ceil((C - A) / refresh-period)
    #
    # (each time the number above changes, we have a "jank").
    # If this happens a lot during an animation, the animation appears
    # janky, even if it runs at 60 fps in average.
    #
    # We use the special "SurfaceView" window name because the statistics for
    # the activity's main window are not updated when the main web content is
    # composited into a SurfaceView.
    results = self._adb.RunShellCommand(
        'dumpsys SurfaceFlinger --latency SurfaceView', log_result=True)
    if not len(results):
      return (None, None)

    refresh_period = int(results[0])
    last_timestamp = previous_timestamp
    first_timestamp = 0
    for line in results[1:]:
      fields = line.split()
      if len(fields) == 3:
        timestamp = long(fields[0])
        last_timestamp = long(fields[2])
        if (timestamp > previous_timestamp):
          if not first_timestamp:
            first_timestamp = timestamp
          # This is integral equivalent of ceil((C-A) / refresh-period)
          latency_ns = int(last_timestamp - timestamp)
          latencies.append((latency_ns + refresh_period - 1) / refresh_period)
    return (first_timestamp, last_timestamp)

  def _GetSurfaceStatsLegacy(self):
    """Legacy method (before JellyBean), returns the current Surface index
       and timestamp.

    Calculate FPS by measuring the difference of Surface index returned by
    SurfaceFlinger in a period of time.

    Returns:
      Dict of {page_flip_count (or 0 if there was an error), timestamp}.
    """
    results = self._adb.RunShellCommand('service call SurfaceFlinger 1013')
    assert len(results) == 1
    match = re.search('^Result: Parcel\((\w+)', results[0])
    cur_surface = 0
    if match:
      try:
        cur_surface = int(match.group(1), 16)
      except Exception:
        logging.error('Failed to parse current surface from ' + match.group(1))
    else:
      logging.warning('Failed to call SurfaceFlinger surface ' + results[0])
    return {
        'page_flip_count': cur_surface,
        'timestamp': datetime.datetime.now(),
    }
