1 /* 2 * Copyright (C) 2008 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.eventanalyzer; 18 19 import com.android.ddmlib.AdbCommandRejectedException; 20 import com.android.ddmlib.AndroidDebugBridge; 21 import com.android.ddmlib.IDevice; 22 import com.android.ddmlib.Log; 23 import com.android.ddmlib.TimeoutException; 24 import com.android.ddmlib.Log.ILogOutput; 25 import com.android.ddmlib.Log.LogLevel; 26 import com.android.ddmlib.log.EventContainer; 27 import com.android.ddmlib.log.EventLogParser; 28 import com.android.ddmlib.log.InvalidTypeException; 29 import com.android.ddmlib.log.LogReceiver; 30 import com.android.ddmlib.log.LogReceiver.ILogListener; 31 import com.android.ddmlib.log.LogReceiver.LogEntry; 32 33 import java.io.BufferedReader; 34 import java.io.BufferedWriter; 35 import java.io.File; 36 import java.io.FileInputStream; 37 import java.io.FileWriter; 38 import java.io.FilenameFilter; 39 import java.io.IOException; 40 import java.io.InputStreamReader; 41 import java.util.ArrayList; 42 import java.util.Collections; 43 import java.util.Set; 44 import java.util.TreeMap; 45 46 /** 47 * Connects to a device using ddmlib and analyze its event log. 48 */ 49 public class EventAnalyzer implements ILogListener { 50 51 private final static int TAG_ACTIVITY_LAUNCH_TIME = 30009; 52 private final static char DATA_SEPARATOR = ','; 53 54 private final static String CVS_EXT = ".csv"; 55 private final static String TAG_FILE_EXT = ".tag"; //$NON-NLS-1$ 56 57 private EventLogParser mParser; 58 private TreeMap<String, ArrayList<Long>> mLaunchMap = new TreeMap<String, ArrayList<Long>>(); 59 60 String mInputTextFile = null; 61 String mInputBinaryFile = null; 62 String mInputDevice = null; 63 String mInputFolder = null; 64 String mAlternateTagFile = null; 65 String mOutputFile = null; 66 main(String[] args)67 public static void main(String[] args) { 68 new EventAnalyzer().run(args); 69 } 70 run(String[] args)71 private void run(String[] args) { 72 if (args.length == 0) { 73 printUsageAndQuit(); 74 } 75 76 int index = 0; 77 do { 78 String argument = args[index++]; 79 80 if ("-s".equals(argument)) { 81 checkInputValidity("-s"); 82 83 if (index == args.length) { 84 printUsageAndQuit(); 85 } 86 87 mInputDevice = args[index++]; 88 } else if ("-fb".equals(argument)) { 89 checkInputValidity("-fb"); 90 91 if (index == args.length) { 92 printUsageAndQuit(); 93 } 94 95 mInputBinaryFile = args[index++]; 96 } else if ("-ft".equals(argument)) { 97 checkInputValidity("-ft"); 98 99 if (index == args.length) { 100 printUsageAndQuit(); 101 } 102 103 mInputTextFile = args[index++]; 104 } else if ("-F".equals(argument)) { 105 checkInputValidity("-F"); 106 107 if (index == args.length) { 108 printUsageAndQuit(); 109 } 110 111 mInputFolder = args[index++]; 112 } else if ("-t".equals(argument)) { 113 if (index == args.length) { 114 printUsageAndQuit(); 115 } 116 117 mAlternateTagFile = args[index++]; 118 } else { 119 // get the filepath and break. 120 mOutputFile = argument; 121 122 // should not be any other device. 123 if (index < args.length) { 124 printAndExit("Too many arguments!", false /* terminate */); 125 } 126 } 127 } while (index < args.length); 128 129 if ((mInputTextFile == null && mInputBinaryFile == null && mInputFolder == null && 130 mInputDevice == null)) { 131 printUsageAndQuit(); 132 } 133 134 File outputParent = new File(mOutputFile).getParentFile(); 135 if (outputParent == null || outputParent.isDirectory() == false) { 136 printAndExit(String.format("%1$s is not a valid ouput file", mOutputFile), 137 false /* terminate */); 138 } 139 140 // redirect the log output to /dev/null 141 Log.setLogOutput(new ILogOutput() { 142 public void printAndPromptLog(LogLevel logLevel, String tag, String message) { 143 // pass 144 } 145 146 public void printLog(LogLevel logLevel, String tag, String message) { 147 // pass 148 } 149 }); 150 151 try { 152 if (mInputBinaryFile != null) { 153 parseBinaryLogFile(); 154 } else if (mInputTextFile != null) { 155 parseTextLogFile(mInputTextFile); 156 } else if (mInputFolder != null) { 157 parseFolder(mInputFolder); 158 } else if (mInputDevice != null) { 159 parseLogFromDevice(); 160 } 161 162 // analyze the data gathered by the parser methods 163 analyzeData(); 164 } catch (Exception e) { 165 e.printStackTrace(); 166 } 167 } 168 169 /** 170 * Parses a binary event log file located at {@link #mInputBinaryFile}. 171 * @throws IOException 172 */ parseBinaryLogFile()173 private void parseBinaryLogFile() throws IOException { 174 mParser = new EventLogParser(); 175 176 String tagFile = mInputBinaryFile + TAG_FILE_EXT; 177 if (mParser.init(tagFile) == false) { 178 // if we have an alternate location 179 if (mAlternateTagFile != null) { 180 if (mParser.init(mAlternateTagFile) == false) { 181 printAndExit("Failed to get event tags from " + mAlternateTagFile, 182 false /* terminate*/); 183 } 184 } else { 185 printAndExit("Failed to get event tags from " + tagFile, false /* terminate*/); 186 } 187 } 188 189 LogReceiver receiver = new LogReceiver(this); 190 191 byte[] buffer = new byte[256]; 192 193 FileInputStream fis = new FileInputStream(mInputBinaryFile); 194 195 int count; 196 while ((count = fis.read(buffer)) != -1) { 197 receiver.parseNewData(buffer, 0, count); 198 } 199 } 200 201 /** 202 * Parse a text Log file. 203 * @param filePath the location of the file. 204 * @throws IOException 205 */ parseTextLogFile(String filePath)206 private void parseTextLogFile(String filePath) throws IOException { 207 mParser = new EventLogParser(); 208 209 String tagFile = filePath + TAG_FILE_EXT; 210 if (mParser.init(tagFile) == false) { 211 // if we have an alternate location 212 if (mAlternateTagFile != null) { 213 if (mParser.init(mAlternateTagFile) == false) { 214 printAndExit("Failed to get event tags from " + mAlternateTagFile, 215 false /* terminate*/); 216 } 217 } else { 218 printAndExit("Failed to get event tags from " + tagFile, false /* terminate*/); 219 } 220 } 221 222 // read the lines from the file and process them. 223 BufferedReader reader = new BufferedReader( 224 new InputStreamReader(new FileInputStream(filePath))); 225 226 String line; 227 while ((line = reader.readLine()) != null) { 228 processEvent(mParser.parse(line)); 229 } 230 } 231 parseLogFromDevice()232 private void parseLogFromDevice() throws IOException, TimeoutException, 233 AdbCommandRejectedException { 234 // init the lib 235 AndroidDebugBridge.init(false /* debugger support */); 236 237 try { 238 AndroidDebugBridge bridge = AndroidDebugBridge.createBridge(); 239 240 // we can't just ask for the device list right away, as the internal thread getting 241 // them from ADB may not be done getting the first list. 242 // Since we don't really want getDevices() to be blocking, we wait here manually. 243 int count = 0; 244 while (bridge.hasInitialDeviceList() == false) { 245 try { 246 Thread.sleep(100); 247 count++; 248 } catch (InterruptedException e) { 249 // pass 250 } 251 252 // let's not wait > 10 sec. 253 if (count > 100) { 254 printAndExit("Timeout getting device list!", true /* terminate*/); 255 } 256 } 257 258 // now get the devices 259 IDevice[] devices = bridge.getDevices(); 260 261 for (IDevice device : devices) { 262 if (device.getSerialNumber().equals(mInputDevice)) { 263 grabLogFrom(device); 264 return; 265 } 266 } 267 268 System.err.println("Could not find " + mInputDevice); 269 } finally { 270 AndroidDebugBridge.terminate(); 271 } 272 } 273 274 /** 275 * Parses the log files located in the folder, and its sub-folders. 276 * @param folderPath the path to the folder. 277 */ parseFolder(String folderPath)278 private void parseFolder(String folderPath) { 279 File f = new File(folderPath); 280 if (f.isDirectory() == false) { 281 printAndExit(String.format("%1$s is not a valid folder", folderPath), 282 false /* terminate */); 283 } 284 285 String[] files = f.list(new FilenameFilter() { 286 public boolean accept(File dir, String name) { 287 name = name.toLowerCase(); 288 return name.endsWith(".tag") == false; 289 } 290 }); 291 292 for (String file : files) { 293 try { 294 f = new File(folderPath + File.separator + file); 295 if (f.isDirectory()) { 296 parseFolder(f.getAbsolutePath()); 297 } else { 298 parseTextLogFile(f.getAbsolutePath()); 299 } 300 } catch (IOException e) { 301 // ignore this file. 302 } 303 } 304 } 305 grabLogFrom(IDevice device)306 private void grabLogFrom(IDevice device) throws IOException, TimeoutException, 307 AdbCommandRejectedException { 308 mParser = new EventLogParser(); 309 if (mParser.init(device) == false) { 310 printAndExit("Failed to get event-log-tags from " + device.getSerialNumber(), 311 true /* terminate*/); 312 } 313 314 LogReceiver receiver = new LogReceiver(this); 315 316 device.runEventLogService(receiver); 317 } 318 319 /** 320 * Analyze the data and writes it to {@link #mOutputFile} 321 * @throws IOException 322 */ analyzeData()323 private void analyzeData() throws IOException { 324 BufferedWriter writer = null; 325 try { 326 // make sure the file name has the proper extension. 327 if (mOutputFile.toLowerCase().endsWith(CVS_EXT) == false) { 328 mOutputFile = mOutputFile + CVS_EXT; 329 } 330 331 writer = new BufferedWriter(new FileWriter(mOutputFile)); 332 StringBuilder builder = new StringBuilder(); 333 334 // write the list of launch start. One column per activity. 335 Set<String> activities = mLaunchMap.keySet(); 336 337 // write the column headers. 338 for (String activity : activities) { 339 builder.append(activity).append(DATA_SEPARATOR); 340 } 341 writer.write(builder.append('\n').toString()); 342 343 // loop on the activities and write their values. 344 boolean moreValues = true; 345 int index = 0; 346 while (moreValues) { 347 moreValues = false; 348 builder.setLength(0); 349 350 for (String activity : activities) { 351 // get the activity list. 352 ArrayList<Long> list = mLaunchMap.get(activity); 353 if (index < list.size()) { 354 moreValues = true; 355 builder.append(list.get(index).longValue()).append(DATA_SEPARATOR); 356 } else { 357 builder.append(DATA_SEPARATOR); 358 } 359 } 360 361 // write the line. 362 if (moreValues) { 363 writer.write(builder.append('\n').toString()); 364 } 365 366 index++; 367 } 368 369 // write per-activity stats. 370 for (String activity : activities) { 371 builder.setLength(0); 372 builder.append(activity).append(DATA_SEPARATOR); 373 374 // get the activity list. 375 ArrayList<Long> list = mLaunchMap.get(activity); 376 377 // sort the list 378 Collections.sort(list); 379 380 // write min/max 381 builder.append(list.get(0).longValue()).append(DATA_SEPARATOR); 382 builder.append(list.get(list.size()-1).longValue()).append(DATA_SEPARATOR); 383 384 // write median value 385 builder.append(list.get(list.size()/2).longValue()).append(DATA_SEPARATOR); 386 387 // compute and write average 388 long total = 0; // despite being encoded on a long, the values are low enough that 389 // a Long should be enough to compute the total 390 for (Long value : list) { 391 total += value.longValue(); 392 } 393 builder.append(total / list.size()).append(DATA_SEPARATOR); 394 395 // finally write the data. 396 writer.write(builder.append('\n').toString()); 397 } 398 } finally { 399 writer.close(); 400 } 401 } 402 403 /* 404 * (non-Javadoc) 405 * @see com.android.ddmlib.log.LogReceiver.ILogListener#newData(byte[], int, int) 406 */ newData(byte[] data, int offset, int length)407 public void newData(byte[] data, int offset, int length) { 408 // we ignore raw data. New entries are processed in #newEntry(LogEntry) 409 } 410 411 /* 412 * (non-Javadoc) 413 * @see com.android.ddmlib.log.LogReceiver.ILogListener#newEntry(com.android.ddmlib.log.LogReceiver.LogEntry) 414 */ newEntry(LogEntry entry)415 public void newEntry(LogEntry entry) { 416 // parse and process the entry data. 417 processEvent(mParser.parse(entry)); 418 } 419 processEvent(EventContainer event)420 private void processEvent(EventContainer event) { 421 if (event != null && event.mTag == TAG_ACTIVITY_LAUNCH_TIME) { 422 // get the activity name 423 try { 424 String name = event.getValueAsString(0); 425 426 // get the launch time 427 Object value = event.getValue(1); 428 if (value instanceof Long) { 429 addLaunchTime(name, (Long)value); 430 } 431 432 } catch (InvalidTypeException e) { 433 // Couldn't get the name as a string... 434 // Ignore this event. 435 } 436 } 437 } 438 addLaunchTime(String name, Long value)439 private void addLaunchTime(String name, Long value) { 440 ArrayList<Long> list = mLaunchMap.get(name); 441 442 if (list == null) { 443 list = new ArrayList<Long>(); 444 mLaunchMap.put(name, list); 445 } 446 447 list.add(value); 448 } 449 checkInputValidity(String option)450 private void checkInputValidity(String option) { 451 if (mInputTextFile != null || mInputBinaryFile != null) { 452 printAndExit(String.format("ERROR: %1$s cannot be used with an input file.", option), 453 false /* terminate */); 454 } else if (mInputFolder != null) { 455 printAndExit(String.format("ERROR: %1$s cannot be used with an input file.", option), 456 false /* terminate */); 457 } else if (mInputDevice != null) { 458 printAndExit(String.format("ERROR: %1$s cannot be used with an input device serial number.", 459 option), false /* terminate */); 460 } 461 } 462 printUsageAndQuit()463 private static void printUsageAndQuit() { 464 // 80 cols marker: 01234567890123456789012345678901234567890123456789012345678901234567890123456789 465 System.out.println("Usage:"); 466 System.out.println(" eventanalyzer [-t <TAG_FILE>] <SOURCE> <OUTPUT>"); 467 System.out.println(""); 468 System.out.println("Possible sources:"); 469 System.out.println(" -fb <file> The path to a binary event log, gathered by dumpeventlog"); 470 System.out.println(" -ft <file> The path to a text event log, gathered by adb logcat -b events"); 471 System.out.println(" -F <folder> The path to a folder containing multiple text log files."); 472 System.out.println(" -s <serial> The serial number of the Device to grab the event log from."); 473 System.out.println("Options:"); 474 System.out.println(" -t <file> The path to tag file to use in case the one associated with"); 475 System.out.println(" the source is missing"); 476 477 System.exit(1); 478 } 479 480 printAndExit(String message, boolean terminate)481 private static void printAndExit(String message, boolean terminate) { 482 System.out.println(message); 483 if (terminate) { 484 AndroidDebugBridge.terminate(); 485 } 486 System.exit(1); 487 } 488 } 489