1 /*
2  * Copyright (C) 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.powermodel;
18 
19 import java.io.InputStream;
20 import java.io.IOException;
21 import java.util.regex.Pattern;
22 import java.util.regex.Matcher;
23 import java.util.ArrayList;
24 import java.util.Arrays;
25 import java.util.HashMap;
26 import javax.xml.parsers.ParserConfigurationException;
27 import javax.xml.parsers.SAXParser;
28 import javax.xml.parsers.SAXParserFactory;
29 import org.xml.sax.Attributes;
30 import org.xml.sax.Locator;
31 import org.xml.sax.SAXException;
32 import org.xml.sax.SAXParseException;
33 import org.xml.sax.helpers.DefaultHandler;
34 
35 import com.android.powermodel.component.AudioProfile;
36 import com.android.powermodel.component.BluetoothProfile;
37 import com.android.powermodel.component.CameraProfile;
38 import com.android.powermodel.component.CpuProfile;
39 import com.android.powermodel.component.FlashlightProfile;
40 import com.android.powermodel.component.GpsProfile;
41 import com.android.powermodel.component.ModemProfile;
42 import com.android.powermodel.component.ScreenProfile;
43 import com.android.powermodel.component.VideoProfile;
44 import com.android.powermodel.component.WifiProfile;
45 import com.android.powermodel.util.Conversion;
46 
47 public class PowerProfile {
48 
49     // Remaining fields from the android code for which the actual usage is unclear.
50     //   battery.capacity
51     //   bluetooth.controller.voltage
52     //   modem.controller.voltage
53     //   gps.voltage
54     //   wifi.controller.voltage
55     //   radio.on
56     //   radio.scanning
57     //   radio.active
58     //   memory.bandwidths
59     //   wifi.batchedscan
60     //   wifi.scan
61     //   wifi.on
62     //   wifi.active
63     //   wifi.controller.tx_levels
64 
65     private static Pattern RE_CLUSTER_POWER = Pattern.compile("cpu.cluster_power.cluster([0-9]*)");
66     private static Pattern RE_CORE_SPEEDS = Pattern.compile("cpu.core_speeds.cluster([0-9]*)");
67     private static Pattern RE_CORE_POWER = Pattern.compile("cpu.core_power.cluster([0-9]*)");
68 
69     private HashMap<Component, ComponentProfile> mComponents = new HashMap();
70 
71     /**
72      * Which element we are currently parsing.
73      */
74     enum ElementState {
75         BEGIN,
76         TOP,
77         ITEM,
78         ARRAY,
79         VALUE
80     }
81 
82     /**
83      * Implements the reading and power model logic.
84      */
85     private static class Parser {
86         private final InputStream mStream;
87         private final PowerProfile mResult;
88 
89         // Builders for the ComponentProfiles.
90         private final AudioProfile mAudio = new AudioProfile();
91         private final BluetoothProfile mBluetooth = new BluetoothProfile();
92         private final CameraProfile mCamera = new CameraProfile();
93         private final CpuProfile.Builder mCpuBuilder = new CpuProfile.Builder();
94         private final FlashlightProfile mFlashlight = new FlashlightProfile();
95         private final GpsProfile.Builder mGpsBuilder = new GpsProfile.Builder();
96         private final ModemProfile.Builder mModemBuilder = new ModemProfile.Builder();
97         private final ScreenProfile mScreen = new ScreenProfile();
98         private final VideoProfile mVideo = new VideoProfile();
99         private final WifiProfile mWifi = new WifiProfile();
100 
101         /**
102          * Constructor to capture the parameters to read.
103          */
Parser(InputStream stream)104         Parser(InputStream stream) {
105             mStream = stream;
106             mResult = new PowerProfile();
107         }
108 
109         /**
110          * Read the stream, parse it, and apply the power model.
111          * Do not call this more than once.
112          */
parse()113         PowerProfile parse() throws ParseException {
114             final SAXParserFactory factory = SAXParserFactory.newInstance();
115             AndroidResourceHandler handler = null;
116             try {
117                 final SAXParser saxParser = factory.newSAXParser();
118 
119                 handler = new AndroidResourceHandler() {
120                     @Override
121                     public void onItem(Locator locator, String name, float value)
122                             throws SAXParseException {
123                         Parser.this.onItem(locator, name, value);
124                     }
125 
126                     @Override
127                     public void onArray(Locator locator, String name, float[] value)
128                             throws SAXParseException {
129                         Parser.this.onArray(locator, name, value);
130                     }
131                 };
132 
133                 saxParser.parse(mStream, handler);
134             } catch (ParserConfigurationException ex) {
135                 // Coding error, not runtime error.
136                 throw new RuntimeException(ex);
137             } catch (SAXParseException ex) {
138                 throw new ParseException(ex.getLineNumber(), ex.getMessage(), ex);
139             } catch (SAXException | IOException ex) {
140                 // Make a guess about the line number.
141                 throw new ParseException(handler.getLineNumber(), ex.getMessage(), ex);
142             }
143 
144             // TODO: This doesn't cover the multiple algorithms. Some refactoring will
145             // be necessary.
146             mResult.mComponents.put(Component.AUDIO, mAudio);
147             mResult.mComponents.put(Component.BLUETOOTH, mBluetooth);
148             mResult.mComponents.put(Component.CAMERA, mCamera);
149             mResult.mComponents.put(Component.CPU, mCpuBuilder.build());
150             mResult.mComponents.put(Component.FLASHLIGHT, mFlashlight);
151             mResult.mComponents.put(Component.GPS, mGpsBuilder.build());
152             mResult.mComponents.put(Component.MODEM, mModemBuilder.build());
153             mResult.mComponents.put(Component.SCREEN, mScreen);
154             mResult.mComponents.put(Component.VIDEO, mVideo);
155             mResult.mComponents.put(Component.WIFI, mWifi);
156 
157             return mResult;
158         }
159 
160         /**
161          * Handles an item tag in the power_profile.xml.
162          */
onItem(Locator locator, String name, float value)163         public void onItem(Locator locator, String name, float value) throws SAXParseException {
164             Integer index;
165             try {
166                 if ("ambient.on".equals(name)) {
167                     mScreen.ambientMa = value;
168                 } else if ("audio".equals(name)) {
169                     mAudio.onMa = value;
170                 } else if ("bluetooth.controller.idle".equals(name)) {
171                     mBluetooth.idleMa = value;
172                 } else if ("bluetooth.controller.rx".equals(name)) {
173                     mBluetooth.rxMa = value;
174                 } else if ("bluetooth.controller.tx".equals(name)) {
175                     mBluetooth.txMa = value;
176                 } else if ("camera.avg".equals(name)) {
177                     mCamera.onMa = value;
178                 } else if ("camera.flashlight".equals(name)) {
179                     mFlashlight.onMa = value;
180                 } else if ("cpu.suspend".equals(name)) {
181                     mCpuBuilder.setSuspendMa(value);
182                 } else if ("cpu.idle".equals(name)) {
183                     mCpuBuilder.setIdleMa(value);
184                 } else if ("cpu.active".equals(name)) {
185                     mCpuBuilder.setActiveMa(value);
186                 } else if ((index = matchIndexedRegex(locator, RE_CLUSTER_POWER, name)) != null) {
187                     mCpuBuilder.setClusterPower(index, value);
188                 } else if ("gps.on".equals(name)) {
189                     mGpsBuilder.setOnMa(value);
190                 } else if ("modem.controller.sleep".equals(name)) {
191                     mModemBuilder.setSleepMa(value);
192                 } else if ("modem.controller.idle".equals(name)) {
193                     mModemBuilder.setIdleMa(value);
194                 } else if ("modem.controller.rx".equals(name)) {
195                     mModemBuilder.setRxMa(value);
196                 } else if ("radio.scanning".equals(name)) {
197                     mModemBuilder.setScanningMa(value);
198                 } else if ("screen.on".equals(name)) {
199                     mScreen.onMa = value;
200                 } else if ("screen.full".equals(name)) {
201                     mScreen.fullMa = value;
202                 } else if ("video".equals(name)) {
203                     mVideo.onMa = value;
204                 } else if ("wifi.controller.idle".equals(name)) {
205                     mWifi.idleMa = value;
206                 } else if ("wifi.controller.rx".equals(name)) {
207                     mWifi.rxMa = value;
208                 } else if ("wifi.controller.tx".equals(name)) {
209                     mWifi.txMa = value;
210                 } else {
211                     // TODO: Uncomment this when we have all of the items parsed.
212                     // throw new SAXParseException("Unhandled <item name=\"" + name + "\"> element",
213                     //        locator, ex);
214 
215                 }
216             } catch (ParseException ex) {
217                 throw new SAXParseException(ex.getMessage(), locator, ex);
218             }
219         }
220 
221         /**
222          * Handles an array tag in the power_profile.xml.
223          */
onArray(Locator locator, String name, float[] value)224         public void onArray(Locator locator, String name, float[] value) throws SAXParseException {
225             Integer index;
226             try {
227                 if ("cpu.clusters.cores".equals(name)) {
228                     mCpuBuilder.setCoreCount(Conversion.toIntArray(value));
229                 } else if ((index = matchIndexedRegex(locator, RE_CORE_SPEEDS, name)) != null) {
230                     mCpuBuilder.setCoreSpeeds(index, Conversion.toIntArray(value));
231                 } else if ((index = matchIndexedRegex(locator, RE_CORE_POWER, name)) != null) {
232                     mCpuBuilder.setCorePower(index, value);
233                 } else if ("gps.signalqualitybased".equals(name)) {
234                     mGpsBuilder.setSignalMa(value);
235                 } else if ("modem.controller.tx".equals(name)) {
236                     mModemBuilder.setTxMa(value);
237                 } else {
238                     // TODO: Uncomment this when we have all of the items parsed.
239                     // throw new SAXParseException("Unhandled <item name=\"" + name + "\"> element",
240                     //        locator, ex);
241                 }
242             } catch (ParseException ex) {
243                 throw new SAXParseException(ex.getMessage(), locator, ex);
244             }
245         }
246     }
247 
248     /**
249      * SAX XML handler that can parse the android resource files.
250      * In our case, all elements are floats.
251      */
252     abstract static class AndroidResourceHandler extends DefaultHandler {
253         /**
254          * The set of names already processed. Map of name to line number.
255          */
256         private HashMap<String,Integer> mAlreadySeen = new HashMap<String,Integer>();
257 
258         /**
259          * Where in the document we are parsing.
260          */
261         private Locator mLocator;
262 
263         /**
264          * Which element we are currently parsing.
265          */
266         private ElementState mState = ElementState.BEGIN;
267 
268         /**
269          * Saved name from item and array elements.
270          */
271         private String mName;
272 
273         /**
274          * The text that is currently being captured, or null if {@link #startCapturingText()}
275          * has not been called.
276          */
277         private StringBuilder mText;
278 
279         /**
280          * The array values that have been parsed so for for this array. Null if we are
281          * not inside an array tag.
282          */
283         private ArrayList<Float> mArray;
284 
285         /**
286          * Called when an item tag is encountered.
287          */
onItem(Locator locator, String name, float value)288         public abstract void onItem(Locator locator, String name, float value)
289                 throws SAXParseException;
290 
291         /**
292          * Called when an array is encountered.
293          */
onArray(Locator locator, String name, float[] value)294         public abstract void onArray(Locator locator, String name, float[] value)
295                 throws SAXParseException;
296 
297         /**
298          * If we have a Locator set, return the line number, otherwise return 0.
299          */
getLineNumber()300         public int getLineNumber() {
301             return mLocator != null ? mLocator.getLineNumber() : 0;
302         }
303 
304         /**
305          * Handle setting the parse location object.
306          */
setDocumentLocator(Locator locator)307         public void setDocumentLocator(Locator locator) {
308             mLocator = locator;
309         }
310 
311         /**
312          * Handle beginning of an element.
313          *
314          * @param ns Namespace uri
315          * @param ln Local name (inside namespace)
316          * @param element Tag name
317          */
318         @Override
startElement(String ns, String ln, String element, Attributes attr)319         public void startElement(String ns, String ln, String element,
320                 Attributes attr) throws SAXException {
321             switch (mState) {
322                 case BEGIN:
323                     // Outer element, we don't care the tag name.
324                     mState = ElementState.TOP;
325                     return;
326                 case TOP:
327                     if ("item".equals(element)) {
328                         mState = ElementState.ITEM;
329                         saveNameAttribute(attr);
330                         startCapturingText();
331                         return;
332                     } else if ("array".equals(element)) {
333                         mState = ElementState.ARRAY;
334                         mArray = new ArrayList<Float>();
335                         saveNameAttribute(attr);
336                         return;
337                     }
338                     break;
339                 case ARRAY:
340                     if ("value".equals(element)) {
341                         mState = ElementState.VALUE;
342                         startCapturingText();
343                         return;
344                     }
345                     break;
346             }
347             throw new SAXParseException("unexpected element: '" + element + "'", mLocator);
348         }
349 
350         /**
351          * Handle end of an element.
352          *
353          * @param ns Namespace uri
354          * @param ln Local name (inside namespace)
355          * @param element Tag name
356          */
357         @Override
endElement(String ns, String ln, String element)358         public void endElement(String ns, String ln, String element) throws SAXException {
359             switch (mState) {
360                 case ITEM: {
361                     float value = parseFloat(finishCapturingText());
362                     mState = ElementState.TOP;
363                     onItem(mLocator, mName, value);
364                     break;
365                 }
366                 case ARRAY: {
367                     final int N = mArray.size();
368                     float[] values = new float[N];
369                     for (int i=0; i<N; i++) {
370                         values[i] = mArray.get(i);
371                     }
372                     mArray = null;
373                     mState = ElementState.TOP;
374                     onArray(mLocator, mName, values);
375                     break;
376                 }
377                 case VALUE: {
378                     mArray.add(parseFloat(finishCapturingText()));
379                     mState = ElementState.ARRAY;
380                     break;
381                 }
382             }
383         }
384 
385         /**
386          * Interstitial text received.
387          *
388          * @throws SAXException if there shouldn't be non-whitespace text here
389          */
390         @Override
characters(char text[], int start, int length)391         public void characters(char text[], int start, int length) throws SAXException {
392             if (mText == null && length > 0 && !isWhitespace(text, start, length)) {
393                 throw new SAXParseException("unexpected text: '"
394                         + firstLine(text, start, length).trim() + "'", mLocator);
395             }
396             if (mText != null) {
397                 mText.append(text, start, length);
398             }
399         }
400 
401         /**
402          * Begin collecting text from inside an element.
403          */
startCapturingText()404         private void startCapturingText() {
405             if (mText != null) {
406                 throw new RuntimeException("ASSERTION FAILED: Shouldn't be already capturing"
407                         + " text. mState=" + mState.name()
408                         + " line=" + mLocator.getLineNumber()
409                         + " column=" + mLocator.getColumnNumber());
410             }
411             mText = new StringBuilder();
412         }
413 
414         /**
415          * Stop capturing text from inside an element.
416          *
417          * @return the captured text
418          */
finishCapturingText()419         private String finishCapturingText() {
420             if (mText == null) {
421                 throw new RuntimeException("ASSERTION FAILED: Should already be capturing"
422                         + " text. mState=" + mState.name()
423                         + " line=" + mLocator.getLineNumber()
424                         + " column=" + mLocator.getColumnNumber());
425             }
426             final String result = mText.toString().trim();
427             mText = null;
428             return result;
429         }
430 
431         /**
432          * Get the "name" attribute.
433          *
434          * @throws SAXParseException if the name attribute is not present or if
435          * the name has already been seen in the file.
436          */
saveNameAttribute(Attributes attr)437         private void saveNameAttribute(Attributes attr) throws SAXParseException {
438             final String name = attr.getValue("name");
439             if (name == null) {
440                 throw new SAXParseException("expected 'name' attribute", mLocator);
441             }
442             Integer prev = mAlreadySeen.put(name, mLocator.getLineNumber());
443             if (prev != null) {
444                 throw new SAXParseException("name '" + name + "' already seen on line: " + prev,
445                         mLocator);
446             }
447             mName = name;
448         }
449 
450         /**
451          * Gets the float value of the string.
452          *
453          * @throws SAXParseException if 'text' can't be parsed as a float.
454          */
parseFloat(String text)455         private float parseFloat(String text) throws SAXParseException {
456             try {
457                 return Float.parseFloat(text);
458             } catch (NumberFormatException ex) {
459                 throw new SAXParseException("not a valid float value: '" + text + "'",
460                         mLocator, ex);
461             }
462         }
463     }
464 
465     /**
466      * Return whether the given substring is all whitespace.
467      */
isWhitespace(char[] text, int start, int length)468     private static boolean isWhitespace(char[] text, int start, int length) {
469         for (int i = start; i < (start + length); i++) {
470             if (!Character.isSpace(text[i])) {
471                 return false;
472             }
473         }
474         return true;
475     }
476 
477     /**
478      * Return the contents of text up to the first newline.
479      */
firstLine(char[] text, int start, int length)480     private static String firstLine(char[] text, int start, int length) {
481         // TODO: The line number will be wrong if we skip preceeding blank lines.
482         while (length > 0) {
483             if (Character.isSpace(text[start])) {
484                 start++;
485                 length--;
486             }
487         }
488         int newlen = 0;
489         for (; newlen < length; newlen++) {
490             final char c = text[newlen];
491             if (c == '\n' || c == '\r') {
492                 break;
493             }
494         }
495         return new String(text, start, newlen);
496     }
497 
498     /**
499      * If the pattern matches, return the first group of that as an Integer.
500      * If not return null.
501      */
matchIndexedRegex(Locator locator, Pattern pattern, String text)502     private static Integer matchIndexedRegex(Locator locator, Pattern pattern, String text)
503             throws SAXParseException {
504         final Matcher m = pattern.matcher(text);
505         if (m.matches()) {
506             try {
507                 return Integer.parseInt(m.group(1));
508             } catch (NumberFormatException ex) {
509                 throw new SAXParseException("Invalid field name: '" + text + "'", locator, ex);
510             }
511         } else {
512             return null;
513         }
514     }
515 
parse(InputStream stream)516     public static PowerProfile parse(InputStream stream) throws ParseException {
517         return (new Parser(stream)).parse();
518     }
519 
PowerProfile()520     private PowerProfile() {
521     }
522 
getComponent(Component component)523     public ComponentProfile getComponent(Component component) {
524         return mComponents.get(component);
525     }
526 
527 }
528