1# Copyright 2017 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5import logging
6import numpy
7import os
8import tempfile
9import time
10
11from autotest_lib.client.bin import test
12from autotest_lib.client.bin.input import input_device
13from autotest_lib.client.bin.input.input_event_player import InputEventPlayer
14from autotest_lib.client.common_lib import error
15from autotest_lib.client.common_lib import file_utils
16from autotest_lib.client.common_lib.cros import chrome
17from telemetry.timeline import model as model_module
18from telemetry.timeline import tracing_config
19
20
21_COMPOSE_BUTTON_CLASS = 'y hC'
22_DISCARD_BUTTON_CLASS = 'ezvJwb bG AK ew IB'
23_IDLE_FOR_INBOX_STABLIZED = 30
24_INBOX_URL = 'https://inbox.google.com'
25_KEYIN_TEST_DATA = 'input_test_data'
26# In order to have focus switched from url bar to body text area of the compose
27# frame, 18 times of tab switch are performed accodingly which includes one
28# hidden iframe then the whole inbox frame, 8 items in title bar of inbox frame,
29# and 'Compose' button, 3 items of top-level compose frame:close, maximize and
30# minimize, then 'To' text area, To's drop-down,'Subject' text area, and the
31# final 'Body' text area.
32_NUMBER_OF_TABS_TO_COMPOSER_FRAME = 18
33_PRESS_TAB_KEY = 'key_event_tab'
34_SCRIPT_TIMEOUT = 5
35_TARGET_EVENT = 'InputLatency::Char'
36_TARGET_TRACING_CATEGORIES = 'input, latencyInfo'
37_TRACING_TIMEOUT = 60
38
39
40class performance_InboxInputLatency(test.test):
41    """Invoke Inbox composer, inject key events then measure latency."""
42    version = 1
43
44    def initialize(self):
45        # Create a virtual keyboard device for key event playback.
46        device_node = input_device.get_device_node(input_device.KEYBOARD_TYPES)
47        if not device_node:
48            raise error.TestFail('Could not find keyboard device node')
49        self.keyboard = input_device.InputDevice(device_node)
50
51        # Instantiate Chrome browser.
52        with tempfile.NamedTemporaryFile() as cap:
53            file_utils.download_file(chrome.CAP_URL, cap.name)
54            password = cap.read().rstrip()
55
56        self.browser = chrome.Chrome(gaia_login=True,
57                                     username=chrome.CAP_USERNAME,
58                                     password=password)
59        self.tab = self.browser.browser.tabs[0]
60
61        # Setup Chrome Tracing.
62        config = tracing_config.TracingConfig()
63        category_filter = config.chrome_trace_config.category_filter
64        category_filter.AddFilterString(_TARGET_TRACING_CATEGORIES)
65        config.enable_chrome_trace = True
66        self.target_tracing_config = config
67
68    def cleanup(self):
69        if self.browser:
70            self.browser.close()
71
72    def inject_key_events(self, event_file):
73        """
74        Replay key events from file.
75
76        @param event_file: the file with key events recorded.
77
78        """
79        current_dir = os.path.dirname(__file__)
80        event_file_path = os.path.join(current_dir, event_file)
81        InputEventPlayer().playback(self.keyboard, event_file_path)
82
83    def click_button_by_class_name(self, class_name):
84        """
85        Locate a button by its class name and click it.
86
87        @param class_name: the class name of the button.
88
89        """
90        button_query = 'document.getElementsByClassName("' + class_name +'")'
91        # Make sure the target button is available
92        self.tab.WaitForJavaScriptCondition(button_query + '.length == 1',
93                                            timeout=_SCRIPT_TIMEOUT)
94        # Perform click action
95        self.tab.ExecuteJavaScript(button_query + '[0].click();')
96
97    def setup_inbox_composer(self):
98        """Navigate to Inbox, and click the compose button."""
99        self.tab.Navigate(_INBOX_URL)
100        tracing_controller = self.tab.browser.platform.tracing_controller
101        if not tracing_controller.IsChromeTracingSupported():
102            raise Exception('Chrome tracing not supported')
103
104        # Idle for making inbox tab stablized, i.e. not busy in syncing with
105        # backend inbox server.
106        time.sleep(_IDLE_FOR_INBOX_STABLIZED)
107
108        # Pressing tabs to jump into composer frame as a workaround.
109        self.click_button_by_class_name(_COMPOSE_BUTTON_CLASS)
110        for _ in range(_NUMBER_OF_TABS_TO_COMPOSER_FRAME):
111            self.inject_key_events(_PRESS_TAB_KEY)
112
113    def teardown_inbox_composer(self):
114        """Discards the draft mail."""
115        self.click_button_by_class_name(_DISCARD_BUTTON_CLASS)
116
117    def measure_input_latency(self):
118        """Injects key events then measure and report the latency."""
119        tracing_controller = self.tab.browser.platform.tracing_controller
120        tracing_controller.StartTracing(self.target_tracing_config,
121                                        timeout=_TRACING_TIMEOUT)
122        # Inject pre-recorded test key events
123        self.inject_key_events(_KEYIN_TEST_DATA)
124        results = tracing_controller.StopTracing()
125
126        # Iterate recorded events and output target latency events
127        timeline_model = model_module.TimelineModel(results)
128        event_iter = timeline_model.IterAllEvents(
129                event_type_predicate=model_module.IsSliceOrAsyncSlice)
130
131        # Extract and report the latency information
132        latency_data = []
133        previous_start = 0.0
134        for event in event_iter:
135            if event.name == _TARGET_EVENT and event.start != previous_start:
136                logging.info('input char latency = %f ms', event.duration)
137                latency_data.append(event.duration)
138                previous_start = event.start
139        operators = ['mean', 'std', 'max', 'min']
140        for operator in operators:
141            description = 'input_char_latency_' + operator
142            value = getattr(numpy, operator)(latency_data)
143            logging.info('%s = %f', description, value)
144            self.output_perf_value(description=description,
145                                   value=value,
146                                   units='ms',
147                                   higher_is_better=False)
148
149    def run_once(self):
150        self.setup_inbox_composer()
151        self.measure_input_latency()
152        self.teardown_inbox_composer()
153