• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.app.StatsManager;
20 import android.app.StatsManager.StatsUnavailableException;
21 import android.content.Context;
22 import android.os.SystemClock;
23 import android.util.Log;
24 import android.util.Pair;
25 import android.util.StatsLog;
26 
27 import androidx.test.InstrumentationRegistry;
28 
29 import com.android.internal.os.nano.StatsdConfigProto;
30 import com.android.os.nano.AtomsProto;
31 
32 import com.google.protobuf.nano.CodedOutputByteBufferNano;
33 import com.google.protobuf.nano.InvalidProtocolBufferNanoException;
34 
35 import java.io.IOException;
36 import java.util.ArrayList;
37 import java.util.Arrays;
38 import java.util.Comparator;
39 import java.util.List;
40 import java.util.UUID;
41 
42 /**
43  * StatsdHelper consist of basic utilities that will be used to setup statsd
44  * config, parse the collected information and remove the statsd config.
45  */
46 public class StatsdHelper {
47     private static final String LOG_TAG = StatsdHelper.class.getSimpleName();
48     private static final long MAX_ATOMS = 2000;
49     private static final long METRIC_DELAY_MS = 3000;
50     private long mConfigId = -1;
51     private StatsManager mStatsManager;
52 
53     /**
54      * Add simple event configurations using a list of atom ids.
55      *
56      * @param atomIdList uniquely identifies the information that we need to track by statsManager.
57      * @return true if the configuration is added successfully, otherwise false.
58      */
addEventConfig(List<Integer> atomIdList)59     public boolean addEventConfig(List<Integer> atomIdList) {
60         long configId = System.currentTimeMillis();
61         StatsdConfigProto.StatsdConfig config = getSimpleSources(configId);
62         List<StatsdConfigProto.EventMetric> metrics = new ArrayList<>(atomIdList.size());
63         List<StatsdConfigProto.AtomMatcher> atomMatchers = new ArrayList<>(atomIdList.size());
64         for (Integer atomId : atomIdList) {
65             int atomUniqueId = getUniqueId();
66             StatsdConfigProto.EventMetric metric = new StatsdConfigProto.EventMetric();
67             metric.id = getUniqueId();
68             metric.what = atomUniqueId;
69             metrics.add(metric);
70             atomMatchers.add(getSimpleAtomMatcher(atomUniqueId, atomId));
71         }
72         config.eventMetric = metrics.toArray(new StatsdConfigProto.EventMetric[0]);
73         config.atomMatcher = atomMatchers.toArray(new StatsdConfigProto.AtomMatcher[0]);
74         try {
75             adoptShellIdentity();
76             getStatsManager().addConfig(configId, toByteArray(config));
77             dropShellIdentity();
78         } catch (Exception e) {
79             Log.e(LOG_TAG, "Not able to setup the event config.", e);
80             return false;
81         }
82         Log.i(LOG_TAG, "Successfully added config with config-id:" + configId);
83         setConfigId(configId);
84         return true;
85     }
86 
87     /**
88      * Build gauge metric config based on trigger events (i.e AppBreadCrumbReported).
89      * Whenever the events are triggered via StatsLog.logEvent() collect the gauge metrics.
90      * It doesn't matter what the log event is. It could be 0 or 1.
91      * In order to capture the usage during the test take the difference of gauge metrics
92      * before and after the test.
93      *
94      * @param atomIdList List of atoms to be collected in gauge metrics.
95      * @return if the config is added successfully otherwise false.
96      */
addGaugeConfig(List<Integer> atomIdList)97     public boolean addGaugeConfig(List<Integer> atomIdList) {
98         long configId = System.currentTimeMillis();
99         StatsdConfigProto.StatsdConfig config = getSimpleSources(configId);
100         int appBreadCrumbUniqueId = getUniqueId();
101         config.whitelistedAtomIds =
102                 new int[] {AtomsProto.Atom.APP_BREADCRUMB_REPORTED_FIELD_NUMBER};
103         List<StatsdConfigProto.AtomMatcher> matchers = new ArrayList<>(atomIdList.size());
104         List<StatsdConfigProto.GaugeMetric> gaugeMetrics = new ArrayList<>();
105         // Needed for collecting gauge metric based on trigger events.
106         matchers.add(
107                 getSimpleAtomMatcher(
108                         appBreadCrumbUniqueId,
109                         AtomsProto.Atom.APP_BREADCRUMB_REPORTED_FIELD_NUMBER));
110         for (Integer atomId : atomIdList) {
111             int atomUniqueId = getUniqueId();
112             // Build Gauge metric config.
113             StatsdConfigProto.GaugeMetric gaugeMetric = new StatsdConfigProto.GaugeMetric();
114             gaugeMetric.id = getUniqueId();
115             gaugeMetric.what = atomUniqueId;
116             StatsdConfigProto.FieldFilter fieldFilter = new StatsdConfigProto.FieldFilter();
117             fieldFilter.includeAll = true;
118             gaugeMetric.gaugeFieldsFilter = fieldFilter;
119             gaugeMetric.maxNumGaugeAtomsPerBucket = MAX_ATOMS;
120             gaugeMetric.samplingType = StatsdConfigProto.GaugeMetric.FIRST_N_SAMPLES;
121             gaugeMetric.triggerEvent = appBreadCrumbUniqueId;
122             gaugeMetric.bucket = StatsdConfigProto.CTS;
123             matchers.add(getSimpleAtomMatcher(atomUniqueId, atomId));
124             gaugeMetrics.add(gaugeMetric);
125         }
126         config.atomMatcher = matchers.toArray(new StatsdConfigProto.AtomMatcher[0]);
127         config.gaugeMetric = gaugeMetrics.toArray(new StatsdConfigProto.GaugeMetric[0]);
128         try {
129             adoptShellIdentity();
130             getStatsManager().addConfig(configId, toByteArray(config));
131             StatsLog.logEvent(0);
132             // Dump the counters before the test started.
133             SystemClock.sleep(METRIC_DELAY_MS);
134             dropShellIdentity();
135         } catch (Exception e) {
136             Log.e(LOG_TAG, "Not able to setup the gauge config.", e);
137             return false;
138         }
139 
140         Log.i(LOG_TAG, "Successfully added config with config-id:" + configId);
141         setConfigId(configId);
142         return true;
143     }
144 
145     /** Create simple atom matcher with the given id and the field id. */
getSimpleAtomMatcher(int id, int fieldId)146     private StatsdConfigProto.AtomMatcher getSimpleAtomMatcher(int id, int fieldId) {
147         StatsdConfigProto.AtomMatcher atomMatcher = new StatsdConfigProto.AtomMatcher();
148         atomMatcher.id = id;
149         StatsdConfigProto.SimpleAtomMatcher simpleAtomMatcher =
150                 new StatsdConfigProto.SimpleAtomMatcher();
151         simpleAtomMatcher.atomId = fieldId;
152         atomMatcher.setSimpleAtomMatcher(simpleAtomMatcher);
153         return atomMatcher;
154     }
155 
156     /**
157      * Create a statsd config with the list of authorized source that can write metrics.
158      *
159      * @param configId unique id of the configuration tracked by StatsManager.
160      */
getSimpleSources(long configId)161     private static StatsdConfigProto.StatsdConfig getSimpleSources(long configId) {
162         StatsdConfigProto.StatsdConfig config = new StatsdConfigProto.StatsdConfig();
163         config.id = configId;
164         String[] allowedLogSources =
165                 new String[] {
166                     "AID_ROOT",
167                     "AID_SYSTEM",
168                     "AID_RADIO",
169                     "AID_BLUETOOTH",
170                     "AID_GRAPHICS",
171                     "AID_STATSD",
172                     "AID_INCIENTD"
173                 };
174         String[] defaultPullPackages =
175                 new String[] {"AID_SYSTEM", "AID_RADIO", "AID_STATSD", "AID_GPU_SERVICE"};
176         int[] whitelistedAtomIds =
177                 new int[] {
178                     AtomsProto.Atom.UI_INTERACTION_FRAME_INFO_REPORTED_FIELD_NUMBER,
179                     AtomsProto.Atom.UI_ACTION_LATENCY_REPORTED_FIELD_NUMBER
180                 };
181         config.allowedLogSource = allowedLogSources;
182         config.defaultPullPackages = defaultPullPackages;
183         config.whitelistedAtomIds = whitelistedAtomIds;
184         return config;
185     }
186 
187     /** Returns accumulated StatsdStats. */
getStatsdStatsReport()188     public com.android.os.nano.StatsLog.StatsdStatsReport getStatsdStatsReport() {
189         com.android.os.nano.StatsLog.StatsdStatsReport report =
190                 new com.android.os.nano.StatsLog.StatsdStatsReport();
191         try {
192             adoptShellIdentity();
193             byte[] serializedReports = getStatsManager().getStatsMetadata();
194             report = com.android.os.nano.StatsLog.StatsdStatsReport.parseFrom(serializedReports);
195             dropShellIdentity();
196         } catch (InvalidProtocolBufferNanoException | StatsUnavailableException se) {
197             Log.e(LOG_TAG, "Retrieving StatsdStats report failed.", se);
198         }
199         return report;
200     }
201 
202     /** Returns the list of EventMetricData tracked under the config. */
getEventMetrics()203     public List<com.android.os.nano.StatsLog.EventMetricData> getEventMetrics() {
204         List<com.android.os.nano.StatsLog.EventMetricData> eventData = new ArrayList<>();
205         com.android.os.nano.StatsLog.ConfigMetricsReportList reportList = null;
206         try {
207             if (getConfigId() != -1) {
208                 adoptShellIdentity();
209                 byte[] serializedReports = getStatsManager().getReports(getConfigId());
210                 reportList =
211                         com.android.os.nano.StatsLog.ConfigMetricsReportList.parseFrom(
212                                 serializedReports);
213                 dropShellIdentity();
214             }
215         } catch (InvalidProtocolBufferNanoException | StatsUnavailableException se) {
216             Log.e(LOG_TAG, "Retrieving event metrics failed.", se);
217             return eventData;
218         }
219 
220         if (reportList != null && reportList.reports.length > 0) {
221             com.android.os.nano.StatsLog.ConfigMetricsReport configReport = reportList.reports[0];
222             for (com.android.os.nano.StatsLog.StatsLogReport metric : configReport.metrics) {
223                 com.android.os.nano.StatsLog.StatsLogReport.EventMetricDataWrapper
224                         eventMetricDataWrapper = metric.getEventMetrics();
225                 List<com.android.os.nano.StatsLog.EventMetricData> backfilledData =
226                         new ArrayList<>();
227                 if (eventMetricDataWrapper != null) {
228                     for (com.android.os.nano.StatsLog.EventMetricData eventMetricData :
229                             eventMetricDataWrapper.data) {
230                         backfilledData.addAll(backfillEventMetricData(eventMetricData));
231                     }
232                     backfilledData.sort(Comparator.comparing(d -> d.elapsedTimestampNanos));
233                     eventData.addAll(backfilledData);
234                 }
235             }
236         }
237         Log.i(LOG_TAG, "Number of events: " + eventData.size());
238         return eventData;
239     }
240 
241     /** Returns the list of GaugeMetric data tracked under the config. */
getGaugeMetrics()242     public List<com.android.os.nano.StatsLog.GaugeMetricData> getGaugeMetrics() {
243         com.android.os.nano.StatsLog.ConfigMetricsReportList reportList = null;
244         List<com.android.os.nano.StatsLog.GaugeMetricData> gaugeData = new ArrayList<>();
245         try {
246             if (getConfigId() != -1) {
247                 adoptShellIdentity();
248                 StatsLog.logEvent(0);
249                 // Dump the the counters after the test completed.
250                 SystemClock.sleep(METRIC_DELAY_MS);
251                 reportList =
252                         com.android.os.nano.StatsLog.ConfigMetricsReportList.parseFrom(
253                                 getStatsManager().getReports(getConfigId()));
254                 dropShellIdentity();
255             }
256         } catch (InvalidProtocolBufferNanoException | StatsUnavailableException se) {
257             Log.e(LOG_TAG, "Retrieving gauge metrics failed.", se);
258             return gaugeData;
259         }
260 
261         if (reportList != null && reportList.reports.length > 0) {
262             com.android.os.nano.StatsLog.ConfigMetricsReport configReport = reportList.reports[0];
263             for (com.android.os.nano.StatsLog.StatsLogReport metric : configReport.metrics) {
264                 com.android.os.nano.StatsLog.StatsLogReport.GaugeMetricDataWrapper
265                         gaugeMetricDataWrapper = metric.getGaugeMetrics();
266                 backfillGaugeMetricData(gaugeMetricDataWrapper);
267                 if (gaugeMetricDataWrapper != null) {
268                     gaugeData.addAll(Arrays.asList(gaugeMetricDataWrapper.data));
269                 }
270             }
271         }
272         Log.i(LOG_TAG, "Number of Gauge data: " + gaugeData.size());
273         return gaugeData;
274     }
275 
276     /**
277      * Remove the existing config tracked in the statsd.
278      *
279      * @return true if the config is removed successfully otherwise false.
280      */
removeStatsConfig()281     public boolean removeStatsConfig() {
282         Log.i(LOG_TAG, "Removing statsd config-id: " + getConfigId());
283         try {
284             adoptShellIdentity();
285             getStatsManager().removeConfig(getConfigId());
286             dropShellIdentity();
287             Log.i(LOG_TAG, "Successfully removed config-id: " + getConfigId());
288             return true;
289         } catch (StatsUnavailableException e) {
290             Log.e(LOG_TAG, String.format("Not able to remove the config-id: %d due to %s ",
291                     getConfigId(), e.getMessage()));
292             return false;
293         }
294     }
295 
296     /** Returns the package name for the UID if it is available. Otherwise return null. */
getPackageName(int uid)297     public String getPackageName(int uid) {
298         String pkgName =
299                 InstrumentationRegistry.getTargetContext().getPackageManager().getNameForUid(uid);
300         // Remove the UID appended at the end of the package name.
301         if (pkgName != null) {
302             String[] pkgNameSplit = pkgName.split(String.format("\\:%d", uid));
303             return pkgNameSplit[0];
304         }
305         return pkgName;
306     }
307 
backfillEventMetricData( com.android.os.nano.StatsLog.EventMetricData metricData)308     private List<com.android.os.nano.StatsLog.EventMetricData> backfillEventMetricData(
309             com.android.os.nano.StatsLog.EventMetricData metricData) {
310         if (metricData.aggregatedAtomInfo == null) {
311             return List.of(metricData);
312         }
313         List<com.android.os.nano.StatsLog.EventMetricData> data = new ArrayList<>();
314         com.android.os.nano.StatsLog.AggregatedAtomInfo atomInfo = metricData.aggregatedAtomInfo;
315         for (long timestamp : atomInfo.elapsedTimestampNanos) {
316             com.android.os.nano.StatsLog.EventMetricData newMetricData =
317                     new com.android.os.nano.StatsLog.EventMetricData();
318             newMetricData.atom = atomInfo.atom;
319             newMetricData.elapsedTimestampNanos = timestamp;
320             data.add(newMetricData);
321         }
322         return data;
323     }
324 
backfillGaugeMetricData( com.android.os.nano.StatsLog.StatsLogReport.GaugeMetricDataWrapper dataWrapper)325     protected void backfillGaugeMetricData(
326             com.android.os.nano.StatsLog.StatsLogReport.GaugeMetricDataWrapper dataWrapper) {
327         if (dataWrapper == null) {
328             return;
329         }
330         for (com.android.os.nano.StatsLog.GaugeMetricData gaugeMetricData : dataWrapper.data) {
331             for (com.android.os.nano.StatsLog.GaugeBucketInfo bucketInfo :
332                     gaugeMetricData.bucketInfo) {
333                 backfillGaugeBucket(bucketInfo);
334             }
335         }
336     }
337 
backfillGaugeBucket(com.android.os.nano.StatsLog.GaugeBucketInfo bucketInfo)338     private void backfillGaugeBucket(com.android.os.nano.StatsLog.GaugeBucketInfo bucketInfo) {
339         if (bucketInfo.atom.length != 0) {
340             return;
341         }
342         List<Pair<AtomsProto.Atom, Long>> atomTimestampData = new ArrayList<>();
343         for (com.android.os.nano.StatsLog.AggregatedAtomInfo atomInfo :
344                 bucketInfo.aggregatedAtomInfo) {
345             for (long timestampNs : atomInfo.elapsedTimestampNanos) {
346                 atomTimestampData.add(Pair.create(atomInfo.atom, timestampNs));
347             }
348         }
349         atomTimestampData.sort(Comparator.comparing(o -> o.second));
350         bucketInfo.atom = new AtomsProto.Atom[atomTimestampData.size()];
351         bucketInfo.elapsedTimestampNanos = new long[atomTimestampData.size()];
352         for (int i = 0; i < atomTimestampData.size(); i++) {
353             bucketInfo.atom[i] = atomTimestampData.get(i).first;
354             bucketInfo.elapsedTimestampNanos[i] = atomTimestampData.get(i).second;
355         }
356     }
357 
358     /** Gets {@code StatsManager}, used to configure, collect and remove the statsd configs. */
getStatsManager()359     private StatsManager getStatsManager() {
360         if (mStatsManager == null) {
361             mStatsManager = (StatsManager) InstrumentationRegistry.getTargetContext().
362                     getSystemService(Context.STATS_MANAGER);
363         }
364         return mStatsManager;
365     }
366 
367     /** Returns the package name associated with this UID if available, or null otherwise. */
368     /**
369      * Serializes a {@link StatsdConfigProto.StatsdConfig}.
370      *
371      * @return byte[]
372      */
toByteArray(StatsdConfigProto.StatsdConfig config)373     private static byte[] toByteArray(StatsdConfigProto.StatsdConfig config) throws IOException {
374         byte[] serialized = new byte[config.getSerializedSize()];
375         CodedOutputByteBufferNano outputByteBufferNano =
376                 CodedOutputByteBufferNano.newInstance(serialized);
377         config.writeTo(outputByteBufferNano);
378         return serialized;
379     }
380 
381     /** Sets the statsd config id currently tracked by this class. */
setConfigId(long configId)382     private void setConfigId(long configId) {
383         mConfigId = configId;
384     }
385 
386     /** Returns the statsd config id currently tracked by this class. */
getConfigId()387     private long getConfigId() {
388         return mConfigId;
389     }
390 
391     /** Returns a unique identifier using a {@code UUID}'s hashcode. */
getUniqueId()392     private static int getUniqueId() {
393         return UUID.randomUUID().hashCode();
394     }
395 
396     /**
397      * Adopts shell permission identity needed to access StatsManager service
398      */
adoptShellIdentity()399     public static void adoptShellIdentity() {
400         InstrumentationRegistry.getInstrumentation().getUiAutomation()
401                 .adoptShellPermissionIdentity();
402     }
403 
404     /**
405      * Drop shell permission identity
406      */
dropShellIdentity()407     public static void dropShellIdentity() {
408         InstrumentationRegistry.getInstrumentation().getUiAutomation()
409                 .dropShellPermissionIdentity();
410     }
411 
412 }
413