Snap for 8952093 from 9b2ace18f2df233012b1ba17e506a34fb9ec6240 to sdk-release

Change-Id: Ib86ffced43b4563adfbed5be4b11264ab07d6f6e
diff --git a/.classpath b/.classpath
index 5d181ad..5eb32b6 100644
--- a/.classpath
+++ b/.classpath
@@ -53,5 +53,6 @@
 	<classpathentry kind="var" path="TRADEFED_ROOT/out/soong/.intermediates/tools/tradefederation/core/tradefed-invocation-grpc/linux_glibc_common/javac/tradefed-invocation-grpc.jar"/>
 	<classpathentry kind="var" path="TRADEFED_ROOT/out/soong/.intermediates/tools/tradefederation/core/lab-resource-grpc/linux_glibc_common/javac/lab-resource-grpc.jar"/>
 	<classpathentry kind="var" path="TRADEFED_ROOT/out/soong/.intermediates/tools/tradefederation/core/tradefed-device-manager-grpc/linux_glibc_common/javac/tradefed-device-manager-grpc.jar"/>
+	<classpathentry kind="var" path="TRADEFED_ROOT/out/soong/.intermediates/external/perfetto/perfetto_trace-full/linux_glibc_common/combined/perfetto_trace-full.jar"/>
 	<classpathentry kind="output" path="bin"/>
 </classpath>
diff --git a/common_util/Android.bp b/common_util/Android.bp
index 1b4fa4e..e03cc3d 100644
--- a/common_util/Android.bp
+++ b/common_util/Android.bp
@@ -32,6 +32,8 @@
     ],
     static_libs: [
         "commons-compress-prebuilt",
+        // Trace protos to do invocation tracing
+        "perfetto_trace-full",
     ],
     libs: [
         "ddmlib-prebuilt",
diff --git a/common_util/com/android/tradefed/invoker/logger/InvocationMetricLogger.java b/common_util/com/android/tradefed/invoker/logger/InvocationMetricLogger.java
index 70c7c8c..bba8557 100644
--- a/common_util/com/android/tradefed/invoker/logger/InvocationMetricLogger.java
+++ b/common_util/com/android/tradefed/invoker/logger/InvocationMetricLogger.java
@@ -51,6 +51,7 @@
         INSTRUMENTATION_RERUN_FROM_FILE("instrumentation_rerun_from_file", true),
         INSTRUMENTATION_RERUN_SERIAL("instrumentation_rerun_serial", true),
         DOWNLOAD_RETRY_COUNT("download_retry_count", true),
+        METADATA_RETRY_COUNT("metadata_retry_count", true),
         XTS_STAGE_TESTS_TIME("xts_stage_tests_time_ms", true),
         XTS_STAGE_TESTS_BYTES("xts_stage_tests_bytes", true),
         XTS_PARTIAL_DOWNLOAD_FALLBACK_COUNT("xts_partial_download_fallback_count", true),
@@ -136,6 +137,9 @@
         CLOUD_DEVICE_STABLE_HOST_IMAGE("stable_host_image_name", false),
         CLOUD_DEVICE_STABLE_HOST_IMAGE_PROJECT("stable_host_image_project", false),
 
+        SHUTDOWN_BEFORE_TEST("shutdown_before_test", false),
+        SHUTDOWN_AFTER_TEST("shutdown_after_test", false),
+        SHUTDOWN_LATENCY("shutdown_latency_ms", false),
         SHUTDOWN_HARD_LATENCY("shutdown_hard_latency_ms", false),
         DEVICE_COUNT("device_count", false),
         DEVICE_DONE_TIMESTAMP("device_done_timestamp", false),
@@ -181,6 +185,7 @@
         SETUP_START("tf_setup_start_timestamp", false),
         SETUP_END("tf_setup_end_timestamp", false),
         SETUP_PAIR("tf_setup_pair_timestamp", true),
+        TEST_SETUP_PAIR("tf_test_setup_pair_timestamp", true),
         FLASHING_FROM_FASTBOOTD("flashing_from_fastbootd", true),
         FLASHING_TIME("flashing_time_ms", true),
         FLASHING_PERMIT_LATENCY("flashing_permit_latency_ms", true),
@@ -203,6 +208,24 @@
 
         LAB_PREPARER_NOT_ILAB("lab_preparer_not_ilab", true),
         TARGET_PREPARER_IS_ILAB("target_preparer_is_ilab", true),
+
+        ART_RUN_TEST_CHECKER_COMMAND_TIME_MS("art_run_test_checker_command_time_ms", true),
+
+        // Following are trace events also reporting as metrics
+        invocation_warm_up("invocation_warm_up", true),
+        dynamic_download("dynamic_download", true),
+        fetch_artifact("fetch_artifact", true),
+        start_logcat("start_logcat", true),
+        pre_sharding_required_setup("pre_sharding_required_setup", true),
+        sharding("sharding", true),
+        lab_setup("lab_setup", true),
+        test_setup("test_setup", true),
+        test_execution("test_execution", true),
+        check_device_availability("check_device_availability", true),
+        bugreport("bugreport", true),
+        test_teardown("test_teardown", true),
+        test_cleanup("test_cleanup", true),
+        log_and_release_device("log_and_release_device", true),
         ;
 
         private final String mKeyName;
diff --git a/common_util/com/android/tradefed/invoker/tracing/ActiveTrace.java b/common_util/com/android/tradefed/invoker/tracing/ActiveTrace.java
new file mode 100644
index 0000000..2a495a2
--- /dev/null
+++ b/common_util/com/android/tradefed/invoker/tracing/ActiveTrace.java
@@ -0,0 +1,225 @@
+/*
+ * 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.tradefed.invoker.tracing;
+
+import com.android.ddmlib.Log.LogLevel;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.util.FileUtil;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+import java.util.zip.GZIPInputStream;
+
+import perfetto.protos.PerfettoTrace.DebugAnnotation;
+import perfetto.protos.PerfettoTrace.ProcessDescriptor;
+import perfetto.protos.PerfettoTrace.ThreadDescriptor;
+import perfetto.protos.PerfettoTrace.Trace;
+import perfetto.protos.PerfettoTrace.TracePacket;
+import perfetto.protos.PerfettoTrace.TrackDescriptor;
+import perfetto.protos.PerfettoTrace.TrackEvent;
+
+/** Main class helping to describe and manage an active trace. */
+public class ActiveTrace {
+
+    public static final String TRACE_KEY = "invocation-trace";
+    private final long pid;
+    private final long tid;
+    private final long traceUuid;
+    private final int uid = 5555; // TODO: collect a real uid
+    private final Map<Long, Long> mThreadToTracker;
+    // File where the final trace gets outputed
+    private File mTraceOutput;
+
+    /**
+     * Constructor.
+     *
+     * @param pid Current process id
+     * @param tid Current thread id
+     */
+    public ActiveTrace(long pid, long tid) {
+        this.pid = pid;
+        this.tid = tid;
+        this.traceUuid = UUID.randomUUID().getMostSignificantBits() & Long.MAX_VALUE;
+        mThreadToTracker = new HashMap<>();
+    }
+
+    /** Start the tracing and report the metadata of the trace. */
+    public void startTracing(boolean isSubprocess) {
+        if (mTraceOutput != null) {
+            throw new IllegalStateException("Tracing was already started.");
+        }
+        try {
+            mTraceOutput = FileUtil.createTempFile(TRACE_KEY, ".perfetto-trace");
+        } catch (IOException e) {
+            CLog.e(e);
+        }
+        // Initialize all the trace metadata
+        createMainInvocationTracker((int) pid, (int) tid, traceUuid, isSubprocess);
+    }
+
+    /** Provide the trace file from a subprocess to be added to the parent. */
+    public void addSubprocessTrace(File subTrace) {
+        if (mTraceOutput == null) {
+            return;
+        }
+
+        try (FileInputStream stream = new FileInputStream(subTrace)) {
+            try (GZIPInputStream gzip = new GZIPInputStream(stream)) {
+                CLog.logAndDisplay(LogLevel.DEBUG, "merging with gzipped %s", subTrace);
+                FileUtil.writeToFile(gzip, mTraceOutput, true);
+                return;
+            } catch (IOException e) {
+                CLog.logAndDisplay(LogLevel.DEBUG, "%s isn't gzip.", subTrace);
+            }
+        } catch (IOException e) {
+            CLog.e(e);
+        }
+
+        try (FileInputStream stream = new FileInputStream(subTrace)) {
+            FileUtil.writeToFile(stream, mTraceOutput, true);
+        } catch (IOException e) {
+            CLog.e(e);
+        }
+    }
+
+    public void reportTraceEvent(String categories, String name, TrackEvent.Type type) {
+        reportTraceEvent(categories, name, (int) tid, null, type);
+    }
+
+    /**
+     * Very basic event reporting to do START / END of traces.
+     *
+     * @param categories Category associated with event
+     * @param name Event name
+     * @param type Type of the event being reported
+     */
+    public void reportTraceEvent(
+            String categories, String name, int threadId, String threadName, TrackEvent.Type type) {
+        long traceIdentifier = traceUuid;
+        if (threadId != this.tid) {
+            if (mThreadToTracker.containsKey(Long.valueOf(threadId))) {
+                traceIdentifier = mThreadToTracker.get(Long.valueOf(threadId));
+            } else {
+                traceIdentifier = UUID.randomUUID().getMostSignificantBits() & Long.MAX_VALUE;
+                createThreadTracker((int) pid, threadId, threadName, traceIdentifier);
+                mThreadToTracker.put(Long.valueOf(threadId), Long.valueOf(traceIdentifier));
+            }
+        }
+        TracePacket.Builder tracePacket =
+                TracePacket.newBuilder()
+                        .setTrustedUid(uid)
+                        .setTrustedPid((int) pid)
+                        .setTimestamp(System.nanoTime())
+                        .setTrustedPacketSequenceId(1)
+                        .setSequenceFlags(1)
+                        .setProcessDescriptor(ProcessDescriptor.newBuilder().setPid((int) pid))
+                        .setThreadDescriptor(ThreadDescriptor.newBuilder().setTid(threadId))
+                        .setTrackEvent(
+                                TrackEvent.newBuilder()
+                                        .setTrackUuid(traceIdentifier)
+                                        .setName(name)
+                                        .setType(type)
+                                        .addCategories(categories)
+                                        .addDebugAnnotations(
+                                                DebugAnnotation.newBuilder().setName(name)));
+        writeToTrace(tracePacket.build());
+    }
+
+    /** Reports the final trace files and clean up resources as needed. */
+    public File finalizeTracing() {
+        CLog.logAndDisplay(LogLevel.DEBUG, "Finalizing trace: %s", mTraceOutput);
+        File trace = mTraceOutput;
+        mTraceOutput = null;
+        return trace;
+    }
+
+    private String createProcessName(boolean isSubprocess) {
+        if (isSubprocess) {
+            return "subprocess-test-invocation";
+        }
+        return "test-invocation";
+    }
+
+    private void createMainInvocationTracker(
+            int pid, int tid, long traceUuid, boolean isSubprocess) {
+        TrackDescriptor.Builder descriptor =
+                TrackDescriptor.newBuilder()
+                        .setUuid(traceUuid)
+                        .setName(createProcessName(isSubprocess))
+                        .setThread(
+                                ThreadDescriptor.newBuilder()
+                                        .setTid(tid)
+                                        .setThreadName("invocation-thread")
+                                        .setPid(pid))
+                        .setProcess(
+                                ProcessDescriptor.newBuilder()
+                                        .setPid(pid)
+                                        .setProcessName(createProcessName(isSubprocess)));
+
+        TracePacket.Builder traceTrackDescriptor =
+                TracePacket.newBuilder()
+                        .setTrustedUid(uid)
+                        .setTimestamp(System.nanoTime())
+                        .setTrustedPacketSequenceId(1)
+                        .setSequenceFlags(1)
+                        .setTrustedPid(pid)
+                        .setTrackDescriptor(descriptor.build());
+
+        writeToTrace(traceTrackDescriptor.build());
+    }
+
+    private void createThreadTracker(int pid, int tid, String threadName, long traceUuid) {
+        TrackDescriptor.Builder descriptor =
+                TrackDescriptor.newBuilder()
+                        .setUuid(traceUuid)
+                        .setThread(
+                                ThreadDescriptor.newBuilder()
+                                        .setTid(tid)
+                                        .setThreadName(threadName)
+                                        .setPid(pid));
+
+        TracePacket.Builder traceTrackDescriptor =
+                TracePacket.newBuilder()
+                        .setTrustedUid(uid)
+                        .setTimestamp(System.nanoTime())
+                        .setTrustedPacketSequenceId(1)
+                        .setSequenceFlags(1)
+                        .setTrustedPid(pid)
+                        .setTrackDescriptor(descriptor.build());
+
+        writeToTrace(traceTrackDescriptor.build());
+    }
+
+    private synchronized void writeToTrace(TracePacket packet) {
+        if (mTraceOutput == null) {
+            return;
+        }
+        // Perfetto UI supports repeated Trace
+        Trace wrappingTrace = Trace.newBuilder().addPacket(packet).build();
+        try (FileOutputStream out = new FileOutputStream(mTraceOutput, true)) {
+            wrappingTrace.writeTo(out);
+            out.flush();
+        } catch (IOException e) {
+            CLog.e("Failed to write execution trace to file.");
+            CLog.e(e);
+        }
+    }
+}
diff --git a/common_util/com/android/tradefed/invoker/tracing/CloseableTraceScope.java b/common_util/com/android/tradefed/invoker/tracing/CloseableTraceScope.java
new file mode 100644
index 0000000..d9952f4
--- /dev/null
+++ b/common_util/com/android/tradefed/invoker/tracing/CloseableTraceScope.java
@@ -0,0 +1,82 @@
+/*
+ * 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.tradefed.invoker.tracing;
+
+import com.android.tradefed.invoker.logger.InvocationMetricLogger;
+import com.android.tradefed.invoker.logger.InvocationMetricLogger.InvocationMetricKey;
+
+import com.google.common.base.Enums;
+import com.google.common.base.Optional;
+
+import perfetto.protos.PerfettoTrace.TrackEvent;
+
+/** A scoped class that allows to report tracing section via try-with-resources */
+public class CloseableTraceScope implements AutoCloseable {
+
+    private static final String DEFAULT_CATEGORY = "invocation";
+    private final String category;
+    private final String name;
+    private final long startTime;
+
+    /**
+     * Report a scoped trace.
+     *
+     * @param category The category of the operation
+     * @param name The name for reporting the section
+     */
+    public CloseableTraceScope(String category, String name) {
+        this.category = category;
+        this.name = name;
+        this.startTime = System.currentTimeMillis();
+        ActiveTrace trace = TracingLogger.getActiveTrace();
+        if (trace == null) {
+            return;
+        }
+        int threadId = (int) Thread.currentThread().getId();
+        String threadName = Thread.currentThread().getName();
+        trace.reportTraceEvent(
+                category, name, threadId, threadName, TrackEvent.Type.TYPE_SLICE_BEGIN);
+
+    }
+
+    /** Constructor. */
+    public CloseableTraceScope(String name) {
+        this(DEFAULT_CATEGORY, name);
+    }
+
+    /** Constructor for reporting scope from threads. */
+    public CloseableTraceScope() {
+        this(DEFAULT_CATEGORY, Thread.currentThread().getName());
+    }
+
+    @Override
+    public void close() {
+        ActiveTrace trace = TracingLogger.getActiveTrace();
+        if (trace == null) {
+            return;
+        }
+        int threadId = (int) Thread.currentThread().getId();
+        String threadName = Thread.currentThread().getName();
+        trace.reportTraceEvent(
+                category, name, threadId, threadName, TrackEvent.Type.TYPE_SLICE_END);
+        Optional<InvocationMetricKey> optionalKey =
+                Enums.getIfPresent(InvocationMetricKey.class, name);
+        if (optionalKey.isPresent()) {
+            InvocationMetricLogger.addInvocationPairMetrics(
+                    optionalKey.get(), startTime, System.currentTimeMillis());
+        }
+    }
+}
diff --git a/common_util/com/android/tradefed/invoker/tracing/TracingLogger.java b/common_util/com/android/tradefed/invoker/tracing/TracingLogger.java
new file mode 100644
index 0000000..1b2c866
--- /dev/null
+++ b/common_util/com/android/tradefed/invoker/tracing/TracingLogger.java
@@ -0,0 +1,63 @@
+/*
+ * 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.tradefed.invoker.tracing;
+
+import java.io.File;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/** Class that helps to manage tracing for each test invocation. */
+public class TracingLogger {
+
+    private static final Map<ThreadGroup, ActiveTrace> mPerGroupActiveTrace =
+            Collections.synchronizedMap(new HashMap<ThreadGroup, ActiveTrace>());
+
+    /**
+     * Creates and register an active trace for an invocation.
+     *
+     * @param pid Current process id
+     * @param tid Current thread id
+     */
+    public static ActiveTrace createActiveTrace(long pid, long tid) {
+        ThreadGroup group = Thread.currentThread().getThreadGroup();
+        synchronized (mPerGroupActiveTrace) {
+            ActiveTrace trace = new ActiveTrace(pid, tid);
+            mPerGroupActiveTrace.put(group, trace);
+            return trace;
+        }
+    }
+
+    /** Returns the current active trace for the invocation, or null if none. */
+    public static ActiveTrace getActiveTrace() {
+        ThreadGroup group = Thread.currentThread().getThreadGroup();
+        synchronized (mPerGroupActiveTrace) {
+            return mPerGroupActiveTrace.get(group);
+        }
+    }
+
+    /** Finalize the tracing and clear the tracking. */
+    public static File finalizeTrace() {
+        ThreadGroup group = Thread.currentThread().getThreadGroup();
+        synchronized (mPerGroupActiveTrace) {
+            ActiveTrace trace = mPerGroupActiveTrace.remove(group);
+            if (trace != null) {
+                return trace.finalizeTracing();
+            }
+        }
+        return null;
+    }
+}
diff --git a/common_util/com/android/tradefed/result/LogDataType.java b/common_util/com/android/tradefed/result/LogDataType.java
index 689e619..5dca1c6 100644
--- a/common_util/com/android/tradefed/result/LogDataType.java
+++ b/common_util/com/android/tradefed/result/LogDataType.java
@@ -73,6 +73,7 @@
     CPU_INFO("txt", "text/plain", false, true), // dumpsys cpuinfo
     JACOCO_CSV("csv", "text/csv", false, true), // JaCoCo coverage report in CSV format
     JACOCO_XML("xml", "text/xml", false, true), // JaCoCo coverage report in XML format
+    JACOCO_EXEC("exec", "application/octet-stream", false, false), //JaCoCo coverage execution file
     ATRACE("atr", "text/plain", true, false), // atrace -z format
     KERNEL_TRACE("dat", "text/plain", false, false), // raw kernel ftrace buffer
     DIR("", "text/plain", false, false),
diff --git a/common_util/com/android/tradefed/result/error/InfraErrorIdentifier.java b/common_util/com/android/tradefed/result/error/InfraErrorIdentifier.java
index ed952a4..902f9a1 100644
--- a/common_util/com/android/tradefed/result/error/InfraErrorIdentifier.java
+++ b/common_util/com/android/tradefed/result/error/InfraErrorIdentifier.java
@@ -41,6 +41,9 @@
     LAB_HOST_FILESYSTEM_ERROR(500_013, FailureStatus.INFRA_FAILURE),
     TRADEFED_SHUTTING_DOWN(500_014, FailureStatus.INFRA_FAILURE),
     LAB_HOST_FILESYSTEM_FULL(500_015, FailureStatus.INFRA_FAILURE),
+    TRADEFED_SKIPPED_TESTS_DURING_SHUTDOWN(500_016, FailureStatus.CANCELLED),
+    // 500_400 - 500_500: General errors - subprocess related
+    INTERRUPTED_DURING_SUBPROCESS_SHUTDOWN(500_401, FailureStatus.INFRA_FAILURE),
 
     // 500_501 - 501_000: Build, Artifacts download related errors
     ARTIFACT_REMOTE_PATH_NULL(500_501, FailureStatus.INFRA_FAILURE),
@@ -91,6 +94,7 @@
     UNEXPECTED_DEVICE_CONFIGURED(505_254, FailureStatus.CUSTOMER_ISSUE),
     KEYSTORE_CONFIG_ERROR(505_255, FailureStatus.DEPENDENCY_ISSUE),
     TEST_MAPPING_PATH_COLLISION(505_256, FailureStatus.DEPENDENCY_ISSUE),
+    TEST_MAPPING_FILE_FORMAT_ISSUE(505_257, FailureStatus.CUSTOMER_ISSUE),
 
     UNDETERMINED(510_000, FailureStatus.UNSET);
 
diff --git a/common_util/com/android/tradefed/util/StreamUtil.java b/common_util/com/android/tradefed/util/StreamUtil.java
index e2b1341..8526886 100644
--- a/common_util/com/android/tradefed/util/StreamUtil.java
+++ b/common_util/com/android/tradefed/util/StreamUtil.java
@@ -219,11 +219,15 @@
         inStream.skip(offset);
         byte[] buf = new byte[BUF_SIZE];
         long totalRetrievedSize = 0;
-        int retrievedSize = -1;
         try {
-            while ((retrievedSize = inStream.read(buf)) != -1) {
-                if (size > 0 && size < totalRetrievedSize + retrievedSize) {
-                    retrievedSize = (int) (size - totalRetrievedSize);
+            while (true) {
+                int maxReadSize =
+                        size > 0
+                                ? (int) Math.min(size - totalRetrievedSize, buf.length)
+                                : buf.length;
+                int retrievedSize = inStream.read(buf, 0, maxReadSize);
+                if (retrievedSize == -1) {
+                    break;
                 }
                 outStream.write(buf, 0, retrievedSize);
                 totalRetrievedSize += retrievedSize;
diff --git a/global_configuration/com/android/tradefed/config/GlobalConfiguration.java b/global_configuration/com/android/tradefed/config/GlobalConfiguration.java
index 45452ab..9d93d23 100644
--- a/global_configuration/com/android/tradefed/config/GlobalConfiguration.java
+++ b/global_configuration/com/android/tradefed/config/GlobalConfiguration.java
@@ -873,7 +873,7 @@
     public File cloneConfigWithFilter(Set<String> exclusionPatterns, String... allowlistConfigs)
             throws IOException {
         return cloneConfigWithFilter(
-                exclusionPatterns, new NoOpConfigOptionValueTransformer(), allowlistConfigs);
+                exclusionPatterns, new NoOpConfigOptionValueTransformer(), true, allowlistConfigs);
     }
 
     /** {@inheritDoc} */
@@ -881,17 +881,22 @@
     public File cloneConfigWithFilter(
             Set<String> exclusionPatterns,
             IConfigOptionValueTransformer transformer,
+            boolean deepCopy,
             String... allowlistConfigs)
             throws IOException {
         IConfigurationFactory configFactory = getConfigurationFactory();
         IGlobalConfiguration copy = null;
-        try {
-            // Use a copy with default original options
-            copy =
-                    configFactory.createGlobalConfigurationFromArgs(
-                            mOriginalArgs, new ArrayList<>());
-        } catch (ConfigurationException e) {
-            throw new IOException(e);
+        if (deepCopy) {
+            try {
+                // Use a copy with default original options
+                copy =
+                        configFactory.createGlobalConfigurationFromArgs(
+                                mOriginalArgs, new ArrayList<>());
+            } catch (ConfigurationException e) {
+                throw new IOException(e);
+            }
+        } else {
+            copy = this;
         }
 
         File filteredGlobalConfig = FileUtil.createTempFile("filtered_global_config", ".config");
diff --git a/global_configuration/com/android/tradefed/config/IGlobalConfiguration.java b/global_configuration/com/android/tradefed/config/IGlobalConfiguration.java
index 996d0cd..45ed870 100644
--- a/global_configuration/com/android/tradefed/config/IGlobalConfiguration.java
+++ b/global_configuration/com/android/tradefed/config/IGlobalConfiguration.java
@@ -393,6 +393,7 @@
     public File cloneConfigWithFilter(
             Set<String> exclusionPatterns,
             IConfigOptionValueTransformer transformer,
+            boolean deepCopy,
             String... allowlistConfigs)
             throws IOException;
 
diff --git a/global_configuration/com/android/tradefed/host/HostOptions.java b/global_configuration/com/android/tradefed/host/HostOptions.java
index 1cee254..876d0ad 100644
--- a/global_configuration/com/android/tradefed/host/HostOptions.java
+++ b/global_configuration/com/android/tradefed/host/HostOptions.java
@@ -128,6 +128,7 @@
     private List<String> mPreconfiguredVirtualDevicePool = new ArrayList<>();
 
     private Map<PermitLimitType, Semaphore> mConcurrentLocks = new HashMap<>();
+    private Map<PermitLimitType, Integer> mInternalConcurrentLimits = new HashMap<>();
 
     /** {@inheritDoc} */
     @Override
@@ -213,8 +214,8 @@
 
     /** {@inheritDoc} */
     @Override
-    public Set<String> getKnownPreconfigureVirtualDevicePool() {
-        return new HashSet<>(mPreconfiguredVirtualDevicePool);
+    public List<String> getKnownPreconfigureVirtualDevicePool() {
+        return new ArrayList<>(mPreconfiguredVirtualDevicePool);
     }
 
     /** {@inheritDoc} */
@@ -262,15 +263,18 @@
         if (!mConcurrentLocks.isEmpty()) {
             return;
         }
+        mInternalConcurrentLimits.putAll(mConcurrentLimit);
         // Backfill flasher & download limit from their dedicated option
-        if (!mConcurrentLimit.containsKey(PermitLimitType.CONCURRENT_FLASHER)) {
-            mConcurrentLimit.put(PermitLimitType.CONCURRENT_FLASHER, mConcurrentFlasherLimit);
+        if (!mInternalConcurrentLimits.containsKey(PermitLimitType.CONCURRENT_FLASHER)) {
+            mInternalConcurrentLimits.put(
+                    PermitLimitType.CONCURRENT_FLASHER, mConcurrentFlasherLimit);
         }
-        if (!mConcurrentLimit.containsKey(PermitLimitType.CONCURRENT_DOWNLOAD)) {
-            mConcurrentLimit.put(PermitLimitType.CONCURRENT_DOWNLOAD, mConcurrentDownloadLimit);
+        if (!mInternalConcurrentLimits.containsKey(PermitLimitType.CONCURRENT_DOWNLOAD)) {
+            mInternalConcurrentLimits.put(
+                    PermitLimitType.CONCURRENT_DOWNLOAD, mConcurrentDownloadLimit);
         }
 
-        for (Entry<PermitLimitType, Integer> limits : mConcurrentLimit.entrySet()) {
+        for (Entry<PermitLimitType, Integer> limits : mInternalConcurrentLimits.entrySet()) {
             if (limits.getValue() == null) {
                 continue;
             }
@@ -287,7 +291,9 @@
         CLog.i(
                 "Requesting a '%s' permit out of the max limit of %s. Current queue "
                         + "length: %s",
-                type, mConcurrentLimit.get(type), mConcurrentLocks.get(type).getQueueLength());
+                type,
+                mInternalConcurrentLimits.get(type),
+                mConcurrentLocks.get(type).getQueueLength());
         try {
             mConcurrentLocks.get(type).acquire();
         } catch (InterruptedException e) {
@@ -316,6 +322,6 @@
         if (!mConcurrentLocks.containsKey(type)) {
             return 0;
         }
-        return mConcurrentLimit.get(type) - mConcurrentLocks.get(type).availablePermits();
+        return mInternalConcurrentLimits.get(type) - mConcurrentLocks.get(type).availablePermits();
     }
 }
diff --git a/global_configuration/com/android/tradefed/host/IHostOptions.java b/global_configuration/com/android/tradefed/host/IHostOptions.java
index d9634f5..1f27475 100644
--- a/global_configuration/com/android/tradefed/host/IHostOptions.java
+++ b/global_configuration/com/android/tradefed/host/IHostOptions.java
@@ -83,7 +83,7 @@
     Set<String> getKnownRemoteDeviceIpPool();
 
     /** Known preconfigured virtual device pool. */
-    Set<String> getKnownPreconfigureVirtualDevicePool();
+    List<String> getKnownPreconfigureVirtualDevicePool();
 
     /** Check if it should use the zip64 format in partial download or not. */
     boolean getUseZip64InPartialDownload();
diff --git a/javatests/.classpath b/javatests/.classpath
index b950e2d..84cbf5b 100644
--- a/javatests/.classpath
+++ b/javatests/.classpath
@@ -40,5 +40,6 @@
 	<classpathentry kind="var" path="TRADEFED_ROOT/prebuilts/tools/common/m2/repository/com/google/code/gson/gson/2.8.0/gson-2.8.0.jar"/>
 	<classpathentry kind="var" path="TRADEFED_ROOT/out/soong/.intermediates/external/opencensus-java/api/opencensus-java-api/linux_glibc_common/javac/opencensus-java-api.jar"/>
 	<classpathentry kind="var" path="TRADEFED_ROOT/out/soong/.intermediates/tools/tradefederation/core/tradefed-device-manager-grpc/linux_glibc_common/javac/tradefed-device-manager-grpc.jar"/>
+	<classpathentry kind="var" path="TRADEFED_ROOT/out/soong/.intermediates/external/perfetto/perfetto_trace-full/linux_glibc_common/combined/perfetto_trace-full.jar"/>
 	<classpathentry kind="output" path="bin"/>
 </classpath>
diff --git a/javatests/Android.bp b/javatests/Android.bp
index 99e222e..3e2eabd 100644
--- a/javatests/Android.bp
+++ b/javatests/Android.bp
@@ -61,6 +61,7 @@
         "libprotobuf-java-full",
         "truth-prebuilt",
         "loganalysis",
+        "perfetto_trace-full",
     ],
 
     manifest: "MANIFEST.mf",
diff --git a/javatests/com/android/tradefed/UnitTests.java b/javatests/com/android/tradefed/UnitTests.java
index cc561cb..fbfdad9 100644
--- a/javatests/com/android/tradefed/UnitTests.java
+++ b/javatests/com/android/tradefed/UnitTests.java
@@ -86,6 +86,7 @@
 import com.android.tradefed.device.WaitDeviceRecoveryTest;
 import com.android.tradefed.device.WifiHelperTest;
 import com.android.tradefed.device.cloud.AcloudConfigParserTest;
+import com.android.tradefed.device.cloud.CommonLogRemoteFileUtilTest;
 import com.android.tradefed.device.cloud.GceAvdInfoTest;
 import com.android.tradefed.device.cloud.GceManagerTest;
 import com.android.tradefed.device.cloud.GceRemoteCmdFormatterTest;
@@ -231,6 +232,9 @@
 import com.android.tradefed.suite.checker.SystemServerStatusCheckerTest;
 import com.android.tradefed.suite.checker.TimeStatusCheckerTest;
 import com.android.tradefed.suite.checker.UserCheckerTest;
+import com.android.tradefed.suite.checker.baseline.DeviceBaselineSetterTest;
+import com.android.tradefed.suite.checker.baseline.LockSettingsBaselineSetterTest;
+import com.android.tradefed.suite.checker.baseline.SettingsBaselineSetterTest;
 import com.android.tradefed.targetprep.AllTestAppsInstallSetupTest;
 import com.android.tradefed.targetprep.AoaTargetPreparerTest;
 import com.android.tradefed.targetprep.AppSetupTest;
@@ -554,6 +558,7 @@
 
     // device.cloud
     AcloudConfigParserTest.class,
+    CommonLogRemoteFileUtilTest.class,
     GceAvdInfoTest.class,
     GceManagerTest.class,
     GceRemoteCmdFormatterTest.class,
@@ -808,6 +813,11 @@
     TimeStatusCheckerTest.class,
     UserCheckerTest.class,
 
+    // suite/checker/baseline
+    DeviceBaselineSetterTest.class,
+    LockSettingsBaselineSetterTest.class,
+    SettingsBaselineSetterTest.class,
+
     // testtype
     AndroidJUnitTestTest.class,
     ArtGTestTest.class,
diff --git a/javatests/com/android/tradefed/cluster/ClusterCommandSchedulerTest.java b/javatests/com/android/tradefed/cluster/ClusterCommandSchedulerTest.java
index 5e7c892..b9fb04a 100644
--- a/javatests/com/android/tradefed/cluster/ClusterCommandSchedulerTest.java
+++ b/javatests/com/android/tradefed/cluster/ClusterCommandSchedulerTest.java
@@ -253,13 +253,14 @@
                     }
 
                     @Override
-                    public void execCommand(IScheduledInvocationListener listener, String[] args)
+                    public long execCommand(IScheduledInvocationListener listener, String[] args)
                             throws ConfigurationException, NoDeviceException {
                         ArrayList<String> execCmdArgs = new ArrayList<>();
                         for (String arg : args) {
                             execCmdArgs.add(arg);
                         }
                         mExecCmdArgs.push(execCmdArgs);
+                        return 1;
                     }
 
                     @Override
@@ -896,13 +897,14 @@
                     }
 
                     @Override
-                    public void execCommand(IScheduledInvocationListener listener, String[] args)
+                    public long execCommand(IScheduledInvocationListener listener, String[] args)
                             throws ConfigurationException, NoDeviceException {
                         ArrayList<String> execCmdArgs = new ArrayList<>();
                         for (String arg : args) {
                             execCmdArgs.add(arg);
                         }
                         mExecCmdArgs.push(execCmdArgs);
+                        return 1;
                     }
 
                     @Override
diff --git a/javatests/com/android/tradefed/command/CommandRunnerTest.java b/javatests/com/android/tradefed/command/CommandRunnerTest.java
index dafa532..375d4f2 100644
--- a/javatests/com/android/tradefed/command/CommandRunnerTest.java
+++ b/javatests/com/android/tradefed/command/CommandRunnerTest.java
@@ -175,6 +175,7 @@
             "-n",
             "--no-return-null",
             "--no-throw-build-error",
+            "--no-enable-tracing",
             "--log-file-path",
             mLogDir.getAbsolutePath()
         };
@@ -193,6 +194,7 @@
             mConfig.getAbsolutePath(),
             "-n",
             "--test-throw-unresponsive",
+            "--no-enable-tracing",
             "--log-file-path",
             mLogDir.getAbsolutePath()
         };
@@ -215,6 +217,7 @@
             mConfig.getAbsolutePath(),
             "-n",
             "--test-throw-not-available",
+            "--no-enable-tracing",
             "--log-file-path",
             mLogDir.getAbsolutePath()
         };
@@ -237,6 +240,7 @@
             mConfig.getAbsolutePath(),
             "-n",
             "--test-throw-runtime",
+            "--no-enable-tracing",
             "--log-file-path",
             mLogDir.getAbsolutePath()
         };
@@ -282,6 +286,7 @@
             mConfig.getAbsolutePath(),
             "-s",
             "impossibleSerialThatWillNotBeFound",
+            "--no-enable-tracing",
             "--log-file-path",
             mLogDir.getAbsolutePath()
         };
diff --git a/javatests/com/android/tradefed/command/CommandSchedulerFuncTest.java b/javatests/com/android/tradefed/command/CommandSchedulerFuncTest.java
index 1f01084..c4b2a81 100644
--- a/javatests/com/android/tradefed/command/CommandSchedulerFuncTest.java
+++ b/javatests/com/android/tradefed/command/CommandSchedulerFuncTest.java
@@ -271,7 +271,7 @@
         }
 
         @Override
-        public void notifyInvocationStopped(String message, ErrorIdentifier errorId) {
+        public void notifyInvocationForceStopped(String message, ErrorIdentifier errorId) {
             printedStop = true;
         }
     }
diff --git a/javatests/com/android/tradefed/command/CommandSchedulerTest.java b/javatests/com/android/tradefed/command/CommandSchedulerTest.java
index 66a8632..3f2cb32 100644
--- a/javatests/com/android/tradefed/command/CommandSchedulerTest.java
+++ b/javatests/com/android/tradefed/command/CommandSchedulerTest.java
@@ -68,6 +68,8 @@
 import com.android.tradefed.result.ILogSaver;
 import com.android.tradefed.result.ITestInvocationListener;
 import com.android.tradefed.service.TradefedFeatureServer;
+import com.android.tradefed.service.management.DeviceManagementGrpcServer;
+import com.android.tradefed.service.management.TestInvocationManagementServer;
 import com.android.tradefed.util.FileUtil;
 import com.android.tradefed.util.Pair;
 import com.android.tradefed.util.RunUtil;
@@ -140,6 +142,16 @@
         }
 
         @Override
+        protected TestInvocationManagementServer getTestInvocationManagementServer() {
+            return null;
+        }
+
+        @Override
+        protected DeviceManagementGrpcServer getDeviceManagementServer() {
+            return null;
+        }
+
+        @Override
         protected IInvocationContext createInvocationContext() {
             return mContext;
         }
diff --git a/javatests/com/android/tradefed/config/ConfigurationFactoryTest.java b/javatests/com/android/tradefed/config/ConfigurationFactoryTest.java
index a249d5c..ae1005e 100644
--- a/javatests/com/android/tradefed/config/ConfigurationFactoryTest.java
+++ b/javatests/com/android/tradefed/config/ConfigurationFactoryTest.java
@@ -35,6 +35,7 @@
 import com.android.tradefed.log.ILeveledLogOutput;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.targetprep.DeviceWiper;
+import com.android.tradefed.targetprep.ILabPreparer;
 import com.android.tradefed.targetprep.StubTargetPreparer;
 import com.android.tradefed.targetprep.multi.StubMultiTargetPreparer;
 import com.android.tradefed.util.FileUtil;
@@ -1801,12 +1802,15 @@
         }
     }
 
+    /** Class to test out lab preparer parsing */
+    public static final class TestLabPreparer extends StubTargetPreparer implements ILabPreparer {}
+
     @Test
     public void testParse_labPreparer() throws Exception {
         String normalConfig =
                 "<configuration description=\"desc\" >\n"
                         + "  <lab_preparer class=\""
-                        + StubTargetPreparer.class.getName()
+                        + TestLabPreparer.class.getName()
                         + "\">\n"
                         + "     <option name=\"test-boolean-option\" value=\"false\"/>"
                         + "  </lab_preparer>\n"
diff --git a/javatests/com/android/tradefed/device/LocalAndroidVirtualDeviceTest.java b/javatests/com/android/tradefed/device/LocalAndroidVirtualDeviceTest.java
index 2cd60c2..1d89599 100644
--- a/javatests/com/android/tradefed/device/LocalAndroidVirtualDeviceTest.java
+++ b/javatests/com/android/tradefed/device/LocalAndroidVirtualDeviceTest.java
@@ -121,7 +121,8 @@
                     + "    \"logs\": ["
                     + "     {"
                     + "      \"path\": \"%s\","
-                    + "      \"type\": \"KERNEL_LOG\""
+                    + "      \"type\": \"KERNEL_LOG\","
+                    + "      \"name\": \"kernel.1.log\""
                     + "     }"
                     + "    ]"
                     + "   }"
@@ -141,7 +142,8 @@
                     + "    \"logs\": ["
                     + "     {"
                     + "      \"path\": \"%s\","
-                    + "      \"type\": \"KERNEL_LOG\""
+                    + "      \"type\": \"KERNEL_LOG\","
+                    + "      \"name\": \"kernel.1.log\""
                     + "     }"
                     + "    ]"
                     + "   }"
@@ -445,7 +447,7 @@
 
         assertFinalDeviceState(mLocalAvd.getIDevice());
         verify(acloudDeleteRunUtil).setEnvVariable(eq("TMPDIR"), any());
-        verify(testLogger).testLog(any(), eq(LogDataType.KERNEL_LOG), any());
+        verify(testLogger).testLog(eq("kernel.1.log"), eq(LogDataType.KERNEL_LOG), any());
 
         Assert.assertFalse(new File(reportFile.getValue()).exists());
         for (Map.Entry<String, ArgumentCaptor<String>> entry : captureDirs.entrySet()) {
@@ -501,7 +503,7 @@
 
         assertFinalDeviceState(mLocalAvd.getIDevice());
         verify(acloudDeleteRunUtil).setEnvVariable(eq("TMPDIR"), any());
-        verify(testLogger).testLog(any(), eq(LogDataType.KERNEL_LOG), any());
+        verify(testLogger).testLog(eq("kernel.1.log"), eq(LogDataType.KERNEL_LOG), any());
 
         Assert.assertFalse(new File(reportFile.getValue()).exists());
         Assert.assertFalse(capturedHostPackageDir.exists());
@@ -553,7 +555,7 @@
         mLocalAvd.postInvocationTearDown(expectedException);
 
         assertFinalDeviceState(mLocalAvd.getIDevice());
-        verify(testLogger).testLog(any(), eq(LogDataType.KERNEL_LOG), any());
+        verify(testLogger).testLog(eq("kernel.1.log"), eq(LogDataType.KERNEL_LOG), any());
 
         Assert.assertFalse(new File(reportFile.getValue()).exists());
         Assert.assertFalse(capturedHostPackageDir.exists());
diff --git a/javatests/com/android/tradefed/device/TestDeviceFuncTest.java b/javatests/com/android/tradefed/device/TestDeviceFuncTest.java
index 854af8e..2c7f4b8 100644
--- a/javatests/com/android/tradefed/device/TestDeviceFuncTest.java
+++ b/javatests/com/android/tradefed/device/TestDeviceFuncTest.java
@@ -44,6 +44,7 @@
 import com.android.tradefed.util.ProcessInfo;
 import com.android.tradefed.util.RunUtil;
 import com.android.tradefed.util.StreamUtil;
+import android.platform.test.annotations.FlakyTest;
 
 import org.junit.Assume;
 import org.junit.Before;
@@ -664,6 +665,7 @@
     }
 
     /** Test device soft-restart detection API. */
+    @FlakyTest
     @Test
     public void testDeviceSoftRestart() throws DeviceNotAvailableException {
         CLog.i("testDeviceSoftRestartSince");
diff --git a/javatests/com/android/tradefed/device/cloud/CommonLogRemoteFileUtilTest.java b/javatests/com/android/tradefed/device/cloud/CommonLogRemoteFileUtilTest.java
new file mode 100644
index 0000000..15f49d0
--- /dev/null
+++ b/javatests/com/android/tradefed/device/cloud/CommonLogRemoteFileUtilTest.java
@@ -0,0 +1,91 @@
+/*
+ * 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.tradefed.device.cloud;
+
+import com.android.tradefed.config.ConfigurationException;
+import com.android.tradefed.config.OptionSetter;
+import com.android.tradefed.device.TestDeviceOptions;
+import com.android.tradefed.log.ITestLogger;
+import com.android.tradefed.result.LogDataType;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.CommandStatus;
+import com.android.tradefed.util.IRunUtil;
+import com.google.common.net.HostAndPort;
+import java.util.List;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+/** Unit tests for {@link CommonLogRemoteFileUtil}. */
+@RunWith(JUnit4.class)
+public class CommonLogRemoteFileUtilTest {
+    @Mock private IRunUtil mRunUtil;
+    @Mock private ITestLogger mTestLogger;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+    }
+
+    @Test
+    public void testFetchCommonFilesWithLogsEntries() throws ConfigurationException {
+        GceAvdInfo info = new GceAvdInfo("mock-instance", HostAndPort.fromString("192.0.2.2"));
+        List<GceAvdInfo.LogFileEntry> logs = info.getLogs();
+        logs.add(new GceAvdInfo.LogFileEntry("/test/text", LogDataType.TEXT, "log1.txt"));
+        logs.add(new GceAvdInfo.LogFileEntry("/test/log2.txt", LogDataType.UNKNOWN, ""));
+        logs.add(new GceAvdInfo.LogFileEntry("/tombstones", LogDataType.DIR, "tombstones-zip"));
+        TestDeviceOptions options = new TestDeviceOptions();
+        OptionSetter setter = new OptionSetter(options);
+        setter.setOptionValue("use-oxygen", "false");
+        setter.setOptionValue(TestDeviceOptions.INSTANCE_TYPE_OPTION, "CUTTLEFISH");
+        Mockito.when(mRunUtil.runTimedCmd(Mockito.anyLong(), Mockito.any()))
+                .thenReturn(new CommandResult(CommandStatus.SUCCESS));
+
+        CommonLogRemoteFileUtil.fetchCommonFiles(mTestLogger, info, options, mRunUtil);
+
+        Mockito.verify(mTestLogger)
+                .testLog(Mockito.eq("log1.txt"), Mockito.eq(LogDataType.TEXT), Mockito.any());
+        Mockito.verify(mTestLogger)
+                .testLog(
+                        Mockito.startsWith("log2"), Mockito.eq(LogDataType.UNKNOWN), Mockito.any());
+        Mockito.verify(mTestLogger, Mockito.times(2))
+                .testLog(Mockito.any(), Mockito.any(), Mockito.any());
+    }
+
+    @Test
+    public void testFetchCommonFilesWithoutLogsEntries() throws ConfigurationException {
+        GceAvdInfo info = new GceAvdInfo("mock-instance", HostAndPort.fromString("192.0.2.2"));
+        TestDeviceOptions options = new TestDeviceOptions();
+        OptionSetter setter = new OptionSetter(options);
+        setter.setOptionValue("use-oxygen", "false");
+        setter.setOptionValue(TestDeviceOptions.INSTANCE_TYPE_OPTION, "CUTTLEFISH");
+        Mockito.when(mRunUtil.runTimedCmd(Mockito.anyLong(), Mockito.any()))
+                .thenReturn(new CommandResult(CommandStatus.SUCCESS));
+
+        CommonLogRemoteFileUtil.fetchCommonFiles(mTestLogger, info, options, mRunUtil);
+
+        Mockito.verify(mTestLogger)
+                .testLog(
+                        Mockito.eq("cuttlefish_launcher.log"),
+                        Mockito.eq(LogDataType.CUTTLEFISH_LOG),
+                        Mockito.any());
+    }
+}
diff --git a/javatests/com/android/tradefed/device/cloud/GceAvdInfoTest.java b/javatests/com/android/tradefed/device/cloud/GceAvdInfoTest.java
index 0c409d5..4885591 100644
--- a/javatests/com/android/tradefed/device/cloud/GceAvdInfoTest.java
+++ b/javatests/com/android/tradefed/device/cloud/GceAvdInfoTest.java
@@ -57,7 +57,8 @@
                         + "          \"logs\": [\n"
                         + "            {\n"
                         + "              \"path\": \"/text/log\",\n"
-                        + "              \"type\": \"TEXT\"\n"
+                        + "              \"type\": \"TEXT\",\n"
+                        + "              \"name\": \"log.txt\"\n"
                         + "            },\n"
                         + "            {\n"
                         + "              \"path\": \"/unknown/log\",\n"
@@ -75,10 +76,14 @@
         assertNotNull(avd);
         assertEquals(avd.hostAndPort().getHost(), "104.154.62.236");
         assertEquals(avd.instanceName(), "gce-x86-phone-userdebug-2299773-22cf");
-        Map<String, LogDataType> logs = avd.getLogs();
+        List<GceAvdInfo.LogFileEntry> logs = avd.getLogs();
         assertEquals(logs.size(), 2);
-        assertEquals(logs.get("/text/log"), LogDataType.TEXT);
-        assertEquals(logs.get("/unknown/log"), LogDataType.UNKNOWN);
+        assertEquals(logs.get(0).path, "/text/log");
+        assertEquals(logs.get(0).type, LogDataType.TEXT);
+        assertEquals(logs.get(0).name, "log.txt");
+        assertEquals(logs.get(1).path, "/unknown/log");
+        assertEquals(logs.get(1).type, LogDataType.UNKNOWN);
+        assertEquals(logs.get(1).name, "");
         assertTrue(avd.getBuildVars().isEmpty());
     }
 
diff --git a/javatests/com/android/tradefed/device/metric/JavaCodeCoverageCollectorTest.java b/javatests/com/android/tradefed/device/metric/JavaCodeCoverageCollectorTest.java
index 71191ba..3b90d2b 100644
--- a/javatests/com/android/tradefed/device/metric/JavaCodeCoverageCollectorTest.java
+++ b/javatests/com/android/tradefed/device/metric/JavaCodeCoverageCollectorTest.java
@@ -16,6 +16,8 @@
 
 package com.android.tradefed.device.metric;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.junit.Assert.fail;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
@@ -53,6 +55,7 @@
 
 import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
 import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
+import org.jacoco.core.tools.ExecFileLoader;
 import org.jacoco.core.data.ExecutionData;
 import org.jacoco.core.data.ExecutionDataStore;
 import org.jacoco.core.data.ExecutionDataWriter;
@@ -64,6 +67,7 @@
 import org.junit.rules.TemporaryFolder;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
+import org.mockito.ArgumentCaptor;
 import org.mockito.InOrder;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
@@ -240,6 +244,7 @@
         mockCoverageFileOnDevice(DEVICE_PATH);
         when(mMockDevice.isAdbRoot()).thenReturn(false);
         doReturn("").when(mMockDevice).executeShellCommand(anyString());
+        returnFileContentsOnShellCommand(mMockDevice, createTarGz(ImmutableMap.of()));
 
         // Simulate a test run.
         mCodeCoverageCollector.init(mMockContext, mFakeListener);
@@ -377,6 +382,92 @@
         verifyNoMoreInteractions(mMockDevice);
     }
 
+    @Test
+    public void testMergeSingleMeasurement_logReceived() throws Exception {
+        enableJavaCoverage();
+        mCoverageOptionsSetter.setOptionValue("merge-coverage", "true");
+
+        doReturn("").when(mMockDevice).executeShellCommand(anyString());
+
+        ByteString measurement = measurement(firstHalfCovered(JavaCodeCoverageCollector.class));
+        File tarGz = createTarGz(ImmutableMap.of("path/to/coverage.ec", measurement));
+        returnFileContentsOnShellCommand(mMockDevice, tarGz);
+
+        // Simulate a test run.
+        mCodeCoverageCollector.init(mMockContext, mFakeListener);
+        mCodeCoverageCollector.testRunStarted(RUN_NAME, TEST_COUNT);
+        mCodeCoverageCollector.testRunEnded(ELAPSED_TIME, new HashMap<String, Metric>());
+
+        // Validate the logged coverage data.
+        ArgumentCaptor<ByteString> stream = ArgumentCaptor.forClass(ByteString.class);
+        verify(mFakeListener).testLog(anyString(), eq(LogDataType.COVERAGE), stream.capture());
+
+        ExecFileLoader execFileLoader = new ExecFileLoader();
+        execFileLoader.load(stream.getValue().newInput());
+
+        ExecutionDataStore execData = execFileLoader.getExecutionDataStore();
+        boolean[] firstHalf = new boolean[PROBE_COUNT];
+        for (int i = 0; i < PROBE_COUNT / 2; i++) {
+            firstHalf[i] = true;
+        }
+
+        assertThat(execData.contains(vmName(JavaCodeCoverageCollector.class))).isTrue();
+        assertThat(getProbes(JavaCodeCoverageCollector.class, execData)).isEqualTo(firstHalf);
+    }
+
+    @Test
+    public void testMergeMultipleMeasurements_logContainsAllData() throws Exception {
+        enableJavaCoverage();
+        mCoverageOptionsSetter.setOptionValue("merge-coverage", "true");
+
+        doReturn("").when(mMockDevice).executeShellCommand(anyString());
+
+        ByteString firstHalfCollector =
+                measurement(firstHalfCovered(JavaCodeCoverageCollector.class));
+        ByteString secondHalfCollector =
+                measurement(secondHalfCovered(JavaCodeCoverageCollector.class));
+        ByteString partialCollectorTest =
+                measurement(partiallyCovered(JavaCodeCoverageCollectorTest.class));
+        File tarGz =
+                createTarGz(
+                        ImmutableMap.of(
+                                "JavaCodeCoverageColletor1.ec", firstHalfCollector,
+                                "JavaCodeCoverageCollector2.ec", secondHalfCollector,
+                                "JavaCodeCoverageCollectorTest.ec", partialCollectorTest));
+        returnFileContentsOnShellCommand(mMockDevice, tarGz);
+
+        // Simulate a test run.
+        mCodeCoverageCollector.init(mMockContext, mFakeListener);
+        mCodeCoverageCollector.testRunStarted(RUN_NAME, TEST_COUNT);
+        mCodeCoverageCollector.testRunEnded(ELAPSED_TIME, new HashMap<String, Metric>());
+
+        // Validate the logged coverage data.
+        ArgumentCaptor<ByteString> stream = ArgumentCaptor.forClass(ByteString.class);
+        verify(mFakeListener).testLog(anyString(), eq(LogDataType.COVERAGE), stream.capture());
+
+        ExecFileLoader execFileLoader = new ExecFileLoader();
+        execFileLoader.load(stream.getValue().newInput());
+
+        ExecutionDataStore execData = execFileLoader.getExecutionDataStore();
+
+        // Check coverage data for JavaCodeCoverageCollector. All probes should be true if the data
+        // merged successfully.
+        boolean[] fullyCovered = new boolean[PROBE_COUNT];
+        Arrays.fill(fullyCovered, Boolean.TRUE);
+
+        assertThat(execData.contains(vmName(JavaCodeCoverageCollector.class))).isTrue();
+        assertThat(getProbes(JavaCodeCoverageCollector.class, execData)).isEqualTo(fullyCovered);
+
+        // Check coverage data for JavaCodeCoverageCollectorTest. Only the first probe should be
+        // true.
+        boolean[] partiallyCovered = new boolean[PROBE_COUNT];
+        partiallyCovered[0] = true;
+
+        assertThat(execData.contains(vmName(JavaCodeCoverageCollectorTest.class))).isTrue();
+        assertThat(getProbes(JavaCodeCoverageCollectorTest.class, execData))
+                .isEqualTo(partiallyCovered);
+    }
+
     private void mockCoverageFileOnDevice(String devicePath)
             throws IOException, DeviceNotAvailableException {
         File coverageFile = folder.newFile(new File(devicePath).getName());
@@ -404,6 +495,22 @@
         return new ExecutionData(classId(clazz), vmName(clazz), probes);
     }
 
+    private static <T> ExecutionData firstHalfCovered(Class<T> clazz) throws IOException {
+        boolean[] probes = new boolean[PROBE_COUNT];
+        for (int i = 0; i < PROBE_COUNT / 2; i++) {
+            probes[i] = true;
+        }
+        return new ExecutionData(classId(clazz), vmName(clazz), probes);
+    }
+
+    private static <T> ExecutionData secondHalfCovered(Class<T> clazz) throws IOException {
+        boolean[] probes = new boolean[PROBE_COUNT];
+        for (int i = PROBE_COUNT / 2; i < PROBE_COUNT; i++) {
+            probes[i] = true;
+        }
+        return new ExecutionData(classId(clazz), vmName(clazz), probes);
+    }
+
     private static <T> long classId(Class<T> clazz) throws IOException {
         return Long.valueOf(CRC64.classId(classBytes(clazz).toByteArray()));
     }
diff --git a/javatests/com/android/tradefed/invoker/RemoteInvocationExecutionTest.java b/javatests/com/android/tradefed/invoker/RemoteInvocationExecutionTest.java
index b13a738..b90c75a 100644
--- a/javatests/com/android/tradefed/invoker/RemoteInvocationExecutionTest.java
+++ b/javatests/com/android/tradefed/invoker/RemoteInvocationExecutionTest.java
@@ -181,6 +181,7 @@
                     globalConfig.cloneConfigWithFilter(
                             new HashSet<>(),
                             new RemoteInvocationExecution.FileOptionValueTransformer("/foo/"),
+                            true,
                             new String[] {
                                 GlobalConfiguration.HOST_OPTIONS_TYPE_NAME,
                                 GlobalConfiguration.KEY_STORE_TYPE_NAME
diff --git a/javatests/com/android/tradefed/invoker/ShardMainResultForwarderTest.java b/javatests/com/android/tradefed/invoker/ShardMainResultForwarderTest.java
index 6ff723a..c0e6679 100644
--- a/javatests/com/android/tradefed/invoker/ShardMainResultForwarderTest.java
+++ b/javatests/com/android/tradefed/invoker/ShardMainResultForwarderTest.java
@@ -184,8 +184,9 @@
         Mockito.verify(mMockLogListener, times(1))
                 .testLog(Mockito.any(), Mockito.any(), Mockito.any());
         // The callback was received all the way to the last reporter.
-        Mockito.verify(mMockLogListener, times(1))
+        Mockito.verify(mMockLogListener, times(2))
                 .testLogSaved(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any());
+        Mockito.verify(mMockLogListener, times(1)).logAssociation(Mockito.any(), Mockito.any());
         Mockito.verify(mMockLogListener, times(1)).invocationEnded(500L);
     }
 
diff --git a/javatests/com/android/tradefed/invoker/TestInvocationTest.java b/javatests/com/android/tradefed/invoker/TestInvocationTest.java
index 8ac35b2..a2c3f1e 100644
--- a/javatests/com/android/tradefed/invoker/TestInvocationTest.java
+++ b/javatests/com/android/tradefed/invoker/TestInvocationTest.java
@@ -24,6 +24,7 @@
 import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -363,7 +364,7 @@
     }
 
     private void verifyMockSuccessListeners() throws IOException {
-        verifyMockListeners(InvocationStatus.SUCCESS, null, false, true, false);
+        verifyMockListeners(InvocationStatus.SUCCESS, null, false, true, false, false);
     }
 
     private void stubMockFailureListeners(Throwable throwable) throws IOException {
@@ -371,7 +372,7 @@
     }
 
     private void verifyMockFailureListeners(Throwable throwable) throws IOException {
-        verifyMockListeners(InvocationStatus.FAILED, throwable, false, true, false);
+        verifyMockListeners(InvocationStatus.FAILED, throwable, false, true, false, false);
     }
 
     private void stubMockFailureListenersAny(Throwable throwable, boolean stubFailures)
@@ -381,7 +382,7 @@
 
     private void verifyMockFailureListenersAny(Throwable throwable, boolean stubFailures)
             throws IOException {
-        verifyMockListeners(InvocationStatus.FAILED, throwable, stubFailures, true, false);
+        verifyMockListeners(InvocationStatus.FAILED, throwable, stubFailures, true, false, false);
     }
 
     private void stubMockFailureListeners(
@@ -391,15 +392,16 @@
 
     private void verifyMockFailureListeners(
             Throwable throwable, boolean stubFailures, boolean reportHostLog) throws IOException {
-        verifyMockListeners(InvocationStatus.FAILED, throwable, stubFailures, reportHostLog, false);
+        verifyMockListeners(
+                InvocationStatus.FAILED, throwable, stubFailures, reportHostLog, false, false);
     }
 
     private void stubMockStoppedListeners() throws IOException {
         stubMockListeners(InvocationStatus.SUCCESS, null, false, true, true);
     }
 
-    private void verifyMockStoppedListeners() throws IOException {
-        verifyMockListeners(InvocationStatus.SUCCESS, null, false, true, true);
+    private void verifyMockStoppedListeners(boolean testSkipped) throws IOException {
+        verifyMockListeners(InvocationStatus.SUCCESS, null, false, true, true, testSkipped);
     }
 
     private void verifySummaryListener() {
@@ -606,7 +608,8 @@
             Throwable throwable,
             boolean stubFailures,
             boolean reportHostLog,
-            boolean stopped)
+            boolean stopped,
+            boolean testSkipped)
             throws IOException {
         // invocationStarted
         mInOrderTestListener
@@ -714,7 +717,9 @@
         } else {
             // Handle build error bugreport listeners
             if (throwable instanceof BuildError) {
-            } else if (!(throwable instanceof TargetSetupError) && !mShardingEarlyFailure) {
+            } else if (!(throwable instanceof TargetSetupError)
+                    && !mShardingEarlyFailure
+                    && !testSkipped) {
                 // Handle test logcat listeners
                 mInOrderTestListener
                         .verify(mMockTestListener)
@@ -1149,7 +1154,8 @@
     }
 
     /**
-     * Test metrics SHUTDOWN_HARD_LATENCY is collected when the invocation is stopped/interrupted.
+     * Test that tests were skipped and metrics SHUTDOWN_HARD_LATENCY is collected when the
+     * invocation is stopped/interrupted before test phase started.
      */
     @Test
     public void testInvoke_metricsCollectedWhenStopped() throws Throwable {
@@ -1159,14 +1165,15 @@
         stubMockStoppedListeners();
         stubNormalInvoke(test);
 
-        mTestInvocation.notifyInvocationStopped("Stopped", InfraErrorIdentifier.INVOCATION_TIMEOUT);
+        mTestInvocation.notifyInvocationForceStopped(
+                "Stopped", InfraErrorIdentifier.INVOCATION_TIMEOUT);
         mTestInvocation.invoke(mStubInvocationMetadata, mStubConfiguration, mockRescheduler);
 
-        verify(test).run(Mockito.any(), Mockito.any());
+        verify(test, never()).run(Mockito.any(), Mockito.any());
         verify(mMockPreparer).tearDown(Mockito.any(), Mockito.any());
 
         verifyNormalInvoke(test);
-        verifyMockStoppedListeners();
+        verifyMockStoppedListeners(true);
 
         assertTrue(
                 mStubInvocationMetadata
diff --git a/javatests/com/android/tradefed/observatory/TestDiscoveryExecutorTest.java b/javatests/com/android/tradefed/observatory/TestDiscoveryExecutorTest.java
index 3101413..4709ad7 100644
--- a/javatests/com/android/tradefed/observatory/TestDiscoveryExecutorTest.java
+++ b/javatests/com/android/tradefed/observatory/TestDiscoveryExecutorTest.java
@@ -74,7 +74,7 @@
 
     /** Test the executor to discover test modules from multiple tests. */
     @Test
-    public void testDiscoverTestModules() throws Exception {
+    public void testDiscoverTestDependencies() throws Exception {
         // Mock to return some include filters
         BaseTestSuite test1 = new BaseTestSuite();
         Set<String> includeFilters1 = new HashSet<>();
@@ -107,66 +107,6 @@
         try {
             String output = mTestDiscoveryExecutor.discoverDependencies(new String[0]);
             String expected =
-                    "{\"TestDependencies\":[\"TestModule1\",\"TestModule2\",\"TestModule3\","
-                            + "\"TestModule4\",\"TestModule5\",\"TestModule6\""
-                            + ",\"someapk.apk\"]}";
-            assertEquals(expected, output);
-        } catch (Exception e) {
-            fail(String.format("Should not throw exception %s", e.getMessage()));
-        }
-    }
-
-    /** Test the executor to handle where there is no tests from the config. */
-    @Test
-    public void testDiscoverNoTestModules() throws Exception {
-        // Mock to return no include filters
-        when(mMockedConfiguration.getTests()).thenReturn(new ArrayList<>());
-        // We don't test with real command line input here. Because for a real command line input,
-        // the test module names will be different with respect to those test config resource files
-        // can be changed in different builds.
-        try {
-            mTestDiscoveryExecutor.discoverDependencies(new String[0]);
-            fail("Should throw an IllegalStateException");
-        } catch (Exception e) {
-            assertTrue(e instanceof IllegalStateException);
-        }
-    }
-
-    /** Test the executor to discover test modules from multiple tests. */
-    @Test
-    public void testDiscoverTestDependenciesV2() throws Exception {
-        // Mock to return some include filters
-        BaseTestSuite test1 = new BaseTestSuite();
-        Set<String> includeFilters1 = new HashSet<>();
-        includeFilters1.add("TestModule1 class#function1");
-        includeFilters1.add("TestModule2");
-        includeFilters1.add("x86_64 TestModule3 class#function3");
-        test1.setIncludeFilter(includeFilters1);
-
-        BaseTestSuite test2 = new BaseTestSuite();
-        Set<String> includeFilters2 = new HashSet<>();
-        includeFilters2.add("TestModule1 class#function6");
-        includeFilters2.add("x86 TestModule4");
-        includeFilters2.add("TestModule5 class#function2");
-        includeFilters2.add("TestModule6");
-        test2.setIncludeFilter(includeFilters2);
-
-        List<IRemoteTest> testList = new ArrayList<>();
-        testList.add(test1);
-        testList.add(test2);
-        when(mMockedConfiguration.getTests()).thenReturn(testList);
-        List<Object> preparers = new ArrayList<>();
-        preparers.add(new DiscoverablePreparer());
-        when(mMockedConfiguration.getAllConfigurationObjectsOfType(
-                        Configuration.TARGET_PREPARER_TYPE_NAME))
-                .thenReturn(preparers);
-
-        // We don't test with real command line input here. Because for a real command line input,
-        // the test module names will be different with respect to those test config resource files
-        // can be changed in different builds.
-        try {
-            String output = mTestDiscoveryExecutor.discoverDependenciesV2(new String[0]);
-            String expected =
                     "{\"TestModules\":[\"TestModule1\",\"TestModule2\",\"TestModule3\","
                             + "\"TestModule4\",\"TestModule5\",\"TestModule6\"],"
                             + "\"TestDependencies\":[\"someapk.apk\"]}";
@@ -178,12 +118,12 @@
 
     /** Test the executor to handle where there is no tests from the config. */
     @Test
-    public void testDiscoverDependenciesV2_NoTestModules() throws Exception {
+    public void testDiscoverDependencies_NoTestModules() throws Exception {
         // Mock to return no include filters
         when(mMockedConfiguration.getTests()).thenReturn(new ArrayList<>());
 
         try {
-            mTestDiscoveryExecutor.discoverDependenciesV2(new String[0]);
+            mTestDiscoveryExecutor.discoverDependencies(new String[0]);
             fail("Should throw an TestDiscoveryException");
         } catch (Exception e) {
             assertTrue(e instanceof TestDiscoveryException);
diff --git a/javatests/com/android/tradefed/observatory/TestDiscoveryInvokerTest.java b/javatests/com/android/tradefed/observatory/TestDiscoveryInvokerTest.java
index 53bf0c2..6d3fc77 100644
--- a/javatests/com/android/tradefed/observatory/TestDiscoveryInvokerTest.java
+++ b/javatests/com/android/tradefed/observatory/TestDiscoveryInvokerTest.java
@@ -56,7 +56,8 @@
     private IConfiguration mConfiguration;
     private ICommandOptions mCommandOptions;
     private TestDiscoveryInvoker mTestDiscoveryInvoker;
-    private static final String CONFIG_NAME = "test_config_name";
+    private static final String DEFAULT_TEST_CONFIG_NAME = "default_config_name";
+    private static final String TEST_CONFIG_NAME = "test_config_name";
     private static final String TEST_MODULE_1_NAME = "test_module_1";
     private static final String TEST_MODULE_2_NAME = "test_module_2";
 
@@ -69,13 +70,6 @@
         when(mCommandOptions.getInvocationData()).thenReturn(new UniqueMultiMap<String, String>());
         mRootDir = FileUtil.createTempDir("test_suite_root");
         File mainDir = FileUtil.createNamedTempDir(mRootDir, "android-xts");
-        mTestDiscoveryInvoker =
-                new TestDiscoveryInvoker(mConfiguration, mRootDir) {
-                    @Override
-                    IRunUtil getRunUtil() {
-                        return mRunUtil;
-                    }
-                };
         File toolsDir = FileUtil.createNamedTempDir(mainDir, "tools");
         mTradefedJar = FileUtil.createNamedTempDir(toolsDir, "tradefed.jar");
         mCompatibilityJar = FileUtil.createNamedTempDir(toolsDir, "compatibility_mock.jar");
@@ -88,98 +82,14 @@
 
     /** Test the invocation when all necessary information are in the command line. */
     @Test
-    public void testSuccessTestDiscoveryInvocation() throws Exception {
-        String successStdout =
-                "{\"TestDependencies\":[" + TEST_MODULE_1_NAME + "," + TEST_MODULE_2_NAME + "]}";
-        String commandLine =
-                String.format(
-                        "random/test/name --cts-package-name android-cts.zip --cts-params"
-                            + " --include-test-log-tags --cts-params --log-level --cts-params"
-                            + " VERBOSE --cts-params --logcat-on-failure --config-name %s"
-                            + " --cts-params --test-tag-suffix --cts-params x86 --cts-params"
-                            + " --compatibility:test-arg --cts-params"
-                            + " com.android.tradefed.testtype.HostTest:include-annotation:android.platform.test.annotations.Presubmit"
-                            + " --cts-params --compatibility:include-filter --cts-params %s"
-                            + " --cts-params --compatibility:include-filter --cts-params %s"
-                            + " --test-tag --cts-params camera-presubmit --test-tag"
-                            + " camera-presubmit --post-method=TEST_ARTIFACT",
-                        CONFIG_NAME, TEST_MODULE_1_NAME, TEST_MODULE_2_NAME);
-        when(mConfiguration.getCommandLine()).thenReturn(commandLine);
-        Mockito.doAnswer(
-                        new Answer<Object>() {
-                            @Override
-                            public Object answer(InvocationOnMock mock) throws Throwable {
-                                Set<String> args = new HashSet<>();
-                                for (int i = 1; i < mock.getArguments().length; i++) {
-                                    args.add(mock.getArgument(i));
-                                }
-
-                                // Those are the necessary args that we care about
-                                assertTrue(
-                                        args.contains(
-                                                mCompatibilityJar.getAbsolutePath()
-                                                        + ":"
-                                                        + mTradefedJar.getAbsolutePath()));
-                                assertTrue(
-                                        args.contains(
-                                                TestDiscoveryInvoker
-                                                        .TRADEFED_OBSERVATORY_ENTRY_PATH));
-                                assertTrue(args.contains(CONFIG_NAME));
-                                assertTrue(args.contains("--compatibility:include-filter"));
-                                assertTrue(args.contains(TEST_MODULE_1_NAME));
-                                assertTrue(args.contains(TEST_MODULE_2_NAME));
-
-                                // Both cts params and config name should already been filtered out
-                                // and applied
-                                assertFalse(args.contains("--cts-params"));
-                                assertFalse(args.contains("--config-name"));
-                                CommandResult res = new CommandResult();
-                                res.setExitCode(0);
-                                res.setStatus(CommandStatus.SUCCESS);
-                                res.setStdout(successStdout);
-                                return res;
-                            }
-                        })
-                .when(mRunUtil)
-                .runTimedCmd(Mockito.anyLong(), Mockito.any());
-        List<String> testModules = mTestDiscoveryInvoker.discoverTestModuleNames();
-        assertEquals(testModules.size(), 2);
-        assertTrue(testModules.contains(TEST_MODULE_1_NAME));
-        assertTrue(testModules.contains(TEST_MODULE_2_NAME));
-    }
-
-    /**
-     * Test the invocation when the command line does not have all necessary information for the
-     * subprocess.
-     */
-    @Test
-    public void testFailTestDiscoveryInvocation() throws Exception {
-        // --config-name is missing from the cmd
-        String commandLine =
-                String.format(
-                        "random/test/name --cts-package-name android-cts.zip --cts-params"
-                            + " --include-test-log-tags --cts-params --log-level --cts-params"
-                            + " VERBOSE --cts-params --logcat-on-failure --cts-params"
-                            + " --test-tag-suffix --cts-params x86 --cts-params"
-                            + " --compatibility:test-arg --cts-params"
-                            + " com.android.tradefed.testtype.HostTest:include-annotation:android.platform.test.annotations.Presubmit"
-                            + " --cts-params --compatibility:include-filter --cts-params %s"
-                            + " --cts-params --compatibility:include-filter --cts-params %s"
-                            + " --test-tag --cts-params camera-presubmit --test-tag"
-                            + " camera-presubmit --post-method=TEST_ARTIFACT",
-                        TEST_MODULE_1_NAME, TEST_MODULE_2_NAME);
-        when(mConfiguration.getCommandLine()).thenReturn(commandLine);
-        try {
-            mTestDiscoveryInvoker.discoverTestModuleNames();
-            fail("Should throw a ConfigurationException");
-        } catch (ConfigurationException expected) {
-            // Expected
-        }
-    }
-
-    /** Test the invocation when all necessary information are in the command line. */
-    @Test
     public void testSuccessTestDependencyDiscovery() throws Exception {
+        mTestDiscoveryInvoker =
+                new TestDiscoveryInvoker(mConfiguration, mRootDir) {
+                    @Override
+                    IRunUtil getRunUtil() {
+                        return mRunUtil;
+                    }
+                };
         String successStdout =
                 "{\"TestModules\":[" + TEST_MODULE_1_NAME + "," + TEST_MODULE_2_NAME + "]}";
         String commandLine =
@@ -194,7 +104,7 @@
                             + " --cts-params --compatibility:include-filter --cts-params %s"
                             + " --test-tag --cts-params camera-presubmit --test-tag"
                             + " camera-presubmit --post-method=TEST_ARTIFACT",
-                        CONFIG_NAME, TEST_MODULE_1_NAME, TEST_MODULE_2_NAME);
+                        TEST_CONFIG_NAME, TEST_MODULE_1_NAME, TEST_MODULE_2_NAME);
         when(mConfiguration.getCommandLine()).thenReturn(commandLine);
         Mockito.doAnswer(
                         new Answer<Object>() {
@@ -215,7 +125,7 @@
                                         args.contains(
                                                 TestDiscoveryInvoker
                                                         .TRADEFED_OBSERVATORY_ENTRY_PATH));
-                                assertTrue(args.contains(CONFIG_NAME));
+                                assertTrue(args.contains(TEST_CONFIG_NAME));
                                 assertTrue(args.contains("--compatibility:include-filter"));
                                 assertTrue(args.contains(TEST_MODULE_1_NAME));
                                 assertTrue(args.contains(TEST_MODULE_2_NAME));
@@ -252,7 +162,14 @@
      */
     @Test
     public void testFailTestDependencyDiscovery() throws Exception {
-        // --config-name is missing from the cmd
+        mTestDiscoveryInvoker =
+                new TestDiscoveryInvoker(mConfiguration, mRootDir) {
+                    @Override
+                    IRunUtil getRunUtil() {
+                        return mRunUtil;
+                    }
+                };
+        // --config-name is missing from the cmd, and no default is provided
         String commandLine =
                 String.format(
                         "random/test/name --cts-package-name android-cts.zip --cts-params"
@@ -268,10 +185,89 @@
                         TEST_MODULE_1_NAME, TEST_MODULE_2_NAME);
         when(mConfiguration.getCommandLine()).thenReturn(commandLine);
         try {
-            mTestDiscoveryInvoker.discoverTestModuleNames();
+            mTestDiscoveryInvoker.discoverTestDependencies();
             fail("Should throw a ConfigurationException");
         } catch (ConfigurationException expected) {
             // Expected
         }
     }
+
+    /**
+     * Test the invocation when command line args does not contain a config name but default config
+     * name is provided.
+     */
+    @Test
+    public void testTestDependencyDiscovery_NoConfigNameInArgs() throws Exception {
+        mTestDiscoveryInvoker =
+                new TestDiscoveryInvoker(mConfiguration, DEFAULT_TEST_CONFIG_NAME, mRootDir) {
+                    @Override
+                    IRunUtil getRunUtil() {
+                        return mRunUtil;
+                    }
+                };
+        String successStdout =
+                "{\"TestModules\":[" + TEST_MODULE_1_NAME + "," + TEST_MODULE_2_NAME + "]}";
+        String commandLine =
+                String.format(
+                        "random/test/name --cts-package-name android-cts.zip --cts-params"
+                            + " --include-test-log-tags --cts-params --log-level --cts-params"
+                            + " VERBOSE --cts-params --logcat-on-failure --cts-params"
+                            + " --test-tag-suffix --cts-params x86 --cts-params"
+                            + " --compatibility:test-arg --cts-params"
+                            + " com.android.tradefed.testtype.HostTest:include-annotation:android.platform.test.annotations.Presubmit"
+                            + " --cts-params --compatibility:include-filter --cts-params %s"
+                            + " --cts-params --compatibility:include-filter --cts-params %s"
+                            + " --test-tag --cts-params camera-presubmit --test-tag"
+                            + " camera-presubmit --post-method=TEST_ARTIFACT",
+                        TEST_MODULE_1_NAME, TEST_MODULE_2_NAME);
+        when(mConfiguration.getCommandLine()).thenReturn(commandLine);
+        Mockito.doAnswer(
+                        new Answer<Object>() {
+                            @Override
+                            public Object answer(InvocationOnMock mock) throws Throwable {
+                                Set<String> args = new HashSet<>();
+                                for (int i = 1; i < mock.getArguments().length; i++) {
+                                    args.add(mock.getArgument(i));
+                                }
+
+                                // Those are the necessary args that we care about
+                                assertTrue(
+                                        args.contains(
+                                                mCompatibilityJar.getAbsolutePath()
+                                                        + ":"
+                                                        + mTradefedJar.getAbsolutePath()));
+                                assertTrue(
+                                        args.contains(
+                                                TestDiscoveryInvoker
+                                                        .TRADEFED_OBSERVATORY_ENTRY_PATH));
+                                assertTrue(args.contains(DEFAULT_TEST_CONFIG_NAME));
+                                assertTrue(args.contains("--compatibility:include-filter"));
+                                assertTrue(args.contains(TEST_MODULE_1_NAME));
+                                assertTrue(args.contains(TEST_MODULE_2_NAME));
+
+                                // Both cts params and config name should already been filtered out
+                                // and applied
+                                assertFalse(args.contains("--cts-params"));
+                                assertFalse(args.contains("--config-name"));
+                                CommandResult res = new CommandResult();
+                                res.setExitCode(0);
+                                res.setStatus(CommandStatus.SUCCESS);
+                                res.setStdout(successStdout);
+                                return res;
+                            }
+                        })
+                .when(mRunUtil)
+                .runTimedCmd(Mockito.anyLong(), Mockito.any());
+        Map<String, List<String>> testDependencies =
+                mTestDiscoveryInvoker.discoverTestDependencies();
+        assertEquals(testDependencies.size(), 1);
+        assertTrue(
+                testDependencies
+                        .get(TestDiscoveryInvoker.TEST_MODULES_LIST_KEY)
+                        .contains(TEST_MODULE_1_NAME));
+        assertTrue(
+                testDependencies
+                        .get(TestDiscoveryInvoker.TEST_MODULES_LIST_KEY)
+                        .contains(TEST_MODULE_2_NAME));
+    }
 }
diff --git a/javatests/com/android/tradefed/postprocessor/PerfettoGenericPostProcessorTest.java b/javatests/com/android/tradefed/postprocessor/PerfettoGenericPostProcessorTest.java
index 3e0ef3d..805d5da 100644
--- a/javatests/com/android/tradefed/postprocessor/PerfettoGenericPostProcessorTest.java
+++ b/javatests/com/android/tradefed/postprocessor/PerfettoGenericPostProcessorTest.java
@@ -331,6 +331,32 @@
                 15120269);
     }
 
+    /** Test metrics enabled with multiple key and string value prefixing. */
+    @Test
+    public void testParsingWithMultipleKeyAndStringValuePrefixing()
+            throws ConfigurationException, IOException {
+        setupPerfettoMetricFile(METRIC_FILE_FORMAT.text, true, true);
+        mOptionSetter.setOptionValue(PREFIX_OPTION, PREFIX_OPTION_VALUE);
+        mOptionSetter.setOptionValue(KEY_PREFIX_OPTION,
+                "perfetto.protos.ProcessRenderInfo.process_name");
+        mOptionSetter.setOptionValue(KEY_PREFIX_OPTION,
+                "perfetto.protos.ProcessRenderInfo.rt_cpu_time_ms");
+        mOptionSetter.setOptionValue(ALL_METRICS_OPTION, "true");
+        Map<String, LogFile> testLogs = new HashMap<>();
+        testLogs.put(
+                PREFIX_OPTION_VALUE,
+                new LogFile(
+                        perfettoMetricProtoFile.getAbsolutePath(), "some.url", LogDataType.TEXTPB));
+        Map<String, Metric.Builder> parsedMetrics = mProcessor
+                .processRunMetricsAndLogs(new HashMap<>(), testLogs);
+
+        assertMetricsContain(
+                parsedMetrics,
+                "perfetto_android_hwui_metric-process_info-process_name-com.android.systemui-"
+                + "rt_cpu_time_ms-2481-all_mem_min",
+                15120269);
+    }
+
     /** Test metrics enabled with key and integer value prefixing. */
     @Test
     public void testParsingWithKeyAndIntegerValuePrefixing()
diff --git a/javatests/com/android/tradefed/result/proto/StreamProtoResultReporterTest.java b/javatests/com/android/tradefed/result/proto/StreamProtoResultReporterTest.java
index 414f2a1..e4bdf6f 100644
--- a/javatests/com/android/tradefed/result/proto/StreamProtoResultReporterTest.java
+++ b/javatests/com/android/tradefed/result/proto/StreamProtoResultReporterTest.java
@@ -118,6 +118,7 @@
         } finally {
             receiver.joinReceiver(5000);
             receiver.close();
+            mReporter.closeSocket();
         }
         InOrder inOrder = Mockito.inOrder(mMockListener);
         inOrder.verify(mMockListener).invocationStarted(Mockito.any());
@@ -178,6 +179,7 @@
             mReporter.invocationEnded(500L);
         } finally {
             receiver.close();
+            mReporter.closeSocket();
         }
 
         assertNull(receiver.getError());
@@ -227,6 +229,7 @@
         } finally {
             receiver.joinReceiver(5000);
             receiver.close();
+            mReporter.closeSocket();
         }
 
         verify(mMockListener).testModuleStarted(Mockito.any());
diff --git a/javatests/com/android/tradefed/service/management/DeviceManagementGrpcServerTest.java b/javatests/com/android/tradefed/service/management/DeviceManagementGrpcServerTest.java
index 3dff8f4..3cfc682 100644
--- a/javatests/com/android/tradefed/service/management/DeviceManagementGrpcServerTest.java
+++ b/javatests/com/android/tradefed/service/management/DeviceManagementGrpcServerTest.java
@@ -22,6 +22,7 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import com.android.tradefed.command.ICommandScheduler;
 import com.android.tradefed.command.remote.DeviceDescriptor;
 import com.android.tradefed.device.DeviceAllocationState;
 import com.android.tradefed.device.FreeDeviceState;
@@ -62,6 +63,7 @@
 
     private DeviceManagementGrpcServer mServer;
     @Mock private IDeviceManager mMockDeviceManager;
+    @Mock private ICommandScheduler mMockCommandScheduler;
     @Mock private StreamObserver<GetDevicesStatusResponse> mGetDevicesStatusObserver;
     @Mock private StreamObserver<ReserveDeviceResponse> mReserveDeviceResponseObserver;
     @Mock private StreamObserver<ReleaseReservationResponse> mReleaseReservationResponseObserver;
@@ -72,7 +74,7 @@
     @Before
     public void setUp() {
         Server server = null;
-        mServer = new DeviceManagementGrpcServer(server, mMockDeviceManager);
+        mServer = new DeviceManagementGrpcServer(server, mMockDeviceManager, mMockCommandScheduler);
     }
 
     @Test
diff --git a/javatests/com/android/tradefed/service/management/TestInvocationManagementServerTest.java b/javatests/com/android/tradefed/service/management/TestInvocationManagementServerTest.java
index 20b519c..373861f 100644
--- a/javatests/com/android/tradefed/service/management/TestInvocationManagementServerTest.java
+++ b/javatests/com/android/tradefed/service/management/TestInvocationManagementServerTest.java
@@ -34,6 +34,8 @@
 import com.proto.tradefed.invocation.InvocationStatus;
 import com.proto.tradefed.invocation.NewTestCommandRequest;
 import com.proto.tradefed.invocation.NewTestCommandResponse;
+import com.proto.tradefed.invocation.StopInvocationRequest;
+import com.proto.tradefed.invocation.StopInvocationResponse;
 
 import org.junit.Before;
 import org.junit.Rule;
@@ -63,8 +65,10 @@
     @Mock private DeviceManagementGrpcServer mMockDeviceManagement;
     @Mock private StreamObserver<NewTestCommandResponse> mRequestObserver;
     @Mock private StreamObserver<InvocationDetailResponse> mDetailObserver;
+    @Mock private StreamObserver<StopInvocationResponse> mStopInvocationObserver;
     @Captor ArgumentCaptor<NewTestCommandResponse> mResponseCaptor;
     @Captor ArgumentCaptor<InvocationDetailResponse> mResponseDetailCaptor;
+    @Captor ArgumentCaptor<StopInvocationResponse> mStopInvocationCaptor;
 
     @Before
     public void setUp() {
@@ -161,4 +165,47 @@
         assertThat(record.exists()).isTrue();
         FileUtil.deleteFile(record);
     }
+
+    @Test
+    public void testSubmitTestCommand_andStop() throws Exception {
+        doAnswer(
+                        invocation -> {
+                            Object listeners = invocation.getArgument(0);
+                            ((IScheduledInvocationListener) listeners)
+                                    .invocationComplete(null, null);
+                            return 1L;
+                        })
+                .when(mMockScheduler)
+                .execCommand(Mockito.any(), Mockito.any());
+        when(mMockScheduler.stopInvocation(Mockito.anyInt(), Mockito.any())).thenReturn(true);
+        NewTestCommandRequest.Builder requestBuilder =
+                NewTestCommandRequest.newBuilder().addArgs("empty");
+        mServer.submitTestCommand(requestBuilder.build(), mRequestObserver);
+
+        verify(mRequestObserver).onNext(mResponseCaptor.capture());
+        NewTestCommandResponse response = mResponseCaptor.getValue();
+        String invocationId = response.getInvocationId();
+        assertThat(invocationId).isNotEmpty();
+
+        StopInvocationRequest.Builder stopRequest =
+                StopInvocationRequest.newBuilder()
+                        .setInvocationId(invocationId)
+                        .setReason("stopping you");
+        mServer.stopInvocation(stopRequest.build(), mStopInvocationObserver);
+        verify(mStopInvocationObserver).onNext(mStopInvocationCaptor.capture());
+        StopInvocationResponse stopResponse = mStopInvocationCaptor.getValue();
+        assertThat(stopResponse.getStatus()).isEqualTo(StopInvocationResponse.Status.SUCCESS);
+
+        // Query the file to delete it
+        InvocationDetailRequest.Builder detailBuilder =
+                InvocationDetailRequest.newBuilder().setInvocationId(response.getInvocationId());
+        mServer.getInvocationDetail(detailBuilder.build(), mDetailObserver);
+        verify(mDetailObserver).onNext(mResponseDetailCaptor.capture());
+        InvocationDetailResponse responseDetails = mResponseDetailCaptor.getValue();
+        assertThat(responseDetails.getInvocationStatus().getStatus())
+                .isEqualTo(InvocationStatus.Status.DONE);
+        File record = new File(responseDetails.getTestRecordPath());
+        assertThat(record.exists()).isTrue();
+        FileUtil.deleteFile(record);
+    }
 }
diff --git a/javatests/com/android/tradefed/suite/checker/baseline/DeviceBaselineSetterTest.java b/javatests/com/android/tradefed/suite/checker/baseline/DeviceBaselineSetterTest.java
new file mode 100644
index 0000000..5e5ada3
--- /dev/null
+++ b/javatests/com/android/tradefed/suite/checker/baseline/DeviceBaselineSetterTest.java
@@ -0,0 +1,61 @@
+/*
+ * 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.tradefed.suite.checker.baseline;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import com.android.tradefed.device.ITestDevice;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link DeviceBaselineSetter}. */
+@RunWith(JUnit4.class)
+public final class DeviceBaselineSetterTest {
+    private static final String SETTING_NAME = "test";
+
+    private static class Setter extends DeviceBaselineSetter {
+        public Setter(JSONObject object, String name) throws JSONException {
+            super(object, name);
+        }
+
+        @Override
+        public boolean setBaseline(ITestDevice mDevice) {
+            return true;
+        }
+    }
+
+    /** Test that the experimental flag is set to false when the input filed is null or false. */
+    @Test
+    public void isExperimental_withoutTrueExperimentalField_returnFalse() throws Exception {
+        assertFalse(new Setter(new JSONObject("{}"), SETTING_NAME).isExperimental());
+        assertFalse(
+                new Setter(new JSONObject("{\"experimental\": false}"), SETTING_NAME)
+                        .isExperimental());
+    }
+
+    /** Test that the experimental flag is set to true when the input filed is true. */
+    @Test
+    public void isExperimental_withTrueExperimentalField_returnTrue() throws Exception {
+        assertTrue(
+                new Setter(new JSONObject("{\"experimental\": true}"), SETTING_NAME)
+                        .isExperimental());
+    }
+}
diff --git a/javatests/com/android/tradefed/suite/checker/baseline/LockSettingsBaselineSetterTest.java b/javatests/com/android/tradefed/suite/checker/baseline/LockSettingsBaselineSetterTest.java
new file mode 100644
index 0000000..18e2908
--- /dev/null
+++ b/javatests/com/android/tradefed/suite/checker/baseline/LockSettingsBaselineSetterTest.java
@@ -0,0 +1,94 @@
+/*
+ * 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.tradefed.suite.checker.baseline;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.android.tradefed.device.ITestDevice;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link LockSettingsBaselineSetter}. */
+@RunWith(JUnit4.class)
+public final class LockSettingsBaselineSetterTest {
+
+    private ITestDevice mMockDevice;
+    private LockSettingsBaselineSetter mSetter;
+    private JSONObject mJsonObject;
+    private static final String SETTING_NAME = "test";
+    private static final String SETTING_STRING = "{\"clear_pwds\": [\"0000\", \"1234\"]}";
+    private static final String GET_LOCK_SCREEN_COMMAND = "locksettings get-disabled";
+    private static final String LOCK_SCREEN_OFF_COMMAND = "locksettings set-disabled true";
+    private static final String CLEAR_PWD_COMMAND = "locksettings clear --old %s";
+
+    @Before
+    public void setup() throws Exception {
+        mMockDevice = mock(ITestDevice.class);
+        mJsonObject = new JSONObject(SETTING_STRING);
+        mSetter = new LockSettingsBaselineSetter(mJsonObject, SETTING_NAME);
+    }
+
+    @Test
+    public void lockSettingsDeviceBaselineSetter_noPasswordField_throwsException()
+            throws Exception {
+        mJsonObject.remove("clear_pwds");
+        assertThrows(
+                JSONException.class,
+                () -> new LockSettingsBaselineSetter(mJsonObject, SETTING_NAME));
+    }
+
+    /** Test that the setter skips removing passwords when lock-screen is turned off. */
+    @Test
+    public void setBaseline_lockScreenOff_skipRemovingPasswords() throws Exception {
+        when(mMockDevice.executeShellCommand(GET_LOCK_SCREEN_COMMAND)).thenReturn("true");
+        assertTrue(mSetter.setBaseline(mMockDevice));
+        verify(mMockDevice).executeShellCommand(GET_LOCK_SCREEN_COMMAND);
+        verify(mMockDevice, never()).executeShellCommand(LOCK_SCREEN_OFF_COMMAND);
+        verify(mMockDevice, never()).executeShellCommand(String.format(CLEAR_PWD_COMMAND, "0000"));
+        verify(mMockDevice, never()).executeShellCommand(String.format(CLEAR_PWD_COMMAND, "1234"));
+    }
+
+    /** Test that the setter removes passwords successfully. */
+    @Test
+    public void setBaseline_setSucceeds_passwordsRemoved() throws Exception {
+        when(mMockDevice.executeShellCommand(GET_LOCK_SCREEN_COMMAND)).thenReturn("false", "true");
+        assertTrue(mSetter.setBaseline(mMockDevice));
+        verify(mMockDevice, times(2)).executeShellCommand(GET_LOCK_SCREEN_COMMAND);
+        verify(mMockDevice).executeShellCommand(LOCK_SCREEN_OFF_COMMAND);
+        verify(mMockDevice).executeShellCommand(String.format(CLEAR_PWD_COMMAND, "0000"));
+        verify(mMockDevice).executeShellCommand(String.format(CLEAR_PWD_COMMAND, "1234"));
+    }
+
+    /** Test that the setter returns false when the baseline is failed to set. */
+    @Test
+    public void setBaseline_setFails_returnFalse() throws Exception {
+        when(mMockDevice.executeShellCommand(GET_LOCK_SCREEN_COMMAND)).thenReturn("false");
+        assertFalse(mSetter.setBaseline(mMockDevice));
+    }
+}
diff --git a/javatests/com/android/tradefed/suite/checker/baseline/SettingsBaselineSetterTest.java b/javatests/com/android/tradefed/suite/checker/baseline/SettingsBaselineSetterTest.java
new file mode 100644
index 0000000..5a32610
--- /dev/null
+++ b/javatests/com/android/tradefed/suite/checker/baseline/SettingsBaselineSetterTest.java
@@ -0,0 +1,94 @@
+/*
+ * 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.tradefed.suite.checker.baseline;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.android.tradefed.device.ITestDevice;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link SettingsBaselineSetter}. */
+@RunWith(JUnit4.class)
+public final class SettingsBaselineSetterTest {
+
+    private ITestDevice mMockDevice;
+    private SettingsBaselineSetter mSetter;
+    private JSONObject mJsonObject;
+    private static final String SETTING_NAME = "test";
+    private static final String SETTING_STRING =
+            "{\"namespace\": \"global\", \"key\": "
+                    + "\"stay_on_while_plugged_in\", \"value\": \"7\"}";
+
+    @Before
+    public void setup() throws Exception {
+        mMockDevice = mock(ITestDevice.class);
+        mJsonObject = new JSONObject(SETTING_STRING);
+        mSetter = new SettingsBaselineSetter(mJsonObject, SETTING_NAME);
+    }
+
+    @Test
+    public void settingsDeviceBaselineSetter_noNamespaceField_throwsException() throws Exception {
+        mJsonObject.remove("namespace");
+        assertThrows(
+                JSONException.class, () -> new SettingsBaselineSetter(mJsonObject, SETTING_NAME));
+    }
+
+    @Test
+    public void settingsDeviceBaselineSetter_noKeyField_throwsException() throws Exception {
+        mJsonObject.remove("key");
+        assertThrows(
+                JSONException.class, () -> new SettingsBaselineSetter(mJsonObject, SETTING_NAME));
+    }
+
+    @Test
+    public void settingsDeviceBaselineSetter_noValueField_throwsException() throws Exception {
+        mJsonObject.remove("value");
+        assertThrows(
+                JSONException.class, () -> new SettingsBaselineSetter(mJsonObject, SETTING_NAME));
+    }
+
+    /** Test that the setter returns false when the setting key doesn't exist. */
+    @Test
+    public void setBaseline_invalidKey_returnFalse() throws Exception {
+        when(mMockDevice.getSetting("global", "stay_on_while_plugged_in")).thenReturn(null);
+        assertFalse(mSetter.setBaseline(mMockDevice));
+    }
+
+    /** Test that the setter returns false when the baseline is failed to set. */
+    @Test
+    public void setBaseline_setFails_returnFalse() throws Exception {
+        when(mMockDevice.getSetting("global", "stay_on_while_plugged_in")).thenReturn("0");
+        assertFalse(mSetter.setBaseline(mMockDevice));
+    }
+
+    /** Test that the setter returns true when the baseline is set successfully. */
+    @Test
+    public void setBaseline_setSucceeds_returnTrue() throws Exception {
+        when(mMockDevice.getSetting("global", "stay_on_while_plugged_in")).thenReturn("7");
+        assertTrue(mSetter.setBaseline(mMockDevice));
+    }
+}
diff --git a/javatests/com/android/tradefed/targetprep/DynamicSystemPreparerTest.java b/javatests/com/android/tradefed/targetprep/DynamicSystemPreparerTest.java
index e0f0148..32a457c 100644
--- a/javatests/com/android/tradefed/targetprep/DynamicSystemPreparerTest.java
+++ b/javatests/com/android/tradefed/targetprep/DynamicSystemPreparerTest.java
@@ -64,6 +64,7 @@
     private File mSystemImageDir;
     private File mSystemImageZip;
     private File mMultiSystemImageZip;
+    private boolean mShouldTimeOut;
     // The object under test.
     private DynamicSystemPreparer mPreparer;
 
@@ -85,7 +86,14 @@
                         "DynamicSystem_multi");
         mBuildInfo = new BuildInfo();
 
-        mPreparer = new DynamicSystemPreparer();
+        mShouldTimeOut = false;
+        mPreparer =
+                new DynamicSystemPreparer() {
+                    @Override
+                    boolean hasTimedOut(long deadline) {
+                        return mShouldTimeOut;
+                    }
+                };
 
         IInvocationContext context = new InvocationContext();
         context.addAllocatedDevice("device", mMockDevice);
@@ -107,7 +115,7 @@
     private File createImageDir(String... fileNames) throws IOException {
         File tempDir = FileUtil.createTempDir("createImageDir");
         for (String fileName : fileNames) {
-            new File(tempDir, fileName).createNewFile();
+            FileUtil.writeToFile("test", new File(tempDir, fileName));
         }
         return tempDir;
     }
@@ -168,6 +176,18 @@
     }
 
     @Test
+    public void testSetUp_imageConversionTimeout() throws BuildError, DeviceNotAvailableException {
+        mBuildInfo.setFile(SYSTEM_IMAGE_ZIP_NAME, mSystemImageZip, "0");
+        mShouldTimeOut = true;
+        try {
+            mPreparer.setUp(mTestInfo);
+            Assert.fail("setUp() should have thrown.");
+        } catch (TargetSetupError e) {
+            Assert.assertEquals("Fail to create image archive.", e.getMessage());
+        }
+    }
+
+    @Test
     public void testSetUp_installationFail() throws BuildError, DeviceNotAvailableException {
         mBuildInfo.setFile(SYSTEM_IMAGE_ZIP_NAME, mSystemImageZip, "0");
         Mockito.when(
diff --git a/javatests/com/android/tradefed/targetprep/GkiDeviceFlashPreparerTest.java b/javatests/com/android/tradefed/targetprep/GkiDeviceFlashPreparerTest.java
index ed9e50c..d5f18b4 100644
--- a/javatests/com/android/tradefed/targetprep/GkiDeviceFlashPreparerTest.java
+++ b/javatests/com/android/tradefed/targetprep/GkiDeviceFlashPreparerTest.java
@@ -285,6 +285,58 @@
         verify(mMockDevice).postBootSetup();
     }
 
+    /* Verifies that preparer can flash GKI boot image and vendor_boot, vendor_dlkm, dtbo images */
+    @Test
+    public void testSetup_vendor_img_Success() throws Exception {
+        File imgDir = FileUtil.createTempDir("img_folder", mTmpDir);
+        File bootImg = new File(imgDir, "boot-5.4.img");
+        File vendorBootImg = new File(imgDir, "vendor_boot.img");
+        File dtboImg = new File(imgDir, "dtbo.img");
+        File vendorDlkmImg = new File(imgDir, "vendor_dlkm.img");
+        FileUtil.writeToFile("ddd", bootImg);
+        FileUtil.writeToFile("123", vendorBootImg);
+        FileUtil.writeToFile("456", dtboImg);
+        FileUtil.writeToFile("789", vendorDlkmImg);
+        mBuildInfo.setFile("gki_boot.img", bootImg, "0");
+        mBuildInfo.setFile("vendor_boot.img", vendorBootImg, "0");
+        mBuildInfo.setFile("vendor_dlkm.img", vendorDlkmImg, "0");
+        mBuildInfo.setFile("dtbo.img", dtboImg, "0");
+
+        when(mMockDevice.executeLongFastbootCommand(
+                        "flash", "boot", mBuildInfo.getFile("gki_boot.img").getAbsolutePath()))
+                .thenReturn(mSuccessResult);
+        when(mMockDevice.executeLongFastbootCommand(
+                        "flash",
+                        "vendor_boot",
+                        mBuildInfo.getFile("vendor_boot.img").getAbsolutePath()))
+                .thenReturn(mSuccessResult);
+        when(mMockDevice.executeLongFastbootCommand(
+                        "flash",
+                        "vendor_dlkm",
+                        mBuildInfo.getFile("vendor_dlkm.img").getAbsolutePath()))
+                .thenReturn(mSuccessResult);
+        when(mMockDevice.executeLongFastbootCommand(
+                        "flash", "dtbo", mBuildInfo.getFile("dtbo.img").getAbsolutePath()))
+                .thenReturn(mSuccessResult);
+        when(mMockDevice.executeLongFastbootCommand("-w")).thenReturn(mSuccessResult);
+
+        when(mMockDevice.enableAdbRoot()).thenReturn(Boolean.TRUE);
+
+        mPreparer.setUp(mTestInfo);
+        mPreparer.tearDown(mTestInfo, null);
+
+        verify(mMockDevice).rebootIntoBootloader();
+        verify(mMockRunUtil).allowInterrupt(false);
+        verify(mMockRunUtil).allowInterrupt(true);
+        verify(mMockDevice).rebootIntoFastbootd();
+        verify(mMockRunUtil).sleep(anyLong());
+        verify(mMockDevice).rebootUntilOnline();
+        verify(mMockDevice).setDate(null);
+        verify(mMockDevice).waitForDeviceAvailable(anyLong());
+        verify(mMockDevice).setRecoveryMode(RecoveryMode.AVAILABLE);
+        verify(mMockDevice).postBootSetup();
+    }
+
     /* Verifies that preparer can flash GKI boot image from a Zip file*/
     @Test
     public void testSetup_Success_FromZip() throws Exception {
diff --git a/javatests/com/android/tradefed/testtype/HostTestTest.java b/javatests/com/android/tradefed/testtype/HostTestTest.java
index ed75949..9afdf32 100644
--- a/javatests/com/android/tradefed/testtype/HostTestTest.java
+++ b/javatests/com/android/tradefed/testtype/HostTestTest.java
@@ -573,7 +573,7 @@
             assertTrue(mHelloWorld != null && mFoobar != null);
             assertTrue(
                     "Expects 'hello' value to be 'hello:world'", mHelloWorld.equals("hello:world"));
-            assertTrue("Expects 'foobar' value to be 'baz:qux'", mFoobar.equals("baz:qux"));
+            assertTrue("Expects 'foobar' value to be 'baz:qux=wap'", mFoobar.equals("baz:qux=wap"));
 
             metrics.addTestMetric("hello", mHelloWorld);
             metrics.addTestMetric("foobar", mFoobar);
@@ -2480,7 +2480,8 @@
                         + ":gcs-bucket-file:gs\\://bucket/path/file");
         setter.setOptionValue("set-option", "hello:hello\\:world");
         setter.setOptionValue(
-                "set-option", OptionEscapeColonTestCase.class.getName() + ":foobar:baz\\:qux");
+                "set-option",
+                OptionEscapeColonTestCase.class.getName() + ":foobar:baz\\:qux\\=wap");
         TestDescription testGcsBucket =
                 new TestDescription(OptionEscapeColonTestCase.class.getName(), "testGcsBucket");
         TestDescription testEscapeStrings =
@@ -2491,9 +2492,12 @@
 
         verify(mListener).testRunStarted((String) Mockito.any(), Mockito.eq(2));
         verify(mListener).testStarted(Mockito.eq(testGcsBucket));
+        verify(mListener, times(0)).testFailed(Mockito.eq(testGcsBucket), (String) Mockito.any());
         verify(mListener)
                 .testEnded(Mockito.eq(testGcsBucket), (HashMap<String, Metric>) Mockito.any());
         verify(mListener).testStarted(Mockito.eq(testEscapeStrings));
+        verify(mListener, times(0))
+                .testFailed(Mockito.eq(testEscapeStrings), (String) Mockito.any());
         verify(mListener)
                 .testEnded(Mockito.eq(testEscapeStrings), (HashMap<String, Metric>) Mockito.any());
         verify(mListener).testRunEnded(Mockito.anyLong(), (HashMap<String, Metric>) Mockito.any());
diff --git a/javatests/com/android/tradefed/testtype/suite/AtestRunnerTest.java b/javatests/com/android/tradefed/testtype/suite/AtestRunnerTest.java
index d2ee71d..472bfad 100644
--- a/javatests/com/android/tradefed/testtype/suite/AtestRunnerTest.java
+++ b/javatests/com/android/tradefed/testtype/suite/AtestRunnerTest.java
@@ -17,12 +17,9 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
-import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.when;
 
-import com.android.tradefed.build.DeviceBuildInfo;
 import com.android.tradefed.build.IDeviceBuildInfo;
 import com.android.tradefed.config.IConfiguration;
 import com.android.tradefed.config.OptionSetter;
@@ -41,7 +38,9 @@
 import com.android.tradefed.util.FileUtil;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
@@ -58,6 +57,8 @@
 @RunWith(JUnit4.class)
 public class AtestRunnerTest {
 
+    @Rule public TemporaryFolder mTempFolder = new TemporaryFolder();
+
     private static final String TEST_CONFIG =
         "<configuration description=\"Runs a stub tests part of some suite\">\n"
             + "    <test class=\"com.android.tradefed.testtype.suite.SuiteModuleLoaderTest"
@@ -93,12 +94,12 @@
     @Before
     public void setUp() throws Exception {
         mRunner = new AbiAtestRunner();
-        mBuildInfo = spy(new DeviceBuildInfo());
+        mBuildInfo = mock(IDeviceBuildInfo.class);
         mMockDevice = mock(ITestDevice.class);
         mRunner.setBuild(mBuildInfo);
         mRunner.setDevice(mMockDevice);
 
-        doReturn(new File("some-dir")).when(mBuildInfo).getTestsDir();
+        when(mBuildInfo.getTestsDir()).thenReturn(mTempFolder.newFolder());
 
         CommandResult result = new CommandResult(CommandStatus.SUCCESS);
         result.setStdout("Supported states: [\n" +
diff --git a/javatests/com/android/tradefed/testtype/suite/BaseTestSuiteTest.java b/javatests/com/android/tradefed/testtype/suite/BaseTestSuiteTest.java
index da6cbd7..7bf8092 100644
--- a/javatests/com/android/tradefed/testtype/suite/BaseTestSuiteTest.java
+++ b/javatests/com/android/tradefed/testtype/suite/BaseTestSuiteTest.java
@@ -47,7 +47,9 @@
 import com.google.common.truth.Truth;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 import org.mockito.Mock;
@@ -66,6 +68,8 @@
 /** Unit tests for {@link BaseTestSuite}. */
 @RunWith(JUnit4.class)
 public class BaseTestSuiteTest {
+    @Rule public TemporaryFolder mTempFolder = new TemporaryFolder();
+
     private BaseTestSuite mRunner;
     private IDeviceBuildInfo mBuildInfo;
     @Mock ITestDevice mMockDevice;
@@ -82,6 +86,8 @@
         mRunner.setBuild(mBuildInfo);
         mRunner.setDevice(mMockDevice);
 
+        mBuildInfo.setTestsDir(mTempFolder.newFolder(), "testsdir");
+
         IInvocationContext context = new InvocationContext();
         context.addAllocatedDevice(ConfigurationDef.DEFAULT_DEVICE_NAME, mMockDevice);
         context.addDeviceBuildInfo(ConfigurationDef.DEFAULT_DEVICE_NAME, mBuildInfo);
diff --git a/javatests/com/android/tradefed/testtype/suite/ITestSuiteTest.java b/javatests/com/android/tradefed/testtype/suite/ITestSuiteTest.java
index 47a0eef..0e202af 100644
--- a/javatests/com/android/tradefed/testtype/suite/ITestSuiteTest.java
+++ b/javatests/com/android/tradefed/testtype/suite/ITestSuiteTest.java
@@ -88,7 +88,9 @@
 import com.android.tradefed.util.MultiMap;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 import org.mockito.ArgumentCaptor;
@@ -112,6 +114,8 @@
 @RunWith(JUnit4.class)
 public class ITestSuiteTest {
 
+    @Rule public TemporaryFolder mTempFolder = new TemporaryFolder();
+
     private static final MultiMap<String, String> METADATA_INCLUDES = new MultiMap<>();
     private static final MultiMap<String, String> METADATA_EXCLUDES = new MultiMap<>();
     private static final String EMPTY_CONFIG = "empty";
@@ -1816,6 +1820,7 @@
     @Test
     public void testStageTestArtifacts() throws Exception {
         String remoteFilePath = "gs://module1/tests.zip";
+        File testsDir = mTempFolder.newFolder();
         DynamicRemoteFileResolver dynamicResolver =
                 new DynamicRemoteFileResolver() {
                     @Override
@@ -1825,7 +1830,7 @@
                             List<String> includeFilters,
                             List<String> excludeFilters)
                             throws BuildRetrievalError {
-                        assertEquals(new File("tests_dir"), destDir);
+                        assertEquals(destDir, testsDir);
                         assertEquals(remoteFilePath, remoteFilePath);
                         assertArrayEquals(new String[] {"/test/"}, includeFilters.toArray());
                         assertArrayEquals(new String[] {"[.]config$"}, excludeFilters.toArray());
@@ -1835,7 +1840,7 @@
         setter.setOptionValue("partial-download-via-feature", "false");
         mTestSuite.setDynamicResolver(dynamicResolver);
         IDeviceBuildInfo mockBuildInfo = mock(IDeviceBuildInfo.class);
-        when(mockBuildInfo.getTestsDir()).thenReturn(new File("tests_dir"));
+        when(mockBuildInfo.getTestsDir()).thenReturn(testsDir);
         when(mockBuildInfo.getRemoteFiles())
                 .thenReturn(new HashSet<File>(Arrays.asList(new File(remoteFilePath))));
         mTestSuite.setBuild(mockBuildInfo);
diff --git a/javatests/com/android/tradefed/testtype/suite/TestMappingSuiteRunnerTest.java b/javatests/com/android/tradefed/testtype/suite/TestMappingSuiteRunnerTest.java
index 0862114..1e567bd 100644
--- a/javatests/com/android/tradefed/testtype/suite/TestMappingSuiteRunnerTest.java
+++ b/javatests/com/android/tradefed/testtype/suite/TestMappingSuiteRunnerTest.java
@@ -54,7 +54,9 @@
 import com.android.tradefed.util.testmapping.TestOption;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 import org.mockito.Mock;
@@ -81,11 +83,12 @@
 @RunWith(JUnit4.class)
 public class TestMappingSuiteRunnerTest {
 
+    @Rule public TemporaryFolder mTempFolder = new TemporaryFolder();
+
     private static final String ABI_1 = "arm64-v8a";
     private static final String ABI_2 = "armeabi-v7a";
     private static final String DISABLED_PRESUBMIT_TESTS = "disabled-presubmit-tests";
     private static final String EMPTY_CONFIG = "empty";
-    private static final String NON_EXISTING_DIR = "non-existing-dir";
     private static final String TEST_CONFIG_NAME = "test";
     private static final String TEST_DATA_DIR = "testdata";
     private static final String TEST_MAPPING = "TEST_MAPPING";
@@ -119,6 +122,7 @@
         mRunner = new AbiTestMappingSuite();
         mRunner.setBuild(mBuildInfo);
         mRunner.setDevice(mMockDevice);
+        mRunner.setSkipjarLoading(false);
 
         mOptionSetter = new OptionSetter(mRunner);
         mOptionSetter.setOptionValue("suite-config-prefix", "suite");
@@ -126,6 +130,7 @@
         mRunner2 = new FakeTestMappingSuiteRunner();
         mRunner2.setBuild(mBuildInfo);
         mRunner2.setDevice(mMockDevice);
+        mRunner2.setSkipjarLoading(false);
 
         mMainlineRunner = new FakeMainlineTMSR();
         mMainlineRunner.setBuild(mBuildInfo);
@@ -140,7 +145,7 @@
         mTestInfo = TestInformation.newBuilder().setInvocationContext(context).build();
 
         when(mBuildInfo.getFile(BuildInfoFileKey.TARGET_LINKED_DIR)).thenReturn(null);
-        when(mBuildInfo.getTestsDir()).thenReturn(new File(NON_EXISTING_DIR));
+        when(mBuildInfo.getTestsDir()).thenReturn(mTempFolder.newFolder());
         when(mMockDevice.getProperty(Mockito.any())).thenReturn(ABI_1);
         when(mMockDevice.getProperty(Mockito.any())).thenReturn(ABI_2);
         when(mMockDevice.getIDevice()).thenReturn(mock(IDevice.class));
@@ -236,7 +241,7 @@
 
             IDeviceBuildInfo mockBuildInfo = mock(IDeviceBuildInfo.class);
             when(mockBuildInfo.getFile(BuildInfoFileKey.TARGET_LINKED_DIR)).thenReturn(null);
-            when(mockBuildInfo.getTestsDir()).thenReturn(new File("non-existing-dir"));
+            when(mockBuildInfo.getTestsDir()).thenReturn(mTempFolder.newFolder());
             when(mockBuildInfo.getFile(TEST_MAPPINGS_ZIP)).thenReturn(zipFile);
 
             mRunner.setBuild(mockBuildInfo);
@@ -330,7 +335,7 @@
 
             IDeviceBuildInfo mockBuildInfo = mock(IDeviceBuildInfo.class);
             when(mockBuildInfo.getFile(BuildInfoFileKey.TARGET_LINKED_DIR)).thenReturn(null);
-            when(mockBuildInfo.getTestsDir()).thenReturn(new File("non-existing-dir"));
+            when(mockBuildInfo.getTestsDir()).thenReturn(mTempFolder.newFolder());
             when(mockBuildInfo.getFile(TEST_MAPPINGS_ZIP)).thenReturn(zipFile);
 
             mRunner.setBuild(mockBuildInfo);
@@ -395,7 +400,7 @@
 
             IDeviceBuildInfo mockBuildInfo = mock(IDeviceBuildInfo.class);
             when(mockBuildInfo.getFile(BuildInfoFileKey.TARGET_LINKED_DIR)).thenReturn(null);
-            when(mockBuildInfo.getTestsDir()).thenReturn(new File("non-existing-dir"));
+            when(mockBuildInfo.getTestsDir()).thenReturn(mTempFolder.newFolder());
             when(mockBuildInfo.getFile(TEST_MAPPINGS_ZIP)).thenReturn(zipFile);
 
             mRunner.setBuild(mockBuildInfo);
@@ -508,7 +513,7 @@
 
             IDeviceBuildInfo mockBuildInfo = mock(IDeviceBuildInfo.class);
             when(mockBuildInfo.getFile(BuildInfoFileKey.HOST_LINKED_DIR)).thenReturn(null);
-            when(mockBuildInfo.getTestsDir()).thenReturn(new File("non-existing-dir"));
+            when(mockBuildInfo.getTestsDir()).thenReturn(mTempFolder.newFolder());
             when(mockBuildInfo.getFile(TEST_MAPPINGS_ZIP)).thenReturn(zipFile);
 
             mRunner.setBuild(mockBuildInfo);
@@ -551,7 +556,7 @@
 
             IDeviceBuildInfo mockBuildInfo = mock(IDeviceBuildInfo.class);
             when(mockBuildInfo.getFile(BuildInfoFileKey.TARGET_LINKED_DIR)).thenReturn(null);
-            when(mockBuildInfo.getTestsDir()).thenReturn(new File("non-existing-dir"));
+            when(mockBuildInfo.getTestsDir()).thenReturn(mTempFolder.newFolder());
             when(mockBuildInfo.getFile(TEST_MAPPINGS_ZIP)).thenReturn(zipFile);
             when(mockBuildInfo.getRemoteFiles()).thenReturn(null);
 
@@ -595,7 +600,7 @@
 
             IDeviceBuildInfo mockBuildInfo = mock(IDeviceBuildInfo.class);
             when(mockBuildInfo.getFile(BuildInfoFileKey.TARGET_LINKED_DIR)).thenReturn(null);
-            when(mockBuildInfo.getTestsDir()).thenReturn(new File("non-existing-dir"));
+            when(mockBuildInfo.getTestsDir()).thenReturn(mTempFolder.newFolder());
             when(mockBuildInfo.getFile(TEST_MAPPINGS_ZIP)).thenReturn(zipFile);
 
             mTestInfo
@@ -630,7 +635,7 @@
             ZipUtil.createZip(srcDir, zipFile);
 
             IDeviceBuildInfo mockBuildInfo = mock(IDeviceBuildInfo.class);
-            when(mockBuildInfo.getTestsDir()).thenReturn(new File("non-existing-dir"));
+            when(mockBuildInfo.getTestsDir()).thenReturn(mTempFolder.newFolder());
             when(mockBuildInfo.getFile(TEST_MAPPINGS_ZIP)).thenReturn(zipFile);
 
             mRunner.setBuild(mockBuildInfo);
@@ -689,7 +694,7 @@
 
             IDeviceBuildInfo mockBuildInfo = mock(IDeviceBuildInfo.class);
             when(mockBuildInfo.getFile(BuildInfoFileKey.TARGET_LINKED_DIR)).thenReturn(null);
-            when(mockBuildInfo.getTestsDir()).thenReturn(new File("non-existing-dir"));
+            when(mockBuildInfo.getTestsDir()).thenReturn(mTempFolder.newFolder());
             when(mockBuildInfo.getFile(TEST_MAPPINGS_ZIP)).thenReturn(zipFile);
 
             mRunner.setBuild(mockBuildInfo);
@@ -738,7 +743,7 @@
 
             IDeviceBuildInfo mockBuildInfo = mock(IDeviceBuildInfo.class);
             when(mockBuildInfo.getFile(BuildInfoFileKey.TARGET_LINKED_DIR)).thenReturn(null);
-            when(mockBuildInfo.getTestsDir()).thenReturn(new File("non-existing-dir"));
+            when(mockBuildInfo.getTestsDir()).thenReturn(mTempFolder.newFolder());
             when(mockBuildInfo.getFile(TEST_MAPPINGS_ZIP)).thenReturn(zipFile);
 
             mRunner.setBuild(mockBuildInfo);
@@ -879,7 +884,6 @@
     @Test
     public void testLoadTests_moduleDifferentoptions() throws Exception {
         File tempDir = null;
-        File tempTestsDir = null;
         try {
             mOptionSetter.setOptionValue("test-mapping-test-group", "presubmit");
 
@@ -899,7 +903,7 @@
 
             IDeviceBuildInfo mockBuildInfo = mock(IDeviceBuildInfo.class);
             when(mockBuildInfo.getFile(BuildInfoFileKey.TARGET_LINKED_DIR)).thenReturn(null);
-            when(mockBuildInfo.getTestsDir()).thenReturn(tempTestsDir);
+            when(mockBuildInfo.getTestsDir()).thenReturn(mTempFolder.newFolder());
             when(mockBuildInfo.getFile(TEST_MAPPINGS_ZIP)).thenReturn(zipFile);
             when(mockBuildInfo.getBuildBranch()).thenReturn("branch");
             when(mockBuildInfo.getBuildFlavor()).thenReturn("flavor");
@@ -927,7 +931,6 @@
             }
         } finally {
             FileUtil.recursiveDelete(tempDir);
-            FileUtil.recursiveDelete(tempTestsDir);
         }
     }
 
@@ -1046,7 +1049,7 @@
 
             IDeviceBuildInfo mockBuildInfo = mock(IDeviceBuildInfo.class);
             when(mockBuildInfo.getFile(BuildInfoFileKey.TARGET_LINKED_DIR)).thenReturn(null);
-            when(mockBuildInfo.getTestsDir()).thenReturn(new File("non-existing-dir"));
+            when(mockBuildInfo.getTestsDir()).thenReturn(mTempFolder.newFolder());
             when(mockBuildInfo.getFile(TEST_MAPPINGS_ZIP)).thenReturn(zipFile);
             mRunner.setBuild(mockBuildInfo);
             mRunner.setPrioritizeHostConfig(true);
@@ -1114,12 +1117,12 @@
 
             IDeviceBuildInfo mockBuildInfo = mock(IDeviceBuildInfo.class);
             when(mockBuildInfo.getFile(BuildInfoFileKey.TARGET_LINKED_DIR)).thenReturn(null);
-            when(mockBuildInfo.getTestsDir()).thenReturn(new File("non-existing-dir"));
+            when(mockBuildInfo.getTestsDir()).thenReturn(mTempFolder.newFolder());
             when(mockBuildInfo.getFile(TEST_MAPPINGS_ZIP)).thenReturn(zipFile);
             when(mockBuildInfo.getFile("extra-zip")).thenReturn(zipFile);
             mRunner.setBuild(mockBuildInfo);
 
-            LinkedHashMap<String, IConfiguration> configMap = mRunner.loadTests();
+            mRunner.loadTests();
             fail("Should have thrown an exception.");
         } catch (HarnessRuntimeException expected) {
             // expected
@@ -1146,7 +1149,7 @@
 
             IDeviceBuildInfo mockBuildInfo = mock(IDeviceBuildInfo.class);
             when(mockBuildInfo.getFile(BuildInfoFileKey.TARGET_LINKED_DIR)).thenReturn(null);
-            when(mockBuildInfo.getTestsDir()).thenReturn(new File("non-existing-dir"));
+            when(mockBuildInfo.getTestsDir()).thenReturn(mTempFolder.newFolder());
             when(mockBuildInfo.getFile(TEST_MAPPINGS_ZIP)).thenReturn(zipFile);
             when(mockBuildInfo.getFile("extra-zip")).thenReturn(zipFile2);
             mRunner.setBuild(mockBuildInfo);
@@ -1175,11 +1178,11 @@
 
             IDeviceBuildInfo mockBuildInfo = mock(IDeviceBuildInfo.class);
             when(mockBuildInfo.getFile(BuildInfoFileKey.TARGET_LINKED_DIR)).thenReturn(null);
-            when(mockBuildInfo.getTestsDir()).thenReturn(new File("non-existing-dir"));
+            when(mockBuildInfo.getTestsDir()).thenReturn(mTempFolder.newFolder());
             when(mockBuildInfo.getFile(TEST_MAPPINGS_ZIP)).thenReturn(zipFile);
             when(mockBuildInfo.getFile("extra-zip")).thenReturn(null);
             mRunner.setBuild(mockBuildInfo);
-            LinkedHashMap<String, IConfiguration> configMap = mRunner.loadTests();
+            mRunner.loadTests();
             fail("Should have thrown an exception.");
         } catch (HarnessRuntimeException expected) {
             // expected
diff --git a/javatests/com/android/tradefed/util/SubprocessTestResultsParserTest.java b/javatests/com/android/tradefed/util/SubprocessTestResultsParserTest.java
index ef65509..241712f 100644
--- a/javatests/com/android/tradefed/util/SubprocessTestResultsParserTest.java
+++ b/javatests/com/android/tradefed/util/SubprocessTestResultsParserTest.java
@@ -549,43 +549,6 @@
     }
 
     @Test
-    public void testParse_logAssociation_zipped() throws Exception {
-        ILogSaverListener mockRunListener = mock(ILogSaverListener.class);
-
-        File logDir = FileUtil.createTempDir("log-assos-dir");
-        File log = FileUtil.createTempFile("dataname-log-assos", ".txt", logDir);
-        File zipLog = ZipUtil.createZip(logDir);
-        LogFile logFile = new LogFile(zipLog.getAbsolutePath(), null, LogDataType.TEXT);
-        File serializedLogFile = null;
-        File tmp = FileUtil.createTempFile("sub", "unit");
-        SubprocessTestResultsParser resultParser = null;
-        try {
-            serializedLogFile = SerializationUtil.serialize(logFile);
-            resultParser =
-                    new SubprocessTestResultsParser(mockRunListener, new InvocationContext());
-            String logAssociation =
-                    String.format(
-                            "LOG_ASSOCIATION {\"loggedFile\":\"%s\",\"dataName\":\"dataname\"}\n",
-                            serializedLogFile.getAbsolutePath());
-            FileUtil.writeToFile(logAssociation, tmp, true);
-            resultParser.parseFile(tmp);
-
-            verify(mockRunListener)
-                    .testLog(
-                            Mockito.eq("subprocess-dataname"),
-                            Mockito.eq(LogDataType.ZIP),
-                            Mockito.any());
-        } finally {
-            StreamUtil.close(resultParser);
-            FileUtil.deleteFile(serializedLogFile);
-            FileUtil.deleteFile(tmp);
-            FileUtil.deleteFile(log);
-            FileUtil.recursiveDelete(logDir);
-            FileUtil.deleteFile(zipLog);
-        }
-    }
-
-    @Test
     public void testParse_avoidDoubleLog() throws Exception {
         ILogSaverListener mockRunListener = mock(ILogSaverListener.class);
 
diff --git a/javatests/res/config/tf/unit-runner.xml b/javatests/res/config/tf/unit-runner.xml
index ddc527c..aae299b 100644
--- a/javatests/res/config/tf/unit-runner.xml
+++ b/javatests/res/config/tf/unit-runner.xml
@@ -24,4 +24,5 @@
     <result_reporter class="com.android.tradefed.result.SubprocessResultsReporter">
         <option name="output-test-log" value="true" />
     </result_reporter>
+    <result_reporter class="com.android.tradefed.result.proto.StreamProtoResultReporter" />
 </configuration>
diff --git a/proto/device/device_manager.proto b/proto/device/device_manager.proto
index 4511d6f..1a792c5 100644
--- a/proto/device/device_manager.proto
+++ b/proto/device/device_manager.proto
@@ -32,6 +32,23 @@
   // Get the devices status
   rpc GetDevicesStatus(GetDevicesStatusRequest)
       returns (GetDevicesStatusResponse) {}
+  // Apply to stop leasing tests. The RPC returns immediately and doesn't wait
+  // for all leasing being stopped.
+  rpc StopLeasing(StopLeasingRequest) returns (StopLeasingResponse) {}
+}
+
+// The request of stopping leasing tests.
+message StopLeasingRequest {}
+
+// The response of stopping leasing tests.
+message StopLeasingResponse {
+  enum Result {
+    UNKNOWN = 0;
+    SUCCEED = 1;
+    FAIL = 2;
+  }
+  Result result = 1;
+  string message = 2;
 }
 
 // The request to reserve device.
diff --git a/proto/invocation/invocation_manager.proto b/proto/invocation/invocation_manager.proto
index 2b7ea76..830000c 100644
--- a/proto/invocation/invocation_manager.proto
+++ b/proto/invocation/invocation_manager.proto
@@ -29,6 +29,8 @@
   rpc SubmitTestCommand(NewTestCommandRequest) returns (NewTestCommandResponse) {}
   // Query the invocation detail info of a specific test command.
   rpc GetInvocationDetail(InvocationDetailRequest) returns (InvocationDetailResponse) {}
+  // Request an invocation to be stopped, non-blocking.
+  rpc StopInvocation(StopInvocationRequest) returns (StopInvocationResponse) {}
 }
 
 // A new TF test request.
@@ -48,6 +50,27 @@
   CommandErrorInfo command_error_info = 2;
 }
 
+// Request the invocation to stop
+message StopInvocationRequest {
+  // Invocation id of the test to request stop
+  string invocation_id = 1;
+  // Specify a reason to be associated with the stop request
+  string reason = 2;
+}
+
+message StopInvocationResponse {
+  // Type of invocation status
+  enum Status {
+    UNSPECIFIED = 0;
+    SUCCESS = 1;
+    ERROR = 2;
+  }
+  // The type of status.
+  Status status = 1;
+  // If set, an error occurred and is described by ErrorInfo.
+  CommandErrorInfo command_error_info = 2;
+}
+
 // The current status of the test command.
 message InvocationStatus {
   // Type of invocation status
diff --git a/res/config/checker/baseline_config.json b/res/config/checker/baseline_config.json
index 5098aa1..7f1d372 100644
--- a/res/config/checker/baseline_config.json
+++ b/res/config/checker/baseline_config.json
@@ -1,12 +1,31 @@
 {
   "keep_screen_on": {
+    "class_name": "com.android.tradefed.suite.checker.baseline.SettingsBaselineSetter",
     "namespace": "global",
     "key": "stay_on_while_plugged_in",
     "value": "7"
   },
   "disable_os_auto_update": {
+    "class_name": "com.android.tradefed.suite.checker.baseline.SettingsBaselineSetter",
     "namespace": "global",
     "key": "ota_disable_automatic_update",
     "value": "1"
+  },
+  "disable_device_config_sync": {
+    "class_name": "com.android.tradefed.suite.checker.baseline.SettingsBaselineSetter",
+    "namespace": "global",
+    "key": "device_config_sync_disabled",
+    "value": "1",
+    "experimental": true
+  },
+  "disable_usb_app_verification": {
+    "class_name": "com.android.tradefed.suite.checker.baseline.SettingsBaselineSetter",
+    "namespace": "secure",
+    "key": "verifier_verify_adb_installs",
+    "value": "0"
+  },
+  "clear_lock_screen": {
+    "class_name": "com.android.tradefed.suite.checker.baseline.LockSettingsBaselineSetter",
+    "clear_pwds": ["0000", "1234", "12345", "private"]
   }
 }
\ No newline at end of file
diff --git a/src/com/android/tradefed/cluster/ClusterCommandEvent.java b/src/com/android/tradefed/cluster/ClusterCommandEvent.java
index 50a11e0..ab2f636 100644
--- a/src/com/android/tradefed/cluster/ClusterCommandEvent.java
+++ b/src/com/android/tradefed/cluster/ClusterCommandEvent.java
@@ -57,7 +57,8 @@
         InvocationEnded,
         InvocationCompleted,
         TestRunInProgress,
-        TestEnded
+        TestEnded,
+        Unleased
     }
 
     private long mTimestamp;
diff --git a/src/com/android/tradefed/cluster/ClusterCommandScheduler.java b/src/com/android/tradefed/cluster/ClusterCommandScheduler.java
index 1c6cd99..23f919c 100644
--- a/src/com/android/tradefed/cluster/ClusterCommandScheduler.java
+++ b/src/com/android/tradefed/cluster/ClusterCommandScheduler.java
@@ -33,6 +33,7 @@
 import com.android.tradefed.device.battery.BatteryController;
 import com.android.tradefed.device.battery.IBatteryInfo;
 import com.android.tradefed.device.battery.IBatteryInfo.BatteryState;
+import com.android.tradefed.error.HarnessRuntimeException;
 import com.android.tradefed.error.IHarnessException;
 import com.android.tradefed.host.IHostOptions.PermitLimitType;
 import com.android.tradefed.invoker.IInvocationContext;
@@ -58,6 +59,7 @@
 
 import java.io.File;
 import java.io.IOException;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
@@ -290,7 +292,14 @@
             mFailureDescription = failure;
             mError = failure.getErrorMessage();
             if (failure.getCause() != null) {
-                mError = StreamUtil.getStackTrace(failure.getCause());
+                Throwable cause = failure.getCause();
+                mError = StreamUtil.getStackTrace(cause);
+                if (cause instanceof HarnessRuntimeException
+                        && InfraErrorIdentifier.TRADEFED_SKIPPED_TESTS_DURING_SHUTDOWN.equals(
+                                ((HarnessRuntimeException) cause).getErrorId())) {
+                    // Tests were not run, so un-lease the command so that it can be rescheduled.
+                    unleaseCommands(Arrays.asList(mCommandTask));
+                }
             }
         }
 
@@ -530,7 +539,8 @@
             return;
         }
         if (isShuttingDown()) {
-            CLog.d("Tradefed shutting down, ignoring commands.");
+            CLog.d("Tradefed shutting down, unleasing commands.");
+            unleaseCommands(commands);
             return;
         }
         execCommands(commands);
@@ -675,9 +685,11 @@
      * @param commands a list of {@link ClusterCommand}s fetched from the cluster command queue.
      */
     void execCommands(final List<ClusterCommand> commands) {
+        int commandIdx = 0;
         for (final ClusterCommand commandTask : commands) {
             if (isShuttingDown()) {
-                CLog.d("Tradefed shutting down, ignoring remaining commands.");
+                CLog.d("Tradefed shutting down, unleasing remaining commands.");
+                unleaseCommands(commands.subList(commandIdx, commands.size()));
                 return;
             }
             try {
@@ -743,6 +755,7 @@
                 eventUploader.postEvent(eventBuilder.build());
                 eventUploader.flush();
             }
+            commandIdx++;
         }
     }
 
@@ -868,4 +881,22 @@
             CLog.e("failed to upload host state %s to TFC: %s", state.toString(), e);
         }
     }
+
+    /**
+     * Notifies TFC of commands that were not executed and need to be rescheduled.
+     *
+     * @param commands a list of {@link ClusterCommand} that need to be unleased to get rescheduled.
+     */
+    private synchronized void unleaseCommands(final List<ClusterCommand> commands) {
+        IClusterEventUploader<ClusterCommandEvent> eventUploader =
+                getClusterClient().getCommandEventUploader();
+        for (ClusterCommand command : commands) {
+            ClusterCommandEvent.Builder eventBuilder =
+                    ClusterCommandEvent.createEventBuilder(command)
+                            .setHostName(ClusterHostUtil.getHostName())
+                            .setType(ClusterCommandEvent.Type.Unleased);
+            eventUploader.postEvent(eventBuilder.build());
+        }
+        eventUploader.flush();
+    }
 }
diff --git a/src/com/android/tradefed/command/CommandOptions.java b/src/com/android/tradefed/command/CommandOptions.java
index 2e9970e..78470c2 100644
--- a/src/com/android/tradefed/command/CommandOptions.java
+++ b/src/com/android/tradefed/command/CommandOptions.java
@@ -146,6 +146,11 @@
     public static final String USE_REMOTE_SANDBOX = "use-remote-sandbox";
 
     @Option(
+            name = "remote-files",
+            description = "A list of files references to store in build info")
+    private Set<String> mRemoteFiles = new LinkedHashSet<>();
+
+    @Option(
         name = USE_SANDBOX,
         description = "Set if the invocation should use a sandbox to run or not."
     )
@@ -273,6 +278,9 @@
                                   + "under developing, not for other uses.")
     private Integer mMultiDeviceCount;
 
+    @Option(name = "enable-tracing", description = "Enable test invocation tracing.")
+    private boolean mTracingEnabled = true;
+
     /**
      * Set the help mode for the config.
      * <p/>
@@ -508,6 +516,12 @@
 
     /** {@inheritDoc} */
     @Override
+    public Set<String> getRemoteFiles() {
+        return mRemoteFiles;
+    }
+
+    /** {@inheritDoc} */
+    @Override
     public boolean shouldUseSandboxing() {
         return mUseSandbox;
     }
@@ -703,4 +717,10 @@
     public void setMultiDeviceCount(int count) {
         mMultiDeviceCount = count;
     }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean isTracingEnabled() {
+        return mTracingEnabled;
+    }
 }
diff --git a/src/com/android/tradefed/command/CommandScheduler.java b/src/com/android/tradefed/command/CommandScheduler.java
index 0c73817..eb3c36d 100644
--- a/src/com/android/tradefed/command/CommandScheduler.java
+++ b/src/com/android/tradefed/command/CommandScheduler.java
@@ -61,12 +61,19 @@
 import com.android.tradefed.invoker.InvocationContext;
 import com.android.tradefed.invoker.TestInvocation;
 import com.android.tradefed.invoker.shard.ParentShardReplicate;
+import com.android.tradefed.invoker.tracing.ActiveTrace;
+import com.android.tradefed.invoker.tracing.CloseableTraceScope;
+import com.android.tradefed.invoker.tracing.TracingLogger;
 import com.android.tradefed.log.ILogRegistry.EventType;
 import com.android.tradefed.log.LogRegistry;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.result.ConsoleResultReporter;
+import com.android.tradefed.result.FileInputStreamSource;
 import com.android.tradefed.result.ILogSaver;
+import com.android.tradefed.result.ILogSaverListener;
 import com.android.tradefed.result.ITestInvocationListener;
+import com.android.tradefed.result.LogDataType;
+import com.android.tradefed.result.LogFile;
 import com.android.tradefed.result.LogSaverResultForwarder;
 import com.android.tradefed.result.ResultForwarder;
 import com.android.tradefed.result.error.ErrorIdentifier;
@@ -78,6 +85,7 @@
 import com.android.tradefed.service.management.TestInvocationManagementServer;
 import com.android.tradefed.targetprep.DeviceFailedToBootError;
 import com.android.tradefed.testtype.IRemoteTest;
+import com.android.tradefed.testtype.SubprocessTfLauncher;
 import com.android.tradefed.testtype.suite.retry.RetryRescheduler;
 import com.android.tradefed.util.ArrayUtil;
 import com.android.tradefed.util.FileUtil;
@@ -142,6 +150,8 @@
 
     /** map of device to active invocation threads */
     private Map<IInvocationContext, InvocationThread> mInvocationThreadMap;
+    /** Track invocation that are done and terminating */
+    private Map<IInvocationContext, InvocationThread> mInvocationThreadMapTerminating;
 
     /** timer for scheduling commands to be re-queued for execution */
     private ScheduledThreadPoolExecutor mCommandTimer;
@@ -587,22 +597,31 @@
             if (mClient != null) {
                 mClient.notifyTradefedInvocationStartEvent();
             }
+            IConfiguration config = mCmd.getConfiguration();
+            if (config.getCommandOptions().isTracingEnabled()) {
+                long pid = ProcessHandle.current().pid();
+                long tid = Thread.currentThread().getId();
+                ActiveTrace trace = TracingLogger.createActiveTrace(pid, tid);
+                trace.startTracing(
+                        config.getCommandOptions()
+                                .getInvocationData()
+                                .containsKey(SubprocessTfLauncher.SUBPROCESS_TAG_NAME));
+            }
             mStartTime = System.currentTimeMillis();
             ITestInvocation instance = getInvocation();
-            IConfiguration config = mCmd.getConfiguration();
-
-            for (final IScheduledInvocationListener listener : mListeners) {
-                try {
-                    listener.invocationInitiated(mInvocationContext);
-                } catch (Throwable anyException) {
-                    CLog.e("Exception caught while calling invocationInitiated:");
-                    CLog.e(anyException);
+            try (CloseableTraceScope ignore = new CloseableTraceScope("init")) {
+                for (final IScheduledInvocationListener listener : mListeners) {
+                    try {
+                        listener.invocationInitiated(mInvocationContext);
+                    } catch (Throwable anyException) {
+                        CLog.e("Exception caught while calling invocationInitiated:");
+                        CLog.e(anyException);
+                    }
                 }
             }
-
             Exception trackDeviceException = null;
             boolean lastInvocationSet = false;
-            try {
+            try (CloseableTraceScope ignore = new CloseableTraceScope("test-invocation")) {
                 // Copy the command options invocation attributes to the invocation if it has not
                 // been already done.
                 if (!config.getConfigurationDescription().shouldUseSandbox()
@@ -633,7 +652,7 @@
                         e);
                 setLastInvocationExitCode(ExitCode.FATAL_HOST_ERROR, e);
                 lastInvocationSet = true;
-                shutdown();
+                shutdown(true);
             } catch (Throwable e) {
                 setLastInvocationExitCode(ExitCode.THROWABLE_EXCEPTION, e);
                 lastInvocationSet = true;
@@ -649,25 +668,26 @@
                         "Updating command %d with elapsed time %d ms",
                         mCmd.getCommandTracker().getId(),
                         elapsedTime);
-                // remove invocation thread first so another invocation can be started on device
-                // when freed
-                removeInvocationThread(this);
+                try (CloseableTraceScope ignore = new CloseableTraceScope("finalize_invocation")) {
+                    // remove invocation thread first so another invocation can be started on device
+                    // when freed
+                    removeInvocationThread(this);
 
-                checkStrayThreads();
+                    checkStrayThreads();
 
-                Map<ITestDevice, FreeDeviceState> deviceStates =
-                        createReleaseMap(mInvocationContext, trackDeviceException);
-                for (final IScheduledInvocationListener listener : mListeners) {
-                    try {
-                        listener.invocationComplete(mInvocationContext, deviceStates);
-                    } catch (Throwable anyException) {
-                        CLog.e("Exception caught while calling invocationComplete:");
-                        CLog.e(anyException);
+                    Map<ITestDevice, FreeDeviceState> deviceStates =
+                            createReleaseMap(mInvocationContext, trackDeviceException);
+                    for (final IScheduledInvocationListener listener : mListeners) {
+                        try {
+                            listener.invocationComplete(mInvocationContext, deviceStates);
+                        } catch (Throwable anyException) {
+                            CLog.e("Exception caught while calling invocationComplete:");
+                            CLog.e(anyException);
+                        }
                     }
-                }
-                if (!lastInvocationSet && instance.getExitInfo() != null) {
-                    setLastInvocationExitCode(
-                            instance.getExitInfo().mExitCode, instance.getExitInfo().mStack);
+                    if (!lastInvocationSet && instance.getExitInfo() != null) {
+                        setLastInvocationExitCode(
+                                instance.getExitInfo().mExitCode, instance.getExitInfo().mStack);
                 }
                 if (getFeatureServer() != null) {
                     getFeatureServer().unregisterInvocation(config);
@@ -675,13 +695,55 @@
                 mCmd.commandFinished(elapsedTime);
                 logInvocationEndedEvent(
                         mCmd.getCommandTracker().getId(), elapsedTime, mInvocationContext);
-                CLog.d("Finalizing the logger and invocation.");
+                }
+                CLog.logAndDisplay(LogLevel.INFO, "Finalizing the logger and invocation.");
+                ActiveTrace trace = TracingLogger.getActiveTrace();
+                if (trace != null) {
+                    File traceFile = trace.finalizeTracing();
+                    if (traceFile != null) {
+                        logTrace(traceFile, config);
+                    }
+                }
                 if (config.getCommandOptions().reportInvocationComplete()) {
                     LogSaverResultForwarder.reportEndHostLog(
-                            config.getLogSaver(), TestInvocation.TRADEFED_INVOC_COMPLETE_HOST_LOG);
+                            config.getTestInvocationListeners(),
+                            config.getLogSaver(),
+                            TestInvocation.TRADEFED_INVOC_COMPLETE_HOST_LOG);
                     config.getLogOutput().closeLog();
                     LogRegistry.getLogRegistry().unregisterLogger();
                 }
+                clearTerminating(this);
+            }
+        }
+
+        /** Special handling to send the trace file from subprocess when needed. */
+        private void logTrace(File traceFile, IConfiguration config) {
+            if (config.getCommandOptions()
+                    .getInvocationData()
+                    .containsKey(SubprocessTfLauncher.SUBPROCESS_TAG_NAME)) {
+                CLog.logAndDisplay(LogLevel.INFO, "Sending trace from subprocess");
+                LogFile perfettoTrace =
+                        new LogFile(traceFile.getAbsolutePath(), null, LogDataType.PERFETTO);
+                for (ITestInvocationListener listener : config.getTestInvocationListeners()) {
+                    try {
+                        if (listener instanceof ILogSaverListener) {
+                            ((ILogSaverListener) listener)
+                                    .logAssociation(ActiveTrace.TRACE_KEY, perfettoTrace);
+                        }
+                    } catch (Exception e) {
+                        CLog.logAndDisplay(LogLevel.ERROR, e.getMessage());
+                        CLog.e(e);
+                    }
+                }
+            } else {
+                try (FileInputStreamSource source = new FileInputStreamSource(traceFile, true)) {
+                    LogSaverResultForwarder.logFile(
+                            config.getTestInvocationListeners(),
+                            config.getLogSaver(),
+                            source,
+                            ActiveTrace.TRACE_KEY,
+                            LogDataType.PERFETTO);
+                }
             }
         }
 
@@ -738,6 +800,11 @@
             return mInvocationContext;
         }
 
+        /** Notify invocation on {@link CommandScheduler#shutdown()}. */
+        public void notifyInvocationStop(String message) {
+            getInvocation().notifyInvocationStopped(message);
+        }
+
         /**
          * Stops a running invocation. {@link CommandScheduler#shutdownHard()} will stop all running
          * invocations.
@@ -751,7 +818,7 @@
          * invocations.
          */
         public void stopInvocation(String message, ErrorIdentifier errorId) {
-            getInvocation().notifyInvocationStopped(message, errorId);
+            getInvocation().notifyInvocationForceStopped(message, errorId);
             for (ITestDevice device : mInvocationContext.getDevices()) {
                 if (TestDeviceState.ONLINE.equals(device.getDeviceState())) {
                     // Kill all running processes on device.
@@ -938,6 +1005,7 @@
         mSleepingCommands = new HashSet<>();
         mExecutingCommands = new HashSet<>();
         mInvocationThreadMap = new HashMap<IInvocationContext, InvocationThread>();
+        mInvocationThreadMapTerminating = new HashMap<IInvocationContext, InvocationThread>();
         // use a ScheduledThreadPoolExecutorTimer as a single-threaded timer. This class
         // is used instead of a java.util.Timer because it offers advanced shutdown options
         mCommandTimer = new ScheduledThreadPoolExecutor(1);
@@ -1104,6 +1172,7 @@
             }
             CLog.i("Waiting for invocation threads to complete");
             waitForAllInvocationThreads();
+            waitForTerminatingInvocationThreads();
             exit(manager);
             cleanUp();
             CLog.logAndDisplay(LogLevel.INFO, "All done");
@@ -1220,10 +1289,10 @@
     }
 
     /** Wait until all invocation threads complete. */
-    protected void waitForAllInvocationThreads() {
+    private void waitForAllInvocationThreads() {
         List<InvocationThread> threadListCopy;
         synchronized (this) {
-            threadListCopy = new ArrayList<InvocationThread>(mInvocationThreadMap.size());
+            threadListCopy = new ArrayList<>();
             threadListCopy.addAll(mInvocationThreadMap.values());
         }
         for (Thread thread : threadListCopy) {
@@ -1231,6 +1300,17 @@
         }
     }
 
+    private void waitForTerminatingInvocationThreads() {
+        List<InvocationThread> threadListCopy;
+        synchronized (this) {
+            threadListCopy = new ArrayList<>();
+            threadListCopy.addAll(mInvocationThreadMapTerminating.values());
+        }
+        for (Thread thread : threadListCopy) {
+            waitForThread(thread);
+        }
+    }
+
     private void exit(IDeviceManager manager) {
         if (manager != null) {
             manager.terminate();
@@ -1619,7 +1699,7 @@
 
     /** {@inheritDoc} */
     @Override
-    public void execCommand(
+    public long execCommand(
             IInvocationContext context, IScheduledInvocationListener listener, String[] args)
             throws ConfigurationException, NoDeviceException {
         assertStarted();
@@ -1632,7 +1712,7 @@
             // createConfiguration can be long for things like sandbox, so ensure we did not
             // start a shutdown in the meantime.
             CLog.w("Tradefed is shutting down, ignoring command.");
-            return;
+            return -1;
         }
 
         ExecutableCommand execCmd = createExecutableCommand(cmdTracker, config, false);
@@ -1646,12 +1726,14 @@
             }
             CLog.d("Executing '%s' on '%s'", cmdTracker.getArgs()[0], devices);
             startInvocation(context, execCmd, listener, new FreeDeviceHandler(manager));
+            return execCmd.getCommandTracker().getId();
         } else {
             // Log adb output just to help debug
-            String adbOutput =
-                    ((DeviceManager) GlobalConfiguration.getDeviceManagerInstance())
-                            .executeGlobalAdbCommand("devices");
-            CLog.e("'adb devices' output:\n%s", adbOutput);
+            if (getDeviceManager() instanceof DeviceManager) {
+                String adbOutput =
+                        ((DeviceManager) getDeviceManager()).executeGlobalAdbCommand("devices");
+                CLog.e("'adb devices' output:\n%s", adbOutput);
+            }
             throw new NoDeviceException(
                     String.format(
                             "No device match for allocation. Reason: %s.\ncommand: %s",
@@ -1662,7 +1744,7 @@
 
     /** {@inheritDoc} */
     @Override
-    public void execCommand(
+    public long execCommand(
             IScheduledInvocationListener listener, ITestDevice device, String[] args)
             throws ConfigurationException {
         // TODO: add support for execCommand multi-device allocation
@@ -1690,13 +1772,14 @@
         context.setConfigurationDescriptor(config.getConfigurationDescription());
         context.addAllocatedDevice(config.getDeviceConfig().get(0).getDeviceName(), device);
         startInvocation(context, execCmd, listener);
+        return execCmd.getCommandTracker().getId();
     }
 
     /** {@inheritDoc} */
     @Override
-    public void execCommand(IScheduledInvocationListener listener, String[] args)
+    public long execCommand(IScheduledInvocationListener listener, String[] args)
             throws ConfigurationException, NoDeviceException {
-        execCommand(createInvocationContext(), listener, args);
+        return execCommand(createInvocationContext(), listener, args);
     }
 
     /**
@@ -1812,6 +1895,11 @@
     /** Removes a {@link InvocationThread} from the active list. */
     private synchronized void removeInvocationThread(InvocationThread invThread) {
         mInvocationThreadMap.remove(invThread.getInvocationContext());
+        mInvocationThreadMapTerminating.put(invThread.getInvocationContext(), invThread);
+    }
+
+    private synchronized void clearTerminating(InvocationThread invThread) {
+        mInvocationThreadMapTerminating.remove(invThread.getInvocationContext());
     }
 
     private synchronized void throwIfDeviceInInvocationThread(List<ITestDevice> devices) {
@@ -1846,13 +1934,18 @@
         return mCommandTimer.isShutdown() || mShutdownOnEmpty;
     }
 
-    /**
-     * {@inheritDoc}
-     */
+    /** {@inheritDoc} */
     @Override
-    public synchronized void shutdown() {
+    public synchronized void shutdown(boolean notifyStop) {
         setHostState(HostState.QUITTING);
         doShutdown();
+
+        if (notifyStop) {
+            String reason = "Tradefed is notified to stop";
+            for (InvocationThread thread : mInvocationThreadMap.values()) {
+                thread.notifyInvocationStop(reason);
+            }
+        }
     }
 
     private synchronized void doShutdown() {
diff --git a/src/com/android/tradefed/command/Console.java b/src/com/android/tradefed/command/Console.java
index 3528a50..5eb8211 100644
--- a/src/com/android/tradefed/command/Console.java
+++ b/src/com/android/tradefed/command/Console.java
@@ -172,7 +172,7 @@
                     exitMode = "commands";
                     mScheduler.shutdownOnEmpty();
                 } else {
-                    mScheduler.shutdown();
+                    mScheduler.shutdown(true);
                 }
                 printLine("Signalling command scheduler for shutdown.");
                 printLine(
@@ -1390,7 +1390,8 @@
                     deviceManagementServer =
                             new DeviceManagementGrpcServer(
                                     deviceManagementPort,
-                                    GlobalConfiguration.getDeviceManagerInstance());
+                                    GlobalConfiguration.getDeviceManagerInstance(),
+                                    GlobalConfiguration.getInstance().getCommandScheduler());
                     GlobalConfiguration.getInstance()
                             .setDeviceManagementServer(deviceManagementServer);
                     deviceManagementServer.start();
diff --git a/src/com/android/tradefed/command/ICommandOptions.java b/src/com/android/tradefed/command/ICommandOptions.java
index bd537b0..b7f20ea 100644
--- a/src/com/android/tradefed/command/ICommandOptions.java
+++ b/src/com/android/tradefed/command/ICommandOptions.java
@@ -155,6 +155,9 @@
     /** Returns the data passed to the invocation to describe it */
     public UniqueMultiMap<String, String> getInvocationData();
 
+    /** Returns the list of remote files configured. */
+    public Set<String> getRemoteFiles();
+
     /** Returns true if we should use Tf containers to run the invocation */
     public boolean shouldUseSandboxing();
 
@@ -256,4 +259,7 @@
 
     /** Sets the number of expected devices for multi-device tests. */
     public void setMultiDeviceCount(int count);
+
+    /** Returns whether or not invocation tracing is enabled. */
+    public boolean isTracingEnabled();
 }
diff --git a/src/com/android/tradefed/command/ICommandScheduler.java b/src/com/android/tradefed/command/ICommandScheduler.java
index 6ddc842..cebfde8 100644
--- a/src/com/android/tradefed/command/ICommandScheduler.java
+++ b/src/com/android/tradefed/command/ICommandScheduler.java
@@ -106,11 +106,11 @@
      *
      * @param listener the {@link ICommandScheduler.IScheduledInvocationListener} to be informed
      * @param args the command arguments
-     *
+     * @return The invocation id of the scheduled command.
      * @throws ConfigurationException if command was invalid
      * @throws NoDeviceException if there is no device to use
      */
-    public void execCommand(IScheduledInvocationListener listener, String[] args)
+    public long execCommand(IScheduledInvocationListener listener, String[] args)
             throws ConfigurationException, NoDeviceException;
 
     /**
@@ -119,11 +119,12 @@
      * @param listener the {@link ICommandScheduler.IScheduledInvocationListener} to be informed
      * @param device the {@link ITestDevice} to use
      * @param args the command arguments
-     *
+     * @return The invocation id of the scheduled command.
      * @throws ConfigurationException if command was invalid
      */
-    public void execCommand(IScheduledInvocationListener listener, ITestDevice device,
-            String[] args) throws ConfigurationException;
+    public long execCommand(
+            IScheduledInvocationListener listener, ITestDevice device, String[] args)
+            throws ConfigurationException;
 
     /**
      * Directly allocates a device and executes a command without adding it to the command queue
@@ -135,7 +136,7 @@
      * @throws ConfigurationException if command was invalid
      * @throws NoDeviceException if there is no device to use
      */
-    public void execCommand(
+    public long execCommand(
             IInvocationContext context, IScheduledInvocationListener listener, String[] args)
             throws ConfigurationException, NoDeviceException;
 
@@ -146,14 +147,23 @@
 
     /**
      * Attempt to gracefully shutdown the command scheduler.
-     * <p/>
-     * Clears commands waiting to be tested, and requests that all invocations in progress
-     * shut down gracefully.
-     * <p/>
-     * After shutdown is called, the scheduler main loop will wait for all invocations in progress
-     * to complete before exiting completely.
+     *
+     * <p>Clears commands waiting to be tested, and requests that all invocations in progress shut
+     * down gracefully.
+     *
+     * <p>After shutdown is called, the scheduler main loop will wait for all invocations in
+     * progress to complete before exiting completely.
      */
-    public void shutdown();
+    default void shutdown() {
+        shutdown(false);
+    }
+
+    /**
+     * Attempt to gracefully shutdown the command scheduler.
+     *
+     * @param notifyStop if true, notifies invocations of TF shutdown.
+     */
+    public void shutdown(boolean notifyStop);
 
     /**
      * Similar to {@link #shutdown()}, but will instead wait for all commands to be executed
diff --git a/src/com/android/tradefed/config/ConfigurationDef.java b/src/com/android/tradefed/config/ConfigurationDef.java
index e595de6..0f8bc17 100644
--- a/src/com/android/tradefed/config/ConfigurationDef.java
+++ b/src/com/android/tradefed/config/ConfigurationDef.java
@@ -19,6 +19,8 @@
 import com.android.tradefed.device.metric.IMetricCollector;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.result.error.InfraErrorIdentifier;
+import com.android.tradefed.targetprep.ILabPreparer;
+import com.android.tradefed.targetprep.ITargetPreparer;
 
 import java.io.File;
 import java.lang.reflect.InvocationTargetException;
@@ -346,6 +348,22 @@
         config.setConfigurationObjectList(Configuration.DEVICE_NAME, deviceObjectList);
         injectOptions(config, mOptionList);
 
+        List<ITargetPreparer> notILab = new ArrayList<>();
+        for (IDeviceConfiguration deviceConfig : config.getDeviceConfig()) {
+            for (ITargetPreparer labPreparer : deviceConfig.getLabPreparers()) {
+                if (!(labPreparer instanceof ILabPreparer)) {
+                    notILab.add(labPreparer);
+                }
+            }
+        }
+        if (!notILab.isEmpty()) {
+            throw new ConfigurationException(
+                    String.format(
+                            "The following were specified as lab_preparer "
+                                    + "but aren't ILabPreparer: %s",
+                            notILab),
+                    InfraErrorIdentifier.OPTION_CONFIGURATION_ERROR);
+        }
         return config;
     }
 
diff --git a/src/com/android/tradefed/config/filter/OptionFetcher.java b/src/com/android/tradefed/config/filter/OptionFetcher.java
index 58a0656..6f62432 100644
--- a/src/com/android/tradefed/config/filter/OptionFetcher.java
+++ b/src/com/android/tradefed/config/filter/OptionFetcher.java
@@ -38,13 +38,9 @@
  */
 public class OptionFetcher implements AutoCloseable {
 
-    /**
-     * Set of options that should align with the parent process.
-     */
-    private static final Set<String> OPTION_TO_FETCH = ImmutableSet.of(
-            "retry-isolation-grade",
-            "avd-in-parent"
-            );
+    /** Set of options that should align with the parent process. */
+    private static final Set<String> OPTION_TO_FETCH =
+            ImmutableSet.of("retry-isolation-grade", "avd-in-parent", "enable-tracing");
 
     private TradefedFeatureClient mClient;
 
diff --git a/src/com/android/tradefed/device/BackgroundDeviceAction.java b/src/com/android/tradefed/device/BackgroundDeviceAction.java
index 53c6fd7..32fda9a 100644
--- a/src/com/android/tradefed/device/BackgroundDeviceAction.java
+++ b/src/com/android/tradefed/device/BackgroundDeviceAction.java
@@ -21,6 +21,7 @@
 import com.android.ddmlib.IShellOutputReceiver;
 import com.android.ddmlib.ShellCommandUnresponsiveException;
 import com.android.ddmlib.TimeoutException;
+import com.android.tradefed.invoker.tracing.CloseableTraceScope;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.util.IRunUtil;
 import com.android.tradefed.util.RunUtil;
@@ -82,27 +83,31 @@
         String separator = String.format(
                 "\n========== beginning of new [%s] output ==========\n", mDescriptor);
         while (!isCancelled()) {
-            if (mLogStartDelay > 0) {
-                CLog.d("Sleep for %d before starting %s for %s.", mLogStartDelay, mDescriptor,
-                        mTestDevice.getSerialNumber());
-                getRunUtil().sleep(mLogStartDelay);
-            }
-            blockUntilOnlineNoThrow();
-            // check again if the operation has been cancelled after the wait for online
-            if (isCancelled()) {
-                break;
-            }
-            CLog.d("Starting %s for %s.", mDescriptor, mTestDevice.getSerialNumber());
-            mReceiver.addOutput(separator.getBytes(), 0, separator.length());
-            try {
-                mTestDevice.getIDevice().executeShellCommand(mCommand, mReceiver,
-                        0, TimeUnit.MILLISECONDS);
-            } catch (AdbCommandRejectedException e) {
-                // For command rejected wait a bit to let the device reach a stable state again.
-                getRunUtil().sleep(ONLINE_POLL_INTERVAL_MS);
-                waitForDeviceRecovery(e.getClass().getName());
-            } catch (IOException | ShellCommandUnresponsiveException | TimeoutException e) {
-                waitForDeviceRecovery(e.getClass().getName());
+            try (CloseableTraceScope ignore = new CloseableTraceScope()) {
+                if (mLogStartDelay > 0) {
+                    CLog.d(
+                            "Sleep for %d before starting %s for %s.",
+                            mLogStartDelay, mDescriptor, mTestDevice.getSerialNumber());
+                    getRunUtil().sleep(mLogStartDelay);
+                }
+                blockUntilOnlineNoThrow();
+                // check again if the operation has been cancelled after the wait for online
+                if (isCancelled()) {
+                    break;
+                }
+                CLog.d("Starting %s for %s.", mDescriptor, mTestDevice.getSerialNumber());
+                mReceiver.addOutput(separator.getBytes(), 0, separator.length());
+                try {
+                    mTestDevice
+                            .getIDevice()
+                            .executeShellCommand(mCommand, mReceiver, 0, TimeUnit.MILLISECONDS);
+                } catch (AdbCommandRejectedException e) {
+                    // For command rejected wait a bit to let the device reach a stable state again.
+                    getRunUtil().sleep(ONLINE_POLL_INTERVAL_MS);
+                    waitForDeviceRecovery(e.getClass().getName());
+                } catch (IOException | ShellCommandUnresponsiveException | TimeoutException e) {
+                    waitForDeviceRecovery(e.getClass().getName());
+                }
             }
         }
     }
diff --git a/src/com/android/tradefed/device/DeviceManager.java b/src/com/android/tradefed/device/DeviceManager.java
index 9fc5c78..dbd0f6f 100644
--- a/src/com/android/tradefed/device/DeviceManager.java
+++ b/src/com/android/tradefed/device/DeviceManager.java
@@ -548,20 +548,17 @@
             //  hostname.google.com:vsoc-1
             String[] parts = preconfigureDevice.split(":", 2);
             preconfigureHostUsers.putIfAbsent(parts[0], new ArrayList<>());
-            preconfigureHostUsers.get(parts[0]).add(parts[1]);
+            preconfigureHostUsers.get(parts[0]).add(parts.length > 1 ? parts[1] : null);
         }
         for (Map.Entry<String, List<String>> hostUsers : preconfigureHostUsers.entrySet()) {
             for (int i = 0; i < hostUsers.getValue().size(); i++) {
-                addAvailableDevice(
-                        new RemoteAvdIDevice(
-                                String.format(
-                                        "%s-%s-%s",
-                                        GCE_DEVICE_SERIAL_PREFIX,
-                                        hostUsers.getKey(),
-                                        hostUsers.getValue().get(i)),
-                                hostUsers.getKey(),
-                                hostUsers.getValue().get(i),
-                                i));
+                String user = hostUsers.getValue().get(i);
+                String serial =
+                        String.format("%s-%s-%d", GCE_DEVICE_SERIAL_PREFIX, hostUsers.getKey(), i);
+                if (user != null) {
+                    serial += "-" + user;
+                }
+                addAvailableDevice(new RemoteAvdIDevice(serial, hostUsers.getKey(), user, i));
             }
         }
 
diff --git a/src/com/android/tradefed/device/LocalAndroidVirtualDevice.java b/src/com/android/tradefed/device/LocalAndroidVirtualDevice.java
index f027f48..f3809c8 100644
--- a/src/com/android/tradefed/device/LocalAndroidVirtualDevice.java
+++ b/src/com/android/tradefed/device/LocalAndroidVirtualDevice.java
@@ -26,7 +26,6 @@
 import com.android.tradefed.result.FileInputStreamSource;
 import com.android.tradefed.result.ITestLoggerReceiver;
 import com.android.tradefed.result.InputStreamSource;
-import com.android.tradefed.result.LogDataType;
 import com.android.tradefed.result.error.DeviceErrorIdentifier;
 import com.android.tradefed.result.error.InfraErrorIdentifier;
 import com.android.tradefed.targetprep.TargetSetupError;
@@ -49,7 +48,6 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
-import java.util.Map;
 
 /** The class for local virtual devices running on TradeFed host. */
 public class LocalAndroidVirtualDevice extends RemoteAndroidDevice implements ITestLoggerReceiver {
@@ -559,12 +557,15 @@
             CLog.e(ex);
             return;
         }
-        for (Map.Entry<String, LogDataType> log : mGceAvdInfo.getLogs().entrySet()) {
-            File file = new File(log.getKey());
+        for (GceAvdInfo.LogFileEntry log : mGceAvdInfo.getLogs()) {
+            File file = new File(log.path);
             if (file.exists()) {
                 try (InputStreamSource source = new FileInputStreamSource(file)) {
                     if (file.toPath().toRealPath().startsWith(realInstanceDir)) {
-                        mTestLogger.testLog(file.getName(), log.getValue(), source);
+                        mTestLogger.testLog(
+                                Strings.isNullOrEmpty(log.name) ? file.getName() : log.name,
+                                log.type,
+                                source);
                     } else {
                         CLog.w("%s is not in instance directory.", file.getAbsolutePath());
                     }
diff --git a/src/com/android/tradefed/device/ManagedDeviceList.java b/src/com/android/tradefed/device/ManagedDeviceList.java
index c67283d..de07e87 100644
--- a/src/com/android/tradefed/device/ManagedDeviceList.java
+++ b/src/com/android/tradefed/device/ManagedDeviceList.java
@@ -60,7 +60,18 @@
                     event = DeviceEvent.EXPLICIT_ALLOCATE_REQUEST;
                 }
                 DeviceEventResponse r = element.handleAllocationEvent(event);
-                return r.stateChanged && r.allocationState == DeviceAllocationState.Allocated;
+                boolean res =
+                        r.stateChanged && r.allocationState == DeviceAllocationState.Allocated;
+                if (!res) {
+                    mDeviceSelectionMatcher
+                            .getNoMatchReason()
+                            .put(
+                                    "already_allocated",
+                                    String.format(
+                                            "Device %s is matching but " + "already allocated.",
+                                            element.getIDevice().getSerialNumber()));
+                }
+                return res;
             }
             return false;
         }
diff --git a/src/com/android/tradefed/device/NativeDevice.java b/src/com/android/tradefed/device/NativeDevice.java
index 13719d4..4f691de 100644
--- a/src/com/android/tradefed/device/NativeDevice.java
+++ b/src/com/android/tradefed/device/NativeDevice.java
@@ -3556,6 +3556,15 @@
                 doAdbReboot(mode, null);
             }
 
+            // We want to wait on a command that verifies we've rebooted.
+            // However, it is possible to issue this command too quickly and get
+            // a response before the device has begun the reboot process (see
+            // b/242200753).
+            // While not as clean as we'd like, we wait 1.5 seconds before
+            // issuing any waiting commands, as devices generally take much
+            // longer than 1.5 seconds to reboot anyway.
+            getRunUtil().sleep(1500);
+
             if (RebootMode.REBOOT_INTO_FASTBOOTD.equals(mode)
                     && getHostOptions().isFastbootdEnable()) {
                 if (!mStateMonitor.waitForDeviceFastbootd(
diff --git a/src/com/android/tradefed/device/cloud/CommonLogRemoteFileUtil.java b/src/com/android/tradefed/device/cloud/CommonLogRemoteFileUtil.java
index a6070ad..e98b0ea 100644
--- a/src/com/android/tradefed/device/cloud/CommonLogRemoteFileUtil.java
+++ b/src/com/android/tradefed/device/cloud/CommonLogRemoteFileUtil.java
@@ -28,7 +28,7 @@
 import com.android.tradefed.util.IRunUtil;
 import com.android.tradefed.util.MultiMap;
 import com.android.tradefed.util.ZipUtil;
-
+import com.google.common.base.Strings;
 import java.io.File;
 import java.io.IOException;
 import java.util.ArrayList;
@@ -189,8 +189,7 @@
             CLog.e("GceAvdInfo was null, cannot collect remote files.");
             return;
         }
-        // Capture known extra files
-        List<KnownLogFileEntry> toFetch = KNOWN_FILES_TO_FETCH.get(options.getInstanceType());
+        List<KnownLogFileEntry> toFetch = null;
         if (options.useOxygen()) {
             // Override the list of logs to collect when the device is hosted by Oxygen service.
             toFetch = new ArrayList<>(OXYGEN_LOG_FILES);
@@ -198,10 +197,28 @@
                     gceAvd, options, runUtil, 60000, OXYGEN_CUTTLEFISH_LOG_DIR)) {
                 toFetch.addAll(OXYGEN_LOG_FILES_FALLBACK);
             }
+        } else {
+            boolean reported = false;
+            for (GceAvdInfo.LogFileEntry entry : gceAvd.getLogs()) {
+                if (logRemoteFile(
+                        testLogger,
+                        gceAvd,
+                        options,
+                        runUtil,
+                        entry.path,
+                        entry.type,
+                        Strings.isNullOrEmpty(entry.name) ? null : entry.name)) {
+                    reported = true;
+                }
+            }
+            if (!reported) {
+                CLog.i("GceAvdInfo does not contain logs. Fall back to known log files.");
+                toFetch = KNOWN_FILES_TO_FETCH.get(options.getInstanceType());
+            }
         }
         if (toFetch != null) {
             for (KnownLogFileEntry entry : toFetch) {
-                LogRemoteFile(
+                logRemoteFile(
                         testLogger,
                         gceAvd,
                         options,
@@ -218,7 +235,7 @@
         }
         for (String file : options.getRemoteFetchFilePattern()) {
             // TODO: Improve type of files.
-            LogRemoteFile(
+            logRemoteFile(
                     testLogger, gceAvd, options, runUtil, file, LogDataType.CUTTLEFISH_LOG, null);
         }
     }
@@ -354,8 +371,9 @@
      * @param logType The expected type of the pulled log.
      * @param baseName The base name that will be used to log the file, if null the actually file
      *     name will be used.
+     * @return whether the file is logged successfully.
      */
-    private static void LogRemoteFile(
+    private static boolean logRemoteFile(
             ITestLogger testLogger,
             GceAvdInfo gceAvd,
             TestDeviceOptions options,
@@ -363,7 +381,11 @@
             String fileToRetrieve,
             LogDataType logType,
             String baseName) {
-        GceManager.logNestedRemoteFile(
+        if (baseName != null && baseName.startsWith(TOMBSTONES_ZIP_NAME)) {
+            // TODO(b/154175542): Refactor fetchTombstones.
+            return false;
+        }
+        return GceManager.logNestedRemoteFile(
                 testLogger, gceAvd, options, runUtil, fileToRetrieve, logType, baseName);
     }
 }
diff --git a/src/com/android/tradefed/device/cloud/GceAvdInfo.java b/src/com/android/tradefed/device/cloud/GceAvdInfo.java
index efbe3d8..417f37e 100644
--- a/src/com/android/tradefed/device/cloud/GceAvdInfo.java
+++ b/src/com/android/tradefed/device/cloud/GceAvdInfo.java
@@ -41,13 +41,41 @@
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 /** Structure to hold relevant data for a given GCE AVD instance. */
 public class GceAvdInfo {
 
+    public static class LogFileEntry {
+        public final String path;
+        public final LogDataType type;
+        // The name is optional and defaults to an empty string.
+        public final String name;
+
+        @VisibleForTesting
+        LogFileEntry(String path, LogDataType type, String name) {
+            this.path = path;
+            this.type = type;
+            this.name = name;
+        }
+
+        LogFileEntry(JSONObject log) throws JSONException {
+            path = log.getString("path");
+            type = parseLogDataType(log.getString("type"));
+            name = log.optString("name", "");
+        }
+
+        private LogDataType parseLogDataType(String typeString) {
+            try {
+                return LogDataType.valueOf(typeString);
+            } catch (IllegalArgumentException e) {
+                CLog.w("Unknown log type in GCE AVD info: %s", typeString);
+                return LogDataType.UNKNOWN;
+            }
+        }
+    }
+
     public static final List<String> BUILD_VARS =
             Arrays.asList(
                     "build_id",
@@ -69,7 +97,7 @@
     private String mErrors;
     private GceStatus mStatus;
     private HashMap<String, String> mBuildVars;
-    private Map<String, LogDataType> mLogs;
+    private List<LogFileEntry> mLogs;
     private boolean mIsIpPreconfigured = false;
 
     public static enum GceStatus {
@@ -83,7 +111,7 @@
         mInstanceName = instanceName;
         mHostAndPort = hostAndPort;
         mBuildVars = new HashMap<String, String>();
-        mLogs = new HashMap<String, LogDataType>();
+        mLogs = new ArrayList<LogFileEntry>();
     }
 
     public GceAvdInfo(
@@ -137,7 +165,7 @@
     }
 
     /** Return the map from local or remote log paths to types. */
-    public Map<String, LogDataType> getLogs() {
+    public List<LogFileEntry> getLogs() {
         return mLogs;
     }
 
@@ -245,7 +273,7 @@
                                     errorId,
                                     errors,
                                     gceStatus);
-                    avdInfo.mLogs.putAll(parseLogField(d));
+                    avdInfo.mLogs.addAll(parseLogField(d));
                     for (String buildVar : BUILD_VARS) {
                         if (d.has(buildVar) && !d.getString(buildVar).trim().isEmpty()) {
                             avdInfo.addBuildVar(buildVar, d.getString(buildVar).trim());
@@ -376,29 +404,18 @@
      * Parse log paths from a device object.
      *
      * @param device the device object in JSON.
-     * @return a map from log paths to {@link LogDataType}.
+     * @return a list of {@link LogFileEntry}.
      * @throws JSONException if any required property is missing.
      */
-    private static Map<String, LogDataType> parseLogField(JSONObject device) throws JSONException {
-        Map<String, LogDataType> logs = new HashMap<String, LogDataType>();
+    private static List<LogFileEntry> parseLogField(JSONObject device) throws JSONException {
+        List<LogFileEntry> logs = new ArrayList<LogFileEntry>();
         JSONArray logArray = device.optJSONArray("logs");
         if (logArray == null) {
             return logs;
         }
         for (int i = 0; i < logArray.length(); i++) {
             JSONObject logObject = logArray.getJSONObject(i);
-            String path = logObject.getString("path");
-            String typeString = logObject.getString("type");
-            LogDataType type;
-            try {
-                type = LogDataType.valueOf(typeString);
-            } catch (IllegalArgumentException e) {
-                CLog.w("Unknown log type in GCE AVD info: %s", typeString);
-                type = LogDataType.UNKNOWN;
-            }
-            if (logs.put(path, type) != null) {
-                CLog.w("Repeated log path in GCE AVD info: %s", path);
-            }
+            logs.add(new LogFileEntry(logObject));
         }
         return logs;
     }
diff --git a/src/com/android/tradefed/device/cloud/GceManager.java b/src/com/android/tradefed/device/cloud/GceManager.java
index 5b6f696..584d558 100644
--- a/src/com/android/tradefed/device/cloud/GceManager.java
+++ b/src/com/android/tradefed/device/cloud/GceManager.java
@@ -822,15 +822,16 @@
      * @param runUtil a {@link IRunUtil} to execute commands.
      * @param remoteFilePath The remote path where to find the file.
      * @param type the {@link LogDataType} of the logged file.
+     * @return whether the file is logged successfully.
      */
-    public static void logNestedRemoteFile(
+    public static boolean logNestedRemoteFile(
             ITestLogger logger,
             GceAvdInfo gceAvd,
             TestDeviceOptions options,
             IRunUtil runUtil,
             String remoteFilePath,
             LogDataType type) {
-        logNestedRemoteFile(logger, gceAvd, options, runUtil, remoteFilePath, type, null);
+        return logNestedRemoteFile(logger, gceAvd, options, runUtil, remoteFilePath, type, null);
     }
 
     /**
@@ -845,8 +846,9 @@
      * @param type the {@link LogDataType} of the logged file.
      * @param baseName The base name to use to log the file. If null the actual file name will be
      *     used.
+     * @return whether the file is logged successfully.
      */
-    public static void logNestedRemoteFile(
+    public static boolean logNestedRemoteFile(
             ITestLogger logger,
             GceAvdInfo gceAvd,
             TestDeviceOptions options,
@@ -864,6 +866,7 @@
             if (remoteFile != null) {
                 // If we happened to fetch a directory, log all the subfiles
                 logDirectory(remoteFile, baseName, logger, type);
+                return true;
             }
         } else {
             remoteFile =
@@ -871,8 +874,10 @@
                             gceAvd, options, runUtil, REMOTE_FILE_OP_TIMEOUT, remoteFilePath);
             if (remoteFile != null) {
                 logFile(remoteFile, baseName, logger, type);
+                return true;
             }
         }
+        return false;
     }
 
     private static void logDirectory(
diff --git a/src/com/android/tradefed/device/cloud/RemoteAndroidVirtualDevice.java b/src/com/android/tradefed/device/cloud/RemoteAndroidVirtualDevice.java
index 6bb5160..cb4eceb 100644
--- a/src/com/android/tradefed/device/cloud/RemoteAndroidVirtualDevice.java
+++ b/src/com/android/tradefed/device/cloud/RemoteAndroidVirtualDevice.java
@@ -19,6 +19,7 @@
 import com.android.ddmlib.IDevice.DeviceState;
 import com.android.tradefed.build.IBuildInfo;
 import com.android.tradefed.command.remote.DeviceDescriptor;
+import com.android.tradefed.config.GlobalConfiguration;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.DeviceUnresponsiveException;
 import com.android.tradefed.device.IDeviceMonitor;
@@ -29,6 +30,7 @@
 import com.android.tradefed.device.TestDeviceOptions;
 import com.android.tradefed.device.TestDeviceOptions.InstanceType;
 import com.android.tradefed.device.cloud.GceAvdInfo.GceStatus;
+import com.android.tradefed.host.IHostOptions.PermitLimitType;
 import com.android.tradefed.invoker.logger.InvocationMetricLogger;
 import com.android.tradefed.invoker.logger.InvocationMetricLogger.InvocationMetricKey;
 import com.android.tradefed.log.ITestLogger;
@@ -54,6 +56,7 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
+import java.util.concurrent.TimeUnit;
 
 import javax.annotation.Nullable;
 
@@ -105,8 +108,30 @@
 
             // Launch GCE helper script.
             long startTime = getCurrentTime();
-            launchGce(info, attributes);
-            long remainingTime = getOptions().getGceCmdTimeout() - (getCurrentTime() - startTime);
+            long remainingTime = 0;
+            try {
+                if (GlobalConfiguration.getInstance().getHostOptions().getConcurrentFlasherLimit()
+                        != null) {
+                    GlobalConfiguration.getInstance()
+                            .getHostOptions()
+                            .takePermit(PermitLimitType.CONCURRENT_FLASHER);
+                    long queueTime = System.currentTimeMillis() - startTime;
+                    CLog.v(
+                            "Fetch and launch CVD permit obtained after %ds",
+                            TimeUnit.MILLISECONDS.toSeconds(queueTime));
+                }
+                launchGce(info, attributes);
+                remainingTime =
+                        getOptions().getGceCmdTimeout()
+                                - (getCurrentTime() - startTime);
+            } finally {
+                if (GlobalConfiguration.getInstance().getHostOptions().getConcurrentFlasherLimit()
+                        != null) {
+                    GlobalConfiguration.getInstance()
+                            .getHostOptions()
+                            .returnPermit(PermitLimitType.CONCURRENT_FLASHER);
+                }
+            }
             if (remainingTime < 0) {
                 throw new DeviceNotAvailableException(
                         String.format(
diff --git a/src/com/android/tradefed/device/metric/JavaCodeCoverageCollector.java b/src/com/android/tradefed/device/metric/JavaCodeCoverageCollector.java
index 653086f..993620b 100644
--- a/src/com/android/tradefed/device/metric/JavaCodeCoverageCollector.java
+++ b/src/com/android/tradefed/device/metric/JavaCodeCoverageCollector.java
@@ -21,7 +21,6 @@
 
 import com.android.tradefed.config.IConfiguration;
 import com.android.tradefed.config.IConfigurationReceiver;
-import com.android.tradefed.config.Option;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.invoker.IInvocationContext;
@@ -43,6 +42,8 @@
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Splitter;
 
+import org.jacoco.core.tools.ExecFileLoader;
+
 import java.io.BufferedOutputStream;
 import java.io.File;
 import java.io.FileOutputStream;
@@ -71,13 +72,7 @@
     // Timeout for pulling coverage files from the device, in minutes.
     private static final long TIMEOUT_MINUTES = 20;
 
-    @Deprecated
-    @Option(
-            name = "merge-coverage-measurements",
-            description =
-                    "Merge coverage measurements after all tests are complete rather than logging"
-                            + " individual measurements.")
-    private boolean mMergeCoverageMeasurements = false;
+    private ExecFileLoader mExecFileLoader;
 
     private JavaCodeCoverageFlusher mFlusher;
     private IConfiguration mConfiguration;
@@ -117,15 +112,10 @@
     }
 
     @VisibleForTesting
-    public void setCoverageFlusher(JavaCodeCoverageFlusher flusher) {
+    void setCoverageFlusher(JavaCodeCoverageFlusher flusher) {
         mFlusher = flusher;
     }
 
-    @VisibleForTesting
-    public void setMergeMeasurements(boolean merge) {
-        mMergeCoverageMeasurements = merge;
-    }
-
     @Override
     public void onTestRunEnd(DeviceMetricData runData, final Map<String, Metric> runMetrics)
             throws DeviceNotAvailableException {
@@ -166,7 +156,7 @@
                                 "Failed to pull test coverage file %s from the device.",
                                 testCoveragePath);
                     } else {
-                        logCoverageMeasurement(testCoverage);
+                        saveCoverageMeasurement(testCoverage);
                     }
                 }
 
@@ -192,7 +182,7 @@
                 // Decompress the files and log the measurements.
                 untarDir = TarUtil.extractTarGzipToTemp(coverageTarGz, "java_coverage");
                 for (String coveragePath : FileUtil.findFiles(untarDir, ".*\\.ec")) {
-                    logCoverageMeasurement(new File(coveragePath));
+                    saveCoverageMeasurement(new File(coveragePath));
                 }
             } catch (IOException e) {
                 throw new RuntimeException(e);
@@ -206,9 +196,36 @@
                 cleanUpDeviceCoverageFiles(device);
             }
         }
+
+        // Log the merged coverage data file if the flag is set.
+        if (shouldMergeCoverage() && (mExecFileLoader != null)) {
+            File mergedCoverage = null;
+            try {
+                mergedCoverage = FileUtil.createTempFile("merged_java_coverage", ".ec");
+                mExecFileLoader.save(mergedCoverage, false);
+                logCoverageMeasurement(mergedCoverage);
+            } catch (IOException e) {
+                throw new RuntimeException(e);
+            } finally {
+                mExecFileLoader = null;
+                FileUtil.deleteFile(mergedCoverage);
+            }
+        }
     }
 
-    /** Saves files as Java coverage measurements. */
+    /** Saves Java coverage file data. */
+    private void saveCoverageMeasurement(File coverageFile) throws IOException {
+        if (shouldMergeCoverage()) {
+            if (mExecFileLoader == null) {
+                mExecFileLoader = new ExecFileLoader();
+            }
+            mExecFileLoader.load(coverageFile);
+        } else {
+            logCoverageMeasurement(coverageFile);
+        }
+    }
+
+    /** Logs files as Java coverage measurements. */
     private void logCoverageMeasurement(File coverageFile) {
         try (FileInputStreamSource source = new FileInputStreamSource(coverageFile, true)) {
             testLog(
@@ -264,4 +281,8 @@
                         .getCoverageToolchains()
                         .contains(CoverageOptions.Toolchain.JACOCO);
     }
+
+    private boolean shouldMergeCoverage() {
+        return mConfiguration != null && mConfiguration.getCoverageOptions().shouldMergeCoverage();
+    }
 }
diff --git a/src/com/android/tradefed/invoker/ITestInvocation.java b/src/com/android/tradefed/invoker/ITestInvocation.java
index a3d6dbd..1396547 100644
--- a/src/com/android/tradefed/invoker/ITestInvocation.java
+++ b/src/com/android/tradefed/invoker/ITestInvocation.java
@@ -47,8 +47,16 @@
      * Notify the {@link TestInvocation} that TradeFed has been requested to stop.
      *
      * @param message The message associated with stopping the invocation
+     * @param errorId Identifier associated with the forced stop
      */
-    public default void notifyInvocationStopped(String message, ErrorIdentifier errorId) {}
+    public default void notifyInvocationForceStopped(String message, ErrorIdentifier errorId) {}
+
+    /**
+     * Notify the {@link TestInvocation} that TradeFed will eventually shutdown.
+     *
+     * @param message The message associated with stopping the invocation
+     */
+    public default void notifyInvocationStopped(String message) {}
 
     /** The exit information of the given invocation. */
     public default ExitInformation getExitInfo() {
diff --git a/src/com/android/tradefed/invoker/InvocationExecution.java b/src/com/android/tradefed/invoker/InvocationExecution.java
index f1920b9..a928e07d 100644
--- a/src/com/android/tradefed/invoker/InvocationExecution.java
+++ b/src/com/android/tradefed/invoker/InvocationExecution.java
@@ -25,6 +25,7 @@
 import com.android.tradefed.build.IBuildProvider;
 import com.android.tradefed.build.IDeviceBuildInfo;
 import com.android.tradefed.build.IDeviceBuildProvider;
+import com.android.tradefed.command.ICommandOptions;
 import com.android.tradefed.config.GlobalConfiguration;
 import com.android.tradefed.config.IConfiguration;
 import com.android.tradefed.config.IConfigurationReceiver;
@@ -48,6 +49,7 @@
 import com.android.tradefed.invoker.logger.TfObjectTracker;
 import com.android.tradefed.invoker.shard.IShardHelper;
 import com.android.tradefed.invoker.shard.TestsPoolPoller;
+import com.android.tradefed.invoker.tracing.CloseableTraceScope;
 import com.android.tradefed.log.ITestLogger;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.result.ByteArrayInputStreamSource;
@@ -191,6 +193,7 @@
             throw re;
         }
         setBinariesVersion(testInfo.getContext());
+        copyRemoteFiles(config.getCommandOptions(), testInfo.getBuildInfo());
         return true;
     }
 
@@ -259,7 +262,8 @@
         mTrackLabPreparers = new ConcurrentHashMap<>();
         mTrackTargetPreparers = new ConcurrentHashMap<>();
         InvocationMetricLogger.addInvocationMetrics(InvocationMetricKey.SETUP_START, start);
-        try {
+        try (CloseableTraceScope ignored =
+                new CloseableTraceScope(InvocationMetricKey.lab_setup.name())) {
             for (String deviceName : testInfo.getContext().getDeviceConfigNames()) {
                 ITestDevice device = testInfo.getContext().getDevice(deviceName);
                 CLog.d("Starting setup for device: '%s'", device.getSerialNumber());
@@ -276,7 +280,16 @@
                     listener,
                     testInfo,
                     "multi pre target preparer setup");
-
+            runLabPreparersSetup(testInfo, config, listener);
+        } finally {
+            long end = System.currentTimeMillis();
+            InvocationMetricLogger.addInvocationMetrics(InvocationMetricKey.SETUP_END, end);
+            InvocationMetricLogger.addInvocationPairMetrics(
+                    InvocationMetricKey.SETUP_PAIR, start, end);
+        }
+        long startPreparer = System.currentTimeMillis();
+        try (CloseableTraceScope ignored =
+                new CloseableTraceScope(InvocationMetricKey.test_setup.name())) {
             runPreparersSetup(testInfo, config, listener);
 
             // After all the individual setup, make the multi-devices setup
@@ -291,12 +304,11 @@
             // Note: These metrics are handled in a try in case of a kernel reset or device issue.
             // Setup timing metric. It does not include flashing time on boot tests.
             long end = System.currentTimeMillis();
-            InvocationMetricLogger.addInvocationMetrics(InvocationMetricKey.SETUP_END, end);
             InvocationMetricLogger.addInvocationPairMetrics(
-                    InvocationMetricKey.SETUP_PAIR, start, end);
+                    InvocationMetricKey.TEST_SETUP_PAIR, startPreparer, end);
             long setupDuration = end - start;
             InvocationMetricLogger.addInvocationMetrics(InvocationMetricKey.SETUP, setupDuration);
-            CLog.d("Setup duration: %s'", TimeUtil.formatElapsedTime(setupDuration));
+            CLog.d("Total setup duration: %s'", TimeUtil.formatElapsedTime(setupDuration));
             // Upload the setup logcat after setup is complete.
             for (ITestDevice device : testInfo.getDevices()) {
                 reportLogs(device, listener, Stage.SETUP);
@@ -304,7 +316,7 @@
         }
     }
 
-    private void runPreparersSetup(
+    private void runLabPreparersSetup(
             TestInformation testInfo, IConfiguration config, ITestLogger listener)
             throws TargetSetupError, BuildError, DeviceNotAvailableException {
         int index = 0;
@@ -330,13 +342,6 @@
                                     getLabPreparersToRun(config, deviceName),
                                     mTrackLabPreparers.get(deviceName),
                                     listener);
-                            runPreparationOnDevice(
-                                    replicated,
-                                    deviceName,
-                                    deviceIndex,
-                                    getTargetPreparersToRun(config, deviceName),
-                                    mTrackTargetPreparers.get(deviceName),
-                                    listener);
                             return true;
                         };
                 callableTasks.add(callableTask);
@@ -370,6 +375,61 @@
                         getLabPreparersToRun(config, deviceName),
                         mTrackLabPreparers.get(deviceName),
                         listener);
+                index++;
+            }
+        }
+    }
+
+    private void runPreparersSetup(
+            TestInformation testInfo, IConfiguration config, ITestLogger listener)
+            throws TargetSetupError, BuildError, DeviceNotAvailableException {
+        int index = 0;
+        if ((config.getCommandOptions().shouldUseParallelSetup()
+                        || config.getCommandOptions().shouldUseReplicateSetup())
+                && config.getDeviceConfig().size() > 1) {
+            CLog.d("Using parallel setup.");
+            ParallelDeviceExecutor<Boolean> executor =
+                    new ParallelDeviceExecutor<>(testInfo.getContext().getDevices().size());
+            List<Callable<Boolean>> callableTasks = new ArrayList<>();
+            for (String deviceName : testInfo.getContext().getDeviceConfigNames()) {
+                final int deviceIndex = index;
+                // Replicate TestInfo
+                TestInformation replicated =
+                        TestInformation.createModuleTestInfo(testInfo, testInfo.getContext());
+                Callable<Boolean> callableTask =
+                        () -> {
+                            runPreparationOnDevice(
+                                    replicated,
+                                    deviceName,
+                                    deviceIndex,
+                                    getTargetPreparersToRun(config, deviceName),
+                                    mTrackTargetPreparers.get(deviceName),
+                                    listener);
+                            return true;
+                        };
+                callableTasks.add(callableTask);
+                index++;
+            }
+            Duration timeout = config.getCommandOptions().getParallelSetupTimeout();
+            executor.invokeAll(callableTasks, timeout.toMillis(), TimeUnit.MILLISECONDS);
+            if (executor.hasErrors()) {
+                List<Throwable> errors = executor.getErrors();
+                // TODO: Handle throwing multi-exceptions, right now throw the first one.
+                for (Throwable error : errors) {
+                    if (error instanceof TargetSetupError) {
+                        throw (TargetSetupError) error;
+                    }
+                    if (error instanceof BuildError) {
+                        throw (BuildError) error;
+                    }
+                    if (error instanceof DeviceNotAvailableException) {
+                        throw (DeviceNotAvailableException) error;
+                    }
+                    throw new RuntimeException(error);
+                }
+            }
+        } else {
+            for (String deviceName : testInfo.getContext().getDeviceConfigNames()) {
                 runPreparationOnDevice(
                         testInfo,
                         deviceName,
@@ -414,7 +474,8 @@
             CLog.d(
                     "starting lab preparer '%s' on device: '%s'",
                     preparer, device.getSerialNumber());
-            try {
+            try (CloseableTraceScope ignore =
+                    new CloseableTraceScope(preparer.getClass().getSimpleName())) {
                 testInfo.setActiveDeviceIndex(index);
                 preparer.setUp(testInfo);
             } finally {
@@ -464,7 +525,8 @@
 
             long startTime = System.currentTimeMillis();
             CLog.d("starting preparer '%s' on device: '%s'", preparer, device.getSerialNumber());
-            try {
+            try (CloseableTraceScope ignore =
+                    new CloseableTraceScope(preparer.getClass().getSimpleName())) {
                 testInfo.setActiveDeviceIndex(index);
                 preparer.setUp(testInfo);
             } finally {
@@ -497,15 +559,18 @@
             return;
         }
         long start = System.currentTimeMillis();
-        customizeDevicePreInvocation(config, context);
-        for (String deviceName : context.getDeviceConfigNames()) {
-            ITestDevice device = context.getDevice(deviceName);
+        try (CloseableTraceScope ignore = new CloseableTraceScope("device_pre_invocation_setup")) {
+            customizeDevicePreInvocation(config, context);
+            for (String deviceName : context.getDeviceConfigNames()) {
+                ITestDevice device = context.getDevice(deviceName);
 
-            CLog.d("Starting device pre invocation setup for : '%s'", device.getSerialNumber());
-            if (device instanceof ITestLoggerReceiver) {
-                ((ITestLoggerReceiver) context.getDevice(deviceName)).setTestLogger(logger);
+                CLog.d("Starting device pre invocation setup for : '%s'", device.getSerialNumber());
+                if (device instanceof ITestLoggerReceiver) {
+                    ((ITestLoggerReceiver) context.getDevice(deviceName)).setTestLogger(logger);
+                }
+                device.preInvocationSetup(
+                        context.getBuildInfo(deviceName), context.getAttributes());
             }
-            device.preInvocationSetup(context.getBuildInfo(deviceName), context.getAttributes());
         }
         // Also report device pre invocation into setup
         InvocationMetricLogger.addInvocationPairMetrics(
@@ -531,14 +596,10 @@
             CLog.i("--disable-invocation-setup-and-teardown, skipping post-invocation teardown.");
             return;
         }
-        long start = System.currentTimeMillis();
         for (String deviceName : context.getDeviceConfigNames()) {
             ITestDevice device = context.getDevice(deviceName);
             device.postInvocationTearDown(exception);
         }
-        // Also report device post invocation into teardown
-        InvocationMetricLogger.addInvocationPairMetrics(
-                InvocationMetricKey.TEARDOWN_PAIR, start, System.currentTimeMillis());
     }
 
     /** Runs the {@link IMultiTargetPreparer} specified. */
@@ -631,82 +692,83 @@
         Throwable deferredThrowable;
         long start = System.currentTimeMillis();
         InvocationMetricLogger.addInvocationMetrics(InvocationMetricKey.TEARDOWN_START, start);
-
-        List<IMultiTargetPreparer> multiPreparers = config.getMultiTargetPreparers();
-        deferredThrowable =
-                runMultiTargetPreparersTearDown(
-                        multiPreparers,
-                        testInfo,
-                        logger,
-                        exception,
-                        "multi target preparer teardown");
-
-        int deviceIndex = 0;
-        for (String deviceName : context.getDeviceConfigNames()) {
-            ITestDevice device = context.getDevice(deviceName);
-            device.clearLastConnectedWifiNetwork();
-
-            List<ITargetPreparer> targetPreparersToRun =
-                    getTargetPreparersToRun(config, deviceName);
-            Throwable firstLocalThrowable =
-                    runPreparersTearDown(
+        try {
+            List<IMultiTargetPreparer> multiPreparers = config.getMultiTargetPreparers();
+            deferredThrowable =
+                    runMultiTargetPreparersTearDown(
+                            multiPreparers,
                             testInfo,
-                            device,
-                            deviceName,
-                            deviceIndex,
                             logger,
                             exception,
-                            targetPreparersToRun,
-                            mTrackTargetPreparers);
-            if (deferredThrowable == null) {
-                deferredThrowable = firstLocalThrowable;
+                            "multi target preparer teardown");
+
+            int deviceIndex = 0;
+            for (String deviceName : context.getDeviceConfigNames()) {
+                ITestDevice device = context.getDevice(deviceName);
+                device.clearLastConnectedWifiNetwork();
+
+                List<ITargetPreparer> targetPreparersToRun =
+                        getTargetPreparersToRun(config, deviceName);
+                Throwable firstLocalThrowable =
+                        runPreparersTearDown(
+                                testInfo,
+                                device,
+                                deviceName,
+                                deviceIndex,
+                                logger,
+                                exception,
+                                targetPreparersToRun,
+                                mTrackTargetPreparers);
+                if (deferredThrowable == null) {
+                    deferredThrowable = firstLocalThrowable;
+                }
+
+                List<ITargetPreparer> labPreparersToRun = getLabPreparersToRun(config, deviceName);
+                Throwable secondLocalThrowable =
+                        runPreparersTearDown(
+                                testInfo,
+                                device,
+                                deviceName,
+                                deviceIndex,
+                                logger,
+                                exception,
+                                labPreparersToRun,
+                                mTrackLabPreparers);
+                if (deferredThrowable == null) {
+                    deferredThrowable = secondLocalThrowable;
+                }
+
+                deviceIndex++;
             }
 
-            List<ITargetPreparer> labPreparersToRun = getLabPreparersToRun(config, deviceName);
-            Throwable secondLocalThrowable =
-                    runPreparersTearDown(
+            // Extra tear down step for the device
+            if (exception == null) {
+                exception = deferredThrowable;
+            }
+            runDevicePostInvocationTearDown(context, config, exception);
+
+            // After all, run the multi_pre_target_preparer tearDown.
+            List<IMultiTargetPreparer> multiPrePreparers = config.getMultiPreTargetPreparers();
+            Throwable preTargetTearDownException =
+                    runMultiTargetPreparersTearDown(
+                            multiPrePreparers,
                             testInfo,
-                            device,
-                            deviceName,
-                            deviceIndex,
                             logger,
                             exception,
-                            labPreparersToRun,
-                            mTrackLabPreparers);
+                            "multi pre target preparer teardown");
             if (deferredThrowable == null) {
-                deferredThrowable = secondLocalThrowable;
+                deferredThrowable = preTargetTearDownException;
             }
 
-            deviceIndex++;
+            // Collect adb logs.
+            logHostAdb(config, logger);
+        } finally {
+            long end = System.currentTimeMillis();
+            InvocationMetricLogger.addInvocationMetrics(InvocationMetricKey.TEARDOWN_END, end);
+            InvocationMetricLogger.addInvocationPairMetrics(
+                    InvocationMetricKey.TEARDOWN_PAIR, start, end);
         }
 
-        // Extra tear down step for the device
-        if (exception == null) {
-            exception = deferredThrowable;
-        }
-        runDevicePostInvocationTearDown(context, config, exception);
-
-        // After all, run the multi_pre_target_preparer tearDown.
-        List<IMultiTargetPreparer> multiPrePreparers = config.getMultiPreTargetPreparers();
-        Throwable preTargetTearDownException =
-                runMultiTargetPreparersTearDown(
-                        multiPrePreparers,
-                        testInfo,
-                        logger,
-                        exception,
-                        "multi pre target preparer teardown");
-        if (deferredThrowable == null) {
-            deferredThrowable = preTargetTearDownException;
-        }
-
-        // Collect adb logs.
-        logHostAdb(config, logger);
-
-        long end = System.currentTimeMillis();
-        InvocationMetricLogger.addInvocationMetrics(InvocationMetricKey.TEARDOWN_END, end);
-        InvocationMetricLogger.addInvocationPairMetrics(
-                InvocationMetricKey.TEARDOWN_PAIR, start, end);
-
         if (deferredThrowable != null) {
             throw deferredThrowable;
         }
@@ -827,118 +889,123 @@
         Runtime.getRuntime().addShutdownHook(reporterThread);
         TestInvocation.printStageDelimiter(Stage.TEST, false);
         long start = System.currentTimeMillis();
-        try {
+        try (CloseableTraceScope ignored =
+                new CloseableTraceScope(InvocationMetricKey.test_execution.name())) {
             GetPreviousPassedHelper previousPassHelper = new GetPreviousPassedHelper();
             // Add new exclude filters to global filters
             Set<String> previousPassedFilters = previousPassHelper.getPreviousPassedFilters(config);
             // TODO: Ensure global filters are cloned for local sharding
             config.getGlobalFilters().addPreviousPassedTests(previousPassedFilters);
             for (IRemoteTest test : config.getTests()) {
-                TfObjectTracker.countWithParents(test.getClass());
-                // For compatibility of those receivers, they are assumed to be single device alloc.
-                if (test instanceof IDeviceTest) {
-                    ((IDeviceTest) test).setDevice(info.getDevice());
-                }
-                if (test instanceof IBuildReceiver) {
-                    ((IBuildReceiver) test).setBuild(info.getBuildInfo());
-                }
-                if (test instanceof ISystemStatusCheckerReceiver) {
-                    ((ISystemStatusCheckerReceiver) test)
-                            .setSystemStatusChecker(config.getSystemStatusCheckers());
-                }
-                if (test instanceof IInvocationContextReceiver) {
-                    ((IInvocationContextReceiver) test).setInvocationContext(info.getContext());
-                }
+                try (CloseableTraceScope remoteTest =
+                        new CloseableTraceScope(test.getClass().getSimpleName())) {
+                    TfObjectTracker.countWithParents(test.getClass());
+                    // For compatibility of those receivers, they are assumed to be single device
+                    // alloc.
+                    if (test instanceof IDeviceTest) {
+                        ((IDeviceTest) test).setDevice(info.getDevice());
+                    }
+                    if (test instanceof IBuildReceiver) {
+                        ((IBuildReceiver) test).setBuild(info.getBuildInfo());
+                    }
+                    if (test instanceof ISystemStatusCheckerReceiver) {
+                        ((ISystemStatusCheckerReceiver) test)
+                                .setSystemStatusChecker(config.getSystemStatusCheckers());
+                    }
+                    if (test instanceof IInvocationContextReceiver) {
+                        ((IInvocationContextReceiver) test).setInvocationContext(info.getContext());
+                    }
 
-                updateAutoCollectors(config);
+                    updateAutoCollectors(config);
 
-                IRetryDecision decision = config.getRetryDecision();
-                // Apply the filters
-                if (test instanceof ITestFilterReceiver) {
-                    config.getGlobalFilters().applyFiltersToTest((ITestFilterReceiver) test);
-                } else if (test instanceof BaseTestSuite) {
-                    config.getGlobalFilters().applyFiltersToTest((BaseTestSuite) test);
-                }
-                // Handle the no-retry use case
-                if (!decision.isAutoRetryEnabled()
-                        || RetryStrategy.NO_RETRY.equals(decision.getRetryStrategy())
-                        || test instanceof ITestSuite
-                        // TODO: Handle auto-retry in local-sharding for non-suite
-                        || test instanceof TestsPoolPoller
-                        // If test doesn't support auto-retry
-                        || (!(test instanceof ITestFilterReceiver)
-                                && !(test instanceof IAutoRetriableTest))) {
+                    IRetryDecision decision = config.getRetryDecision();
+                    // Apply the filters
+                    if (test instanceof ITestFilterReceiver) {
+                        config.getGlobalFilters().applyFiltersToTest((ITestFilterReceiver) test);
+                    } else if (test instanceof BaseTestSuite) {
+                        config.getGlobalFilters().applyFiltersToTest((BaseTestSuite) test);
+                    }
+                    // Handle the no-retry use case
+                    if (!decision.isAutoRetryEnabled()
+                            || RetryStrategy.NO_RETRY.equals(decision.getRetryStrategy())
+                            || test instanceof ITestSuite
+                            // TODO: Handle auto-retry in local-sharding for non-suite
+                            || test instanceof TestsPoolPoller
+                            // If test doesn't support auto-retry
+                            || (!(test instanceof ITestFilterReceiver)
+                                    && !(test instanceof IAutoRetriableTest))) {
+                        try {
+                            runTest(config, info, listener, test);
+                        } finally {
+                            CurrentInvocation.setRunIsolation(IsolationGrade.NOT_ISOLATED);
+                            CurrentInvocation.setModuleIsolation(IsolationGrade.NOT_ISOLATED);
+                        }
+                        remainingTests.remove(test);
+                        continue;
+                    }
+                    CLog.d("Using RetryLogSaverResultForwarder to forward results.");
+                    ModuleListener mainGranularRunListener = new ModuleListener(null);
+                    RetryLogSaverResultForwarder runListener =
+                            initializeListeners(config, listener, mainGranularRunListener);
+                    mainGranularRunListener.setAttemptIsolation(
+                            CurrentInvocation.runCurrentIsolation());
                     try {
-                        runTest(config, info, listener, test);
+                        runTest(config, info, runListener, test);
                     } finally {
                         CurrentInvocation.setRunIsolation(IsolationGrade.NOT_ISOLATED);
                         CurrentInvocation.setModuleIsolation(IsolationGrade.NOT_ISOLATED);
                     }
                     remainingTests.remove(test);
-                    continue;
-                }
-                CLog.d("Using RetryLogSaverResultForwarder to forward results.");
-                ModuleListener mainGranularRunListener = new ModuleListener(null);
-                RetryLogSaverResultForwarder runListener =
-                        initializeListeners(config, listener, mainGranularRunListener);
-                mainGranularRunListener.setAttemptIsolation(
-                        CurrentInvocation.runCurrentIsolation());
-                try {
-                    runTest(config, info, runListener, test);
-                } finally {
-                    CurrentInvocation.setRunIsolation(IsolationGrade.NOT_ISOLATED);
-                    CurrentInvocation.setModuleIsolation(IsolationGrade.NOT_ISOLATED);
-                }
-                remainingTests.remove(test);
-                runListener.incrementAttempt();
+                    runListener.incrementAttempt();
 
-                // Avoid entering the loop if no retry to be done.
-                if (!decision.shouldRetry(
-                        test, 0, mainGranularRunListener.getTestRunForAttempts(0))) {
-                    continue;
-                }
-                // Avoid rechecking the shouldRetry below the first time as it could retrigger
-                // reboot.
-                boolean firstCheck = true;
-                long startTime = System.currentTimeMillis();
-                try {
-                    PrettyPrintDelimiter.printStageDelimiter("Starting auto-retry");
-                    for (int attemptNumber = 1;
-                            attemptNumber < decision.getMaxRetryCount();
-                            attemptNumber++) {
-                        if (!firstCheck) {
-                            boolean retry =
-                                    decision.shouldRetry(
-                                            test,
-                                            attemptNumber - 1,
-                                            mainGranularRunListener.getTestRunForAttempts(
-                                                    attemptNumber - 1));
-                            if (!retry) {
-                                continue;
-                            }
-                        }
-                        firstCheck = false;
-                        CLog.d("auto-retry attempt number '%s'", attemptNumber);
-                        mainGranularRunListener.setAttemptIsolation(
-                                CurrentInvocation.runCurrentIsolation());
-                        try {
-                            // Run the tests again
-                            runTest(config, info, runListener, test);
-                        } finally {
-                            CurrentInvocation.setRunIsolation(IsolationGrade.NOT_ISOLATED);
-                            CurrentInvocation.setModuleIsolation(IsolationGrade.NOT_ISOLATED);
-                        }
-                        runListener.incrementAttempt();
+                    // Avoid entering the loop if no retry to be done.
+                    if (!decision.shouldRetry(
+                            test, 0, mainGranularRunListener.getTestRunForAttempts(0))) {
+                        continue;
                     }
-                    // Feed the last attempt if we reached here.
-                    decision.addLastAttempt(
-                            mainGranularRunListener.getTestRunForAttempts(
-                                    decision.getMaxRetryCount() - 1));
-                } finally {
-                    RetryStatistics retryStats = decision.getRetryStatistics();
-                    // Track how long we spend in retry
-                    retryStats.mRetryTime = System.currentTimeMillis() - startTime;
-                    addRetryTime(retryStats.mRetryTime);
+                    // Avoid rechecking the shouldRetry below the first time as it could retrigger
+                    // reboot.
+                    boolean firstCheck = true;
+                    long startTime = System.currentTimeMillis();
+                    try {
+                        PrettyPrintDelimiter.printStageDelimiter("Starting auto-retry");
+                        for (int attemptNumber = 1;
+                                attemptNumber < decision.getMaxRetryCount();
+                                attemptNumber++) {
+                            if (!firstCheck) {
+                                boolean retry =
+                                        decision.shouldRetry(
+                                                test,
+                                                attemptNumber - 1,
+                                                mainGranularRunListener.getTestRunForAttempts(
+                                                        attemptNumber - 1));
+                                if (!retry) {
+                                    continue;
+                                }
+                            }
+                            firstCheck = false;
+                            CLog.d("auto-retry attempt number '%s'", attemptNumber);
+                            mainGranularRunListener.setAttemptIsolation(
+                                    CurrentInvocation.runCurrentIsolation());
+                            try {
+                                // Run the tests again
+                                runTest(config, info, runListener, test);
+                            } finally {
+                                CurrentInvocation.setRunIsolation(IsolationGrade.NOT_ISOLATED);
+                                CurrentInvocation.setModuleIsolation(IsolationGrade.NOT_ISOLATED);
+                            }
+                            runListener.incrementAttempt();
+                        }
+                        // Feed the last attempt if we reached here.
+                        decision.addLastAttempt(
+                                mainGranularRunListener.getTestRunForAttempts(
+                                        decision.getMaxRetryCount() - 1));
+                    } finally {
+                        RetryStatistics retryStats = decision.getRetryStatistics();
+                        // Track how long we spend in retry
+                        retryStats.mRetryTime = System.currentTimeMillis() - startTime;
+                        addRetryTime(retryStats.mRetryTime);
+                    }
                 }
             }
         } finally {
@@ -1188,6 +1255,15 @@
         }
     }
 
+    private void copyRemoteFiles(ICommandOptions options, IBuildInfo info) {
+        for (String remoteFile : options.getRemoteFiles()) {
+            info.setFile(
+                    IBuildInfo.REMOTE_FILE_PREFIX,
+                    new File(remoteFile),
+                    IBuildInfo.REMOTE_FILE_VERSION);
+        }
+    }
+
     /** Convert the legacy *-on-failure options to the new auto-collect. */
     private void updateAutoCollectors(IConfiguration config) {
         if (config.getCommandOptions().captureScreenshotOnFailure()) {
diff --git a/src/com/android/tradefed/invoker/RemoteInvocationExecution.java b/src/com/android/tradefed/invoker/RemoteInvocationExecution.java
index 555667e..0706df9 100644
--- a/src/com/android/tradefed/invoker/RemoteInvocationExecution.java
+++ b/src/com/android/tradefed/invoker/RemoteInvocationExecution.java
@@ -258,7 +258,7 @@
                 globalConfig =
                         GlobalConfiguration.getInstance()
                                 .cloneConfigWithFilter(
-                                        new HashSet<>(), fileTransformer, allowListConfigs);
+                                        new HashSet<>(), fileTransformer, true, allowListConfigs);
             } catch (IOException e) {
                 listener.invocationFailed(createInvocationFailure(e, FailureStatus.INFRA_FAILURE));
                 return;
diff --git a/src/com/android/tradefed/invoker/TestInvocation.java b/src/com/android/tradefed/invoker/TestInvocation.java
index b918149..ee0689a 100644
--- a/src/com/android/tradefed/invoker/TestInvocation.java
+++ b/src/com/android/tradefed/invoker/TestInvocation.java
@@ -60,6 +60,7 @@
 import com.android.tradefed.invoker.sandbox.SandboxedInvocationExecution;
 import com.android.tradefed.invoker.shard.LastShardDetector;
 import com.android.tradefed.invoker.shard.ShardHelper;
+import com.android.tradefed.invoker.tracing.CloseableTraceScope;
 import com.android.tradefed.log.BaseLeveledLogOutput;
 import com.android.tradefed.log.ILeveledLogOutput;
 import com.android.tradefed.log.ILogRegistry;
@@ -187,7 +188,13 @@
     private String mStopCause = null;
     private ErrorIdentifier mStopErrorId = null;
     private Long mStopRequestTime = null;
+    private Long mSoftStopRequestTime = null;
+    private boolean mShutdownBeforeTest = false;
     private boolean mTestStarted = false;
+    private boolean mTestDone = false;
+    private boolean mTestsNotRan = false;
+    private boolean mForcedStopRequestedAfterTest = false;
+
     private boolean mInvocationFailed = false;
     private boolean mDelegatedInvocation = false;
     private List<IScheduledInvocationListener> mSchedulerListeners = new ArrayList<>();
@@ -329,6 +336,8 @@
                 throw t;
             }
         } finally {
+            mTestDone = true;
+            long bugreportStartTime = System.currentTimeMillis();
             // Only capture logcat for TEST if we started the test phase.
             if (mTestStarted) {
                 for (ITestDevice device : context.getDevices()) {
@@ -347,51 +356,64 @@
                     }
                 }
                 if (bugreportName != null) {
-                    if (context.getDevices().size() == 1 || badDevice != null) {
-                        ITestDevice collectBugreport = badDevice;
-                        if (collectBugreport == null) {
-                            collectBugreport = context.getDevices().get(0);
+                    try (CloseableTraceScope ignore =
+                            new CloseableTraceScope(InvocationMetricKey.bugreport.name())) {
+                        if (context.getDevices().size() == 1 || badDevice != null) {
+                            ITestDevice collectBugreport = badDevice;
+                            if (collectBugreport == null) {
+                                collectBugreport = context.getDevices().get(0);
+                            }
+                            // If we have identified a faulty device only take the bugreport on it.
+                            takeBugreport(collectBugreport, listener, bugreportName);
+                        } else if (context.getDevices().size() > 1) {
+                            ParallelDeviceExecutor<Boolean> executor =
+                                    new ParallelDeviceExecutor<>(context.getDevices().size());
+                            List<Callable<Boolean>> callableTasks = new ArrayList<>();
+                            final String reportName = bugreportName;
+                            for (ITestDevice device : context.getDevices()) {
+                                Callable<Boolean> callableTask =
+                                        () -> {
+                                            CLog.d(
+                                                    "Start taking bugreport on '%s'",
+                                                    device.getSerialNumber());
+                                            takeBugreport(device, listener, reportName);
+                                            return true;
+                                        };
+                                callableTasks.add(callableTask);
+                            }
+                            // Capture the bugreports best effort, ignore the results.
+                            executor.invokeAll(callableTasks, 5, TimeUnit.MINUTES);
                         }
-                        // If we have identified a faulty device only take the bugreport on it.
-                        takeBugreport(collectBugreport, listener, bugreportName);
-                    } else if (context.getDevices().size() > 1) {
-                        ParallelDeviceExecutor<Boolean> executor =
-                                new ParallelDeviceExecutor<>(context.getDevices().size());
-                        List<Callable<Boolean>> callableTasks = new ArrayList<>();
-                        final String reportName = bugreportName;
-                        for (ITestDevice device : context.getDevices()) {
-                            Callable<Boolean> callableTask =
-                                    () -> {
-                                        CLog.d("Start taking bugreport on '%s'",
-                                                device.getSerialNumber());
-                                        takeBugreport(device, listener, reportName);
-                                        return true;
-                                    };
-                            callableTasks.add(callableTask);
-                        }
-                        // Capture the bugreports best effort, ignore the results.
-                        executor.invokeAll(callableTasks, 5, TimeUnit.MINUTES);
+                    }
+                    reportRecoveryLogs(context.getDevices(), listener);
+                }
+            }
+            try (CloseableTraceScope ignore =
+                    new CloseableTraceScope(InvocationMetricKey.check_device_availability.name())) {
+                // Save the device executeShellCommand logs
+                logExecuteShellCommand(context.getDevices(), listener);
+                if (exception == null) {
+                    exception = mUnavailableMonitor.getUnavailableException();
+                    if (exception != null) {
+                        CLog.e("Found a test level only device unavailable exception:");
+                        CLog.e(exception);
                     }
                 }
-                reportRecoveryLogs(context.getDevices(), listener);
-            }
-            // Save the device executeShellCommand logs
-            logExecuteShellCommand(context.getDevices(), listener);
-            if (exception == null) {
-                exception = mUnavailableMonitor.getUnavailableException();
-                if (exception != null) {
-                    CLog.e("Found a test level only device unavailable exception:");
-                    CLog.e(exception);
+                if (exception == null) {
+                    CLog.d("Checking that devices are online.");
+                    exception = checkDevicesAvailable(context.getDevices(), listener);
+                } else {
+                    CLog.d("Skip online check as an exception was already reported: %s", exception);
                 }
+                // Report bugreport and various check as part of teardown
+                InvocationMetricLogger.addInvocationPairMetrics(
+                        InvocationMetricKey.TEARDOWN_PAIR,
+                        bugreportStartTime,
+                        System.currentTimeMillis());
+                mStatus = "tearing down";
             }
-            if (exception == null) {
-                CLog.d("Checking that devices are online.");
-                exception = checkDevicesAvailable(context.getDevices(), listener);
-            } else {
-                CLog.d("Skip online check as an exception was already reported: %s", exception);
-            }
-            mStatus = "tearing down";
-            try {
+            try (CloseableTraceScope ignore =
+                    new CloseableTraceScope(InvocationMetricKey.test_teardown.name())) {
                 invocationPath.doTeardown(testInfo, config, listener, exception);
             } catch (Throwable e) {
                 tearDownException = e;
@@ -405,54 +427,101 @@
                             listener);
                 }
             }
-            // Capture last logcat before releasing the device.
-            for (ITestDevice device : context.getDevices()) {
-                invocationPath.reportLogs(device, listener, Stage.TEARDOWN);
-            }
-            mStatus = "done running tests";
-            CurrentInvocation.setActionInProgress(ActionInProgress.FREE_RESOURCES);
-
-            // Ensure we always deregister the logger
-            for (String deviceName : context.getDeviceConfigNames()) {
-                if (!(context.getDevice(deviceName).getIDevice() instanceof StubDevice)) {
-                    context.getDevice(deviceName).stopLogcat();
-                    CLog.i(
-                            "Done stopping logcat for %s",
-                            context.getDevice(deviceName).getSerialNumber());
+            try (CloseableTraceScope ignore =
+                    new CloseableTraceScope(InvocationMetricKey.log_and_release_device.name())) {
+                // Capture last logcat before releasing the device.
+                for (ITestDevice device : context.getDevices()) {
+                    invocationPath.reportLogs(device, listener, Stage.TEARDOWN);
                 }
-            }
+                mStatus = "done running tests";
+                CurrentInvocation.setActionInProgress(ActionInProgress.FREE_RESOURCES);
 
-            Map<ITestDevice, FreeDeviceState> devicesStates =
-                    handleAndLogReleaseState(context, exception, tearDownException);
-            if (config.getCommandOptions().earlyDeviceRelease()) {
-                context.markReleasedEarly();
-                for (IScheduledInvocationListener scheduleListener : mSchedulerListeners) {
-                    scheduleListener.releaseDevices(context, devicesStates);
+                // Ensure we always deregister the logger
+                for (String deviceName : context.getDeviceConfigNames()) {
+                    if (!(context.getDevice(deviceName).getIDevice() instanceof StubDevice)) {
+                        context.getDevice(deviceName).stopLogcat();
+                        CLog.i(
+                                "Done stopping logcat for %s",
+                                context.getDevice(deviceName).getSerialNumber());
+                    }
                 }
+
+                Map<ITestDevice, FreeDeviceState> devicesStates =
+                        handleAndLogReleaseState(context, exception, tearDownException);
+                if (config.getCommandOptions().earlyDeviceRelease()) {
+                    context.markReleasedEarly();
+                    for (IScheduledInvocationListener scheduleListener : mSchedulerListeners) {
+                        scheduleListener.releaseDevices(context, devicesStates);
+                    }
+                }
+                // Log count of allocated devices for test accounting
+                addInvocationMetric(
+                        InvocationMetricKey.DEVICE_COUNT, context.getNumDevicesAllocated());
+                // Track the timestamp when we are done with devices
+                addInvocationMetric(
+                        InvocationMetricKey.DEVICE_DONE_TIMESTAMP, System.currentTimeMillis());
             }
-            // Log count of allocated devices for test accounting
-            addInvocationMetric(InvocationMetricKey.DEVICE_COUNT, context.getNumDevicesAllocated());
-            // Track the timestamp when we are done with devices
-            addInvocationMetric(
-                    InvocationMetricKey.DEVICE_DONE_TIMESTAMP, System.currentTimeMillis());
-            try {
+            try (CloseableTraceScope ignore =
+                    new CloseableTraceScope(InvocationMetricKey.test_cleanup.name())) {
                 // Clean up host.
                 invocationPath.doCleanUp(context, config, exception);
-                if (mStopCause != null) {
-                    String message =
-                            String.format(
-                                    "Invocation was interrupted due to: %s, results will be "
-                                            + "affected.",
-                                    mStopCause);
-                    if (mStopErrorId == null) {
-                        mStopErrorId = InfraErrorIdentifier.INVOCATION_CANCELLED;
+                if (mSoftStopRequestTime != null) { // soft stop occurred
+                    long latency = System.currentTimeMillis() - mSoftStopRequestTime;
+                    InvocationMetricLogger.addInvocationMetrics(
+                            InvocationMetricKey.SHUTDOWN_LATENCY, latency);
+                    InvocationMetricLogger.addInvocationMetrics(
+                            InvocationMetricKey.SHUTDOWN_BEFORE_TEST,
+                            Boolean.toString(mShutdownBeforeTest));
+                    if (mTestsNotRan) {
+                        String message =
+                                String.format("Notified of soft shut down. Did not run tests");
+                        FailureDescription failure =
+                                FailureDescription.create(message)
+                                        .setErrorIdentifier(
+                                                InfraErrorIdentifier
+                                                        .TRADEFED_SKIPPED_TESTS_DURING_SHUTDOWN)
+                                        .setCause(
+                                                new HarnessRuntimeException(
+                                                        message,
+                                                        InfraErrorIdentifier
+                                                                .TRADEFED_SKIPPED_TESTS_DURING_SHUTDOWN));
+                        // report failure so that command can be un-leased
+                        reportFailure(failure, listener);
                     }
-                    FailureDescription failure =
-                            FailureDescription.create(message)
-                                    .setErrorIdentifier(mStopErrorId)
-                                    .setCause(new HarnessRuntimeException(message, mStopErrorId));
-                    reportFailure(failure, listener);
-                    PrettyPrintDelimiter.printStageDelimiter(message);
+                }
+                if (mStopCause != null) { // Forced stop occurred
+                    if (mForcedStopRequestedAfterTest) {
+                        InvocationMetricLogger.addInvocationMetrics(
+                                InvocationMetricKey.SHUTDOWN_AFTER_TEST, "true");
+                        CLog.d(
+                                "Forced shutdown occurred after test phase execution. It shouldn't"
+                                        + " have impact on test results.");
+                    } else {
+                        String message =
+                                String.format(
+                                        "Invocation was interrupted due to: %s%s",
+                                        mStopCause,
+                                        mTestsNotRan
+                                                ? ". Tests were not run."
+                                                : ", results will be affected");
+                        if (mStopErrorId == null) {
+                            mStopErrorId = InfraErrorIdentifier.INVOCATION_CANCELLED;
+                        }
+                        // if invocation is stopped and tests were not run, report invocation
+                        // failure with correct error identifier so that command can be
+                        // un-leased
+                        if (mTestsNotRan) {
+                            mStopErrorId =
+                                    InfraErrorIdentifier.TRADEFED_SKIPPED_TESTS_DURING_SHUTDOWN;
+                        }
+                        FailureDescription failure =
+                                FailureDescription.create(message)
+                                        .setErrorIdentifier(mStopErrorId)
+                                        .setCause(
+                                                new HarnessRuntimeException(message, mStopErrorId));
+                        reportFailure(failure, listener);
+                        PrettyPrintDelimiter.printStageDelimiter(message);
+                    }
                     if (mStopRequestTime != null) {
                         // This is not 100% perfect since result reporting can still run a bit
                         // longer, but this is our last opportunity to report it.
@@ -502,6 +571,15 @@
         logDeviceBatteryLevel(testInfo.getContext(), "initial -> setup");
         CurrentInvocation.setActionInProgress(ActionInProgress.SETUP);
         invocationPath.doSetup(testInfo, config, listener);
+        // Don't run tests if notified of soft/forced shutdown
+        if (mSoftStopRequestTime != null || mStopRequestTime != null) {
+            // Throw an exception so that it can be reported as an invocation failure
+            // and command can be un-leased
+            mTestsNotRan = true;
+            throw new RunInterruptedException(
+                    "Notified of shut down. Will not run tests",
+                    InfraErrorIdentifier.TRADEFED_SKIPPED_TESTS_DURING_SHUTDOWN);
+        }
         logDeviceBatteryLevel(testInfo.getContext(), "setup -> test");
         mTestStarted = true;
         CurrentInvocation.setActionInProgress(ActionInProgress.TEST);
@@ -853,144 +931,156 @@
             IRescheduler rescheduler,
             ITestInvocationListener... extraListeners)
             throws DeviceNotAvailableException, Throwable {
-        if (!config.getInopOptions().isEmpty()) {
-            context.addInvocationAttribute(
-                    "inop-options", Joiner.on(",").join(config.getInopOptions()));
-        }
-        // Carry the reference of the server so it can be used within the same process.
-        if (config.getConfigurationDescription().getAllMetaData().getUniqueMap()
-                .containsKey(TradefedFeatureServer.SERVER_REFERENCE)) {
-            InvocationMetricLogger.addInvocationMetrics(
-                    InvocationMetricKey.SERVER_REFERENCE,
-                    config.getConfigurationDescription().getAllMetaData().getUniqueMap()
-                        .get(TradefedFeatureServer.SERVER_REFERENCE));
-        }
-        // Only log invocation_start in parent
-        boolean isSuprocess = isSubprocess(config);
-        if (!isSuprocess) {
-            InvocationMetricLogger.addInvocationMetrics(
-                    InvocationMetricKey.INVOCATION_START, System.currentTimeMillis());
-        } else {
-            CLog.d("Fetching options from parent.");
-            // Get options from the parent process
-            try (OptionFetcher fetchOtpions = new OptionFetcher()) {
-                fetchOtpions.fetchParentOptions(config);
-            }
-        }
-        // Handle the automated reporting
-        applyAutomatedReporters(config);
-
-        if (config.getCommandOptions().delegatedEarlyDeviceRelease()
-                && System.getenv(DelegatedInvocationExecution.DELEGATED_MODE_VAR) != null) {
-            // If in a subprocess, add the early device release feature as a listener.
-            mSchedulerListeners.add(new DeviceReleaseReporter());
-        }
-
-        for (ITestInvocationListener listener : extraListeners) {
-            if (listener instanceof IScheduledInvocationListener) {
-                mSchedulerListeners.add((IScheduledInvocationListener) listener);
-            }
-        }
-        // Create the TestInformation for the invocation
-        // TODO: Use invocation-id in the workfolder name
-        Object sharedInfoObject =
-                config.getConfigurationObject(ShardHelper.SHARED_TEST_INFORMATION);
-        TestInformation sharedTestInfo = null;
-        TestInformation info = null;
-        if (sharedInfoObject != null) {
-            sharedTestInfo = (TestInformation) sharedInfoObject;
-            // During sharding we share everything except the invocation context
-            info = TestInformation.createModuleTestInfo(sharedTestInfo, context);
-        }
-        if (info == null) {
-            File mWorkFolder = FileUtil.createTempDir("tf-workfolder");
-            info =
-                    TestInformation.newBuilder()
-                            .setInvocationContext(context)
-                            .setDependenciesFolder(mWorkFolder)
-                            .build();
-        }
-        // Register the test info to the configuration to be usable.
-        config.setConfigurationObject(TradefedFeatureServer.TEST_INFORMATION_OBJECT, info);
-        CurrentInvocation.addInvocationInfo(InvocationInfo.WORK_FOLDER, info.dependenciesFolder());
-
-        CleanUpInvocationFiles cleanUpThread = new CleanUpInvocationFiles(info, config);
-        Runtime.getRuntime().addShutdownHook(cleanUpThread);
-        registerExecutionFiles(info.executionFiles());
-
-        List<ITestInvocationListener> allListeners =
-                new ArrayList<>(config.getTestInvocationListeners().size() + extraListeners.length);
-        // If it's not a subprocess, report the passed tests.
-        ReportPassedTests reportPass = null;
-        if (config.getConfigurationObject(TradefedDelegator.DELEGATE_OBJECT) == null
-                && config.getCommandOptions().reportPassedTests()
-                && !isSubprocess(config)) {
-            reportPass = new ReportPassedTests();
-            reportPass.setConfiguration(config);
-            allListeners.add(reportPass);
-        }
-        allListeners.addAll(config.getTestInvocationListeners());
-        allListeners.addAll(Arrays.asList(extraListeners));
-        allListeners.add(mUnavailableMonitor);
+        RunMode mode = RunMode.REGULAR;
         ITestInvocationListener listener = null;
-
-        // Auto retry feature
-        IRetryDecision decision = config.getRetryDecision();
+        TestInformation info = null;
         ResultAggregator aggregator = null;
-        decision.setInvocationContext(context);
-        if (decision instanceof ITestInformationReceiver) {
-            ((ITestInformationReceiver) decision).setTestInformation(info);
-        }
-        // We don't need the aggregator in the subprocess because the parent will take care of it.
-        if (!config.getCommandOptions()
-                .getInvocationData()
-                .containsKey(SubprocessTfLauncher.SUBPROCESS_TAG_NAME)) {
-            if (decision.isAutoRetryEnabled()
-                    && decision.getMaxRetryCount() > 1
-                    && !RetryStrategy.NO_RETRY.equals(decision.getRetryStrategy())) {
-                CLog.d(
-                        "Auto-retry enabled, using the ResultAggregator to handle multiple"
-                                + " retries.");
-                aggregator = new ResultAggregator(allListeners, decision.getRetryStrategy());
-                aggregator.setUpdatedReporting(decision.useUpdatedReporting());
-                allListeners = Arrays.asList(aggregator);
-            } else {
-                mEventsLogger = new EventsLoggerListener("all-events");
-                allListeners.add(mEventsLogger);
+        CleanUpInvocationFiles cleanUpThread = null;
+        try (CloseableTraceScope ignore =
+                new CloseableTraceScope(InvocationMetricKey.invocation_warm_up.name())) {
+            if (!config.getInopOptions().isEmpty()) {
+                context.addInvocationAttribute(
+                        "inop-options", Joiner.on(",").join(config.getInopOptions()));
             }
-        }
-
-        if (!config.getPostProcessors().isEmpty()) {
-            ITestInvocationListener forwarder = new ResultAndLogForwarder(allListeners);
-            // Post-processors are the first layer around the final reporters.
-            for (IPostProcessor postProcessor : config.getPostProcessors()) {
-                if (postProcessor.isDisabled()) {
-                    CLog.d("%s has been disabled. skipping.", postProcessor);
-                } else {
-                    forwarder = postProcessor.init(forwarder);
+            // Carry the reference of the server so it can be used within the same process.
+            if (config.getConfigurationDescription()
+                    .getAllMetaData()
+                    .getUniqueMap()
+                    .containsKey(TradefedFeatureServer.SERVER_REFERENCE)) {
+                InvocationMetricLogger.addInvocationMetrics(
+                        InvocationMetricKey.SERVER_REFERENCE,
+                        config.getConfigurationDescription()
+                                .getAllMetaData()
+                                .getUniqueMap()
+                                .get(TradefedFeatureServer.SERVER_REFERENCE));
+            }
+            // Only log invocation_start in parent
+            boolean isSuprocess = isSubprocess(config);
+            if (!isSuprocess) {
+                InvocationMetricLogger.addInvocationMetrics(
+                        InvocationMetricKey.INVOCATION_START, System.currentTimeMillis());
+            } else {
+                CLog.d("Fetching options from parent.");
+                // Get options from the parent process
+                try (OptionFetcher fetchOtpions = new OptionFetcher()) {
+                    fetchOtpions.fetchParentOptions(config);
                 }
             }
-            listener = new LogSaverResultForwarder(config.getLogSaver(), Arrays.asList(forwarder));
-        } else {
-            listener = new LogSaverResultForwarder(config.getLogSaver(), allListeners);
-        }
-        if (reportPass != null) {
-            reportPass.setLogger(listener);
-        }
+            // Handle the automated reporting
+            applyAutomatedReporters(config);
 
-        RunMode mode = RunMode.REGULAR;
-        if (config.getConfigurationDescription().shouldUseSandbox()) {
-            mode = RunMode.SANDBOX;
-        }
-        if (config.getCommandOptions().shouldUseSandboxing()) {
-            mode = RunMode.PARENT_SANDBOX;
-        }
-        if (context.getDevices().get(0) instanceof ManagedRemoteDevice) {
-            mode = RunMode.REMOTE_INVOCATION;
-        }
-        if (config.getConfigurationObject(TradefedDelegator.DELEGATE_OBJECT) != null) {
-            mDelegatedInvocation = true;
-            mode = RunMode.DELEGATED_INVOCATION;
+            if (config.getCommandOptions().delegatedEarlyDeviceRelease()
+                    && System.getenv(DelegatedInvocationExecution.DELEGATED_MODE_VAR) != null) {
+                // If in a subprocess, add the early device release feature as a listener.
+                mSchedulerListeners.add(new DeviceReleaseReporter());
+            }
+
+            for (ITestInvocationListener extra : extraListeners) {
+                if (extra instanceof IScheduledInvocationListener) {
+                    mSchedulerListeners.add((IScheduledInvocationListener) extra);
+                }
+            }
+            // Create the TestInformation for the invocation
+            // TODO: Use invocation-id in the workfolder name
+            Object sharedInfoObject =
+                    config.getConfigurationObject(ShardHelper.SHARED_TEST_INFORMATION);
+            TestInformation sharedTestInfo = null;
+            if (sharedInfoObject != null) {
+                sharedTestInfo = (TestInformation) sharedInfoObject;
+                // During sharding we share everything except the invocation context
+                info = TestInformation.createModuleTestInfo(sharedTestInfo, context);
+            }
+            if (info == null) {
+                File mWorkFolder = FileUtil.createTempDir("tf-workfolder");
+                info =
+                        TestInformation.newBuilder()
+                                .setInvocationContext(context)
+                                .setDependenciesFolder(mWorkFolder)
+                                .build();
+            }
+            // Register the test info to the configuration to be usable.
+            config.setConfigurationObject(TradefedFeatureServer.TEST_INFORMATION_OBJECT, info);
+            CurrentInvocation.addInvocationInfo(
+                    InvocationInfo.WORK_FOLDER, info.dependenciesFolder());
+
+            cleanUpThread = new CleanUpInvocationFiles(info, config);
+            Runtime.getRuntime().addShutdownHook(cleanUpThread);
+            registerExecutionFiles(info.executionFiles());
+
+            List<ITestInvocationListener> allListeners =
+                    new ArrayList<>(
+                            config.getTestInvocationListeners().size() + extraListeners.length);
+            // If it's not a subprocess, report the passed tests.
+            ReportPassedTests reportPass = null;
+            if (config.getConfigurationObject(TradefedDelegator.DELEGATE_OBJECT) == null
+                    && config.getCommandOptions().reportPassedTests()
+                    && !isSubprocess(config)) {
+                reportPass = new ReportPassedTests();
+                reportPass.setConfiguration(config);
+                allListeners.add(reportPass);
+            }
+            allListeners.addAll(config.getTestInvocationListeners());
+            allListeners.addAll(Arrays.asList(extraListeners));
+            allListeners.add(mUnavailableMonitor);
+
+            // Auto retry feature
+            IRetryDecision decision = config.getRetryDecision();
+            decision.setInvocationContext(context);
+            if (decision instanceof ITestInformationReceiver) {
+                ((ITestInformationReceiver) decision).setTestInformation(info);
+            }
+            // We don't need the aggregator in the subprocess because the parent will take care of
+            // it.
+            if (!config.getCommandOptions()
+                    .getInvocationData()
+                    .containsKey(SubprocessTfLauncher.SUBPROCESS_TAG_NAME)) {
+                if (decision.isAutoRetryEnabled()
+                        && decision.getMaxRetryCount() > 1
+                        && !RetryStrategy.NO_RETRY.equals(decision.getRetryStrategy())) {
+                    CLog.d(
+                            "Auto-retry enabled, using the ResultAggregator to handle multiple"
+                                    + " retries.");
+                    aggregator = new ResultAggregator(allListeners, decision.getRetryStrategy());
+                    aggregator.setUpdatedReporting(decision.useUpdatedReporting());
+                    allListeners = Arrays.asList(aggregator);
+                } else {
+                    mEventsLogger = new EventsLoggerListener("all-events");
+                    allListeners.add(mEventsLogger);
+                }
+            }
+
+            if (!config.getPostProcessors().isEmpty()) {
+                ITestInvocationListener forwarder = new ResultAndLogForwarder(allListeners);
+                // Post-processors are the first layer around the final reporters.
+                for (IPostProcessor postProcessor : config.getPostProcessors()) {
+                    if (postProcessor.isDisabled()) {
+                        CLog.d("%s has been disabled. skipping.", postProcessor);
+                    } else {
+                        forwarder = postProcessor.init(forwarder);
+                    }
+                }
+                listener =
+                        new LogSaverResultForwarder(config.getLogSaver(), Arrays.asList(forwarder));
+            } else {
+                listener = new LogSaverResultForwarder(config.getLogSaver(), allListeners);
+            }
+            if (reportPass != null) {
+                reportPass.setLogger(listener);
+            }
+
+            if (config.getConfigurationDescription().shouldUseSandbox()) {
+                mode = RunMode.SANDBOX;
+            }
+            if (config.getCommandOptions().shouldUseSandboxing()) {
+                mode = RunMode.PARENT_SANDBOX;
+            }
+            if (context.getDevices().get(0) instanceof ManagedRemoteDevice) {
+                mode = RunMode.REMOTE_INVOCATION;
+            }
+            if (config.getConfigurationObject(TradefedDelegator.DELEGATE_OBJECT) != null) {
+                mDelegatedInvocation = true;
+                mode = RunMode.DELEGATED_INVOCATION;
+            }
         }
         IInvocationExecution invocationPath = createInvocationExec(mode);
         updateInvocationContext(context, config);
@@ -1006,7 +1096,8 @@
             mStatus = "resolving dynamic options";
             long startDynamic = System.currentTimeMillis();
             boolean resolverSuccess = false;
-            try {
+            try (CloseableTraceScope ignored =
+                    new CloseableTraceScope(InvocationMetricKey.dynamic_download.name())) {
                 resolverSuccess =
                         invokeRemoteDynamic(context, config, listener, invocationPath, mode);
             } finally {
@@ -1029,7 +1120,8 @@
             InvocationMetricLogger.addInvocationMetrics(
                     InvocationMetricKey.FETCH_BUILD_START, start);
             boolean providerSuccess = false;
-            try {
+            try (CloseableTraceScope ignored =
+                    new CloseableTraceScope(InvocationMetricKey.fetch_artifact.name())) {
                 providerSuccess =
                         invokeFetchBuild(info, config, rescheduler, listener, invocationPath);
             } finally {
@@ -1046,7 +1138,8 @@
             if (!providerSuccess) {
                 return;
             }
-            try {
+            try (CloseableTraceScope ignore =
+                    new CloseableTraceScope(InvocationMetricKey.start_logcat.name())) {
                 for (String deviceName : context.getDeviceConfigNames()) {
                     context.getDevice(deviceName).clearLastConnectedWifiNetwork();
                     // TODO: Report invocation error if setOptions() fails
@@ -1088,10 +1181,12 @@
                 // we call the device setup early to meet all the requirements.
                 boolean startInvocationCalled = false;
                 if (shardCount != null && shardIndex != null) {
-                    deviceInit = true;
-                    startInvocation(config, context, listener);
-                    startInvocationCalled = true;
-                    try {
+                    try (CloseableTraceScope ignored =
+                            new CloseableTraceScope(
+                                    InvocationMetricKey.pre_sharding_required_setup.name())) {
+                        deviceInit = true;
+                        startInvocation(config, context, listener);
+                        startInvocationCalled = true;
                         invocationPath.runDevicePreInvocationSetup(context, config, listener);
                     } catch (DeviceNotAvailableException | TargetSetupError e) {
                         CLog.e(e);
@@ -1122,7 +1217,8 @@
                 // Apply global filters before sharding so they are taken into account.
                 config.getGlobalFilters().setUpFilters(config);
 
-                try {
+                try (CloseableTraceScope ignored =
+                        new CloseableTraceScope(InvocationMetricKey.sharding.name())) {
                     sharding = invocationPath.shardConfig(config, info, rescheduler, listener);
                 } catch (RuntimeException unexpected) {
                     CLog.e("Exception during sharding.");
@@ -1199,8 +1295,9 @@
             }
 
             config.cleanConfigurationData();
-
-            Runtime.getRuntime().removeShutdownHook(cleanUpThread);
+            if (cleanUpThread != null) {
+                Runtime.getRuntime().removeShutdownHook(cleanUpThread);
+            }
         }
     }
 
@@ -1234,11 +1331,21 @@
     }
 
     @Override
-    public void notifyInvocationStopped(String message, ErrorIdentifier errorId) {
+    public void notifyInvocationForceStopped(String message, ErrorIdentifier errorId) {
         mStopCause = message;
         mStopErrorId = errorId;
         if (mStopRequestTime == null) {
             mStopRequestTime = System.currentTimeMillis();
+            mForcedStopRequestedAfterTest = mTestDone;
+        }
+    }
+
+    @Override
+    public void notifyInvocationStopped(String message) {
+        if (mSoftStopRequestTime == null) {
+            mSoftStopRequestTime = System.currentTimeMillis();
+            // If test isn't started yet, we know we could have stopped.
+            mShutdownBeforeTest = !mTestStarted;
         }
     }
 
@@ -1278,16 +1385,19 @@
 
     private void logExecuteShellCommand(List<ITestDevice> devices, ITestLogger logger) {
         for (ITestDevice device : devices) {
+            if (device.getIDevice() instanceof StubDevice) {
+                continue;
+            }
             if (!(device instanceof NativeDevice)) {
-                return;
+                continue;
             }
             File log = ((NativeDevice) device).getExecuteShellCommandLog();
             if (log == null || !log.exists()) {
-                return;
+                continue;
             }
             if (log.length() == 0) {
                 CLog.d("executeShellCommandLog file was empty, skip logging.");
-                return;
+                continue;
             }
             try (InputStreamSource source = new FileInputStreamSource(log)) {
                 logger.testLog(
diff --git a/src/com/android/tradefed/invoker/sandbox/ParentSandboxInvocationExecution.java b/src/com/android/tradefed/invoker/sandbox/ParentSandboxInvocationExecution.java
index 180d1d3..eb4021b 100644
--- a/src/com/android/tradefed/invoker/sandbox/ParentSandboxInvocationExecution.java
+++ b/src/com/android/tradefed/invoker/sandbox/ParentSandboxInvocationExecution.java
@@ -30,6 +30,7 @@
 import com.android.tradefed.invoker.InvocationExecution;
 import com.android.tradefed.invoker.TestInformation;
 import com.android.tradefed.invoker.TestInvocation.Stage;
+import com.android.tradefed.invoker.tracing.CloseableTraceScope;
 import com.android.tradefed.log.ITestLogger;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.result.ITestInvocationListener;
@@ -159,7 +160,9 @@
     public void runTests(
             TestInformation info, IConfiguration config, ITestInvocationListener listener)
             throws Throwable {
-        prepareAndRunSandbox(info, config, listener);
+        try (CloseableTraceScope ignore = new CloseableTraceScope("prepareAndRunSandbox")) {
+            prepareAndRunSandbox(info, config, listener);
+        }
     }
 
     @Override
diff --git a/src/com/android/tradefed/result/LogFileSaver.java b/src/com/android/tradefed/result/LogFileSaver.java
index 59c1678..9976e40 100644
--- a/src/com/android/tradefed/result/LogFileSaver.java
+++ b/src/com/android/tradefed/result/LogFileSaver.java
@@ -307,7 +307,7 @@
      */
     public File saveAndGZipLogFile(String dataName, LogDataType dataType, File fileToLog)
             throws IOException {
-        if (dataType.isCompressed()) {
+        if (dataType.isCompressed() || fileToLog.getName().endsWith(".gz")) {
             CLog.d("Log data for %s is already compressed, skipping compression", dataName);
             return saveLogFile(dataName, dataType, fileToLog);
         }
@@ -343,6 +343,9 @@
      */
     public File createCompressedLogFile(String dataName, LogDataType origDataType)
             throws IOException {
+        if (mInvLogDir != null && !mInvLogDir.exists()) {
+            mInvLogDir.mkdirs();
+        }
         // add underscore to end of data name to make generated name more readable
         return FileUtil.createTempFile(dataName + "_",
                 String.format(".%s.%s", origDataType.getFileExt(), LogDataType.GZIP.getFileExt()),
diff --git a/src/com/android/tradefed/result/LogSaverResultForwarder.java b/src/com/android/tradefed/result/LogSaverResultForwarder.java
index 97a7641..06d7758 100644
--- a/src/com/android/tradefed/result/LogSaverResultForwarder.java
+++ b/src/com/android/tradefed/result/LogSaverResultForwarder.java
@@ -16,6 +16,7 @@
 
 package com.android.tradefed.result;
 
+import com.android.ddmlib.Log.LogLevel;
 import com.android.tradefed.invoker.IInvocationContext;
 import com.android.tradefed.invoker.TestInvocation;
 import com.android.tradefed.invoker.logger.InvocationMetricLogger;
@@ -74,11 +75,38 @@
             CLog.e("Caught runtime exception from log saver: %s", mLogSaver.getClass().getName());
             CLog.e(e);
         }
-        reportEndHostLog(mLogSaver, TestInvocation.TRADEFED_END_HOST_LOG);
+        reportEndHostLog(getListeners(), mLogSaver, TestInvocation.TRADEFED_END_HOST_LOG);
+    }
+
+    /** Log a final file before completion */
+    public static void logFile(
+            List<ITestInvocationListener> listeners,
+            ILogSaver saver,
+            InputStreamSource source,
+            String name,
+            LogDataType type) {
+        try (InputStream stream = source.createInputStream()) {
+            LogFile logFile = saver.saveLogData(name, type, stream);
+
+            for (ITestInvocationListener listener : listeners) {
+                try {
+                    if (listener instanceof ILogSaverListener) {
+                        ((ILogSaverListener) listener).testLogSaved(name, type, source, logFile);
+                        ((ILogSaverListener) listener).logAssociation(name, logFile);
+                    }
+                } catch (Exception e) {
+                    CLog.logAndDisplay(LogLevel.ERROR, e.getMessage());
+                    CLog.e(e);
+                }
+            }
+        } catch (IOException e) {
+            CLog.e(e);
+        }
     }
 
     /** Reports host_log from session in progress. */
-    public static void reportEndHostLog(ILogSaver saver, String name) {
+    public static void reportEndHostLog(
+            List<ITestInvocationListener> listeners, ILogSaver saver, String name) {
         LogRegistry registry = (LogRegistry) LogRegistry.getLogRegistry();
         try (InputStreamSource source = registry.getLogger().getLog()) {
             if (source == null) {
@@ -87,9 +115,7 @@
                 }
                 return;
             }
-            try (InputStream stream = source.createInputStream()) {
-                saver.saveLogData(name, LogDataType.HOST_LOG, stream);
-            }
+            logFile(listeners, saver, source, name, LogDataType.HOST_LOG);
             if (SystemUtil.isRemoteEnvironment()) {
                 try (InputStream stream = source.createInputStream()) {
                     // In remote environment, dump to the stdout so we can get the logs in the
diff --git a/src/com/android/tradefed/result/ResultAndLogForwarder.java b/src/com/android/tradefed/result/ResultAndLogForwarder.java
index f2e34e2..45f2eb4 100644
--- a/src/com/android/tradefed/result/ResultAndLogForwarder.java
+++ b/src/com/android/tradefed/result/ResultAndLogForwarder.java
@@ -28,6 +28,10 @@
         super(listeners);
     }
 
+    public ResultAndLogForwarder(ITestInvocationListener... listeners) {
+        super(listeners);
+    }
+
     @Override
     public void invocationStarted(IInvocationContext context) {
         InvocationSummaryHelper.reportInvocationStarted(getListeners(), context);
diff --git a/src/com/android/tradefed/result/proto/ProtoResultParser.java b/src/com/android/tradefed/result/proto/ProtoResultParser.java
index d1f3f63..a6f9eb3 100644
--- a/src/com/android/tradefed/result/proto/ProtoResultParser.java
+++ b/src/com/android/tradefed/result/proto/ProtoResultParser.java
@@ -23,6 +23,8 @@
 import com.android.tradefed.invoker.logger.InvocationMetricLogger.InvocationMetricKey;
 import com.android.tradefed.invoker.logger.TfObjectTracker;
 import com.android.tradefed.invoker.proto.InvocationContext.Context;
+import com.android.tradefed.invoker.tracing.ActiveTrace;
+import com.android.tradefed.invoker.tracing.TracingLogger;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
 import com.android.tradefed.result.ActionInProgress;
@@ -42,6 +44,7 @@
 import com.android.tradefed.result.proto.TestRecordProto.FailureStatus;
 import com.android.tradefed.result.proto.TestRecordProto.TestRecord;
 import com.android.tradefed.testtype.suite.ModuleDefinition;
+import com.android.tradefed.util.FileUtil;
 import com.android.tradefed.util.MultiMap;
 import com.android.tradefed.util.SerializationUtil;
 import com.android.tradefed.util.proto.TestRecordProtoUtil;
@@ -323,6 +326,11 @@
         // Still report the logs even if not reporting the invocation level.
         handleLogs(endInvocationProto);
 
+        if (mInvocationEnded) {
+            CLog.d("Re-entry in invocationEnded, most likely for subprocess final logs.");
+            return;
+        }
+
         // Get final context in case it changed.
         Any anyDescription = endInvocationProto.getDescription();
         if (!anyDescription.is(Context.class)) {
@@ -569,8 +577,8 @@
                 }
                 File path = new File(file.getPath());
                 if (Strings.isNullOrEmpty(file.getUrl()) && path.exists()) {
+                    LogDataType type = file.getType();
                     try (InputStreamSource source = new FileInputStreamSource(path)) {
-                        LogDataType type = file.getType();
                         // File might have already been compressed
                         if (file.getPath().endsWith(LogDataType.ZIP.getFileExt())) {
                             type = LogDataType.ZIP;
@@ -578,10 +586,22 @@
                         log("Logging %s from subprocess: %s ", entry.getKey(), file.getPath());
                         logger.testLog(mFilePrefix + entry.getKey(), type, source);
                     }
+                    if (ActiveTrace.TRACE_KEY.equals(entry.getKey())
+                            && LogDataType.PERFETTO.equals(type)) {
+                        CLog.d("Log the subprocess trace");
+                        TracingLogger.getActiveTrace().addSubprocessTrace(path);
+                        FileUtil.deleteFile(path);
+                    }
                 } else {
                     log(
-                            "Logging %s from subprocess. url: %s, path: %s",
-                            entry.getKey(), file.getUrl(), file.getPath());
+                            "Logging %s from subprocess. url: %s, path: %s [exists: %s]",
+                            entry.getKey(), file.getUrl(), file.getPath(), path.exists());
+                    if (ActiveTrace.TRACE_KEY.equals(entry.getKey())
+                            && LogDataType.PERFETTO.equals(file.getType())
+                            && path.exists()) {
+                        CLog.d("Log the subprocess trace");
+                        TracingLogger.getActiveTrace().addSubprocessTrace(path);
+                    }
                     logger.logAssociation(mFilePrefix + entry.getKey(), file);
                 }
             } catch (InvalidProtocolBufferException e) {
diff --git a/src/com/android/tradefed/result/proto/ProtoResultReporter.java b/src/com/android/tradefed/result/proto/ProtoResultReporter.java
index 5bccc48..ce78efb 100644
--- a/src/com/android/tradefed/result/proto/ProtoResultReporter.java
+++ b/src/com/android/tradefed/result/proto/ProtoResultReporter.java
@@ -15,6 +15,7 @@
  */
 package com.android.tradefed.result.proto;
 
+import com.android.ddmlib.Log.LogLevel;
 import com.android.tradefed.config.Option;
 import com.android.tradefed.config.OptionClass;
 import com.android.tradefed.error.HarnessException;
@@ -70,6 +71,8 @@
     private FailureDescription mInvocationFailureDescription = null;
     /** Whether or not a testModuleStart had currently been called. */
     private boolean mModuleInProgress = false;
+    /** Track whether or not invocation ended has been reported. */
+    private boolean mInvocationEnded = false;
 
     @Override
     public boolean supportGranularResults() {
@@ -145,6 +148,13 @@
      */
     public void processTestCaseEnded(TestRecord testCaseRecord) {}
 
+    /**
+     * Use the invocation record to send one by one all the final logs of the invocation.
+     *
+     * @param invocationLogs The finalized proto representing the invocation.
+     */
+    public void processFinalInvocationLogs(TestRecord invocationLogs) {}
+
     // Invocation events
 
     @Override
@@ -221,6 +231,7 @@
             CLog.e("Failed to process invocation ended:");
             CLog.e(e);
         }
+        mInvocationEnded = true;
     }
 
     // Module events (optional when there is no suite)
@@ -510,6 +521,11 @@
             return;
         }
         TestRecord.Builder current = mLatestChild.peek();
+        if (mInvocationEnded) {
+            // For after invocation ended events, report artifacts one by one.
+            current.clearArtifacts();
+            current.clearChildren();
+        }
         Map<String, Any> fullmap = new HashMap<>();
         fullmap.putAll(current.getArtifactsMap());
         Any any = Any.pack(createFileProto(logFile));
@@ -522,6 +538,10 @@
         } while (fullmap.containsKey(key));
         fullmap.put(key, any);
         current.putAllArtifacts(fullmap);
+        if (mInvocationEnded) {
+            CLog.logAndDisplay(LogLevel.DEBUG, "process final logs: %s", logFile.getPath());
+            processFinalInvocationLogs(current.build());
+        }
     }
 
     /**
diff --git a/src/com/android/tradefed/result/proto/StreamProtoResultReporter.java b/src/com/android/tradefed/result/proto/StreamProtoResultReporter.java
index fab982a..5ccea82 100644
--- a/src/com/android/tradefed/result/proto/StreamProtoResultReporter.java
+++ b/src/com/android/tradefed/result/proto/StreamProtoResultReporter.java
@@ -83,11 +83,18 @@
     }
 
     @Override
+    public void processFinalInvocationLogs(TestRecord invocationLogs) {
+        writeRecordToSocket(invocationLogs);
+    }
+
+    @Override
     public void processFinalProto(TestRecord finalRecord) {
         try {
             writeRecordToSocket(finalRecord);
         } finally {
-            closeSocket();
+            // Upon invocation ended, trigger the end of the socket when the process finishes
+            SocketFinisher thread = new SocketFinisher();
+            Runtime.getRuntime().addShutdownHook(thread);
         }
     }
 
@@ -112,4 +119,18 @@
             CLog.e(e);
         }
     }
+
+    /** Threads that help terminating the socket. */
+    private class SocketFinisher extends Thread {
+
+        public SocketFinisher() {
+            super();
+            setName("StreamProtoResultReporter-socket-finisher");
+        }
+
+        @Override
+        public void run() {
+            closeSocket();
+        }
+    }
 }
diff --git a/src/com/android/tradefed/sandbox/SandboxConfigUtil.java b/src/com/android/tradefed/sandbox/SandboxConfigUtil.java
index 199fcb4..826b4ec 100644
--- a/src/com/android/tradefed/sandbox/SandboxConfigUtil.java
+++ b/src/com/android/tradefed/sandbox/SandboxConfigUtil.java
@@ -18,6 +18,7 @@
 import com.android.tradefed.config.ConfigurationException;
 import com.android.tradefed.config.GlobalConfiguration;
 import com.android.tradefed.config.IConfiguration;
+import com.android.tradefed.config.NoOpConfigOptionValueTransformer;
 import com.android.tradefed.config.proxy.AutomatedReporters;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.result.error.ErrorIdentifier;
@@ -124,6 +125,9 @@
         if (result.getStderr().contains(InfraErrorIdentifier.OPTION_CONFIGURATION_ERROR.name())) {
             error = InfraErrorIdentifier.OPTION_CONFIGURATION_ERROR;
         }
+        if (result.getStderr().contains(InfraErrorIdentifier.GCS_ERROR.name())) {
+            error = InfraErrorIdentifier.GCS_ERROR;
+        }
         throw new SandboxConfigurationException(errorMessage, error);
     }
 
@@ -151,6 +155,8 @@
 
     /** Create a global config with only the keystore to make it available in subprocess. */
     public static File dumpFilteredGlobalConfig(Set<String> exclusionPatterns) throws IOException {
-        return GlobalConfiguration.getInstance().cloneConfigWithFilter(exclusionPatterns);
+        return GlobalConfiguration.getInstance()
+                .cloneConfigWithFilter(
+                        exclusionPatterns, new NoOpConfigOptionValueTransformer(), false);
     }
 }
diff --git a/src/com/android/tradefed/service/management/DeviceManagementGrpcServer.java b/src/com/android/tradefed/service/management/DeviceManagementGrpcServer.java
index 03672aa..54e3955 100644
--- a/src/com/android/tradefed/service/management/DeviceManagementGrpcServer.java
+++ b/src/com/android/tradefed/service/management/DeviceManagementGrpcServer.java
@@ -1,6 +1,7 @@
 package com.android.tradefed.service.management;
 
 import com.android.annotations.VisibleForTesting;
+import com.android.tradefed.command.ICommandScheduler;
 import com.android.tradefed.command.remote.DeviceDescriptor;
 import com.android.tradefed.device.DeviceAllocationState;
 import com.android.tradefed.device.DeviceSelectionOptions;
@@ -20,6 +21,8 @@
 import com.proto.tradefed.device.ReserveDeviceRequest;
 import com.proto.tradefed.device.ReserveDeviceResponse;
 import com.proto.tradefed.device.ReserveDeviceResponse.Result;
+import com.proto.tradefed.device.StopLeasingRequest;
+import com.proto.tradefed.device.StopLeasingResponse;
 
 import java.io.IOException;
 import java.util.Map;
@@ -37,6 +40,7 @@
 
     private final Server mServer;
     private final IDeviceManager mDeviceManager;
+    private final ICommandScheduler mCommandScheduler;
     private final Map<String, ReservationInformation> mSerialToReservation =
             new ConcurrentHashMap<>();
 
@@ -47,21 +51,27 @@
                 : null;
     }
 
-    public DeviceManagementGrpcServer(int port, IDeviceManager deviceManager) {
-        this(ServerBuilder.forPort(port), deviceManager);
+    public DeviceManagementGrpcServer(
+            int port, IDeviceManager deviceManager, ICommandScheduler scheduler) {
+        this(ServerBuilder.forPort(port), deviceManager, scheduler);
     }
 
     @VisibleForTesting
     public DeviceManagementGrpcServer(
-            ServerBuilder<?> serverBuilder, IDeviceManager deviceManager) {
+            ServerBuilder<?> serverBuilder,
+            IDeviceManager deviceManager,
+            ICommandScheduler scheduler) {
         mServer = serverBuilder.addService(this).build();
         mDeviceManager = deviceManager;
+        mCommandScheduler = scheduler;
     }
 
     @VisibleForTesting
-    public DeviceManagementGrpcServer(Server server, IDeviceManager deviceManager) {
+    public DeviceManagementGrpcServer(
+            Server server, IDeviceManager deviceManager, ICommandScheduler scheduler) {
         mServer = server;
         mDeviceManager = deviceManager;
+        mCommandScheduler = scheduler;
     }
 
     /** Start the grpc server. */
@@ -135,9 +145,18 @@
                     .setMessage("serial requested was null or empty.");
             responseObserver.onNext(responseBuilder.build());
             responseObserver.onCompleted();
+            return;
         }
 
         DeviceDescriptor descriptor = mDeviceManager.getDeviceDescriptor(serial);
+        if (descriptor == null) {
+            responseBuilder
+                    .setResult(Result.UNKNOWN)
+                    .setMessage("No descriptor found for serial " + serial);
+            responseObserver.onNext(responseBuilder.build());
+            responseObserver.onCompleted();
+            return;
+        }
         if (DeviceAllocationState.Allocated.equals(descriptor.getState())) {
             Result result = Result.ALREADY_ALLOCATED;
             if (mSerialToReservation.containsKey(serial)) {
@@ -169,6 +188,25 @@
         responseObserver.onCompleted();
     }
 
+    @Override
+    public void stopLeasing(
+            StopLeasingRequest request, StreamObserver<StopLeasingResponse> responseObserver) {
+        StopLeasingResponse.Builder responseBuilder = StopLeasingResponse.newBuilder();
+
+        // Notify to stop leasing
+        try {
+            mCommandScheduler.shutdown();
+            responseBuilder.setResult(StopLeasingResponse.Result.SUCCEED);
+        } catch (RuntimeException e) {
+            // This might happen in case scheduler isn't started or in bad state.
+            responseBuilder.setResult(StopLeasingResponse.Result.FAIL);
+            responseBuilder.setMessage(e.getMessage());
+        }
+
+        responseObserver.onNext(responseBuilder.build());
+        responseObserver.onCompleted();
+    }
+
     private DeviceStatus descriptorToStatus(DeviceDescriptor descriptor) {
         DeviceStatus.Builder deviceStatusBuilder = DeviceStatus.newBuilder();
         deviceStatusBuilder.setDeviceId(descriptor.getSerial());
diff --git a/src/com/android/tradefed/service/management/TestInvocationManagementServer.java b/src/com/android/tradefed/service/management/TestInvocationManagementServer.java
index dabb313..240b449 100644
--- a/src/com/android/tradefed/service/management/TestInvocationManagementServer.java
+++ b/src/com/android/tradefed/service/management/TestInvocationManagementServer.java
@@ -34,6 +34,8 @@
 import com.proto.tradefed.invocation.InvocationStatus.Status;
 import com.proto.tradefed.invocation.NewTestCommandRequest;
 import com.proto.tradefed.invocation.NewTestCommandResponse;
+import com.proto.tradefed.invocation.StopInvocationRequest;
+import com.proto.tradefed.invocation.StopInvocationResponse;
 import com.proto.tradefed.invocation.TestInvocationManagementGrpc.TestInvocationManagementImplBase;
 
 import java.io.File;
@@ -57,7 +59,18 @@
     private final Server mServer;
     private final ICommandScheduler mCommandScheduler;
     private final DeviceManagementGrpcServer mDeviceReservationManager;
-    private Map<String, ScheduledInvocationForwarder> mTracker = new HashMap<>();
+    private Map<String, InvocationInformation> mTracker = new HashMap<>();
+
+    public class InvocationInformation {
+        public final long invocationId;
+        public final ScheduledInvocationForwarder scheduledInvocationForwarder;
+
+        InvocationInformation(
+                long invocationId, ScheduledInvocationForwarder scheduledInvocationForwarder) {
+            this.invocationId = invocationId;
+            this.scheduledInvocationForwarder = scheduledInvocationForwarder;
+        }
+    }
 
     /** Returns the port used by the server. */
     public static Integer getPort() {
@@ -132,15 +145,22 @@
             if (!request.getReservationIdList().isEmpty()) {
                 device = getReservedDevices(request.getReservationIdList());
             }
+            long invocationId = -1;
             if (device == null) {
-                mCommandScheduler.execCommand(forwarder, command);
+                invocationId = mCommandScheduler.execCommand(forwarder, command);
             } else {
-                mCommandScheduler.execCommand(forwarder, device, command);
+                invocationId = mCommandScheduler.execCommand(forwarder, device, command);
             }
-            // TODO: Align trackerId with true invocation id
-            String trackerId = UUID.randomUUID().toString();
-            mTracker.put(trackerId, forwarder);
-            responseBuilder.setInvocationId(trackerId);
+            if (invocationId == -1) {
+                responseBuilder.setCommandErrorInfo(
+                        CommandErrorInfo.newBuilder()
+                                .setErrorMessage("Something went wrong to execute the command."));
+            } else {
+                // TODO: Align trackerId with true invocation id
+                String trackerId = UUID.randomUUID().toString();
+                mTracker.put(trackerId, new InvocationInformation(invocationId, forwarder));
+                responseBuilder.setInvocationId(trackerId);
+            }
         } catch (ConfigurationException | IOException | RuntimeException e) {
             // TODO: Expand proto to convey those errors
             // return a response without invocation id
@@ -165,10 +185,16 @@
         String invocationId = request.getInvocationId();
         if (mTracker.containsKey(invocationId)) {
             responseBuilder.setInvocationStatus(
-                    createStatus(mTracker.get(invocationId).getListeners()));
+                    createStatus(
+                            mTracker.get(invocationId)
+                                    .scheduledInvocationForwarder
+                                    .getListeners()));
             if (responseBuilder.getInvocationStatus().getStatus().equals(Status.DONE)) {
                 responseBuilder.setTestRecordPath(
-                        getProtoPath(mTracker.get(invocationId).getListeners()));
+                        getProtoPath(
+                                mTracker.get(invocationId)
+                                        .scheduledInvocationForwarder
+                                        .getListeners()));
                 // Finish the tracking after returning the first status done.
                 mTracker.remove(invocationId);
             }
@@ -182,6 +208,37 @@
         responseObserver.onCompleted();
     }
 
+    @Override
+    public void stopInvocation(
+            StopInvocationRequest request,
+            StreamObserver<StopInvocationResponse> responseObserver) {
+        StopInvocationResponse.Builder responseBuilder = StopInvocationResponse.newBuilder();
+        String invocationId = request.getInvocationId();
+        if (mTracker.containsKey(invocationId)) {
+            long realInvocationId = mTracker.get(invocationId).invocationId;
+            boolean found =
+                    mCommandScheduler.stopInvocation((int) realInvocationId, request.getReason());
+            if (found) {
+                responseBuilder.setStatus(StopInvocationResponse.Status.SUCCESS);
+            } else {
+                responseBuilder
+                        .setStatus(StopInvocationResponse.Status.ERROR)
+                        .setCommandErrorInfo(
+                                CommandErrorInfo.newBuilder()
+                                        .setErrorMessage(
+                                                "No running matching invocation to stop."));
+            }
+        } else {
+            responseBuilder
+                    .setStatus(StopInvocationResponse.Status.ERROR)
+                    .setCommandErrorInfo(
+                            CommandErrorInfo.newBuilder()
+                                    .setErrorMessage("invocation id is not tracked."));
+        }
+        responseObserver.onNext(responseBuilder.build());
+        responseObserver.onCompleted();
+    }
+
     private InvocationStatus createStatus(List<ITestInvocationListener> listeners) {
         InvocationStatus.Builder invocationStatusBuilder = InvocationStatus.newBuilder();
         Status status = Status.UNKNOWN;
diff --git a/src/com/android/tradefed/suite/checker/DeviceBaselineChecker.java b/src/com/android/tradefed/suite/checker/DeviceBaselineChecker.java
index 106e9fb..63b9e30 100644
--- a/src/com/android/tradefed/suite/checker/DeviceBaselineChecker.java
+++ b/src/com/android/tradefed/suite/checker/DeviceBaselineChecker.java
@@ -21,7 +21,6 @@
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.suite.checker.StatusCheckerResult.CheckStatus;
 import com.android.tradefed.suite.checker.baseline.DeviceBaselineSetter;
-import com.android.tradefed.suite.checker.baseline.SettingsBaselineSetter;
 import com.android.tradefed.testtype.suite.ITestSuite;
 import com.android.tradefed.util.StreamUtil;
 
@@ -33,17 +32,26 @@
 
 import java.io.InputStream;
 import java.io.IOException;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
 
 /** Set device baseline settings before each module. */
 public class DeviceBaselineChecker implements ISystemStatusChecker {
 
-    private static final String DEVICE_BASELINE_CONFIG_FILE =
-            "/config/checker/baseline_config.json";
-    private List<DeviceBaselineSetter> mDeviceBaselineSetters = null;
+    private static final String DEVICE_BASELINE_CONFIG_FILE = "/config/checker/baseline_config.json";
+    // Thread pool size to set device baselines.
+    private static final int N_THREAD = 8;
+    private static final String SET_SUCCESS_MESSAGE = "SUCCESS";
+    private List<DeviceBaselineSetter> mDeviceBaselineSetters;
 
     @Option(
             name = "enable-device-baseline-settings",
@@ -58,6 +66,10 @@
                             + "Each value is the setter’s name")
     private Set<String> mEnableExperimentDeviceBaselineSetters = new HashSet<>();
 
+    public static String getSetSuccessMessage() {
+        return SET_SUCCESS_MESSAGE;
+    }
+
     @VisibleForTesting
     void setDeviceBaselineSetters(List<DeviceBaselineSetter> deviceBaselineSetters) {
         mDeviceBaselineSetters = deviceBaselineSetters;
@@ -76,17 +88,21 @@
                 for (int i = 0; i < names.length(); i++) {
                     String name = names.getString(i);
                     JSONObject objectValue = jsonObject.getJSONObject(name);
-                    DeviceBaselineSetter deviceBaselineSetter =
-                            new SettingsBaselineSetter(
-                                    name,
-                                    objectValue.getString("namespace"),
-                                    objectValue.getString("key"),
-                                    objectValue.getString("value"),
-                                    objectValue.has("experimental")
-                                            && objectValue.getBoolean("experimental"));
-                    deviceBaselineSetters.add(deviceBaselineSetter);
+                    // Create a setter according to the class name.
+                    String className = objectValue.getString("class_name");
+                    Class<? extends DeviceBaselineSetter> setterClass =
+                            (Class<? extends DeviceBaselineSetter>) Class.forName(className);
+                    Constructor<? extends DeviceBaselineSetter> constructor =
+                            setterClass.getConstructor(JSONObject.class, String.class);
+                    deviceBaselineSetters.add(constructor.newInstance(objectValue, name));
                 }
-            } catch (JSONException | IOException e) {
+            } catch (JSONException
+                    | IOException
+                    | ClassNotFoundException
+                    | NoSuchMethodException
+                    | InstantiationException
+                    | IllegalAccessException
+                    | InvocationTargetException e) {
                 throw new RuntimeException(e);
             }
         }
@@ -95,23 +111,41 @@
 
     /** {@inheritDoc} */
     @Override
-    public StatusCheckerResult preExecutionCheck(ITestDevice mDevice)
+    public StatusCheckerResult preExecutionCheck(ITestDevice device)
             throws DeviceNotAvailableException {
         if (mDeviceBaselineSetters == null) {
             initializeDeviceBaselineSetters();
         }
         StatusCheckerResult result = new StatusCheckerResult(CheckStatus.SUCCESS);
         StringBuilder errorMessage = new StringBuilder();
+        ExecutorService pool = Executors.newFixedThreadPool(N_THREAD);
+        List<SetterHelper> setterHelperList = new ArrayList<>();
         for (DeviceBaselineSetter setter : mDeviceBaselineSetters) {
             // Check if the device baseline setting should be skipped.
             if (setter.isExperimental()
                     && !mEnableExperimentDeviceBaselineSetters.contains(setter.getName())) {
                 continue;
             }
-            if (!setter.setBaseline(mDevice)) {
-                result.setStatus(CheckStatus.FAILED);
-                errorMessage.append(String.format("Failed to set baseline %s. ", setter.getName()));
+            setterHelperList.add(new SetterHelper(setter, device));
+        }
+        try {
+            // Set device baseline settings in parallel.
+            List<Future<String>> setterResultList = pool.invokeAll(setterHelperList);
+            for (Future<String> setterResult : setterResultList) {
+                if (!SET_SUCCESS_MESSAGE.equals(setterResult.get())) {
+                    result.setStatus(CheckStatus.FAILED);
+                    errorMessage.append(setterResult.get());
+                }
             }
+        } catch (ExecutionException e) {
+            result.setStatus(CheckStatus.FAILED);
+            errorMessage.append(e.getMessage());
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            result.setStatus(CheckStatus.FAILED);
+            errorMessage.append(e.getMessage());
+        } finally {
+            pool.shutdown();
         }
         if (result.getStatus() == CheckStatus.FAILED) {
             result.setErrorMessage(errorMessage.toString());
@@ -120,3 +154,23 @@
         return result;
     }
 }
+
+class SetterHelper implements Callable<String> {
+
+    private final DeviceBaselineSetter mSetter;
+    private final ITestDevice mDevice;
+
+    SetterHelper(DeviceBaselineSetter setter, ITestDevice device) {
+        mSetter = setter;
+        mDevice = device;
+    }
+
+    @Override
+    public String call() throws Exception {
+        // Set device baseline settings.
+        if (!mSetter.setBaseline(mDevice)) {
+            return String.format("Failed to set baseline %s. ", mSetter.getName());
+        }
+        return DeviceBaselineChecker.getSetSuccessMessage();
+    }
+}
diff --git a/src/com/android/tradefed/suite/checker/baseline/DeviceBaselineSetter.java b/src/com/android/tradefed/suite/checker/baseline/DeviceBaselineSetter.java
index 6645b53..29aee7d 100644
--- a/src/com/android/tradefed/suite/checker/baseline/DeviceBaselineSetter.java
+++ b/src/com/android/tradefed/suite/checker/baseline/DeviceBaselineSetter.java
@@ -19,11 +19,24 @@
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
 
+import org.json.JSONException;
+import org.json.JSONObject;
+
 /** Abstract class used to create a device baseline setting. */
 public abstract class DeviceBaselineSetter {
 
+    private final String mName;
+    private final boolean mExperimental;
+
+    public DeviceBaselineSetter(JSONObject object, String name) throws JSONException {
+        mName = name;
+        mExperimental = object.has("experimental") && object.getBoolean("experimental");
+    }
+
     /** Gets the unique name of the setter. */
-    public abstract String getName();
+    public String getName() {
+        return mName;
+    }
 
     /** Sets the baseline setting for the device. */
     public abstract boolean setBaseline(ITestDevice mDevice) throws DeviceNotAvailableException;
@@ -35,6 +48,6 @@
      * applied unless the option enable-device-baseline-settings is set to false.
      */
     public boolean isExperimental() {
-        return false;
+        return mExperimental;
     }
 }
diff --git a/src/com/android/tradefed/suite/checker/baseline/LockSettingsBaselineSetter.java b/src/com/android/tradefed/suite/checker/baseline/LockSettingsBaselineSetter.java
new file mode 100644
index 0000000..65623e2
--- /dev/null
+++ b/src/com/android/tradefed/suite/checker/baseline/LockSettingsBaselineSetter.java
@@ -0,0 +1,59 @@
+/*
+ * 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.tradefed.suite.checker.baseline;
+
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/** A setter to remove screen lock settings. */
+public class LockSettingsBaselineSetter extends DeviceBaselineSetter {
+    private final List<String> mClearPwdCommands;
+    private static final String GET_LOCK_SCREEN_COMMAND = "locksettings get-disabled";
+    private static final String LOCK_SCREEN_OFF_COMMAND = "locksettings set-disabled true";
+    private static final String CLEAR_PWD_COMMAND = "locksettings clear --old %s";
+
+    public LockSettingsBaselineSetter(JSONObject object, String name) throws JSONException {
+        super(object, name);
+        List<String> clearPwdCommands = new ArrayList<>();
+        JSONArray pwds = object.getJSONArray("clear_pwds");
+        for (int index = 0; index < pwds.length(); index++) {
+            clearPwdCommands.add(String.format(CLEAR_PWD_COMMAND, pwds.getString(index)));
+        }
+        mClearPwdCommands = clearPwdCommands;
+    }
+
+    @Override
+    public boolean setBaseline(ITestDevice mDevice) throws DeviceNotAvailableException {
+        if ("true".equals(mDevice.executeShellCommand(GET_LOCK_SCREEN_COMMAND).trim())) {
+            return true;
+        }
+        // Clear old passwords.
+        for (String command : mClearPwdCommands) {
+            mDevice.executeShellCommand(command);
+        }
+        // Turn off lock-screen option.
+        mDevice.executeShellCommand(LOCK_SCREEN_OFF_COMMAND);
+        return "true".equals(mDevice.executeShellCommand(GET_LOCK_SCREEN_COMMAND).trim());
+    }
+}
diff --git a/src/com/android/tradefed/suite/checker/baseline/SettingsBaselineSetter.java b/src/com/android/tradefed/suite/checker/baseline/SettingsBaselineSetter.java
index 72b2f96..17476c2 100644
--- a/src/com/android/tradefed/suite/checker/baseline/SettingsBaselineSetter.java
+++ b/src/com/android/tradefed/suite/checker/baseline/SettingsBaselineSetter.java
@@ -19,22 +19,21 @@
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
 
+import org.json.JSONException;
+import org.json.JSONObject;
+
 /** A common setter to handle device baseline settings via ITestDevice.setSetting. */
 public class SettingsBaselineSetter extends DeviceBaselineSetter {
 
-    private final String mName;
     private final String mNamespace;
     private final String mKey;
     private final String mValue;
-    private final boolean mExperimental;
 
-    public SettingsBaselineSetter(
-            String name, String namespace, String key, String value, boolean experimental) {
-        mName = name;
-        mNamespace = namespace;
-        mKey = key;
-        mValue = value;
-        mExperimental = experimental;
+    public SettingsBaselineSetter(JSONObject object, String name) throws JSONException {
+        super(object, name);
+        mNamespace = object.getString("namespace");
+        mKey = object.getString("key");
+        mValue = object.getString("value");
     }
 
     @Override
@@ -43,14 +42,4 @@
         String settingValue = mDevice.getSetting(mNamespace, mKey);
         return settingValue != null && settingValue.equals(mValue);
     }
-
-    @Override
-    public String getName() {
-        return mName;
-    }
-
-    @Override
-    public boolean isExperimental() {
-        return mExperimental;
-    }
 }
\ No newline at end of file
diff --git a/src/com/android/tradefed/targetprep/GkiDeviceFlashPreparer.java b/src/com/android/tradefed/targetprep/GkiDeviceFlashPreparer.java
index dc4c672..d24fee9 100644
--- a/src/com/android/tradefed/targetprep/GkiDeviceFlashPreparer.java
+++ b/src/com/android/tradefed/targetprep/GkiDeviceFlashPreparer.java
@@ -82,6 +82,11 @@
     private String mDtboImageName = "dtbo.img";
 
     @Option(
+            name = "vendor-dlkm-image-name",
+            description = "The file name in BuildInfo that provides vendor_dlkm image.")
+    private String mVendorDlkmImageName = "vendor_dlkm.img";
+
+    @Option(
             name = "boot-image-file-name",
             description =
                     "The boot image file name to search for if gki-boot-image-name in "
@@ -103,6 +108,13 @@
     private String mDtboImageFileName = "dtbo.img";
 
     @Option(
+            name = "vendor-dlkm-image-file-name",
+            description =
+                    "The vendor_dlkm image file name to search for if vendor-dlkm-image-name in "
+                            + "BuildInfo is a zip file or directory, for example vendor_dlkm.img.")
+    private String mVendorDlkmImageFileName = "vendor_dlkm.img";
+
+    @Option(
             name = "post-reboot-device-into-user-space",
             description = "whether to boot the device in user space after flash.")
     private boolean mPostRebootDeviceIntoUserSpace = true;
@@ -112,6 +124,9 @@
             description = "Whether to wipe device after GKI boot image flash.")
     private boolean mShouldWipeDevice = true;
 
+    @Option(name = "oem-disable-verity", description = "Whether to run oem disable-verity.")
+    private boolean mShouldDisableOemVerity = false;
+
     @Option(
             name = "boot-header-version",
             description = "The version of the boot.img header. Set to 3 by default.")
@@ -193,6 +208,9 @@
     private void flashGki(ITestDevice device, IBuildInfo buildInfo, File tmpDir)
             throws TargetSetupError, DeviceNotAvailableException {
         device.rebootIntoBootloader();
+        if (mShouldDisableOemVerity) {
+            executeFastbootCmd(device, "oem disable-verity");
+        }
         long start = System.currentTimeMillis();
         getHostOptions().takePermit(PermitLimitType.CONCURRENT_FLASHER);
         CLog.v(
@@ -219,8 +237,20 @@
                                 tmpDir);
                 executeFastbootCmd(device, "flash", "dtbo", dtboImg.getAbsolutePath());
             }
+
             executeFastbootCmd(device, "flash", "boot", mBootImg.getAbsolutePath());
 
+            if (buildInfo.getFile(mVendorDlkmImageName) != null) {
+                File vendorDlkmImg =
+                        getRequestedFile(
+                                device,
+                                mVendorDlkmImageFileName,
+                                buildInfo.getFile(mVendorDlkmImageName),
+                                tmpDir);
+                device.rebootIntoFastbootd();
+                executeFastbootCmd(device, "flash", "vendor_dlkm", vendorDlkmImg.getAbsolutePath());
+            }
+
             if (mShouldWipeDevice) {
                 executeFastbootCmd(device, "-w");
             }
diff --git a/src/com/android/tradefed/targetprep/RunOnSecondaryUserTargetPreparer.java b/src/com/android/tradefed/targetprep/RunOnSecondaryUserTargetPreparer.java
index 3847358..6fa973d 100644
--- a/src/com/android/tradefed/targetprep/RunOnSecondaryUserTargetPreparer.java
+++ b/src/com/android/tradefed/targetprep/RunOnSecondaryUserTargetPreparer.java
@@ -154,7 +154,7 @@
                         "instrumentation-arg", SKIP_TESTS_REASON_KEY, reason.replace(" ", "\\ "));
             } catch (ConfigurationException e) {
                 throw new TargetSetupError(
-                        "Error setting skip-tests-reason", device.getDeviceDescriptor());
+                        "Error setting skip-tests-reason", e, device.getDeviceDescriptor());
             }
         }
 
diff --git a/src/com/android/tradefed/targetprep/RunOnWorkProfileTargetPreparer.java b/src/com/android/tradefed/targetprep/RunOnWorkProfileTargetPreparer.java
index aba3523..8c1bf8e 100644
--- a/src/com/android/tradefed/targetprep/RunOnWorkProfileTargetPreparer.java
+++ b/src/com/android/tradefed/targetprep/RunOnWorkProfileTargetPreparer.java
@@ -195,7 +195,7 @@
                         "instrumentation-arg", SKIP_TESTS_REASON_KEY, reason.replace(" ", "\\ "));
             } catch (ConfigurationException e) {
                 throw new TargetSetupError(
-                        "Error setting skip-tests-reason", device.getDeviceDescriptor());
+                        "Error setting skip-tests-reason", e, device.getDeviceDescriptor());
             }
         }
 
diff --git a/src/com/android/tradefed/targetprep/TestAppInstallSetup.java b/src/com/android/tradefed/targetprep/TestAppInstallSetup.java
index 657359c..bc60a5b 100644
--- a/src/com/android/tradefed/targetprep/TestAppInstallSetup.java
+++ b/src/com/android/tradefed/targetprep/TestAppInstallSetup.java
@@ -33,6 +33,7 @@
 import com.android.tradefed.invoker.InvocationContext;
 import com.android.tradefed.invoker.TestInformation;
 import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.observatory.IDiscoverDependencies;
 import com.android.tradefed.result.error.DeviceErrorIdentifier;
 import com.android.tradefed.result.error.InfraErrorIdentifier;
 import com.android.tradefed.testtype.IAbi;
@@ -77,7 +78,8 @@
  * the first.
  */
 @OptionClass(alias = "tests-zip-app")
-public class TestAppInstallSetup extends BaseTargetPreparer implements IAbiReceiver {
+public class TestAppInstallSetup extends BaseTargetPreparer
+        implements IAbiReceiver, IDiscoverDependencies {
 
     /** The mode the apk should be install in. */
     private enum InstallMode {
@@ -824,4 +826,17 @@
 
         return incrementalInstallSessionBuilder;
     }
+
+    @Override
+    public Set<String> reportDependencies() {
+        Set<String> deps = new HashSet<String>();
+        for (File f : getTestsFileName()) {
+            if (!f.exists()) deps.add(f.getName());
+        }
+        for (String testAppNames : mSplitApkFileNames) {
+            List<String> apkNames = Arrays.asList(testAppNames.split(","));
+            deps.addAll(apkNames);
+        }
+        return deps;
+    }
 }
diff --git a/src/com/android/tradefed/testtype/SubprocessTfLauncher.java b/src/com/android/tradefed/testtype/SubprocessTfLauncher.java
index 67a506a..5f1c571 100644
--- a/src/com/android/tradefed/testtype/SubprocessTfLauncher.java
+++ b/src/com/android/tradefed/testtype/SubprocessTfLauncher.java
@@ -25,6 +25,7 @@
 import com.android.tradefed.config.proxy.AutomatedReporters;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.error.HarnessRuntimeException;
+import com.android.tradefed.invoker.DelegatedInvocationExecution;
 import com.android.tradefed.invoker.IInvocationContext;
 import com.android.tradefed.invoker.RemoteInvocationExecution;
 import com.android.tradefed.invoker.TestInformation;
@@ -276,6 +277,7 @@
         mRunUtil.unsetEnvVariable(GlobalConfiguration.GLOBAL_CONFIG_SERVER_CONFIG_VARIABLE);
         mRunUtil.unsetEnvVariable(ANDROID_SERIAL_VAR);
         mRunUtil.unsetEnvVariable(RemoteInvocationExecution.START_FEATURE_SERVER);
+        mRunUtil.unsetEnvVariable(DelegatedInvocationExecution.DELEGATED_MODE_VAR);
         for (String variable : AutomatedReporters.REPORTER_MAPPING) {
             mRunUtil.unsetEnvVariable(variable);
         }
diff --git a/src/com/android/tradefed/testtype/TfTestLauncher.java b/src/com/android/tradefed/testtype/TfTestLauncher.java
index e3b65e7..f09128a 100644
--- a/src/com/android/tradefed/testtype/TfTestLauncher.java
+++ b/src/com/android/tradefed/testtype/TfTestLauncher.java
@@ -96,6 +96,9 @@
             + "Can be repeated.")
     private List<String> mSubApkPath = new ArrayList<String>();
 
+    @Option(name = "skip-temp-dir-check", description = "Whether or not to skip temp dir check.")
+    private boolean mSkipTmpDirCheck = false;
+
     // The regex pattern of temp files to be found in the temporary dir of the subprocess.
     // Any file not matching the patterns, or multiple files in the temporary dir match the same
     // pattern, is considered as test failure.
@@ -228,6 +231,9 @@
      */
     @VisibleForTesting
     protected void testTmpDirClean(File tmpDir, ITestInvocationListener listener) {
+        if (mSkipTmpDirCheck) {
+            return;
+        }
         listener.testRunStarted("temporaryFiles", 1);
         TestDescription tid = new TestDescription("temporary-files", "testIfClean");
         listener.testStarted(tid);
diff --git a/src/com/android/tradefed/testtype/coverage/CoverageOptions.java b/src/com/android/tradefed/testtype/coverage/CoverageOptions.java
index dfb3263..2c5f7f4 100644
--- a/src/com/android/tradefed/testtype/coverage/CoverageOptions.java
+++ b/src/com/android/tradefed/testtype/coverage/CoverageOptions.java
@@ -61,6 +61,9 @@
     )
     private List<String> mCoverageProcesses = new ArrayList<>();
 
+    @Option(name = "merge-coverage", description = "Merge coverage measurements before logging.")
+    private boolean mMergeCoverage = false;
+
     @Option(
             name = "reset-coverage-before-test",
             description = "Reset coverage before running each test.")
@@ -113,6 +116,11 @@
         return ImmutableList.copyOf(mCoverageProcesses);
     }
 
+    /** Returns whether to merge coverage measurements together before logging. */
+    public boolean shouldMergeCoverage() {
+        return mMergeCoverage;
+    }
+
     /**
      * Returns whether coverage measurements should be reset before each test.
      *
diff --git a/src/com/android/tradefed/testtype/suite/BaseTestSuite.java b/src/com/android/tradefed/testtype/suite/BaseTestSuite.java
index 1f1c98f..1b04f5f 100644
--- a/src/com/android/tradefed/testtype/suite/BaseTestSuite.java
+++ b/src/com/android/tradefed/testtype/suite/BaseTestSuite.java
@@ -229,6 +229,10 @@
     private Set<IAbi> mAbis = new LinkedHashSet<>();
     private Set<DeviceFoldableState> mFoldableStates = new LinkedHashSet<>();
 
+    public void setSkipjarLoading(boolean skipJarLoading) {
+        mSkipJarLoading = skipJarLoading;
+    }
+
     /** {@inheritDoc} */
     @Override
     public LinkedHashMap<String, IConfiguration> loadTests() {
diff --git a/src/com/android/tradefed/testtype/suite/GranularRetriableTestWrapper.java b/src/com/android/tradefed/testtype/suite/GranularRetriableTestWrapper.java
index 0220229..c13803c 100644
--- a/src/com/android/tradefed/testtype/suite/GranularRetriableTestWrapper.java
+++ b/src/com/android/tradefed/testtype/suite/GranularRetriableTestWrapper.java
@@ -31,9 +31,11 @@
 import com.android.tradefed.invoker.logger.CurrentInvocation;
 import com.android.tradefed.invoker.logger.CurrentInvocation.IsolationGrade;
 import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
 import com.android.tradefed.result.FailureDescription;
 import com.android.tradefed.result.ILogSaver;
 import com.android.tradefed.result.ITestInvocationListener;
+import com.android.tradefed.result.ResultAndLogForwarder;
 import com.android.tradefed.result.TestRunResult;
 import com.android.tradefed.result.error.ErrorIdentifier;
 import com.android.tradefed.retry.IRetryDecision;
@@ -48,6 +50,7 @@
 
 import java.time.Duration;
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
@@ -316,6 +319,8 @@
     private final void intraModuleRun(TestInformation testInfo, ITestInvocationListener runListener)
             throws DeviceNotAvailableException {
         mMainGranularRunListener.setAttemptIsolation(CurrentInvocation.runCurrentIsolation());
+        StartEndCollector startEndCollector = new StartEndCollector(runListener);
+        runListener = startEndCollector;
         try {
             List<IMetricCollector> clonedCollectors = cloneCollectors(mRunMetricCollectors);
             if (mTest instanceof IMetricCollectorReceiver) {
@@ -347,7 +352,15 @@
             CLog.e("Module '%s' - test '%s' threw exception:", mModuleId, mTest.getClass());
             CLog.e(re);
             CLog.e("Proceeding to the next test.");
+            if (!startEndCollector.mRunStartReported) {
+                CLog.e("Event mismatch ! the test runner didn't report any testRunStart.");
+                runListener.testRunStarted(mModule.getId(), 0);
+            }
             runListener.testRunFailed(createFromException(re));
+            if (!startEndCollector.mRunEndedReported) {
+                CLog.e("Event mismatch ! the test runner didn't report any testRunEnded.");
+                runListener.testRunEnded(0L, new HashMap<String, Metric>());
+            }
         } catch (DeviceUnresponsiveException due) {
             // being able to catch a DeviceUnresponsiveException here implies that recovery was
             // successful, and test execution should proceed to next module.
@@ -433,4 +446,46 @@
         }
         return failure;
     }
+
+    /** Class helper to catch missing run start and end. */
+    public class StartEndCollector extends ResultAndLogForwarder {
+
+        public boolean mRunStartReported = false;
+        public boolean mRunEndedReported = false;
+
+        StartEndCollector(ITestInvocationListener listener) {
+            super(listener);
+        }
+
+        @Override
+        public void testRunStarted(String runName, int testCount) {
+            super.testRunStarted(runName, testCount);
+            mRunStartReported = true;
+        }
+
+        @Override
+        public void testRunStarted(String runName, int testCount, int attemptNumber) {
+            super.testRunStarted(runName, testCount, attemptNumber);
+            mRunStartReported = true;
+        }
+
+        @Override
+        public void testRunStarted(
+                String runName, int testCount, int attemptNumber, long startTime) {
+            super.testRunStarted(runName, testCount, attemptNumber, startTime);
+            mRunStartReported = true;
+        }
+
+        @Override
+        public void testRunEnded(long elapsedTime, HashMap<String, Metric> runMetrics) {
+            super.testRunEnded(elapsedTime, runMetrics);
+            mRunEndedReported = true;
+        }
+
+        @Override
+        public void testRunEnded(long elapsedTimeMillis, Map<String, String> runMetrics) {
+            super.testRunEnded(elapsedTimeMillis, runMetrics);
+            mRunEndedReported = true;
+        }
+    }
 }
diff --git a/src/com/android/tradefed/testtype/suite/ITestSuite.java b/src/com/android/tradefed/testtype/suite/ITestSuite.java
index 6118bc0..573ccc0 100644
--- a/src/com/android/tradefed/testtype/suite/ITestSuite.java
+++ b/src/com/android/tradefed/testtype/suite/ITestSuite.java
@@ -48,6 +48,7 @@
 import com.android.tradefed.invoker.logger.TfObjectTracker;
 import com.android.tradefed.invoker.shard.token.ITokenRequest;
 import com.android.tradefed.invoker.shard.token.TokenProperty;
+import com.android.tradefed.invoker.tracing.CloseableTraceScope;
 import com.android.tradefed.log.ITestLogger;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
@@ -394,8 +395,12 @@
 
     public File getTestsDir() throws FileNotFoundException {
         IBuildInfo build = getBuildInfo();
+        File testsDir = null;
         if (build instanceof IDeviceBuildInfo) {
-            return ((IDeviceBuildInfo) build).getTestsDir();
+            testsDir = ((IDeviceBuildInfo) build).getTestsDir();
+        }
+        if (testsDir != null && testsDir.exists()) {
+            return testsDir;
         }
         // TODO: handle multi build?
         throw new FileNotFoundException("Could not found a tests dir folder.");
@@ -546,46 +551,47 @@
             mDirectModule.setBuild(mBuildInfo);
             return runModules;
         }
+        try (CloseableTraceScope ignore = new CloseableTraceScope("suite:createExecutionList")) {
+            LinkedHashMap<String, IConfiguration> runConfig = loadAndFilter();
+            if (runConfig.isEmpty()) {
+                CLog.i("No config were loaded. Nothing to run.");
+                return runModules;
+            }
 
-        LinkedHashMap<String, IConfiguration> runConfig = loadAndFilter();
-        if (runConfig.isEmpty()) {
-            CLog.i("No config were loaded. Nothing to run.");
+            Map<String, List<ITargetPreparer>> suitePreparersPerDevice =
+                    getAllowedPreparerPerDevice(mMainConfiguration);
+
+            for (Entry<String, IConfiguration> config : runConfig.entrySet()) {
+                // Validate the configuration, it will throw if not valid.
+                ValidateSuiteConfigHelper.validateConfig(config.getValue());
+                Map<String, List<ITargetPreparer>> preparersPerDevice =
+                        getPreparerPerDevice(config.getValue());
+                ModuleDefinition module =
+                        new ModuleDefinition(
+                                config.getKey(),
+                                config.getValue().getTests(),
+                                preparersPerDevice,
+                                suitePreparersPerDevice,
+                                config.getValue().getMultiTargetPreparers(),
+                                config.getValue());
+                if (mDisableAutoRetryTimeReporting) {
+                    module.disableAutoRetryReportingTime();
+                }
+                module.setDevice(mDevice);
+                module.setBuild(mBuildInfo);
+                runModules.add(module);
+            }
+
+            /** Randomize all the modules to be ran if random-order is set and no sharding. */
+            if (mRandomOrder) {
+                randomizeTestModules(runModules, mRandomSeed);
+            }
+
+            CLog.logAndDisplay(LogLevel.DEBUG, "[Total Unique Modules = %s]", runModules.size());
+            // Free the map once we are done with it.
+            runConfig = null;
             return runModules;
         }
-
-        Map<String, List<ITargetPreparer>> suitePreparersPerDevice =
-                getAllowedPreparerPerDevice(mMainConfiguration);
-
-        for (Entry<String, IConfiguration> config : runConfig.entrySet()) {
-            // Validate the configuration, it will throw if not valid.
-            ValidateSuiteConfigHelper.validateConfig(config.getValue());
-            Map<String, List<ITargetPreparer>> preparersPerDevice =
-                    getPreparerPerDevice(config.getValue());
-            ModuleDefinition module =
-                    new ModuleDefinition(
-                            config.getKey(),
-                            config.getValue().getTests(),
-                            preparersPerDevice,
-                            suitePreparersPerDevice,
-                            config.getValue().getMultiTargetPreparers(),
-                            config.getValue());
-            if (mDisableAutoRetryTimeReporting) {
-                module.disableAutoRetryReportingTime();
-            }
-            module.setDevice(mDevice);
-            module.setBuild(mBuildInfo);
-            runModules.add(module);
-        }
-
-        /** Randomize all the modules to be ran if random-order is set and no sharding.*/
-        if (mRandomOrder) {
-            randomizeTestModules(runModules, mRandomSeed);
-        }
-
-        CLog.logAndDisplay(LogLevel.DEBUG, "[Total Unique Modules = %s]", runModules.size());
-        // Free the map once we are done with it.
-        runConfig = null;
-        return runModules;
     }
 
     /**
@@ -731,63 +737,66 @@
                     continue;
                 }
 
-                // Populate the module context with devices and builds
-                for (String deviceName : mContext.getDeviceConfigNames()) {
-                    module.getModuleInvocationContext()
-                            .addAllocatedDevice(deviceName, mContext.getDevice(deviceName));
-                    module.getModuleInvocationContext()
-                            .addDeviceBuildInfo(deviceName, mContext.getBuildInfo(deviceName));
-                }
-                // Add isolation status before module start for reporting
-                if (!IsolationGrade.NOT_ISOLATED.equals(
-                        CurrentInvocation.moduleCurrentIsolation())) {
-                    module.getModuleInvocationContext()
-                            .addInvocationAttribute(
-                                    ModuleDefinition.MODULE_ISOLATED,
-                                    CurrentInvocation.moduleCurrentIsolation().toString());
-                }
-                // Only the module callback will be called here.
-                ITestInvocationListener listenerWithCollectors = listener;
-                if (mMetricCollectors != null) {
-                    for (IMetricCollector collector :
-                         CollectorHelper.cloneCollectors(mMetricCollectors)) {
-                        if (collector.isDisabled()) {
-                            CLog.d("%s has been disabled. Skipping.", collector);
-                        } else {
-                            if (collector instanceof IConfigurationReceiver) {
-                                ((IConfigurationReceiver) collector)
-                                        .setConfiguration(module.getModuleConfiguration());
+                try (CloseableTraceScope ignore = new CloseableTraceScope(module.getId())) {
+                    // Populate the module context with devices and builds
+                    for (String deviceName : mContext.getDeviceConfigNames()) {
+                        module.getModuleInvocationContext()
+                                .addAllocatedDevice(deviceName, mContext.getDevice(deviceName));
+                        module.getModuleInvocationContext()
+                                .addDeviceBuildInfo(deviceName, mContext.getBuildInfo(deviceName));
+                    }
+                    // Add isolation status before module start for reporting
+                    if (!IsolationGrade.NOT_ISOLATED.equals(
+                            CurrentInvocation.moduleCurrentIsolation())) {
+                        module.getModuleInvocationContext()
+                                .addInvocationAttribute(
+                                        ModuleDefinition.MODULE_ISOLATED,
+                                        CurrentInvocation.moduleCurrentIsolation().toString());
+                    }
+                    // Only the module callback will be called here.
+                    ITestInvocationListener listenerWithCollectors = listener;
+                    if (mMetricCollectors != null) {
+                        for (IMetricCollector collector :
+                                CollectorHelper.cloneCollectors(mMetricCollectors)) {
+                            if (collector.isDisabled()) {
+                                CLog.d("%s has been disabled. Skipping.", collector);
+                            } else {
+                                if (collector instanceof IConfigurationReceiver) {
+                                    ((IConfigurationReceiver) collector)
+                                            .setConfiguration(module.getModuleConfiguration());
+                                }
+                                listenerWithCollectors =
+                                        collector.init(
+                                                module.getModuleInvocationContext(),
+                                                listenerWithCollectors);
+                                TfObjectTracker.countWithParents(collector.getClass());
                             }
-                            listenerWithCollectors =
-                                    collector.init(
-                                            module.getModuleInvocationContext(),
-                                            listenerWithCollectors);
-                            TfObjectTracker.countWithParents(collector.getClass());
                         }
                     }
+                    listenerWithCollectors.testModuleStarted(module.getModuleInvocationContext());
+                    mModuleInProgress = module;
+                    // Trigger module start on module level listener too
+                    new ResultForwarder(moduleListeners)
+                            .testModuleStarted(module.getModuleInvocationContext());
+                    TestInformation moduleInfo =
+                            TestInformation.createModuleTestInfo(
+                                    testInfo, module.getModuleInvocationContext());
+                    try {
+                        runSingleModule(
+                                module, moduleInfo, listener, moduleListeners, failureListener);
+                    } finally {
+                        // Trigger module end on module level listener too
+                        new ResultForwarder(moduleListeners).testModuleEnded();
+                        // clear out module invocation context since we are now done with module
+                        // execution
+                        listenerWithCollectors.testModuleEnded();
+                        mModuleInProgress = null;
+                        // Following modules will not be isolated if no action is taken
+                        CurrentInvocation.setModuleIsolation(IsolationGrade.NOT_ISOLATED);
+                    }
+                    // Module isolation routine
+                    moduleIsolation(mContext, listener);
                 }
-                listenerWithCollectors.testModuleStarted(module.getModuleInvocationContext());
-                mModuleInProgress = module;
-                // Trigger module start on module level listener too
-                new ResultForwarder(moduleListeners)
-                        .testModuleStarted(module.getModuleInvocationContext());
-                TestInformation moduleInfo =
-                        TestInformation.createModuleTestInfo(
-                                testInfo, module.getModuleInvocationContext());
-                try {
-                    runSingleModule(module, moduleInfo, listener, moduleListeners, failureListener);
-                } finally {
-                    // Trigger module end on module level listener too
-                    new ResultForwarder(moduleListeners).testModuleEnded();
-                    // clear out module invocation context since we are now done with module
-                    // execution
-                    listenerWithCollectors.testModuleEnded();
-                    mModuleInProgress = null;
-                    // Following modules will not be isolated if no action is taken
-                    CurrentInvocation.setModuleIsolation(IsolationGrade.NOT_ISOLATED);
-                }
-                // Module isolation routine
-                moduleIsolation(mContext, listener);
             }
         } catch (DeviceNotAvailableException e) {
             CLog.e(
diff --git a/src/com/android/tradefed/testtype/suite/TestMappingSuiteRunner.java b/src/com/android/tradefed/testtype/suite/TestMappingSuiteRunner.java
index 40da0c4..99027a2 100644
--- a/src/com/android/tradefed/testtype/suite/TestMappingSuiteRunner.java
+++ b/src/com/android/tradefed/testtype/suite/TestMappingSuiteRunner.java
@@ -122,6 +122,15 @@
                             + "only the tests on the triggering device build will be run.")
     private List<String> mAdditionalTestMappingZips = new ArrayList<>();
 
+    @Option(
+            name = "test-mapping-unmatched-file-pattern-paths",
+            description =
+                    "A list of modified paths that does not match with a certain file_pattern in "
+                            + "the TEST_MAPPING file. This is used only for Work Node, and handled "
+                            + "by provider service. If none is specified, all tests are needed "
+                            + "to run for the given change.")
+    private Set<String> mUnmatchedFilePatternPaths = new HashSet<>();
+
     /** Special definition in the test mapping structure. */
     private static final String TEST_MAPPING_INCLUDE_FILTER = "include-filter";
 
@@ -129,6 +138,10 @@
 
     private IBuildInfo mBuildInfo;
 
+    public TestMappingSuiteRunner() {
+        setSkipjarLoading(true);
+    }
+
     /**
      * Load the tests configuration that will be run. Each tests is defined by a {@link
      * IConfiguration} and a unique name under which it will report results. There are 2 ways to
diff --git a/src/com/android/tradefed/util/GCSFileDownloader.java b/src/com/android/tradefed/util/GCSFileDownloader.java
index bcea029..ab49a98 100644
--- a/src/com/android/tradefed/util/GCSFileDownloader.java
+++ b/src/com/android/tradefed/util/GCSFileDownloader.java
@@ -248,8 +248,10 @@
         for (String subRemoteFolder : subRemoteFolders) {
             String subFolderName = Paths.get(subRemoteFolder).getFileName().toString();
             File subFolder = new File(localFolder, subFolderName);
-            if (new File(localFolder, subFolderName).exists()
-                    && !new File(localFolder, subFolderName).isDirectory()) {
+            if (!subFolder.exists()) {
+                return false;
+            }
+            if (!subFolder.isDirectory()) {
                 CLog.w("%s exists as a non-directory.", subFolder);
                 subFolder = new File(localFolder, subFolderName + "_folder");
             }
diff --git a/src/com/android/tradefed/util/SubprocessExceptionParser.java b/src/com/android/tradefed/util/SubprocessExceptionParser.java
index 5214a1a..91ad69f 100644
--- a/src/com/android/tradefed/util/SubprocessExceptionParser.java
+++ b/src/com/android/tradefed/util/SubprocessExceptionParser.java
@@ -19,6 +19,7 @@
 import com.android.tradefed.error.HarnessRuntimeException;
 import com.android.tradefed.error.IHarnessException;
 import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.result.error.ErrorIdentifier;
 import com.android.tradefed.result.error.InfraErrorIdentifier;
 import com.android.tradefed.sandbox.TradefedSandboxRunner;
 
@@ -83,6 +84,10 @@
                                 + "Using HarnessRuntimeException instead.");
             }
         }
-        throw new HarnessRuntimeException(message, InfraErrorIdentifier.UNDETERMINED);
+        ErrorIdentifier id = InfraErrorIdentifier.UNDETERMINED;
+        if (CommandStatus.TIMED_OUT.equals(result.getStatus())) {
+            id = InfraErrorIdentifier.INVOCATION_TIMEOUT;
+        }
+        throw new HarnessRuntimeException(message, id);
     }
 }
diff --git a/src/com/android/tradefed/util/SubprocessTestResultsParser.java b/src/com/android/tradefed/util/SubprocessTestResultsParser.java
index 6caeac3..7a1d697 100644
--- a/src/com/android/tradefed/util/SubprocessTestResultsParser.java
+++ b/src/com/android/tradefed/util/SubprocessTestResultsParser.java
@@ -20,6 +20,8 @@
 import com.android.tradefed.invoker.logger.InvocationMetricLogger;
 import com.android.tradefed.invoker.logger.InvocationMetricLogger.InvocationGroupMetricKey;
 import com.android.tradefed.invoker.logger.InvocationMetricLogger.InvocationMetricKey;
+import com.android.tradefed.invoker.tracing.ActiveTrace;
+import com.android.tradefed.invoker.tracing.TracingLogger;
 import com.android.tradefed.invoker.logger.TfObjectTracker;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
@@ -554,13 +556,14 @@
                             assosInfo.mDataName);
                     return;
                 }
-                try (InputStreamSource source = new FileInputStreamSource(path)) {
+                try (InputStreamSource source = new FileInputStreamSource(path, true)) {
                     LogDataType type = file.getType();
-                    // File might have already been compressed
-                    if (file.getPath().endsWith(LogDataType.ZIP.getFileExt())) {
-                        type = LogDataType.ZIP;
-                    }
                     CLog.d("Logging %s from subprocess: %s ", assosInfo.mDataName, file.getPath());
+                    if (ActiveTrace.TRACE_KEY.equals(assosInfo.mDataName)
+                            && LogDataType.PERFETTO.equals(type)) {
+                        CLog.d("Log the subprocess trace");
+                        TracingLogger.getActiveTrace().addSubprocessTrace(path);
+                    }
                     mListener.testLog(name, type, source);
                 }
             } else {
diff --git a/src/com/android/tradefed/util/testmapping/TestMapping.java b/src/com/android/tradefed/util/testmapping/TestMapping.java
index 4f5f4fc..62f5cb4 100644
--- a/src/com/android/tradefed/util/testmapping/TestMapping.java
+++ b/src/com/android/tradefed/util/testmapping/TestMapping.java
@@ -212,7 +212,8 @@
 
         if (errorMessage != null) {
             CLog.e(errorMessage);
-            throw new RuntimeException(errorMessage);
+            throw new HarnessRuntimeException(
+                    errorMessage, InfraErrorIdentifier.TEST_MAPPING_FILE_FORMAT_ISSUE);
         }
     }
 
diff --git a/test_framework/com/android/tradefed/device/metric/BluetoothConnectionLatencyCollector.java b/test_framework/com/android/tradefed/device/metric/BluetoothConnectionLatencyCollector.java
index 711eba6..4b294fe 100644
--- a/test_framework/com/android/tradefed/device/metric/BluetoothConnectionLatencyCollector.java
+++ b/test_framework/com/android/tradefed/device/metric/BluetoothConnectionLatencyCollector.java
@@ -44,7 +44,7 @@
             "bluetooth_connection_latency";
 
     /** A map associates Bluetooth profile number to the descriptive name used for metric key. */
-    protected static final ImmutableMap<Integer, String> bluetoothProfilesMap =
+    protected static final ImmutableMap<Integer, String> BLUETOOTH_PROFILES_MAP =
             ImmutableMap.<Integer, String>builder()
                     .put(BluetoothProfile.HEADSET.getProfile(), "headset")
                     .put(BluetoothProfile.A2DP.getProfile(), "a2dp")
@@ -105,12 +105,12 @@
             }
             int bluetoothProfile = metric.getDimensionLeafValuesInWhat(0).getValueInt();
             String metricKey;
-            if (bluetoothProfilesMap.containsKey(bluetoothProfile)) {
+            if (BLUETOOTH_PROFILES_MAP.containsKey(bluetoothProfile)) {
                 metricKey =
                         String.join(
                                 "_",
                                 BLUETOOTH_CONNECTION_LATENCY_METRIC_KEY,
-                                bluetoothProfilesMap.get(bluetoothProfile),
+                                BLUETOOTH_PROFILES_MAP.get(bluetoothProfile),
                                 "ms");
             } else {
                 metricKey =
diff --git a/test_framework/com/android/tradefed/device/metric/BluetoothConnectionStateCollector.java b/test_framework/com/android/tradefed/device/metric/BluetoothConnectionStateCollector.java
index a55fcda..1fc4d57 100644
--- a/test_framework/com/android/tradefed/device/metric/BluetoothConnectionStateCollector.java
+++ b/test_framework/com/android/tradefed/device/metric/BluetoothConnectionStateCollector.java
@@ -69,15 +69,15 @@
             HashMap<String, List<Long>> btProfilesStates) {
         for (String key : btProfilesStates.keySet()) {
             List<Long> states = btProfilesStates.get(key);
-            String metricKey2 = String.join("_", key, "connection_state_changed");
+            String metricKey = String.join("_", key, "connection_state_changed");
             NumericValues values = NumericValues.newBuilder().addAllNumericValue(states).build();
             Measurements measurements = Measurements.newBuilder().setNumericValues(values).build();
 
             CLog.d(
                     "Adding metric on device %s with key %s and values %s",
-                    device.getSerialNumber(), metricKey2, states.toString());
+                    device.getSerialNumber(), metricKey, states.toString());
             runData.addMetricForDevice(
-                    device, metricKey2, Metric.newBuilder().setMeasurements(measurements));
+                    device, metricKey, Metric.newBuilder().setMeasurements(measurements));
         }
     }
 
@@ -85,25 +85,28 @@
             ITestDevice device,
             EventMetricData metric,
             HashMap<String, List<Long>> btProfilesStates) {
-        Atom atom = metric.getAtom();
-        if (atom.hasBluetoothConnectionStateChanged()) {
-            BluetoothConnectionStateChanged bluetoothConnectionStateChanged =
-                    atom.getBluetoothConnectionStateChanged();
-            int btState = bluetoothConnectionStateChanged.getState().getNumber();
-            int btProfile = bluetoothConnectionStateChanged.getBtProfile();
+        Atom atom = metric.hasAtom() ? metric.getAtom() : metric.getAggregatedAtomInfo().getAtom();
+        if (!atom.hasBluetoothConnectionStateChanged()) {
             CLog.d(
-                    "Processing connection state changed atom on device %s for profile number %d",
-                    device.getSerialNumber(), btProfile);
-            if (bluetoothProfilesMap.containsKey(btProfile)) {
-                String btProfileName = bluetoothProfilesMap.get(btProfile);
-                List<Long> states =
-                        btProfilesStates.getOrDefault(btProfileName, new ArrayList<Long>());
-                states.add((long) btState);
-                btProfilesStates.put(btProfileName, states);
-                CLog.d(
-                        "Processed connection state changed atom on device %s profile %s value %d",
-                        device.getSerialNumber(), btProfileName, btState);
-            }
+                    "Atom does not have a bluetooth_connection_state_changed info."
+                            + " Skipping reporting");
+            return;
+        }
+        BluetoothConnectionStateChanged bluetoothConnectionStateChanged =
+                atom.getBluetoothConnectionStateChanged();
+        int btState = bluetoothConnectionStateChanged.getState().getNumber();
+        int btProfile = bluetoothConnectionStateChanged.getBtProfile();
+        CLog.d(
+                "Processing connection state changed atom on device %s for profile number %d",
+                device.getSerialNumber(), btProfile);
+        if (BLUETOOTH_PROFILES_MAP.containsKey(btProfile)) {
+            String btProfileName = BLUETOOTH_PROFILES_MAP.get(btProfile);
+            List<Long> states = btProfilesStates.getOrDefault(btProfileName, new ArrayList<Long>());
+            states.add((long) btState);
+            btProfilesStates.put(btProfileName, states);
+            CLog.d(
+                    "Processed connection state changed atom on device %s profile %s value %d",
+                    device.getSerialNumber(), btProfileName, btState);
         }
     }
 }
diff --git a/test_framework/com/android/tradefed/device/metric/HostStatsdMetricCollector.java b/test_framework/com/android/tradefed/device/metric/HostStatsdMetricCollector.java
index cbaeb0d..3528631 100644
--- a/test_framework/com/android/tradefed/device/metric/HostStatsdMetricCollector.java
+++ b/test_framework/com/android/tradefed/device/metric/HostStatsdMetricCollector.java
@@ -61,7 +61,7 @@
 
     @Override
     public void onTestRunStart(DeviceMetricData runData) throws DeviceNotAvailableException {
-        if (mPerRun) {
+        if (mPerRun && mBinaryConfig != null) {
             mDeviceConfigIds.clear();
             startCollection();
         }
@@ -69,7 +69,7 @@
 
     @Override
     public void onTestStart(DeviceMetricData testData) throws DeviceNotAvailableException {
-        if (!mPerRun) {
+        if (!mPerRun && mBinaryConfig != null) {
             mDeviceConfigIds.clear();
             startCollection();
         }
@@ -84,7 +84,7 @@
     public void onTestEnd(
             DeviceMetricData testData, final Map<String, Metric> currentTestCaseMetrics)
             throws DeviceNotAvailableException {
-        if (!mPerRun) {
+        if (!mPerRun && mBinaryConfig != null) {
             stopCollection(testData, !mTestFailed);
         }
         mTestCount++;
@@ -94,7 +94,7 @@
     @Override
     public void onTestRunEnd(DeviceMetricData runData, final Map<String, Metric> currentRunMetrics)
             throws DeviceNotAvailableException {
-        if (mPerRun) {
+        if (mPerRun && mBinaryConfig != null) {
             stopCollection(runData, true);
         }
     }
diff --git a/test_framework/com/android/tradefed/postprocessor/PerfettoGenericPostProcessor.java b/test_framework/com/android/tradefed/postprocessor/PerfettoGenericPostProcessor.java
index 02a48c7..d1b9930 100644
--- a/test_framework/com/android/tradefed/postprocessor/PerfettoGenericPostProcessor.java
+++ b/test_framework/com/android/tradefed/postprocessor/PerfettoGenericPostProcessor.java
@@ -438,8 +438,8 @@
         Map<String, Metric.Builder> convertedMetrics = new HashMap<String, Metric.Builder>();
         List<String> keyPrefixes = new ArrayList<String>();
 
-        // Keys that will be used to prefix the other keys in the same proto message.
-        List<String> keyPrefixOtherFields = new ArrayList<>();
+        // Key that will be used to prefix the other keys in the same proto message.
+        String keyPrefixOtherFields = "";
 
         // TODO(b/15014555): Cleanup the parsing logic.
         for (Entry<FieldDescriptor, Object> entry : fields.entrySet()) {
@@ -448,7 +448,10 @@
                     // Check if the current field has to be used as prefix for other fields
                     // and add it to the list of prefixes.
                     if (mPerfettoPrefixKeyFields.contains(entry.getKey().toString())) {
-                        keyPrefixOtherFields.add(String.format("%s-%s",
+                        if (!keyPrefixOtherFields.isEmpty()) {
+                            keyPrefixOtherFields = keyPrefixOtherFields.concat("-");
+                        }
+                        keyPrefixOtherFields = keyPrefixOtherFields.concat(String.format("%s-%s",
                                 entry.getKey().getName().toString(), entry.getValue().toString()));
                         continue;
                     }
@@ -476,7 +479,10 @@
                                     entry.getKey().getName().toString(),
                                     entry.getValue().toString()));
                     if (mPerfettoPrefixKeyFields.contains(entry.getKey().toString())) {
-                        keyPrefixOtherFields.add(String.format("%s-%s",
+                        if (!keyPrefixOtherFields.isEmpty()) {
+                            keyPrefixOtherFields = keyPrefixOtherFields.concat("-");
+                        }
+                        keyPrefixOtherFields =  keyPrefixOtherFields.concat(String.format("%s-%s",
                                 entry.getKey().getName().toString(), entry.getValue().toString()));
                     }
                 }
@@ -556,9 +562,9 @@
         // Add prefix key to all the keys in current proto message which has numeric values.
         Map<String, Metric.Builder> additionalConvertedMetrics =
                 new HashMap<String, Metric.Builder>();
-        for (String prefix : keyPrefixOtherFields) {
+        if (!keyPrefixOtherFields.isEmpty()) {
             for (Map.Entry<String, Metric.Builder> currentMetric : convertedMetrics.entrySet()) {
-                additionalConvertedMetrics.put(String.format("%s-%s", prefix,
+                additionalConvertedMetrics.put(String.format("%s-%s", keyPrefixOtherFields,
                         currentMetric.getKey()), currentMetric.getValue());
             }
         }
diff --git a/test_framework/com/android/tradefed/targetprep/ArtChrootPreparer.java b/test_framework/com/android/tradefed/targetprep/ArtChrootPreparer.java
index df3ffd9..e23e10b 100644
--- a/test_framework/com/android/tradefed/targetprep/ArtChrootPreparer.java
+++ b/test_framework/com/android/tradefed/targetprep/ArtChrootPreparer.java
@@ -25,14 +25,15 @@
 import com.android.tradefed.util.CommandStatus;
 import com.android.tradefed.util.FileUtil;
 
+import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
+import org.apache.commons.compress.archivers.zip.ZipFile;
+
 import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.IOException;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
-import org.apache.commons.compress.archivers.zip.ZipFile;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.ListIterator;
 
 /** Create chroot directory for ART tests. */
 @OptionClass(alias = "art-chroot-preparer")
@@ -53,12 +54,16 @@
         "/apex/com.android.os.statsd",
         "/apex/com.android.runtime",
         "/dev",
+        "/dev/cpuset",
+        "/etc",
         "/linkerconfig",
         "/proc",
         "/sys",
         "/system",
     };
 
+    private List<String> mMountPoints = new ArrayList<>();
+
     @Override
     public void setUp(TestInformation testInfo)
             throws TargetSetupError, BuildError, DeviceNotAvailableException {
@@ -76,6 +81,7 @@
         for (String dir : MOUNTS) {
             adbShell(device, "mkdir -p %s%s", CHROOT_PATH, dir);
             adbShell(device, "mount --bind %s %s%s", dir, CHROOT_PATH, dir);
+            mMountPoints.add(CHROOT_PATH + dir);
         }
 
         // Activate APEXes in the chroot.
@@ -119,6 +125,7 @@
         String loopbackDevice = adbShell(device, "losetup -f -s %s", deviceApexImg);
         adbShell(device, "mkdir -p %s", deviceApexDir);
         adbShell(device, "mount -o loop,ro %s %s", loopbackDevice, deviceApexDir);
+        mMountPoints.add(deviceApexDir);
     }
 
     @Override
@@ -142,17 +149,10 @@
     }
 
     private void cleanup(ITestDevice device) throws TargetSetupError, DeviceNotAvailableException {
-        String mounts = adbShell(device, "mount");
-        Pattern pattern = Pattern.compile("^([^ ]+) on ([^ ]+) type ([^ ]+) .*$");
-        for (String mount : mounts.split("\n")) {
-            Matcher matcher = pattern.matcher(mount);
-            if (!matcher.matches()) {
-                throw new TargetSetupError(
-                        String.format("Failed to parse mount command output: %s", mount));
-            }
-            if (matcher.group(2).startsWith(CHROOT_PATH)) {
-                adbShell(device, "umount %s", matcher.group(2));
-            }
+        // Unmount in the reverse order because there are nested mount points.
+        ListIterator listIterator = mMountPoints.listIterator(mMountPoints.size());
+        while (listIterator.hasPrevious()) {
+            adbShell(device, "umount %s", listIterator.previous());
         }
         adbShell(device, "rm -rf %s", CHROOT_PATH);
     }
diff --git a/test_framework/com/android/tradefed/targetprep/DynamicSystemPreparer.java b/test_framework/com/android/tradefed/targetprep/DynamicSystemPreparer.java
index 5887e47..d9622e6 100644
--- a/test_framework/com/android/tradefed/targetprep/DynamicSystemPreparer.java
+++ b/test_framework/com/android/tradefed/targetprep/DynamicSystemPreparer.java
@@ -15,8 +15,6 @@
  */
 package com.android.tradefed.targetprep;
 
-import static com.android.tradefed.util.SparseImageUtil.SparseInputStream;
-
 import com.android.tradefed.build.IBuildInfo;
 import com.android.tradefed.config.Option;
 import com.android.tradefed.config.OptionClass;
@@ -30,8 +28,9 @@
 import com.android.tradefed.util.CommandResult;
 import com.android.tradefed.util.CommandStatus;
 import com.android.tradefed.util.FileUtil;
+import com.android.tradefed.util.SparseImageUtil.SparseInputStream;
 import com.android.tradefed.util.StreamUtil;
-
+import com.google.common.annotations.VisibleForTesting;
 import java.io.BufferedInputStream;
 import java.io.BufferedOutputStream;
 import java.io.File;
@@ -40,6 +39,7 @@
 import java.io.FilterInputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.OutputStream;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
@@ -54,7 +54,7 @@
  * System Update.
  */
 @OptionClass(alias = "dynamic-system-update")
-public class DynamicSystemPreparer extends BaseTargetPreparer implements ILabPreparer {
+public class DynamicSystemPreparer extends BaseTargetPreparer {
     static final int DSU_MAX_WAIT_SEC = 10 * 60;
 
     private static final int ANDROID_API_R = 30;
@@ -63,6 +63,7 @@
     private static final String SYSTEM_EXT_IMAGE_NAME = "system_ext.img";
     private static final String PRODUCT_IMAGE_NAME = "product.img";
 
+    private static final long COPY_STREAM_SIZE = 16 << 20;
     private static final String DEST_PATH = "/sdcard/system.raw.gz";
     private static final String DEST_ZIP_PATH = "/sdcard/system.zip";
 
@@ -92,6 +93,12 @@
             description = "Number of GB to be allocated for DSU user-data.")
     private long mUserDataSizeInGb = 16L; // 16GB
 
+    @Option(
+            name = "image-conversion-timeout",
+            description = "The timeout for decompressing, unsparsing, and compressing the images.",
+            isTimeVal = true)
+    private long mImageConversionTimeoutMs = 20 * 60 * 1000;
+
     private boolean isDSURunning(ITestDevice device) throws DeviceNotAvailableException {
         CollectingOutputReceiver receiver = new CollectingOutputReceiver();
         device.executeShellCommand("gsi_tool status", receiver);
@@ -144,11 +151,31 @@
         }
     }
 
+    @VisibleForTesting
+    boolean hasTimedOut(long deadline) {
+        return System.currentTimeMillis() >= deadline;
+    }
+
+    private void copyStreamsWithDeadline(
+            InputStream inStream, OutputStream outStream, long size, long deadline)
+            throws IOException {
+        long totalCopiedSize = 0;
+        while (totalCopiedSize < size) {
+            if (hasTimedOut(deadline)) {
+                throw new IOException("Cannot copy streams within timeout.");
+            }
+            long copySize = Math.min(COPY_STREAM_SIZE, size - totalCopiedSize);
+            StreamUtil.copyStreams(inStream, outStream, 0, copySize);
+            totalCopiedSize += copySize;
+        }
+    }
+
     @Override
     public void setUp(TestInformation testInfo)
             throws TargetSetupError, BuildError, DeviceNotAvailableException {
         ITestDevice device = testInfo.getDevice();
         IBuildInfo buildInfo = testInfo.getBuildInfo();
+        final long imageConversionDeadline = System.currentTimeMillis() + mImageConversionTimeoutMs;
         File systemImageZipFile = buildInfo.getFile(mSystemImageZipName);
         if (systemImageZipFile == null) {
             throw new BuildError(
@@ -180,7 +207,11 @@
                     try (FileOutputStream foStream = new FileOutputStream(systemImageGZ);
                             BufferedOutputStream boStream = new BufferedOutputStream(foStream);
                             GZIPOutputStream out = new GZIPOutputStream(boStream)) {
-                        StreamUtil.copyStreams(systemImageStream, out);
+                        copyStreamsWithDeadline(
+                                systemImageStream,
+                                out,
+                                systemImageStream.size(),
+                                imageConversionDeadline);
                     }
 
                     dsuPushSrc = systemImageGZ;
@@ -201,7 +232,11 @@
                             BufferedOutputStream boStream = new BufferedOutputStream(foStream);
                             ZipOutputStream out = new ZipOutputStream(boStream)) {
                         out.putNextEntry(new ZipEntry(SYSTEM_IMAGE_NAME));
-                        StreamUtil.copyStreams(systemImageStream, out);
+                        copyStreamsWithDeadline(
+                                systemImageStream,
+                                out,
+                                systemImageStream.size(),
+                                imageConversionDeadline);
                         out.closeEntry();
                         // Also look for any system-like partition images.
                         for (String imageName :
@@ -210,7 +245,8 @@
                                     getUnsparsedImageStream(systemImageZipFile, imageName)) {
                                 if (sis != null) {
                                     out.putNextEntry(new ZipEntry(imageName));
-                                    StreamUtil.copyStreams(sis, out);
+                                    copyStreamsWithDeadline(
+                                            sis, out, sis.size(), imageConversionDeadline);
                                     out.closeEntry();
                                 }
                             }
@@ -266,7 +302,10 @@
         } catch (IOException e) {
             CLog.e(e);
             throw new TargetSetupError(
-                    "fail to install the DynamicSystemUpdate", e, device.getDeviceDescriptor());
+                    "Fail to create image archive.",
+                    e,
+                    device.getDeviceDescriptor(),
+                    InfraErrorIdentifier.FAIL_TO_CREATE_FILE);
         } finally {
             for (File tempFile : tempFiles) {
                 FileUtil.deleteFile(tempFile);
diff --git a/test_framework/com/android/tradefed/targetprep/PushFilePreparer.java b/test_framework/com/android/tradefed/targetprep/PushFilePreparer.java
index c22487a..a3ed846 100644
--- a/test_framework/com/android/tradefed/targetprep/PushFilePreparer.java
+++ b/test_framework/com/android/tradefed/targetprep/PushFilePreparer.java
@@ -28,6 +28,7 @@
 import com.android.tradefed.invoker.IInvocationContext;
 import com.android.tradefed.invoker.TestInformation;
 import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.observatory.IDiscoverDependencies;
 import com.android.tradefed.result.error.DeviceErrorIdentifier;
 import com.android.tradefed.result.error.ErrorIdentifier;
 import com.android.tradefed.result.error.InfraErrorIdentifier;
@@ -59,7 +60,7 @@
  */
 @OptionClass(alias = "push-file")
 public class PushFilePreparer extends BaseTargetPreparer
-        implements IAbiReceiver, IInvocationContextReceiver {
+        implements IAbiReceiver, IInvocationContextReceiver, IDiscoverDependencies {
     private static final String MEDIA_SCAN_INTENT =
             "am broadcast -a android.intent.action.MEDIA_MOUNTED -d file://%s "
                     + "--receiver-include-background";
@@ -465,4 +466,19 @@
             }
         }
     }
+
+    @Override
+    public Set<String> reportDependencies() {
+        Set<String> deps = new HashSet<>();
+        try {
+            for (File f : getPushSpecs(null).values()) {
+                if (!f.exists()) {
+                    deps.add(f.getName());
+                }
+            }
+        } catch (TargetSetupError e) {
+            CLog.e(e);
+        }
+        return deps;
+    }
 }
diff --git a/test_framework/com/android/tradefed/targetprep/PythonVirtualenvPreparer.java b/test_framework/com/android/tradefed/targetprep/PythonVirtualenvPreparer.java
index e92e3ab..2b4ed98 100644
--- a/test_framework/com/android/tradefed/targetprep/PythonVirtualenvPreparer.java
+++ b/test_framework/com/android/tradefed/targetprep/PythonVirtualenvPreparer.java
@@ -177,7 +177,16 @@
             throw new TargetSetupError(
                     "virtualenv is not installed.", device.getDeviceDescriptor());
         }
-        String version = stdout.split(" ")[1];
+        CLog.d("Output from virtualenv --version: %s", stdout);
+        String[] split = stdout.split(" ");
+        if (split.length < 2) {
+            throw new TargetSetupError(
+                    String.format(
+                            "Something is wrong with your installed virtualenv version: %s",
+                            stdout),
+                    device.getDeviceDescriptor());
+        }
+        String version = split[1];
         int majorVersion = Integer.parseInt(version.split("\\.")[0]);
         if (majorVersion < 20) {
             throw new TargetSetupError(
diff --git a/test_framework/com/android/tradefed/targetprep/WaitForDeviceDatetimePreparer.java b/test_framework/com/android/tradefed/targetprep/WaitForDeviceDatetimePreparer.java
index fb32012..daf9d21 100644
--- a/test_framework/com/android/tradefed/targetprep/WaitForDeviceDatetimePreparer.java
+++ b/test_framework/com/android/tradefed/targetprep/WaitForDeviceDatetimePreparer.java
@@ -21,6 +21,7 @@
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.invoker.TestInformation;
 import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.result.error.DeviceErrorIdentifier;
 import com.android.tradefed.util.IRunUtil;
 import com.android.tradefed.util.RunUtil;
 import com.android.tradefed.util.TimeUtil;
@@ -62,9 +63,10 @@
             if (mForceSetupError) {
                 throw new TargetSetupError(
                         String.format(
-                                "datetime on device is incorrect after " + "wait timeout of '%s'",
+                                "datetime on device is incorrect after wait timeout of '%s'",
                                 TimeUtil.formatElapsedTime(mDatetimeWaitTimeout)),
-                        device.getDeviceDescriptor());
+                        device.getDeviceDescriptor(),
+                        DeviceErrorIdentifier.DEVICE_UNEXPECTED_RESPONSE);
             } else {
                 CLog.w("datetime on device is incorrect after wait timeout.");
             }
diff --git a/test_framework/com/android/tradefed/targetprep/multi/PairingMultiTargetPreparer.java b/test_framework/com/android/tradefed/targetprep/multi/PairingMultiTargetPreparer.java
index 7a58d34..f636534 100644
--- a/test_framework/com/android/tradefed/targetprep/multi/PairingMultiTargetPreparer.java
+++ b/test_framework/com/android/tradefed/targetprep/multi/PairingMultiTargetPreparer.java
@@ -22,6 +22,7 @@
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.invoker.TestInformation;
+import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.result.error.DeviceErrorIdentifier;
 import com.android.tradefed.result.error.InfraErrorIdentifier;
 import com.android.tradefed.targetprep.BuildError;
@@ -89,12 +90,14 @@
             throws TargetSetupError, BuildError, DeviceNotAvailableException {
         setDeviceInfos(testInformation.getContext().getDeviceBuildMap());
         try {
+            CLog.d("Enabling bluetooth on %s", mPrimaryDevice.getDeviceDescriptor());
             if (!mUtil.enable(mPrimaryDevice)) {
                 throw new TargetSetupError(
                         "Failed to enable Bluetooth",
                         mPrimaryDevice.getDeviceDescriptor(),
                         DeviceErrorIdentifier.DEVICE_UNEXPECTED_RESPONSE);
             }
+            CLog.d("Enabling bluetooth on %s", mCompanionDevice.getDeviceDescriptor());
             if (!mUtil.enable(mCompanionDevice)) {
                 throw new TargetSetupError(
                         "Failed to enable Bluetooth",
@@ -102,6 +105,7 @@
                         DeviceErrorIdentifier.DEVICE_UNEXPECTED_RESPONSE);
             }
             mUtil.setBtPairTimeout(mPairingTimeout);
+            CLog.d("Starting pairing bluetooth on %s", mPrimaryDevice.getDeviceDescriptor());
             if (!mUtil.pair(mPrimaryDevice, mCompanionDevice)) {
                 throw new TargetSetupError(
                         "Bluetooth pairing failed.",
@@ -111,6 +115,7 @@
             // Always enable PBAP between primary and companion devices in case it's not enabled
             // For now, assume PBAP client profile is always on primary device, and enable PBAP on
             // companion device.
+            CLog.d("Enabling PBAP on %s", mCompanionDevice.getDeviceDescriptor());
             if (!mUtil.changeProfileAccessPermission(
                     mCompanionDevice,
                     mPrimaryDevice,
@@ -121,6 +126,7 @@
                         mCompanionDevice.getDeviceDescriptor(),
                         DeviceErrorIdentifier.DEVICE_UNEXPECTED_RESPONSE);
             }
+            CLog.d("Enabling PBAP_CLIENT on %s", mPrimaryDevice.getDeviceDescriptor());
             if (!mUtil.setProfilePriority(
                     mPrimaryDevice,
                     mCompanionDevice,
@@ -131,6 +137,7 @@
                         mPrimaryDevice.getDeviceDescriptor(),
                         DeviceErrorIdentifier.DEVICE_UNEXPECTED_RESPONSE);
             }
+            CLog.d("Connecting to profiles");
             if (mConnectDevices && mProfiles.size() > 0) {
                 if (!mUtil.connect(mPrimaryDevice, mCompanionDevice, mProfiles)) {
                     throw new TargetSetupError(
diff --git a/test_framework/com/android/tradefed/testtype/ArtRunTest.java b/test_framework/com/android/tradefed/testtype/ArtRunTest.java
index bff97f9..c0a47de 100644
--- a/test_framework/com/android/tradefed/testtype/ArtRunTest.java
+++ b/test_framework/com/android/tradefed/testtype/ArtRunTest.java
@@ -20,6 +20,8 @@
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.invoker.TestInformation;
+import com.android.tradefed.invoker.logger.InvocationMetricLogger;
+import com.android.tradefed.invoker.logger.InvocationMetricLogger.InvocationMetricKey;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
 import com.android.tradefed.result.FileInputStreamSource;
@@ -557,9 +559,15 @@
      * @return An optional error message, empty if the Checker invocation was successful
      */
     protected Optional<String> runChecker(String[] checkerCommandLine) {
-        CLog.d("About to run Checker command: %s", String.join(" ", checkerCommandLine));
+        String checkerCommandLineString = String.join(" ", checkerCommandLine);
+        CLog.d("About to run Checker command: %s", checkerCommandLineString);
+        long startTime = System.currentTimeMillis();
         CommandResult result =
                 RunUtil.getDefault().runTimedCmd(CHECKER_TIMEOUT_MS, checkerCommandLine);
+        long duration = System.currentTimeMillis() - startTime;
+        CLog.i("Checker command `%s` executed in %s ms", checkerCommandLineString, duration);
+        InvocationMetricLogger.addInvocationMetrics(
+            InvocationMetricKey.ART_RUN_TEST_CHECKER_COMMAND_TIME_MS, duration);
         if (result.getStatus() != CommandStatus.SUCCESS) {
             String errorMessage;
             if (result.getStatus() == CommandStatus.TIMED_OUT) {
diff --git a/test_framework/com/android/tradefed/testtype/HostGTest.java b/test_framework/com/android/tradefed/testtype/HostGTest.java
index be79fed..1ccb70f 100644
--- a/test_framework/com/android/tradefed/testtype/HostGTest.java
+++ b/test_framework/com/android/tradefed/testtype/HostGTest.java
@@ -24,12 +24,16 @@
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.error.HarnessRuntimeException;
 import com.android.tradefed.invoker.TestInformation;
+import com.android.tradefed.invoker.TestInvocation;
 import com.android.tradefed.log.ITestLogger;
 import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
+import com.android.tradefed.result.FailureDescription;
 import com.android.tradefed.result.FileInputStreamSource;
 import com.android.tradefed.result.ITestInvocationListener;
 import com.android.tradefed.result.LogDataType;
 import com.android.tradefed.result.error.TestErrorIdentifier;
+import com.android.tradefed.result.proto.TestRecordProto.FailureStatus;
 import com.android.tradefed.util.CommandResult;
 import com.android.tradefed.util.CommandStatus;
 import com.android.tradefed.util.FileUtil;
@@ -44,6 +48,7 @@
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
 import java.util.stream.Collectors;
 
@@ -262,11 +267,14 @@
         File gTestFile = null;
         try {
             gTestFile = FileUtil.findFile(moduleName, getAbi(), scanDirs.toArray(new File[] {}));
+            if (gTestFile != null && gTestFile.isDirectory()) {
+                // Search the exact file in subdir
+                gTestFile = FileUtil.findFile(moduleName, getAbi(), gTestFile);
+            }
         } catch (IOException e) {
             throw new RuntimeException(e);
         }
-
-        if (gTestFile == null || gTestFile.isDirectory()) {
+        if (gTestFile == null) {
             // If we ended up here we most likely failed to find the proper file as is, so we
             // search for it with a potential suffix (which is allowed).
             try {
@@ -288,8 +296,12 @@
         }
 
         if (!gTestFile.canExecute()) {
-            throw new RuntimeException(
-                    String.format("%s is not executable!", gTestFile.getAbsolutePath()));
+            reportFailure(
+                    listener,
+                    gTestFile.getName(),
+                    new RuntimeException(
+                            String.format("%s is not executable!", gTestFile.getAbsolutePath())));
+            return;
         }
 
         listener = getGTestListener(listener);
@@ -299,4 +311,15 @@
         CLog.i("Running gtest %s %s", gTestFile.getName(), flags);
         runTest(resultParser, gTestFile, flags, listener);
     }
+
+    private void reportFailure(
+            ITestInvocationListener listener, String runName, RuntimeException exception) {
+        listener.testRunStarted(runName, 0);
+        listener.testRunFailed(createFailure(exception));
+        listener.testRunEnded(0L, new HashMap<String, Metric>());
+    }
+
+    private FailureDescription createFailure(Exception e) {
+        return TestInvocation.createFailureFromException(e, FailureStatus.TEST_FAILURE);
+    }
 }
diff --git a/test_framework/com/android/tradefed/testtype/HostTest.java b/test_framework/com/android/tradefed/testtype/HostTest.java
index 32aff60..72e8a18 100644
--- a/test_framework/com/android/tradefed/testtype/HostTest.java
+++ b/test_framework/com/android/tradefed/testtype/HostTest.java
@@ -131,13 +131,13 @@
 
     public static final String SET_OPTION_NAME = "set-option";
     public static final String SET_OPTION_DESC =
-            "Options to be passed down to the class under test, key and value should be "
-                    + "separated by colon \":\"; for example, if class under test supports "
-                    + "\"--iteration 1\" from a command line, it should be passed in as"
-                    + " \"--set-option iteration:1\" or \"--set-option iteration:key=value\" for "
-                    + "passing options to map; escaping of \"=\" is currently not supported."
-                    + "A particular class can be targetted by specifying it. "
-                    + "\" --set-option <fully qualified class>:<option name>:<option value>\"";
+            "Options to be passed down to the class under test, key and value should be separated"
+                + " by colon \":\"; for example, if class under test supports \"--iteration 1\""
+                + " from a command line, it should be passed in as \"--set-option iteration:1\" or"
+                + " \"--set-option iteration:key=value\" for passing options to map. Values that"
+                + " contain \":\" or \"=\" can be escaped with a backslash. A particular class can"
+                + " be targeted by specifying it. \" --set-option <fully qualified class>:<option"
+                + " name>:<option value>\"";
 
     @Option(name = SET_OPTION_NAME, description = SET_OPTION_DESC)
     private List<String> mKeyValueOptions = new ArrayList<>();
@@ -1154,18 +1154,24 @@
 
     private static void injectOption(OptionSetter setter, String origItem, String key, String value)
             throws ConfigurationException {
-        if (value.contains("=")) {
-            String[] values = value.split("=");
-            if (values.length != 2) {
-                throw new RuntimeException(
-                        String.format(
-                                "set-option provided '%s' format is invalid. Only one "
-                                        + "'=' is allowed",
-                                origItem));
-            }
-            setter.setOptionValue(key, values[0], values[1]);
+        String esc = "\\";
+        String delim = "=";
+        String regex = "(?<!" + Pattern.quote(esc) + ")" + Pattern.quote(delim);
+        String escDelim = Pattern.quote(esc) + Pattern.quote(delim);
+        String[] values = value.split(regex);
+        if (values.length == 1) {
+            setter.setOptionValue(key, values[0].replaceAll(escDelim, delim));
+        } else if (values.length == 2) {
+            setter.setOptionValue(
+                    key,
+                    values[0].replaceAll(escDelim, delim),
+                    values[1].replaceAll(escDelim, delim));
         } else {
-            setter.setOptionValue(key, value);
+            throw new RuntimeException(
+                    String.format(
+                            "set-option provided '%s' format is invalid. Only one "
+                                    + "'=' is allowed",
+                            origItem));
         }
     }
 
diff --git a/test_framework/com/android/tradefed/testtype/IsolatedHostTest.java b/test_framework/com/android/tradefed/testtype/IsolatedHostTest.java
index b7effd7..f525a1c 100644
--- a/test_framework/com/android/tradefed/testtype/IsolatedHostTest.java
+++ b/test_framework/com/android/tradefed/testtype/IsolatedHostTest.java
@@ -179,6 +179,8 @@
     public void run(TestInformation testInfo, ITestInvocationListener listener)
             throws DeviceNotAvailableException {
         mReportedFailure = false;
+        Process isolationRunner = null;
+
         try {
             mServer = new ServerSocket(0);
             mServer.setSoTimeout(mSocketTimeout);
@@ -204,8 +206,7 @@
             mSubprocessLog = FileUtil.createTempFile("subprocess-logs", "");
             runner.setRedirectStderrToStdout(true);
 
-            Process isolationRunner =
-                    runner.runCmdInBackground(Redirect.to(mSubprocessLog), cmdArgs);
+            isolationRunner = runner.runCmdInBackground(Redirect.to(mSubprocessLog), cmdArgs);
             CLog.v("Started subprocess.");
 
             Socket socket = mServer.accept();
@@ -238,9 +239,7 @@
                     .setCommand(RunnerOp.RUNNER_OP_STOP)
                     .build()
                     .writeDelimitedTo(socket.getOutputStream());
-            // Ensure the subprocess finishes
-            isolationRunner.waitFor(1, TimeUnit.MINUTES);
-        } catch (IOException | InterruptedException e) {
+        } catch (IOException e) {
             if (!mReportedFailure) {
                 // Avoid overriding the failure
                 FailureDescription failure =
@@ -250,6 +249,40 @@
                 listener.testRunEnded(0L, new HashMap<String, Metric>());
             }
         } finally {
+            try {
+                // Ensure the subprocess finishes
+                if (isolationRunner != null) {
+                    if (isolationRunner.isAlive()) {
+                        CLog.v(
+                                "Subprocess is still alive after test phase - waiting for it to"
+                                        + " terminate.");
+                        isolationRunner.waitFor(10, TimeUnit.SECONDS);
+                        if (isolationRunner.isAlive()) {
+                            CLog.v(
+                                    "Subprocess is still alive after test phase - requesting"
+                                            + " termination.");
+                            // Isolation runner still alive for some reason, try to kill it
+                            isolationRunner.destroy();
+                            isolationRunner.waitFor(10, TimeUnit.SECONDS);
+
+                            // If the process is still alive after trying to kill it nicely
+                            // then end it forcibly.
+                            if (isolationRunner.isAlive()) {
+                                CLog.v(
+                                        "Subprocess is still alive after test phase - forcibly"
+                                                + " terminating it.");
+                                isolationRunner.destroyForcibly();
+                            }
+                        }
+                    }
+                }
+            } catch (InterruptedException e) {
+                throw new HarnessRuntimeException(
+                        "Interrupted while stopping subprocess",
+                        e,
+                        InfraErrorIdentifier.INTERRUPTED_DURING_SUBPROCESS_SHUTDOWN);
+            }
+
             FileUtil.deleteFile(mIsolationJar);
         }
     }
diff --git a/test_framework/com/android/tradefed/testtype/pandora/PtsBotTest.java b/test_framework/com/android/tradefed/testtype/pandora/PtsBotTest.java
index 4ec8d01..ce431ba 100644
--- a/test_framework/com/android/tradefed/testtype/pandora/PtsBotTest.java
+++ b/test_framework/com/android/tradefed/testtype/pandora/PtsBotTest.java
@@ -28,7 +28,9 @@
 import com.android.tradefed.testtype.IRemoteTest;
 import com.android.tradefed.testtype.ITestFilterReceiver;
 import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.CommandStatus;
 import com.android.tradefed.util.FileUtil;
+import com.android.tradefed.util.IRunUtil;
 import com.android.tradefed.util.PythonVirtualenvHelper;
 import com.android.tradefed.util.RunUtil;
 
@@ -60,6 +62,8 @@
     private static final int HCI_ROOTCANAL_PORT = 6211;
     private static final int HCI_PROXY_PORT = 1234;
 
+    private IRunUtil mRunUtil = new RunUtil();
+
     @Option(name = "pts-bot-path", description = "pts-bot binary path.")
     private File ptsBotPath = new File("pts-bot");
 
@@ -134,6 +138,28 @@
         return physical ? HCI_PROXY_PORT : HCI_ROOTCANAL_PORT;
     }
 
+    private void displayPtsBotVersion() {
+        CommandResult c;
+        c = mRunUtil.runTimedCmd(5000, "which", ptsBotPath.getPath());
+        if (c.getStatus() != CommandStatus.SUCCESS) {
+            CLog.e("Failed to get pts-bot path");
+            CLog.e(
+                    "Status: %s\nStdout: %s\nStderr: %s",
+                    c.getStatus(), c.getStdout(), c.getStderr());
+            throw new RuntimeException("Failed to get pts-bot path. Error:\n" + c.getStderr());
+        }
+        String ptsBotAbsolutePath = c.getStdout().trim();
+        c = mRunUtil.runTimedCmd(5000, ptsBotAbsolutePath, "--version");
+        if (c.getStatus() != CommandStatus.SUCCESS) {
+            CLog.e("Failed to get pts-bot version");
+            CLog.e(
+                    "Status: %s\nStdout: %s\nStderr: %s",
+                    c.getStatus(), c.getStdout(), c.getStderr());
+            throw new RuntimeException("Failed to get pts-bot version. Error:\n" + c.getStderr());
+        }
+        CLog.d("pts-bot version: %s", c.getStdout().trim());
+    }
+
     @Override
     public void run(TestInformation testInfo, ITestInvocationListener listener)
             throws DeviceNotAvailableException {
@@ -163,6 +189,11 @@
             }
         }
 
+        // Test ressources files are not executable
+        ptsBotPath.setExecutable(true);
+
+        displayPtsBotVersion();
+
         CLog.i("Tests config file: %s", testsConfigFile.getPath());
         CLog.i("Profiles to be tested: %s", profiles);
 
@@ -354,9 +385,7 @@
     }
 
     private ProcessBuilder ptsBot(TestInformation testInfo, String... args) {
-        List<String> command = new ArrayList();
-
-        ptsBotPath.setExecutable(true);
+        List<String> command = new ArrayList<>();
 
         command.add(ptsBotPath.getPath());
         command.add("-c");
@@ -381,7 +410,7 @@
         if (venvDir != null) {
             String packagePath =
                     PythonVirtualenvHelper.getPackageInstallLocation(
-                            new RunUtil(), venvDir.getAbsolutePath());
+                            mRunUtil, venvDir.getAbsolutePath());
             pythonPath += ":" + packagePath;
         }
 
diff --git a/test_observatory/com/android/tradefed/observatory/TestDiscoveryExecutor.java b/test_observatory/com/android/tradefed/observatory/TestDiscoveryExecutor.java
index bc76f28..84323f4 100644
--- a/test_observatory/com/android/tradefed/observatory/TestDiscoveryExecutor.java
+++ b/test_observatory/com/android/tradefed/observatory/TestDiscoveryExecutor.java
@@ -25,8 +25,9 @@
 import com.android.tradefed.testtype.suite.BaseTestSuite;
 import com.android.tradefed.testtype.suite.SuiteTestFilter;
 
-import org.json.JSONArray;
-import org.json.JSONObject;
+import com.google.gson.Gson;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -77,39 +78,9 @@
      * Discover test dependencies base on command line args.
      *
      * @param args the command line args of the test.
-     * @return A JSON string with test module names.
-     */
-    @Deprecated(forRemoval = true)
-    public String discoverDependencies(String[] args) throws Exception {
-        // Create IConfiguration base on command line args.
-        IConfiguration config = getConfiguration(args);
-        List<IRemoteTest> tests = config.getTests();
-
-        // Tests could be empty if input args are corrupted.
-        if (tests == null || tests.isEmpty()) {
-            throw new IllegalStateException(
-                    "Tradefed Observatory discovered no tests from the IConfiguration created from"
-                            + " command line args.");
-        }
-        Set<String> allDependencies = new HashSet<>(discoverTestModulesFromTests(tests));
-        allDependencies.addAll(discoverDependencies(config));
-        List<String> allDependenciesList = new ArrayList<>(allDependencies);
-
-        // Sort it so that it's always in the same order
-        Collections.sort(allDependenciesList);
-        JSONObject jsonObject = new JSONObject();
-        JSONArray jsonArray = new JSONArray(allDependenciesList);
-        jsonObject.put(TestDiscoveryInvoker.TEST_DEPENDENCIES_LIST_KEY, jsonArray);
-        return jsonObject.toString();
-    }
-
-    /**
-     * Discover test dependencies base on command line args.
-     *
-     * @param args the command line args of the test.
      * @return A JSON string with one test module names array and one other test dependency array.
      */
-    public String discoverDependenciesV2(String[] args) throws Exception {
+    public String discoverDependencies(String[] args) throws Exception {
         // Create IConfiguration base on command line args.
         IConfiguration config = getConfiguration(args);
         List<IRemoteTest> tests = config.getTests();
@@ -122,15 +93,22 @@
         }
 
         List<String> testModules = new ArrayList<>(discoverTestModulesFromTests(tests));
+
+        if (testModules == null || testModules.isEmpty()) {
+            throw new TestDiscoveryException(
+                    "Tradefed Observatory discovered no test modules from the test config, it"
+                            + " might be component-based.");
+        }
         List<String> testDependencies = new ArrayList<>(discoverDependencies(config));
         Collections.sort(testModules);
         Collections.sort(testDependencies);
 
-        JSONObject jsonObject = new JSONObject();
-        JSONArray testModulesArray = new JSONArray(testModules);
-        JSONArray testDependenciesArray = new JSONArray(testDependencies);
-        jsonObject.put(TestDiscoveryInvoker.TEST_MODULES_LIST_KEY, testModulesArray);
-        jsonObject.put(TestDiscoveryInvoker.TEST_DEPENDENCIES_LIST_KEY, testDependenciesArray);
+        JsonObject jsonObject = new JsonObject();
+        Gson gson = new Gson();
+        JsonArray testModulesArray = gson.toJsonTree(testModules).getAsJsonArray();
+        JsonArray testDependenciesArray = gson.toJsonTree(testDependencies).getAsJsonArray();
+        jsonObject.add(TestDiscoveryInvoker.TEST_MODULES_LIST_KEY, testModulesArray);
+        jsonObject.add(TestDiscoveryInvoker.TEST_DEPENDENCIES_LIST_KEY, testDependenciesArray);
         return jsonObject.toString();
     }
 
diff --git a/test_observatory/com/android/tradefed/observatory/TestDiscoveryInvoker.java b/test_observatory/com/android/tradefed/observatory/TestDiscoveryInvoker.java
index ed2de02..9eb88b1 100644
--- a/test_observatory/com/android/tradefed/observatory/TestDiscoveryInvoker.java
+++ b/test_observatory/com/android/tradefed/observatory/TestDiscoveryInvoker.java
@@ -57,6 +57,7 @@
 public class TestDiscoveryInvoker {
 
     private final IConfiguration mConfiguration;
+    private final String mDefaultConfigName;
     private final File mRootDir;
     public static final String TRADEFED_OBSERVATORY_ENTRY_PATH =
             TestDiscoveryExecutor.class.getName();
@@ -68,40 +69,21 @@
         return RunUtil.getDefault();
     }
 
+    /** Creates an {@link TestDiscoveryInvoker} with a {@link IConfiguration} and root directory. */
     public TestDiscoveryInvoker(IConfiguration config, File rootDir) {
         mConfiguration = config;
+        mDefaultConfigName = null;
         mRootDir = rootDir;
     }
 
     /**
-     * Retrieve a list of test module names by using Tradefed Observatory.
-     *
-     * @return A list of test module names.
-     * @throws IOException
-     * @throws JSONException
-     * @throws ConfigurationException
+     * Creates an {@link TestDiscoveryInvoker} with a {@link IConfiguration}, test launcher's
+     * default config name and root directory.
      */
-    @Deprecated(forRemoval = true)
-    public List<String> discoverTestModuleNames()
-            throws IOException, JSONException, ConfigurationException {
-        List<String> testModuleNames = new ArrayList<>();
-        // Build the classpath base on test root directory which should contain all the jars
-        String classPath = buildClasspath(mRootDir);
-        // Build command line args to query the tradefed.jar in the root directory
-        List<String> args = buildJavaCmdForXtsDiscovery(classPath);
-        String[] subprocessArgs = args.toArray(new String[args.size()]);
-        CommandResult res = getRunUtil().runTimedCmd(20000, subprocessArgs);
-        if (res.getExitCode() != 0 || !res.getStatus().equals(CommandStatus.SUCCESS)) {
-            CLog.e(
-                    "Tradefed observatory error, unable to discover test module names. command"
-                            + " used: %s error: %s",
-                    Joiner.on(" ").join(subprocessArgs), res.getStderr());
-            return testModuleNames;
-        }
-        String stdout = res.getStdout();
-        CLog.i(String.format("Tradefed Observatory returned in stdout: %s", stdout));
-        testModuleNames.addAll(parseTestModules(stdout));
-        return testModuleNames;
+    public TestDiscoveryInvoker(IConfiguration config, String defaultConfigName, File rootDir) {
+        mConfiguration = config;
+        mDefaultConfigName = defaultConfigName;
+        mRootDir = rootDir;
     }
 
     /**
@@ -172,12 +154,21 @@
         String configName = ctsParserSettings.mConfigName;
 
         if (configName == null) {
-            throw new ConfigurationException(
-                    String.format(
-                            "Failed to extract config-name from parent test command options,"
-                                    + " unable to build args to invoke tradefed observatory. Parent"
-                                    + " test command options is: %s",
-                            fullCommandLineArgs));
+            if (mDefaultConfigName == null) {
+                throw new ConfigurationException(
+                        String.format(
+                                "Failed to extract config-name from parent test command options,"
+                                        + " unable to build args to invoke tradefed observatory."
+                                        + " Parent test command options is: %s",
+                                fullCommandLineArgs));
+            } else {
+                CLog.i(
+                        String.format(
+                                "No config name provided in the command args, use default config"
+                                        + " name %s",
+                                mDefaultConfigName));
+                configName = mDefaultConfigName;
+            }
         }
         List<String> args = new ArrayList<>();
         args.add(SystemUtil.getRunningJavaBinaryPath().getAbsolutePath());