OSDN Git Service

Create statsd loadtest app.
authorStefan Lafon <stlafon@google.com>
Sat, 25 Nov 2017 19:59:11 +0000 (11:59 -0800)
committerStefan Lafon <stlafon@google.com>
Sun, 3 Dec 2017 18:12:03 +0000 (10:12 -0800)
That app automatically sends a bunch of events to logd.
The frequency is adjustable.

Test: Ran statsd.

Change-Id: I7a88c140e39d22549cc76ef68059d292e85a8a24

17 files changed:
cmds/statsd/tools/loadtest/Android.mk [new file with mode: 0644]
cmds/statsd/tools/loadtest/AndroidManifest.xml [new file with mode: 0644]
cmds/statsd/tools/loadtest/res/drawable-hdpi/ic_launcher.png [new file with mode: 0644]
cmds/statsd/tools/loadtest/res/drawable-mdpi/ic_launcher.png [new file with mode: 0644]
cmds/statsd/tools/loadtest/res/drawable-xhdpi/ic_launcher.png [new file with mode: 0644]
cmds/statsd/tools/loadtest/res/drawable-xxhdpi/ic_launcher.png [new file with mode: 0644]
cmds/statsd/tools/loadtest/res/layout/activity_loadtest.xml [new file with mode: 0644]
cmds/statsd/tools/loadtest/res/raw/loadtest_config [new file with mode: 0755]
cmds/statsd/tools/loadtest/res/values/integers.xml [new file with mode: 0644]
cmds/statsd/tools/loadtest/res/values/strings.xml [new file with mode: 0644]
cmds/statsd/tools/loadtest/src/com/android/statsd/loadtest/BatteryStatsParser.java [new file with mode: 0644]
cmds/statsd/tools/loadtest/src/com/android/statsd/loadtest/ConfigFactory.java [new file with mode: 0644]
cmds/statsd/tools/loadtest/src/com/android/statsd/loadtest/DisplayProtoUtils.java [new file with mode: 0644]
cmds/statsd/tools/loadtest/src/com/android/statsd/loadtest/LoadtestActivity.java [new file with mode: 0644]
cmds/statsd/tools/loadtest/src/com/android/statsd/loadtest/NumericalWatcher.java [new file with mode: 0644]
cmds/statsd/tools/loadtest/src/com/android/statsd/loadtest/PerfData.java [new file with mode: 0644]
cmds/statsd/tools/loadtest/src/com/android/statsd/loadtest/SequencePusher.java [new file with mode: 0644]

diff --git a/cmds/statsd/tools/loadtest/Android.mk b/cmds/statsd/tools/loadtest/Android.mk
new file mode 100644 (file)
index 0000000..f3f0a7c
--- /dev/null
@@ -0,0 +1,35 @@
+# 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)
diff --git a/cmds/statsd/tools/loadtest/AndroidManifest.xml b/cmds/statsd/tools/loadtest/AndroidManifest.xml
new file mode 100644 (file)
index 0000000..d74c707
--- /dev/null
@@ -0,0 +1,43 @@
+<?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>
diff --git a/cmds/statsd/tools/loadtest/res/drawable-hdpi/ic_launcher.png b/cmds/statsd/tools/loadtest/res/drawable-hdpi/ic_launcher.png
new file mode 100644 (file)
index 0000000..55621cc
Binary files /dev/null and b/cmds/statsd/tools/loadtest/res/drawable-hdpi/ic_launcher.png differ
diff --git a/cmds/statsd/tools/loadtest/res/drawable-mdpi/ic_launcher.png b/cmds/statsd/tools/loadtest/res/drawable-mdpi/ic_launcher.png
new file mode 100644 (file)
index 0000000..11ec206
Binary files /dev/null and b/cmds/statsd/tools/loadtest/res/drawable-mdpi/ic_launcher.png differ
diff --git a/cmds/statsd/tools/loadtest/res/drawable-xhdpi/ic_launcher.png b/cmds/statsd/tools/loadtest/res/drawable-xhdpi/ic_launcher.png
new file mode 100644 (file)
index 0000000..7c02b78
Binary files /dev/null and b/cmds/statsd/tools/loadtest/res/drawable-xhdpi/ic_launcher.png differ
diff --git a/cmds/statsd/tools/loadtest/res/drawable-xxhdpi/ic_launcher.png b/cmds/statsd/tools/loadtest/res/drawable-xxhdpi/ic_launcher.png
new file mode 100644 (file)
index 0000000..915d914
Binary files /dev/null and b/cmds/statsd/tools/loadtest/res/drawable-xxhdpi/ic_launcher.png differ
diff --git a/cmds/statsd/tools/loadtest/res/layout/activity_loadtest.xml b/cmds/statsd/tools/loadtest/res/layout/activity_loadtest.xml
new file mode 100644 (file)
index 0000000..82964ab
--- /dev/null
@@ -0,0 +1,187 @@
+<?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>
diff --git a/cmds/statsd/tools/loadtest/res/raw/loadtest_config b/cmds/statsd/tools/loadtest/res/raw/loadtest_config
new file mode 100755 (executable)
index 0000000..7822367
Binary files /dev/null and b/cmds/statsd/tools/loadtest/res/raw/loadtest_config differ
diff --git a/cmds/statsd/tools/loadtest/res/values/integers.xml b/cmds/statsd/tools/loadtest/res/values/integers.xml
new file mode 100644 (file)
index 0000000..76b5692
--- /dev/null
@@ -0,0 +1,25 @@
+<?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>
diff --git a/cmds/statsd/tools/loadtest/res/values/strings.xml b/cmds/statsd/tools/loadtest/res/values/strings.xml
new file mode 100644 (file)
index 0000000..9913503
--- /dev/null
@@ -0,0 +1,32 @@
+<?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):&#160;</string>
+    <string name="burst_label">burst:&#160;</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):&#160;</string>
+    <string name="replication_label">metric replication:&#160;</string>
+    <string name="duration_label">test duration (mins):&#160;</string>
+    <string name="start"> &#160;Start&#160; </string>
+    <string name="stop"> &#160;Stop&#160; </string>
+
+</resources>
diff --git a/cmds/statsd/tools/loadtest/src/com/android/statsd/loadtest/BatteryStatsParser.java b/cmds/statsd/tools/loadtest/src/com/android/statsd/loadtest/BatteryStatsParser.java
new file mode 100644 (file)
index 0000000..96e6bef
--- /dev/null
@@ -0,0 +1,112 @@
+/*
+ * 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;
+    }
+}
diff --git a/cmds/statsd/tools/loadtest/src/com/android/statsd/loadtest/ConfigFactory.java b/cmds/statsd/tools/loadtest/src/com/android/statsd/loadtest/ConfigFactory.java
new file mode 100644 (file)
index 0000000..7ecc124
--- /dev/null
@@ -0,0 +1,301 @@
+/*
+ * 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);
+    }
+}
diff --git a/cmds/statsd/tools/loadtest/src/com/android/statsd/loadtest/DisplayProtoUtils.java b/cmds/statsd/tools/loadtest/src/com/android/statsd/loadtest/DisplayProtoUtils.java
new file mode 100644 (file)
index 0000000..5e8f26d
--- /dev/null
@@ -0,0 +1,247 @@
+/*
+ * 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!");
+    }
+}
diff --git a/cmds/statsd/tools/loadtest/src/com/android/statsd/loadtest/LoadtestActivity.java b/cmds/statsd/tools/loadtest/src/com/android/statsd/loadtest/LoadtestActivity.java
new file mode 100644 (file)
index 0000000..3ae85a7
--- /dev/null
@@ -0,0 +1,533 @@
+/*
+ * 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());
+            }
+        });
+    }
+}
diff --git a/cmds/statsd/tools/loadtest/src/com/android/statsd/loadtest/NumericalWatcher.java b/cmds/statsd/tools/loadtest/src/com/android/statsd/loadtest/NumericalWatcher.java
new file mode 100644 (file)
index 0000000..57f85b5
--- /dev/null
@@ -0,0 +1,88 @@
+/*
+ * 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;
+    }
+}
diff --git a/cmds/statsd/tools/loadtest/src/com/android/statsd/loadtest/PerfData.java b/cmds/statsd/tools/loadtest/src/com/android/statsd/loadtest/PerfData.java
new file mode 100644 (file)
index 0000000..e3e23f5
--- /dev/null
@@ -0,0 +1,162 @@
+/*
+ * 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;
+    }
+}
diff --git a/cmds/statsd/tools/loadtest/src/com/android/statsd/loadtest/SequencePusher.java b/cmds/statsd/tools/loadtest/src/com/android/statsd/loadtest/SequencePusher.java
new file mode 100644 (file)
index 0000000..d4b2aa4
--- /dev/null
@@ -0,0 +1,165 @@
+/*
+ * 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 */);
+        }
+    }
+}