1 /*
2  * Copyright (C) 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.tradefed.invoker.logger;
17 
18 import com.android.tradefed.log.LogUtil.CLog;
19 
20 import java.util.Collections;
21 import java.util.HashMap;
22 import java.util.Map;
23 
24 /** A utility class for an invocation to log some metrics. */
25 public class InvocationMetricLogger {
26 
27     /** Some special named key that we will always populate for the invocation. */
28     public enum InvocationMetricKey {
29         WIFI_AP_NAME("wifi_ap_name", false),
30         CLEARED_RUN_ERROR("cleared_run_error", true),
31         FETCH_BUILD("fetch_build_time_ms", true),
32         SETUP("setup_time_ms", true),
33         SHARDING_DEVICE_SETUP_TIME("remote_device_sharding_setup_ms", true),
34         AUTO_RETRY_TIME("auto_retry_time_ms", true),
35         STAGE_TESTS_TIME("stage_tests_time_ms", true),
36         STAGE_TESTS_BYTES("stage_tests_bytes", true),
37         STAGE_TESTS_INDIVIDUAL_DOWNLOADS("stage_tests_individual_downloads", true),
38         SHUTDOWN_HARD_LATENCY("shutdown_hard_latency_ms", false),
39         DEVICE_DONE_TIMESTAMP("device_done_timestamp", false),
40         DEVICE_RELEASE_STATE("device_release_state", false),
41         DEVICE_LOST_DETECTED("device_lost_detected", false),
42         VIRTUAL_DEVICE_LOST_DETECTED("virtual_device_lost_detected", false),
43         DEVICE_RECOVERY("device_recovery", true),
44         DEVICE_RECOVERY_FROM_RECOVERY("device_recovery_from_recovery", true),
45         DEVICE_RECOVERY_FAIL("device_recovery_fail", true),
46         SANDBOX_EXIT_CODE("sandbox_exit_code", false),
47         CF_FETCH_ARTIFACT_TIME("cf_fetch_artifact_time_ms", false),
48         CF_GCE_CREATE_TIME("cf_gce_create_time_ms", false),
49         CF_LAUNCH_CVD_TIME("cf_launch_cvd_time_ms", false),
50         CF_INSTANCE_COUNT("cf_instance_count", false);
51 
52         private final String mKeyName;
53         // Whether or not to add the value when the key is added again.
54         private final boolean mAdditive;
55 
InvocationMetricKey(String key, boolean additive)56         private InvocationMetricKey(String key, boolean additive) {
57             mKeyName = key;
58             mAdditive = additive;
59         }
60 
61         @Override
toString()62         public String toString() {
63             return mKeyName;
64         }
65 
shouldAdd()66         public boolean shouldAdd() {
67             return mAdditive;
68         }
69     }
70 
InvocationMetricLogger()71     private InvocationMetricLogger() {}
72 
73     /**
74      * Track metrics per ThreadGroup as a proxy to invocation since an invocation run within one
75      * threadgroup.
76      */
77     private static final Map<ThreadGroup, Map<String, String>> mPerGroupMetrics =
78             Collections.synchronizedMap(new HashMap<ThreadGroup, Map<String, String>>());
79 
80     /**
81      * Add one key-value to be tracked at the invocation level.
82      *
83      * @param key The key under which the invocation metric will be tracked.
84      * @param value The value of the invocation metric.
85      */
addInvocationMetrics(InvocationMetricKey key, long value)86     public static void addInvocationMetrics(InvocationMetricKey key, long value) {
87         if (key.shouldAdd()) {
88             String existingVal = getInvocationMetrics().get(key.toString());
89             long existingLong = 0L;
90             if (existingVal != null) {
91                 try {
92                     existingLong = Long.parseLong(existingVal);
93                 } catch (NumberFormatException e) {
94                     CLog.e(
95                             "%s is expected to contain a number, instead found: %s",
96                             key.toString(), existingVal);
97                 }
98             }
99             value += existingLong;
100         }
101         addInvocationMetrics(key.toString(), Long.toString(value));
102     }
103 
104     /**
105      * Add one key-value to be tracked at the invocation level.
106      *
107      * @param key The key under which the invocation metric will be tracked.
108      * @param value The value of the invocation metric.
109      */
addInvocationMetrics(InvocationMetricKey key, String value)110     public static void addInvocationMetrics(InvocationMetricKey key, String value) {
111         if (key.shouldAdd()) {
112             String existingVal = getInvocationMetrics().get(key.toString());
113             if (existingVal != null) {
114                 value = String.format("%s,%s", existingVal, value);
115             }
116         }
117         addInvocationMetrics(key.toString(), value);
118     }
119 
120     /**
121      * Add one key-value to be tracked at the invocation level. Don't expose the String key yet to
122      * avoid abuse, stick to the official {@link InvocationMetricKey} to start with.
123      *
124      * @param key The key under which the invocation metric will be tracked.
125      * @param value The value of the invocation metric.
126      */
addInvocationMetrics(String key, String value)127     private static void addInvocationMetrics(String key, String value) {
128         ThreadGroup group = Thread.currentThread().getThreadGroup();
129         synchronized (mPerGroupMetrics) {
130             if (mPerGroupMetrics.get(group) == null) {
131                 mPerGroupMetrics.put(group, new HashMap<>());
132             }
133             mPerGroupMetrics.get(group).put(key, value);
134         }
135     }
136 
137     /** Returns the Map of invocation metrics for the invocation in progress. */
getInvocationMetrics()138     public static Map<String, String> getInvocationMetrics() {
139         ThreadGroup group = Thread.currentThread().getThreadGroup();
140         synchronized (mPerGroupMetrics) {
141             if (mPerGroupMetrics.get(group) == null) {
142                 mPerGroupMetrics.put(group, new HashMap<>());
143             }
144         }
145         return new HashMap<>(mPerGroupMetrics.get(group));
146     }
147 
148     /** Clear the invocation metrics for an invocation. */
clearInvocationMetrics()149     public static void clearInvocationMetrics() {
150         ThreadGroup group = Thread.currentThread().getThreadGroup();
151         synchronized (mPerGroupMetrics) {
152             mPerGroupMetrics.remove(group);
153         }
154     }
155 }
156