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