blob: b7a550bf4c8bd2ab82c7944165ca02f13195e2b4 [file] [log] [blame]
#!/usr/bin/env python
# SPDX-License-Identifier: Apache-2.0
#
# Copyright (C) 2017, ARM Limited, Google, and contributors.
#
# 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.
#
from __future__ import division
import os
import json
from lxml import etree
from power_average import PowerAverage
from cpu_frequency_power_average import CpuFrequencyPowerAverage
class PowerProfile:
def __init__(self):
self.xml = etree.Element('device', name='Android')
default_comments = {
'none' : 'Nothing',
'battery.capacity' : 'This is the battery capacity in mAh',
'cpu.idle' : 'Power consumption when CPU is suspended',
'cpu.awake' : 'Additional power consumption when CPU is in a kernel'
' idle loop',
'cpu.clusters.cores' : 'Number of cores each CPU cluster contains',
'screen.on' : 'Additional power used when screen is turned on at'
' minimum brightness',
'screen.full' : 'Additional power used when screen is at maximum'
' brightness, compared to screen at minimum brightness',
'camera.flashlight' : 'Average power used by the camera flash module'
' when on.',
'camera.avg' : 'Average power use by the camera subsystem for a typical'
' camera application.',
'gps.on' : 'Additional power used when GPS is acquiring a signal.',
'bluetooth.controller.idle' : 'Average current draw (mA) of the'
' Bluetooth controller when idle.',
'bluetooth.controller.rx' : 'Average current draw (mA) of the Bluetooth'
' controller when receiving.',
'bluetooth.controller.tx' : 'Average current draw (mA) of the Bluetooth'
' controller when transmitting.',
'bluetooth.controller.voltage' : 'Average operating voltage (mV) of the'
' Bluetooth controller.',
'modem.controller.idle' : 'Average current draw (mA) of the modem'
' controller when idle.',
'modem.controller.rx' : 'Average current draw (mA) of the modem'
' controller when receiving.',
'modem.controller.tx' : 'Average current draw (mA) of the modem'
' controller when transmitting.',
'modem.controller.voltage' : 'Average operating voltage (mV) of the'
' modem controller.',
'wifi.controller.idle' : 'Average current draw (mA) of the Wi-Fi'
' controller when idle.',
'wifi.controller.rx' : 'Average current draw (mA) of the Wi-Fi'
' controller when receiving.',
'wifi.controller.tx' : 'Average current draw (mA) of the Wi-Fi'
' controller when transmitting.',
'wifi.controller.voltage' : 'Average operating voltage (mV) of the'
' Wi-Fi controller.',
}
def _add_comment(self, item, name, comment):
if (not comment) and (name in PowerProfile.default_comments):
comment = PowerProfile.default_comments[name]
if comment:
self.xml.append(etree.Comment(comment))
def add_item(self, name, value, comment=None):
if self.get_item(name) is not None:
raise RuntimeWarning('{} already added. Skipping.'.format(name))
return
item = etree.Element('item', name=name)
item.text = str(value)
self._add_comment(self.xml, name, comment)
self.xml.append(item)
def get_item(self, name):
items = self.xml.findall(".//*[@name='{}']".format(name))
if len(items) == 0:
return None
return float(items[0].text)
def add_array(self, name, values, comment=None, subcomments=None):
array = etree.Element('array', name=name)
for i, value in enumerate(values):
entry = etree.Element('value')
entry.text = str(value)
if subcomments:
array.append(etree.Comment(subcomments[i]))
array.append(entry)
self._add_comment(self.xml, name, comment)
self.xml.append(array)
def __str__(self):
return etree.tostring(self.xml, pretty_print=True)
class PowerProfileGenerator:
def __init__(self, emeter, datasheet):
self.emeter = emeter
self.datasheet = datasheet
self.power_profile = PowerProfile()
self.clusters = None
def get(self):
self._compute_measurements()
self._import_datasheet()
return self.power_profile
def _run_experiment(self, filename, duration, out_prefix, args=''):
os.system('python {} --duration {} --out_prefix {} {} '.format(
os.path.join(os.environ['LISA_HOME'], 'experiments', filename),
duration, out_prefix, args))
def _power_average(self, results_dir, start=None, remove_outliers=False):
column = self.emeter['power_column']
sample_rate_hz = self.emeter['sample_rate_hz']
return PowerAverage.get(os.path.join(os.environ['LISA_HOME'], 'results',
results_dir, 'samples.csv'), column, sample_rate_hz,
start=start, remove_outliers=remove_outliers) * 1000
def _cpu_freq_power_average(self):
duration = 120
self._run_experiment(os.path.join('power', 'eas',
'run_cpu_frequency.py'), duration, 'cpu_freq')
self.clusters = CpuFrequencyPowerAverage.get(
os.path.join(os.environ['LISA_HOME'], 'results',
'CpuFrequency_cpu_freq'), os.path.join(os.environ['LISA_HOME'],
'results', 'CpuFrequency', 'platform.json'),
self.emeter['power_column'])
def _remove_cpu_idle(self, power):
cpu_idle_power = self.power_profile.get_item('cpu.idle')
if cpu_idle_power is None:
self._measure_cpu_idle()
cpu_idle_power = self.power_profile.get_item('cpu.idle')
return power - cpu_idle_power
def _remove_cpu_active(self, power, duration, results_dir):
if self.clusters is None:
self._cpu_freq_power_average()
cfile = os.path.join(os.environ['LISA_HOME'], 'results', results_dir,
'time_in_state.json')
with open(cfile, 'r') as f:
time_in_state_json = json.load(f)
energy = 0.0
for cl in sorted(time_in_state_json['clusters']):
time_in_state_cpus = set(int(c) for c in time_in_state_json['clusters'][cl])
for cluster in self.clusters:
if time_in_state_cpus == set(cluster.get_cpus()):
cpu_cnt = len(cluster.get_cpus())
for freq, time_cs in time_in_state_json['time_delta'][cl].iteritems():
time_s = time_cs * 0.01
energy += time_s * cluster.get_cpu_cost(int(freq))
# TODO remove cpu cluster cost and addtional base cost
# This will require updating the cpu_frequency script
# to calcualte the base cost and a kernel patch to
# keep track of cluster time
return power - energy / duration * 1000
def _remove_screen_full(self, power, duration, image):
out_prefix = image.split('.')[0]
results_dir = 'DisplayImage_{}'.format(out_prefix)
self._run_experiment('run_display_image.py', duration, out_prefix,
args='--collect=energy,time_in_state --brightness 100 --image={}'.format(image))
display_plus_cpu_power = self._power_average(results_dir)
display_power = self._remove_cpu_active(display_plus_cpu_power,
duration, results_dir)
return power - display_power
# The power profile defines cpu.idle as a suspended cpu
def _measure_cpu_idle(self):
duration = 120
self._run_experiment('run_suspend_resume.py', duration, 'cpu_idle',
args='--collect energy')
power = self._power_average('SuspendResume_cpu_idle',
start=duration*0.25, remove_outliers=True)
self.power_profile.add_item('cpu.idle', power)
# The power profile defines cpu.awake as an idle cpu
def _measure_cpu_awake(self):
duration = 120
self._run_experiment('run_idle_resume.py', duration, 'cpu_awake',
args='--collect energy')
power = self._power_average('IdleResume_cpu_awake', start=duration*0.25,
remove_outliers=True)
power = self._remove_cpu_idle(power)
self.power_profile.add_item('cpu.awake', power)
def _measure_screen_on(self):
duration = 120
results_dir = 'DisplayImage_screen_on'
self._run_experiment('run_display_image.py', duration, 'screen_on',
args='--collect=energy,time_in_state --brightness 0')
power = self._power_average(results_dir)
power = self._remove_cpu_active(power, duration, results_dir)
self.power_profile.add_item('screen.on', power)
def _measure_screen_full(self):
duration = 120
results_dir = 'DisplayImage_screen_full'
self._run_experiment('run_display_image.py', duration, 'screen_full',
args='--collect=energy,time_in_state --brightness 100')
power = self._power_average(results_dir)
power = self._remove_cpu_active(power, duration, results_dir)
self.power_profile.add_item('screen.full', power)
def _measure_cpu_cluster_cores(self):
if self.clusters is None:
self._cpu_freq_power_average()
cpu_cluster_cores = [ len(cluster.get_cpus()) for cluster in self.clusters ]
self.power_profile.add_array('cpu.clusters.cores', cpu_cluster_cores)
def _measure_cpu_base_cluster(self):
if self.clusters is None:
self._cpu_freq_power_average()
for i, cluster in enumerate(self.clusters):
comment = 'Additional power used when any cpu core is turned on'\
' in cluster{}. Does not include the power used by the cpu'\
' core(s).'.format(i)
self.power_profile.add_item('cpu.base.cluster{}'.format(i),
cluster.get_cluster_cost()*1000, comment)
def _measure_cpu_speeds_cluster(self):
if self.clusters is None:
self._cpu_freq_power_average()
for i, cluster in enumerate(self.clusters):
comment = 'Different CPU speeds as reported in /sys/devices/system/'\
'cpu/cpu{}/cpufreq/scaling_available_frequencies'.format(
cluster.get_cpus()[0])
freqs = cluster.get_cpu_freqs()
self.power_profile.add_array('cpu.speeds.cluster{}'.format(i), freqs,
comment)
def _measure_cpu_active_cluster(self):
if self.clusters is None:
self._cpu_freq_power_average()
for i, cluster in enumerate(self.clusters):
freqs = cluster.get_cpu_freqs()
cpu_active = [ cluster.get_cpu_cost(freq)*1000 for freq in freqs ]
comment = 'Additional power used by a CPU from cluster {} when'\
' running at different speeds. Currently this measurement'\
' also includes cluster cost.'.format(i)
subcomments = [ '{} MHz CPU speed'.format(freq*0.001) for freq in freqs ]
self.power_profile.add_array('cpu.active.cluster{}'.format(i),
cpu_active, comment, subcomments)
def _measure_camera_flashlight(self):
duration = 120
results_dir = 'CameraFlashlight_camera_flashlight'
self._run_experiment(os.path.join('power', 'profile',
'run_camera_flashlight.py'), duration, 'camera_flashlight',
args='--collect=energy,time_in_state')
power = self._power_average(results_dir)
power = self._remove_screen_full(power, duration,
'power_profile_camera_flashlight.png')
power = self._remove_cpu_active(power, duration, results_dir)
self.power_profile.add_item('camera.flashlight', power)
def _measure_camera_avg(self):
duration = 120
results_dir = 'CameraAvg_camera_avg'
self._run_experiment(os.path.join('power', 'profile',
'run_camera_avg.py'), duration, 'camera_avg',
args='--collect=energy,time_in_state')
power = self._power_average(results_dir)
power = self._remove_screen_full(power, duration,
'power_profile_camera_avg.png')
power = self._remove_cpu_active(power, duration, results_dir)
self.power_profile.add_item('camera.avg', power)
def _measure_gps_on(self):
duration = 120
results_dir = 'GpsOn_gps_on'
self._run_experiment(os.path.join('power', 'profile', 'run_gps_on.py'),
duration, 'gps_on', args='--collect=energy,time_in_state')
power = self._power_average(results_dir)
power = self._remove_screen_full(power, duration,
'power_profile_gps_on.png')
power = self._remove_cpu_active(power, duration, results_dir)
self.power_profile.add_item('gps.on', power)
def _compute_measurements(self):
self._measure_cpu_idle()
self._measure_cpu_awake()
self._measure_cpu_cluster_cores()
self._measure_cpu_base_cluster()
self._measure_cpu_speeds_cluster()
self._measure_cpu_active_cluster()
self._measure_screen_on()
self._measure_screen_full()
self._measure_camera_flashlight()
self._measure_camera_avg()
self._measure_gps_on()
def _import_datasheet(self):
for item in sorted(self.datasheet.keys()):
self.power_profile.add_item(item, self.datasheet[item])
my_emeter = {
'power_column' : 'output_power',
'sample_rate_hz' : 500,
}
my_datasheet = {
# Add datasheet values in the following format:
#
# 'none' : 0,
#
# 'battery.capacity' : 0,
#
# 'bluetooth.controller.idle' : 0,
# 'bluetooth.controller.rx' : 0,
# 'bluetooth.controller.tx' : 0,
# 'bluetooth.controller.voltage' : 0,
#
# 'modem.controller.idle' : 0,
# 'modem.controller.rx' : 0,
# 'modem.controller.tx' : 0,
# 'modem.controller.voltage' : 0,
#
# 'wifi.controller.idle' : 0,
# 'wifi.controller.rx' : 0,
# 'wifi.controller.tx' : 0,
# 'wifi.controller.voltage' : 0,
}
power_profile_generator = PowerProfileGenerator(my_emeter, my_datasheet)
print power_profile_generator.get()