Bug 717349 - Add optional render and checkerboarding profiling. r=kats a=akeybl
authorChris Lord <chrislord.net@gmail.com>
Thu, 02 Feb 2012 09:02:32 +0000
changeset 88346 dfce8408f9407da0a2f1f96b0998b386a329e5b1
parent 88345 976580a0c9d79f3e717e060e011f3ed00c3d0e53
child 88347 aeb3e5db799e88c7a800839accf3e7d34a80df3c
push idunknown
push userunknown
push dateunknown
reviewerskats, akeybl
bugs717349
milestone12.0a2
Bug 717349 - Add optional render and checkerboarding profiling. r=kats a=akeybl This adds checkerboard profiling to LayerRenderer, that can be enabled either by enabling debug logging of the "GeckoLayerRendererProf" tag, or via reflection using PanningPerfAPI.
mobile/android/base/gfx/Layer.java
mobile/android/base/gfx/LayerRenderer.java
mobile/android/base/gfx/MultiTileLayer.java
mobile/android/base/gfx/PanningPerfAPI.java
mobile/android/base/gfx/TileLayer.java
--- a/mobile/android/base/gfx/Layer.java
+++ b/mobile/android/base/gfx/Layer.java
@@ -36,16 +36,17 @@
  *
  * ***** END LICENSE BLOCK ***** */
 
 package org.mozilla.gecko.gfx;
 
 import android.graphics.Point;
 import android.graphics.PointF;
 import android.graphics.RectF;
+import android.graphics.Region;
 import android.util.Log;
 import java.util.concurrent.locks.ReentrantLock;
 import javax.microedition.khronos.opengles.GL10;
 import org.mozilla.gecko.FloatUtils;
 
 public abstract class Layer {
     private final ReentrantLock mTransactionLock;
     private boolean mInTransaction;
@@ -101,16 +102,25 @@ public abstract class Layer {
     protected RectF getBounds(RenderContext context, FloatSize size) {
         float scaleFactor = context.zoomFactor / mResolution;
         float x = mOrigin.x * scaleFactor, y = mOrigin.y * scaleFactor;
         float width = size.width * scaleFactor, height = size.height * scaleFactor;
         return new RectF(x, y, x + width, y + height);
     }
 
     /**
+     * Returns the region of the layer that is considered valid. The default
+     * implementation of this will return the bounds of the layer, but this
+     * may be overridden.
+     */
+    public Region getValidRegion(RenderContext context) {
+        return new Region(RectUtils.round(getBounds(context, new FloatSize(getSize()))));
+    }
+
+    /**
      * Call this before modifying the layer. Note that, for TileLayers, "modifying the layer"
      * includes altering the underlying CairoImage in any way. Thus you must call this function
      * before modifying the byte buffer associated with this layer.
      *
      * This function may block, so you should never call this on the main UI thread.
      */
     public void beginTransaction(LayerView aView) {
         if (mTransactionLock.isHeldByCurrentThread())
--- a/mobile/android/base/gfx/LayerRenderer.java
+++ b/mobile/android/base/gfx/LayerRenderer.java
@@ -50,31 +50,34 @@ import org.mozilla.gecko.gfx.TextureGene
 import org.mozilla.gecko.gfx.TextLayer;
 import org.mozilla.gecko.gfx.TileLayer;
 import android.content.Context;
 import android.content.SharedPreferences;
 import android.graphics.Point;
 import android.graphics.PointF;
 import android.graphics.Rect;
 import android.graphics.RectF;
+import android.graphics.Region;
+import android.graphics.RegionIterator;
 import android.opengl.GLSurfaceView;
 import android.os.SystemClock;
 import android.util.DisplayMetrics;
 import android.util.Log;
 import android.view.WindowManager;
 import javax.microedition.khronos.egl.EGLConfig;
 import javax.microedition.khronos.opengles.GL10;
 import java.nio.IntBuffer;
 import java.util.ArrayList;
 
 /**
  * The layer renderer implements the rendering logic for a layer view.
  */
 public class LayerRenderer implements GLSurfaceView.Renderer {
     private static final String LOGTAG = "GeckoLayerRenderer";
+    private static final String PROFTAG = "GeckoLayerRendererProf";
 
     /*
      * The amount of time a frame is allowed to take to render before we declare it a dropped
      * frame.
      */
     private static final int MAX_FRAME_TIME = 16;   /* 1000 ms / 60 FPS */
 
     private static final int FRAME_RATE_METER_WIDTH = 64;
@@ -94,16 +97,22 @@ public class LayerRenderer implements GL
 
     private ArrayList<Layer> mExtraLayers = new ArrayList<Layer>();
 
     // Dropped frames display
     private int[] mFrameTimings;
     private int mCurrentFrame, mFrameTimingsSum, mDroppedFrames;
     private boolean mShowFrameRate;
 
+    // Render profiling output
+    private int mFramesRendered;
+    private float mCompleteFramesRendered;
+    private boolean mProfileRender;
+    private long mProfileOutputTime;
+
     /* Used by robocop for testing purposes */
     private IntBuffer mPixelBuffer;
 
     public LayerRenderer(LayerView view) {
         mView = view;
 
         LayerController controller = view.getController();
 
@@ -142,17 +151,17 @@ public class LayerRenderer implements GL
         mFadeRunnable = new FadeRunnable();
 
         mFrameTimings = new int[60];
         mCurrentFrame = mFrameTimingsSum = mDroppedFrames = 0;
         mShowFrameRate = false;
     }
 
     public void onSurfaceCreated(GL10 gl, EGLConfig config) {
-        checkFrameRateMonitorEnabled();
+        checkMonitoringEnabled();
 
         gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT, GL10.GL_FASTEST);
         gl.glDisable(GL10.GL_DITHER);
         gl.glEnable(GL10.GL_TEXTURE_2D);
 
         int maxTextureSizeResult[] = new int[1];
         gl.glGetIntegerv(GL10.GL_MAX_TEXTURE_SIZE, maxTextureSizeResult, 0);
         mMaxTextureSize = maxTextureSizeResult[0];
@@ -259,21 +268,60 @@ public class LayerRenderer implements GL
             /* Draw the vertical scrollbar. */
             IntSize screenSize = new IntSize(controller.getViewportSize());
             if (pageRect.height() > screenSize.height)
                 mVertScrollLayer.draw(pageContext);
 
             /* Draw the horizontal scrollbar. */
             if (pageRect.width() > screenSize.width)
                 mHorizScrollLayer.draw(pageContext);
+
+            /* Measure how much of the screen is checkerboarding */
+            if ((rootLayer != null) &&
+                (mProfileRender || PanningPerfAPI.isRecordingCheckerboard())) {
+                // Find out how much of the viewport area is valid
+                Rect viewport = RectUtils.round(pageContext.viewport);
+                Region validRegion = rootLayer.getValidRegion(pageContext);
+                validRegion.op(viewport, Region.Op.INTERSECT);
+
+                float checkerboard = 0.0f;
+                if (!(validRegion.isRect() && validRegion.getBounds().equals(viewport))) {
+                    int screenArea = viewport.width() * viewport.height();
+                    validRegion.op(viewport, Region.Op.REVERSE_DIFFERENCE);
+
+                    // XXX The assumption here is that a Region never has overlapping
+                    //     rects. This is true, as evidenced by reading the SkRegion
+                    //     source, but is not mentioned in the Android documentation,
+                    //     and so is liable to change.
+                    //     If it does change, this code will need to be reevaluated.
+                    Rect r = new Rect();
+                    int checkerboardArea = 0;
+                    for (RegionIterator i = new RegionIterator(validRegion); i.next(r);) {
+                        checkerboardArea += r.width() * r.height();
+                    }
+
+                    checkerboard = checkerboardArea / (float)screenArea;
+                }
+
+                PanningPerfAPI.recordCheckerboard(checkerboard);
+
+                mCompleteFramesRendered += 1.0f - checkerboard;
+                mFramesRendered ++;
+
+                if (frameStartTime - mProfileOutputTime > 1000) {
+                    mProfileOutputTime = frameStartTime;
+                    printCheckerboardStats();
+                }
+            }
         }
 
         /* Draw the FPS. */
         if (mShowFrameRate) {
             updateDroppedFrames(frameStartTime);
+
             try {
                 gl.glEnable(GL10.GL_BLEND);
                 gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA);
                 mFrameRateLayer.draw(screenContext);
             } finally {
                 gl.glDisable(GL10.GL_BLEND);
             }
         }
@@ -290,16 +338,22 @@ public class LayerRenderer implements GL
             synchronized (pixelBuffer) {
                 pixelBuffer.position(0);
                 gl.glReadPixels(0, 0, (int)screenContext.viewport.width(), (int)screenContext.viewport.height(), GL10.GL_RGBA, GL10.GL_UNSIGNED_BYTE, pixelBuffer);
                 pixelBuffer.notify();
             }
         }
     }
 
+    private void printCheckerboardStats() {
+        Log.d(PROFTAG, "Frames rendered over last 1000ms: " + mCompleteFramesRendered + "/" + mFramesRendered);
+        mFramesRendered = 0;
+        mCompleteFramesRendered = 0;
+    }
+
     /** Used by robocop for testing purposes. Not for production use! */
     IntBuffer getPixels() {
         IntBuffer pixelBuffer = IntBuffer.allocate(mView.getWidth() * mView.getHeight());
         synchronized (pixelBuffer) {
             mPixelBuffer = pixelBuffer;
             mView.requestRender();
             try {
                 pixelBuffer.wait();
@@ -377,17 +431,16 @@ public class LayerRenderer implements GL
         mFrameTimingsSum += frameElapsedTime;
         mDroppedFrames -= (mFrameTimings[mCurrentFrame] + 1) / MAX_FRAME_TIME;
         mDroppedFrames += (frameElapsedTime + 1) / MAX_FRAME_TIME;
 
         mFrameTimings[mCurrentFrame] = frameElapsedTime;
         mCurrentFrame = (mCurrentFrame + 1) % mFrameTimings.length;
 
         int averageTime = mFrameTimingsSum / mFrameTimings.length;
-
         mFrameRateLayer.beginTransaction();
         try {
             mFrameRateLayer.setText(averageTime + " ms/" + mDroppedFrames);
         } finally {
             mFrameRateLayer.endTransaction();
         }
     }
 
@@ -398,24 +451,25 @@ public class LayerRenderer implements GL
             Point origin = new Point(width - FRAME_RATE_METER_WIDTH - 8,
                                      height - FRAME_RATE_METER_HEIGHT + 8);
             mFrameRateLayer.setOrigin(origin);
         } finally {
             mFrameRateLayer.endTransaction();
         }
     }
 
-    private void checkFrameRateMonitorEnabled() {
+    private void checkMonitoringEnabled() {
         /* Do this I/O off the main thread to minimize its impact on startup time. */
         new Thread(new Runnable() {
             @Override
             public void run() {
                 Context context = mView.getContext();
                 SharedPreferences preferences = context.getSharedPreferences("GeckoApp", 0);
                 mShowFrameRate = preferences.getBoolean("showFrameRate", false);
+                mProfileRender = Log.isLoggable(PROFTAG, Log.DEBUG);
             }
         }).start();
     }
 
     private void updateCheckerboardLayer(GL10 gl, RenderContext renderContext) {
         int newCheckerboardColor = mView.getController().getCheckerboardColor();
         if (newCheckerboardColor == mCheckerboardImage.getColor()) {
             return;
--- a/mobile/android/base/gfx/MultiTileLayer.java
+++ b/mobile/android/base/gfx/MultiTileLayer.java
@@ -433,16 +433,28 @@ public class MultiTileLayer extends Laye
             invalidateBuffer();
         }
     }
 
     public void setRenderOffset(Point offset) {
         mRenderOffset.set(offset.x, offset.y);
     }
 
+    @Override
+    public Region getValidRegion(RenderContext context) {
+        Region validRegion = new Region();
+        for (SubTile tile : mTiles) {
+            if (tile.key == null || tile.getValidTextureArea().isEmpty())
+                continue;
+            validRegion.op(tile.getValidRegion(context), Region.Op.UNION);
+        }
+
+        return validRegion;
+    }
+
     /**
      * Invalidates all sub-tiles. This should be called if the source backing
      * this layer has changed. This method is only valid inside a transaction.
      */
     protected void invalidateTiles() {
         if (!inTransaction()) {
             throw new RuntimeException("invalidateTiles() is only valid inside a transaction");
         }
--- a/mobile/android/base/gfx/PanningPerfAPI.java
+++ b/mobile/android/base/gfx/PanningPerfAPI.java
@@ -15,16 +15,17 @@
  * The Original Code is Mozilla Android code.
  *
  * The Initial Developer of the Original Code is Mozilla Foundation.
  * Portions created by the Initial Developer are Copyright (C) 2011
  * the Initial Developer. All Rights Reserved.
  *
  * Contributor(s):
  *   Kartikaya Gupta <kgupta@mozilla.com>
+ *   Chris Lord <chrislord.net@gmail.com>
  *
  * Alternatively, the contents of this file may be used under the terms of
  * either the GNU General Public License Version 2 or later (the "GPL"), or
  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  * in which case the provisions of the GPL or the LGPL are applicable instead
  * of those above. If you wish to allow use of your version of this file only
  * under the terms of either the GPL or the LGPL, and not to allow others to
  * use your version of this file under the terms of the MPL, indicate your
@@ -45,42 +46,80 @@ import android.util.Log;
 public class PanningPerfAPI {
     private static final String LOGTAG = "GeckoPanningPerfAPI";
 
     // make this large enough to avoid having to resize the frame time
     // list, as that may be expensive and impact the thing we're trying
     // to measure.
     private static final int EXPECTED_FRAME_COUNT = 2048;
 
-    private static boolean mRecording = false;
+    private static boolean mRecordingFrames = false;
     private static List<Long> mFrameTimes;
-    private static long mStartTime;
+    private static long mFrameStartTime;
+
+    private static boolean mRecordingCheckerboard = false;
+    private static List<Float> mCheckerboardAmounts;
+    private static long mCheckerboardStartTime;
 
     public static void startFrameTimeRecording() {
-        if (mRecording) {
+        if (mRecordingFrames) {
             Log.e(LOGTAG, "Error: startFrameTimeRecording() called while already recording!");
             return;
         }
-        mRecording = true;
+        mRecordingFrames = true;
         if (mFrameTimes == null) {
             mFrameTimes = new ArrayList<Long>(EXPECTED_FRAME_COUNT);
         } else {
             mFrameTimes.clear();
         }
-        mStartTime = SystemClock.uptimeMillis();
+        mFrameStartTime = SystemClock.uptimeMillis();
     }
 
     public static List<Long> stopFrameTimeRecording() {
-        if (!mRecording) {
+        if (!mRecordingFrames) {
             Log.e(LOGTAG, "Error: stopFrameTimeRecording() called when not recording!");
             return null;
         }
-        mRecording = false;
+        mRecordingFrames = false;
         return mFrameTimes;
     }
 
     public static void recordFrameTime() {
         // this will be called often, so try to make it as quick as possible
-        if (mRecording) {
-            mFrameTimes.add(SystemClock.uptimeMillis() - mStartTime);
+        if (mRecordingFrames) {
+            mFrameTimes.add(SystemClock.uptimeMillis() - mFrameStartTime);
+        }
+    }
+
+    public static boolean isRecordingCheckerboard() {
+        return mRecordingCheckerboard;
+    }
+
+    public static void startCheckerboardRecording() {
+        if (mRecordingCheckerboard) {
+            Log.e(LOGTAG, "Error: startCheckerboardRecording() called while already recording!");
+            return;
+        }
+        mRecordingCheckerboard = true;
+        if (mCheckerboardAmounts == null) {
+            mCheckerboardAmounts = new ArrayList<Float>(EXPECTED_FRAME_COUNT);
+        } else {
+            mCheckerboardAmounts.clear();
+        }
+        mCheckerboardStartTime = SystemClock.uptimeMillis();
+    }
+
+    public static List<Float> stopCheckerboardRecording() {
+        if (!mRecordingCheckerboard) {
+            Log.e(LOGTAG, "Error: stopCheckerboardRecording() called when not recording!");
+            return null;
+        }
+        mRecordingCheckerboard = false;
+        return mCheckerboardAmounts;
+    }
+
+    public static void recordCheckerboard(float amount) {
+        // this will be called often, so try to make it as quick as possible
+        if (mRecordingCheckerboard) {
+            mCheckerboardAmounts.add(amount);
         }
     }
 }
--- a/mobile/android/base/gfx/TileLayer.java
+++ b/mobile/android/base/gfx/TileLayer.java
@@ -32,18 +32,20 @@
  * and other provisions required by the GPL or the LGPL. If you do not delete
  * the provisions above, a recipient may use your version of this file under
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
 
 package org.mozilla.gecko.gfx;
 
+import android.graphics.Point;
 import android.graphics.Rect;
 import android.graphics.RectF;
+import android.graphics.Region;
 import android.opengl.GLES20;
 import android.util.Log;
 import javax.microedition.khronos.opengles.GL10;
 import javax.microedition.khronos.opengles.GL11Ext;
 import java.nio.Buffer;
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
 import java.nio.FloatBuffer;
@@ -145,16 +147,32 @@ public abstract class TileLayer extends 
      * Returns a handle to the valid texture area rectangle. Modifying this
      * Rect will modify the valid texture area for this layer.
      */
     public Rect getValidTextureArea() {
         return mValidTextureRect;
     }
 
     @Override
+    public Region getValidRegion(RenderContext context) {
+        if (mValidTextureRect.isEmpty())
+            return new Region();
+
+        Point origin = getOrigin();
+        float scaleFactor = context.zoomFactor / getResolution();
+        float x = (origin.x + mValidTextureRect.left) * scaleFactor;
+        float y = (origin.y + mValidTextureRect.top) * scaleFactor;
+        float width = mValidTextureRect.width() * scaleFactor;
+        float height = mValidTextureRect.height() * scaleFactor;
+
+        return new Region(Math.round(x), Math.round(y),
+                          Math.round(x + width), Math.round(y + height));
+    }
+
+    @Override
     protected boolean performUpdates(GL10 gl, RenderContext context) {
         super.performUpdates(gl, context);
 
         // Reallocate the texture if the size has changed
         validateTexture(gl);
 
         // Don't do any work if the image has an invalid size.
         if (!mImage.getSize().isPositive())