Tradefed test for Hermatic App Launch Perfromance verification

Change-Id: I0a591f6f8cb028fab013a022e78e73c971614a33
diff --git a/prod-tests/res/config/performance/hermetic-launch.xml b/prod-tests/res/config/performance/hermetic-launch.xml
new file mode 100644
index 0000000..ac5794c
--- /dev/null
+++ b/prod-tests/res/config/performance/hermetic-launch.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<configuration description="Testing the app launch performance" >
+
+    <option
+        name="test-tag"
+        value="HermeticLaunchTest" />
+
+    <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup" >
+        <option
+            name="test-file-name"
+            value="PerformanceLaunch.apk" />
+        <option
+            name="test-file-name"
+            value="PerformanceAppTest.apk" />
+    </target_preparer>
+
+    <test class="com.android.performance.tests.HermeticLaunchTest" >
+        <option
+            name="package"
+            value="com.android.performanceapp.tests" />
+        <option
+            name="target-package"
+            value="com.android.performanceLaunch" />
+        <option
+            name="launch-count"
+            value="15" />
+    </test>
+
+</configuration>
\ No newline at end of file
diff --git a/prod-tests/src/com/android/performance/tests/HermeticLaunchTest.java b/prod-tests/src/com/android/performance/tests/HermeticLaunchTest.java
new file mode 100644
index 0000000..a6864e0
--- /dev/null
+++ b/prod-tests/src/com/android/performance/tests/HermeticLaunchTest.java
@@ -0,0 +1,631 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.performance.tests;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import junit.framework.Assert;
+
+import com.android.ddmlib.IDevice;
+import com.android.ddmlib.testrunner.IRemoteAndroidTestRunner;
+import com.android.ddmlib.testrunner.RemoteAndroidTestRunner;
+import com.android.ddmlib.testrunner.TestResult;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.config.Option.Importance;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.device.LogcatReceiver;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.result.CollectingTestListener;
+import com.android.tradefed.result.ITestInvocationListener;
+import com.android.tradefed.result.InputStreamSource;
+import com.android.tradefed.result.LogDataType;
+import com.android.tradefed.testtype.IDeviceTest;
+import com.android.tradefed.testtype.IRemoteTest;
+import com.android.tradefed.util.FileUtil;
+
+/**
+ * To test the app launch performance for the list of activities present in the given target package
+ * or the custom list of activities present in the target package.Activities are launched number of
+ * times present in the launch count. Launch time is analyzed from the logcat data and more detailed
+ * timing(section names) is analyzed from the atrace files captured when launching each activity.
+ */
+public class HermeticLaunchTest implements IRemoteTest, IDeviceTest {
+
+    private static final String BINDAPPLICATION = "bindApplication";
+    private static final String ACTIVITYSTART = "activityStart";
+    private static final String LAYOUT = "layout";
+    private static final String DRAW = "draw";
+    private static final String TOTALLAUNCHTIME = "totalLaunchTime";
+    private static final String LOGCAT_CMD = "logcat -v threadtime ActivityManager:* *:s";
+    private static final String LAUNCH_PREFIX="^\\d*-\\d*\\s*\\d*:\\d*:\\d*.\\d*\\s*\\d*\\s*"
+            + "\\d*\\s*I ActivityManager: Displayed\\s*";
+    private static final String LAUNCH_SUFFIX=":\\s*\\+(?<launchtime>.[a-zA-Z\\d]*)\\s*"
+            + "(?<totallaunch>.*)\\s*$";
+    private static final Pattern LAUNCH_ENTRY=Pattern.compile("^\\d*-\\d*\\s*\\d*:\\d*:\\d*."
+            + "\\d*\\s*\\d*\\s*\\d*\\s*I ActivityManager: Displayed\\s*(?<launchinfo>.*)\\s*$");
+    private static final Pattern TRACE_ENTRY1 = Pattern.compile(
+            "^\\s*\\<\\.+\\>-(?<tid>\\d+)\\s+\\[\\d+\\]\\s+\\S{4}\\s+" +
+            "(?<secs>\\d+)\\.(?<usecs>\\d+):\\s+(?<function>.*)\\s*$");
+    private static final Pattern TRACE_ENTRY2 = Pattern.compile("^\\s*[a-zA-Z].*-(?<tid>\\d+)"
+            + "\\s*\\(\\s*\\d+\\)\\s*\\[\\d+\\]\\s+\\S{4}\\s+" +
+            "(?<secs>\\d+)\\.(?<usecs>\\d+):\\s+(?<function>.*)\\s*$");
+    private static final Pattern ATRACE_BEGIN = Pattern
+            .compile("tracing_mark_write: B\\|(?<pid>\\d+)\\|(?<name>.+)");
+    private static final Pattern ATRACE_END = Pattern.compile("tracing_mark_write: E");
+    private static final Pattern ATRACE_COUNTER = Pattern
+            .compile("tracing_mark_write: C\\|(?<pid>\\d+)\\|(?<name>[^|]+)\\|(?<value>\\d+)");
+    private static final Pattern ATRACE_HEADER_ENTRIES = Pattern
+            .compile("# entries-in-buffer/entries-written:\\s+(?<buffered>\\d+)/"
+                    + "(?<written>\\d+)\\s+#P:\\d+\\s*");
+    private static final int LOGCAT_SIZE = 20971520; //20 mb
+    private static final long SEC_TO_MILLI = 1000;
+    private static final long MILLI_TO_MICRO = 1000;
+
+    @Option(name = "runner", description = "The instrumentation test runner class name to use.")
+    private String mRunnerName = "android.support.test.runner.AndroidJUnitRunner";
+
+    @Option(name = "package", shortName = 'p',
+            description = "The manifest package name of the Android test application to run.",
+            importance = Importance.IF_UNSET)
+    private String mPackageName = "com.android.performanceapp.tests";
+
+    @Option(name = "target-package", description = "package which contains all the "
+            + "activities to launch")
+    private String mtargetPackage = null;
+
+    @Option(name = "activity-names", description = "Fully qualified activity "
+            + "names separated by comma"
+            + "If not set then all the activities will be included for launching")
+    private String mactivityNames = "";
+
+    @Option(name = "launch-count", description = "number of time to launch the each activity")
+    private int mlaunchCount = 10;
+
+    private ITestDevice mDevice = null;
+    private IRemoteAndroidTestRunner mRunner;
+    private LogcatReceiver mLogcat;
+    private Set<String> mSectionSet = new HashSet<>();
+    private Map<String, String> mActivityTraceFileMap;
+    private Map<String, Map<String, String>> mActivityTimeResultMap = new HashMap<>();
+    private Map<String,String> activityErrMsg=new HashMap<>();
+
+    @Override
+    public void run(ITestInvocationListener listener)
+            throws DeviceNotAvailableException {
+
+        mLogcat = new LogcatReceiver(getDevice(), LOGCAT_CMD, LOGCAT_SIZE, 0);
+        mLogcat.start();
+        mSectionSet.add(BINDAPPLICATION);
+        mSectionSet.add(ACTIVITYSTART);
+        mSectionSet.add(LAYOUT);
+        mSectionSet.add(DRAW);
+
+        mRunner = createRemoteAndroidTestRunner(mPackageName, mRunnerName,
+                mDevice.getIDevice());
+        CollectingTestListener collectingListener = new CollectingTestListener();
+        mDevice.runInstrumentationTests(mRunner, collectingListener);
+
+        Collection<TestResult> testResultsCollection = collectingListener
+                .getCurrentRunResults().getTestResults().values();
+        List<TestResult> testResults = new ArrayList<>(
+                testResultsCollection);
+        /*
+         * Expected Metrics : Map of <activity name>=<comma separated list of atrace file names in
+         * external storage of the device>
+         */
+        mActivityTraceFileMap = testResults.get(0).getMetrics();
+        Assert.assertTrue("Unable to get the path to the trace files stored in the device",
+                (mActivityTraceFileMap != null && !mActivityTraceFileMap.isEmpty()));
+
+        // Analyze the logcat data to get total launch time
+        analyzeLogCatData(mActivityTraceFileMap.keySet());
+
+        //Stop the logcat
+        mLogcat.stop();
+
+        // Analyze the atrace data to get bindApplication,activityStart etc..
+        analyzeAtraceData();
+
+        // Report the metrics to dashboard
+        reportMetrics(listener);
+
+    }
+
+    /**
+     * Report run metrics by creating an empty test run to stick them in.
+     * @param listener The {@link ITestInvocationListener} of test results
+     * @param metrics The {@link Map} that contains metrics for the given test
+     */
+    private void reportMetrics(ITestInvocationListener listener) {
+        for (String activityName : mActivityTimeResultMap.keySet()) {
+            // Get the activity name alone from pkgname.activityname
+            String[] activityNameSplit = activityName.split("\\.");
+            if (!activityErrMsg.containsKey(activityName)) {
+                Map<String, String> activityMetrics = mActivityTimeResultMap.get(activityName);
+                if (activityMetrics != null && !activityMetrics.isEmpty()) {
+                    CLog.v("Metrics for the activity : %s", activityName);
+                    for (String sectionName : activityMetrics.keySet()) {
+                        CLog.v(String.format("Section name : %s - Time taken : %s",
+                                sectionName, activityMetrics.get(sectionName)));
+                    }
+                    listener.testRunStarted(
+                            activityNameSplit[activityNameSplit.length - 1].trim(), 0);
+                    listener.testRunEnded(0, activityMetrics);
+                }
+            } else {
+                listener.testRunStarted(
+                        activityNameSplit[activityNameSplit.length - 1].trim(), 0);
+                listener.testRunFailed(activityErrMsg.get(activityName));
+            }
+        }
+    }
+
+    /**
+     * Method to create the runner with given list of arguments
+     * @return the {@link IRemoteAndroidTestRunner} to use.
+     * @throws DeviceNotAvailableException
+     */
+    IRemoteAndroidTestRunner createRemoteAndroidTestRunner(String packageName,
+            String runnerName, IDevice device)
+            throws DeviceNotAvailableException {
+        RemoteAndroidTestRunner runner = new RemoteAndroidTestRunner(
+                packageName, runnerName, device);
+        runner.addInstrumentationArg("targetpackage", mtargetPackage);
+        runner.addInstrumentationArg("launchcount", mlaunchCount + "");
+        if (mactivityNames != null && !mactivityNames.isEmpty()) {
+            runner.addInstrumentationArg("activitylist", mactivityNames);
+        }
+        return runner;
+    }
+
+    /**
+     * To analyze the log cat data to get the display time reported by activity manager during the
+     * launches
+     * activitySet is set of activityNames returned as a part of testMetrics from the device
+     */
+    public void analyzeLogCatData(Set<String> activitySet) {
+        Map<String, List<Integer>> amLaunchTimes = new HashMap<>();
+        InputStreamSource input = mLogcat.getLogcatData();
+        InputStream inputStream = input.createInputStream();
+        BufferedReader br = new BufferedReader(new InputStreamReader(
+                inputStream));
+        Map<Pattern, String> activityPatternMap = new HashMap<>();
+        Matcher match = null;
+        String line;
+
+        /*
+         * Sample line format in logcat
+         * 06-17 16:55:49.6 60 642 I ActivityManager: Displayed pkg/.activity: +Tms (total +9s9ms)
+         */
+        for (String activityName : activitySet) {
+            int lastIndex = activityName.lastIndexOf(".");
+            /*
+             * actvitySet has set of activity names in the format packageName.activityName
+             * logcat has the format packageName/.activityName --> activityAlias
+             */
+            String activityAlias = activityName.subSequence(0, lastIndex)
+                    + "/" + activityName.subSequence(lastIndex, activityName.length());
+            String finalPattern = LAUNCH_PREFIX + activityAlias + LAUNCH_SUFFIX;
+            activityPatternMap.put(Pattern.compile(finalPattern),
+                    activityName);
+        }
+
+        try {
+            while ((line = br.readLine()) != null) {
+                /*
+                 * Launch entry needed otherwise we will end up in comparing all the lines for all
+                 * the patterns
+                 */
+                if ((match = matches(LAUNCH_ENTRY, line)) != null) {
+                    for (Pattern pattern : activityPatternMap.keySet()) {
+                        if ((match = matches(pattern, line)) != null) {
+                            int displayTimeInMs = extractLaunchTime(match.group("launchtime"));
+                            String activityName = activityPatternMap.get(pattern);
+                            if (amLaunchTimes.containsKey(activityName)) {
+                                amLaunchTimes.get(activityName).add(displayTimeInMs);
+                            } else {
+                                List<Integer> launchTimes = new ArrayList<>();
+                                launchTimes.add(displayTimeInMs);
+                                amLaunchTimes.put(activityName, launchTimes);
+                            }
+                        }
+                    }
+                }
+            }
+        } catch (IOException io) {
+            CLog.e(io);
+        }
+
+        // Verify logcat data
+        for (String activityName : amLaunchTimes.keySet()) {
+            Assert.assertEquals("Data lost for launch time for the activity :"
+                    + activityName, amLaunchTimes.get(activityName).size(), mlaunchCount);
+        }
+
+        /*
+         * Extract and store the average launch time data reported by activity manager for each
+         * activity
+         */
+        for (String activityName : amLaunchTimes.keySet()) {
+            Double totalTime = 0d;
+            for (Integer launchTime : amLaunchTimes.get(activityName)) {
+                totalTime += launchTime;
+            }
+            Double averageTime = new Double(totalTime
+                    / amLaunchTimes.get(activityName).size());
+            if (mActivityTimeResultMap.containsKey(activityName)) {
+                mActivityTimeResultMap.get(activityName).put(TOTALLAUNCHTIME,
+                        String.format("%.2f", averageTime));
+            } else {
+                Map<String, String> launchTime = new HashMap<>();
+                launchTime.put(TOTALLAUNCHTIME,
+                        String.format("%.2f", averageTime));
+                mActivityTimeResultMap.put(activityName, launchTime);
+            }
+        }
+    }
+
+    /**
+     * To extract the launch time displayed in given line
+     * @param currentLine
+     * @return
+     */
+    public int extractLaunchTime(String duration) {
+        String formattedString = duration.replace("ms", "");
+        if (formattedString.contains("s")) {
+            String[] splitString = formattedString.split("s");
+            int finalTimeInMs = Integer.parseInt(splitString[0]) * 1000;
+            finalTimeInMs = finalTimeInMs + Integer.parseInt(splitString[1]);
+            return finalTimeInMs;
+        } else {
+            return Integer.parseInt(formattedString);
+        }
+    }
+
+    /**
+     * To analyze the trace data collected in the device during each activity launch.
+     */
+    public void analyzeAtraceData() throws DeviceNotAvailableException {
+        for (String activityName : mActivityTraceFileMap.keySet()) {
+            try {
+                // Get the list of associated filenames for given activity
+                String filePathAll = mActivityTraceFileMap.get(activityName);
+                Assert.assertNotNull(
+                        String.format("Unable to find trace file paths for activity : %s",
+                                activityName), filePathAll);
+                String[] filePaths = filePathAll.split(",");
+                Assert.assertEquals(String.format("Unable to find file path for all the launches "
+                        + "for the activity :%s", activityName), filePaths.length, mlaunchCount);
+                // Pull and parse the info
+                List<Map<String, List<SectionPeriod>>> mutipleLaunchTraceInfo =
+                        new LinkedList<>();
+                for (int count = 0; count < filePaths.length; count++) {
+                    File currentAtraceFile = pullAtraceInfoFile(filePaths[count]);
+                    String[] splitName = filePaths[count].split("-");
+                    // Process id is appended to original file name
+                    Map<String, List<SectionPeriod>> singleLaunchTraceInfo = parseAtraceInfoFile(
+                            currentAtraceFile,
+                            splitName[splitName.length - 1]);
+                    // Remove the atrace files
+                    FileUtil.deleteFile(currentAtraceFile);
+                    mutipleLaunchTraceInfo.add(singleLaunchTraceInfo);
+                }
+
+                // Verify and Average out the aTrace Info and store it in result map
+                averageAtraceData(activityName, mutipleLaunchTraceInfo);
+            } catch (FileNotFoundException foe) {
+                CLog.e(foe);
+                activityErrMsg.put(activityName,
+                        "Unable to find the trace file for the activity launch :" + activityName);
+            } catch (IOException ioe) {
+                CLog.e(ioe);
+                activityErrMsg.put(activityName,
+                        "Unable to read the contents of the atrace file for the activity :"
+                                + activityName);
+            }
+        }
+
+    }
+
+    /**
+     * To pull the trace file from the device
+     * @param aTraceFile
+     * @return
+     * @throws DeviceNotAvailableException
+     */
+    public File pullAtraceInfoFile(String aTraceFile)
+            throws DeviceNotAvailableException {
+        String dir = "${EXTERNAL_STORAGE}/atrace_logs";
+        File atraceFileHandler = null;
+        atraceFileHandler = getDevice().pullFile(dir + "/" + aTraceFile);
+        Assert.assertTrue("Unable to retrieve the atrace files", atraceFileHandler != null);
+        return atraceFileHandler;
+    }
+
+    /**
+     * To parse and find the time taken for the given section names in each launch
+     * @param currentAtraceFile
+     * @param sectionSet
+     * @param processId
+     * @return
+     * @throws FileNotFoundException,IOException
+     */
+    public Map<String, List<SectionPeriod>> parseAtraceInfoFile(
+            File currentAtraceFile, String processId)
+            throws FileNotFoundException, IOException {
+        CLog.v("Currently parsing :" + currentAtraceFile.getName());
+        String line;
+        BufferedReader br = null;
+        br = new BufferedReader(new FileReader(currentAtraceFile));
+        LinkedList<TraceRecord> processStack = new LinkedList<>();
+        Map<String, List<SectionPeriod>> sectionInfo = new HashMap<>();
+
+        while ((line = br.readLine()) != null) {
+            // Skip extra lines that aren't part of the trace
+            if (line.isEmpty() || line.startsWith("capturing trace...")
+                    || line.equals("TRACE:") || line.equals("done")) {
+                continue;
+            }
+            // Header information
+            Matcher match = null;
+            // Check if any trace entries were lost
+            if ((match = matches(ATRACE_HEADER_ENTRIES, line)) != null) {
+                int buffered = Integer.parseInt(match.group("buffered"));
+                int written = Integer.parseInt(match.group("written"));
+                if (written != buffered) {
+                    CLog.w(String.format("%d trace entries lost for the file %s",
+                            written - buffered, currentAtraceFile.getName()));
+                }
+            } else if ((match = matches(TRACE_ENTRY1, line)) != null
+                    || (match = matches(TRACE_ENTRY2, line)) != null) {
+                /*
+                 * Two trace entries because trace format differs across devices <...>-tid [yyy]
+                 * ...1 zzz.ttt: tracing_mark_write: B|xxxx|tag_name pkg.name ( tid) [yyy] ...1
+                 * zzz.tttt: tracing_mark_write: B|xxxx|tag_name
+                 */
+                long timestamp = SEC_TO_MILLI
+                        * Long.parseLong(match.group("secs"))
+                        + Long.parseLong(match.group("usecs")) / MILLI_TO_MICRO;
+                // Get the function name from the trace entry
+                String taskId = match.group("tid");
+                String function = match.group("function");
+                // Analyze the lines that matches the processid
+                if (!taskId.equals(processId)) {
+                    continue;
+                }
+                if ((match = matches(ATRACE_BEGIN, function)) != null) {
+                    // Matching pattern looks like tracing_mark_write: B|xxxx|tag_name
+                    String sectionName = match.group("name");
+                    // Push to the stack
+                    processStack.add(new TraceRecord(sectionName, taskId,
+                            timestamp));
+                } else if ((match = matches(ATRACE_END, function)) != null) {
+                    /*
+                     * Matching pattern looks like tracing_mark_write: E Pop from the stack when end
+                     * reaches
+                     */
+                    TraceRecord matchingBegin = processStack.removeLast();
+                    if (mSectionSet.contains(matchingBegin.name)) {
+                        if (sectionInfo.containsKey(matchingBegin.name)) {
+                            sectionInfo.get(matchingBegin.name).add(
+                                    new SectionPeriod(matchingBegin.timestamp,
+                                            timestamp));
+                        } else {
+                            List<SectionPeriod> infoList = new LinkedList<>();
+                            infoList.add(new SectionPeriod(
+                                    matchingBegin.timestamp, timestamp));
+                            sectionInfo.put(matchingBegin.name, infoList);
+                        }
+                    }
+                } else if ((match = matches(ATRACE_COUNTER, function)) != null) {
+                    // Skip this for now. May want to track these later if needed.
+                }
+            }
+
+        }
+        br.close();
+        return sectionInfo;
+    }
+
+    /**
+     * To take the average of the multiple launches for each activity
+     * @param activityName
+     * @param mutipleLaunchTraceInfo
+     */
+    public void averageAtraceData(String activityName,
+            List<Map<String, List<SectionPeriod>>> mutipleLaunchTraceInfo) {
+        if (!verifyAtraceMapInfo(mutipleLaunchTraceInfo)) {
+            activityErrMsg.put(activityName, "Not all the section info captured for the activity :"
+                    + activityName);
+            return;
+        }
+        Map<String, Double> launchSum = new HashMap<>();
+        for (String sectionName : mSectionSet) {
+            launchSum.put(sectionName, 0d);
+        }
+        for (Map<String, List<SectionPeriod>> singleLaunchInfo : mutipleLaunchTraceInfo) {
+            for (String sectionName : singleLaunchInfo.keySet()) {
+                for (SectionPeriod secPeriod : singleLaunchInfo
+                        .get(sectionName)) {
+                    if (sectionName.equals(DRAW)) {
+                        // Get the first draw time for the launch
+                        Double currentSum = launchSum.get(sectionName)
+                                + secPeriod.duration;
+                        launchSum.put(sectionName, currentSum);
+                        break;
+                    }
+                    //Sum the multiple layout times before the first draw in this launch
+                    if (sectionName.equals(LAYOUT)) {
+                        Double drawStartTime = singleLaunchInfo.get(DRAW)
+                                .get(0).startTime;
+                        if (drawStartTime < secPeriod.startTime) {
+                            break;
+                        }
+                    }
+                    Double currentSum = launchSum.get(sectionName) + secPeriod.duration;
+                    launchSum.put(sectionName, currentSum);
+                }
+            }
+        }
+        // Update the final result map
+        for (String sectionName : mSectionSet) {
+            Double averageTime = launchSum.get(sectionName)
+                    / mutipleLaunchTraceInfo.size();
+            mActivityTimeResultMap.get(activityName).put(sectionName,
+                    String.format("%.2f", averageTime));
+        }
+    }
+
+    /**
+     * To check if all the section info caught for all the app launches
+     * @param multipleLaunchTraceInfo
+     * @return
+     */
+    public boolean verifyAtraceMapInfo(
+            List<Map<String, List<SectionPeriod>>> multipleLaunchTraceInfo) {
+        for (Map<String, List<SectionPeriod>> singleLaunchInfo : multipleLaunchTraceInfo) {
+            if (singleLaunchInfo.size() != mSectionSet.size()) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+
+    /**
+     * Checks whether {@code line} matches the given {@link Pattern}.
+     * @return The resulting {@link Matcher} obtained by matching the {@code line} against
+     *         {@code pattern}, or null if the {@code line} does not match.
+     */
+    private static Matcher matches(Pattern pattern, String line) {
+        Matcher ret = pattern.matcher(line);
+        return ret.matches() ? ret : null;
+    }
+
+    @Override
+    public void setDevice(ITestDevice device) {
+        mDevice = device;
+    }
+
+    @Override
+    public ITestDevice getDevice() {
+        return mDevice;
+    }
+
+    /**
+     * A record to keep track of the section start time,end time and the duration in milliseconds.
+     */
+    public static class SectionPeriod {
+
+        private double startTime;
+        private double endTime;
+        private double duration;
+
+        public SectionPeriod(double startTime, double endTime) {
+            this.startTime = startTime;
+            this.endTime = endTime;
+            this.duration = endTime - startTime;
+        }
+
+        public double getStartTime() {
+            return startTime;
+        }
+
+        public void setStartTime(long startTime) {
+            this.startTime = startTime;
+        }
+
+        public double getEndTime() {
+            return endTime;
+        }
+
+        public void setEndTime(long endTime) {
+            this.endTime = endTime;
+        }
+
+        public double getDuration() {
+            return duration;
+        }
+
+        public void setDuration(long duration) {
+            this.duration = duration;
+        }
+    }
+
+    /**
+     * A record of a trace event. Includes the name of the section, and the time that the event
+     * occurred (in milliseconds).
+     */
+    public static class TraceRecord {
+
+        private String name;
+        private String processId;
+        private double timestamp;
+
+        /**
+         * Construct a new {@link TraceRecord} with the given {@code name} and {@code timestamp} .
+         */
+        public TraceRecord(String name, String processId, long timestamp) {
+            this.name = name;
+            this.processId = processId;
+            this.timestamp = timestamp;
+        }
+
+        public String getName() {
+            return name;
+        }
+
+        public void setName(String name) {
+            this.name = name;
+        }
+
+        public String getProcessId() {
+            return processId;
+        }
+
+        public void setProcessId(String processId) {
+            this.processId = processId;
+        }
+
+        public double getTimestamp() {
+            return timestamp;
+        }
+
+        public void setTimestamp(long timestamp) {
+            this.timestamp = timestamp;
+        }
+    }
+
+}