1 /*
2  * Copyright (C) 2020 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 
17 package android.cts.statsdatom.lib;
18 
19 import static com.google.common.truth.Truth.assertThat;
20 import static com.google.common.truth.Truth.assertWithMessage;
21 
22 import com.android.os.StatsLog;
23 
24 import com.android.os.AtomsProto.Atom;
25 import com.android.os.StatsLog;
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.GaugeBucketInfo;
30 import com.android.os.StatsLog.GaugeMetricData;
31 import com.android.os.StatsLog.StatsLogReport;
32 import com.android.tradefed.device.ITestDevice;
33 import com.android.tradefed.log.LogUtil.CLog;
34 import com.android.tradefed.util.Pair;
35 
36 import com.google.protobuf.InvalidProtocolBufferException;
37 
38 import java.util.ArrayList;
39 import java.util.Collections;
40 import java.util.Comparator;
41 import java.util.List;
42 import java.util.stream.Collectors;
43 
44 public final class ReportUtils {
45     private static final String DUMP_REPORT_CMD = "cmd stats dump-report";
46     private static final long NS_PER_SEC = (long) 1E+9;
47 
48     /**
49      * Returns a list of event metrics, which is sorted by timestamp, from the statsd report.
50      * Note: Calling this function deletes the report from statsd.
51      */
getEventMetricDataList(ITestDevice device)52     public static List<EventMetricData> getEventMetricDataList(ITestDevice device)
53             throws Exception {
54         ConfigMetricsReportList reportList = getReportList(device);
55         return getEventMetricDataList(reportList);
56     }
57 
58     /**
59      * Extracts and sorts the EventMetricData from the given ConfigMetricsReportList (which must
60      * contain a single report) and sorts the atoms by timestamp within the report.
61      */
getEventMetricDataList(ConfigMetricsReportList reportList)62     public static List<EventMetricData> getEventMetricDataList(ConfigMetricsReportList reportList)
63             throws Exception {
64         assertThat(reportList.getReportsCount()).isEqualTo(1);
65         ConfigMetricsReport report = reportList.getReports(0);
66 
67         List<EventMetricData> data = new ArrayList<>();
68         for (StatsLogReport metric : report.getMetricsList()) {
69             for (EventMetricData metricData :
70                     metric.getEventMetrics().getDataList()) {
71                 if (metricData.hasAtom()) {
72                     data.add(metricData);
73                 } else {
74                     data.addAll(backfillAggregatedAtomsInEventMetric(metricData));
75                 }
76             }
77         }
78         data.sort(Comparator.comparing(EventMetricData::getElapsedTimestampNanos));
79 
80         CLog.d("Get EventMetricDataList as following:\n");
81         for (EventMetricData d : data) {
82             CLog.d("Atom at " + d.getElapsedTimestampNanos() + ":\n" + d.getAtom().toString());
83         }
84         return data;
85     }
86 
87 
backfillAggregatedAtomsInEventMetric( EventMetricData metricData)88     private static List<EventMetricData> backfillAggregatedAtomsInEventMetric(
89             EventMetricData metricData) {
90         if (!metricData.hasAggregatedAtomInfo()) {
91             return Collections.emptyList();
92         }
93         List<EventMetricData> data = new ArrayList<>();
94         StatsLog.AggregatedAtomInfo atomInfo = metricData.getAggregatedAtomInfo();
95         for (long timestamp : atomInfo.getElapsedTimestampNanosList()) {
96             data.add(EventMetricData.newBuilder()
97                     .setAtom(atomInfo.getAtom())
98                     .setElapsedTimestampNanos(timestamp)
99                     .build());
100         }
101         return data;
102     }
103 
getGaugeMetricAtoms(ITestDevice device)104     public static List<Atom> getGaugeMetricAtoms(ITestDevice device) throws Exception {
105         return getGaugeMetricAtoms(device, /*checkTimestampTruncated=*/false);
106     }
107 
108     /**
109      * Returns a list of gauge atoms from the statsd report. Assumes that there is only one bucket
110      * for the gauge metric.
111      * Note: calling this function deletes the report from statsd.
112      *
113      * @param checkTimestampTrucated if true, checks that atom timestmaps are properly truncated
114      */
getGaugeMetricAtoms(ITestDevice device, boolean checkTimestampTruncated)115     public static List<Atom> getGaugeMetricAtoms(ITestDevice device,
116             boolean checkTimestampTruncated) throws Exception {
117         ConfigMetricsReportList reportList = getReportList(device);
118         assertThat(reportList.getReportsCount()).isEqualTo(1);
119         ConfigMetricsReport report = reportList.getReports(0);
120         assertThat(report.getMetricsCount()).isEqualTo(1);
121         CLog.d("Got the following report: " + report.getMetrics(0).getGaugeMetrics().toString());
122         List<Atom> atoms = new ArrayList<>();
123         for (GaugeMetricData d : report.getMetrics(0).getGaugeMetrics().getDataList()) {
124             assertThat(d.getBucketInfoCount()).isEqualTo(1);
125             GaugeBucketInfo bucketInfo = d.getBucketInfo(0);
126             if (bucketInfo.getAtomCount() != 0) {
127                 atoms.addAll(bucketInfo.getAtomList());
128             } else {
129                 atoms.addAll(backFillGaugeBucketAtoms(bucketInfo.getAggregatedAtomInfoList()));
130             }
131             if (checkTimestampTruncated) {
132                 for (long timestampNs : bucketInfo.getElapsedTimestampNanosList()) {
133                     assertTimestampIsTruncated(timestampNs);
134                 }
135             }
136         }
137 
138         CLog.d("Got the following GaugeMetric atoms:\n");
139         for (Atom atom : atoms) {
140             CLog.d("Atom:\n" + atom.toString());
141         }
142         return atoms;
143     }
144 
backFillGaugeBucketAtoms( List<StatsLog.AggregatedAtomInfo> atomInfoList)145     private static List<Atom> backFillGaugeBucketAtoms(
146             List<StatsLog.AggregatedAtomInfo> atomInfoList) {
147         List<Pair<Atom, Long>> atomTimestamp = new ArrayList<>();
148         for (StatsLog.AggregatedAtomInfo atomInfo : atomInfoList) {
149             for (long timestampNs : atomInfo.getElapsedTimestampNanosList()) {
150                 atomTimestamp.add(Pair.create(atomInfo.getAtom(), timestampNs));
151             }
152         }
153         atomTimestamp.sort(Comparator.comparing(o -> o.second));
154         return atomTimestamp.stream().map(p -> p.first).collect(Collectors.toList());
155     }
156 
157     /**
158      * Delete all pre-existing reports corresponding to the CTS config.
159      */
clearReports(ITestDevice device)160     public static void clearReports(ITestDevice device) throws Exception {
161         getReportList(device);
162     }
163 
164     /**
165      * Retrieves the ConfigMetricsReports corresponding to the CTS config from statsd.
166      * Note: Calling this functions deletes the report from statsd.
167      */
getReportList(ITestDevice device)168     private static ConfigMetricsReportList getReportList(ITestDevice device) throws Exception {
169         try {
170             String cmd = String.join(" ", DUMP_REPORT_CMD, ConfigUtils.CONFIG_ID_STRING,
171                     "--include_current_bucket", "--proto");
172             ConfigMetricsReportList reportList = DeviceUtils.getShellCommandOutput(device,
173                     ConfigMetricsReportList.parser(), cmd);
174             return reportList;
175         } catch (InvalidProtocolBufferException ex) {
176             int hostUid = DeviceUtils.getHostUid(device);
177             CLog.e("Failed to fetch and parse the statsd output report. Perhaps there is not a "
178                     + "valid statsd config for the requested uid=" + hostUid + ", id="
179                     + ConfigUtils.CONFIG_ID + ".");
180             throw ex;
181         }
182     }
183 
184     /**
185      * Checks that a timestamp has been truncated to a multiple of 5 min.
186      */
assertTimestampIsTruncated(long timestampNs)187     private static void assertTimestampIsTruncated(long timestampNs) {
188         long fiveMinutesInNs = NS_PER_SEC * 5 * 60;
189         assertWithMessage("Timestamp is not truncated")
190                 .that(timestampNs % fiveMinutesInNs).isEqualTo(0);
191     }
192 
ReportUtils()193     private ReportUtils() {}
194 }
195