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