/* * 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.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); }