| #! /usr/bin/env python3 |
| # |
| # Copyright 2020 The ANGLE Project 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 that re-captures the traces in the restricted trace folder. We can |
| use this to update traces without needing to re-run the app on a device. |
| ''' |
| |
| import argparse |
| import fnmatch |
| import json |
| import logging |
| import os |
| import re |
| import shutil |
| import stat |
| import subprocess |
| import sys |
| |
| from gen_restricted_traces import get_context as get_context |
| |
| DEFAULT_TEST_SUITE = 'angle_perftests' |
| DEFAULT_TEST_JSON = 'restricted_traces.json' |
| DEFAULT_LOG_LEVEL = 'info' |
| DEFAULT_BACKUP_FOLDER = 'retrace-backups' |
| |
| # We preserve select metadata in the trace header that can't be re-captured properly. |
| # Currently this is just the set of default framebuffer surface config bits. |
| METADATA_KEYWORDS = ['kDefaultFramebuffer'] |
| |
| EXIT_SUCCESS = 0 |
| EXIT_FAILURE = 1 |
| |
| |
| def get_script_dir(): |
| return os.path.dirname(sys.argv[0]) |
| |
| |
| # TODO(jmadill): Remove after retrace. http://anglebug.com/5133 |
| def json_metadata_exists(trace): |
| json_file_name = os.path.join(get_script_dir(), '%s/%s.json') % (trace, trace) |
| return os.path.isfile(json_file_name) |
| |
| |
| def load_json_metadata(trace): |
| json_file_name = os.path.join(get_script_dir(), '%s/%s.json') % (trace, trace) |
| with open(json_file_name) as f: |
| return json.loads(f.read())['TraceMetadata'] |
| |
| |
| def src_trace_path(trace): |
| return os.path.join(get_script_dir(), trace) |
| |
| |
| def context_header(trace, trace_path): |
| context_id = get_context(trace_path) |
| # TODO(jmadill): Remove after retrace. http://anglebug.com/5133 |
| for try_path_expr in ['%s_capture_context%s.h', '%s_context%s.h']: |
| header = try_path_expr % (trace, context_id) |
| try_path = os.path.join(trace_path, header) |
| if os.path.isfile(try_path): |
| return try_path |
| logging.fatal('Could not find context header for %s' % trace) |
| return None |
| |
| |
| def get_num_frames(trace): |
| if json_metadata_exists(trace): |
| json_metadata = load_json_metadata(trace) |
| if 'FrameEnd' in json_metadata: |
| return int(json_metadata['FrameEnd']) |
| |
| trace_path = src_trace_path(trace) |
| |
| lo = 99999999 |
| hi = 0 |
| |
| for file in os.listdir(trace_path): |
| match = re.match(r'.+_context\d_frame(\d+)\.cpp', file) |
| if match: |
| frame = int(match.group(1)) |
| if frame < lo: |
| lo = frame |
| if frame > hi: |
| hi = frame |
| |
| return hi - lo + 1 |
| |
| |
| def get_trace_metadata(trace): |
| trace_path = src_trace_path(trace) |
| header_file = context_header(trace, trace_path) |
| metadata = [] |
| with open(header_file, 'rt') as f: |
| for line in f.readlines(): |
| for keyword in METADATA_KEYWORDS: |
| if keyword in line: |
| metadata += [line] |
| return metadata |
| |
| |
| def replace_metadata(header_file, metadata): |
| lines = [] |
| replaced = False |
| with open(header_file, 'rt') as f: |
| for line in f.readlines(): |
| found_keyword = False |
| for keyword in METADATA_KEYWORDS: |
| if keyword in line: |
| found_keyword = True |
| break |
| |
| if found_keyword: |
| if not replaced: |
| replaced = True |
| lines += metadata |
| else: |
| lines += [line] |
| |
| with open(header_file, 'wt') as f: |
| f.writelines(lines) |
| |
| |
| def path_contains_header(path): |
| for file in os.listdir(path): |
| if fnmatch.fnmatch(file, '*.h'): |
| return True |
| return False |
| |
| |
| def chmod_directory(directory, perm): |
| assert os.path.isdir(directory) |
| for file in os.listdir(directory): |
| fn = os.path.join(directory, file) |
| os.chmod(fn, perm) |
| |
| |
| def ensure_rmdir(directory): |
| if os.path.isdir(directory): |
| chmod_directory(directory, stat.S_IWRITE) |
| shutil.rmtree(directory) |
| |
| |
| def copy_trace_folder(old_path, new_path): |
| logging.info('%s -> %s' % (old_path, new_path)) |
| ensure_rmdir(new_path) |
| shutil.copytree(old_path, new_path) |
| |
| |
| def backup_traces(args, traces): |
| for trace in fnmatch.filter(traces, args.traces): |
| trace_path = src_trace_path(trace) |
| trace_backup_path = os.path.join(args.out_path, trace) |
| copy_trace_folder(trace_path, trace_backup_path) |
| |
| |
| # TODO(jmadill): Remove this once migrated. http://anglebug.com/5133 |
| def run_code_generation(): |
| python_binary = 'py.exe' if os.name == 'nt' else 'python3' |
| angle_dir = os.path.join(get_script_dir(), '..', '..', '..') |
| gen_path = os.path.join(angle_dir, 'scripts', 'run_code_generation.py') |
| subprocess.check_call([python_binary, gen_path]) |
| |
| |
| def restore_traces(args, traces): |
| for trace in fnmatch.filter(traces, args.traces): |
| trace_path = src_trace_path(trace) |
| trace_backup_path = os.path.join(args.out_path, trace) |
| if not os.path.isdir(trace_backup_path): |
| logging.error('Trace folder not found at %s' % trace_backup_path) |
| else: |
| copy_trace_folder(trace_backup_path, trace_path) |
| # TODO(jmadill): Remove this once migrated. http://anglebug.com/5133 |
| angle_dir = os.path.join(get_script_dir(), '..', '..', '..') |
| json_path = os.path.join(angle_dir, 'scripts', 'code_generation_hashes', |
| 'restricted_traces.json') |
| if os.path.exists(json_path): |
| os.unlink(json_path) |
| run_code_generation() |
| |
| |
| def run_autoninja(args): |
| autoninja_binary = 'autoninja' |
| if os.name == 'nt': |
| autoninja_binary += '.bat' |
| |
| autoninja_args = [autoninja_binary, '-C', args.gn_path, args.test_suite] |
| logging.debug('Calling %s' % ' '.join(autoninja_args)) |
| subprocess.check_call(autoninja_args) |
| |
| |
| def run_test_suite(args, trace, max_steps, additional_args, additional_env): |
| trace_binary = os.path.join(args.gn_path, args.test_suite) |
| if os.name == 'nt': |
| trace_binary += '.exe' |
| |
| renderer = 'vulkan' if args.no_swiftshader else 'vulkan_swiftshader' |
| trace_filter = '--gtest_filter=TracePerfTest.Run/%s_%s' % (renderer, trace) |
| run_args = [ |
| trace_binary, |
| trace_filter, |
| '--max-steps-performed', |
| str(max_steps), |
| ] + additional_args |
| if not args.no_swiftshader: |
| run_args += ['--enable-all-trace-tests'] |
| |
| env = {**os.environ.copy(), **additional_env} |
| env_string = ' '.join(['%s=%s' % item for item in additional_env.items()]) |
| if env_string: |
| env_string += ' ' |
| |
| logging.info('%s%s' % (env_string, ' '.join(run_args))) |
| subprocess.check_call(run_args, env=env) |
| |
| |
| def upgrade_traces(args, traces): |
| run_autoninja(args) |
| |
| failures = [] |
| |
| for trace in fnmatch.filter(traces, args.traces): |
| logging.debug('Tracing %s' % trace) |
| |
| trace_path = os.path.abspath(os.path.join(args.out_path, trace)) |
| if not os.path.isdir(trace_path): |
| os.makedirs(trace_path) |
| elif args.no_overwrite and path_contains_header(trace_path): |
| logging.info('Skipping "%s" because the out folder already exists' % trace) |
| continue |
| |
| num_frames = get_num_frames(trace) |
| metadata = get_trace_metadata(trace) |
| |
| logging.debug('Read metadata: %s' % str(metadata)) |
| |
| if json_metadata_exists(trace): |
| json_metadata = load_json_metadata(trace) |
| else: |
| json_metadata = {} |
| |
| max_steps = min(args.limit, num_frames) if args.limit else num_frames |
| |
| # We start tracing from frame 2. --retrace-mode issues a Swap() after Setup() so we can |
| # accurately re-trace the MEC. |
| additional_env = { |
| 'ANGLE_CAPTURE_LABEL': trace, |
| 'ANGLE_CAPTURE_OUT_DIR': trace_path, |
| 'ANGLE_CAPTURE_FRAME_START': '2', |
| 'ANGLE_CAPTURE_FRAME_END': str(max_steps + 1), |
| } |
| if args.validation: |
| additional_env['ANGLE_CAPTURE_VALIDATION'] = '1' |
| # Also turn on shader output init to ensure we have no undefined values. |
| # This feature is also enabled in replay when using --validation. |
| additional_env[ |
| 'ANGLE_FEATURE_OVERRIDES_ENABLED'] = 'allocateNonZeroMemory:forceInitShaderVariables' |
| if args.validation_expr: |
| additional_env['ANGLE_CAPTURE_VALIDATION_EXPR'] = args.validation_expr |
| if args.trim: |
| additional_env['ANGLE_CAPTURE_TRIM_ENABLED'] = '1' |
| if args.no_trim: |
| additional_env['ANGLE_CAPTURE_TRIM_ENABLED'] = '0' |
| |
| additional_args = ['--retrace-mode'] |
| |
| try: |
| run_test_suite(args, trace, max_steps, additional_args, additional_env) |
| |
| header_file = context_header(trace, trace_path) |
| |
| if not os.path.exists(header_file): |
| logging.error('There was a problem tracing "%s", could not find header file: %s' % |
| (trace, header_file)) |
| failures += [trace] |
| else: |
| replace_metadata(header_file, metadata) |
| except: |
| logging.exception('There was an exception running "%s":' % trace) |
| failures += [trace] |
| |
| if failures: |
| print('The following traces failed to upgrade:\n') |
| print('\n'.join([' ' + trace for trace in failures])) |
| return EXIT_FAILURE |
| |
| return EXIT_SUCCESS |
| |
| |
| def validate_traces(args, traces): |
| restore_traces(args, traces) |
| run_autoninja(args) |
| |
| additional_args = ['--validation'] |
| additional_env = { |
| 'ANGLE_FEATURE_OVERRIDES_ENABLED': 'allocateNonZeroMemory:forceInitShaderVariables' |
| } |
| |
| failures = [] |
| |
| for trace in fnmatch.filter(traces, args.traces): |
| num_frames = get_num_frames(trace) |
| max_steps = min(args.limit, num_frames) if args.limit else num_frames |
| try: |
| run_test_suite(args, trace, max_steps, additional_args, additional_env) |
| except: |
| logging.error('There was a failure running "%s".' % trace) |
| failures += [trace] |
| |
| if failures: |
| print('The following traces failed to validate:\n') |
| print('\n'.join([' ' + trace for trace in failures])) |
| return EXIT_FAILURE |
| |
| return EXIT_SUCCESS |
| |
| |
| def main(): |
| parser = argparse.ArgumentParser() |
| parser.add_argument('-l', '--log', help='Logging level.', default=DEFAULT_LOG_LEVEL) |
| parser.add_argument( |
| '--test-suite', |
| help='Test Suite. Default is %s' % DEFAULT_TEST_SUITE, |
| default=DEFAULT_TEST_SUITE) |
| parser.add_argument( |
| '--no-swiftshader', |
| help='Trace against native Vulkan.', |
| action='store_true', |
| default=False) |
| |
| subparsers = parser.add_subparsers(dest='command', required=True, help='Command to run.') |
| |
| backup_parser = subparsers.add_parser( |
| 'backup', help='Copies trace contents into a saved folder.') |
| backup_parser.add_argument( |
| 'traces', help='Traces to back up. Supports fnmatch expressions.', default='*') |
| backup_parser.add_argument( |
| '-o', |
| '--out-path', |
| '--backup-path', |
| help='Destination folder. Default is "%s".' % DEFAULT_BACKUP_FOLDER, |
| default=DEFAULT_BACKUP_FOLDER) |
| |
| restore_parser = subparsers.add_parser( |
| 'restore', help='Copies traces from a saved folder to the trace folder.') |
| restore_parser.add_argument( |
| '-o', |
| '--out-path', |
| '--backup-path', |
| help='Path the traces were saved. Default is "%s".' % DEFAULT_BACKUP_FOLDER, |
| default=DEFAULT_BACKUP_FOLDER) |
| restore_parser.add_argument( |
| 'traces', help='Traces to restore. Supports fnmatch expressions.', default='*') |
| |
| upgrade_parser = subparsers.add_parser( |
| 'upgrade', help='Re-trace existing traces, upgrading the format.') |
| upgrade_parser.add_argument('gn_path', help='GN build path') |
| upgrade_parser.add_argument('out_path', help='Output directory') |
| upgrade_parser.add_argument( |
| '-f', '--traces', '--filter', help='Trace filter. Defaults to all.', default='*') |
| upgrade_parser.add_argument( |
| '-n', |
| '--no-overwrite', |
| help='Skip traces which already exist in the out directory.', |
| action='store_true') |
| upgrade_parser.add_argument( |
| '--validation', help='Enable state serialization validation calls.', action='store_true') |
| upgrade_parser.add_argument( |
| '--validation-expr', |
| help='Validation expression, used to add more validation checkpoints.') |
| upgrade_parser.add_argument( |
| '--limit', |
| '--frame-limit', |
| type=int, |
| help='Limits the number of captured frames to produce a shorter trace than the original.') |
| upgrade_parser.add_argument( |
| '--trim', action='store_true', help='Enables trace trimming. Breaks replay validation.') |
| upgrade_parser.add_argument( |
| '--no-trim', action='store_true', help='Disables trace trimming. Useful for validation.') |
| upgrade_parser.set_defaults(trim=True) |
| |
| validate_parser = subparsers.add_parser( |
| 'validate', help='Runs the an updated test suite with validation enabled.') |
| validate_parser.add_argument('gn_path', help='GN build path') |
| validate_parser.add_argument('out_path', help='Path to the upgraded trace folder.') |
| validate_parser.add_argument( |
| 'traces', help='Traces to validate. Supports fnmatch expressions.', default='*') |
| validate_parser.add_argument( |
| '--limit', '--frame-limit', type=int, help='Limits the number of tested frames.') |
| |
| args, extra_flags = parser.parse_known_args() |
| |
| logging.basicConfig(level=args.log.upper()) |
| |
| # Load trace names |
| with open(os.path.join(get_script_dir(), DEFAULT_TEST_JSON)) as f: |
| traces = json.loads(f.read()) |
| |
| traces = [trace.split(' ')[0] for trace in traces['traces']] |
| |
| if args.command == 'backup': |
| return backup_traces(args, traces) |
| elif args.command == 'restore': |
| return restore_traces(args, traces) |
| elif args.command == 'upgrade': |
| return upgrade_traces(args, traces) |
| elif args.command == 'validate': |
| return validate_traces(args, traces) |
| else: |
| logging.fatal('Unknown command: %s' % args.command) |
| return EXIT_FAILURE |
| |
| |
| if __name__ == '__main__': |
| sys.exit(main()) |