Merge "Add initial GamePerformance test."
This commit is contained in:
39
tests/GamePerformance/Android.mk
Normal file
39
tests/GamePerformance/Android.mk
Normal file
@@ -0,0 +1,39 @@
|
||||
# Copyright (C) 2018 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)
|
||||
|
||||
# Don't include this package in any target
|
||||
LOCAL_MODULE_TAGS := tests
|
||||
|
||||
LOCAL_DEX_PREOPT := false
|
||||
|
||||
LOCAL_PROGUARD_ENABLED := disabled
|
||||
|
||||
LOCAL_SRC_FILES := $(call all-java-files-under, src)
|
||||
|
||||
LOCAL_STATIC_JAVA_LIBRARIES := android-support-test
|
||||
|
||||
LOCAL_JAVA_LIBRARIES := android.test.base android.test.runner
|
||||
|
||||
LOCAL_PACKAGE_NAME := GamePerformance
|
||||
|
||||
LOCAL_PRIVATE_PLATFORM_APIS := true
|
||||
|
||||
LOCAL_CERTIFICATE := platform
|
||||
|
||||
|
||||
include $(BUILD_PACKAGE)
|
||||
40
tests/GamePerformance/AndroidManifest.xml
Normal file
40
tests/GamePerformance/AndroidManifest.xml
Normal file
@@ -0,0 +1,40 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
* Copyright (C) 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
-->
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="android.gameperformance">
|
||||
<uses-sdk android:minSdkVersion="25"/>
|
||||
<uses-feature android:glEsVersion="0x00020000" android:required="true" />
|
||||
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<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" >
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<uses-library android:name="android.test.runner" />
|
||||
</application>
|
||||
|
||||
<!-- self-instrumenting test package. -->
|
||||
<instrumentation android:name="android.test.InstrumentationTestRunner"
|
||||
android:targetPackage="android.gameperformance">
|
||||
</instrumentation>
|
||||
</manifest>
|
||||
25
tests/GamePerformance/res/values/themes.xml
Normal file
25
tests/GamePerformance/res/values/themes.xml
Normal file
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
* Copyright (C) 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
-->
|
||||
<resources>
|
||||
<style name="noeffects" parent="@android:style/Theme.Holo.NoActionBar.Fullscreen">
|
||||
<item name="android:windowNoTitle">true</item>
|
||||
<item name="android:windowFullscreen">true</item>
|
||||
<item name="android:fadingEdge">none</item>
|
||||
<item name="android:windowContentTransitions">false</item>
|
||||
<item name="android:windowAnimationStyle">@null</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* Copyright (C) 2018 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.io.BufferedReader;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
|
||||
import android.app.Instrumentation;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* Helper that runs atrace command for required duration and category. Results are read from
|
||||
* the output of atrace and serialized to the provided file. We cannot use direct atrace to
|
||||
* file because atrace is executed in UI automator context and analysis is done in test context.
|
||||
* In last case output file is not accessible from the both contexts.
|
||||
*/
|
||||
public class ATraceRunner extends AsyncTask<Void, Integer, Boolean>{
|
||||
private final static String TAG = "ATraceRunner";
|
||||
|
||||
// Report that atrace is done.
|
||||
public interface Delegate {
|
||||
public void onProcessed(boolean success);
|
||||
}
|
||||
|
||||
private final Instrumentation mInstrumentation;
|
||||
private final String mOutput;
|
||||
private final int mTimeInSeconds;
|
||||
private final String mCategory;
|
||||
private final Delegate mDelegate;
|
||||
|
||||
public ATraceRunner(Instrumentation instrumentation,
|
||||
String output,
|
||||
int timeInSeconds,
|
||||
String category,
|
||||
Delegate delegate) {
|
||||
mInstrumentation = instrumentation;
|
||||
mOutput = output;
|
||||
mTimeInSeconds = timeInSeconds;
|
||||
mCategory = category;
|
||||
mDelegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Boolean doInBackground(Void... params) {
|
||||
BufferedReader bufferedReader = null;
|
||||
FileWriter writer = null;
|
||||
try {
|
||||
// Run the command.
|
||||
final String cmd = "atrace -t " + mTimeInSeconds + " " + mCategory;
|
||||
Log.i(TAG, "Running atrace... " + cmd);
|
||||
writer = new FileWriter(mOutput);
|
||||
final ParcelFileDescriptor fd =
|
||||
mInstrumentation.getUiAutomation().executeShellCommand(cmd);
|
||||
bufferedReader = new BufferedReader(
|
||||
new InputStreamReader(new ParcelFileDescriptor.AutoCloseInputStream(fd)));
|
||||
String line;
|
||||
while ((line = bufferedReader.readLine()) != null) {
|
||||
writer.write(line);
|
||||
writer.write("\n");
|
||||
}
|
||||
Log.i(TAG, "Running atrace... DONE");
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
Log.i(TAG, "atrace failed", e);
|
||||
return false;
|
||||
} finally {
|
||||
Utils.closeQuietly(bufferedReader);
|
||||
Utils.closeQuietly(writer);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Boolean result) {
|
||||
mDelegate.onProcessed(result);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Copyright (C) 2018 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.Random;
|
||||
|
||||
import javax.microedition.khronos.egl.EGLConfig;
|
||||
import javax.microedition.khronos.opengles.GL10;
|
||||
|
||||
import android.content.Context;
|
||||
import android.opengl.GLES20;
|
||||
import android.opengl.GLSurfaceView;
|
||||
|
||||
public class CustomOpenGLView extends GLSurfaceView {
|
||||
private Random mRandom;
|
||||
private List<Long> mFrameTimes;
|
||||
|
||||
public CustomOpenGLView(Context context) {
|
||||
super(context);
|
||||
|
||||
mRandom = new Random();
|
||||
mFrameTimes = new ArrayList<Long>();
|
||||
|
||||
setEGLContextClientVersion(2);
|
||||
|
||||
setRenderer(new GLSurfaceView.Renderer() {
|
||||
@Override
|
||||
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
|
||||
GLES20.glClearColor(1.0f, 0.0f, 0.0f, 1.0f);
|
||||
gl.glClearDepthf(1.0f);
|
||||
gl.glEnable(GL10.GL_DEPTH_TEST);
|
||||
gl.glDepthFunc(GL10.GL_LEQUAL);
|
||||
|
||||
gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT,
|
||||
GL10.GL_NICEST); }
|
||||
|
||||
@Override
|
||||
public void onSurfaceChanged(GL10 gl, int width, int height) {
|
||||
GLES20.glViewport(0, 0, width, height);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDrawFrame(GL10 gl) {
|
||||
GLES20.glClearColor(
|
||||
mRandom.nextFloat(), mRandom.nextFloat(), mRandom.nextFloat(), 1.0f);
|
||||
gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
|
||||
synchronized (mFrameTimes) {
|
||||
mFrameTimes.add(System.currentTimeMillis());
|
||||
}
|
||||
}
|
||||
});
|
||||
setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets frame times in order to calculate fps for 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
/*
|
||||
* Copyright (C) 2018 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.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import android.os.Trace;
|
||||
import android.view.Surface;
|
||||
import android.view.SurfaceHolder;
|
||||
import android.view.SurfaceView;
|
||||
|
||||
/**
|
||||
* Minimal SurfaceView that sends buffer on request.
|
||||
*/
|
||||
public class CustomSurfaceView extends SurfaceView implements SurfaceHolder.Callback {
|
||||
// Tag for trace when buffer is requested.
|
||||
public final static String LOCAL_REQUEST_BUFFER = "localRequestBuffer";
|
||||
// Tag for trace when buffer is posted.
|
||||
public final static String LOCAL_POST_BUFFER = "localPostBuffer";
|
||||
|
||||
private final Object mSurfaceLock = new Object();
|
||||
// Keeps frame times. Used to calculate fps.
|
||||
private List<Long> mFrameTimes;
|
||||
// Surface to send.
|
||||
private Surface mSurface;
|
||||
private Handler mHandler;
|
||||
|
||||
private Runnable mInvalidateSurfaceTask = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
synchronized (mSurfaceLock) {
|
||||
if (mSurface == null) {
|
||||
return;
|
||||
}
|
||||
invalidateSurface(true, true);
|
||||
mHandler.post(this);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public CustomSurfaceView(Context context) {
|
||||
super(context);
|
||||
mFrameTimes = new ArrayList<Long>();
|
||||
getHolder().addCallback(this);
|
||||
getHolder().setFormat(PixelFormat.OPAQUE);
|
||||
|
||||
HandlerThread thread = new HandlerThread("SurfaceInvalidator");
|
||||
thread.start();
|
||||
mHandler = new Handler(thread.getLooper());
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets frame times in order to calculate fps for different test pass.
|
||||
*/
|
||||
public void resetFrameTimes() {
|
||||
synchronized (mSurfaceLock) {
|
||||
mFrameTimes.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns current fps based on collected frame times.
|
||||
*/
|
||||
public double getFps() {
|
||||
synchronized (mSurfaceLock) {
|
||||
if (mFrameTimes.size() < 2) {
|
||||
return 0.0f;
|
||||
}
|
||||
return 1000.0 * mFrameTimes.size() /
|
||||
(mFrameTimes.get(mFrameTimes.size() - 1) - mFrameTimes.get(0));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates surface.
|
||||
* @param traceCalls set to true in case we need register trace calls. Not used for warm-up.
|
||||
* @param drawFps perform drawing current fps on surface to have some payload on surface.
|
||||
*/
|
||||
public void invalidateSurface(boolean traceCalls, boolean drawFps) {
|
||||
synchronized (mSurfaceLock) {
|
||||
if (mSurface == null) {
|
||||
throw new IllegalStateException("Surface is not ready");
|
||||
}
|
||||
if (traceCalls) {
|
||||
Trace.traceBegin(Trace.TRACE_TAG_GRAPHICS, LOCAL_REQUEST_BUFFER);
|
||||
}
|
||||
Canvas canvas = mSurface.lockCanvas(null);
|
||||
if (traceCalls) {
|
||||
Trace.traceEnd(Trace.TRACE_TAG_GRAPHICS);
|
||||
}
|
||||
|
||||
if (drawFps) {
|
||||
int textSize = canvas.getHeight() / 24;
|
||||
Paint paint = new Paint();
|
||||
paint.setTextSize(textSize);
|
||||
paint.setColor(0xFFFF8040);
|
||||
canvas.drawARGB(92, 255, 255, 255);
|
||||
canvas.drawText("FPS: " + String.format("%.2f", getFps()), 10, 300, paint);
|
||||
}
|
||||
|
||||
if (traceCalls) {
|
||||
Trace.traceBegin(Trace.TRACE_TAG_GRAPHICS, LOCAL_POST_BUFFER);
|
||||
}
|
||||
mSurface.unlockCanvasAndPost(canvas);
|
||||
if (traceCalls) {
|
||||
Trace.traceEnd(Trace.TRACE_TAG_GRAPHICS);
|
||||
}
|
||||
|
||||
mFrameTimes.add(System.currentTimeMillis());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait until surface is created and ready to use or return immediately if surface
|
||||
* already exists.
|
||||
*/
|
||||
public void waitForSurfaceReady() {
|
||||
synchronized (mSurfaceLock) {
|
||||
if (mSurface == null) {
|
||||
try {
|
||||
mSurfaceLock.wait(5000);
|
||||
} catch(InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
if (mSurface == null)
|
||||
throw new IllegalStateException("Surface is not ready.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits until surface is destroyed or return immediately if surface does not exist.
|
||||
*/
|
||||
public void waitForSurfaceDestroyed() {
|
||||
synchronized (mSurfaceLock) {
|
||||
if (mSurface != null) {
|
||||
try {
|
||||
mSurfaceLock.wait(5000);
|
||||
} catch(InterruptedException e) {
|
||||
}
|
||||
}
|
||||
if (mSurface != null)
|
||||
throw new IllegalStateException("Surface still exists.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void surfaceCreated(SurfaceHolder holder) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
|
||||
// This method is always called at least once, after surfaceCreated.
|
||||
synchronized (mSurfaceLock) {
|
||||
mSurface = holder.getSurface();
|
||||
mSurfaceLock.notify();
|
||||
mHandler.post(mInvalidateSurfaceTask);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void surfaceDestroyed(SurfaceHolder holder) {
|
||||
synchronized (mSurfaceLock) {
|
||||
mHandler.removeCallbacks(mInvalidateSurfaceTask);
|
||||
mSurface = null;
|
||||
mSurfaceLock.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
* Copyright (C) 2018 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.concurrent.CountDownLatch;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.graphics.Rect;
|
||||
import android.os.Bundle;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.RelativeLayout;
|
||||
|
||||
/**
|
||||
* Minimal activity that holds SurfaceView or GLSurfaceView.
|
||||
* call attachSurfaceView or attachOpenGLView to switch views.
|
||||
*/
|
||||
public class GamePerformanceActivity extends Activity {
|
||||
private CustomSurfaceView mSurfaceView = null;
|
||||
private CustomOpenGLView mOpenGLView = null;
|
||||
private RelativeLayout mRootLayout;
|
||||
|
||||
public void attachSurfaceView() throws InterruptedException {
|
||||
synchronized (mRootLayout) {
|
||||
if (mSurfaceView != null) {
|
||||
return;
|
||||
}
|
||||
final CountDownLatch latch = new CountDownLatch(1);
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (mOpenGLView != null) {
|
||||
mRootLayout.removeView(mOpenGLView);
|
||||
mOpenGLView = null;
|
||||
}
|
||||
mSurfaceView = new CustomSurfaceView(GamePerformanceActivity.this);
|
||||
mRootLayout.addView(mSurfaceView);
|
||||
latch.countDown();
|
||||
}
|
||||
});
|
||||
latch.await();
|
||||
mSurfaceView.waitForSurfaceReady();
|
||||
}
|
||||
}
|
||||
|
||||
public void attachOpenGLView() throws InterruptedException {
|
||||
synchronized (mRootLayout) {
|
||||
if (mOpenGLView != null) {
|
||||
return;
|
||||
}
|
||||
final CountDownLatch latch = new CountDownLatch(1);
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (mSurfaceView != null) {
|
||||
mRootLayout.removeView(mSurfaceView);
|
||||
mSurfaceView = null;
|
||||
}
|
||||
mOpenGLView = new CustomOpenGLView(GamePerformanceActivity.this);
|
||||
mRootLayout.addView(mOpenGLView);
|
||||
latch.countDown();
|
||||
}
|
||||
});
|
||||
latch.await();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||
|
||||
// To layouts in parent. First contains list of Surfaces and second
|
||||
// controls. Controls stay on top.
|
||||
mRootLayout = new RelativeLayout(this);
|
||||
mRootLayout.setLayoutParams(new ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT));
|
||||
|
||||
Rect rect = new Rect();
|
||||
getWindow().getDecorView().getWindowVisibleDisplayFrame(rect);
|
||||
|
||||
mOpenGLView = new CustomOpenGLView(this);
|
||||
mRootLayout.addView(mOpenGLView);
|
||||
|
||||
setContentView(mRootLayout);
|
||||
}
|
||||
|
||||
public void resetFrameTimes() {
|
||||
if (mSurfaceView != null) {
|
||||
mSurfaceView.resetFrameTimes();
|
||||
} else if (mOpenGLView != null) {
|
||||
mOpenGLView.resetFrameTimes();
|
||||
} else {
|
||||
throw new IllegalStateException("Nothing attached");
|
||||
}
|
||||
}
|
||||
|
||||
public double getFps() {
|
||||
if (mSurfaceView != null) {
|
||||
return mSurfaceView.getFps();
|
||||
} else if (mOpenGLView != null) {
|
||||
return mOpenGLView.getFps();
|
||||
} else {
|
||||
throw new IllegalStateException("Nothing attached");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright (C) 2018 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.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Trace;
|
||||
import android.test.ActivityInstrumentationTestCase2;
|
||||
import android.test.suitebuilder.annotation.SmallTest;
|
||||
import android.util.Log;
|
||||
|
||||
public class GamePerformanceTest extends
|
||||
ActivityInstrumentationTestCase2<GamePerformanceActivity> {
|
||||
private final static String TAG = "GamePerformanceTest";
|
||||
|
||||
private final static int GRAPHIC_BUFFER_WARMUP_LOOP_CNT = 60;
|
||||
|
||||
public GamePerformanceTest() {
|
||||
super(GamePerformanceActivity.class);
|
||||
}
|
||||
|
||||
@SmallTest
|
||||
public void testGraphicBufferMetrics() throws IOException, InterruptedException {
|
||||
Bundle status = new Bundle();
|
||||
|
||||
for (int i = 0; i < 2; ++i) {
|
||||
if (i == 0) {
|
||||
getActivity().attachSurfaceView();
|
||||
} else {
|
||||
getActivity().attachOpenGLView();
|
||||
}
|
||||
|
||||
// Perform warm-up.
|
||||
Thread.sleep(2000);
|
||||
|
||||
// Once atrace is done, this one is triggered.
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
|
||||
final String passTag = i == 0 ? "surface" : "opengl";
|
||||
final String output = (new File(getInstrumentation().getContext().getFilesDir(),
|
||||
"atrace_" + passTag + ".log")).getAbsolutePath();
|
||||
Log.i(TAG, "Collecting traces to " + output);
|
||||
new ATraceRunner(getInstrumentation(), output, 5, "gfx", new ATraceRunner.Delegate() {
|
||||
@Override
|
||||
public void onProcessed(boolean success) {
|
||||
latch.countDown();
|
||||
}
|
||||
}).execute();
|
||||
|
||||
// Reset frame times and perform invalidation loop while atrace is running.
|
||||
getActivity().resetFrameTimes();
|
||||
latch.await();
|
||||
|
||||
// Copy results.
|
||||
final Map<String, Double> metrics =
|
||||
GraphicBufferMetrics.processGraphicBufferResult(output, passTag);
|
||||
for (Map.Entry<String, Double> metric : metrics.entrySet()) {
|
||||
status.putDouble(metric.getKey(), metric.getValue());
|
||||
}
|
||||
// Also record FPS.
|
||||
status.putDouble(passTag + "_fps", getActivity().getFps());
|
||||
}
|
||||
|
||||
getInstrumentation().sendStatus(Activity.RESULT_OK, status);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,530 @@
|
||||
/*
|
||||
* Copyright (C) 2018 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.io.BufferedReader;
|
||||
import java.io.FileReader;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.StringTokenizer;
|
||||
import java.util.TreeMap;
|
||||
import java.util.TreeSet;
|
||||
|
||||
/**
|
||||
* Utility class that performs analysis of atrace logs. This is implemented without Android
|
||||
* dependencies and therefore can be used in stand-alone mode.
|
||||
* Idea of this is to track atrace gfx event from graphics buffer producer/consumer.
|
||||
* We analyze here from surfaceflinger
|
||||
* queueBuffer - event when buffer was queued.
|
||||
* acquireBuffer - event when buffer was requested for composition.
|
||||
* releaseBuffer - even when buffer was released after composition.
|
||||
* This also track events, issued locally
|
||||
* localPostBuffer - event when buffer was posted from the local app.
|
||||
*
|
||||
* queueBuffer, acquireBuffer, releaseBuffer is accompanied with buffer name so we
|
||||
* can track life-cycle of particular buffer.
|
||||
* We don't have such information for localPostBuffer, however we can track next queueBuffer
|
||||
* from surfaceflinger corresponds to previous localPostBuffer.
|
||||
*
|
||||
* Following results are calculated:
|
||||
* post_time_[min/max/avr]_mcs - time for localPostBuffer duration.
|
||||
* ready_time_[min/max/avr]_mcs - time from localPostBuffer to when buffer was acquired by
|
||||
* surfaceflinger.
|
||||
* latency_[min/max/avr]_mcs - time from localPostBuffer to when buffer was released by
|
||||
* surfaceflinger.
|
||||
* missed_frame_percents - percentage of missed frames (frames that do not have right sequence
|
||||
* of events).
|
||||
*
|
||||
* Following is example of atrace logs from different platforms
|
||||
* <...>-5237 (-----) [000] ...1 228.380392: tracing_mark_write: B|11|SurfaceView - android.gameperformance/android.gameperformance.GamePerformanceActivity#0: 2
|
||||
* surfaceflinger-5855 ( 5855) [001] ...1 169.627364: tracing_mark_write: B|24|acquireBuffer
|
||||
* HwBinder:617_2-652 ( 617) [002] d..1 360262.921756: sde_evtlog: 617|sde_encoder_virt_atomic_check:855|19|0|0|0|0|0|0|0|0|0|0|0|0|0|0
|
||||
*/
|
||||
public class GraphicBufferMetrics {
|
||||
private final static String TAG = "GraphicBufferMetrics";
|
||||
|
||||
private final static String KEY_POST_TIME = "post_time";
|
||||
private final static String KEY_READY_TIME = "ready_time";
|
||||
private final static String KEY_LATENCY = "latency";
|
||||
private final static String SUFFIX_MIN = "min";
|
||||
private final static String SUFFIX_MAX = "max";
|
||||
private final static String SUFFIX_MEDIAN = "median";
|
||||
private final static String KEY_MISSED_FRAME_RATE = "missed_frame_percents";
|
||||
|
||||
private final static int EVENT_POST_BUFFER = 0;
|
||||
private final static int EVENT_QUEUE_BUFFER = 1;
|
||||
private final static int EVENT_ACQUIRE_BUFFER = 2;
|
||||
private final static int EVENT_RELEASE_BUFFER = 3;
|
||||
|
||||
// atrace prints this line. Used as a marker to make sure that we can parse its output.
|
||||
private final static String ATRACE_HEADER =
|
||||
"# TASK-PID TGID CPU# |||| TIMESTAMP FUNCTION";
|
||||
|
||||
private final static String[] KNOWN_PHRASES = new String[] {
|
||||
"capturing trace... done", "TRACE:"};
|
||||
private final static List<String> KNWON_PHRASES_LIST = Arrays.asList(KNOWN_PHRASES);
|
||||
|
||||
// Type of the atrace event we can parse and analyze.
|
||||
private final static String FUNCTION_TRACING_MARK_WRITE = "tracing_mark_write";
|
||||
|
||||
// Trace event we can ignore. It contains current timestamp information for the atrace output.
|
||||
private final static String TRACE_EVENT_CLOCK_SYNC = "trace_event_clock_sync:";
|
||||
|
||||
// Threshold we consider test passes successfully. If we cannot collect enough amount of frames
|
||||
// let fail the test. 50 is calculated 10 frames per second running for five seconds.
|
||||
private final static int MINIMAL_SAMPLE_CNT_TO_PASS = 50;
|
||||
|
||||
/**
|
||||
* Raw event in atrace. Stored hierarchically.
|
||||
*/
|
||||
private static class RawEvent {
|
||||
// Parent of this event or null for the root holder.
|
||||
public final RawEvent mParent;
|
||||
// Time of the event in mcs.
|
||||
public final long mTime;
|
||||
// Duration of the event in mcs.
|
||||
public long mDuration;
|
||||
// Name/body of the event.
|
||||
public final String mName;
|
||||
// Children events.
|
||||
public final List<RawEvent> mChildren;
|
||||
|
||||
public RawEvent(RawEvent parent, long time, String name) {
|
||||
mParent = parent;
|
||||
mTime = time;
|
||||
mName = name;
|
||||
mDuration = -1;
|
||||
mChildren = new ArrayList<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively finds child events.
|
||||
* @param path specify path to events. For example a/b. That means to find child with name
|
||||
* 'a' of the current event and in this child find child with name 'b'. Path
|
||||
* can consist from only one segment and that means we analyze only children of
|
||||
* the current event.
|
||||
* @param collector to collect found events.
|
||||
*/
|
||||
public void findEvents(String path, List<RawEvent> collector) {
|
||||
final int separator = path.indexOf('/');
|
||||
final String current = separator > 0 ? path.substring(0, separator) : path;
|
||||
final String nextPath = separator > 0 ? path.substring(separator + 1) : null;
|
||||
for (RawEvent child : mChildren) {
|
||||
if (child.mName.equals(current)) {
|
||||
if (nextPath != null) {
|
||||
child.findEvents(nextPath, collector);
|
||||
} else {
|
||||
collector.add(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void dump(String prefix) {
|
||||
System.err.print(prefix);
|
||||
System.err.println(mTime + "[" + mDuration + "] " + mName);
|
||||
for (RawEvent e : mChildren) {
|
||||
e.dump(prefix + " ");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes graphic buffer event. local post, queued, acquired, released.
|
||||
*/
|
||||
private static class BufferEvent {
|
||||
public final int mType;
|
||||
public final long mTime;
|
||||
public final long mDuration;
|
||||
public final String mBufferId;
|
||||
|
||||
public BufferEvent(int type, long time, long duration, String bufferId) {
|
||||
mType = type;
|
||||
mTime = time;
|
||||
mDuration = duration;
|
||||
mBufferId = bufferId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Type: " + mType + ". Time: " + mTime +
|
||||
"[" + mDuration + "]. Buffer: " + mBufferId + ".";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if given char is digit.
|
||||
*/
|
||||
private static boolean isDigitChar(char c) {
|
||||
return (c >= '0') && (c <= '9');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if given char is digit or '.'.
|
||||
*/
|
||||
private static boolean isDoubleDigitChar(char c) {
|
||||
return (c == '.') || isDigitChar(c);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert timestamp string that represents double value in seconds to long time that represents
|
||||
* timestamp in microseconds.
|
||||
*/
|
||||
private static long getTimeStamp(String timeStampStr) {
|
||||
return (long)(1000000.0 * Double.parseDouble(timeStampStr));
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads atrace log and build event model. Result is a map, where key specifies event for the
|
||||
* particular thread. Value is the synthetic root RawEvent that holds all events for the
|
||||
* thread. Events are stored hierarchically.
|
||||
*/
|
||||
private static Map<Integer, RawEvent> buildEventModel(String fileName) throws IOException {
|
||||
Map<Integer, RawEvent> result = new HashMap<>();
|
||||
|
||||
BufferedReader bufferedReader = null;
|
||||
String line = "";
|
||||
boolean headerDetected = false;
|
||||
try {
|
||||
bufferedReader = new BufferedReader(new FileReader(fileName));
|
||||
while ((line = bufferedReader.readLine()) != null) {
|
||||
// Make sure we find comment that describes output format we can with.
|
||||
headerDetected |= line.equals(ATRACE_HEADER);
|
||||
// Skip comments.
|
||||
if (line.startsWith("#")) {
|
||||
continue;
|
||||
}
|
||||
// Skip known service output
|
||||
if (KNWON_PHRASES_LIST.contains(line)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!headerDetected) {
|
||||
// We don't know the format of this line.
|
||||
throw new IllegalStateException("Header was not detected");
|
||||
}
|
||||
|
||||
// TASK-PID in header exists at position 12. PID position 17 should contains first
|
||||
// digit of thread id after the '-'.
|
||||
if (!isDigitChar(line.charAt(17)) || line.charAt(16) != '-') {
|
||||
throw new IllegalStateException("Failed to parse thread id: " + line);
|
||||
}
|
||||
int rightIndex = line.indexOf(' ', 17);
|
||||
final String threadIdStr = line.substring(17, rightIndex);
|
||||
final int threadId = Integer.parseInt(threadIdStr);
|
||||
|
||||
// TIMESTAMP in header exists at position 45
|
||||
// This position should point in the middle of timestamp which is ended by ':'.
|
||||
int leftIndex = 45;
|
||||
while (isDoubleDigitChar(line.charAt(leftIndex))) {
|
||||
--leftIndex;
|
||||
}
|
||||
rightIndex = line.indexOf(':', 45);
|
||||
|
||||
final String timeStampString = line.substring(leftIndex + 1, rightIndex);
|
||||
final long timeStampMcs = getTimeStamp(timeStampString);
|
||||
|
||||
// Find function name, pointed by FUNCTION. Long timestamp can shift if position
|
||||
// so use end of timestamp to find the function which is ended by ':'.
|
||||
leftIndex = rightIndex + 1;
|
||||
while (Character.isWhitespace(line.charAt(leftIndex))) {
|
||||
++leftIndex;
|
||||
}
|
||||
rightIndex = line.indexOf(':', leftIndex);
|
||||
final String function = line.substring(leftIndex, rightIndex);
|
||||
|
||||
if (!function.equals(FUNCTION_TRACING_MARK_WRITE)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Rest of the line is event body.
|
||||
leftIndex = rightIndex + 1;
|
||||
while (Character.isWhitespace(line.charAt(leftIndex))) {
|
||||
++leftIndex;
|
||||
}
|
||||
|
||||
final String event = line.substring(leftIndex);
|
||||
if (event.startsWith(TRACE_EVENT_CLOCK_SYNC)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse event, for example:
|
||||
// B|615|SurfaceView - android.gameperformance.GamePerformanceActivity#0: 1
|
||||
// E|615
|
||||
// C|11253|operation id|2
|
||||
StringTokenizer eventTokenizer = new StringTokenizer(event, "|");
|
||||
final String eventType = eventTokenizer.nextToken();
|
||||
|
||||
// Attach root on demand.
|
||||
if (!result.containsKey(threadId)) {
|
||||
result.put(threadId, new RawEvent(null /* parent */,
|
||||
timeStampMcs,
|
||||
"#ROOT_" + threadId));
|
||||
}
|
||||
|
||||
switch (eventType) {
|
||||
case "B": {
|
||||
// Log entry starts.
|
||||
eventTokenizer.nextToken(); // PID
|
||||
String eventText = eventTokenizer.nextToken();
|
||||
while (eventTokenizer.hasMoreTokens()) {
|
||||
eventText += " ";
|
||||
eventText += eventTokenizer.nextToken();
|
||||
}
|
||||
RawEvent parent = result.get(threadId);
|
||||
RawEvent current = new RawEvent(parent, timeStampMcs, eventText);
|
||||
parent.mChildren.add(current);
|
||||
result.put(threadId, current);
|
||||
}
|
||||
break;
|
||||
case "E": {
|
||||
// Log entry ends.
|
||||
RawEvent current = result.get(threadId);
|
||||
current.mDuration = timeStampMcs - current.mTime;
|
||||
if (current.mParent == null) {
|
||||
// Detect a tail of the previous call. Remove last child element if it
|
||||
// exists once it does not belong to the root.
|
||||
if (!current.mChildren.isEmpty()) {
|
||||
current.mChildren.remove(current.mChildren.size() -1);
|
||||
}
|
||||
} else {
|
||||
result.put(threadId, current.mParent);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "C":
|
||||
// Counter, ignore
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException(
|
||||
"Unrecognized trace: " + line + " # " + eventType + " # " + event);
|
||||
}
|
||||
}
|
||||
|
||||
// Detect incomplete events and detach from the root.
|
||||
Set<Integer> threadIds = new TreeSet<>();
|
||||
threadIds.addAll(result.keySet());
|
||||
for (int threadId : threadIds) {
|
||||
RawEvent root = result.get(threadId);
|
||||
if (root.mParent == null) {
|
||||
// Last trace was closed.
|
||||
continue;
|
||||
}
|
||||
// Find the root.
|
||||
while (root.mParent != null) {
|
||||
root = root.mParent;
|
||||
}
|
||||
// Discard latest incomplete element.
|
||||
root.mChildren.remove(root.mChildren.size() - 1);
|
||||
result.put(threadId, root);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new IOException("Failed to process " + line, e);
|
||||
} finally {
|
||||
Utils.closeQuietly(bufferedReader);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes provided atrace log and calculates graphics buffer metrics.
|
||||
* @param fileName name of atrace log file.
|
||||
* @param testTag tag to separate results for the different passes.
|
||||
*/
|
||||
public static Map<String, Double> processGraphicBufferResult(
|
||||
String fileName, String testTag) throws IOException {
|
||||
final Map<Integer, RawEvent> model = buildEventModel(fileName);
|
||||
|
||||
List<RawEvent> collectorPostBuffer = new ArrayList<>();
|
||||
List<RawEvent> collectorQueueBuffer = new ArrayList<>();
|
||||
List<RawEvent> collectorReleaseBuffer = new ArrayList<>();
|
||||
List<RawEvent> collectorAcquireBuffer = new ArrayList<>();
|
||||
|
||||
// Collect required events.
|
||||
for (RawEvent root : model.values()) {
|
||||
// Surface view
|
||||
root.findEvents("localPostBuffer", collectorPostBuffer);
|
||||
// OpengGL view
|
||||
root.findEvents("eglSwapBuffersWithDamageKHR", collectorPostBuffer);
|
||||
|
||||
root.findEvents("queueBuffer", collectorQueueBuffer);
|
||||
root.findEvents("onMessageReceived/handleMessageInvalidate/latchBuffer/" +
|
||||
"updateTexImage/acquireBuffer",
|
||||
collectorAcquireBuffer);
|
||||
// PI stack
|
||||
root.findEvents(
|
||||
"onMessageReceived/handleMessageRefresh/postComposition/releaseBuffer",
|
||||
collectorReleaseBuffer);
|
||||
// NYC stack
|
||||
root.findEvents(
|
||||
"onMessageReceived/handleMessageRefresh/releaseBuffer",
|
||||
collectorReleaseBuffer);
|
||||
}
|
||||
|
||||
// Convert raw event to buffer events.
|
||||
List<BufferEvent> bufferEvents = new ArrayList<>();
|
||||
for (RawEvent event : collectorPostBuffer) {
|
||||
bufferEvents.add(
|
||||
new BufferEvent(EVENT_POST_BUFFER, event.mTime, event.mDuration, null));
|
||||
}
|
||||
toBufferEvents(EVENT_QUEUE_BUFFER, collectorQueueBuffer, bufferEvents);
|
||||
toBufferEvents(EVENT_ACQUIRE_BUFFER, collectorAcquireBuffer, bufferEvents);
|
||||
toBufferEvents(EVENT_RELEASE_BUFFER, collectorReleaseBuffer, bufferEvents);
|
||||
|
||||
// Sort events based on time. These events are originally taken from different threads so
|
||||
// order is not always preserved.
|
||||
Collections.sort(bufferEvents, new Comparator<BufferEvent>() {
|
||||
@Override
|
||||
public int compare(BufferEvent o1, BufferEvent o2) {
|
||||
if (o1.mTime < o2.mTime) {
|
||||
return -1;
|
||||
} if (o1.mTime > o2.mTime) {
|
||||
return +1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Collect samples.
|
||||
List<Long> postTimes = new ArrayList<>();
|
||||
List<Long> readyTimes = new ArrayList<>();
|
||||
List<Long> latencyTimes = new ArrayList<>();
|
||||
long missedCnt = 0;
|
||||
|
||||
for (int i = 0; i < bufferEvents.size(); ++i) {
|
||||
if (bufferEvents.get(i).mType != EVENT_POST_BUFFER) {
|
||||
continue;
|
||||
}
|
||||
final int queueIndex = findNextOfType(bufferEvents, i + 1, EVENT_QUEUE_BUFFER);
|
||||
if (queueIndex < 0) {
|
||||
break;
|
||||
}
|
||||
final int acquireIndex = findNextOfBufferId(bufferEvents, queueIndex + 1,
|
||||
bufferEvents.get(queueIndex).mBufferId);
|
||||
if (acquireIndex < 0) {
|
||||
break;
|
||||
}
|
||||
if (bufferEvents.get(acquireIndex).mType != EVENT_ACQUIRE_BUFFER) {
|
||||
// Was not actually presented.
|
||||
++missedCnt;
|
||||
continue;
|
||||
}
|
||||
final int releaseIndex = findNextOfBufferId(bufferEvents, acquireIndex + 1,
|
||||
bufferEvents.get(queueIndex).mBufferId);
|
||||
if (releaseIndex < 0) {
|
||||
break;
|
||||
}
|
||||
if (bufferEvents.get(releaseIndex).mType != EVENT_RELEASE_BUFFER) {
|
||||
// Was not actually presented.
|
||||
++missedCnt;
|
||||
continue;
|
||||
}
|
||||
|
||||
postTimes.add(bufferEvents.get(i).mDuration);
|
||||
readyTimes.add(
|
||||
bufferEvents.get(acquireIndex).mTime - bufferEvents.get(i).mTime);
|
||||
latencyTimes.add(
|
||||
bufferEvents.get(releaseIndex).mTime - bufferEvents.get(i).mTime);
|
||||
}
|
||||
|
||||
if (postTimes.size() < MINIMAL_SAMPLE_CNT_TO_PASS) {
|
||||
throw new IllegalStateException("Too few sample cnt: " + postTimes.size() +". " +
|
||||
MINIMAL_SAMPLE_CNT_TO_PASS + " is required.");
|
||||
}
|
||||
|
||||
Map<String, Double> status = new TreeMap<>();
|
||||
addResults(status, testTag, KEY_POST_TIME, postTimes);
|
||||
addResults(status, testTag, KEY_READY_TIME, readyTimes);
|
||||
addResults(status, testTag, KEY_LATENCY, latencyTimes);
|
||||
status.put(testTag + "_" + KEY_MISSED_FRAME_RATE,
|
||||
100.0 * missedCnt / (missedCnt + postTimes.size()));
|
||||
return status;
|
||||
}
|
||||
|
||||
private static void addResults(
|
||||
Map<String, Double> status, String tag, String key, List<Long> times) {
|
||||
Collections.sort(times);
|
||||
long min = times.get(0);
|
||||
long max = times.get(0);
|
||||
for (long time : times) {
|
||||
min = Math.min(min, time);
|
||||
max = Math.max(max, time);
|
||||
}
|
||||
status.put(tag + "_" + key + "_" + SUFFIX_MIN, (double)min);
|
||||
status.put(tag + "_" + key + "_" + SUFFIX_MAX, (double)max);
|
||||
status.put(tag + "_" + key + "_" + SUFFIX_MEDIAN, (double)times.get(times.size() / 2));
|
||||
}
|
||||
|
||||
// Helper to convert surface flinger events to buffer events.
|
||||
private static void toBufferEvents(
|
||||
int type, List<RawEvent> rawEvents, List<BufferEvent> bufferEvents) {
|
||||
for (RawEvent event : rawEvents) {
|
||||
if (event.mChildren.isEmpty()) {
|
||||
throw new IllegalStateException("Buffer name is expected");
|
||||
}
|
||||
final String bufferName = event.mChildren.get(0).mName;
|
||||
if (bufferName.startsWith("SurfaceView - android.gameperformance")) {
|
||||
bufferEvents.add(
|
||||
new BufferEvent(type, event.mTime, event.mDuration, bufferName));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static int findNextOfType(List<BufferEvent> events, int startIndex, int type) {
|
||||
for (int i = startIndex; i < events.size(); ++i) {
|
||||
if (events.get(i).mType == type) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static int findNextOfBufferId(
|
||||
List<BufferEvent> events, int startIndex, String bufferId) {
|
||||
for (int i = startIndex; i < events.size(); ++i) {
|
||||
if (bufferId.equals(events.get(i).mBufferId)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
if (args.length != 1) {
|
||||
System.err.println("Usage: " + TAG + " atrace.log");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
System.out.println("Results:");
|
||||
for (Map.Entry<?, ?> entry :
|
||||
processGraphicBufferResult(args[0], "default").entrySet()) {
|
||||
System.out.println(" " + entry.getKey() + " = " + entry.getValue());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
31
tests/GamePerformance/src/android/gameperformance/Utils.java
Normal file
31
tests/GamePerformance/src/android/gameperformance/Utils.java
Normal file
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright (C) 2018 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.io.Closeable;
|
||||
import java.io.IOException;
|
||||
|
||||
public class Utils {
|
||||
public static void closeQuietly(Closeable closeable) {
|
||||
try {
|
||||
if (closeable != null) {
|
||||
closeable.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user