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 package com.android.statsd.shelltools.testdrive; 17 18 import com.android.internal.os.StatsdConfigProto; 19 import com.android.internal.os.StatsdConfigProto.AtomMatcher; 20 import com.android.internal.os.StatsdConfigProto.EventMetric; 21 import com.android.internal.os.StatsdConfigProto.FieldFilter; 22 import com.android.internal.os.StatsdConfigProto.GaugeMetric; 23 import com.android.internal.os.StatsdConfigProto.PullAtomPackages; 24 import com.android.internal.os.StatsdConfigProto.SimpleAtomMatcher; 25 import com.android.internal.os.StatsdConfigProto.StatsdConfig; 26 import com.android.internal.os.StatsdConfigProto.TimeUnit; 27 import com.android.os.AtomsProto.Atom; 28 import com.android.os.StatsLog; 29 import com.android.os.StatsLog.ConfigMetricsReport; 30 import com.android.os.StatsLog.ConfigMetricsReportList; 31 import com.android.os.StatsLog.StatsLogReport; 32 import com.android.statsd.shelltools.Utils; 33 34 import com.google.common.annotations.VisibleForTesting; 35 import com.google.common.io.Files; 36 37 import java.io.File; 38 import java.io.IOException; 39 import java.util.ArrayList; 40 import java.util.Arrays; 41 import java.util.Collections; 42 import java.util.HashSet; 43 import java.util.List; 44 import java.util.Scanner; 45 import java.util.Set; 46 import java.util.TreeSet; 47 import java.util.logging.Level; 48 import java.util.logging.Logger; 49 50 public class TestDrive { 51 52 private static final int METRIC_ID_BASE = 1111; 53 private static final long ATOM_MATCHER_ID_BASE = 1234567; 54 private static final long APP_BREADCRUMB_MATCHER_ID = 1111111; 55 private static final int PULL_ATOM_START = 10000; 56 private static final int MAX_PLATFORM_ATOM_TAG = 100000; 57 private static final int VENDOR_PULLED_ATOM_START_TAG = 150000; 58 private static final long CONFIG_ID = 54321; 59 private static final String[] ALLOWED_LOG_SOURCES = { 60 "AID_GRAPHICS", 61 "AID_INCIDENTD", 62 "AID_STATSD", 63 "AID_RADIO", 64 "com.android.systemui", 65 "com.android.vending", 66 "AID_SYSTEM", 67 "AID_ROOT", 68 "AID_BLUETOOTH", 69 "AID_LMKD", 70 "com.android.managedprovisioning", 71 "AID_MEDIA", 72 "AID_NETWORK_STACK", 73 "com.google.android.providers.media.module", 74 "com.android.imsserviceentitlement", 75 "com.google.android.cellbroadcastreceiver", 76 "AID_KEYSTORE", 77 }; 78 private static final String[] DEFAULT_PULL_SOURCES = { 79 "AID_KEYSTORE", 80 "AID_RADIO", 81 "AID_SYSTEM", 82 }; 83 private static final Logger LOGGER = Logger.getLogger(TestDrive.class.getName()); 84 85 @VisibleForTesting 86 String mDeviceSerial = null; 87 @VisibleForTesting 88 Dumper mDumper = new BasicDumper(); 89 boolean mPressToContinue = false; 90 main(String[] args)91 public static void main(String[] args) { 92 final Configuration configuration = new Configuration(); 93 final TestDrive testDrive = new TestDrive(); 94 Utils.setUpLogger(LOGGER, false); 95 96 if (!testDrive.processArgs(configuration, args, 97 Utils.getDeviceSerials(LOGGER), Utils.getDefaultDevice(LOGGER))) { 98 return; 99 } 100 101 final ConfigMetricsReportList reports = testDrive.testDriveAndGetReports( 102 configuration.createConfig(), configuration.hasPulledAtoms(), 103 configuration.hasPushedAtoms()); 104 if (reports != null) { 105 configuration.dumpMetrics(reports, testDrive.mDumper); 106 } 107 } 108 processArgs(Configuration configuration, String[] args, List<String> connectedDevices, String defaultDevice)109 boolean processArgs(Configuration configuration, String[] args, List<String> connectedDevices, 110 String defaultDevice) { 111 if (args.length < 1) { 112 LOGGER.severe("Usage: ./test_drive [-one] " 113 + "[-p additional_allowed_package] " 114 + "[-s DEVICE_SERIAL_NUMBER] " 115 + "<atomId1> <atomId2> ... <atomIdN>"); 116 return false; 117 } 118 119 int first_arg = 0; 120 // Consume all flags, which must precede all atoms 121 for (; first_arg < args.length; ++first_arg) { 122 String arg = args[first_arg]; 123 int remaining_args = args.length - first_arg; 124 if (remaining_args >= 2 && arg.equals("-one")) { 125 LOGGER.info("Creating one event metric to catch all pushed atoms."); 126 configuration.mOnePushedAtomEvent = true; 127 } else if (remaining_args >= 2 && arg.equals("-terse")) { 128 LOGGER.info("Terse output format."); 129 mDumper = new TerseDumper(); 130 } else if (remaining_args >= 3 && arg.equals("-p")) { 131 configuration.mAdditionalAllowedPackage = args[++first_arg]; 132 } else if (remaining_args >= 3 && arg.equals("-s")) { 133 mDeviceSerial = args[++first_arg]; 134 } else if (remaining_args >= 2 && arg.equals("-e")) { 135 mPressToContinue = true; 136 } else { 137 break; // Found the atom list 138 } 139 } 140 141 mDeviceSerial = Utils.chooseDevice(mDeviceSerial, connectedDevices, defaultDevice, LOGGER); 142 if (mDeviceSerial == null) { 143 return false; 144 } 145 146 for ( ; first_arg < args.length; ++first_arg) { 147 String atom = args[first_arg]; 148 try { 149 configuration.addAtom(Integer.valueOf(atom)); 150 } catch (NumberFormatException e) { 151 LOGGER.severe("Bad atom id provided: " + atom); 152 } 153 } 154 155 return configuration.hasPulledAtoms() || configuration.hasPushedAtoms(); 156 } 157 testDriveAndGetReports(StatsdConfig config, boolean hasPulledAtoms, boolean hasPushedAtoms)158 private ConfigMetricsReportList testDriveAndGetReports(StatsdConfig config, 159 boolean hasPulledAtoms, boolean hasPushedAtoms) { 160 if (config == null) { 161 LOGGER.severe("Failed to create valid config."); 162 return null; 163 } 164 165 String remoteConfigPath = null; 166 try { 167 remoteConfigPath = pushConfig(config, mDeviceSerial); 168 LOGGER.info("Pushed the following config to statsd on device '" + mDeviceSerial 169 + "':"); 170 LOGGER.info(config.toString()); 171 if (hasPushedAtoms) { 172 LOGGER.info("Now please play with the device to trigger the event."); 173 } 174 if (!hasPulledAtoms) { 175 if (mPressToContinue) { 176 LOGGER.info("Press enter after you finish playing with the device..."); 177 Scanner scanner = new Scanner(System.in); 178 scanner.nextLine(); 179 } else { 180 LOGGER.info( 181 "All events should be dumped after 1 min ..."); 182 Thread.sleep(60_000); 183 } 184 } else { 185 LOGGER.info("All events should be dumped after 1.5 minutes ..."); 186 Thread.sleep(15_000); 187 Utils.logAppBreadcrumb(0, 0, LOGGER, mDeviceSerial); 188 Thread.sleep(75_000); 189 } 190 return Utils.getReportList(CONFIG_ID, true, false, LOGGER, 191 mDeviceSerial); 192 } catch (Exception e) { 193 LOGGER.log(Level.SEVERE, "Failed to test drive: " + e.getMessage(), e); 194 } finally { 195 removeConfig(mDeviceSerial); 196 if (remoteConfigPath != null) { 197 try { 198 Utils.runCommand(null, LOGGER, 199 "adb", "-s", mDeviceSerial, "shell", "rm", 200 remoteConfigPath); 201 } catch (Exception e) { 202 LOGGER.log(Level.WARNING, 203 "Unable to remove remote config file: " + remoteConfigPath, e); 204 } 205 } 206 } 207 return null; 208 } 209 210 static class Configuration { 211 boolean mOnePushedAtomEvent = false; 212 @VisibleForTesting 213 Set<Integer> mPushedAtoms = new TreeSet<>(); 214 @VisibleForTesting 215 Set<Integer> mPulledAtoms = new TreeSet<>(); 216 @VisibleForTesting 217 String mAdditionalAllowedPackage = null; 218 private final Set<Long> mTrackedMetrics = new HashSet<>(); 219 dumpMetrics(ConfigMetricsReportList reportList, Dumper dumper)220 private void dumpMetrics(ConfigMetricsReportList reportList, Dumper dumper) { 221 // We may get multiple reports. Take the last one. 222 ConfigMetricsReport report = reportList.getReports(reportList.getReportsCount() - 1); 223 for (StatsLogReport statsLog : report.getMetricsList()) { 224 if (isTrackedMetric(statsLog.getMetricId())) { 225 dumper.dump(statsLog); 226 } 227 } 228 } 229 isTrackedMetric(long metricId)230 boolean isTrackedMetric(long metricId) { 231 return mTrackedMetrics.contains(metricId); 232 } 233 isPulledAtom(int atomId)234 static boolean isPulledAtom(int atomId) { 235 return atomId >= PULL_ATOM_START && atomId <= MAX_PLATFORM_ATOM_TAG 236 || atomId >= VENDOR_PULLED_ATOM_START_TAG; 237 } 238 addAtom(Integer atom)239 void addAtom(Integer atom) { 240 if (Atom.getDescriptor().findFieldByNumber(atom) == null) { 241 LOGGER.severe("No such atom found: " + atom); 242 return; 243 } 244 if (isPulledAtom(atom)) { 245 mPulledAtoms.add(atom); 246 } else { 247 mPushedAtoms.add(atom); 248 } 249 } 250 hasPulledAtoms()251 private boolean hasPulledAtoms() { 252 return !mPulledAtoms.isEmpty(); 253 } 254 hasPushedAtoms()255 private boolean hasPushedAtoms() { 256 return !mPushedAtoms.isEmpty(); 257 } 258 createConfig()259 StatsdConfig createConfig() { 260 long metricId = METRIC_ID_BASE; 261 long atomMatcherId = ATOM_MATCHER_ID_BASE; 262 263 StatsdConfig.Builder builder = baseBuilder(); 264 265 if (hasPulledAtoms()) { 266 builder.addAtomMatcher( 267 createAtomMatcher( 268 Atom.APP_BREADCRUMB_REPORTED_FIELD_NUMBER, 269 APP_BREADCRUMB_MATCHER_ID)); 270 } 271 272 for (int atomId : mPulledAtoms) { 273 builder.addAtomMatcher(createAtomMatcher(atomId, atomMatcherId)); 274 GaugeMetric.Builder gaugeMetricBuilder = GaugeMetric.newBuilder(); 275 gaugeMetricBuilder 276 .setId(metricId) 277 .setWhat(atomMatcherId) 278 .setTriggerEvent(APP_BREADCRUMB_MATCHER_ID) 279 .setGaugeFieldsFilter(FieldFilter.newBuilder().setIncludeAll(true).build()) 280 .setBucket(TimeUnit.ONE_MINUTE) 281 .setSamplingType(GaugeMetric.SamplingType.FIRST_N_SAMPLES) 282 .setMaxNumGaugeAtomsPerBucket(100); 283 builder.addGaugeMetric(gaugeMetricBuilder.build()); 284 atomMatcherId++; 285 mTrackedMetrics.add(metricId++); 286 } 287 288 // A simple atom matcher for each pushed atom. 289 List<AtomMatcher> simpleAtomMatchers = new ArrayList<>(); 290 for (int atomId : mPushedAtoms) { 291 final AtomMatcher atomMatcher = createAtomMatcher(atomId, atomMatcherId++); 292 simpleAtomMatchers.add(atomMatcher); 293 builder.addAtomMatcher(atomMatcher); 294 } 295 296 if (mOnePushedAtomEvent) { 297 // Create a union event metric, using an matcher that matches all pulled atoms. 298 AtomMatcher unionAtomMatcher = createUnionMatcher(simpleAtomMatchers, 299 atomMatcherId); 300 builder.addAtomMatcher(unionAtomMatcher); 301 EventMetric.Builder eventMetricBuilder = EventMetric.newBuilder(); 302 eventMetricBuilder.setId(metricId).setWhat(unionAtomMatcher.getId()); 303 builder.addEventMetric(eventMetricBuilder.build()); 304 mTrackedMetrics.add(metricId++); 305 } else { 306 // Create multiple event metrics, one per pulled atom. 307 for (AtomMatcher atomMatcher : simpleAtomMatchers) { 308 EventMetric.Builder eventMetricBuilder = EventMetric.newBuilder(); 309 eventMetricBuilder 310 .setId(metricId) 311 .setWhat(atomMatcher.getId()); 312 builder.addEventMetric(eventMetricBuilder.build()); 313 mTrackedMetrics.add(metricId++); 314 } 315 } 316 317 return builder.build(); 318 } 319 createAtomMatcher(int atomId, long matcherId)320 private static AtomMatcher createAtomMatcher(int atomId, long matcherId) { 321 AtomMatcher.Builder atomMatcherBuilder = AtomMatcher.newBuilder(); 322 atomMatcherBuilder 323 .setId(matcherId) 324 .setSimpleAtomMatcher(SimpleAtomMatcher.newBuilder().setAtomId(atomId)); 325 return atomMatcherBuilder.build(); 326 } 327 createUnionMatcher(List<AtomMatcher> simpleAtomMatchers, long atomMatcherId)328 private AtomMatcher createUnionMatcher(List<AtomMatcher> simpleAtomMatchers, 329 long atomMatcherId) { 330 AtomMatcher.Combination.Builder combinationBuilder = 331 AtomMatcher.Combination.newBuilder(); 332 combinationBuilder.setOperation(StatsdConfigProto.LogicalOperation.OR); 333 for (AtomMatcher matcher : simpleAtomMatchers) { 334 combinationBuilder.addMatcher(matcher.getId()); 335 } 336 AtomMatcher.Builder atomMatcherBuilder = AtomMatcher.newBuilder(); 337 atomMatcherBuilder.setId(atomMatcherId).setCombination(combinationBuilder.build()); 338 return atomMatcherBuilder.build(); 339 } 340 baseBuilder()341 private StatsdConfig.Builder baseBuilder() { 342 ArrayList<String> allowedSources = new ArrayList<>(); 343 Collections.addAll(allowedSources, ALLOWED_LOG_SOURCES); 344 if (mAdditionalAllowedPackage != null) { 345 allowedSources.add(mAdditionalAllowedPackage); 346 } 347 return StatsdConfig.newBuilder() 348 .addAllAllowedLogSource(allowedSources) 349 .addAllDefaultPullPackages(Arrays.asList(DEFAULT_PULL_SOURCES)) 350 .addPullAtomPackages(PullAtomPackages.newBuilder() 351 .setAtomId(Atom.MEDIA_DRM_ACTIVITY_INFO_FIELD_NUMBER) 352 .addPackages("AID_MEDIA")) 353 .addPullAtomPackages(PullAtomPackages.newBuilder() 354 .setAtomId(Atom.GPU_STATS_GLOBAL_INFO_FIELD_NUMBER) 355 .addPackages("AID_GPU_SERVICE")) 356 .addPullAtomPackages(PullAtomPackages.newBuilder() 357 .setAtomId(Atom.GPU_STATS_APP_INFO_FIELD_NUMBER) 358 .addPackages("AID_GPU_SERVICE")) 359 .addPullAtomPackages(PullAtomPackages.newBuilder() 360 .setAtomId(Atom.TRAIN_INFO_FIELD_NUMBER) 361 .addPackages("AID_STATSD")) 362 .addPullAtomPackages(PullAtomPackages.newBuilder() 363 .setAtomId(Atom.GENERAL_EXTERNAL_STORAGE_ACCESS_STATS_FIELD_NUMBER) 364 .addPackages("com.google.android.providers.media.module")) 365 .setHashStringsInMetricReport(false); 366 } 367 } 368 369 interface Dumper { dump(StatsLogReport report)370 void dump(StatsLogReport report); 371 } 372 373 static class BasicDumper implements Dumper { 374 @Override dump(StatsLogReport report)375 public void dump(StatsLogReport report) { 376 System.out.println(report.toString()); 377 } 378 } 379 380 static class TerseDumper extends BasicDumper { 381 @Override dump(StatsLogReport report)382 public void dump(StatsLogReport report) { 383 if (report.hasGaugeMetrics()) { 384 dumpGaugeMetrics(report); 385 } 386 if (report.hasEventMetrics()) { 387 dumpEventMetrics(report); 388 } 389 } dumpEventMetrics(StatsLogReport report)390 void dumpEventMetrics(StatsLogReport report) { 391 final List<StatsLog.EventMetricData> data = report.getEventMetrics().getDataList(); 392 if (data.isEmpty()) { 393 return; 394 } 395 long firstTimestampNanos = data.get(0).getElapsedTimestampNanos(); 396 for (StatsLog.EventMetricData event : data) { 397 final double deltaSec = (event.getElapsedTimestampNanos() - firstTimestampNanos) 398 / 1e9; 399 System.out.println( 400 String.format("+%.3fs: %s", deltaSec, event.getAtom().toString())); 401 } 402 } dumpGaugeMetrics(StatsLogReport report)403 void dumpGaugeMetrics(StatsLogReport report) { 404 final List<StatsLog.GaugeMetricData> data = report.getGaugeMetrics().getDataList(); 405 if (data.isEmpty()) { 406 return; 407 } 408 for (StatsLog.GaugeMetricData gauge : data) { 409 System.out.println(gauge.toString()); 410 } 411 } 412 } 413 pushConfig(StatsdConfig config, String deviceSerial)414 private static String pushConfig(StatsdConfig config, String deviceSerial) 415 throws IOException, InterruptedException { 416 File configFile = File.createTempFile("statsdconfig", ".config"); 417 configFile.deleteOnExit(); 418 Files.write(config.toByteArray(), configFile); 419 String remotePath = "/data/local/tmp/" + configFile.getName(); 420 Utils.runCommand(null, LOGGER, "adb", "-s", deviceSerial, 421 "push", configFile.getAbsolutePath(), remotePath); 422 Utils.runCommand(null, LOGGER, "adb", "-s", deviceSerial, 423 "shell", "cat", remotePath, "|", Utils.CMD_UPDATE_CONFIG, 424 String.valueOf(CONFIG_ID)); 425 return remotePath; 426 } 427 removeConfig(String deviceSerial)428 private static void removeConfig(String deviceSerial) { 429 try { 430 Utils.runCommand(null, LOGGER, "adb", "-s", deviceSerial, 431 "shell", Utils.CMD_REMOVE_CONFIG, String.valueOf(CONFIG_ID)); 432 } catch (Exception e) { 433 LOGGER.severe("Failed to remove config: " + e.getMessage()); 434 } 435 } 436 } 437