Bug 1593170. Make the reftest harness deal with SynchronizeForSnapshot and FlushRendering returning promises. r=mattwoodrow
☠☠ backed out by bdca724cf3a2 ☠ ☠
authorTimothy Nikkel <tnikkel@gmail.com>
Mon, 18 Nov 2019 00:45:25 +0000
changeset 502362 a004de6493422688953b5b8ea2fc5e1522240f47
parent 502361 78d380a2241afa5c60a90ac8ceca9b3108b99345
child 502363 80bd8cadf835988a4ea48084d80cf47eb4aa5b24
push id114172
push userdluca@mozilla.com
push dateTue, 19 Nov 2019 11:31:10 +0000
treeherdermozilla-inbound@b5c5ba07d3db [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmattwoodrow
bugs1593170
milestone72.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 1593170. Make the reftest harness deal with SynchronizeForSnapshot and FlushRendering returning promises. r=mattwoodrow The code comment mostly explains the design. Basically, we force nothing to happen while we wait for the promises to finish and instead record what we need to do once the promise is finished, and do those pending tasks when it's finished. Differential Revision: https://phabricator.services.mozilla.com/D51344
layout/tools/reftest/reftest-content.js
--- a/layout/tools/reftest/reftest-content.js
+++ b/layout/tools/reftest/reftest-content.js
@@ -525,49 +525,173 @@ function FlushRendering(aFlushMode) {
     }, function(reason) {
         // We expect actors to go away causing sendQuery's to fail, so
         // just note it.
         LogInfo("FlushRendering sendQuery to parent rejected: " + reason);
     });
 }
 
 function WaitForTestEnd(contentRootElement, inPrintMode, spellCheckedElements, forURL) {
+    // WaitForTestEnd works via the MakeProgress function below. It is responsible for
+    // moving through the states listed above and calling FlushRendering. We also listen
+    // for a number of events, the most important of which is the AfterPaintListener,
+    // which is responsible for updating the canvas after paints. In a fission world
+    // FlushRendering and updating the canvas must necessarily be async operations.
+    // During these async operations we want to wait for them to finish and we don't
+    // want to try to do anything else (what would we even want to do while only some of
+    // the processes involved have flushed layout or updated their layer trees?). So
+    // we call OperationInProgress whenever we are about to go back to the event loop
+    // during one of these calls, and OperationCompleted when it finishes. This prevents
+    // anything else from running while we wait and getting us into a confused state. We
+    // then record anything that happens while we are waiting to make sure that the
+    // right actions are triggered. The possible actions are basically calling
+    // MakeProgress from a setTimeout, and updating the canvas for an after paint event.
+    // The after paint listener just stashes the rects and we update them after a
+    // completed MakeProgress call. This is handled by
+    // HandlePendingTasksAfterMakeProgress, which also waits for any pending after paint
+    // events. The general sequence of events is:
+    //   - MakeProgress
+    //   - HandlePendingTasksAfterMakeProgress
+    //     - wait for after paint event if one is pending
+    //     - update canvas for after paint events we have received
+    //   - MakeProgress
+    //   etc
+
+    var setTimeoutCallMakeProgressWhenComplete = false;
+
+    var operationInProgress = false;
+    function OperationInProgress() {
+        if (operationInProgress != false) {
+            LogWarning("Nesting atomic operations?");
+        }
+        operationInProgress = true;
+    }
+    function OperationCompleted() {
+        if (operationInProgress != true) {
+            LogWarning("Mismatched OperationInProgress/OperationCompleted calls?");
+        }
+        operationInProgress = false;
+        if (setTimeoutCallMakeProgressWhenComplete) {
+            setTimeoutCallMakeProgressWhenComplete = false;
+            setTimeout(CallMakeProgress, 0);
+        }
+    }
+    function AssertNoOperationInProgress() {
+        if (operationInProgress) {
+            LogWarning("AssertNoOperationInProgress but operationInProgress");
+        }
+    }
+
+    var updateCanvasPending = false;
+    var updateCanvasRects = []
+
     var stopAfterPaintReceived = false;
     var currentDoc = content.document;
     var state = STATE_WAITING_TO_FIRE_INVALIDATE_EVENT;
 
+    var setTimeoutMakeProgressPending = false;
+
+    function CallSetTimeoutMakeProgress() {
+        if (setTimeoutMakeProgressPending) {
+            return;
+        }
+        setTimeoutMakeProgressPending = true;
+        setTimeout(CallMakeProgress, 0);
+    }
+
+    // This should only ever be called from a timeout.
+    function CallMakeProgress() {
+        if (operationInProgress) {
+            setTimeoutCallMakeProgressWhenComplete = true;
+            return;
+        }
+        setTimeoutMakeProgressPending = false;
+        MakeProgress();
+    }
+
+    var waitingForAnAfterPaint = false;
+
+    // Updates the canvas if there are pending updates for it. Checks if we
+    // need to call MakeProgress.
+    function HandlePendingTasksAfterMakeProgress() {
+        AssertNoOperationInProgress();
+
+        if ((state == STATE_WAITING_TO_FIRE_INVALIDATE_EVENT || state == STATE_WAITING_TO_FINISH) &&
+            shouldWaitForPendingPaints()) {
+            LogInfo("HandlePendingTasksAfterMakeProgress waiting for a MozAfterPaint");
+            // We are in a state where we wait for MozAfterPaint to clear and a
+            // MozAfterPaint event is pending, give it a chance to fire, but don't
+            // let anything else run.
+            waitingForAnAfterPaint = true;
+            OperationInProgress();
+            return;
+        }
+
+        if (updateCanvasPending) {
+            LogInfo("HandlePendingTasksAfterMakeProgress updating canvas");
+            updateCanvasPending = false;
+            let rects = updateCanvasRects;
+            updateCanvasRects = [];
+            OperationInProgress();
+            let promise = SendUpdateCanvasForEvent(rects, contentRootElement);
+            promise.then(function () {
+                OperationCompleted();
+                // After paint events are fired immediately after a paint (one
+                // of the things that can call us). Don't confuse ourselves by
+                // firing synchronously if we triggered the paint ourselves.
+                CallSetTimeoutMakeProgress();
+            });
+        }
+    }
+
     function AfterPaintListener(event) {
         LogInfo("AfterPaintListener in " + event.target.document.location.href);
         if (event.target.document != currentDoc) {
             // ignore paint events for subframes or old documents in the window.
             // Invalidation in subframes will cause invalidation in the toplevel document anyway.
             return;
         }
 
-        SendUpdateCanvasForEvent(forURL, event, contentRootElement);
-        // These events are fired immediately after a paint. Don't
-        // confuse ourselves by firing synchronously if we triggered the
-        // paint ourselves.
-        setTimeout(MakeProgress, 0);
+        updateCanvasPending = true;
+        for (let i = 0; i < event.clientRects.length; ++i) {
+            let r = event.clientRects[i];
+
+            // Copy the rect; it's content and we are chrome, which means if the
+            // document goes away (and it can in some crashtests) our reference
+            // to it will be turned into a dead wrapper that we can't acccess.
+            updateCanvasRects.push({ left: r.left, top: r.top, right: r.right, bottom: r.bottom });
+        }
+
+        if (waitingForAnAfterPaint) {
+            waitingForAnAfterPaint = false;
+            OperationCompleted();
+        }
+
+        if (!operationInProgress) {
+            HandlePendingTasksAfterMakeProgress();
+        }
+        // Otherwise we know that eventually after the operation finishes we
+        // will get a MakeProgress and/or HandlePendingTasksAfterMakeProgress
+        // call, so we don't need to do anything.
     }
 
     function AttrModifiedListener() {
         LogInfo("AttrModifiedListener fired");
         // Wait for the next return-to-event-loop before continuing --- for
         // example, the attribute may have been modified in an subdocument's
         // load event handler, in which case we need load event processing
         // to complete and unsuppress painting before we check isMozAfterPaintPending.
-        setTimeout(MakeProgress, 0);
+        CallSetTimeoutMakeProgress();
     }
 
     function ExplicitPaintsCompleteListener() {
         LogInfo("ExplicitPaintsCompleteListener fired");
         // Since this can fire while painting, don't confuse ourselves by
         // firing synchronously. It's fine to do this asynchronously.
-        setTimeout(MakeProgress, 0);
+        CallSetTimeoutMakeProgress();
     }
 
     function RemoveListeners() {
         // OK, we can end the test now.
         removeEventListener("MozAfterPaint", AfterPaintListener, false);
         if (contentRootElement) {
             contentRootElement.removeEventListener("DOMAttrModified", AttrModifiedListener);
         }
@@ -582,45 +706,65 @@ function WaitForTestEnd(contentRootEleme
     // change from returning true to returning false is monitored via some kind
     // of event listener which eventually calls this function.
     function MakeProgress() {
         if (state >= STATE_COMPLETED) {
             LogInfo("MakeProgress: STATE_COMPLETED");
             return;
         }
 
+        LogInfo("MakeProgress");
+
         // We don't need to flush styles any more when we are in the state
         // after reftest-wait has removed.
+        OperationInProgress();
+        let promise = Promise.resolve(undefined);
         if (state != STATE_WAITING_TO_FINISH) {
           // If we are waiting for the MozReftestInvalidate event we don't want
           // to flush throttled animations. Flushing throttled animations can
           // continue to cause new MozAfterPaint events even when all the
           // rendering we're concerned about should have ceased. Since
           // MozReftestInvalidate won't be sent until we finish waiting for all
           // MozAfterPaint events, we should avoid flushing throttled animations
           // here or else we'll never leave this state.
           flushMode = (state === STATE_WAITING_TO_FIRE_INVALIDATE_EVENT)
                     ? FlushMode.IGNORE_THROTTLED_ANIMATIONS
                     : FlushMode.ALL;
-          FlushRendering(flushMode);
+          promise = FlushRendering(flushMode);
         }
+        promise.then(function () {
+            OperationCompleted();
+            MakeProgress2();
+            // If there is an operation in progress then we know there will be
+            // a MakeProgress call is will happen after it finishes.
+            if (!operationInProgress) {
+                HandlePendingTasksAfterMakeProgress();
+            }
+        });
+    }
 
+    function MakeProgress2() {
         switch (state) {
         case STATE_WAITING_TO_FIRE_INVALIDATE_EVENT: {
             LogInfo("MakeProgress: STATE_WAITING_TO_FIRE_INVALIDATE_EVENT");
-            if (shouldWaitForExplicitPaintWaiters() || shouldWaitForPendingPaints()) {
+            if (shouldWaitForExplicitPaintWaiters() || shouldWaitForPendingPaints() ||
+                updateCanvasPending) {
                 gFailureReason = "timed out waiting for pending paint count to reach zero";
                 if (shouldWaitForExplicitPaintWaiters()) {
                     gFailureReason += " (waiting for MozPaintWaitFinished)";
                     LogInfo("MakeProgress: waiting for MozPaintWaitFinished");
                 }
                 if (shouldWaitForPendingPaints()) {
                     gFailureReason += " (waiting for MozAfterPaint)";
                     LogInfo("MakeProgress: waiting for MozAfterPaint");
                 }
+                if (updateCanvasPending) {
+                    gFailureReason += " (waiting for updateCanvasPending)";
+                    LogInfo("MakeProgress: waiting for updateCanvasPending");
+                }
                 return;
             }
 
             state = STATE_WAITING_FOR_REFTEST_WAIT_REMOVAL;
             var hasReftestWait = shouldWaitForReftestWaitRemoval(contentRootElement);
             // Notify the test document that now is a good time to test some invalidation
             LogInfo("MakeProgress: dispatching MozReftestInvalidate");
             if (contentRootElement) {
@@ -644,20 +788,27 @@ function WaitForTestEnd(contentRootEleme
             if (!inPrintMode && doPrintMode(contentRootElement)) {
                 LogInfo("MakeProgress: setting up print mode");
                 setupPrintMode();
             }
 
             if (hasReftestWait && !shouldWaitForReftestWaitRemoval(contentRootElement)) {
                 // MozReftestInvalidate handler removed reftest-wait.
                 // We expect something to have been invalidated...
-                FlushRendering(FlushMode.ALL);
-                if (!shouldWaitForPendingPaints() && !shouldWaitForExplicitPaintWaiters()) {
-                    LogWarning("MozInvalidateEvent didn't invalidate");
-                }
+                OperationInProgress();
+                let promise = FlushRendering(FlushMode.ALL);
+                promise.then(function () {
+                    OperationCompleted();
+                    if (!updateCanvasPending && !shouldWaitForPendingPaints() &&
+                        !shouldWaitForExplicitPaintWaiters()) {
+                        LogWarning("MozInvalidateEvent didn't invalidate");
+                    }
+                    MakeProgress();
+                });
+                return;
             }
             // Try next state
             MakeProgress();
             return;
         }
 
         case STATE_WAITING_FOR_REFTEST_WAIT_REMOVAL:
             LogInfo("MakeProgress: STATE_WAITING_FOR_REFTEST_WAIT_REMOVAL");
@@ -684,17 +835,21 @@ function WaitForTestEnd(contentRootEleme
             LogInfo("MakeProgress: STATE_WAITING_FOR_APZ_FLUSH");
             gFailureReason = "timed out waiting for APZ flush to complete";
 
             var os = Cc[NS_OBSERVER_SERVICE_CONTRACTID].getService(Ci.nsIObserverService);
             var flushWaiter = function(aSubject, aTopic, aData) {
                 if (aTopic) LogInfo("MakeProgress: apz-repaints-flushed fired");
                 os.removeObserver(flushWaiter, "apz-repaints-flushed");
                 state = STATE_WAITING_TO_FINISH;
-                MakeProgress();
+                if (operationInProgress) {
+                    CallSetTimeoutMakeProgress();
+                } else {
+                    MakeProgress();
+                }
             };
             os.addObserver(flushWaiter, "apz-repaints-flushed");
 
             var willSnapshot = IsSnapshottableTestType();
             var noFlush =
                 !(contentRootElement &&
                   contentRootElement.classList.contains("reftest-no-flush"));
             if (noFlush && willSnapshot && windowUtils().flushApzRepaints()) {
@@ -708,27 +863,32 @@ function WaitForTestEnd(contentRootEleme
         case STATE_WAITING_FOR_APZ_FLUSH:
             LogInfo("MakeProgress: STATE_WAITING_FOR_APZ_FLUSH");
             // Nothing to do here; once we get the apz-repaints-flushed event
             // we will go to STATE_WAITING_TO_FINISH
             return;
 
         case STATE_WAITING_TO_FINISH:
             LogInfo("MakeProgress: STATE_WAITING_TO_FINISH");
-            if (shouldWaitForExplicitPaintWaiters() || shouldWaitForPendingPaints()) {
+            if (shouldWaitForExplicitPaintWaiters() || shouldWaitForPendingPaints() ||
+                updateCanvasPending) {
                 gFailureReason = "timed out waiting for pending paint count to " +
                     "reach zero (after reftest-wait removed and switch to print mode)";
                 if (shouldWaitForExplicitPaintWaiters()) {
                     gFailureReason += " (waiting for MozPaintWaitFinished)";
                     LogInfo("MakeProgress: waiting for MozPaintWaitFinished");
                 }
                 if (shouldWaitForPendingPaints()) {
                     gFailureReason += " (waiting for MozAfterPaint)";
                     LogInfo("MakeProgress: waiting for MozAfterPaint");
                 }
+                if (updateCanvasPending) {
+                    gFailureReason += " (waiting for updateCanvasPending)";
+                    LogInfo("MakeProgress: waiting for updateCanvasPending");
+                }
                 return;
             }
             if (contentRootElement) {
               var elements = getNoPaintElements(contentRootElement);
               for (var i = 0; i < elements.length; ++i) {
                   if (windowUtils().checkAndClearPaintedState(elements[i])) {
                       SendFailedNoPaint();
                   }
@@ -782,32 +942,40 @@ function WaitForTestEnd(contentRootEleme
     }
     gExplicitPendingPaintsCompleteHook = ExplicitPaintsCompleteListener;
     gTimeoutHook = RemoveListeners;
 
     // Listen for spell checks on spell-checked elements.
     var numPendingSpellChecks = spellCheckedElements.length;
     function decNumPendingSpellChecks() {
         --numPendingSpellChecks;
-        MakeProgress();
+        if (operationInProgress) {
+            CallSetTimeoutMakeProgress();
+        } else {
+            MakeProgress();
+        }
     }
     for (let editable of spellCheckedElements) {
         try {
             onSpellCheck(editable, decNumPendingSpellChecks);
         } catch (err) {
             // The element may not have an editor, so ignore it.
             setTimeout(decNumPendingSpellChecks, 0);
         }
     }
 
     // Take a full snapshot now that all our listeners are set up. This
     // ensures it's impossible for us to miss updates between taking the snapshot
     // and adding our listeners.
-    SendInitCanvasWithSnapshot(forURL);
-    MakeProgress();
+    OperationInProgress();
+    let promise = SendInitCanvasWithSnapshot(forURL);
+    promise.then(function () {
+        OperationCompleted();
+        MakeProgress();
+    });
 }
 
 function OnDocumentLoad(event)
 {
     var currentDoc = content.document;
     if (event.target != currentDoc)
         // Ignore load events for subframes.
         return;
@@ -850,29 +1018,29 @@ function OnDocumentLoad(event)
     var contentRootElement = currentDoc ? currentDoc.documentElement : null;
     currentDoc = null;
     setupFullZoom(contentRootElement);
     setupTextZoom(contentRootElement);
     setupViewport(contentRootElement);
     setupDisplayport(contentRootElement);
     var inPrintMode = false;
 
-    function AfterOnLoadScripts() {
+    async function AfterOnLoadScripts() {
         // Regrab the root element, because the document may have changed.
         var contentRootElement =
           content.document ? content.document.documentElement : null;
 
         // Flush the document in case it got modified in a load event handler.
-        FlushRendering(FlushMode.ALL);
+        await FlushRendering(FlushMode.ALL);
 
         // Take a snapshot now. We need to do this before we check whether
         // we should wait, since this might trigger dispatching of
         // MozPaintWait events and make shouldWaitForExplicitPaintWaiters() true
         // below.
-        var painted = SendInitCanvasWithSnapshot(ourURL);
+        let painted = await SendInitCanvasWithSnapshot(ourURL);
 
         if (shouldWaitForExplicitPaintWaiters() ||
             (!inPrintMode && doPrintMode(contentRootElement)) ||
             // If we didn't force a paint above, in
             // InitCurrentCanvasWithSnapshot, so we should wait for a
             // paint before we consider them done.
             !painted) {
             LogInfo("AfterOnLoadScripts belatedly entering WaitForTestEnd");
@@ -1133,29 +1301,30 @@ function IsSnapshottableTestType()
     // Script, load-only, and PDF-print tests do not need any snapshotting.
     return !(gCurrentTestType == TYPE_SCRIPT ||
              gCurrentTestType == TYPE_LOAD ||
              gCurrentTestType == TYPE_PRINT);
 }
 
 const SYNC_DEFAULT = 0x0;
 const SYNC_ALLOW_DISABLE = 0x1;
+// Returns a promise that resolve when the snapshot is done.
 function SynchronizeForSnapshot(flags)
 {
     if (!IsSnapshottableTestType()) {
-        return;
+        return Promise.resolve(undefined);
     }
 
     if (flags & SYNC_ALLOW_DISABLE) {
         var docElt = content.document.documentElement;
         if (docElt &&
             (docElt.hasAttribute("reftest-no-sync-layers") ||
              docElt.classList.contains("reftest-no-flush"))) {
             LogInfo("Test file chose to skip SynchronizeForSnapshot");
-            return;
+            return Promise.resolve(undefined);
         }
     }
 
     let browsingContext = content.docShell.browsingContext;
     let promise = content.getWindowGlobalChild().getActor("ReftestFission").sendQuery("UpdateLayerTree", {browsingContext});
     return promise.then(function (result) {
         for (let errorString of result) {
             LogError(errorString);
@@ -1289,47 +1458,53 @@ function SendFailedOpaqueLayer(why)
     sendAsyncMessage("reftest:FailedOpaqueLayer", { why: why });
 }
 
 function SendFailedAssignedLayer(why)
 {
     sendAsyncMessage("reftest:FailedAssignedLayer", { why: why });
 }
 
-// Return true if a snapshot was taken.
+// Returns a promise that resolves to a bool that indicates if a snapshot was taken.
 function SendInitCanvasWithSnapshot(forURL)
 {
     if (forURL != gCurrentURL) {
         LogInfo("SendInitCanvasWithSnapshot called for previous document");
         // Lie and say we painted because it doesn't matter, this is a test we
         // are already done with that is clearing out. Then AfterOnLoadScripts
         // should finish quicker if that is who is calling us.
-        return true;
+        return Promise.resolve(true);
     }
 
     // If we're in the same process as the top-level XUL window, then
     // drawing that window will also update our layers, so no
     // synchronization is needed.
     //
     // NB: this is a test-harness optimization only, it must not
     // affect the validity of the tests.
     if (gBrowserIsRemote) {
-        SynchronizeForSnapshot(SYNC_DEFAULT);
+        let promise = SynchronizeForSnapshot(SYNC_DEFAULT);
+        return promise.then(function () {
+            let ret = sendSyncMessage("reftest:InitCanvasWithSnapshot")[0];
+
+            gHaveCanvasSnapshot = ret.painted;
+            return ret.painted;
+        });
     }
 
     // For in-process browser, we have to make a synchronous request
     // here to make the above optimization valid, so that MozWaitPaint
     // events dispatched (synchronously) during painting are received
     // before we check the paint-wait counter.  For out-of-process
     // browser though, it doesn't wrt correctness whether this request
     // is sync or async.
-    var ret = sendSyncMessage("reftest:InitCanvasWithSnapshot")[0];
+    let ret = sendSyncMessage("reftest:InitCanvasWithSnapshot")[0];
 
     gHaveCanvasSnapshot = ret.painted;
-    return ret.painted;
+    return Promise.resolve(ret.painted);
 }
 
 function SendScriptResults(runtimeMs, error, results)
 {
     sendAsyncMessage("reftest:ScriptResults",
                      { runtimeMs: runtimeMs, error: error, results: results });
 }
 
@@ -1357,51 +1532,52 @@ function roundTo(x, fraction)
 function elementDescription(element)
 {
     return '<' + element.localName +
         [].slice.call(element.attributes).map((attr) =>
             ` ${attr.nodeName}="${attr.value}"`).join('') +
         '>';
 }
 
-function SendUpdateCanvasForEvent(forURL, event, contentRootElement)
+function SendUpdateCanvasForEvent(forURL, rectList, contentRootElement)
 {
     if (forURL != gCurrentURL) {
         LogInfo("SendUpdateCanvasForEvent called for previous document");
         // This is a test we are already done with that is clearing out.
         // Don't do anything.
-        return;
+        return Promise.resolve(undefined);
     }
 
     var win = content;
     var scale = markupDocumentViewer().fullZoom;
 
     var rects = [ ];
     if (shouldSnapshotWholePage(contentRootElement)) {
       // See comments in SendInitCanvasWithSnapshot() re: the split
       // logic here.
       if (!gBrowserIsRemote) {
           sendSyncMessage("reftest:UpdateWholeCanvasForInvalidation");
       } else {
-          SynchronizeForSnapshot(SYNC_ALLOW_DISABLE);
-          sendAsyncMessage("reftest:UpdateWholeCanvasForInvalidation");
+          let promise = SynchronizeForSnapshot(SYNC_ALLOW_DISABLE);
+          return promise.then(function () {
+            sendAsyncMessage("reftest:UpdateWholeCanvasForInvalidation");
+          });
       }
-      return;
+      return Promise.resolve(undefined);
     }
 
     var message;
     if (gIsWebRenderEnabled && !windowUtils().isMozAfterPaintPending) {
         // Webrender doesn't have invalidation, so we just invalidate the whole
         // screen once we don't have anymore paints pending. This will force
         // the snapshot.
 
         LogInfo("Webrender enabled, sending update whole canvas for invalidation");
         message = "reftest:UpdateWholeCanvasForInvalidation";
     } else {
-        var rectList = event.clientRects;
         LogInfo("SendUpdateCanvasForEvent with " + rectList.length + " rects");
         for (var i = 0; i < rectList.length; ++i) {
             var r = rectList[i];
             // Set left/top/right/bottom to "device pixel" boundaries
             var left = Math.floor(roundTo(r.left * scale, 0.001));
             var top = Math.floor(roundTo(r.top * scale, 0.001));
             var right = Math.ceil(roundTo(r.right * scale, 0.001));
             var bottom = Math.ceil(roundTo(r.bottom * scale, 0.001));
@@ -1413,18 +1589,23 @@ function SendUpdateCanvasForEvent(forURL
         message = "reftest:UpdateCanvasForInvalidation";
     }
 
     // See comments in SendInitCanvasWithSnapshot() re: the split
     // logic here.
     if (!gBrowserIsRemote) {
         sendSyncMessage(message, { rects: rects });
     } else {
-        SynchronizeForSnapshot(SYNC_ALLOW_DISABLE);
-        sendAsyncMessage(message, { rects: rects });
+        let promise = SynchronizeForSnapshot(SYNC_ALLOW_DISABLE);
+        return promise.then(function () {
+            sendAsyncMessage(message, { rects: rects });
+        });
     }
+
+    return Promise.resolve(undefined);
 }
+
 if (content.document.readyState == "complete") {
   // load event has already fired for content, get started
   OnInitialLoad();
 } else {
   addEventListener("load", OnInitialLoad, true);
 }