blob: 5f0800d795289688be9a6ca4d1db736473bc7dcb [file] [log] [blame]
#! /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())