From e1f57d6f44909a66c7ab0af33dbc5289287e823a Mon Sep 17 00:00:00 2001 From: Eino-Ville Talvala Date: Thu, 29 May 2014 17:17:07 -0700 Subject: [PATCH] Camera2: Add CPU/GPU overhead measurement to legacy mode Dumps GL and CPU processing duration and frame timestamps to a file, whenever the device is closed or the stream configuration is changed. - Add PerfMeasurement class to legacy mode - Wire up minimal usage to SurfaceTextureRenderer Change-Id: Ic9d74ca26f706780b746175aa615c7aae4ae52e7 --- .../camera2/legacy/PerfMeasurement.java | 309 ++++++++++++++++ .../legacy/SurfaceTextureRenderer.java | 101 +++++- core/jni/Android.mk | 1 + core/jni/AndroidRuntime.cpp | 2 + ...ardware_camera2_legacy_PerfMeasurement.cpp | 335 ++++++++++++++++++ 5 files changed, 743 insertions(+), 5 deletions(-) create mode 100644 core/java/android/hardware/camera2/legacy/PerfMeasurement.java create mode 100644 core/jni/android_hardware_camera2_legacy_PerfMeasurement.cpp diff --git a/core/java/android/hardware/camera2/legacy/PerfMeasurement.java b/core/java/android/hardware/camera2/legacy/PerfMeasurement.java new file mode 100644 index 0000000000000..b930ec26f02e0 --- /dev/null +++ b/core/java/android/hardware/camera2/legacy/PerfMeasurement.java @@ -0,0 +1,309 @@ +/* + * Copyright (C) 2014 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.hardware.camera2.legacy; + +import android.os.SystemClock; +import android.util.Log; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.Queue; + +/** + * GPU and CPU performance measurement for the legacy implementation. + * + *

Measures CPU and GPU processing duration for a set of operations, and dumps + * the results into a file.

+ * + *

Rough usage: + *

+ * {@code
+ *   
+ *   
+ *   mPerfMeasurement.startTimer();
+ *   ...render a frame...
+ *   mPerfMeasurement.stopTimer();
+ *   
+ *   mPerfMeasurement.dumpPerformanceData("/sdcard/my_data.txt");
+ * }
+ * 
+ *

+ * + *

All calls to this object must be made within the same thread, and the same GL context. + * PerfMeasurement cannot be used outside of a GL context. The only exception is + * dumpPerformanceData, which can be called outside of a valid GL context.

+ */ +class PerfMeasurement { + private static final String TAG = "PerfMeasurement"; + + public static final int DEFAULT_MAX_QUERIES = 3; + + private final long mNativeContext; + + private int mCompletedQueryCount = 0; + + /** + * Values for completed measurements + */ + private ArrayList mCollectedGpuDurations = new ArrayList<>(); + private ArrayList mCollectedCpuDurations = new ArrayList<>(); + private ArrayList mCollectedTimestamps = new ArrayList<>(); + + /** + * Values for in-progress measurements (waiting for async GPU results) + */ + private Queue mTimestampQueue = new LinkedList<>(); + private Queue mCpuDurationsQueue = new LinkedList<>(); + + private long mStartTimeNs; + + /** + * The value returned by {@link #nativeGetNextGlDuration} if no new timing + * measurement is available since the last call. + */ + private static final long NO_DURATION_YET = -1l; + + /** + * The value returned by {@link #nativeGetNextGlDuration} if timing failed for + * the next timing interval + */ + private static final long FAILED_TIMING = -2l; + + /** + * Create a performance measurement object with a maximum of {@value #DEFAULT_MAX_QUERIES} + * in-progess queries. + */ + public PerfMeasurement() { + mNativeContext = nativeCreateContext(DEFAULT_MAX_QUERIES); + } + + /** + * Create a performance measurement object with maxQueries as the maximum number of + * in-progress queries. + * + * @param maxQueries maximum in-progress queries, must be larger than 0. + * @throws IllegalArgumentException if maxQueries is less than 1. + */ + public PerfMeasurement(int maxQueries) { + if (maxQueries < 1) throw new IllegalArgumentException("maxQueries is less than 1"); + mNativeContext = nativeCreateContext(maxQueries); + } + + /** + * Returns true if the Gl timing methods will work, false otherwise. + * + *

Must be called within a valid GL context.

+ */ + public static boolean isGlTimingSupported() { + return nativeQuerySupport(); + } + + /** + * Dump collected data to file, and clear the stored data. + * + *

+ * Format is a simple csv-like text file with a header, + * followed by a 3-column list of values in nanoseconds: + *

+     *   timestamp gpu_duration cpu_duration
+     *     
+     *     
+     *     
+     *   ....
+     * 
+ *

+ */ + public void dumpPerformanceData(String path) { + try (BufferedWriter dump = new BufferedWriter(new FileWriter(path))) { + dump.write("timestamp gpu_duration cpu_duration\n"); + for (int i = 0; i < mCollectedGpuDurations.size(); i++) { + dump.write(String.format("%d %d %d\n", + mCollectedTimestamps.get(i), + mCollectedGpuDurations.get(i), + mCollectedCpuDurations.get(i))); + } + mCollectedTimestamps.clear(); + mCollectedGpuDurations.clear(); + mCollectedCpuDurations.clear(); + } catch (IOException e) { + Log.e(TAG, "Error writing data dump to " + path + ":" + e); + } + } + + /** + * Start a GPU/CPU timing measurement. + * + *

Call before starting a rendering pass. Only one timing measurement can be active at once, + * so {@link #stopTimer} must be called before the next call to this method.

+ * + * @throws IllegalStateException if the maximum number of queries are in progress already, + * or the method is called multiple times in a row, or there is + * a GPU error. + */ + public void startTimer() { + nativeStartGlTimer(mNativeContext); + mStartTimeNs = SystemClock.elapsedRealtimeNanos(); + } + + /** + * Finish a GPU/CPU timing measurement. + * + *

Call after finishing all the drawing for a rendering pass. Only one timing measurement can + * be active at once, so {@link #startTimer} must be called before the next call to this + * method.

+ * + * @throws IllegalStateException if no GL timer is currently started, or there is a GPU + * error. + */ + public void stopTimer() { + // Complete CPU timing + long endTimeNs = SystemClock.elapsedRealtimeNanos(); + mCpuDurationsQueue.add(endTimeNs - mStartTimeNs); + // Complete GL timing + nativeStopGlTimer(mNativeContext); + + // Poll to see if GL timing results have arrived; if so + // store the results for a frame + long duration = getNextGlDuration(); + if (duration > 0) { + mCollectedGpuDurations.add(duration); + mCollectedTimestamps.add(mTimestampQueue.isEmpty() ? + NO_DURATION_YET : mTimestampQueue.poll()); + mCollectedCpuDurations.add(mCpuDurationsQueue.isEmpty() ? + NO_DURATION_YET : mCpuDurationsQueue.poll()); + } + if (duration == FAILED_TIMING) { + // Discard timestamp and CPU measurement since GPU measurement failed + if (!mTimestampQueue.isEmpty()) { + mTimestampQueue.poll(); + } + if (!mCpuDurationsQueue.isEmpty()) { + mCpuDurationsQueue.poll(); + } + } + } + + /** + * Add a timestamp to a timing measurement. These are queued up and matched to completed + * workload measurements as they become available. + */ + public void addTimestamp(long timestamp) { + mTimestampQueue.add(timestamp); + } + + /** + * Get the next available GPU timing measurement. + * + *

Since the GPU works asynchronously, the results of a single start/stopGlTimer measurement + * will only be available some time after the {@link #stopTimer} call is made. Poll this method + * until the result becomes available. If multiple start/endTimer measurements are made in a + * row, the results will be available in FIFO order.

+ * + * @return The measured duration of the GPU workload for the next pending query, or + * {@link #NO_DURATION_YET} if no queries are pending or the next pending query has not + * yet finished, or {@link #FAILED_TIMING} if the GPU was unable to complete the + * measurement. + * + * @throws IllegalStateException If there is a GPU error. + * + */ + private long getNextGlDuration() { + long duration = nativeGetNextGlDuration(mNativeContext); + if (duration > 0) { + mCompletedQueryCount++; + } + return duration; + } + + /** + * Returns the number of measurements so far that returned a valid duration + * measurement. + */ + public int getCompletedQueryCount() { + return mCompletedQueryCount; + } + + @Override + protected void finalize() { + nativeDeleteContext(mNativeContext); + } + + /** + * Create a native performance measurement context. + * + * @param maxQueryCount maximum in-progress queries; must be >= 1. + */ + private static native long nativeCreateContext(int maxQueryCount); + + /** + * Delete the native context. + * + *

Not safe to call more than once.

+ */ + private static native void nativeDeleteContext(long contextHandle); + + /** + * Query whether the relevant Gl extensions are available for Gl timing + */ + private static native boolean nativeQuerySupport(); + + /** + * Start a GL timing section. + * + *

All GL commands between this method and the next {@link #nativeEndGlTimer} will be + * included in the timing.

+ * + *

Must be called from the same thread as calls to {@link #nativeEndGlTimer} and + * {@link #nativeGetNextGlDuration}.

+ * + * @throws IllegalStateException if a GL error occurs or start is called repeatedly. + */ + protected static native void nativeStartGlTimer(long contextHandle); + + /** + * Finish a GL timing section. + * + *

Some time after this call returns, the time the GPU took to + * execute all work submitted between the latest {@link #nativeStartGlTimer} and + * this call, will become available from calling {@link #nativeGetNextGlDuration}.

+ * + *

Must be called from the same thread as calls to {@link #nativeStartGlTimer} and + * {@link #nativeGetNextGlDuration}.

+ * + * @throws IllegalStateException if a GL error occurs or stop is called before start + */ + protected static native void nativeStopGlTimer(long contextHandle); + + /** + * Get the next available GL duration measurement, in nanoseconds. + * + *

Must be called from the same thread as calls to {@link #nativeStartGlTimer} and + * {@link #nativeEndGlTimer}.

+ * + * @return the next GL duration measurement, or {@link #NO_DURATION_YET} if + * no new measurement is available, or {@link #FAILED_TIMING} if timing + * failed for the next duration measurement. + * @throws IllegalStateException if a GL error occurs + */ + protected static native long nativeGetNextGlDuration(long contextHandle); + + +} diff --git a/core/java/android/hardware/camera2/legacy/SurfaceTextureRenderer.java b/core/java/android/hardware/camera2/legacy/SurfaceTextureRenderer.java index 9969fd2369d4f..daa64c05e8b35 100644 --- a/core/java/android/hardware/camera2/legacy/SurfaceTextureRenderer.java +++ b/core/java/android/hardware/camera2/legacy/SurfaceTextureRenderer.java @@ -17,6 +17,7 @@ package android.hardware.camera2.legacy; import android.graphics.ImageFormat; import android.graphics.SurfaceTexture; +import android.os.Environment; import android.opengl.EGL14; import android.opengl.EGLConfig; import android.opengl.EGLContext; @@ -25,10 +26,13 @@ import android.opengl.EGLSurface; import android.opengl.GLES11Ext; import android.opengl.GLES20; import android.opengl.Matrix; +import android.text.format.Time; import android.util.Log; import android.util.Size; import android.view.Surface; +import android.os.SystemProperties; +import java.io.File; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.FloatBuffer; @@ -126,6 +130,9 @@ public class SurfaceTextureRenderer { private int maPositionHandle; private int maTextureHandle; + private PerfMeasurement mPerfMeasurer = null; + private static final String LEGACY_PERF_PROPERTY = "persist.camera.legacy_perf"; + public SurfaceTextureRenderer() { mTriangleVertices = ByteBuffer.allocateDirect(mTriangleVerticesData.length * FLOAT_SIZE_BYTES).order(ByteOrder.nativeOrder()).asFloatBuffer(); @@ -219,7 +226,6 @@ public class SurfaceTextureRenderer { GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /*offset*/ 0, /*count*/ 4); checkGlError("glDrawArrays"); - GLES20.glFinish(); } /** @@ -327,9 +333,16 @@ public class SurfaceTextureRenderer { EGL14.EGL_NONE }; for (EGLSurfaceHolder holder : surfaces) { - holder.eglSurface = EGL14.eglCreateWindowSurface(mEGLDisplay, mConfigs, holder.surface, - surfaceAttribs, 0); - checkEglError("eglCreateWindowSurface"); + try { + Size size = LegacyCameraDevice.getSurfaceSize(holder.surface); + holder.width = size.getWidth(); + holder.height = size.getHeight(); + holder.eglSurface = EGL14.eglCreateWindowSurface(mEGLDisplay, mConfigs, + holder.surface, surfaceAttribs, /*offset*/ 0); + checkEglError("eglCreateWindowSurface"); + } catch (LegacyExceptionUtils.BufferQueueAbandonedException e) { + Log.w(TAG, "Surface abandoned, skipping...", e); + } } } @@ -367,6 +380,7 @@ public class SurfaceTextureRenderer { if (mEGLDisplay != EGL14.EGL_NO_DISPLAY) { EGL14.eglMakeCurrent(mEGLDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_CONTEXT); + dumpGlTiming(); if (mSurfaces != null) { for (EGLSurfaceHolder holder : mSurfaces) { if (holder.eglSurface != null) { @@ -417,6 +431,65 @@ public class SurfaceTextureRenderer { } } + /** + * Save a measurement dump to disk, in + * {@code /sdcard/CameraLegacy/durations_