Bug 480958: Update viewport dimensions as per MozScrolledAreaChanged [r=mark.finkle r=stuart]
authorRoy Frostig <froystig@cs.stanford.edu>
Wed, 21 Oct 2009 14:05:55 -0400
changeset 65692 d1a5f2580838c5055f002912054861581799a8c2
parent 65691 4ca634c149e79965f1a1a015b5de91f97d2f0cd2
child 65693 48e9b919986c58d90e6d1a3b08777ccbe12172a4
push id1
push userroot
push dateTue, 26 Apr 2011 22:38:44 +0000
treeherdermozilla-beta@bfdb6e623a36 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmark, stuart
bugs480958
Bug 480958: Update viewport dimensions as per MozScrolledAreaChanged [r=mark.finkle r=stuart]
mobile/chrome/content/BrowserView.js
mobile/chrome/content/TileManager.js.in
mobile/chrome/content/browser.js
--- a/mobile/chrome/content/BrowserView.js
+++ b/mobile/chrome/content/BrowserView.js
@@ -164,16 +164,20 @@ BrowserView.Util = {
   createBrowserViewportState: function createBrowserViewportState() {
     return new BrowserView.BrowserViewportState(new Rect(0, 0, 1, 1), 0, 0, 1);
   },
 
   getViewportStateFromBrowser: function getViewportStateFromBrowser(browser) {
     return browser.__BrowserView__vps;
   },
 
+  /**
+   * Calling this is likely to cause a reflow of the browser's document.  Use
+   * wisely.
+   */
   getBrowserDimensions: function getBrowserDimensions(browser) {
     let cdoc = browser.contentDocument;
     if (cdoc instanceof SVGDocument) {
       let rect = cdoc.rootElement.getBoundingClientRect();
       return [Math.ceil(rect.width), Math.ceil(rect.height)];
     }
 
     // These might not exist yet depending on page load state
@@ -239,57 +243,73 @@ BrowserView.prototype = {
     } catch(e) {}
     this._tileManager = new TileManager(this._appendTile, this._removeTile, this, cacheSize);
     this._visibleRectFactory = visibleRectFactory;
 
     this._idleServiceObserver = new BrowserView.IdleServiceObserver(this);
     this._idleService = Cc["@mozilla.org/widget/idleservice;1"].getService(Ci.nsIIdleService);
     this._idleService.addIdleObserver(this._idleServiceObserver, kBrowserViewPrefetchBeginIdleWait);
   },
-  
+
   uninit: function uninit() {
     this.setBrowser(null, null, false);
     this._idleService.removeIdleObserver(this._idleServiceObserver, kBrowserViewPrefetchBeginIdleWait);
   },
 
   getVisibleRect: function getVisibleRect() {
     return this._visibleRectFactory();
   },
 
   setViewportDimensions: function setViewportDimensions(width, height, causedByZoom) {
     let bvs = this._browserViewportState;
 
     if (!bvs)
       return;
 
+    let oldwidth  = bvs.viewportRect.right;
+    let oldheight = bvs.viewportRect.bottom;
     bvs.viewportRect.right  = width;
     bvs.viewportRect.bottom = height;
 
+    let sizeChanged = (oldwidth != width || oldheight != height);
+
     // XXX we might not want the user's page to disappear from under them
     // at this point, which could happen if the container gets resized such
     // that visible rect becomes entirely outside of viewport rect.  might
     // be wise to define what UX should be in this case, like a move occurs.
     // then again, we could also argue this is the responsibility of the
     // caller who would do such a thing...
 
-    this._viewportChanged(true, !!causedByZoom);
+    this._viewportChanged(sizeChanged, sizeChanged && !!causedByZoom);
   },
 
-  setZoomLevel: function setZoomLevel(zl) {
+  /**
+   * @return [width, height]
+   */
+  getViewportDimensions: function getViewportDimensions() {
+    let bvs = this._browserViewportState;
+
+    if (!bvs)
+      throw "Cannot get viewport dimensions when no browser is set";
+
+    return [bvs.viewportRect.right, bvs.viewportRect.bottom];
+  },
+
+  setZoomLevel: function setZoomLevel(zoomLevel) {
     let bvs = this._browserViewportState;
 
     if (!bvs)
       return;
 
-    let newZL = BrowserView.Util.clampZoomLevel(zl);
+    let newZoomLevel = BrowserView.Util.clampZoomLevel(zoomLevel);
 
-    if (newZL != bvs.zoomLevel) {
+    if (newZoomLevel != bvs.zoomLevel) {
       let browserW = this.viewportToBrowser(bvs.viewportRect.right);
       let browserH = this.viewportToBrowser(bvs.viewportRect.bottom);
-      bvs.zoomLevel = newZL; // side-effect: now scale factor in transformations is newZL
+      bvs.zoomLevel = newZoomLevel; // side-effect: now scale factor in transformations is newZoomLevel
       this.setViewportDimensions(this.browserToViewport(browserW),
                                  this.browserToViewport(browserH),
                                  true);
       this.zoomChanged = true;
     }
   },
 
   getZoomLevel: function getZoomLevel() {
@@ -390,47 +410,41 @@ BrowserView.prototype = {
   /**
    * Swap out the current browser and browser viewport state with a new pair.
    */
   setBrowser: function setBrowser(browser, browserViewportState, doZoom) {
     if (browser && !browserViewportState) {
       throw "Cannot set non-null browser with null BrowserViewportState";
     }
 
-    let browserChanged = (this._browser !== browser);
+    let oldBrowser = this._browser;
 
-    if (this._browser) {
-      this._browser.removeEventListener("MozAfterPaint", this.handleMozAfterPaint, false);
-      this._browser.removeEventListener("scroll", this.handlePageScroll, false);
+    let browserChanged = (oldBrowser !== browser);
 
-      // !!! --- RESIZE HACK BEGIN -----
-      // change to the real event type and perhaps refactor the handler function name
-      this._browser.removeEventListener("FakeMozAfterSizeChange", this.handleMozAfterSizeChange, false);
-      // !!! --- RESIZE HACK END -------
+    if (oldBrowser) {
+      oldBrowser.removeEventListener("MozAfterPaint", this.handleMozAfterPaint, false);
+      oldBrowser.removeEventListener("scroll", this.handlePageScroll, false);
+      oldBrowser.removeEventListener("MozScrolledAreaChanged", this.handleMozScrolledAreaChanged, false);
 
-      this._browser.setAttribute("type", "content");
-      this._browser.docShell.isOffScreenBrowser = false;
+      oldBrowser.setAttribute("type", "content");
+      oldBrowser.docShell.isOffScreenBrowser = false;
     }
 
     this._browser = browser;
     this._contentWindow = (browser) ? browser.contentWindow : null;
     this._browserViewportState = browserViewportState;
 
     if (browser) {
       browser.setAttribute("type", "content-primary");
 
       this.beginBatchOperation();
 
       browser.addEventListener("MozAfterPaint", this.handleMozAfterPaint, false);
       browser.addEventListener("scroll", this.handlePageScroll, false);
-
-      // !!! --- RESIZE HACK BEGIN -----
-      // change to the real event type and perhaps refactor the handler function name
-      browser.addEventListener("FakeMozAfterSizeChange", this.handleMozAfterSizeChange, false);
-      // !!! --- RESIZE HACK END -------
+      browser.addEventListener("MozScrolledAreaChanged", this.handleMozScrolledAreaChanged, false);
 
       if (doZoom) {
         browser.docShell.isOffScreenBrowser = true;
         this.zoomToPage();
       }
 
       this._viewportChanged(browserChanged, browserChanged);
 
@@ -473,59 +487,62 @@ BrowserView.prototype = {
   },
 
   /** If browser scrolls, pan content to new scroll area. */
   handlePageScroll: function handlePageScroll(aEvent) {
     if (aEvent.target != this._browser.contentDocument)
       return;
 
     let { x: scrollX, y: scrollY } = BrowserView.Util.getContentScrollOffset(this._browser);
-    Browser.contentScrollboxScroller.scrollTo(this.browserToViewport(scrollX), 
+    Browser.contentScrollboxScroller.scrollTo(this.browserToViewport(scrollX),
                                               this.browserToViewport(scrollY));
     this.onAfterVisibleMove();
   },
 
-  // !!! --- RESIZE HACK BEGIN -----
-  simulateMozAfterSizeChange: function simulateMozAfterSizeChange() {
-    let [w, h] = BrowserView.Util.getBrowserDimensions(this._browser);
-    let ev = document.createEvent("MouseEvents");
-    ev.initMouseEvent("FakeMozAfterSizeChange", false, false, window, 0, w, h, 0, 0, false, false, false, false, 0, null);
-    this._browser.dispatchEvent(ev);
-  },
-  // !!! --- RESIZE HACK END -------
+  handleMozScrolledAreaChanged: function handleMozScrolledAreaChanged(ev) {
+    if (ev.target != this._browser.contentDocument)
+      return;
+
+    let { x: scrollX, y: scrollY } = BrowserView.Util.getContentScrollOffset(this._browser);
 
-  handleMozAfterSizeChange: function handleMozAfterSizeChange(ev) {
-    // !!! --- RESIZE HACK BEGIN -----
-    // get the correct properties off of the event, these are wrong because
-    // we're using a MouseEvent, as it has an X and Y prop of some sort and
-    // we piggyback on that.
-    let w = ev.screenX;
-    let h = ev.screenY;
-    // !!! --- RESIZE HACK END -------
-    this.setViewportDimensions(this.browserToViewport(w), this.browserToViewport(h));
+    let x = ev.x + scrollX;
+    let y = ev.y + scrollY;
+    let w = ev.width;
+    let h = ev.height;
+
+    /* Adjust width and height from the incoming event properties so that we
+       ignore changes to width and height contributed by growth in page
+       quadrants other than x > 0 && y > 0. */
+    if (x < 0) w += x;
+    if (y < 0) h += y;
+
+    this.setViewportDimensions(this.browserToViewport(w),
+                               this.browserToViewport(h));
   },
 
   zoomToPage: function zoomToPage() {
     let browser = this._browser;
 
     if (!browser)
       return;
 
     var windowUtils = browser.contentWindow
                              .QueryInterface(Ci.nsIInterfaceRequestor)
                              .getInterface(Ci.nsIDOMWindowUtils);
     var handheldFriendly = windowUtils.getDocumentMetadata("HandheldFriendly");
-    
+
     if (handheldFriendly == "true") {
       browser.className = "browser-handheld";
       this.setZoomLevel(1);
       browser.markupDocumentViewer.textZoom = 1;
     } else {
       browser.className = "browser";
-      let [w, h] = BrowserView.Util.getBrowserDimensions(browser);
+      let bvs = this._browserViewportState;  // browser exists, so bvs must as well
+      let w = this.viewportToBrowser(bvs.viewportRect.right);
+      let h = this.viewportToBrowser(bvs.viewportRect.bottom);
       this.setZoomLevel(BrowserView.Util.pageZoomLevel(this.getVisibleRect(), w, h));
     }
   },
 
   zoom: function zoom(aDirection) {
     let bvs = this._browserViewportState;
 
     if (!bvs)
@@ -536,16 +553,59 @@ BrowserView.prototype = {
 
     var zoomDelta = 0.05; // 1/20
     if (aDirection >= 0)
       zoomDelta *= -1;
 
     this.setZoomLevel(bvs.zoomLevel + zoomDelta);
   },
 
+  //
+  // XXXrf  This method is used as a workaround for the fact that
+  // MozAfterPaint events do not guarantee to inform us of all
+  // invalidated paints (See
+  // https://developer.mozilla.org/en/Gecko-Specific_DOM_Events#Important_notes
+  // for details on what the event *does* guarantee).  This is only an
+  // issue when the same current <browser> is used to navigate to a
+  // new page.  Unless a zoom was issued during the page transition
+  // (e.g. a call to zoomToPage() or something of that nature), we
+  // aren't guaranteed that we've actually invalidated the entire
+  // page.  We don't want to leave bits of the previous page in the
+  // view of the new one, so this method exists as a way for Browser
+  // to inform us that the page is changing, and that we really ought
+  // to invalidate everything.  Ideally, we wouldn't have to rely on
+  // this being called, and we would get proper invalidates for the
+  // whole page no matter what is or is not visible.
+  //
+  // Note that this workaround isn't necessary in almost all cases.
+  // Most of the time, one of the following two conditions is
+  // satisfied.  Either
+  //   (1) Pages have different widths so the Browser calls a
+  //       zoomToPage() which forces a dirtyAll, or
+  //   (2) MozAfterPaint does indeed inform us of dirtyRects covering
+  //       the entire page (everything that could possibly become
+  //       visible).
+  //
+  // An example where the workaround is necessary is in going from the
+  // firstrun page to a Twitter feed.  Condition (1) isn't satisfied
+  // because both pages have the same in-browser width.  Condition (2)
+  // isn't satisfied for all of the page because of what appears to be
+  // some kind of overflowing container div.  To see this, place some
+  // dumps of the rectangles incoming in the HandleMozAfterPaint
+  // method, and navigate to someone's Twitter feed.  Only sidebar on
+  // the right-hand side of the feed is invalidated across the page,
+  // and otherwise, the 500x800 visible region is invalidated as well.
+  //
+  // See browser.js for where this is invoked.
+  //
+  invalidateEntireView: function invalidateEntireView() {
+    if (this._browserViewportState)
+      this._viewportChanged(false, true);
+  },
+
   /**
    * Render a rectangle within the browser viewport to the destination canvas
    * under the given scale.
    *
    * @param destCanvas The destination canvas into which the image is rendered.
    * @param destWidth Destination width
    * @param destHeight Destination height
    * @param srcRect [optional] The source rectangle in BrowserView coordinates.
--- a/mobile/chrome/content/TileManager.js.in
+++ b/mobile/chrome/content/TileManager.js.in
@@ -146,27 +146,102 @@ TileManager.prototype = {
    * critical rect.
    */
   viewportChangeHandler: function viewportChangeHandler(viewportRect,
                                                         criticalRect,
                                                         boundsSizeChanged,
                                                         dirtyAll) {
     let tc = this._tileCache;
 
-    tc.iBound = Math.ceil(viewportRect.right / kTileWidth);
-    tc.jBound = Math.ceil(viewportRect.bottom / kTileHeight);
+    let iBoundOld = tc.iBound;
+    let jBoundOld = tc.jBound;
+    let iBound = tc.iBound = Math.ceil(viewportRect.right / kTileWidth) - 1;
+    let jBound = tc.jBound = Math.ceil(viewportRect.bottom / kTileHeight) - 1;
 
     if (criticalRect.isEmpty() || !criticalRect.equals(this._criticalRect)) {
       this.beginCriticalMove(criticalRect);
-      this.endCriticalMove(criticalRect, !boundsSizeChanged);
+      this.endCriticalMove(criticalRect, !(dirtyAll || boundsSizeChanged));
     }
 
-    if (boundsSizeChanged) {
-      // TODO fastpath if !dirtyAll
+    if (dirtyAll) {
       this.dirtyRects([viewportRect.clone()], true);
+    } else if (boundsSizeChanged) {
+
+      //
+      // This is a special case.  The bounds size changed, but we are
+      // told that not everything is dirty (so mayhap content grew or
+      // shrank vertically or horizontally).  We might have old tiles
+      // around in those areas just due to the fact that they haven't
+      // been yet evicted, so we patrol the new regions in search of
+      // any such leftover tiles and mark those we find as dirty.
+      //
+      // The two dirty rects below mark dirty any renegade tiles in
+      // the newly annexed grid regions as per the following diagram
+      // of the "new" viewport.
+      //
+      //   +------------+------+
+      //   |old         | A    |
+      //   |viewport    |      |
+      //   |            |      |
+      //   |            |      |
+      //   |            |      |
+      //   +------------+      |
+      //   | B          |      |
+      //   |            |      |
+      //   +------------+------+
+      //
+      // The first rectangle covers annexed region A, the second
+      // rectangle covers annexed region B.
+      //
+      // XXXrf If the tiles are large, then we are creating some
+      // redundant work here by invalidating the entire tile that
+      // the old viewport boundary crossed (note markDirty() being
+      // called with no rectangle parameter).  The rectangular area
+      // within the tile lying beyond the old boundary is certainly
+      // dirty, but not the area before.  Moreover, since we mark
+      // dirty entire tiles that may cross into the old viewport,
+      // they might, in particular, cross into the critical rect
+      // (which is anyhwere in the old viewport), so we call a
+      // criticalRectPaint() for such cleanup. We do all this more
+      // or less because we don't have much of a notion of "the old
+      // viewport" here except for in the sense that we know the
+      // index bounds on the tilecache grid from before (and the new
+      // index bounds now).
+      //
+
+      let t, l, b, r, rect;
+      let rects = [];
+
+      if (iBoundOld <= iBound) {
+        l = iBoundOld * kTileWidth;
+        t = 0;
+        r = (iBound + 1) * kTileWidth;
+        b = (jBound + 1) * kTileHeight;
+
+        rect = new Rect(l, t, r - l, b - t);
+        rect.restrictTo(viewportRect);
+
+        if (!rect.isEmpty())
+          rects.push(rect);
+      }
+
+      if (jBoundOld <= jBound) {
+        l = 0;
+        t = jBoundOld * kTileHeight;
+        r = (iBound + 1) * kTileWidth;
+        b = (jBound + 1) * kTileHeight;
+
+        rect = new Rect(l, t, r - l, b - t);
+        rect.restrictTo(viewportRect);
+
+        if (!rect.isEmpty())
+          rects.push(rect);
+      }
+
+      this.dirtyRects(rects, true);
     }
   },
 
   dirtyRects: function dirtyRects(rects, doCriticalRender) {
     let criticalIsDirty = false;
     let criticalRect = this._criticalRect;
     let tc = this._tileCache;
     let crawler = this._crawler;
--- a/mobile/chrome/content/browser.js
+++ b/mobile/chrome/content/browser.js
@@ -537,17 +537,17 @@ var Browser = {
     bv.commitBatchOperation();
 
     // If some add-ons were disabled during during an application update, alert user
     if (gPrefService.prefHasUserValue("extensions.disabledAddons")) {
       let addons = gPrefService.getCharPref("extensions.disabledAddons").split(",");
       if (addons.length > 0) {
         let disabledStrings = document.getElementById("bundle_browser").getString("alertAddonsDisabled");
         let label = PluralForm.get(addons.length, disabledStrings).replace("#1", addons.length);
-  
+
         let alerts = Cc["@mozilla.org/alerts-service;1"].getService(Ci.nsIAlertsService);
         alerts.showAlertNotification(URI_GENERIC_ICON_XPINSTALL, strings.getString("alertAddons"),
                                      label, false, "", null);
       }
       gPrefService.clearUserPref("extensions.disabledAddons");
     }
 
     // Re-enable plugins if we had previously disabled them. We should get rid of
@@ -1269,17 +1269,17 @@ Browser.MainDragger.prototype = {
     let windowUtils = frame.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
 
     windowUtils.getScrollXY(false, origX, origY);
     frame.scrollBy(doffset.x, doffset.y);
     windowUtils.getScrollXY(false, newX, newY);
 
     doffset.subtract(newX.value - origX.value, newY.value - origY.value);
   }
-  
+
 };
 
 function nsBrowserAccess()
 {
 }
 
 nsBrowserAccess.prototype = {
   QueryInterface: function(aIID) {
@@ -1382,17 +1382,17 @@ const BrowserSearch = {
   get searchService() {
     delete this.searchService;
     return this.searchService = Cc["@mozilla.org/browser/search-service;1"].getService(Ci.nsIBrowserSearchService);
   },
 
   get engines() {
     if (this._engines)
       return this._engines;
-    return this._engines = this.searchService.getVisibleEngines({ });    
+    return this._engines = this.searchService.getVisibleEngines({ });
   },
 
   addPageSearchEngine: function (aEngine, aDocument) {
     // Clean the engine referenced for document that didn't exist anymore
     let browsers = Browser.browsers;
     this._allEngines = this._allEngines.filter(function(element) {
        return browsers.some(function (browser) browser.contentDocument == element.doc);
     }, this);
@@ -1420,17 +1420,17 @@ const BrowserSearch = {
       return;
     }
 
     // XXX limit to the first search engine for now
     for (let i = 0; i<1; i++) {
       let button = document.createElement("button");
       button.className = "search-engine-button button-dark";
       button.setAttribute("oncommand", "BrowserSearch.addPermanentSearchEngine(this.engine);this.parentNode.hidden=true;");
-      
+
       let engine = newEngines[i];
       button.engine = engine.engine;
       button.setAttribute("label", engine.engine.title);
       button.setAttribute("image", BrowserUI._favicon.src);
 
       container.appendChild(button);
     }
 
@@ -1587,17 +1587,17 @@ IdentityHandler.prototype = {
   /**
    * Determine the identity of the page being displayed by examining its SSL cert
    * (if available) and, if necessary, update the UI to reflect this.
    */
   checkIdentity: function() {
     let state = Browser.selectedTab.getIdentityState();
     let location = getBrowser().contentWindow.location;
     let currentStatus = getBrowser().securityUI.QueryInterface(Ci.nsISSLStatusProvider).SSLStatus;
-    
+
     this._lastStatus = currentStatus;
     this._lastLocation = {};
     try {
       // make a copy of the passed in location to avoid cycles
       this._lastLocation = { host: location.host, hostname: location.hostname, port: location.port };
     } catch (ex) { }
 
     if (state & Ci.nsIWebProgressListener.STATE_IDENTITY_EV_TOPLEVEL)
@@ -1761,17 +1761,17 @@ IdentityHandler.prototype = {
 
     BrowserUI.pushPopup(this, [this._identityPopup, this._identityBox]);
     BrowserUI.lockToolbar();
   },
 
   hide: function ih_hide() {
     this._identityPopup.hidden = true;
     this._identityBox.removeAttribute("open");
-    
+
     BrowserUI.popPopup();
     BrowserUI.unlockToolbar();
   },
 
   /**
    * Click handler for the identity-box element in primary chrome.
    */
   handleIdentityButtonEvent: function(event) {
@@ -1992,17 +1992,17 @@ const gSessionHistoryObserver = {
     }
   }
 };
 
 var MemoryObserver = {
   observe: function() {
     let memory = Cc["@mozilla.org/xpcom/memory-service;1"].getService(Ci.nsIMemory);
     do {
-      Browser.windowUtils.garbageCollect();      
+      Browser.windowUtils.garbageCollect();
     } while (memory.isLowMemory() && Browser.sacrificeTab());
   }
 };
 
 #ifdef WINCE
 // Windows Mobile does not resize the window automatically when the soft
 // keyboard is displayed. Maemo does resize the window.
 var SoftKeyboardObserver = {
@@ -2030,37 +2030,37 @@ function getNotificationBox(aWindow) {
 function importDialog(parent, src, arguments) {
   // load the dialog with a synchronous XHR
   let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance();
   xhr.open("GET", src, false);
   xhr.overrideMimeType("text/xml");
   xhr.send(null);
   if (!xhr.responseXML)
     return null;
-  
+
   let doc = xhr.responseXML.documentElement;
- 
+
   var dialog  = null;
-  
+
   // we need to insert before select-container if we want it to show correctly
   let selectContainer = document.getElementById("select-container");
   let parent = selectContainer.parentNode;
-  
+
   // emit DOMWillOpenModalDialog event
   let event = document.createEvent("Events");
   event.initEvent("DOMWillOpenModalDialog", true, false);
   let dispatcher = parent || getBrowser();
   dispatcher.dispatchEvent(event);
 
-  // create a full-screen semi-opaque box as a background 
+  // create a full-screen semi-opaque box as a background
   let back = document.createElement("box");
   back.setAttribute("class", "modal-block");
   dialog = back.appendChild(document.importNode(doc, true));
   parent.insertBefore(back, selectContainer);
-  
+
   dialog.arguments = arguments;
   dialog.parent = parent;
   return dialog;
 }
 
 function showDownloadManager(aWindowContext, aID, aReason) {
   BrowserUI.showPanel("downloads-container");
   // TODO: select the download with aID
@@ -2396,20 +2396,16 @@ Tab.prototype = {
   /**
    * Throttles redraws to once every second while loading the page, zooming to fit page if
    * user hasn't started zooming.
    */
   _resizeAndPaint: function() {
     let bv = Browser._browserView;
 
     if (this == Browser.selectedTab) {
-      // !!! --- RESIZE HACK BEGIN -----
-      bv.simulateMozAfterSizeChange();
-      // !!! --- RESIZE HACK END -----
-
       let restoringPage = (this._state != null);
 
       if (!this._browserViewportState.zoomChanged && !restoringPage) {
         // Only fit page if user hasn't started zooming around and this is a page that
         // isn't being restored.
         bv.zoomToPage();
       }
 
@@ -2435,16 +2431,20 @@ Tab.prototype = {
     //  dump("!!! Already loading this tab, please file a bug\n");
 
     this._loading = true;
     this._browserViewportState.zoomChanged = false;
 
     if (!this._loadingTimeout) {
       if (this == Browser.selectedTab) {
         Browser._browserView.beginBatchOperation();
+
+        // XXXrf  This is a workaround.  See the comment at the top of
+        // this method's definition in BrowserView.js for details.
+        Browser._browserView.invalidateEntireView();
       }
       this._loadingTimeout = setTimeout(Util.bind(this._resizeAndPaint, this), 2000);
     }
   },
 
   endLoading: function() {
     //if (!this._loading)
     //  dump("!!! Already finished loading this tab, please file a bug\n");