cedf5734d8315bd6947f12c10b13140246a8ea57
[platform/framework/web/crosswalk.git] / src / build / android / pylib / perf / test_runner.py
1 # Copyright 2013 The Chromium 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
5 """Runs perf tests.
6
7 Our buildbot infrastructure requires each slave to run steps serially.
8 This is sub-optimal for android, where these steps can run independently on
9 multiple connected devices.
10
11 The buildbots will run this script multiple times per cycle:
12 - First: all steps listed in --steps in will be executed in parallel using all
13 connected devices. Step results will be pickled to disk. Each step has a unique
14 name. The result code will be ignored if the step name is listed in
15 --flaky-steps.
16 The buildbot will treat this step as a regular step, and will not process any
17 graph data.
18
19 - Then, with -print-step STEP_NAME: at this stage, we'll simply print the file
20 with the step results previously saved. The buildbot will then process the graph
21 data accordingly.
22
23
24 The JSON steps file contains a dictionary in the format:
25 [
26   ["step_name_foo", "script_to_execute foo"],
27   ["step_name_bar", "script_to_execute bar"]
28 ]
29
30 This preserves the order in which the steps are executed.
31
32 The JSON flaky steps file contains a list with step names which results should
33 be ignored:
34 [
35   "step_name_foo",
36   "step_name_bar"
37 ]
38
39 Note that script_to_execute necessarily have to take at least the following
40 option:
41   --device: the serial number to be passed to all adb commands.
42 """
43
44 import datetime
45 import logging
46 import os
47 import pickle
48 import sys
49 import threading
50 import time
51
52 from pylib import constants
53 from pylib import forwarder
54 from pylib import pexpect
55 from pylib.base import base_test_result
56 from pylib.base import base_test_runner
57
58
59 def PrintTestOutput(test_name):
60   """Helper method to print the output of previously executed test_name.
61
62   Args:
63     test_name: name of the test that has been previously executed.
64
65   Returns:
66     exit code generated by the test step.
67   """
68   file_name = os.path.join(constants.PERF_OUTPUT_DIR, test_name)
69   if not os.path.exists(file_name):
70     logging.error('File not found %s', file_name)
71     return 1
72
73   with file(file_name, 'r') as f:
74     persisted_result = pickle.loads(f.read())
75   logging.info('*' * 80)
76   logging.info('Output from:')
77   logging.info(persisted_result['cmd'])
78   logging.info('*' * 80)
79   print persisted_result['output']
80
81   return persisted_result['exit_code']
82
83
84 def PrintSummary(test_names):
85   logging.info('*' * 80)
86   logging.info('Sharding summary')
87   total_time = 0
88   for test_name in test_names:
89     file_name = os.path.join(constants.PERF_OUTPUT_DIR, test_name)
90     if not os.path.exists(file_name):
91       logging.info('%s : No status file found', test_name)
92       continue
93     with file(file_name, 'r') as f:
94       result = pickle.loads(f.read())
95     logging.info('%s : exit_code=%d in %d secs at %s',
96                  result['name'], result['exit_code'], result['total_time'],
97                  result['device'])
98     total_time += result['total_time']
99   logging.info('Total steps time: %d secs', total_time)
100
101
102 class _HeartBeatLogger(object):
103   # How often to print the heartbeat on flush().
104   _PRINT_INTERVAL = 30.0
105
106   def __init__(self):
107     """A file-like class for keeping the buildbot alive."""
108     self._len = 0
109     self._tick = time.time()
110     self._stopped = threading.Event()
111     self._timer = threading.Thread(target=self._runner)
112     self._timer.start()
113
114   def _runner(self):
115     while not self._stopped.is_set():
116       self.flush()
117       self._stopped.wait(_HeartBeatLogger._PRINT_INTERVAL)
118
119   def write(self, data):
120     self._len += len(data)
121
122   def flush(self):
123     now = time.time()
124     if now - self._tick >= _HeartBeatLogger._PRINT_INTERVAL:
125       self._tick = now
126       print '--single-step output length %d' % self._len
127       sys.stdout.flush()
128
129   def stop(self):
130     self._stopped.set()
131
132
133 class TestRunner(base_test_runner.BaseTestRunner):
134   def __init__(self, test_options, device, tests, flaky_tests):
135     """A TestRunner instance runs a perf test on a single device.
136
137     Args:
138       test_options: A PerfOptions object.
139       device: Device to run the tests.
140       tests: a dict mapping test_name to command.
141       flaky_tests: a list of flaky test_name.
142     """
143     super(TestRunner, self).__init__(device, None, 'Release')
144     self._options = test_options
145     self._tests = tests
146     self._flaky_tests = flaky_tests
147
148   @staticmethod
149   def _IsBetter(result):
150     if result['actual_exit_code'] == 0:
151       return True
152     pickled = os.path.join(constants.PERF_OUTPUT_DIR,
153                            result['name'])
154     if not os.path.exists(pickled):
155       return True
156     with file(pickled, 'r') as f:
157       previous = pickle.loads(f.read())
158     return result['actual_exit_code'] < previous['actual_exit_code']
159
160   @staticmethod
161   def _SaveResult(result):
162     if TestRunner._IsBetter(result):
163       with file(os.path.join(constants.PERF_OUTPUT_DIR,
164                              result['name']), 'w') as f:
165         f.write(pickle.dumps(result))
166
167   def _LaunchPerfTest(self, test_name):
168     """Runs a perf test.
169
170     Args:
171       test_name: the name of the test to be executed.
172
173     Returns:
174       A tuple containing (Output, base_test_result.ResultType)
175     """
176     try:
177       logging.warning('Unmapping device ports')
178       forwarder.Forwarder.UnmapAllDevicePorts(self.device)
179       self.device.old_interface.RestartAdbdOnDevice()
180     except Exception as e:
181       logging.error('Exception when tearing down device %s', e)
182
183     cmd = ('%s --device %s' %
184            (self._tests[test_name], self.device.old_interface.GetDevice()))
185     logging.info('%s : %s', test_name, cmd)
186     start_time = datetime.datetime.now()
187
188     timeout = 5400
189     if self._options.no_timeout:
190       timeout = None
191     full_cmd = cmd
192     if self._options.dry_run:
193       full_cmd = 'echo %s' % cmd
194
195     logfile = sys.stdout
196     if self._options.single_step:
197       # Just print a heart-beat so that the outer buildbot scripts won't timeout
198       # without response.
199       logfile = _HeartBeatLogger()
200     cwd = os.path.abspath(constants.DIR_SOURCE_ROOT)
201     if full_cmd.startswith('src/'):
202       cwd = os.path.abspath(os.path.join(constants.DIR_SOURCE_ROOT, os.pardir))
203     output, exit_code = pexpect.run(
204         full_cmd, cwd=cwd,
205         withexitstatus=True, logfile=logfile, timeout=timeout,
206         env=os.environ)
207     if self._options.single_step:
208       # Stop the logger.
209       logfile.stop()
210     end_time = datetime.datetime.now()
211     if exit_code is None:
212       exit_code = -1
213     logging.info('%s : exit_code=%d in %d secs at %s',
214                  test_name, exit_code, (end_time - start_time).seconds,
215                  self.device.old_interface.GetDevice())
216     result_type = base_test_result.ResultType.FAIL
217     if exit_code == 0:
218       result_type = base_test_result.ResultType.PASS
219     actual_exit_code = exit_code
220     if test_name in self._flaky_tests:
221       # The exit_code is used at the second stage when printing the
222       # test output. If the test is flaky, force to "0" to get that step green
223       # whilst still gathering data to the perf dashboards.
224       # The result_type is used by the test_dispatcher to retry the test.
225       exit_code = 0
226
227     persisted_result = {
228         'name': test_name,
229         'output': output,
230         'exit_code': exit_code,
231         'actual_exit_code': actual_exit_code,
232         'result_type': result_type,
233         'total_time': (end_time - start_time).seconds,
234         'device': self.device.old_interface.GetDevice(),
235         'cmd': cmd,
236     }
237     self._SaveResult(persisted_result)
238
239     return (output, result_type)
240
241   def RunTest(self, test_name):
242     """Run a perf test on the device.
243
244     Args:
245       test_name: String to use for logging the test result.
246
247     Returns:
248       A tuple of (TestRunResults, retry).
249     """
250     _, result_type = self._LaunchPerfTest(test_name)
251     results = base_test_result.TestRunResults()
252     results.AddResult(base_test_result.BaseTestResult(test_name, result_type))
253     retry = None
254     if not results.DidRunPass():
255       retry = test_name
256     return results, retry