summaryrefslogtreecommitdiffstats
path: root/tools/power/mach_commands.py
blob: 117a96e1c71565c88f7ad985615774a71e09d867 (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
# 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/.

from mach.decorators import Command, CommandArgument
from packaging.version import Version


def is_osx_10_10_or_greater(cls):
    import platform

    release = platform.mac_ver()[0]
    return release and Version(release) >= Version("10.10")


# Get system power consumption and related measurements.
@Command(
    "power",
    category="misc",
    conditions=[is_osx_10_10_or_greater],
    description="Get system power consumption and related measurements for "
    "all running browsers. Available only on Mac OS X 10.10 and above. "
    "Requires root access.",
)
@CommandArgument(
    "-i",
    "--interval",
    type=int,
    default=30000,
    help="The sample period, measured in milliseconds. Defaults to 30000.",
)
def power(command_context, interval):
    """
    Get system power consumption and related measurements.
    """
    import os
    import re
    import subprocess

    rapl = os.path.join(command_context.topobjdir, "dist", "bin", "rapl")

    interval = str(interval)

    # Run a trivial command with |sudo| to gain temporary root privileges
    # before |rapl| and |powermetrics| are called. This ensures that |rapl|
    # doesn't start measuring while |powermetrics| is waiting for the root
    # password to be entered.
    try:
        subprocess.check_call(["sudo", "true"])
    except Exception:
        print("\nsudo failed; aborting")
        return 1

    # This runs rapl in the background because nothing in this script
    # depends on the output. This is good because we want |rapl| and
    # |powermetrics| to run at the same time.
    subprocess.Popen([rapl, "-n", "1", "-i", interval])

    lines = subprocess.check_output(
        [
            "sudo",
            "powermetrics",
            "--samplers",
            "tasks",
            "--show-process-coalition",
            "--show-process-gpu",
            "-n",
            "1",
            "-i",
            interval,
        ],
        universal_newlines=True,
    )

    # When run with --show-process-coalition, |powermetrics| groups outputs
    # into process coalitions, each of which has a leader.
    #
    # For example, when Firefox runs from the dock, its coalition looks
    # like this:
    #
    #   org.mozilla.firefox
    #     firefox
    #     plugin-container
    #
    # When Safari runs from the dock:
    #
    #   com.apple.Safari
    #     Safari
    #     com.apple.WebKit.Networking
    #     com.apple.WebKit.WebContent
    #     com.apple.WebKit.WebContent
    #
    # When Chrome runs from the dock:
    #
    #   com.google.Chrome
    #     Google Chrome
    #     Google Chrome Helper
    #     Google Chrome Helper
    #
    # In these cases, we want to print the whole coalition.
    #
    # Also, when you run any of them from the command line, things are the
    # same except that the leader is com.apple.Terminal and there may be
    # non-browser processes in the coalition, e.g.:
    #
    #  com.apple.Terminal
    #    firefox
    #    plugin-container
    #    <and possibly other, non-browser processes>
    #
    # Also, the WindowServer and kernel coalitions and processes are often
    # relevant.
    #
    # We want to print all these but omit uninteresting coalitions. We
    # could do this by properly parsing powermetrics output, but it's
    # simpler and more robust to just grep for a handful of identifying
    # strings.

    print()  # blank line between |rapl| output and |powermetrics| output

    for line in lines.splitlines():
        # Search for the following things.
        #
        # - '^Name' is for the columns headings line.
        #
        # - 'firefox' and 'plugin-container' are for Firefox
        #
        # - 'Safari\b' and 'WebKit' are for Safari. The '\b' excludes
        #   SafariCloudHistoryPush, which is a process that always
        #   runs, even when Safari isn't open.
        #
        # - 'Chrome' is for Chrome.
        #
        # - 'Terminal' is for the terminal. If no browser is running from
        #   within the terminal, it will show up unnecessarily. This is a
        #   minor disadvantage of this very simple parsing strategy.
        #
        # - 'WindowServer' is for the WindowServer.
        #
        # - 'kernel' is for the kernel.
        #
        if re.search(
            r"(^Name|firefox|plugin-container|Safari\b|WebKit|Chrome|Terminal|WindowServer|kernel)",  # NOQA: E501
            line,
        ):
            print(line)

    return 0