How To Test Android Performance Using FPS

While doing performance testing of Light Racer 2, I had to figure out how to do the fastest common operations. One problem I found was that I was drawing a static background in a 32 bit color mode with transparency when it was much faster to draw it in 16 bit with none. Another thing I wanted to check for was to see what was faster for finding half of a number: Division by two or multiplication by point-five? This matters because I do a whole lot of that in the game to place graphics and find mid-points for various physics and AI stuff. I wasn't sure which would be faster because the ARM processor in a G1 has neither a hardware divider nor a floating point unit. I wrote this little utility to tell me how many frames per second I can get with various operations. Also - Divide by two is at least twice as fast.

The code is just the most basic layout, activity, surface view and thread that runs whatever you want and reports the average FPS for the last 10 frames. It's nice because I copy and paste various pieces of code in and loop it any number of times to find out how much CPU it eats up. I like to get things to drop the rate down to 30FPS because I know that it is eating 100% of my allocated CPU for a game. This helps when figuring out what is breaking the CPU budget for your game. I thought my AI was killing my game but it turned out that drawing too many full screen layers in ARGB_8888 was. I switched the base layer to RGB_565, which is the native output format and it made a world of difference. I found that out because I set up a looping test that would draw a black bitmap 10 times and I compared the FPS for all of the different modes.

Just put the stuff you want to test in the Thread and watch those FPS.

Here is my code:

res/layout/main.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="fill_parent"
android:layout_height="fill_parent">

<net.rbgrn.maxfps.MaxFPSView
android:layout_width="fill_parent" android:layout_height="fill_parent"
android:id="@+id/maxfps" />

</FrameLayout>

net.rbgrn.maxfps.MaxFPSActivity

package net.rbgrn.maxfps;

import android.app.Activity;
import android.os.Bundle;
import android.view.Window;
import android.view.WindowManager;

public class MaxFPSActivity extends Activity {

private MaxFPSView maxFpsView;

private MaxFPSThread maxFpsThread;;

protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
setContentView(R.layout.main);
}

@Override
protected void onStart() {
super.onStart();
startGame();
}

private void startGame() {
maxFpsView = (MaxFPSView) findViewById(R.id.maxfps);

// hack to force the surface view to set the parameters
maxFpsThread = maxFpsView.getThread();
maxFpsThread.setRunning(true);
maxFpsThread.start();
}
}

net.rbgrn.maxfps.MaxFPSThread

package net.rbgrn.maxfps;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.util.Log;
import android.view.SurfaceHolder;

public class MaxFPSThread extends Thread {
private static final String TAG = "MaxFPSThread";
public static final Bitmap.Config BITMAP_CONFIG = Bitmap.Config.ARGB_8888;
public static final Bitmap.Config FAST_BITMAP_CONFIG = Bitmap.Config.RGB_565;

private Context mContext;
private SurfaceHolder surfaceHolder;
private int width;
private int height;
private boolean isSurfaceCreated;
private boolean isRunning;
private boolean isPaused;

private long lastTickMs;
private long curTickMs;
private int tickDelta;

private long lastFrameDraw = 0;
private int frameSamplesCollected = 0;
private int frameSampleTime = 0;
private int fps = 0;

private Paint basePaint;
private Paint fpsPaint;
private Bitmap fullScreenBitmap;
private Bitmap layerBitmap;
private Canvas layerCanvas;

public MaxFPSThread(SurfaceHolder surfaceHolder, Context context) {
Log.d(TAG, "New Instance");
// get handles to some important objects
this.surfaceHolder = surfaceHolder;
this.mContext = context;
init();
}

public void setSurfaceCreated(boolean surfaceCreated) {
isSurfaceCreated = surfaceCreated;
}

public void setSurfaceSize(int width, int height) {
// synchronized to make sure these all change atomically
synchronized (surfaceHolder) {
if (width != this.width && height != this.height) {
this.width = width;
this.height = height;
}
}
}

public void setRunning(boolean b) {
isRunning = b;
}

public void pause() {
synchronized (surfaceHolder) {
isPaused = true;
}
}

public void unpause() {
synchronized (surfaceHolder) {
isPaused = false;
}
}

@Override
public void run() {
// wait for surface to become available
while (!isSurfaceCreated && isRunning) {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
}
}
// Set the last tick to right now.
lastTickMs = System.currentTimeMillis();
while (isRunning) {
while (isPaused && isRunning) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
// coming out of pause, we don't want to jump ahead so we have to set the last tick to now.
lastTickMs = System.currentTimeMillis();
}
Canvas c = null;
try {
synchronized (surfaceHolder) {
c = surfaceHolder.lockCanvas();
if (isSurfaceCreated) {
doDraw(c);
}
}
} catch (Throwable t) {
throw new RuntimeException(t);
} finally {
if (c != null) {
surfaceHolder.unlockCanvasAndPost(c);
}
}
}
cleanUp();
}

private void doDraw(Canvas canvas) {
int worldWidth = this.width;
int worldHeight = this.height;
Paint basePaint = this.basePaint;
canvas.drawRect(0, 0, worldWidth, worldHeight, basePaint);

// BEGIN TEST CODE
Bitmap fullScreenBitmap = this.fullScreenBitmap;
Canvas layerCanvas = this.layerCanvas;
if (fullScreenBitmap == null) {
// init
this.fullScreenBitmap = Bitmap.createBitmap(worldWidth, worldHeight, FAST_BITMAP_CONFIG);
fullScreenBitmap = this.fullScreenBitmap;
}
if (layerCanvas == null) {
this.layerBitmap = Bitmap.createBitmap(worldWidth, worldHeight, BITMAP_CONFIG);
this.layerCanvas = new Canvas(this.layerBitmap);
layerCanvas = this.layerCanvas;
}
int howMany = 0;
// Test full screen layer draws in different modes
for (int i = 0; i < howMany; i++) {
layerCanvas.drawBitmap(fullScreenBitmap, 0, 0, basePaint);
}
howMany = 100000;
// Test divide vs multiply
int a;
int numerator = 10;
for (int i = 0; i < howMany; i++) {
a = (int) (numerator * .5f);
}
// END TEST CODE
canvas.drawBitmap(layerBitmap, 0, 0, basePaint);
long now = System.currentTimeMillis();
if (lastFrameDraw != 0) {
int time = (int) (now - lastFrameDraw);
frameSampleTime += time;
frameSamplesCollected++;
if (frameSamplesCollected == 10) {
fps = (int) (10000 / frameSampleTime);
frameSampleTime = 0;
frameSamplesCollected = 0;
}
canvas.drawText(fps + " fps", worldWidth - 60, worldHeight - 20, fpsPaint);
}
lastFrameDraw = now;

}

private void init() {
basePaint = new Paint();
fpsPaint = new Paint();
fpsPaint.setARGB(255,255,255,255);
fpsPaint.setTextSize(16);
}

private void cleanUp() {
basePaint = null;
fpsPaint = null;
fullScreenBitmap = null;
}

}

net.rbgrn.maxfps.MaxFPSView

package net.rbgrn.maxfps;

import android.content.Context;
import android.graphics.PixelFormat;
import android.util.AttributeSet;
import android.util.Log;
import android.view.KeyEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.SurfaceHolder.Callback;

public class MaxFPSView extends SurfaceView implements Callback {
private static final String TAG = "LightRacerView";

/** The thread that actually draws the animation */
private MaxFPSThread thread;

private boolean isSurfaceCreated;

private int surfaceWidth, surfaceHeight;

public MaxFPSView(Context context, AttributeSet attrs) {
super(context, attrs);
Log.d(TAG, "New Instance");
// register our interest in hearing about changes to our surface
SurfaceHolder holder = getHolder();
holder.setFormat(PixelFormat.OPAQUE);
// holder.setType(SurfaceHolder.SURFACE_TYPE_GPU);
holder.addCallback(this);

setFocusable(true); // make sure we get key events
}

/**
* Fetches the game thread corresponding to this LightRacerView.
*
* @return the game thread
*/
public MaxFPSThread getThread() {
if (thread == null || !thread.isAlive()) {
// create thread only; it's started in surfaceCreated()
thread = new MaxFPSThread(getHolder(), getContext());
updateThreadSurfaceState();
if (surfaceHeight > 0 && surfaceWidth > 0) {
thread.setSurfaceSize(surfaceWidth, surfaceHeight);
}
}
return thread;
}

/**
* Standard window-focus override. Notice focus lost so we can pause on focus lost. e.g. user switches to take a
* call.
*/
@Override
public void onWindowFocusChanged(boolean hasWindowFocus) {
if (!hasWindowFocus) {
if (thread != null) {
thread.pause();
}
} else {
if (thread != null) {
thread.unpause();
}
}
}

/* Callback invoked when the surface dimensions change. */
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
surfaceWidth = width;
surfaceHeight = height;
thread.setSurfaceSize(width, height);
}

/*
* Callback invoked when the Surface has been created and is ready to be used.
*/
public void surfaceCreated(SurfaceHolder holder) {
isSurfaceCreated = true;
updateThreadSurfaceState();
if (!hasFocus()) {
requestFocusFromTouch();
}
}

/*
* Callback invoked when the Surface has been destroyed and must no longer be touched. WARNING: after this method
* returns, the Surface/Canvas must never be touched again!
*/
public void surfaceDestroyed(SurfaceHolder holder) {
isSurfaceCreated = false;
updateThreadSurfaceState();
// we have to tell thread to shut down & wait for it to finish, or else
// it might touch the Surface after we return and explode
}

private void updateThreadSurfaceState() {
if (thread != null) {
thread.setSurfaceCreated(isSurfaceCreated);
}
}

private void stopThread() {
if (thread != null) {
Log.d(TAG, "Stopping Thread");
boolean retry = true;
thread.setRunning(false);
while (retry) {
try {
thread.join();
retry = false;
} catch (InterruptedException e) {
}
}
Log.d(TAG, "Thread Stopped");
}
}

public void releaseAllResources() {
Log.d(TAG, "Releasing Resources");
if (getHolder() != null) {
getHolder().removeCallback(this);
}
stopThread();
}
}

To use this, I just created a default android project and then dumped these classes and the layout in. You'll need to change your manifest to point at MaxFPSActivity as the launch activity and it should all work.

Enjoy!

13 Comments

Post a comment here or discuss this and other topics in the forums

Hey, im having the same

Hey, im having the same problem..
but not sure where and what's wrong with it..

really appreciate some solutions...

Great article

After just skimming through the first section of this article, I changed my images from ARGB_8888 to RGB_565, and this more than doubled my frame rate.

So happy I found this page.

Performance numbers

Hi, I'm the previous Anonymous poster with the 'More performance numbers' post. I don't know why it said Anonymous, it should have said Alex Berg, perhaps its because I don't have an account...

Ben, that's very nice info, I didn't realize there were cheaper random generators available, though in retrospect i suppose that's obvious as we don't require the super good pseudorandomness in games. Previously I have made a table containing a precalculated set of randoms. The table-idea is still valid, because then I can also precompute the Math.sin and Math.cos for those random numbers, but otherwise tables with randoms also sucks because it's actually rather expensive to make a lookup in the table.

I'll definitely replace my random calls with one of the cheaper methods you mentioned.

Alex

Hmm.. I'd need more info than

Hmm.. I'd need more info than that. Could you paste more of the stack trace for me?

wont run

I copied and pasted your code exactly
Android tells me that the application stopped unexpectedly
I debugged the program and found that the problem is at
setContentView(R.layout.main)

Do you have any suggestions

Random number generation

Following on from Anonymous' excellent data. Let me add my results from testing different random number generators.

This is running on the emulator using Android 1.5
(double, int, long) is the return value of the method.

100,000 calls to:

Algoithm Return Time taken
Math.Random double 2288 ms
LCGRandom int 541 ms
XORShiftRandom long 556 ms
Mersenne-Twister int 1466 ms
Mersenne-Twister float 1598 ms
Mersenne-Twister double 3001 ms

In summary, if you want integers then use the XORShift (LCG is not a good algorithm considering you get better results from the XORShift for roughly the same cost). If you need floats etc. then use the Mersenne-Twister.

If somebody would like the code to run on a real Android please contact me at ben (dot) hesketh (at) gmail (dot) com

Sources:
LCG is the simple Linear Congruential Generator
(http://en.wikipedia.org/wiki/Linear_congruential_generator)

Mersenne-Twister (http://en.wikipedia.org/wiki/Mersenne_twister) implementation is from here
(http://www.math.sci.hiroshima-u.ac.jp/~m-mat/MT/VERSIONS/JAVA/MTRandom.java)

XORShift is from here (http://www.javamex.com/tutorials/random_numbers/xorshift.shtml)

Did you remove most of my

Did you remove most of my test code first? In my example, there is an extra layer drawn and also 10,000 calls for some math. I marked up the code with BEGIN TEST CODE and END TEST CODE so you could see where you could cut out all of my tests and put in your own.

Low fps

Thanks for great code, I have low fps problem when I try to run MaxFPSActivity without any modification, then I got 2 fps on screen, after that, I try to modify
MaxFPSThread.doDraw(Canvas) method, just draw a 320x480 bitmap, actually I scaled it from 320x80, again run MaxFPSActivity, then I got 0 fps, is there somthing wrongs? thanks for any reply.

B.J.

Wow, thank you for that

Wow, thank you for that excellent data! I formatted it into a table and am assuming that the comma is a decimal point.

I find the long division result and also the random number generator results very interesting.

Perhaps there is a faster RNG than Java's Random? I use a lot of RNG in my code and I didn't realize that it had such a performance hit.

More performance numbers

I find the above method and MaxFPS program useful if I need to compare two specific methods. However I find it more useful to test the speed various relevant basic functions, so I now the speed of each function compared to each other, and so I can make informed desicions when coding the rest of the game. I ran a test for 100 million basic operations on ints, longs, floats and doubles, as well as some basic other functions. The tests was executed on an the Android Developer 1 phone (ie. the G1), with 1.5 firmware. I got the following results:

Ints Longs Floats Doubles
Addition 1 2,12 4,9 6,22
Subtraction 0,98 2,12 5,04 6,31
Multiplication 1,08 2,8 4,14 4,98
Division 2,49 16,08 3,55 4,73
Comparison 7,18 7,33 9,73 11,92
Cast to int 0,69 2,22 2,1
random 41,86 78,59 52,88 91,04
Math.sin/cos 807,1

The table above is pretty unreadable, so you'll have to paste the results to your favorite spreadsheet, or similar. The random-row shows the time it takes to generate random.nextInt(), random.nextLong(), random.nextFloat(), random.nextDouble(). It can be seen that the execution time is approximately linear in the number of random bits which are generated (long and doubles have 64bits, int and float 32bits). I gather from this that my measurements are reasonably sound.

All values above are the time the operation takes compared to that of the integer addition. There are minor variations in the test results, as is seen by the difference in execution time of addition and subtraction (which should be identical).

I also made the following measurements (again relative to integer addition):
read static field 6,9
read field 12,9
write int[] 13,9
read int[] 10,4
These numbers are relevant, if one for instance considers making an array of 1000 random numbers and 1000 Math.sin and Math.cos computations, so one can see the cost of looking up an array compared to that of random.nextInt() for instance.

I have not because it's such

I have not because it's such a subjective thing. Each game would have to be implemented in OpenGL a little different. Mostly I'm assuming that people use 2-faced, billboarded (kinda) polys that hold the texture that is to look like 2d. The thing I really like about that is the z-order you get, which is something I had issues with when designing my game object interface. For example, my racers have 3 different layers: Trails, the Racer and the Explosion. I wasn't able to use the update,draw flow exactly like that on them. It was more like update,drawTrail, drawRacer, drawExplosion. I would have been able to do that cleaner in GL.

Canvas vs. OpenGL

Some 2d games are done using OpenGL. Have you compared that with the 2d calls you're currently doing to see which would be better in terms of performance and looks?

Thanks!

Thanks for posting this! this should be VERY handy!