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