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