| #!/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()) |