Flutter Engine
The Flutter Engine
Loading...
Searching...
No Matches
malioc_diff.py
Go to the documentation of this file.
1#!/usr/bin/env vpython3
2# Copyright 2013 The Flutter Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6import argparse
7import difflib
8import json
9import os
10import sys
11
12# This script detects performance impacting changes to shaders.
13#
14# When the GN build is configured with the path to the `malioc` tool, the
15# results of its analysis will be placed under `out/$CONFIG/gen/malioc` in
16# separate .json files. That path should be supplied to this script as the
17# `--after` argument. This script compares those results against previous
18# results in a golden file checked in to the tree under
19# `flutter/impeller/tools/malioc.json`. That file should be passed to this
20# script as the `--before` argument. To create or update the golden file,
21# passing the `--update` flag will cause the data from the `--after` path to
22# overwrite the file at the `--before` path.
23#
24# Configure and build:
25# $ flutter/tools/gn --malioc-path path/to/malioc
26# $ ninja -C out/host_debug
27#
28# Analyze
29# $ flutter/impeller/tools/malioc_diff.py \
30# --before flutter/impeller/tools/malioc.json \
31# --after out/host_debug/gen/malioc
32#
33# If there are differences between before and after, whether positive or
34# negative, the exit code for this script will be 1, and 0 otherwise.
35
36SRC_ROOT = os.path.dirname(
37 os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
38)
39
40CORES = [
41 'Mali-G78', # Pixel 6 / 2020
42 'Mali-T880', # 2016
43]
44
45# Path to the engine root checkout. This is used to calculate absolute
46# paths if relative ones are passed to the script.
47BUILD_ROOT_DIR = os.path.abspath(os.path.join(os.path.realpath(__file__), '..', '..', '..', '..'))
48
49
50def parse_args(argv):
51 parser = argparse.ArgumentParser(
52 description='A script that compares before/after malioc analysis results',
53 )
54 parser.add_argument(
55 '--after',
56 '-a',
57 type=str,
58 help='The path to a directory tree containing new malioc results in json files.',
59 )
60 parser.add_argument(
61 '--before',
62 '-b',
63 type=str,
64 help='The path to a json file containing existing malioc results.',
65 )
66 parser.add_argument(
67 '--after-relative-to-src',
68 type=str,
69 help=(
70 'A relative path calculated from the engine src directory to '
71 'a directory tree containing new malioc results in json files'
72 ),
73 )
74 parser.add_argument(
75 '--before-relative-to-src',
76 type=str,
77 help=(
78 'A relative path calculated from the engine src directory to '
79 'a json file containing existing malioc results in json files'
80 ),
81 )
82 parser.add_argument(
83 '--print-diff',
84 '-p',
85 default=False,
86 action='store_true',
87 help='Print a unified diff to stdout when differences are found.',
88 )
89 parser.add_argument(
90 '--update',
91 '-u',
92 default=False,
93 action='store_true',
94 help='Write results from the --after tree to the --before file.',
95 )
96 parser.add_argument(
97 '--verbose',
98 '-v',
99 default=False,
100 action='store_true',
101 help='Emit verbose output.',
102 )
103 return parser.parse_args(argv)
104
105
107 if not args.after and not args.after_relative_to_src:
108 print('--after argument or --after-relative-to-src must be specified.')
109 return False
110
111 if not args.before and not args.before_relative_to_src:
112 print('--before argument or --before-relative-to-src must be specified.')
113 return False
114
115 # Generate full paths if relative ones are provided with before and
116 # after taking precedence.
117 args.before = (args.before or os.path.join(BUILD_ROOT_DIR, args.before_relative_to_src))
118 args.after = (args.after or os.path.join(BUILD_ROOT_DIR, args.after_relative_to_src))
119
120 if not args.after or not os.path.isdir(args.after):
121 print('The --after argument must refer to a directory.')
122 return False
123 if not args.before or (not args.update and not os.path.isfile(args.before)):
124 print('The --before argument must refer to an existing file.')
125 return False
126 return True
127
128
129# Reads the 'performance' section of the malioc analysis results.
130def read_malioc_file_performance(performance_json):
131 performance = {}
132 performance['pipelines'] = performance_json['pipelines']
133
134 longest_path_cycles = performance_json['longest_path_cycles']
135 performance['longest_path_cycles'] = longest_path_cycles['cycle_count']
136 performance['longest_path_bound_pipelines'] = longest_path_cycles['bound_pipelines']
137
138 shortest_path_cycles = performance_json['shortest_path_cycles']
139 performance['shortest_path_cycles'] = shortest_path_cycles['cycle_count']
140 performance['shortest_path_bound_pipelines'] = shortest_path_cycles['bound_pipelines']
141
142 total_cycles = performance_json['total_cycles']
143 performance['total_cycles'] = total_cycles['cycle_count']
144 performance['total_bound_pipelines'] = total_cycles['bound_pipelines']
145 return performance
146
147
148# Parses the json output from malioc, which follows the schema defined in
149# `mali_offline_compiler/samples/json_schemas/performance-schema.json`.
150def read_malioc_file(malioc_tree, json_file):
151 with open(json_file, 'r') as file:
152 json_obj = json.load(file)
153
154 build_gen_dir = os.path.dirname(malioc_tree)
155
156 results = []
157 for shader in json_obj['shaders']:
158 # Ignore cores not in the allowlist above.
159 if shader['hardware']['core'] not in CORES:
160 continue
161 result = {}
162 filename = os.path.relpath(shader['filename'], build_gen_dir)
163 if filename.startswith('../..'):
164 filename = filename[6:]
165 if filename.startswith('../'):
166 filename = filename[3:]
167 result['filename'] = filename
168 result['core'] = shader['hardware']['core']
169 result['type'] = shader['shader']['type']
170 for prop in shader['properties']:
171 result[prop['name']] = prop['value']
172
173 result['variants'] = {}
174 for variant in shader['variants']:
175 variant_result = {}
176 for prop in variant['properties']:
177 variant_result[prop['name']] = prop['value']
178
179 performance_json = variant['performance']
180 performance = read_malioc_file_performance(performance_json)
181 variant_result['performance'] = performance
182 result['variants'][variant['name']] = variant_result
183 results.append(result)
184
185 return results
186
187
188# Parses a tree of malioc performance json files.
189#
190# The parsing results are returned in a map keyed by the shader file name, whose
191# values are maps keyed by the core type. The values in these maps are the
192# performance properties of the shader on the core reported by malioc. This
193# structure allows for a fast lookup and comparison against the golen file.
194def read_malioc_tree(malioc_tree):
195 results = {}
196 for root, _, files in os.walk(malioc_tree):
197 for file in files:
198 if not file.endswith('.json'):
199 continue
200 full_path = os.path.join(root, file)
201 for shader in read_malioc_file(malioc_tree, full_path):
202 if shader['filename'] not in results:
203 results[shader['filename']] = {}
204 results[shader['filename']][shader['core']] = shader
205 return results
206
207
208# Converts a list to a string in which each list element is left-aligned in
209# a space of `width` characters, and separated by `sep`. The separator does not
210# count against the `width`. If `width` is 0, then the width is unconstrained.
211def pretty_list(lst, fmt='s', sep='', width=12):
212 formats = ['{:<{width}{fmt}}' if ele is not None else '{:<{width}s}' for ele in lst]
213 sanitized_list = [x if x is not None else 'null' for x in lst]
214 return (sep.join(formats)).format(width='' if width == 0 else width, fmt=fmt, *sanitized_list)
215
216
217def compare_performance(variant, before, after):
218 cycles = [['longest_path_cycles', 'longest_path_bound_pipelines'],
219 ['shortest_path_cycles', 'shortest_path_bound_pipelines'],
220 ['total_cycles', 'total_bound_pipelines']]
221 differences = []
222 for cycle in cycles:
223 if before[cycle[0]] == after[cycle[0]]:
224 continue
225 before_cycles = before[cycle[0]]
226 before_bounds = before[cycle[1]]
227 after_cycles = after[cycle[0]]
228 after_bounds = after[cycle[1]]
229 differences += [
230 '{} in variant {}\n{}{}\n{:<8}{}{}\n{:<8}{}{}\n'.format(
231 cycle[0],
232 variant,
233 ' ' * 8,
234 pretty_list(before['pipelines'] + ['bound']), # Column labels.
235 'before',
236 pretty_list(before_cycles, fmt='f'),
237 pretty_list(before_bounds, sep=',', width=0),
238 'after',
239 pretty_list(after_cycles, fmt='f'),
240 pretty_list(after_bounds, sep=',', width=0),
241 )
242 ]
243 return differences
244
245
246def compare_variants(befores, afters):
247 differences = []
248 for variant_name, before_variant in befores.items():
249 after_variant = afters[variant_name]
250 for variant_key, before_variant_val in before_variant.items():
251 after_variant_val = after_variant[variant_key]
252 if variant_key == 'performance':
253 differences += compare_performance(variant_name, before_variant_val, after_variant_val)
254 elif before_variant_val != after_variant_val:
255 differences += [
256 'In variant {}:\n {vkey}: {} <- before\n {vkey}: {} <- after'.format(
257 variant_name,
258 before_variant_val,
259 after_variant_val,
260 vkey=variant_key,
261 )
262 ]
263 return differences
264
265
266# Compares two shaders. Prints a report and returns True if there are
267# differences, and returns False otherwise.
268def compare_shaders(malioc_tree, before_shader, after_shader):
269 differences = []
270 for key, before_val in before_shader.items():
271 after_val = after_shader[key]
272 if key == 'variants':
273 differences += compare_variants(before_val, after_val)
274 elif key == 'performance':
275 differences += compare_performance('Default', before_val, after_val)
276 elif before_val != after_val:
277 differences += ['{}:\n {} <- before\n {} <- after'.format(key, before_val, after_val)]
278
279 if bool(differences):
280 build_gen_dir = os.path.dirname(malioc_tree)
281 filename = before_shader['filename']
282 core = before_shader['core']
283 typ = before_shader['type']
284 print('Changes found in shader {} on core {}:'.format(filename, core))
285 for diff in differences:
286 print(diff)
287 print(
288 '\nFor a full report, run:\n $ malioc --{} --core {} {}/{}\n'.format(
289 typ.lower(), core, build_gen_dir, filename
290 )
291 )
292
293 return bool(differences)
294
295
296def main(argv):
297 args = parse_args(argv[1:])
298 if not validate_args(args):
299 return 1
300
301 after_json = read_malioc_tree(args.after)
302 if not bool(after_json):
303 print('Did not find any malioc results under {}.'.format(args.after))
304 return 1
305
306 if args.update:
307 # Write the new results to the file given by --before, then exit.
308 with open(args.before, 'w') as file:
309 json.dump(after_json, file, sort_keys=True, indent=2)
310 return 0
311
312 with open(args.before, 'r') as file:
313 before_json = json.load(file)
314
315 changed = False
316 for filename, shaders in before_json.items():
317 if filename not in after_json.keys():
318 print('Shader "{}" has been removed.'.format(filename))
319 changed = True
320 continue
321 for core, before_shader in shaders.items():
322 if core not in after_json[filename].keys():
323 continue
324 after_shader = after_json[filename][core]
325 if compare_shaders(args.after, before_shader, after_shader):
326 changed = True
327
328 for filename, shaders in after_json.items():
329 if filename not in before_json:
330 print('Shader "{}" is new.'.format(filename))
331 changed = True
332
333 if changed:
334 print(
335 'There are new shaders, shaders have been removed, or performance '
336 'changes to existing shaders. The golden file must be updated after a '
337 'build of android_debug_unopt using the --malioc-path flag to the '
338 'flutter/tools/gn script.\n\n'
339 '$ ./flutter/impeller/tools/malioc_diff.py --before {} --after {} --update'.format(
340 args.before, args.after
341 )
342 )
343 if args.print_diff:
344 before_lines = json.dumps(before_json, sort_keys=True, indent=2).splitlines(keepends=True)
345 after_lines = json.dumps(after_json, sort_keys=True, indent=2).splitlines(keepends=True)
346 before_path = os.path.relpath(os.path.abspath(args.before), start=SRC_ROOT)
347 diff = difflib.unified_diff(before_lines, after_lines, fromfile=before_path)
348 print('\nYou can alternately apply the diff below:')
349 print('patch -p0 <<DONE')
350 print(*diff, sep='')
351 print('DONE')
352
353 return 1 if changed else 0
354
355
356if __name__ == '__main__':
357 sys.exit(main(sys.argv))
void print(void *str)
Definition bridge.cpp:126
uint32_t uint32_t * format
Definition main.py:1
validate_args(args)
compare_shaders(malioc_tree, before_shader, after_shader)
read_malioc_file_performance(performance_json)
compare_performance(variant, before, after)
parse_args(argv)
pretty_list(lst, fmt='s', sep='', width=12)
read_malioc_file(malioc_tree, json_file)
compare_variants(befores, afters)
read_malioc_tree(malioc_tree)