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