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