Home | History | Annotate | Download | only in metrics
      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 
     61     AtomMetricTester(ITestDevice device) {
     62         mDevice = device;
     63     }
     64 
     65     void cleanLogs() throws Exception {
     66         if (isStatsdDisabled()) {
     67             return;
     68         }
     69         removeConfig(CONFIG_ID);
     70         getReportList(); // Clears data.
     71     }
     72 
     73     private static StatsdConfig.Builder createConfigBuilder() {
     74         return StatsdConfig.newBuilder().setId(CONFIG_ID)
     75                 .addAllowedLogSource("AID_SYSTEM");
     76     }
     77 
     78     void createAndUploadConfig(int atomTag) throws Exception {
     79         StatsdConfig.Builder conf = createConfigBuilder();
     80         addAtomEvent(conf, atomTag);
     81         uploadConfig(conf);
     82     }
     83 
     84     private void uploadConfig(StatsdConfig.Builder config) throws Exception {
     85         uploadConfig(config.build());
     86     }
     87 
     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 
     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      */
    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      */
    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. */
    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. */
    145     private static FieldValueMatcher.Builder createFvm(int field) {
    146         return FieldValueMatcher.newBuilder().setField(field);
    147     }
    148 
    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      */
    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      */
    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). */
    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      */
    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 
    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 }