mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
This was an early attempt to support batching that ended up not being important, as well as an attempted work around for high memory reads that ended being unrelated (it was blending) Since its not a performance improvement and just makes things more complicated, remove the feature.
359 lines
12 KiB
Python
Executable File
359 lines
12 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():
|
|
if variant_name in afters:
|
|
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))
|