blob: 671e12bc7493955b0fd58d9322cc135c80279d74 [file] [log] [blame]
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.bedstead.metricsrecorder;
import static com.android.os.nano.AtomsProto.Atom.DEVICE_POLICY_EVENT_FIELD_NUMBER;
import com.android.bedstead.nene.exceptions.AdbException;
import com.android.bedstead.nene.exceptions.NeneException;
import com.android.bedstead.nene.utils.ShellCommand;
import com.android.framework.protobuf.nano.CodedOutputByteBufferNano;
import com.android.framework.protobuf.nano.InvalidProtocolBufferNanoException;
import com.android.framework.protobuf.nano.MessageNano;
import com.android.internal.os.nano.StatsdConfigProto.AtomMatcher;
import com.android.internal.os.nano.StatsdConfigProto.EventMetric;
import com.android.internal.os.nano.StatsdConfigProto.FieldValueMatcher;
import com.android.internal.os.nano.StatsdConfigProto.SimpleAtomMatcher;
import com.android.internal.os.nano.StatsdConfigProto.StatsdConfig;
import com.android.os.nano.AtomsProto;
import com.android.os.nano.StatsLog;
import com.android.os.nano.StatsLog.ConfigMetricsReportList;
import com.android.queryable.Queryable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* Metrics testing utility
*
* <p>Example usage:
* <pre>{@code
* try (EnterpriseMetricsRecorder r = EnterpriseMetricsRecorder.create() {
* // Call code which generates metrics
*
* assertThat(r.query().poll()).isNotNull();
* }
*
* }</pre>
*/
public class EnterpriseMetricsRecorder implements AutoCloseable, Queryable {
/** Create a {@link EnterpriseMetricsRecorder} and begin listening for metrics. */
public static EnterpriseMetricsRecorder create() {
EnterpriseMetricsRecorder r = new EnterpriseMetricsRecorder();
r.start(DEVICE_POLICY_EVENT_FIELD_NUMBER);
return r;
}
private static final long CONFIG_ID = "cts_config".hashCode();
private final List<EnterpriseMetricInfo> mData = new ArrayList<>();
private EnterpriseMetricsRecorder() {
}
private void start(int atomTag) {
cleanLogs();
createAndUploadConfig(atomTag);
}
/**
* Begin querying the recorded metrics.
*/
public MetricQueryBuilder query() {
return new MetricQueryBuilder(this);
}
List<EnterpriseMetricInfo> fetchLatestData() {
mData.addAll(getEventMetricDataList(getReportList()));
return mData;
}
@Override
public void close() {
cleanLogs();
}
private void createAndUploadConfig(int atomTag) {
StatsdConfig conf = new StatsdConfig();
conf.id = CONFIG_ID;
conf.allowedLogSource = new String[]{"AID_SYSTEM"};
addAtomEvent(conf, atomTag);
uploadConfig(conf);
}
private void addAtomEvent(StatsdConfig conf, int atomTag) {
addAtomEvent(conf, atomTag, new ArrayList<>());
}
private void addAtomEvent(StatsdConfig conf, int atomTag,
List<FieldValueMatcher> fvms) {
String atomName = "Atom" + System.nanoTime();
String eventName = "Event" + System.nanoTime();
SimpleAtomMatcher sam = new SimpleAtomMatcher();
sam.atomId = atomTag;
if (fvms != null) {
sam.fieldValueMatcher = fvms.toArray(new FieldValueMatcher[]{});
}
AtomMatcher atomMatcher = new AtomMatcher();
atomMatcher.id = atomName.hashCode();
atomMatcher.setSimpleAtomMatcher(sam);
conf.atomMatcher = new AtomMatcher[]{
atomMatcher
};
EventMetric eventMetric = new EventMetric();
eventMetric.id = eventName.hashCode();
eventMetric.what = atomName.hashCode();
conf.eventMetric = new EventMetric[]{
eventMetric
};
}
private void uploadConfig(StatsdConfig config) {
byte[] bytes = new byte[config.getSerializedSize()];
CodedOutputByteBufferNano b = CodedOutputByteBufferNano.newInstance(bytes);
try {
ShellCommand.builder("cmd stats config update")
.addOperand(CONFIG_ID)
.writeToStdIn(MessageNano.toByteArray(config))
.validate(String::isEmpty)
.execute();
} catch (AdbException e) {
throw new NeneException("Error uploading config", e);
}
}
private void cleanLogs() {
removeConfig(CONFIG_ID);
getReportList(); // Clears data.
}
private void removeConfig(long configId) {
try {
ShellCommand.builder("cmd stats config remove").addOperand(configId)
.validate(String::isEmpty).execute();
} catch (AdbException e) {
throw new NeneException("Error removing config " + configId, e);
}
}
private ConfigMetricsReportList getReportList() {
try {
byte[] bytes = ShellCommand.builder("cmd stats dump-report")
.addOperand(CONFIG_ID)
.addOperand("--include_current_bucket")
.addOperand("--proto")
.forBytes()
.execute();
return ConfigMetricsReportList.parseFrom(bytes);
} catch (AdbException e) {
throw new NeneException("Error getting stat report list", e);
} catch (InvalidProtocolBufferNanoException e) {
throw new NeneException("Invalid proto", e);
}
}
private List<EnterpriseMetricInfo> getEventMetricDataList(
ConfigMetricsReportList reportList) {
return Arrays.stream(reportList.reports)
.flatMap(s -> Arrays.stream(s.metrics.clone()))
.filter(s -> s.getEventMetrics() != null && s.getEventMetrics().data != null)
.flatMap(statsLogReport -> Arrays.stream(
statsLogReport.getEventMetrics().data.clone()))
.flatMap(eventMetricData -> Arrays.stream(
backfillAggregatedAtomsinEventMetric(eventMetricData)))
.sorted(Comparator.comparing(e -> e.elapsedTimestampNanos))
.map(e -> e.atom)
.filter((Objects::nonNull))
.map(AtomsProto.Atom::getDevicePolicyEvent)
.filter((Objects::nonNull))
.map(EnterpriseMetricInfo::new)
.collect(Collectors.toList());
}
private StatsLog.EventMetricData[] backfillAggregatedAtomsinEventMetric(
StatsLog.EventMetricData metricData) {
if (metricData.aggregatedAtomInfo == null) {
return new StatsLog.EventMetricData[]{metricData};
}
List<StatsLog.EventMetricData> data = new ArrayList<>();
StatsLog.AggregatedAtomInfo atomInfo = metricData.aggregatedAtomInfo;
for (long timestamp : atomInfo.elapsedTimestampNanos) {
StatsLog.EventMetricData newMetricData = new StatsLog.EventMetricData();
newMetricData.atom = atomInfo.atom;
newMetricData.elapsedTimestampNanos = timestamp;
data.add(newMetricData);
}
return data.toArray(new StatsLog.EventMetricData[0]);
}
}