summaryrefslogtreecommitdiffstats
path: root/testing/raptor/raptor/power.py
blob: c34046d761ca7682c814f469eb140c1b29511f65 (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
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
# 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 os
import re
import time

from logger.logger import RaptorLogger
from mozdevice import ADBError, ADBTimeoutError

LOG = RaptorLogger(component="raptor-power")

P2_PATH = "/sys/class/power_supply/battery/input_suspend"
G5_PATH = "/sys/class/power_supply/battery/charging_enabled"
S7_PATH = "/sys/class/power_supply/battery/batt_slate_mode"


def get_device_type(device, timeout=10):
    """Returns the type of device being tested. Currently
    it can either be Pixel 2, Moto G5, or Samsung S7."""
    device_type = device.shell_output("getprop ro.product.model", timeout=timeout)
    if device_type == "Pixel 2":
        pass
    elif device_type == "Moto G (5)":
        pass
    elif device_type == "SM-G930F":
        # samsung s7 galaxy (exynos)
        pass
    else:
        raise Exception("TEST-UNEXPECTED-FAIL | Unknown device ('%s')!" % device_type)
    return device_type


def change_charging_state(device, device_type, enable=True, timeout=10):
    """Changes the charging state. If enable is True, charging will be enabled,
    otherwise it will be disabled."""
    try:
        if device_type == "Pixel 2":
            status = 0 if enable else 1
            device.shell_bool("echo %s > %s" % (status, P2_PATH), timeout=timeout)
        elif device_type == "Moto G (5)":
            status = 1 if enable else 0
            device.shell_bool("echo %s > %s" % (status, G5_PATH), timeout=timeout)
        elif device_type == "SM-G930F":
            status = 0 if enable else 1
            device.shell_bool("echo %s > %s" % (status, S7_PATH), timeout=timeout)
    except (ADBTimeoutError, ADBError) as e:
        raise Exception(
            "TEST-UNEXPECTED-FAIL | Failed to %s charging. Error: %s"
            % (
                "enable" if enable else "disable",
                "{}: {}".format(e.__class__.__name__, e),
            )
        )


def is_charging_disabled(device, device_type, timeout=10):
    """True if charging is already disabled."""
    disabled = False
    if device_type == "Pixel 2":
        disabled = (
            device.shell_output("cat %s 2>/dev/null" % P2_PATH, timeout=timeout).strip()
            == "1"
        )
    elif device_type == "Moto G (5)":
        disabled = (
            device.shell_output("cat %s 2>/dev/null" % G5_PATH, timeout=timeout).strip()
            == "0"
        )
    elif device_type == "SM-G930F":
        disabled = (
            device.shell_output("cat %s 2>/dev/null" % S7_PATH, timeout=timeout).strip()
            == "1"
        )
    return disabled


def is_charging_enabled(device, device_type):
    """True if charging is already enabled."""
    return not is_charging_disabled(device, device_type)


def enable_charging(device):
    """Enables charging on a supported device."""
    device_type = get_device_type(device)
    if is_charging_enabled(device, device_type):
        return

    # ProxyLogger returns a RuntimeError when the
    # logger isn't initialized. Catching this error
    # is the only way to tell if the logger wasn't
    # initialized.
    enabling_str = "Enabling charging..."
    try:
        LOG.info(enabling_str)
    except RuntimeError:
        print(enabling_str)

    change_charging_state(device, device_type, enable=True)


def disable_charging(device):
    """Disables charging on a supported device."""
    device_type = get_device_type(device)
    if is_charging_disabled(device, device_type):
        return

    disabling_str = "Disabling charging..."
    try:
        LOG.info(disabling_str)
    except RuntimeError:
        print(disabling_str)

    change_charging_state(device, device_type, enable=False)


def init_android_power_test(raptor):
    upload_dir = os.getenv("MOZ_UPLOAD_DIR")
    if not upload_dir:
        LOG.critical(
            "%s power test ignored; MOZ_UPLOAD_DIR unset" % raptor.config["app"]
        )
        return
    # Disable adaptive brightness - do not restore the value since this setting
    # should always be disabled.
    raptor.device.shell_output("settings put system screen_brightness_mode 0")

    # Set the screen-off timeout to two (2) hours, since the device will be running
    # disconnected, and would otherwise turn off the screen, thereby halting
    # execution of the test. Save the current value so we can restore it later
    # since it is a persistent change.
    raptor.screen_off_timeout = raptor.device.shell_output(
        "settings get system screen_off_timeout"
    ).strip()
    raptor.device.shell_output("settings put system screen_off_timeout 7200000")

    # Set the screen brightness to ~50% for consistency of measurements across
    # devices and save its current value to restore it later. Screen brightness
    # values range from 0 to 255.
    raptor.screen_brightness = raptor.device.shell_output(
        "settings get system screen_brightness"
    ).strip()
    raptor.device.shell_output("settings put system screen_brightness 127")

    raptor.device.shell_output("dumpsys batterystats --reset")
    raptor.device.shell_output("dumpsys batterystats --enable full-wake-history")

    filepath = os.path.join(upload_dir, "battery-before.txt")
    with open(filepath, "w") as output:
        output.write(raptor.device.shell_output("dumpsys battery"))

    raptor.test_start_time = int(time.time())


# The batterystats output for Estimated power use differs
# for Android 7 and Android 8 and later.
#
# Android 7
# Estimated power use (mAh):
#   Capacity: 2100, Computed drain: 625, actual drain: 1197-1218
#   Unaccounted: 572 ( )
#   Uid u0a78: 329 ( cpu=329 )
#   Screen: 190
#   Cell standby: 87.6 ( radio=87.6 )
#   Idle: 4.10
#   Uid 1000: 1.82 ( cpu=0.537 sensor=1.28 )
#   Wifi: 0.800 ( cpu=0.310 wifi=0.490 )
# Android 8
# Estimated power use (mAh):
#   Capacity: 2700, Computed drain: 145, actual drain: 135-162
#   Screen: 68.2 Excluded from smearing
#   Uid u0a208: 61.7 ( cpu=60.5 wifi=1.28 ) Including smearing: 141 ( screen=67.7 proportional=9. )
#   Cell standby: 2.49 ( radio=2.49 ) Excluded from smearing
#   Idle: 1.63 Excluded from smearing
#   Bluetooth: 0.527 ( cpu=0.00319 bt=0.524 ) Including smearing: 0.574 ( proportional=... )
#   Wifi: 0.423 ( cpu=0.343 wifi=0.0800 ) Including smearing: 0.461 ( proportional=0.0375 )
#
# For Android 8, the cpu, wifi, screen, and proportional values are available from
# the Uid line for the app. If the test does not run long enough, it
# appears that the screen value from the Uid will be missing, but the
# standalone Screen value is available.
#
# For Android 7, only the cpu value is available from the Uid line. We
# can use the Screen and Wifi values for Android 7 from the Screen
# and Wifi lines, which might include contributions from the system or
# other apps; however, it should still be useful for spotting changes in power
# usage.
#
# If the energy values from the Uid line for Android 8 are available, they
# will be used. If for any reason either/both screen or wifi power is
# missing, the values from the Screen and Wifi lines will be used.
#
# If only the cpu energy value is available, it will be used
# along with the values from the Screen and Wifi lines.


def finish_android_power_test(raptor, test_name, os_baseline=False):
    upload_dir = os.getenv("MOZ_UPLOAD_DIR")
    if not upload_dir:
        LOG.critical(
            "%s power test ignored because MOZ_UPLOAD_DIR was not set" % test_name
        )
        return
    # Restore screen_off_timeout and screen brightness.
    raptor.device.shell_output(
        "settings put system screen_off_timeout %s" % raptor.screen_off_timeout
    )
    raptor.device.shell_output(
        "settings put system screen_brightness %s" % raptor.screen_brightness
    )

    test_end_time = int(time.time())

    filepath = os.path.join(upload_dir, "battery-after.txt")
    with open(filepath, "w") as output:
        output.write(raptor.device.shell_output("dumpsys battery"))
    verbose = raptor.device._verbose
    raptor.device._verbose = False
    filepath = os.path.join(upload_dir, "batterystats.csv")
    with open(filepath, "w") as output:
        output.write(raptor.device.shell_output("dumpsys batterystats --checkin"))
    filepath = os.path.join(upload_dir, "batterystats.txt")
    with open(filepath, "w") as output:
        batterystats = raptor.device.shell_output("dumpsys batterystats")
        output.write(batterystats)
    raptor.device._verbose = verbose

    # Get the android version
    android_version = raptor.device.shell_output(
        "getprop ro.build.version.release"
    ).strip()
    major_android_version = int(android_version.split(".")[0])

    estimated_power = False
    uid = None
    total = cpu = wifi = smearing = screen = proportional = 0
    full_screen = 0
    full_wifi = 0
    re_uid = re.compile(r'proc=([^:]+):"%s"' % raptor.config["binary"])
    re_wifi = re.compile(r".*wifi=([\d.]+).*")
    re_cpu = re.compile(r".*cpu=([\d.]+).*")
    re_estimated_power = re.compile(r"\s+Estimated power use [(]mAh[)]")
    re_proportional = re.compile(r"proportional=([\d.]+)")
    re_screen = re.compile(r"screen=([\d.]+)")
    re_full_screen = re.compile(r"\s+Screen:\s+([\d.]+)")
    re_full_wifi = re.compile(r"\s+Wifi:\s+([\d.]+)")

    re_smear = re.compile(r".*smearing:\s+([\d.]+)\s+.*")
    re_power = re.compile(
        r"\s+Uid\s+\w+[:]\s+([\d.]+) [(]([\s\w\d.\=]*)(?:([)] "
        r"Including smearing:.*)|(?:[)]))"
    )

    batterystats = batterystats.split("\n")
    for line in batterystats:
        if uid is None and not os_baseline:
            # The proc line containing the Uid and app name appears
            # before the Estimated power line.
            match = re_uid.search(line)
            if match:
                uid = match.group(1)
                re_power = re.compile(
                    r"\s+Uid %s[:]\s+([\d.]+) [(]([\s\w\d.\=]*)(?:([)] "
                    r"Including smearing:.*)|(?:[)]))" % uid
                )
                continue
        if not estimated_power:
            # Do not attempt to parse data until we have seen
            # Estimated Power in the output.
            match = re_estimated_power.match(line)
            if match:
                estimated_power = True
            continue
        if full_screen == 0:
            match = re_full_screen.match(line)
            if match and match.group(1):
                full_screen += float(match.group(1))
                continue
        if full_wifi == 0:
            match = re_full_wifi.match(line)
            if match and match.group(1):
                full_wifi += float(match.group(1))
                continue
        if re_power:
            match = re_power.match(line)
            if match:
                ttotal, breakdown, smear_info = match.groups()
                total += float(ttotal) if ttotal else 0

                cpu_match = re_cpu.match(breakdown)
                if cpu_match and cpu_match.group(1):
                    cpu += float(cpu_match.group(1))

                wifi_match = re_wifi.match(breakdown)
                if wifi_match and wifi_match.group(1):
                    wifi += float(wifi_match.group(1))

                if smear_info:
                    # Smearing and screen power are only
                    # available on android 8+
                    smear_match = re_smear.match(smear_info)
                    if smear_match and smear_match.group(1):
                        smearing += float(smear_match.group(1))
                    screen_match = re_screen.search(line)
                    if screen_match and screen_match.group(1):
                        screen += float(screen_match.group(1))
                    prop_match = re_proportional.search(smear_info)
                    if prop_match and prop_match.group(1):
                        proportional += float(prop_match.group(1))
        if full_screen and full_wifi and (cpu and wifi and smearing or total):
            # Stop parsing batterystats once we have a full set of data.
            # If we are running an OS baseline, stop when we've exhausted
            # the list of entries.
            if not os_baseline:
                break
            elif line.replace(" ", "") == "":
                break

    cpu = total if cpu == 0 else cpu
    screen = full_screen if screen == 0 else screen
    wifi = full_wifi if wifi == 0 else wifi

    if os_baseline:
        uid = "all"
    LOG.info(
        "power data for uid: %s, cpu: %s, wifi: %s, screen: %s, proportional: %s"
        % (uid, cpu, wifi, screen, proportional)
    )

    # Send power data directly to the control-server results handler
    # so it can be formatted and output for perfherder ingestion

    power_data = {
        "type": "power",
        "test": test_name,
        "unit": "mAh",
        "values": {
            "cpu": float(cpu),
            "wifi": float(wifi),
            "screen": float(screen),
        },
    }

    if major_android_version >= 8:
        power_data["values"]["proportional"] = float(proportional)

    if os_baseline:
        raptor.os_baseline_data = power_data
    else:
        LOG.info("submitting power data via control server directly")

        raptor.control_server.submit_supporting_data(power_data)
        if raptor.os_baseline_data:
            # raptor.power_test_time is only used by test_power.py
            # for testing power measurement parsing
            test_time = raptor.power_test_time
            if not test_time:
                test_time = float(test_end_time - raptor.test_start_time) / 60
            LOG.info("Approximate power test time %s" % str(test_time))

            def calculate_pc(power_measure, baseline_measure):
                if not baseline_measure:
                    LOG.error("Power test baseline_measure is Zero.")
                    return 0
                # pylint --py3k W1619
                return (
                    100 * ((power_measure + baseline_measure) / baseline_measure)
                ) - 100

            pc_power_data = {
                "type": "power",
                "test": power_data["test"] + "-%change",
                "unit": "%",
                "values": {},
            }
            for power_measure in power_data["values"]:
                # pylint --py3k W1619
                pc_power_data["values"][power_measure] = calculate_pc(
                    (power_data["values"][power_measure] / test_time),
                    raptor.os_baseline_data["values"][power_measure],
                )

            raptor.control_server.submit_supporting_data(pc_power_data)
            raptor.control_server.submit_supporting_data(raptor.os_baseline_data)

        # Generate power bugreport zip
        LOG.info("generating power bugreport zip")
        raptor.device.command_output(["bugreport", upload_dir])