Bug 732576. make getViewTransform fast. r=kats
authorJeff Muizelaar <jmuizelaar@mozilla.com>
Fri, 02 Mar 2012 14:31:27 -0500
changeset 89305 730e026a21795c457ffbc9e307f6b124ab133e95
parent 89304 3be651e9ac2aa795d03c66f5434bf430aca06871
child 89307 4012267a4f08f2b689da1bb6e1ee426c4fb3805e
push id22242
push userkgupta@mozilla.com
push dateWed, 14 Mar 2012 15:19:09 +0000
treeherdermozilla-central@936ef50fa498 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskats
bugs732576
milestone13.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 732576. make getViewTransform fast. r=kats This removes the logging, locking and allocations from getViewTransform. This reduces the time spent from an median of 6.3ms to 0.061ms We use a new scheme where the view transform is immutable and the member variable containing it is atomically overwritten. So we may get a slightly old view transform but this won't be a problem.
mobile/android/base/GeckoApp.java
mobile/android/base/Makefile.in
mobile/android/base/gfx/GeckoLayerClient.java
mobile/android/base/gfx/ImmutableViewportMetrics.java
mobile/android/base/gfx/LayerController.java
mobile/android/base/gfx/ViewportMetrics.java
mobile/android/base/ui/PanZoomController.java
--- a/mobile/android/base/GeckoApp.java
+++ b/mobile/android/base/GeckoApp.java
@@ -46,16 +46,17 @@ import org.mozilla.gecko.gfx.GeckoLayerC
 import org.mozilla.gecko.gfx.IntSize;
 import org.mozilla.gecko.gfx.Layer;
 import org.mozilla.gecko.gfx.LayerController;
 import org.mozilla.gecko.gfx.LayerView;
 import org.mozilla.gecko.gfx.PlaceholderLayerClient;
 import org.mozilla.gecko.gfx.RectUtils;
 import org.mozilla.gecko.gfx.SurfaceTextureLayer;
 import org.mozilla.gecko.gfx.ViewportMetrics;
+import org.mozilla.gecko.gfx.ImmutableViewportMetrics;
 import org.mozilla.gecko.Tab.HistoryEntry;
 
 import java.io.*;
 import java.util.*;
 import java.util.regex.Pattern;
 import java.util.regex.Matcher;
 import java.util.zip.*;
 import java.net.URL;
@@ -1370,22 +1371,22 @@ abstract public class GeckoApp
                 PluginLayoutParams lp;
 
                 Tabs tabs = Tabs.getInstance();
                 Tab tab = tabs.getSelectedTab();
 
                 if (tab == null)
                     return;
 
-                ViewportMetrics targetViewport = mLayerController.getViewportMetrics();
-                ViewportMetrics pluginViewport;
+                ImmutableViewportMetrics targetViewport = mLayerController.getViewportMetrics();
+                ImmutableViewportMetrics pluginViewport;
                 
                 try {
                     JSONObject viewportObject = new JSONObject(metadata);
-                    pluginViewport = new ViewportMetrics(viewportObject);
+                    pluginViewport = new ImmutableViewportMetrics(new ViewportMetrics(viewportObject));
                 } catch (JSONException e) {
                     Log.e(LOGTAG, "Bad viewport metadata: ", e);
                     return;
                 }
 
                 if (mPluginContainer.indexOfChild(view) == -1) {
                     lp = new PluginLayoutParams(x, y, w, h, pluginViewport);
 
@@ -1565,17 +1566,17 @@ abstract public class GeckoApp
 
         if (tab == null)
             return;
 
         repositionPluginViews(tab, setVisible);
     }
 
     public void repositionPluginViews(Tab tab, boolean setVisible) {
-        ViewportMetrics targetViewport = mLayerController.getViewportMetrics();
+        ImmutableViewportMetrics targetViewport = mLayerController.getViewportMetrics();
 
         if (targetViewport == null)
             return;
 
         for (View view : tab.getPluginViews()) {
             PluginLayoutParams lp = (PluginLayoutParams)view.getLayoutParams();
             lp.reposition(targetViewport);
 
@@ -2724,55 +2725,55 @@ class PluginLayoutParams extends Absolut
 {
     private static final int MAX_DIMENSION = 2048;
     private static final String LOGTAG = "GeckoApp.PluginLayoutParams";
 
     private int mOriginalX;
     private int mOriginalY;
     private int mOriginalWidth;
     private int mOriginalHeight;
-    private ViewportMetrics mOriginalViewport;
+    private ImmutableViewportMetrics mOriginalViewport;
     private float mLastResolution;
 
-    public PluginLayoutParams(int aX, int aY, int aWidth, int aHeight, ViewportMetrics aViewport) {
+    public PluginLayoutParams(int aX, int aY, int aWidth, int aHeight, ImmutableViewportMetrics aViewport) {
         super(aWidth, aHeight, aX, aY);
 
-        Log.i(LOGTAG, "Creating plugin at " + aX + ", " + aY + ", " + aWidth + "x" + aHeight + ", (" + (aViewport.getZoomFactor() * 100) + "%)");
+        Log.i(LOGTAG, "Creating plugin at " + aX + ", " + aY + ", " + aWidth + "x" + aHeight + ", (" + (aViewport.zoomFactor * 100) + "%)");
 
         mOriginalX = aX;
         mOriginalY = aY;
         mOriginalWidth = aWidth;
         mOriginalHeight = aHeight;
         mOriginalViewport = aViewport;
-        mLastResolution = aViewport.getZoomFactor();
+        mLastResolution = aViewport.zoomFactor;
 
         clampToMaxSize();
     }
 
     private void clampToMaxSize() {
         if (width > MAX_DIMENSION || height > MAX_DIMENSION) {
             if (width > height) {
                 height = (int)(((float)height/(float)width) * MAX_DIMENSION);
                 width = MAX_DIMENSION;
             } else {
                 width = (int)(((float)width/(float)height) * MAX_DIMENSION);
                 height = MAX_DIMENSION;
             }
         }
     }
 
-    public void reset(int aX, int aY, int aWidth, int aHeight, ViewportMetrics aViewport) {
+    public void reset(int aX, int aY, int aWidth, int aHeight, ImmutableViewportMetrics aViewport) {
         PointF origin = aViewport.getOrigin();
 
         x = mOriginalX = aX + (int)origin.x;
         y = mOriginalY = aY + (int)origin.y;
         width = mOriginalWidth = aWidth;
         height = mOriginalHeight = aHeight;
         mOriginalViewport = aViewport;
-        mLastResolution = aViewport.getZoomFactor();
+        mLastResolution = aViewport.zoomFactor;
 
         clampToMaxSize();
     }
 
     private void reposition(Point aOffset, float aResolution) {
         x = mOriginalX + aOffset.x;
         y = mOriginalY + aOffset.y;
 
@@ -2780,22 +2781,22 @@ class PluginLayoutParams extends Absolut
             width = Math.round(aResolution * mOriginalWidth);
             height = Math.round(aResolution * mOriginalHeight);
             mLastResolution = aResolution;
 
             clampToMaxSize();
         }
     }
 
-    public void reposition(ViewportMetrics viewport) {
+    public void reposition(ImmutableViewportMetrics viewport) {
         PointF targetOrigin = viewport.getOrigin();
         PointF originalOrigin = mOriginalViewport.getOrigin();
 
         Point offset = new Point(Math.round(originalOrigin.x - targetOrigin.x),
                                  Math.round(originalOrigin.y - targetOrigin.y));
 
-        reposition(offset, viewport.getZoomFactor());
+        reposition(offset, viewport.zoomFactor);
     }
 
     public float getLastResolution() {
         return mLastResolution;
     }
 }
--- a/mobile/android/base/Makefile.in
+++ b/mobile/android/base/Makefile.in
@@ -111,16 +111,17 @@ FENNEC_JAVA_FILES = \
   gfx/CairoImage.java \
   gfx/CairoUtils.java \
   gfx/CheckerboardImage.java \
   gfx/FlexibleGLSurfaceView.java \
   gfx/FloatSize.java \
   gfx/GeckoLayerClient.java \
   gfx/GLController.java \
   gfx/GLThread.java \
+  gfx/ImmutableViewportMetrics.java \
   gfx/InputConnectionHandler.java \
   gfx/IntSize.java \
   gfx/Layer.java \
   gfx/LayerController.java \
   gfx/LayerRenderer.java \
   gfx/LayerView.java \
   gfx/NinePatchTileLayer.java \
   gfx/PanningPerfAPI.java \
--- a/mobile/android/base/gfx/GeckoLayerClient.java
+++ b/mobile/android/base/gfx/GeckoLayerClient.java
@@ -90,23 +90,29 @@ public class GeckoLayerClient implements
     // inside a transaction, so no synchronization is needed.
     private boolean mUpdateViewportOnEndDraw;
 
     private String mLastCheckerboardColor;
 
     /* Used by robocop for testing purposes */
     private DrawListener mDrawListener;
 
+    /* Used as a temporary ViewTransform by getViewTransform */
+    private ViewTransform mCurrentViewTransform;
+
     public GeckoLayerClient(Context context) {
         mScreenSize = new IntSize(0, 0);
         mBufferSize = new IntSize(0, 0);
         mDisplayPortMargins = new Rect(DEFAULT_DISPLAY_PORT_MARGIN,
                                        DEFAULT_DISPLAY_PORT_MARGIN,
                                        DEFAULT_DISPLAY_PORT_MARGIN,
                                        DEFAULT_DISPLAY_PORT_MARGIN);
+        // we can fill this in with dummy values because it is always written
+        // to before being read
+        mCurrentViewTransform = new ViewTransform(0, 0, 1);
     }
 
     /** Attaches the root layer to the layer controller so that Gecko appears. */
     void setLayerController(LayerController layerController) {
         mLayerController = layerController;
 
         layerController.setRoot(mRootLayer);
         if (mGeckoViewport != null) {
@@ -312,28 +318,29 @@ public class GeckoLayerClient implements
     public ViewportMetrics getGeckoViewportMetrics() {
         // Return a copy, as we modify this inside the Gecko thread
         if (mGeckoViewport != null)
             return new ViewportMetrics(mGeckoViewport);
         return null;
     }
 
     /** This function is invoked by Gecko via JNI; be careful when modifying signature. */
+    /* This functions needs to be fast because it is called by the compositor every frame.
+     * It avoids taking any locks or allocating any objects. We keep around a
+     * mCurrentViewTransform so we don't need to allocate a new ViewTransform
+     * everytime we're called. NOTE: we could probably switch to returning a ImmutableViewportMetrics
+     * which would avoid the copy into mCurrentViewTransform. */
     public ViewTransform getViewTransform() {
-        // NB: We don't begin a transaction here because this can be called in a synchronous
-        // manner between beginDrawing() and endDrawing(), and that will cause a deadlock.
-
-        synchronized (mLayerController) {
-            ViewportMetrics viewportMetrics = mLayerController.getViewportMetrics();
-            PointF viewportOrigin = viewportMetrics.getOrigin();
-            float scrollX = viewportOrigin.x; 
-            float scrollY = viewportOrigin.y;
-            float zoomFactor = viewportMetrics.getZoomFactor();
-            return new ViewTransform(scrollX, scrollY, zoomFactor);
-        }
+        // getViewportMetrics is thread safe so we don't need to synchronize
+        // on myLayerController.
+        ImmutableViewportMetrics viewportMetrics = mLayerController.getViewportMetrics();
+        mCurrentViewTransform.x = viewportMetrics.viewportRectLeft;
+        mCurrentViewTransform.y = viewportMetrics.viewportRectTop;
+        mCurrentViewTransform.scale = viewportMetrics.zoomFactor;
+        return mCurrentViewTransform;
     }
 
     /** This function is invoked by Gecko via JNI; be careful when modifying signature. */
     public LayerRenderer.Frame createFrame() {
         // Create the shaders and textures if necessary.
         if (!mLayerRendererInitialized) {
             mLayerRenderer.checkMonitoringEnabled();
             mLayerRenderer.createDefaultProgram();
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/gfx/ImmutableViewportMetrics.java
@@ -0,0 +1,64 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.gfx;
+
+import android.graphics.PointF;
+import android.graphics.RectF;
+
+/**
+ * ImmutableViewportMetrics are used to store the viewport metrics
+ * in way that we can access a version of them from multiple threads
+ * without having to take a lock
+ */
+public class ImmutableViewportMetrics {
+
+    // We need to flatten the RectF and FloatSize structures
+    // because Java doesn't have the concept of const classes
+    public final float pageSizeWidth;
+    public final float pageSizeHeight;
+    public final float viewportRectBottom;
+    public final float viewportRectLeft;
+    public final float viewportRectRight;
+    public final float viewportRectTop;
+    public final float zoomFactor;
+
+    public ImmutableViewportMetrics(ViewportMetrics m) {
+        RectF viewportRect = m.getViewport();
+        viewportRectBottom = viewportRect.bottom;
+        viewportRectLeft = viewportRect.left;
+        viewportRectRight = viewportRect.right;
+        viewportRectTop = viewportRect.top;
+
+        FloatSize pageSize = m.getPageSize();
+        pageSizeWidth = pageSize.width;
+        pageSizeHeight = pageSize.height;
+
+        zoomFactor = m.getZoomFactor();
+    }
+
+    // some helpers to make ImmutableViewportMetrics act more like ViewportMetrics
+
+    public PointF getOrigin() {
+        return new PointF(viewportRectLeft, viewportRectTop);
+    }
+
+    public FloatSize getSize() {
+        return new FloatSize(viewportRectRight - viewportRectLeft, viewportRectBottom - viewportRectTop);
+    }
+
+    public RectF getViewport() {
+        return new RectF(viewportRectLeft,
+                         viewportRectTop,
+                         viewportRectRight,
+                         viewportRectBottom);
+    }
+
+    public FloatSize getPageSize() {
+        return new FloatSize(pageSizeWidth, pageSizeHeight);
+    }
+
+
+}
--- a/mobile/android/base/gfx/LayerController.java
+++ b/mobile/android/base/gfx/LayerController.java
@@ -76,17 +76,30 @@ import java.util.regex.Pattern;
  * Many methods require that the monitor be held, with a synchronized (controller) { ... } block.
  */
 public class LayerController implements Tabs.OnTabsChangedListener {
     private static final String LOGTAG = "GeckoLayerController";
 
     private Layer mRootLayer;                   /* The root layer. */
     private LayerView mView;                    /* The main rendering view. */
     private Context mContext;                   /* The current context. */
-    private ViewportMetrics mViewportMetrics;   /* The current viewport metrics. */
+
+    /* This is volatile so that we can read and write to it from different threads.
+     * We avoid synchronization to make getting the viewport metrics from
+     * the compositor as cheap as possible. The viewport is immutable so
+     * we don't need to worry about anyone mutating it while we're reading from it.
+     * Specifically:
+     * 1) reading mViewportMetrics from any thread is fine without synchronization
+     * 2) writing to mViewportMetrics requires synchronizing on the layer controller object
+     * 3) whenver reading multiple fields from mViewportMetrics without synchronization (i.e. in
+     *    case 1 above) you should always frist grab a local copy of the reference, and then use
+     *    that because mViewportMetrics might get reassigned in between reading the different
+     *    fields. */
+    private volatile ImmutableViewportMetrics mViewportMetrics;   /* The current viewport metrics. */
+
     private boolean mWaitForTouchListeners;
 
     private PanZoomController mPanZoomController;
     /*
      * The panning and zooming controller, which interprets pan and zoom gestures for us and
      * updates our visible rect appropriately.
      */
 
@@ -119,17 +132,17 @@ public class LayerController implements 
     private PointF initialTouchLocation = null;
 
     private static Pattern sColorPattern;
 
     public LayerController(Context context) {
         mContext = context;
 
         mForceRedraw = true;
-        mViewportMetrics = new ViewportMetrics();
+        mViewportMetrics = new ImmutableViewportMetrics(new ViewportMetrics());
         mPanZoomController = new PanZoomController(this);
         mView = new LayerView(context, this);
 
         Tabs.getInstance().registerOnTabsChangedListener(this);
 
         ViewConfiguration vc = ViewConfiguration.get(mContext); 
         mTimeout = vc.getLongPressTimeout();
     }
@@ -147,17 +160,17 @@ public class LayerController implements 
 
     public void setForceRedraw() {
         mForceRedraw = true;
     }
 
     public Layer getRoot()                        { return mRootLayer; }
     public LayerView getView()                    { return mView; }
     public Context getContext()                   { return mContext; }
-    public ViewportMetrics getViewportMetrics()   { return mViewportMetrics; }
+    public ImmutableViewportMetrics getViewportMetrics()   { return mViewportMetrics; }
 
     public RectF getViewport() {
         return mViewportMetrics.getViewport();
     }
 
     public FloatSize getViewportSize() {
         return mViewportMetrics.getSize();
     }
@@ -166,17 +179,17 @@ public class LayerController implements 
         return mViewportMetrics.getPageSize();
     }
 
     public PointF getOrigin() {
         return mViewportMetrics.getOrigin();
     }
 
     public float getZoomFactor() {
-        return mViewportMetrics.getZoomFactor();
+        return mViewportMetrics.zoomFactor;
     }
 
     public Bitmap getBackgroundPattern()    { return getDrawable("background"); }
     public Bitmap getShadowPattern()        { return getDrawable("shadow"); }
 
     public PanZoomController getPanZoomController()                                 { return mPanZoomController; }
     public GestureDetector.OnGestureListener getGestureListener()                   { return mPanZoomController; }
     public SimpleScaleGestureDetector.SimpleScaleGestureListener getScaleGestureListener() {
@@ -198,74 +211,80 @@ public class LayerController implements 
      *
      * TODO: Refactor this to use an interface. Expose that interface only to the view and not
      * to the layer client. That way, the layer client won't be tempted to call this, which might
      * result in an infinite loop.
      */
     public void setViewportSize(FloatSize size) {
         // Resize the viewport, and modify its zoom factor so that the page retains proportionally
         // zoomed relative to the screen.
-        float oldHeight = mViewportMetrics.getSize().height;
-        float oldWidth = mViewportMetrics.getSize().width;
-        float oldZoomFactor = mViewportMetrics.getZoomFactor();
-        mViewportMetrics.setSize(size);
+        ViewportMetrics viewportMetrics = new ViewportMetrics(mViewportMetrics);
+        float oldHeight = viewportMetrics.getSize().height;
+        float oldWidth = viewportMetrics.getSize().width;
+        float oldZoomFactor = viewportMetrics.getZoomFactor();
+        viewportMetrics.setSize(size);
 
         // if the viewport got larger (presumably because the vkb went away), and the page
         // is smaller than the new viewport size, increase the page size so that the panzoomcontroller
         // doesn't zoom in to make it fit (bug 718270). this page size change is in anticipation of
         // gecko increasing the page size to match the new viewport size, which will happen the next
         // time we get a draw update.
         if (size.width >= oldWidth && size.height >= oldHeight) {
-            FloatSize pageSize = mViewportMetrics.getPageSize();
+            FloatSize pageSize = viewportMetrics.getPageSize();
             if (pageSize.width < size.width || pageSize.height < size.height) {
-                mViewportMetrics.setPageSize(new FloatSize(Math.max(pageSize.width, size.width),
+                viewportMetrics.setPageSize(new FloatSize(Math.max(pageSize.width, size.width),
                                                            Math.max(pageSize.height, size.height)));
             }
         }
 
         // For rotations, we want the focus point to be at the top left.
         boolean rotation = (size.width > oldWidth && size.height < oldHeight) ||
                            (size.width < oldWidth && size.height > oldHeight);
         PointF newFocus;
         if (rotation) {
             newFocus = new PointF(0, 0);
         } else {
             newFocus = new PointF(size.width / 2.0f, size.height / 2.0f);
         }
         float newZoomFactor = size.width * oldZoomFactor / oldWidth;
-        mViewportMetrics.scaleTo(newZoomFactor, newFocus);
+        viewportMetrics.scaleTo(newZoomFactor, newFocus);
+        mViewportMetrics = new ImmutableViewportMetrics(viewportMetrics);
 
         setForceRedraw();
 
         if (mLayerClient != null) {
             mLayerClient.viewportSizeChanged();
             notifyLayerClientOfGeometryChange();
         }
 
         mPanZoomController.abortAnimation();
         mView.requestRender();
     }
 
     /** Scrolls the viewport by the given offset. You must hold the monitor while calling this. */
     public void scrollBy(PointF point) {
-        PointF origin = mViewportMetrics.getOrigin();
+        ViewportMetrics viewportMetrics = new ViewportMetrics(mViewportMetrics);
+        PointF origin = viewportMetrics.getOrigin();
         origin.offset(point.x, point.y);
-        mViewportMetrics.setOrigin(origin);
+        viewportMetrics.setOrigin(origin);
+        mViewportMetrics = new ImmutableViewportMetrics(viewportMetrics);
 
         notifyLayerClientOfGeometryChange();
         GeckoApp.mAppContext.repositionPluginViews(false);
         mView.requestRender();
     }
 
     /** Sets the current page size. You must hold the monitor while calling this. */
     public void setPageSize(FloatSize size) {
         if (mViewportMetrics.getPageSize().fuzzyEquals(size))
             return;
 
-        mViewportMetrics.setPageSize(size);
+        ViewportMetrics viewportMetrics = new ViewportMetrics(mViewportMetrics);
+        viewportMetrics.setPageSize(size);
+        mViewportMetrics = new ImmutableViewportMetrics(viewportMetrics);
 
         // Page size is owned by the layer client, so no need to notify it of
         // this change.
 
         mView.post(new Runnable() {
             public void run() {
                 mPanZoomController.pageSizeUpdated();
                 mView.requestRender();
@@ -275,33 +294,35 @@ public class LayerController implements 
 
     /**
      * Sets the entire viewport metrics at once. This function does not notify the layer client or
      * the pan/zoom controller, so you will need to call notifyLayerClientOfGeometryChange() or
      * notifyPanZoomControllerOfGeometryChange() after calling this. You must hold the monitor
      * while calling this.
      */
     public void setViewportMetrics(ViewportMetrics viewport) {
-        mViewportMetrics = new ViewportMetrics(viewport);
+        mViewportMetrics = new ImmutableViewportMetrics(viewport);
         // this function may or may not be called on the UI thread,
         // but repositionPluginViews must only be called on the UI thread.
         GeckoApp.mAppContext.runOnUiThread(new Runnable() {
             public void run() {
                 GeckoApp.mAppContext.repositionPluginViews(false);
             }
         });
         mView.requestRender();
     }
 
     /**
      * Scales the viewport, keeping the given focus point in the same place before and after the
      * scale operation. You must hold the monitor while calling this.
      */
     public void scaleWithFocus(float zoomFactor, PointF focus) {
-        mViewportMetrics.scaleTo(zoomFactor, focus);
+        ViewportMetrics viewportMetrics = new ViewportMetrics(mViewportMetrics);
+        viewportMetrics.scaleTo(zoomFactor, focus);
+        mViewportMetrics = new ImmutableViewportMetrics(viewportMetrics);
 
         // We assume the zoom level will only be modified by the
         // PanZoomController, so no need to notify it of this change.
         notifyLayerClientOfGeometryChange();
         GeckoApp.mAppContext.repositionPluginViews(false);
         mView.requestRender();
     }
 
@@ -370,20 +391,21 @@ public class LayerController implements 
      * pixels measured from the top left corner of the root layer, in the coordinate system of the
      * layer itself. This method is used by the viewport controller as part of the process of
      * translating touch events to Gecko's coordinate system.
      */
     public PointF convertViewPointToLayerPoint(PointF viewPoint) {
         if (mRootLayer == null)
             return null;
 
+        ImmutableViewportMetrics viewportMetrics = mViewportMetrics;
         // Undo the transforms.
-        PointF origin = mViewportMetrics.getOrigin();
+        PointF origin = viewportMetrics.getOrigin();
         PointF newPoint = new PointF(origin.x, origin.y);
-        float zoom = mViewportMetrics.getZoomFactor();
+        float zoom = viewportMetrics.zoomFactor;
         viewPoint.x /= zoom;
         viewPoint.y /= zoom;
         newPoint.offset(viewPoint.x, viewPoint.y);
 
         Rect rootPosition = mRootLayer.getPosition();
         newPoint.offset(-rootPosition.left, -rootPosition.top);
 
         return newPoint;
--- a/mobile/android/base/gfx/ViewportMetrics.java
+++ b/mobile/android/base/gfx/ViewportMetrics.java
@@ -74,16 +74,26 @@ public class ViewportMetrics {
     }
 
     public ViewportMetrics(ViewportMetrics viewport) {
         mPageSize = new FloatSize(viewport.getPageSize());
         mViewportRect = new RectF(viewport.getViewport());
         mZoomFactor = viewport.getZoomFactor();
     }
 
+    public ViewportMetrics(ImmutableViewportMetrics viewport) {
+        mPageSize = new FloatSize(viewport.pageSizeWidth, viewport.pageSizeHeight);
+        mViewportRect = new RectF(viewport.viewportRectLeft,
+                                  viewport.viewportRectTop,
+                                  viewport.viewportRectRight,
+                                  viewport.viewportRectBottom);
+        mZoomFactor = viewport.zoomFactor;
+    }
+
+
     public ViewportMetrics(JSONObject json) throws JSONException {
         float x = (float)json.getDouble("x");
         float y = (float)json.getDouble("y");
         float width = (float)json.getDouble("width");
         float height = (float)json.getDouble("height");
         float pageWidth = (float)json.getDouble("pageWidth");
         float pageHeight = (float)json.getDouble("pageHeight");
         float zoom = (float)json.getDouble("zoom");
--- a/mobile/android/base/ui/PanZoomController.java
+++ b/mobile/android/base/ui/PanZoomController.java
@@ -223,17 +223,17 @@ public class PanZoomController
             mState = PanZoomState.NOTHING;
         }
     }
 
     /** This must be called on the UI thread. */
     public void pageSizeUpdated() {
         if (mState == PanZoomState.NOTHING) {
             ViewportMetrics validated = getValidViewportMetrics();
-            if (! mController.getViewportMetrics().fuzzyEquals(validated)) {
+            if (! (new ViewportMetrics(mController.getViewportMetrics())).fuzzyEquals(validated)) {
                 // page size changed such that we are now in overscroll. snap to the
                 // the nearest valid viewport
                 mController.setViewportMetrics(validated);
                 mController.notifyLayerClientOfGeometryChange();
             }
         }
     }