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