diff --git a/tools/ci_build/build.py b/tools/ci_build/build.py old mode 100755 new mode 100644 index 98eb63758d..269acac1de --- a/tools/ci_build/build.py +++ b/tools/ci_build/build.py @@ -278,9 +278,6 @@ def parse_arguments(): help="Build with shared libc++ instead of the default static libc++.") parser.add_argument("--android_run_emulator", action="store_true", help="Start up an Android emulator if needed.") - parser.add_argument("--test_binary_size", action="store_true", - help="If enabled, build will fail when the built binary size is larger than the threshold. " - "This only applies to Android Minimal build for now.") parser.add_argument("--ios", action='store_true', help="build for ios") parser.add_argument( @@ -1187,23 +1184,6 @@ def run_android_tests(args, source_dir, config, cwd): run_adb_shell( 'export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:{0} && {0}/onnxruntime_shared_lib_test'.format( device_dir)) - elif args.android_abi == 'arm64-v8a': - # For Android arm64 abi we are only verify the size of the binary generated by minimal build config - # Will fail the build if the shared_lib size is larger than the threshold - if args.minimal_build and config == 'MinSizeRel' and args.build_shared_lib and args.test_binary_size: - # set current size limit to 1165KB which is 110K large than 1.5.2 release. - bin_size_threshold = 1165000 - bin_actual_size = os.path.getsize(os.path.join(cwd, 'libonnxruntime.so')) - log.info('Android arm64 minsizerel libonnxruntime.so size [' + str(bin_actual_size) + 'B]') - # Write the binary size to a file for uploading later - with open(os.path.join(cwd, 'binary_size_data.txt'), 'w') as file: - file.writelines([ - 'os,arch,build_config,size\n', - 'android,arm64-v8a,minimal-baseline,' + str(bin_actual_size) + '\n' - ]) - if bin_actual_size > bin_size_threshold: - raise BuildError('Android arm64 minsizerel libonnxruntime.so size [' + str(bin_actual_size) + - 'B] is bigger than threshold [' + str(bin_size_threshold) + 'B]') def run_ios_tests(args, source_dir, config, cwd): diff --git a/tools/ci_build/github/linux/ort_minimal/__init__.py b/tools/ci_build/github/linux/ort_minimal/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tools/ci_build/github/linux/ort_minimal/build_minimal_ort_android_baseline_and_report_bin_size.sh b/tools/ci_build/github/linux/ort_minimal/build_minimal_ort_android_baseline_and_report_bin_size.sh index be3fd48c73..34b3f34d7c 100644 --- a/tools/ci_build/github/linux/ort_minimal/build_minimal_ort_android_baseline_and_report_bin_size.sh +++ b/tools/ci_build/github/linux/ort_minimal/build_minimal_ort_android_baseline_and_report_bin_size.sh @@ -26,16 +26,20 @@ python3 /onnxruntime_src/tools/ci_build/build.py \ --build_java \ --disable_ml_ops \ --disable_exceptions \ - --test_binary_size \ --include_ops_by_config /home/onnxruntimedev/.test_data/include_no_operators.config -# Install the mysql connector -python3 -m pip install --user mysql-connector-python +# set current size limit to 1165KB. +python3 /onnxruntime_src/tools/ci_build/github/linux/ort_minimal/check_build_binary_size.py \ + --threshold=1165000 \ + /build/MinSizeRel/libonnxruntime.so # Post the binary size info to ort mysql DB # The report script's DB connection failure will not fail the pipeline # To reduce noise, we only report binary size for Continuous integration (a merge to master or rel-*) if [[ $BUILD_REASON == "IndividualCI" || $BUILD_REASON == "BatchedCI" ]]; then + # Install the mysql connector + python3 -m pip install --user mysql-connector-python + python3 /onnxruntime_src/tools/ci_build/github/windows/post_binary_sizes_to_dashboard.py \ --ignore_db_error \ --commit_hash=$BUILD_SOURCEVERSION \ diff --git a/tools/ci_build/github/linux/ort_minimal/check_build_binary_size.py b/tools/ci_build/github/linux/ort_minimal/check_build_binary_size.py new file mode 100644 index 0000000000..4fa40172fe --- /dev/null +++ b/tools/ci_build/github/linux/ort_minimal/check_build_binary_size.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import argparse +import os +import sys + +# local helpers +import readelf_utils + + +def _check_binary_size(path, readelf, threshold, os_str, arch, build_config): + + print('Checking binary size of {} using {}'.format(path, readelf)) + ondisk_size = os.path.getsize(path) + + print('Section:size in bytes') + # call get_section_sizes to dump the section info to stdout + sections = readelf_utils.get_section_sizes(path, readelf, sys.stdout) + sections_total = sum(sections.values()) + + print('Sections total={} bytes'.format(sections_total)) + print('File size={} bytes'.format(ondisk_size)) + + # Write the binary size to a file for uploading later + # On-disk binary size jumps in 4KB increments so we use the total of the sections as it has finer granularity. + # Note that the sum of the section is slightly larger than the on-disk size + # due to packing and/or alignment adjustments. + with open(os.path.join(os.path.dirname(path), 'binary_size_data.txt'), 'w') as file: + file.writelines([ + 'os,arch,build_config,size\n', + '{},{},{},{}\n'.format(os_str, arch, build_config, sections_total) + ]) + + if sections_total > threshold: + raise RuntimeError('Sections total size for {} of {} exceeds threshold of {} by {}. On-disk size={}' + .format(path, sections_total, threshold, sections_total - threshold, ondisk_size)) + + +def main(): + argparser = argparse.ArgumentParser(description='Check the binary size for provided path and ' + 'create a text file for upload to the performance dashboard.') + + # required + argparser.add_argument('-t', '--threshold', type=int, required=True, + help='Return error if binary size exceeds this threshold.') + # optional + argparser.add_argument('-r', '--readelf_path', type=str, default='readelf', help='Path to readelf executable.') + argparser.add_argument('--os', type=str, default='android', + help='OS value to include in binary_size_data.txt') + argparser.add_argument('--arch', type=str, default='arm64-v8a', + help='Arch value to include in binary_size_data.txt') + argparser.add_argument('--build_config', type=str, default='minimal-baseline', + help='Build_config value to include in binary_size_data.txt') + + # file to analyze + argparser.add_argument('path', type=os.path.realpath, help='Path to binary to check.') + + args = argparser.parse_args() + + _check_binary_size(args.path, args.readelf_path, args.threshold, args.os, args.arch, args.build_config) + + +if __name__ == '__main__': + main() diff --git a/tools/ci_build/github/linux/ort_minimal/readelf_utils.py b/tools/ci_build/github/linux/ort_minimal/readelf_utils.py new file mode 100644 index 0000000000..bc39fea215 --- /dev/null +++ b/tools/ci_build/github/linux/ort_minimal/readelf_utils.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python3 +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +''' +Utilities to help analyze the sections in a binary using readelf. +''' + +import argparse +import collections +import os +import re +import sys +import subprocess + + +def get_section_sizes(binary_path, readelf_path, dump_to_file=None): + ''' + Get the size of each section using readelf. + :param binary_path: Path to binary to analyze. + :param readelf_path: Path to readelf binary. Default is 'readelf'. + :param dump_to_file: File object to write section sizes and diagnostic info to. Defaults to None. + :return: + ''' + + cmd = [readelf_path, '--sections', '--wide', binary_path] + result = subprocess.run(cmd, stdout=subprocess.PIPE) + result.check_returncode() + output = result.stdout.decode('utf-8') + + section_sizes = {} + + # Parse output in this format: + # [Nr] Name Type Address Off Size ES Flg Lk Inf Al + for match in re.finditer(r'\[[\s\d]+\] (\..*)$', output, re.MULTILINE): + items = match.group(1).split() + name = items[0] + # convert size from hex to int + size = int(items[4], 16) + section_sizes[name] = size + + if dump_to_file: + print('{}:{}'.format(name, size), file=dump_to_file) + + return section_sizes + + +def diff_sections_total_size(base_binary_path, binary_path, readelf_path='readelf'): + ''' + Diff the sections entries for two binaries. + :param base_binary_path: Path to base binary for diff. + :param binary_path: Path to binary to diff using. + :param readelf_path: Path to 'readelf' binary. Defaults to 'readelf' + :return: Ordered dictionary containing size of diff for all sections with a diff, the diff for the sum of the + sections in the 'Sections total' entry, and the diff for the on-disk file size in the 'File size' entry + ''' + + filesize = os.path.getsize(binary_path) + base_filesize = os.path.getsize(base_binary_path) + + section_sizes = get_section_sizes(binary_path, readelf_path) + base_section_sizes = get_section_sizes(base_binary_path, readelf_path) + merged_keys = set(base_section_sizes.keys()) | set(section_sizes.keys()) + + base_total = 0 + total = 0 + results = collections.OrderedDict() + + for section in merged_keys: + base_size = base_section_sizes[section] if section in base_section_sizes else 0 + size = section_sizes[section] if section in section_sizes else 0 + + base_total += base_size + total += size + + if size != base_size: + results[section] = size - base_size + + results['Sections total'] = total - base_total + results['File size'] = filesize - base_filesize + + return results + + +def main(): + argparser = argparse.ArgumentParser( + description='Analyze sections in a binary using readelf. ' + 'Perform a diff between two binaries if --base_binary_path is specified.') + + argparser.add_argument('-r', '--readelf_path', type=str, + help='Path to readelf executable.') + argparser.add_argument('-b', '--base_binary_path', type=os.path.realpath, + default=None, help='Path to base binary if performing a diff between two binaries.') + argparser.add_argument('-w', '--write_to', type=str, default=None, + help='Path to write output to. Writes to stdout if not provided.') + argparser.add_argument('binary_path', type=os.path.realpath, + help='Shared library to analyze.') + + args = argparser.parse_args() + + out_file = sys.stdout + if args.write_to: + out_file = open(args.write_to, 'w') + + if args.base_binary_path: + diffs = diff_sections_total_size(args.base_binary_path, args.binary_path, args.readelf_path) + for key, value in diffs.items(): + print('{}:{}'.format(key, value), file=out_file) + else: + section_sizes = get_section_sizes(args.binary_path, args.readelf_path, out_file) + filesize = os.path.getsize(args.binary_path) + print('Sections total:{}'.format(sum(section_sizes.values())), file=out_file) + print('File size:{}'.format(filesize), file=out_file) + + if args.write_to: + out_file.close() + + +if __name__ == '__main__': + main()