That app automatically sends a bunch of events to logd.
The frequency is adjustable.
Test: Ran statsd.
Change-Id: I7a88c140e39d22549cc76ef68059d292e85a8a24
--- /dev/null
+# Copyright (C) 2017 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.
+#
+#
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_SRC_FILES += ../../src/stats_log.proto \
+ ../../src/atoms_copy.proto \
+ ../../src/statsd_config.proto
+
+LOCAL_PROTOC_FLAGS := --proto_path=$(LOCAL_PATH)/../../src/
+
+LOCAL_PROTOC_OPTIMIZE_TYPE := lite-static
+
+LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
+
+LOCAL_PACKAGE_NAME := StatsdLoadtest
+LOCAL_CERTIFICATE := platform
+LOCAL_PRIVILEGED_MODULE := true
+LOCAL_DEX_PREOPT := false
+include $(BUILD_PACKAGE)
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 2007, 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.
+*/
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.statsd.loadtest"
+ android:sharedUserId="android.uid.system"
+ android:versionCode="1"
+ android:versionName="1.0" >
+
+ <uses-permission android:name="android.permission.DUMP" />
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+ <application
+ android:allowBackup="true"
+ android:icon="@drawable/ic_launcher"
+ android:label="@string/app_name" >
+ <activity
+ android:name=".LoadtestActivity"
+ android:label="@string/app_name"
+ android:launchMode="singleTop" >
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ <receiver android:name=".LoadtestActivity$PusherAlarmReceiver" />
+ <receiver android:name=".LoadtestActivity$StopperAlarmReceiver"/>
+ </application>
+</manifest>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 2007, 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.
+*/
+-->
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" >
+
+ <LinearLayout
+ android:id="@+id/outside"
+ android:clickable="true"
+ android:focusable="true"
+ android:focusableInTouchMode="true"
+ android:gravity="center"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_marginRight="10dp"
+ android:layout_marginLeft="10dp"
+ android:orientation="vertical">
+ <requestFocus />
+
+ <LinearLayout
+ android:gravity="center"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="horizontal">
+ <TextView
+ android:textSize="30dp"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:text="@string/replication_label" />
+ <EditText
+ android:id="@+id/replication"
+ android:inputType="number"
+ android:layout_weight="1"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:maxLength="3"
+ android:text="@integer/replication_default"
+ android:textSize="30dp"/>
+ </LinearLayout>
+
+ <LinearLayout
+ android:gravity="center"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="horizontal">
+ <TextView
+ android:textSize="30dp"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:text="@string/bucket_label" />
+ <EditText
+ android:id="@+id/bucket"
+ android:inputType="number"
+ android:layout_weight="1"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:maxLength="3"
+ android:text="@integer/bucket_default"
+ android:textSize="30dp"/>
+ </LinearLayout>
+
+ <LinearLayout
+ android:gravity="center"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="horizontal">
+ <TextView
+ android:textSize="30dp"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:text="@string/period_label" />
+ <EditText
+ android:id="@+id/period"
+ android:inputType="number"
+ android:layout_weight="1"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:maxLength="3"
+ android:text="@integer/period_default"
+ android:textSize="30dp"/>
+ </LinearLayout>
+
+ <LinearLayout
+ android:gravity="center"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="horizontal">
+ <TextView
+ android:textSize="30dp"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:text="@string/burst_label" />
+ <EditText
+ android:id="@+id/burst"
+ android:inputType="number"
+ android:layout_weight="1"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:maxLength="2"
+ android:text="@integer/burst_default"
+ android:textSize="30dp"/>
+ </LinearLayout>
+
+ <LinearLayout
+ android:gravity="center"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="horizontal">
+ <TextView
+ android:textSize="30dp"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:text="@string/duration_label" />
+ <EditText
+ android:id="@+id/duration"
+ android:inputType="number"
+ android:layout_weight="1"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:maxLength="4"
+ android:text="@integer/duration_default"
+ android:textSize="30dp"/>
+ </LinearLayout>
+ <CheckBox
+ android:id="@+id/placebo"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/placebo"
+ android:checked="false" />
+
+ <Space
+ android:layout_width="1dp"
+ android:layout_height="30dp"/>
+
+ <Button
+ android:id="@+id/start_stop"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="#ffff0000"
+ android:text="@string/start"
+ android:textSize="50dp"/>
+
+ <Space
+ android:layout_width="1dp"
+ android:layout_height="30dp"/>
+
+ <Button
+ android:id="@+id/display_output"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/display_output"
+ android:textSize="30dp"/>
+ <Button
+ android:id="@+id/display_perf"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/display_perf"
+ android:textSize="30dp"/>
+
+ <Space
+ android:layout_width="1dp"
+ android:layout_height="30dp"/>
+
+ <TextView
+ android:id="@+id/report_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+
+ </LinearLayout>
+
+</ScrollView>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 2007, 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.
+*/
+-->
+<resources>
+ <integer name="bucket_default">10</integer>
+ <integer name="burst_default">1</integer>
+ <integer name="period_default">2</integer>
+ <integer name="replication_default">1</integer>
+ <integer name="duration_default">240</integer>
+</resources>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 2007, 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.
+*/
+-->
+<resources>
+ <string name="app_name">Statsd Loadtest</string>
+ <string name="bucket_label">bucket size (mins): </string>
+ <string name="burst_label">burst: </string>
+ <string name="display_output">Show metrics data</string>
+ <string name="display_perf">Show perf data</string>
+ <string name="placebo">placebo</string>
+ <string name="period_label">logging period (secs): </string>
+ <string name="replication_label">metric replication: </string>
+ <string name="duration_label">test duration (mins): </string>
+ <string name="start">  Start  </string>
+ <string name="stop">  Stop  </string>
+
+</resources>
--- /dev/null
+/*
+ * Copyright (C) 2017 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.statsd.loadtest;
+
+import android.annotation.Nullable;
+import android.util.Log;
+import java.text.ParseException;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class BatteryStatsParser {
+
+ private static final Pattern LINE_PATTERN =
+ Pattern.compile("\\s*\\+*(\\S*)\\s\\(\\d+\\)\\s(\\d\\d\\d)\\s.*");
+ private static final Pattern TIME_PATTERN =
+ Pattern.compile("(\\d+)?(h)?(\\d+)?(m)?(\\d+)?(s)?(\\d+)?(ms)?");
+ private static final String TAG = "BatteryStatsParser";
+
+ private boolean mHistoryStarted;
+ private boolean mHistoryEnded;
+
+ public BatteryStatsParser() {
+ }
+
+ @Nullable
+ public String parseLine(String line) {
+ if (mHistoryEnded) {
+ return null;
+ }
+ if (!mHistoryStarted) {
+ if (line.contains("Battery History")) {
+ mHistoryStarted = true;
+ }
+ return null;
+ }
+ if (line.isEmpty()) {
+ mHistoryEnded = true;
+ return null;
+ }
+ Matcher lineMatcher = LINE_PATTERN.matcher(line);
+ if (lineMatcher.find() && lineMatcher.group(1) != null && lineMatcher.group(2) != null) {
+ if (lineMatcher.group(1).equals("0")) {
+ return "0," + lineMatcher.group(2) + "\n";
+ } else {
+ Matcher timeMatcher = TIME_PATTERN.matcher(lineMatcher.group(1));
+ if (timeMatcher.find()) {
+ Long time = getTime(lineMatcher.group(1));
+ if (time != null) {
+ return time + "," + lineMatcher.group(2) + "\n";
+ } else {
+ return null; // bad time
+ }
+ } else {
+ return null; // bad or no time
+ }
+ }
+ }
+ return null;
+ }
+
+ @Nullable
+ private Long getTime(String group) {
+ if ("0".equals(group)) {
+ return 0L;
+ }
+ Matcher timeMatcher = TIME_PATTERN.matcher(group);
+ if (!timeMatcher.find()) {
+ return null;
+ }
+
+ // Get rid of "ms".
+ String[] matches = group.split("ms", -1);
+ if (matches.length > 1) {
+ group = matches[0];
+ }
+
+ long time = 0L;
+ matches = group.split("h");
+ if (matches.length > 1) {
+ time += Long.parseLong(matches[0]) * 60 * 60 * 1000; // hours
+ group = matches[1];
+ }
+ matches = group.split("m");
+ if (matches.length > 1) {
+ time += Long.parseLong(matches[0]) * 60 * 1000; // minutes
+ group = matches[1];
+ }
+ matches = group.split("s");
+ if (matches.length > 1) {
+ time += Long.parseLong(matches[0]) * 1000; // seconds
+ group = matches[1];
+ }
+
+ if (!group.isEmpty()) {
+ time += Long.parseLong(group); // milliseconds
+ }
+ return time;
+ }
+}
--- /dev/null
+/*
+ * Copyright (C) 2017 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.statsd.loadtest;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.util.Log;
+import android.util.StatsLog;
+
+import com.android.internal.os.StatsdConfigProto.Bucket;
+import com.android.internal.os.StatsdConfigProto.Condition;
+import com.android.internal.os.StatsdConfigProto.CountMetric;
+import com.android.internal.os.StatsdConfigProto.DurationMetric;
+import com.android.internal.os.StatsdConfigProto.EventConditionLink;
+import com.android.internal.os.StatsdConfigProto.EventMetric;
+import com.android.internal.os.StatsdConfigProto.GaugeMetric;
+import com.android.internal.os.StatsdConfigProto.ValueMetric;
+import com.android.internal.os.StatsdConfigProto.KeyMatcher;
+import com.android.internal.os.StatsdConfigProto.KeyValueMatcher;
+import com.android.internal.os.StatsdConfigProto.LogEntryMatcher;
+import com.android.internal.os.StatsdConfigProto.SimpleCondition;
+import com.android.internal.os.StatsdConfigProto.SimpleLogEntryMatcher;
+import com.android.internal.os.StatsdConfigProto.StatsdConfig;
+
+import java.io.InputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Creates StatsdConfig protos for loadtesting.
+ */
+public class ConfigFactory {
+ public static final String CONFIG_NAME = "LOADTEST";
+
+ private static final String TAG = "ConfigFactory";
+
+ private final StatsdConfig mTemplate;
+
+ public ConfigFactory(Context context) {
+ // Read the config template from the resoures.
+ Resources res = context.getResources();
+ byte[] template = null;
+ StatsdConfig templateProto = null;
+ try {
+ InputStream inputStream = res.openRawResource(R.raw.loadtest_config);
+ template = new byte[inputStream.available()];
+ inputStream.read(template);
+ templateProto = StatsdConfig.parseFrom(template);
+ } catch (IOException e) {
+ Log.e(TAG, "Unable to read or parse loadtest config template. Using an empty config.");
+ }
+ mTemplate = templateProto == null ? StatsdConfig.newBuilder().build() : templateProto;
+
+ Log.d(TAG, "Loadtest template config: " + mTemplate);
+ }
+
+ /**
+ * Generates a config.
+ *
+ * All configs are based on the same template.
+ * That template is designed to make the most use of the set of atoms that {@code SequencePusher}
+ * pushes, and to exercise as many of the metrics features as possible.
+ * Furthermore, by passing a replication factor to this method, one can artificially inflate
+ * the number of metrics in the config. One can also adjust the bucket size for aggregate
+ * metrics.
+ *
+ * @param replication The number of times each metric is replicated in the config.
+ * If the config template has n metrics, the generated config will have n * replication
+ * ones
+ * @param bucketMillis The bucket size, in milliseconds, for aggregate metrics
+ * @param placebo If true, only return an empty config
+ * @return The serialized config
+ */
+ public byte[] getConfig(int replication, long bucketMillis, boolean placebo) {
+ StatsdConfig.Builder config = StatsdConfig.newBuilder()
+ .setName(CONFIG_NAME);
+ if (placebo) {
+ replication = 0; // Config will be empty, aside from a name.
+ }
+ int numMetrics = 0;
+ for (int i = 0; i < replication; i++) {
+ // metrics
+ for (EventMetric metric : mTemplate.getEventMetricList()) {
+ addEventMetric(metric, i, config);
+ numMetrics++;
+ }
+ for (CountMetric metric : mTemplate.getCountMetricList()) {
+ addCountMetric(metric, i, bucketMillis, config);
+ numMetrics++;
+ }
+ for (DurationMetric metric : mTemplate.getDurationMetricList()) {
+ addDurationMetric(metric, i, bucketMillis, config);
+ numMetrics++;
+ }
+ for (GaugeMetric metric : mTemplate.getGaugeMetricList()) {
+ addGaugeMetric(metric, i, bucketMillis, config);
+ numMetrics++;
+ }
+ for (ValueMetric metric : mTemplate.getValueMetricList()) {
+ addValueMetric(metric, i, bucketMillis, config);
+ numMetrics++;
+ }
+ // conditions
+ for (Condition condition : mTemplate.getConditionList()) {
+ addCondition(condition, i, config);
+ }
+ // matchers
+ for (LogEntryMatcher matcher : mTemplate.getLogEntryMatcherList()) {
+ addMatcher(matcher, i, config);
+ }
+ }
+
+ Log.d(TAG, "Loadtest config is : " + config.build());
+ Log.d(TAG, "Generated config has " + numMetrics + " metrics");
+
+ return config.build().toByteArray();
+ }
+
+ /**
+ * Creates {@link EventConditionLink}s that are identical to the one passed to this method,
+ * except that the names are appended with the provided suffix.
+ */
+ private List<EventConditionLink> getLinks(
+ List<EventConditionLink> links, int suffix) {
+ List<EventConditionLink> newLinks = new ArrayList();
+ for (EventConditionLink link : links) {
+ newLinks.add(link.toBuilder()
+ .setCondition(link.getCondition() + suffix)
+ .build());
+ }
+ return newLinks;
+ }
+
+ /**
+ * Creates an {@link EventMetric} based on the template. Makes sure that all names are appended
+ * with the provided suffix. Then adds that metric to the config.
+ */
+ private void addEventMetric(EventMetric template, int suffix, StatsdConfig.Builder config) {
+ EventMetric.Builder metric = template.toBuilder()
+ .setName(template.getName() + suffix)
+ .setWhat(template.getWhat() + suffix);
+ if (template.hasCondition()) {
+ metric.setCondition(template.getCondition() + suffix);
+ }
+ if (template.getLinksCount() > 0) {
+ List<EventConditionLink> links = getLinks(template.getLinksList(), suffix);
+ metric.clearLinks();
+ metric.addAllLinks(links);
+ }
+ config.addEventMetric(metric);
+ }
+
+ private Bucket getBucket(long bucketMillis) {
+ return Bucket.newBuilder()
+ .setBucketSizeMillis(bucketMillis)
+ .build();
+ }
+
+ /**
+ * Creates a {@link CountMetric} based on the template. Makes sure that all names are appended
+ * with the provided suffix, and overrides the bucket size. Then adds that metric to the config.
+ */
+ private void addCountMetric(CountMetric template, int suffix, long bucketMillis,
+ StatsdConfig.Builder config) {
+ CountMetric.Builder metric = template.toBuilder()
+ .setName(template.getName() + suffix)
+ .setWhat(template.getWhat() + suffix);
+ if (template.hasCondition()) {
+ metric.setCondition(template.getCondition() + suffix);
+ }
+ if (template.getLinksCount() > 0) {
+ List<EventConditionLink> links = getLinks(template.getLinksList(), suffix);
+ metric.clearLinks();
+ metric.addAllLinks(links);
+ }
+ metric.setBucket(getBucket(bucketMillis));
+ config.addCountMetric(metric);
+ }
+
+ /**
+ * Creates a {@link DurationMetric} based on the template. Makes sure that all names are appended
+ * with the provided suffix, and overrides the bucket size. Then adds that metric to the config.
+ */
+ private void addDurationMetric(DurationMetric template, int suffix, long bucketMillis,
+ StatsdConfig.Builder config) {
+ DurationMetric.Builder metric = template.toBuilder()
+ .setName(template.getName() + suffix)
+ .setWhat(template.getWhat() + suffix);
+ if (template.hasCondition()) {
+ metric.setCondition(template.getCondition() + suffix);
+ }
+ if (template.getLinksCount() > 0) {
+ List<EventConditionLink> links = getLinks(template.getLinksList(), suffix);
+ metric.clearLinks();
+ metric.addAllLinks(links);
+ }
+ metric.setBucket(getBucket(bucketMillis));
+ config.addDurationMetric(metric);
+ }
+
+ /**
+ * Creates a {@link GaugeMetric} based on the template. Makes sure that all names are appended
+ * with the provided suffix, and overrides the bucket size. Then adds that metric to the config.
+ */
+ private void addGaugeMetric(GaugeMetric template, int suffix, long bucketMillis,
+ StatsdConfig.Builder config) {
+ GaugeMetric.Builder metric = template.toBuilder()
+ .setName(template.getName() + suffix)
+ .setWhat(template.getWhat() + suffix);
+ if (template.hasCondition()) {
+ metric.setCondition(template.getCondition() + suffix);
+ }
+ if (template.getLinksCount() > 0) {
+ List<EventConditionLink> links = getLinks(template.getLinksList(), suffix);
+ metric.clearLinks();
+ metric.addAllLinks(links);
+ }
+ metric.setBucket(getBucket(bucketMillis));
+ config.addGaugeMetric(metric);
+ }
+
+ /**
+ * Creates a {@link ValueMetric} based on the template. Makes sure that all names are appended
+ * with the provided suffix, and overrides the bucket size. Then adds that metric to the config.
+ */
+ private void addValueMetric(ValueMetric template, int suffix, long bucketMillis,
+ StatsdConfig.Builder config) {
+ ValueMetric.Builder metric = template.toBuilder()
+ .setName(template.getName() + suffix)
+ .setWhat(template.getWhat() + suffix);
+ if (template.hasCondition()) {
+ metric.setCondition(template.getCondition() + suffix);
+ }
+ if (template.getLinksCount() > 0) {
+ List<EventConditionLink> links = getLinks(template.getLinksList(), suffix);
+ metric.clearLinks();
+ metric.addAllLinks(links);
+ }
+ metric.setBucket(getBucket(bucketMillis));
+ config.addValueMetric(metric);
+ }
+
+ /**
+ * Creates a {@link Condition} based on the template. Makes sure that all names
+ * are appended with the provided suffix. Then adds that condition to the config.
+ */
+ private void addCondition(Condition template, int suffix, StatsdConfig.Builder config) {
+ Condition.Builder condition = template.toBuilder()
+ .setName(template.getName() + suffix);
+ if (template.hasCombination()) {
+ Condition.Combination.Builder cb = template.getCombination().toBuilder()
+ .clearCondition();
+ for (String child : template.getCombination().getConditionList()) {
+ cb.addCondition(child + suffix);
+ }
+ condition.setCombination(cb.build());
+ }
+ if (template.hasSimpleCondition()) {
+ SimpleCondition.Builder sc = template.getSimpleCondition().toBuilder()
+ .setStart(template.getSimpleCondition().getStart() + suffix)
+ .setStop(template.getSimpleCondition().getStop() + suffix);
+ if (template.getSimpleCondition().hasStopAll()) {
+ sc.setStopAll(template.getSimpleCondition().getStopAll() + suffix);
+ }
+ condition.setSimpleCondition(sc.build());
+ }
+ config.addCondition(condition);
+ }
+
+ /**
+ * Creates a {@link LogEntryMatcher} based on the template. Makes sure that all names
+ * are appended with the provided suffix. Then adds that matcher to the config.
+ */
+ private void addMatcher(LogEntryMatcher template, int suffix, StatsdConfig.Builder config) {
+ LogEntryMatcher.Builder matcher = template.toBuilder()
+ .setName(template.getName() + suffix);
+ if (template.hasCombination()) {
+ LogEntryMatcher.Combination.Builder cb = template.getCombination().toBuilder()
+ .clearMatcher();
+ for (String child : template.getCombination().getMatcherList()) {
+ cb.addMatcher(child + suffix);
+ }
+ matcher.setCombination(cb);
+ }
+ config.addLogEntryMatcher(matcher);
+ }
+}
--- /dev/null
+/*
+ * Copyright (C) 2017 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.statsd.loadtest;
+
+import android.text.format.DateFormat;
+
+import com.android.os.StatsLog;
+
+import java.util.List;
+
+public class DisplayProtoUtils {
+ public static void displayLogReport(StringBuilder sb, StatsLog.ConfigMetricsReportList reports) {
+ sb.append("ConfigKey: ");
+ if (reports.hasConfigKey()) {
+ com.android.os.StatsLog.ConfigMetricsReportList.ConfigKey key = reports.getConfigKey();
+ sb.append("\tuid: ").append(key.getUid()).append(" name: ").append(key.getName())
+ .append("\n");
+ }
+
+ for (StatsLog.ConfigMetricsReport report : reports.getReportsList()) {
+ sb.append("StatsLogReport size: ").append(report.getMetricsCount()).append("\n");
+ for (StatsLog.StatsLogReport log : report.getMetricsList()) {
+ sb.append("\n\n");
+ sb.append("metric id: ").append(log.getMetricName()).append("\n");
+ sb.append("start time:").append(getDateStr(log.getStartReportNanos())).append("\n");
+ sb.append("end time:").append(getDateStr(log.getEndReportNanos())).append("\n");
+
+ switch (log.getDataCase()) {
+ case DURATION_METRICS:
+ sb.append("Duration metric data\n");
+ displayDurationMetricData(sb, log);
+ break;
+ case EVENT_METRICS:
+ sb.append("Event metric data\n");
+ displayEventMetricData(sb, log);
+ break;
+ case COUNT_METRICS:
+ sb.append("Count metric data\n");
+ displayCountMetricData(sb, log);
+ break;
+ case GAUGE_METRICS:
+ sb.append("Gauge metric data\n");
+ displayGaugeMetricData(sb, log);
+ break;
+ case VALUE_METRICS:
+ sb.append("Value metric data\n");
+ displayValueMetricData(sb, log);
+ break;
+ case DATA_NOT_SET:
+ sb.append("No metric data\n");
+ break;
+ }
+ }
+ }
+ }
+
+ public static String getDateStr(long nanoSec) {
+ return DateFormat.format("dd/MM hh:mm:ss", nanoSec/1000000).toString();
+ }
+
+ private static void displayDimension(StringBuilder sb, List<StatsLog.KeyValuePair> pairs) {
+ for (com.android.os.StatsLog.KeyValuePair kv : pairs) {
+ sb.append(kv.getKey()).append(":");
+ if (kv.hasValueBool()) {
+ sb.append(kv.getValueBool());
+ } else if (kv.hasValueFloat()) {
+ sb.append(kv.getValueFloat());
+ } else if (kv.hasValueInt()) {
+ sb.append(kv.getValueInt());
+ } else if (kv.hasValueStr()) {
+ sb.append(kv.getValueStr());
+ }
+ sb.append(" ");
+ }
+ }
+
+ public static void displayDurationMetricData(StringBuilder sb, StatsLog.StatsLogReport log) {
+ StatsLog.StatsLogReport.DurationMetricDataWrapper durationMetricDataWrapper
+ = log.getDurationMetrics();
+ sb.append("Dimension size: ").append(durationMetricDataWrapper.getDataCount()).append("\n");
+ for (StatsLog.DurationMetricData duration : durationMetricDataWrapper.getDataList()) {
+ sb.append("dimension: ");
+ displayDimension(sb, duration.getDimensionList());
+ sb.append("\n");
+
+ for (StatsLog.DurationBucketInfo info : duration.getBucketInfoList()) {
+ sb.append("\t[").append(getDateStr(info.getStartBucketNanos())).append("-")
+ .append(getDateStr(info.getEndBucketNanos())).append("] -> ")
+ .append(info.getDurationNanos()).append(" ns\n");
+ }
+ }
+ }
+
+ public static void displayEventMetricData(StringBuilder sb, StatsLog.StatsLogReport log) {
+ sb.append("Contains ").append(log.getEventMetrics().getDataCount()).append(" events\n");
+ StatsLog.StatsLogReport.EventMetricDataWrapper eventMetricDataWrapper =
+ log.getEventMetrics();
+ for (StatsLog.EventMetricData event : eventMetricDataWrapper.getDataList()) {
+ sb.append(getDateStr(event.getTimestampNanos())).append(": ");
+ switch (event.getAtom().getPushedCase()) {
+ case SETTING_CHANGED:
+ sb.append("SETTING_CHANGED\n");
+ break;
+ case SYNC_STATE_CHANGED:
+ sb.append("SYNC_STATE_CHANGED\n");
+ break;
+ case AUDIO_STATE_CHANGED:
+ sb.append("AUDIO_STATE_CHANGED\n");
+ break;
+ case CAMERA_STATE_CHANGED:
+ sb.append("CAMERA_STATE_CHANGED\n");
+ break;
+ case ISOLATED_UID_CHANGED:
+ sb.append("ISOLATED_UID_CHANGED\n");
+ break;
+ case SCREEN_STATE_CHANGED:
+ sb.append("SCREEN_STATE_CHANGED\n");
+ break;
+ case SENSOR_STATE_CHANGED:
+ sb.append("SENSOR_STATE_CHANGED\n");
+ break;
+ case BATTERY_LEVEL_CHANGED:
+ sb.append("BATTERY_LEVEL_CHANGED\n");
+ break;
+ case PLUGGED_STATE_CHANGED:
+ sb.append("PLUGGED_STATE_CHANGED\n");
+ break;
+ case WAKEUP_ALARM_OCCURRED:
+ sb.append("WAKEUP_ALARM_OCCURRED\n");
+ break;
+ case BLE_SCAN_STATE_CHANGED:
+ sb.append("BLE_SCAN_STATE_CHANGED\n");
+ break;
+ case CHARGING_STATE_CHANGED:
+ sb.append("CHARGING_STATE_CHANGED\n");
+ break;
+ case GPS_SCAN_STATE_CHANGED:
+ sb.append("GPS_SCAN_STATE_CHANGED\n");
+ break;
+ case KERNEL_WAKEUP_REPORTED:
+ sb.append("KERNEL_WAKEUP_REPORTED\n");
+ break;
+ case WAKELOCK_STATE_CHANGED:
+ sb.append("WAKELOCK_STATE_CHANGED\n");
+ break;
+ case WIFI_LOCK_STATE_CHANGED:
+ sb.append("WIFI_LOCK_STATE_CHANGED\n");
+ break;
+ case WIFI_SCAN_STATE_CHANGED:
+ sb.append("WIFI_SCAN_STATE_CHANGED\n");
+ break;
+ case BLE_SCAN_RESULT_RECEIVED:
+ sb.append("BLE_SCAN_RESULT_RECEIVED\n");
+ break;
+ case DEVICE_ON_STATUS_CHANGED:
+ sb.append("DEVICE_ON_STATUS_CHANGED\n");
+ break;
+ case FLASHLIGHT_STATE_CHANGED:
+ sb.append("FLASHLIGHT_STATE_CHANGED\n");
+ break;
+ case SCREEN_BRIGHTNESS_CHANGED:
+ sb.append("SCREEN_BRIGHTNESS_CHANGED\n");
+ break;
+ case UID_PROCESS_STATE_CHANGED:
+ sb.append("UID_PROCESS_STATE_CHANGED\n");
+ break;
+ case UID_WAKELOCK_STATE_CHANGED:
+ sb.append("UID_WAKELOCK_STATE_CHANGED\n");
+ break;
+ case DEVICE_TEMPERATURE_REPORTED:
+ sb.append("DEVICE_TEMPERATURE_REPORTED\n");
+ break;
+ case SCHEDULED_JOB_STATE_CHANGED:
+ sb.append("SCHEDULED_JOB_STATE_CHANGED\n");
+ break;
+ case MEDIA_CODEC_ACTIVITY_CHANGED:
+ sb.append("MEDIA_CODEC_ACTIVITY_CHANGED\n");
+ break;
+ case WIFI_SIGNAL_STRENGTH_CHANGED:
+ sb.append("WIFI_SIGNAL_STRENGTH_CHANGED\n");
+ break;
+ case PHONE_SIGNAL_STRENGTH_CHANGED:
+ sb.append("PHONE_SIGNAL_STRENGTH_CHANGED\n");
+ break;
+ case DEVICE_IDLE_MODE_STATE_CHANGED:
+ sb.append("DEVICE_IDLE_MODE_STATE_CHANGED\n");
+ break;
+ case BATTERY_SAVER_MODE_STATE_CHANGED:
+ sb.append("BATTERY_SAVER_MODE_STATE_CHANGED\n");
+ break;
+ case PROCESS_LIFE_CYCLE_STATE_CHANGED:
+ sb.append("PROCESS_LIFE_CYCLE_STATE_CHANGED\n");
+ break;
+ case ACTIVITY_FOREGROUND_STATE_CHANGED:
+ sb.append("ACTIVITY_FOREGROUND_STATE_CHANGED\n");
+ break;
+ case BLE_UNOPTIMIZED_SCAN_STATE_CHANGED:
+ sb.append("BLE_UNOPTIMIZED_SCAN_STATE_CHANGED\n");
+ break;
+ case LONG_PARTIAL_WAKELOCK_STATE_CHANGED:
+ sb.append("LONG_PARTIAL_WAKELOCK_STATE_CHANGED\n");
+ break;
+ case PUSHED_NOT_SET:
+ sb.append("PUSHED_NOT_SET\n");
+ break;
+ }
+ }
+ }
+
+ public static void displayCountMetricData(StringBuilder sb, StatsLog.StatsLogReport log) {
+ StatsLog.StatsLogReport.CountMetricDataWrapper countMetricDataWrapper
+ = log.getCountMetrics();
+ sb.append("Dimension size: ").append(countMetricDataWrapper.getDataCount()).append("\n");
+ for (StatsLog.CountMetricData count : countMetricDataWrapper.getDataList()) {
+ sb.append("dimension: ");
+ displayDimension(sb, count.getDimensionList());
+ sb.append("\n");
+
+ for (StatsLog.CountBucketInfo info : count.getBucketInfoList()) {
+ sb.append("\t[").append(getDateStr(info.getStartBucketNanos())).append("-")
+ .append(getDateStr(info.getEndBucketNanos())).append("] -> ")
+ .append(info.getCount()).append("\n");
+ }
+ }
+ }
+
+ public static void displayGaugeMetricData(StringBuilder sb, StatsLog.StatsLogReport log) {
+ sb.append("Display me!");
+ }
+
+ public static void displayValueMetricData(StringBuilder sb, StatsLog.StatsLogReport log) {
+ sb.append("Display me!");
+ }
+}
--- /dev/null
+/*
+ * Copyright (C) 2017 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.statsd.loadtest;
+
+import android.app.Activity;
+import android.app.AlarmManager;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Color;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IStatsManager;
+import android.os.PowerManager;
+import android.os.PowerManager.WakeLock;
+import android.os.ServiceManager;
+import android.os.SystemClock;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.util.Log;
+import android.util.StatsLog;
+import android.util.StatsManager;
+import android.view.View;
+import android.view.inputmethod.InputMethodManager;
+import android.view.MotionEvent;
+import android.view.View.OnFocusChangeListener;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.EditText;
+import android.widget.TextView;
+import android.widget.Toast;
+
+/**
+ * Runs a load test for statsd.
+ * How it works:
+ * <ul>
+ * <li> Sets up and pushes a custom config with metrics that exercise a large swath of code paths.
+ * <li> Periodically logs certain atoms into logd.
+ * <li> Impact on battery can be printed to logcat, or a bug report can be filed and analyzed
+ * in battery Historian.
+ * </ul>
+ * The load depends on how demanding the config is, as well as how frequently atoms are pushsed
+ * to logd. Those are all controlled by 4 adjustable parameters:
+ * <ul>
+ * <li> The 'replication' parameter artificially multiplies the number of metrics in the config.
+ * <li> The bucket size controls the time-bucketing the aggregate metrics.
+ * <li> The period parameter controls how frequently atoms are pushed to logd.
+ * <li> The 'burst' parameter controls how many atoms are pushed at the same time (per period).
+ * </ul>
+ */
+public class LoadtestActivity extends Activity {
+
+ private static final String TAG = "StatsdLoadtest";
+ private static final String TYPE = "type";
+ private static final String ALARM = "push_alarm";
+ private static final String START = "start";
+ private static final String STOP = "stop";
+
+ public final static class PusherAlarmReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Intent activityIntent = new Intent(context, LoadtestActivity.class);
+ activityIntent.putExtra(TYPE, ALARM);
+ context.startActivity(activityIntent);
+ }
+ }
+
+ public final static class StopperAlarmReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Intent activityIntent = new Intent(context, LoadtestActivity.class);
+ activityIntent.putExtra(TYPE, STOP);
+ context.startActivity(activityIntent);
+ }
+ }
+
+ private AlarmManager mAlarmMgr;
+
+ /** Used to periodically log atoms to logd. */
+ private PendingIntent mPushPendingIntent;
+
+ /** Used to end the loadtest. */
+ private PendingIntent mStopPendingIntent;
+
+ private Button mStartStop;
+ private EditText mReplicationText;
+ private EditText mBucketText;
+ private EditText mPeriodText;
+ private EditText mBurstText;
+ private EditText mDurationText;
+ private TextView mReportText;
+ private CheckBox mPlaceboCheckBox;
+
+ /** For measuring perf data. */
+ private PerfData mPerfData;
+
+ /** For communicating with statsd. */
+ private StatsManager mStatsManager;
+
+ private PowerManager mPowerManager;
+ private WakeLock mWakeLock;
+
+ /**
+ * If true, we only measure the effect of the loadtest infrastructure. No atom are pushed and
+ * the configuration is empty.
+ */
+ private boolean mPlacebo;
+
+ /** The burst size. */
+ private int mBurst;
+
+ /** The metrics replication. */
+ private int mReplication;
+
+ /** The period, in seconds, at which batches of atoms are pushed. */
+ private long mPeriodSecs;
+
+ /** The bucket size, in minutes, for aggregate metrics. */
+ private long mBucketMins;
+
+ /** The duration, in minutes, of the loadtest. */
+ private long mDurationMins;
+
+ /** Whether the loadtest has started. */
+ private boolean mStarted = false;
+
+ /** Orchestrates the logging of pushed events into logd. */
+ private SequencePusher mPusher;
+
+ /** Generates statsd configs. */
+ private ConfigFactory mFactory;
+
+ /** For intra-minute periods. */
+ private final Handler mHandler = new Handler();
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ Log.d(TAG, "Starting loadtest");
+
+ setContentView(R.layout.activity_loadtest);
+ mReportText = (TextView) findViewById(R.id.report_text);
+ initBurst();
+ initReplication();
+ initBucket();
+ initPeriod();
+ initDuration();
+ initPlacebo();
+
+ // Hide the keyboard outside edit texts.
+ findViewById(R.id.outside).setOnTouchListener(new View.OnTouchListener() {
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ InputMethodManager imm =
+ (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
+ if (getCurrentFocus() != null) {
+ imm.hideSoftInputFromWindow(getCurrentFocus().getWindowToken(), 0);
+ }
+ return true;
+ }
+ });
+
+ mStartStop = findViewById(R.id.start_stop);
+ mStartStop.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ if (mStarted) {
+ stopLoadtest(true);
+ } else {
+ startLoadtest();
+ }
+ }
+ });
+
+ findViewById(R.id.display_output).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ fetchAndDisplayData();
+ }
+ });
+
+ findViewById(R.id.display_perf).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ mPerfData.publishData(LoadtestActivity.this, mPlacebo, mReplication, mBucketMins,
+ mPeriodSecs, mBurst);
+ }
+ });
+
+ mAlarmMgr = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
+ mStatsManager = (StatsManager) getSystemService("stats");
+ mPowerManager = (PowerManager) getSystemService(Context.POWER_SERVICE);
+ mFactory = new ConfigFactory(this);
+ mPerfData = new PerfData();
+ stopLoadtest(false);
+ mReportText.setText("");
+ }
+
+ @Override
+ public void onNewIntent(Intent intent) {
+ String type = intent.getStringExtra(TYPE);
+ if (type == null) {
+ return;
+ }
+ switch (type) {
+ case ALARM:
+ onAlarm(intent);
+ break;
+ case START:
+ startLoadtest();
+ break;
+ case STOP:
+ stopLoadtest(true);
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown type: " + type);
+ }
+ }
+
+ @Override
+ public void onDestroy() {
+ Log.d(TAG, "Destroying");
+ stopLoadtest(false);
+ clearConfigs();
+ super.onDestroy();
+ }
+
+ private void onAlarm(Intent intent) {
+ Log.d(TAG, "ON ALARM");
+
+ // Set the next task.
+ scheduleNext();
+
+ // Do the work.
+ mWakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "StatsdLoadTest");
+ mWakeLock.acquire();
+ if (mPusher != null) {
+ mPusher.next();
+ }
+ mWakeLock.release();
+ mWakeLock = null;
+ }
+
+ /** Schedules the next cycle of pushing atoms into logd. */
+ private void scheduleNext() {
+ Intent intent = new Intent(this, PusherAlarmReceiver.class);
+ intent.putExtra(TYPE, ALARM);
+ mPushPendingIntent = PendingIntent.getBroadcast(this, 0, intent, 0);
+ long nextTime = SystemClock.elapsedRealtime() + mPeriodSecs * 1000;
+ mAlarmMgr.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, nextTime, mPushPendingIntent);
+ }
+
+ private synchronized void startLoadtest() {
+ if (mStarted) {
+ return;
+ }
+
+ // Clean up the state.
+ stopLoadtest(false);
+
+ // Prepare to push a sequence of atoms to logd.
+ mPusher = new SequencePusher(mBurst, mPlacebo);
+
+ // Create a config and push it to statsd.
+ if (!setConfig(mFactory.getConfig(mReplication, mBucketMins * 60 * 1000, mPlacebo))) {
+ return;
+ }
+
+ // Remember to stop in the future.
+ Intent intent = new Intent(this, StopperAlarmReceiver.class);
+ intent.putExtra(TYPE, STOP);
+ mStopPendingIntent = PendingIntent.getBroadcast(this, 0, intent, 0);
+ long nextTime = SystemClock.elapsedRealtime() + mDurationMins * 60 * 1000;
+ mAlarmMgr.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, nextTime, mStopPendingIntent);
+
+ // Log atoms.
+ scheduleNext();
+
+ // Reset battery data.
+ mPerfData.resetData(this);
+
+ mReportText.setText("");
+
+ updateStarted(true);
+ }
+
+ private synchronized void stopLoadtest(boolean publishPerfData) {
+ if (mPushPendingIntent != null) {
+ Log.d(TAG, "Canceling pre-existing push alarm");
+ mAlarmMgr.cancel(mPushPendingIntent);
+ mPushPendingIntent = null;
+ }
+ if (mStopPendingIntent != null) {
+ Log.d(TAG, "Canceling pre-existing stop alarm");
+ mAlarmMgr.cancel(mStopPendingIntent);
+ mStopPendingIntent = null;
+ }
+ if (mWakeLock != null) {
+ mWakeLock.release();
+ mWakeLock = null;
+ }
+ fetchAndDisplayData();
+ clearConfigs();
+ updateStarted(false);
+ if (publishPerfData) {
+ mPerfData.publishData(this, mPlacebo, mReplication, mBucketMins, mPeriodSecs, mBurst);
+ }
+ }
+
+ private synchronized void updateStarted(boolean started) {
+ mStarted = started;
+ mStartStop.setBackgroundColor(started ?
+ Color.parseColor("#FFFF0000") : Color.parseColor("#FF00FF00"));
+ mStartStop.setText(started ? getString(R.string.stop) : getString(R.string.start));
+ updateControlsEnabled();
+ }
+
+ private void updateControlsEnabled() {
+ mBurstText.setEnabled(!mPlacebo && !mStarted);
+ mReplicationText.setEnabled(!mPlacebo && !mStarted);
+ mPeriodText.setEnabled(!mStarted);
+ mBucketText.setEnabled(!mPlacebo && !mStarted);
+ mDurationText.setEnabled(!mStarted);
+ mPlaceboCheckBox.setEnabled(!mStarted);
+ }
+
+ private void fetchAndDisplayData() {
+ if (!statsdRunning()) {
+ return;
+ }
+ if (mStatsManager != null) {
+ byte[] data = mStatsManager.getData(ConfigFactory.CONFIG_NAME);
+ if (data != null) {
+ displayData(data);
+ } else {
+ mReportText.setText("Failed to pull data");
+ }
+ }
+ }
+
+ private void displayData(byte[] data) {
+ com.android.os.StatsLog.ConfigMetricsReportList reports = null;
+ boolean good = false;
+ if (data != null) {
+ try {
+ reports = com.android.os.StatsLog.ConfigMetricsReportList.parseFrom(data);
+ good = true;
+ } catch (com.google.protobuf.InvalidProtocolBufferException e) {
+ // display it in the text view.
+ }
+ }
+ int size = data == null ? 0 : data.length;
+ StringBuilder sb = new StringBuilder();
+ sb.append(good ? "Proto parsing OK!" : "Proto parsing Error!");
+ sb.append(" size:").append(size).append("\n");
+
+ if (good && reports != null) {
+ DisplayProtoUtils.displayLogReport(sb, reports);
+ mReportText.setText(sb.toString());
+ }
+ }
+
+ private boolean statsdRunning() {
+ if (IStatsManager.Stub.asInterface(ServiceManager.getService("stats")) == null) {
+ Log.d(TAG, "Statsd not running");
+ Toast.makeText(LoadtestActivity.this, "Statsd NOT running!", Toast.LENGTH_LONG).show();
+ return false;
+ }
+ return true;
+ }
+
+ private int sanitizeInt(int val, int min, int max) {
+ if (val > max) {
+ val = max;
+ } else if (val < min) {
+ val = min;
+ }
+ return val;
+ }
+
+ private void clearConfigs() {
+ // TODO: Clear all configs instead of specific ones.
+ if (mStatsManager != null) {
+ if (!mStatsManager.removeConfiguration("fake")) {
+ Log.d(TAG, "Removed \"fake\" statsd configs.");
+ } else {
+ Log.d(TAG, "Failed to remove \"fake\" config. Loadtest results cannot be trusted.");
+ }
+ if (mStarted) {
+ if (!mStatsManager.removeConfiguration(ConfigFactory.CONFIG_NAME)) {
+ Log.d(TAG, "Removed loadtest statsd configs.");
+ } else {
+ Log.d(TAG, "Failed to remove loadtest configs.");
+ }
+ }
+ }
+ }
+
+ private boolean setConfig(byte[] config) {
+ if (mStatsManager != null) {
+ if (mStatsManager.addConfiguration(ConfigFactory.CONFIG_NAME,
+ config, getPackageName(), LoadtestActivity.this.getClass().getName())) {
+ Log.d(TAG, "Config pushed to statsd");
+ return true;
+ } else {
+ Log.d(TAG, "Failed to push config to statsd");
+ }
+ }
+ return false;
+ }
+
+ private synchronized void setReplication(int replication) {
+ mReplication = replication;
+ }
+
+ private synchronized void setPeriodSecs(long periodSecs) {
+ mPeriodSecs = periodSecs;
+ }
+
+ private synchronized void setBucketMins(long bucketMins) {
+ mBucketMins = bucketMins;
+ }
+
+ private synchronized void setBurst(int burst) {
+ mBurst = burst;
+ }
+
+ private synchronized void setDurationMins(long durationMins) {
+ mDurationMins = durationMins;
+ }
+
+ private synchronized void setPlacebo(boolean placebo) {
+ mPlacebo = placebo;
+ updateControlsEnabled();
+ }
+
+ private void handleFocus(EditText editText) {
+ editText.setOnFocusChangeListener(new OnFocusChangeListener() {
+ @Override
+ public void onFocusChange(View v, boolean hasFocus) {
+ if (!hasFocus && editText.getText().toString().isEmpty()) {
+ editText.setText("-1", TextView.BufferType.EDITABLE);
+ }
+ }
+ });
+ }
+
+ private void initBurst() {
+ mBurst = getResources().getInteger(R.integer.burst_default);
+ mBurstText = (EditText) findViewById(R.id.burst);
+ mBurstText.addTextChangedListener(new NumericalWatcher(mBurstText, 0, 50) {
+ @Override
+ public void onNewValue(int newValue) {
+ setBurst(newValue);
+ }
+ });
+ handleFocus(mBurstText);
+ }
+
+ private void initReplication() {
+ mReplication = getResources().getInteger(R.integer.replication_default);
+ mReplicationText = (EditText) findViewById(R.id.replication);
+ mReplicationText.addTextChangedListener(new NumericalWatcher(mReplicationText, 1, 100) {
+ @Override
+ public void onNewValue(int newValue) {
+ setReplication(newValue);
+ }
+ });
+ handleFocus(mReplicationText);
+ }
+
+ private void initBucket() {
+ mBucketMins = getResources().getInteger(R.integer.bucket_default);
+ mBucketText = (EditText) findViewById(R.id.bucket);
+ mBucketText.addTextChangedListener(new NumericalWatcher(mBucketText, 1, 24 * 60) {
+ @Override
+ public void onNewValue(int newValue) {
+ setBucketMins(newValue);
+ }
+ });
+ handleFocus(mBucketText);
+ }
+
+ private void initPeriod() {
+ mPeriodSecs = getResources().getInteger(R.integer.period_default);
+ mPeriodText = (EditText) findViewById(R.id.period);
+ mPeriodText.addTextChangedListener(new NumericalWatcher(mPeriodText, 1, 60) {
+ @Override
+ public void onNewValue(int newValue) {
+ setPeriodSecs(newValue);
+ }
+ });
+ handleFocus(mPeriodText);
+ }
+
+ private void initDuration() {
+ mDurationMins = getResources().getInteger(R.integer.duration_default);
+ mDurationText = (EditText) findViewById(R.id.duration);
+ mDurationText.addTextChangedListener(new NumericalWatcher(mDurationText, 1, 24 * 60) {
+ @Override
+ public void onNewValue(int newValue) {
+ setDurationMins(newValue);
+ }
+ });
+ handleFocus(mDurationText);
+ }
+
+ private void initPlacebo() {
+ mPlaceboCheckBox = findViewById(R.id.placebo);
+ mPlacebo = false;
+ mPlaceboCheckBox.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ setPlacebo(((CheckBox) view).isChecked());
+ }
+ });
+ }
+}
--- /dev/null
+/*
+ * Copyright (C) 2017 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.statsd.loadtest;
+
+import android.app.Activity;
+import android.app.AlarmManager;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.util.Log;
+import android.util.StatsLog;
+import android.util.StatsManager;
+import android.view.View;
+import android.widget.EditText;
+import android.widget.TextView;
+import android.widget.Toast;
+import android.os.IStatsManager;
+import android.os.ServiceManager;
+import android.view.View.OnFocusChangeListener;
+
+public abstract class NumericalWatcher implements TextWatcher {
+
+ private static final String TAG = "NumericalWatcher";
+
+ private final TextView mTextView;
+ private final int mMin;
+ private final int mMax;
+ private int currentValue = -1;
+
+ public NumericalWatcher(TextView textView, int min, int max) {
+ mTextView = textView;
+ mMin = min;
+ mMax = max;
+ }
+
+ public abstract void onNewValue(int newValue);
+
+ @Override
+ final public void afterTextChanged(Editable editable) {
+ String s = mTextView.getText().toString();
+ if (s.isEmpty()) {
+ return;
+ }
+ int unsanitized = Integer.parseInt(s);
+ int newValue = sanitize(unsanitized);
+
+ Log.d(TAG, "YOYO " + currentValue + " " + newValue + " " + unsanitized);
+
+ if (currentValue != newValue || unsanitized != newValue) {
+ currentValue = newValue;
+ editable.clear();
+ editable.append(newValue + "");
+ }
+ onNewValue(newValue);
+ }
+
+ @Override
+ final public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
+
+ @Override
+ final public void onTextChanged(CharSequence s, int start, int before, int count) {}
+
+ private int sanitize(int val) {
+ if (val > mMax) {
+ val = mMax;
+ } else if (val < mMin) {
+ val = mMin;
+ }
+ return val;
+ }
+}
--- /dev/null
+/*
+ * Copyright (C) 2017 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.statsd.loadtest;
+
+import android.annotation.Nullable;
+import android.app.Activity;
+import android.app.AlarmManager;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Environment;
+import android.util.Log;
+import android.os.Debug;
+
+import java.io.BufferedReader;
+import java.io.Closeable;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.FileWriter;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.IOException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+/** Prints some information about the device via Dumpsys in order to evaluate health metrics. */
+public class PerfData {
+
+ private static final String TAG = "PerfData";
+ private static final String DUMP_FILENAME = TAG + "_dump.tmp";
+
+ public void resetData(Context context) {
+ runDumpsysStats(context, "batterystats", "--reset");
+ }
+
+ public void publishData(Context context, boolean placebo, int replication, long bucketMins,
+ long periodSecs, int burst) {
+ publishBatteryData(context, placebo, replication, bucketMins, periodSecs, burst);
+ }
+
+ private void publishBatteryData(Context context, boolean placebo, int replication,
+ long bucketMins, long periodSecs, int burst) {
+ // Don't use --checkin.
+ runDumpsysStats(context, "batterystats");
+ writeBatteryData(context, placebo, replication, bucketMins, periodSecs, burst);
+ }
+
+ private void runDumpsysStats(Context context, String cmd, String... args) {
+ boolean success = false;
+ // Call dumpsys Dump statistics to a file.
+ FileOutputStream fo = null;
+ try {
+ fo = context.openFileOutput(DUMP_FILENAME, Context.MODE_PRIVATE);
+ if (!Debug.dumpService(cmd, fo.getFD(), args)) {
+ Log.w(TAG, "Dumpsys failed.");
+ }
+ success = true;
+ } catch (IOException | SecurityException | NullPointerException e) {
+ // SecurityException may occur when trying to dump multi-user info.
+ // NPE can occur during dumpService (root cause unknown).
+ throw new RuntimeException(e);
+ } finally {
+ closeQuietly(fo);
+ }
+ }
+
+ private String readDumpFile(Context context) {
+ StringBuilder sb = new StringBuilder();
+ FileInputStream fi = null;
+ BufferedReader br = null;
+ try {
+ fi = context.openFileInput(DUMP_FILENAME);
+ br = new BufferedReader(new InputStreamReader(fi));
+ String line = br.readLine();
+ while (line != null) {
+ sb.append(line);
+ sb.append(System.lineSeparator());
+ line = br.readLine();
+ }
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ } finally {
+ closeQuietly(br);
+ }
+ return sb.toString();
+ }
+
+ private static void closeQuietly(@Nullable Closeable c) {
+ if (c != null) {
+ try {
+ c.close();
+ } catch (IOException ignore) {
+ }
+ }
+ }
+
+ public void writeBatteryData(Context context, boolean placebo, int replication, long bucketMins,
+ long periodSecs, int burst) {
+ BatteryStatsParser parser = new BatteryStatsParser();
+ FileInputStream fi = null;
+ BufferedReader br = null;
+ String suffix = new SimpleDateFormat("YYYY_MM_dd_HH_mm_ss").format(new Date());
+ File batteryDataFile = new File(getStorageDir(), "battery_" + suffix + ".csv");
+ Log.d(TAG, "Writing battery data to " + batteryDataFile.getAbsolutePath());
+
+ FileWriter writer = null;
+ try {
+ fi = context.openFileInput(DUMP_FILENAME);
+ writer = new FileWriter(batteryDataFile);
+ writer.append("time,battery_level"
+ + getColumnName(placebo, replication, bucketMins, periodSecs, burst) + "\n");
+ br = new BufferedReader(new InputStreamReader(fi));
+ String line = br.readLine();
+ while (line != null) {
+ String recordLine = parser.parseLine(line);
+ if (recordLine != null) {
+ writer.append(recordLine);
+ }
+ line = br.readLine();
+ }
+ writer.flush();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ } finally {
+ closeQuietly(writer);
+ closeQuietly(br);
+ }
+ }
+
+ private File getStorageDir() {
+ File file = new File(Environment.getExternalStoragePublicDirectory(
+ Environment.DIRECTORY_DOCUMENTS), "loadtest");
+ if (!file.mkdirs()) {
+ Log.e(TAG, "Directory not created");
+ }
+ return file;
+ }
+
+ private String getColumnName(boolean placebo, int replication, long bucketMins, long periodSecs,
+ int burst) {
+ if (placebo) {
+ return "_placebo_p=" + periodSecs;
+ }
+ return "_r=" + replication + "_bkt=" + bucketMins + "_p=" + periodSecs + "_bst=" + burst;
+ }
+}
--- /dev/null
+/*
+ * Copyright (C) 2017 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.statsd.loadtest;
+
+import android.util.Log;
+import android.util.StatsLog;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Manages the pushing of atoms into logd for loadtesting.
+ * We rely on small number of pushed atoms, and a config with metrics based on those atoms.
+ * The atoms are:
+ * <ul>
+ * <li> BatteryLevelChanged - For EventMetric, CountMetric and GaugeMetric (no dimensions).
+ * <li> BleScanResultReceived - For CountMetric and ValueMetric, sliced by uid.
+ * <li> ChargingStateChanged - For DurationMetric (no dimension).
+ * <li> GpsScanStateChanged - For DurationMetric, sliced by uid.
+ * <li> ScreenStateChanged - For Conditions with no dimensions.
+ * <li> AudioStateChanged - For Conditions with dimensions (uid).
+ * </ul>
+ * The sequence is played over and over at a given frequency.
+ */
+public class SequencePusher {
+ private static final String TAG = "SequencePusher";
+
+ /** Some atoms are pushed in burst of {@code mBurst} events. */
+ private final int mBurst;
+
+ /** If this is true, we don't log anything in logd. */
+ private final boolean mPlacebo;
+
+ /** Current state in the automaton. */
+ private int mCursor = 0;
+
+ public SequencePusher(int burst, boolean placebo) {
+ mBurst = burst;
+ mPlacebo = placebo;
+ }
+
+ /**
+ * Pushes the next atom to logd.
+ * This follows a small automaton which makes the right events and conditions overlap:
+ * (0) Push a burst of BatteryLevelChanged atoms.
+ * (1) Push a burst of BleScanResultReceived atoms.
+ * (2) Push ChargingStateChanged with BATTERY_STATUS_CHARGING once.
+ * (3) Push a burst of GpsScanStateChanged atoms with ON, with a different uid each time.
+ * (4) Push ChargingStateChanged with BATTERY_STATUS_NOT_CHARGING once.
+ * (5) Push a burst GpsScanStateChanged atoms with OFF, with a different uid each time.
+ * (6) Push ScreenStateChanged with STATE_ON once.
+ * (7) Push a burst of AudioStateChanged with ON, with a different uid each time.
+ * (8) Repeat steps (0)-(5).
+ * (9) Push ScreenStateChanged with STATE_OFF once.
+ * (10) Push a burst of AudioStateChanged with OFF, with a different uid each time.
+ * and repeat.
+ */
+ public void next() {
+ Log.d(TAG, "Next step: " + mCursor);
+ if (mPlacebo) {
+ return;
+ }
+ switch (mCursor) {
+ case 0:
+ case 8:
+ for (int i = 0; i < mBurst; i++) {
+ StatsLog.write(StatsLog.BATTERY_LEVEL_CHANGED, 50 + i /* battery_level */);
+ }
+ break;
+ case 1:
+ case 9:
+ for (int i = 0; i < mBurst; i++) {
+ StatsLog.write(StatsLog.BLE_SCAN_RESULT_RECEIVED, i /* uid */,
+ 100 /* num_of_results */);
+ }
+ break;
+ case 2:
+ case 10:
+ StatsLog.write(StatsLog.CHARGING_STATE_CHANGED,
+ StatsLog.CHARGING_STATE_CHANGED__CHARGING_STATE__BATTERY_STATUS_CHARGING
+ /* charging_state */);
+ break;
+ case 3:
+ case 11:
+ for (int i = 0; i < mBurst; i++) {
+ StatsLog.write(StatsLog.GPS_SCAN_STATE_CHANGED, i /* uid */,
+ StatsLog.GPS_SCAN_STATE_CHANGED__STATE__ON /* state */);
+ }
+ break;
+ case 4:
+ case 12:
+ StatsLog.write(StatsLog.CHARGING_STATE_CHANGED,
+ StatsLog.CHARGING_STATE_CHANGED__CHARGING_STATE__BATTERY_STATUS_NOT_CHARGING
+ /* charging_state */);
+ break;
+ case 5:
+ case 13:
+ for (int i = 0; i < mBurst; i++) {
+ StatsLog.write(StatsLog.GPS_SCAN_STATE_CHANGED, i /* uid */,
+ StatsLog.GPS_SCAN_STATE_CHANGED__STATE__OFF /* state */);
+ }
+ break;
+ case 6:
+ StatsLog.write(StatsLog.SCREEN_STATE_CHANGED,
+ StatsLog.SCREEN_STATE_CHANGED__DISPLAY_STATE__STATE_ON /* display_state */);
+ break;
+ case 7:
+ for (int i = 0; i < mBurst; i++) {
+ StatsLog.write(StatsLog.AUDIO_STATE_CHANGED, i /* uid */,
+ StatsLog.AUDIO_STATE_CHANGED__STATE__ON /* state */);
+ }
+ break;
+ case 14:
+ StatsLog.write(StatsLog.SCREEN_STATE_CHANGED,
+ StatsLog.SCREEN_STATE_CHANGED__DISPLAY_STATE__STATE_OFF /* display_state */);
+ break;
+ case 15:
+ for (int i = 0; i < mBurst; i++) {
+ StatsLog.write(StatsLog.AUDIO_STATE_CHANGED, i /* uid */,
+ StatsLog.AUDIO_STATE_CHANGED__STATE__OFF /* state */);
+ }
+ break;
+ default:
+ }
+ mCursor++;
+ if (mCursor > 15) {
+ mCursor = 0;
+ }
+ }
+
+ /**
+ * Properly finishes in order to be close all conditions and durations.
+ */
+ public void finish() {
+ // Screen goes back to off. This will ensure that conditions get back to false.
+ StatsLog.write(StatsLog.SCREEN_STATE_CHANGED,
+ StatsLog.SCREEN_STATE_CHANGED__DISPLAY_STATE__STATE_OFF /* display_state */);
+ for (int i = 0; i < mBurst; i++) {
+ StatsLog.write(StatsLog.AUDIO_STATE_CHANGED, i /* uid */,
+ StatsLog.AUDIO_STATE_CHANGED__STATE__OFF /* state */);
+ }
+ // Stop charging, to ensure the corresponding durations are closed.
+ StatsLog.write(StatsLog.CHARGING_STATE_CHANGED,
+ StatsLog.CHARGING_STATE_CHANGED__CHARGING_STATE__BATTERY_STATUS_NOT_CHARGING
+ /* charging_state */);
+ // Stop scanning GPS, to ensure the corresponding conditions get back to false.
+ for (int i = 0; i < mBurst; i++) {
+ StatsLog.write(StatsLog.GPS_SCAN_STATE_CHANGED, i /* uid */,
+ StatsLog.GPS_SCAN_STATE_CHANGED__STATE__OFF /* state */);
+ }
+ }
+}