普通文本  |  407行  |  15.16 KB

#!/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.suspend' : 'Power consumption when CPU is suspended',
        'cpu.idle' : '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.cpu = 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.cpu= 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_suspend(self, power):
        cpu_suspend_power = self.power_profile.get_item('cpu.suspend')
        if cpu_suspend_power is None:
            self._measure_cpu_suspend()
            cpu_suspend_power = self.power_profile.get_item('cpu.suspend')

        return power - cpu_suspend_power

    def _remove_cpu_power(self, power, duration, results_dir):
        if self.cpu 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.cpu.get_clusters():
                if time_in_state_cpus == set(self.cpu.get_cores(cluster)):
                    cpu_cnt = len(self.cpu.get_cores(cluster))

                    for freq, time_cs in time_in_state_json['time_delta'][cl].iteritems():
                        time_s = time_cs * 0.01
                        energy += time_s * self.cpu.get_core_cost(cluster, int(freq))

                    # TODO remove cpu cluster cost and addtional base cost
                    # This will require 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_power(display_plus_cpu_power,
                duration, results_dir)

        return power - display_power

    def _measure_cpu_suspend(self):
        duration = 120

        self._run_experiment('run_suspend_resume.py', duration, 'cpu_suspend',
                args='--collect energy')
        power = self._power_average('SuspendResume_cpu_suspend',
                start=duration*0.25, remove_outliers=True)

        self.power_profile.add_item('cpu.suspend', power)

    def _measure_cpu_idle(self):
        duration = 120

        self._run_experiment('run_idle_resume.py', duration, 'cpu_idle',
                args='--collect energy')
        power = self._power_average('IdleResume_cpu_idle', start=duration*0.25,
                remove_outliers=True)

        power = self._remove_cpu_suspend(power)

        self.power_profile.add_item('cpu.idle', 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_power(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_power(power, duration, results_dir)

        self.power_profile.add_item('screen.full', power)

    def _measure_cpu_cluster_cores(self):
        if self.cpu is None:
            self._cpu_freq_power_average()

        self.power_profile.add_array('cpu.clusters.cores', self.cpu.get_clusters())

    def _measure_cpu_active_power(self):
        if self.cpu is None:
            self._cpu_freq_power_average()

        comment = 'Additional power used when any cpu core is turned on'\
                ' in any cluster. Does not include the power used by the cpu'\
                ' cluster(s) or core(s).'
        self.power_profile.add_item('cpu.active', self.cpu.get_active_cost()*1000,
                comment)

    def _measure_cpu_cluster_power(self):
        if self.cpu is None:
            self._cpu_freq_power_average()

        clusters = self.cpu.get_clusters()

        for cluster in clusters:
            cluster_power = self.cpu.get_cluster_cost(cluster)

            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(cluster)
            self.power_profile.add_item('cpu.cluster_power.cluster{}'.format(cluster),
                    cluster_power*1000, comment)

    def _measure_cpu_core_speeds(self):
        if self.cpu is None:
            self._cpu_freq_power_average()

        clusters = self.cpu.get_clusters()

        for cluster in clusters:
            core_speeds = self.cpu.get_core_freqs(cluster)

            comment = 'Different CPU speeds as reported in /sys/devices/system/'\
                    'cpu/cpuX/cpufreq/scaling_available_frequencies'
            self.power_profile.add_array('cpu.core_speeds.cluster{}'.format(cluster),
                    core_speeds, comment)

    def _measure_cpu_core_power(self):
        if self.cpu is None:
            self._cpu_freq_power_average()

        clusters = self.cpu.get_clusters()

        for cluster in clusters:
            core_speeds = self.cpu.get_core_freqs(cluster)

            core_powers = [ self.cpu.get_core_cost(cluster, core_speed)*1000 for core_speed in core_speeds ]
            comment = 'Additional power used by a CPU from cluster {} when'\
                    ' running at different speeds. Currently this measurement'\
                    ' also includes cluster cost.'.format(cluster)
            subcomments = [ '{} MHz CPU speed'.format(core_speed*0.001) for core_speed in core_speeds ]

            self.power_profile.add_array('cpu.core_power.cluster{}'.format(cluster),
                    core_powers, 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_power(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_power(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_power(power, duration, results_dir)

        self.power_profile.add_item('gps.on', power)

    def _compute_measurements(self):
        #self._measure_cpu_suspend()
        #self._measure_cpu_idle()
        self._measure_cpu_cluster_cores()
        self._measure_cpu_active_power()
        self._measure_cpu_cluster_power()
        self._measure_cpu_core_speeds()
        self._measure_cpu_core_power()
        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()