diff options
| author | 2017-12-03 19:47:25 +0000 | |
|---|---|---|
| committer | 2017-12-03 19:47:25 +0000 | |
| commit | e1e892d4a0a0d686871dd3f50d26505a9694bc61 (patch) | |
| tree | 42da21201ea0e8ca253138e6fb78a4d8de13e582 | |
| parent | f156c04912d9b50e9d735daae9ea209721eaa371 (diff) | |
| parent | bc7a04b88db83b6bb91c2c50a4cd8b6bafabac06 (diff) | |
Merge "Create statsd loadtest app."
17 files changed, 1930 insertions, 0 deletions
diff --git a/cmds/statsd/tools/loadtest/Android.mk b/cmds/statsd/tools/loadtest/Android.mk new file mode 100644 index 000000000000..f3f0a7c2128f --- /dev/null +++ b/cmds/statsd/tools/loadtest/Android.mk @@ -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 index 000000000000..d74c707a011f --- /dev/null +++ b/cmds/statsd/tools/loadtest/AndroidManifest.xml @@ -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 Binary files differnew file mode 100644 index 000000000000..55621cc1074f --- /dev/null +++ b/cmds/statsd/tools/loadtest/res/drawable-hdpi/ic_launcher.png diff --git a/cmds/statsd/tools/loadtest/res/drawable-mdpi/ic_launcher.png b/cmds/statsd/tools/loadtest/res/drawable-mdpi/ic_launcher.png Binary files differnew file mode 100644 index 000000000000..11ec2068be19 --- /dev/null +++ b/cmds/statsd/tools/loadtest/res/drawable-mdpi/ic_launcher.png diff --git a/cmds/statsd/tools/loadtest/res/drawable-xhdpi/ic_launcher.png b/cmds/statsd/tools/loadtest/res/drawable-xhdpi/ic_launcher.png Binary files differnew file mode 100644 index 000000000000..7c02b784aa5d --- /dev/null +++ b/cmds/statsd/tools/loadtest/res/drawable-xhdpi/ic_launcher.png diff --git a/cmds/statsd/tools/loadtest/res/drawable-xxhdpi/ic_launcher.png b/cmds/statsd/tools/loadtest/res/drawable-xxhdpi/ic_launcher.png Binary files differnew file mode 100644 index 000000000000..915d91441349 --- /dev/null +++ b/cmds/statsd/tools/loadtest/res/drawable-xxhdpi/ic_launcher.png 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 index 000000000000..82964ab1d821 --- /dev/null +++ b/cmds/statsd/tools/loadtest/res/layout/activity_loadtest.xml @@ -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 Binary files differnew file mode 100755 index 000000000000..78223674285d --- /dev/null +++ b/cmds/statsd/tools/loadtest/res/raw/loadtest_config diff --git a/cmds/statsd/tools/loadtest/res/values/integers.xml b/cmds/statsd/tools/loadtest/res/values/integers.xml new file mode 100644 index 000000000000..76b56923bd84 --- /dev/null +++ b/cmds/statsd/tools/loadtest/res/values/integers.xml @@ -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 index 000000000000..991350332da0 --- /dev/null +++ b/cmds/statsd/tools/loadtest/res/values/strings.xml @@ -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): </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> 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 index 000000000000..96e6bef600d1 --- /dev/null +++ b/cmds/statsd/tools/loadtest/src/com/android/statsd/loadtest/BatteryStatsParser.java @@ -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 index 000000000000..7ecc124e5426 --- /dev/null +++ b/cmds/statsd/tools/loadtest/src/com/android/statsd/loadtest/ConfigFactory.java @@ -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 index 000000000000..5e8f26d5adbc --- /dev/null +++ b/cmds/statsd/tools/loadtest/src/com/android/statsd/loadtest/DisplayProtoUtils.java @@ -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 index 000000000000..3ae85a7d0bf2 --- /dev/null +++ b/cmds/statsd/tools/loadtest/src/com/android/statsd/loadtest/LoadtestActivity.java @@ -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 index 000000000000..57f85b5db317 --- /dev/null +++ b/cmds/statsd/tools/loadtest/src/com/android/statsd/loadtest/NumericalWatcher.java @@ -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 index 000000000000..e3e23f58a26f --- /dev/null +++ b/cmds/statsd/tools/loadtest/src/com/android/statsd/loadtest/PerfData.java @@ -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 index 000000000000..d4b2aa4ff241 --- /dev/null +++ b/cmds/statsd/tools/loadtest/src/com/android/statsd/loadtest/SequencePusher.java @@ -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 */); + } + } +} |