• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/python3
2"""
3 * Copyright (C) 2021 The Android Open Source Project
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *      http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16"""
17
18'''
19Measure CPU related power on Pixel 6 or later devices using ODPM,
20the On Device Power Measurement tool.
21Generate a CSV report for putting in a spreadsheet
22'''
23
24import argparse
25import os
26import re
27import subprocess
28import sys
29import time
30
31# defaults
32PRE_DELAY_SECONDS = 0.5 # time to sleep before command to avoid adb unroot error
33DEFAULT_NUM_ITERATIONS = 5
34DEFAULT_FILE_NAME = 'energy_commands.txt'
35
36'''
37Default rail assignments
38philburk-macbookpro3:expt philburk$ adb shell cat /sys/bus/iio/devices/iio\:device0/energy_value
39t=349894
40CH0(T=349894)[S10M_VDD_TPU], 5578756
41CH1(T=349894)[VSYS_PWR_MODEM], 29110940
42CH2(T=349894)[VSYS_PWR_RFFE], 3166046
43CH3(T=349894)[S2M_VDD_CPUCL2], 30203502
44CH4(T=349894)[S3M_VDD_CPUCL1], 23377533
45CH5(T=349894)[S4M_VDD_CPUCL0], 46356942
46CH6(T=349894)[S5M_VDD_INT], 10771876
47CH7(T=349894)[S1M_VDD_MIF], 21091363
48philburk-macbookpro3:expt philburk$ adb shell cat /sys/bus/iio/devices/iio\:device1/energy_value
49t=359458
50CH0(T=359458)[VSYS_PWR_WLAN_BT], 45993209
51CH1(T=359458)[L2S_VDD_AOC_RET], 2822928
52CH2(T=359458)[S9S_VDD_AOC], 6923706
53CH3(T=359458)[S5S_VDDQ_MEM], 4658202
54CH4(T=359458)[S10S_VDD2L], 5506273
55CH5(T=359458)[S4S_VDD2H_MEM], 14254574
56CH6(T=359458)[S2S_VDD_G3D], 5315420
57CH7(T=359458)[VSYS_PWR_DISPLAY], 81221665
58'''
59
60'''
61LDO2M(L2M_ALIVE):DDR  -> DRAM Array Core Power
62BUCK4S(S4S_VDD2H_MEM):DDR -> Normal operation data and control path logic circuits
63BUCK5S(S5S_VDDQ_MEM):DDR -> LPDDR I/O interface
64BUCK10S(S10S_VDD2L):DDR  -> DVFSC (1600Mbps or lower) operation data and control path logic circuits
65BUCK1M (S1M_VDD_MIF):  SoC side Memory InterFace and Controller
66'''
67
68# Map between rail name and human readable name.
69ENERGY_DICTIONARY = { \
70        'S4M_VDD_CPUCL0': 'CPU0', \
71        'S3M_VDD_CPUCL1': 'CPU1', \
72        'S2M_VDD_CPUCL2': 'CPU2', \
73        'S1M_VDD_MIF': 'MIF', \
74        'L2M_ALIVE': 'DDRAC', \
75        'S4S_VDD2H_MEM': 'DDRNO', \
76        'S10S_VDD2L': 'DDR16', \
77        'S5S_VDDQ_MEM': 'DDRIO', \
78        'VSYS_PWR_DISPLAY': 'SCREEN'}
79
80SORTED_ENERGY_LIST = sorted(ENERGY_DICTIONARY, key=ENERGY_DICTIONARY.get)
81
82# Sometimes adb returns 1 for no apparent reason.
83# So try several times.
84# @return 0 on success
85def adbTryMultiple(command):
86    returnCode = 1
87    count = 0
88    limit = 5
89    while count < limit and returnCode != 0:
90        print(('Try to adb {} {} of {}'.format(command, count, limit)))
91        subprocess.call(["adb", "wait-for-device"])
92        time.sleep(PRE_DELAY_SECONDS)
93        returnCode = subprocess.call(["adb", command])
94        print(('returnCode = {}'.format(returnCode)))
95        count += 1
96    return returnCode
97
98# Sometimes "adb root" returns 1!
99# So try several times.
100# @return 0 on success
101def adbRoot():
102    return adbTryMultiple("root");
103
104# Sometimes "adb unroot" returns 1!
105# So try several times.
106# @return 0 on success
107def adbUnroot():
108    return adbTryMultiple("unroot");
109
110# @param commandString String containing shell command
111# @return Both the stdout and stderr of the commands run
112def runCommand(commandString):
113    print(commandString)
114    if commandString == "adb unroot":
115        result = adbUnroot()
116    elif commandString == "adb root":
117        result = adbRoot()
118    else:
119        commandArray = commandString.split(' ')
120        result = subprocess.run(commandArray, check=True, capture_output=True).stdout
121    return result
122
123# @param commandString String containing ADB command
124# @return Both the stdout and stderr of the commands run
125def adbCommand(commandString):
126    if commandString == "unroot":
127        result = adbUnroot()
128    elif commandString == "root":
129        result = adbRoot()
130    else:
131        print(("adb " + commandString))
132        commandArray = ["adb"] + commandString.split(' ')
133        subprocess.call(["adb", "wait-for-device"])
134        result = subprocess.run(commandArray, check=True, capture_output=True).stdout
135    return result
136
137# Parse a line that looks like "CH3(T=10697635)[S2M_VDD_CPUCL2], 116655335"
138# Use S2M_VDD_CPUCL2 as the tag and set value to the number
139# in the report dictionary.
140def parseEnergyValue(string):
141    return tuple(re.split('\[|\], +', string)[1:])
142
143# Read accumulated energy into a dictionary.
144def measureEnergyForDevice(deviceIndex, report):
145    # print("measureEnergyForDevice " + str(deviceIndex))
146    tableBytes = adbCommand( \
147            'shell cat /sys/bus/iio/devices/iio\:device{}/energy_value'\
148            .format(deviceIndex))
149    table = tableBytes.decode("utf-8")
150    # print(table)
151    for count, line in enumerate(table.splitlines()):
152        if count > 0:
153            tagEnergy = parseEnergyValue(line)
154            report[tagEnergy[0]] = int(tagEnergy[1].strip())
155    # print(report)
156
157def measureEnergyOnce():
158    adbCommand("root")
159    report = {}
160    d0 = measureEnergyForDevice(0, report)
161    d1 = measureEnergyForDevice(1, report)
162    adbUnroot()
163    return report
164
165# Subtract numeric values for matching keys.
166def subtractReports(A, B):
167    return {x: A[x] - B[x] for x in A if x in B}
168
169# Add numeric values for matching keys.
170def addReports(A, B):
171    return {x: A[x] + B[x] for x in A if x in B}
172
173# Divide numeric values by divisor.
174# @return Modified copy of report.
175def divideReport(report, divisor):
176    return {key: val / divisor for key, val in list(report.items())}
177
178# Generate a dictionary that is the difference between two measurements over time.
179def measureEnergyOverTime(duration):
180    report1 = measureEnergyOnce()
181    print(("Measure energy for " + str(duration) + " seconds."))
182    time.sleep(duration)
183    report2 = measureEnergyOnce()
184    return subtractReports(report2, report1)
185
186# Generate a CSV string containing the human readable headers.
187def formatEnergyHeader():
188    header = ""
189    for tag in SORTED_ENERGY_LIST:
190        header += ENERGY_DICTIONARY[tag] + ", "
191    return header
192
193# Generate a CSV string containing the numeric values.
194def formatEnergyData(report):
195    data = ""
196    for tag in SORTED_ENERGY_LIST:
197        if tag in list(report.keys()):
198            data += str(report[tag]) + ", "
199        else:
200            data += "-1,"
201    return data
202
203def printEnergyReport(report):
204    s = "\n"
205    s += "Values are in microWattSeconds\n"
206    s += "Report below is CSV format for pasting into a spreadsheet:\n"
207    s += formatEnergyHeader() + "\n"
208    s += formatEnergyData(report) + "\n"
209    print(s)
210
211# Generate a dictionary that is the difference between two measurements
212# before and after executing the command.
213def measureEnergyForCommand(command):
214    report1 = measureEnergyOnce()
215    print(("Measure energy for:  " + command))
216    result = runCommand(command)
217    report2 = measureEnergyOnce()
218    # print(result)
219    return subtractReports(report2, report1)
220
221# Average the results of several measurements for one command.
222def averageEnergyForCommand(command, count):
223    print("=================== #0\n")
224    sumReport = measureEnergyForCommand(command)
225    for i in range(1, count):
226        print(("=================== #" + str(i) + "\n"))
227        report = measureEnergyForCommand(command)
228        sumReport = addReports(sumReport, report)
229    print(sumReport)
230    return divideReport(sumReport, count)
231
232# Parse a list of commands in a file.
233# Lines ending in "\" are continuation lines.
234# Lines beginning with "#" are comments.
235def measureEnergyForCommands(fileName):
236    finalReport = "------------------------------------\n"
237    finalReport += "comment, command, " + formatEnergyHeader() + "\n"
238    comment = ""
239    try:
240        fp = open(fileName)
241        line = fp.readline()
242        while line:
243            command = line.strip()
244            if command.startswith("#"):
245                # ignore comment
246                print((command + "\n"))
247                comment = command[1:].strip() # remove leading '#'
248            elif command.endswith('\\'):
249                command = command[:-1].strip() # remove trailing '\'
250                runCommand(command)
251            elif command:
252                report = averageEnergyForCommand(command, DEFAULT_NUM_ITERATIONS)
253                finalReport += comment + ", " + command + ", " + formatEnergyData(report) + "\n"
254                print(finalReport)
255            line = fp.readline()
256    finally:
257        fp.close()
258    return finalReport
259
260def main():
261    # parse command line args
262    parser = argparse.ArgumentParser()
263    parser.add_argument('-s', '--seconds',
264            help="Measure power for N seconds. Ignore scriptFile.",
265            type=float)
266    parser.add_argument("fileName",
267            nargs = '?',
268            help="Path to file containing commands to be measured."
269                    + " Default path = " + DEFAULT_FILE_NAME + "."
270                    + " Lines ending in '\' are continuation lines."
271                    + " Lines beginning with '#' are comments.",
272                    default=DEFAULT_FILE_NAME)
273    args=parser.parse_args();
274
275    print(("seconds  = " + str(args.seconds)))
276    print(("fileName = " + str(args.fileName)))
277    # Process command line
278    if args.seconds:
279        report = measureEnergyOverTime(args.seconds)
280        printEnergyReport(report)
281    else:
282        report = measureEnergyForCommands(args.fileName)
283        print(report)
284    print("Finished.\n")
285    return 0
286
287if __name__ == '__main__':
288    sys.exit(main())
289