Home | History | Annotate | Download | only in tests
      1 /*
      2  * Copyright (C) 2015 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.performance.tests;
     18 
     19 import com.android.ddmlib.IDevice;
     20 import com.android.ddmlib.testrunner.IRemoteAndroidTestRunner;
     21 import com.android.ddmlib.testrunner.RemoteAndroidTestRunner;
     22 import com.android.ddmlib.testrunner.TestResult;
     23 import com.android.tradefed.config.Option;
     24 import com.android.tradefed.config.Option.Importance;
     25 import com.android.tradefed.device.DeviceNotAvailableException;
     26 import com.android.tradefed.device.ITestDevice;
     27 import com.android.tradefed.device.LogcatReceiver;
     28 import com.android.tradefed.log.LogUtil.CLog;
     29 import com.android.tradefed.result.CollectingTestListener;
     30 import com.android.tradefed.result.FileInputStreamSource;
     31 import com.android.tradefed.result.ITestInvocationListener;
     32 import com.android.tradefed.result.InputStreamSource;
     33 import com.android.tradefed.result.LogDataType;
     34 import com.android.tradefed.testtype.IDeviceTest;
     35 import com.android.tradefed.testtype.IRemoteTest;
     36 import com.android.tradefed.util.FileUtil;
     37 import com.android.tradefed.util.StreamUtil;
     38 
     39 import org.junit.Assert;
     40 
     41 import java.io.BufferedReader;
     42 import java.io.File;
     43 import java.io.FileNotFoundException;
     44 import java.io.FileReader;
     45 import java.io.IOException;
     46 import java.io.InputStreamReader;
     47 import java.util.ArrayList;
     48 import java.util.Collection;
     49 import java.util.HashMap;
     50 import java.util.HashSet;
     51 import java.util.LinkedList;
     52 import java.util.List;
     53 import java.util.Map;
     54 import java.util.Set;
     55 import java.util.regex.Matcher;
     56 import java.util.regex.Pattern;
     57 
     58 /**
     59  * To test the app launch performance for the list of activities present in the given target package
     60  * or the custom list of activities present in the target package.Activities are launched number of
     61  * times present in the launch count. Launch time is analyzed from the logcat data and more detailed
     62  * timing(section names) is analyzed from the atrace files captured when launching each activity.
     63  */
     64 public class HermeticLaunchTest implements IRemoteTest, IDeviceTest {
     65 
     66     private static enum AtraceSectionOptions {
     67         LAYOUT("layout"),
     68         DRAW("draw"),
     69         BINDAPPLICATION("bindApplication"),
     70         ACTIVITYSTART("activityStart"),
     71         ONCREATE("onCreate");
     72 
     73         private final String name;
     74 
     75         private AtraceSectionOptions(String s) {
     76             this.name = s;
     77         }
     78 
     79         @Override
     80         public String toString() {
     81             return name;
     82         }
     83     }
     84 
     85     private static final String TOTALLAUNCHTIME = "totalLaunchTime";
     86     private static final String LOGCAT_CMD = "logcat -v threadtime ActivityManager:* *:s";
     87     private static final String LAUNCH_PREFIX="^\\d*-\\d*\\s*\\d*:\\d*:\\d*.\\d*\\s*\\d*\\s*"
     88             + "\\d*\\s*I ActivityManager: Displayed\\s*";
     89     private static final String LAUNCH_SUFFIX=":\\s*\\+(?<launchtime>.[a-zA-Z\\d]*)\\s*"
     90             + "(?<totallaunch>.*)\\s*$";
     91     private static final Pattern LAUNCH_ENTRY=Pattern.compile("^\\d*-\\d*\\s*\\d*:\\d*:\\d*."
     92             + "\\d*\\s*\\d*\\s*\\d*\\s*I ActivityManager: Displayed\\s*(?<launchinfo>.*)\\s*$");
     93     private static final Pattern TRACE_ENTRY1 = Pattern.compile(
     94             "^[^-]*-(?<tid>\\d+)\\s+\\[\\d+\\]\\s+\\S{4}\\s+" +
     95             "(?<secs>\\d+)\\.(?<usecs>\\d+):\\s+(?<function>.*)\\s*$");
     96     private static final Pattern TRACE_ENTRY2 = Pattern.compile(
     97             "^[^-]*-(?<tid>\\d+)\\s*\\(\\s*\\d*-*\\)\\s*\\[\\d+\\]\\s+\\S{4}\\s+" +
     98             "(?<secs>\\d+)\\.(?<usecs>\\d+):\\s+(?<function>.*)\\s*$");
     99     private static final Pattern ATRACE_BEGIN = Pattern
    100             .compile("tracing_mark_write: B\\|(?<pid>\\d+)\\|(?<name>.+)");
    101     private static final Pattern ATRACE_END = Pattern.compile("tracing_mark_write: E");
    102     private static final Pattern ATRACE_COUNTER = Pattern
    103             .compile("tracing_mark_write: C\\|(?<pid>\\d+)\\|(?<name>[^|]+)\\|(?<value>\\d+)");
    104     private static final Pattern ATRACE_HEADER_ENTRIES = Pattern
    105             .compile("# entries-in-buffer/entries-written:\\s+(?<buffered>\\d+)/"
    106                     + "(?<written>\\d+)\\s+#P:\\d+\\s*");
    107     private static final int LOGCAT_SIZE = 20971520; //20 mb
    108     private static final long SEC_TO_MILLI = 1000;
    109     private static final long MILLI_TO_MICRO = 1000;
    110 
    111     @Option(name = "runner", description = "The instrumentation test runner class name to use.")
    112     private String mRunnerName = "android.support.test.runner.AndroidJUnitRunner";
    113 
    114     @Option(name = "package", shortName = 'p',
    115             description = "The manifest package name of the Android test application to run.",
    116             importance = Importance.IF_UNSET)
    117     private String mPackageName = "com.android.performanceapp.tests";
    118 
    119     @Option(name = "target-package", description = "package which contains all the "
    120             + "activities to launch")
    121     private String mtargetPackage = null;
    122 
    123     @Option(name = "activity-names", description = "Fully qualified activity "
    124             + "names separated by comma"
    125             + "If not set then all the activities will be included for launching")
    126     private String mactivityNames = "";
    127 
    128     @Option(name = "launch-count", description = "number of time to launch the each activity")
    129     private int mlaunchCount = 10;
    130 
    131     @Option(name = "save-atrace", description = "Upload the atrace file in permanent storage")
    132     private boolean msaveAtrace = false;
    133 
    134     @Option(name = "atrace-section", description = "Section to be parsed from atrace file. "
    135             + "This option can be repeated")
    136     private Set<AtraceSectionOptions> mSectionOptionSet = new HashSet<>();
    137 
    138     private ITestDevice mDevice = null;
    139     private IRemoteAndroidTestRunner mRunner;
    140     private LogcatReceiver mLogcat;
    141     private Set<String> mSectionSet = new HashSet<>();
    142     private Map<String, String> mActivityTraceFileMap;
    143     private Map<String, Map<String, String>> mActivityTimeResultMap = new HashMap<>();
    144     private Map<String,String> activityErrMsg=new HashMap<>();
    145 
    146     @Override
    147     public void run(ITestInvocationListener listener)
    148             throws DeviceNotAvailableException {
    149 
    150         mLogcat = new LogcatReceiver(getDevice(), LOGCAT_CMD, LOGCAT_SIZE, 0);
    151         mLogcat.start();
    152         try {
    153             if (mSectionOptionSet.isEmpty()) {
    154                 // Default sections
    155                 mSectionOptionSet.add(AtraceSectionOptions.LAYOUT);
    156                 mSectionOptionSet.add(AtraceSectionOptions.DRAW);
    157                 mSectionOptionSet.add(AtraceSectionOptions.BINDAPPLICATION);
    158                 mSectionOptionSet.add(AtraceSectionOptions.ACTIVITYSTART);
    159                 mSectionOptionSet.add(AtraceSectionOptions.ONCREATE);
    160             } else if (mSectionOptionSet.contains(AtraceSectionOptions.LAYOUT)) {
    161                 // If layout is added, draw should also be included
    162                 mSectionOptionSet.add(AtraceSectionOptions.DRAW);
    163             }
    164 
    165             for (AtraceSectionOptions sectionOption : mSectionOptionSet) {
    166                 mSectionSet.add(sectionOption.toString());
    167             }
    168 
    169             //Remove if there is already existing atrace_logs folder
    170             mDevice.executeShellCommand("rm -rf ${EXTERNAL_STORAGE}/atrace_logs");
    171 
    172             mRunner = createRemoteAndroidTestRunner(mPackageName, mRunnerName,
    173                         mDevice.getIDevice());
    174             CollectingTestListener collectingListener = new CollectingTestListener();
    175             mDevice.runInstrumentationTests(mRunner, collectingListener);
    176 
    177             Collection<TestResult> testResultsCollection = collectingListener
    178                         .getCurrentRunResults().getTestResults().values();
    179             List<TestResult> testResults = new ArrayList<>(
    180                         testResultsCollection);
    181             /*
    182              * Expected Metrics : Map of <activity name>=<comma separated list of atrace file names in
    183              * external storage of the device>
    184              */
    185             mActivityTraceFileMap = testResults.get(0).getMetrics();
    186             Assert.assertTrue("Unable to get the path to the trace files stored in the device",
    187                     (mActivityTraceFileMap != null && !mActivityTraceFileMap.isEmpty()));
    188 
    189             // Analyze the logcat data to get total launch time
    190             analyzeLogCatData(mActivityTraceFileMap.keySet());
    191         } finally {
    192             // Stop the logcat
    193             mLogcat.stop();
    194         }
    195 
    196         // Analyze the atrace data to get bindApplication,activityStart etc..
    197         analyzeAtraceData(listener);
    198 
    199         // Report the metrics to dashboard
    200         reportMetrics(listener);
    201     }
    202 
    203     /**
    204      * Report run metrics by creating an empty test run to stick them in.
    205      * @param listener The {@link ITestInvocationListener} of test results
    206      */
    207     private void reportMetrics(ITestInvocationListener listener) {
    208         for (String activityName : mActivityTimeResultMap.keySet()) {
    209             // Get the activity name alone from pkgname.activityname
    210             String[] activityNameSplit = activityName.split("\\.");
    211             if (!activityErrMsg.containsKey(activityName)) {
    212                 Map<String, String> activityMetrics = mActivityTimeResultMap.get(activityName);
    213                 if (activityMetrics != null && !activityMetrics.isEmpty()) {
    214                     CLog.v("Metrics for the activity : %s", activityName);
    215                     for (String sectionName : activityMetrics.keySet()) {
    216                         CLog.v(String.format("Section name : %s - Time taken : %s",
    217                                 sectionName, activityMetrics.get(sectionName)));
    218                     }
    219                     listener.testRunStarted(
    220                             activityNameSplit[activityNameSplit.length - 1].trim(), 0);
    221                     listener.testRunEnded(0, activityMetrics);
    222                 }
    223             } else {
    224                 listener.testRunStarted(
    225                         activityNameSplit[activityNameSplit.length - 1].trim(), 0);
    226                 listener.testRunFailed(activityErrMsg.get(activityName));
    227             }
    228         }
    229     }
    230 
    231     /**
    232      * Method to create the runner with given list of arguments
    233      * @return the {@link IRemoteAndroidTestRunner} to use.
    234      * @throws DeviceNotAvailableException
    235      */
    236     IRemoteAndroidTestRunner createRemoteAndroidTestRunner(String packageName,
    237             String runnerName, IDevice device)
    238             throws DeviceNotAvailableException {
    239         RemoteAndroidTestRunner runner = new RemoteAndroidTestRunner(
    240                 packageName, runnerName, device);
    241         runner.addInstrumentationArg("targetpackage", mtargetPackage);
    242         runner.addInstrumentationArg("launchcount", mlaunchCount + "");
    243         if (mactivityNames != null && !mactivityNames.isEmpty()) {
    244             runner.addInstrumentationArg("activitylist", mactivityNames);
    245         }
    246         return runner;
    247     }
    248 
    249     /**
    250      * To analyze the log cat data to get the display time reported by activity manager during the
    251      * launches
    252      * activitySet is set of activityNames returned as a part of testMetrics from the device
    253      */
    254     public void analyzeLogCatData(Set<String> activitySet) {
    255         Map<String, List<Integer>> amLaunchTimes = new HashMap<>();
    256 
    257         Map<Pattern, String> activityPatternMap = new HashMap<>();
    258         Matcher match = null;
    259         String line;
    260 
    261         /*
    262          * Sample line format in logcat
    263          * 06-17 16:55:49.6 60 642 I ActivityManager: Displayed pkg/.activity: +Tms (total +9s9ms)
    264          */
    265         for (String activityName : activitySet) {
    266             int lastIndex = activityName.lastIndexOf(".");
    267             /*
    268              * actvitySet has set of activity names in the format packageName.activityName
    269              * logcat has the format packageName/.activityName --> activityAlias
    270              */
    271             String activityAlias = activityName.subSequence(0, lastIndex)
    272                     + "/" + activityName.subSequence(lastIndex, activityName.length());
    273             String finalPattern = LAUNCH_PREFIX + activityAlias + LAUNCH_SUFFIX;
    274             activityPatternMap.put(Pattern.compile(finalPattern),
    275                     activityName);
    276         }
    277 
    278         try (InputStreamSource input = mLogcat.getLogcatData();
    279                 BufferedReader br =
    280                         new BufferedReader(new InputStreamReader(input.createInputStream()))) {
    281             while ((line = br.readLine()) != null) {
    282                 /*
    283                  * Launch entry needed otherwise we will end up in comparing all the lines for all
    284                  * the patterns
    285                  */
    286                 if ((match = matches(LAUNCH_ENTRY, line)) != null) {
    287                     for (Pattern pattern : activityPatternMap.keySet()) {
    288                         if ((match = matches(pattern, line)) != null) {
    289                             CLog.v("Launch Info : %s", line);
    290                             int displayTimeInMs = extractLaunchTime(match.group("launchtime"));
    291                             String activityName = activityPatternMap.get(pattern);
    292                             if (amLaunchTimes.containsKey(activityName)) {
    293                                 amLaunchTimes.get(activityName).add(displayTimeInMs);
    294                             } else {
    295                                 List<Integer> launchTimes = new ArrayList<>();
    296                                 launchTimes.add(displayTimeInMs);
    297                                 amLaunchTimes.put(activityName, launchTimes);
    298                             }
    299                         }
    300                     }
    301                 }
    302             }
    303         } catch (IOException io) {
    304             CLog.e(io);
    305         }
    306 
    307         // Verify logcat data
    308         for (String activityName : amLaunchTimes.keySet()) {
    309             Assert.assertEquals("Data lost for launch time for the activity :"
    310                     + activityName, amLaunchTimes.get(activityName).size(), mlaunchCount);
    311         }
    312 
    313         /*
    314          * Extract and store the average launch time data reported by activity manager for each
    315          * activity
    316          */
    317         for (String activityName : amLaunchTimes.keySet()) {
    318             Double totalTime = 0d;
    319             for (Integer launchTime : amLaunchTimes.get(activityName)) {
    320                 totalTime += launchTime;
    321             }
    322             Double averageTime = Double.valueOf(totalTime / amLaunchTimes.get(activityName).size());
    323             if (mActivityTimeResultMap.containsKey(activityName)) {
    324                 mActivityTimeResultMap.get(activityName).put(TOTALLAUNCHTIME,
    325                         String.format("%.2f", averageTime));
    326             } else {
    327                 Map<String, String> launchTime = new HashMap<>();
    328                 launchTime.put(TOTALLAUNCHTIME,
    329                         String.format("%.2f", averageTime));
    330                 mActivityTimeResultMap.put(activityName, launchTime);
    331             }
    332         }
    333     }
    334 
    335     /**
    336      * To extract the launch time displayed in given line
    337      *
    338      * @param duration
    339      * @return
    340      */
    341     public int extractLaunchTime(String duration) {
    342         String formattedString = duration.replace("ms", "");
    343         if (formattedString.contains("s")) {
    344             String[] splitString = formattedString.split("s");
    345             int finalTimeInMs = Integer.parseInt(splitString[0]) * 1000;
    346             finalTimeInMs = finalTimeInMs + Integer.parseInt(splitString[1]);
    347             return finalTimeInMs;
    348         } else {
    349             return Integer.parseInt(formattedString);
    350         }
    351     }
    352 
    353     /**
    354      * To analyze the trace data collected in the device during each activity launch.
    355      */
    356     public void analyzeAtraceData(ITestInvocationListener listener) throws DeviceNotAvailableException {
    357         for (String activityName : mActivityTraceFileMap.keySet()) {
    358             try {
    359                 // Get the list of associated filenames for given activity
    360                 String filePathAll = mActivityTraceFileMap.get(activityName);
    361                 Assert.assertNotNull(
    362                         String.format("Unable to find trace file paths for activity : %s",
    363                                 activityName), filePathAll);
    364                 String[] filePaths = filePathAll.split(",");
    365                 Assert.assertEquals(String.format("Unable to find file path for all the launches "
    366                         + "for the activity :%s", activityName), filePaths.length, mlaunchCount);
    367                 // Pull and parse the info
    368                 List<Map<String, List<SectionPeriod>>> mutipleLaunchTraceInfo =
    369                         new LinkedList<>();
    370                 for (int count = 0; count < filePaths.length; count++) {
    371                     File currentAtraceFile = pullAtraceInfoFile(filePaths[count]);
    372                     String[] splitName = filePaths[count].split("-");
    373                     // Process id is appended to original file name
    374                     Map<String, List<SectionPeriod>> singleLaunchTraceInfo = parseAtraceInfoFile(
    375                             currentAtraceFile,
    376                             splitName[splitName.length - 1]);
    377                     // Upload the file if needed
    378                     if (msaveAtrace) {
    379                         try (FileInputStreamSource stream =
    380                                 new FileInputStreamSource(currentAtraceFile)) {
    381                             listener.testLog(currentAtraceFile.getName(), LogDataType.TEXT, stream);
    382                         }
    383                     }
    384                     // Remove the atrace files
    385                     FileUtil.deleteFile(currentAtraceFile);
    386                     mutipleLaunchTraceInfo.add(singleLaunchTraceInfo);
    387                 }
    388 
    389                 // Verify and Average out the aTrace Info and store it in result map
    390                 averageAtraceData(activityName, mutipleLaunchTraceInfo);
    391             } catch (FileNotFoundException foe) {
    392                 CLog.e(foe);
    393                 activityErrMsg.put(activityName,
    394                         "Unable to find the trace file for the activity launch :" + activityName);
    395             } catch (IOException ioe) {
    396                 CLog.e(ioe);
    397                 activityErrMsg.put(activityName,
    398                         "Unable to read the contents of the atrace file for the activity :"
    399                                 + activityName);
    400             }
    401         }
    402 
    403     }
    404 
    405     /**
    406      * To pull the trace file from the device
    407      * @param aTraceFile
    408      * @return
    409      * @throws DeviceNotAvailableException
    410      */
    411     public File pullAtraceInfoFile(String aTraceFile)
    412             throws DeviceNotAvailableException {
    413         String dir = "${EXTERNAL_STORAGE}/atrace_logs";
    414         File atraceFileHandler = null;
    415         atraceFileHandler = getDevice().pullFile(dir + "/" + aTraceFile);
    416         Assert.assertTrue("Unable to retrieve the atrace files", atraceFileHandler != null);
    417         return atraceFileHandler;
    418     }
    419 
    420     /**
    421      * To parse and find the time taken for the given section names in each launch
    422      * @param currentAtraceFile
    423      * @param sectionSet
    424      * @param processId
    425      * @return
    426      * @throws FileNotFoundException,IOException
    427      */
    428     public Map<String, List<SectionPeriod>> parseAtraceInfoFile(
    429             File currentAtraceFile, String processId)
    430             throws FileNotFoundException, IOException {
    431         CLog.v("Currently parsing :" + currentAtraceFile.getName());
    432         String line;
    433         BufferedReader br = null;
    434         br = new BufferedReader(new FileReader(currentAtraceFile));
    435         LinkedList<TraceRecord> processStack = new LinkedList<>();
    436         Map<String, List<SectionPeriod>> sectionInfo = new HashMap<>();
    437 
    438         while ((line = br.readLine()) != null) {
    439             // Skip extra lines that aren't part of the trace
    440             if (line.isEmpty() || line.startsWith("capturing trace...")
    441                     || line.equals("TRACE:") || line.equals("done")) {
    442                 continue;
    443             }
    444             // Header information
    445             Matcher match = null;
    446             // Check if any trace entries were lost
    447             if ((match = matches(ATRACE_HEADER_ENTRIES, line)) != null) {
    448                 int buffered = Integer.parseInt(match.group("buffered"));
    449                 int written = Integer.parseInt(match.group("written"));
    450                 if (written != buffered) {
    451                     CLog.w(String.format("%d trace entries lost for the file %s",
    452                             written - buffered, currentAtraceFile.getName()));
    453                 }
    454             } else if ((match = matches(TRACE_ENTRY1, line)) != null
    455                     || (match = matches(TRACE_ENTRY2, line)) != null) {
    456                 /*
    457                  * Two trace entries because trace format differs across devices <...>-tid [yyy]
    458                  * ...1 zzz.ttt: tracing_mark_write: B|xxxx|tag_name pkg.name ( tid) [yyy] ...1
    459                  * zzz.tttt: tracing_mark_write: B|xxxx|tag_name
    460                  */
    461                 long timestamp = SEC_TO_MILLI
    462                         * Long.parseLong(match.group("secs"))
    463                         + Long.parseLong(match.group("usecs")) / MILLI_TO_MICRO;
    464                 // Get the function name from the trace entry
    465                 String taskId = match.group("tid");
    466                 String function = match.group("function");
    467                 // Analyze the lines that matches the processid
    468                 if (!taskId.equals(processId)) {
    469                     continue;
    470                 }
    471                 if ((match = matches(ATRACE_BEGIN, function)) != null) {
    472                     // Matching pattern looks like tracing_mark_write: B|xxxx|tag_name
    473                     String sectionName = match.group("name");
    474                     // Push to the stack
    475                     processStack.add(new TraceRecord(sectionName, taskId,
    476                             timestamp));
    477                 } else if ((match = matches(ATRACE_END, function)) != null) {
    478                     /*
    479                      * Matching pattern looks like tracing_mark_write: E Pop from the stack when end
    480                      * reaches
    481                      */
    482                     TraceRecord matchingBegin = processStack.removeLast();
    483                     if (mSectionSet.contains(matchingBegin.name)) {
    484                         if (sectionInfo.containsKey(matchingBegin.name)) {
    485                             SectionPeriod newSecPeriod = new SectionPeriod(matchingBegin.timestamp,
    486                                     timestamp);
    487                             CLog.v(String.format("Section :%s took :%f msecs ", matchingBegin.name,
    488                                     newSecPeriod.duration));
    489                             sectionInfo.get(matchingBegin.name).add(newSecPeriod);
    490                         } else {
    491                             List<SectionPeriod> infoList = new LinkedList<>();
    492                             SectionPeriod newSecPeriod = new SectionPeriod(matchingBegin.timestamp,
    493                                     timestamp);
    494                             CLog.v(String.format("Section :%s took :%f msecs ", matchingBegin.name,
    495                                     newSecPeriod.duration));
    496                             infoList.add(newSecPeriod);
    497                             sectionInfo.put(matchingBegin.name, infoList);
    498                         }
    499                     }
    500                 } else if ((match = matches(ATRACE_COUNTER, function)) != null) {
    501                     // Skip this for now. May want to track these later if needed.
    502                 }
    503             }
    504 
    505         }
    506         StreamUtil.close(br);
    507         return sectionInfo;
    508     }
    509 
    510     /**
    511      * To take the average of the multiple launches for each activity
    512      * @param activityName
    513      * @param mutipleLaunchTraceInfo
    514      */
    515     public void averageAtraceData(String activityName,
    516             List<Map<String, List<SectionPeriod>>> mutipleLaunchTraceInfo) {
    517         String verificationResult = verifyAtraceMapInfo(mutipleLaunchTraceInfo);
    518         if (verificationResult != null) {
    519             CLog.w(
    520                     "Not all the section info captured for the activity :%s. Missing: %s. "
    521                             + "Please go to atrace file to look for detail.",
    522                     activityName, verificationResult);
    523         }
    524         Map<String, Double> launchSum = new HashMap<>();
    525         for (String sectionName : mSectionSet) {
    526             launchSum.put(sectionName, 0d);
    527         }
    528         for (Map<String, List<SectionPeriod>> singleLaunchInfo : mutipleLaunchTraceInfo) {
    529             for (String sectionName : singleLaunchInfo.keySet()) {
    530                 for (SectionPeriod secPeriod : singleLaunchInfo
    531                         .get(sectionName)) {
    532                     if (sectionName.equals(AtraceSectionOptions.DRAW.toString())) {
    533                         // Get the first draw time for the launch
    534                         Double currentSum = launchSum.get(sectionName)
    535                                 + secPeriod.duration;
    536                         launchSum.put(sectionName, currentSum);
    537                         break;
    538                     }
    539                     //Sum the multiple layout times before the first draw in this launch
    540                     if (sectionName.equals(AtraceSectionOptions.LAYOUT.toString())) {
    541                         Double drawStartTime = singleLaunchInfo
    542                                 .get(AtraceSectionOptions.DRAW.toString()).get(0).startTime;
    543                         if (drawStartTime < secPeriod.startTime) {
    544                             break;
    545                         }
    546                     }
    547                     Double currentSum = launchSum.get(sectionName) + secPeriod.duration;
    548                     launchSum.put(sectionName, currentSum);
    549                 }
    550             }
    551         }
    552         // Update the final result map
    553         for (String sectionName : mSectionSet) {
    554             Double averageTime = launchSum.get(sectionName)
    555                     / mutipleLaunchTraceInfo.size();
    556             mActivityTimeResultMap.get(activityName).put(sectionName,
    557                     String.format("%.2f", averageTime));
    558         }
    559     }
    560 
    561     /**
    562      * To check if all the section info caught for all the app launches
    563      *
    564      * @param multipleLaunchTraceInfo
    565      * @return String: the missing section name, null if no section info missing.
    566      */
    567     public String verifyAtraceMapInfo(
    568             List<Map<String, List<SectionPeriod>>> multipleLaunchTraceInfo) {
    569         for (Map<String, List<SectionPeriod>> singleLaunchInfo : multipleLaunchTraceInfo) {
    570             Set<String> testSet = new HashSet<>(mSectionSet);
    571             testSet.removeAll(singleLaunchInfo.keySet());
    572             if (testSet.size() != 0) {
    573                 return testSet.toString();
    574             }
    575         }
    576         return null;
    577     }
    578 
    579 
    580     /**
    581      * Checks whether {@code line} matches the given {@link Pattern}.
    582      * @return The resulting {@link Matcher} obtained by matching the {@code line} against
    583      *         {@code pattern}, or null if the {@code line} does not match.
    584      */
    585     private static Matcher matches(Pattern pattern, String line) {
    586         Matcher ret = pattern.matcher(line);
    587         return ret.matches() ? ret : null;
    588     }
    589 
    590     @Override
    591     public void setDevice(ITestDevice device) {
    592         mDevice = device;
    593     }
    594 
    595     @Override
    596     public ITestDevice getDevice() {
    597         return mDevice;
    598     }
    599 
    600     /**
    601      * A record to keep track of the section start time,end time and the duration in milliseconds.
    602      */
    603     public static class SectionPeriod {
    604 
    605         private double startTime;
    606         private double endTime;
    607         private double duration;
    608 
    609         public SectionPeriod(double startTime, double endTime) {
    610             this.startTime = startTime;
    611             this.endTime = endTime;
    612             this.duration = endTime - startTime;
    613         }
    614 
    615         public double getStartTime() {
    616             return startTime;
    617         }
    618 
    619         public void setStartTime(long startTime) {
    620             this.startTime = startTime;
    621         }
    622 
    623         public double getEndTime() {
    624             return endTime;
    625         }
    626 
    627         public void setEndTime(long endTime) {
    628             this.endTime = endTime;
    629         }
    630 
    631         public double getDuration() {
    632             return duration;
    633         }
    634 
    635         public void setDuration(long duration) {
    636             this.duration = duration;
    637         }
    638     }
    639 
    640     /**
    641      * A record of a trace event. Includes the name of the section, and the time that the event
    642      * occurred (in milliseconds).
    643      */
    644     public static class TraceRecord {
    645 
    646         private String name;
    647         private String processId;
    648         private double timestamp;
    649 
    650         /**
    651          * Construct a new {@link TraceRecord} with the given {@code name} and {@code timestamp} .
    652          */
    653         public TraceRecord(String name, String processId, long timestamp) {
    654             this.name = name;
    655             this.processId = processId;
    656             this.timestamp = timestamp;
    657         }
    658 
    659         public String getName() {
    660             return name;
    661         }
    662 
    663         public void setName(String name) {
    664             this.name = name;
    665         }
    666 
    667         public String getProcessId() {
    668             return processId;
    669         }
    670 
    671         public void setProcessId(String processId) {
    672             this.processId = processId;
    673         }
    674 
    675         public double getTimestamp() {
    676             return timestamp;
    677         }
    678 
    679         public void setTimestamp(long timestamp) {
    680             this.timestamp = timestamp;
    681         }
    682     }
    683 
    684 }
    685