Flutter Engine
The Flutter Engine
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 if variant_name in afters:
250 after_variant = afters[variant_name]
251 for variant_key, before_variant_val in before_variant.items():
252 after_variant_val = after_variant[variant_key]
253 if variant_key == 'performance':
254 differences += compare_performance(variant_name, before_variant_val, after_variant_val)
255 elif before_variant_val != after_variant_val:
256 differences += [
257 'In variant {}:\n {vkey}: {} <- before\n {vkey}: {} <- after'.format(
258 variant_name,
259 before_variant_val,
260 after_variant_val,
261 vkey=variant_key,
262 )
263 ]
264 return differences
265
266
267# Compares two shaders. Prints a report and returns True if there are
268# differences, and returns False otherwise.
269def compare_shaders(malioc_tree, before_shader, after_shader):
270 differences = []
271 for key, before_val in before_shader.items():
272 after_val = after_shader[key]
273 if key == 'variants':
274 differences += compare_variants(before_val, after_val)
275 elif key == 'performance':
276 differences += compare_performance('Default', before_val, after_val)
277 elif before_val != after_val:
278 differences += ['{}:\n {} <- before\n {} <- after'.format(key, before_val, after_val)]
279
280 if bool(differences):
281 build_gen_dir = os.path.dirname(malioc_tree)
282 filename = before_shader['filename']
283 core = before_shader['core']
284 typ = before_shader['type']
285 print('Changes found in shader {} on core {}:'.format(filename, core))
286 for diff in differences:
287 print(diff)
288 print(
289 '\nFor a full report, run:\n $ malioc --{} --core {} {}/{}\n'.format(
290 typ.lower(), core, build_gen_dir, filename
291 )
292 )
293
294 return bool(differences)
295
296
297def main(argv):
298 args = parse_args(argv[1:])
299 if not validate_args(args):
300 return 1
301
302 after_json = read_malioc_tree(args.after)
303 if not bool(after_json):
304 print('Did not find any malioc results under {}.'.format(args.after))
305 return 1
306
307 if args.update:
308 # Write the new results to the file given by --before, then exit.
309 with open(args.before, 'w') as file:
310 json.dump(after_json, file, sort_keys=True, indent=2)
311 return 0
312
313 with open(args.before, 'r') as file:
314 before_json = json.load(file)
315
316 changed = False
317 for filename, shaders in before_json.items():
318 if filename not in after_json.keys():
319 print('Shader "{}" has been removed.'.format(filename))
320 changed = True
321 continue
322 for core, before_shader in shaders.items():
323 if core not in after_json[filename].keys():
324 continue
325 after_shader = after_json[filename][core]
326 if compare_shaders(args.after, before_shader, after_shader):
327 changed = True
328
329 for filename, shaders in after_json.items():
330 if filename not in before_json:
331 print('Shader "{}" is new.'.format(filename))
332 changed = True
333
334 if changed:
335 print(
336 'There are new shaders, shaders have been removed, or performance '
337 'changes to existing shaders. The golden file must be updated after a '
338 'build of android_debug_unopt using the --malioc-path flag to the '
339 'flutter/tools/gn script.\n\n'
340 '$ ./flutter/impeller/tools/malioc_diff.py --before {} --after {} --update'.format(
341 args.before, args.after
342 )
343 )
344 if args.print_diff:
345 before_lines = json.dumps(before_json, sort_keys=True, indent=2).splitlines(keepends=True)
346 after_lines = json.dumps(after_json, sort_keys=True, indent=2).splitlines(keepends=True)
347 before_path = os.path.relpath(os.path.abspath(args.before), start=SRC_ROOT)
348 diff = difflib.unified_diff(before_lines, after_lines, fromfile=before_path)
349 print('\nYou can alternately apply the diff below:')
350 print('patch -p0 <<DONE')
351 print(*diff, sep='')
352 print('DONE')
353
354 return 1 if changed else 0
355
356
357if __name__ == '__main__':
358 sys.exit(main(sys.argv))
uint32_t uint32_t * format
Definition: main.py:1
def read_malioc_file(malioc_tree, json_file)
Definition: malioc_diff.py:150
def compare_performance(variant, before, after)
Definition: malioc_diff.py:217
def read_malioc_tree(malioc_tree)
Definition: malioc_diff.py:194
def compare_variants(befores, afters)
Definition: malioc_diff.py:246
def validate_args(args)
Definition: malioc_diff.py:106
def pretty_list(lst, fmt='s', sep='', width=12)
Definition: malioc_diff.py:211
def main(argv)
Definition: malioc_diff.py:297
def read_malioc_file_performance(performance_json)
Definition: malioc_diff.py:130
def compare_shaders(malioc_tree, before_shader, after_shader)
Definition: malioc_diff.py:269
def parse_args(argv)
Definition: malioc_diff.py:50
def print(*args, **kwargs)
Definition: run_tests.py:49