mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Relative paths are required for integrating this tool as a test in engine v2. [C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style
386 lines
13 KiB
Python
Executable File
386 lines
13 KiB
Python
Executable File
#!/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))
|