summaryrefslogtreecommitdiffstats
path: root/testing/raptor/raptor/cpu.py
blob: aa43f0690118edf7251a3d2b2ca9f7aacc7c35f5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
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