Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit e1f57d6f authored by Eino-Ville Talvala's avatar Eino-Ville Talvala
Browse files

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
parent 20b716bf
Loading
Loading
Loading
Loading
+309 −0
Original line number Diff line number Diff line
/*
 * 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.
 *
 * <p>Measures CPU and GPU processing duration for a set of operations, and dumps
 * the results into a file.</p>
 *
 * <p>Rough usage:
 * <pre>
 * {@code
 *   <set up workload>
 *   <start long-running workload>
 *   mPerfMeasurement.startTimer();
 *   ...render a frame...
 *   mPerfMeasurement.stopTimer();
 *   <end workload>
 *   mPerfMeasurement.dumpPerformanceData("/sdcard/my_data.txt");
 * }
 * </pre>
 * </p>
 *
 * <p>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.</p>
 */
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<Long> mCollectedGpuDurations = new ArrayList<>();
    private ArrayList<Long> mCollectedCpuDurations = new ArrayList<>();
    private ArrayList<Long> mCollectedTimestamps = new ArrayList<>();

    /**
     * Values for in-progress measurements (waiting for async GPU results)
     */
    private Queue<Long> mTimestampQueue = new LinkedList<>();
    private Queue<Long> 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.
     *
     * <p>Must be called within a valid GL context.</p>
     */
    public static boolean isGlTimingSupported() {
        return nativeQuerySupport();
    }

    /**
     * Dump collected data to file, and clear the stored data.
     *
     * <p>
     * Format is a simple csv-like text file with a header,
     * followed by a 3-column list of values in nanoseconds:
     * <pre>
     *   timestamp gpu_duration cpu_duration
     *   <long> <long> <long>
     *   <long> <long> <long>
     *   <long> <long> <long>
     *   ....
     * </pre>
     * </p>
     */
    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.
     *
     * <p>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.</p>
     *
     * @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.
     *
     * <p>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.</p>
     *
     * @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.
     *
     * <p>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.</p>
     *
     * @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.
     *
     * <p>Not safe to call more than once.</p>
     */
    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.
     *
     * <p>All GL commands between this method and the next {@link #nativeEndGlTimer} will be
     * included in the timing.</p>
     *
     * <p>Must be called from the same thread as calls to {@link #nativeEndGlTimer} and
     * {@link #nativeGetNextGlDuration}.</p>
     *
     * @throws IllegalStateException if a GL error occurs or start is called repeatedly.
     */
    protected static native void nativeStartGlTimer(long contextHandle);

    /**
     * Finish a GL timing section.
     *
     * <p>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}.</p>
     *
     * <p>Must be called from the same thread as calls to {@link #nativeStartGlTimer} and
     * {@link #nativeGetNextGlDuration}.</p>
     *
     * @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.
     *
     * <p>Must be called from the same thread as calls to {@link #nativeStartGlTimer} and
     * {@link #nativeEndGlTimer}.</p>
     *
     * @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);


}
+96 −5
Original line number Diff line number Diff line
@@ -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);
            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_<time>_<width1>x<height1>_...txt}
     */
    private void dumpGlTiming() {
        if (mPerfMeasurer == null) return;

        File legacyStorageDir = new File(Environment.getExternalStorageDirectory(), "CameraLegacy");
        if (!legacyStorageDir.exists()){
            if (!legacyStorageDir.mkdirs()){
                Log.e(TAG, "Failed to create directory for data dump");
                return;
            }
        }

        StringBuilder path = new StringBuilder(legacyStorageDir.getPath());
        path.append(File.separator);
        path.append("durations_");

        Time now = new Time();
        now.setToNow();
        path.append(now.format2445());
        path.append("_S");
        for (EGLSurfaceHolder surface : mSurfaces) {
            path.append(String.format("_%d_%d", surface.width, surface.height));
        }
        path.append("_C");
        for (EGLSurfaceHolder surface : mConversionSurfaces) {
            path.append(String.format("_%d_%d", surface.width, surface.height));
        }
        path.append(".txt");
        mPerfMeasurer.dumpPerformanceData(path.toString());
    }

    private void setupGlTiming() {
        if (PerfMeasurement.isGlTimingSupported()) {
            Log.d(TAG, "Enabling GL performance measurement");
            mPerfMeasurer = new PerfMeasurement();
        } else {
            Log.d(TAG, "GL performance measurement not supported on this device");
            mPerfMeasurer = null;
        }
    }

    private void beginGlTiming() {
        if (mPerfMeasurer == null) return;
        mPerfMeasurer.startTimer();
    }

    private void addGlTimestamp(long timestamp) {
        if (mPerfMeasurer == null) return;
        mPerfMeasurer.addTimestamp(timestamp);
    }

    private void endGlTiming() {
        if (mPerfMeasurer == null) return;
        mPerfMeasurer.stopTimer();
    }

    /**
     * Return the surface texture to draw to - this is the texture use to when producing output
     * surface buffers.
@@ -474,6 +547,11 @@ public class SurfaceTextureRenderer {
                mConversionSurfaces.get(0).eglSurface);
        initializeGLState();
        mSurfaceTexture = new SurfaceTexture(getTextureId());

        // Set up performance tracking if enabled
        if (SystemProperties.getBoolean(LEGACY_PERF_PROPERTY, false)) {
            setupGlTiming();
        }
    }

    /**
@@ -494,8 +572,19 @@ public class SurfaceTextureRenderer {
        }

        checkGlError("before updateTexImage");

        if (targetSurfaces == null) {
            mSurfaceTexture.updateTexImage();
            return;
        }

        beginGlTiming();

        mSurfaceTexture.updateTexImage();
        if (targetSurfaces == null) return;

        long timestamp = mSurfaceTexture.getTimestamp();
        addGlTimestamp(timestamp);

        List<Long> targetSurfaceIds = LegacyCameraDevice.getSurfaceIds(targetSurfaces);
        for (EGLSurfaceHolder holder : mSurfaces) {
            if (LegacyCameraDevice.containsSurfaceId(holder.surface, targetSurfaceIds)) {
@@ -522,6 +611,8 @@ public class SurfaceTextureRenderer {
                }
            }
        }

        endGlTiming();
    }

    /**
+1 −0
Original line number Diff line number Diff line
@@ -139,6 +139,7 @@ LOCAL_SRC_FILES:= \
	android_hardware_Camera.cpp \
	android_hardware_camera2_CameraMetadata.cpp \
	android_hardware_camera2_legacy_LegacyCameraDevice.cpp \
	android_hardware_camera2_legacy_PerfMeasurement.cpp \
	android_hardware_camera2_DngCreator.cpp \
	android_hardware_SensorManager.cpp \
	android_hardware_SerialPort.cpp \
+2 −0
Original line number Diff line number Diff line
@@ -79,6 +79,7 @@ extern int register_android_opengl_jni_GLES31Ext(JNIEnv* env);
extern int register_android_hardware_Camera(JNIEnv *env);
extern int register_android_hardware_camera2_CameraMetadata(JNIEnv *env);
extern int register_android_hardware_camera2_legacy_LegacyCameraDevice(JNIEnv *env);
extern int register_android_hardware_camera2_legacy_PerfMeasurement(JNIEnv *env);
extern int register_android_hardware_camera2_DngCreator(JNIEnv *env);
extern int register_android_hardware_SensorManager(JNIEnv *env);
extern int register_android_hardware_SerialPort(JNIEnv *env);
@@ -1314,6 +1315,7 @@ static const RegJNIRec gRegJNI[] = {
    REG_JNI(register_android_hardware_Camera),
    REG_JNI(register_android_hardware_camera2_CameraMetadata),
    REG_JNI(register_android_hardware_camera2_legacy_LegacyCameraDevice),
    REG_JNI(register_android_hardware_camera2_legacy_PerfMeasurement),
    REG_JNI(register_android_hardware_camera2_DngCreator),
    REG_JNI(register_android_hardware_SensorManager),
    REG_JNI(register_android_hardware_SerialPort),
+335 −0

File added.

Preview size limit exceeded, changes collapsed.