blob: ebc0fffe13c087b0fedcf75a030f49780b3c0fd1 [file] [log] [blame]
/*
* Copyright (C) 2022 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 com.android.ddmlib.testrunner.IRemoteAndroidTestRunner;
import com.android.ddmlib.testrunner.RemoteAndroidTestRunner;
import com.android.loganalysis.item.DmesgActionInfoItem;
import com.android.loganalysis.item.DmesgServiceInfoItem;
import com.android.loganalysis.item.DmesgStageInfoItem;
import com.android.loganalysis.item.GenericTimingItem;
import com.android.loganalysis.item.SystemServicesTimingItem;
import com.android.loganalysis.parser.DmesgParser;
import com.android.loganalysis.parser.TimingsLogParser;
import com.android.tradefed.build.IBuildInfo;
import com.android.tradefed.config.IConfiguration;
import com.android.tradefed.config.IConfigurationReceiver;
import com.android.tradefed.config.Option;
import com.android.tradefed.config.Option.Importance;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.LogcatReceiver;
import com.android.tradefed.device.metric.IMetricCollector;
import com.android.tradefed.device.metric.IMetricCollectorReceiver;
import com.android.tradefed.error.IHarnessException;
import com.android.tradefed.invoker.IInvocationContext;
import com.android.tradefed.invoker.TestInformation;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
import com.android.tradefed.result.CollectingTestListener;
import com.android.tradefed.result.FailureDescription;
import com.android.tradefed.result.ITestInvocationListener;
import com.android.tradefed.result.InputStreamSource;
import com.android.tradefed.result.LogDataType;
import com.android.tradefed.result.TestDescription;
import com.android.tradefed.result.error.ErrorIdentifier;
import com.android.tradefed.result.proto.TestRecordProto.FailureStatus;
import com.android.tradefed.testtype.IBuildReceiver;
import com.android.tradefed.testtype.IDeviceTest;
import com.android.tradefed.testtype.IRemoteTest;
import com.android.tradefed.testtype.InstalledInstrumentationsTest;
import com.android.tradefed.testtype.IInvocationContextReceiver;
import com.android.tradefed.util.CommandResult;
import com.android.tradefed.util.CommandStatus;
import com.android.tradefed.util.IRunUtil;
import com.android.tradefed.util.RunUtil;
import com.google.common.annotations.VisibleForTesting;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/** Performs successive reboots and computes various boottime metrics */
public class BootTimeTest extends InstalledInstrumentationsTest
implements IRemoteTest,
IDeviceTest,
IBuildReceiver,
IConfigurationReceiver,
IInvocationContextReceiver,
IMetricCollectorReceiver {
protected static final String ONLINE = "online";
protected static final String BOOTTIME_TEST = "BootTimeTest";
protected static final long INVALID_TIME_DURATION = -1;
private static final String SUCCESSIVE_BOOT_TEST = "SuccessiveBootTest";
private static final String SUCCESSIVE_BOOT_UNLOCK_TEST = "SuccessiveBootUnlockTest";
private static final String SUCCESSIVE_ONLINE = "successive-online";
private static final String SUCCESSIVE_BOOT = "successive-boot";
private static final String LOGCAT_CMD = "logcat *:V";
private static final String LOGCAT_CMD_ALL = "logcat -b all *:V";
private static final String LOGCAT_CMD_STATISTICS = "logcat --statistics --pid %d";
private static final String LOGCAT_CMD_STATISTICS_ALL = "logcat --statistics";
private static final long LOGCAT_SIZE = 80 * 1024 * 1024;
private static final String BOOT_COMPLETED_PROP = "getprop sys.boot_completed";
private static final String BOOT_COMPLETED_VAL = "1";
private static final String BOOT_TIME_PROP = "ro.boot.boottime";
private static final String BOOTLOADER_PREFIX = "bootloader-";
private static final String BOOTLOADER_TIME = "bootloader_time";
private static final String LOGCAT_FILE = "Successive_reboots_logcat";
private static final String LOGCAT_UNLOCK_FILE = "Successive_reboots_unlock_logcat";
private static final String BOOT_COMPLETE_ACTION = "sys.boot_completed=1";
private static final String RUNNER = "androidx.test.runner.AndroidJUnitRunner";
private static final String PACKAGE_NAME = "com.android.boothelper";
private static final String CLASS_NAME = "com.android.boothelper.BootHelperTest";
private static final String SETUP_PIN_TEST = "setupLockScreenPin";
private static final String UNLOCK_PIN_TEST = "unlockScreenWithPin";
private static final String UNLOCK_TIME = "screen_unlocktime";
private static final String F2FS_SHUTDOWN_COMMAND = "f2fs_io shutdown 4 /data";
private static final String F2FS_SHUTDOWN_SUCCESS_OUTPUT = "Shutdown /data with level=4";
private static final int BOOT_COMPLETE_POLL_INTERVAL = 1000;
private static final int BOOT_COMPLETE_POLL_RETRY_COUNT = 45;
private static final String ROOT = "root";
private static final String DMESG_FILE = "/data/local/tmp/dmesglogs.txt";
private static final String DUMP_DMESG = String.format("dmesg > %s", DMESG_FILE);
private static final String INIT = "init_";
private static final String START_TIME = "_START_TIME";
private static final String DURATION = "_DURATION";
private static final String END_TIME = "_END_TIME";
private static final String ACTION = "action_";
private static final String INIT_STAGE = "init_stage_";
/** logcat command for selinux, pinnerservice, fatal messages */
private static final String LOGCAT_COMBINED_CMD =
"logcat -b all auditd:* PinnerService:* \\*:F";
private static final String TOTAL_BOOT_TIME = "TOTAL_BOOT_TIME";
private static final String BOOT_PHASE_1000 = "starting_phase_1000";
private static final String METRIC_KEY_SEPARATOR = "_";
private static final String LOGCAT_STATISTICS_SIZE = "logcat_statistics_size_bytes";
private static final String LOGCAT_STATISTICS_DIFF_SIZE =
"logcat_statistics_size_bytes_dropped";
private static final String DMESG_BOOT_COMPLETE_TIME =
"dmesg_action_sys.boot_completed_first_timestamp";
private static final String MAKE_DIR = "mkdir %s";
private static final String FOLDER_NAME_FORMAT = "sample_%s";
private static final String RANDOM_FILE_CMD = "dd if=/dev/urandom of=%s bs=%d%s count=1";
private static final String KB_IDENTIFIER = "k";
private static final String MB_IDENTIFIER = "m";
private static final String BYTES_TRANSFERRED = "bytes transferred";
private static final String RM_DIR = "rm -rf %s";
private static final String RAMDUMP_STATUS = "ramdump -s";
private static final String BOOTLOADER_PHASE_SW = "SW";
private static final String STORAGE_TYPE = "ro.boot.hardware.ufs";
private static final String PERFETTO_TRACE_FILE_CHECK_CMD = "ls -l /data/misc/perfetto-traces";
private static final String FINAL_PERFETTO_TRACE_LOCATION = "/data/local/tmp/%s.perfetto-trace";
private static final String PERFETTO_TRACE_MV_CMD =
"mv /data/misc/perfetto-traces/boottrace.perfetto-trace %s";
private static final String TIMESTAMP_PID =
"^(\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}.\\d{3})\\s+" + "(\\d+)\\s+(\\d+)\\s+([A-Z])\\s+";
// 04-05 18:26:52.637 2161 2176 I BootHelperTest: Screen Unlocked
private static final String ALL_PROCESS_CMD = "ps -A";
private static final Pattern SCREEN_UNLOCKED =
Pattern.compile(TIMESTAMP_PID + "(.+?)\\s*: Screen Unlocked$");
// 04-05 18:26:54.320 1013 1121 I ActivityManager: Displayed
// com.google.android.apps.nexuslauncher/.NexusLauncherActivity: +648ms
private static final Pattern DISPLAYED_LAUNCHER =
Pattern.compile(
TIMESTAMP_PID
+ "(.+?)\\s*: Displayed"
+ " com.google.android.apps.nexuslauncher/.NexusLauncherActivity:"
+ "\\s*(.*)$");
/** Matches the line indicating kernel start. It is starting point of the whole boot process */
private static final Pattern KERNEL_START_PATTERN = Pattern.compile("Linux version");
/** Matches the logcat line indicating boot completed */
private static final Pattern LOGCAT_BOOT_COMPLETED = Pattern.compile("Starting phase 1000");
private static final Pattern LOGCAT_STATISTICS_HEADER_PATTERN =
Pattern.compile(
"^size\\/num\\s*(\\w+)\\s*(\\w+)\\s*(\\w+)\\s*(\\w+)\\s*(\\w+)*",
Pattern.CASE_INSENSITIVE);
private static final Pattern LOGCAT_STATISTICS_TOTAL_PATTERN =
Pattern.compile(
"Total\\s*(\\d+)\\/(\\d+)\\s*(\\d+)\\/(\\d+)\\s*(\\d+)\\/(\\d+)\\s*"
+ "(\\d+)\\/(\\d+)\\s*(\\d+)\\/(\\d+).*",
Pattern.CASE_INSENSITIVE);
private static final Pattern LOGCAT_STATISTICS_NOW_PATTERN =
Pattern.compile(
"Now\\s+(\\d+)\\/(\\d+)\\s*(\\d+)\\/(\\d+)\\s*(\\d+)\\/(\\d+)\\s*"
+ "(\\d+)\\/(\\d+)\\s*(\\d+)\\/(\\d+).*",
Pattern.CASE_INSENSITIVE);
private static final Pattern LOGCAT_STATISTICS_PID_PATTERN =
Pattern.compile(
"Logging for this PID.*\\s+([0-9]+)$",
Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
private static final String METRIC_COUNT = "MetricCount";
@Option(name = "test-run-name", description = "run name to report to result reporters")
private String mTestRunName = BOOTTIME_TEST;
@Option(name = "device-boot-time", description = "Max time in ms to wait for device to boot.")
protected long mDeviceBootTime = 5 * 60 * 1000;
@Option(
name = "first-boot",
description = "Calculate the boot time metrics after flashing the device")
private boolean mFirstBoot = true;
@Option(
name = "successive-boot-prepare-cmd",
description =
"A list of adb commands to run after first boot to prepare for successive"
+ " boot tests")
private List<String> mDeviceSetupCommands = new ArrayList<>();
@Option(
name = "test-file-name",
description =
"Name of a apk in expanded test zip to install on device. Can be repeated.",
importance = Importance.IF_UNSET)
private List<File> mTestFileNames = new ArrayList<>();
@Option(
name = "alt-dir",
description =
"Alternate directory to look for the apk if the apk is not in the tests zip"
+ " file. For each alternate dir, will look in //, //data/app, "
+ "//DATA/app,"
+ " //DATA/app/apk_name/ and //DATA/priv-app/apk_name/. Can be "
+ "repeated."
+ " Look for apks in last alt-dir first.")
private List<File> mAltDirs = new ArrayList<>();
@Option(name = "successive-boot", description = "Calculate the successive boot delay info")
private boolean mSuccessiveBoot = false;
@Option(
name = "boot-count",
description =
"Number of times to boot the devices to calculate the successive boot delay."
+ " Second boot after the first boot will be skipped for correctness.")
private int mBootCount = 5;
@Option(
name = "boot-delay",
isTimeVal = true,
description = "Time to wait between the successive boots.")
private long mBootDelayTime = 2000;
@Option(
name = "after-boot-delay",
isTimeVal = true,
description = "Time to wait immediately after the successive boots.")
private long mAfterBootDelayTime = 0;
@Option(
name = "post-initial-boot-idle",
isTimeVal = true,
description =
"Time to keep device idle after the initial boot up. This can help stablize "
+ "certain metrics like detecting post boot crashes and "
+ "SELinux denials.")
private long mPostInitialBootIdle = 0L;
@Option(
name = "granular-boot-info",
description = "Parse the granular timing info from successive boot time.")
private boolean mGranularBootInfo = false;
@Option(
name = "boot-time-pattern",
description =
"Named boot time regex patterns which are used to capture signals in logcat and"
+ " calculate duration between device boot to the signal being logged."
+ " Key: name of custom boot metric, Value: regex to match single"
+ " logcat line. Maybe repeated.")
private Map<String, String> mBootTimePatterns = new HashMap<>();
@Option(name = "dmesg-info", description = "Collect the init services info from dmesg logs.")
private boolean mDmesgInfo = false;
// Use this flag not to dump the dmesg logs immediately after the device is online.
// Use it only if some of the boot dmesg logs are cleared when waiting until boot completed.
// By default this is set to true which might result in duplicate logging.
@Option(
name = "dump-dmesg-immediate",
description =
"Whether to dump the dmesg logs" + "immediately after the device is online")
private boolean mDumpDmesgImmediate = true;
@Option(name = "bootloader-info", description = "Collect the boot loader timing.")
private boolean mBootloaderInfo = false;
@Option(
name = "report-storage-suffix-metric",
description = "separately report boot time" + " results for storage type")
private boolean mReportStorageSuffixMetric = false;
@Option(
name = "report-boottime-per-iteration",
description = "separately report boot time results per iteration")
private boolean mBootTimePerIteration = true;
// 03-10 21:43:40.328 1005 1005 D SystemServerTiming:StartWifi took to
// complete: 3474ms
// 03-10 21:43:40.328 1005 1005 D component:subcomponent took to complete:
// 3474ms
@Option(
name = "components",
shortName = 'c',
description =
"Comma separated list of component names to parse the granular boot info"
+ " printed in the logcat.")
private String mComponentNames = null;
@Option(
name = "full-components",
shortName = 'f',
description =
"Comma separated list of component_subcomponent names to parse the granular"
+ " boot info printed in the logcat.")
private String mFullCompNames = null;
@Option(
name = "test-reboot-unlock",
description = "Test the reboot scenario with" + "screen unlock.")
private boolean mRebootUnlock = false;
@Option(
name = "force-f2fs-shutdown",
description = "Force f2fs shutdown to trigger fsck check during the reboot.")
private boolean mForceF2FsShutdown = false;
@Option(
name = "skip-pin-setup",
description =
"Skip the pin setup if already set once"
+ "and not needed for the second run especially in local testing.")
private boolean mSkipPinSetup = false;
@Option(
name = "collect-logcat-info",
description = "Run logcat --statistics command and collect data")
private boolean mCollectLogcat = false;
@Option(
name = "metric-prefix-pattern-for-count",
description =
"A list of metric prefix pattern that will be used to count number of metrics"
+ " generated in the test")
private List<String> mMetricPrefixPatternForCount = new ArrayList<>();
private IBuildInfo mBuildInfo;
private IConfiguration mConfiguration;
private TestInformation mTestInfo;
private Map<String, List<Double>> mBootInfo = new LinkedHashMap<>();
private Map<String, Double> mBootIterationInfo = new LinkedHashMap<>();
private LogcatReceiver mRebootLogcatReceiver = null;
protected String mExtraFirstBootError = null;
private IRemoteAndroidTestRunner mRunner = null;
private Set<String> mParsedLines = new HashSet<String>();
private List<String> mInstalledPackages = new ArrayList<String>();
private IInvocationContext mInvocationContext = null;
private List<IMetricCollector> mCollectors = new ArrayList<>();
/** {@inheritDoc} */
@Override
public void setBuild(IBuildInfo buildInfo) {
mBuildInfo = buildInfo;
}
/** {@inheritDoc} */
@Override
public void setConfiguration(IConfiguration configuration) {
// Ensure the configuration is set on InstalledInstrumentationsTest
super.setConfiguration(configuration);
mConfiguration = configuration;
}
public TestInformation getTestInformation() {
return mTestInfo;
}
@VisibleForTesting
void setRebootLogcatReceiver(LogcatReceiver receiver) {
mRebootLogcatReceiver = receiver;
}
@VisibleForTesting
void setDmesgBootCompleteTime(List<Double> bootCompleteTime) {
mBootInfo.put(DMESG_BOOT_COMPLETE_TIME, bootCompleteTime);
}
@VisibleForTesting
void setDmesgBootIterationTime(Double bootCompleteTime) {
mBootIterationInfo.put(DMESG_BOOT_COMPLETE_TIME, bootCompleteTime);
}
@VisibleForTesting
List<Double> getBootMetricValues(String bootMetricKey) {
return mBootInfo.getOrDefault(bootMetricKey, new ArrayList<>());
}
@Override
public void setMetricCollectors(List<IMetricCollector> collectors) {
mCollectors = collectors;
}
@Override
public void setInvocationContext(IInvocationContext invocationContext) {
mInvocationContext = invocationContext;
}
/** Returns the {@link IBuildInfo} for the test. */
public IBuildInfo getBuildInfo() {
return mBuildInfo;
}
/** Returns the {@link IRunUtil} instance to use to run some command. */
public IRunUtil getRunUtil() {
return RunUtil.getDefault();
}
/** {@inheritDoc} */
@Override
public void run(TestInformation testInfo, ITestInvocationListener listener)
throws DeviceNotAvailableException {
mTestInfo = testInfo;
long start = System.currentTimeMillis();
listener.testRunStarted(mTestRunName, 1);
for (IMetricCollector collector : mCollectors) {
listener = collector.init(mInvocationContext, listener);
}
try {
try {
// Set the current date from the host in test device.
getDevice().setDate(null);
// Setup device for successive boots, e.g. dismiss SuW
setupDeviceForSuccessiveBoots();
Map<String, String> successiveResult = new HashMap<>();
boolean isSuccessiveBootsSuccess = true;
TestDescription successiveBootTestId =
new TestDescription(
String.format("%s.%s", BOOTTIME_TEST, BOOTTIME_TEST),
SUCCESSIVE_BOOT_TEST);
try {
// Skip second boot from successive boot delay calculation
doSecondBoot();
testSuccessiveBoots(false, listener);
// Reports average value of individual metrics collected
listener.testStarted(successiveBootTestId);
} catch (RuntimeException re) {
CLog.e(re);
isSuccessiveBootsSuccess = false;
listener.testStarted(successiveBootTestId);
listener.testFailed(
successiveBootTestId,
String.format("RuntimeException during successive reboots: %s", re));
} catch (DeviceNotAvailableException dnae) {
CLog.e("Device not available after successive reboots");
CLog.e(dnae);
isSuccessiveBootsSuccess = false;
listener.testStarted(successiveBootTestId);
listener.testFailed(
successiveBootTestId,
String.format(
"Device not available after successive reboots; %s", dnae));
} finally {
if (isSuccessiveBootsSuccess) {
if (null != mRebootLogcatReceiver) {
try (InputStreamSource logcatData =
mRebootLogcatReceiver.getLogcatData()) {
listener.testLog(LOGCAT_FILE, LogDataType.TEXT, logcatData);
}
mRebootLogcatReceiver.stop();
}
listener.testEnded(successiveBootTestId, successiveResult);
// Report separately the hardware Storage specific boot time results.
if (!successiveResult.isEmpty() && mReportStorageSuffixMetric) {
reportStorageSpecificMetrics(listener, successiveResult);
}
} else {
listener.testEnded(successiveBootTestId, successiveResult);
}
}
// Test to measure the reboot time and time from unlocking the
// screen using the pin
// till the NexusLauncherActivity is displayed.
if (mRebootUnlock) {
mBootInfo.clear();
Map<String, String> successiveBootUnlockResult = new HashMap<>();
TestDescription successiveBootUnlockTestId =
new TestDescription(
String.format("%s.%s", BOOTTIME_TEST, BOOTTIME_TEST),
SUCCESSIVE_BOOT_UNLOCK_TEST);
try {
// If pin is already set skip the setup method otherwise
// setup the pin.
if (!mSkipPinSetup) {
mRunner = createRemoteAndroidTestRunner(SETUP_PIN_TEST);
getDevice()
.runInstrumentationTests(mRunner, new CollectingTestListener());
}
testSuccessiveBoots(true, listener);
} finally {
if (null != mRebootLogcatReceiver) {
try (InputStreamSource logcatData =
mRebootLogcatReceiver.getLogcatData()) {
listener.testLog(LOGCAT_UNLOCK_FILE, LogDataType.TEXT, logcatData);
}
mRebootLogcatReceiver.stop();
}
listener.testStarted(successiveBootUnlockTestId);
listener.testEnded(successiveBootUnlockTestId, successiveBootUnlockResult);
}
}
} finally {
// Finish run for boot test. Health check below will start its own test run.
listener.testRunEnded(
System.currentTimeMillis() - start, new HashMap<String, Metric>());
}
} finally {
finalTearDown(listener);
}
}
/** Final optional tear down step for the Boot test. */
public void finalTearDown(ITestInvocationListener listener) throws DeviceNotAvailableException {
// empty on purpose
}
/**
* Look for the perfetto trace file collected during reboot under /data/misc/perfetto-traces and
* copy the file under /data/local/tmp using the test iteration name and return the path to the
* newly copied trace file.
*/
private String processPerfettoFile(String testId) throws DeviceNotAvailableException {
CommandResult result = getDevice().executeShellV2Command(PERFETTO_TRACE_FILE_CHECK_CMD);
if (result != null) {
CLog.i(
"Command Output: Cmd - %s, Output - %s, Error - %s, Status - %s",
PERFETTO_TRACE_FILE_CHECK_CMD,
result.getStdout(),
result.getStderr(),
result.getStatus());
if (CommandStatus.SUCCESS.equals(result.getStatus())
&& result.getStdout().contains("boottrace.perfetto-trace")) {
// Move the perfetto trace file to the new location and rename it using the test
// name.
String finalTraceFileLocation =
String.format(FINAL_PERFETTO_TRACE_LOCATION, testId);
CommandResult moveResult =
getDevice()
.executeShellV2Command(
String.format(
PERFETTO_TRACE_MV_CMD, finalTraceFileLocation));
if (moveResult != null) {
CLog.i(
"Command Output: Cmd - %s, Output - %s, Error - %s, Status - %s",
PERFETTO_TRACE_MV_CMD,
moveResult.getStdout(),
moveResult.getStderr(),
moveResult.getStatus());
if (CommandStatus.SUCCESS.equals(result.getStatus())) {
return finalTraceFileLocation;
}
}
}
}
return null;
}
private void setupDeviceForSuccessiveBoots() throws DeviceNotAvailableException {
// Run the list of post first boot setup commands
for (String cmd : mDeviceSetupCommands) {
CommandResult result;
result = getDevice().executeShellV2Command(cmd);
if (!CommandStatus.SUCCESS.equals(result.getStatus())) {
CLog.w(
"Post boot setup cmd: '%s' failed, returned:\nstdout:%s\nstderr:%s",
cmd, result.getStdout(), result.getStderr());
}
}
}
/** Do the second boot on the device to exclude from the successive boot time calcualtions */
private void doSecondBoot() throws DeviceNotAvailableException {
getDevice().nonBlockingReboot();
getDevice().waitForDeviceOnline();
getDevice().waitForDeviceAvailable(mDeviceBootTime);
}
/**
* Clear the existing logs and start capturing the logcat
*
* <p>It will record from all logcat buffers if user provided any custom boot time metric
* patterns
*/
private void clearAndStartLogcat() throws DeviceNotAvailableException {
getDevice().executeShellCommand("logcat -c");
boolean allBuffers = !mBootTimePatterns.isEmpty();
if (mRebootLogcatReceiver == null) {
mRebootLogcatReceiver =
new LogcatReceiver(
getDevice(), allBuffers ? LOGCAT_CMD_ALL : LOGCAT_CMD, LOGCAT_SIZE, 0);
}
mRebootLogcatReceiver.start();
}
/**
* To perform the successive boot for given boot count and take the average of boot time and
* online time delays
*
* @param dismissPin to dismiss pin after reboot
*/
private void testSuccessiveBoots(boolean dismissPin, ITestInvocationListener listener)
throws DeviceNotAvailableException {
CLog.v("Waiting for %d msecs before successive boots.", mBootDelayTime);
getRunUtil().sleep(mBootDelayTime);
for (int count = 0; count < mBootCount; count++) {
getDevice().enableAdbRoot();
// Property used for collecting the perfetto trace file on boot.
getDevice().executeShellCommand("setprop persist.debug.perfetto.boottrace 1");
// Attempt to shurdown F2FS if the option is enabled.
if (mForceF2FsShutdown) {
String output = getDevice().executeShellCommand(F2FS_SHUTDOWN_COMMAND).trim();
if (!F2FS_SHUTDOWN_SUCCESS_OUTPUT.equalsIgnoreCase(output)) {
CLog.e("Unable to shutdown the F2FS.");
} else {
CLog.i("F2FS shutdown successful.");
}
}
DmesgParser dmesgLogParser = null;
double bootStart = INVALID_TIME_DURATION;
double onlineTime = INVALID_TIME_DURATION;
double bootTime = INVALID_TIME_DURATION;
String testId = String.format("%s.%s$%d", BOOTTIME_TEST, BOOTTIME_TEST, (count + 1));
TestDescription successiveBootIterationTestId;
if (!dismissPin) {
successiveBootIterationTestId =
new TestDescription(testId, String.format("%s", SUCCESSIVE_BOOT_TEST));
} else {
successiveBootIterationTestId =
new TestDescription(
testId, String.format("%s", SUCCESSIVE_BOOT_UNLOCK_TEST));
}
if (mGranularBootInfo || dismissPin) {
clearAndStartLogcat();
getRunUtil().sleep(5000);
}
getDevice().nonBlockingReboot();
bootStart = System.currentTimeMillis();
getDevice().waitForDeviceOnline();
onlineTime = System.currentTimeMillis() - bootStart;
getDevice().enableAdbRoot();
dmesgLogParser = new DmesgParser();
if (mDmesgInfo && mDumpDmesgImmediate) {
// Collect the dmesg logs after device is online and
// after the device is boot completed to avoid losing the
// initial logs in some devices.
parseDmesgInfo(dmesgLogParser);
}
try {
waitForBootCompleted();
} catch (InterruptedException e) {
CLog.e("Sleep Interrupted");
CLog.e(e);
} catch (DeviceNotAvailableException dne) {
CLog.e("Device not available");
CLog.e(dne);
}
bootTime = System.currentTimeMillis() - bootStart;
if (mDmesgInfo) {
// Collect the dmesg logs after device is online and
// after the device is boot completed to avoid losing the
// initial logs.
parseDmesgInfo(dmesgLogParser);
if (!dmesgLogParser.getServiceInfoItems().isEmpty()) {
analyzeDmesgServiceInfo(dmesgLogParser.getServiceInfoItems().values());
}
if (!dmesgLogParser.getStageInfoItems().isEmpty()) {
analyzeDmesgStageInfo(dmesgLogParser.getStageInfoItems());
}
if (!dmesgLogParser.getActionInfoItems().isEmpty()) {
analyzeDmesgActionInfo(dmesgLogParser.getActionInfoItems());
}
}
// Parse logcat info
Map<String, String> collectLogcatInfoResult = new HashMap<>();
if (mCollectLogcat) {
collectLogcatInfoResult.putAll(collectLogcatInfo());
}
// Parse bootloader timing info
if (mBootloaderInfo) analyzeBootloaderTimingInfo();
if (dismissPin) {
getRunUtil().sleep(2000);
mRunner = createRemoteAndroidTestRunner(UNLOCK_PIN_TEST);
getDevice().runInstrumentationTests(mRunner, new CollectingTestListener());
}
if (mBootTimePerIteration) {
listener.testStarted(successiveBootIterationTestId);
}
CLog.v("Waiting for %d msecs immediately after successive boot.", mAfterBootDelayTime);
getRunUtil().sleep(mAfterBootDelayTime);
if (onlineTime != INVALID_TIME_DURATION) {
if (mBootInfo.containsKey(SUCCESSIVE_ONLINE)) {
mBootInfo.get(SUCCESSIVE_ONLINE).add(onlineTime);
} else {
List<Double> onlineDelayList = new ArrayList<Double>();
onlineDelayList.add(onlineTime);
mBootInfo.put(SUCCESSIVE_ONLINE, onlineDelayList);
}
mBootIterationInfo.put(SUCCESSIVE_ONLINE, onlineTime);
}
if (bootTime != INVALID_TIME_DURATION) {
if (mBootInfo.containsKey(SUCCESSIVE_BOOT)) {
mBootInfo.get(SUCCESSIVE_BOOT).add(bootTime);
} else {
List<Double> bootDelayList = new ArrayList<>();
bootDelayList.add(bootTime);
mBootInfo.put(SUCCESSIVE_BOOT, bootDelayList);
}
mBootIterationInfo.put(SUCCESSIVE_BOOT, bootTime);
}
if (mGranularBootInfo) {
getRunUtil().sleep(15000);
analyzeGranularBootInfo();
analyzeCustomBootInfo();
try (InputStreamSource logcatData = mRebootLogcatReceiver.getLogcatData()) {
listener.testLog(
String.format("%s_%d", LOGCAT_FILE, (count + 1)),
LogDataType.TEXT,
logcatData);
}
if (count != (mBootCount - 1)) {
mRebootLogcatReceiver.stop();
mRebootLogcatReceiver = null;
}
}
if (dismissPin) {
analyzeUnlockBootInfo();
if (count != (mBootCount - 1)) {
mRebootLogcatReceiver.stop();
mRebootLogcatReceiver = null;
}
}
String perfettoTraceFilePath = processPerfettoFile(testId.replace("$", "_"));
if (mBootTimePerIteration) {
Map<String, String> iterationResult = new HashMap<>();
for (Map.Entry<String, Double> bootData : mBootIterationInfo.entrySet()) {
iterationResult.put(bootData.getKey(), bootData.getValue().toString());
}
if (perfettoTraceFilePath != null) {
iterationResult.put("perfetto_file_path", perfettoTraceFilePath);
}
if (!collectLogcatInfoResult.isEmpty()) {
iterationResult.putAll(collectLogcatInfoResult);
}
// If metric-prefix-pattern-for-count is present, calculate the count
// of all metrics with the prefix pattern and add the count as a new metric to the
// iterationResult map.
if (!mMetricPrefixPatternForCount.isEmpty()) {
for (String metricPrefixPattern : mMetricPrefixPatternForCount) {
long metricCount =
iterationResult.entrySet().stream()
.filter(
(entry) ->
entry.getKey()
.startsWith(metricPrefixPattern))
.count();
iterationResult.put(
metricPrefixPattern + METRIC_COUNT, Long.toString(metricCount));
}
}
listener.testEnded(successiveBootIterationTestId, iterationResult);
}
mBootIterationInfo.clear();
CLog.v("Waiting for %d msecs after boot completed.", mBootDelayTime);
getRunUtil().sleep(mBootDelayTime);
}
}
/** Get the logcat statistics info */
private Map<String, String> collectLogcatInfo() throws DeviceNotAvailableException {
Map<String, String> results = new HashMap<>();
// Parse logcat in global level
results.putAll(extractLogcatInfo(-1, "general"));
// Get all the process PIDs first
Map<Integer, String> pids = getPids();
if (pids.size() == 0) {
CLog.e("Failed to get all the valid process PIDs");
return results;
}
// Parse logcat in process level
for (Map.Entry<Integer, String> process : pids.entrySet()) {
results.putAll(extractLogcatInfo(process.getKey(), process.getValue()));
}
return results;
}
/** Get pid of all processes */
private Map<Integer, String> getPids() throws DeviceNotAvailableException {
// return pids
Map<Integer, String> pids = new HashMap<>();
String pidOutput = getDevice().executeShellCommand(ALL_PROCESS_CMD);
// Sample output for the process info
// Sample command : "ps -A"
// Sample output :
// system 4533 410 13715708 78536 do_freezer_trap 0 S com.android.keychain
// root 32552 2 0 0 worker_thread 0 I [kworker/6:0-memlat_wq]
String[] lines = pidOutput.split(System.lineSeparator());
for (String line : lines) {
String[] splitLine = line.split("\\s+");
// Skip the first (i.e header) line from "ps -A" output.
if (splitLine[1].equalsIgnoreCase("PID")) {
continue;
}
String processName = splitLine[splitLine.length - 1].replace("\n", "").trim();
pids.put(Integer.parseInt(splitLine[1]), processName);
}
return pids;
}
/** Get the logcat statistics info */
private Map<String, String> extractLogcatInfo(int pid, String processName)
throws DeviceNotAvailableException {
Map<String, String> results = new HashMap<>();
String runCommand =
pid == -1 ? LOGCAT_CMD_STATISTICS_ALL : String.format(LOGCAT_CMD_STATISTICS, pid);
String output = getDevice().executeShellCommand(runCommand);
String[] outputList = output.split("\\n\\n");
if (pid == -1) {
// General statistics
// Sample output for $ logcat --statistics
// size/num main system crash kernel Total
// Total 33/23 96/91 3870/4 70/1 513/41
// Now 92/70 4/15 0/0 13/11 33/26
// Logspan 5:15:15.15 11d 20:37:31.37 13:20:54.185 11d 20:40:06.816
// Overhead 253454 56415 255139 1330477
Matcher matcherHeader = LOGCAT_STATISTICS_HEADER_PATTERN.matcher(outputList[0]);
Matcher matcherTotal = LOGCAT_STATISTICS_TOTAL_PATTERN.matcher(outputList[0]);
Matcher matcherNow = LOGCAT_STATISTICS_NOW_PATTERN.matcher(outputList[0]);
boolean headerFound = matcherHeader.find();
boolean totalFound = matcherTotal.find();
boolean nowFound = matcherNow.find();
if (headerFound && totalFound && nowFound) {
// There are 6 columns in the output, but we just want to extract column 1 to 4
for (int i = 1; i < 5; i++) {
String bufferHeader = matcherHeader.group(i).trim();
results.put(
String.join(
METRIC_KEY_SEPARATOR,
LOGCAT_STATISTICS_SIZE,
bufferHeader,
processName),
matcherTotal.group(i * 2 - 1).trim());
results.put(
String.join(
METRIC_KEY_SEPARATOR,
LOGCAT_STATISTICS_DIFF_SIZE,
bufferHeader,
processName),
Integer.toString(
Integer.valueOf(matcherTotal.group(i * 2 - 1).trim())
- Integer.valueOf(matcherNow.group(i * 2 - 1).trim())));
}
}
} else if (outputList.length > 1) {
// Process statistics
// Sample output for $ logcat --statistics --pid 3330
// Logging for this PID: Size Pruned
// PID/UID COMMAND LINE BYTES NUM
// 3330/10171 ...ogle.android.googlequicksearchbox:interactor 13024
boolean pidFound = false;
for (int i = 0; i < outputList.length; i++) {
Matcher matcherPid = LOGCAT_STATISTICS_PID_PATTERN.matcher(outputList[i]);
pidFound = matcherPid.find();
if (!pidFound) continue;
CLog.d(
"Process %s with pid %d : logcat statistics output = %s",
processName, pid, outputList[i]);
results.put(
String.join(METRIC_KEY_SEPARATOR, LOGCAT_STATISTICS_SIZE, processName),
matcherPid.group(1).trim());
break;
}
if (!pidFound) {
// the process doesn't found in the logcat statistics output
CLog.d(
"Process %s with pid %d doesn't exist in logcat statistics.",
processName, pid);
}
}
return results;
}
/**
* Parse the logcat file for granular boot info (eg different system services start time) based
* on the component name or full component name (i.e component_subcompname)
*/
private void analyzeGranularBootInfo() {
String[] compStr = new String[0];
String[] fullCompStr = new String[0];
boolean isFilterSet = false;
if (null != mComponentNames) {
compStr = mComponentNames.split(",");
isFilterSet = true;
}
if (null != mFullCompNames) {
fullCompStr = mFullCompNames.split(",");
isFilterSet = true;
}
Set<String> compSet = new HashSet<>(Arrays.asList(compStr));
Set<String> fullCompSet = new HashSet<>(Arrays.asList(fullCompStr));
try (InputStreamSource logcatData = mRebootLogcatReceiver.getLogcatData();
InputStream logcatStream = logcatData.createInputStream();
InputStreamReader logcatReader = new InputStreamReader(logcatStream);
BufferedReader br = new BufferedReader(logcatReader)) {
TimingsLogParser parser = new TimingsLogParser();
List<SystemServicesTimingItem> items = parser.parseSystemServicesTimingItems(br);
for (SystemServicesTimingItem item : items) {
String componentName = item.getComponent();
String fullCompName =
String.format("%s_%s", item.getComponent(), item.getSubcomponent());
// If filter not set then capture timing info for all the
// components otherwise
// only for the given component names and full component
// names.
if (!isFilterSet
|| compSet.contains(componentName)
|| fullCompSet.contains(fullCompName)) {
Double time =
item.getDuration() != null ? item.getDuration() : item.getStartTime();
if (time == null) {
continue;
}
if (mBootInfo.containsKey(fullCompName)) {
mBootInfo.get(fullCompName).add(time);
} else {
List<Double> delayList = new ArrayList<>();
delayList.add(time);
mBootInfo.put(fullCompName, delayList);
}
mBootIterationInfo.put(fullCompName, time);
}
}
} catch (IOException ioe) {
CLog.e("Problem in parsing the granular boot delay information");
CLog.e(ioe);
}
}
/** Parse the logcat file to get boot time metrics given patterns defined by tester. */
private void analyzeCustomBootInfo() {
if (mBootTimePatterns.isEmpty()) return;
Double dmesgBootCompleteTimes;
TimingsLogParser parser = new TimingsLogParser();
parser.addDurationPatternPair(BOOT_PHASE_1000, KERNEL_START_PATTERN, LOGCAT_BOOT_COMPLETED);
for (Map.Entry<String, String> pattern : mBootTimePatterns.entrySet()) {
CLog.d(
"Adding boot metric with name: %s, pattern: %s",
pattern.getKey(), pattern.getValue());
parser.addDurationPatternPair(
pattern.getKey(), KERNEL_START_PATTERN, Pattern.compile(pattern.getValue()));
}
try (InputStreamSource logcatData = mRebootLogcatReceiver.getLogcatData();
InputStream logcatStream = logcatData.createInputStream();
InputStreamReader logcatReader = new InputStreamReader(logcatStream);
BufferedReader br = new BufferedReader(logcatReader)) {
if (mBootIterationInfo.containsKey(DMESG_BOOT_COMPLETE_TIME)) {
dmesgBootCompleteTimes = mBootIterationInfo.get(DMESG_BOOT_COMPLETE_TIME);
} else {
CLog.d("Missing dmesg boot complete signals");
return;
}
List<GenericTimingItem> items = parser.parseGenericTimingItems(br);
Map<String, GenericTimingItem> itemsMap = new HashMap<>();
GenericTimingItem logcatBootCompleteItem = new GenericTimingItem();
for (GenericTimingItem item : items) {
if (BOOT_PHASE_1000.equals(item.getName())) {
logcatBootCompleteItem = item;
} else {
itemsMap.put(item.getName(), item);
}
}
if (logcatBootCompleteItem.getName() == null) {
CLog.e("Missing boot complete signals from logcat");
return;
}
for (Map.Entry<String, GenericTimingItem> metric : itemsMap.entrySet()) {
GenericTimingItem itemsForMetric = metric.getValue();
if (itemsForMetric.getName().isEmpty()) {
CLog.e("Missing value for metric %s", metric.getKey());
continue;
}
List<Double> values = mBootInfo.getOrDefault(metric.getKey(), new ArrayList<>());
double duration =
dmesgBootCompleteTimes
+ itemsForMetric.getEndTime()
- logcatBootCompleteItem.getEndTime();
values.add(duration);
mBootInfo.put(metric.getKey(), values);
mBootIterationInfo.put(metric.getKey(), duration);
CLog.d("Added boot metric: %s with duration values: %s", metric.getKey(), values);
}
} catch (IOException e) {
CLog.e("Problem when parsing custom boot time info");
CLog.e(e);
}
}
/**
* Collect the dmesg logs and parse the service info(start and end time), start time of boot
* stages and actions being processed, logged in the dmesg file.
*/
private void parseDmesgInfo(DmesgParser dmesgLogParser) throws DeviceNotAvailableException {
// Dump the dmesg logs to a file in the device
getDevice().executeShellCommand(DUMP_DMESG);
try {
File dmesgFile = getDevice().pullFile(DMESG_FILE);
BufferedReader input =
new BufferedReader(new InputStreamReader(new FileInputStream(dmesgFile)));
dmesgLogParser.parseInfo(input);
dmesgFile.delete();
} catch (IOException ioe) {
CLog.e("Failed to analyze the dmesg logs", ioe);
}
}
/**
* Analyze the services info parsed from the dmesg logs and construct the metrics as a part of
* boot time data.
*
* @param serviceInfoItems contains the start time, end time and the duration of each service
* logged in the dmesg log file.
*/
private void analyzeDmesgServiceInfo(Collection<DmesgServiceInfoItem> serviceInfoItems) {
for (DmesgServiceInfoItem infoItem : serviceInfoItems) {
if (infoItem.getStartTime() != null) {
String key = String.format("%s%s%s", INIT, infoItem.getServiceName(), START_TIME);
if (mBootInfo.get(key) != null) {
mBootInfo.get(key).add(infoItem.getStartTime().doubleValue());
} else {
List<Double> timeList = new ArrayList<Double>();
timeList.add(infoItem.getStartTime().doubleValue());
mBootInfo.put(key, timeList);
}
mBootIterationInfo.put(key, infoItem.getStartTime().doubleValue());
}
if (infoItem.getServiceDuration() != -1L) {
String key = String.format("%s%s%s", INIT, infoItem.getServiceName(), DURATION);
if (mBootInfo.get(key) != null) {
mBootInfo.get(key).add(infoItem.getServiceDuration().doubleValue());
} else {
List<Double> timeList = new ArrayList<Double>();
timeList.add(infoItem.getServiceDuration().doubleValue());
mBootInfo.put(key, timeList);
}
mBootIterationInfo.put(key, infoItem.getServiceDuration().doubleValue());
}
if (infoItem.getEndTime() != null) {
String key = String.format("%s%s%s", INIT, infoItem.getServiceName(), END_TIME);
if (mBootInfo.get(key) != null) {
mBootInfo.get(key).add(infoItem.getEndTime().doubleValue());
} else {
List<Double> timeList = new ArrayList<Double>();
timeList.add(infoItem.getEndTime().doubleValue());
mBootInfo.put(key, timeList);
}
mBootIterationInfo.put(key, infoItem.getEndTime().doubleValue());
}
}
}
/**
* Analyze the boot stages info parsed from the dmesg logs and construct the metrics as a part
* of boot time data.
*
* @param stageInfoItems contains the start time of each stage logged in the dmesg log file.
*/
private void analyzeDmesgStageInfo(Collection<DmesgStageInfoItem> stageInfoItems) {
for (DmesgStageInfoItem stageInfoItem : stageInfoItems) {
if (stageInfoItem.getStartTime() != null) {
String key =
String.format(
"%s%s%s", INIT_STAGE, stageInfoItem.getStageName(), START_TIME);
List<Double> values = mBootInfo.getOrDefault(key, new ArrayList<>());
values.add(stageInfoItem.getStartTime().doubleValue());
mBootInfo.put(key, values);
mBootIterationInfo.put(key, stageInfoItem.getStartTime().doubleValue());
} else if (stageInfoItem.getDuration() != null) {
List<Double> values =
mBootInfo.getOrDefault(stageInfoItem.getStageName(), new ArrayList<>());
values.add(stageInfoItem.getDuration().doubleValue());
mBootInfo.put(stageInfoItem.getStageName(), values);
mBootIterationInfo.put(
stageInfoItem.getStageName(), stageInfoItem.getDuration().doubleValue());
}
}
}
/**
* Analyze each action info parsed from the dmesg logs and construct the metrics as a part of
* boot time data.
*
* @param actionInfoItems contains the start time of processing of each action logged in the
* dmesg log file.
*/
private void analyzeDmesgActionInfo(Collection<DmesgActionInfoItem> actionInfoItems) {
boolean isFirstBootCompletedAction = true;
for (DmesgActionInfoItem actionInfoItem : actionInfoItems) {
if (actionInfoItem.getStartTime() != null) {
if (actionInfoItem.getActionName().startsWith(BOOT_COMPLETE_ACTION)
&& isFirstBootCompletedAction) {
CLog.i(
"Using Action: %s_%s for first boot complete timestamp :%s",
actionInfoItem.getActionName(),
actionInfoItem.getSourceName(),
actionInfoItem.getStartTime().doubleValue());
// Record the first boot complete time stamp.
List<Double> dmesgBootCompleteTimes =
mBootInfo.getOrDefault(DMESG_BOOT_COMPLETE_TIME, new ArrayList<>());
dmesgBootCompleteTimes.add(actionInfoItem.getStartTime().doubleValue());
mBootInfo.put(DMESG_BOOT_COMPLETE_TIME, dmesgBootCompleteTimes);
mBootIterationInfo.put(
DMESG_BOOT_COMPLETE_TIME, actionInfoItem.getStartTime().doubleValue());
isFirstBootCompletedAction = false;
}
String key =
String.format(
"%s%s_%s%s",
ACTION,
actionInfoItem.getActionName(),
actionInfoItem.getSourceName() != null
? actionInfoItem.getSourceName()
: "",
START_TIME);
List<Double> values = mBootInfo.getOrDefault(key, new ArrayList<>());
values.add(actionInfoItem.getStartTime().doubleValue());
mBootInfo.put(key, values);
mBootIterationInfo.put(key, actionInfoItem.getStartTime().doubleValue());
}
}
}
/**
* Analyze the time taken by different phases in boot loader by parsing the system property
* ro.boot.boottime
*/
private void analyzeBootloaderTimingInfo() throws DeviceNotAvailableException {
String bootLoaderVal = getDevice().getProperty(BOOT_TIME_PROP);
// Sample Output : 1BLL:89,1BLE:590,2BLL:0,2BLE:1344,SW:6734,KL:1193
if (bootLoaderVal != null) {
String[] bootLoaderPhases = bootLoaderVal.split(",");
double bootLoaderTotalTime = 0d;
for (String bootLoaderPhase : bootLoaderPhases) {
String[] bootKeyVal = bootLoaderPhase.split(":");
String key = String.format("%s%s", BOOTLOADER_PREFIX, bootKeyVal[0]);
if (mBootInfo.containsKey(key)) {
mBootInfo.get(key).add(Double.parseDouble(bootKeyVal[1]));
} else {
List<Double> timeList = new ArrayList<Double>();
timeList.add(Double.parseDouble(bootKeyVal[1]));
mBootInfo.put(key, timeList);
}
mBootIterationInfo.put(key, Double.parseDouble(bootKeyVal[1]));
// SW is the time spent on the warning screen. So ignore it in
// final boot time calculation.
if (!BOOTLOADER_PHASE_SW.equalsIgnoreCase(bootKeyVal[0])) {
bootLoaderTotalTime += Double.parseDouble(bootKeyVal[1]);
}
}
// Report bootloader time as well in the dashboard.
CLog.i("Bootloader time is :%s", bootLoaderTotalTime);
if (mBootInfo.containsKey(BOOTLOADER_TIME)) {
mBootInfo.get(BOOTLOADER_TIME).add(bootLoaderTotalTime);
} else {
List<Double> timeList = new ArrayList<Double>();
timeList.add(bootLoaderTotalTime);
mBootInfo.put(BOOTLOADER_TIME, timeList);
}
mBootIterationInfo.put(BOOTLOADER_TIME, bootLoaderTotalTime);
// First "action_sys.boot_completed=1_START_TIME" is parsed already from dmesg logs.
// Current dmesg boot complete time should always be the last one in value list.
// Calculate the sum of bootLoaderTotalTime and boot completed flag set time.
List<Double> bootCompleteTimes =
mBootInfo.getOrDefault(DMESG_BOOT_COMPLETE_TIME, new ArrayList<>());
double bootCompleteTime =
bootCompleteTimes.isEmpty()
? 0L
: bootCompleteTimes.get(bootCompleteTimes.size() - 1);
double totalBootTime = bootLoaderTotalTime + bootCompleteTime;
if (mBootInfo.containsKey(TOTAL_BOOT_TIME)) {
mBootInfo.get(TOTAL_BOOT_TIME).add(totalBootTime);
} else {
List<Double> timeList = new ArrayList<Double>();
timeList.add(totalBootTime);
mBootInfo.put(TOTAL_BOOT_TIME, timeList);
}
mBootIterationInfo.put(TOTAL_BOOT_TIME, totalBootTime);
}
}
/**
* Parse the logcat file and calculate the time difference between the screen unlocked timestamp
* till the Nexus launcher activity is displayed.
*/
private void analyzeUnlockBootInfo() {
try (InputStreamSource logcatData = mRebootLogcatReceiver.getLogcatData();
InputStream logcatStream = logcatData.createInputStream();
InputStreamReader logcatReader = new InputStreamReader(logcatStream);
BufferedReader br = new BufferedReader(logcatReader)) {
boolean logOrderTracker = false;
double unlockInMillis = 0d;
String line;
while ((line = br.readLine()) != null) {
Matcher match = null;
if ((match = matches(SCREEN_UNLOCKED, line)) != null && !isDuplicateLine(line)) {
mParsedLines.add(line);
Date time = parseTime(match.group(1));
unlockInMillis = time.getTime();
logOrderTracker = true;
} else if ((match = matches(DISPLAYED_LAUNCHER, line)) != null
&& !isDuplicateLine(line)
&& logOrderTracker) {
Date time = parseTime(match.group(1));
if (mBootInfo.containsKey(UNLOCK_TIME)) {
mBootInfo.get(UNLOCK_TIME).add(time.getTime() - unlockInMillis);
} else {
List<Double> screenUnlockTime = new ArrayList<Double>();
screenUnlockTime.add(time.getTime() - unlockInMillis);
mBootInfo.put(UNLOCK_TIME, screenUnlockTime);
}
mBootIterationInfo.put(UNLOCK_TIME, time.getTime() - unlockInMillis);
logOrderTracker = false;
}
}
} catch (IOException ioe) {
CLog.e("Problem in parsing screen unlock delay from logcat.");
CLog.e(ioe);
}
}
/**
* To check if the line is duplicate entry in the log file.
*
* @return true if log line are duplicated
*/
private boolean isDuplicateLine(String currentLine) {
if (mParsedLines.contains(currentLine)) {
return true;
} else {
mParsedLines.add(currentLine);
return false;
}
}
/**
* Report the reboot time results separately under the storage specific reporting unit.
*
* @param results contains boot time test data
*/
private void reportStorageSpecificMetrics(
ITestInvocationListener listener, Map<String, String> results)
throws DeviceNotAvailableException {
String storageType = getDevice().getProperty(STORAGE_TYPE);
if (null != storageType && !storageType.isEmpty()) {
// Construct reporting id based on UFS type
// Example : DeviceBootTest.DeviceBootTest#SuccessiveBootTest-64GB
TestDescription successiveStorageBootTestId =
new TestDescription(
String.format("%s.%s", BOOTTIME_TEST, BOOTTIME_TEST),
String.format("%s-%s", SUCCESSIVE_BOOT_TEST, storageType));
listener.testStarted(successiveStorageBootTestId);
listener.testEnded(successiveStorageBootTestId, results);
}
}
/** Concatenate given list of values to comma separated string */
public String concatenateTimeValues(List<Double> timeInfo) {
StringBuilder timeString = new StringBuilder();
for (Double time : timeInfo) {
timeString.append(time);
timeString.append(",");
}
return timeString.toString();
}
/**
* 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;
}
/**
* Method to create the runner with given testName
*
* @return the {@link IRemoteAndroidTestRunner} to use.
*/
IRemoteAndroidTestRunner createRemoteAndroidTestRunner(String testName)
throws DeviceNotAvailableException {
RemoteAndroidTestRunner runner =
new RemoteAndroidTestRunner(PACKAGE_NAME, RUNNER, getDevice().getIDevice());
runner.setMethodName(CLASS_NAME, testName);
return runner;
}
/** Wait until the sys.boot_completed is set */
private void waitForBootCompleted() throws InterruptedException, DeviceNotAvailableException {
for (int i = 0; i < BOOT_COMPLETE_POLL_RETRY_COUNT; i++) {
if (isBootCompleted()) {
return;
}
Thread.sleep(BOOT_COMPLETE_POLL_INTERVAL);
}
}
/** Returns true if boot completed property is set to true. */
private boolean isBootCompleted() throws DeviceNotAvailableException {
return BOOT_COMPLETED_VAL.equals(
getDevice().executeShellCommand(BOOT_COMPLETED_PROP).trim());
}
/**
* Parse the timestamp and return a {@link Date}.
*
* @param timeStr The timestamp in the format {@code MM-dd HH:mm:ss.SSS}.
* @return The {@link Date}.
*/
private Date parseTime(String timeStr) {
DateFormat yearFormatter = new SimpleDateFormat("yyyy");
String mYear = yearFormatter.format(new Date());
DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
try {
return formatter.parse(String.format("%s-%s", mYear, timeStr));
} catch (ParseException e) {
return null;
}
}
/**
* @return the time to wait between the reboots.
*/
public long getBootDelayTime() {
return mBootDelayTime;
}
final FailureDescription createFailureFromException(
String additionalMessage, Exception exception, FailureStatus defaultStatus) {
String message = exception.getMessage();
if (additionalMessage != null) {
message = String.format("%s\n%s", additionalMessage, message);
}
FailureDescription failure = FailureDescription.create(message).setCause(exception);
failure.setFailureStatus(defaultStatus);
if (exception instanceof IHarnessException) {
ErrorIdentifier id = ((IHarnessException) exception).getErrorId();
failure.setErrorIdentifier(id);
failure.setOrigin(((IHarnessException) exception).getOrigin());
if (id != null) {
failure.setFailureStatus(id.status());
}
}
return failure;
}
}