mirror of
https://github.com/saymrwulf/onnxruntime.git
synced 2026-07-27 20:02:15 +00:00
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:
parent
5d9552cc8b
commit
e54e2f969d
5 changed files with 192 additions and 23 deletions
20
tools/ci_build/build.py
Executable file → Normal file
20
tools/ci_build/build.py
Executable file → Normal 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):
|
||||
|
|
|
|||
0
tools/ci_build/github/linux/ort_minimal/__init__.py
Normal file
0
tools/ci_build/github/linux/ort_minimal/__init__.py
Normal 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 \
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
119
tools/ci_build/github/linux/ort_minimal/readelf_utils.py
Normal file
119
tools/ci_build/github/linux/ort_minimal/readelf_utils.py
Normal 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()
|
||||
Loading…
Reference in a new issue