1 /*
2  * Copyright (C) 2018 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 
17 package com.android.helpers;
18 
19 import android.util.Log;
20 
21 import com.android.os.AtomsProto.AppStartFullyDrawn;
22 import com.android.os.AtomsProto.AppStartOccurred;
23 import com.android.os.AtomsProto.Atom;
24 import com.android.os.AtomsProto.ProcessStartTime;
25 import com.android.os.StatsLog.EventMetricData;
26 
27 import java.util.ArrayList;
28 import java.util.HashMap;
29 import java.util.List;
30 import java.util.Map;
31 import java.util.Map.Entry;
32 import java.util.stream.Collectors;
33 
34 /**
35  * AppStartupHelper consist of helper methods to set the app
36  * startup configs in statsd to track the app startup related
37  * performance metrics and retrieve the necessary information from
38  * statsd using the config id.
39  */
40 public class AppStartupHelper implements ICollectorHelper<StringBuilder> {
41 
42     private static final String LOG_TAG = AppStartupHelper.class.getSimpleName();
43 
44     private static final String COLD_STARTUP = "cold_startup";
45     private static final String WARM_STARTUP = "warm_startup";
46     private static final String HOT_STARTUP = "hot_startup";
47     private static final String COUNT = "count";
48     private static final String TOTAL_COUNT = "total_count";
49 
50     private static final String STARTUP_FULLY_DRAWN_UNKNOWN = "startup_fully_drawn_unknown";
51     private static final String STARTUP_FULLY_DRAWN_WITH_BUNDLE = "startup_fully_drawn_with_bundle";
52     private static final String STARTUP_FULLY_DRAWN_WITHOUT_BUNDLE =
53             "startup_fully_drawn_without_bundle";
54 
55     private static final String PROCESS_START = "process_start";
56     private static final String PROCESS_START_DELAY = "process_start_delay";
57     private static final String TRANSITION_DELAY_MILLIS = "transition_delay_millis";
58     private boolean isProcStartDetailsDisabled;
59 
60     private StatsdHelper mStatsdHelper = new StatsdHelper();
61 
62     /**
63      * Set up the app startup statsd config to track the metrics during the app start occurred.
64      */
65     @Override
startCollecting()66     public boolean startCollecting() {
67         Log.i(LOG_TAG, "Adding app startup configs to statsd.");
68         List<Integer> atomIdList = new ArrayList<>();
69         atomIdList.add(Atom.APP_START_OCCURRED_FIELD_NUMBER);
70         atomIdList.add(Atom.APP_START_FULLY_DRAWN_FIELD_NUMBER);
71         if (!isProcStartDetailsDisabled) {
72             atomIdList.add(Atom.PROCESS_START_TIME_FIELD_NUMBER);
73         }
74         return mStatsdHelper.addEventConfig(atomIdList);
75     }
76 
77     /**
78      * Collect the app startup metrics tracked during the app startup occurred from the statsd.
79      */
80     @Override
getMetrics()81     public Map<String, StringBuilder> getMetrics() {
82         List<EventMetricData> eventMetricData = mStatsdHelper.getEventMetrics();
83         Map<String, StringBuilder> appStartResultMap = new HashMap<>();
84         Map<String, Integer> appStartCountMap = new HashMap<>();
85         Map<String, Integer> tempResultCountMap = new HashMap<>();
86         for (EventMetricData dataItem : eventMetricData) {
87             Atom atom = dataItem.getAtom();
88             if (atom.hasAppStartOccurred()) {
89                 AppStartOccurred appStartAtom = atom.getAppStartOccurred();
90                 String pkgName = appStartAtom.getPkgName();
91                 String transitionType = appStartAtom.getType().toString();
92                 int windowsDrawnMillis = appStartAtom.getWindowsDrawnDelayMillis();
93                 int transitionDelayMillis = appStartAtom.getTransitionDelayMillis();
94                 Log.i(LOG_TAG, String.format("Pkg Name: %s, Transition Type: %s, "
95                         + "WindowDrawnDelayMillis: %s, TransitionDelayMillis: %s",
96                         pkgName, transitionType, windowsDrawnMillis, transitionDelayMillis));
97 
98                 String metricTypeKey = "";
99                 String metricTransitionKey = "";
100                 // To track number of startups per type per package.
101                 String metricCountKey = "";
102                 // To track total number of startups per type.
103                 String totalCountKey = "";
104                 String typeKey = "";
105                 switch (appStartAtom.getType()) {
106                     case COLD:
107                         typeKey = COLD_STARTUP;
108                         break;
109                     case WARM:
110                         typeKey = WARM_STARTUP;
111                         break;
112                     case HOT:
113                         typeKey = HOT_STARTUP;
114                         break;
115                     case UNKNOWN:
116                         break;
117                 }
118                 if (!typeKey.isEmpty()) {
119                     metricTypeKey = MetricUtility.constructKey(typeKey, pkgName);
120                     metricCountKey = MetricUtility.constructKey(typeKey, COUNT, pkgName);
121                     totalCountKey = MetricUtility.constructKey(typeKey, TOTAL_COUNT);
122 
123                     // Update the windows drawn delay metrics.
124                     MetricUtility.addMetric(metricTypeKey, windowsDrawnMillis, appStartResultMap);
125                     MetricUtility.addMetric(metricCountKey, appStartCountMap);
126                     MetricUtility.addMetric(totalCountKey, appStartCountMap);
127 
128                     // Update the transition delay metrics.
129                     metricTransitionKey = MetricUtility.constructKey(typeKey,
130                             TRANSITION_DELAY_MILLIS, pkgName);
131                     MetricUtility.addMetric(metricTransitionKey, transitionDelayMillis,
132                             appStartResultMap);
133                 }
134             }
135             if (atom.hasAppStartFullyDrawn()) {
136                 AppStartFullyDrawn appFullyDrawnAtom = atom.getAppStartFullyDrawn();
137                 String pkgName = appFullyDrawnAtom.getPkgName();
138                 String transitionType = appFullyDrawnAtom.getType().toString();
139                 long startupTimeMillis = appFullyDrawnAtom.getAppStartupTimeMillis();
140                 Log.i(LOG_TAG, String.format("Pkg Name: %s, Transition Type: %s, "
141                         + "AppStartupTimeMillis: %d", pkgName, transitionType, startupTimeMillis));
142 
143                 String metricKey = "";
144                 switch (appFullyDrawnAtom.getType()) {
145                     case UNKNOWN:
146                         metricKey = MetricUtility.constructKey(
147                                 STARTUP_FULLY_DRAWN_UNKNOWN, pkgName);
148                         break;
149                     case WITH_BUNDLE:
150                         metricKey = MetricUtility.constructKey(
151                                 STARTUP_FULLY_DRAWN_WITH_BUNDLE, pkgName);
152                         break;
153                     case WITHOUT_BUNDLE:
154                         metricKey = MetricUtility.constructKey(
155                                 STARTUP_FULLY_DRAWN_WITHOUT_BUNDLE, pkgName);
156                         break;
157                 }
158                 if (!metricKey.isEmpty()) {
159                     MetricUtility.addMetric(metricKey, startupTimeMillis, appStartResultMap);
160                 }
161             }
162             // ProcessStartTime reports startup time for both foreground and background process.
163             if (atom.hasProcessStartTime()) {
164                 ProcessStartTime processStartTimeAtom = atom.getProcessStartTime();
165                 String processName = processStartTimeAtom.getProcessName();
166                 // Number of milliseconds it takes to finish start of the process.
167                 long processStartDelayMillis = processStartTimeAtom.getProcessStartDelayMillis();
168                 // Treating activity hosting type as foreground and everything else as background.
169                 String hostingType = processStartTimeAtom.getHostingType().equals("activity")
170                         ? "fg" : "bg";
171                 Log.i(LOG_TAG, String.format("Process Name: %s, Start Type: %s, Hosting Type: %s,"
172                         + " ProcessStartDelayMillis: %d", processName,
173                         processStartTimeAtom.getType().toString(),
174                         hostingType, processStartDelayMillis));
175 
176                 String metricKey = "";
177                 // To track number of startups per type per package.
178                 String metricCountKey = "";
179                 // To track total number of startups per type.
180                 String totalCountKey = "";
181                 String typeKey = "";
182                 switch (processStartTimeAtom.getType()) {
183                     case COLD:
184                         typeKey = COLD_STARTUP;
185                         break;
186                     case WARM:
187                         typeKey = WARM_STARTUP;
188                         break;
189                     case HOT:
190                         typeKey = HOT_STARTUP;
191                         break;
192                     case UNKNOWN:
193                         break;
194                 }
195                 if (!typeKey.isEmpty()) {
196                     metricKey = MetricUtility.constructKey(typeKey,
197                             PROCESS_START_DELAY, processName, hostingType);
198                     metricCountKey = MetricUtility.constructKey(typeKey, PROCESS_START,
199                             COUNT, processName, hostingType);
200                     totalCountKey = MetricUtility.constructKey(typeKey, PROCESS_START,
201                             TOTAL_COUNT);
202                     // Update the metrics
203                     if (isProcStartDetailsDisabled) {
204                         MetricUtility.addMetric(metricCountKey, tempResultCountMap);
205                     } else {
206                         MetricUtility.addMetric(metricKey, processStartDelayMillis,
207                                 appStartResultMap);
208                         MetricUtility.addMetric(metricCountKey, appStartCountMap);
209                     }
210 
211                     MetricUtility.addMetric(totalCountKey, appStartCountMap);
212                 }
213             }
214         }
215 
216         if (isProcStartDetailsDisabled) {
217             for (Entry<String, Integer> entry : tempResultCountMap.entrySet()) {
218                 Log.i(LOG_TAG, String.format("Process_delay_key: %s, Count: %d", entry.getKey(),
219                         entry.getValue()));
220             }
221         }
222 
223         // Cast to StringBuilder as the raw app startup metric could be comma separated values
224         // if there are multiple app launches.
225         Map<String, StringBuilder> finalCountMap = appStartCountMap
226                 .entrySet()
227                 .stream()
228                 .collect(
229                         Collectors.toMap(Map.Entry::getKey,
230                                 e -> new StringBuilder(Integer.toString(e.getValue()))));
231         // Add the count map in the app start result map.
232         appStartResultMap.putAll(finalCountMap);
233         return appStartResultMap;
234     }
235 
236     /**
237      * Remove the statsd config used to track the app startup metrics.
238      */
239     @Override
stopCollecting()240     public boolean stopCollecting() {
241         return mStatsdHelper.removeStatsConfig();
242     }
243 
244     /**
245      * Disable process start detailed metrics.
246      */
setDisableProcStartDetails()247     public void setDisableProcStartDetails() {
248         isProcStartDetailsDisabled = true;
249     }
250 }
251