• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#! /usr/bin/env python3
2#
3# Copyright 2021 The ANGLE Project 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#
7'''
8Pixel 6 (ARM based) specific script that measures the following for each restricted_trace:
9- Wall time per frame
10- GPU time per frame (if run with --vsync)
11- CPU time per frame
12- GPU power per frame
13- CPU power per frame
14- GPU memory per frame
15
16Setup:
17
18  autoninja -C out/<config> angle_trace_perf_tests angle_apks
19
20Recommended command to run:
21
22  out/<config>/restricted_trace_perf --fixedtime 10 --power --memory --output-tag android.$(date '+%Y%m%d') --loop-count 5
23
24Alternatively, you can pass the build directory and run from anywhere:
25
26  python3 restricted_trace_perf.py --fixedtime 10 --power --output-tag android.$(date '+%Y%m%d') --loop-count 5 --build-dir ../../../out/<config>
27
28- This will run through all the traces 5 times with the native driver, then 5 times with vulkan (via ANGLE)
29- 10 second run time with one warmup loop
30
31Output will be printed to the terminal as it is collected.
32
33Of the 5 runs, the high and low for each data point will be dropped, average of the remaining three will be tracked in the summary spreadsheet.
34'''
35
36import argparse
37import contextlib
38import copy
39import csv
40import fnmatch
41import json
42import logging
43import os
44import pathlib
45import posixpath
46import re
47import statistics
48import subprocess
49import sys
50import threading
51import time
52
53from collections import defaultdict, namedtuple
54from datetime import datetime
55
56PY_UTILS = str(pathlib.Path(__file__).resolve().parents[1] / 'py_utils')
57if PY_UTILS not in sys.path:
58    os.stat(PY_UTILS) and sys.path.insert(0, PY_UTILS)
59import android_helper
60import angle_path_util
61import angle_test_util
62
63DEFAULT_TEST_DIR = '.'
64DEFAULT_LOG_LEVEL = 'info'
65DEFAULT_ANGLE_PACKAGE = 'com.android.angle.test'
66
67Result = namedtuple('Result', ['stdout', 'stderr', 'time'])
68
69
70class _global(object):
71    current_user = None
72    storage_dir = None
73    cache_dir = None
74
75
76def init():
77    _global.current_user = run_adb_shell_command('am get-current-user').strip()
78    _global.storage_dir = '/data/user/' + _global.current_user + '/com.android.angle.test/files'
79    _global.cache_dir = '/data/user_de/' + _global.current_user + '/com.android.angle.test/cache'
80    logging.debug('Running with user %s, storage %s, cache %s', _global.current_user,
81                  _global.storage_dir, _global.cache_dir)
82
83
84def run_async_adb_shell_command(cmd):
85    logging.debug('Kicking off subprocess %s' % (cmd))
86
87    try:
88        async_process = subprocess.Popen([android_helper.FindAdb(), 'shell', cmd],
89                                         stdout=subprocess.PIPE,
90                                         stderr=subprocess.STDOUT)
91    except subprocess.CalledProcessError as e:
92        raise RuntimeError("command '{}' return with error (code {}): {}".format(
93            e.cmd, e.returncode, e.output))
94
95    logging.debug('Done launching subprocess')
96
97    return async_process
98
99
100def run_adb_command(args):
101    return android_helper._AdbRun(args).decode()
102
103
104def run_adb_shell_command(cmd):
105    return android_helper._AdbShell(cmd).decode()
106
107
108def run_adb_shell_command_with_run_as(cmd):
109    return android_helper._AdbShellWithRunAs(cmd).decode()
110
111
112def run_async_adb_command(args):
113    return run_async_command('adb ' + args)
114
115
116def cleanup():
117    run_adb_shell_command_with_run_as('rm -f ' + _global.storage_dir + '/out.txt ' +
118                                      _global.storage_dir + '/gpumem.txt')
119
120
121def clear_blob_cache():
122    run_adb_shell_command_with_run_as('rm -rf ' + _global.cache_dir)
123
124
125def select_device(device_arg):
126    # The output from 'adb devices' always includes a header and a new line at the end.
127    result_dev_out = run_adb_command(['devices']).strip()
128
129    result_header_end = result_dev_out.find('\n')
130    result_dev_out = '' if result_header_end == -1 else result_dev_out[result_header_end:]
131    result_dev_out = result_dev_out.split()[0::2]
132
133    def print_device_list():
134        print('\nList of available devices:\n{}'.format('\n'.join(result_dev_out)))
135
136    num_connected_devices = len(result_dev_out)
137
138    # Check the device arg first. If there is none, use the ANDROID SERIAL env var.
139    android_serial_env = os.environ.get('ANDROID_SERIAL')
140    device_serial = device_arg if device_arg != '' else android_serial_env
141
142    # Check for device exceptions
143    if num_connected_devices == 0:
144        logging.error('DeviceError: No devices detected. Please connect a device and try again.')
145        exit()
146
147    if num_connected_devices > 1 and device_serial is None:
148        logging.error(
149            'DeviceError: More than one device detected. Please specify a target device\n'
150            'through either the --device option or $ANDROID_SERIAL, and try again.')
151        print_device_list()
152        exit()
153
154    if device_serial is not None and device_serial not in result_dev_out:
155        logging.error('DeviceError: Device with serial {} not detected.'.format(device_serial))
156        if device_arg != '':
157            logging.error('Please update the --device input and try again.')
158        else:
159            logging.error('Please update $ANDROID_SERIAL and try again.')
160        print_device_list()
161        exit()
162
163    # Select device
164    if device_serial is not None:
165        logging.info('Device with serial {} selected.'.format(device_serial))
166        os.environ['ANDROID_SERIAL'] = device_serial
167
168    else:
169        logging.info('Default device ({}) selected.'.format(result_dev_out[0]))
170
171
172def get_mode(args):
173    mode = ''
174    if args.vsync:
175        mode = 'vsync'
176    elif args.offscreen:
177        mode = 'offscreen'
178
179    return mode
180
181
182def get_trace_width(mode):
183    width = 40
184    if mode == 'vsync':
185        width += 5
186    elif mode == 'offscreen':
187        width += 10
188
189    return width
190
191
192def pull_screenshot(args, screenshot_device_dir, renderer):
193    # We don't know the name of the screenshots, since they could have a KeyFrame
194    # Rather than look that up and piece together a file name, we can pull the single screenshot in a generic fashion
195    files = run_adb_shell_command('ls -1 %s' % screenshot_device_dir).split('\n')
196
197    # There might not be a screenshot if the test was skipped
198    files = list(filter(None, (f.strip() for f in files)))  # Remove empty strings and whitespace
199    if files:
200        assert len(files) == 1, 'Multiple files(%s) in %s, expected 1: %s' % (
201            len(files), screenshot_device_dir, files)
202
203        # Grab the single screenshot
204        src_file = files[0]
205
206        # Rename the file to reflect renderer, since we force everything through the platform using "native"
207        dst_file = src_file.replace("native", renderer)
208
209        # Use CWD if no location provided
210        screenshot_dst = args.screenshot_dir if args.screenshot_dir != '' else '.'
211
212        logging.info('Pulling screenshot %s to %s' % (dst_file, screenshot_dst))
213        run_adb_command([
214            'pull',
215            posixpath.join(screenshot_device_dir, src_file),
216            posixpath.join(screenshot_dst, dst_file)
217        ])
218
219
220# This function changes to the target directory, then 'yield' passes execution to the inner part of
221# the 'with' block that invoked it. The 'finally' block is executed at the end of the 'with' block,
222# including when exceptions are raised.
223@contextlib.contextmanager
224def run_from_dir(dir):
225    # If not set, just run the command and return
226    if not dir:
227        yield
228        return
229    # Otherwise, change directories
230    cwd = os.getcwd()
231    os.chdir(dir)
232    try:
233        yield
234    finally:
235        os.chdir(cwd)
236
237
238def run_trace(trace, args, screenshot_device_dir):
239    mode = get_mode(args)
240
241    # Kick off a subprocess that collects peak gpu memory periodically
242    # Note the 0.25 below is the delay (in seconds) between memory checks
243    if args.memory:
244        run_adb_command([
245            'push',
246            os.path.join(angle_path_util.ANGLE_ROOT_DIR, 'src', 'tests', 'restricted_traces',
247                         'gpumem.sh'), '/data/local/tmp'
248        ])
249        memory_command = 'sh /data/local/tmp/gpumem.sh 0.25 ' + _global.storage_dir
250        memory_process = run_async_adb_shell_command(memory_command)
251
252    flags = [
253        '--gtest_filter=TraceTest.' + trace, '--use-gl=native', '--verbose', '--verbose-logging'
254    ]
255    if mode != '':
256        flags.append('--' + mode)
257    if args.maxsteps != '':
258        flags += ['--max-steps-performed', args.maxsteps]
259    if args.fixedtime != '':
260        flags += ['--fixed-test-time-with-warmup', args.fixedtime]
261    if args.minimizegpuwork:
262        flags.append('--minimize-gpu-work')
263    if screenshot_device_dir != None:
264        flags += ['--screenshot-dir', screenshot_device_dir]
265    if args.screenshot_frame != '':
266        flags += ['--screenshot-frame', args.screenshot_frame]
267    if args.fps_limit != '':
268        flags += ['--fps-limit', args.fps_limit]
269
270    # Build a command that can be run directly over ADB, for example:
271    r'''
272adb shell am instrument -w \
273    -e org.chromium.native_test.NativeTestInstrumentationTestRunner.StdoutFile /data/user/0/com.android.angle.test/files/out.txt \
274    -e org.chromium.native_test.NativeTest.CommandLineFlags \
275    "--gtest_filter=TraceTest.empires_and_puzzles\ --use-angle=vulkan\ --screenshot-dir\ /data/user/0/com.android.angle.test/files\ --screenshot-frame\ 2\ --max-steps-performed\ 2\ --no-warmup" \
276    -e org.chromium.native_test.NativeTestInstrumentationTestRunner.ShardNanoTimeout "1000000000000000000" \
277    -e org.chromium.native_test.NativeTestInstrumentationTestRunner.NativeTestActivity com.android.angle.test.AngleUnitTestActivity \
278    com.android.angle.test/org.chromium.build.gtest_apk.NativeTestInstrumentationTestRunner
279    '''
280    shell_command = r'''
281am instrument -w \
282    -e org.chromium.native_test.NativeTestInstrumentationTestRunner.StdoutFile {storage}/out.txt \
283    -e org.chromium.native_test.NativeTest.CommandLineFlags "{flags}" \
284    -e org.chromium.native_test.NativeTestInstrumentationTestRunner.ShardNanoTimeout "1000000000000000000" \
285    -e org.chromium.native_test.NativeTestInstrumentationTestRunner.NativeTestActivity \
286    com.android.angle.test.AngleUnitTestActivity \
287    com.android.angle.test/org.chromium.build.gtest_apk.NativeTestInstrumentationTestRunner
288    '''.format(
289        flags=r' '.join(flags),
290        storage=_global.storage_dir).strip()  # Note: space escaped due to subprocess shell=True
291
292    start_time = time.time()
293    result = run_adb_shell_command(shell_command)
294    time_elapsed = time.time() - start_time
295
296    if args.memory:
297        logging.debug('Killing gpumem subprocess')
298        memory_process.kill()
299
300    return time_elapsed
301
302
303def get_test_time():
304    # Pull the results from the device and parse
305    result = run_adb_shell_command_with_run_as('cat ' + _global.storage_dir +
306                                               '/out.txt | grep -v Error | grep -v Frame')
307
308    measured_time = None
309
310    for line in result.splitlines():
311        logging.debug('Checking line: %s' % line)
312
313        # Look for this line and grab the second to last entry:
314        #   Mean result time: 1.2793 ms
315        if "Mean result time" in line:
316            measured_time = line.split()[-2]
317            break
318
319        # Check for skipped tests
320        if "Test skipped due to missing extension" in line:
321            missing_ext = line.split()[-1]
322            logging.debug('Skipping test due to missing extension: %s' % missing_ext)
323            measured_time = missing_ext
324            break
325
326    if measured_time is None:
327        if '[  PASSED  ]' in result.stdout:
328            measured_time = 'missing'
329        else:
330            measured_time = 'crashed'
331
332    return measured_time
333
334
335def get_gpu_memory(trace_duration):
336    # Pull the results from the device and parse
337    result = run_adb_shell_command_with_run_as('cat ' + _global.storage_dir +
338                                               '/gpumem.txt | awk "NF"')
339
340    # The gpumem script grabs snapshots of memory per process
341    # Output looks like this, repeated once per sleep_duration of the test:
342    #
343    # time_elapsed: 9
344    # com.android.angle.test:test_process 16513
345    # Memory snapshot for GPU 0:
346    # Global total: 516833280
347    # Proc 504 total: 170385408
348    # Proc 1708 total: 33767424
349    # Proc 2011 total: 17018880
350    # Proc 16513 total: 348479488
351    # Proc 27286 total: 20877312
352    # Proc 27398 total: 1028096
353
354    # Gather the memory at each snapshot
355    time_elapsed = ''
356    test_process = ''
357    gpu_mem = []
358    gpu_mem_sustained = []
359    for line in result.splitlines():
360        logging.debug('Checking line: %s' % line)
361
362        if "time_elapsed" in line:
363            time_elapsed = line.split()[-1]
364            logging.debug('time_elapsed: %s' % time_elapsed)
365            continue
366
367        # Look for this line and grab the last entry:
368        #   com.android.angle.test:test_process 31933
369        if "com.android.angle.test" in line:
370            test_process = line.split()[-1]
371            logging.debug('test_process: %s' % test_process)
372            continue
373
374        # If we don't know test process yet, break
375        if test_process == '':
376            continue
377
378        # If we made it this far, we have the process id
379
380        # Look for this line and grab the last entry:
381        #   Proc 31933 total: 235184128
382        if test_process in line:
383            gpu_mem_entry = line.split()[-1]
384            logging.debug('Adding: %s to gpu_mem' % gpu_mem_entry)
385            gpu_mem.append(int(gpu_mem_entry))
386            # logging.debug('gpu_mem contains: %i' % ' '.join(gpu_mem))
387            if safe_cast_float(time_elapsed) >= (safe_cast_float(trace_duration) / 2):
388                # Start tracking sustained memory usage at the half way point
389                logging.debug('Adding: %s to gpu_mem_sustained' % gpu_mem_entry)
390                gpu_mem_sustained.append(int(gpu_mem_entry))
391            continue
392
393    gpu_mem_max = 0
394    if len(gpu_mem) != 0:
395        gpu_mem_max = max(gpu_mem)
396
397    gpu_mem_average = 0
398    if len(gpu_mem_sustained) != 0:
399        gpu_mem_average = statistics.mean(gpu_mem_sustained)
400
401    return gpu_mem_average, gpu_mem_max
402
403
404def get_proc_memory():
405    # Pull the results from the device and parse
406    result = run_adb_shell_command_with_run_as('cat ' + _global.storage_dir + '/out.txt')
407    memory_median = ''
408    memory_max = ''
409
410    for line in result.splitlines():
411        # Look for "memory_max" in the line and grab the second to last entry:
412        logging.debug('Checking line: %s' % line)
413
414        if "memory_median" in line:
415            memory_median = line.split()[-2]
416            continue
417
418        if "memory_max" in line:
419            memory_max = line.split()[-2]
420            continue
421
422    return safe_cast_int(memory_max), safe_cast_int(memory_median)
423
424
425def get_gpu_time():
426    # Pull the results from the device and parse
427    result = run_adb_shell_command_with_run_as('cat ' + _global.storage_dir + '/out.txt')
428    gpu_time = '0'
429
430    for line in result.splitlines():
431        # Look for "gpu_time" in the line and grab the second to last entry:
432        logging.debug('Checking line: %s' % line)
433
434        if "gpu_time" in line:
435            gpu_time = line.split()[-2]
436            break
437
438    return gpu_time
439
440
441def get_cpu_time():
442    # Pull the results from the device and parse
443    result = run_adb_shell_command_with_run_as('cat ' + _global.storage_dir + '/out.txt')
444    cpu_time = '0'
445
446    for line in result.splitlines():
447        # Look for "cpu_time" in the line and grab the second to last entry:
448        logging.debug('Checking line: %s' % line)
449
450        if "cpu_time" in line:
451            cpu_time = line.split()[-2]
452            break
453
454    return cpu_time
455
456
457def get_frame_count():
458    # Pull the results from the device and parse
459    result = run_adb_shell_command_with_run_as('cat ' + _global.storage_dir +
460                                               '/out.txt | grep -v Error | grep -v Frame')
461
462    frame_count = 0
463
464    for line in result.splitlines():
465        logging.debug('Checking line: %s' % line)
466        if "trial_steps" in line:
467            frame_count = line.split()[-2]
468            break
469
470    logging.debug('Frame count: %s' % frame_count)
471    return frame_count
472
473
474class GPUPowerStats():
475
476    # GPU power measured in uWs
477
478    def __init__(self):
479        self.power = {'gpu': 0, 'big_cpu': 0, 'mid_cpu': 0, 'little_cpu': 0}
480
481    def gpu_delta(self, starting):
482        return self.power['gpu'] - starting.power['gpu']
483
484    def cpu_delta(self, starting):
485        big = self.power['big_cpu'] - starting.power['big_cpu']
486        mid = self.power['mid_cpu'] - starting.power['mid_cpu']
487        little = self.power['little_cpu'] - starting.power['little_cpu']
488        return big + mid + little
489
490    def get_power_data(self):
491        energy_value_result = run_adb_shell_command(
492            'cat /sys/bus/iio/devices/iio:device*/energy_value')
493        # Output like this (ordering doesn't matter)
494        #
495        # t=251741617
496        # CH0(T=251741617)[VSYS_PWR_WLAN_BT], 192612469095
497        # CH1(T=251741617)[L2S_VDD_AOC_RET], 1393792850
498        # CH2(T=251741617)[S9S_VDD_AOC], 16816975638
499        # CH3(T=251741617)[S5S_VDDQ_MEM], 2720913855
500        # CH4(T=251741617)[S10S_VDD2L], 3656592412
501        # CH5(T=251741617)[S4S_VDD2H_MEM], 4597271837
502        # CH6(T=251741617)[S2S_VDD_G3D], 3702041607
503        # CH7(T=251741617)[L9S_GNSS_CORE], 88535064
504        # t=16086645
505        # CH0(T=16086645)[PPVAR_VSYS_PWR_DISP], 611687448
506        # CH1(T=16086645)[VSYS_PWR_MODEM], 179646995
507        # CH2(T=16086645)[VSYS_PWR_RFFE], 0
508        # CH3(T=16086645)[S2M_VDD_CPUCL2], 124265856
509        # CH4(T=16086645)[S3M_VDD_CPUCL1], 170096352
510        # CH5(T=16086645)[S4M_VDD_CPUCL0], 289995530
511        # CH6(T=16086645)[S5M_VDD_INT], 190164699
512        # CH7(T=16086645)[S1M_VDD_MIF], 196512891
513
514        id_map = {
515            r'S2S_VDD_G3D\b|S2S_VDD_GPU\b': 'gpu',
516            r'S\d+M_VDD_CPUCL2\b|S2M_VDD_CPU2\b': 'big_cpu',
517            r'S\d+M_VDD_CPUCL1\b|S3M_VDD_CPU1\b': 'mid_cpu',
518            r'S\d+M_VDD_CPUCL0\b|S4M_VDD_CPU\b': 'little_cpu',
519        }
520
521        for m in id_map.values():
522            self.power[m] = 0  # Set to 0 to check for missing values and dupes below
523
524        for line in energy_value_result.splitlines():
525            for mid, m in id_map.items():
526                if re.search(mid, line):
527                    value = int(line.split()[1])
528                    logging.debug('Power metric %s (%s): %d', mid, m, value)
529                    assert self.power[m] == 0, 'Duplicate power metric: %s (%s)' % (mid, m)
530                    self.power[m] = value
531
532        for mid, m in id_map.items():
533            assert self.power[m] != 0, 'Power metric not found: %s (%s)' % (mid, m)
534
535
536def wait_for_test_warmup(done_event):
537    p = subprocess.Popen(['adb', 'logcat', '*:S', 'ANGLE:I'],
538                         stdout=subprocess.PIPE,
539                         text=True,
540                         bufsize=1)  # line-buffered
541    os.set_blocking(p.stdout.fileno(), False)
542
543    start_time = time.time()
544    while True:
545        line = p.stdout.readline()  # non-blocking as per set_blocking above
546
547        # Look for text logged by the harness when warmup is complete and a test is starting
548        if 'running test name' in line:
549            p.kill()
550            break
551        if done_event.is_set():
552            logging.warning('Test finished without logging to logcat')
553            p.kill()
554            break
555
556        time.sleep(0.05)
557
558        p.poll()
559        if p.returncode != None:
560            logging.warning('Logcat terminated unexpectedly')
561            return
562
563
564def collect_power(done_event, test_fixedtime, results):
565    # Starting point is post test warmup as there are spikes during warmup
566    wait_for_test_warmup(done_event)
567
568    starting_power = GPUPowerStats()
569    starting_power.get_power_data()
570    logging.debug('Starting power: %s' % starting_power.power)
571
572    duration = test_fixedtime - 1.0  # Avoid measuring through test termination
573    start_time = time.time()
574    while time.time() - start_time < duration:
575        if done_event.is_set():
576            logging.warning('Test finished earlier than expected by collect_power')
577            break
578        time.sleep(0.05)
579
580    ending_power = GPUPowerStats()
581    ending_power.get_power_data()
582    dt = time.time() - start_time
583    logging.debug('Ending power: %s' % ending_power.power)
584
585    results.update({
586        # 1e6 for uW -> W
587        'cpu': ending_power.cpu_delta(starting_power) / dt / 1e6,
588        'gpu': ending_power.gpu_delta(starting_power) / dt / 1e6,
589    })
590
591
592def get_thermal_info():
593    out = run_adb_shell_command('dumpsys android.hardware.thermal.IThermal/default')
594    result = []
595    for line in out.splitlines():
596        if 'ThrottlingStatus:' in line:
597            name = re.search('Name: ([^ ]*)', line).group(1)
598            if ('VIRTUAL-SKIN' in name and
599                    '-CHARGE-' not in name and  # only supposed to affect charging speed
600                    'MODEL' not in name.split('-')):  # different units and not used for throttling
601                result.append(line)
602
603    if not result:
604        logging.error('Unexpected dumpsys IThermal response:\n%s', out)
605        raise RuntimeError('Unexpected dumpsys IThermal response, logged above')
606    return result
607
608
609def set_vendor_thermal_control(disabled=None):
610    if disabled:
611        # When disabling, first wait for vendor throttling to end to reset all state
612        waiting = True
613        while waiting:
614            waiting = False
615            for line in get_thermal_info():
616                if 'ThrottlingStatus: NONE' not in line:
617                    logging.info('Waiting for vendor throttling to finish: %s', line.strip())
618                    time.sleep(10)
619                    waiting = True
620                    break
621
622    run_adb_shell_command('setprop persist.vendor.disable.thermal.control %d' % disabled)
623
624
625def sleep_until_temps_below(limit_temp):
626    waiting = True
627    while waiting:
628        waiting = False
629        for line in get_thermal_info():
630            v = float(re.search('CurrentValue: ([^ ]*)', line).group(1))
631            if v > limit_temp:
632                logging.info('Waiting for device temps below %.1f: %s', limit_temp, line.strip())
633                time.sleep(5)
634                waiting = True
635                break
636
637
638def sleep_until_temps_below_thermalservice(limit_temp):
639    waiting = True
640    while waiting:
641        waiting = False
642        lines = run_adb_shell_command('dumpsys thermalservice').splitlines()
643        assert 'HAL Ready: true' in lines
644        for l in lines[lines.index('Current temperatures from HAL:') + 1:]:
645            if 'Temperature{' not in l:
646                break
647            v = re.search(r'mValue=([^,}]+)', l).group(1)
648            # Note: on some Pixel devices odd component temps are also reported here
649            # but some other key ones are not (e.g. CPU ODPM controlling cpu freqs)
650            if float(v) > limit_temp:
651                logging.info('Waiting for device temps below %.1f: %s', limit_temp, l.strip())
652                time.sleep(5)
653                waiting = True
654                break
655
656
657def sleep_until_battery_level(min_battery_level):
658    while True:
659        level = int(run_adb_shell_command('dumpsys battery get level').strip())
660        if level >= min_battery_level:
661            break
662        logging.info('Waiting for device battery level to reach %d. Current level: %d',
663                     min_battery_level, level)
664        time.sleep(10)
665
666
667def drop_high_low_and_average(values):
668    if len(values) >= 3:
669        values.remove(min(values))
670        values.remove(max(values))
671
672    average = statistics.mean(values)
673
674    variance = 0
675    if len(values) >= 2 and average != 0:
676        variance = statistics.stdev(values) / average
677
678    print(average, variance)
679
680    return float(average), float(variance)
681
682
683def get_angle_version():
684    angle_version = android_helper._Run('git rev-parse HEAD'.split(' ')).decode().strip()
685    origin_main_version = android_helper._Run(
686        'git rev-parse origin/main'.split(' ')).decode().strip()
687    if origin_main_version != angle_version:
688        angle_version += ' (origin/main %s)' % origin_main_version
689    return angle_version
690
691
692def safe_divide(x, y):
693    if y == 0:
694        return 0
695    return x / y
696
697
698def safe_cast_float(x):
699    if x == '':
700        return 0
701    return float(x)
702
703
704def safe_cast_int(x):
705    if x == '':
706        return 0
707    return int(x)
708
709
710def percent(x):
711    return "%.2f%%" % (x * 100)
712
713
714def main():
715    parser = argparse.ArgumentParser()
716    parser.add_argument('-f', '--filter', help='Trace filter. Defaults to all.', default='*')
717    parser.add_argument('-l', '--log', help='Logging level.', default=DEFAULT_LOG_LEVEL)
718    parser.add_argument(
719        '--renderer',
720        help='Which renderer to use: native, vulkan (via ANGLE), or default (' +
721        'GLES driver selected by system). Providing no option will run twice, native and vulkan',
722        default='both')
723    parser.add_argument(
724        '--walltimeonly',
725        help='Limit output to just wall time',
726        action='store_true',
727        default=False)
728    parser.add_argument(
729        '--power', help='Include CPU/GPU power used per trace', action='store_true', default=False)
730    parser.add_argument(
731        '--memory',
732        help='Include CPU/GPU memory used per trace',
733        action='store_true',
734        default=False)
735    parser.add_argument('--maxsteps', help='Run for fixed set of frames', default='')
736    parser.add_argument('--fixedtime', help='Run for fixed set of time', default='')
737    parser.add_argument(
738        '--minimizegpuwork',
739        help='Whether to run with minimized GPU work',
740        action='store_true',
741        default=False)
742    parser.add_argument('--output-tag', help='Tag for output files.', required=True)
743    parser.add_argument('--angle-version', help='Specify ANGLE version string.', default='')
744    parser.add_argument(
745        '--loop-count', help='How many times to loop through the traces', default=5)
746    parser.add_argument(
747        '--device', help='Which device to run the tests on (use serial)', default='')
748    parser.add_argument(
749        '--sleep', help='Add a sleep of this many seconds between each test)', type=int, default=0)
750    parser.add_argument(
751        '--custom-throttling-temp',
752        help='Custom thermal throttling with limit set to this temperature (off by default)',
753        type=float)
754    parser.add_argument(
755        '--custom-throttling-thermalservice-temp',
756        help='Custom thermal throttling (thermalservice) with limit set to this temperature (off by default)',
757        type=float)
758    parser.add_argument(
759        '--min-battery-level',
760        help='Sleep between tests if battery level drops below this value (off by default)',
761        type=int)
762    parser.add_argument(
763        '--angle-package',
764        help='Where to load ANGLE libraries from. This will load from the test APK by default, ' +
765        'but you can point to any APK that contains ANGLE. Specify \'system\' to use libraries ' +
766        'already on the device',
767        default=DEFAULT_ANGLE_PACKAGE)
768    parser.add_argument(
769        '--build-dir',
770        help='Where to find the APK on the host, i.e. out/Android. If unset, it is assumed you ' +
771        'are running from the build dir already, or are using the wrapper script ' +
772        'out/<config>/restricted_trace_perf.',
773        default='')
774    parser.add_argument(
775        '--screenshot-dir',
776        help='Host (local) directory to store screenshots of keyframes, which is frame 1 unless ' +
777        'the trace JSON file contains \'KeyFrames\'.',
778        default='')
779    parser.add_argument(
780        '--screenshot-frame',
781        help='Specify a specific frame to screenshot. Uses --screenshot-dir if provied.',
782        default='')
783    parser.add_argument(
784        '--fps-limit', help='Limit replay framerate to specified value', default='')
785
786    group = parser.add_mutually_exclusive_group()
787    group.add_argument(
788        '--vsync',
789        help='Whether to run the trace in vsync mode',
790        action='store_true',
791        default=False)
792    group.add_argument(
793        '--offscreen',
794        help='Whether to run the trace in offscreen mode',
795        action='store_true',
796        default=False)
797
798    args = parser.parse_args()
799
800    angle_test_util.SetupLogging(args.log.upper())
801
802    with run_from_dir(args.build_dir):
803        android_helper.Initialize("angle_trace_tests")  # includes adb root
804
805    # Determine some starting parameters
806    init()
807
808    try:
809        if args.custom_throttling_temp:
810            set_vendor_thermal_control(disabled=1)
811        run_traces(args)
812    finally:
813        if args.custom_throttling_temp:
814            set_vendor_thermal_control(disabled=0)
815        # Clean up settings, including in case of exceptions (including Ctrl-C)
816        run_adb_shell_command('settings delete global angle_debug_package')
817        run_adb_shell_command('settings delete global angle_gl_driver_selection_pkgs')
818        run_adb_shell_command('settings delete global angle_gl_driver_selection_values')
819
820    return 0
821
822
823def logged_args():
824    parser = argparse.ArgumentParser()
825    parser.add_argument('--output-tag')
826    _, extra_args = parser.parse_known_args()
827    return ' '.join(extra_args)
828
829
830def run_traces(args):
831    # Load trace names
832    test_json = os.path.join(args.build_dir, 'gen/trace_list.json')
833    with open(os.path.join(DEFAULT_TEST_DIR, test_json)) as f:
834        traces = json.loads(f.read())
835
836    failures = []
837
838    mode = get_mode(args)
839    trace_width = get_trace_width(mode)
840
841    # Select the target device
842    select_device(args.device)
843
844    # Add an underscore to the mode for use in loop below
845    if mode != '':
846        mode = mode + '_'
847
848    # Create output CSV
849    raw_data_filename = "raw_data." + args.output_tag + ".csv"
850    output_file = open(raw_data_filename, 'w', newline='')
851    output_writer = csv.writer(output_file)
852
853    # Set some widths that allow easily reading the values, but fit on smaller monitors.
854    column_width = {
855        'trace': trace_width,
856        'wall_time': 15,
857        'gpu_time': 15,
858        'cpu_time': 15,
859        'gpu_power': 10,
860        'cpu_power': 10,
861        'gpu_mem_sustained': 20,
862        'gpu_mem_peak': 15,
863        'proc_mem_median': 20,
864        'proc_mem_peak': 15
865    }
866
867    if args.walltimeonly:
868        print('%-*s' % (trace_width, 'wall_time_per_frame'))
869    else:
870        print('%-*s %-*s %-*s %-*s %-*s %-*s %-*s %-*s %-*s %-*s' %
871              (column_width['trace'], 'trace', column_width['wall_time'], 'wall_time',
872               column_width['gpu_time'], 'gpu_time', column_width['cpu_time'], 'cpu_time',
873               column_width['gpu_power'], 'gpu_power', column_width['cpu_power'], 'cpu_power',
874               column_width['gpu_mem_sustained'], 'gpu_mem_sustained',
875               column_width['gpu_mem_peak'], 'gpu_mem_peak', column_width['proc_mem_median'],
876               'proc_mem_median', column_width['proc_mem_peak'], 'proc_mem_peak'))
877        output_writer.writerow([
878            'trace', 'wall_time(ms)', 'gpu_time(ms)', 'cpu_time(ms)', 'gpu_power(W)',
879            'cpu_power(W)', 'gpu_mem_sustained', 'gpu_mem_peak', 'proc_mem_median', 'proc_mem_peak'
880        ])
881
882    if args.power:
883        starting_power = GPUPowerStats()
884        ending_power = GPUPowerStats()
885
886    renderers = []
887    if args.renderer != "both":
888        renderers.append(args.renderer)
889    else:
890        renderers = ("native", "vulkan")
891
892    wall_times = defaultdict(dict)
893    gpu_times = defaultdict(dict)
894    cpu_times = defaultdict(dict)
895    gpu_powers = defaultdict(dict)
896    cpu_powers = defaultdict(dict)
897    gpu_mem_sustaineds = defaultdict(dict)
898    gpu_mem_peaks = defaultdict(dict)
899    proc_mem_medians = defaultdict(dict)
900    proc_mem_peaks = defaultdict(dict)
901
902    # Organize the data for writing out
903    rows = defaultdict(dict)
904
905    def populate_row(rows, name, results):
906        if len(rows[name]) == 0:
907            rows[name] = defaultdict(list)
908        for renderer, wtimes in results.items():
909            average, variance = drop_high_low_and_average(wtimes)
910            rows[name][renderer].append(average)
911            rows[name][renderer].append(variance)
912
913    # Generate the SUMMARY output
914    summary_file = open("summary." + args.output_tag + ".csv", 'w', newline='')
915    summary_writer = csv.writer(summary_file)
916
917    android_version = run_adb_shell_command('getprop ro.build.fingerprint').strip()
918    angle_version = args.angle_version or get_angle_version()
919    # test_time = run_command('date \"+%Y%m%d\"').stdout.read().strip()
920
921    summary_writer.writerow([
922        "\"Android: " + android_version + "\n" + "ANGLE: " + angle_version + "\n" +
923        #  "Date: " + test_time + "\n" +
924        "Source: " + raw_data_filename + "\n" + "Args: " + logged_args() + "\""
925    ])
926
927    trace_number = 0
928
929    if (len(renderers) == 1) and (renderers[0] != "both"):
930        renderer_name = renderers[0]
931        summary_writer.writerow([
932            "#", "\"Trace\"", f"\"{renderer_name}\nwall\ntime\nper\nframe\n(ms)\"",
933            f"\"{renderer_name}\nwall\ntime\nvariance\"",
934            f"\"{renderer_name}\nGPU\ntime\nper\nframe\n(ms)\"",
935            f"\"{renderer_name}\nGPU\ntime\nvariance\"",
936            f"\"{renderer_name}\nCPU\ntime\nper\nframe\n(ms)\"",
937            f"\"{renderer_name}\nCPU\ntime\nvariance\"", f"\"{renderer_name}\nGPU\npower\n(W)\"",
938            f"\"{renderer_name}\nGPU\npower\nvariance\"", f"\"{renderer_name}\nCPU\npower\n(W)\"",
939            f"\"{renderer_name}\nCPU\npower\nvariance\"", f"\"{renderer_name}\nGPU\nmem\n(B)\"",
940            f"\"{renderer_name}\nGPU\nmem\nvariance\"",
941            f"\"{renderer_name}\npeak\nGPU\nmem\n(B)\"",
942            f"\"{renderer_name}\npeak\nGPU\nmem\nvariance\"",
943            f"\"{renderer_name}\nprocess\nmem\n(B)\"",
944            f"\"{renderer_name}\nprocess\nmem\nvariance\"",
945            f"\"{renderer_name}\npeak\nprocess\nmem\n(B)\"",
946            f"\"{renderer_name}\npeak\nprocess\nmem\nvariance\""
947        ])
948    else:
949        summary_writer.writerow([
950            "#", "\"Trace\"", "\"Native\nwall\ntime\nper\nframe\n(ms)\"",
951            "\"Native\nwall\ntime\nvariance\"", "\"ANGLE\nwall\ntime\nper\nframe\n(ms)\"",
952            "\"ANGLE\nwall\ntime\nvariance\"", "\"wall\ntime\ncompare\"",
953            "\"Native\nGPU\ntime\nper\nframe\n(ms)\"", "\"Native\nGPU\ntime\nvariance\"",
954            "\"ANGLE\nGPU\ntime\nper\nframe\n(ms)\"", "\"ANGLE\nGPU\ntime\nvariance\"",
955            "\"GPU\ntime\ncompare\"", "\"Native\nCPU\ntime\nper\nframe\n(ms)\"",
956            "\"Native\nCPU\ntime\nvariance\"", "\"ANGLE\nCPU\ntime\nper\nframe\n(ms)\"",
957            "\"ANGLE\nCPU\ntime\nvariance\"", "\"CPU\ntime\ncompare\"",
958            "\"Native\nGPU\npower\n(W)\"", "\"Native\nGPU\npower\nvariance\"",
959            "\"ANGLE\nGPU\npower\n(W)\"", "\"ANGLE\nGPU\npower\nvariance\"",
960            "\"GPU\npower\ncompare\"", "\"Native\nCPU\npower\n(W)\"",
961            "\"Native\nCPU\npower\nvariance\"", "\"ANGLE\nCPU\npower\n(W)\"",
962            "\"ANGLE\nCPU\npower\nvariance\"", "\"CPU\npower\ncompare\"",
963            "\"Native\nGPU\nmem\n(B)\"", "\"Native\nGPU\nmem\nvariance\"",
964            "\"ANGLE\nGPU\nmem\n(B)\"", "\"ANGLE\nGPU\nmem\nvariance\"", "\"GPU\nmem\ncompare\"",
965            "\"Native\npeak\nGPU\nmem\n(B)\"", "\"Native\npeak\nGPU\nmem\nvariance\"",
966            "\"ANGLE\npeak\nGPU\nmem\n(B)\"", "\"ANGLE\npeak\nGPU\nmem\nvariance\"",
967            "\"GPU\npeak\nmem\ncompare\"", "\"Native\nprocess\nmem\n(B)\"",
968            "\"Native\nprocess\nmem\nvariance\"", "\"ANGLE\nprocess\nmem\n(B)\"",
969            "\"ANGLE\nprocess\nmem\nvariance\"", "\"process\nmem\ncompare\"",
970            "\"Native\npeak\nprocess\nmem\n(B)\"", "\"Native\npeak\nprocess\nmem\nvariance\"",
971            "\"ANGLE\npeak\nprocess\nmem\n(B)\"", "\"ANGLE\npeak\nprocess\nmem\nvariance\"",
972            "\"process\npeak\nmem\ncompare\""
973        ])
974
975    with run_from_dir(args.build_dir):
976        android_helper.PrepareTestSuite("angle_trace_tests")
977
978    for trace in fnmatch.filter(traces, args.filter):
979
980        print(
981            "\nStarting run for %s loopcount %i with %s at %s\n" %
982            (trace, int(args.loop_count), renderers, datetime.now().strftime('%Y-%m-%d %H:%M:%S')))
983
984        with run_from_dir(args.build_dir):
985            android_helper.PrepareRestrictedTraces([trace])
986
987        # Start with clean data containers for each trace
988        rows.clear()
989        wall_times.clear()
990        gpu_times.clear()
991        cpu_times.clear()
992        gpu_powers.clear()
993        cpu_powers.clear()
994        gpu_mem_sustaineds.clear()
995        gpu_mem_peaks.clear()
996        proc_mem_medians.clear()
997        proc_mem_peaks.clear()
998
999        for i in range(int(args.loop_count)):
1000
1001            for renderer in renderers:
1002
1003                if renderer == "native":
1004                    # Force the settings to native
1005                    run_adb_shell_command(
1006                        'settings put global angle_gl_driver_selection_pkgs com.android.angle.test'
1007                    )
1008                    run_adb_shell_command(
1009                        'settings put global angle_gl_driver_selection_values native')
1010                elif renderer == "vulkan":
1011                    # Force the settings to ANGLE
1012                    run_adb_shell_command(
1013                        'settings put global angle_gl_driver_selection_pkgs com.android.angle.test'
1014                    )
1015                    run_adb_shell_command(
1016                        'settings put global angle_gl_driver_selection_values angle')
1017                elif renderer == "default":
1018                    logging.info(
1019                        'Deleting Android settings for forcing selection of GLES driver, ' +
1020                        'allowing system to load the default')
1021                    run_adb_shell_command('settings delete global angle_debug_package')
1022                    run_adb_shell_command('settings delete global angle_gl_driver_all_angle')
1023                    run_adb_shell_command('settings delete global angle_gl_driver_selection_pkgs')
1024                    run_adb_shell_command(
1025                        'settings delete global angle_gl_driver_selection_values')
1026                else:
1027                    logging.error('Unsupported renderer {}'.format(renderer))
1028                    exit()
1029
1030                if args.angle_package == 'system':
1031                    # Clear the debug package so ANGLE will be loaded from /system/lib64
1032                    run_adb_shell_command('settings delete global angle_debug_package')
1033                else:
1034                    # Otherwise, load ANGLE from the specified APK
1035                    run_adb_shell_command('settings put global angle_debug_package ' +
1036                                          args.angle_package)
1037
1038                # Remove any previous perf results
1039                cleanup()
1040                # Clear blob cache to avoid post-warmup cache eviction b/298028816
1041                clear_blob_cache()
1042
1043                test = trace.split(' ')[0]
1044
1045                if args.power:
1046                    assert args.fixedtime, '--power requires --fixedtime'
1047                    done_event = threading.Event()
1048                    run_adb_command(['logcat', '-c'])  # needed for wait_for_test_warmup
1049                    power_results = {}  # output arg
1050                    power_thread = threading.Thread(
1051                        target=collect_power,
1052                        args=(done_event, float(args.fixedtime), power_results))
1053                    power_thread.daemon = True
1054                    power_thread.start()
1055
1056                # We scope the run_trace call so we can use a temp dir for screenshots that gets deleted
1057                with contextlib.ExitStack() as stack:
1058                    screenshot_device_dir = None
1059                    if args.screenshot_dir != '' or args.screenshot_frame != '':
1060                        temp_dir = stack.enter_context(android_helper._TempDeviceDir())
1061                        screenshot_device_dir = temp_dir
1062
1063                    logging.debug('Running %s' % test)
1064                    test_time = run_trace(test, args, screenshot_device_dir)
1065
1066                    if screenshot_device_dir:
1067                        pull_screenshot(args, screenshot_device_dir, renderer)
1068
1069                gpu_power, cpu_power = 0, 0
1070                if args.power:
1071                    done_event.set()
1072                    power_thread.join(timeout=2)
1073                    if power_thread.is_alive():
1074                        logging.warning('collect_power thread did not terminate')
1075                    else:
1076                        gpu_power = power_results['gpu']
1077                        cpu_power = power_results['cpu']
1078
1079                wall_time = get_test_time()
1080
1081                gpu_time = get_gpu_time() if args.vsync else '0'
1082
1083                cpu_time = get_cpu_time()
1084
1085                gpu_mem_sustained, gpu_mem_peak = 0, 0
1086                proc_mem_peak, proc_mem_median = 0, 0
1087                if args.memory:
1088                    gpu_mem_sustained, gpu_mem_peak = get_gpu_memory(test_time)
1089                    logging.debug(
1090                        '%s = %i, %s = %i' %
1091                        ('gpu_mem_sustained', gpu_mem_sustained, 'gpu_mem_peak', gpu_mem_peak))
1092
1093                    proc_mem_peak, proc_mem_median = get_proc_memory()
1094
1095                trace_name = mode + renderer + '_' + test
1096
1097                if len(wall_times[test]) == 0:
1098                    wall_times[test] = defaultdict(list)
1099                try:
1100                    wt = safe_cast_float(wall_time)
1101                except ValueError:  # e.g. 'crashed'
1102                    wt = -1
1103                wall_times[test][renderer].append(wt)
1104
1105                if len(gpu_times[test]) == 0:
1106                    gpu_times[test] = defaultdict(list)
1107                gpu_times[test][renderer].append(safe_cast_float(gpu_time))
1108
1109                if len(cpu_times[test]) == 0:
1110                    cpu_times[test] = defaultdict(list)
1111                cpu_times[test][renderer].append(safe_cast_float(cpu_time))
1112
1113                if len(gpu_powers[test]) == 0:
1114                    gpu_powers[test] = defaultdict(list)
1115                gpu_powers[test][renderer].append(safe_cast_float(gpu_power))
1116
1117                if len(cpu_powers[test]) == 0:
1118                    cpu_powers[test] = defaultdict(list)
1119                cpu_powers[test][renderer].append(safe_cast_float(cpu_power))
1120
1121                if len(gpu_mem_sustaineds[test]) == 0:
1122                    gpu_mem_sustaineds[test] = defaultdict(list)
1123                gpu_mem_sustaineds[test][renderer].append(safe_cast_int(gpu_mem_sustained))
1124
1125                if len(gpu_mem_peaks[test]) == 0:
1126                    gpu_mem_peaks[test] = defaultdict(list)
1127                gpu_mem_peaks[test][renderer].append(safe_cast_int(gpu_mem_peak))
1128
1129                if len(proc_mem_medians[test]) == 0:
1130                    proc_mem_medians[test] = defaultdict(list)
1131                proc_mem_medians[test][renderer].append(safe_cast_int(proc_mem_median))
1132
1133                if len(proc_mem_peaks[test]) == 0:
1134                    proc_mem_peaks[test] = defaultdict(list)
1135                proc_mem_peaks[test][renderer].append(safe_cast_int(proc_mem_peak))
1136
1137                if args.walltimeonly:
1138                    print('%-*s' % (trace_width, wall_time))
1139                else:
1140                    print(
1141                        '%-*s %-*s %-*s %-*s %-*s %-*s %-*i %-*i %-*i %-*i' %
1142                        (column_width['trace'], trace_name, column_width['wall_time'], wall_time,
1143                         column_width['gpu_time'], gpu_time, column_width['cpu_time'], cpu_time,
1144                         column_width['gpu_power'], '%.3f' % gpu_power, column_width['cpu_power'],
1145                         '%.3f' % cpu_power, column_width['gpu_mem_sustained'], gpu_mem_sustained,
1146                         column_width['gpu_mem_peak'], gpu_mem_peak,
1147                         column_width['proc_mem_median'], proc_mem_median,
1148                         column_width['proc_mem_peak'], proc_mem_peak))
1149                    output_writer.writerow([
1150                        mode + renderer + '_' + test, wall_time, gpu_time, cpu_time, gpu_power,
1151                        cpu_power, gpu_mem_sustained, gpu_mem_peak, proc_mem_median, proc_mem_peak
1152                    ])
1153
1154
1155                # Early exit for testing
1156                #exit()
1157
1158                # Depending on workload, sleeps might be needed to dissipate heat or recharge battery
1159                if args.sleep != 0:
1160                    time.sleep(args.sleep)
1161
1162                if args.custom_throttling_temp:
1163                    sleep_until_temps_below(args.custom_throttling_temp)
1164
1165                if args.custom_throttling_thermalservice_temp:
1166                    sleep_until_temps_below_thermalservice(
1167                        args.custom_throttling_thermalservice_temp)
1168
1169                if args.min_battery_level:
1170                    sleep_until_battery_level(args.min_battery_level)
1171
1172            print()  # New line for readability
1173
1174        for name, results in wall_times.items():
1175            populate_row(rows, name, results)
1176
1177        for name, results in gpu_times.items():
1178            populate_row(rows, name, results)
1179
1180        for name, results in cpu_times.items():
1181            populate_row(rows, name, results)
1182
1183        for name, results in gpu_powers.items():
1184            populate_row(rows, name, results)
1185
1186        for name, results in cpu_powers.items():
1187            populate_row(rows, name, results)
1188
1189        for name, results in gpu_mem_sustaineds.items():
1190            populate_row(rows, name, results)
1191
1192        for name, results in gpu_mem_peaks.items():
1193            populate_row(rows, name, results)
1194
1195        for name, results in proc_mem_medians.items():
1196            populate_row(rows, name, results)
1197
1198        for name, results in proc_mem_peaks.items():
1199            populate_row(rows, name, results)
1200
1201        if (len(renderers) == 1) and (renderers[0] != "both"):
1202            renderer_name = renderers[0]
1203            for name, data in rows.items():
1204                trace_number += 1
1205                summary_writer.writerow([
1206                    trace_number,
1207                    name,
1208                    # wall_time
1209                    "%.3f" % data[renderer_name][0],
1210                    percent(data[renderer_name][1]),
1211                    # GPU time
1212                    "%.3f" % data[renderer_name][2],
1213                    percent(data[renderer_name][3]),
1214                    # CPU time
1215                    "%.3f" % data[renderer_name][4],
1216                    percent(data[renderer_name][5]),
1217                    # GPU power
1218                    "%.3f" % data[renderer_name][6],
1219                    percent(data[renderer_name][7]),
1220                    # CPU power
1221                    "%.3f" % data[renderer_name][8],
1222                    percent(data[renderer_name][9]),
1223                    # GPU mem
1224                    int(data[renderer_name][10]),
1225                    percent(data[renderer_name][11]),
1226                    # GPU peak mem
1227                    int(data[renderer_name][12]),
1228                    percent(data[renderer_name][13]),
1229                    # process mem
1230                    int(data[renderer_name][14]),
1231                    percent(data[renderer_name][15]),
1232                    # process peak mem
1233                    int(data[renderer_name][16]),
1234                    percent(data[renderer_name][17]),
1235                ])
1236        else:
1237            for name, data in rows.items():
1238                trace_number += 1
1239                summary_writer.writerow([
1240                    trace_number,
1241                    name,
1242                    # wall_time
1243                    "%.3f" % data["native"][0],
1244                    percent(data["native"][1]),
1245                    "%.3f" % data["vulkan"][0],
1246                    percent(data["vulkan"][1]),
1247                    percent(safe_divide(data["native"][0], data["vulkan"][0])),
1248                    # GPU time
1249                    "%.3f" % data["native"][2],
1250                    percent(data["native"][3]),
1251                    "%.3f" % data["vulkan"][2],
1252                    percent(data["vulkan"][3]),
1253                    percent(safe_divide(data["native"][2], data["vulkan"][2])),
1254                    # CPU time
1255                    "%.3f" % data["native"][4],
1256                    percent(data["native"][5]),
1257                    "%.3f" % data["vulkan"][4],
1258                    percent(data["vulkan"][5]),
1259                    percent(safe_divide(data["native"][4], data["vulkan"][4])),
1260                    # GPU power
1261                    "%.3f" % data["native"][6],
1262                    percent(data["native"][7]),
1263                    "%.3f" % data["vulkan"][6],
1264                    percent(data["vulkan"][7]),
1265                    percent(safe_divide(data["native"][6], data["vulkan"][6])),
1266                    # CPU power
1267                    "%.3f" % data["native"][8],
1268                    percent(data["native"][9]),
1269                    "%.3f" % data["vulkan"][8],
1270                    percent(data["vulkan"][9]),
1271                    percent(safe_divide(data["native"][8], data["vulkan"][8])),
1272                    # GPU mem
1273                    int(data["native"][10]),
1274                    percent(data["native"][11]),
1275                    int(data["vulkan"][10]),
1276                    percent(data["vulkan"][11]),
1277                    percent(safe_divide(data["native"][10], data["vulkan"][10])),
1278                    # GPU peak mem
1279                    int(data["native"][12]),
1280                    percent(data["native"][13]),
1281                    int(data["vulkan"][12]),
1282                    percent(data["vulkan"][13]),
1283                    percent(safe_divide(data["native"][12], data["vulkan"][12])),
1284                    # process mem
1285                    int(data["native"][14]),
1286                    percent(data["native"][15]),
1287                    int(data["vulkan"][14]),
1288                    percent(data["vulkan"][15]),
1289                    percent(safe_divide(data["native"][14], data["vulkan"][14])),
1290                    # process peak mem
1291                    int(data["native"][16]),
1292                    percent(data["native"][17]),
1293                    int(data["vulkan"][16]),
1294                    percent(data["vulkan"][17]),
1295                    percent(safe_divide(data["native"][16], data["vulkan"][16]))
1296                ])
1297
1298
1299if __name__ == '__main__':
1300    sys.exit(main())
1301