Create the structure that will track the usage of our objects

Test: unit tests
Bug: 148783241
Change-Id: I44eadd26a3a8f363276103becb53b77c364081a6
diff --git a/invocation_interfaces/com/android/tradefed/invoker/logger/CurrentInvocation.java b/invocation_interfaces/com/android/tradefed/invoker/logger/CurrentInvocation.java
index 42eec08..4427fe5 100644
--- a/invocation_interfaces/com/android/tradefed/invoker/logger/CurrentInvocation.java
+++ b/invocation_interfaces/com/android/tradefed/invoker/logger/CurrentInvocation.java
@@ -87,7 +87,9 @@
     /** Clear the invocation info for an invocation. */
     public static void clearInvocationInfos() {
         ThreadGroup group = Thread.currentThread().getThreadGroup();
-        mPerGroupInfo.remove(group);
+        synchronized (mPerGroupInfo) {
+            mPerGroupInfo.remove(group);
+        }
     }
 
     /**
diff --git a/invocation_interfaces/com/android/tradefed/invoker/logger/InvocationMetricLogger.java b/invocation_interfaces/com/android/tradefed/invoker/logger/InvocationMetricLogger.java
index f9718cb..8b8b802 100644
--- a/invocation_interfaces/com/android/tradefed/invoker/logger/InvocationMetricLogger.java
+++ b/invocation_interfaces/com/android/tradefed/invoker/logger/InvocationMetricLogger.java
@@ -137,6 +137,8 @@
     /** Clear the invocation metrics for an invocation. */
     public static void clearInvocationMetrics() {
         ThreadGroup group = Thread.currentThread().getThreadGroup();
-        mPerGroupMetrics.remove(group);
+        synchronized (mPerGroupMetrics) {
+            mPerGroupMetrics.remove(group);
+        }
     }
 }
diff --git a/src/com/android/tradefed/invoker/TestInvocation.java b/src/com/android/tradefed/invoker/TestInvocation.java
index b84a2ed..b384bc9 100644
--- a/src/com/android/tradefed/invoker/TestInvocation.java
+++ b/src/com/android/tradefed/invoker/TestInvocation.java
@@ -38,6 +38,7 @@
 import com.android.tradefed.invoker.logger.CurrentInvocation.InvocationInfo;
 import com.android.tradefed.invoker.logger.InvocationMetricLogger;
 import com.android.tradefed.invoker.logger.InvocationMetricLogger.InvocationMetricKey;
+import com.android.tradefed.invoker.logger.TfObjectTracker;
 import com.android.tradefed.invoker.sandbox.ParentSandboxInvocationExecution;
 import com.android.tradefed.invoker.sandbox.SandboxedInvocationExecution;
 import com.android.tradefed.invoker.shard.LastShardDetector;
@@ -402,6 +403,7 @@
                     }
                 }
             } finally {
+                TfObjectTracker.clearTracking();
                 CurrentInvocation.clearInvocationInfos();
                 invocationPath.cleanUpBuilds(context, config);
             }
diff --git a/src/com/android/tradefed/invoker/logger/TfObjectTracker.java b/src/com/android/tradefed/invoker/logger/TfObjectTracker.java
new file mode 100644
index 0000000..cdb79ef
--- /dev/null
+++ b/src/com/android/tradefed/invoker/logger/TfObjectTracker.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2020 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.logger;
+
+import com.android.tradefed.build.IBuildProvider;
+import com.android.tradefed.device.metric.IMetricCollector;
+import com.android.tradefed.postprocessor.IPostProcessor;
+import com.android.tradefed.suite.checker.ISystemStatusChecker;
+import com.android.tradefed.targetprep.ITargetPreparer;
+import com.android.tradefed.targetprep.multi.IMultiTargetPreparer;
+import com.android.tradefed.testtype.IRemoteTest;
+
+import com.google.common.collect.ImmutableSet;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+/** A utility to track the usage of the different Trade Fedederation objects. */
+public class TfObjectTracker {
+
+    public static final String TF_OBJECTS_TRACKING_KEY = "tf_objects_tracking";
+    private static final Set<Class<?>> TRACKED_CLASSES =
+            ImmutableSet.of(
+                    IBuildProvider.class,
+                    IMetricCollector.class,
+                    IMultiTargetPreparer.class,
+                    IPostProcessor.class,
+                    IRemoteTest.class,
+                    ISystemStatusChecker.class,
+                    ITargetPreparer.class);
+
+    private TfObjectTracker() {}
+
+    private static final Map<ThreadGroup, Map<String, Long>> mPerGroupUsage =
+            new ConcurrentHashMap<ThreadGroup, Map<String, Long>>();
+
+    /** Count the occurrence of a give class and its super classes until the Tradefed interface. */
+    public static void countWithParents(Class<?> object) {
+        if (!count(object)) {
+            return;
+        }
+        // Track all the super class until not a TF interface to get a full picture.
+        countWithParents(object.getSuperclass());
+    }
+
+    /**
+     * Count the current occurrence only if it's part of the tracked objects.
+     *
+     * @param object The object to track
+     * @return True if the object was tracked, false otherwise.
+     */
+    public static boolean count(Class<?> object) {
+        ThreadGroup group = Thread.currentThread().getThreadGroup();
+        String qualifiedName = object.getName();
+
+        boolean tracked = false;
+        for (Class<?> classTracked : TRACKED_CLASSES) {
+            if (classTracked.isAssignableFrom(object)) {
+                tracked = true;
+                break;
+            }
+        }
+        if (!tracked) {
+            return false;
+        }
+        if (mPerGroupUsage.get(group) == null) {
+            mPerGroupUsage.put(group, new ConcurrentHashMap<>());
+        }
+        Map<String, Long> countMap = mPerGroupUsage.get(group);
+        long count = 0;
+        if (countMap.get(qualifiedName) != null) {
+            count = countMap.get(qualifiedName);
+        }
+        count++;
+        countMap.put(qualifiedName, count);
+        return true;
+    }
+
+    /** Returns the usage of the tracked objects. */
+    public static Map<String, Long> getUsage() {
+        ThreadGroup group = Thread.currentThread().getThreadGroup();
+        if (mPerGroupUsage.get(group) == null) {
+            mPerGroupUsage.put(group, new ConcurrentHashMap<>());
+        }
+        return new HashMap<>(mPerGroupUsage.get(group));
+    }
+
+    /** Stop tracking the current invocation. This is called automatically by the harness. */
+    public static void clearTracking() {
+        ThreadGroup group = Thread.currentThread().getThreadGroup();
+        mPerGroupUsage.remove(group);
+    }
+}
diff --git a/tests/src/com/android/tradefed/UnitTests.java b/tests/src/com/android/tradefed/UnitTests.java
index c299b28..77813ab 100644
--- a/tests/src/com/android/tradefed/UnitTests.java
+++ b/tests/src/com/android/tradefed/UnitTests.java
@@ -135,6 +135,7 @@
 import com.android.tradefed.invoker.TestInvocationTest;
 import com.android.tradefed.invoker.UnexecutedTestReporterThreadTest;
 import com.android.tradefed.invoker.logger.InvocationMetricLoggerTest;
+import com.android.tradefed.invoker.logger.TfObjectTrackerTest;
 import com.android.tradefed.invoker.sandbox.ParentSandboxInvocationExecutionTest;
 import com.android.tradefed.invoker.shard.ShardHelperTest;
 import com.android.tradefed.invoker.shard.StrictShardHelperTest;
@@ -552,6 +553,7 @@
 
     // invoker.logger
     InvocationMetricLoggerTest.class,
+    TfObjectTrackerTest.class,
 
     // invoker.shard
     ShardHelperTest.class,
diff --git a/tests/src/com/android/tradefed/invoker/logger/TfObjectTrackerTest.java b/tests/src/com/android/tradefed/invoker/logger/TfObjectTrackerTest.java
new file mode 100644
index 0000000..a170e8e
--- /dev/null
+++ b/tests/src/com/android/tradefed/invoker/logger/TfObjectTrackerTest.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2020 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.logger;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tradefed.command.CommandOptions;
+import com.android.tradefed.targetprep.BaseTargetPreparer;
+import com.android.tradefed.targetprep.TestAppInstallSetup;
+import com.android.tradefed.targetprep.suite.SuiteApkInstaller;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Map;
+import java.util.UUID;
+
+/** Unit tests for {@link TfObjectTracker}. */
+@RunWith(JUnit4.class)
+public class TfObjectTrackerTest {
+
+    @Test
+    public void testNotTracking() throws Exception {
+        String uuid = UUID.randomUUID().toString();
+        String group = "unit-test-group-" + uuid;
+        ThreadGroup testGroup = new ThreadGroup(group);
+        Map<String, Long> metrics = logMetric(testGroup, true, CommandOptions.class);
+        assertTrue(metrics.isEmpty());
+    }
+
+    @Test
+    public void testTrackingWithParents() throws Exception {
+        String uuid = UUID.randomUUID().toString();
+        String group = "unit-test-group-" + uuid;
+        ThreadGroup testGroup = new ThreadGroup(group);
+        Map<String, Long> metrics = logMetric(testGroup, true, SuiteApkInstaller.class);
+        assertEquals(3, metrics.size());
+        assertEquals(1, metrics.get(SuiteApkInstaller.class.getName()).longValue());
+        assertEquals(1, metrics.get(TestAppInstallSetup.class.getName()).longValue());
+        assertEquals(1, metrics.get(BaseTargetPreparer.class.getName()).longValue());
+        // Add a second time
+        metrics = logMetric(testGroup, true, SuiteApkInstaller.class);
+        assertEquals(3, metrics.size());
+        assertEquals(2, metrics.get(SuiteApkInstaller.class.getName()).longValue());
+        assertEquals(2, metrics.get(TestAppInstallSetup.class.getName()).longValue());
+        assertEquals(2, metrics.get(BaseTargetPreparer.class.getName()).longValue());
+    }
+
+    @Test
+    public void testTracking() throws Exception {
+        String uuid = UUID.randomUUID().toString();
+        String group = "unit-test-group-" + uuid;
+        ThreadGroup testGroup = new ThreadGroup(group);
+        Map<String, Long> metrics = logMetric(testGroup, false, SuiteApkInstaller.class);
+        assertEquals(1, metrics.size());
+        assertEquals(1, metrics.get(SuiteApkInstaller.class.getName()).longValue());
+        assertNull(metrics.get(TestAppInstallSetup.class.getName()));
+        assertNull(metrics.get(BaseTargetPreparer.class.getName()));
+        // Add a second time
+        metrics = logMetric(testGroup, false, SuiteApkInstaller.class);
+        assertEquals(1, metrics.size());
+        assertEquals(2, metrics.get(SuiteApkInstaller.class.getName()).longValue());
+        assertNull(metrics.get(TestAppInstallSetup.class.getName()));
+        assertNull(metrics.get(BaseTargetPreparer.class.getName()));
+    }
+
+    private Map<String, Long> logMetric(
+            ThreadGroup testGroup, boolean trackWithParents, Class<?> toTrack) throws Exception {
+        TestRunnable runnable = new TestRunnable(toTrack, trackWithParents);
+        Thread testThread = new Thread(testGroup, runnable);
+        testThread.setName("TfObjectTrackerTest-test-thread");
+        testThread.setDaemon(true);
+        testThread.start();
+        testThread.join(10000);
+        return runnable.getUsageMap();
+    }
+
+    private class TestRunnable implements Runnable {
+
+        private Class<?> mToTrack;
+        private Map<String, Long> mResultMap;
+        private boolean mTrackWithParents;
+
+        public TestRunnable(Class<?> toTrack, boolean trackWithParents) {
+            mToTrack = toTrack;
+            mTrackWithParents = trackWithParents;
+        }
+
+        @Override
+        public void run() {
+            if (mTrackWithParents) {
+                TfObjectTracker.countWithParents(mToTrack);
+            } else {
+                TfObjectTracker.count(mToTrack);
+            }
+            mResultMap = TfObjectTracker.getUsage();
+        }
+
+        public Map<String, Long> getUsageMap() {
+            return mResultMap;
+        }
+    }
+}