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 the list of EventMetricData tracked under the config. */ getEventMetrics()186 public List<com.android.os.nano.StatsLog.EventMetricData> getEventMetrics() { 187 List<com.android.os.nano.StatsLog.EventMetricData> eventData = new ArrayList<>(); 188 com.android.os.nano.StatsLog.ConfigMetricsReportList reportList = null; 189 try { 190 if (getConfigId() != -1) { 191 adoptShellIdentity(); 192 byte[] serializedReports = getStatsManager().getReports(getConfigId()); 193 reportList = 194 com.android.os.nano.StatsLog.ConfigMetricsReportList.parseFrom( 195 serializedReports); 196 dropShellIdentity(); 197 } 198 } catch (InvalidProtocolBufferNanoException | StatsUnavailableException se) { 199 Log.e(LOG_TAG, "Retreiving event metrics failed.", se); 200 return eventData; 201 } 202 203 if (reportList != null && reportList.reports.length > 0) { 204 com.android.os.nano.StatsLog.ConfigMetricsReport configReport = reportList.reports[0]; 205 for (com.android.os.nano.StatsLog.StatsLogReport metric : configReport.metrics) { 206 com.android.os.nano.StatsLog.StatsLogReport.EventMetricDataWrapper 207 eventMetricDataWrapper = metric.getEventMetrics(); 208 if (eventMetricDataWrapper != null) { 209 eventData.addAll(Arrays.asList(eventMetricDataWrapper.data)); 210 } 211 } 212 } 213 Log.i(LOG_TAG, "Number of events: " + eventData.size()); 214 return eventData; 215 } 216 217 /** Returns the list of GaugeMetric data tracked under the config. */ getGaugeMetrics()218 public List<com.android.os.nano.StatsLog.GaugeMetricData> getGaugeMetrics() { 219 com.android.os.nano.StatsLog.ConfigMetricsReportList reportList = null; 220 List<com.android.os.nano.StatsLog.GaugeMetricData> gaugeData = new ArrayList<>(); 221 try { 222 if (getConfigId() != -1) { 223 adoptShellIdentity(); 224 StatsLog.logEvent(0); 225 // Dump the the counters after the test completed. 226 SystemClock.sleep(METRIC_DELAY_MS); 227 reportList = 228 com.android.os.nano.StatsLog.ConfigMetricsReportList.parseFrom( 229 getStatsManager().getReports(getConfigId())); 230 dropShellIdentity(); 231 } 232 } catch (InvalidProtocolBufferNanoException | StatsUnavailableException se) { 233 Log.e(LOG_TAG, "Retreiving gauge metrics failed.", se); 234 return gaugeData; 235 } 236 237 if (reportList != null && reportList.reports.length > 0) { 238 com.android.os.nano.StatsLog.ConfigMetricsReport configReport = reportList.reports[0]; 239 for (com.android.os.nano.StatsLog.StatsLogReport metric : configReport.metrics) { 240 com.android.os.nano.StatsLog.StatsLogReport.GaugeMetricDataWrapper 241 gaugeMetricDataWrapper = metric.getGaugeMetrics(); 242 if (gaugeMetricDataWrapper != null) { 243 gaugeData.addAll(Arrays.asList(gaugeMetricDataWrapper.data)); 244 } 245 } 246 } 247 Log.i(LOG_TAG, "Number of Gauge data: " + gaugeData.size()); 248 return gaugeData; 249 } 250 251 /** 252 * Remove the existing config tracked in the statsd. 253 * 254 * @return true if the config is removed successfully otherwise false. 255 */ removeStatsConfig()256 public boolean removeStatsConfig() { 257 Log.i(LOG_TAG, "Removing statsd config-id: " + getConfigId()); 258 try { 259 adoptShellIdentity(); 260 getStatsManager().removeConfig(getConfigId()); 261 dropShellIdentity(); 262 Log.i(LOG_TAG, "Successfully removed config-id: " + getConfigId()); 263 return true; 264 } catch (StatsUnavailableException e) { 265 Log.e(LOG_TAG, String.format("Not able to remove the config-id: %d due to %s ", 266 getConfigId(), e.getMessage())); 267 return false; 268 } 269 } 270 271 /** Returns the package name for the UID if it is available. Otherwise return null. */ getPackageName(int uid)272 public String getPackageName(int uid) { 273 String pkgName = 274 InstrumentationRegistry.getTargetContext().getPackageManager().getNameForUid(uid); 275 // Remove the UID appended at the end of the package name. 276 if (pkgName != null) { 277 String[] pkgNameSplit = pkgName.split(String.format("\\:%d", uid)); 278 return pkgNameSplit[0]; 279 } 280 return pkgName; 281 } 282 283 /** Gets {@code StatsManager}, used to configure, collect and remove the statsd configs. */ getStatsManager()284 private StatsManager getStatsManager() { 285 if (mStatsManager == null) { 286 mStatsManager = (StatsManager) InstrumentationRegistry.getTargetContext(). 287 getSystemService(Context.STATS_MANAGER); 288 } 289 return mStatsManager; 290 } 291 292 /** Returns the package name associated with this UID if available, or null otherwise. */ 293 /** 294 * Serializes a {@link StatsdConfigProto.StatsdConfig}. 295 * 296 * @return byte[] 297 */ toByteArray(StatsdConfigProto.StatsdConfig config)298 private static byte[] toByteArray(StatsdConfigProto.StatsdConfig config) throws IOException { 299 byte[] serialized = new byte[config.getSerializedSize()]; 300 CodedOutputByteBufferNano outputByteBufferNano = 301 CodedOutputByteBufferNano.newInstance(serialized); 302 config.writeTo(outputByteBufferNano); 303 return serialized; 304 } 305 306 /** Sets the statsd config id currently tracked by this class. */ setConfigId(long configId)307 private void setConfigId(long configId) { 308 mConfigId = configId; 309 } 310 311 /** Returns the statsd config id currently tracked by this class. */ getConfigId()312 private long getConfigId() { 313 return mConfigId; 314 } 315 316 /** Returns a unique identifier using a {@code UUID}'s hashcode. */ getUniqueId()317 private static int getUniqueId() { 318 return UUID.randomUUID().hashCode(); 319 } 320 321 /** 322 * Adopts shell permission identity needed to access StatsManager service 323 */ adoptShellIdentity()324 public static void adoptShellIdentity() { 325 InstrumentationRegistry.getInstrumentation().getUiAutomation() 326 .adoptShellPermissionIdentity(); 327 } 328 329 /** 330 * Drop shell permission identity 331 */ dropShellIdentity()332 public static void dropShellIdentity() { 333 InstrumentationRegistry.getInstrumentation().getUiAutomation() 334 .dropShellPermissionIdentity(); 335 } 336 337 } 338