#!/usr/bin/python3 """ * Copyright (C) 2021 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. """ ''' Measure CPU related power on Pixel 6 or later devices using ODPM, the On Device Power Measurement tool. Generate a CSV report for putting in a spreadsheet ''' import argparse import os import re import subprocess import sys import time # defaults PRE_DELAY_SECONDS = 0.5 # time to sleep before command to avoid adb unroot error DEFAULT_NUM_ITERATIONS = 5 DEFAULT_FILE_NAME = 'energy_commands.txt' ''' Default rail assignments philburk-macbookpro3:expt philburk$ adb shell cat /sys/bus/iio/devices/iio\:device0/energy_value t=349894 CH0(T=349894)[S10M_VDD_TPU], 5578756 CH1(T=349894)[VSYS_PWR_MODEM], 29110940 CH2(T=349894)[VSYS_PWR_RFFE], 3166046 CH3(T=349894)[S2M_VDD_CPUCL2], 30203502 CH4(T=349894)[S3M_VDD_CPUCL1], 23377533 CH5(T=349894)[S4M_VDD_CPUCL0], 46356942 CH6(T=349894)[S5M_VDD_INT], 10771876 CH7(T=349894)[S1M_VDD_MIF], 21091363 philburk-macbookpro3:expt philburk$ adb shell cat /sys/bus/iio/devices/iio\:device1/energy_value t=359458 CH0(T=359458)[VSYS_PWR_WLAN_BT], 45993209 CH1(T=359458)[L2S_VDD_AOC_RET], 2822928 CH2(T=359458)[S9S_VDD_AOC], 6923706 CH3(T=359458)[S5S_VDDQ_MEM], 4658202 CH4(T=359458)[S10S_VDD2L], 5506273 CH5(T=359458)[S4S_VDD2H_MEM], 14254574 CH6(T=359458)[S2S_VDD_G3D], 5315420 CH7(T=359458)[VSYS_PWR_DISPLAY], 81221665 ''' ''' LDO2M(L2M_ALIVE):DDR -> DRAM Array Core Power BUCK4S(S4S_VDD2H_MEM):DDR -> Normal operation data and control path logic circuits BUCK5S(S5S_VDDQ_MEM):DDR -> LPDDR I/O interface BUCK10S(S10S_VDD2L):DDR -> DVFSC (1600Mbps or lower) operation data and control path logic circuits BUCK1M (S1M_VDD_MIF): SoC side Memory InterFace and Controller ''' # Map between rail name and human readable name. ENERGY_DICTIONARY = { \ 'S4M_VDD_CPUCL0': 'CPU0', \ 'S3M_VDD_CPUCL1': 'CPU1', \ 'S2M_VDD_CPUCL2': 'CPU2', \ 'S1M_VDD_MIF': 'MIF', \ 'L2M_ALIVE': 'DDRAC', \ 'S4S_VDD2H_MEM': 'DDRNO', \ 'S10S_VDD2L': 'DDR16', \ 'S5S_VDDQ_MEM': 'DDRIO', \ 'VSYS_PWR_DISPLAY': 'SCREEN'} SORTED_ENERGY_LIST = sorted(ENERGY_DICTIONARY, key=ENERGY_DICTIONARY.get) # Sometimes adb returns 1 for no apparent reason. # So try several times. # @return 0 on success def adbTryMultiple(command): returnCode = 1 count = 0 limit = 5 while count < limit and returnCode != 0: print(('Try to adb {} {} of {}'.format(command, count, limit))) subprocess.call(["adb", "wait-for-device"]) time.sleep(PRE_DELAY_SECONDS) returnCode = subprocess.call(["adb", command]) print(('returnCode = {}'.format(returnCode))) count += 1 return returnCode # Sometimes "adb root" returns 1! # So try several times. # @return 0 on success def adbRoot(): return adbTryMultiple("root"); # Sometimes "adb unroot" returns 1! # So try several times. # @return 0 on success def adbUnroot(): return adbTryMultiple("unroot"); # @param commandString String containing shell command # @return Both the stdout and stderr of the commands run def runCommand(commandString): print(commandString) if commandString == "adb unroot": result = adbUnroot() elif commandString == "adb root": result = adbRoot() else: commandArray = commandString.split(' ') result = subprocess.run(commandArray, check=True, capture_output=True).stdout return result # @param commandString String containing ADB command # @return Both the stdout and stderr of the commands run def adbCommand(commandString): if commandString == "unroot": result = adbUnroot() elif commandString == "root": result = adbRoot() else: print(("adb " + commandString)) commandArray = ["adb"] + commandString.split(' ') subprocess.call(["adb", "wait-for-device"]) result = subprocess.run(commandArray, check=True, capture_output=True).stdout return result # Parse a line that looks like "CH3(T=10697635)[S2M_VDD_CPUCL2], 116655335" # Use S2M_VDD_CPUCL2 as the tag and set value to the number # in the report dictionary. def parseEnergyValue(string): return tuple(re.split('\[|\], +', string)[1:]) # Read accumulated energy into a dictionary. def measureEnergyForDevice(deviceIndex, report): # print("measureEnergyForDevice " + str(deviceIndex)) tableBytes = adbCommand( \ 'shell cat /sys/bus/iio/devices/iio\:device{}/energy_value'\ .format(deviceIndex)) table = tableBytes.decode("utf-8") # print(table) for count, line in enumerate(table.splitlines()): if count > 0: tagEnergy = parseEnergyValue(line) report[tagEnergy[0]] = int(tagEnergy[1].strip()) # print(report) def measureEnergyOnce(): adbCommand("root") report = {} d0 = measureEnergyForDevice(0, report) d1 = measureEnergyForDevice(1, report) adbUnroot() return report # Subtract numeric values for matching keys. def subtractReports(A, B): return {x: A[x] - B[x] for x in A if x in B} # Add numeric values for matching keys. def addReports(A, B): return {x: A[x] + B[x] for x in A if x in B} # Divide numeric values by divisor. # @return Modified copy of report. def divideReport(report, divisor): return {key: val / divisor for key, val in list(report.items())} # Generate a dictionary that is the difference between two measurements over time. def measureEnergyOverTime(duration): report1 = measureEnergyOnce() print(("Measure energy for " + str(duration) + " seconds.")) time.sleep(duration) report2 = measureEnergyOnce() return subtractReports(report2, report1) # Generate a CSV string containing the human readable headers. def formatEnergyHeader(): header = "" for tag in SORTED_ENERGY_LIST: header += ENERGY_DICTIONARY[tag] + ", " return header # Generate a CSV string containing the numeric values. def formatEnergyData(report): data = "" for tag in SORTED_ENERGY_LIST: if tag in list(report.keys()): data += str(report[tag]) + ", " else: data += "-1," return data def printEnergyReport(report): s = "\n" s += "Values are in microWattSeconds\n" s += "Report below is CSV format for pasting into a spreadsheet:\n" s += formatEnergyHeader() + "\n" s += formatEnergyData(report) + "\n" print(s) # Generate a dictionary that is the difference between two measurements # before and after executing the command. def measureEnergyForCommand(command): report1 = measureEnergyOnce() print(("Measure energy for: " + command)) result = runCommand(command) report2 = measureEnergyOnce() # print(result) return subtractReports(report2, report1) # Average the results of several measurements for one command. def averageEnergyForCommand(command, count): print("=================== #0\n") sumReport = measureEnergyForCommand(command) for i in range(1, count): print(("=================== #" + str(i) + "\n")) report = measureEnergyForCommand(command) sumReport = addReports(sumReport, report) print(sumReport) return divideReport(sumReport, count) # Parse a list of commands in a file. # Lines ending in "\" are continuation lines. # Lines beginning with "#" are comments. def measureEnergyForCommands(fileName): finalReport = "------------------------------------\n" finalReport += "comment, command, " + formatEnergyHeader() + "\n" comment = "" try: fp = open(fileName) line = fp.readline() while line: command = line.strip() if command.startswith("#"): # ignore comment print((command + "\n")) comment = command[1:].strip() # remove leading '#' elif command.endswith('\\'): command = command[:-1].strip() # remove trailing '\' runCommand(command) elif command: report = averageEnergyForCommand(command, DEFAULT_NUM_ITERATIONS) finalReport += comment + ", " + command + ", " + formatEnergyData(report) + "\n" print(finalReport) line = fp.readline() finally: fp.close() return finalReport def main(): # parse command line args parser = argparse.ArgumentParser() parser.add_argument('-s', '--seconds', help="Measure power for N seconds. Ignore scriptFile.", type=float) parser.add_argument("fileName", nargs = '?', help="Path to file containing commands to be measured." + " Default path = " + DEFAULT_FILE_NAME + "." + " Lines ending in '\' are continuation lines." + " Lines beginning with '#' are comments.", default=DEFAULT_FILE_NAME) args=parser.parse_args(); print(("seconds = " + str(args.seconds))) print(("fileName = " + str(args.fileName))) # Process command line if args.seconds: report = measureEnergyOverTime(args.seconds) printEnergyReport(report) else: report = measureEnergyForCommands(args.fileName) print(report) print("Finished.\n") return 0 if __name__ == '__main__': sys.exit(main())