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