energy.py revision 87267394f63aad602a201701399787a9c0e5d796
1# SPDX-License-Identifier: Apache-2.0
2#
3# Copyright (C) 2015, ARM Limited and contributors.
4#
5# Licensed under the Apache License, Version 2.0 (the "License"); you may
6# 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, WITHOUT
13# 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
18import devlib
19import json
20import logging
21import time
22
23# Default energy measurements for each board
24DEFAULT_ENERGY_METER = {
25
26    # ARM TC2: by default use HWMON
27    'tc2' : {
28        'instrument' : 'hwmon',
29        'conf' : {
30            'sites' : [ 'A7 Jcore', 'A15 Jcore' ],
31            'kinds' : [ 'energy']
32        }
33    },
34
35    # ARM Juno: by default use HWMON
36    'juno' : {
37        'instrument' : 'hwmon',
38        'conf' : {
39            'sites' : [ 'a53', 'a57' ],
40            'kinds' : [ 'energy' ]
41        }
42    },
43
44    # Hikey: by default use AEP
45    'hikey' : {
46        'instrument' : 'aep',
47    }
48
49}
50
51class EnergyMeter(object):
52
53    _meter = None
54
55    def __init__(self, target):
56        self._target = target
57
58    @staticmethod
59    def getInstance(target, conf, force=False):
60
61        if not force and EnergyMeter._meter:
62            return EnergyMeter._meter
63
64        # Initialize energy probe to board default
65        if 'board' in conf and \
66            conf['board'] in DEFAULT_ENERGY_METER:
67                emeter = DEFAULT_ENERGY_METER[conf['board']]
68                logging.debug('%14s - using default energy meter for [%s]',
69                        'EnergyMeter', conf['board'])
70        else:
71            return None
72
73        if emeter['instrument'] == 'hwmon':
74            EnergyMeter._meter = HWMon(target, emeter['conf'])
75        elif emeter['instrument'] == 'aep':
76            EnergyMeter._meter = Aep(target)
77        return EnergyMeter._meter
78
79    def sample(self):
80        raise NotImplementedError('Missing implementation')
81
82    def reset(self):
83        raise NotImplementedError('Missing implementation')
84
85    def report(self, out_dir):
86        raise NotImplementedError('Missing implementation')
87
88class HWMon(EnergyMeter):
89
90    def __init__(self, target, hwmon_conf=None):
91        super(HWMon, self).__init__(target)
92
93        # The HWMon energy meter
94        self._hwmon = None
95
96        # Energy readings
97        self.readings = {}
98
99        if 'hwmon' not in self._target.modules:
100            logging.info('%14s - HWMON module not enabled',
101                    'EnergyMeter')
102            logging.warning('%14s - Energy sampling disabled by configuration',
103                    'EnergyMeter')
104            return
105
106        # Initialize HWMON instrument
107        logging.info('%14s - Scanning for HWMON channels, may take some time...', 'EnergyMeter')
108        self._hwmon = devlib.HwmonInstrument(self._target)
109
110        # Configure channels for energy measurements
111        logging.debug('%14s - Enabling channels %s', 'EnergyMeter', hwmon_conf)
112        self._hwmon.reset(**hwmon_conf)
113
114        # Logging enabled channels
115        logging.info('%14s - Channels selected for energy sampling:\n%s',
116                'EnergyMeter', str(self._hwmon.active_channels))
117
118    def sample(self):
119        if self._hwmon is None:
120            return
121        samples = self._hwmon.take_measurement()
122        for s in samples:
123            label = s.channel.label\
124                    .replace('_energy', '')\
125                    .replace(" ", "_")
126            value = s.value
127
128            if label not in self.readings:
129                self.readings[label] = {
130                        'last'  : value,
131                        'delta' : 0,
132                        'total' : 0
133                        }
134                continue
135
136            self.readings[label]['delta'] = value - self.readings[label]['last']
137            self.readings[label]['last']  = value
138            self.readings[label]['total'] += self.readings[label]['delta']
139
140        logging.debug('SAMPLE: %s', self.readings)
141        return self.readings
142
143    def reset(self):
144        if self._hwmon is None:
145            return
146        self.sample()
147        for label in self.readings:
148            self.readings[label]['delta'] = 0
149            self.readings[label]['total'] = 0
150        logging.debug('RESET: %s', self.readings)
151
152
153    def report(self, out_dir, out_file='energy.json'):
154        if self._hwmon is None:
155            return
156        # Retrive energy consumption data
157        nrg = self.sample()
158        # Reformat data for output generation
159        clusters_nrg = {}
160        for ch in nrg:
161            nrg_total = nrg[ch]['total']
162            logging.info('%14s - Energy [%16s]: %.6f',
163                    'EnergyReport', ch, nrg_total)
164            if self._target.little_core.upper() in ch.upper():
165                clusters_nrg['LITTLE'] = '{:.6f}'.format(nrg_total)
166            elif self._target.big_core.upper() in ch.upper():
167                clusters_nrg['big'] = '{:.6f}'.format(nrg_total)
168            else:
169                logging.warning('%14s - Unable to bind hwmon channel [%s]'\
170                        ' to a big.LITTLE cluster',
171                        'EnergyReport', ch)
172                clusters_nrg[ch] = '{:.6f}'.format(nrg_total)
173        if 'LITTLE' not in clusters_nrg:
174                logging.warning('%14s - No energy data for LITTLE cluster',
175                        'EnergyMeter')
176        if 'big' not in clusters_nrg:
177                logging.warning('%14s - No energy data for big cluster',
178                        'EnergyMeter')
179
180        # Dump data as JSON file
181        nrg_file = '{}/{}'.format(out_dir, out_file)
182        with open(nrg_file, 'w') as ofile:
183            json.dump(clusters_nrg, ofile, sort_keys=True, indent=4)
184
185        return (clusters_nrg, nrg_file)
186
187class Aep(EnergyMeter):
188
189    def __init__(self, target):
190        super(Aep, self).__init__(target)
191
192        # Energy readings
193        self.readings = {}
194
195        # Time (start and diff) for power measurment
196        self.time = {}
197
198        # Initialize instrument
199        # Only one channel (first AEP channel: pc1 ... probe channel 1) is used
200        self._aep = devlib.EnergyProbeInstrument(self._target, labels=["pc1"], resistor_values=[0.033])
201
202        # Configure channels for energy measurements
203        logging.debug('EnergyMeter - Enabling channels')
204        self._aep.reset()
205
206        # Logging enabled channels
207        logging.info('%14s - Channels selected for energy sampling:\n%s',
208                'EnergyMeter', str(self._aep.active_channels))
209
210    def __calc_nrg(self, samples):
211
212        power = {'sum' : 0, 'count' : 0, 'avg' : 0}
213
214        for s in samples:
215            power['sum'] += s[1].value # s[1] ... power value of channel 1
216            power['count'] += 1
217
218        power['avg'] =  power['sum'] / power['count']
219
220        nrg = power['avg'] * self.time['diff']
221
222        logging.debug('avg power: %.6f count: %s time: %.6f nrg: %.6f',
223	              power['avg'], power['count'], self.time['diff'] , nrg)
224        return nrg
225
226    def sample(self):
227        if self._aep is None:
228            return
229
230        self.time['diff'] = time.time() - self.time['start']
231        self._aep.stop()
232
233        csv_data = self._aep.get_data("/tmp/aep.csv")
234        samples = csv_data.measurements()
235
236        value = self.__calc_nrg(samples)
237
238        self.readings['last'] = value
239        self.readings['delta'] = value
240        self.readings['total'] = value
241
242        logging.debug('SAMPLE: %s', self.readings)
243        return self.readings
244
245    def reset(self):
246        if self._aep is None:
247            return
248
249        logging.debug('RESET: %s', self.readings)
250
251        self._aep.start()
252        self.time['start'] = time.time()
253
254    def report(self, out_dir, out_file='energy.json'):
255        if self._aep is None:
256            return
257
258        # Retrieve energy consumption data
259        nrg = self.sample()
260
261        # Reformat data for output generation
262        clusters_nrg = {}
263        clusters_nrg['LITTLE'] = '{:.6f}'.format(self.readings['total'])
264
265        # Dump data as JSON file
266        nrg_file = '{}/{}'.format(out_dir, out_file)
267        with open(nrg_file, 'w') as ofile:
268            json.dump(clusters_nrg, ofile, sort_keys=True, indent=4)
269
270        return (clusters_nrg, nrg_file)
271
272# vim :set tabstop=4 shiftwidth=4 expandtab
273