# 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 unittest
import random

from metrics import discrepancy
from metrics import smoothness
from metrics.gpu_rendering_stats import GpuRenderingStats
from telemetry.page import page
from telemetry.page.page_measurement_results import PageMeasurementResults
from telemetry.core.backends.chrome.tracing_backend import RawTraceResultImpl
from telemetry.core.backends.chrome.trace_result import TraceResult

class MockTimer(object):
  """ An instance of this class is used as a global timer to generate
      random durations for stats and consistent timestamps for all mock trace
      events.
  """
  def __init__(self):
    self.microseconds = 0

  def Advance(self, low = 0, high = 100000):
    duration = random.randint(low, high)
    self.microseconds += duration
    return duration

class MockFrame(object):
  """ This class mocks rendering stats, texture stats and latency stats for
      a single frame
  """
  def __init__(self, mock_timer):
    """ Initialize the stats to random values """
    self.start = mock_timer.microseconds
    self.main_stats = {}
    self.impl_stats = {}
    self.texture_stats = {}
    self.latency_stats = {}
    self.main_stats['animation_frame_count'] = 0
    self.main_stats['screen_frame_count'] = 0
    self.main_stats['paint_time'] = mock_timer.Advance()
    self.main_stats['record_time'] = mock_timer.Advance()
    self.main_stats['commit_time'] = mock_timer.Advance()
    self.main_stats['commit_count'] = 1
    self.main_stats['painted_pixel_count'] = random.randint(0, 2000000)
    self.main_stats['recorded_pixel_count'] = random.randint(0, 2000000)
    self.main_stats['image_gathering_count'] = random.randint(0, 100)
    self.main_stats['image_gathering_time'] = mock_timer.Advance()
    self.impl_stats['screen_frame_count'] = 1
    self.impl_stats['dropped_frame_count'] = random.randint(0, 4)
    self.impl_stats['rasterize_time'] = mock_timer.Advance()
    self.impl_stats['rasterize_time_for_now_bins_on_pending_tree'] = \
        mock_timer.Advance()
    self.impl_stats['best_rasterize_time'] = mock_timer.Advance()
    self.impl_stats['rasterized_pixel_count'] = random.randint(0, 2000000)
    self.impl_stats['impl_thread_scroll_count'] = random.randint(0, 4)
    self.impl_stats['main_thread_scroll_count'] = random.randint(0, 4)
    self.impl_stats['drawn_layer_count'] = random.randint(0, 10)
    self.impl_stats['missing_tile_count'] = random.randint(0, 10000)
    self.impl_stats['deferred_image_decode_count'] = random.randint(0, 100)
    self.impl_stats['deferred_image_cache_hit_count'] = random.randint(0, 50)
    self.impl_stats['tile_analysis_count'] = random.randint(0, 10000)
    self.impl_stats['solid_color_tile_analysis_count'] = random.randint(0, 1000)
    self.impl_stats['deferred_image_decode_time'] = mock_timer.Advance()
    self.impl_stats['tile_analysis_time'] = mock_timer.Advance()
    self.texture_stats['texture_upload_count'] = random.randint(0, 1000)
    self.texture_stats['texture_upload_time'] = mock_timer.Advance()
    self.latency_stats['input_event_count'] = random.randint(0, 20)
    self.latency_stats['input_event_latency'] = mock_timer.Advance()
    self.latency_stats['touch_ui_count'] = random.randint(0, 20)
    self.latency_stats['touch_ui_latency'] = mock_timer.Advance()
    self.latency_stats['touch_acked_count'] = random.randint(0, 20)
    self.latency_stats['touch_acked_latency'] = mock_timer.Advance()
    self.latency_stats['scroll_update_count'] = random.randint(0, 20)
    self.latency_stats['scroll_update_latency'] = mock_timer.Advance()
    self.end = mock_timer.microseconds
    self.duration = self.end - self.start

  def AddToRenderingStats(self, rendering_stats):
    """ Add the stats for this frame to a mock RenderingStats object (i.e. a
        dictionary)
    """
    rs = rendering_stats
    rs['totalTimeInSeconds'] += self.duration / 1e6
    rs['numFramesSentToScreen'] += (self.main_stats['screen_frame_count'] +
                                    self.impl_stats['screen_frame_count'])
    rs['droppedFrameCount'] += self.impl_stats['dropped_frame_count']
    rs['numImplThreadScrolls'] += self.impl_stats['impl_thread_scroll_count']
    rs['numMainThreadScrolls'] += self.impl_stats['main_thread_scroll_count']
    rs['numLayersDrawn'] += self.impl_stats['drawn_layer_count']
    rs['numMissingTiles'] += self.impl_stats['missing_tile_count']
    rs['textureUploadCount'] += self.texture_stats['texture_upload_count']
    rs['totalTextureUploadTimeInSeconds'] += \
        self.texture_stats['texture_upload_time']
    rs['totalCommitCount'] += self.main_stats['commit_count']
    rs['totalCommitTimeInSeconds'] += self.main_stats['commit_time']
    rs['totalDeferredImageDecodeCount'] += self.impl_stats[
        'deferred_image_decode_count']
    rs['totalDeferredImageDecodeTimeInSeconds'] += self.impl_stats[
        'deferred_image_decode_time']
    rs['totalDeferredImageCacheHitCount'] += self.impl_stats[
        'deferred_image_cache_hit_count']
    rs['totalImageGatheringCount'] += self.main_stats['image_gathering_count']
    rs['totalImageGatheringTimeInSeconds'] += self.main_stats[
        'image_gathering_time']
    rs['totalTilesAnalyzed'] += self.impl_stats['tile_analysis_count']
    rs['totalTileAnalysisTimeInSeconds'] += self.impl_stats[
        'tile_analysis_time']
    rs['solidColorTilesAnalyzed'] += self.impl_stats[
        'solid_color_tile_analysis_count']
    rs['inputEventCount'] += self.latency_stats['input_event_count']
    rs['totalInputLatency'] += self.latency_stats['input_event_latency']
    rs['touchUICount'] += self.latency_stats['touch_ui_count']
    rs['totalTouchUILatency'] += self.latency_stats['touch_ui_latency']
    rs['touchAckedCount'] += self.latency_stats['touch_acked_count']
    rs['totalTouchAckedLatency'] += self.latency_stats['touch_acked_latency']
    rs['scrollUpdateCount'] += self.latency_stats['scroll_update_count']
    rs['totalScrollUpdateLatency'] += self.latency_stats[
        'scroll_update_latency']

  def AppendTraceEventForMainThreadStats(self, trace):
    """ Append a trace event with the main thread stats to trace """
    event = {'name': 'MainThreadRenderingStats::IssueTraceEvent',
             'tts': self.end,
             'pid': 20978,
             'ts': self.end,
             'cat': 'benchmark',
             's': 't',
             'tid': 11,
             'ph': 'i',
             'args': {'data': self.main_stats}}
    trace.append(event)

  def AppendTraceEventForImplThreadStats(self, trace):
    """ Append a trace event with the impl thread stat to trace """
    event = {'name': 'ImplThreadRenderingStats::IssueTraceEvent',
             'tts': self.end,
             'pid': 20978,
             'ts': self.end,
             'cat': 'benchmark',
             's': 't',
             'tid': 11,
             'ph': 'i',
             'args': {'data': self.impl_stats}}
    trace.append(event)

class SmoothnessMetricsUnitTest(unittest.TestCase):
  def FindTimelineMarker(self, timeline):
    events = [s for
              s in timeline.GetAllEventsOfName(
                  smoothness.TIMELINE_MARKER)
              if s.parent_slice == None]
    if len(events) != 1:
      raise LookupError, 'timeline marker not found'
    return events[0]

  def testCalcResultsTraceEvents(self):
    # Make the test repeatable by seeding the random number generator
    random.seed(1234567)
    mock_timer = MockTimer()
    trace = []
    rendering_stats = {
        'totalTimeInSeconds': 0.0,
        'numFramesSentToScreen': 0.0,
        'droppedFrameCount': 0.0,
        'numImplThreadScrolls': 0.0,
        'numMainThreadScrolls': 0.0,
        'numLayersDrawn': 0.0,
        'numMissingTiles': 0.0,
        'textureUploadCount': 0.0,
        'totalTextureUploadTimeInSeconds': 0.0,
        'totalCommitCount': 0.0,
        'totalCommitTimeInSeconds': 0.0,
        'totalDeferredImageDecodeCount': 0.0,
        'totalDeferredImageDecodeTimeInSeconds': 0.0,
        'totalDeferredImageCacheHitCount': 0.0,
        'totalImageGatheringCount': 0.0,
        'totalImageGatheringTimeInSeconds': 0.0,
        'totalTilesAnalyzed': 0.0,
        'totalTileAnalysisTimeInSeconds': 0.0,
        'solidColorTilesAnalyzed': 0.0,
        'inputEventCount': 0.0,
        'totalInputLatency': 0.0,
        'touchUICount': 0.0,
        'totalTouchUILatency': 0.0,
        'touchAckedCount': 0.0,
        'totalTouchAckedLatency': 0.0,
        'scrollUpdateCount': 0.0,
        'totalScrollUpdateLatency': 0.0}

    # Append a start trace event for the timeline marker
    trace.append({'name': smoothness.TIMELINE_MARKER,
                  'tts': mock_timer.microseconds,
                  'args': {},
                  'pid': 20978,
                  'ts': mock_timer.microseconds,
                  'cat': 'webkit',
                  'tid': 11,
                  'ph': 'S',
                  'id': '0xafb37737e249e055'})
    # Generate 100 random mock frames, append their trace events, and accumulate
    # stats in rendering_stats.
    for _ in xrange(0, 100):
      mock_frame = MockFrame(mock_timer)
      mock_frame.AppendTraceEventForMainThreadStats(trace)
      mock_frame.AppendTraceEventForImplThreadStats(trace)
      mock_frame.AddToRenderingStats(rendering_stats)
    # Append finish trace event for timeline marker
    trace.append({'name': smoothness.TIMELINE_MARKER,
                  'tts': mock_timer.microseconds,
                  'args': {},
                  'pid': 20978,
                  'ts': mock_timer.microseconds,
                  'cat': 'webkit',
                  'tid': 11,
                  'ph': 'F',
                  'id': '0xafb37737e249e055'})

    # Create timeline object from the trace
    trace_impl = RawTraceResultImpl(trace)
    trace_result = TraceResult(trace_impl)
    timeline = trace_result.AsTimelineModel()


    timeline_marker = self.FindTimelineMarker(timeline)
    stats = GpuRenderingStats(timeline_marker,
                              rendering_stats,
                              True)

    res = PageMeasurementResults()
    res.WillMeasurePage(page.Page('http://foo.com/', None))
    smoothness.CalcResults(stats, res)
    res.DidMeasurePage()

    rs = rendering_stats

    # Scroll Results
    self.assertAlmostEquals(
        round(rs['totalTimeInSeconds'] / rs['numFramesSentToScreen'] * 1000.0,
              3),
        res.page_results[0]['mean_frame_time'].value, 2)
    # We don't verify the correctness of the discrepancy computation
    # itself, because we have a separate unit test for that purpose.
    self.assertEquals(
        round(discrepancy.FrameDiscrepancy(stats.screen_frame_timestamps,
                                           True), 4),
        res.page_results[0]['experimental_jank'].value)
    self.assertAlmostEquals(
        round(rs['droppedFrameCount'] / (rs['numFramesSentToScreen'] +
                                         rs['droppedFrameCount']) * 100.0, 1),
        res.page_results[0]['dropped_percent'].value)
    self.assertAlmostEquals(
        round(rs['numImplThreadScrolls'] / (rs['numImplThreadScrolls'] +
                                            rs['numMainThreadScrolls']) * 100.0,
              1),
        res.page_results[0]['percent_impl_scrolled'].value)
    self.assertAlmostEquals(
        round(rs['numLayersDrawn'] / rs['numFramesSentToScreen'], 1),
        res.page_results[0]['average_num_layers_drawn'].value)
    self.assertAlmostEquals(
        round(rs['numMissingTiles'] / rs['numFramesSentToScreen'], 1),
        res.page_results[0]['average_num_missing_tiles'].value)

    # Texture Upload Results
    self.assertAlmostEquals(
        round(rs['totalCommitTimeInSeconds'] / rs['totalCommitCount'] * 1000.0,
              3),
        res.page_results[0]['average_commit_time'].value)
    self.assertEquals(
        rs['textureUploadCount'],
        res.page_results[0]['texture_upload_count'].value)
    self.assertEquals(
        rs['totalTextureUploadTimeInSeconds'],
        res.page_results[0]['total_texture_upload_time'].value)

    # Image Decoding Results
    self.assertEquals(
        rs['totalDeferredImageDecodeCount'],
        res.page_results[0]['total_deferred_image_decode_count'].value)
    self.assertEquals(
        rs['totalDeferredImageCacheHitCount'],
        res.page_results[0]['total_image_cache_hit_count'].value)
    self.assertAlmostEquals(
        round(rs['totalImageGatheringTimeInSeconds'] /
              rs['totalImageGatheringCount'] * 1000.0, 3),
        res.page_results[0]['average_image_gathering_time'].value)
    self.assertEquals(
        rs['totalDeferredImageDecodeTimeInSeconds'],
        res.page_results[0]['total_deferred_image_decoding_time'].value)

    # Tile Analysis Results
    self.assertEquals(
        rs['totalTilesAnalyzed'],
        res.page_results[0]['total_tiles_analyzed'].value)
    self.assertEquals(
        rs['solidColorTilesAnalyzed'],
        res.page_results[0]['solid_color_tiles_analyzed'].value)
    self.assertAlmostEquals(
        round(rs['totalTileAnalysisTimeInSeconds'] /
              rs['totalTilesAnalyzed'] * 1000.0, 3),
        res.page_results[0]['average_tile_analysis_time'].value)

    # Latency Results
    self.assertAlmostEquals(
        round(rs['totalInputLatency'] / rs['inputEventCount'] * 1000.0, 3),
        res.page_results[0]['average_latency'].value)
    self.assertAlmostEquals(
        round(rs['totalTouchUILatency'] / rs['touchUICount'] * 1000.0, 3),
        res.page_results[0]['average_touch_ui_latency'].value)
    self.assertAlmostEquals(
        round(rs['totalTouchAckedLatency'] / rs['touchAckedCount'] * 1000.0, 3),
        res.page_results[0]['average_touch_acked_latency'].value)
    self.assertAlmostEquals(
        round(rs['totalScrollUpdateLatency'] / rs['scrollUpdateCount'] * 1000.0,
              3),
        res.page_results[0]['average_scroll_update_latency'].value)
