| #!/usr/bin/env vpython3 | |
| # Copyright 2013 The Flutter Authors. All rights reserved. | |
| # Use of this source code is governed by a BSD-style license that can be | |
| # found in the LICENSE file. | |
| import argparse | |
| import difflib | |
| import json | |
| import os | |
| import sys | |
| # This script detects performance impacting changes to shaders. | |
| # | |
| # When the GN build is configured with the path to the `malioc` tool, the | |
| # results of its analysis will be placed under `out/$CONFIG/gen/malioc` in | |
| # separate .json files. That path should be supplied to this script as the | |
| # `--after` argument. This script compares those results against previous | |
| # results in a golden file checked in to the tree under | |
| # `flutter/impeller/tools/malioc.json`. That file should be passed to this | |
| # script as the `--before` argument. To create or update the golden file, | |
| # passing the `--update` flag will cause the data from the `--after` path to | |
| # overwrite the file at the `--before` path. | |
| # | |
| # Configure and build: | |
| # $ flutter/tools/gn --malioc-path path/to/malioc | |
| # $ ninja -C out/host_debug | |
| # | |
| # Analyze | |
| # $ flutter/impeller/tools/malioc_diff.py \ | |
| # --before flutter/impeller/tools/malioc.json \ | |
| # --after out/host_debug/gen/malioc | |
| # | |
| # If there are differences between before and after, whether positive or | |
| # negative, the exit code for this script will be 1, and 0 otherwise. | |
| SRC_ROOT = os.path.dirname( | |
| os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) | |
| ) | |
| CORES = [ | |
| 'Mali-G78', # Pixel 6 / 2020 | |
| 'Mali-T880', # 2016 | |
| ] | |
| # Path to the engine root checkout. This is used to calculate absolute | |
| # paths if relative ones are passed to the script. | |
| BUILD_ROOT_DIR = os.path.abspath(os.path.join(os.path.realpath(__file__), '..', '..', '..', '..')) | |
| def parse_args(argv): | |
| parser = argparse.ArgumentParser( | |
| description='A script that compares before/after malioc analysis results', | |
| ) | |
| parser.add_argument( | |
| '--after', | |
| '-a', | |
| type=str, | |
| help='The path to a directory tree containing new malioc results in json files.', | |
| ) | |
| parser.add_argument( | |
| '--before', | |
| '-b', | |
| type=str, | |
| help='The path to a json file containing existing malioc results.', | |
| ) | |
| parser.add_argument( | |
| '--after-relative-to-src', | |
| type=str, | |
| help=( | |
| 'A relative path calculated from the engine src directory to ' | |
| 'a directory tree containing new malioc results in json files' | |
| ), | |
| ) | |
| parser.add_argument( | |
| '--before-relative-to-src', | |
| type=str, | |
| help=( | |
| 'A relative path calculated from the engine src directory to ' | |
| 'a json file containing existing malioc results in json files' | |
| ), | |
| ) | |
| parser.add_argument( | |
| '--print-diff', | |
| '-p', | |
| default=False, | |
| action='store_true', | |
| help='Print a unified diff to stdout when differences are found.', | |
| ) | |
| parser.add_argument( | |
| '--update', | |
| '-u', | |
| default=False, | |
| action='store_true', | |
| help='Write results from the --after tree to the --before file.', | |
| ) | |
| parser.add_argument( | |
| '--verbose', | |
| '-v', | |
| default=False, | |
| action='store_true', | |
| help='Emit verbose output.', | |
| ) | |
| return parser.parse_args(argv) | |
| def validate_args(args): | |
| if not args.after and not args.after_relative_to_src: | |
| print('--after argument or --after-relative-to-src must be specified.') | |
| return False | |
| if not args.before and not args.before_relative_to_src: | |
| print('--before argument or --before-relative-to-src must be specified.') | |
| return False | |
| # Generate full paths if relative ones are provided with before and | |
| # after taking precedence. | |
| args.before = (args.before or os.path.join(BUILD_ROOT_DIR, args.before_relative_to_src)) | |
| args.after = (args.after or os.path.join(BUILD_ROOT_DIR, args.after_relative_to_src)) | |
| if not args.after or not os.path.isdir(args.after): | |
| print('The --after argument must refer to a directory.') | |
| return False | |
| if not args.before or (not args.update and not os.path.isfile(args.before)): | |
| print('The --before argument must refer to an existing file.') | |
| return False | |
| return True | |
| # Reads the 'performance' section of the malioc analysis results. | |
| def read_malioc_file_performance(performance_json): | |
| performance = {} | |
| performance['pipelines'] = performance_json['pipelines'] | |
| longest_path_cycles = performance_json['longest_path_cycles'] | |
| performance['longest_path_cycles'] = longest_path_cycles['cycle_count'] | |
| performance['longest_path_bound_pipelines'] = longest_path_cycles['bound_pipelines'] | |
| shortest_path_cycles = performance_json['shortest_path_cycles'] | |
| performance['shortest_path_cycles'] = shortest_path_cycles['cycle_count'] | |
| performance['shortest_path_bound_pipelines'] = shortest_path_cycles['bound_pipelines'] | |
| total_cycles = performance_json['total_cycles'] | |
| performance['total_cycles'] = total_cycles['cycle_count'] | |
| performance['total_bound_pipelines'] = total_cycles['bound_pipelines'] | |
| return performance | |
| # Parses the json output from malioc, which follows the schema defined in | |
| # `mali_offline_compiler/samples/json_schemas/performance-schema.json`. | |
| def read_malioc_file(malioc_tree, json_file): | |
| with open(json_file, 'r') as file: | |
| json_obj = json.load(file) | |
| build_gen_dir = os.path.dirname(malioc_tree) | |
| results = [] | |
| for shader in json_obj['shaders']: | |
| # Ignore cores not in the allowlist above. | |
| if shader['hardware']['core'] not in CORES: | |
| continue | |
| result = {} | |
| filename = os.path.relpath(shader['filename'], build_gen_dir) | |
| if filename.startswith('../..'): | |
| filename = filename[6:] | |
| if filename.startswith('../'): | |
| filename = filename[3:] | |
| result['filename'] = filename | |
| result['core'] = shader['hardware']['core'] | |
| result['type'] = shader['shader']['type'] | |
| for prop in shader['properties']: | |
| result[prop['name']] = prop['value'] | |
| result['variants'] = {} | |
| for variant in shader['variants']: | |
| variant_result = {} | |
| for prop in variant['properties']: | |
| variant_result[prop['name']] = prop['value'] | |
| performance_json = variant['performance'] | |
| performance = read_malioc_file_performance(performance_json) | |
| variant_result['performance'] = performance | |
| result['variants'][variant['name']] = variant_result | |
| results.append(result) | |
| return results | |
| # Parses a tree of malioc performance json files. | |
| # | |
| # The parsing results are returned in a map keyed by the shader file name, whose | |
| # values are maps keyed by the core type. The values in these maps are the | |
| # performance properties of the shader on the core reported by malioc. This | |
| # structure allows for a fast lookup and comparison against the golen file. | |
| def read_malioc_tree(malioc_tree): | |
| results = {} | |
| for root, _, files in os.walk(malioc_tree): | |
| for file in files: | |
| if not file.endswith('.json'): | |
| continue | |
| full_path = os.path.join(root, file) | |
| for shader in read_malioc_file(malioc_tree, full_path): | |
| if shader['filename'] not in results: | |
| results[shader['filename']] = {} | |
| results[shader['filename']][shader['core']] = shader | |
| return results | |
| # Converts a list to a string in which each list element is left-aligned in | |
| # a space of `width` characters, and separated by `sep`. The separator does not | |
| # count against the `width`. If `width` is 0, then the width is unconstrained. | |
| def pretty_list(lst, fmt='s', sep='', width=12): | |
| formats = ['{:<{width}{fmt}}' if ele is not None else '{:<{width}s}' for ele in lst] | |
| sanitized_list = [x if x is not None else 'null' for x in lst] | |
| return (sep.join(formats)).format(width='' if width == 0 else width, fmt=fmt, *sanitized_list) | |
| def compare_performance(variant, before, after): | |
| cycles = [['longest_path_cycles', 'longest_path_bound_pipelines'], | |
| ['shortest_path_cycles', 'shortest_path_bound_pipelines'], | |
| ['total_cycles', 'total_bound_pipelines']] | |
| differences = [] | |
| for cycle in cycles: | |
| if before[cycle[0]] == after[cycle[0]]: | |
| continue | |
| before_cycles = before[cycle[0]] | |
| before_bounds = before[cycle[1]] | |
| after_cycles = after[cycle[0]] | |
| after_bounds = after[cycle[1]] | |
| differences += [ | |
| '{} in variant {}\n{}{}\n{:<8}{}{}\n{:<8}{}{}\n'.format( | |
| cycle[0], | |
| variant, | |
| ' ' * 8, | |
| pretty_list(before['pipelines'] + ['bound']), # Column labels. | |
| 'before', | |
| pretty_list(before_cycles, fmt='f'), | |
| pretty_list(before_bounds, sep=',', width=0), | |
| 'after', | |
| pretty_list(after_cycles, fmt='f'), | |
| pretty_list(after_bounds, sep=',', width=0), | |
| ) | |
| ] | |
| return differences | |
| def compare_variants(befores, afters): | |
| differences = [] | |
| for variant_name, before_variant in befores.items(): | |
| after_variant = afters[variant_name] | |
| for variant_key, before_variant_val in before_variant.items(): | |
| after_variant_val = after_variant[variant_key] | |
| if variant_key == 'performance': | |
| differences += compare_performance(variant_name, before_variant_val, after_variant_val) | |
| elif before_variant_val != after_variant_val: | |
| differences += [ | |
| 'In variant {}:\n {vkey}: {} <- before\n {vkey}: {} <- after'.format( | |
| variant_name, | |
| before_variant_val, | |
| after_variant_val, | |
| vkey=variant_key, | |
| ) | |
| ] | |
| return differences | |
| # Compares two shaders. Prints a report and returns True if there are | |
| # differences, and returns False otherwise. | |
| def compare_shaders(malioc_tree, before_shader, after_shader): | |
| differences = [] | |
| for key, before_val in before_shader.items(): | |
| after_val = after_shader[key] | |
| if key == 'variants': | |
| differences += compare_variants(before_val, after_val) | |
| elif key == 'performance': | |
| differences += compare_performance('Default', before_val, after_val) | |
| elif before_val != after_val: | |
| differences += ['{}:\n {} <- before\n {} <- after'.format(key, before_val, after_val)] | |
| if bool(differences): | |
| build_gen_dir = os.path.dirname(malioc_tree) | |
| filename = before_shader['filename'] | |
| core = before_shader['core'] | |
| typ = before_shader['type'] | |
| print('Changes found in shader {} on core {}:'.format(filename, core)) | |
| for diff in differences: | |
| print(diff) | |
| print( | |
| '\nFor a full report, run:\n $ malioc --{} --core {} {}/{}\n'.format( | |
| typ.lower(), core, build_gen_dir, filename | |
| ) | |
| ) | |
| return bool(differences) | |
| def main(argv): | |
| args = parse_args(argv[1:]) | |
| if not validate_args(args): | |
| return 1 | |
| after_json = read_malioc_tree(args.after) | |
| if not bool(after_json): | |
| print('Did not find any malioc results under {}.'.format(args.after)) | |
| return 1 | |
| if args.update: | |
| # Write the new results to the file given by --before, then exit. | |
| with open(args.before, 'w') as file: | |
| json.dump(after_json, file, sort_keys=True, indent=2) | |
| return 0 | |
| with open(args.before, 'r') as file: | |
| before_json = json.load(file) | |
| changed = False | |
| for filename, shaders in before_json.items(): | |
| if filename not in after_json.keys(): | |
| print('Shader "{}" has been removed.'.format(filename)) | |
| changed = True | |
| continue | |
| for core, before_shader in shaders.items(): | |
| if core not in after_json[filename].keys(): | |
| continue | |
| after_shader = after_json[filename][core] | |
| if compare_shaders(args.after, before_shader, after_shader): | |
| changed = True | |
| for filename, shaders in after_json.items(): | |
| if filename not in before_json: | |
| print('Shader "{}" is new.'.format(filename)) | |
| changed = True | |
| if changed: | |
| print( | |
| 'There are new shaders, shaders have been removed, or performance ' | |
| 'changes to existing shaders. The golden file must be updated after a ' | |
| 'build of android_debug_unopt using the --malioc-path flag to the ' | |
| 'flutter/tools/gn script.\n\n' | |
| '$ ./flutter/impeller/tools/malioc_diff.py --before {} --after {} --update'.format( | |
| args.before, args.after | |
| ) | |
| ) | |
| if args.print_diff: | |
| before_lines = json.dumps(before_json, sort_keys=True, indent=2).splitlines(keepends=True) | |
| after_lines = json.dumps(after_json, sort_keys=True, indent=2).splitlines(keepends=True) | |
| before_path = os.path.relpath(os.path.abspath(args.before), start=SRC_ROOT) | |
| diff = difflib.unified_diff(before_lines, after_lines, fromfile=before_path) | |
| print('\nYou can alternately apply the diff below:') | |
| print('patch -p0 <<DONE') | |
| print(*diff, sep='') | |
| print('DONE') | |
| return 1 if changed else 0 | |
| if __name__ == '__main__': | |
| sys.exit(main(sys.argv)) |