diff --git a/ci/licenses_golden/excluded_files b/ci/licenses_golden/excluded_files index 97322a42fa8fa..f857f386b7942 100644 --- a/ci/licenses_golden/excluded_files +++ b/ci/licenses_golden/excluded_files @@ -149,6 +149,7 @@ ../../../flutter/impeller/tessellator/tessellator_unittests.cc ../../../flutter/impeller/tools/build_metal_library.py ../../../flutter/impeller/tools/check_licenses.py +../../../flutter/impeller/tools/malioc_diff.py ../../../flutter/impeller/tools/xxd.py ../../../flutter/impeller/typographer/typographer_unittests.cc ../../../flutter/lib/snapshot/libraries.json diff --git a/impeller/fixtures/BUILD.gn b/impeller/fixtures/BUILD.gn index f91c66ef7f11b..6ff7eeddc2c13 100644 --- a/impeller/fixtures/BUILD.gn +++ b/impeller/fixtures/BUILD.gn @@ -7,6 +7,10 @@ import("//flutter/testing/testing.gni") impeller_shaders("shader_fixtures") { name = "fixtures" + + # Not analyzing because they are not performance critical, and mipmap uses + # textureLod, which uses an extension that malioc does not support. + analyze = false shaders = [ "array.frag", "array.vert", diff --git a/impeller/playground/imgui/BUILD.gn b/impeller/playground/imgui/BUILD.gn index 3b0c53b10acd2..497bb5c199c1d 100644 --- a/impeller/playground/imgui/BUILD.gn +++ b/impeller/playground/imgui/BUILD.gn @@ -6,6 +6,10 @@ import("//flutter/impeller/tools/impeller.gni") impeller_shaders("imgui_shaders") { name = "imgui" + + # Not analyzing because they are not performance critical, and mipmap uses + # textureLod, which uses an extension that malioc does not support. + analyze = false shaders = [ "imgui_raster.vert", "imgui_raster.frag", diff --git a/impeller/tools/BUILD.gn b/impeller/tools/BUILD.gn new file mode 100644 index 0000000000000..a8bf1e0068548 --- /dev/null +++ b/impeller/tools/BUILD.gn @@ -0,0 +1,31 @@ +# 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("//build/compiled_action.gni") +import("//flutter/common/config.gni") +import("//flutter/impeller/tools/malioc.gni") +import("//flutter/testing/testing.gni") + +declare_args() { + # Maximum number of malioc processes to run in parallel. + # + # To avoid out-of-memory errors we explicitly reduce the number of jobs. + impeller_concurrent_malioc_jobs = -1 +} + +if (impeller_concurrent_malioc_jobs == -1) { + _script = "//flutter/build/get_concurrent_jobs.py" + _args = [ + "--reserve-memory=1GB", + "--memory-per-job", + "malioc=100MB", + ] + _concurrent_jobs = exec_script(_script, _args, "json", [ _script ]) + impeller_concurrent_malioc_jobs = _concurrent_jobs.malioc + assert(impeller_concurrent_malioc_jobs > 0) +} + +pool("malioc_pool") { + depth = impeller_concurrent_malioc_jobs +} diff --git a/impeller/tools/impeller.gni b/impeller/tools/impeller.gni index e2c35817d64df..5a4c695494c03 100644 --- a/impeller/tools/impeller.gni +++ b/impeller/tools/impeller.gni @@ -4,6 +4,7 @@ import("//build/compiled_action.gni") import("//flutter/common/config.gni") +import("//flutter/impeller/tools/malioc.gni") import("//flutter/testing/testing.gni") declare_args() { @@ -467,6 +468,7 @@ template("blobcat_library") { template("impeller_shaders_gles") { assert(defined(invoker.shaders), "Impeller shaders must be specified.") assert(defined(invoker.name), "Name of the shader library must be specified.") + assert(defined(invoker.analyze), "Whether to analyze must be specified.") shaders_base_name = string_join("", [ @@ -494,10 +496,23 @@ template("impeller_shaders_gles") { defines = [ "IMPELLER_TARGET_OPENGLES" ] } + gles_shaders = + filter_include(get_target_outputs(":$impellerc_gles"), [ "*.gles" ]) + + if (invoker.analyze) { + analyze_lib = "analyze_$target_name" + malioc_analyze_shaders(analyze_lib) { + shaders = gles_shaders + if (defined(invoker.gles_language_version)) { + gles_language_version = invoker.gles_language_version + } + deps = [ ":$impellerc_gles" ] + } + } + gles_lib = "genlib_$target_name" blobcat_library(gles_lib) { - shaders = - filter_include(get_target_outputs(":$impellerc_gles"), [ "*.gles" ]) + shaders = gles_shaders deps = [ ":$impellerc_gles" ] } @@ -519,6 +534,10 @@ template("impeller_shaders_gles") { group(target_name) { public_deps = [ ":$embed_gles_lib" ] + if (invoker.analyze) { + public_deps += [ ":$analyze_lib" ] + } + if (!impeller_enable_metal && !impeller_enable_vulkan) { public_deps += [ ":$reflect_gles" ] } @@ -589,6 +608,10 @@ template("impeller_shaders") { } if (impeller_enable_opengles) { + analyze = true + if (defined(invoker.analyze) && !invoker.analyze) { + analyze = false + } gles_shaders = "gles_$target_name" impeller_shaders_gles(gles_shaders) { name = invoker.name @@ -600,6 +623,7 @@ template("impeller_shaders") { } else { shaders = invoker.shaders } + analyze = analyze } } diff --git a/impeller/tools/malioc.gni b/impeller/tools/malioc.gni new file mode 100644 index 0000000000000..17e845637b892 --- /dev/null +++ b/impeller/tools/malioc.gni @@ -0,0 +1,124 @@ +# 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("//build/compiled_action.gni") +import("//flutter/common/config.gni") +import("//flutter/testing/testing.gni") + +declare_args() { + # Path to the Mali offline compiler tool 'malioc'. + impeller_malioc_path = "" + + impeller_malioc_cores = [] +} + +if (impeller_malioc_path != "" && impeller_malioc_cores == []) { + core_list_file = "$root_gen_dir/mali_core_list.json" + exec_script("//build/gn_run_binary.py", + [ + rebase_path(impeller_malioc_path, root_build_dir), + "--list", + "--format", + "json", + "--output", + rebase_path(core_list_file), + ]) + _mali_cores = read_file(core_list_file, "json") + foreach(mali_core, _mali_cores.cores) { + impeller_malioc_cores += [ mali_core.core ] + } +} + +template("malioc_analyze_shaders") { + # TODO(zra): Check that gles_language_version is in the supported set. For now + # assume that if it is set, it is being set to 460, which malioc does not + # support. + if (impeller_malioc_path == "" || defined(invoker.gles_language_version)) { + if (defined(invoker.gles_language_version) && + invoker.gles_language_version != "460") { + print("Disabling analysis for shaders in $target_name due to gles", + "version explicitly set to ${invoker.gles_language_version}.") + } + group(target_name) { + not_needed(invoker, "*") + } + } else { + target_deps = [] + foreach(core, impeller_malioc_cores) { + foreach(source, invoker.shaders) { + shader_file_name = get_path_info(source, "name") + analysis_target = "${target_name}_${shader_file_name}_${core}_malioc" + target_deps += [ ":$analysis_target" ] + action(analysis_target) { + forward_variables_from(invoker, + "*", + [ + "args", + "depfile", + "inputs", + "outputs", + "pool", + "script", + ]) + + script = "//build/gn_run_binary.py" + pool = "//flutter/impeller/tools:malioc_pool" + + # Should be "gles" or "vkspv" + backend_ext = get_path_info(source, "extension") + assert(backend_ext == "gles", + "Shader for unsupported backend passed to malioc: {{source}}") + + # Nest all malioc output under its own subdirectory of root_gen_dir + # so that it's easier to diff it against the state before any changes. + subdir = rebase_path(target_gen_dir, root_gen_dir) + output_file = + "$root_gen_dir/malioc/$subdir/${shader_file_name}.$core.json" + outputs = [ output_file ] + + # Determine the kind of the shader from the file name + name = get_path_info(source, "name") + shader_kind_ext = get_path_info(name, "extension") + + if (shader_kind_ext == "comp") { + shader_kind_flag = "--compute" + } else if (shader_kind_ext == "frag") { + shader_kind_flag = "--fragment" + } else if (shader_kind_ext == "geom") { + shader_kind_flag = "--geometry" + } else if (shader_kind_ext == "tesc") { + shader_kind_flag = "--tessellation_control" + } else if (shader_kind_ext == "tese") { + shader_kind_flag = "--tessellation_evaluation" + } else if (shader_kind_ext == "vert") { + shader_kind_flag = "--vertex" + } else { + assert(false, "Unknown shader kind: {{source}}") + } + + args = [ + rebase_path(impeller_malioc_path, root_build_dir), + "--format", + "json", + shader_kind_flag, + "--core", + core, + "--output", + rebase_path(output_file), + ] + + if (backend_ext == "vkspv") { + args += [ "--vulkan" ] + } + + args += [ rebase_path(source) ] + } + } + } + + group(target_name) { + deps = target_deps + } + } +} diff --git a/impeller/tools/malioc_diff.py b/impeller/tools/malioc_diff.py new file mode 100755 index 0000000000000..6e8b7bd44eb6c --- /dev/null +++ b/impeller/tools/malioc_diff.py @@ -0,0 +1,219 @@ +#!/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 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. + + +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( + '--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 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 + + +# 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']: + result = {} + result['filename'] = os.path.relpath(shader['filename'], build_gen_dir) + 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 = variant['performance'] + variant_result['pipelines'] = performance['pipelines'] + variant_result['longest_path_cycles'] = performance['longest_path_cycles' + ]['cycle_count'] + variant_result['shortest_path_cycles'] = performance[ + 'shortest_path_cycles']['cycle_count'] + variant_result['total_cycles'] = performance['total_cycles']['cycle_count' + ] + 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 + + +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 before_variant_val != after_variant_val: + differences += [ + '{} in variant {}:\n {} <- before\n {} <- after'.format( + variant_key, variant_name, before_variant_val, after_variant_val + ) + ] + return differences + + +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 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 {} {}/{}'.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) + return 0 + + with open(args.before, 'r') as file: + before_json = json.load(file) + + changed = False + for filename, shaders in before_json.items(): + for core, before_shader in shaders.items(): + 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. Run with --update to update checked-in results' + .format(filename) + ) + changed = True + + return 1 if changed else 0 + + +if __name__ == '__main__': + sys.exit(main(sys.argv)) diff --git a/tools/gn b/tools/gn index 21e2c2af0e6c0..663f453e60923 100755 --- a/tools/gn +++ b/tools/gn @@ -557,6 +557,9 @@ def to_gn_args(args): if args.prebuilt_impellerc is not None: gn_args['impeller_use_prebuilt_impellerc'] = args.prebuilt_impellerc + if args.malioc_path is not None: + gn_args['impeller_malioc_path'] = args.malioc_path + # ANGLE is exclusively used for: # - Windows at runtime # - Non-fuchsia host unit tests (is_host_build evaluates to false). @@ -980,6 +983,10 @@ def parse_args(args): help='Enables experimental 3d support.' ) + parser.add_argument( + '--malioc-path', type=str, help='The path to the malioc tool.' + ) + # Sanitizers. parser.add_argument('--asan', default=False, action='store_true') parser.add_argument('--lsan', default=False, action='store_true')