aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xcompilation_stats.py101
-rwxr-xr-xrun.py5
-rwxr-xr-xtools/benchmarks/run.py45
-rwxr-xr-xtools/compilation_statistics/run.py315
-rw-r--r--tools/utils.py43
5 files changed, 179 insertions, 330 deletions
diff --git a/compilation_stats.py b/compilation_stats.py
new file mode 100755
index 0000000..78dea5f
--- /dev/null
+++ b/compilation_stats.py
@@ -0,0 +1,101 @@
+#! /usr/bin/env python3
+
+# Copyright (C) 2021 Linaro Limited. All rights received.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import argparse
+import os
+import sys
+import json
+import tempfile
+import shutil
+
+from collections import OrderedDict
+
+dir_benchs = os.path.dirname(os.path.realpath(__file__))
+dir_tools = os.path.join(dir_benchs, '..')
+sys.path.insert(0, dir_tools)
+
+from tools import utils, utils_adb, utils_print, utils_stats
+
+def BuildOptions():
+ parser = argparse.ArgumentParser(
+ description = "Collect compilation statistics.",
+ # Print default values.
+ formatter_class = argparse.ArgumentDefaultsHelpFormatter)
+ utils.AddCommonRunOptions(parser)
+ utils.AddOutputFormatOptions(parser, utils.default_output_formats)
+ args = parser.parse_args()
+ return args
+
+def SaveAndPrintResults(apk,
+ compilation_times,
+ section_sizes,
+ output_json_filename):
+ output_obj = utils.ReadJSON(output_json_filename)
+
+ apk_basename = os.path.basename(apk)
+ output_obj[apk_basename] = dict()
+
+ output_obj[apk_basename].update(compilation_times)
+ print("Compilation times (seconds):")
+ utils.PrintData(compilation_times)
+
+ output_obj[apk_basename]["Executable size"] = section_sizes
+ print("Executable sizes (bytes):")
+ utils.PrintData(section_sizes)
+
+ with open(output_json_filename, "w") as fp:
+ json.dump(output_obj, fp, indent=2)
+
+if __name__ == "__main__":
+ # create temp directory to pull executables from the device
+ # to measure their size
+ work_dir = tempfile.mkdtemp()
+ try:
+ args = BuildOptions()
+ apk = args.add_pathname[0]
+ apk_name = utils.TargetPathJoin(args.target_copy_path, apk)
+ # command is used to call a shell script using chroot
+ # this script calls dex2oat on a given APK and prints
+ # before/after timestamps
+ # after command is executed we pull the executable from the device
+ # and measure its size
+ if 'ART_COMMAND' in os.environ:
+ command = os.getenv('ART_COMMAND')
+ else:
+ utils.Error("ART_COMMAND is not set.")
+ format_data = {'workdir': os.path.dirname(apk_name)}
+ command = command.format(**format_data)
+
+ compilation_times = []
+ for i in range(args.iterations):
+ print("Compiling APK")
+ results = utils_adb.shell(command, args.target, exit_on_error=False)
+ lines = results[1]
+ compilation_time = utils.ExtractCompilationTimeFromOutput(lines)
+ compilation_times += [compilation_time]
+ print("Compilation took {:.2f}s\n".format(compilation_time))
+
+ # Pull the executable and get its size
+ local_oat = os.path.join(work_dir, apk + '.oat')
+ utils_adb.pull(args.output_oat, local_oat, args.target)
+ section_sizes = utils.GetSectionSizes(local_oat)
+
+ compile_time_dict = OrderedDict([("Time", compilation_times)])
+ SaveAndPrintResults(apk, compile_time_dict, section_sizes, args.output_json)
+
+ finally:
+ shutil.rmtree(work_dir)
+
diff --git a/run.py b/run.py
index e32374d..9256664 100755
--- a/run.py
+++ b/run.py
@@ -23,7 +23,6 @@ from collections import OrderedDict
from tools import utils
from tools import utils_stats
from tools.benchmarks.run import GetAndPrintBenchmarkResults
-from tools.compilation_statistics.run import GetAndPrintCompilationStatisticsResults
def BuildOptions():
parser = argparse.ArgumentParser(
@@ -52,9 +51,5 @@ if __name__ == "__main__":
result = OrderedDict()
result[utils.benchmarks_label] = GetAndPrintBenchmarkResults(args)
- if args.target:
- result[utils.compilation_statistics_label] = \
- GetAndPrintCompilationStatisticsResults(args)
-
utils.OutputObject(result, 'pkl', args.output_pkl)
utils.OutputObject(result, 'json', args.output_json)
diff --git a/tools/benchmarks/run.py b/tools/benchmarks/run.py
index edfb3a6..844d484 100755
--- a/tools/benchmarks/run.py
+++ b/tools/benchmarks/run.py
@@ -18,9 +18,11 @@
import argparse
import csv
import os
+import shutil
import subprocess
import sys
import time
+import tempfile
from collections import OrderedDict
@@ -34,8 +36,6 @@ import utils_stats
bench_runner_main = 'org.linaro.bench.RunBench'
-# Options
-
def BuildOptions():
parser = argparse.ArgumentParser(
description = "Run java benchmarks.",
@@ -160,13 +160,18 @@ def RunBench(apk, classname,
try:
for line in outerr.rstrip().splitlines():
- if not line.startswith('benchmarks/'):
- continue
- name = line.split(":")[0].rstrip()
- score = float(line.split(":")[1].strip().split(" ")[0].strip())
- if name not in result:
- result[name] = list()
- result[name].append(score)
+ if line.startswith('benchmarks/'):
+ name = line.split(":")[0].rstrip()
+ score = float(line.split(":")[1].strip().split(" ")[0].strip())
+ if name not in result:
+ result[name] = list()
+ result[name].append(score)
+ duration = utils.ExtractCompilationTimeFromOutput(outerr)
+ if duration > 0:
+ if utils.compilation_times_label not in result:
+ result[utils.compilation_times_label] = list()
+ result[utils.compilation_times_label].append(duration)
+
except Exception as e:
utils.Warning(str(e) + "\n \-> Error parsing output from %s", e)
rc += 1
@@ -178,6 +183,7 @@ def RunBench(apk, classname,
def RunBenchs(apk, bench_names,
target,
+ output_oat,
auto_calibrate,
iterations=utils.default_n_iterations,
mode=utils.default_mode,
@@ -198,6 +204,16 @@ def RunBenchs(apk, bench_names,
android_root = android_root,
target = target,
cpuset = cpuset)
+ if output_oat is not None:
+ try:
+ work_dir = tempfile.mkdtemp()
+ local_oat = os.path.join(work_dir, 'bench.oat')
+ utils_adb.pull(output_oat, local_oat, target)
+ section_sizes = utils.GetSectionSizes(local_oat)
+ result[utils.compilation_statistics_label] = section_sizes
+ finally:
+ shutil.rmtree(work_dir)
+
return rc
@@ -253,6 +269,7 @@ def GetBenchmarkResults(args):
rc = RunBenchs(remote_apk,
benchmarks,
args.target,
+ args.output_oat,
not args.no_auto_calibrate,
args.iterations,
args.mode,
@@ -269,7 +286,15 @@ def GetBenchmarkResults(args):
def GetAndPrintBenchmarkResults(args):
results = GetBenchmarkResults(args)
utils.PrintData(results)
- unflattened_results = utils.Unflatten(results)
+
+ # remove compilation statistics from the results, it was
+ # already printed
+ filtered_results = dict()
+ for (k,v) in results.items():
+ if utils.compilation_statistics_label not in k:
+ filtered_results[k] = v
+
+ unflattened_results = utils.Unflatten(filtered_results)
utils_stats.ComputeAndPrintGeomeanWithRelativeDiff(unflattened_results)
print('')
return results
diff --git a/tools/compilation_statistics/run.py b/tools/compilation_statistics/run.py
deleted file mode 100755
index 23fa91b..0000000
--- a/tools/compilation_statistics/run.py
+++ /dev/null
@@ -1,315 +0,0 @@
-#! /usr/bin/env python3
-
-# Copyright (C) 2015 Linaro Limited. All rights received.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-import argparse
-import glob
-import json
-import os
-import pickle
-import re
-import shutil
-import subprocess
-import sys
-import tempfile
-import time
-
-from collections import OrderedDict
-
-dir_compilation_statistics = os.path.dirname(os.path.realpath(__file__))
-dir_tools = os.path.join(dir_compilation_statistics, '..')
-sys.path.insert(0, dir_tools)
-
-import utils
-import utils_adb
-import utils_stats
-
-memory_unit_prefixes = {'' : 1, 'G' : 2 ** 30, 'K' : 2 ** 10, 'M' : 2 ** 20}
-sections = set(['.bss', '.rodata', '.text', 'Total'])
-
-def BuildOptions():
- parser = argparse.ArgumentParser(
- description = '''Collect statistics about the APK compilation process on a target
- adb device: Compilation time, memory usage by the compiler
- (arena, Java, and native allocations, and free native memory),
- and size of the generated executable (total, .bss, .rodata, and
- .text section sizes).''',
- # Print default values.
- formatter_class = argparse.ArgumentDefaultsHelpFormatter)
- parser.add_argument('pathnames',
- nargs = '+',
- help='''Path containing APK files or a file name for which
- compilation statistics should be collected.''')
- utils.AddCommonRunOptions(parser)
- utils.AddOutputFormatOptions(parser, utils.default_output_formats)
-
- # TODO: Support running on host?
- # For now override the default value for the `--target`.
- parser.set_defaults(target=utils.adb_default_target_string)
-
- args = parser.parse_args()
-
- # This cannot fire for now since this script always runs on target, but
- # eventually we may want to run on host as well.
- utils.ValidateCommonRunOptions(args)
-
- return args
-
-def GetStats(apk,
- target,
- isa,
- compiler_mode,
- android_root,
- target_copy_path,
- iterations,
- cpuset,
- work_dir,
- boot_oat_file):
- path, env, runtime_param = utils.GetAndroidRootConfiguration(android_root, isa.endswith('64'))
- dex2oat = utils.TargetPathJoin(path, 'dex2oat')
-
- if boot_oat_file:
- oat = utils.TargetPathJoin(target_copy_path, 'boot.' + isa + '.oat')
- art = utils.TargetPathJoin(target_copy_path, 'boot.' + isa + '.art')
-
- # Check if dump file exists.
- dump_oat_file_location = utils.TargetPathJoin(target_copy_path, 'boot.oat.' + isa + '.txt')
- dump_exists_command = "if [ -f %s ] ; then echo found; fi; exit 0" \
- % (dump_oat_file_location)
- # Since we are interested in whether the dump file exists or not, we can't simply execute
- # [ -f file_name ] since newer versions of adb return the error code of the command that's
- # being executed. Therefore, if the file is found we output a string and at the end we
- # always return error code 0, so that we can get an error only if the failure was due
- # to adb not executing properly.
- rc, out = utils_adb.shell(dump_exists_command, target)
- # The command prints an extra new line as well.
- if out.strip() != "found":
- # Dump the oat file the first time, keeping only the parts we are interested in.
- dump_command = 'oatdump --oat-file=%s | grep "dex2oat-" > %s' % (boot_oat_file, \
- dump_oat_file_location)
- utils_adb.shell(dump_command, target)
- # Read dex2oat-host from dump file.
- dex2oat_host_command = 'grep "dex2oat-host" %s' % (dump_oat_file_location)
- rc, out = utils_adb.shell(dex2oat_host_command, target)
- if rc:
- utils.Error("Dump file doesn't contain dex2oat-host.")
- if out.strip() == 'dex2oat-host = x86-64':
- utils.Error("boot.oat was built on a x84-64 machine, which is most likely the" \
- " host: %s \nWe want it to be built on the target instead. Have you" \
- " configured the device with WITH_DEXPREOPT=false ?" % out)
-
- # Read command.
- dex2oat_cmdline_command = 'grep "dex2oat-cmdline" %s' % (dump_oat_file_location)
- rc, out = utils_adb.shell(dex2oat_cmdline_command, target)
- if rc:
- utils.Error("Dump file doesn't contain dex2oat-cmldine.")
- command = out.strip()
- # Replace destination: --oat-file, fix beginning of command.
- command = re.sub("--oat-file=(.+?) --", "--oat-file=%s --" % oat, command)
- command = re.sub("--image=(.+?) --", "--image=%s --" % art, command)
- command = re.sub("dex2oat-cmdline +=", dex2oat, command)
- # Force 1 thread only - we want compilation times to be as stable as possible and we are
- # interested in single thread performance, not multi-thread (throughput).
- command = re.sub(" -j\d+ ", " -j1 ", command)
- # Remove newline at end.
- command = re.sub("\n$", "", command)
- command = '(echo $BASHPID && '
-
- if cpuset:
- command += 'echo $BASHPID > /dev/cpuset/' + cpuset + '/tasks && '
-
- command += env + ' exec ' + command + ') | head -n1'
- else:
- runtime_arguments = ' --runtime-arg -Xnorelocate '
-
- for param in runtime_param:
- runtime_arguments += '--runtime-arg ' + param + ' '
-
- apk_path = utils.TargetPathJoin(target_copy_path, apk)
- oat = apk_path + '.' + isa + '.oat'
- dex2oat_options = utils.GetDex2oatOptions(compiler_mode)
- # Only the output of the first command is necessary; execute in a subshell
- # to guarantee PID value; only one thread is used for compilation to reduce
- # measurement noise.
- command = '(echo $BASHPID && '
-
- if cpuset:
- command += 'echo $BASHPID > /dev/cpuset/' + cpuset + '/tasks && '
-
- command += env + ' exec ' + dex2oat + \
- ' -j1' + runtime_arguments + ' '.join(dex2oat_options) + \
- ' --dex-file=' + apk_path + ' --oat-file=' + oat
- command += ' --instruction-set=' + isa + ') | head -n1'
-
- linux_target = os.getenv('ART_TARGET_LINUX', 'false') == 'true'
- dex2oat_time_regex = '.*?took (?P<value>.*?)(?P<unit>[mnu]{,1})s.*?\)'
- compilation_times = []
- for i in range(iterations):
- rc, stdout = utils_adb.shell(command, target)
- if linux_target:
- # On Linux, dex2oat writes to stdout, and output of compilation time is likely last
- for out in reversed(stdout.splitlines()):
- compile_time = re.match(dex2oat_time_regex, out)
- if compile_time:
- break
- else:
- # To simplify parsing, assume that PID values are rarely recycled by the system.
- stats_command = 'logcat -dsv process dex2oat | grep "^I([[:space:]]*' + \
- stdout.rstrip() + ').*took" | tail -n1'
- rc, out = utils_adb.shell(stats_command, target)
- compile_time = re.match(dex2oat_time_regex, out)
-
- if not compile_time:
- utils.Error('dex2oat failed; check adb logcat.')
-
- value = float(compile_time.group('value')) * \
- utils.si_unit_prefixes[compile_time.group('unit')]
- compilation_times.append(value)
-
- # The rest of the statistics are deterministic, so there is no need to run several
- # iterations; just get the values from the last run.
- out = out[compile_time.end():]
- # Newer versions of dex2oat also have number of threads output, that we need to get rid of
- out = re.sub('\(threads:\s+[0-9]+\) ', '', out)
- memory_stats = OrderedDict()
- byte_size = True
-
- for m in re.findall(' (.*?)=([0-9]+)([GKM]?)B( \(([0-9]+)B\))?', out):
- # Old versions of dex2oat do not show the exact memory usage values in bytes, so
- # try to parse the output in the new format first, and if that fails, fall back
- # to the legacy one.
- if m[4]:
- value = int(m[4])
- else:
- value = int(m[1]) * memory_unit_prefixes[m[2]]
-
- if m[2]:
- byte_size = False
-
- memory_stats[m[0]] = [value]
-
- if not byte_size:
- utils.Warning('Memory usage values have been rounded down, so they might be '
- 'inaccurate.')
-
- if boot_oat_file:
- local_oat = os.path.join(utils.dir_root, work_dir, "boot.%s.oat" % isa)
- else:
- local_oat = os.path.join(utils.dir_root, work_dir, apk + '.oat')
- utils_adb.pull(oat, local_oat, target)
- command = ['size', '-A', '-d', local_oat]
- rc, outerr = utils.Command(command)
- section_sizes = OrderedDict((s[0], [int(s[1])]) for s
- in re.findall('(\S+)\s+([0-9]+).*', outerr)
- if s[0] in sections)
- return OrderedDict([(utils.compilation_times_label, compilation_times),
- (utils.memory_stats_label, memory_stats),
- (utils.oat_size_label, section_sizes)])
-
-
-def GetCompilationStatisticsResults(args):
- utils.CheckDependencies(['adb', 'size'])
- isa = utils_adb.GetISA(args.target, args.mode)
- res = OrderedDict()
- work_dir = tempfile.mkdtemp()
- apk_list = set()
- boot_oat_file = None
-
- for pathname in args.pathnames:
- if pathname == "boot.oat":
- # Check if multiple boot.oat parameters have been passed.
- if boot_oat_file:
- continue
-
- # Get ISA list to check that the environment is in a good state.
- isa_list = utils_adb.GetISAList(args.target)
- # The oat cache is accessible only to root.
- utils_adb.root(args.target)
- # Find oat file on device.
- find_command = 'find / -type d \( -name proc -o -name sys \) -prune -o ' \
- '-name "*boot.oat" -print 2>/dev/null'
- rc, out = utils_adb.shell(find_command, args.target)
- boot_oat_files = out.splitlines()[:-1]
-
- if len(boot_oat_files) != len(isa_list):
- utils.Error("Number of architectures different from number of boot.oat files. " \
- "The list of boot.oat files is here:\n\n %s\n\nMake sure there are " \
- "no stale boot.oat files in %s or some other directory. " \
- "Another possibility is that you didn't build Android with " \
- "`WITH_DEXPREOPT=false`. Do a `lunch` and then `WITH_DEXPREOPT=false " \
- "make -j$(nproc)`." % (boot_oat_files, args.target_copy_path))
- # Order both lists. Now, as long as both oat files have the same parent dir, order
- # should match.
- isa_list.sort()
- boot_oat_files.sort()
- # Remove leading dot and trailing whitespace.
- boot_oat_file = boot_oat_files[isa_list.index(isa)][1:].strip()
- apk_list.add("boot.oat " + isa)
- elif os.path.isfile(pathname):
- apk_list.add(pathname)
- else:
- dentries = [dentry for dentry in glob.glob(os.path.join(pathname, '*.apk'))
- if os.path.isfile(dentry)]
-
- for d in dentries:
- apk_list.add(d)
-
- for apk in sorted(apk_list):
- # pathname just contains boot.oat.
- if apk[:8] == "boot.oat":
- res[apk] = GetStats(apk, args.target, isa, args.compiler_mode, args.android_root,
- args.target_copy_path, args.iterations, args.cpuset, work_dir,
- boot_oat_file)
- # This is a local path for boot.oat. We get stats locally without compiling on target.
- elif apk[-8:] == "boot.oat":
- res["boot.oat"] = GetLocalOatSizeStats(apk)
- else:
- utils_adb.push(apk, args.target_copy_path, args.target)
- apk_name = os.path.basename(apk)
- res[apk_name] = GetStats(apk_name, args.target, isa, args.compiler_mode,
- args.android_root, args.target_copy_path,
- args.iterations, args.cpuset, work_dir, None)
-
- shutil.rmtree(work_dir)
- return res
-
-def GetAndPrintCompilationStatisticsResults(args):
- results = GetCompilationStatisticsResults(args)
- utils.PrintData(results)
- print('')
- return results
-
-def GetLocalOatSizeStats(oat_path):
- command = ['size', '-A', '-d', oat_path]
- rc, outerr = utils.Command(command)
- section_sizes = OrderedDict((s[0], [int(s[1])]) for s
- in re.findall('(\S+)\s+([0-9]+).*', outerr)
- if s[0] in sections)
- # Add total file size in bytes.
- command = ['stat', '-c', '%s', oat_path]
- rc, outerr = utils.Command(command)
- section_sizes['FileSize'] = [int(outerr)]
- return OrderedDict([(utils.oat_size_label, section_sizes)])
-
-if __name__ == "__main__":
- # TODO: Mac OS support
- if os.uname().sysname != 'Linux':
- utils.Error('Running this script is supported only on Linux.')
-
- args = BuildOptions()
- stats = GetAndPrintCompilationStatisticsResults(args)
-
- utils.OutputObject(stats, 'pkl', args.output_pkl)
- utils.OutputObject(stats, 'json', args.output_json)
diff --git a/tools/utils.py b/tools/utils.py
index 6a0ab5f..b2e5e3d 100644
--- a/tools/utils.py
+++ b/tools/utils.py
@@ -18,6 +18,7 @@ import fnmatch
import json
import os
import pickle
+import re
import subprocess
import sys
import time
@@ -55,6 +56,13 @@ default_mode = ''
default_compiler_mode = None
default_n_iterations = 1
+# used in shell scripts for compile stats
+before_timestamp_key = "Before: "
+after_timestamp_key = "After: "
+
+sections = set(['.bss', '.rodata', '.text', 'Total'])
+
+
# TODO: Use python's logging and warning capabilities instead!
def Info(message):
print('INFO: ' + message)
@@ -181,6 +189,9 @@ def AddCommonRunOptions(parser):
nargs='+',
default = None,
help = 'Add pathnames to be considered for compilation statistics.')
+ opts.add_argument('--output-oat',
+ default=None,
+ help = 'Full name of the compiled executable file')
def ValidateCommonRunOptions(args):
options_requiring_target_mode = ['mode', 'compiler-mode']
@@ -469,3 +480,35 @@ def TargetPathJoin(path, *paths):
path = path.replace(os.sep, '/')
return path
+
+def ReadJSON(json_filename):
+ output_obj = dict()
+ try:
+ with open(json_filename, "r") as fp:
+ output_obj = json.load(fp)
+ except IOError:
+ pass
+ return output_obj
+
+def ExtractCompilationTimeFromOutput(lines):
+ timestamp_before = 0
+ timestamp_after = 0
+ # Extracting the size and timestamps (before and after compilation)
+ # cmdline.sh script is expected to print lines with
+ # two timestamps: before (before_timestamp_key)
+ # and after (after_timestamp_key) compilation
+ for line in lines.rstrip().splitlines():
+ if (line.startswith(after_timestamp_key)):
+ timestamp_after = float(line[len(after_timestamp_key):])
+ if (line.startswith(before_timestamp_key)):
+ timestamp_before = float(line[len(before_timestamp_key):])
+ return timestamp_after - timestamp_before
+
+def GetSectionSizes(path_to_executable):
+ command = ['size', '-A', '-d', path_to_executable]
+ rc, outerr = Command(command)
+ section_sizes = OrderedDict(
+ (section_line[0], [int(section_line[1])]) for section_line
+ in re.findall('(\S+)\s+([0-9]+).*', outerr)
+ if section_line[0] in sections)
+ return section_sizes