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