aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRobert Wu <85952307+robertwu1@users.noreply.github.com>2023-08-16 08:42:10 -0700
committerGitHub <noreply@github.com>2023-08-16 08:42:10 -0700
commite94d769135fdddfc21d752b353f9d32f8ea79f3f (patch)
tree7cc1d68adc93c12d2e13a831c3112e90e1ad446c
parenta2a5e63047ed80befabf2e8fc0b3d06e300ae4ef (diff)
downloadoboe-e94d769135fdddfc21d752b353f9d32f8ea79f3f.tar.gz
OboeTester: Cold Start Latency Tests (#1887)
-rw-r--r--apps/OboeTester/app/src/main/AndroidManifest.xml5
-rw-r--r--apps/OboeTester/app/src/main/cpp/TestColdStartLatency.cpp118
-rw-r--r--apps/OboeTester/app/src/main/cpp/TestColdStartLatency.h76
-rw-r--r--apps/OboeTester/app/src/main/cpp/jni-bridge.cpp46
-rw-r--r--apps/OboeTester/app/src/main/java/com/mobileer/oboetester/ExtraTestsActivity.java4
-rw-r--r--apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestColdStartLatencyActivity.java210
-rw-r--r--apps/OboeTester/app/src/main/res/layout/activity_cold_start_latency.xml157
-rw-r--r--apps/OboeTester/app/src/main/res/layout/activity_extra_tests.xml10
-rw-r--r--apps/OboeTester/app/src/main/res/values/strings.xml20
9 files changed, 646 insertions, 0 deletions
diff --git a/apps/OboeTester/app/src/main/AndroidManifest.xml b/apps/OboeTester/app/src/main/AndroidManifest.xml
index 4c769014..fdfd8d95 100644
--- a/apps/OboeTester/app/src/main/AndroidManifest.xml
+++ b/apps/OboeTester/app/src/main/AndroidManifest.xml
@@ -115,6 +115,11 @@
android:label="@string/title_dynamic_load"
android:exported="true" />
+ <activity
+ android:name=".TestColdStartLatencyActivity"
+ android:label="@string/title_cold_start_latency"
+ android:exported="true" />
+
<service
android:name=".MidiTapTester"
android:permission="android.permission.BIND_MIDI_DEVICE_SERVICE"
diff --git a/apps/OboeTester/app/src/main/cpp/TestColdStartLatency.cpp b/apps/OboeTester/app/src/main/cpp/TestColdStartLatency.cpp
new file mode 100644
index 00000000..7de1c7ab
--- /dev/null
+++ b/apps/OboeTester/app/src/main/cpp/TestColdStartLatency.cpp
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2023 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.
+ */
+
+#include <stdlib.h>
+#include <aaudio/AAudioExtensions.h>
+
+#include "common/OboeDebug.h"
+#include "common/AudioClock.h"
+#include "TestColdStartLatency.h"
+#include "OboeTools.h"
+
+using namespace oboe;
+
+int32_t TestColdStartLatency::open(bool useInput, bool useLowLatency, bool useMmap, bool
+ useExclusive) {
+
+ mDataCallback = std::make_shared<MyDataCallback>();
+
+ // Enable MMAP if needed
+ bool wasMMapEnabled = AAudioExtensions::getInstance().isMMapEnabled();
+ AAudioExtensions::getInstance().setMMapEnabled(useMmap);
+
+ int64_t beginOpenNanos = AudioClock::getNanoseconds();
+
+ AudioStreamBuilder builder;
+ Result result = builder.setFormat(AudioFormat::Float)
+ ->setPerformanceMode(useLowLatency ? PerformanceMode::LowLatency :
+ PerformanceMode::None)
+ ->setDirection(useInput ? Direction::Input : Direction::Output)
+ ->setChannelCount(kChannelCount)
+ ->setDataCallback(mDataCallback)
+ ->setSharingMode(useExclusive ? SharingMode::Exclusive : SharingMode::Shared)
+ ->openStream(mStream);
+
+ int64_t endOpenNanos = AudioClock::getNanoseconds();
+ int64_t actualDurationNanos = endOpenNanos - beginOpenNanos;
+ mOpenTimeMicros = actualDurationNanos / NANOS_PER_MICROSECOND;
+
+ // Revert MMAP back to its previous state
+ AAudioExtensions::getInstance().setMMapEnabled(wasMMapEnabled);
+
+ mDeviceId = mStream->getDeviceId();
+
+ return (int32_t) result;
+}
+
+int32_t TestColdStartLatency::start() {
+ mBeginStartNanos = AudioClock::getNanoseconds();
+ Result result = mStream->requestStart();
+ int64_t endStartNanos = AudioClock::getNanoseconds();
+ int64_t actualDurationNanos = endStartNanos - mBeginStartNanos;
+ mStartTimeMicros = actualDurationNanos / NANOS_PER_MICROSECOND;
+ return (int32_t) result;
+}
+
+int32_t TestColdStartLatency::close() {
+ Result result1 = mStream->requestStop();
+ Result result2 = mStream->close();
+ return (int32_t)((result1 != Result::OK) ? result1 : result2);
+}
+
+int32_t TestColdStartLatency::getColdStartTimeMicros() {
+ int64_t position;
+ int64_t timestampNanos;
+ if (mStream->getDirection() == Direction::Output) {
+ auto result = mStream->getTimestamp(CLOCK_MONOTONIC);
+ if (!result) {
+ return -1; // ERROR
+ }
+ auto frameTimestamp = result.value();
+ // Calculate the time that frame[0] would have been played by the speaker.
+ position = frameTimestamp.position;
+ timestampNanos = frameTimestamp.timestamp;
+ } else {
+ position = mStream->getFramesRead();
+ timestampNanos = AudioClock::getNanoseconds();
+ }
+ double sampleRate = (double) mStream->getSampleRate();
+
+ int64_t elapsedNanos = NANOS_PER_SECOND * (position / sampleRate);
+ int64_t timeOfFrameZero = timestampNanos - elapsedNanos;
+ int64_t coldStartLatencyNanos = timeOfFrameZero - mBeginStartNanos;
+ return coldStartLatencyNanos / NANOS_PER_MICROSECOND;
+}
+
+// Callback that sleeps then touches the audio buffer.
+DataCallbackResult TestColdStartLatency::MyDataCallback::onAudioReady(
+ AudioStream *audioStream,
+ void *audioData,
+ int32_t numFrames) {
+ float *floatData = (float *) audioData;
+ const int numSamples = numFrames * kChannelCount;
+ if (audioStream->getDirection() == Direction::Output) {
+ // Fill mono buffer with a sine wave.
+ for (int i = 0; i < numSamples; i++) {
+ *floatData++ = sinf(mPhase) * 0.2f;
+ if ((i % kChannelCount) == (kChannelCount - 1)) {
+ mPhase += kPhaseIncrement;
+ // Wrap the phase around in a circle.
+ if (mPhase >= M_PI) mPhase -= 2 * M_PI;
+ }
+ }
+ }
+ return DataCallbackResult::Continue;
+}
diff --git a/apps/OboeTester/app/src/main/cpp/TestColdStartLatency.h b/apps/OboeTester/app/src/main/cpp/TestColdStartLatency.h
new file mode 100644
index 00000000..7c70687e
--- /dev/null
+++ b/apps/OboeTester/app/src/main/cpp/TestColdStartLatency.h
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2023 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.
+ */
+
+#ifndef OBOETESTER_TEST_COLD_START_LATENCY_H
+#define OBOETESTER_TEST_COLD_START_LATENCY_H
+
+#include "oboe/Oboe.h"
+#include <thread>
+
+/**
+ * Test for getting the cold start latency
+ */
+class TestColdStartLatency {
+public:
+
+ int32_t open(bool useInput, bool useLowLatency, bool useMmap, bool useExclusive);
+ int32_t start();
+ int32_t close();
+
+ int32_t getColdStartTimeMicros();
+
+ int32_t getOpenTimeMicros() {
+ return (int32_t) (mOpenTimeMicros.load());
+ }
+
+ int32_t getStartTimeMicros() {
+ return (int32_t) (mStartTimeMicros.load());
+ }
+
+ int32_t getDeviceId() {
+ return mDeviceId;
+ }
+
+protected:
+ std::atomic<int64_t> mBeginStartNanos{0};
+ std::atomic<double> mOpenTimeMicros{0};
+ std::atomic<double> mStartTimeMicros{0};
+ std::atomic<double> mColdStartTimeMicros{0};
+ std::atomic<int32_t> mDeviceId{0};
+
+private:
+
+ class MyDataCallback : public oboe::AudioStreamDataCallback { public:
+
+ MyDataCallback() {}
+
+ oboe::DataCallbackResult onAudioReady(
+ oboe::AudioStream *audioStream,
+ void *audioData,
+ int32_t numFrames) override;
+ private:
+ // For sine generator.
+ float mPhase = 0.0f;
+ static constexpr float kPhaseIncrement = 2.0f * (float) M_PI * 440.0f / 48000.0f;
+ };
+
+ std::shared_ptr<oboe::AudioStream> mStream;
+ std::shared_ptr<MyDataCallback> mDataCallback;
+
+ static constexpr int kChannelCount = 1;
+};
+
+#endif //OBOETESTER_TEST_COLD_START_LATENCY_H
diff --git a/apps/OboeTester/app/src/main/cpp/jni-bridge.cpp b/apps/OboeTester/app/src/main/cpp/jni-bridge.cpp
index 2907f947..7b9dfcf9 100644
--- a/apps/OboeTester/app/src/main/cpp/jni-bridge.cpp
+++ b/apps/OboeTester/app/src/main/cpp/jni-bridge.cpp
@@ -28,6 +28,7 @@
#include "oboe/Oboe.h"
#include "NativeAudioContext.h"
+#include "TestColdStartLatency.h"
#include "TestErrorCallback.h"
#include "TestRoutingCrash.h"
@@ -885,4 +886,49 @@ Java_com_mobileer_oboetester_TestRouteDuringCallbackActivity_getSleepTimeMicros(
return sRoutingCrash.getSleepTimeMicros();
}
+static TestColdStartLatency sColdStartLatency;
+
+JNIEXPORT jint JNICALL
+Java_com_mobileer_oboetester_TestColdStartLatencyActivity_openStream(
+ JNIEnv *env, jobject instance,
+ jboolean useInput, jboolean useLowLatency, jboolean useMmap, jboolean useExclusive) {
+ return sColdStartLatency.open(useInput, useLowLatency, useMmap, useExclusive);
+}
+
+JNIEXPORT jint JNICALL
+Java_com_mobileer_oboetester_TestColdStartLatencyActivity_startStream(
+ JNIEnv *env, jobject instance) {
+ return sColdStartLatency.start();
+}
+
+JNIEXPORT jint JNICALL
+Java_com_mobileer_oboetester_TestColdStartLatencyActivity_closeStream(
+ JNIEnv *env, jobject instance) {
+ return sColdStartLatency.close();
+}
+
+JNIEXPORT jint JNICALL
+Java_com_mobileer_oboetester_TestColdStartLatencyActivity_getOpenTimeMicros(
+ JNIEnv *env, jobject instance) {
+ return sColdStartLatency.getOpenTimeMicros();
+}
+
+JNIEXPORT jint JNICALL
+Java_com_mobileer_oboetester_TestColdStartLatencyActivity_getStartTimeMicros(
+ JNIEnv *env, jobject instance) {
+ return sColdStartLatency.getStartTimeMicros();
+}
+
+JNIEXPORT jint JNICALL
+Java_com_mobileer_oboetester_TestColdStartLatencyActivity_getColdStartTimeMicros(
+ JNIEnv *env, jobject instance) {
+ return sColdStartLatency.getColdStartTimeMicros();
+}
+
+JNIEXPORT jint JNICALL
+Java_com_mobileer_oboetester_TestColdStartLatencyActivity_getDeviceId(
+ JNIEnv *env, jobject instance) {
+ return sColdStartLatency.getDeviceId();
+}
+
}
diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/ExtraTestsActivity.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/ExtraTestsActivity.java
index 6f52bfca..c744329b 100644
--- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/ExtraTestsActivity.java
+++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/ExtraTestsActivity.java
@@ -36,4 +36,8 @@ public class ExtraTestsActivity extends BaseOboeTesterActivity {
public void onLaunchDynamicWorkloadTest(View view) {
launchTestActivity(DynamicWorkloadActivity.class);
}
+
+ public void onLaunchColdStartLatencyTest(View view) {
+ launchTestActivity(TestColdStartLatencyActivity.class);
+ }
}
diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestColdStartLatencyActivity.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestColdStartLatencyActivity.java
new file mode 100644
index 00000000..b16bcd4a
--- /dev/null
+++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestColdStartLatencyActivity.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright 2023 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.mobileer.oboetester;
+
+import static com.mobileer.oboetester.TestAudioActivity.TAG;
+
+import android.app.Activity;
+import android.content.Context;
+import android.media.AudioManager;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.View;
+import android.view.WindowManager;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.RadioButton;
+import android.widget.Spinner;
+import android.widget.TextView;
+
+import java.util.Random;
+
+/**
+ * Test for getting the cold start latency
+ */
+public class TestColdStartLatencyActivity extends Activity {
+
+ private TextView mStatusView;
+ private MyStreamSniffer mStreamSniffer;
+ private AudioManager mAudioManager;
+ private RadioButton mOutputButton;
+ private RadioButton mInputButton;
+ private CheckBox mLowLatencyCheckBox;
+ private CheckBox mMmapCheckBox;
+ private CheckBox mExclusiveCheckBox;
+ private Spinner mStartStabilizeDelaySpinner;
+ private Spinner mCloseOpenDelaySpinner;
+ private Spinner mOpenStartDelaySpinner;
+ private Button mStartButton;
+ private Button mStopButton;
+
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_cold_start_latency);
+ mStatusView = (TextView) findViewById(R.id.text_status);
+ mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
+
+ mStartButton = (Button) findViewById(R.id.button_start_test);
+ mStopButton = (Button) findViewById(R.id.button_stop_test);
+ mOutputButton = (RadioButton) findViewById(R.id.direction_output);
+ mInputButton = (RadioButton) findViewById(R.id.direction_input);
+ mMmapCheckBox = (CheckBox) findViewById(R.id.checkbox_mmap);
+ mExclusiveCheckBox = (CheckBox) findViewById(R.id.checkbox_exclusive);
+ mLowLatencyCheckBox = (CheckBox) findViewById(R.id.checkbox_low_latency);
+ mStartStabilizeDelaySpinner = (Spinner) findViewById(R.id.spinner_start_stabilize_time);
+ mStartStabilizeDelaySpinner.setSelection(7); // Set to 1000 ms by default
+ mCloseOpenDelaySpinner = (Spinner) findViewById(R.id.spinner_close_open_time);
+ mOpenStartDelaySpinner = (Spinner) findViewById(R.id.spinner_open_start_time);
+
+ setButtonsEnabled(false);
+ }
+
+ public void onStartColdStartLatencyTest(View view) {
+ keepScreenOn(true);
+ stopSniffer();
+ mStreamSniffer = new MyStreamSniffer();
+ mStreamSniffer.start();
+ setButtonsEnabled(true);
+ }
+
+ public void onStopColdStartLatencyTest(View view) {
+ keepScreenOn(false);
+ stopSniffer();
+ setButtonsEnabled(false);
+ }
+
+ private void setButtonsEnabled(boolean running) {
+ mStartButton.setEnabled(!running);
+ mStopButton.setEnabled(running);
+ mOutputButton.setEnabled(!running);
+ mInputButton.setEnabled(!running);
+ mLowLatencyCheckBox.setEnabled(!running);
+ mMmapCheckBox.setEnabled(!running);
+ mExclusiveCheckBox.setEnabled(!running);
+ mStartStabilizeDelaySpinner.setEnabled(!running);
+ mCloseOpenDelaySpinner.setEnabled(!running);
+ mOpenStartDelaySpinner.setEnabled(!running);
+ }
+
+ protected class MyStreamSniffer extends Thread {
+ boolean enabled = true;
+ StringBuffer statusBuffer = new StringBuffer();
+ int loopCount;
+
+ @Override
+ public void run() {
+ boolean useInput = mInputButton.isChecked();
+ boolean useLowLatency = mLowLatencyCheckBox.isChecked();
+ boolean useMmap = mMmapCheckBox.isChecked();
+ boolean useExclusive = mExclusiveCheckBox.isChecked();
+ Log.d(TAG,(useInput ? "IN" : "OUT")
+ + ", " + (useLowLatency ? "LOW_LATENCY" : "NOT LOW_LATENCY")
+ + ", " + (useMmap ? "MMAP" : "NOT MMAP")
+ + ", " + (useExclusive ? "EXCLUSIVE" : "SHARED"));
+ String closeSleepTimeText =
+ (String) mCloseOpenDelaySpinner.getAdapter().getItem(
+ mCloseOpenDelaySpinner.getSelectedItemPosition());
+ int closedSleepTimeMillis = Integer.parseInt(closeSleepTimeText);
+ Log.d(TAG, "Sleep before open time = " + closedSleepTimeMillis + " msec");
+ String openSleepTimeText = (String) mOpenStartDelaySpinner.getAdapter().getItem(
+ mOpenStartDelaySpinner.getSelectedItemPosition());
+ int openSleepTimeMillis = Integer.parseInt(openSleepTimeText);
+ Log.d(TAG, "Sleep after open Time = " + openSleepTimeMillis + " msec");
+ String startStabilizeTimeText = (String) mStartStabilizeDelaySpinner.getAdapter().getItem(
+ mStartStabilizeDelaySpinner.getSelectedItemPosition());
+ int startSleepTimeMillis = Integer.parseInt(startStabilizeTimeText);
+ Log.d(TAG, "Sleep after start Time = " + startSleepTimeMillis + " msec");
+ while (enabled) {
+ loopCount++;
+ try {
+ sleep(closedSleepTimeMillis);
+ openStream(useInput, useLowLatency, useMmap, useExclusive);
+ log("-------#" + loopCount + " Device Id: " + getDeviceId());
+ log("open() Latency: " + getOpenTimeMicros() / 1000 + " msec");
+ sleep(openSleepTimeMillis);
+ startStream();
+ log("requestStart() Latency: " + getStartTimeMicros() / 1000 + " msec");
+ sleep(startSleepTimeMillis);
+ log("Cold Start Latency: " + getColdStartTimeMicros() / 1000 + " msec");
+ closeStream();
+ } catch (InterruptedException e) {
+ enabled = false;
+ } finally {
+ closeStream();
+ }
+ }
+ }
+
+ // Log to screen and logcat.
+ private void log(String text) {
+ statusBuffer.append(text + "\n");
+ showStatus(statusBuffer.toString());
+ }
+
+ // Stop the test thread.
+ void finish() {
+ enabled = false;
+ interrupt();
+ try {
+ join(2000);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ protected void showStatus(final String message) {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mStatusView.setText(message);
+ }
+ });
+ }
+
+ private native int openStream(boolean useInput, boolean useLowLatency, boolean useMmap,
+ boolean useExclusive);
+ private native int startStream();
+ private native int closeStream();
+ private native int getOpenTimeMicros();
+ private native int getStartTimeMicros();
+ private native int getColdStartTimeMicros();
+ private native int getDeviceId();
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ stopSniffer();
+ }
+
+ private void stopSniffer() {
+ if (mStreamSniffer != null) {
+ mStreamSniffer.finish();
+ mStreamSniffer = null;
+ }
+ }
+
+ protected void keepScreenOn(boolean on) {
+ if (on) {
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ } else {
+ getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ }
+ }
+}
diff --git a/apps/OboeTester/app/src/main/res/layout/activity_cold_start_latency.xml b/apps/OboeTester/app/src/main/res/layout/activity_cold_start_latency.xml
new file mode 100644
index 00000000..b4798e45
--- /dev/null
+++ b/apps/OboeTester/app/src/main/res/layout/activity_cold_start_latency.xml
@@ -0,0 +1,157 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:context=".TestColdStartLatencyActivity">
+
+ <LinearLayout
+ android:id="@+id/buttonGrid"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <RadioGroup
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:checkedButton="@+id/direction_output"
+ android:orientation="horizontal">
+
+ <RadioButton
+ android:id="@+id/direction_output"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Output" />
+
+ <RadioButton
+ android:id="@+id/direction_input"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Input" />
+ </RadioGroup>
+
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <CheckBox
+ android:id="@+id/checkbox_mmap"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginRight="8sp"
+ android:text="MMAP"
+ android:checked="true" />
+
+ <CheckBox
+ android:id="@+id/checkbox_low_latency"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginRight="8sp"
+ android:text="LOW_LATENCY"
+ android:checked="true" />
+
+ <CheckBox
+ android:id="@+id/checkbox_exclusive"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginRight="8sp"
+ android:text="EXCLUSIVE"
+ android:checked="true" />
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/start_stabilize_prompt" />
+
+ <Spinner
+ android:id="@+id/spinner_start_stabilize_time"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:entries="@array/sleep_times" />
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/close_open_prompt" />
+
+ <Spinner
+ android:id="@+id/spinner_close_open_time"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:entries="@array/sleep_times" />
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/open_start_prompt" />
+
+ <Spinner
+ android:id="@+id/spinner_open_start_time"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:entries="@array/sleep_times" />
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <Button
+ android:id="@+id/button_start_test"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:backgroundTint="@color/button_tint"
+ android:onClick="onStartColdStartLatencyTest"
+ android:text="Start Test" />
+ <Button
+ android:id="@+id/button_stop_test"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:backgroundTint="@color/button_tint"
+ android:onClick="onStopColdStartLatencyTest"
+ android:text="Stop Test" />
+ </LinearLayout>
+
+ <ScrollView
+ android:id="@+id/text_log_scroller"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+ <TextView
+ android:id="@+id/text_status"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:fontFamily="monospace"
+ android:gravity="bottom"
+ android:scrollbars="vertical"
+ android:text="@string/init_status" />
+ </ScrollView>
+
+ </LinearLayout>
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/apps/OboeTester/app/src/main/res/layout/activity_extra_tests.xml b/apps/OboeTester/app/src/main/res/layout/activity_extra_tests.xml
index 37f587f6..347db970 100644
--- a/apps/OboeTester/app/src/main/res/layout/activity_extra_tests.xml
+++ b/apps/OboeTester/app/src/main/res/layout/activity_extra_tests.xml
@@ -71,5 +71,15 @@
android:backgroundTint="@color/button_tint"
android:onClick="onLaunchDynamicWorkloadTest"
android:text="CPU Load" />
+
+ <Button
+ android:id="@+id/buttonColdStartLatency"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_columnWeight="1"
+ android:layout_gravity="fill"
+ android:backgroundTint="@color/button_tint"
+ android:onClick="onLaunchColdStartLatencyTest"
+ android:text="Cold Start Latency" />
</GridLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/apps/OboeTester/app/src/main/res/values/strings.xml b/apps/OboeTester/app/src/main/res/values/strings.xml
index f1ef155a..ad708b77 100644
--- a/apps/OboeTester/app/src/main/res/values/strings.xml
+++ b/apps/OboeTester/app/src/main/res/values/strings.xml
@@ -227,6 +227,7 @@
<string name="title_error_callback">Error Callback Test</string>
<string name="title_route_during_callback">Route Callback Test</string>
<string name="title_dynamic_load">Dynamic CPU Load</string>
+ <string name="title_cold_start_latency">Cold Start Latency</string>
<string name="need_record_audio_permission">"This app needs RECORD_AUDIO permission"</string>
<string name="share">Share</string>
@@ -270,4 +271,23 @@
<string name="channel_mask_prompt">ChannelMask:</string>
+ <string name="close_open_prompt">, Sleep before open (ms)</string>
+ <string name="open_start_prompt">, Sleep between open and start (ms)</string>
+ <string name="start_stabilize_prompt">, Sleep for start to stabilize (ms)</string>
+ <string-array name="sleep_times">
+ <item>0</item>
+ <item>4</item>
+ <item>10</item>
+ <item>20</item>
+ <item>30</item>
+ <item>100</item>
+ <item>300</item>
+ <item>1000</item>
+ <item>2000</item>
+ <item>5000</item>
+ <item>10000</item>
+ <item>20000</item>
+ <item>50000</item>
+ </string-array>
+
</resources>