blob: 0dbf3c593d1294925361b5a5e35aafcc5f922e43 [file] [log] [blame]
Chinglin Yue559b122021-01-07 18:56:06 +08001#!/usr/bin/env python3
2
3# Copyright 2021 The Chromium OS Authors. All rights reserved.
4# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6"""Wrapper to run the platform.BootPerf tast test.
7
8This script runs the 'platform.BootPerf' boot timing test and store the results
9for later analysis by the 'showbootdata' script.
10
11NOTE: This script must be run from inside the chromeos build chroot
12environment.
13"""
14
15import argparse
16import datetime
17from distutils import dir_util
18from distutils import file_util
19import glob
20import os
21import os.path
22import re
23import shutil
24import stat
25import subprocess
26import sys
27import tempfile
28import textwrap
29
30# Tast tmp results (/tmp/bootperf.XXXXXX) structure:
31# device-config.txt
32# full.txt
33# results.json
34# streamed_results.jsonl
35# system_logs/
36# tests/
37# platform.BootPerf/
38# log.txt
39# results-chart.json
40# raw.001/
41# raw.002/
42# (one dir of raw data for each boot iteration)
43# timing.json
44
45# Constants
46_TEST = 'platform.BootPerf'
47_RESULTS_DIR = f'tests/{_TEST}'
48_RESULTS_KEYVAL = f'{_RESULTS_DIR}/results-chart.json'
49_RESULTS_SUMMARY = 'results_json'
50_RESULTS_SUMMARY_FILES = [
51 _RESULTS_DIR,
52 'device-config.txt',
53 'full.txt',
54 'results.json',
55 'streamed_results.jsonl',
56 'timing.json',
57]
58_RESULTS_SUMMARY_FILES_RAW_GLOB = f'{_RESULTS_DIR}/raw.*'
59
60# Structure of a results directory:
61# $RESULTS_SUMMARY/ - file
62# $RUNDIR.$ITER/ - directory
63# $RUNDIR_LOG - file
64# $RUNDIR_SUMMARY/ - directory
65# $RUNDIR_ALL_RESULTS/ - optional directory
66# If you add any other content under the results directory, you'll
67# probably need to change extra_files(), below.
68_RUNDIR = 'run'
69_RUNDIR_LOG = 'log.txt'
70_RUNDIR_SUMMARY = 'summary'
71_RUNDIR_ALL_RESULTS = 'logs'
72
73_DESCRIPTION = """\
74Summary:
75 Run the {} tast test, and store results in the
76 given destination directory. The test target is specified by
77 <ip_address>.
78
79 By default, the test is run once; if <count> is given, the test is
80 run that many times. By default, each test run reboots the test target 10
81 times, and this can be overridden using the [-r REBOOT_ITERATIONS] option.
82
83 If the destination directory doesn't exist, it is created. If the
84 destination directory already holds test results, additional
85 results are added in without overwriting earlier results.
86
87 If no destination is specified, the current directory is used,
88 provided that the directory is empty, or has been previously used
89 as a destination directory for this command.
90
91 By default, only a summary subset of the log files created by
92 tast are preserved; with --keep_logs the (potentially large)
93 logs are preserved with the test result.
94""".format(_TEST)
95
96
97def print_error(error):
98 """A utility function for printing a color-highlighted error if possible."""
99 cred = '\033[1;31;40m'
100 cend = '\033[0m'
101 if hasattr(sys.stdout, 'isatty') and sys.stdout.isatty():
102 error = f'{cred}ERROR: {error}{cend}'
103 print(error)
104
105
106def _assert_in_chroot():
107 """Asserts that we are inside the cros chroot."""
108 if not os.path.exists('/etc/cros_chroot_version'):
109 print_error(
110 textwrap.dedent("""\
111 This script must be run inside the chroot. Run this first:
112 cros_sdk
113 """))
114 sys.exit(1)
115
116
117class BootPerf:
118 """Runs the boot timing tests.
119
120 This class drives the execution running the boot timing test:
121 * Parse and validate command line arguments.
122 * Handle working and output directories.
123 * Run the platform.BootPerf tast test.
124 * Collect test results.
125 """
126
127 def __init__(self):
128 self.parser = None
129 self.args = None
130 self.count = None
131 self.current_iter = None
132 self.output_dir = None
133 self.tmp_dir = None
134
135 def process_cmdline(self):
136 """Process the command line arguments."""
137 parser = argparse.ArgumentParser(
138 epilog=_DESCRIPTION,
139 formatter_class=argparse.RawDescriptionHelpFormatter)
140 # Positional arguments: ip-address and (optional) count.
141 parser.add_argument('ip_address', help='Address of the test target')
142 parser.add_argument(
143 'count',
144 nargs='?',
145 help='The number of iterations (default: %(default)s)',
146 type=int,
147 default=1)
148 parser.add_argument(
149 '-o',
150 '--output_dir',
151 help='Specify output directory for results')
152 parser.add_argument(
153 '-k',
154 '--keep_logs',
155 help='Keep tast log files',
156 action='store_true')
157 parser.add_argument(
158 '-r',
159 '--reboot_iterations',
160 help='Specify the number of reboots in each iteration ' \
161 '(default: %(default)s)',
162 type=int,
163 default=10)
164 self.parser = parser
165 self.args = parser.parse_args()
166
167 def validate_args(self):
168 """Utility for validating command line arguments."""
169 if self.args.count <= 0:
170 self.print_usage('<count> argument must be a positive number')
171 self.count = self.args.count
172
173 if self.args.reboot_iterations <= 0:
174 self.print_usage(
175 '[-r REBOOT_ITERATIONS] argument must be a positive number')
176
177 def print_usage(self, error=None):
178 """Prints usage help message and terminates the script."""
179 self.parser.print_help()
180 if error:
181 print_error(error)
182 sys.exit(1)
183
184 def _validate_output_dir(self):
185 """Check for extra files in the output dir other than _RUNDIR ones.
186
187 Also gets the current iteration number.
188 """
189 max_iter = 0
190 for entry in os.listdir(self.output_dir):
191 basename = os.path.basename(entry)
192 if basename == _RESULTS_SUMMARY:
193 continue
194 matches = re.match(_RUNDIR + r'\.(\d+)', basename)
195 if matches is None:
196 print_error(
197 textwrap.dedent("""\
198 No results directory specified, and current directory
199 contains contents other than run results.
200 You can override this error by using the --output_dir option
201 """))
202 self.print_usage()
203 # Update to find the current iteration.
204 max_iter = max(max_iter, int(matches.group(1)))
205
206 self.current_iter = max_iter + 1
207
208 def _current_iter_str(self):
209 """Utility for converting the current iteration numbder string.
210
211 Returns:
212 The current iteration number as a string.
213 """
214 return '{:03d}'.format(self.current_iter)
215
216 def prepare_directories(self):
217 """Prepares the working temp and output directories for the test."""
218 self._process_output_dir()
219 self._validate_output_dir()
220 self._make_tmp_dir()
221
222 def _process_output_dir(self):
223 """Creates the output dir or use the current working dir for test output."""
224 if self.args.output_dir is not None:
225 if not os.path.exists(self.args.output_dir):
226 try:
227 os.mkdir(self.args.output_dir)
228 except OSError:
229 self.print_usage(f'Unable to create {self.args.output_dir}')
230 self.output_dir = self.args.output_dir
231 else:
232 self.output_dir = os.getcwd()
233
234 def _make_tmp_dir(self):
235 """Creates a temp directory as the test working dir."""
236 self.tmp_dir = tempfile.mkdtemp(prefix='bootperf.')
237
238 def _copy_results_summary(self, dst_dir):
239 """Copies the summary of test artifacts."""
240 for res in _RESULTS_SUMMARY_FILES:
241 src = os.path.join(self.tmp_dir, res)
242 dst = os.path.join(dst_dir, res)
243
244 assert os.path.isdir(src) or os.path.isfile(src)
245
246 if os.path.isdir(src):
247 dir_util.copy_tree(src, dst)
248 else:
249 file_util.copy_file(src, dst)
250 # The reboots dir contains archives of syslog messages for each reboot
251 # and can be potentially large. Remove it from the summary directory.
252 shutil.rmtree(os.path.join(dst_dir, _RESULTS_DIR, 'reboots'))
253
254 def _run_boot_test_once(self):
255 """Run the platform.BootPerf tast test once."""
256 remote = self.args.ip_address
257
258 iter_rundir = f'{_RUNDIR}.{self._current_iter_str()}'
259 logfile = os.path.join(os.getcwd(), iter_rundir, _RUNDIR_LOG)
260 summary_dir = os.path.join(iter_rundir, _RUNDIR_SUMMARY)
261 all_results_dir = os.path.join(iter_rundir, _RUNDIR_ALL_RESULTS)
262
263 os.mkdir(iter_rundir)
264 time_now = datetime.datetime.now().strftime('%H:%M:%S')
265 print(f'Test started: {time_now} - {logfile}')
266
267 # bootperf is typically run by devs in local tests, where rootfs
268 # verification is often disabled. Disable the assertion of rootfs
269 # verification in tast.platform.BootPerf.
270 skiprootfs_check = '-var=platform.BootPerf.skipRootfsCheck=true'
271 # Test option of the number of reboots in each test run. Default is 10.
272 # Note that the test runs for <count> times so the total number of reboots
273 # will be 10*<count>.
274 iterations = '-var=platform.BootPerf.iterations={}'.format(
275 self.args.reboot_iterations)
276
277 tast_args = [
278 'tast',
279 'run',
280 f'--resultsdir={self.tmp_dir}',
281 skiprootfs_check,
282 iterations,
283 remote,
284 _TEST,
285 ]
286 with open(logfile, 'w') as output:
287 subprocess.call(
288 tast_args, stdout=output, stderr=output, cwd=self.output_dir)
289
290 if not os.path.exists(os.path.join(self.tmp_dir, _RESULTS_KEYVAL)):
291 print_error(
292 textwrap.dedent("""\
293 No results file; terminating test runs.
294 Check {} for output from the test run,
295 and see {} for full test logs and output.
296 """.format(logfile, self.tmp_dir)))
297 sys.exit(1)
298
299 os.mkdir(summary_dir)
300 self._copy_results_summary(summary_dir)
301
302 if self.args.keep_logs:
303 shutil.move(self.tmp_dir, all_results_dir)
304 os.chmod(
305 all_results_dir, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR
306 | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH)
307 else:
308 shutil.rmtree(self.tmp_dir)
309 del self.tmp_dir
310
311 self.current_iter += 1
312
313 def _copy_all_results_summary(self):
314 """Utility to copy all results-chart.json into the summary file."""
315 with open(os.path.join(self.output_dir, _RESULTS_SUMMARY), 'w') as outf:
316 for path in glob.glob(
317 os.path.join(f'{_RUNDIR}.???', _RUNDIR_SUMMARY, _RESULTS_KEYVAL)):
318 with open(path) as inf:
319 for line in inf:
320 outf.write(line)
321
322 def run_boot_test(self):
323 """Main function to run the boot performance test.
324
325 Run the boot performance test for the given count, putting output into the
326 current directory.
327
328 Arguments are <ip-address> and <count> arguments, as for the main command.
329
330 We terminate test runs if the _RESULTS_SUMMARY file isn't produced;
331 generally this is the result of a serious error (e.g. disk full) that
332 won't go away if we just plow on.
333 """
334 for _ in range(self.count):
335 self._run_boot_test_once()
336
337 print('Test finished:', datetime.datetime.now().strftime('%H:%M:%S'))
338 self._copy_all_results_summary()
339
340
341def main():
342 _assert_in_chroot()
343
344 boot_perf = BootPerf()
345 boot_perf.process_cmdline()
346 boot_perf.validate_args()
347 boot_perf.prepare_directories()
348 boot_perf.run_boot_test()
349
350
351if __name__ == '__main__':
352 main()