Use readelf for minimal build binary size checks. (#6338)

* Use readelf for minimal build binary size checks.
The on-disk size grows in 4KB chunks which makes it hard to see how much growth an individual checkin causes.
Only downside is that the sum of the sections is larger than the on-disk size (assumably things get packed smaller on disk and some of the section alignment constraints can be ignored)

* Remove unused function
This commit is contained in:
Scott McKay 2021-01-15 07:46:02 +10:00 committed by GitHub
parent 5d9552cc8b
commit e54e2f969d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 192 additions and 23 deletions

20
tools/ci_build/build.py Executable file → Normal file
View file

@ -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):

View file

@ -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 \

View file

@ -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()

View file

@ -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()