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.cts.devicepolicy.metrics; 17 18 import static junit.framework.Assert.assertTrue; 19 20 import com.android.internal.os.StatsdConfigProto.AtomMatcher; 21 import com.android.internal.os.StatsdConfigProto.EventMetric; 22 import com.android.internal.os.StatsdConfigProto.FieldValueMatcher; 23 import com.android.internal.os.StatsdConfigProto.SimpleAtomMatcher; 24 import com.android.internal.os.StatsdConfigProto.StatsdConfig; 25 import com.android.os.AtomsProto.Atom; 26 import com.android.os.StatsLog.ConfigMetricsReport; 27 import com.android.os.StatsLog.ConfigMetricsReportList; 28 import com.android.os.StatsLog.EventMetricData; 29 import com.android.os.StatsLog.StatsLogReport; 30 import com.android.tradefed.device.CollectingByteOutputReceiver; 31 import com.android.tradefed.device.DeviceNotAvailableException; 32 import com.android.tradefed.device.ITestDevice; 33 import com.android.tradefed.log.LogUtil.CLog; 34 import com.google.common.io.Files; 35 import com.google.protobuf.InvalidProtocolBufferException; 36 import com.google.protobuf.MessageLite; 37 import com.google.protobuf.Parser; 38 import java.io.File; 39 import java.util.ArrayList; 40 import java.util.Comparator; 41 import java.util.List; 42 import java.util.function.Predicate; 43 import java.util.stream.Collectors; 44 45 /** 46 * Tests Statsd atoms. 47 * <p/> 48 * Uploads statsd event configs, retrieves logs from host side and validates them 49 * against specified criteria. 50 */ 51 class AtomMetricTester { 52 private static final String UPDATE_CONFIG_CMD = "cat %s | cmd stats config update %d"; 53 private static final String DUMP_REPORT_CMD = 54 "cmd stats dump-report %d --include_current_bucket --proto"; 55 private static final String REMOVE_CONFIG_CMD = "cmd stats config remove %d"; 56 /** ID of the config, which evaluates to -1572883457. */ 57 private static final long CONFIG_ID = "cts_config".hashCode(); 58 59 private final ITestDevice mDevice; 60 AtomMetricTester(ITestDevice device)61 AtomMetricTester(ITestDevice device) { 62 mDevice = device; 63 } 64 cleanLogs()65 void cleanLogs() throws Exception { 66 if (isStatsdDisabled()) { 67 return; 68 } 69 removeConfig(CONFIG_ID); 70 getReportList(); // Clears data. 71 } 72 createConfigBuilder()73 private static StatsdConfig.Builder createConfigBuilder() { 74 return StatsdConfig.newBuilder().setId(CONFIG_ID) 75 .addAllowedLogSource("AID_SYSTEM"); 76 } 77 createAndUploadConfig(int atomTag)78 void createAndUploadConfig(int atomTag) throws Exception { 79 StatsdConfig.Builder conf = createConfigBuilder(); 80 addAtomEvent(conf, atomTag); 81 uploadConfig(conf); 82 } 83 uploadConfig(StatsdConfig.Builder config)84 private void uploadConfig(StatsdConfig.Builder config) throws Exception { 85 uploadConfig(config.build()); 86 } 87 uploadConfig(StatsdConfig config)88 private void uploadConfig(StatsdConfig config) throws Exception { 89 CLog.d("Uploading the following config:\n" + config.toString()); 90 File configFile = File.createTempFile("statsdconfig", ".config"); 91 configFile.deleteOnExit(); 92 Files.write(config.toByteArray(), configFile); 93 String remotePath = "/data/local/tmp/" + configFile.getName(); 94 mDevice.pushFile(configFile, remotePath); 95 mDevice.executeShellCommand(String.format(UPDATE_CONFIG_CMD, remotePath, CONFIG_ID)); 96 mDevice.executeShellCommand("rm " + remotePath); 97 } 98 removeConfig(long configId)99 private void removeConfig(long configId) throws Exception { 100 mDevice.executeShellCommand(String.format(REMOVE_CONFIG_CMD, configId)); 101 } 102 103 /** 104 * Gets the statsd report and sorts it. 105 * Note that this also deletes that report from statsd. 106 */ getEventMetricDataList()107 List<EventMetricData> getEventMetricDataList() throws Exception { 108 ConfigMetricsReportList reportList = getReportList(); 109 return getEventMetricDataList(reportList); 110 } 111 112 /** 113 * Extracts and sorts the EventMetricData from the given ConfigMetricsReportList (which must 114 * contain a single report). 115 */ getEventMetricDataList(ConfigMetricsReportList reportList)116 private List<EventMetricData> getEventMetricDataList(ConfigMetricsReportList reportList) 117 throws Exception { 118 assertTrue("Expected one report", reportList.getReportsCount() == 1); 119 final ConfigMetricsReport report = reportList.getReports(0); 120 final List<StatsLogReport> metricsList = report.getMetricsList(); 121 return metricsList.stream() 122 .flatMap(statsLogReport -> statsLogReport.getEventMetrics().getDataList().stream()) 123 .sorted(Comparator.comparing(EventMetricData::getElapsedTimestampNanos)) 124 .peek(eventMetricData -> { 125 CLog.d("Atom at " + eventMetricData.getElapsedTimestampNanos() 126 + ":\n" + eventMetricData.getAtom().toString()); 127 }) 128 .collect(Collectors.toList()); 129 } 130 131 /** Gets the statsd report. Note that this also deletes that report from statsd. */ getReportList()132 private ConfigMetricsReportList getReportList() throws Exception { 133 try { 134 return getDump(ConfigMetricsReportList.parser(), 135 String.format(DUMP_REPORT_CMD, CONFIG_ID)); 136 } catch (com.google.protobuf.InvalidProtocolBufferException e) { 137 CLog.e("Failed to fetch and parse the statsd output report. " 138 + "Perhaps there is not a valid statsd config for the requested " 139 + "uid=" + getHostUid() + ", id=" + CONFIG_ID + "."); 140 throw (e); 141 } 142 } 143 144 /** Creates a FieldValueMatcher.Builder corresponding to the given field. */ createFvm(int field)145 private static FieldValueMatcher.Builder createFvm(int field) { 146 return FieldValueMatcher.newBuilder().setField(field); 147 } 148 addAtomEvent(StatsdConfig.Builder conf, int atomTag)149 private void addAtomEvent(StatsdConfig.Builder conf, int atomTag) throws Exception { 150 addAtomEvent(conf, atomTag, new ArrayList<FieldValueMatcher.Builder>()); 151 } 152 153 /** 154 * Adds an event to the config for an atom that matches the given keys. 155 * 156 * @param conf configuration 157 * @param atomTag atom tag (from atoms.proto) 158 * @param fvms list of FieldValueMatcher.Builders to attach to the atom. May be null. 159 */ addAtomEvent(StatsdConfig.Builder conf, int atomTag, List<FieldValueMatcher.Builder> fvms)160 private void addAtomEvent(StatsdConfig.Builder conf, int atomTag, 161 List<FieldValueMatcher.Builder> fvms) throws Exception { 162 163 final String atomName = "Atom" + System.nanoTime(); 164 final String eventName = "Event" + System.nanoTime(); 165 166 SimpleAtomMatcher.Builder sam = SimpleAtomMatcher.newBuilder().setAtomId(atomTag); 167 if (fvms != null) { 168 for (FieldValueMatcher.Builder fvm : fvms) { 169 sam.addFieldValueMatcher(fvm); 170 } 171 } 172 conf.addAtomMatcher(AtomMatcher.newBuilder() 173 .setId(atomName.hashCode()) 174 .setSimpleAtomMatcher(sam)); 175 conf.addEventMetric(EventMetric.newBuilder() 176 .setId(eventName.hashCode()) 177 .setWhat(atomName.hashCode())); 178 } 179 180 /** 181 * Removes all elements from data prior to the first occurrence of an element for which 182 * the <code>atomMatcher</code> predicate returns <code>true</code>. 183 * After this method is called, the first element of data (if non-empty) is guaranteed to be 184 * an element in state. 185 * 186 * @param atomMatcher predicate that takes an Atom and returns <code>true</code> if it 187 * fits criteria. 188 */ dropWhileNot(List<EventMetricData> metricData, Predicate<Atom> atomMatcher)189 static void dropWhileNot(List<EventMetricData> metricData, Predicate<Atom> atomMatcher) { 190 int firstStateIdx; 191 for (firstStateIdx = 0; firstStateIdx < metricData.size(); firstStateIdx++) { 192 final Atom atom = metricData.get(firstStateIdx).getAtom(); 193 if (atomMatcher.test(atom)) { 194 break; 195 } 196 } 197 if (firstStateIdx == 0) { 198 // First first element already is in state, so there's nothing to do. 199 return; 200 } 201 metricData.subList(0, firstStateIdx).clear(); 202 } 203 204 /** Returns the UID of the host, which should always either be SHELL (2000) or ROOT (0). */ getHostUid()205 private int getHostUid() throws DeviceNotAvailableException { 206 String strUid = ""; 207 try { 208 strUid = mDevice.executeShellCommand("id -u"); 209 return Integer.parseInt(strUid.trim()); 210 } catch (NumberFormatException e) { 211 CLog.e("Failed to get host's uid via shell command. Found " + strUid); 212 // Fall back to alternative method... 213 if (mDevice.isAdbRoot()) { 214 return 0; // ROOT 215 } else { 216 return 2000; // SHELL 217 } 218 } 219 } 220 221 /** 222 * Execute a shell command on device and get the results of 223 * that as a proto of the given type. 224 * 225 * @param parser A protobuf parser object. e.g. MyProto.parser() 226 * @param command The adb shell command to run. e.g. "dumpsys fingerprint --proto" 227 * 228 * @throws DeviceNotAvailableException If there was a problem communicating with 229 * the test device. 230 * @throws InvalidProtocolBufferException If there was an error parsing 231 * the proto. Note that a 0 length buffer is not necessarily an error. 232 */ getDump(Parser<T> parser, String command)233 private <T extends MessageLite> T getDump(Parser<T> parser, String command) 234 throws DeviceNotAvailableException, InvalidProtocolBufferException { 235 final CollectingByteOutputReceiver receiver = new CollectingByteOutputReceiver(); 236 mDevice.executeShellCommand(command, receiver); 237 return parser.parseFrom(receiver.getOutput()); 238 } 239 isStatsdDisabled()240 boolean isStatsdDisabled() throws DeviceNotAvailableException { 241 // if ro.statsd.enable doesn't exist, statsd is running by default. 242 if ("false".equals(mDevice.getProperty("ro.statsd.enable")) 243 && "true".equals(mDevice.getProperty("ro.config.low_ram"))) { 244 CLog.d("Statsd is not enabled on the device"); 245 return true; 246 } 247 return false; 248 } 249 }