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