arc: Extends cheets_GamePerfmance test to capture more metrics.

This adds extra metrics per each frame to capture:
  * Maximum number of triangles to render.
  * Fill and blend rate in kpixels
  * Maximum number of device calls to render.
  * Maximum number of UI controls.
This also capture results in two mode, first mode without extra load
and second mode with extra 2 threads that load CPU and emulate heavy
app/game.

Test: Locally
Bug: 13553231
Bug: 1347063273
Change-Id: I87491634fa38bd5e04d47d62154a0da8e467213f
(cherry picked from commit f3dae277aa990dcb99d5a0bab1f9ccc141b9e4d9)
diff --git a/tests/GamePerformance/AndroidManifest.xml b/tests/GamePerformance/AndroidManifest.xml
index b331e2c..2ff7fa6 100644
--- a/tests/GamePerformance/AndroidManifest.xml
+++ b/tests/GamePerformance/AndroidManifest.xml
@@ -16,7 +16,9 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.gameperformance">
+    package="android.gameperformance"
+    android:versionCode="3"
+    android:versionName="3.0" >
     <uses-sdk android:minSdkVersion="25"/>
     <uses-feature android:glEsVersion="0x00020000" android:required="true" />
 
@@ -24,7 +26,8 @@
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
     <application android:theme="@style/noeffects">
         <uses-library android:name="android.test.runner" />
-        <activity android:name="android.gameperformance.GamePerformanceActivity" >
+        <activity android:name="android.gameperformance.GamePerformanceActivity"
+                  android:screenOrientation="landscape" >
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <category android:name="android.intent.category.LAUNCHER" />
diff --git a/tests/GamePerformance/res/drawable/animation.xml b/tests/GamePerformance/res/drawable/animation.xml
new file mode 100644
index 0000000..b423ff0
--- /dev/null
+++ b/tests/GamePerformance/res/drawable/animation.xml
@@ -0,0 +1,29 @@
+<!--
+ * 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.
+ -->
+
+<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
+     android:id="@+id/animation" android:oneshot="false">
+    <item android:drawable="@drawable/digit_0" android:duration="15" />
+    <item android:drawable="@drawable/digit_1" android:duration="15" />
+    <item android:drawable="@drawable/digit_2" android:duration="15" />
+    <item android:drawable="@drawable/digit_3" android:duration="15" />
+    <item android:drawable="@drawable/digit_4" android:duration="15" />
+    <item android:drawable="@drawable/digit_5" android:duration="15" />
+    <item android:drawable="@drawable/digit_6" android:duration="15" />
+    <item android:drawable="@drawable/digit_7" android:duration="15" />
+    <item android:drawable="@drawable/digit_8" android:duration="15" />
+    <item android:drawable="@drawable/digit_9" android:duration="15" />
+ </animation-list>
\ No newline at end of file
diff --git a/tests/GamePerformance/res/drawable/digit_0.png b/tests/GamePerformance/res/drawable/digit_0.png
new file mode 100644
index 0000000..7264e3e
--- /dev/null
+++ b/tests/GamePerformance/res/drawable/digit_0.png
Binary files differ
diff --git a/tests/GamePerformance/res/drawable/digit_1.png b/tests/GamePerformance/res/drawable/digit_1.png
new file mode 100644
index 0000000..f098a71
--- /dev/null
+++ b/tests/GamePerformance/res/drawable/digit_1.png
Binary files differ
diff --git a/tests/GamePerformance/res/drawable/digit_2.png b/tests/GamePerformance/res/drawable/digit_2.png
new file mode 100644
index 0000000..f08cd31
--- /dev/null
+++ b/tests/GamePerformance/res/drawable/digit_2.png
Binary files differ
diff --git a/tests/GamePerformance/res/drawable/digit_3.png b/tests/GamePerformance/res/drawable/digit_3.png
new file mode 100644
index 0000000..497df8a
--- /dev/null
+++ b/tests/GamePerformance/res/drawable/digit_3.png
Binary files differ
diff --git a/tests/GamePerformance/res/drawable/digit_4.png b/tests/GamePerformance/res/drawable/digit_4.png
new file mode 100644
index 0000000..10efe8c
--- /dev/null
+++ b/tests/GamePerformance/res/drawable/digit_4.png
Binary files differ
diff --git a/tests/GamePerformance/res/drawable/digit_5.png b/tests/GamePerformance/res/drawable/digit_5.png
new file mode 100644
index 0000000..1018a2f
--- /dev/null
+++ b/tests/GamePerformance/res/drawable/digit_5.png
Binary files differ
diff --git a/tests/GamePerformance/res/drawable/digit_6.png b/tests/GamePerformance/res/drawable/digit_6.png
new file mode 100644
index 0000000..593c467
--- /dev/null
+++ b/tests/GamePerformance/res/drawable/digit_6.png
Binary files differ
diff --git a/tests/GamePerformance/res/drawable/digit_7.png b/tests/GamePerformance/res/drawable/digit_7.png
new file mode 100644
index 0000000..041b95f
--- /dev/null
+++ b/tests/GamePerformance/res/drawable/digit_7.png
Binary files differ
diff --git a/tests/GamePerformance/res/drawable/digit_8.png b/tests/GamePerformance/res/drawable/digit_8.png
new file mode 100644
index 0000000..f8fa496
--- /dev/null
+++ b/tests/GamePerformance/res/drawable/digit_8.png
Binary files differ
diff --git a/tests/GamePerformance/res/drawable/digit_9.png b/tests/GamePerformance/res/drawable/digit_9.png
new file mode 100644
index 0000000..303b1da
--- /dev/null
+++ b/tests/GamePerformance/res/drawable/digit_9.png
Binary files differ
diff --git a/tests/GamePerformance/res/drawable/logo.png b/tests/GamePerformance/res/drawable/logo.png
new file mode 100644
index 0000000..61391df
--- /dev/null
+++ b/tests/GamePerformance/res/drawable/logo.png
Binary files differ
diff --git a/tests/GamePerformance/src/android/gameperformance/BaseTest.java b/tests/GamePerformance/src/android/gameperformance/BaseTest.java
new file mode 100644
index 0000000..b0640b44
--- /dev/null
+++ b/tests/GamePerformance/src/android/gameperformance/BaseTest.java
@@ -0,0 +1,152 @@
+/*
+ * 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 android.gameperformance;
+
+import java.text.DecimalFormat;
+import java.util.concurrent.TimeUnit;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.util.Log;
+import android.view.WindowManager;
+
+/**
+ * Base class for a test that performs bisection to determine maximum
+ * performance of a metric test measures.
+ */
+public abstract class BaseTest  {
+    private final static String TAG = "BaseTest";
+
+    // Time to wait for render warm up. No statistics is collected during this pass.
+    private final static long WARM_UP_TIME = TimeUnit.SECONDS.toMillis(5);
+
+    // Perform pass to probe the configuration using iterations. After each iteration current FPS is
+    // checked and if it looks obviously bad, pass gets stopped earlier. Once all iterations are
+    // done and final FPS is above PASS_THRESHOLD pass to probe is considered successful.
+    private final static long TEST_ITERATION_TIME = TimeUnit.SECONDS.toMillis(12);
+    private final static int TEST_ITERATION_COUNT = 5;
+
+    // FPS pass test threshold, in ratio from ideal FPS, that matches device
+    // refresh rate.
+    private final static double PASS_THRESHOLD = 0.95;
+    // FPS threshold, in ratio from ideal FPS, to identify that current pass to probe is obviously
+    // bad and to stop pass earlier.
+    private final static double OBVIOUS_BAD_THRESHOLD = 0.90;
+
+    private static DecimalFormat DOUBLE_FORMATTER = new DecimalFormat("#.##");
+
+    private final GamePerformanceActivity mActivity;
+
+    // Device's refresh rate.
+    private final double mRefreshRate;
+
+    public BaseTest(@NonNull GamePerformanceActivity activity) {
+        mActivity = activity;
+        final WindowManager windowManager =
+                (WindowManager)getContext().getSystemService(Context.WINDOW_SERVICE);
+        mRefreshRate = windowManager.getDefaultDisplay().getRefreshRate();
+    }
+
+    @NonNull
+    public Context getContext() {
+        return mActivity;
+    }
+
+    @NonNull
+    public GamePerformanceActivity getActivity() {
+        return mActivity;
+    }
+
+    // Returns name of the test.
+    public abstract String getName();
+
+    // Returns unit name.
+    public abstract String getUnitName();
+
+    // Returns number of measured units per one bisection unit.
+    public abstract double getUnitScale();
+
+    // Initializes test.
+    public abstract void initUnits(double unitCount);
+
+    // Initializes probe pass.
+    protected abstract void initProbePass(int probe);
+
+    // Frees probe pass.
+    protected abstract void freeProbePass();
+
+    /**
+     * Performs the test and returns maximum number of measured units achieved. Unit is test
+     * specific and name is returned by getUnitName. Returns 0 in case of failure.
+     */
+    public double run() {
+        try {
+            Log.i(TAG, "Test started " + getName());
+
+            final double passFps = PASS_THRESHOLD * mRefreshRate;
+            final double obviousBadFps = OBVIOUS_BAD_THRESHOLD * mRefreshRate;
+
+            // Bisection bounds. Probe value is taken as middle point. Then it used to initialize
+            // test with probe * getUnitScale units. In case probe passed, lowLimit is updated to
+            // probe, otherwise upLimit is updated to probe. lowLimit contains probe that passes
+            // and upLimit contains the probe that fails. Each iteration narrows the range.
+            // Iterations continue until range is collapsed and lowLimit contains actual test
+            // result.
+            int lowLimit = 0;  // Initially 0, that is recognized as failure.
+            int upLimit = 250;
+
+            while (true) {
+                int probe = (lowLimit + upLimit) / 2;
+                if (probe == lowLimit) {
+                    Log.i(TAG, "Test done: " + DOUBLE_FORMATTER.format(probe * getUnitScale()) +
+                               " " + getUnitName());
+                    return probe * getUnitScale();
+                }
+
+                Log.i(TAG, "Start probe: " + DOUBLE_FORMATTER.format(probe * getUnitScale()) + " " +
+                           getUnitName());
+                initProbePass(probe);
+
+                Thread.sleep(WARM_UP_TIME);
+
+                getActivity().resetFrameTimes();
+
+                double fps = 0.0f;
+                for (int i = 0; i < TEST_ITERATION_COUNT; ++i) {
+                    Thread.sleep(TEST_ITERATION_TIME);
+                    fps = getActivity().getFps();
+                    if (fps < obviousBadFps) {
+                        // Stop test earlier, we could not fit the loading.
+                        break;
+                    }
+                }
+
+                freeProbePass();
+
+                Log.i(TAG, "Finish probe: " + DOUBLE_FORMATTER.format(probe * getUnitScale()) +
+                           " " + getUnitName() + " - " + DOUBLE_FORMATTER.format(fps) + " FPS.");
+                if (fps < passFps) {
+                    upLimit = probe;
+                } else {
+                    lowLimit = probe;
+                }
+            }
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            return 0;
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/GamePerformance/src/android/gameperformance/CPULoadThread.java b/tests/GamePerformance/src/android/gameperformance/CPULoadThread.java
new file mode 100644
index 0000000..fa6f03b
--- /dev/null
+++ b/tests/GamePerformance/src/android/gameperformance/CPULoadThread.java
@@ -0,0 +1,61 @@
+/*
+ * 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 android.gameperformance;
+
+/**
+ * Ballast thread that emulates CPU load by performing heavy computation in loop.
+ */
+public class CPULoadThread extends Thread {
+    private boolean mStopRequest;
+
+    public CPULoadThread() {
+        mStopRequest = false;
+    }
+
+    private static double computePi() {
+        double accumulator = 0;
+        double prevAccumulator = -1;
+        int index = 1;
+        while (true) {
+            accumulator += ((1.0 / (2.0 * index - 1)) - (1.0 / (2.0 * index + 1)));
+            if (accumulator == prevAccumulator) {
+                break;
+            }
+            prevAccumulator = accumulator;
+            index += 2;
+        }
+        return 4 * accumulator;
+    }
+
+    // Requests thread to stop.
+    public void issueStopRequest() {
+        synchronized (this) {
+            mStopRequest = true;
+        }
+    }
+
+    @Override
+    public void run() {
+        // Load CPU by PI computation.
+        while (computePi() != 0) {
+            synchronized (this) {
+                if (mStopRequest) {
+                    break;
+                }
+            }
+        }
+    }
+}
diff --git a/tests/GamePerformance/src/android/gameperformance/ControlsTest.java b/tests/GamePerformance/src/android/gameperformance/ControlsTest.java
new file mode 100644
index 0000000..6c36ddc
--- /dev/null
+++ b/tests/GamePerformance/src/android/gameperformance/ControlsTest.java
@@ -0,0 +1,73 @@
+/*
+ * 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 android.gameperformance;
+
+import android.annotation.NonNull;
+
+/**
+ * Tests that verifies how many UI controls can be handled to keep FPS close to device refresh rate.
+ * As a test UI control ImageView with an infinite animation is chosen. The animation has refresh
+ * rate ~67Hz that forces all devices to refresh UI at highest possible rate.
+ */
+public class ControlsTest extends BaseTest {
+    public ControlsTest(@NonNull GamePerformanceActivity activity) {
+        super(activity);
+    }
+
+    @NonNull
+    public CustomControlView getView() {
+        return getActivity().getControlView();
+    }
+
+    @Override
+    protected void initProbePass(int probe) {
+        try {
+            getActivity().attachControlView();
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            return;
+        }
+        initUnits(probe * getUnitScale());
+    }
+
+    @Override
+    protected void freeProbePass() {
+    }
+
+    @Override
+    public String getName() {
+        return "control_count";
+    }
+
+    @Override
+    public String getUnitName() {
+        return "controls";
+    }
+
+    @Override
+    public double getUnitScale() {
+        return 5.0;
+    }
+
+    @Override
+    public void initUnits(double controlCount) {
+        try {
+            getView().createControls(getActivity(), (int)Math.round(controlCount));
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/GamePerformance/src/android/gameperformance/CustomControlView.java b/tests/GamePerformance/src/android/gameperformance/CustomControlView.java
new file mode 100644
index 0000000..219085a
--- /dev/null
+++ b/tests/GamePerformance/src/android/gameperformance/CustomControlView.java
@@ -0,0 +1,128 @@
+/*
+ * 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 android.gameperformance;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+
+import android.annotation.MainThread;
+import android.annotation.NonNull;
+import android.annotation.WorkerThread;
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.drawable.AnimationDrawable;
+import android.util.Log;
+import android.view.WindowManager;
+import android.widget.AbsoluteLayout;
+import android.widget.ImageView;
+import android.widget.RelativeLayout;
+
+/**
+ * View that holds requested number of UI controls as ImageView with an infinite animation.
+ */
+public class CustomControlView extends AbsoluteLayout {
+    private final static int CONTROL_DIMENTION = 48;
+
+    private final int mPerRowControlCount;
+    private List<Long> mFrameTimes = new ArrayList<>();
+
+    public CustomControlView(@NonNull Context context) {
+        super(context);
+
+        final WindowManager windowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
+        mPerRowControlCount = windowManager.getDefaultDisplay().getWidth() / CONTROL_DIMENTION;
+    }
+
+    /**
+     * Helper class that overrides ImageView and observes draw requests. Only
+     * one such control is created which is the first control in the view.
+     */
+    class ReferenceImageView extends ImageView {
+        public ReferenceImageView(Context context) {
+            super(context);
+        }
+        @Override
+        public void draw(Canvas canvas) {
+            reportFrame();
+            super.draw(canvas);
+        }
+    }
+
+    @WorkerThread
+    public void createControls(
+            @NonNull Activity activity, int controlCount) throws InterruptedException {
+        synchronized (this) {
+            final CountDownLatch latch = new CountDownLatch(1);
+            activity.runOnUiThread(new Runnable() {
+                @Override
+                public void run() {
+                    removeAllViews();
+
+                    for (int i = 0; i < controlCount; ++i) {
+                        final ImageView image = (i == 0) ?
+                                new ReferenceImageView(activity) : new ImageView(activity);
+                        final int x = (i % mPerRowControlCount) * CONTROL_DIMENTION;
+                        final int y = (i / mPerRowControlCount) * CONTROL_DIMENTION;
+                        final AbsoluteLayout.LayoutParams layoutParams =
+                                new AbsoluteLayout.LayoutParams(
+                                        CONTROL_DIMENTION, CONTROL_DIMENTION, x, y);
+                        image.setLayoutParams(layoutParams);
+                        image.setBackgroundResource(R.drawable.animation);
+                        final AnimationDrawable animation =
+                                (AnimationDrawable)image.getBackground();
+                        animation.start();
+                        addView(image);
+                    }
+
+                    latch.countDown();
+                }
+            });
+            latch.await();
+        }
+    }
+
+    @MainThread
+    private void reportFrame() {
+        final long time = System.currentTimeMillis();
+        synchronized (mFrameTimes) {
+            mFrameTimes.add(time);
+        }
+    }
+
+    /**
+     * Resets frame times in order to calculate FPS for the different test pass.
+     */
+    public void resetFrameTimes() {
+        synchronized (mFrameTimes) {
+            mFrameTimes.clear();
+        }
+    }
+
+    /**
+     * Returns current FPS based on collected frame times.
+     */
+    public double getFps() {
+        synchronized (mFrameTimes) {
+            if (mFrameTimes.size() < 2) {
+                return 0.0f;
+            }
+            return 1000.0 * mFrameTimes.size() /
+                    (mFrameTimes.get(mFrameTimes.size() - 1) - mFrameTimes.get(0));
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/GamePerformance/src/android/gameperformance/CustomOpenGLView.java b/tests/GamePerformance/src/android/gameperformance/CustomOpenGLView.java
index 2b37280..08697ae 100644
--- a/tests/GamePerformance/src/android/gameperformance/CustomOpenGLView.java
+++ b/tests/GamePerformance/src/android/gameperformance/CustomOpenGLView.java
@@ -17,23 +17,36 @@
 
 import java.util.ArrayList;
 import java.util.List;
-import java.util.Random;
 
 import javax.microedition.khronos.egl.EGLConfig;
 import javax.microedition.khronos.opengles.GL10;
 
+import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.content.Context;
 import android.opengl.GLES20;
 import android.opengl.GLSurfaceView;
+import android.util.Log;
 
 public class CustomOpenGLView extends GLSurfaceView {
-    private Random mRandom;
-    private List<Long> mFrameTimes;
+    public final static String TAG = "CustomOpenGLView";
 
-    public CustomOpenGLView(Context context) {
+    private final List<Long> mFrameTimes;
+    private final Object mLock = new Object();
+    private boolean mRenderReady = false;
+    private FrameDrawer mFrameDrawer = null;
+
+    private float mRenderRatio;
+    private int mRenderWidth;
+    private int mRenderHeight;
+
+    public interface FrameDrawer {
+        public void drawFrame(@NonNull GL10 gl);
+    }
+
+    public CustomOpenGLView(@NonNull Context context) {
         super(context);
 
-        mRandom = new Random();
         mFrameTimes = new ArrayList<Long>();
 
         setEGLContextClientVersion(2);
@@ -41,25 +54,35 @@
         setRenderer(new GLSurfaceView.Renderer() {
             @Override
             public void onSurfaceCreated(GL10 gl, EGLConfig config) {
+                Log.i(TAG, "SurfaceCreated: " + config);
                 GLES20.glClearColor(1.0f, 0.0f, 0.0f, 1.0f);
                 gl.glClearDepthf(1.0f);
-                gl.glEnable(GL10.GL_DEPTH_TEST);
+                gl.glDisable(GL10.GL_DEPTH_TEST);
                 gl.glDepthFunc(GL10.GL_LEQUAL);
 
                 gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT,
-                          GL10.GL_NICEST);            }
+                          GL10.GL_NICEST);
+                synchronized (mLock) {
+                    mRenderReady = true;
+                    mLock.notify();
+                }
+            }
 
             @Override
             public void onSurfaceChanged(GL10 gl, int width, int height) {
+                Log.i(TAG, "SurfaceChanged: " + width + "x" + height);
                 GLES20.glViewport(0, 0, width, height);
+                setRenderBounds(width, height);
             }
 
             @Override
             public void onDrawFrame(GL10 gl) {
-                GLES20.glClearColor(
-                        mRandom.nextFloat(), mRandom.nextFloat(), mRandom.nextFloat(), 1.0f);
+                GLES20.glClearColor(0.25f, 0.25f, 0.25f, 1.0f);
                 gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
-                synchronized (mFrameTimes) {
+                synchronized (mLock) {
+                    if (mFrameDrawer != null) {
+                        mFrameDrawer.drawFrame(gl);
+                    }
                     mFrameTimes.add(System.currentTimeMillis());
                 }
             }
@@ -67,20 +90,38 @@
         setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);
     }
 
+    public void setRenderBounds(int width, int height) {
+        mRenderWidth = width;
+        mRenderHeight = height;
+        mRenderRatio = (float) mRenderWidth / mRenderHeight;
+    }
+
+    public float getRenderRatio() {
+        return mRenderRatio;
+    }
+
+    public int getRenderWidth() {
+        return mRenderWidth;
+    }
+
+    public int getRenderHeight() {
+        return mRenderHeight;
+    }
+
     /**
-     * Resets frame times in order to calculate fps for different test pass.
+     * Resets frame times in order to calculate FPS for the different test pass.
      */
     public void resetFrameTimes() {
-        synchronized (mFrameTimes) {
+        synchronized (mLock) {
             mFrameTimes.clear();
         }
     }
 
     /**
-     * Returns current fps based on collected frame times.
+     * Returns current FPS based on collected frame times.
      */
     public double getFps() {
-        synchronized (mFrameTimes) {
+        synchronized (mLock) {
             if (mFrameTimes.size() < 2) {
                 return 0.0f;
             }
@@ -88,4 +129,26 @@
                     (mFrameTimes.get(mFrameTimes.size() - 1) - mFrameTimes.get(0));
         }
     }
+
+    /**
+     * Waits for render attached to the view.
+     */
+    public void waitRenderReady() {
+        synchronized (mLock) {
+            while (!mRenderReady) {
+                try {
+                    mLock.wait();
+                } catch (InterruptedException e) {
+                    Thread.currentThread().interrupt();
+                }
+            }
+        }
+    }
+
+    /**
+     * Sets/resets frame drawer.
+     */
+    public void setFrameDrawer(@Nullable FrameDrawer frameDrawer) {
+        mFrameDrawer = frameDrawer;
+    }
 }
diff --git a/tests/GamePerformance/src/android/gameperformance/DeviceCallsOpenGLTest.java b/tests/GamePerformance/src/android/gameperformance/DeviceCallsOpenGLTest.java
new file mode 100644
index 0000000..df2ae5c
--- /dev/null
+++ b/tests/GamePerformance/src/android/gameperformance/DeviceCallsOpenGLTest.java
@@ -0,0 +1,60 @@
+/*
+ * 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 android.gameperformance;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import android.annotation.NonNull;
+
+/**
+ * Tests that verifies maximum number of device calls to render the geometry to keep FPS close to
+ * the device refresh rate. This uses trivial one triangle patch that is rendered multiple times.
+ */
+public class DeviceCallsOpenGLTest extends RenderPatchOpenGLTest  {
+
+    public DeviceCallsOpenGLTest(@NonNull GamePerformanceActivity activity) {
+        super(activity);
+    }
+
+    @Override
+    public String getName() {
+        return "device_calls";
+    }
+
+    @Override
+    public String getUnitName() {
+        return "calls";
+    }
+
+    @Override
+    public double getUnitScale() {
+        return 25.0;
+    }
+
+    @Override
+    public void initUnits(double deviceCallsD) {
+        final List<RenderPatchAnimation> renderPatches = new ArrayList<>();
+        final RenderPatch renderPatch = new RenderPatch(1 /* triangleCount */,
+                                                        0.05f /* dimension */,
+                                                        RenderPatch.TESSELLATION_BASE);
+        final int deviceCalls = (int)Math.round(deviceCallsD);
+        for (int i = 0; i < deviceCalls; ++i) {
+            renderPatches.add(new RenderPatchAnimation(renderPatch, getView().getRenderRatio()));
+        }
+        setRenderPatches(renderPatches);
+    }
+}
\ No newline at end of file
diff --git a/tests/GamePerformance/src/android/gameperformance/FillRateOpenGLTest.java b/tests/GamePerformance/src/android/gameperformance/FillRateOpenGLTest.java
new file mode 100644
index 0000000..9b26193
--- /dev/null
+++ b/tests/GamePerformance/src/android/gameperformance/FillRateOpenGLTest.java
@@ -0,0 +1,93 @@
+/*
+ * 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 android.gameperformance;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.microedition.khronos.opengles.GL;
+
+import android.annotation.NonNull;
+import android.opengl.GLES20;
+
+/**
+ * Tests that verifies maximum fill rate per frame can be used to keep FPS close to the device
+ * refresh rate. It works in two modes, blend disabled and blend enabled. This uses few big simple
+ * quad patches.
+ */
+public class FillRateOpenGLTest extends RenderPatchOpenGLTest  {
+    private final float[] BLEND_COLOR = new float[] { 1.0f, 1.0f, 1.0f, 0.2f };
+
+    private final boolean mTestBlend;
+
+    public FillRateOpenGLTest(@NonNull GamePerformanceActivity activity, boolean testBlend) {
+        super(activity);
+        mTestBlend = testBlend;
+    }
+
+    @Override
+    public String getName() {
+        return mTestBlend ? "blend_rate" : "fill_rate";
+    }
+
+    @Override
+    public String getUnitName() {
+        return "screens";
+    }
+
+    @Override
+    public double getUnitScale() {
+        return 0.2;
+    }
+
+    @Override
+    public void initUnits(double screens) {
+        final CustomOpenGLView view = getView();
+        final int pixelRate = (int)Math.round(screens * view.getHeight() * view.getWidth());
+        final int maxPerPath = view.getHeight() * view.getHeight();
+
+        final int patchCount = (int)(pixelRate + maxPerPath -1) / maxPerPath;
+        final float patchDimension =
+                (float)((Math.sqrt(2.0f) * pixelRate / patchCount) / maxPerPath);
+
+        final List<RenderPatchAnimation> renderPatches = new ArrayList<>();
+        final RenderPatch renderPatch = new RenderPatch(2 /* triangleCount for quad */,
+                                                        patchDimension,
+                                                        RenderPatch.TESSELLATION_BASE);
+        for (int i = 0; i < patchCount; ++i) {
+            renderPatches.add(new RenderPatchAnimation(renderPatch, getView().getRenderRatio()));
+        }
+        setRenderPatches(renderPatches);
+    }
+
+    @Override
+    public float[] getColor() {
+        return BLEND_COLOR;
+    }
+
+    @Override
+    public void onBeforeDraw(GL gl) {
+        if (!mTestBlend) {
+            return;
+        }
+
+        // Enable blend if needed.
+        GLES20.glEnable(GLES20.GL_BLEND);
+        OpenGLUtils.checkGlError("disableBlend");
+        GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA);
+        OpenGLUtils.checkGlError("blendFunction");
+    }
+}
\ No newline at end of file
diff --git a/tests/GamePerformance/src/android/gameperformance/GamePerformanceActivity.java b/tests/GamePerformance/src/android/gameperformance/GamePerformanceActivity.java
index b0e6196..dc745f1 100644
--- a/tests/GamePerformance/src/android/gameperformance/GamePerformanceActivity.java
+++ b/tests/GamePerformance/src/android/gameperformance/GamePerformanceActivity.java
@@ -25,14 +25,32 @@
 import android.widget.RelativeLayout;
 
 /**
- * Minimal activity that holds SurfaceView or GLSurfaceView.
- * call attachSurfaceView or attachOpenGLView to switch views.
+ * Minimal activity that holds different types of views.
+ * call attachSurfaceView, attachOpenGLView or attachControlView to switch
+ * the view.
  */
 public class GamePerformanceActivity extends Activity {
     private CustomSurfaceView mSurfaceView = null;
     private CustomOpenGLView mOpenGLView = null;
+    private CustomControlView mControlView = null;
+
     private RelativeLayout mRootLayout;
 
+    private void detachAllViews() {
+        if (mOpenGLView != null) {
+            mRootLayout.removeView(mOpenGLView);
+            mOpenGLView = null;
+        }
+        if (mSurfaceView != null) {
+            mRootLayout.removeView(mSurfaceView);
+            mSurfaceView = null;
+        }
+        if (mControlView != null) {
+            mRootLayout.removeView(mControlView);
+            mControlView = null;
+        }
+    }
+
     public void attachSurfaceView() throws InterruptedException {
         synchronized (mRootLayout) {
             if (mSurfaceView != null) {
@@ -42,10 +60,7 @@
             runOnUiThread(new Runnable() {
                 @Override
                 public void run() {
-                    if (mOpenGLView != null) {
-                        mRootLayout.removeView(mOpenGLView);
-                        mOpenGLView = null;
-                    }
+                    detachAllViews();
                     mSurfaceView = new CustomSurfaceView(GamePerformanceActivity.this);
                     mRootLayout.addView(mSurfaceView);
                     latch.countDown();
@@ -65,10 +80,7 @@
             runOnUiThread(new Runnable() {
                 @Override
                 public void run() {
-                    if (mSurfaceView != null) {
-                        mRootLayout.removeView(mSurfaceView);
-                        mSurfaceView = null;
-                    }
+                    detachAllViews();
                     mOpenGLView = new CustomOpenGLView(GamePerformanceActivity.this);
                     mRootLayout.addView(mOpenGLView);
                     latch.countDown();
@@ -78,6 +90,40 @@
         }
     }
 
+    public void attachControlView() throws InterruptedException {
+        synchronized (mRootLayout) {
+            if (mControlView != null) {
+                return;
+            }
+            final CountDownLatch latch = new CountDownLatch(1);
+            runOnUiThread(new Runnable() {
+                @Override
+                public void run() {
+                    detachAllViews();
+                    mControlView = new CustomControlView(GamePerformanceActivity.this);
+                    mRootLayout.addView(mControlView);
+                    latch.countDown();
+                }
+            });
+            latch.await();
+        }
+    }
+
+
+    public CustomOpenGLView getOpenGLView() {
+        if (mOpenGLView == null) {
+            throw new RuntimeException("OpenGL view is not attached");
+        }
+        return mOpenGLView;
+    }
+
+    public CustomControlView getControlView() {
+        if (mControlView == null) {
+            throw new RuntimeException("Control view is not attached");
+        }
+        return mControlView;
+    }
+
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
@@ -105,6 +151,8 @@
             mSurfaceView.resetFrameTimes();
         } else if (mOpenGLView != null) {
             mOpenGLView.resetFrameTimes();
+        } else if (mControlView != null) {
+            mControlView.resetFrameTimes();
         } else {
             throw new IllegalStateException("Nothing attached");
         }
@@ -115,6 +163,8 @@
             return mSurfaceView.getFps();
         } else if (mOpenGLView != null) {
             return mOpenGLView.getFps();
+        } else if (mControlView != null) {
+            return mControlView.getFps();
         } else {
             throw new IllegalStateException("Nothing attached");
         }
diff --git a/tests/GamePerformance/src/android/gameperformance/GamePerformanceTest.java b/tests/GamePerformance/src/android/gameperformance/GamePerformanceTest.java
index e5de7d7..d6e2861 100644
--- a/tests/GamePerformance/src/android/gameperformance/GamePerformanceTest.java
+++ b/tests/GamePerformance/src/android/gameperformance/GamePerformanceTest.java
@@ -17,14 +17,18 @@
 
 import java.io.File;
 import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Map;
 import java.util.concurrent.CountDownLatch;
 
+import android.annotation.NonNull;
 import android.app.Activity;
 import android.content.Context;
 import android.graphics.PixelFormat;
 import android.os.Build;
 import android.os.Bundle;
+import android.os.Debug;
 import android.os.Trace;
 import android.test.ActivityInstrumentationTestCase2;
 import android.test.suitebuilder.annotation.SmallTest;
@@ -84,4 +88,50 @@
 
         getInstrumentation().sendStatus(Activity.RESULT_OK, status);
     }
+
+    @SmallTest
+    public void testPerformanceMetricsWithoutExtraLoad() throws IOException, InterruptedException {
+        final Bundle status = runPerformanceTests("no_extra_load_");
+        getInstrumentation().sendStatus(Activity.RESULT_OK, status);
+    }
+
+    @SmallTest
+    public void testPerformanceMetricsWithExtraLoad() throws IOException, InterruptedException {
+        // Start CPU ballast threads first.
+        CPULoadThread[] cpuLoadThreads = new CPULoadThread[2];
+        for (int i = 0; i < cpuLoadThreads.length; ++i) {
+            cpuLoadThreads[i] = new CPULoadThread();
+            cpuLoadThreads[i].start();
+        }
+
+        final Bundle status = runPerformanceTests("extra_load_");
+
+        for (int i = 0; i < cpuLoadThreads.length; ++i) {
+            cpuLoadThreads[i].issueStopRequest();
+            cpuLoadThreads[i].join();
+        }
+
+        getInstrumentation().sendStatus(Activity.RESULT_OK, status);
+    }
+
+    @NonNull
+    private Bundle runPerformanceTests(@NonNull String prefix) {
+        final Bundle status = new Bundle();
+
+        final GamePerformanceActivity activity = getActivity();
+
+        final List<BaseTest> tests = new ArrayList<>();
+        tests.add(new TriangleCountOpenGLTest(activity));
+        tests.add(new FillRateOpenGLTest(activity, false /* testBlend */));
+        tests.add(new FillRateOpenGLTest(activity, true /* testBlend */));
+        tests.add(new DeviceCallsOpenGLTest(activity));
+        tests.add(new ControlsTest(activity));
+
+        for (BaseTest test : tests) {
+            final double result = test.run();
+            status.putDouble(prefix + test.getName(), result);
+        }
+
+        return status;
+    }
 }
diff --git a/tests/GamePerformance/src/android/gameperformance/OpenGLTest.java b/tests/GamePerformance/src/android/gameperformance/OpenGLTest.java
new file mode 100644
index 0000000..1d3f95c
--- /dev/null
+++ b/tests/GamePerformance/src/android/gameperformance/OpenGLTest.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 android.gameperformance;
+
+import javax.microedition.khronos.opengles.GL;
+import javax.microedition.khronos.opengles.GL10;
+
+import android.annotation.NonNull;
+import android.gameperformance.CustomOpenGLView.FrameDrawer;
+
+/**
+ * Base class for all OpenGL based tests.
+ */
+public abstract class OpenGLTest extends BaseTest {
+    public OpenGLTest(@NonNull GamePerformanceActivity activity) {
+        super(activity);
+    }
+
+    @NonNull
+    public CustomOpenGLView getView() {
+        return getActivity().getOpenGLView();
+    }
+
+    // Performs test drawing.
+    protected abstract void draw(GL gl);
+
+    // Initializes the test on first draw call.
+    private class ParamFrameDrawer implements FrameDrawer {
+        private final double mUnitCount;
+        private boolean mInited;
+
+        public ParamFrameDrawer(double unitCount) {
+            mUnitCount = unitCount;
+            mInited = false;
+        }
+
+        @Override
+        public void drawFrame(GL10 gl) {
+            if (!mInited) {
+                initUnits(mUnitCount);
+                mInited = true;
+            }
+            draw(gl);
+        }
+    }
+
+    @Override
+    protected void initProbePass(int probe) {
+        try {
+            getActivity().attachOpenGLView();
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            return;
+        }
+        getView().waitRenderReady();
+        getView().setFrameDrawer(new ParamFrameDrawer(probe * getUnitScale()));
+    }
+
+    @Override
+    protected void freeProbePass() {
+        getView().setFrameDrawer(null);
+    }
+}
\ No newline at end of file
diff --git a/tests/GamePerformance/src/android/gameperformance/OpenGLUtils.java b/tests/GamePerformance/src/android/gameperformance/OpenGLUtils.java
new file mode 100644
index 0000000..4f98c52
--- /dev/null
+++ b/tests/GamePerformance/src/android/gameperformance/OpenGLUtils.java
@@ -0,0 +1,92 @@
+/*
+ * 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 android.gameperformance;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.graphics.BitmapFactory;
+import android.opengl.GLES20;
+import android.opengl.GLUtils;
+import android.util.Log;
+
+/**
+ * Helper class for OpenGL.
+ */
+public class OpenGLUtils {
+    private final static String TAG = "OpenGLUtils";
+
+    public static void checkGlError(String glOperation) {
+        final int error = GLES20.glGetError();
+        if (error == GLES20.GL_NO_ERROR) {
+            return;
+        }
+        final String errorMessage = glOperation + ": glError " + error;
+        Log.e(TAG, errorMessage);
+    }
+
+    public static int loadShader(int type, String shaderCode) {
+        final int shader = GLES20.glCreateShader(type);
+        checkGlError("createShader");
+
+        GLES20.glShaderSource(shader, shaderCode);
+        checkGlError("shaderSource");
+        GLES20.glCompileShader(shader);
+        checkGlError("shaderCompile");
+
+        return shader;
+    }
+
+    public static int createProgram(@NonNull String vertexShaderCode,
+                                    @NonNull String fragmentShaderCode) {
+        final int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexShaderCode);
+        final int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentShaderCode);
+
+        final int program = GLES20.glCreateProgram();
+        checkGlError("createProgram");
+        GLES20.glAttachShader(program, vertexShader);
+        checkGlError("attachVertexShader");
+        GLES20.glAttachShader(program, fragmentShader);
+        checkGlError("attachFragmentShader");
+        GLES20.glLinkProgram(program);
+        checkGlError("linkProgram");
+
+        return program;
+    }
+
+    public static int createTexture(@NonNull Context context, int resource) {
+        final BitmapFactory.Options options = new BitmapFactory.Options();
+        options.inScaled = false;
+
+        final int[] textureHandle = new int[1];
+        GLES20.glGenTextures(1, textureHandle, 0);
+        OpenGLUtils.checkGlError("GenTextures");
+        final int handle = textureHandle[0];
+
+        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, handle);
+        GLES20.glTexParameteri(
+                GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
+        GLES20.glTexParameteri(
+                GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
+        GLUtils.texImage2D(
+                GLES20.GL_TEXTURE_2D,
+                0,
+                BitmapFactory.decodeResource(
+                        context.getResources(), resource, options),
+                0);
+
+        return handle;
+    }
+}
\ No newline at end of file
diff --git a/tests/GamePerformance/src/android/gameperformance/RenderPatch.java b/tests/GamePerformance/src/android/gameperformance/RenderPatch.java
new file mode 100644
index 0000000..2e69a61
--- /dev/null
+++ b/tests/GamePerformance/src/android/gameperformance/RenderPatch.java
@@ -0,0 +1,150 @@
+/*
+ * 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 android.gameperformance;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * Helper class that generates patch to render. Patch is a regular polygon with the center in 0.
+ * Regular polygon fits in circle with requested radius.
+ */
+public class RenderPatch {
+    public static final int FLOAT_SIZE = 4;
+    public static final int SHORT_SIZE = 2;
+    public static final int VERTEX_COORD_COUNT = 3;
+    public static final int VERTEX_STRIDE = VERTEX_COORD_COUNT * FLOAT_SIZE;
+    public static final int TEXTURE_COORD_COUNT = 2;
+    public static final int TEXTURE_STRIDE = TEXTURE_COORD_COUNT * FLOAT_SIZE;
+
+    // Tessellation is done using points on circle.
+    public static final int TESSELLATION_BASE = 0;
+    // Tesselation is done using extra point in 0.
+    public static final int TESSELLATION_TO_CENTER = 1;
+
+    // Radius of circle that fits polygon.
+    private final float mDimension;
+
+    private final ByteBuffer mVertexBuffer;
+    private final ByteBuffer mTextureBuffer;
+    private final ByteBuffer mIndexBuffer;
+
+    public RenderPatch(int triangleCount, float dimension, int tessellation) {
+        mDimension = dimension;
+
+        int pointCount;
+        int externalPointCount;
+
+        if (triangleCount < 1) {
+            throw new IllegalArgumentException("Too few triangles to perform tessellation");
+        }
+
+        switch (tessellation) {
+        case TESSELLATION_BASE:
+            externalPointCount = triangleCount + 2;
+            pointCount = externalPointCount;
+            break;
+        case TESSELLATION_TO_CENTER:
+            if (triangleCount < 3) {
+                throw new IllegalArgumentException(
+                        "Too few triangles to perform tessellation to center");
+            }
+            externalPointCount = triangleCount;
+            pointCount = triangleCount + 1;
+            break;
+        default:
+            throw new IllegalArgumentException("Wrong tesselation requested");
+        }
+
+        if (pointCount > Short.MAX_VALUE) {
+            throw new IllegalArgumentException("Number of requested triangles is too big");
+        }
+
+        mVertexBuffer = ByteBuffer.allocateDirect(pointCount * VERTEX_STRIDE);
+        mVertexBuffer.order(ByteOrder.nativeOrder());
+
+        mTextureBuffer = ByteBuffer.allocateDirect(pointCount * TEXTURE_STRIDE);
+        mTextureBuffer.order(ByteOrder.nativeOrder());
+
+        for (int i = 0; i < externalPointCount; ++i) {
+            // Use 45 degree rotation to make quad aligned along axises in case
+            // triangleCount is four.
+            final double angle = Math.PI * 0.25 + (Math.PI * 2.0 * i) / (externalPointCount);
+            // Positions
+            mVertexBuffer.putFloat((float) (dimension * Math.sin(angle)));
+            mVertexBuffer.putFloat((float) (dimension * Math.cos(angle)));
+            mVertexBuffer.putFloat(0.0f);
+            // Texture coordinates.
+            mTextureBuffer.putFloat((float) (0.5 + 0.5 * Math.sin(angle)));
+            mTextureBuffer.putFloat((float) (0.5 - 0.5 * Math.cos(angle)));
+        }
+
+        if (tessellation == TESSELLATION_TO_CENTER) {
+            // Add center point.
+            mVertexBuffer.putFloat(0.0f);
+            mVertexBuffer.putFloat(0.0f);
+            mVertexBuffer.putFloat(0.0f);
+            mTextureBuffer.putFloat(0.5f);
+            mTextureBuffer.putFloat(0.5f);
+        }
+
+        mIndexBuffer =
+                ByteBuffer.allocateDirect(
+                        triangleCount * 3 /* indices per triangle */ * SHORT_SIZE);
+        mIndexBuffer.order(ByteOrder.nativeOrder());
+
+        switch (tessellation) {
+        case TESSELLATION_BASE:
+            for (int i = 0; i < triangleCount; ++i) {
+                mIndexBuffer.putShort((short) 0);
+                mIndexBuffer.putShort((short) (i + 1));
+                mIndexBuffer.putShort((short) (i + 2));
+            }
+            break;
+        case TESSELLATION_TO_CENTER:
+            for (int i = 0; i < triangleCount; ++i) {
+                mIndexBuffer.putShort((short)i);
+                mIndexBuffer.putShort((short)((i + 1) % externalPointCount));
+                mIndexBuffer.putShort((short)externalPointCount);
+            }
+            break;
+        }
+
+        if (mVertexBuffer.remaining() != 0 || mTextureBuffer.remaining() != 0 || mIndexBuffer.remaining() != 0) {
+            throw new RuntimeException("Failed to fill buffers");
+        }
+
+        mVertexBuffer.position(0);
+        mTextureBuffer.position(0);
+        mIndexBuffer.position(0);
+    }
+
+    public float getDimension() {
+        return mDimension;
+    }
+
+    public ByteBuffer getVertexBuffer() {
+        return mVertexBuffer;
+    }
+
+    public ByteBuffer getTextureBuffer() {
+        return mTextureBuffer;
+    }
+
+    public ByteBuffer getIndexBuffer() {
+        return mIndexBuffer;
+    }
+}
\ No newline at end of file
diff --git a/tests/GamePerformance/src/android/gameperformance/RenderPatchAnimation.java b/tests/GamePerformance/src/android/gameperformance/RenderPatchAnimation.java
new file mode 100644
index 0000000..7dcdb00
--- /dev/null
+++ b/tests/GamePerformance/src/android/gameperformance/RenderPatchAnimation.java
@@ -0,0 +1,101 @@
+/*
+ * 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 android.gameperformance;
+
+import java.util.Random;
+
+import android.annotation.NonNull;
+import android.opengl.Matrix;
+
+/**
+ * Class that performs bouncing animation for RenderPatch on the screen.
+ */
+public class RenderPatchAnimation {
+    private final static Random RANDOM = new Random();
+
+    private final RenderPatch mRenderPatch;
+    // Bounds of animation
+    private final float mAvailableX;
+    private final float mAvailableY;
+
+    // Crurrent position.
+    private float mPosX;
+    private float mPosY;
+    // Direction of movement.
+    private float mDirX;
+    private float mDirY;
+
+    private float[] mMatrix;
+
+    public RenderPatchAnimation(@NonNull RenderPatch renderPatch, float ratio) {
+        mRenderPatch = renderPatch;
+
+        mAvailableX = ratio - mRenderPatch.getDimension();
+        mAvailableY = 1.0f - mRenderPatch.getDimension();
+
+        mPosX = 2.0f * mAvailableX * RANDOM.nextFloat() - mAvailableX;
+        mPosY = 2.0f * mAvailableY * RANDOM.nextFloat() - mAvailableY;
+        mMatrix = new float[16];
+
+        // Evenly distributed in cycle, normalized.
+        while (true) {
+            mDirX = 2.0f * RANDOM.nextFloat() - 1.0f;
+            mDirY = mRenderPatch.getDimension() < 1.0f ? 2.0f * RANDOM.nextFloat() - 1.0f : 0.0f;
+
+            final float length = (float)Math.sqrt(mDirX * mDirX + mDirY * mDirY);
+            if (length <= 1.0f && length > 0.0f) {
+                mDirX /= length;
+                mDirY /= length;
+                break;
+            }
+        }
+    }
+
+    @NonNull
+    public RenderPatch getRenderPatch() {
+        return mRenderPatch;
+    }
+
+    /**
+     * Performs the next update. t specifies the distance to travel along the direction. This checks
+     * if patch goes out of screen and invert axis direction if needed.
+     */
+    public void update(float t) {
+        mPosX += mDirX * t;
+        mPosY += mDirY * t;
+        if (mPosX < -mAvailableX) {
+            mDirX = Math.abs(mDirX);
+        } else if (mPosX > mAvailableX) {
+            mDirX = -Math.abs(mDirX);
+        }
+        if (mPosY < -mAvailableY) {
+            mDirY = Math.abs(mDirY);
+        } else if (mPosY > mAvailableY) {
+            mDirY = -Math.abs(mDirY);
+        }
+    }
+
+    /**
+     * Returns Model/View/Projection transform for the patch.
+     */
+    public float[] getTransform(@NonNull float[] vpMatrix) {
+        Matrix.setIdentityM(mMatrix, 0);
+        mMatrix[12] = mPosX;
+        mMatrix[13] = mPosY;
+        Matrix.multiplyMM(mMatrix, 0, vpMatrix, 0, mMatrix, 0);
+        return mMatrix;
+    }
+}
\ No newline at end of file
diff --git a/tests/GamePerformance/src/android/gameperformance/RenderPatchOpenGLTest.java b/tests/GamePerformance/src/android/gameperformance/RenderPatchOpenGLTest.java
new file mode 100644
index 0000000..7492cc0
--- /dev/null
+++ b/tests/GamePerformance/src/android/gameperformance/RenderPatchOpenGLTest.java
@@ -0,0 +1,188 @@
+/*
+ * 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 android.gameperformance;
+
+import java.util.List;
+
+import javax.microedition.khronos.opengles.GL;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.opengl.GLES20;
+import android.opengl.Matrix;
+
+/**
+ * Base class for all OpenGL based tests that use RenderPatch as a base.
+ */
+public abstract class RenderPatchOpenGLTest extends OpenGLTest {
+    private final float[] COLOR = new float[] { 1.0f, 1.0f, 1.0f, 1.0f };
+
+    private final String VERTEX_SHADER =
+            "uniform mat4 uMVPMatrix;"
+                    + "attribute vec4 vPosition;"
+                    + "attribute vec2 vTexture;"
+                    + "varying vec2 vTex;"
+                    + "void main() {"
+                    + "  vTex = vTexture;"
+                    + "  gl_Position = uMVPMatrix * vPosition;"
+                    + "}";
+
+    private final String FRAGMENT_SHADER =
+            "precision mediump float;"
+                    + "uniform sampler2D uTexture;"
+                    + "uniform vec4 uColor;"
+                    + "varying vec2 vTex;"
+                    + "void main() {"
+                    + "  vec4 color = texture2D(uTexture, vTex);"
+                    + "  gl_FragColor = uColor * color;"
+                    + "}";
+
+    private List<RenderPatchAnimation> mRenderPatches;
+
+    private int mProgram = -1;
+    private int mMVPMatrixHandle;
+    private int mTextureHandle;
+    private int mPositionHandle;
+    private int mColorHandle;
+    private int mTextureCoordHandle;
+
+    private final float[] mVPMatrix = new float[16];
+
+    public RenderPatchOpenGLTest(@NonNull GamePerformanceActivity activity) {
+        super(activity);
+    }
+
+    protected void setRenderPatches(@NonNull List<RenderPatchAnimation> renderPatches) {
+        mRenderPatches = renderPatches;
+    }
+
+    private void ensureInited() {
+        if (mProgram >= 0) {
+            return;
+        }
+
+        mProgram = OpenGLUtils.createProgram(VERTEX_SHADER, FRAGMENT_SHADER);
+
+        // get handle to fragment shader's uColor member
+        GLES20.glUseProgram(mProgram);
+        OpenGLUtils.checkGlError("useProgram");
+
+        // get handle to shape's transformation matrix
+        mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix");
+        OpenGLUtils.checkGlError("get uMVPMatrix");
+
+        mTextureHandle = GLES20.glGetUniformLocation(mProgram, "uTexture");
+        OpenGLUtils.checkGlError("uTexture");
+        // get handle to vertex shader's vPosition member
+        mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");
+        OpenGLUtils.checkGlError("vPosition");
+        mTextureCoordHandle = GLES20.glGetAttribLocation(mProgram, "vTexture");
+        OpenGLUtils.checkGlError("vTexture");
+        mColorHandle = GLES20.glGetUniformLocation(mProgram, "uColor");
+        OpenGLUtils.checkGlError("uColor");
+
+        mTextureHandle = OpenGLUtils.createTexture(getContext(), R.drawable.logo);
+
+        final float[] projectionMatrix = new float[16];
+        final float[] viewMatrix = new float[16];
+
+        final float ratio = getView().getRenderRatio();
+        Matrix.orthoM(projectionMatrix, 0, -ratio, ratio, -1, 1, -1, 1);
+        Matrix.setLookAtM(viewMatrix, 0, 0, 0, -0.5f, 0f, 0f, 0f, 0f, 1.0f, 0.0f);
+        Matrix.multiplyMM(mVPMatrix, 0, projectionMatrix, 0, viewMatrix, 0);
+    }
+
+    /**
+     * Returns global color for patch.
+     */
+    public float[] getColor() {
+        return COLOR;
+    }
+
+    /**
+     * Extra setup for particular tests.
+     */
+    public void onBeforeDraw(GL gl) {
+    }
+
+    @Override
+    public void draw(GL gl) {
+        ensureInited();
+
+        GLES20.glUseProgram(mProgram);
+        OpenGLUtils.checkGlError("useProgram");
+
+        GLES20.glDisable(GLES20.GL_BLEND);
+        OpenGLUtils.checkGlError("disableBlend");
+
+        GLES20.glEnableVertexAttribArray(mPositionHandle);
+        OpenGLUtils.checkGlError("enableVertexAttributes");
+
+        GLES20.glEnableVertexAttribArray(mTextureCoordHandle);
+        OpenGLUtils.checkGlError("enableTexturesAttributes");
+
+        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextureHandle);
+        OpenGLUtils.checkGlError("setTexture");
+
+        GLES20.glUniform4fv(mColorHandle, 1, getColor(), 0);
+        OpenGLUtils.checkGlError("setColor");
+
+        onBeforeDraw(gl);
+
+        for (final RenderPatchAnimation renderPatchAnimation : mRenderPatches) {
+
+            renderPatchAnimation.update(0.01f);
+            GLES20.glUniformMatrix4fv(mMVPMatrixHandle,
+                                      1,
+                                      false,
+                                      renderPatchAnimation.getTransform(mVPMatrix),
+                                      0);
+            OpenGLUtils.checkGlError("setTransform");
+
+            GLES20.glVertexAttribPointer(
+                    mPositionHandle,
+                    RenderPatch.VERTEX_COORD_COUNT,
+                    GLES20.GL_FLOAT,
+                    false /* normalized */,
+                    RenderPatch.VERTEX_STRIDE,
+                    renderPatchAnimation.getRenderPatch().getVertexBuffer());
+            OpenGLUtils.checkGlError("setVertexAttribute");
+
+            GLES20.glVertexAttribPointer(
+                    mTextureCoordHandle,
+                    RenderPatch.TEXTURE_COORD_COUNT,
+                    GLES20.GL_FLOAT,
+                    false /* normalized */,
+                    RenderPatch.TEXTURE_STRIDE,
+                    renderPatchAnimation.getRenderPatch().getTextureBuffer());
+            OpenGLUtils.checkGlError("setTextureAttribute");
+
+            // Draw the patch.
+            final int indicesCount =
+                    renderPatchAnimation.getRenderPatch().getIndexBuffer().capacity() /
+                    RenderPatch.SHORT_SIZE;
+            GLES20.glDrawElements(
+                    GLES20.GL_TRIANGLES,
+                    indicesCount,
+                    GLES20.GL_UNSIGNED_SHORT,
+                    renderPatchAnimation.getRenderPatch().getIndexBuffer());
+            OpenGLUtils.checkGlError("drawPatch");
+        }
+
+        GLES20.glDisableVertexAttribArray(mPositionHandle);
+        GLES20.glDisableVertexAttribArray(mTextureCoordHandle);
+    }
+}
\ No newline at end of file
diff --git a/tests/GamePerformance/src/android/gameperformance/TriangleCountOpenGLTest.java b/tests/GamePerformance/src/android/gameperformance/TriangleCountOpenGLTest.java
new file mode 100644
index 0000000..593f37b
--- /dev/null
+++ b/tests/GamePerformance/src/android/gameperformance/TriangleCountOpenGLTest.java
@@ -0,0 +1,66 @@
+/*
+ * 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 android.gameperformance;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import android.annotation.NonNull;
+
+/**
+ * Test that measures maximum amount of triangles can be rasterized keeping FPS close to the device
+ * refresh rate. It is has very few devices call and each call contains big amount of triangles.
+ * Total filling area is around one screen.
+ */
+public class TriangleCountOpenGLTest extends RenderPatchOpenGLTest  {
+    // Based on index buffer of short values.
+    private final static int MAX_TRIANGLES_IN_PATCH = 32000;
+
+    public TriangleCountOpenGLTest(@NonNull GamePerformanceActivity activity) {
+        super(activity);
+    }
+
+    @Override
+    public String getName() {
+        return "triangle_count";
+    }
+
+    @Override
+    public String getUnitName() {
+        return "ktriangles";
+    }
+
+    @Override
+    public double getUnitScale() {
+        return 2.0;
+    }
+
+    @Override
+    public void initUnits(double trianlgeCountD) {
+        final int triangleCount = (int)Math.round(trianlgeCountD * 1000.0);
+        final List<RenderPatchAnimation> renderPatches = new ArrayList<>();
+        final int patchCount =
+                (triangleCount + MAX_TRIANGLES_IN_PATCH - 1) / MAX_TRIANGLES_IN_PATCH;
+        final int patchTriangleCount = triangleCount / patchCount;
+        for (int i = 0; i < patchCount; ++i) {
+            final RenderPatch renderPatch = new RenderPatch(patchTriangleCount,
+                                                            0.5f /* dimension */,
+                                                            RenderPatch.TESSELLATION_TO_CENTER);
+            renderPatches.add(new RenderPatchAnimation(renderPatch, getView().getRenderRatio()));
+        }
+        setRenderPatches(renderPatches);
+    }
+}
\ No newline at end of file