# Copyright 2016 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 collections
import copy
import unittest

from activity_lens import (ActivityLens, _EventsTree)
import clovis_constants
import test_utils
import tracing


class ActivityLensTestCase(unittest.TestCase):
  @classmethod
  def _EventsFromRawEvents(cls, raw_events):
    tracing_track = tracing.TracingTrack(None,
        clovis_constants.DEFAULT_CATEGORIES)
    tracing_track.Handle(
        'Tracing.dataCollected', {'params': {'value': raw_events}})
    return tracing_track.GetEvents()

  def setUp(self):
    self.tracing_track = tracing.TracingTrack(None,
        clovis_constants.DEFAULT_CATEGORIES)

  def testGetRendererMainThread(self):
    first_renderer_tid = 12345
    second_renderer_tid = 123456
    raw_events =  [
        {u'args': {u'name': u'CrBrowserMain'},
         u'cat': u'__metadata',
         u'name': u'thread_name',
         u'ph': u'M',
         u'pid': 1,
         u'tid': 123,
         u'ts': 0},
        {u'args': {u'name': u'CrRendererMain'},
         u'cat': u'__metadata',
         u'name': u'thread_name',
         u'ph': u'M',
         u'pid': 1,
         u'tid': first_renderer_tid,
         u'ts': 0},
        {u'args': {u'name': u'CrRendererMain'},
         u'cat': u'__metadata',
         u'name': u'thread_name',
         u'ph': u'M',
         u'pid': 1,
         u'tid': second_renderer_tid,
         u'ts': 0}]
    raw_events += [
        {u'args': {u'data': {}},
         u'cat': u'devtools.timeline,v8',
         u'name': u'FunctionCall',
         u'ph': u'X',
         u'pid': 1,
         u'tdur': 0,
         u'tid': first_renderer_tid,
         u'ts': 251427174674,
         u'tts': 5107725}] * 100
    raw_events += [
        {u'args': {u'data': {}},
         u'cat': u'devtools.timeline,v8',
         u'name': u'FunctionCall',
         u'ph': u'X',
         u'pid': 1,
         u'tdur': 0,
         u'tid': second_renderer_tid,
         u'ts': 251427174674,
         u'tts': 5107725}] * 150
    # There are more events from first_renderer_tid when (incorrectly) ignoring
    # the PID.
    raw_events += [
        {u'args': {u'data': {}},
         u'cat': u'devtools.timeline,v8',
         u'name': u'FunctionCall',
         u'ph': u'X',
         u'pid': 12,
         u'tdur': 0,
         u'tid': first_renderer_tid,
         u'ts': 251427174674,
         u'tts': 5107725}] * 100
    events = self._EventsFromRawEvents(raw_events)
    self.assertEquals((1, second_renderer_tid),
                      ActivityLens._GetRendererMainThreadId(events))

  def testThreadBusyness(self):
    raw_events =  [
        {u'args': {},
         u'cat': u'toplevel',
         u'dur': 200 * 1000,
         u'name': u'MessageLoop::RunTask',
         u'ph': u'X',
         u'pid': 123,
         u'tid': 123,
         u'ts': 0,
         u'tts': 56485},
        {u'args': {},
         u'cat': u'toplevel',
         u'dur': 8 * 200,
         u'name': u'MessageLoop::NestedSomething',
         u'ph': u'X',
         u'pid': 123,
         u'tid': 123,
         u'ts': 0,
         u'tts': 0}]
    events = self._EventsFromRawEvents(raw_events)
    self.assertEquals(200, ActivityLens._ThreadBusyness(events, 0, 1000))
    # Clamping duration.
    self.assertEquals(100, ActivityLens._ThreadBusyness(events, 0, 100))
    self.assertEquals(50, ActivityLens._ThreadBusyness(events, 25, 75))

  def testScriptExecuting(self):
    url = u'http://example.com/script.js'
    raw_events = [
        {u'args': {u'data': {u'scriptName': url}},
         u'cat': u'devtools.timeline,v8',
         u'dur': 250 * 1000,
         u'name': u'FunctionCall',
         u'ph': u'X',
         u'pid': 123,
         u'tdur': 247,
         u'tid': 123,
         u'ts': 0,
         u'tts': 0},
        {u'args': {u'data': {}},
         u'cat': u'devtools.timeline,v8',
         u'dur': 350 * 1000,
         u'name': u'EvaluateScript',
         u'ph': u'X',
         u'pid': 123,
         u'tdur': 247,
         u'tid': 123,
         u'ts': 0,
         u'tts': 0}]
    events = self._EventsFromRawEvents(raw_events)
    self.assertEquals(2, len(ActivityLens._ScriptsExecuting(events, 0, 1000)))
    self.assertTrue(None in ActivityLens._ScriptsExecuting(events, 0, 1000))
    self.assertEquals(
        350, ActivityLens._ScriptsExecuting(events, 0, 1000)[None])
    self.assertTrue(url in ActivityLens._ScriptsExecuting(events, 0, 1000))
    self.assertEquals(250, ActivityLens._ScriptsExecuting(events, 0, 1000)[url])
    # Aggreagates events.
    raw_events.append({u'args': {u'data': {}},
                       u'cat': u'devtools.timeline,v8',
                       u'dur': 50 * 1000,
                       u'name': u'EvaluateScript',
                       u'ph': u'X',
                       u'pid': 123,
                       u'tdur': 247,
                       u'tid': 123,
                       u'ts': 0,
                       u'tts': 0})
    events = self._EventsFromRawEvents(raw_events)
    self.assertEquals(
        350 + 50, ActivityLens._ScriptsExecuting(events, 0, 1000)[None])

  def testParsing(self):
    css_url = u'http://example.com/style.css'
    html_url = u'http://example.com/yeah.htnl'
    raw_events = [
        {u'args': {u'data': {u'styleSheetUrl': css_url}},
         u'cat': u'blink,devtools.timeline',
         u'dur': 400 * 1000,
         u'name': u'ParseAuthorStyleSheet',
         u'ph': u'X',
         u'pid': 32723,
         u'tdur': 49721,
         u'tid': 32738,
         u'ts': 0,
         u'tts': 216148},
        {u'args': {u'beginData': {u'url': html_url}},
         u'cat': u'devtools.timeline',
         u'dur': 42 * 1000,
         u'name': u'ParseHTML',
         u'ph': u'X',
         u'pid': 32723,
         u'tdur': 49721,
         u'tid': 32738,
         u'ts': 0,
         u'tts': 5032310},]
    events = self._EventsFromRawEvents(raw_events)
    self.assertEquals(2, len(ActivityLens._Parsing(events, 0, 1000)))
    self.assertTrue(css_url in ActivityLens._Parsing(events, 0, 1000))
    self.assertEquals(400, ActivityLens._Parsing(events, 0, 1000)[css_url])
    self.assertTrue(html_url in ActivityLens._Parsing(events, 0, 1000))
    self.assertEquals(42, ActivityLens._Parsing(events, 0, 1000)[html_url])

  def testBreakdownEdgeActivityByInitiator(self):
    requests = [test_utils.MakeRequest(0, 1, 10, 20, 30),
                test_utils.MakeRequest(0, 1, 50, 60, 70)]
    raw_events = [
        {u'args': {u'beginData': {u'url': requests[0].url}},
         u'cat': u'devtools.timeline',
         u'dur': 12 * 1000,
         u'name': u'ParseHTML',
         u'ph': u'X',
         u'pid': 1,
         u'tid': 1,
         u'ts': 25 * 1000},
        {u'args': {u'data': {'scriptName': requests[0].url}},
         u'cat': u'devtools.timeline,v8',
         u'dur': 0,
         u'name': u'EvaluateScript',
         u'ph': u'X',
         u'pid': 1,
         u'tid': 1,
         u'ts': 0},
        {u'cat': u'toplevel',
         u'dur': 100 * 1000,
         u'name': u'MessageLoop::RunTask',
         u'ph': u'X',
         u'pid': 1,
         u'tid': 1,
         u'ts': 0},
        {u'args': {u'name': u'CrRendererMain'},
         u'cat': u'__metadata',
         u'name': u'thread_name',
         u'ph': u'M',
         u'pid': 1,
         u'tid': 1,
         u'ts': 0}]
    activity = self._ActivityLens(requests, raw_events)
    dep = (requests[0], requests[1], 'parser')
    self.assertEquals(
        {'unrelated_work': 18, 'idle': 0, 'script': 0, 'parsing': 12,
         'other_url': 0, 'unknown_url': 0},
        activity.BreakdownEdgeActivityByInitiator(dep))
    dep = (requests[0], requests[1], 'other')
    # Truncating the event from the parent request end.
    self.assertEquals(
        {'unrelated_work': 13, 'idle': 0, 'script': 0, 'parsing': 7,
         'other_url': 0, 'unknown_url': 0},
        activity.BreakdownEdgeActivityByInitiator(dep))
    # Unknown URL
    raw_events[0]['args']['beginData']['url'] = None
    activity = self._ActivityLens(requests, raw_events)
    dep = (requests[0], requests[1], 'parser')
    self.assertEquals(
        {'unrelated_work': 18, 'idle': 0, 'script': 0, 'parsing': 0,
         'other_url': 0, 'unknown_url': 12},
        activity.BreakdownEdgeActivityByInitiator(dep))
    # Script
    raw_events[1]['ts'] = 40 * 1000
    raw_events[1]['dur'] = 6 * 1000
    activity = self._ActivityLens(requests, raw_events)
    dep = (requests[0], requests[1], 'script')
    self.assertEquals(
        {'unrelated_work': 7, 'idle': 0, 'script': 6, 'parsing': 0,
         'other_url': 0, 'unknown_url': 7},
        activity.BreakdownEdgeActivityByInitiator(dep))
    # Other URL
    raw_events[1]['args']['data']['scriptName'] = 'http://other.com/url'
    activity = self._ActivityLens(requests, raw_events)
    self.assertEquals(
        {'unrelated_work': 7, 'idle': 0, 'script': 0., 'parsing': 0.,
         'other_url': 6., 'unknown_url': 7.},
        activity.BreakdownEdgeActivityByInitiator(dep))

  def testMainRendererThreadBusyness(self):
    raw_events =  [
        {u'args': {u'name': u'CrRendererMain'},
         u'cat': u'__metadata',
         u'name': u'thread_name',
         u'ph': u'M',
         u'pid': 1,
         u'tid': 12,
         u'ts': 0},
        {u'args': {},
         u'cat': u'toplevel',
         u'dur': 200 * 1000,
         u'name': u'MessageLoop::RunTask',
         u'ph': u'X',
         u'pid': 1,
         u'tid': 12,
         u'ts': 0,
         u'tts': 56485},
        {u'args': {},
         u'cat': u'toplevel',
         u'dur': 8 * 200,
         u'name': u'MessageLoop::NestedSomething',
         u'ph': u'X',
         u'pid': 1,
         u'tid': 12,
         u'ts': 0,
         u'tts': 0},
        {u'args': {},
         u'cat': u'toplevel',
         u'dur': 500 * 1000,
         u'name': u'MessageLoop::RunTask',
         u'ph': u'X',
         u'pid': 12,
         u'tid': 12,
         u'ts': 0,
         u'tts': 56485}]
    lens = self._ActivityLens([], raw_events)
    # Ignore events from another PID.
    self.assertEquals(200, lens.MainRendererThreadBusyness(0, 1000))
    # Clamping duration.
    self.assertEquals(100, lens.MainRendererThreadBusyness(0, 100))
    self.assertEquals(50, lens.MainRendererThreadBusyness(25, 75))
    # Other PID.
    raw_events[0]['pid'] = 12
    lens = self._ActivityLens([], raw_events)
    self.assertEquals(500, lens.MainRendererThreadBusyness(0, 1000))

  def _ActivityLens(self, requests, raw_events):
    loading_trace = test_utils.LoadingTraceFromEvents(
        requests, None, raw_events)
    return ActivityLens(loading_trace)


class EventsTreeTestCase(unittest.TestCase):
  FakeEvent = collections.namedtuple(
      'FakeEvent', ('name', 'start_msec', 'end_msec'))
  _ROOT_EVENT = FakeEvent('-1', 0, 20)
  _EVENTS = [
      FakeEvent('0', 2, 4), FakeEvent('1', 1, 5),
      FakeEvent('2', 6, 9),
      FakeEvent('3', 13, 14), FakeEvent('4', 14, 17), FakeEvent('5', 12, 18)]

  def setUp(self):
    self.tree = _EventsTree(self._ROOT_EVENT, copy.deepcopy(self._EVENTS))

  def testEventsTreeConstruction(self):
    self.assertEquals(self._ROOT_EVENT, self.tree.event)
    self.assertEquals(3, len(self.tree.children))
    self.assertEquals(self._EVENTS[1], self.tree.children[0].event)
    self.assertEquals(self._EVENTS[0], self.tree.children[0].children[0].event)
    self.assertEquals(self._EVENTS[2], self.tree.children[1].event)
    self.assertEquals([], self.tree.children[1].children)
    self.assertEquals(self._EVENTS[5], self.tree.children[2].event)
    self.assertEquals(2, len(self.tree.children[2].children))

  def testDominatingEventsWithNames(self):
    self.assertListEqual(
        [self._ROOT_EVENT], self.tree.DominatingEventsWithNames(('-1')))
    self.assertListEqual(
        [self._ROOT_EVENT], self.tree.DominatingEventsWithNames(('-1', '0')))
    self.assertListEqual(
        [self._EVENTS[1], self._EVENTS[5]],
        self.tree.DominatingEventsWithNames(('1', '5')))


if __name__ == '__main__':
  unittest.main()
