lin
2025-07-30 fcd736bf35fd93b563e9bbf594f2aa7b62028cc9
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
#!/usr/bin/env python
# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
 
"""Script to compare the performance of two different chromeOS builds.
 
This script is meant to be used when the performance impact of a change in
chromeOS needs to be analyzed. It requires that you have already created two
chromeOS test images (one with the change, and one without), and that you have
at least one device available on which to run performance tests.
 
This script is actually a light-weight wrapper around crosperf, a tool for
automatically imaging one or more chromeOS devices with particular builds,
running a set of tests on those builds, and then notifying the user of test
results (along with some statistical analysis of perf keyvals). This wrapper
script performs the following tasks:
 
1) Creates a crosperf "experiment" file to be consumed by crosperf.
2) Invokes crosperf using the created experiment file. Crosperf produces 2
outputs: an e-mail that is sent to the user who invoked it; and an output
folder that is named based on the given --experiment-name, which is created in
the directory in which this script was run.
3) Parses the results of crosperf and outputs a summary of relevant data. This
script produces output in a CSV file, as well as in stdout.
 
Before running this script for the first time, you should set up your system to
run sudo without prompting for a password (otherwise, crosperf prompts for a
sudo password). You should only have to do that once per host machine.
 
Once you're set up with passwordless sudo, you can run the script (preferably
from an empty directory, since several output files are produced):
 
> python perf_compare.py --crosperf=CROSPERF_EXE --image-1=IMAGE_1 \
  --image-2=IMAGE_2 --board-1=BOARD_1 --board-2=BOARD_2 --remote-1=REMOTE_1 \
  --remote-2=REMOTE_2
 
You'll need to specify the following inputs: the full path to the crosperf
executable; the absolute paths to 2 locally-built chromeOS images (which must
reside in the "typical location" relative to the chroot, as required by
crosperf); the name of the boards associated with the 2 images (if both images
have the same board, you can specify that single board with --board=BOARD); and
the IP addresses of the 2 remote devices on which to run crosperf (if you have
only a single device available, specify it with --remote=REMOTE). Run with -h to
see the full set of accepted command-line arguments.
 
Notes:
 
1) When you run this script, it will delete any previously-created crosperf
output directories and created CSV files based on the specified
--experiment-name.  If you don't want to lose any old crosperf/CSV data, either
move it to another location, or run this script with a different
--experiment-name.
2) This script will only run the benchmarks and process the perf keys specified
in the file "perf_benchmarks.json".  Some benchmarks output more perf keys than
what are specified in perf_benchmarks.json, and these will appear in the
crosperf outputs, but not in the outputs produced specifically by this script.
"""
 
 
import json
import logging
import math
import optparse
import os
import re
import shutil
import subprocess
import sys
 
 
_ITERATIONS = 5
_IMAGE_1_NAME = 'Image1'
_IMAGE_2_NAME = 'Image2'
_DEFAULT_EXPERIMENT_NAME = 'perf_comparison'
_ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
_BENCHMARK_INFO_FILE_NAME = os.path.join(_ROOT_DIR, 'perf_benchmarks.json')
_CROSPERF_REPORT_LINE_DELIMITER = '\t'
_EXPERIMENT_FILE_NAME = 'experiment.txt'
 
_BENCHMARK_INFO_TEMPLATE = """
benchmark: {benchmark} {{
  autotest_name: {autotest_name}
  autotest_args: --use_emerged {autotest_args}
  iterations: {iterations}
}}
"""
 
_IMAGE_INFO_TEMPLATE = """
label: {label} {{
  chromeos_image: {image}
  board: {board}
  remote: {remote}
}}
"""
 
 
def prompt_for_input(prompt_message):
    """Prompts for user input and returns the inputted text as a string."""
    return raw_input('%s:> ' % prompt_message)
 
 
def identify_benchmarks_to_run(benchmark_info, iteration_nums, perf_keys):
    """Identifies which benchmarks to run, and for how many iterations.
 
    @param benchmark_info: A list of dictionaries containing information about
        the complete set of default perf benchmarks to run.
    @param iteration_nums: See output_benchmarks_info().
    @param perf_keys: See output_benchmarks_info().
 
    @return A tuple (X, Y), where X is a list of dictionaries containing
        information about the set of benchmarks to run, and Y is the set of
        perf keys requested to be run.
    """
    perf_keys_requested = set()
    benchmarks_to_run = []
    if not perf_keys:
        # Run every benchmark for the specified number of iterations.
        benchmarks_to_run = benchmark_info
        for benchmark in benchmarks_to_run:
            benchmark['iterations'] = iteration_nums[0]
            for perf_key in benchmark['perf_keys']:
                perf_keys_requested.add(perf_key)
    else:
        # Identify which benchmarks to run, and for how many iterations.
        identified_benchmarks = {}
        for i, perf_key in enumerate(perf_keys):
            perf_keys_requested.add(perf_key)
            benchmarks = [benchmark for benchmark in benchmark_info
                          if perf_key in benchmark['perf_keys']]
            if not benchmarks:
                logging.error('Perf key "%s" isn\'t associated with a known '
                              'benchmark.', perf_key)
                sys.exit(1)
            elif len(benchmarks) > 1:
                logging.error('Perf key "%s" is associated with more than one '
                              'benchmark, but should be unique.', perf_key)
                sys.exit(1)
            benchmark_to_add = benchmarks[0]
            benchmark_to_add = identified_benchmarks.setdefault(
                benchmark_to_add['benchmark'], benchmark_to_add)
            if len(iteration_nums) == 1:
                # If only a single iteration number is specified, we assume
                # that applies to every benchmark.
                benchmark_to_add['iterations'] = iteration_nums[0]
            else:
                # The user must have specified a separate iteration number for
                # each perf key.  If the benchmark associated with the current
                # perf key already has an interation number associated with it,
                # choose the maximum of the two.
                iter_num = iteration_nums[i]
                if 'iterations' in benchmark_to_add:
                    benchmark_to_add['iterations'] = (
                        iter_num if iter_num > benchmark_to_add['iterations']
                        else benchmark_to_add['iterations'])
                else:
                    benchmark_to_add['iterations'] = iter_num
        benchmarks_to_run = identified_benchmarks.values()
 
    return benchmarks_to_run, perf_keys_requested
 
 
def output_benchmarks_info(f, iteration_nums, perf_keys):
    """Identifies details of benchmarks to run, and writes that info to a file.
 
    @param f: A file object that is writeable.
    @param iteration_nums: A list of one or more integers representing the
        number of iterations to run for one or more benchmarks.
    @param perf_keys: A list of one or more string perf keys we need to
        run, or None if we should use the complete set of default perf keys.
 
    @return Set of perf keys actually requested to be run in the output file.
    """
    benchmark_info = []
    with open(_BENCHMARK_INFO_FILE_NAME, 'r') as f_bench:
        benchmark_info = json.load(f_bench)
 
    benchmarks_to_run, perf_keys_requested = identify_benchmarks_to_run(
        benchmark_info, iteration_nums, perf_keys)
 
    for benchmark in benchmarks_to_run:
        f.write(_BENCHMARK_INFO_TEMPLATE.format(
                    benchmark=benchmark['benchmark'],
                    autotest_name=benchmark['autotest_name'],
                    autotest_args=benchmark.get('autotest_args', ''),
                    iterations=benchmark['iterations']))
 
    return perf_keys_requested
 
 
def output_image_info(f, label, image, board, remote):
    """Writes information about a given image to an output file.
 
    @param f: A file object that is writeable.
    @param label: A string label for the given image.
    @param image: The string path to the image on disk.
    @param board: The string board associated with the image.
    @param remote: The string IP address on which to install the image.
    """
    f.write(_IMAGE_INFO_TEMPLATE.format(
                label=label, image=image, board=board, remote=remote))
 
 
def invoke_crosperf(crosperf_exe, result_dir, experiment_name, board_1, board_2,
                    remote_1, remote_2, iteration_nums, perf_keys, image_1,
                    image_2, image_1_name, image_2_name):
    """Invokes crosperf with a set of benchmarks and waits for it to complete.
 
    @param crosperf_exe: The string path to a crosperf executable.
    @param result_dir: The string name of the directory in which crosperf is
        expected to write its output.
    @param experiment_name: A string name to give the crosperf invocation.
    @param board_1: The string board associated with the first image.
    @param board_2: The string board associated with the second image.
    @param remote_1: The string IP address/name of the first remote device.
    @param remote_2: The string IP address/name of the second remote device.
    @param iteration_nums: A list of integers representing the number of
        iterations to run for the different benchmarks.
    @param perf_keys: A list of perf keys to run, or None to run the full set
        of default perf benchmarks.
    @param image_1: The string path to the first image.
    @param image_2: The string path to the second image.
    @param image_1_name: A string label to give the first image.
    @param image_2_name: A string label to give the second image.
 
    @return A tuple (X, Y), where X is the path to the created crosperf report
        file, and Y is the set of perf keys actually requested to be run.
    """
    # Create experiment file for crosperf.
    with open(_EXPERIMENT_FILE_NAME, 'w') as f:
        f.write('name: {name}\n'.format(name=experiment_name))
        perf_keys_requested = output_benchmarks_info(
            f, iteration_nums, perf_keys)
        output_image_info(f, image_1_name, image_1, board_1, remote_1)
        output_image_info(f, image_2_name, image_2, board_2, remote_2)
 
    # Invoke crosperf with the experiment file.
    logging.info('Invoking crosperf with created experiment file...')
    p = subprocess.Popen([crosperf_exe, _EXPERIMENT_FILE_NAME],
                         stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
 
    # Pass through crosperf output as debug messages until crosperf run is
    # complete.
    while True:
        next_line = p.stdout.readline().strip()
        if not next_line and p.poll() != None:
            break
        logging.debug(next_line)
        sys.stdout.flush()
    p.communicate()
    exit_code = p.returncode
 
    if exit_code:
        logging.error('Crosperf returned exit code %s', exit_code)
        sys.exit(1)
 
    report_file = os.path.join(result_dir, 'results.html')
    if not os.path.exists(report_file):
        logging.error('Crosperf report file missing, cannot proceed.')
        sys.exit(1)
 
    logging.info('Crosperf run complete.')
    logging.info('Crosperf results available in "%s"', result_dir)
    return report_file, perf_keys_requested
 
 
def parse_crosperf_report_file(report_file, perf_keys_requested):
    """Reads in and parses a crosperf report file for relevant perf data.
 
    @param report_file: See generate_results().
    @param perf_keys_requested: See generate_results().
 
    @return A dictionary containing perf information extracted from the crosperf
        report file.
    """
    results = {}
    with open(report_file, 'r') as f:
        contents = f.read()
 
        match = re.search(r'summary-tsv.+?/pre', contents, flags=re.DOTALL)
        contents = match.group(0)
 
        curr_benchmark = None
        for line in contents.splitlines():
            delimiter = r'\s+?'
            match = re.search(
                r'Benchmark:%s(?P<benchmark>\w+?);%sIterations:%s'
                 '(?P<iterations>\w+?)\s' % (delimiter, delimiter, delimiter),
                line)
            if match:
                curr_benchmark = match.group('benchmark')
                iterations = match.group('iterations')
                results[curr_benchmark] = {'iterations': iterations,
                                           'p_values': []}
                continue
            split = line.strip().split(_CROSPERF_REPORT_LINE_DELIMITER)
            if (len(split) == 12 and split[-2] == '--' and
                split[0] not in ['retval', 'iterations'] and
                split[0] in perf_keys_requested):
                results[curr_benchmark]['p_values'].append(
                    (split[0], split[-1]))
 
    return results
 
 
def generate_results(report_file, result_file, perf_keys_requested):
    """Output relevant crosperf results to a CSV file, and to stdout.
 
    This code parses the "results.html" output file of crosperf. It then creates
    a CSV file that has the following format per line:
 
    benchmark_name,num_iterations,perf_key,p_value[,perf_key,p_value]
 
    @param report_file: The string name of the report file created by crosperf.
    @param result_file: A string name for the CSV file to output.
    @param perf_keys_requested: The set of perf keys originally requested to be
        run.
    """
    results = parse_crosperf_report_file(report_file, perf_keys_requested)
 
    # Output p-value data to a CSV file.
    with open(result_file, 'w') as f:
        for bench in results:
            perf_key_substring = ','.join(
                ['%s,%s' % (x[0], x[1]) for x in results[bench]['p_values']])
            f.write('%s,%s,%s\n' % (
                bench, results[bench]['iterations'], perf_key_substring))
 
    logging.info('P-value results available in "%s"', result_file)
 
    # Collect and output some additional summary results to stdout.
    small_p_value = []
    nan_p_value = []
    perf_keys_obtained = set()
    for benchmark in results:
        p_values = results[benchmark]['p_values']
        for key, p_val in p_values:
            perf_keys_obtained.add(key)
            if float(p_val) <= 0.05:
                small_p_value.append((benchmark, key, p_val))
            elif math.isnan(float(p_val)):
                nan_p_value.append((benchmark, key, p_val))
 
    if small_p_value:
        logging.info('The following perf keys showed statistically significant '
             'result differences (p-value <= 0.05):')
        for item in small_p_value:
            logging.info('* [%s] %s (p-value %s)', item[0], item[1], item[2])
    else:
        logging.info('No perf keys showed statistically significant result '
                     'differences (p-value <= 0.05)')
 
    if nan_p_value:
        logging.info('The following perf keys had "NaN" p-values:')
        for item in nan_p_value:
            logging.info('* [%s] %s (p-value %s)', item[0], item[1], item[2])
 
    # Check if any perf keys are missing from what was requested, and notify
    # the user if so.
    for key_requested in perf_keys_requested:
        if key_requested not in perf_keys_obtained:
            logging.warning('Could not find results for requested perf key '
                            '"%s".', key_requested)
 
 
def parse_options():
    """Parses command-line arguments."""
    parser = optparse.OptionParser()
 
    parser.add_option('--crosperf', metavar='PATH', type='string', default=None,
                      help='Absolute path to the crosperf executable '
                           '(required).')
    parser.add_option('--image-1', metavar='PATH', type='string', default=None,
                      help='Absolute path to the first image .bin file '
                           '(required).')
    parser.add_option('--image-2', metavar='PATH', type='string', default=None,
                      help='Absolute path to the second image .bin file '
                           '(required).')
 
    board_group = optparse.OptionGroup(
        parser, 'Specifying the boards (required)')
    board_group.add_option('--board', metavar='BOARD', type='string',
                           default=None,
                           help='Name of the board associated with the images, '
                                'if both images have the same board. If each '
                                'image has a different board, use '
                                'options --board-1 and --board-2 instead.')
    board_group.add_option('--board-1', metavar='BOARD', type='string',
                           default=None,
                           help='Board associated with the first image.')
    board_group.add_option('--board-2', metavar='BOARD', type='string',
                           default=None,
                           help='Board associated with the second image.')
    parser.add_option_group(board_group)
 
    remote_group = optparse.OptionGroup(
        parser, 'Specifying the remote devices (required)')
    remote_group.add_option('--remote', metavar='IP', type='string',
                            default=None,
                            help='IP address/name of remote device to use, if '
                                 'only one physical device is to be used. If '
                                 'using two devices, use options --remote-1 '
                                 'and --remote-2 instead.')
    remote_group.add_option('--remote-1', metavar='IP', type='string',
                            default=None,
                            help='IP address/name of first device to use.')
    remote_group.add_option('--remote-2', metavar='IP', type='string',
                            default=None,
                            help='IP address/name of second device to use.')
    parser.add_option_group(remote_group)
 
    optional_group = optparse.OptionGroup(parser, 'Optional settings')
    optional_group.add_option('--image-1-name', metavar='NAME', type='string',
                              default=_IMAGE_1_NAME,
                              help='Descriptive name for the first image. '
                                   'Defaults to "%default".')
    optional_group.add_option('--image-2-name', metavar='NAME', type='string',
                              default=_IMAGE_2_NAME,
                              help='Descriptive name for the second image. '
                                    'Defaults to "%default".')
    optional_group.add_option('--experiment-name', metavar='NAME',
                              type='string', default=_DEFAULT_EXPERIMENT_NAME,
                              help='A descriptive name for the performance '
                                   'comparison experiment to run. Defaults to '
                                   '"%default".')
    optional_group.add_option('--perf-keys', metavar='KEY1[,KEY2...]',
                              type='string', default=None,
                              help='Comma-separated list of perf keys to '
                                   'evaluate, if you do not want to run the '
                                   'complete set. By default, will evaluate '
                                   'with the complete set of perf keys.')
    optional_group.add_option('--iterations', metavar='N1[,N2...]',
                              type='string', default=str(_ITERATIONS),
                              help='Number of iterations to use to evaluate '
                                   'each perf key (defaults to %default). If '
                                   'specifying a custom list of perf keys '
                                   '(with --perf-keys) and you want to have a '
                                   'different number of iterations for each '
                                   'perf key, specify a comma-separated list '
                                   'of iteration numbers where N1 corresponds '
                                   'to KEY1, N2 corresponds to KEY2, etc.')
    optional_group.add_option('-v', '--verbose', action='store_true',
                              default=False, help='Use verbose logging.')
    parser.add_option_group(optional_group)
 
    options, _ = parser.parse_args()
    return options
 
 
def verify_command_line_options(options, iteration_nums, perf_keys):
    """Verifies there are no errors in the specified command-line options.
 
    @param options: An optparse.Options object.
    @param iteration_nums: An array of numbers representing the number of
        iterations to perform to evaluate each perf key.
    @param perf_keys: A list of strings representing perf keys to evaluate, or
        None if no particular perf keys are specified.
 
    @return True, if there were no errors in the command-line options, or
        False if any error was detected.
    """
    success = True
    if not options.crosperf:
        logging.error('You must specify the path to a crosperf executable.')
        success = False
    if options.crosperf and not os.path.isfile(options.crosperf):
        logging.error('Could not locate crosperf executable "%s".',
                      options.crosperf)
        if options.crosperf.startswith('/google'):
            logging.error('Did you remember to run prodaccess?')
        success = False
    if not options.image_1 or not options.image_2:
        logging.error('You must specify the paths for 2 image .bin files.')
        success = False
    if not options.board and (not options.board_1 or not options.board_2):
        logging.error('You must specify the board name(s): either a single '
                      'board with --board, or else two board names with '
                      '--board-1 and --board-2.')
        success = False
    if options.board and options.board_1 and options.board_2:
        logging.error('Specify either one board with --board, or two boards '
                      'with --board-1 and --board-2, but not both.')
        success = False
    if not options.remote and (not options.remote_1 or not options.remote_2):
        logging.error('You must specify the remote device(s) to use: either a '
                      'single device with --remote, or else two devices with '
                      '--remote-1 and --remote-2.')
        success = False
    if options.remote and options.remote_1 and options.remote_2:
        logging.error('Specify either one remote device with --remote, or two '
                      'devices with --remote-1 and --remote-2, but not both.')
        success = False
    if len(iteration_nums) > 1 and not perf_keys:
        logging.error('You should only specify multiple iteration numbers '
                      'if you\'re specifying a custom list of perf keys to '
                      'evaluate.')
        success = False
    if (options.perf_keys and len(iteration_nums) > 1 and
        len(options.perf_keys.split(',')) > len(iteration_nums)):
        logging.error('You specified %d custom perf keys, but only %d '
                      'iteration numbers.', len(options.perf_keys.split(',')),
                      len(iteration_nums))
        success = False
    return success
 
 
def main():
    options = parse_options()
 
    log_level = logging.DEBUG if options.verbose else logging.INFO
    logging.basicConfig(format='%(asctime)s %(levelname)s: %(message)s',
                        level=log_level)
 
    iteration_nums = [int(i) for i in options.iterations.split(',')]
    perf_keys = options.perf_keys.split(',') if options.perf_keys else None
 
    # Verify there are no errors in the specified command-line options.
    if not verify_command_line_options(options, iteration_nums, perf_keys):
        return 1
 
    # Clean up any old results that will be overwritten.
    result_dir = options.experiment_name + '_results'
    if os.path.isdir(result_dir):
        shutil.rmtree(result_dir)
    result_file = options.experiment_name + '_results.csv'
    if os.path.isfile(result_file):
        os.remove(result_file)
 
    if options.remote:
        remote_1, remote_2 = options.remote, options.remote
    else:
        remote_1, remote_2 = options.remote_1, options.remote_2
 
    if options.board:
        board_1, board_2 = options.board, options.board
    else:
        board_1, board_2 = options.board_1, options.board_2
 
    report_file, perf_keys_requested = invoke_crosperf(
        options.crosperf, result_dir, options.experiment_name, board_1, board_2,
        remote_1, remote_2, iteration_nums, perf_keys, options.image_1,
        options.image_2, options.image_1_name, options.image_2_name)
    generate_results(report_file, result_file, perf_keys_requested)
 
    return 0
 
 
if __name__ == '__main__':
    sys.exit(main())