diff options
Diffstat (limited to 'testing/raptor/raptor/cpu.py')
-rw-r--r-- | testing/raptor/raptor/cpu.py | 169 |
1 files changed, 169 insertions, 0 deletions
diff --git a/testing/raptor/raptor/cpu.py b/testing/raptor/raptor/cpu.py new file mode 100644 index 0000000000..aa43f06901 --- /dev/null +++ b/testing/raptor/raptor/cpu.py @@ -0,0 +1,169 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +import threading +import time + +from logger.logger import RaptorLogger + +LOG = RaptorLogger(component="raptor-cpu") + + +class AndroidCPUProfiler(object): + """AndroidCPUProfiler is used to measure CPU usage over time for an + app in testing. Polls usage at a defined interval and then submits this + as a supporting perfherder data measurement. + + ``` + # Initialize the profiler and start polling for CPU usage of the app + cpu_profiler = AndroidCPUProfiler(raptor, poll_interval=1) + cpu_profiler.start_polling() + + # Or call the following to perform the commands above + cpu_profiler = start_android_cpu_profiler(raptor) + + # Run test... + + # Stop measuring and generate perfherder data + cpu_profiler.generate_android_cpu_profile("browsertime-tp6m") + ``` + """ + + def __init__(self, raptor, poll_interval=10): + """Initialize the Android CPU Profiler. + + :param RaptorAndroid raptor: raptor object which contains android device. + :param float poll_interval: interval, in seconds, at which we should poll + for CPU usage. Defaults to 10 seconds. + """ + self.raptor = raptor + self.polls = [] + self.poll_interval = poll_interval + self.pause_polling = False + + # Get the android version + android_version = self.raptor.device.shell_output( + "getprop ro.build.version.release" + ).strip() + self.android_version = int(android_version.split(".")[0]) + + # Prepare the polling thread (set as daemon for clean exiting) + self.thread = threading.Thread(target=self.poll_cpu, args=(poll_interval,)) + self.thread.daemon = True + + def start_polling(self): + """Start the thread responsible for polling CPU usage.""" + self.thread.start() + + def stop_polling(self): + """Pauses CPU usage polling.""" + self.pause_polling = True + + def poll_cpu(self, poll_interval): + """Polls CPU usage at an interval of `poll_interval`. + + :param float poll_interval: interval at which we poll for CPU usage. + """ + while True: + time.sleep(self.poll_interval) + if self.pause_polling: + continue + self.polls.append(self.get_app_cpu_usage()) + + def get_app_cpu_usage(self): + """Gather a point on an android app's CPU usage. + + :return float: CPU usage found for the app at this point in time. + """ + cpu_usage = 0 + app_name = self.raptor.config["binary"] + verbose = self.raptor.device._verbose + self.raptor.device._verbose = False + + if self.android_version >= 8: + # On android 8 we can use the -O option to order the entries + # in top by %CPU. + cpuinfo = self.raptor.device.shell_output("top -O %CPU -n 1").split("\n") + + for line in cpuinfo: + # A line looks like: + # 14781 u0_a83 0 92.8 12.4 64:53.04 org.mozilla.geckoview_example + data = line.split() + if data[-1] == app_name: + cpu_usage = float(data[3]) + else: + # On android 7, -O is not required since top already orders them by %CPU. + # top also has different columns on this android version. + cpuinfo = self.raptor.device.shell_output("top -n 1").split("\n") + + # Parse the app-specific entries which look like: + # 21165 u0_a196 10 -10 14% S 67 1442392K 163356K fg org.mozilla.geckoview_example + + # Gather the app-specific entries + appcpuinfo = [] + for line in cpuinfo: + if not line.strip().endswith(app_name): + continue + appcpuinfo.append(line) + + # Get CPU usage + for line in appcpuinfo: + data = line.split() + cpu_usage = float(data[4].strip("%")) + + self.raptor.device._verbose = verbose + return cpu_usage + + def generate_android_cpu_profile(self, test_name): + """Use the CPU usage data which was sampled to produce a supporting + perfherder data measurement and submit it to the raptor control + server. Returns the minimum, maximum, and average CPU usage. + + :param str test_name: name of the test that was run. + """ + self.stop_polling() + if len(self.polls) == 0: + LOG.info("No CPU usage data found, submitting as 0% usage.") + self.polls.append(0) + + avg_cpuinfo_data = { + u"type": u"cpu", + u"test": test_name + "-avg", + u"unit": u"%", + u"values": { + u"avg": sum(self.polls) / len(self.polls) + }, # pylint --py3k W1619 + } + self.raptor.control_server.submit_supporting_data(avg_cpuinfo_data) + + min_cpuinfo_data = { + u"type": u"cpu", + u"test": test_name + "-min", + u"unit": u"%", + u"values": {u"min": min(self.polls)}, + } + self.raptor.control_server.submit_supporting_data(min_cpuinfo_data) + + max_cpuinfo_data = { + u"type": u"cpu", + u"test": test_name + "-max", + u"unit": u"%", + u"values": {u"max": max(self.polls)}, + } + self.raptor.control_server.submit_supporting_data(max_cpuinfo_data) + + +def start_android_cpu_profiler(raptor, **kwargs): + """Start the android CPU profiler and return the profiler + object so measurements can be obtained. + + :return AndroidCPUProfiler: the profiler performing the CPU measurements. + """ + if not raptor.device: + LOG.error("No ADB device found in raptor, not creating AndroidCPUProfiler.") + return + + cpu_profiler = AndroidCPUProfiler(raptor, **kwargs) + cpu_profiler.start_polling() + + return cpu_profiler |