Extract NN API Benchmarks into an app that can easily be run by dogfooders. am: f7302c14e6
am: 6ee2a2b58c

Change-Id: Ife8df985f2c254caa145dbc79eaa029c3bc03b0d
diff --git a/dogfood/Android.mk b/dogfood/Android.mk
new file mode 100644
index 0000000..b7fef4d
--- /dev/null
+++ b/dogfood/Android.mk
@@ -0,0 +1,47 @@
+#
+# Copyright (C) 2019 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_STATIC_JAVA_LIBRARIES := android-common androidx.test.rules androidx.appcompat_appcompat androidx-constraintlayout_constraintlayout
+LOCAL_JAVA_LIBRARIES := android.test.runner.stubs android.test.base.stubs
+
+LOCAL_MODULE_TAGS := tests
+LOCAL_COMPATIBILITY_SUITE += device-tests
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src) \
+    $(call all-java-files-under, ../src/com/android/nn/benchmark/core) \
+    $(call all-java-files-under, ../src/com/android/nn/benchmark/evaluators) \
+    $(call all-java-files-under, ../src/com/android/nn/benchmark/imageprocessors) \
+    $(call all-java-files-under, ../src/com/android/nn/benchmark/util)
+LOCAL_JNI_SHARED_LIBRARIES := libnnbenchmark_jni
+
+LOCAL_SDK_VERSION := 27
+LOCAL_ASSET_DIR := $(LOCAL_PATH)/../../models/assets
+
+GOOGLE_TEST_MODELS_DIR := vendor/google/tests/mlts/models/assets
+ifneq ($(wildcard $(GOOGLE_TEST_MODELS_DIR)),)
+LOCAL_ASSET_DIR += $(GOOGLE_TEST_MODELS_DIR)
+endif
+
+LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res $(LOCAL_PATH)/../res
+
+LOCAL_PACKAGE_NAME := NeuralNetworksApiDogfood
+include $(BUILD_PACKAGE)
+
+include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/dogfood/AndroidManifest.xml b/dogfood/AndroidManifest.xml
new file mode 100644
index 0000000..5573e52
--- /dev/null
+++ b/dogfood/AndroidManifest.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+	  package="com.android.nn.dogfood"
+	  android:versionCode="2"
+          android:versionName="2">
+
+  <application
+      android:allowBackup="true"
+      android:icon="@mipmap/ic_launcher"
+      android:label="@string/app_name"
+      android:roundIcon="@mipmap/ic_launcher_round"
+      android:supportsRtl="true"
+      android:theme="@style/AppTheme">
+    <activity android:name=".MainActivity">
+      <intent-filter>
+	<action android:name="android.intent.action.MAIN" />
+
+	<category android:name="android.intent.category.LAUNCHER" />
+      </intent-filter>
+    </activity>
+
+    <service
+	android:name=".BenchmarkJobService"
+	android:label="Benchmark Service"
+	android:permission="android.permission.BIND_JOB_SERVICE" >
+
+    </service>
+  </application>
+
+</manifest>
diff --git a/dogfood/res/drawable-v24/ic_launcher_foreground.xml b/dogfood/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 0000000..1f6bb29
--- /dev/null
+++ b/dogfood/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,34 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:aapt="http://schemas.android.com/aapt"
+    android:width="108dp"
+    android:height="108dp"
+    android:viewportWidth="108"
+    android:viewportHeight="108">
+    <path
+        android:fillType="evenOdd"
+        android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
+        android:strokeWidth="1"
+        android:strokeColor="#00000000">
+        <aapt:attr name="android:fillColor">
+            <gradient
+                android:endX="78.5885"
+                android:endY="90.9159"
+                android:startX="48.7653"
+                android:startY="61.0927"
+                android:type="linear">
+                <item
+                    android:color="#44000000"
+                    android:offset="0.0" />
+                <item
+                    android:color="#00000000"
+                    android:offset="1.0" />
+            </gradient>
+        </aapt:attr>
+    </path>
+    <path
+        android:fillColor="#FFFFFF"
+        android:fillType="nonZero"
+        android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
+        android:strokeWidth="1"
+        android:strokeColor="#00000000" />
+</vector>
diff --git a/dogfood/res/drawable/ic_launcher_background.xml b/dogfood/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..0d025f9
--- /dev/null
+++ b/dogfood/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="108dp"
+    android:height="108dp"
+    android:viewportWidth="108"
+    android:viewportHeight="108">
+    <path
+        android:fillColor="#008577"
+        android:pathData="M0,0h108v108h-108z" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M9,0L9,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,0L19,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M29,0L29,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M39,0L39,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M49,0L49,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M59,0L59,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M69,0L69,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M79,0L79,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M89,0L89,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M99,0L99,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,9L108,9"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,19L108,19"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,29L108,29"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,39L108,39"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,49L108,49"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,59L108,59"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,69L108,69"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,79L108,79"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,89L108,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,99L108,99"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,29L89,29"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,39L89,39"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,49L89,49"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,59L89,59"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,69L89,69"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,79L89,79"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M29,19L29,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M39,19L39,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M49,19L49,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M59,19L59,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M69,19L69,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M79,19L79,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+</vector>
diff --git a/dogfood/res/layout/activity_main.xml b/dogfood/res/layout/activity_main.xml
new file mode 100644
index 0000000..afc57af
--- /dev/null
+++ b/dogfood/res/layout/activity_main.xml
@@ -0,0 +1,31 @@
+<?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=".dogfood.MainActivity">
+  
+  <Button
+      android:id="@+id/start_stop_button"
+      android:layout_width="wrap_content"
+      android:layout_height="wrap_content"
+      android:layout_marginTop="36dp"
+      android:layout_marginLeft="36dp"
+      android:onClick="startStopTestClicked"
+      android:text="Start NN API Test"
+      app:layout_constraintStart_toStartOf="parent"
+      app:layout_constraintTop_toTopOf="parent"
+      />
+
+  <TextView
+      android:id="@+id/message"
+      android:layout_width="wrap_content"
+      android:layout_height="wrap_content"
+      android:layout_marginStart="8dp"
+      android:layout_marginTop="16dp"
+      android:text=""
+      app:layout_constraintStart_toStartOf="@+id/start_button"
+              app:layout_constraintTop_toBottomOf="@+id/start_stop_button" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/dogfood/res/mipmap-anydpi-v26/ic_launcher.xml b/dogfood/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..eca70cf
--- /dev/null
+++ b/dogfood/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@drawable/ic_launcher_background" />
+    <foreground android:drawable="@drawable/ic_launcher_foreground" />
+</adaptive-icon>
\ No newline at end of file
diff --git a/dogfood/res/mipmap-anydpi-v26/ic_launcher_round.xml b/dogfood/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..eca70cf
--- /dev/null
+++ b/dogfood/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@drawable/ic_launcher_background" />
+    <foreground android:drawable="@drawable/ic_launcher_foreground" />
+</adaptive-icon>
\ No newline at end of file
diff --git a/dogfood/res/mipmap-hdpi/ic_launcher.png b/dogfood/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..898f3ed
--- /dev/null
+++ b/dogfood/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/dogfood/res/mipmap-hdpi/ic_launcher_round.png b/dogfood/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000..dffca36
--- /dev/null
+++ b/dogfood/res/mipmap-hdpi/ic_launcher_round.png
Binary files differ
diff --git a/dogfood/res/mipmap-mdpi/ic_launcher.png b/dogfood/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..64ba76f
--- /dev/null
+++ b/dogfood/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/dogfood/res/mipmap-mdpi/ic_launcher_round.png b/dogfood/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 0000000..dae5e08
--- /dev/null
+++ b/dogfood/res/mipmap-mdpi/ic_launcher_round.png
Binary files differ
diff --git a/dogfood/res/mipmap-xhdpi/ic_launcher.png b/dogfood/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..e5ed465
--- /dev/null
+++ b/dogfood/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/dogfood/res/mipmap-xhdpi/ic_launcher_round.png b/dogfood/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..14ed0af
--- /dev/null
+++ b/dogfood/res/mipmap-xhdpi/ic_launcher_round.png
Binary files differ
diff --git a/dogfood/res/mipmap-xxhdpi/ic_launcher.png b/dogfood/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..b0907ca
--- /dev/null
+++ b/dogfood/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/dogfood/res/mipmap-xxhdpi/ic_launcher_round.png b/dogfood/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..d8ae031
--- /dev/null
+++ b/dogfood/res/mipmap-xxhdpi/ic_launcher_round.png
Binary files differ
diff --git a/dogfood/res/mipmap-xxxhdpi/ic_launcher.png b/dogfood/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..2c18de9
--- /dev/null
+++ b/dogfood/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/dogfood/res/mipmap-xxxhdpi/ic_launcher_round.png b/dogfood/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..beed3cd
--- /dev/null
+++ b/dogfood/res/mipmap-xxxhdpi/ic_launcher_round.png
Binary files differ
diff --git a/dogfood/res/values/colors.xml b/dogfood/res/values/colors.xml
new file mode 100644
index 0000000..69b2233
--- /dev/null
+++ b/dogfood/res/values/colors.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <color name="colorPrimary">#008577</color>
+    <color name="colorPrimaryDark">#00574B</color>
+    <color name="colorAccent">#D81B60</color>
+</resources>
diff --git a/dogfood/res/values/strings.xml b/dogfood/res/values/strings.xml
new file mode 100644
index 0000000..729c261
--- /dev/null
+++ b/dogfood/res/values/strings.xml
@@ -0,0 +1,5 @@
+<resources>
+    <string name="app_name">NN API Dogfood</string>
+    <string name="start_button_text">Start test</string>
+    <string name="stop_button_text">Stop test</string>
+</resources>
diff --git a/dogfood/res/values/styles.xml b/dogfood/res/values/styles.xml
new file mode 100644
index 0000000..5885930
--- /dev/null
+++ b/dogfood/res/values/styles.xml
@@ -0,0 +1,11 @@
+<resources>
+
+    <!-- Base application theme. -->
+    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
+        <!-- Customize your theme here. -->
+        <item name="colorPrimary">@color/colorPrimary</item>
+        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
+        <item name="colorAccent">@color/colorAccent</item>
+    </style>
+
+</resources>
diff --git a/dogfood/src/com/android/nn/dogfood/BenchmarkJobService.java b/dogfood/src/com/android/nn/dogfood/BenchmarkJobService.java
new file mode 100644
index 0000000..222f8be
--- /dev/null
+++ b/dogfood/src/com/android/nn/dogfood/BenchmarkJobService.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2019 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.nn.dogfood;
+
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.job.JobParameters;
+import android.app.job.JobScheduler;
+import android.app.job.JobService;
+import android.content.SharedPreferences;
+import android.util.Log;
+
+import androidx.core.app.NotificationCompat;
+import androidx.core.app.NotificationManagerCompat;
+
+import com.android.nn.benchmark.core.BenchmarkResult;
+import com.android.nn.benchmark.core.NNTestBase;
+import com.android.nn.benchmark.core.Processor;
+import com.android.nn.benchmark.core.TestModels;
+
+import java.util.List;
+import java.util.Random;
+
+/** Regularly runs a random selection of the NN API benchmark models */
+public class BenchmarkJobService extends JobService implements Processor.Callback {
+
+    private static final String TAG = "NN_BENCHMARK";
+    private static final String CHANNEL_ID = "default";
+    private static final int NOTIFICATION_ID = 999;
+    public static final int JOB_ID = 1;
+    private NotificationManagerCompat mNotificationManager;
+    private NotificationCompat.Builder mNotification;
+    private boolean mJobStopped = false;
+    private static final int NUM_RUNS = 10;
+    private Processor mProcessor;
+    private JobParameters mJobParameters;
+    private static final String NN_API_DOGFOOD_PREF = "nn_api_dogfood";
+
+    private static int DOGFOOD_MODELS_PER_RUN = 20;
+    private BenchmarkResult mTestResults[];
+
+
+    @Override
+    public boolean onStartJob(JobParameters jobParameters) {
+        mJobParameters = jobParameters;
+        incrementNumRuns();
+        Log.d(TAG, String.format("NN API Benchmarking job %d/%d started", getNumRuns(), NUM_RUNS));
+        showNotification();
+        doBenchmark();
+
+        return true;
+    }
+
+    @Override
+    public boolean onStopJob(JobParameters jobParameters) {
+        Log.d(TAG, String.format("NN API Benchmarking job %d/%d stopped", getNumRuns(), NUM_RUNS));
+        mJobStopped = true;
+
+        return false;
+    }
+
+    public void doBenchmark() {
+
+        mProcessor = new Processor(this, this, randomModelList());
+        mProcessor.setUseNNApi(true);
+        mProcessor.setToggleLong(true);
+        mProcessor.start();
+    }
+
+    public void onBenchmarkFinish(boolean ok) {
+        mProcessor.exit();
+        if (getNumRuns() >= NUM_RUNS) {
+            mNotification
+                    .setProgress(0, 0, false)
+                    .setContentText(
+                            "Benchmarking done! please upload a bug report via BetterBug under"
+                                + " Android > Android OS & > Apps Runtime > Machine Learning")
+                    .setOngoing(false);
+            mNotificationManager.notify(NOTIFICATION_ID, mNotification.build());
+            JobScheduler jobScheduler = getSystemService(JobScheduler.class);
+            jobScheduler.cancel(JOB_ID);
+            resetNumRuns();
+        } else {
+            mNotification
+                    .setProgress(0, 0, false)
+                    .setContentText(
+                            String.format(
+                                    "Background test %d of %d is complete", getNumRuns(), NUM_RUNS))
+                    .setOngoing(false);
+            mNotificationManager.notify(NOTIFICATION_ID, mNotification.build());
+        }
+
+        Log.d(TAG, "NN API Benchmarking job finished");
+        jobFinished(mJobParameters, false);
+    }
+
+    public void onStatusUpdate(int testNumber, int numTests, String modelName) {
+        Log.d(
+                TAG,
+                String.format("Benchmark progress %d of %d - %s", testNumber, numTests, modelName));
+        mNotification.setProgress(numTests, testNumber, false);
+        mNotificationManager.notify(NOTIFICATION_ID, mNotification.build());
+    }
+
+    private void showNotification() {
+        mNotificationManager = NotificationManagerCompat.from(this);
+        NotificationChannel channel =
+                new NotificationChannel(CHANNEL_ID, "Default", NotificationManager.IMPORTANCE_LOW);
+        // mNotificationManager.createNotificationChannel(channel);
+        mNotificationManager = NotificationManagerCompat.from(this);
+        String title = "NN API Dogfood";
+        String msg = String.format("Background test %d of %d is running", getNumRuns(), NUM_RUNS);
+
+        mNotification =
+                new NotificationCompat.Builder(this, CHANNEL_ID)
+                        .setSmallIcon(R.mipmap.ic_launcher)
+                        .setContentTitle(title)
+                        .setContentText("NN API Benchmarking Job")
+                        .setPriority(NotificationCompat.PRIORITY_MAX)
+                        .setOngoing(true);
+        mNotificationManager.notify(NOTIFICATION_ID, mNotification.build());
+    }
+
+    private int[] randomModelList() {
+        long seed = System.currentTimeMillis();
+        List<TestModels.TestModelEntry> testList = TestModels.modelsList();
+
+        Log.v(TAG, "Dogfood run seed " + seed);
+        Random random = new Random(seed);
+        int numModelsToSelect = Math.min(DOGFOOD_MODELS_PER_RUN, testList.size());
+        int[] randomModelIndices = new int[numModelsToSelect];
+
+        for (int i = 0; i < numModelsToSelect; i++) {
+            randomModelIndices[i] = random.nextInt(testList.size());
+        }
+
+        return randomModelIndices;
+    }
+
+    private void incrementNumRuns() {
+        SharedPreferences.Editor editor =
+                getSharedPreferences(NN_API_DOGFOOD_PREF, MODE_PRIVATE).edit();
+        editor.putInt("num_runs", getNumRuns() + 1);
+        editor.apply();
+    }
+
+    private void resetNumRuns() {
+        SharedPreferences.Editor editor =
+                getSharedPreferences(NN_API_DOGFOOD_PREF, MODE_PRIVATE).edit();
+        editor.putInt("num_runs", 0);
+        editor.apply();
+    }
+
+    private int getNumRuns() {
+        SharedPreferences prefs = getSharedPreferences(NN_API_DOGFOOD_PREF, MODE_PRIVATE);
+        return prefs.getInt("num_runs", 0);
+    }
+}
diff --git a/dogfood/src/com/android/nn/dogfood/MainActivity.java b/dogfood/src/com/android/nn/dogfood/MainActivity.java
new file mode 100644
index 0000000..0c259be
--- /dev/null
+++ b/dogfood/src/com/android/nn/dogfood/MainActivity.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2019 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.nn.dogfood;
+
+import android.app.job.JobInfo;
+import android.app.job.JobScheduler;
+import android.content.ComponentName;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.View;
+import android.widget.Button;
+import android.widget.TextView;
+
+import androidx.appcompat.app.AppCompatActivity;
+
+import com.android.nn.benchmark.core.TestModelsListLoader;
+import com.android.nn.benchmark.util.TestExternalStorageActivity;
+
+import java.io.IOException;
+
+public class MainActivity extends AppCompatActivity {
+
+    private static final String TAG = "NN_BENCHMARK";
+    private static final int JOB_FREQUENCY_MILLIS = 15 * 60 * 1000; // 15 minutes
+    private Button mStartStopButton;
+    private TextView mMessage;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_main);
+        TestExternalStorageActivity.testWriteExternalStorage(this, true);
+
+        mStartStopButton = (Button) findViewById(R.id.start_stop_button);
+        mMessage = (TextView) findViewById(R.id.message);
+        try {
+            TestModelsListLoader.parseFromAssets(getAssets());
+        } catch (IOException e) {
+            Log.e(TAG, "Could not load models", e);
+        }
+    }
+
+    public void startStopTestClicked(View v) {
+
+        JobScheduler jobScheduler = (JobScheduler) getSystemService(JOB_SCHEDULER_SERVICE);
+        if (jobScheduler.getPendingJob(BenchmarkJobService.JOB_ID) == null) {
+            // no job is currently scheduled
+            ComponentName componentName = new ComponentName(this, BenchmarkJobService.class);
+            JobInfo jobInfo =
+                    new JobInfo.Builder(BenchmarkJobService.JOB_ID, componentName)
+                            .setPeriodic(JOB_FREQUENCY_MILLIS)
+                            .build();
+            jobScheduler.schedule(jobInfo);
+            mMessage.setText("Benchmark job scheduled, you can leave this app");
+            mStartStopButton.setText(R.string.stop_button_text);
+        } else {
+            jobScheduler.cancel(BenchmarkJobService.JOB_ID);
+            mMessage.setText("Benchmark job cancelled");
+            mStartStopButton.setText(R.string.start_button_text);
+        }
+    }
+}
diff --git a/src/com/android/nn/benchmark/app/NNBenchmark.java b/src/com/android/nn/benchmark/app/NNBenchmark.java
index c29fef8..a45e043 100644
--- a/src/com/android/nn/benchmark/app/NNBenchmark.java
+++ b/src/com/android/nn/benchmark/app/NNBenchmark.java
@@ -19,24 +19,13 @@
 import android.app.Activity;
 import android.content.Intent;
 import android.os.Bundle;
-import android.os.Trace;
-import android.util.Log;
-import android.util.Pair;
 import android.view.WindowManager;
 import android.widget.TextView;
 
-import com.android.nn.benchmark.core.BenchmarkException;
 import com.android.nn.benchmark.core.BenchmarkResult;
-import com.android.nn.benchmark.core.InferenceInOutSequence;
-import com.android.nn.benchmark.core.InferenceResult;
-import com.android.nn.benchmark.core.NNTestBase;
-import com.android.nn.benchmark.core.TestModels;
-import com.android.nn.benchmark.core.UnsupportedSdkException;
+import com.android.nn.benchmark.core.Processor;
 
-import java.util.List;
-import java.io.IOException;
-
-public class NNBenchmark extends Activity {
+public class NNBenchmark extends Activity implements Processor.Callback {
     protected static final String TAG = "NN_BENCHMARK";
 
     public static final String EXTRA_ENABLE_LONG = "enable long";
@@ -52,211 +41,25 @@
     private BenchmarkResult mTestResults[];
 
     private TextView mTextView;
-    private boolean mToggleLong;
-    private boolean mTogglePause;
-
-    private boolean mUseNNApi;
-    private boolean mCompleteInputSet;
-
-    protected void setUseNNApi(boolean useNNApi) {
-        mUseNNApi = useNNApi;
-    }
-
-    protected void setCompleteInputSet(boolean completeInputSet) {
-        mCompleteInputSet = completeInputSet;
-    }
 
     // Initialize the parameters for Instrumentation tests.
     protected void prepareInstrumentationTest() {
         mTestList = new int[1];
         mTestResults = new BenchmarkResult[1];
-        mProcessor = new Processor();
+        mProcessor = new Processor(this, this, mTestList);
     }
 
-    /////////////////////////////////////////////////////////////////////////
-    // Processor is a helper thread for running the work without
-    // blocking the UI thread.
-    class Processor extends Thread {
-        private float mLastResult;
-        private boolean mRun = true;
-        private boolean mDoingBenchmark;
-        private NNTestBase mTest;
-
-        // Method to retrieve benchmark results for instrumentation tests.
-        BenchmarkResult getInstrumentationResult(
-                TestModels.TestModelEntry t, float warmupTimeSeconds, float runTimeSeconds)
-                throws IOException {
-            mTest = changeTest(mTest, t);
-            return getBenchmark(warmupTimeSeconds, runTimeSeconds);
-        }
-
-        // Run one loop of kernels for at least the specified minimum time.
-        // The function returns the average time in ms for the test run
-        private BenchmarkResult runBenchmarkLoop(float minTime, boolean completeInputSet)
-                throws IOException {
-            try {
-                // Run the kernel
-                Pair<List<InferenceInOutSequence>, List<InferenceResult>> results;
-                if (minTime > 0.f) {
-                    if (completeInputSet) {
-                        results = mTest.runBenchmarkCompleteInputSet(1, minTime);
-                    } else {
-                        results = mTest.runBenchmark(minTime);
-                    }
-                } else {
-                    results = mTest.runInferenceOnce();
-                }
-                return BenchmarkResult.fromInferenceResults(
-                        mTest.getTestInfo(),
-                        mUseNNApi ? BenchmarkResult.BACKEND_TFLITE_NNAPI
-                                : BenchmarkResult.BACKEND_TFLITE_CPU,
-                        results.first, results.second, mTest.getEvaluator());
-            } catch (BenchmarkException e) {
-                return new BenchmarkResult(e.getMessage());
-            }
-        }
-
-
-        // Get a benchmark result for a specific test
-        private BenchmarkResult getBenchmark(float warmupTimeSeconds, float runTimeSeconds)
-            throws IOException {
-            try {
-                mTest.checkSdkVersion();
-            } catch (UnsupportedSdkException e) {
-                BenchmarkResult r = new BenchmarkResult(e.getMessage());
-                Log.v(TAG, "Test: " + r.toString());
-                return r;
-            }
-
-            mDoingBenchmark = true;
-
-            long result = 0;
-
-            // We run a short bit of work before starting the actual test
-            // this is to let any power management do its job and respond.
-            // For NNAPI systrace usage documentation, see
-            // frameworks/ml/nn/common/include/Tracing.h.
-            try {
-                final String traceName = "[NN_LA_PWU]runBenchmarkLoop";
-                Trace.beginSection(traceName);
-                runBenchmarkLoop(warmupTimeSeconds, false);
-            } finally {
-                Trace.endSection();
-            }
-
-            // Run the actual benchmark
-            BenchmarkResult r;
-            try {
-                final String traceName = "[NN_LA_PBM]runBenchmarkLoop";
-                Trace.beginSection(traceName);
-                r = runBenchmarkLoop(runTimeSeconds, mCompleteInputSet);
-            } finally {
-                Trace.endSection();
-            }
-
-            Log.v(TAG, "Test: " + r.toString());
-
-            mDoingBenchmark = false;
-            return r;
-        }
-
-        @Override
-        public void run() {
-            while (mRun) {
-                // Our loop for launching tests or benchmarks
-                synchronized (this) {
-                    // We may have been asked to exit while waiting
-                    if (!mRun) return;
-                }
-
-                try {
-                    // Loop over the tests we want to benchmark
-                    for (int ct = 0; (ct < mTestList.length) && mRun; ct++) {
-
-                        // For reproducibility we wait a short time for any sporadic work
-                        // created by the user touching the screen to launch the test to pass.
-                        // Also allows for things to settle after the test changes.
-                        try {
-                            sleep(250);
-                        } catch (InterruptedException e) {
-                        }
-
-                        TestModels.TestModelEntry testModel =
-                            TestModels.modelsList().get(mTestList[ct]);
-                        int testNumber = ct + 1;
-                        runOnUiThread(() -> {
-                            mTextView.setText(
-                                String.format(
-                                    "Running test %d of %d: %s",
-                                    testNumber,
-                                    mTestList.length,
-                                    testModel.toString()));
-                        });
-
-                        // Select the next test
-                        mTest = changeTest(mTest, testModel);
-
-                        // If the user selected the "long pause" option, wait
-                        if (mTogglePause) {
-                            for (int i = 0; (i < 100) && mRun; i++) {
-                                try {
-                                    sleep(100);
-                                } catch (InterruptedException e) {
-                                }
-                            }
-                        }
-
-                        // Run the test
-                        float warmupTime = 0.3f;
-                        float runTime = 1.f;
-                        if (mToggleLong) {
-                            warmupTime = 2.f;
-                            runTime = 10.f;
-                        }
-                        mTestResults[ct] = getBenchmark(warmupTime, runTime);
-                    }
-                    onBenchmarkFinish(mRun);
-                } catch (IOException e) {
-                    Log.e(TAG, "Exception during benchmark run", e);
-                    break;
-                }
-            }
-        }
-
-        public void exit() {
-            mRun = false;
-
-            synchronized (this) {
-                notifyAll();
-            }
-
-            try {
-                this.join();
-            } catch (InterruptedException e) {
-            }
-
-            if (mTest != null) {
-                mTest.destroy();
-                mTest = null;
-            }
-        }
+    public void setUseNNApi(boolean useNNApi) {
+        mProcessor.setUseNNApi(useNNApi);
     }
 
+    public void setCompleteInputSet(boolean completeInputSet) {
+        mProcessor.setCompleteInputSet(completeInputSet);
+    }
 
     private boolean mDoingBenchmark;
     public Processor mProcessor;
 
-    NNTestBase changeTest(NNTestBase oldTestBase, TestModels.TestModelEntry t) {
-        if (oldTestBase != null) {
-            // Make sure we don't leak memory.
-            oldTestBase.destroy();
-        }
-        NNTestBase tb = t.createNNTestBase(mUseNNApi,
-                false /* enableIntermediateTensorsDump */);
-        tb.setupModel(this);
-        return tb;
-    }
-
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
@@ -279,7 +82,7 @@
         if (ok) {
             Intent intent = new Intent();
             intent.putExtra(EXTRA_RESULTS_TESTS, mTestList);
-            intent.putExtra(EXTRA_RESULTS_RESULTS, mTestResults);
+            intent.putExtra(EXTRA_RESULTS_RESULTS, mProcessor.getTestResults());
             setResult(RESULT_OK, intent);
         } else {
             setResult(RESULT_CANCELED);
@@ -287,18 +90,25 @@
         finish();
     }
 
+    public void onStatusUpdate(int testNumber, int numTests, String modelName) {
+        runOnUiThread(
+                () -> {
+                    mTextView.setText(
+                            String.format(
+                                    "Running test %d of %d: %s", testNumber, numTests, modelName));
+                });
+    }
+
     @Override
     protected void onResume() {
         super.onResume();
         Intent i = getIntent();
         mTestList = i.getIntArrayExtra(EXTRA_TESTS);
-        mToggleLong = i.getBooleanExtra(EXTRA_ENABLE_LONG, false);
-        mTogglePause = i.getBooleanExtra(EXTRA_ENABLE_PAUSE, false);
-        setUseNNApi(!i.getBooleanExtra(EXTRA_DISABLE_NNAPI, false));
-
+        mProcessor = new Processor(this, this, mTestList);
+        mProcessor.setToggleLong(i.getBooleanExtra(EXTRA_ENABLE_LONG, false));
+        mProcessor.setTogglePause(i.getBooleanExtra(EXTRA_ENABLE_PAUSE, false));
+        mProcessor.setUseNNApi(!i.getBooleanExtra(EXTRA_DISABLE_NNAPI, false));
         if (mTestList != null) {
-            mTestResults = new BenchmarkResult[mTestList.length];
-            mProcessor = new Processor();
             mProcessor.start();
         }
     }
diff --git a/src/com/android/nn/benchmark/core/NNTestBase.java b/src/com/android/nn/benchmark/core/NNTestBase.java
index d497396..63f7bc8 100644
--- a/src/com/android/nn/benchmark/core/NNTestBase.java
+++ b/src/com/android/nn/benchmark/core/NNTestBase.java
@@ -16,7 +16,7 @@
 
 package com.android.nn.benchmark.core;
 
-import android.app.Activity;
+import android.content.Context;
 import android.content.res.AssetManager;
 import android.os.Build;
 import android.util.Log;
@@ -73,7 +73,7 @@
             String dumpPath,
             List<InferenceInOutSequence> inOutList);
 
-    protected Activity mActivity;
+    protected Context mContext;
     protected TextView mText;
     private String mModelName;
     private String mModelFile;
@@ -135,8 +135,8 @@
         mNNApiDeviceName = Optional.ofNullable(value);
     }
 
-    public final boolean setupModel(Activity ipact) {
-        mActivity = ipact;
+    public final boolean setupModel(Context ipcxt) {
+        mContext = ipcxt;
         String modelFileName = copyAssetToFile();
         if (modelFileName != null) {
             mModelHandle = initModel(
@@ -149,7 +149,7 @@
             resizeInputTensors(mModelHandle, mInputShape);
         }
         if (mEvaluatorConfig != null) {
-            mEvaluator = mEvaluatorConfig.createEvaluator(mActivity.getAssets());
+            mEvaluator = mEvaluatorConfig.createEvaluator(mContext.getAssets());
         }
         return true;
     }
@@ -174,13 +174,13 @@
         List<InferenceInOutSequence> inOutList = new ArrayList<>();
         if (mInputOutputAssets != null) {
             for (InferenceInOutSequence.FromAssets ioAsset : mInputOutputAssets) {
-                inOutList.add(ioAsset.readAssets(mActivity.getAssets()));
+                inOutList.add(ioAsset.readAssets(mContext.getAssets()));
             }
         }
         if (mInputOutputDatasets != null) {
             for (InferenceInOutSequence.FromDataset dataset : mInputOutputDatasets) {
-                inOutList.addAll(dataset.readDataset(mActivity.getAssets(),
-                        mActivity.getCacheDir()));
+                inOutList.addAll(dataset.readDataset(mContext.getAssets(),
+                        mContext.getCacheDir()));
             }
         }
 
@@ -297,11 +297,11 @@
     private String copyAssetToFile() {
         String outFileName;
         String modelAssetName = mModelFile + ".tflite";
-        AssetManager assetManager = mActivity.getAssets();
+        AssetManager assetManager = mContext.getAssets();
         try {
             InputStream in = assetManager.open(modelAssetName);
 
-            outFileName = mActivity.getCacheDir().getAbsolutePath() + "/" + modelAssetName;
+            outFileName = mContext.getCacheDir().getAbsolutePath() + "/" + modelAssetName;
             File outFile = new File(outFileName);
             OutputStream out = new FileOutputStream(outFile);
 
diff --git a/src/com/android/nn/benchmark/core/Processor.java b/src/com/android/nn/benchmark/core/Processor.java
new file mode 100644
index 0000000..1aa6008
--- /dev/null
+++ b/src/com/android/nn/benchmark/core/Processor.java
@@ -0,0 +1,247 @@
+/*
+ * Copyright (C) 2019 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.nn.benchmark.core;
+
+import android.content.Context;
+import android.os.Trace;
+import android.util.Log;
+import android.util.Pair;
+
+import java.io.IOException;
+import java.util.List;
+
+/** Processor is a helper thread for running the work without blocking the UI thread. */
+public class Processor extends Thread {
+
+    public interface Callback {
+        public void onBenchmarkFinish(boolean ok);
+
+        public void onStatusUpdate(int testNumber, int numTests, String modelName);
+    }
+
+    protected static final String TAG = "NN_BENCHMARK";
+    private Context mContext;
+
+    private float mLastResult;
+    private boolean mRun = true;
+    private boolean mDoingBenchmark;
+    private NNTestBase mTest;
+    private int mTestList[];
+    private BenchmarkResult mTestResults[];
+
+    private Processor.Callback mCallback;
+
+    private boolean mUseNNApi;
+    private boolean mCompleteInputSet;
+    private boolean mToggleLong;
+    private boolean mTogglePause;
+
+    public Processor(Context context, Processor.Callback callback, int[] testList) {
+        mContext = context;
+        mCallback = callback;
+        mTestList = testList;
+        if (mTestList != null) {
+            mTestResults = new BenchmarkResult[mTestList.length];
+        }
+    }
+
+    public void setUseNNApi(boolean useNNApi) {
+        mUseNNApi = useNNApi;
+    }
+
+    public void setCompleteInputSet(boolean completeInputSet) {
+        mCompleteInputSet = completeInputSet;
+    }
+
+    public void setToggleLong(boolean toggleLong) {
+        mToggleLong = toggleLong;
+    }
+
+    public void setTogglePause(boolean togglePause) {
+        mTogglePause = togglePause;
+    }
+
+    // Method to retrieve benchmark results for instrumentation tests.
+    public BenchmarkResult getInstrumentationResult(
+            TestModels.TestModelEntry t, float warmupTimeSeconds, float runTimeSeconds)
+            throws IOException {
+        mTest = changeTest(mTest, t);
+        return getBenchmark(warmupTimeSeconds, runTimeSeconds);
+    }
+
+    private NNTestBase changeTest(NNTestBase oldTestBase, TestModels.TestModelEntry t) {
+        if (oldTestBase != null) {
+            // Make sure we don't leak memory.
+            oldTestBase.destroy();
+        }
+        NNTestBase tb = t.createNNTestBase(mUseNNApi, false /* enableIntermediateTensorsDump */);
+        tb.setupModel(mContext);
+        return tb;
+    }
+
+    // Run one loop of kernels for at least the specified minimum time.
+    // The function returns the average time in ms for the test run
+    private BenchmarkResult runBenchmarkLoop(float minTime, boolean completeInputSet)
+            throws IOException {
+        try {
+            // Run the kernel
+            Pair<List<InferenceInOutSequence>, List<InferenceResult>> results;
+            if (minTime > 0.f) {
+                if (completeInputSet) {
+                    results = mTest.runBenchmarkCompleteInputSet(1, minTime);
+                } else {
+                    results = mTest.runBenchmark(minTime);
+                }
+            } else {
+                results = mTest.runInferenceOnce();
+            }
+            return BenchmarkResult.fromInferenceResults(
+                    mTest.getTestInfo(),
+                    mUseNNApi
+                            ? BenchmarkResult.BACKEND_TFLITE_NNAPI
+                            : BenchmarkResult.BACKEND_TFLITE_CPU,
+                    results.first,
+                    results.second,
+                    mTest.getEvaluator());
+        } catch (BenchmarkException e) {
+            return new BenchmarkResult(e.getMessage());
+        }
+    }
+
+    public BenchmarkResult[] getTestResults() {
+        return mTestResults;
+    }
+
+    // Get a benchmark result for a specific test
+    private BenchmarkResult getBenchmark(float warmupTimeSeconds, float runTimeSeconds)
+            throws IOException {
+        try {
+            mTest.checkSdkVersion();
+        } catch (UnsupportedSdkException e) {
+            BenchmarkResult r = new BenchmarkResult(e.getMessage());
+            Log.v(TAG, "Test: " + r.toString());
+            return r;
+        }
+
+        mDoingBenchmark = true;
+
+        long result = 0;
+
+        // We run a short bit of work before starting the actual test
+        // this is to let any power management do its job and respond.
+        // For NNAPI systrace usage documentation, see
+        // frameworks/ml/nn/common/include/Tracing.h.
+        try {
+            final String traceName = "[NN_LA_PWU]runBenchmarkLoop";
+            Trace.beginSection(traceName);
+            runBenchmarkLoop(warmupTimeSeconds, false);
+        } finally {
+            Trace.endSection();
+        }
+
+        // Run the actual benchmark
+        BenchmarkResult r;
+        try {
+            final String traceName = "[NN_LA_PBM]runBenchmarkLoop";
+            Trace.beginSection(traceName);
+            r = runBenchmarkLoop(runTimeSeconds, mCompleteInputSet);
+        } finally {
+            Trace.endSection();
+        }
+
+        Log.v(TAG, "Test: " + r.toString());
+
+        mDoingBenchmark = false;
+        return r;
+    }
+
+    @Override
+    public void run() {
+        while (mRun) {
+            // Our loop for launching tests or benchmarks
+            synchronized (this) {
+                // We may have been asked to exit while waiting
+                if (!mRun) return;
+            }
+
+            try {
+                // Loop over the tests we want to benchmark
+                for (int ct = 0; (ct < mTestList.length) && mRun; ct++) {
+
+                    // For reproducibility we wait a short time for any sporadic work
+                    // created by the user touching the screen to launch the test to pass.
+                    // Also allows for things to settle after the test changes.
+                    try {
+                        sleep(250);
+                    } catch (InterruptedException e) {
+                    }
+
+                    TestModels.TestModelEntry testModel =
+                            TestModels.modelsList().get(mTestList[ct]);
+                    int testNumber = ct + 1;
+                    mCallback.onStatusUpdate(testNumber, mTestList.length, testModel.toString());
+
+                    // Select the next test
+                    mTest = changeTest(mTest, testModel);
+
+                    // If the user selected the "long pause" option, wait
+                    if (mTogglePause) {
+                        for (int i = 0; (i < 100) && mRun; i++) {
+                            try {
+                                sleep(100);
+                            } catch (InterruptedException e) {
+                            }
+                        }
+                    }
+
+                    // Run the test
+                    float warmupTime = 0.3f;
+                    float runTime = 1.f;
+                    if (mToggleLong) {
+                        warmupTime = 2.f;
+                        runTime = 10.f;
+                    }
+                    mTestResults[ct] = getBenchmark(warmupTime, runTime);
+                }
+                mCallback.onBenchmarkFinish(mRun);
+            } catch (IOException e) {
+                Log.e(TAG, "Exception during benchmark run", e);
+                break;
+            }
+        }
+    }
+
+    public void exit() {
+        mRun = false;
+
+        synchronized (this) {
+            notifyAll();
+        }
+        // exit() is called on same thread when run via dogfood BenchmarkJobService
+        if (this != Thread.currentThread()) {
+            try {
+                this.join();
+            } catch (InterruptedException e) {
+            }
+        }
+
+        if (mTest != null) {
+            mTest.destroy();
+            mTest = null;
+        }
+    }
+}