Merge mozilla-central to inbound. a=merge CLOSED TREE
authorMargareta Eliza Balazs <ebalazs@mozilla.com>
Fri, 11 May 2018 12:44:53 +0300
changeset 417953 59a49b12b26846302393edfbd20b5e72ef6b1d85
parent 417952 3f75694d9f8202393dc43283a378bd0f8eaf2fba (current diff)
parent 417874 21f09d7e7214eaebf1e0980494159bd846e1bdd9 (diff)
child 417954 093b3a6f608998cc9d1fb66d68445bf2e30e84df
push id33984
push usercbrindusan@mozilla.com
push dateSat, 12 May 2018 09:47:51 +0000
treeherdermozilla-central@809b0329507e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone62.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
Merge mozilla-central to inbound. a=merge CLOSED TREE
dom/animation/AnimationEffectReadOnly.cpp
dom/animation/AnimationEffectReadOnly.h
dom/animation/AnimationEffectTiming.cpp
dom/animation/AnimationEffectTiming.h
dom/animation/AnimationEffectTimingReadOnly.cpp
dom/animation/AnimationEffectTimingReadOnly.h
dom/animation/KeyframeEffect.cpp
dom/animation/KeyframeEffect.h
dom/animation/KeyframeEffectReadOnly.cpp
dom/animation/KeyframeEffectReadOnly.h
dom/webidl/AnimationEffectReadOnly.webidl
dom/webidl/AnimationEffectTiming.webidl
dom/webidl/AnimationEffectTimingReadOnly.webidl
testing/mozharness/configs/partner_repacks/release_mozilla-release_android.py
testing/mozharness/configs/partner_repacks/staging_release_mozilla-release_android.py
testing/mozharness/mozharness/mozilla/testing/mozpool.py
testing/mozharness/scripts/bouncer_submitter.py
testing/mozharness/scripts/release/publish_balrog.py
testing/web-platform/meta/css/css-timing/cubic-bezier-timing-functions-output.html.ini
testing/web-platform/meta/web-animations/animation-model/keyframe-effects/effect-value-iteration-composite-operation.html.ini
testing/web-platform/meta/web-animations/interfaces/Animatable/animate.html.ini
testing/web-platform/meta/web-animations/interfaces/Animatable/getAnimations.html.ini
testing/web-platform/meta/web-animations/interfaces/Animation/cancel.html.ini
testing/web-platform/meta/web-animations/interfaces/Animation/finished.html.ini
testing/web-platform/meta/web-animations/interfaces/AnimationEffect/updateTiming.html.ini
testing/web-platform/meta/web-animations/interfaces/KeyframeEffect/constructor.html.ini
testing/web-platform/meta/web-animations/interfaces/KeyframeEffect/copy-constructor.html.ini
testing/web-platform/meta/web-animations/interfaces/KeyframeEffect/idlharness.html.ini
testing/web-platform/meta/web-animations/timing-model/animation-effects/phases-and-states.html.ini
testing/web-platform/meta/web-animations/timing-model/animations/updating-the-finished-state.html.ini
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1535,16 +1535,24 @@ pref("browser.tabs.remote.desktopbehavio
 // until bug 1453080 is fixed.
 //
 #if !defined(XP_MACOSX) || defined(NIGHTLY_BUILD)
 pref("browser.tabs.remote.warmup.enabled", true);
 #else
 pref("browser.tabs.remote.warmup.enabled", false);
 #endif
 
+// Caches tab layers to improve perceived performance
+// of tab switches.
+#if defined(NIGHTLY_BUILD)
+pref("browser.tabs.remote.tabCacheSize", 5);
+#else
+pref("browser.tabs.remote.tabCacheSize", 0);
+#endif
+
 pref("browser.tabs.remote.warmup.maxTabs", 3);
 pref("browser.tabs.remote.warmup.unloadDelayMs", 2000);
 
 // For the about:tabcrashed page
 pref("browser.tabs.crashReporting.sendReport", true);
 pref("browser.tabs.crashReporting.includeURL", false);
 pref("browser.tabs.crashReporting.requestEmail", false);
 pref("browser.tabs.crashReporting.emailMe", false);
--- a/browser/base/content/docs/tabbrowser/async-tab-switcher.rst
+++ b/browser/base/content/docs/tabbrowser/async-tab-switcher.rst
@@ -190,16 +190,22 @@ We use a few tricks and optimizations to
 1. Sometimes users switch between the same tabs quickly. We want to optimize for this case by not releasing the layers for tabs until some time has gone by. That way, quick switching just resolves in a re-composite in the compositor, as opposed to a full re-paint and re-upload of the layers from a remote tab’s content process.
 
 2. When a tab hasn’t ever been seen before, and is still in the process of loading (right now, dubiously checked by looking for the “busy” attribute on the ``<xul:tab>``) we show a blank content area until its layers are finally ready. The idea here is to shift perceived lag from the async tab switcher to the network by showing the blank space instead of the tab switch spinner.
 
 3. “Warming” is a nascent optimization that will allow us to pre-emptively render and cache the layers for tabs that we think the user is likely to switch to soon. After a timeout (``browser.tabs.remote.warmup.unloadDelayMs``), “warmed” tabs that aren’t switched to have their layers unloaded and cleared from the cache.
 
 4. On platforms that support ``occlusionstatechange`` events (as of this writing, only macOS) and ``sizemodechange`` events (Windows, macOS and Linux), we stop rendering the layers for the currently selected tab when the window is minimized or fully occluded by another window.
 
+5. Based on the browser.tabs.remote.tabCacheSize pref, we keep recently used tabs'
+layers around to speed up tab switches by avoiding the round trip to the content
+process. This uses a simple array (``_tabLayerCache``) inside tabbrowser.js, which
+we examine when determining if we want to unload a tab's layers or not. This is still
+experimental as of Nightly 62.
+
 .. _async-tab-switcher.warming:
 
 Warming
 =======
 
 Tab warming allows the browser to proactively render and upload layers to the compositor for tabs that the user is likely to switch to. The simplest example is when a user's mouse cursor is hovering over a tab. When this occurs, the async tab switcher is told to put that tab into a warming list, and to set its state to ``STATE_LOADING``, even though the user hasn't yet clicked on it.
 
 Warming a tab queues up a timer to unload background tabs (if no such timer already exists), which will clear out the warmed tab if the user doesn't eventually click on it. The unload will occur even if the user continues to hover the tab.
--- a/browser/base/content/tabbrowser.js
+++ b/browser/base/content/tabbrowser.js
@@ -92,16 +92,18 @@ window._gBrowser = {
   _autoScrollPopup: null,
 
   _previewMode: false,
 
   _lastFindValue: "",
 
   _contentWaitingCount: 0,
 
+  _tabLayerCache: [],
+
   tabAnimationsInProgress: 0,
 
   _XUL_NS: "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul",
 
   /**
    * Binding from browser to tab
    */
   _tabForBrowser: new WeakMap(),
@@ -2324,25 +2326,31 @@ window._gBrowser = {
 
           let lastRelatedTab = openerTab && this._lastRelatedTabMap.get(openerTab);
           aIndex = (lastRelatedTab || openerTab || this.selectedTab)._tPos + 1;
 
           if (lastRelatedTab) {
             lastRelatedTab.owner = null;
           } else if (openerTab) {
             t.owner = openerTab;
+          }
+          // Always set related map if opener exists.
+          if (openerTab) {
             this._lastRelatedTabMap.set(openerTab, t);
           }
         } else {
           // This is intentionally past bounds, see the comment below on insertBefore.
           aIndex = this.tabs.length;
         }
       }
+      // Ensure position respectes tab pinned state.
       if (aPinned) {
         aIndex = Math.min(aIndex, this._numPinnedTabs);
+      } else {
+        aIndex = Math.max(aIndex, this._numPinnedTabs);
       }
 
       // use .item() instead of [] because dragging to the end of the strip goes out of
       // bounds: .item() returns null (so it acts like appendChild), but [] throws
       let tabAfter = this.tabs.item(aIndex);
       this.tabContainer.insertBefore(t, tabAfter);
       if (tabAfter) {
         this._updateTabsAfterInsert();
@@ -2745,16 +2753,23 @@ window._gBrowser = {
       // If we were closed during onbeforeunload, we return false now
       // so we don't (try to) close the same tab again. Of course, we
       // also stop if the unload was cancelled by the user:
       if (aTab.closing || (!timedOut && !permitUnload)) {
         return false;
       }
     }
 
+    // this._switcher would normally cover removing a tab from this
+    // cache, but we may not have one at this time.
+    let tabCacheIndex = this._tabLayerCache.indexOf(aTab);
+    if (tabCacheIndex != -1) {
+      this._tabLayerCache.splice(tabCacheIndex, 1);
+    }
+
     this._blurTab(aTab);
 
     var closeWindow = false;
     var newTab = false;
     if (this.tabs.length - this._removingTabs.length == 1) {
       closeWindow = aCloseWindowWithLastTab != null ? aCloseWindowWithLastTab :
         !window.toolbar.visible ||
         Services.prefs.getBoolPref("browser.tabs.closeWindowWithLastTab");
--- a/browser/base/content/test/tabs/browser.ini
+++ b/browser/base/content/test/tabs/browser.ini
@@ -23,16 +23,17 @@ skip-if = !e10s # Test only relevant for
 [browser_new_web_tab_in_file_process_pref.js]
 skip-if = !e10s # Pref and test only relevant for e10s.
 [browser_newwindow_tabstrip_overflow.js]
 [browser_opened_file_tab_navigated_to_web.js]
 [browser_new_tab_insert_position.js]
 support-files = file_new_tab_page.html
 [browser_overflowScroll.js]
 [browser_pinnedTabs.js]
+[browser_pinnedTabs_clickOpen.js]
 [browser_pinnedTabs_closeByKeyboard.js]
 [browser_positional_attributes.js]
 [browser_preloadedBrowser_zoom.js]
 [browser_reload_deleted_file.js]
 skip-if = (debug && os == 'mac') || (debug && os == 'linux' && bits == 64) #Bug 1421183, disabled on Linux/OSX for leaked windows
 [browser_tabReorder_overflow.js]
 [browser_tabswitch_updatecommands.js]
 [browser_viewsource_of_data_URI_in_file_process.js]
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_pinnedTabs_clickOpen.js
@@ -0,0 +1,47 @@
+/* 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/. */
+
+"use strict";
+
+function index(tab) {
+  return Array.indexOf(gBrowser.tabs, tab);
+}
+
+async function testNewTabPosition(expectedPosition, modifiers = {}) {
+  let opening = BrowserTestUtils.waitForNewTab(gBrowser, "http://mochi.test:8888/");
+  BrowserTestUtils.synthesizeMouseAtCenter("#link", modifiers, gBrowser.selectedBrowser);
+  let newtab = await opening;
+  is(index(newtab), expectedPosition, "clicked tab is in correct position");
+  return newtab;
+}
+
+// Test that a tab opened from a pinned tab is not in the pinned region.
+add_task(async function test_pinned_content_click() {
+  let testUri = "data:text/html;charset=utf-8,<a href=\"http://mochi.test:8888/\" target=\"_blank\" id=\"link\">link</a>";
+  let tabs = [gBrowser.selectedTab, BrowserTestUtils.addTab(gBrowser, testUri), BrowserTestUtils.addTab(gBrowser)];
+  gBrowser.pinTab(tabs[1]);
+  gBrowser.pinTab(tabs[2]);
+
+  // First test new active tabs open at the start of non-pinned tabstrip.
+  await BrowserTestUtils.switchTab(gBrowser, tabs[1]);
+  let newtab1 = await testNewTabPosition(2);
+
+  await BrowserTestUtils.switchTab(gBrowser, tabs[1]);
+  let newtab2 = await testNewTabPosition(2);
+
+  gBrowser.removeTab(newtab1);
+  gBrowser.removeTab(newtab2);
+
+  // Second test new background tabs open in order.
+  let modifiers = AppConstants.platform == "macosx" ? {metaKey: true} : {ctrlKey: true};
+  await BrowserTestUtils.switchTab(gBrowser, tabs[1]);
+
+  newtab1 = await testNewTabPosition(2, modifiers);
+  newtab2 = await testNewTabPosition(3, modifiers);
+
+  gBrowser.removeTab(tabs[1]);
+  gBrowser.removeTab(tabs[2]);
+  gBrowser.removeTab(newtab1);
+  gBrowser.removeTab(newtab2);
+});
--- a/browser/components/payments/res/containers/payment-dialog.js
+++ b/browser/components/payments/res/containers/payment-dialog.js
@@ -105,16 +105,25 @@ export default class PaymentDialog exten
   }
 
   changeShippingOption(optionID) {
     paymentRequest.changeShippingOption({
       optionID,
     });
   }
 
+  _getAdditionalDisplayItems(state) {
+    let methodId = state.selectedPaymentCard;
+    let modifier = paymentRequest.getModifierForPaymentMethod(state, methodId);
+    if (modifier && modifier.additionalDisplayItems) {
+      return modifier.additionalDisplayItems;
+    }
+    return [];
+  }
+
   /**
    * Set some state from the privileged parent process.
    * Other elements that need to set state should use their own `this.requestStore.setState`
    * method provided by the `PaymentStateSubscriberMixin`.
    *
    * @param {object} state - See `PaymentsStore.setState`
    */
   setStateFromParent(state) {
@@ -231,16 +240,20 @@ export default class PaymentDialog exten
     this._cachedState.selectedShippingOption = state.selectedShippingOption;
   }
 
   render(state) {
     let request = state.request;
     let paymentDetails = request.paymentDetails;
     this._hostNameEl.textContent = request.topLevelPrincipal.URI.displayHost;
 
+    let displayItems = request.paymentDetails.displayItems || [];
+    let additionalItems = this._getAdditionalDisplayItems(state);
+    this._viewAllButton.hidden = !displayItems.length && !additionalItems.length;
+
     let shippingType = state.request.paymentOptions.shippingType || "shipping";
     this._shippingAddressPicker.dataset.addAddressTitle =
       this.dataset[shippingType + "AddressTitleAdd"];
     this._shippingAddressPicker.dataset.editAddressTitle =
       this.dataset[shippingType + "AddressTitleEdit"];
 
     let totalItem = paymentRequest.getTotalItem(state);
     let totalAmountEl = this.querySelector("#total > currency-amount");
--- a/browser/components/payments/test/mochitest/test_payment_dialog.html
+++ b/browser/components/payments/test/mochitest/test_payment_dialog.html
@@ -84,16 +84,41 @@ add_task(async function test_initialStat
   let initialState = el1.requestStore.getState();
   let elDetails = el1._orderDetailsOverlay;
 
   is(initialState.orderDetailsShowing, false, "orderDetailsShowing is initially false");
   ok(elDetails.hasAttribute("hidden"), "Check details are hidden");
   is(initialState.page.id, "payment-summary", "Check initial page");
 });
 
+add_task(async function test_viewAllButtonVisibility() {
+  await setup();
+
+  let button = el1._viewAllButton;
+  ok(button.hidden, "Button is initially hidden when there are no items to show");
+
+  // Add a display item.
+  let request = deepClone(el1.requestStore.getState().request);
+  request.paymentDetails.displayItems = [
+    {
+      "label": "Triangle",
+      "amount": {
+        "currency": "CAD",
+        "value": "3",
+      },
+    },
+  ];
+  await el1.requestStore.setState({ request });
+  await asyncElementRendered();
+
+  // Check if the "View all items" button is visible.
+  ok(!button.hidden, "Button is visible");
+});
+
+
 add_task(async function test_viewAllButton() {
   await setup();
 
   let elDetails = el1._orderDetailsOverlay;
   let button = el1._viewAllButton;
 
   button.click();
   await asyncElementRendered();
--- a/browser/modules/AsyncTabSwitcher.jsm
+++ b/browser/modules/AsyncTabSwitcher.jsm
@@ -15,16 +15,18 @@ XPCOMUtils.defineLazyModuleGetters(this,
 });
 
 XPCOMUtils.defineLazyPreferenceGetter(this, "gTabWarmingEnabled",
   "browser.tabs.remote.warmup.enabled");
 XPCOMUtils.defineLazyPreferenceGetter(this, "gTabWarmingMax",
   "browser.tabs.remote.warmup.maxTabs");
 XPCOMUtils.defineLazyPreferenceGetter(this, "gTabWarmingUnloadDelayMs",
   "browser.tabs.remote.warmup.unloadDelayMs");
+XPCOMUtils.defineLazyPreferenceGetter(this, "gTabCacheSize",
+  "browser.tabs.remote.tabCacheSize");
 
 /**
  * The tab switcher is responsible for asynchronously switching
  * tabs in e10s. It waits until the new tab is ready (i.e., the
  * layer tree is available) before switching to it. Then it
  * unloads the layer tree for the old tab.
  *
  * The tab switcher is a state machine. For each tab, it
@@ -286,16 +288,20 @@ class AsyncTabSwitcher {
     }
   }
 
   get minimizedOrFullyOccluded() {
     return this.window.windowState == this.window.STATE_MINIMIZED ||
            this.window.isFullyOccluded;
   }
 
+  get tabLayerCache() {
+    return this.tabbrowser._tabLayerCache;
+  }
+
   finish() {
     this.log("FINISH");
 
     this.assert(this.tabbrowser._switcher);
     this.assert(this.tabbrowser._switcher === this);
     this.assert(!this.spinnerTab);
     this.assert(!this.blankTab);
     this.assert(!this.loadTimer);
@@ -503,16 +509,25 @@ class AsyncTabSwitcher {
   }
 
   // This function runs before every event. It fixes up the state
   // to account for closed tabs.
   preActions() {
     this.assert(this.tabbrowser._switcher);
     this.assert(this.tabbrowser._switcher === this);
 
+    for (let i = 0; i < this.tabLayerCache.length; i++) {
+      let tab = this.tabLayerCache[i];
+      if (!tab.linkedBrowser) {
+        this.tabState.delete(tab);
+        this.tabLayerCache.splice(i, 1);
+        i--;
+      }
+    }
+
     for (let [tab, ] of this.tabState) {
       if (!tab.linkedBrowser) {
         this.tabState.delete(tab);
         this.unwarmTab(tab);
       }
     }
 
     if (this.lastVisibleTab && !this.lastVisibleTab.linkedBrowser) {
@@ -561,26 +576,35 @@ class AsyncTabSwitcher {
     if (!this.loadTimer && !this.minimizedOrFullyOccluded &&
         (stateOfRequestedTab == this.STATE_UNLOADED ||
         stateOfRequestedTab == this.STATE_UNLOADING ||
         this.warmingTabs.has(this.requestedTab))) {
       this.assert(stateOfRequestedTab != this.STATE_LOADED);
       this.loadRequestedTab();
     }
 
+    let numBackgroundCached = 0;
+    for (let tab of this.tabLayerCache) {
+      if (tab !== this.requestedTab) {
+        numBackgroundCached++;
+      }
+    }
+
     // See how many tabs still have work to do.
     let numPending = 0;
     let numWarming = 0;
     for (let [tab, state] of this.tabState) {
       // Skip print preview browsers since they shouldn't affect tab switching.
       if (this.tabbrowser._printPreviewBrowsers.has(tab.linkedBrowser)) {
         continue;
       }
 
-      if (state == this.STATE_LOADED && tab !== this.requestedTab) {
+      if (state == this.STATE_LOADED &&
+          tab !== this.requestedTab &&
+          !this.tabLayerCache.includes(tab)) {
         numPending++;
 
         if (tab !== this.visibleTab) {
           numWarming++;
         }
       }
       if (state == this.STATE_LOADING || state == this.STATE_UNLOADING) {
         numPending++;
@@ -593,18 +617,20 @@ class AsyncTabSwitcher {
     // handlers, which might cause finish() to already have been called.
     // Check for that before calling finish() again.
     if (!this.tabbrowser._switcher) {
       return;
     }
 
     this.maybeFinishTabSwitch();
 
-    if (numWarming > gTabWarmingMax) {
-      this.logState("Hit tabWarmingMax");
+    if (numWarming > gTabWarmingMax || numBackgroundCached > 0) {
+      if (numWarming > gTabWarmingMax) {
+        this.logState("Hit tabWarmingMax");
+      }
       if (this.unloadTimer) {
         this.clearTimer(this.unloadTimer);
       }
       this.unloadNonRequiredTabs();
     }
 
     if (numPending == 0) {
       this.finish();
@@ -627,31 +653,44 @@ class AsyncTabSwitcher {
   // If there are any non-visible and non-requested tabs in
   // STATE_LOADED, sets them to STATE_UNLOADING. Also queues
   // up the unloadTimer to run onUnloadTimeout if there are still
   // tabs in the process of unloading.
   unloadNonRequiredTabs() {
     this.warmingTabs = new WeakSet();
     let numPending = 0;
 
+    for (let tab of this.tabLayerCache) {
+      if (tab !== this.requestedTab) {
+        let browser = tab.linkedBrowser;
+        browser.preserveLayers(true);
+        browser.docShellIsActive = false;
+      }
+    }
+
     // Unload any tabs that can be unloaded.
     for (let [tab, state] of this.tabState) {
       if (this.tabbrowser._printPreviewBrowsers.has(tab.linkedBrowser)) {
         continue;
       }
 
+      let isInLayerCache = this.tabLayerCache.includes(tab);
+
       if (state == this.STATE_LOADED &&
           !this.maybeVisibleTabs.has(tab) &&
           tab !== this.lastVisibleTab &&
           tab !== this.loadingTab &&
-          tab !== this.requestedTab) {
+          tab !== this.requestedTab &&
+          !isInLayerCache) {
         this.setTabState(tab, this.STATE_UNLOADING);
       }
 
-      if (state != this.STATE_UNLOADED && tab !== this.requestedTab) {
+      if (state != this.STATE_UNLOADED &&
+          tab !== this.requestedTab &&
+          !isInLayerCache) {
         numPending++;
       }
     }
 
     if (numPending) {
       // Keep the timer going since there may be more tabs to unload.
       this.unloadTimer = this.setTimer(() => this.onUnloadTimeout(), this.UNLOAD_DELAY);
     }
@@ -865,27 +904,59 @@ class AsyncTabSwitcher {
 
     this.logState("warmupTab " + this.tinfo(tab));
 
     this.warmingTabs.add(tab);
     this.setTabState(tab, this.STATE_LOADING);
     this.queueUnload(gTabWarmingUnloadDelayMs);
   }
 
+  cleanUpTabAfterEviction(tab) {
+    this.assert(tab !== this.requestedTab);
+    let browser = tab.linkedBrowser;
+    if (browser) {
+      browser.preserveLayers(false);
+    }
+    this.setTabState(tab, this.STATE_UNLOADING);
+  }
+
+  evictOldestTabFromCache() {
+    let tab = this.tabLayerCache.shift();
+    this.cleanUpTabAfterEviction(tab);
+  }
+
+  maybePromoteTabInLayerCache(tab) {
+    if (gTabCacheSize > 1 &&
+        tab.linkedBrowser.isRemoteBrowser &&
+        tab.linkedBrowser.currentURI.spec != "about:blank") {
+      let tabIndex = this.tabLayerCache.indexOf(tab);
+
+      if (tabIndex != -1) {
+        this.tabLayerCache.splice(tabIndex, 1);
+      }
+
+      this.tabLayerCache.push(tab);
+
+      if (this.tabLayerCache.length > gTabCacheSize) {
+        this.evictOldestTabFromCache();
+      }
+    }
+  }
+
   // Called when the user asks to switch to a given tab.
   requestTab(tab) {
     if (tab === this.requestedTab) {
       return;
     }
 
+    let tabState = this.getTabState(tab);
     if (gTabWarmingEnabled) {
       let warmingState = "disqualified";
 
       if (this.canWarmTab(tab)) {
-        let tabState = this.getTabState(tab);
         if (tabState == this.STATE_LOADING) {
           warmingState = "stillLoading";
         } else if (tabState == this.STATE_LOADED) {
           warmingState = "loaded";
         } else if (tabState == this.STATE_UNLOADING ||
                    tabState == this.STATE_UNLOADED) {
           // At this point, if the tab's browser was being inserted
           // lazily, we never had a chance to warm it up, and unfortunately
@@ -901,16 +972,19 @@ class AsyncTabSwitcher {
         .add(warmingState);
     }
 
     this._requestingTab = true;
     this.logState("requestTab " + this.tinfo(tab));
     this.startTabSwitch();
 
     this.requestedTab = tab;
+    if (tabState == this.STATE_LOADED) {
+      this.maybeVisibleTabs.clear();
+    }
 
     tab.linkedBrowser.setAttribute("primary", "true");
     if (this.lastPrimaryTab && this.lastPrimaryTab != tab) {
       this.lastPrimaryTab.linkedBrowser.removeAttribute("primary");
     }
     this.lastPrimaryTab = tab;
 
     this.queueUnload(this.UNLOAD_DELAY);
@@ -987,16 +1061,20 @@ class AsyncTabSwitcher {
    * the tab switch (layers are ready, paints are done, spinners
    * are hidden). This checks to make sure all conditions are
    * satisfied, and then records the tab switch as finished.
    */
   maybeFinishTabSwitch() {
     if (this.switchInProgress && this.requestedTab &&
         (this.getTabState(this.requestedTab) == this.STATE_LOADED ||
           this.requestedTab === this.blankTab)) {
+      if (this.requestedTab !== this.blankTab) {
+        this.maybePromoteTabInLayerCache(this.requestedTab);
+      }
+
       // After this point the tab has switched from the content thread's point of view.
       // The changes will be visible after the next refresh driver tick + composite.
       let time = TelemetryStopwatch.timeElapsed("FX_TAB_SWITCH_TOTAL_E10S_MS", this.window);
       if (time != -1) {
         TelemetryStopwatch.finish("FX_TAB_SWITCH_TOTAL_E10S_MS", this.window);
         this.log("DEBUG: tab switch time = " + time);
         this.addMarker("AsyncTabSwitch:Finish");
       }
@@ -1073,29 +1151,47 @@ class AsyncTabSwitcher {
     if (!this.logging())
       return;
 
     let accum = prefix + " ";
     for (let i = 0; i < this.tabbrowser.tabs.length; i++) {
       let tab = this.tabbrowser.tabs[i];
       let state = this.getTabState(tab);
       let isWarming = this.warmingTabs.has(tab);
+      let isCached = this.tabLayerCache.includes(tab);
+      let isClosing = tab.closing;
+      let linkedBrowser = tab.linkedBrowser;
+      let isActive = linkedBrowser && linkedBrowser.docShellIsActive;
+      let isRendered = linkedBrowser && linkedBrowser.renderLayers;
 
       accum += i + ":";
       if (tab === this.lastVisibleTab) accum += "V";
       if (tab === this.loadingTab) accum += "L";
       if (tab === this.requestedTab) accum += "R";
       if (tab === this.blankTab) accum += "B";
-      if (isWarming) accum += "(W)";
+
+      let extraStates = "";
+      if (isWarming) extraStates += "W";
+      if (isCached) extraStates += "C";
+      if (isClosing) extraStates += "X";
+      if (isActive) extraStates += "A";
+      if (isRendered) extraStates += "R";
+      if (extraStates != "") {
+        accum += `(${extraStates})`;
+      }
+
       if (state == this.STATE_LOADED) accum += "(+)";
       if (state == this.STATE_LOADING) accum += "(+?)";
       if (state == this.STATE_UNLOADED) accum += "(-)";
       if (state == this.STATE_UNLOADING) accum += "(-?)";
       accum += " ";
     }
+
+    accum += "cached: " + this.tabLayerCache.length;
+
     if (this._useDumpForLogging) {
       dump(accum + "\n");
     } else {
       Services.console.logStringMessage(accum);
     }
   }
 }
 
--- a/devtools/client/framework/components/ToolboxToolbar.js
+++ b/devtools/client/framework/components/ToolboxToolbar.js
@@ -51,16 +51,18 @@ class ToolboxToolbar extends Component {
       // when clicking outside them?
       //
       // This is a tri-state value that may be true/false or undefined where
       // undefined means that the option is not relevant in this context
       // (i.e. we're not in a browser toolbox).
       disableAutohide: PropTypes.bool,
       // Function to select a tool based on its id.
       selectTool: PropTypes.func,
+      // Function to turn the options panel on / off.
+      toggleOptions: PropTypes.func.isRequired,
       // Function to turn the split console on / off.
       toggleSplitConsole: PropTypes.func,
       // Function to turn the disable pop-up autohide behavior on / off.
       toggleNoAutohide: PropTypes.func,
       // Function to completely close the toolbox.
       closeToolbox: PropTypes.func,
       // Keep a record of what button is focused.
       focusButton: PropTypes.func,
@@ -229,16 +231,18 @@ function renderSeparator() {
  *         Is the split console currently visible?
  *        toolbox is undocked, for example.
  * @param {boolean|undefined} disableAutohide
  *        Are we disabling the behavior where pop-ups are automatically
  *        closed when clicking outside them?
  *        (Only defined for the browser toolbox.)
  * @param {Function} selectTool
  *        Function to select a tool based on its id.
+ * @param {Function} toggleOptions
+ *        Function to turn the options panel on / off.
  * @param {Function} toggleSplitConsole
  *        Function to turn the split console on / off.
  * @param {Function} toggleNoAutohide
  *        Function to turn the disable pop-up autohide behavior on / off.
  * @param {Function} closeToolbox
  *        Completely close the toolbox.
  * @param {Function} focusButton
  *        Keep a record of the currently focused button.
@@ -314,18 +318,20 @@ function renderToolboxControls(props) {
  * @param {string} props.currentHostType
  *        The current docking configuration.
  * @param {boolean} isSplitConsoleActive
  *        Is the split console currently visible?
  * @param {boolean|undefined} disableAutohide
  *        Are we disabling the behavior where pop-ups are automatically
  *        closed when clicking outside them.
  *        (Only defined for the browser toolbox.)
- * @param {Function} props.selectTool
+ * @param {Function} selectTool
  *        Function to select a tool based on its id.
+ * @param {Function} toggleOptions
+ *        Function to turn the options panel on / off.
  * @param {Function} toggleSplitConsole
  *        Function to turn the split console on / off.
  * @param {Function} toggleNoAutohide
  *        Function to turn the disable pop-up autohide behavior on / off.
  * @param {Object} props.L10N
  *        Localization interface.
  * @param {Object} props.toolbox
  *        The devtools toolbox. Used by the Menu component to determine which
@@ -334,17 +340,17 @@ function renderToolboxControls(props) {
 function showMeatballMenu(
   menuButton,
   {
     currentToolId,
     hostTypes,
     currentHostType,
     isSplitConsoleActive,
     disableAutohide,
-    selectTool,
+    toggleOptions,
     toggleSplitConsole,
     toggleNoAutohide,
     L10N,
     toolbox,
   }
 ) {
   const menu = new Menu({ id: "toolbox-meatball-menu" });
 
@@ -397,17 +403,17 @@ function showMeatballMenu(
     }));
   }
 
   // Settings
   menu.append(new MenuItem({
     id: "toolbox-meatball-menu-settings",
     label: L10N.getStr("toolbox.meatballMenu.settings.label"),
     accelerator: L10N.getStr("toolbox.help.key"),
-    click: () => selectTool("options"),
+    click: () => toggleOptions(),
   }));
 
   if (menu.items.length) {
     menu.append(new MenuItem({ type: "separator" }));
   }
 
   // Getting started
   menu.append(new MenuItem({
--- a/devtools/client/framework/test/browser.ini
+++ b/devtools/client/framework/test/browser.ini
@@ -86,16 +86,17 @@ skip-if = os == "mac" # Full keyboard na
 [browser_toolbox_options.js]
 [browser_toolbox_options_multiple_tabs.js]
 [browser_toolbox_options_disable_buttons.js]
 [browser_toolbox_options_disable_cache-01.js]
 [browser_toolbox_options_disable_cache-02.js]
 [browser_toolbox_options_disable_js.js]
 [browser_toolbox_options_enable_serviceworkers_testing.js]
 [browser_toolbox_options_frames_button.js]
+[browser_toolbox_options_panel_toggle.js]
 [browser_toolbox_raise.js]
 disabled=Bug 962258
 [browser_toolbox_races.js]
 [browser_toolbox_ready.js]
 [browser_toolbox_remoteness_change.js]
 run-if = e10s
 [browser_toolbox_select_event.js]
 skip-if = e10s # Bug 1069044 - destroyInspector may hang during shutdown
new file mode 100644
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_options_panel_toggle.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test whether options panel toggled by key event and "Settings" on the meatball menu.
+
+const { Toolbox } = require("devtools/client/framework/toolbox");
+
+add_task(async function() {
+  const tab = await addTab("about:blank");
+  const toolbox = await openToolboxForTab(tab, "webconsole", Toolbox.HostType.BOTTOM);
+
+  info("Check the option panel was selected after sending F1 key event");
+  await sendOptionsKeyEvent(toolbox);
+  is(toolbox.currentToolId, "options", "The options panel should be selected");
+
+  info("Check the last selected panel was selected after sending F1 key event");
+  await sendOptionsKeyEvent(toolbox);
+  is(toolbox.currentToolId, "webconsole", "The webconsole panel should be selected");
+
+  info("Check the option panel was selected after clicking 'Settings' menu");
+  await clickSettingsMenu(toolbox);
+  is(toolbox.currentToolId, "options", "The options panel should be selected");
+
+  info("Check the last selected panel was selected after clicking 'Settings' menu");
+  await sendOptionsKeyEvent(toolbox);
+  is(toolbox.currentToolId, "webconsole", "The webconsole panel should be selected");
+
+  info("Check the combination of key event and 'Settings' menu");
+  await sendOptionsKeyEvent(toolbox);
+  await clickSettingsMenu(toolbox);
+  is(toolbox.currentToolId, "webconsole", "The webconsole panel should be selected");
+  await clickSettingsMenu(toolbox);
+  await sendOptionsKeyEvent(toolbox);
+  is(toolbox.currentToolId, "webconsole", "The webconsole panel should be selected");
+});
+
+async function sendOptionsKeyEvent(toolbox) {
+  const onReady = toolbox.once("select");
+  EventUtils.synthesizeKey("VK_F1", {}, toolbox.win);
+  await onReady;
+}
+
+async function clickSettingsMenu(toolbox) {
+  const onPopupShown = () => {
+    toolbox.doc.removeEventListener("popuphidden", onPopupShown);
+    const menuItem = toolbox.doc.getElementById("toolbox-meatball-menu-settings");
+    EventUtils.synthesizeMouseAtCenter(menuItem, {}, menuItem.ownerGlobal);
+  };
+  toolbox.doc.addEventListener("popupshown", onPopupShown);
+
+  const button = toolbox.doc.getElementById("toolbox-meatball-menu-button");
+  EventUtils.synthesizeMouseAtCenter(button, {}, button.ownerGlobal);
+
+  await toolbox.once("select");
+}
--- a/devtools/client/framework/toolbox.js
+++ b/devtools/client/framework/toolbox.js
@@ -159,16 +159,17 @@ function Toolbox(target, selectedTool, h
   this._onPickerStopped = this._onPickerStopped.bind(this);
   this._onInspectObject = this._onInspectObject.bind(this);
   this._onNewSelectedNodeFront = this._onNewSelectedNodeFront.bind(this);
   this._onToolSelected = this._onToolSelected.bind(this);
   this.updateToolboxButtonsVisibility = this.updateToolboxButtonsVisibility.bind(this);
   this.selectTool = this.selectTool.bind(this);
   this._pingTelemetrySelectTool = this._pingTelemetrySelectTool.bind(this);
   this.toggleSplitConsole = this.toggleSplitConsole.bind(this);
+  this.toggleOptions = this.toggleOptions.bind(this);
 
   this._target.on("close", this.destroy);
 
   if (!selectedTool) {
     selectedTool = Services.prefs.getCharPref(this._prefs.LAST_TOOL);
   }
   this._defaultToolId = selectedTool;
 
@@ -835,27 +836,17 @@ Toolbox.prototype = {
     button.isVisible = this._commandIsVisible(button);
 
     EventEmitter.decorate(button);
 
     return button;
   },
 
   _buildOptions: function() {
-    let selectOptions = event => {
-      // Flip back to the last used panel if we are already
-      // on the options panel.
-      if (this.currentToolId === "options" &&
-          gDevTools.getToolDefinition(this.lastUsedToolId)) {
-        this.selectTool(this.lastUsedToolId, "toggle_settings_off");
-      } else {
-        this.selectTool("options", "toggle_settings_on");
-      }
-    };
-    this.shortcuts.on(L10N.getStr("toolbox.help.key"), selectOptions);
+    this.shortcuts.on(L10N.getStr("toolbox.help.key"), this.toggleOptions);
   },
 
   _splitConsoleOnKeypress: function(e) {
     if (e.keyCode === KeyCodes.DOM_VK_ESCAPE) {
       this.toggleSplitConsole();
       // If the debugger is paused, don't let the ESC key stop any pending
       // navigation.
       if (this._threadClient.state == "paused") {
@@ -1139,16 +1130,17 @@ Toolbox.prototype = {
   },
 
   _mountReactComponent: function() {
     // Ensure the toolbar doesn't try to render until the tool is ready.
     const element = this.React.createElement(this.ToolboxController, {
       L10N,
       currentToolId: this.currentToolId,
       selectTool: this.selectTool,
+      toggleOptions: this.toggleOptions,
       toggleSplitConsole: this.toggleSplitConsole,
       toggleNoAutohide: this.toggleNoAutohide,
       closeToolbox: this.destroy,
       focusButton: this._onToolbarFocus,
       toolbox: this,
       onTabsOrderUpdated: this._onTabsOrderUpdated,
     });
 
@@ -2043,16 +2035,31 @@ Toolbox.prototype = {
              this.closeSplitConsole() :
              this.openSplitConsole();
     }
 
     return promise.resolve();
   },
 
   /**
+   * Toggles the options panel.
+   * If the option panel is already selected then select the last selected panel.
+   */
+  toggleOptions: function() {
+    // Flip back to the last used panel if we are already
+    // on the options panel.
+    if (this.currentToolId === "options" &&
+        gDevTools.getToolDefinition(this.lastUsedToolId)) {
+      this.selectTool(this.lastUsedToolId, "toggle_settings_off");
+    } else {
+      this.selectTool("options", "toggle_settings_on");
+    }
+  },
+
+  /**
    * Tells the target tab to reload.
    */
   reloadTarget: function(force) {
     this.target.activeTab.reload({ force: force });
   },
 
   /**
    * Loads the tool next to the currently selected tool.
--- a/devtools/client/inspector/animation-old/graph-helper.js
+++ b/devtools/client/inspector/animation-old/graph-helper.js
@@ -85,17 +85,17 @@ ProgressGraphHelper.prototype = {
     this.win = null;
   },
 
   /**
    * Return animation duration.
    * @return {Number} duration
    */
   getDuration: function() {
-    return this.animation.effect.timing.duration;
+    return this.animation.effect.getComputedTiming().duration;
   },
 
   /**
    * Return animation's keyframe.
    * @return {Object} keyframe
    */
   getKeyframes: function() {
     return this.keyframes;
@@ -430,17 +430,17 @@ SummaryGraphHelper.prototype = {
     this.isOriginalBehavior = isOriginalBehavior;
   },
 
   /**
    * Set animation fill mode.
    * @param {String} fill - "both", "forwards", "backwards" or "both"
    */
   setFillMode: function(fill) {
-    this.animation.effect.timing.fill = fill;
+    this.animation.effect.updateTiming({ fill });
   },
 
   /**
    * Set true if need to close path in appendShapePath.
    * @param {bool} isClosePathNeeded - true: close, false: open.
    */
   setClosePathNeeded: function(isClosePathNeeded) {
     this.isClosePathNeeded = isClosePathNeeded;
--- a/devtools/client/webconsole/webconsole.html
+++ b/devtools/client/webconsole/webconsole.html
@@ -1,15 +1,17 @@
 <!-- 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/. -->
 <!DOCTYPE html>
 <html dir=""
+      id="devtools-webconsole"
       windowtype="devtools:webconsole"
-      width="900" height="350">
+      width="900" height="350"
+      persist="screenX screenY width height sizemode">
 <head>
   <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
     <link rel="stylesheet" href="chrome://devtools/skin/widgets.css"/>
     <link rel="stylesheet" href="chrome://devtools/skin/webconsole.css"/>
     <link rel="stylesheet" href="chrome://devtools/skin/components-frame.css"/>
     <link rel="stylesheet" href="resource://devtools/client/shared/components/reps/reps.css"/>
     <link rel="stylesheet" href="resource://devtools/client/shared/components/tabs/Tabs.css"/>
     <link rel="stylesheet" href="resource://devtools/client/shared/components/tabs/TabBar.css"/>
--- a/devtools/server/actors/animation.js
+++ b/devtools/server/actors/animation.js
@@ -248,17 +248,17 @@ var AnimationPlayerActor = protocol.Acto
     return this.player.effect.getComputedTiming().iterationStart;
   },
 
   /**
    * Get the animation easing from this player.
    * @return {String}
    */
   getEasing: function() {
-    return this.player.effect.timing.easing;
+    return this.player.effect.getComputedTiming().easing;
   },
 
   /**
    * Get the animation fill mode from this player.
    * @return {String}
    */
   getFill: function() {
     return this.player.effect.getComputedTiming().fill;
--- a/docshell/base/nsDocShell.cpp
+++ b/docshell/base/nsDocShell.cpp
@@ -3495,95 +3495,23 @@ nsDocShell::GetTreeOwner(nsIDocShellTree
 {
   NS_ENSURE_ARG_POINTER(aTreeOwner);
 
   *aTreeOwner = mTreeOwner;
   NS_IF_ADDREF(*aTreeOwner);
   return NS_OK;
 }
 
-#ifdef DEBUG_DOCSHELL_FOCUS
-static void
-PrintDocTree(nsIDocShellTreeItem* aParentNode, int aLevel)
-{
-  for (int32_t i = 0; i < aLevel; i++) {
-    printf("  ");
-  }
-
-  int32_t childWebshellCount;
-  aParentNode->GetChildCount(&childWebshellCount);
-  nsCOMPtr<nsIDocShell> parentAsDocShell(do_QueryInterface(aParentNode));
-  int32_t type = aParentNode->ItemType();
-  nsCOMPtr<nsIPresShell> presShell = parentAsDocShell->GetPresShell();
-  RefPtr<nsPresContext> presContext;
-  parentAsDocShell->GetPresContext(getter_AddRefs(presContext));
-  nsIDocument* doc = presShell->GetDocument();
-
-  nsCOMPtr<nsPIDOMWindowOuter> domwin(doc->GetWindow());
-
-  nsCOMPtr<nsIWidget> widget;
-  nsViewManager* vm = presShell->GetViewManager();
-  if (vm) {
-    vm->GetWidget(getter_AddRefs(widget));
-  }
-  dom::Element* rootElement = doc->GetRootElement();
-
-  printf("DS %p  Ty %s  Doc %p DW %p EM %p CN %p\n",
-         (void*)parentAsDocShell.get(),
-         type == nsIDocShellTreeItem::typeChrome ? "Chr" : "Con",
-         (void*)doc, (void*)domwin.get(),
-         (void*)presContext->EventStateManager(), (void*)rootElement);
-
-  if (childWebshellCount > 0) {
-    for (int32_t i = 0; i < childWebshellCount; i++) {
-      nsCOMPtr<nsIDocShellTreeItem> child;
-      aParentNode->GetChildAt(i, getter_AddRefs(child));
-      PrintDocTree(child, aLevel + 1);
-    }
-  }
-}
-
-static void
-PrintDocTree(nsIDocShellTreeItem* aParentNode)
-{
-  NS_ASSERTION(aParentNode, "Pointer is null!");
-
-  nsCOMPtr<nsIDocShellTreeItem> parentItem;
-  aParentNode->GetParent(getter_AddRefs(parentItem));
-  while (parentItem) {
-    nsCOMPtr<nsIDocShellTreeItem> tmp;
-    parentItem->GetParent(getter_AddRefs(tmp));
-    if (!tmp) {
-      break;
-    }
-    parentItem = tmp;
-  }
-
-  if (!parentItem) {
-    parentItem = aParentNode;
-  }
-
-  PrintDocTree(parentItem, 0);
-}
-#endif
-
 NS_IMETHODIMP
 nsDocShell::SetTreeOwner(nsIDocShellTreeOwner* aTreeOwner)
 {
   if (mIsBeingDestroyed && aTreeOwner) {
     return NS_ERROR_FAILURE;
   }
 
-#ifdef DEBUG_DOCSHELL_FOCUS
-  nsCOMPtr<nsIDocShellTreeItem> item(do_QueryInterface(aTreeOwner));
-  if (item) {
-    PrintDocTree(item);
-  }
-#endif
-
   // Don't automatically set the progress based on the tree owner for frames
   if (!IsFrame()) {
     nsCOMPtr<nsIWebProgress> webProgress =
       do_QueryInterface(GetAsSupports(this));
 
     if (webProgress) {
       nsCOMPtr<nsIWebProgressListener> oldListener =
         do_QueryInterface(mTreeOwner);
--- a/dom/animation/Animation.cpp
+++ b/dom/animation/Animation.cpp
@@ -83,17 +83,17 @@ namespace {
 
 // ---------------------------------------------------------------------------
 //
 // Animation interface:
 //
 // ---------------------------------------------------------------------------
 /* static */ already_AddRefed<Animation>
 Animation::Constructor(const GlobalObject& aGlobal,
-                       AnimationEffectReadOnly* aEffect,
+                       AnimationEffect* aEffect,
                        const Optional<AnimationTimeline*>& aTimeline,
                        ErrorResult& aRv)
 {
   nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
   RefPtr<Animation> animation = new Animation(global);
 
   AnimationTimeline* timeline;
   if (aTimeline.WasPassed()) {
@@ -120,25 +120,25 @@ Animation::SetId(const nsAString& aId)
   if (mId == aId) {
     return;
   }
   mId = aId;
   nsNodeUtils::AnimationChanged(this);
 }
 
 void
-Animation::SetEffect(AnimationEffectReadOnly* aEffect)
+Animation::SetEffect(AnimationEffect* aEffect)
 {
   SetEffectNoUpdate(aEffect);
   PostUpdate();
 }
 
 // https://drafts.csswg.org/web-animations/#setting-the-target-effect
 void
-Animation::SetEffectNoUpdate(AnimationEffectReadOnly* aEffect)
+Animation::SetEffectNoUpdate(AnimationEffect* aEffect)
 {
   RefPtr<Animation> kungFuDeathGrip(this);
 
   if (mEffect == aEffect) {
     return;
   }
 
   AutoMutationBatchForAnimation mb(*this);
@@ -154,27 +154,27 @@ Animation::SetEffectNoUpdate(AnimationEf
 
     // We need to notify observers now because once we set mEffect to null
     // we won't be able to find the target element to notify.
     if (mIsRelevant) {
       nsNodeUtils::AnimationRemoved(this);
     }
 
     // Break links with the old effect and then drop it.
-    RefPtr<AnimationEffectReadOnly> oldEffect = mEffect;
+    RefPtr<AnimationEffect> oldEffect = mEffect;
     mEffect = nullptr;
     oldEffect->SetAnimation(nullptr);
 
     // The following will not do any notification because mEffect is null.
     UpdateRelevance();
   }
 
   if (aEffect) {
     // Break links from the new effect to its previous animation, if any.
-    RefPtr<AnimationEffectReadOnly> newEffect = aEffect;
+    RefPtr<AnimationEffect> newEffect = aEffect;
     Animation* prevAnim = aEffect->GetAnimation();
     if (prevAnim) {
       prevAnim->SetEffect(nullptr);
     }
 
     // Create links with the new effect. SetAnimation(this) will also update
     // mIsRelevant of this animation, and then notify mutation observer if
     // needed by calling Animation::UpdateRelevance(), so we don't need to
@@ -698,17 +698,17 @@ Animation::Tick()
 
   UpdateTiming(SeekFlag::NoSeek, SyncNotifyFlag::Async);
 
   if (!mEffect) {
     return;
   }
 
   // Update layers if we are newly finished.
-  KeyframeEffectReadOnly* keyframeEffect = mEffect->AsKeyframeEffect();
+  KeyframeEffect* keyframeEffect = mEffect->AsKeyframeEffect();
   if (keyframeEffect &&
       !keyframeEffect->Properties().IsEmpty() &&
       !mFinishedAtLastComposeStyle &&
       PlayState() == AnimationPlayState::Finished) {
     PostUpdate();
   }
 }
 
@@ -903,19 +903,19 @@ Animation::ShouldBeSynchronizedWithMainT
     return false;
   }
 
   // Currently only transform animations need to be synchronized
   if (aProperty != eCSSProperty_transform) {
     return false;
   }
 
-  KeyframeEffectReadOnly* keyframeEffect = mEffect
-                                           ? mEffect->AsKeyframeEffect()
-                                           : nullptr;
+  KeyframeEffect* keyframeEffect = mEffect
+                                   ? mEffect->AsKeyframeEffect()
+                                   : nullptr;
   if (!keyframeEffect) {
     return false;
   }
 
   // Are we starting at the same time as other geometric animations?
   // We check this before calling ShouldBlockAsyncTransformAnimations, partly
   // because it's cheaper, but also because it's often the most useful thing
   // to know when you're debugging performance.
@@ -1015,17 +1015,17 @@ Animation::HasLowerCompositeOrderThan(co
 
 void
 Animation::WillComposeStyle()
 {
   mFinishedAtLastComposeStyle = (PlayState() == AnimationPlayState::Finished);
 
   MOZ_ASSERT(mEffect);
 
-  KeyframeEffectReadOnly* keyframeEffect = mEffect->AsKeyframeEffect();
+  KeyframeEffect* keyframeEffect = mEffect->AsKeyframeEffect();
   if (keyframeEffect) {
     keyframeEffect->WillComposeStyle();
   }
 }
 
 void
 Animation::ComposeStyle(RawServoAnimationValueMap& aComposeResult,
                         const nsCSSPropertyIDSet& aPropertiesToSkip)
@@ -1080,17 +1080,17 @@ Animation::ComposeStyle(RawServoAnimatio
         timeToUse = mTimeline->ToTimelineTime(TimeStamp::Now());
       }
       if (!timeToUse.IsNull()) {
         mHoldTime = CurrentTimeFromTimelineTime(
           timeToUse.Value(), mStartTime.Value(), mPlaybackRate);
       }
     }
 
-    KeyframeEffectReadOnly* keyframeEffect = mEffect->AsKeyframeEffect();
+    KeyframeEffect* keyframeEffect = mEffect->AsKeyframeEffect();
     if (keyframeEffect) {
       keyframeEffect->ComposeStyle(aComposeResult, aPropertiesToSkip);
     }
   }
 
   MOZ_ASSERT(pending == Pending(),
              "Pending state should not change during the course of compositing");
 }
@@ -1405,17 +1405,17 @@ Animation::UpdateFinishedState(SeekFlag 
 }
 
 void
 Animation::UpdateEffect()
 {
   if (mEffect) {
     UpdateRelevance();
 
-    KeyframeEffectReadOnly* keyframeEffect = mEffect->AsKeyframeEffect();
+    KeyframeEffect* keyframeEffect = mEffect->AsKeyframeEffect();
     if (keyframeEffect) {
       keyframeEffect->NotifyAnimationTimingUpdated();
     }
   }
 }
 
 void
 Animation::FlushUnanimatedStyle() const
@@ -1429,17 +1429,17 @@ Animation::FlushUnanimatedStyle() const
 
 void
 Animation::PostUpdate()
 {
   if (!mEffect) {
     return;
   }
 
-  KeyframeEffectReadOnly* keyframeEffect = mEffect->AsKeyframeEffect();
+  KeyframeEffect* keyframeEffect = mEffect->AsKeyframeEffect();
   if (!keyframeEffect) {
     return;
   }
   keyframeEffect->RequestRestyle(EffectCompositor::RestyleType::Layer);
 }
 
 void
 Animation::CancelPendingTasks()
--- a/dom/animation/Animation.h
+++ b/dom/animation/Animation.h
@@ -12,17 +12,17 @@
 #include "mozilla/AnimationPerformanceWarning.h"
 #include "mozilla/Attributes.h"
 #include "mozilla/CycleCollectedJSContext.h"
 #include "mozilla/DOMEventTargetHelper.h"
 #include "mozilla/EffectCompositor.h" // For EffectCompositor::CascadeLevel
 #include "mozilla/LinkedList.h"
 #include "mozilla/TimeStamp.h" // for TimeStamp, TimeDuration
 #include "mozilla/dom/AnimationBinding.h" // for AnimationPlayState
-#include "mozilla/dom/AnimationEffectReadOnly.h"
+#include "mozilla/dom/AnimationEffect.h"
 #include "mozilla/dom/AnimationTimeline.h"
 #include "mozilla/dom/Promise.h"
 #include "nsCSSPropertyID.h"
 #include "nsIGlobalObject.h"
 
 // X11 has a #define for CurrentTime.
 #ifdef CurrentTime
 #undef CurrentTime
@@ -91,23 +91,23 @@ public:
   enum class LimitBehavior {
     AutoRewind,
     Continue
   };
 
   // Animation interface methods
   static already_AddRefed<Animation>
   Constructor(const GlobalObject& aGlobal,
-              AnimationEffectReadOnly* aEffect,
+              AnimationEffect* aEffect,
               const Optional<AnimationTimeline*>& aTimeline,
               ErrorResult& aRv);
   void GetId(nsAString& aResult) const { aResult = mId; }
   void SetId(const nsAString& aId);
-  AnimationEffectReadOnly* GetEffect() const { return mEffect; }
-  void SetEffect(AnimationEffectReadOnly* aEffect);
+  AnimationEffect* GetEffect() const { return mEffect; }
+  void SetEffect(AnimationEffect* aEffect);
   AnimationTimeline* GetTimeline() const { return mTimeline; }
   void SetTimeline(AnimationTimeline* aTimeline);
   Nullable<TimeDuration> GetStartTime() const { return mStartTime; }
   void SetStartTime(const Nullable<TimeDuration>& aNewStartTime);
   Nullable<TimeDuration> GetCurrentTime() const {
     return GetCurrentTimeForHoldTime(mHoldTime);
   }
   void SetCurrentTime(const TimeDuration& aNewCurrentTime);
@@ -150,17 +150,17 @@ public:
    * CSSAnimation::PauseFromJS so we leave it for now.
    */
   void PauseFromJS(ErrorResult& aRv) { Pause(aRv); }
 
   // Wrapper functions for Animation DOM methods when called from style.
 
   virtual void CancelFromStyle() { CancelNoUpdate(); }
   void SetTimelineNoUpdate(AnimationTimeline* aTimeline);
-  void SetEffectNoUpdate(AnimationEffectReadOnly* aEffect);
+  void SetEffectNoUpdate(AnimationEffect* aEffect);
 
   virtual void Tick();
   bool NeedsTicks() const
   {
     return Pending() || PlayState() == AnimationPlayState::Running;
   }
 
   /**
@@ -358,17 +358,17 @@ public:
    * style on the main thread (e.g. because it is empty, or is
    * running on the compositor).
    */
   bool CanThrottle() const;
 
   /**
    * Updates various bits of state that we need to update as the result of
    * running ComposeStyle().
-   * See the comment of KeyframeEffectReadOnly::WillComposeStyle for more detail.
+   * See the comment of KeyframeEffect::WillComposeStyle for more detail.
    */
   void WillComposeStyle();
 
   /**
    * Updates |aComposeResult| with the animation values of this animation's
    * effect, if any.
    * Any properties contained in |aPropertiesToSkip| will not be added or
    * updated in |aComposeResult|.
@@ -483,17 +483,17 @@ protected:
   Nullable<TimeDuration> GetUnconstrainedCurrentTime() const
   {
     return GetCurrentTimeForHoldTime(Nullable<TimeDuration>());
   }
 
   nsIDocument* GetRenderedDocument() const;
 
   RefPtr<AnimationTimeline> mTimeline;
-  RefPtr<AnimationEffectReadOnly> mEffect;
+  RefPtr<AnimationEffect> mEffect;
   // The beginning of the delay period.
   Nullable<TimeDuration> mStartTime; // Timeline timescale
   Nullable<TimeDuration> mHoldTime;  // Animation timescale
   Nullable<TimeDuration> mPendingReadyTime; // Timeline timescale
   Nullable<TimeDuration> mPreviousCurrentTime; // Animation timescale
   double mPlaybackRate;
   Maybe<double> mPendingPlaybackRate;
 
rename from dom/animation/AnimationEffectReadOnly.cpp
rename to dom/animation/AnimationEffect.cpp
--- a/dom/animation/AnimationEffectReadOnly.cpp
+++ b/dom/animation/AnimationEffect.cpp
@@ -1,104 +1,98 @@
 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* 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/. */
 
-#include "mozilla/dom/AnimationEffectReadOnly.h"
-#include "mozilla/dom/AnimationEffectReadOnlyBinding.h"
+#include "mozilla/dom/AnimationEffect.h"
+#include "mozilla/dom/AnimationEffectBinding.h"
 
 #include "mozilla/dom/Animation.h"
-#include "mozilla/dom/KeyframeEffectReadOnly.h"
+#include "mozilla/dom/KeyframeEffect.h"
 #include "mozilla/AnimationUtils.h"
 #include "mozilla/FloatingPoint.h"
 
 namespace mozilla {
 namespace dom {
 
-NS_IMPL_CYCLE_COLLECTION_CLASS(AnimationEffectReadOnly)
-NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(AnimationEffectReadOnly)
-  if (tmp->mTiming) {
-    tmp->mTiming->Unlink();
-  }
-  NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocument, mTiming, mAnimation)
+NS_IMPL_CYCLE_COLLECTION_CLASS(AnimationEffect)
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(AnimationEffect)
+  NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocument, mAnimation)
   NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
 
-NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(AnimationEffectReadOnly)
-  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocument, mTiming, mAnimation)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(AnimationEffect)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocument, mAnimation)
 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
 
-NS_IMPL_CYCLE_COLLECTION_TRACE_WRAPPERCACHE(AnimationEffectReadOnly)
+NS_IMPL_CYCLE_COLLECTION_TRACE_WRAPPERCACHE(AnimationEffect)
 
-NS_IMPL_CYCLE_COLLECTING_ADDREF(AnimationEffectReadOnly)
-NS_IMPL_CYCLE_COLLECTING_RELEASE(AnimationEffectReadOnly)
+NS_IMPL_CYCLE_COLLECTING_ADDREF(AnimationEffect)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(AnimationEffect)
 
-NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(AnimationEffectReadOnly)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(AnimationEffect)
   NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
   NS_INTERFACE_MAP_ENTRY(nsISupports)
 NS_INTERFACE_MAP_END
 
-AnimationEffectReadOnly::AnimationEffectReadOnly(
-  nsIDocument* aDocument, AnimationEffectTimingReadOnly* aTiming)
-  : mDocument(aDocument)
-  , mTiming(aTiming)
-{
-  MOZ_ASSERT(aTiming);
-}
-
 // https://drafts.csswg.org/web-animations/#current
 bool
-AnimationEffectReadOnly::IsCurrent() const
+AnimationEffect::IsCurrent() const
 {
   if (!mAnimation || mAnimation->PlayState() == AnimationPlayState::Finished) {
     return false;
   }
 
   ComputedTiming computedTiming = GetComputedTiming();
   return computedTiming.mPhase == ComputedTiming::AnimationPhase::Before ||
          computedTiming.mPhase == ComputedTiming::AnimationPhase::Active;
 }
 
 // https://drafts.csswg.org/web-animations/#in-effect
 bool
-AnimationEffectReadOnly::IsInEffect() const
+AnimationEffect::IsInEffect() const
 {
   ComputedTiming computedTiming = GetComputedTiming();
   return !computedTiming.mProgress.IsNull();
 }
 
-already_AddRefed<AnimationEffectTimingReadOnly>
-AnimationEffectReadOnly::Timing()
+void
+AnimationEffect::SetSpecifiedTiming(const TimingParams& aTiming)
 {
-  RefPtr<AnimationEffectTimingReadOnly> temp(mTiming);
-  return temp.forget();
-}
-
-void
-AnimationEffectReadOnly::SetSpecifiedTiming(const TimingParams& aTiming)
-{
-  if (mTiming->AsTimingParams() == aTiming) {
+  if (mTiming == aTiming) {
     return;
   }
-  mTiming->SetTimingParams(aTiming);
+
+  mTiming = aTiming;
+
   if (mAnimation) {
+    Maybe<nsAutoAnimationMutationBatch> mb;
+    if (AsKeyframeEffect() && AsKeyframeEffect()->GetTarget()) {
+      mb.emplace(AsKeyframeEffect()->GetTarget()->mElement->OwnerDoc());
+    }
+
     mAnimation->NotifyEffectTimingUpdated();
+
+    if (mAnimation->IsRelevant()) {
+      nsNodeUtils::AnimationChanged(mAnimation);
+    }
+
     if (AsKeyframeEffect()) {
       AsKeyframeEffect()->RequestRestyle(EffectCompositor::RestyleType::Layer);
     }
   }
   // For keyframe effects, NotifyEffectTimingUpdated above will eventually cause
-  // KeyframeEffectReadOnly::NotifyAnimationTimingUpdated to be called so it can
+  // KeyframeEffect::NotifyAnimationTimingUpdated to be called so it can
   // update its registration with the target element as necessary.
 }
 
 ComputedTiming
-AnimationEffectReadOnly::GetComputedTimingAt(
+AnimationEffect::GetComputedTimingAt(
     const Nullable<TimeDuration>& aLocalTime,
     const TimingParams& aTiming,
     double aPlaybackRate)
 {
   static const StickyTimeDuration zeroDuration;
 
   // Always return the same object to benefit from return-value optimization.
   ComputedTiming result;
@@ -260,86 +254,95 @@ AnimationEffectReadOnly::GetComputedTimi
   }
 
   MOZ_ASSERT(IsFinite(progress), "Progress value should be finite");
   result.mProgress.SetValue(progress);
   return result;
 }
 
 ComputedTiming
-AnimationEffectReadOnly::GetComputedTiming(const TimingParams* aTiming) const
+AnimationEffect::GetComputedTiming(const TimingParams* aTiming) const
 {
   double playbackRate = mAnimation ? mAnimation->PlaybackRate() : 1;
   return GetComputedTimingAt(GetLocalTime(),
                              aTiming ? *aTiming : SpecifiedTiming(),
                              playbackRate);
 }
 
-// Helper functions for generating a ComputedTimingProperties dictionary
+// Helper function for generating an (Computed)EffectTiming dictionary
 static void
-GetComputedTimingDictionary(const ComputedTiming& aComputedTiming,
-                            const Nullable<TimeDuration>& aLocalTime,
-                            const TimingParams& aTiming,
-                            ComputedTimingProperties& aRetVal)
+GetEffectTimingDictionary(const TimingParams& aTiming, EffectTiming& aRetVal)
 {
-  // AnimationEffectTimingProperties
   aRetVal.mDelay = aTiming.Delay().ToMilliseconds();
   aRetVal.mEndDelay = aTiming.EndDelay().ToMilliseconds();
-  aRetVal.mFill = aComputedTiming.mFill;
-  aRetVal.mIterationStart = aComputedTiming.mIterationStart;
-  aRetVal.mIterations = aComputedTiming.mIterations;
-  aRetVal.mDuration.SetAsUnrestrictedDouble() =
-    aComputedTiming.mDuration.ToMilliseconds();
+  aRetVal.mFill = aTiming.Fill();
+  aRetVal.mIterationStart = aTiming.IterationStart();
+  aRetVal.mIterations = aTiming.Iterations();
+  if (aTiming.Duration()) {
+    aRetVal.mDuration.SetAsUnrestrictedDouble() =
+      aTiming.Duration()->ToMilliseconds();
+  }
   aRetVal.mDirection = aTiming.Direction();
   if (aTiming.TimingFunction()) {
     aRetVal.mEasing.Truncate();
     aTiming.TimingFunction()->AppendToString(aRetVal.mEasing);
   }
+}
 
-  // ComputedTimingProperties
-  aRetVal.mActiveDuration = aComputedTiming.mActiveDuration.ToMilliseconds();
-  aRetVal.mEndTime = aComputedTiming.mEndTime.ToMilliseconds();
-  aRetVal.mLocalTime = AnimationUtils::TimeDurationToDouble(aLocalTime);
-  aRetVal.mProgress = aComputedTiming.mProgress;
+void
+AnimationEffect::GetTiming(EffectTiming& aRetVal) const
+{
+  GetEffectTimingDictionary(SpecifiedTiming(), aRetVal);
+}
+
+void
+AnimationEffect::GetComputedTimingAsDict(ComputedEffectTiming& aRetVal) const
+{
+  // Specified timing
+  GetEffectTimingDictionary(SpecifiedTiming(), aRetVal);
+
+  // Computed timing
+  double playbackRate = mAnimation ? mAnimation->PlaybackRate() : 1;
+  const Nullable<TimeDuration> currentTime = GetLocalTime();
+  ComputedTiming computedTiming =
+    GetComputedTimingAt(currentTime, SpecifiedTiming(), playbackRate);
+
+  aRetVal.mDuration.SetAsUnrestrictedDouble() =
+    computedTiming.mDuration.ToMilliseconds();
+  aRetVal.mFill = computedTiming.mFill;
+  aRetVal.mActiveDuration = computedTiming.mActiveDuration.ToMilliseconds();
+  aRetVal.mEndTime = computedTiming.mEndTime.ToMilliseconds();
+  aRetVal.mLocalTime = AnimationUtils::TimeDurationToDouble(currentTime);
+  aRetVal.mProgress = computedTiming.mProgress;
 
   if (!aRetVal.mProgress.IsNull()) {
     // Convert the returned currentIteration into Infinity if we set
-    // (uint64_t) aComputedTiming.mCurrentIteration to UINT64_MAX
-    double iteration = aComputedTiming.mCurrentIteration == UINT64_MAX
+    // (uint64_t) computedTiming.mCurrentIteration to UINT64_MAX
+    double iteration = computedTiming.mCurrentIteration == UINT64_MAX
                        ? PositiveInfinity<double>()
-                       : static_cast<double>(aComputedTiming.mCurrentIteration);
+                       : static_cast<double>(computedTiming.mCurrentIteration);
     aRetVal.mCurrentIteration.SetValue(iteration);
   }
 }
 
 void
-AnimationEffectReadOnly::GetComputedTimingAsDict(
-  ComputedTimingProperties& aRetVal) const
+AnimationEffect::UpdateTiming(const OptionalEffectTiming& aTiming,
+                              ErrorResult& aRv)
 {
-  double playbackRate = mAnimation ? mAnimation->PlaybackRate() : 1;
-  const Nullable<TimeDuration> currentTime = GetLocalTime();
-  GetComputedTimingDictionary(GetComputedTimingAt(currentTime,
-                                                  SpecifiedTiming(),
-                                                  playbackRate),
-                              currentTime,
-                              SpecifiedTiming(),
-                              aRetVal);
-}
+  TimingParams timing =
+    TimingParams::MergeOptionalEffectTiming(mTiming, aTiming, mDocument, aRv);
+  if (aRv.Failed()) {
+    return;
+  }
 
-AnimationEffectReadOnly::~AnimationEffectReadOnly()
-{
-  // mTiming is cycle collected, so we have to do null check first even though
-  // mTiming shouldn't be null during the lifetime of KeyframeEffect.
-  if (mTiming) {
-    mTiming->Unlink();
-  }
+  SetSpecifiedTiming(timing);
 }
 
 Nullable<TimeDuration>
-AnimationEffectReadOnly::GetLocalTime() const
+AnimationEffect::GetLocalTime() const
 {
   // Since the *animation* start time is currently always zero, the local
   // time is equal to the parent time.
   Nullable<TimeDuration> result;
   if (mAnimation) {
     result = mAnimation->GetCurrentTime();
   }
   return result;
rename from dom/animation/AnimationEffectReadOnly.h
rename to dom/animation/AnimationEffect.h
--- a/dom/animation/AnimationEffectReadOnly.h
+++ b/dom/animation/AnimationEffect.h
@@ -1,71 +1,73 @@
 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* 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/. */
 
-#ifndef mozilla_dom_AnimationEffectReadOnly_h
-#define mozilla_dom_AnimationEffectReadOnly_h
+#ifndef mozilla_dom_AnimationEffect_h
+#define mozilla_dom_AnimationEffect_h
 
 #include "mozilla/ComputedTiming.h"
-#include "mozilla/dom/AnimationEffectTimingReadOnly.h"
 #include "mozilla/dom/BindingDeclarations.h"
 #include "mozilla/dom/Nullable.h"
 #include "mozilla/Maybe.h"
 #include "mozilla/StickyTimeDuration.h"
 #include "mozilla/TimeStamp.h"
 #include "mozilla/TimingParams.h"
 #include "nsCycleCollectionParticipant.h"
 #include "nsWrapperCache.h"
 
 namespace mozilla {
 
 struct ElementPropertyTransition;
 
 namespace dom {
 
 class Animation;
-class AnimationEffectTimingReadOnly;
-class KeyframeEffectReadOnly;
-struct ComputedTimingProperties;
+class KeyframeEffect;
+struct ComputedEffectTiming;
 
-class AnimationEffectReadOnly : public nsISupports,
-                                public nsWrapperCache
+class AnimationEffect : public nsISupports,
+                        public nsWrapperCache
 {
 public:
   NS_DECL_CYCLE_COLLECTING_ISUPPORTS
-  NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(AnimationEffectReadOnly)
+  NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(AnimationEffect)
 
-  AnimationEffectReadOnly(nsIDocument* aDocument,
-                          AnimationEffectTimingReadOnly* aTiming);
+  AnimationEffect(nsIDocument* aDocument, const TimingParams& aTiming)
+    : mDocument(aDocument)
+    , mTiming(aTiming)
+  {
+  }
 
-  virtual KeyframeEffectReadOnly* AsKeyframeEffect() { return nullptr; }
+  virtual KeyframeEffect* AsKeyframeEffect() { return nullptr; }
 
   virtual ElementPropertyTransition* AsTransition() { return nullptr; }
   virtual const ElementPropertyTransition* AsTransition() const
   {
     return nullptr;
   }
 
   nsISupports* GetParentObject() const { return mDocument; }
 
   bool IsCurrent() const;
   bool IsInEffect() const;
   bool HasFiniteActiveDuration() const
   {
     return SpecifiedTiming().ActiveDuration() != TimeDuration::Forever();
   }
 
-  already_AddRefed<AnimationEffectTimingReadOnly> Timing();
-  const TimingParams& SpecifiedTiming() const
-  {
-    return mTiming->AsTimingParams();
-  }
+  // AnimationEffect interface
+  void GetTiming(EffectTiming& aRetVal) const;
+  void GetComputedTimingAsDict(ComputedEffectTiming& aRetVal) const;
+  void UpdateTiming(const OptionalEffectTiming& aTiming, ErrorResult& aRv);
+
+  const TimingParams& SpecifiedTiming() const { return mTiming; }
   void SetSpecifiedTiming(const TimingParams& aTiming);
 
   // This function takes as input the timing parameters of an animation and
   // returns the computed timing at the specified local time.
   //
   // The local time may be null in which case only static parameters such as the
   // active duration are calculated. All other members of the returned object
   // are given a null/initial value.
@@ -75,36 +77,35 @@ public:
   // (because it is not currently active and is not filling at this time).
   static ComputedTiming
   GetComputedTimingAt(const Nullable<TimeDuration>& aLocalTime,
                       const TimingParams& aTiming,
                       double aPlaybackRate);
   // Shortcut that gets the computed timing using the current local time as
   // calculated from the timeline time.
   ComputedTiming GetComputedTiming(const TimingParams* aTiming = nullptr) const;
-  void GetComputedTimingAsDict(ComputedTimingProperties& aRetVal) const;
 
   virtual void SetAnimation(Animation* aAnimation) = 0;
   Animation* GetAnimation() const { return mAnimation; };
 
   /**
    * Returns true if this effect animates one of the properties we consider
    * geometric properties, e.g. properties such as 'width' or 'margin-left'
    * that we try to synchronize with transform animations, on a valid target
    * element.
    */
   virtual bool AffectsGeometry() const = 0;
 
 protected:
-  virtual ~AnimationEffectReadOnly();
+  virtual ~AnimationEffect() = default;
 
   Nullable<TimeDuration> GetLocalTime() const;
 
 protected:
   RefPtr<nsIDocument> mDocument;
-  RefPtr<AnimationEffectTimingReadOnly> mTiming;
   RefPtr<Animation> mAnimation;
+  TimingParams mTiming;
 };
 
 } // namespace dom
 } // namespace mozilla
 
-#endif // mozilla_dom_AnimationEffectReadOnly_h
+#endif // mozilla_dom_AnimationEffect_h
deleted file mode 100644
--- a/dom/animation/AnimationEffectTiming.cpp
+++ /dev/null
@@ -1,152 +0,0 @@
-/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
-/* vim: set ts=8 sts=2 et sw=2 tw=80: */
-/* 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/. */
-
-#include "mozilla/dom/AnimationEffectTiming.h"
-
-#include "mozilla/dom/AnimatableBinding.h"
-#include "mozilla/dom/AnimationEffectTimingBinding.h"
-#include "mozilla/dom/KeyframeEffect.h"
-#include "mozilla/TimingParams.h"
-#include "nsAString.h"
-
-namespace mozilla {
-namespace dom {
-
-JSObject*
-AnimationEffectTiming::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
-{
-  return AnimationEffectTimingBinding::Wrap(aCx, this, aGivenProto);
-}
-
-static inline void
-PostSpecifiedTimingUpdated(KeyframeEffect* aEffect)
-{
-  if (aEffect) {
-    aEffect->NotifySpecifiedTimingUpdated();
-  }
-}
-
-void
-AnimationEffectTiming::SetDelay(double aDelay)
-{
-  TimeDuration delay = TimeDuration::FromMilliseconds(aDelay);
-  if (mTiming.Delay() == delay) {
-    return;
-  }
-  mTiming.SetDelay(delay);
-
-  PostSpecifiedTimingUpdated(mEffect);
-}
-
-void
-AnimationEffectTiming::SetEndDelay(double aEndDelay)
-{
-  TimeDuration endDelay = TimeDuration::FromMilliseconds(aEndDelay);
-  if (mTiming.EndDelay() == endDelay) {
-    return;
-  }
-  mTiming.SetEndDelay(endDelay);
-
-  PostSpecifiedTimingUpdated(mEffect);
-}
-
-void
-AnimationEffectTiming::SetFill(const FillMode& aFill)
-{
-  if (mTiming.Fill() == aFill) {
-    return;
-  }
-  mTiming.SetFill(aFill);
-
-  PostSpecifiedTimingUpdated(mEffect);
-}
-
-void
-AnimationEffectTiming::SetIterationStart(double aIterationStart,
-                                         ErrorResult& aRv)
-{
-  if (mTiming.IterationStart() == aIterationStart) {
-    return;
-  }
-
-  TimingParams::ValidateIterationStart(aIterationStart, aRv);
-  if (aRv.Failed()) {
-    return;
-  }
-
-  mTiming.SetIterationStart(aIterationStart);
-
-  PostSpecifiedTimingUpdated(mEffect);
-}
-
-void
-AnimationEffectTiming::SetIterations(double aIterations, ErrorResult& aRv)
-{
-  if (mTiming.Iterations() == aIterations) {
-    return;
-  }
-
-  TimingParams::ValidateIterations(aIterations, aRv);
-  if (aRv.Failed()) {
-    return;
-  }
-
-  mTiming.SetIterations(aIterations);
-
-  PostSpecifiedTimingUpdated(mEffect);
-}
-
-void
-AnimationEffectTiming::SetDuration(const UnrestrictedDoubleOrString& aDuration,
-                                   ErrorResult& aRv)
-{
-  Maybe<StickyTimeDuration> newDuration =
-    TimingParams::ParseDuration(aDuration, aRv);
-  if (aRv.Failed()) {
-    return;
-  }
-
-  if (mTiming.Duration() == newDuration) {
-    return;
-  }
-
-  mTiming.SetDuration(Move(newDuration));
-
-  PostSpecifiedTimingUpdated(mEffect);
-}
-
-void
-AnimationEffectTiming::SetDirection(const PlaybackDirection& aDirection)
-{
-  if (mTiming.Direction() == aDirection) {
-    return;
-  }
-
-  mTiming.SetDirection(aDirection);
-
-  PostSpecifiedTimingUpdated(mEffect);
-}
-
-void
-AnimationEffectTiming::SetEasing(const nsAString& aEasing, ErrorResult& aRv)
-{
-  Maybe<ComputedTimingFunction> newFunction =
-    TimingParams::ParseEasing(aEasing, mDocument, aRv);
-  if (aRv.Failed()) {
-    return;
-  }
-
-  if (mTiming.TimingFunction() == newFunction) {
-    return;
-  }
-
-  mTiming.SetTimingFunction(Move(newFunction));
-
-  PostSpecifiedTimingUpdated(mEffect);
-}
-
-} // namespace dom
-} // namespace mozilla
deleted file mode 100644
--- a/dom/animation/AnimationEffectTiming.h
+++ /dev/null
@@ -1,49 +0,0 @@
-/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
-/* vim: set ts=8 sts=2 et sw=2 tw=80: */
-/* 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/. */
-
-#ifndef mozilla_dom_AnimationEffectTiming_h
-#define mozilla_dom_AnimationEffectTiming_h
-
-#include "mozilla/dom/AnimationEffectTimingReadOnly.h"
-#include "mozilla/Attributes.h" // For MOZ_NON_OWNING_REF
-#include "nsStringFwd.h"
-
-namespace mozilla {
-namespace dom {
-
-class KeyframeEffect;
-
-class AnimationEffectTiming : public AnimationEffectTimingReadOnly
-{
-public:
-  AnimationEffectTiming(nsIDocument* aDocument,
-                        const TimingParams& aTiming,
-                        KeyframeEffect* aEffect)
-    : AnimationEffectTimingReadOnly(aDocument, aTiming)
-    , mEffect(aEffect) { }
-
-  JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override;
-
-  void Unlink() override { mEffect = nullptr; }
-
-  void SetDelay(double aDelay);
-  void SetEndDelay(double aEndDelay);
-  void SetFill(const FillMode& aFill);
-  void SetIterationStart(double aIterationStart, ErrorResult& aRv);
-  void SetIterations(double aIterations, ErrorResult& aRv);
-  void SetDuration(const UnrestrictedDoubleOrString& aDuration,
-                   ErrorResult& aRv);
-  void SetDirection(const PlaybackDirection& aDirection);
-  void SetEasing(const nsAString& aEasing, ErrorResult& aRv);
-
-private:
-  KeyframeEffect* MOZ_NON_OWNING_REF mEffect;
-};
-
-} // namespace dom
-} // namespace mozilla
-
-#endif // mozilla_dom_AnimationEffectTiming_h
deleted file mode 100644
--- a/dom/animation/AnimationEffectTimingReadOnly.cpp
+++ /dev/null
@@ -1,51 +0,0 @@
-/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
-/* vim: set ts=8 sts=2 et sw=2 tw=80: */
-/* 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/. */
-
-#include "mozilla/dom/AnimationEffectTimingReadOnly.h"
-
-#include "mozilla/AnimationUtils.h"
-#include "mozilla/dom/AnimatableBinding.h"
-#include "mozilla/dom/AnimationEffectTimingReadOnlyBinding.h"
-#include "mozilla/dom/CSSPseudoElement.h"
-#include "mozilla/dom/KeyframeEffectBinding.h"
-
-namespace mozilla {
-namespace dom {
-
-NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(AnimationEffectTimingReadOnly, mDocument)
-
-NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(AnimationEffectTimingReadOnly, AddRef)
-NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(AnimationEffectTimingReadOnly, Release)
-
-JSObject*
-AnimationEffectTimingReadOnly::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
-{
-  return AnimationEffectTimingReadOnlyBinding::Wrap(aCx, this, aGivenProto);
-}
-
-void
-AnimationEffectTimingReadOnly::GetDuration(
-    OwningUnrestrictedDoubleOrString& aRetVal) const
-{
-  if (mTiming.Duration()) {
-    aRetVal.SetAsUnrestrictedDouble() = mTiming.Duration()->ToMilliseconds();
-  } else {
-    aRetVal.SetAsString().AssignLiteral("auto");
-  }
-}
-
-void
-AnimationEffectTimingReadOnly::GetEasing(nsString& aRetVal) const
-{
-  if (mTiming.TimingFunction()) {
-    mTiming.TimingFunction()->AppendToString(aRetVal);
-  } else {
-    aRetVal.AssignLiteral("linear");
-  }
-}
-
-} // namespace dom
-} // namespace mozilla
deleted file mode 100644
--- a/dom/animation/AnimationEffectTimingReadOnly.h
+++ /dev/null
@@ -1,63 +0,0 @@
-/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
-/* vim: set ts=8 sts=2 et sw=2 tw=80: */
-/* 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/. */
-
-#ifndef mozilla_dom_AnimationEffectTimingReadOnly_h
-#define mozilla_dom_AnimationEffectTimingReadOnly_h
-
-#include "js/TypeDecls.h"
-#include "mozilla/Attributes.h"
-#include "mozilla/ErrorResult.h"
-#include "mozilla/TimingParams.h"
-#include "mozilla/dom/BindingDeclarations.h"
-#include "mozilla/dom/UnionTypes.h"
-#include "nsCycleCollectionParticipant.h"
-#include "nsWrapperCache.h"
-
-namespace mozilla {
-namespace dom {
-
-class AnimationEffectTimingReadOnly : public nsWrapperCache
-{
-public:
-  AnimationEffectTimingReadOnly() = default;
-  AnimationEffectTimingReadOnly(nsIDocument* aDocument,
-                                const TimingParams& aTiming)
-    : mDocument(aDocument)
-    , mTiming(aTiming) { }
-
-  NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(AnimationEffectTimingReadOnly)
-  NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_NATIVE_CLASS(AnimationEffectTimingReadOnly)
-
-protected:
-  virtual ~AnimationEffectTimingReadOnly() = default;
-
-public:
-  nsISupports* GetParentObject() const { return mDocument; }
-  JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override;
-
-  double Delay() const { return mTiming.Delay().ToMilliseconds(); }
-  double EndDelay() const { return mTiming.EndDelay().ToMilliseconds(); }
-  FillMode Fill() const { return mTiming.Fill(); }
-  double IterationStart() const { return mTiming.IterationStart(); }
-  double Iterations() const { return mTiming.Iterations(); }
-  void GetDuration(OwningUnrestrictedDoubleOrString& aRetVal) const;
-  PlaybackDirection Direction() const { return mTiming.Direction(); }
-  void GetEasing(nsString& aRetVal) const;
-
-  const TimingParams& AsTimingParams() const { return mTiming; }
-  void SetTimingParams(const TimingParams& aTiming) { mTiming = aTiming; }
-
-  virtual void Unlink() { }
-
-protected:
-  RefPtr<nsIDocument> mDocument;
-  TimingParams mTiming;
-};
-
-} // namespace dom
-} // namespace mozilla
-
-#endif // mozilla_dom_AnimationEffectTimingReadOnly_h
--- a/dom/animation/AnimationUtils.cpp
+++ b/dom/animation/AnimationUtils.cpp
@@ -9,17 +9,17 @@
 #include "nsDebug.h"
 #include "nsAtom.h"
 #include "nsIContent.h"
 #include "nsIDocument.h"
 #include "nsGlobalWindow.h"
 #include "nsString.h"
 #include "xpcpublic.h" // For xpc::NativeGlobal
 #include "mozilla/EffectSet.h"
-#include "mozilla/dom/KeyframeEffectReadOnly.h"
+#include "mozilla/dom/KeyframeEffect.h"
 #include "mozilla/Preferences.h"
 
 namespace mozilla {
 
 /* static */ void
 AnimationUtils::LogAsyncAnimationFailure(nsCString& aMessage,
                                          const nsIContent* aContent)
 {
@@ -73,17 +73,17 @@ AnimationUtils::IsOffscreenThrottlingEna
 
   return sOffscreenThrottlingEnabled;
 }
 
 /* static */ bool
 AnimationUtils::EffectSetContainsAnimatedScale(EffectSet& aEffects,
                                                const nsIFrame* aFrame)
 {
-  for (const dom::KeyframeEffectReadOnly* effect : aEffects) {
+  for (const dom::KeyframeEffect* effect : aEffects) {
     if (effect->ContainsAnimatedScale(aFrame)) {
       return true;
     }
   }
 
   return false;
 }
 
--- a/dom/animation/ComputedTiming.h
+++ b/dom/animation/ComputedTiming.h
@@ -6,17 +6,17 @@
 
 #ifndef mozilla_ComputedTiming_h
 #define mozilla_ComputedTiming_h
 
 #include "mozilla/dom/Nullable.h"
 #include "mozilla/StickyTimeDuration.h"
 #include "mozilla/ComputedTimingFunction.h"
 
-#include "mozilla/dom/AnimationEffectReadOnlyBinding.h" // FillMode
+#include "mozilla/dom/AnimationEffectBinding.h" // FillMode
 
 namespace mozilla {
 
 /**
  * Stores the results of calculating the timing properties of an animation
  * at a given sample time.
  */
 struct ComputedTiming
--- a/dom/animation/EffectCompositor.cpp
+++ b/dom/animation/EffectCompositor.cpp
@@ -6,17 +6,17 @@
 
 #include "EffectCompositor.h"
 
 #include <bitset>
 #include <initializer_list>
 
 #include "mozilla/dom/Animation.h"
 #include "mozilla/dom/Element.h"
-#include "mozilla/dom/KeyframeEffectReadOnly.h"
+#include "mozilla/dom/KeyframeEffect.h"
 #include "mozilla/AnimationComparator.h"
 #include "mozilla/AnimationPerformanceWarning.h"
 #include "mozilla/AnimationTarget.h"
 #include "mozilla/AnimationUtils.h"
 #include "mozilla/AutoRestore.h"
 #include "mozilla/ComputedStyleInlines.h"
 #include "mozilla/EffectSet.h"
 #include "mozilla/LayerAnimationInfo.h"
@@ -33,17 +33,17 @@
 #include "nsIPresShell.h"
 #include "nsIPresShellInlines.h"
 #include "nsLayoutUtils.h"
 #include "nsTArray.h"
 #include "PendingAnimationTracker.h"
 
 using mozilla::dom::Animation;
 using mozilla::dom::Element;
-using mozilla::dom::KeyframeEffectReadOnly;
+using mozilla::dom::KeyframeEffect;
 
 namespace mozilla {
 
 NS_IMPL_CYCLE_COLLECTION_CLASS(EffectCompositor)
 
 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(EffectCompositor)
   for (auto& elementSet : tmp->mElementsToRestyle) {
     elementSet.Clear();
@@ -76,17 +76,17 @@ enum class MatchForCompositor {
   // This animation does not match or can't be run on the compositor and,
   // furthermore, its presence means we should not run any animations for this
   // property on the compositor.
   NoAndBlockThisProperty
 };
 }
 
 static MatchForCompositor
-IsMatchForCompositor(const KeyframeEffectReadOnly& aEffect,
+IsMatchForCompositor(const KeyframeEffect& aEffect,
                      nsCSSPropertyID aProperty,
                      const nsIFrame* aFrame)
 {
   const Animation* animation = aEffect.GetAnimation();
   MOZ_ASSERT(animation);
 
   if (!animation->IsRelevant()) {
     return MatchForCompositor::No;
@@ -192,17 +192,17 @@ FindAnimationsForCompositor(const nsIFra
         AnimationPerformanceWarning(
           AnimationPerformanceWarning::Type::HasRenderingObserver));
       return false;
     }
     content = content->GetParent();
   }
 
   bool foundRunningAnimations = false;
-  for (KeyframeEffectReadOnly* effect : *effects) {
+  for (KeyframeEffect* effect : *effects) {
     MatchForCompositor matchResult =
       IsMatchForCompositor(*effect, aProperty, aFrame);
 
     if (matchResult == MatchForCompositor::NoAndBlockThisProperty) {
       // For a given |aFrame|, we don't want some animations of |aProperty| to
       // run on the compositor and others to run on the main thread, so if any
       // need to be synchronized with the main thread, run them all there.
       if (aMatches) {
@@ -390,33 +390,31 @@ EffectCompositor::UpdateEffectProperties
     return;
   }
 
   // Style context (Gecko) or computed values (Stylo) change might cause CSS
   // cascade level, e.g removing !important, so we should update the cascading
   // result.
   effectSet->MarkCascadeNeedsUpdate();
 
-  for (KeyframeEffectReadOnly* effect : *effectSet) {
+  for (KeyframeEffect* effect : *effectSet) {
     effect->UpdateProperties(aStyle);
   }
 }
 
 
 namespace {
   class EffectCompositeOrderComparator {
   public:
-    bool Equals(const KeyframeEffectReadOnly* a,
-                const KeyframeEffectReadOnly* b) const
+    bool Equals(const KeyframeEffect* a, const KeyframeEffect* b) const
     {
       return a == b;
     }
 
-    bool LessThan(const KeyframeEffectReadOnly* a,
-                  const KeyframeEffectReadOnly* b) const
+    bool LessThan(const KeyframeEffect* a, const KeyframeEffect* b) const
     {
       MOZ_ASSERT(a->GetAnimation() && b->GetAnimation());
       MOZ_ASSERT(
         Equals(a, b) ||
         a->GetAnimation()->HasLowerCompositeOrderThan(*b->GetAnimation()) !=
           b->GetAnimation()->HasLowerCompositeOrderThan(*a->GetAnimation()));
       return a->GetAnimation()->HasLowerCompositeOrderThan(*b->GetAnimation());
     }
@@ -439,30 +437,30 @@ EffectCompositor::GetServoAnimationRule(
              " without a pres shell (e.g. XMLHttpRequest documents)");
 
   EffectSet* effectSet = EffectSet::GetEffectSet(aElement, aPseudoType);
   if (!effectSet) {
     return false;
   }
 
   // Get a list of effects sorted by composite order.
-  nsTArray<KeyframeEffectReadOnly*> sortedEffectList(effectSet->Count());
-  for (KeyframeEffectReadOnly* effect : *effectSet) {
+  nsTArray<KeyframeEffect*> sortedEffectList(effectSet->Count());
+  for (KeyframeEffect* effect : *effectSet) {
     sortedEffectList.AppendElement(effect);
   }
   sortedEffectList.Sort(EffectCompositeOrderComparator());
 
   // If multiple animations affect the same property, animations with higher
   // composite order (priority) override or add or animations with lower
   // priority.
   const nsCSSPropertyIDSet propertiesToSkip =
     aCascadeLevel == CascadeLevel::Animations
       ? effectSet->PropertiesForAnimationsLevel().Inverse()
       : effectSet->PropertiesForAnimationsLevel();
-  for (KeyframeEffectReadOnly* effect : sortedEffectList) {
+  for (KeyframeEffect* effect : sortedEffectList) {
     effect->GetAnimation()->ComposeStyle(*aAnimationValues, propertiesToSkip);
   }
 
   MOZ_ASSERT(effectSet == EffectSet::GetEffectSet(aElement, aPseudoType),
              "EffectSet should not change while composing style");
 
   return true;
 }
@@ -528,17 +526,17 @@ EffectCompositor::GetAnimationsForCompos
 EffectCompositor::ClearIsRunningOnCompositor(const nsIFrame *aFrame,
                                              nsCSSPropertyID aProperty)
 {
   EffectSet* effects = EffectSet::GetEffectSet(aFrame);
   if (!effects) {
     return;
   }
 
-  for (KeyframeEffectReadOnly* effect : *effects) {
+  for (KeyframeEffect* effect : *effects) {
     effect->SetIsRunningOnCompositor(aProperty, false);
   }
 }
 
 /* static */ void
 EffectCompositor::MaybeUpdateCascadeResults(Element* aElement,
                                             CSSPseudoElementType aPseudoType)
 {
@@ -602,17 +600,17 @@ EffectCompositor::GetOverriddenPropertie
   Element* elementToRestyle = GetElementToRestyle(aElement, aPseudoType);
   if (!elementToRestyle) {
     return result;
   }
 
   AutoTArray<nsCSSPropertyID, LayerAnimationInfo::kRecords> propertiesToTrack;
   {
     nsCSSPropertyIDSet propertiesToTrackAsSet;
-    for (KeyframeEffectReadOnly* effect : aEffectSet) {
+    for (KeyframeEffect* effect : aEffectSet) {
       for (const AnimationProperty& property : effect->Properties()) {
         if (nsCSSProps::PropHasFlags(property.mProperty,
                                      CSSPropFlags::CanAnimateOnCompositor) &&
             !propertiesToTrackAsSet.HasProperty(property.mProperty)) {
           propertiesToTrackAsSet.AddProperty(property.mProperty);
           propertiesToTrack.AppendElement(property.mProperty);
         }
       }
@@ -642,18 +640,18 @@ EffectCompositor::UpdateCascadeResults(E
   MOZ_ASSERT(EffectSet::GetEffectSet(aElement, aPseudoType) == &aEffectSet,
              "Effect set should correspond to the specified (pseudo-)element");
   if (aEffectSet.IsEmpty()) {
     aEffectSet.MarkCascadeUpdated();
     return;
   }
 
   // Get a list of effects sorted by composite order.
-  nsTArray<KeyframeEffectReadOnly*> sortedEffectList(aEffectSet.Count());
-  for (KeyframeEffectReadOnly* effect : aEffectSet) {
+  nsTArray<KeyframeEffect*> sortedEffectList(aEffectSet.Count());
+  for (KeyframeEffect* effect : aEffectSet) {
     sortedEffectList.AppendElement(effect);
   }
   sortedEffectList.Sort(EffectCompositeOrderComparator());
 
   // Get properties that override the *animations* level of the cascade.
   //
   // We only do this for properties that we can animate on the compositor
   // since we will apply other properties on the main thread where the usual
@@ -690,17 +688,17 @@ EffectCompositor::UpdateCascadeResults(E
   nsCSSPropertyIDSet prevPropertiesForAnimationsLevel =
     propertiesForAnimationsLevel;
 
   propertiesWithImportantRules.Empty();
   propertiesForAnimationsLevel.Empty();
 
   nsCSSPropertyIDSet propertiesForTransitionsLevel;
 
-  for (const KeyframeEffectReadOnly* effect : sortedEffectList) {
+  for (const KeyframeEffect* effect : sortedEffectList) {
     MOZ_ASSERT(effect->GetAnimation(),
                "Effects on a target element should have an Animation");
     CascadeLevel cascadeLevel = effect->GetAnimation()->CascadeLevel();
 
     for (const AnimationProperty& prop : effect->Properties()) {
       if (overriddenProperties.HasProperty(prop.mProperty)) {
         propertiesWithImportantRules.AddProperty(prop.mProperty);
       }
@@ -761,17 +759,17 @@ EffectCompositor::SetPerformanceWarning(
   nsCSSPropertyID aProperty,
   const AnimationPerformanceWarning& aWarning)
 {
   EffectSet* effects = EffectSet::GetEffectSet(aFrame);
   if (!effects) {
     return;
   }
 
-  for (KeyframeEffectReadOnly* effect : *effects) {
+  for (KeyframeEffect* effect : *effects) {
     effect->SetPerformanceWarning(aProperty, aWarning);
   }
 }
 
 bool
 EffectCompositor::PreTraverse(ServoTraversalFlags aFlags)
 {
   return PreTraverseInSubtree(aFlags, nullptr);
@@ -896,17 +894,17 @@ EffectCompositor::PreTraverseInSubtree(S
       EffectSet* effects = EffectSet::GetEffectSet(target.mElement,
                                                    target.mPseudoType);
       if (!effects) {
         // Drop EffectSets that have been destroyed.
         iter.Remove();
         continue;
       }
 
-      for (KeyframeEffectReadOnly* effect : *effects) {
+      for (KeyframeEffect* effect : *effects) {
         effect->GetAnimation()->WillComposeStyle();
       }
 
       // Remove the element from the list of elements to restyle since we are
       // about to restyle it.
       iter.Remove();
     }
 
@@ -972,17 +970,17 @@ EffectCompositor::PreTraverse(dom::Eleme
                                     cascadeLevel == CascadeLevel::Transitions
                                       ? eRestyle_CSSTransitions
                                       : eRestyle_CSSAnimations);
 
     EffectSet* effects = EffectSet::GetEffectSet(aElement, aPseudoType);
     if (effects) {
       MaybeUpdateCascadeResults(aElement, aPseudoType);
 
-      for (KeyframeEffectReadOnly* effect : *effects) {
+      for (KeyframeEffect* effect : *effects) {
         effect->GetAnimation()->WillComposeStyle();
       }
     }
 
     elementSet.Remove(key);
     found = true;
   }
   return found;
--- a/dom/animation/EffectSet.cpp
+++ b/dom/animation/EffectSet.cpp
@@ -144,28 +144,28 @@ EffectSet::GetEffectSetPropertyAtom(CSSP
     default:
       NS_NOTREACHED("Should not try to get animation effects for a pseudo "
                     "other that :before or :after");
       return nullptr;
   }
 }
 
 void
-EffectSet::AddEffect(dom::KeyframeEffectReadOnly& aEffect)
+EffectSet::AddEffect(dom::KeyframeEffect& aEffect)
 {
   if (mEffects.Contains(&aEffect)) {
     return;
   }
 
   mEffects.PutEntry(&aEffect);
   MarkCascadeNeedsUpdate();
 }
 
 void
-EffectSet::RemoveEffect(dom::KeyframeEffectReadOnly& aEffect)
+EffectSet::RemoveEffect(dom::KeyframeEffect& aEffect)
 {
   if (!mEffects.Contains(&aEffect)) {
     return;
   }
 
   mEffects.RemoveEntry(&aEffect);
   MarkCascadeNeedsUpdate();
 }
--- a/dom/animation/EffectSet.h
+++ b/dom/animation/EffectSet.h
@@ -6,17 +6,17 @@
 
 #ifndef mozilla_EffectSet_h
 #define mozilla_EffectSet_h
 
 #include "mozilla/DebugOnly.h"
 #include "mozilla/EffectCompositor.h"
 #include "mozilla/EnumeratedArray.h"
 #include "mozilla/TimeStamp.h"
-#include "mozilla/dom/KeyframeEffectReadOnly.h"
+#include "mozilla/dom/KeyframeEffect.h"
 #include "nsHashKeys.h" // For nsPtrHashKey
 #include "nsTHashtable.h" // For nsTHashtable
 
 class nsPresContext;
 
 namespace mozilla {
 
 namespace dom {
@@ -61,26 +61,26 @@ public:
   static EffectSet* GetEffectSet(const dom::Element* aElement,
                                  CSSPseudoElementType aPseudoType);
   static EffectSet* GetEffectSet(const nsIFrame* aFrame);
   static EffectSet* GetOrCreateEffectSet(dom::Element* aElement,
                                          CSSPseudoElementType aPseudoType);
   static void DestroyEffectSet(dom::Element* aElement,
                                CSSPseudoElementType aPseudoType);
 
-  void AddEffect(dom::KeyframeEffectReadOnly& aEffect);
-  void RemoveEffect(dom::KeyframeEffectReadOnly& aEffect);
+  void AddEffect(dom::KeyframeEffect& aEffect);
+  void RemoveEffect(dom::KeyframeEffect& aEffect);
 
   void SetMayHaveOpacityAnimation() { mMayHaveOpacityAnim = true; }
   bool MayHaveOpacityAnimation() const { return mMayHaveOpacityAnim; }
   void SetMayHaveTransformAnimation() { mMayHaveTransformAnim = true; }
   bool MayHaveTransformAnimation() const { return mMayHaveTransformAnim; }
 
 private:
-  typedef nsTHashtable<nsRefPtrHashKey<dom::KeyframeEffectReadOnly>>
+  typedef nsTHashtable<nsRefPtrHashKey<dom::KeyframeEffect>>
     OwningEffectSet;
 
 public:
   // A simple iterator to support iterating over the effects in this object in
   // range-based for loops.
   //
   // This allows us to avoid exposing mEffects directly and saves the
   // caller from having to dereference hashtable iterators using
@@ -131,17 +131,17 @@ public:
     }
 
     Iterator& operator++() {
       MOZ_ASSERT(!Done());
       mHashIterator.Next();
       return *this;
     }
 
-    dom::KeyframeEffectReadOnly* operator* ()
+    dom::KeyframeEffect* operator*()
     {
       MOZ_ASSERT(!Done());
       return mHashIterator.Get()->GetKey();
     }
 
   private:
     Iterator() = delete;
     Iterator(const Iterator&) = delete;
--- a/dom/animation/KeyframeEffect.cpp
+++ b/dom/animation/KeyframeEffect.cpp
@@ -1,59 +1,886 @@
 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* 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/. */
 
 #include "mozilla/dom/KeyframeEffect.h"
 
-#include "mozilla/ComputedStyle.h"
+#include "FrameLayerBuilder.h"
+#include "mozilla/dom/Animation.h"
 #include "mozilla/dom/KeyframeAnimationOptionsBinding.h"
-  // For UnrestrictedDoubleOrKeyframeAnimationOptions
-#include "mozilla/dom/AnimationEffectTiming.h"
+  // For UnrestrictedDoubleOrKeyframeAnimationOptions;
+#include "mozilla/dom/CSSPseudoElement.h"
 #include "mozilla/dom/KeyframeEffectBinding.h"
+#include "mozilla/AnimationUtils.h"
+#include "mozilla/AutoRestore.h"
+#include "mozilla/ComputedStyleInlines.h"
+#include "mozilla/EffectSet.h"
+#include "mozilla/FloatingPoint.h" // For IsFinite
+#include "mozilla/LayerAnimationInfo.h"
+#include "mozilla/LookAndFeel.h" // For LookAndFeel::GetInt
+#include "mozilla/KeyframeUtils.h"
+#include "mozilla/ServoBindings.h"
+#include "mozilla/TypeTraits.h"
+#include "Layers.h" // For Layer
+#include "nsComputedDOMStyle.h" // nsComputedDOMStyle::GetComputedStyle
+#include "nsContentUtils.h"
+#include "nsCSSPropertyIDSet.h"
+#include "nsCSSProps.h" // For nsCSSProps::PropHasFlags
+#include "nsCSSPseudoElements.h" // For CSSPseudoElementType
 #include "nsDocument.h" // For nsDocument::IsWebAnimationsEnabled
-#include "nsDOMMutationObserver.h" // For nsAutoAnimationMutationBatch
+#include "nsIFrame.h"
+#include "nsIPresShell.h"
+#include "nsIScriptError.h"
+#include "nsRefreshDriver.h"
 
 namespace mozilla {
+
+bool
+PropertyValuePair::operator==(const PropertyValuePair& aOther) const
+{
+  if (mProperty != aOther.mProperty) {
+    return false;
+  }
+  if (mServoDeclarationBlock == aOther.mServoDeclarationBlock) {
+    return true;
+  }
+  if (!mServoDeclarationBlock || !aOther.mServoDeclarationBlock) {
+    return false;
+  }
+  return Servo_DeclarationBlock_Equals(mServoDeclarationBlock,
+                                       aOther.mServoDeclarationBlock);
+}
+
 namespace dom {
 
-KeyframeEffect::KeyframeEffect(nsIDocument* aDocument,
-                               const Maybe<OwningAnimationTarget>& aTarget,
-                               const TimingParams& aTiming,
-                               const KeyframeEffectParams& aOptions)
-  : KeyframeEffectReadOnly(aDocument, aTarget,
-                           new AnimationEffectTiming(aDocument, aTiming, this),
-                           aOptions)
+NS_IMPL_CYCLE_COLLECTION_INHERITED(KeyframeEffect,
+                                   AnimationEffect,
+                                   mTarget)
+
+NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(KeyframeEffect,
+                                               AnimationEffect)
+NS_IMPL_CYCLE_COLLECTION_TRACE_END
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(KeyframeEffect)
+NS_INTERFACE_MAP_END_INHERITING(AnimationEffect)
+
+NS_IMPL_ADDREF_INHERITED(KeyframeEffect, AnimationEffect)
+NS_IMPL_RELEASE_INHERITED(KeyframeEffect, AnimationEffect)
+
+KeyframeEffect::KeyframeEffect(
+  nsIDocument* aDocument,
+  const Maybe<OwningAnimationTarget>& aTarget,
+  const TimingParams& aTiming,
+  const KeyframeEffectParams& aOptions)
+  : AnimationEffect(aDocument, aTiming)
+  , mTarget(aTarget)
+  , mEffectOptions(aOptions)
+  , mInEffectOnLastAnimationTimingUpdate(false)
+  , mCumulativeChangeHint(nsChangeHint(0))
 {
 }
 
 JSObject*
 KeyframeEffect::WrapObject(JSContext* aCx,
-                           JS::Handle<JSObject*> aGivenProto)
+                                   JS::Handle<JSObject*> aGivenProto)
 {
   return KeyframeEffectBinding::Wrap(aCx, this, aGivenProto);
 }
 
+IterationCompositeOperation KeyframeEffect::IterationComposite(
+  CallerType /*aCallerType*/) const
+{
+  return mEffectOptions.mIterationComposite;
+}
+
+void
+KeyframeEffect::SetIterationComposite(
+  const IterationCompositeOperation& aIterationComposite,
+  CallerType aCallerType)
+{
+  // Ignore iterationComposite if the Web Animations API is not enabled,
+  // then the default value 'Replace' will be used.
+  if (!nsDocument::IsWebAnimationsEnabled(aCallerType)) {
+    return;
+  }
+
+  if (mEffectOptions.mIterationComposite == aIterationComposite) {
+    return;
+  }
+
+  if (mAnimation && mAnimation->IsRelevant()) {
+    nsNodeUtils::AnimationChanged(mAnimation);
+  }
+
+  mEffectOptions.mIterationComposite = aIterationComposite;
+  RequestRestyle(EffectCompositor::RestyleType::Layer);
+}
+
+CompositeOperation
+KeyframeEffect::Composite() const
+{
+  return mEffectOptions.mComposite;
+}
+
+void
+KeyframeEffect::SetComposite(const CompositeOperation& aComposite)
+{
+  if (mEffectOptions.mComposite == aComposite) {
+    return;
+  }
+
+  mEffectOptions.mComposite = aComposite;
+
+  if (mAnimation && mAnimation->IsRelevant()) {
+    nsNodeUtils::AnimationChanged(mAnimation);
+  }
+
+  if (mTarget) {
+    RefPtr<ComputedStyle> computedStyle = GetTargetComputedStyle();
+    if (computedStyle) {
+      UpdateProperties(computedStyle);
+    }
+  }
+}
+
+void
+KeyframeEffect::NotifySpecifiedTimingUpdated()
+{
+  // Use the same document for a pseudo element and its parent element.
+  // Use nullptr if we don't have mTarget, so disable the mutation batch.
+  nsAutoAnimationMutationBatch mb(mTarget ? mTarget->mElement->OwnerDoc()
+                                          : nullptr);
+
+  if (mAnimation) {
+    mAnimation->NotifyEffectTimingUpdated();
+
+    if (mAnimation->IsRelevant()) {
+      nsNodeUtils::AnimationChanged(mAnimation);
+    }
+
+    RequestRestyle(EffectCompositor::RestyleType::Layer);
+  }
+}
+
+void
+KeyframeEffect::NotifyAnimationTimingUpdated()
+{
+  UpdateTargetRegistration();
+
+  // If the effect is not relevant it will be removed from the target
+  // element's effect set. However, effects not in the effect set
+  // will not be included in the set of candidate effects for running on
+  // the compositor and hence they won't have their compositor status
+  // updated. As a result, we need to make sure we clear their compositor
+  // status here.
+  bool isRelevant = mAnimation && mAnimation->IsRelevant();
+  if (!isRelevant) {
+    ResetIsRunningOnCompositor();
+  }
+
+  // Request restyle if necessary.
+  if (mAnimation && !mProperties.IsEmpty() && HasComputedTimingChanged()) {
+    EffectCompositor::RestyleType restyleType =
+      CanThrottle() ?
+      EffectCompositor::RestyleType::Throttled :
+      EffectCompositor::RestyleType::Standard;
+    RequestRestyle(restyleType);
+  }
+
+  // Detect changes to "in effect" status since we need to recalculate the
+  // animation cascade for this element whenever that changes.
+  // Note that updating mInEffectOnLastAnimationTimingUpdate has to be done
+  // after above CanThrottle() call since the function uses the flag inside it.
+  bool inEffect = IsInEffect();
+  if (inEffect != mInEffectOnLastAnimationTimingUpdate) {
+    MarkCascadeNeedsUpdate();
+    mInEffectOnLastAnimationTimingUpdate = inEffect;
+  }
+
+  // If we're no longer "in effect", our ComposeStyle method will never be
+  // called and we will never have a chance to update mProgressOnLastCompose
+  // and mCurrentIterationOnLastCompose.
+  // We clear them here to ensure that if we later become "in effect" we will
+  // request a restyle (above).
+  if (!inEffect) {
+     mProgressOnLastCompose.SetNull();
+     mCurrentIterationOnLastCompose = 0;
+  }
+}
+
+static bool
+KeyframesEqualIgnoringComputedOffsets(const nsTArray<Keyframe>& aLhs,
+                                      const nsTArray<Keyframe>& aRhs)
+{
+  if (aLhs.Length() != aRhs.Length()) {
+    return false;
+  }
+
+  for (size_t i = 0, len = aLhs.Length(); i < len; ++i) {
+    const Keyframe& a = aLhs[i];
+    const Keyframe& b = aRhs[i];
+    if (a.mOffset != b.mOffset ||
+        a.mTimingFunction != b.mTimingFunction ||
+        a.mPropertyValues != b.mPropertyValues) {
+      return false;
+    }
+  }
+  return true;
+}
+
+// https://drafts.csswg.org/web-animations/#dom-keyframeeffect-setkeyframes
+void
+KeyframeEffect::SetKeyframes(JSContext* aContext,
+                             JS::Handle<JSObject*> aKeyframes,
+                             ErrorResult& aRv)
+{
+  nsTArray<Keyframe> keyframes =
+    KeyframeUtils::GetKeyframesFromObject(aContext, mDocument, aKeyframes, aRv);
+  if (aRv.Failed()) {
+    return;
+  }
+
+  RefPtr<ComputedStyle> style = GetTargetComputedStyle();
+  SetKeyframes(Move(keyframes), style);
+}
+
+
+void
+KeyframeEffect::SetKeyframes(
+  nsTArray<Keyframe>&& aKeyframes,
+  const ComputedStyle* aStyle)
+{
+  if (KeyframesEqualIgnoringComputedOffsets(aKeyframes, mKeyframes)) {
+    return;
+  }
+
+  mKeyframes = Move(aKeyframes);
+  KeyframeUtils::DistributeKeyframes(mKeyframes);
+
+  if (mAnimation && mAnimation->IsRelevant()) {
+    nsNodeUtils::AnimationChanged(mAnimation);
+  }
+
+  // We need to call UpdateProperties() unless the target element doesn't have
+  // style (e.g. the target element is not associated with any document).
+  if (aStyle) {
+    UpdateProperties(aStyle);
+    MaybeUpdateFrameForCompositor();
+  }
+}
+
+const AnimationProperty*
+KeyframeEffect::GetEffectiveAnimationOfProperty(nsCSSPropertyID aProperty) const
+{
+  EffectSet* effectSet =
+    EffectSet::GetEffectSet(mTarget->mElement, mTarget->mPseudoType);
+  for (size_t propIdx = 0, propEnd = mProperties.Length();
+       propIdx != propEnd; ++propIdx) {
+    if (aProperty == mProperties[propIdx].mProperty) {
+      const AnimationProperty* result = &mProperties[propIdx];
+      // Skip if there is a property of animation level that is overridden
+      // by !important rules.
+      if (effectSet &&
+          effectSet->PropertiesWithImportantRules()
+            .HasProperty(result->mProperty) &&
+          effectSet->PropertiesForAnimationsLevel()
+            .HasProperty(result->mProperty)) {
+        result = nullptr;
+      }
+      return result;
+    }
+  }
+  return nullptr;
+}
+
+bool
+KeyframeEffect::HasAnimationOfProperty(nsCSSPropertyID aProperty) const
+{
+  for (const AnimationProperty& property : mProperties) {
+    if (property.mProperty == aProperty) {
+      return true;
+    }
+  }
+  return false;
+}
+
+#ifdef DEBUG
+bool
+SpecifiedKeyframeArraysAreEqual(const nsTArray<Keyframe>& aA,
+                                const nsTArray<Keyframe>& aB)
+{
+  if (aA.Length() != aB.Length()) {
+    return false;
+  }
+
+  for (size_t i = 0; i < aA.Length(); i++) {
+    const Keyframe& a = aA[i];
+    const Keyframe& b = aB[i];
+    if (a.mOffset         != b.mOffset ||
+        a.mTimingFunction != b.mTimingFunction ||
+        a.mPropertyValues != b.mPropertyValues) {
+      return false;
+    }
+  }
+
+  return true;
+}
+#endif
+
+void
+KeyframeEffect::UpdateProperties(const ComputedStyle* aStyle)
+{
+  MOZ_ASSERT(aStyle);
+
+  nsTArray<AnimationProperty> properties = BuildProperties(aStyle);
+
+  // We need to update base styles even if any properties are not changed at all
+  // since base styles might have been changed due to parent style changes, etc.
+  EnsureBaseStyles(aStyle, properties);
+
+  if (mProperties == properties) {
+    return;
+  }
+
+  // Preserve the state of the mIsRunningOnCompositor flag.
+  nsCSSPropertyIDSet runningOnCompositorProperties;
+
+  for (const AnimationProperty& property : mProperties) {
+    if (property.mIsRunningOnCompositor) {
+      runningOnCompositorProperties.AddProperty(property.mProperty);
+    }
+  }
+
+  mProperties = Move(properties);
+  UpdateEffectSet();
+
+  for (AnimationProperty& property : mProperties) {
+    property.mIsRunningOnCompositor =
+      runningOnCompositorProperties.HasProperty(property.mProperty);
+  }
+
+  CalculateCumulativeChangeHint(aStyle);
+
+  MarkCascadeNeedsUpdate();
+
+  RequestRestyle(EffectCompositor::RestyleType::Layer);
+}
+
+
+void
+KeyframeEffect::EnsureBaseStyles(
+  const ComputedStyle* aComputedValues,
+  const nsTArray<AnimationProperty>& aProperties)
+{
+  if (!mTarget) {
+    return;
+  }
+
+  mBaseStyleValuesForServo.Clear();
+
+  nsPresContext* presContext =
+    nsContentUtils::GetContextForContent(mTarget->mElement);
+  // If |aProperties| is empty we're not going to dereference |presContext| so
+  // we don't care if it is nullptr.
+  //
+  // We could just return early when |aProperties| is empty and save looking up
+  // the pres context, but that won't save any effort normally since we don't
+  // call this function if we have no keyframes to begin with. Furthermore, the
+  // case where |presContext| is nullptr is so rare (we've only ever seen in
+  // fuzzing, and even then we've never been able to reproduce it reliably)
+  // it's not worth the runtime cost of an extra branch.
+  MOZ_ASSERT(presContext || aProperties.IsEmpty(),
+             "Typically presContext should not be nullptr but if it is"
+             " we should have also failed to calculate the computed values"
+             " passed-in as aProperties");
+
+  RefPtr<ComputedStyle> baseComputedStyle;
+  for (const AnimationProperty& property : aProperties) {
+    EnsureBaseStyle(property,
+                    presContext,
+                    aComputedValues,
+                    baseComputedStyle);
+  }
+}
+
+void
+KeyframeEffect::EnsureBaseStyle(
+  const AnimationProperty& aProperty,
+  nsPresContext* aPresContext,
+  const ComputedStyle* aComputedStyle,
+ RefPtr<ComputedStyle>& aBaseComputedStyle)
+{
+  bool hasAdditiveValues = false;
+
+  for (const AnimationPropertySegment& segment : aProperty.mSegments) {
+    if (!segment.HasReplaceableValues()) {
+      hasAdditiveValues = true;
+      break;
+    }
+  }
+
+  if (!hasAdditiveValues) {
+    return;
+  }
+
+  if (!aBaseComputedStyle) {
+    Element* animatingElement =
+      EffectCompositor::GetElementToRestyle(mTarget->mElement,
+                                            mTarget->mPseudoType);
+    aBaseComputedStyle = aPresContext->StyleSet()->
+      GetBaseContextForElement(animatingElement, aComputedStyle);
+  }
+  RefPtr<RawServoAnimationValue> baseValue =
+    Servo_ComputedValues_ExtractAnimationValue(aBaseComputedStyle,
+                                               aProperty.mProperty).Consume();
+  mBaseStyleValuesForServo.Put(aProperty.mProperty, baseValue);
+}
+
+void
+KeyframeEffect::WillComposeStyle()
+{
+  ComputedTiming computedTiming = GetComputedTiming();
+  mProgressOnLastCompose = computedTiming.mProgress;
+  mCurrentIterationOnLastCompose = computedTiming.mCurrentIteration;
+}
+
+
+void
+KeyframeEffect::ComposeStyleRule(
+  RawServoAnimationValueMap& aAnimationValues,
+  const AnimationProperty& aProperty,
+  const AnimationPropertySegment& aSegment,
+  const ComputedTiming& aComputedTiming)
+{
+  Servo_AnimationCompose(&aAnimationValues,
+                         &mBaseStyleValuesForServo,
+                         aProperty.mProperty,
+                         &aSegment,
+                         &aProperty.mSegments.LastElement(),
+                         &aComputedTiming,
+                         mEffectOptions.mIterationComposite);
+}
+
+void
+KeyframeEffect::ComposeStyle(
+  RawServoAnimationValueMap& aComposeResult,
+  const nsCSSPropertyIDSet& aPropertiesToSkip)
+{
+  ComputedTiming computedTiming = GetComputedTiming();
+
+  // If the progress is null, we don't have fill data for the current
+  // time so we shouldn't animate.
+  if (computedTiming.mProgress.IsNull()) {
+    return;
+  }
+
+  for (size_t propIdx = 0, propEnd = mProperties.Length();
+       propIdx != propEnd; ++propIdx)
+  {
+    const AnimationProperty& prop = mProperties[propIdx];
+
+    MOZ_ASSERT(prop.mSegments[0].mFromKey == 0.0, "incorrect first from key");
+    MOZ_ASSERT(prop.mSegments[prop.mSegments.Length() - 1].mToKey == 1.0,
+               "incorrect last to key");
+
+    if (aPropertiesToSkip.HasProperty(prop.mProperty)) {
+      continue;
+    }
+
+    MOZ_ASSERT(prop.mSegments.Length() > 0,
+               "property should not be in animations if it has no segments");
+
+    // FIXME: Maybe cache the current segment?
+    const AnimationPropertySegment *segment = prop.mSegments.Elements(),
+                                *segmentEnd = segment + prop.mSegments.Length();
+    while (segment->mToKey <= computedTiming.mProgress.Value()) {
+      MOZ_ASSERT(segment->mFromKey <= segment->mToKey, "incorrect keys");
+      if ((segment+1) == segmentEnd) {
+        break;
+      }
+      ++segment;
+      MOZ_ASSERT(segment->mFromKey == (segment-1)->mToKey, "incorrect keys");
+    }
+    MOZ_ASSERT(segment->mFromKey <= segment->mToKey, "incorrect keys");
+    MOZ_ASSERT(segment >= prop.mSegments.Elements() &&
+               size_t(segment - prop.mSegments.Elements()) <
+                 prop.mSegments.Length(),
+               "out of array bounds");
+
+    ComposeStyleRule(aComposeResult, prop, *segment, computedTiming);
+  }
+
+  // If the animation produces a transform change hint that affects the overflow
+  // region, we need to record the current time to unthrottle the animation
+  // periodically when the animation is being throttled because it's scrolled
+  // out of view.
+  if (HasTransformThatMightAffectOverflow()) {
+    nsPresContext* presContext =
+      nsContentUtils::GetContextForContent(mTarget->mElement);
+    if (presContext) {
+      TimeStamp now = presContext->RefreshDriver()->MostRecentRefresh();
+      EffectSet* effectSet =
+        EffectSet::GetEffectSet(mTarget->mElement, mTarget->mPseudoType);
+      MOZ_ASSERT(effectSet, "ComposeStyle should only be called on an effect "
+                            "that is part of an effect set");
+      effectSet->UpdateLastTransformSyncTime(now);
+    }
+  }
+}
+
+bool
+KeyframeEffect::IsRunningOnCompositor() const
+{
+  // We consider animation is running on compositor if there is at least
+  // one property running on compositor.
+  // Animation.IsRunningOnCompotitor will return more fine grained
+  // information in bug 1196114.
+  for (const AnimationProperty& property : mProperties) {
+    if (property.mIsRunningOnCompositor) {
+      return true;
+    }
+  }
+  return false;
+}
+
+void
+KeyframeEffect::SetIsRunningOnCompositor(nsCSSPropertyID aProperty,
+                                         bool aIsRunning)
+{
+  MOZ_ASSERT(nsCSSProps::PropHasFlags(aProperty,
+                                      CSSPropFlags::CanAnimateOnCompositor),
+             "Property being animated on compositor is a recognized "
+             "compositor-animatable property");
+  for (AnimationProperty& property : mProperties) {
+    if (property.mProperty == aProperty) {
+      property.mIsRunningOnCompositor = aIsRunning;
+      // We currently only set a performance warning message when animations
+      // cannot be run on the compositor, so if this animation is running
+      // on the compositor we don't need a message.
+      if (aIsRunning) {
+        property.mPerformanceWarning.reset();
+      }
+      return;
+    }
+  }
+}
+
+void
+KeyframeEffect::ResetIsRunningOnCompositor()
+{
+  for (AnimationProperty& property : mProperties) {
+    property.mIsRunningOnCompositor = false;
+  }
+}
+
+static const KeyframeEffectOptions&
+KeyframeEffectOptionsFromUnion(
+  const UnrestrictedDoubleOrKeyframeEffectOptions& aOptions)
+{
+  MOZ_ASSERT(aOptions.IsKeyframeEffectOptions());
+  return aOptions.GetAsKeyframeEffectOptions();
+}
+
+static const KeyframeEffectOptions&
+KeyframeEffectOptionsFromUnion(
+  const UnrestrictedDoubleOrKeyframeAnimationOptions& aOptions)
+{
+  MOZ_ASSERT(aOptions.IsKeyframeAnimationOptions());
+  return aOptions.GetAsKeyframeAnimationOptions();
+}
+
+template <class OptionsType>
+static KeyframeEffectParams
+KeyframeEffectParamsFromUnion(const OptionsType& aOptions,
+                              CallerType aCallerType)
+{
+  KeyframeEffectParams result;
+  if (aOptions.IsUnrestrictedDouble() ||
+      // Ignore iterationComposite if the Web Animations API is not enabled,
+      // then the default value 'Replace' will be used.
+      !nsDocument::IsWebAnimationsEnabled(aCallerType)) {
+    return result;
+  }
+
+  const KeyframeEffectOptions& options =
+    KeyframeEffectOptionsFromUnion(aOptions);
+  result.mIterationComposite = options.mIterationComposite;
+  result.mComposite = options.mComposite;
+  return result;
+}
+
+/* static */ Maybe<OwningAnimationTarget>
+KeyframeEffect::ConvertTarget(
+  const Nullable<ElementOrCSSPseudoElement>& aTarget)
+{
+  // Return value optimization.
+  Maybe<OwningAnimationTarget> result;
+
+  if (aTarget.IsNull()) {
+    return result;
+  }
+
+  const ElementOrCSSPseudoElement& target = aTarget.Value();
+  MOZ_ASSERT(target.IsElement() || target.IsCSSPseudoElement(),
+             "Uninitialized target");
+
+  if (target.IsElement()) {
+    result.emplace(&target.GetAsElement());
+  } else {
+    RefPtr<Element> elem = target.GetAsCSSPseudoElement().ParentElement();
+    result.emplace(elem, target.GetAsCSSPseudoElement().GetType());
+  }
+  return result;
+}
+
+template <class KeyframeEffectType, class OptionsType>
+/* static */ already_AddRefed<KeyframeEffectType>
+KeyframeEffect::ConstructKeyframeEffect(
+    const GlobalObject& aGlobal,
+    const Nullable<ElementOrCSSPseudoElement>& aTarget,
+    JS::Handle<JSObject*> aKeyframes,
+    const OptionsType& aOptions,
+    ErrorResult& aRv)
+{
+  // We should get the document from `aGlobal` instead of the current Realm
+  // to make this works in Xray case.
+  //
+  // In all non-Xray cases, `aGlobal` matches the current Realm, so this
+  // matches the spec behavior.
+  //
+  // In Xray case, the new objects should be created using the document of
+  // the target global, but the KeyframeEffect constructors are called in the
+  // caller's compartment to access `aKeyframes` object.
+  nsIDocument* doc = AnimationUtils::GetDocumentFromGlobal(aGlobal.Get());
+  if (!doc) {
+    aRv.Throw(NS_ERROR_FAILURE);
+    return nullptr;
+  }
+
+  TimingParams timingParams =
+    TimingParams::FromOptionsUnion(aOptions, doc, aRv);
+  if (aRv.Failed()) {
+    return nullptr;
+  }
+
+  KeyframeEffectParams effectOptions =
+    KeyframeEffectParamsFromUnion(aOptions, aGlobal.CallerType());
+
+  Maybe<OwningAnimationTarget> target = ConvertTarget(aTarget);
+  RefPtr<KeyframeEffectType> effect =
+    new KeyframeEffectType(doc, target, timingParams, effectOptions);
+
+  effect->SetKeyframes(aGlobal.Context(), aKeyframes, aRv);
+  if (aRv.Failed()) {
+    return nullptr;
+  }
+
+  return effect.forget();
+}
+
+template<class KeyframeEffectType>
+/* static */ already_AddRefed<KeyframeEffectType>
+KeyframeEffect::ConstructKeyframeEffect(const GlobalObject& aGlobal,
+                                        KeyframeEffect& aSource,
+                                        ErrorResult& aRv)
+{
+  nsIDocument* doc = AnimationUtils::GetCurrentRealmDocument(aGlobal.Context());
+  if (!doc) {
+    aRv.Throw(NS_ERROR_FAILURE);
+    return nullptr;
+  }
+
+  // Create a new KeyframeEffect object with aSource's target,
+  // iteration composite operation, composite operation, and spacing mode.
+  // The constructor creates a new AnimationEffect object by
+  // aSource's TimingParams.
+  // Note: we don't need to re-throw exceptions since the value specified on
+  //       aSource's timing object can be assumed valid.
+  RefPtr<KeyframeEffectType> effect =
+    new KeyframeEffectType(doc,
+                           aSource.mTarget,
+                           aSource.SpecifiedTiming(),
+                           aSource.mEffectOptions);
+  // Copy cumulative change hint. mCumulativeChangeHint should be the same as
+  // the source one because both of targets are the same.
+  effect->mCumulativeChangeHint = aSource.mCumulativeChangeHint;
+
+  // Copy aSource's keyframes and animation properties.
+  // Note: We don't call SetKeyframes directly, which might revise the
+  //       computed offsets and rebuild the animation properties.
+  effect->mKeyframes = aSource.mKeyframes;
+  effect->mProperties = aSource.mProperties;
+  return effect.forget();
+}
+
+nsTArray<AnimationProperty>
+KeyframeEffect::BuildProperties(const ComputedStyle* aStyle)
+{
+
+  MOZ_ASSERT(aStyle);
+
+  nsTArray<AnimationProperty> result;
+  // If mTarget is null, return an empty property array.
+  if (!mTarget) {
+    return result;
+  }
+
+  // When GetComputedKeyframeValues or GetAnimationPropertiesFromKeyframes
+  // calculate computed values from |mKeyframes|, they could possibly
+  // trigger a subsequent restyle in which we rebuild animations. If that
+  // happens we could find that |mKeyframes| is overwritten while it is
+  // being iterated over. Normally that shouldn't happen but just in case we
+  // make a copy of |mKeyframes| first and iterate over that instead.
+  auto keyframesCopy(mKeyframes);
+
+  result =
+    KeyframeUtils::GetAnimationPropertiesFromKeyframes(
+      keyframesCopy,
+      mTarget->mElement,
+      aStyle,
+      mEffectOptions.mComposite);
+
+#ifdef DEBUG
+  MOZ_ASSERT(SpecifiedKeyframeArraysAreEqual(mKeyframes, keyframesCopy),
+             "Apart from the computed offset members, the keyframes array"
+             " should not be modified");
+#endif
+
+  mKeyframes.SwapElements(keyframesCopy);
+  return result;
+}
+
+void
+KeyframeEffect::UpdateTargetRegistration()
+{
+  if (!mTarget) {
+    return;
+  }
+
+  bool isRelevant = mAnimation && mAnimation->IsRelevant();
+
+  // Animation::IsRelevant() returns a cached value. It only updates when
+  // something calls Animation::UpdateRelevance. Whenever our timing changes,
+  // we should be notifying our Animation before calling this, so
+  // Animation::IsRelevant() should be up-to-date by the time we get here.
+  MOZ_ASSERT(isRelevant == IsCurrent() || IsInEffect(),
+             "Out of date Animation::IsRelevant value");
+
+  if (isRelevant && !mInEffectSet) {
+    EffectSet* effectSet =
+      EffectSet::GetOrCreateEffectSet(mTarget->mElement, mTarget->mPseudoType);
+    effectSet->AddEffect(*this);
+    mInEffectSet = true;
+    UpdateEffectSet(effectSet);
+    nsIFrame* f = GetPrimaryFrame();
+    while (f) {
+      f->MarkNeedsDisplayItemRebuild();
+      f = f->GetNextContinuation();
+    }
+  } else if (!isRelevant && mInEffectSet) {
+    UnregisterTarget();
+  }
+}
+
+void
+KeyframeEffect::UnregisterTarget()
+{
+  if (!mInEffectSet) {
+    return;
+  }
+
+  EffectSet* effectSet =
+    EffectSet::GetEffectSet(mTarget->mElement, mTarget->mPseudoType);
+  MOZ_ASSERT(effectSet, "If mInEffectSet is true, there must be an EffectSet"
+                        " on the target element");
+  mInEffectSet = false;
+  if (effectSet) {
+    effectSet->RemoveEffect(*this);
+
+    if (effectSet->IsEmpty()) {
+      EffectSet::DestroyEffectSet(mTarget->mElement, mTarget->mPseudoType);
+    }
+  }
+  nsIFrame* f = GetPrimaryFrame();
+  while (f) {
+    f->MarkNeedsDisplayItemRebuild();
+    f = f->GetNextContinuation();
+  }
+}
+
+void
+KeyframeEffect::RequestRestyle(EffectCompositor::RestyleType aRestyleType)
+{
+   if (!mTarget) {
+    return;
+  }
+  nsPresContext* presContext = nsContentUtils::GetContextForContent(mTarget->mElement);
+  if (presContext && mAnimation) {
+    presContext->EffectCompositor()->
+      RequestRestyle(mTarget->mElement, mTarget->mPseudoType,
+                     aRestyleType, mAnimation->CascadeLevel());
+  }
+}
+
+already_AddRefed<ComputedStyle>
+KeyframeEffect::GetTargetComputedStyle()
+{
+  if (!GetRenderedDocument()) {
+    return nullptr;
+  }
+
+  MOZ_ASSERT(mTarget,
+             "Should only have a document when we have a target element");
+
+  nsAtom* pseudo = mTarget->mPseudoType < CSSPseudoElementType::Count
+                    ? nsCSSPseudoElements::GetPseudoAtom(mTarget->mPseudoType)
+                    : nullptr;
+
+  OwningAnimationTarget kungfuDeathGrip(mTarget->mElement,
+                                        mTarget->mPseudoType);
+
+  return nsComputedDOMStyle::GetComputedStyle(mTarget->mElement, pseudo);
+}
+
+#ifdef DEBUG
+void
+DumpAnimationProperties(nsTArray<AnimationProperty>& aAnimationProperties)
+{
+  for (auto& p : aAnimationProperties) {
+    printf("%s\n", nsCSSProps::GetStringValue(p.mProperty).get());
+    for (auto& s : p.mSegments) {
+      nsString fromValue, toValue;
+      s.mFromValue.SerializeSpecifiedValue(p.mProperty, fromValue);
+      s.mToValue.SerializeSpecifiedValue(p.mProperty, toValue);
+      printf("  %f..%f: %s..%s\n", s.mFromKey, s.mToKey,
+             NS_ConvertUTF16toUTF8(fromValue).get(),
+             NS_ConvertUTF16toUTF8(toValue).get());
+    }
+  }
+}
+#endif
+
 /* static */ already_AddRefed<KeyframeEffect>
 KeyframeEffect::Constructor(
     const GlobalObject& aGlobal,
     const Nullable<ElementOrCSSPseudoElement>& aTarget,
     JS::Handle<JSObject*> aKeyframes,
     const UnrestrictedDoubleOrKeyframeEffectOptions& aOptions,
     ErrorResult& aRv)
 {
-  return ConstructKeyframeEffect<KeyframeEffect>(aGlobal, aTarget, aKeyframes,
-                                                 aOptions, aRv);
+  return ConstructKeyframeEffect<KeyframeEffect>(aGlobal, aTarget,
+                                                 aKeyframes, aOptions, aRv);
 }
 
 /* static */ already_AddRefed<KeyframeEffect>
 KeyframeEffect::Constructor(const GlobalObject& aGlobal,
-                            KeyframeEffectReadOnly& aSource,
+                            KeyframeEffect& aSource,
                             ErrorResult& aRv)
 {
   return ConstructKeyframeEffect<KeyframeEffect>(aGlobal, aSource, aRv);
 }
 
 /* static */ already_AddRefed<KeyframeEffect>
 KeyframeEffect::Constructor(
     const GlobalObject& aGlobal,
@@ -62,31 +889,38 @@ KeyframeEffect::Constructor(
     const UnrestrictedDoubleOrKeyframeAnimationOptions& aOptions,
     ErrorResult& aRv)
 {
   return ConstructKeyframeEffect<KeyframeEffect>(aGlobal, aTarget, aKeyframes,
                                                  aOptions, aRv);
 }
 
 void
-KeyframeEffect::NotifySpecifiedTimingUpdated()
+KeyframeEffect::GetTarget(Nullable<OwningElementOrCSSPseudoElement>& aRv) const
 {
-  // Use the same document for a pseudo element and its parent element.
-  // Use nullptr if we don't have mTarget, so disable the mutation batch.
-  nsAutoAnimationMutationBatch mb(mTarget ? mTarget->mElement->OwnerDoc()
-                                          : nullptr);
+  if (!mTarget) {
+    aRv.SetNull();
+    return;
+  }
 
-  if (mAnimation) {
-    mAnimation->NotifyEffectTimingUpdated();
+  switch (mTarget->mPseudoType) {
+    case CSSPseudoElementType::before:
+    case CSSPseudoElementType::after:
+      aRv.SetValue().SetAsCSSPseudoElement() =
+        CSSPseudoElement::GetCSSPseudoElement(mTarget->mElement,
+                                              mTarget->mPseudoType);
+      break;
 
-    if (mAnimation->IsRelevant()) {
-      nsNodeUtils::AnimationChanged(mAnimation);
-    }
+    case CSSPseudoElementType::NotPseudo:
+      aRv.SetValue().SetAsElement() = mTarget->mElement;
+      break;
 
-    RequestRestyle(EffectCompositor::RestyleType::Layer);
+    default:
+      NS_NOTREACHED("Animation of unsupported pseudo-type");
+      aRv.SetNull();
   }
 }
 
 void
 KeyframeEffect::SetTarget(const Nullable<ElementOrCSSPseudoElement>& aTarget)
 {
   Maybe<OwningAnimationTarget> newTarget = ConvertTarget(aTarget);
   if (mTarget == newTarget) {
@@ -121,54 +955,847 @@ KeyframeEffect::SetTarget(const Nullable
 
     nsAutoAnimationMutationBatch mb(mTarget->mElement->OwnerDoc());
     if (mAnimation) {
       nsNodeUtils::AnimationAdded(mAnimation);
     }
   }
 }
 
-void
-KeyframeEffect::SetIterationComposite(
-  const IterationCompositeOperation& aIterationComposite,
-  CallerType aCallerType)
+static void
+CreatePropertyValue(nsCSSPropertyID aProperty,
+                    float aOffset,
+                    const Maybe<ComputedTimingFunction>& aTimingFunction,
+                    const AnimationValue& aValue,
+                    dom::CompositeOperation aComposite,
+                    AnimationPropertyValueDetails& aResult)
 {
-  // Ignore iterationComposite if the Web Animations API is not enabled,
-  // then the default value 'Replace' will be used.
-  if (!nsDocument::IsWebAnimationsEnabled(aCallerType)) {
-    return;
+  aResult.mOffset = aOffset;
+
+  if (!aValue.IsNull()) {
+    nsString stringValue;
+    aValue.SerializeSpecifiedValue(aProperty, stringValue);
+    aResult.mValue.Construct(stringValue);
+  }
+
+  if (aTimingFunction) {
+    aResult.mEasing.Construct();
+    aTimingFunction->AppendToString(aResult.mEasing.Value());
+  } else {
+    aResult.mEasing.Construct(NS_LITERAL_STRING("linear"));
   }
 
-  if (mEffectOptions.mIterationComposite == aIterationComposite) {
+  aResult.mComposite = aComposite;
+}
+
+void
+KeyframeEffect::GetProperties(
+    nsTArray<AnimationPropertyDetails>& aProperties,
+    ErrorResult& aRv) const
+{
+  for (const AnimationProperty& property : mProperties) {
+    AnimationPropertyDetails propertyDetails;
+    propertyDetails.mProperty =
+      NS_ConvertASCIItoUTF16(nsCSSProps::GetStringValue(property.mProperty));
+    propertyDetails.mRunningOnCompositor = property.mIsRunningOnCompositor;
+
+    nsAutoString localizedString;
+    if (property.mPerformanceWarning &&
+        property.mPerformanceWarning->ToLocalizedString(localizedString)) {
+      propertyDetails.mWarning.Construct(localizedString);
+    }
+
+    if (!propertyDetails.mValues.SetCapacity(property.mSegments.Length(),
+                                             mozilla::fallible)) {
+      aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
+      return;
+    }
+
+    for (size_t segmentIdx = 0, segmentLen = property.mSegments.Length();
+         segmentIdx < segmentLen;
+         segmentIdx++)
+    {
+      const AnimationPropertySegment& segment = property.mSegments[segmentIdx];
+
+      binding_detail::FastAnimationPropertyValueDetails fromValue;
+      CreatePropertyValue(property.mProperty, segment.mFromKey,
+                          segment.mTimingFunction, segment.mFromValue,
+                          segment.mFromComposite, fromValue);
+      // We don't apply timing functions for zero-length segments, so
+      // don't return one here.
+      if (segment.mFromKey == segment.mToKey) {
+        fromValue.mEasing.Reset();
+      }
+      // The following won't fail since we have already allocated the capacity
+      // above.
+      propertyDetails.mValues.AppendElement(fromValue, mozilla::fallible);
+
+      // Normally we can ignore the to-value for this segment since it is
+      // identical to the from-value from the next segment. However, we need
+      // to add it if either:
+      // a) this is the last segment, or
+      // b) the next segment's from-value differs.
+      if (segmentIdx == segmentLen - 1 ||
+          property.mSegments[segmentIdx + 1].mFromValue != segment.mToValue) {
+        binding_detail::FastAnimationPropertyValueDetails toValue;
+        CreatePropertyValue(property.mProperty, segment.mToKey,
+                            Nothing(), segment.mToValue,
+                            segment.mToComposite, toValue);
+        // It doesn't really make sense to have a timing function on the
+        // last property value or before a sudden jump so we just drop the
+        // easing property altogether.
+        toValue.mEasing.Reset();
+        propertyDetails.mValues.AppendElement(toValue, mozilla::fallible);
+      }
+    }
+
+    aProperties.AppendElement(propertyDetails);
+  }
+}
+
+void
+KeyframeEffect::GetKeyframes(JSContext*& aCx,
+                             nsTArray<JSObject*>& aResult,
+                             ErrorResult& aRv)
+{
+  MOZ_ASSERT(aResult.IsEmpty());
+  MOZ_ASSERT(!aRv.Failed());
+
+  if (!aResult.SetCapacity(mKeyframes.Length(), mozilla::fallible)) {
+    aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
     return;
   }
 
-  if (mAnimation && mAnimation->IsRelevant()) {
-    nsNodeUtils::AnimationChanged(mAnimation);
+  bool isCSSAnimation = mAnimation && mAnimation->AsCSSAnimation();
+
+  // For Servo, when we have CSS Animation @keyframes with variables, we convert
+  // shorthands to longhands if needed, and store a reference to the unparsed
+  // value. When it comes time to serialize, however, what do you serialize for
+  // a longhand that comes from a variable reference in a shorthand? Servo says,
+  // "an empty string" which is not particularly helpful.
+  //
+  // We should just store shorthands as-is (bug 1391537) and then return the
+  // variable references, but for now, since we don't do that, and in order to
+  // be consistent with Gecko, we just expand the variables (assuming we have
+  // enough context to do so). For that we need to grab the ComputedStyle so we
+  // know what custom property values to provide.
+  RefPtr<ComputedStyle> computedStyle;
+  if (isCSSAnimation) {
+    // The following will flush style but that's ok since if you update
+    // a variable's computed value, you expect to see that updated value in the
+    // result of getKeyframes().
+    //
+    // If we don't have a target, the following will return null. In that case
+    // we might end up returning variables as-is or empty string. That should be
+    // acceptable however, since such a case is rare and this is only
+    // short-term (and unshipped) behavior until bug 1391537 is fixed.
+    computedStyle = GetTargetComputedStyle();
+  }
+
+  for (const Keyframe& keyframe : mKeyframes) {
+    // Set up a dictionary object for the explicit members
+    BaseComputedKeyframe keyframeDict;
+    if (keyframe.mOffset) {
+      keyframeDict.mOffset.SetValue(keyframe.mOffset.value());
+    }
+    MOZ_ASSERT(keyframe.mComputedOffset != Keyframe::kComputedOffsetNotSet,
+               "Invalid computed offset");
+    keyframeDict.mComputedOffset.Construct(keyframe.mComputedOffset);
+    if (keyframe.mTimingFunction) {
+      keyframeDict.mEasing.Truncate();
+      keyframe.mTimingFunction.ref().AppendToString(keyframeDict.mEasing);
+    } // else if null, leave easing as its default "linear".
+
+    if (keyframe.mComposite) {
+      keyframeDict.mComposite.SetValue(keyframe.mComposite.value());
+    }
+
+    JS::Rooted<JS::Value> keyframeJSValue(aCx);
+    if (!ToJSValue(aCx, keyframeDict, &keyframeJSValue)) {
+      aRv.Throw(NS_ERROR_FAILURE);
+      return;
+    }
+
+    RefPtr<RawServoDeclarationBlock> customProperties;
+    // A workaround for CSS Animations in servo backend, custom properties in
+    // keyframe are stored in a servo's declaration block. Find the declaration
+    // block to resolve CSS variables in the keyframe.
+    // This workaround will be solved by bug 1391537.
+    if (isCSSAnimation) {
+      for (const PropertyValuePair& propertyValue : keyframe.mPropertyValues) {
+        if (propertyValue.mProperty ==
+              nsCSSPropertyID::eCSSPropertyExtra_variable) {
+          customProperties = propertyValue.mServoDeclarationBlock;
+          break;
+        }
+      }
+    }
+
+    JS::Rooted<JSObject*> keyframeObject(aCx, &keyframeJSValue.toObject());
+    for (const PropertyValuePair& propertyValue : keyframe.mPropertyValues) {
+      nsAutoString stringValue;
+      // Don't serialize the custom properties for this keyframe.
+      if (propertyValue.mProperty ==
+            nsCSSPropertyID::eCSSPropertyExtra_variable) {
+        continue;
+      }
+      if (propertyValue.mServoDeclarationBlock) {
+        Servo_DeclarationBlock_SerializeOneValue(
+          propertyValue.mServoDeclarationBlock,
+          propertyValue.mProperty,
+          &stringValue,
+          computedStyle,
+          customProperties);
+      } else {
+        RawServoAnimationValue* value =
+          mBaseStyleValuesForServo.GetWeak(propertyValue.mProperty);
+
+        if (value) {
+          Servo_AnimationValue_Serialize(value,
+                                         propertyValue.mProperty,
+                                         &stringValue);
+        }
+      }
+
+      const char* name = nsCSSProps::PropertyIDLName(propertyValue.mProperty);
+      JS::Rooted<JS::Value> value(aCx);
+      if (!ToJSValue(aCx, stringValue, &value) ||
+          !JS_DefineProperty(aCx, keyframeObject, name, value,
+                             JSPROP_ENUMERATE)) {
+        aRv.Throw(NS_ERROR_FAILURE);
+        return;
+      }
+    }
+
+    aResult.AppendElement(keyframeObject);
+  }
+}
+
+/* static */ const TimeDuration
+KeyframeEffect::OverflowRegionRefreshInterval()
+{
+  // The amount of time we can wait between updating throttled animations
+  // on the main thread that influence the overflow region.
+  static const TimeDuration kOverflowRegionRefreshInterval =
+    TimeDuration::FromMilliseconds(200);
+
+  return kOverflowRegionRefreshInterval;
+}
+
+bool
+KeyframeEffect::CanThrottle() const
+{
+  // Unthrottle if we are not in effect or current. This will be the case when
+  // our owning animation has finished, is idle, or when we are in the delay
+  // phase (but without a backwards fill). In each case the computed progress
+  // value produced on each tick will be the same so we will skip requesting
+  // unnecessary restyles in NotifyAnimationTimingUpdated. Any calls we *do* get
+  // here will be because of a change in state (e.g. we are newly finished or
+  // newly no longer in effect) in which case we shouldn't throttle the sample.
+  if (!IsInEffect() || !IsCurrent()) {
+    return false;
+  }
+
+  nsIFrame* frame = GetStyleFrame();
+  if (!frame) {
+    // There are two possible cases here.
+    // a) No target element
+    // b) The target element has no frame, e.g. because it is in a display:none
+    //    subtree.
+    // In either case we can throttle the animation because there is no
+    // need to update on the main thread.
+    return true;
+  }
+
+  // Unless we are newly in-effect, we can throttle the animation if the
+  // animation is paint only and the target frame is out of view or the document
+  // is in background tabs.
+  if (mInEffectOnLastAnimationTimingUpdate && CanIgnoreIfNotVisible()) {
+    nsIPresShell* presShell = GetPresShell();
+    if (presShell && !presShell->IsActive()) {
+      return true;
+    }
+
+    const bool isVisibilityHidden =
+      !frame->IsVisibleOrMayHaveVisibleDescendants();
+    if ((isVisibilityHidden && !HasVisibilityChange()) ||
+        frame->IsScrolledOutOfView()) {
+      // If there are transform change hints, unthrottle the animation
+      // periodically since it might affect the overflow region.
+      if (HasTransformThatMightAffectOverflow()) {
+        // Don't throttle finite transform animations since the animation might
+        // suddenly come into view and if it was throttled it will be
+        // out-of-sync.
+        if (HasFiniteActiveDuration()) {
+          return false;
+        }
+
+        return isVisibilityHidden
+          ? CanThrottleTransformChangesInScrollable(*frame)
+          : CanThrottleTransformChanges(*frame);
+      }
+      return true;
+    }
+  }
+
+  // First we need to check layer generation and transform overflow
+  // prior to the property.mIsRunningOnCompositor check because we should
+  // occasionally unthrottle these animations even if the animations are
+  // already running on compositor.
+  for (const LayerAnimationInfo::Record& record :
+        LayerAnimationInfo::sRecords) {
+    // Skip properties that are overridden by !important rules.
+    // (GetEffectiveAnimationOfProperty, as called by
+    // HasEffectiveAnimationOfProperty, only returns a property which is
+    // neither overridden by !important rules nor overridden by other
+    // animation.)
+    if (!HasEffectiveAnimationOfProperty(record.mProperty)) {
+      continue;
+    }
+
+    EffectSet* effectSet = EffectSet::GetEffectSet(mTarget->mElement,
+                                                   mTarget->mPseudoType);
+    MOZ_ASSERT(effectSet, "CanThrottle should be called on an effect "
+                          "associated with a target element");
+    // Note that AnimationInfo::GetGenarationFromFrame() is supposed to work
+    // with the primary frame instead of the style frame.
+    Maybe<uint64_t> generation = layers::AnimationInfo::GetGenerationFromFrame(
+        GetPrimaryFrame(), record.mLayerType);
+    // Unthrottle if the animation needs to be brought up to date
+    if (!generation || effectSet->GetAnimationGeneration() != *generation) {
+      return false;
+    }
+
+    // If this is a transform animation that affects the overflow region,
+    // we should unthrottle the animation periodically.
+    if (HasTransformThatMightAffectOverflow() &&
+        !CanThrottleTransformChangesInScrollable(*frame)) {
+      return false;
+    }
+  }
+
+  for (const AnimationProperty& property : mProperties) {
+    if (!property.mIsRunningOnCompositor) {
+      return false;
+    }
   }
 
-  mEffectOptions.mIterationComposite = aIterationComposite;
-  RequestRestyle(EffectCompositor::RestyleType::Layer);
+  return true;
+}
+
+bool
+KeyframeEffect::CanThrottleTransformChanges(const nsIFrame& aFrame) const
+{
+  TimeStamp now = aFrame.PresContext()->RefreshDriver()->MostRecentRefresh();
+
+  EffectSet* effectSet = EffectSet::GetEffectSet(mTarget->mElement,
+                                                 mTarget->mPseudoType);
+  MOZ_ASSERT(effectSet, "CanThrottleTransformChanges is expected to be called"
+                        " on an effect in an effect set");
+  MOZ_ASSERT(mAnimation, "CanThrottleTransformChanges is expected to be called"
+                         " on an effect with a parent animation");
+  TimeStamp lastSyncTime = effectSet->LastTransformSyncTime();
+  // If this animation can cause overflow, we can throttle some of the ticks.
+  return (!lastSyncTime.IsNull() &&
+    (now - lastSyncTime) < OverflowRegionRefreshInterval());
+}
+
+bool
+KeyframeEffect::CanThrottleTransformChangesInScrollable(nsIFrame& aFrame) const
+{
+  // If the target element is not associated with any documents, we don't care
+  // it.
+  nsIDocument* doc = GetRenderedDocument();
+  if (!doc) {
+    return true;
+  }
+
+  bool hasIntersectionObservers = doc->HasIntersectionObservers();
+
+  // If we know that the animation cannot cause overflow,
+  // we can just disable flushes for this animation.
+
+  // If we don't show scrollbars and have no intersection observers, we don't
+  // care about overflow.
+  if (LookAndFeel::GetInt(LookAndFeel::eIntID_ShowHideScrollbars) == 0 &&
+      !hasIntersectionObservers) {
+    return true;
+  }
+
+  if (CanThrottleTransformChanges(aFrame)) {
+    return true;
+  }
+
+  // If we have any intersection observers, we unthrottle this transform
+  // animation periodically.
+  if (hasIntersectionObservers) {
+    return false;
+  }
+
+  // If the nearest scrollable ancestor has overflow:hidden,
+  // we don't care about overflow.
+  nsIScrollableFrame* scrollable =
+    nsLayoutUtils::GetNearestScrollableFrame(&aFrame);
+  if (!scrollable) {
+    return true;
+  }
+
+  ScrollbarStyles ss = scrollable->GetScrollbarStyles();
+  if (ss.mVertical == NS_STYLE_OVERFLOW_HIDDEN &&
+      ss.mHorizontal == NS_STYLE_OVERFLOW_HIDDEN &&
+      scrollable->GetLogicalScrollPosition() == nsPoint(0, 0)) {
+    return true;
+  }
+
+  return false;
+}
+
+nsIFrame*
+KeyframeEffect::GetStyleFrame() const
+{
+  nsIFrame* frame = GetPrimaryFrame();
+  if (!frame) {
+    return nullptr;
+  }
+
+  return nsLayoutUtils::GetStyleFrame(frame);
+}
+
+nsIFrame*
+KeyframeEffect::GetPrimaryFrame() const
+{
+  nsIFrame* frame = nullptr;
+  if (!mTarget) {
+    return frame;
+  }
+
+  if (mTarget->mPseudoType == CSSPseudoElementType::before) {
+    frame = nsLayoutUtils::GetBeforeFrame(mTarget->mElement);
+  } else if (mTarget->mPseudoType == CSSPseudoElementType::after) {
+    frame = nsLayoutUtils::GetAfterFrame(mTarget->mElement);
+  } else {
+    frame = mTarget->mElement->GetPrimaryFrame();
+    MOZ_ASSERT(mTarget->mPseudoType == CSSPseudoElementType::NotPseudo,
+               "unknown mTarget->mPseudoType");
+  }
+
+  return frame;
+}
+
+nsIDocument*
+KeyframeEffect::GetRenderedDocument() const
+{
+  if (!mTarget) {
+    return nullptr;
+  }
+  return mTarget->mElement->GetComposedDoc();
+}
+
+nsIPresShell*
+KeyframeEffect::GetPresShell() const
+{
+  nsIDocument* doc = GetRenderedDocument();
+  if (!doc) {
+    return nullptr;
+  }
+  return doc->GetShell();
+}
+
+/* static */ bool
+KeyframeEffect::IsGeometricProperty(const nsCSSPropertyID aProperty)
+{
+  MOZ_ASSERT(!nsCSSProps::IsShorthand(aProperty),
+             "Property should be a longhand property");
+
+  switch (aProperty) {
+    case eCSSProperty_bottom:
+    case eCSSProperty_height:
+    case eCSSProperty_left:
+    case eCSSProperty_margin_bottom:
+    case eCSSProperty_margin_left:
+    case eCSSProperty_margin_right:
+    case eCSSProperty_margin_top:
+    case eCSSProperty_padding_bottom:
+    case eCSSProperty_padding_left:
+    case eCSSProperty_padding_right:
+    case eCSSProperty_padding_top:
+    case eCSSProperty_right:
+    case eCSSProperty_top:
+    case eCSSProperty_width:
+      return true;
+    default:
+      return false;
+  }
+}
+
+/* static */ bool
+KeyframeEffect::CanAnimateTransformOnCompositor(
+  const nsIFrame* aFrame,
+  AnimationPerformanceWarning::Type& aPerformanceWarning)
+{
+  // Disallow OMTA for preserve-3d transform. Note that we check the style property
+  // rather than Extend3DContext() since that can recurse back into this function
+  // via HasOpacity(). See bug 779598.
+  if (aFrame->Combines3DTransformWithAncestors() ||
+      aFrame->StyleDisplay()->mTransformStyle == NS_STYLE_TRANSFORM_STYLE_PRESERVE_3D) {
+    aPerformanceWarning = AnimationPerformanceWarning::Type::TransformPreserve3D;
+    return false;
+  }
+  // Note that testing BackfaceIsHidden() is not a sufficient test for
+  // what we need for animating backface-visibility correctly if we
+  // remove the above test for Extend3DContext(); that would require
+  // looking at backface-visibility on descendants as well. See bug 1186204.
+  if (aFrame->BackfaceIsHidden()) {
+    aPerformanceWarning =
+      AnimationPerformanceWarning::Type::TransformBackfaceVisibilityHidden;
+    return false;
+  }
+  // Async 'transform' animations of aFrames with SVG transforms is not
+  // supported.  See bug 779599.
+  if (aFrame->IsSVGTransformed()) {
+    aPerformanceWarning = AnimationPerformanceWarning::Type::TransformSVG;
+    return false;
+  }
+
+  return true;
+}
+
+bool
+KeyframeEffect::ShouldBlockAsyncTransformAnimations(
+  const nsIFrame* aFrame,
+  AnimationPerformanceWarning::Type& aPerformanceWarning) const
+{
+  EffectSet* effectSet =
+    EffectSet::GetEffectSet(mTarget->mElement, mTarget->mPseudoType);
+  for (const AnimationProperty& property : mProperties) {
+    // If there is a property for animations level that is overridden by
+    // !important rules, it should not block other animations from running
+    // on the compositor.
+    // NOTE: We don't currently check for !important rules for properties that
+    // don't run on the compositor. As result such properties (e.g. margin-left)
+    // can still block async animations even if they are overridden by
+    // !important rules.
+    if (effectSet &&
+        effectSet->PropertiesWithImportantRules()
+          .HasProperty(property.mProperty) &&
+        effectSet->PropertiesForAnimationsLevel()
+          .HasProperty(property.mProperty)) {
+      continue;
+    }
+    // Check for geometric properties
+    if (IsGeometricProperty(property.mProperty)) {
+      aPerformanceWarning =
+        AnimationPerformanceWarning::Type::TransformWithGeometricProperties;
+      return true;
+    }
+
+    // Check for unsupported transform animations
+    if (property.mProperty == eCSSProperty_transform) {
+      if (!CanAnimateTransformOnCompositor(aFrame,
+                                           aPerformanceWarning)) {
+        return true;
+      }
+    }
+  }
+
+  // XXX cku temporarily disable async-animation when this frame has any
+  // individual transforms before bug 1425837 been fixed.
+  if (aFrame->StyleDisplay()->HasIndividualTransform()) {
+    return true;
+  }
+
+  return false;
+}
+
+bool
+KeyframeEffect::HasGeometricProperties() const
+{
+  for (const AnimationProperty& property : mProperties) {
+    if (IsGeometricProperty(property.mProperty)) {
+      return true;
+    }
+  }
+
+  return false;
 }
 
 void
-KeyframeEffect::SetComposite(const CompositeOperation& aComposite)
+KeyframeEffect::SetPerformanceWarning(
+  nsCSSPropertyID aProperty,
+  const AnimationPerformanceWarning& aWarning)
+{
+  for (AnimationProperty& property : mProperties) {
+    if (property.mProperty == aProperty &&
+        (!property.mPerformanceWarning ||
+         *property.mPerformanceWarning != aWarning)) {
+      property.mPerformanceWarning = Some(aWarning);
+
+      nsAutoString localizedString;
+      if (nsLayoutUtils::IsAnimationLoggingEnabled() &&
+          property.mPerformanceWarning->ToLocalizedString(localizedString)) {
+        nsAutoCString logMessage = NS_ConvertUTF16toUTF8(localizedString);
+        AnimationUtils::LogAsyncAnimationFailure(logMessage, mTarget->mElement);
+      }
+      return;
+    }
+  }
+}
+
+
+already_AddRefed<ComputedStyle>
+KeyframeEffect::CreateComputedStyleForAnimationValue(
+  nsCSSPropertyID aProperty,
+  const AnimationValue& aValue,
+  nsPresContext* aPresContext,
+  const ComputedStyle* aBaseComputedStyle)
+{
+  MOZ_ASSERT(aBaseComputedStyle,
+             "CreateComputedStyleForAnimationValue needs to be called "
+             "with a valid ComputedStyle");
+
+  ServoStyleSet* styleSet = aPresContext->StyleSet();
+  Element* elementForResolve =
+    EffectCompositor::GetElementToRestyle(mTarget->mElement,
+                                          mTarget->mPseudoType);
+  MOZ_ASSERT(elementForResolve, "The target element shouldn't be null");
+  return styleSet->ResolveServoStyleByAddingAnimation(elementForResolve,
+                                                      aBaseComputedStyle,
+                                                      aValue.mServo);
+}
+
+void
+KeyframeEffect::CalculateCumulativeChangeHint(const ComputedStyle* aComputedStyle)
 {
-  if (mEffectOptions.mComposite == aComposite) {
+  mCumulativeChangeHint = nsChangeHint(0);
+
+  nsPresContext* presContext =
+    nsContentUtils::GetContextForContent(mTarget->mElement);
+  if (!presContext) {
+    // Change hints make no sense if we're not rendered.
+    //
+    // Actually, we cannot even post them anywhere.
+    return;
+  }
+
+  for (const AnimationProperty& property : mProperties) {
+    // For opacity property we don't produce any change hints that are not
+    // included in nsChangeHint_Hints_CanIgnoreIfNotVisible so we can throttle
+    // opacity animations regardless of the change they produce.  This
+    // optimization is particularly important since it allows us to throttle
+    // opacity animations with missing 0%/100% keyframes.
+    if (property.mProperty == eCSSProperty_opacity) {
+      continue;
+    }
+
+    for (const AnimationPropertySegment& segment : property.mSegments) {
+      // In case composite operation is not 'replace' or value is null,
+      // we can't throttle animations which will not cause any layout changes
+      // on invisible elements because we can't calculate the change hint for
+      // such properties until we compose it.
+      if (!segment.HasReplaceableValues()) {
+        mCumulativeChangeHint = ~nsChangeHint_Hints_CanIgnoreIfNotVisible;
+        return;
+      }
+      RefPtr<ComputedStyle> fromContext =
+        CreateComputedStyleForAnimationValue(property.mProperty,
+                                             segment.mFromValue,
+                                             presContext,
+                                             aComputedStyle);
+      if (!fromContext) {
+        mCumulativeChangeHint = ~nsChangeHint_Hints_CanIgnoreIfNotVisible;
+        return;
+      }
+
+      RefPtr<ComputedStyle> toContext =
+        CreateComputedStyleForAnimationValue(property.mProperty,
+                                             segment.mToValue,
+                                             presContext,
+                                             aComputedStyle);
+      if (!toContext) {
+        mCumulativeChangeHint = ~nsChangeHint_Hints_CanIgnoreIfNotVisible;
+        return;
+      }
+
+      uint32_t equalStructs = 0;
+      nsChangeHint changeHint =
+        fromContext->CalcStyleDifference(toContext, &equalStructs);
+
+      mCumulativeChangeHint |= changeHint;
+    }
+  }
+}
+
+void
+KeyframeEffect::SetAnimation(Animation* aAnimation)
+{
+  if (mAnimation == aAnimation) {
     return;
   }
 
-  mEffectOptions.mComposite = aComposite;
+  // Restyle for the old animation.
+  RequestRestyle(EffectCompositor::RestyleType::Layer);
+
+  mAnimation = aAnimation;
+
+  // The order of these function calls is important:
+  // NotifyAnimationTimingUpdated() need the updated mIsRelevant flag to check
+  // if it should create the effectSet or not, and MarkCascadeNeedsUpdate()
+  // needs a valid effectSet, so we should call them in this order.
+  if (mAnimation) {
+    mAnimation->UpdateRelevance();
+  }
+  NotifyAnimationTimingUpdated();
+  if (mAnimation) {
+    MarkCascadeNeedsUpdate();
+  }
+}
+
+bool
+KeyframeEffect::CanIgnoreIfNotVisible() const
+{
+  if (!AnimationUtils::IsOffscreenThrottlingEnabled()) {
+    return false;
+  }
 
-  if (mAnimation && mAnimation->IsRelevant()) {
-    nsNodeUtils::AnimationChanged(mAnimation);
+  // FIXME: For further sophisticated optimization we need to check
+  // change hint on the segment corresponding to computedTiming.progress.
+  return NS_IsHintSubset(
+    mCumulativeChangeHint, nsChangeHint_Hints_CanIgnoreIfNotVisible);
+}
+
+void
+KeyframeEffect::MaybeUpdateFrameForCompositor()
+{
+  nsIFrame* frame = GetStyleFrame();
+  if (!frame) {
+    return;
+  }
+
+  // FIXME: Bug 1272495: If this effect does not win in the cascade, the
+  // NS_FRAME_MAY_BE_TRANSFORMED flag should be removed when the animation
+  // will be removed from effect set or the transform keyframes are removed
+  // by setKeyframes. The latter case will be hard to solve though.
+  for (const AnimationProperty& property : mProperties) {
+    if (property.mProperty == eCSSProperty_transform) {
+      frame->AddStateBits(NS_FRAME_MAY_BE_TRANSFORMED);
+      return;
+    }
+  }
+}
+
+void
+KeyframeEffect::MarkCascadeNeedsUpdate()
+{
+  if (!mTarget) {
+    return;
   }
 
-  if (mTarget) {
-    RefPtr<ComputedStyle> computedStyle = GetTargetComputedStyle();
-    if (computedStyle) {
-      UpdateProperties(computedStyle);
+  EffectSet* effectSet = EffectSet::GetEffectSet(mTarget->mElement,
+                                                 mTarget->mPseudoType);
+  if (!effectSet) {
+    return;
+  }
+  effectSet->MarkCascadeNeedsUpdate();
+}
+
+/* static */ bool
+KeyframeEffect::HasComputedTimingChanged(
+  const ComputedTiming& aComputedTiming,
+  IterationCompositeOperation aIterationComposite,
+  const Nullable<double>& aProgressOnLastCompose,
+  uint64_t aCurrentIterationOnLastCompose)
+{
+  // Typically we don't need to request a restyle if the progress hasn't
+  // changed since the last call to ComposeStyle. The one exception is if the
+  // iteration composite mode is 'accumulate' and the current iteration has
+  // changed, since that will often produce a different result.
+  return aComputedTiming.mProgress != aProgressOnLastCompose ||
+         (aIterationComposite == IterationCompositeOperation::Accumulate &&
+          aComputedTiming.mCurrentIteration != aCurrentIterationOnLastCompose);
+}
+
+bool
+KeyframeEffect::HasComputedTimingChanged() const
+{
+  ComputedTiming computedTiming = GetComputedTiming();
+  return HasComputedTimingChanged(computedTiming,
+                                  mEffectOptions.mIterationComposite,
+                                  mProgressOnLastCompose,
+                                  mCurrentIterationOnLastCompose);
+}
+
+bool
+KeyframeEffect::ContainsAnimatedScale(const nsIFrame* aFrame) const
+{
+  if (!IsCurrent()) {
+    return false;
+  }
+
+  for (const AnimationProperty& prop : mProperties) {
+    if (prop.mProperty != eCSSProperty_transform) {
+      continue;
+    }
+
+    AnimationValue baseStyle = BaseStyle(prop.mProperty);
+    if (baseStyle.IsNull()) {
+      // If we failed to get the base style, we consider it has scale value
+      // here just to be safe.
+      return true;
+    }
+    gfx::Size size = baseStyle.GetScaleValue(aFrame);
+    if (size != gfx::Size(1.0f, 1.0f)) {
+      return true;
+    }
+
+    // This is actually overestimate because there are some cases that combining
+    // the base value and from/to value produces 1:1 scale. But it doesn't
+    // really matter.
+    for (const AnimationPropertySegment& segment : prop.mSegments) {
+      if (!segment.mFromValue.IsNull()) {
+        gfx::Size from = segment.mFromValue.GetScaleValue(aFrame);
+        if (from != gfx::Size(1.0f, 1.0f)) {
+          return true;
+        }
+      }
+      if (!segment.mToValue.IsNull()) {
+        gfx::Size to = segment.mToValue.GetScaleValue(aFrame);
+        if (to != gfx::Size(1.0f, 1.0f)) {
+          return true;
+        }
+      }
+    }
+  }
+
+  return false;
+}
+
+void
+KeyframeEffect::UpdateEffectSet(EffectSet* aEffectSet) const
+{
+  if (!mInEffectSet) {
+    return;
+  }
+
+  EffectSet* effectSet =
+    aEffectSet ? aEffectSet
+               : EffectSet::GetEffectSet(mTarget->mElement,
+                                         mTarget->mPseudoType);
+  if (!effectSet) {
+    return;
+  }
+
+  nsIFrame* frame = GetStyleFrame();
+  if (HasAnimationOfProperty(eCSSProperty_opacity)) {
+    effectSet->SetMayHaveOpacityAnimation();
+    if (frame) {
+      frame->SetMayHaveOpacityAnimation();
+    }
+  }
+  if (HasAnimationOfProperty(eCSSProperty_transform)) {
+    effectSet->SetMayHaveTransformAnimation();
+    if (frame) {
+      frame->SetMayHaveTransformAnimation();
     }
   }
 }
 
 } // namespace dom
 } // namespace mozilla
--- a/dom/animation/KeyframeEffect.h
+++ b/dom/animation/KeyframeEffect.h
@@ -2,87 +2,464 @@
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* 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/. */
 
 #ifndef mozilla_dom_KeyframeEffect_h
 #define mozilla_dom_KeyframeEffect_h
 
+#include "nsChangeHint.h"
+#include "nsCSSPropertyID.h"
+#include "nsCSSPropertyIDSet.h"
+#include "nsCSSValue.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsRefPtrHashtable.h"
+#include "nsTArray.h"
 #include "nsWrapperCache.h"
+#include "mozilla/AnimationPerformanceWarning.h"
+#include "mozilla/AnimationPropertySegment.h"
+#include "mozilla/AnimationTarget.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/ComputedTimingFunction.h"
+#include "mozilla/EffectCompositor.h"
+#include "mozilla/Keyframe.h"
+#include "mozilla/KeyframeEffectParams.h"
+// RawServoDeclarationBlock and associated RefPtrTraits
+#include "mozilla/ServoBindingTypes.h"
+#include "mozilla/StyleAnimationValue.h"
+#include "mozilla/dom/AnimationEffect.h"
 #include "mozilla/dom/BindingDeclarations.h"
-#include "mozilla/dom/KeyframeEffectReadOnly.h"
-#include "mozilla/AnimationTarget.h" // For (Non)OwningAnimationTarget
-#include "mozilla/Maybe.h"
+#include "mozilla/dom/Element.h"
 
 struct JSContext;
 class JSObject;
+class nsIContent;
 class nsIDocument;
+class nsIFrame;
+class nsIPresShell;
 
 namespace mozilla {
 
+class AnimValuesStyleRule;
+enum class CSSPseudoElementType : uint8_t;
 class ErrorResult;
-struct KeyframeEffectParams;
+struct AnimationRule;
 struct TimingParams;
+class EffectSet;
+class ComputedStyle;
+
+namespace dom {
+class ElementOrCSSPseudoElement;
+class GlobalObject;
+class OwningElementOrCSSPseudoElement;
+class UnrestrictedDoubleOrKeyframeAnimationOptions;
+class UnrestrictedDoubleOrKeyframeEffectOptions;
+enum class IterationCompositeOperation : uint8_t;
+enum class CompositeOperation : uint8_t;
+struct AnimationPropertyDetails;
+}
+
+struct AnimationProperty
+{
+  nsCSSPropertyID mProperty = eCSSProperty_UNKNOWN;
+
+  // If true, the propery is currently being animated on the compositor.
+  //
+  // Note that when the owning Animation requests a non-throttled restyle, in
+  // between calling RequestRestyle on its EffectCompositor and when the
+  // restyle is performed, this member may temporarily become false even if
+  // the animation remains on the layer after the restyle.
+  //
+  // **NOTE**: This member is not included when comparing AnimationProperty
+  // objects for equality.
+  bool mIsRunningOnCompositor = false;
+
+  Maybe<AnimationPerformanceWarning> mPerformanceWarning;
+
+  InfallibleTArray<AnimationPropertySegment> mSegments;
+
+  // The copy constructor/assignment doesn't copy mIsRunningOnCompositor and
+  // mPerformanceWarning.
+  AnimationProperty() = default;
+  AnimationProperty(const AnimationProperty& aOther)
+    : mProperty(aOther.mProperty), mSegments(aOther.mSegments) { }
+  AnimationProperty& operator=(const AnimationProperty& aOther)
+  {
+    mProperty = aOther.mProperty;
+    mSegments = aOther.mSegments;
+    return *this;
+  }
+
+  // NOTE: This operator does *not* compare the mIsRunningOnCompositor member.
+  // This is because AnimationProperty objects are compared when recreating
+  // CSS animations to determine if mutation observer change records need to
+  // be created or not. However, at the point when these objects are compared
+  // the mIsRunningOnCompositor will not have been set on the new objects so
+  // we ignore this member to avoid generating spurious change records.
+  bool operator==(const AnimationProperty& aOther) const
+  {
+    return mProperty == aOther.mProperty &&
+           mSegments == aOther.mSegments;
+  }
+  bool operator!=(const AnimationProperty& aOther) const
+  {
+    return !(*this == aOther);
+  }
+};
+
+struct ElementPropertyTransition;
 
 namespace dom {
 
-class ElementOrCSSPseudoElement;
-class GlobalObject;
-class UnrestrictedDoubleOrKeyframeAnimationOptions;
-class UnrestrictedDoubleOrKeyframeEffectOptions;
+class Animation;
 
-class KeyframeEffect : public KeyframeEffectReadOnly
+class KeyframeEffect : public AnimationEffect
 {
 public:
   KeyframeEffect(nsIDocument* aDocument,
                  const Maybe<OwningAnimationTarget>& aTarget,
                  const TimingParams& aTiming,
                  const KeyframeEffectParams& aOptions);
 
-  JSObject* WrapObject(JSContext* aCx,
-                       JS::Handle<JSObject*> aGivenProto) override;
+  NS_DECL_ISUPPORTS_INHERITED
+  NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(KeyframeEffect,
+                                                         AnimationEffect)
 
+  virtual JSObject* WrapObject(JSContext* aCx,
+                               JS::Handle<JSObject*> aGivenProto) override;
+
+  KeyframeEffect* AsKeyframeEffect() override { return this; }
+
+  // KeyframeEffect interface
   static already_AddRefed<KeyframeEffect>
   Constructor(const GlobalObject& aGlobal,
               const Nullable<ElementOrCSSPseudoElement>& aTarget,
               JS::Handle<JSObject*> aKeyframes,
               const UnrestrictedDoubleOrKeyframeEffectOptions& aOptions,
               ErrorResult& aRv);
 
   static already_AddRefed<KeyframeEffect>
   Constructor(const GlobalObject& aGlobal,
-              KeyframeEffectReadOnly& aSource,
+              KeyframeEffect& aSource,
               ErrorResult& aRv);
 
   // Variant of Constructor that accepts a KeyframeAnimationOptions object
   // for use with for Animatable.animate.
   // Not exposed to content.
   static already_AddRefed<KeyframeEffect>
   Constructor(const GlobalObject& aGlobal,
               const Nullable<ElementOrCSSPseudoElement>& aTarget,
               JS::Handle<JSObject*> aKeyframes,
               const UnrestrictedDoubleOrKeyframeAnimationOptions& aOptions,
               ErrorResult& aRv);
 
-  void NotifySpecifiedTimingUpdated();
-
+  void GetTarget(Nullable<OwningElementOrCSSPseudoElement>& aRv) const;
+  Maybe<NonOwningAnimationTarget> GetTarget() const
+  {
+    Maybe<NonOwningAnimationTarget> result;
+    if (mTarget) {
+      result.emplace(*mTarget);
+    }
+    return result;
+  }
   // This method calls GetTargetComputedStyle which is not safe to use when
   // we are in the middle of updating style. If we need to use this when
   // updating style, we should pass the ComputedStyle into this method and use
   // that to update the properties rather than calling
   // GetComputedStyle.
   void SetTarget(const Nullable<ElementOrCSSPseudoElement>& aTarget);
 
-  IterationCompositeOperation IterationComposite(CallerType aCallerType)
-  {
-    return KeyframeEffectReadOnly::IterationComposite();
-  }
+  void GetKeyframes(JSContext*& aCx,
+                    nsTArray<JSObject*>& aResult,
+                    ErrorResult& aRv);
+  void GetProperties(nsTArray<AnimationPropertyDetails>& aProperties,
+                     ErrorResult& aRv) const;
+
+  // aCallerType is not used in the getter so we supply a default value so that
+  // internal users don't need to specify this value.
+  IterationCompositeOperation IterationComposite(
+    CallerType aCallerType = CallerType::System) const;
   void SetIterationComposite(
     const IterationCompositeOperation& aIterationComposite,
     CallerType aCallerType);
+
+  CompositeOperation Composite() const;
   void SetComposite(const CompositeOperation& aComposite);
+
+  void NotifySpecifiedTimingUpdated();
+  void NotifyAnimationTimingUpdated();
+  void RequestRestyle(EffectCompositor::RestyleType aRestyleType);
+  void SetAnimation(Animation* aAnimation) override;
+  void SetKeyframes(JSContext* aContext, JS::Handle<JSObject*> aKeyframes,
+                    ErrorResult& aRv);
+  void SetKeyframes(nsTArray<Keyframe>&& aKeyframes,
+                    const ComputedStyle* aStyle);
+
+  // Returns true if the effect includes |aProperty| regardless of whether the
+  // property is overridden by !important rule.
+  bool HasAnimationOfProperty(nsCSSPropertyID aProperty) const;
+
+  // GetEffectiveAnimationOfProperty returns AnimationProperty corresponding
+  // to a given CSS property if the effect includes the property and the
+  // property is not overridden by !important rules.
+  // Also EffectiveAnimationOfProperty returns true under the same condition.
+  //
+  // NOTE: We don't currently check for !important rules for properties that
+  // can't run on the compositor.
+  bool HasEffectiveAnimationOfProperty(nsCSSPropertyID aProperty) const
+  {
+    return GetEffectiveAnimationOfProperty(aProperty) != nullptr;
+  }
+  const AnimationProperty* GetEffectiveAnimationOfProperty(
+    nsCSSPropertyID aProperty) const;
+
+  const InfallibleTArray<AnimationProperty>& Properties() const
+  {
+    return mProperties;
+  }
+
+  // Update |mProperties| by recalculating from |mKeyframes| using
+  // |aComputedStyle| to resolve specified values.
+  void UpdateProperties(const ComputedStyle* aComputedValues);
+
+  // Update various bits of state related to running ComposeStyle().
+  // We need to update this outside ComposeStyle() because we should avoid
+  // mutating any state in ComposeStyle() since it might be called during
+  // parallel traversal.
+  void WillComposeStyle();
+
+  // Updates |aComposeResult| with the animation values produced by this
+  // AnimationEffect for the current time except any properties contained
+  // in |aPropertiesToSkip|.
+  void ComposeStyle(RawServoAnimationValueMap& aComposeResult,
+                    const nsCSSPropertyIDSet& aPropertiesToSkip);
+
+
+  // Returns true if at least one property is being animated on compositor.
+  bool IsRunningOnCompositor() const;
+  void SetIsRunningOnCompositor(nsCSSPropertyID aProperty, bool aIsRunning);
+  void ResetIsRunningOnCompositor();
+
+  // Returns true if this effect, applied to |aFrame|, contains properties
+  // that mean we shouldn't run transform compositor animations on this element.
+  //
+  // For example, if we have an animation of geometric properties like 'left'
+  // and 'top' on an element, we force all 'transform' animations running at
+  // the same time on the same element to run on the main thread.
+  //
+  // When returning true, |aPerformanceWarning| stores the reason why
+  // we shouldn't run the transform animations.
+  bool ShouldBlockAsyncTransformAnimations(
+    const nsIFrame* aFrame, AnimationPerformanceWarning::Type& aPerformanceWarning) const;
+  bool HasGeometricProperties() const;
+  bool AffectsGeometry() const override
+  {
+    return GetTarget() && HasGeometricProperties();
+  }
+
+  nsIDocument* GetRenderedDocument() const;
+  nsIPresShell* GetPresShell() const;
+
+  // Associates a warning with the animated property on the specified frame
+  // indicating why, for example, the property could not be animated on the
+  // compositor. |aParams| and |aParamsLength| are optional parameters which
+  // will be used to generate a localized message for devtools.
+  void SetPerformanceWarning(
+    nsCSSPropertyID aProperty,
+    const AnimationPerformanceWarning& aWarning);
+
+  // Cumulative change hint on each segment for each property.
+  // This is used for deciding the animation is paint-only.
+  void CalculateCumulativeChangeHint(const ComputedStyle* aStyle);
+
+  // Returns true if all of animation properties' change hints
+  // can ignore painting if the animation is not visible.
+  // See nsChangeHint_Hints_CanIgnoreIfNotVisible in nsChangeHint.h
+  // in detail which change hint can be ignored.
+  bool CanIgnoreIfNotVisible() const;
+
+  // Returns true if the effect is current state and has scale animation.
+  // |aFrame| is used for calculation of scale values.
+  bool ContainsAnimatedScale(const nsIFrame* aFrame) const;
+
+  AnimationValue BaseStyle(nsCSSPropertyID aProperty) const
+  {
+    AnimationValue result;
+    bool hasProperty = false;
+    // We cannot use getters_AddRefs on RawServoAnimationValue because it is
+    // an incomplete type, so Get() doesn't work. Instead, use GetWeak, and
+    // then assign the raw pointer to a RefPtr.
+    result.mServo = mBaseStyleValuesForServo.GetWeak(aProperty, &hasProperty);
+    MOZ_ASSERT(hasProperty || result.IsNull());
+    return result;
+  }
+
+  static bool HasComputedTimingChanged(
+    const ComputedTiming& aComputedTiming,
+    IterationCompositeOperation aIterationComposite,
+    const Nullable<double>& aProgressOnLastCompose,
+    uint64_t aCurrentIterationOnLastCompose);
+
+protected:
+  ~KeyframeEffect() override = default;
+
+  static Maybe<OwningAnimationTarget>
+  ConvertTarget(const Nullable<ElementOrCSSPseudoElement>& aTarget);
+
+  template<class KeyframeEffectType, class OptionsType>
+  static already_AddRefed<KeyframeEffectType>
+  ConstructKeyframeEffect(const GlobalObject& aGlobal,
+                          const Nullable<ElementOrCSSPseudoElement>& aTarget,
+                          JS::Handle<JSObject*> aKeyframes,
+                          const OptionsType& aOptions,
+                          ErrorResult& aRv);
+
+  template<class KeyframeEffectType>
+  static already_AddRefed<KeyframeEffectType>
+  ConstructKeyframeEffect(const GlobalObject& aGlobal,
+                          KeyframeEffect& aSource,
+                          ErrorResult& aRv);
+
+  // Build properties by recalculating from |mKeyframes| using |aComputedStyle|
+  // to resolve specified values. This function also applies paced spacing if
+  // needed.
+  nsTArray<AnimationProperty> BuildProperties(const ComputedStyle* aStyle);
+
+  // This effect is registered with its target element so long as:
+  //
+  // (a) It has a target element, and
+  // (b) It is "relevant" (i.e. yet to finish but not idle, or finished but
+  //     filling forwards)
+  //
+  // As a result, we need to make sure this gets called whenever anything
+  // changes with regards to this effects's timing including changes to the
+  // owning Animation's timing.
+  void UpdateTargetRegistration();
+
+  // Remove the current effect target from its EffectSet.
+  void UnregisterTarget();
+
+  // Update the associated frame state bits so that, if necessary, a stacking
+  // context will be created and the effect sent to the compositor.  We
+  // typically need to do this when the properties referenced by the keyframe
+  // have changed, or when the target frame might have changed.
+  void MaybeUpdateFrameForCompositor();
+
+  // Looks up the ComputedStyle associated with the target element, if any.
+  // We need to be careful to *not* call this when we are updating the style
+  // context. That's because calling GetComputedStyle when we are in the process
+  // of building a ComputedStyle may trigger various forms of infinite
+  // recursion.
+  already_AddRefed<ComputedStyle> GetTargetComputedStyle();
+
+  // A wrapper for marking cascade update according to the current
+  // target and its effectSet.
+  void MarkCascadeNeedsUpdate();
+
+  void EnsureBaseStyles(const ComputedStyle* aComputedValues,
+                        const nsTArray<AnimationProperty>& aProperties);
+
+  // Stylo version of the above function that also first checks for an additive
+  // value in |aProperty|'s list of segments.
+  void EnsureBaseStyle(const AnimationProperty& aProperty,
+                       nsPresContext* aPresContext,
+                       const ComputedStyle* aComputedValues,
+                       RefPtr<ComputedStyle>& aBaseComputedValues);
+
+  Maybe<OwningAnimationTarget> mTarget;
+
+  KeyframeEffectParams mEffectOptions;
+
+  // The specified keyframes.
+  nsTArray<Keyframe>          mKeyframes;
+
+  // A set of per-property value arrays, derived from |mKeyframes|.
+  nsTArray<AnimationProperty> mProperties;
+
+  // The computed progress last time we composed the style rule. This is
+  // used to detect when the progress is not changing (e.g. due to a step
+  // timing function) so we can avoid unnecessary style updates.
+  Nullable<double> mProgressOnLastCompose;
+
+  // The purpose of this value is the same as mProgressOnLastCompose but
+  // this is used to detect when the current iteration is not changing
+  // in the case when iterationComposite is accumulate.
+  uint64_t mCurrentIterationOnLastCompose = 0;
+
+  // We need to track when we go to or from being "in effect" since
+  // we need to re-evaluate the cascade of animations when that changes.
+  bool mInEffectOnLastAnimationTimingUpdate;
+
+  // The non-animated values for properties in this effect that contain at
+  // least one animation value that is composited with the underlying value
+  // (i.e. it uses the additive or accumulate composite mode).
+  nsRefPtrHashtable<nsUint32HashKey, RawServoAnimationValue>
+    mBaseStyleValuesForServo;
+
+  // True if this effect is in the EffectSet for its target element. This is
+  // used as an optimization to avoid unnecessary hashmap lookups on the
+  // EffectSet.
+  bool mInEffectSet = false;
+
+private:
+  nsChangeHint mCumulativeChangeHint;
+
+  void ComposeStyleRule(RawServoAnimationValueMap& aAnimationValues,
+                        const AnimationProperty& aProperty,
+                        const AnimationPropertySegment& aSegment,
+                        const ComputedTiming& aComputedTiming);
+
+
+  already_AddRefed<ComputedStyle> CreateComputedStyleForAnimationValue(
+    nsCSSPropertyID aProperty,
+    const AnimationValue& aValue,
+    nsPresContext* aPresContext,
+    const ComputedStyle* aBaseComputedStyle);
+
+  // Return the primary frame for the target (pseudo-)element.
+  nsIFrame* GetPrimaryFrame() const;
+  // Returns the frame which is used for styling.
+  nsIFrame* GetStyleFrame() const;
+
+  bool CanThrottle() const;
+  bool CanThrottleTransformChanges(const nsIFrame& aFrame) const;
+  bool CanThrottleTransformChangesInScrollable(nsIFrame& aFrame) const;
+
+  // Returns true if the computedTiming has changed since the last
+  // composition.
+  bool HasComputedTimingChanged() const;
+
+  // Returns true unless Gecko limitations prevent performing transform
+  // animations for |aFrame|. When returning true, the reason for the
+  // limitation is stored in |aOutPerformanceWarning|.
+  static bool CanAnimateTransformOnCompositor(
+    const nsIFrame* aFrame,
+    AnimationPerformanceWarning::Type& aPerformanceWarning);
+  static bool IsGeometricProperty(const nsCSSPropertyID aProperty);
+
+  static const TimeDuration OverflowRegionRefreshInterval();
+
+  void UpdateEffectSet(mozilla::EffectSet* aEffectSet = nullptr) const;
+
+  // Returns true if this effect has transform and the transform might affect
+  // the overflow region.
+  // This function is used for updating scroll bars or notifying intersection
+  // observers reflected by the transform.
+  bool HasTransformThatMightAffectOverflow() const
+  {
+    return mCumulativeChangeHint & (nsChangeHint_UpdatePostTransformOverflow |
+                                    nsChangeHint_AddOrRemoveTransform |
+                                    nsChangeHint_UpdateTransformLayer);
+  }
+
+  // Returns true if this effect causes visibility change.
+  // (i.e. 'visibility: hidden' -> 'visibility: visible' and vice versa.)
+  bool HasVisibilityChange() const
+  {
+    return mCumulativeChangeHint & nsChangeHint_VisibilityChange;
+  }
 };
 
 } // namespace dom
 } // namespace mozilla
 
 #endif // mozilla_dom_KeyframeEffect_h
deleted file mode 100644
--- a/dom/animation/KeyframeEffectReadOnly.cpp
+++ /dev/null
@@ -1,1704 +0,0 @@
-/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
-/* vim: set ts=8 sts=2 et sw=2 tw=80: */
-/* 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/. */
-
-#include "mozilla/dom/KeyframeEffectReadOnly.h"
-
-#include "FrameLayerBuilder.h"
-#include "mozilla/dom/Animation.h"
-#include "mozilla/dom/KeyframeAnimationOptionsBinding.h"
-  // For UnrestrictedDoubleOrKeyframeAnimationOptions;
-#include "mozilla/dom/CSSPseudoElement.h"
-#include "mozilla/dom/KeyframeEffectBinding.h"
-#include "mozilla/AnimationUtils.h"
-#include "mozilla/AutoRestore.h"
-#include "mozilla/ComputedStyleInlines.h"
-#include "mozilla/EffectSet.h"
-#include "mozilla/FloatingPoint.h" // For IsFinite
-#include "mozilla/LayerAnimationInfo.h"
-#include "mozilla/LookAndFeel.h" // For LookAndFeel::GetInt
-#include "mozilla/KeyframeUtils.h"
-#include "mozilla/ServoBindings.h"
-#include "mozilla/TypeTraits.h"
-#include "Layers.h" // For Layer
-#include "nsComputedDOMStyle.h" // nsComputedDOMStyle::GetComputedStyle
-#include "nsContentUtils.h"
-#include "nsCSSPropertyIDSet.h"
-#include "nsCSSProps.h" // For nsCSSProps::PropHasFlags
-#include "nsCSSPseudoElements.h" // For CSSPseudoElementType
-#include "nsDocument.h" // For nsDocument::IsWebAnimationsEnabled
-#include "nsIFrame.h"
-#include "nsIPresShell.h"
-#include "nsIScriptError.h"
-#include "nsRefreshDriver.h"
-
-namespace mozilla {
-
-bool
-PropertyValuePair::operator==(const PropertyValuePair& aOther) const
-{
-  if (mProperty != aOther.mProperty) {
-    return false;
-  }
-  if (mServoDeclarationBlock == aOther.mServoDeclarationBlock) {
-    return true;
-  }
-  if (!mServoDeclarationBlock || !aOther.mServoDeclarationBlock) {
-    return false;
-  }
-  return Servo_DeclarationBlock_Equals(mServoDeclarationBlock,
-                                       aOther.mServoDeclarationBlock);
-}
-
-namespace dom {
-
-NS_IMPL_CYCLE_COLLECTION_INHERITED(KeyframeEffectReadOnly,
-                                   AnimationEffectReadOnly,
-                                   mTarget)
-
-NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(KeyframeEffectReadOnly,
-                                               AnimationEffectReadOnly)
-NS_IMPL_CYCLE_COLLECTION_TRACE_END
-
-NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(KeyframeEffectReadOnly)
-NS_INTERFACE_MAP_END_INHERITING(AnimationEffectReadOnly)
-
-NS_IMPL_ADDREF_INHERITED(KeyframeEffectReadOnly, AnimationEffectReadOnly)
-NS_IMPL_RELEASE_INHERITED(KeyframeEffectReadOnly, AnimationEffectReadOnly)
-
-KeyframeEffectReadOnly::KeyframeEffectReadOnly(
-  nsIDocument* aDocument,
-  const Maybe<OwningAnimationTarget>& aTarget,
-  const TimingParams& aTiming,
-  const KeyframeEffectParams& aOptions)
-  : KeyframeEffectReadOnly(aDocument, aTarget,
-                           new AnimationEffectTimingReadOnly(aDocument,
-                                                             aTiming),
-                           aOptions)
-{
-}
-
-KeyframeEffectReadOnly::KeyframeEffectReadOnly(
-  nsIDocument* aDocument,
-  const Maybe<OwningAnimationTarget>& aTarget,
-  AnimationEffectTimingReadOnly* aTiming,
-  const KeyframeEffectParams& aOptions)
-  : AnimationEffectReadOnly(aDocument, aTiming)
-  , mTarget(aTarget)
-  , mEffectOptions(aOptions)
-  , mInEffectOnLastAnimationTimingUpdate(false)
-  , mCumulativeChangeHint(nsChangeHint(0))
-{
-}
-
-JSObject*
-KeyframeEffectReadOnly::WrapObject(JSContext* aCx,
-                                   JS::Handle<JSObject*> aGivenProto)
-{
-  return KeyframeEffectReadOnlyBinding::Wrap(aCx, this, aGivenProto);
-}
-
-IterationCompositeOperation
-KeyframeEffectReadOnly::IterationComposite() const
-{
-  return mEffectOptions.mIterationComposite;
-}
-
-CompositeOperation
-KeyframeEffectReadOnly::Composite() const
-{
-  return mEffectOptions.mComposite;
-}
-
-void
-KeyframeEffectReadOnly::NotifyAnimationTimingUpdated()
-{
-  UpdateTargetRegistration();
-
-  // If the effect is not relevant it will be removed from the target
-  // element's effect set. However, effects not in the effect set
-  // will not be included in the set of candidate effects for running on
-  // the compositor and hence they won't have their compositor status
-  // updated. As a result, we need to make sure we clear their compositor
-  // status here.
-  bool isRelevant = mAnimation && mAnimation->IsRelevant();
-  if (!isRelevant) {
-    ResetIsRunningOnCompositor();
-  }
-
-  // Request restyle if necessary.
-  if (mAnimation && !mProperties.IsEmpty() && HasComputedTimingChanged()) {
-    EffectCompositor::RestyleType restyleType =
-      CanThrottle() ?
-      EffectCompositor::RestyleType::Throttled :
-      EffectCompositor::RestyleType::Standard;
-    RequestRestyle(restyleType);
-  }
-
-  // Detect changes to "in effect" status since we need to recalculate the
-  // animation cascade for this element whenever that changes.
-  // Note that updating mInEffectOnLastAnimationTimingUpdate has to be done
-  // after above CanThrottle() call since the function uses the flag inside it.
-  bool inEffect = IsInEffect();
-  if (inEffect != mInEffectOnLastAnimationTimingUpdate) {
-    MarkCascadeNeedsUpdate();
-    mInEffectOnLastAnimationTimingUpdate = inEffect;
-  }
-
-  // If we're no longer "in effect", our ComposeStyle method will never be
-  // called and we will never have a chance to update mProgressOnLastCompose
-  // and mCurrentIterationOnLastCompose.
-  // We clear them here to ensure that if we later become "in effect" we will
-  // request a restyle (above).
-  if (!inEffect) {
-     mProgressOnLastCompose.SetNull();
-     mCurrentIterationOnLastCompose = 0;
-  }
-}
-
-static bool
-KeyframesEqualIgnoringComputedOffsets(const nsTArray<Keyframe>& aLhs,
-                                      const nsTArray<Keyframe>& aRhs)
-{
-  if (aLhs.Length() != aRhs.Length()) {
-    return false;
-  }
-
-  for (size_t i = 0, len = aLhs.Length(); i < len; ++i) {
-    const Keyframe& a = aLhs[i];
-    const Keyframe& b = aRhs[i];
-    if (a.mOffset != b.mOffset ||
-        a.mTimingFunction != b.mTimingFunction ||
-        a.mPropertyValues != b.mPropertyValues) {
-      return false;
-    }
-  }
-  return true;
-}
-
-// https://drafts.csswg.org/web-animations/#dom-keyframeeffect-setkeyframes
-void
-KeyframeEffectReadOnly::SetKeyframes(JSContext* aContext,
-                                     JS::Handle<JSObject*> aKeyframes,
-                                     ErrorResult& aRv)
-{
-  nsTArray<Keyframe> keyframes =
-    KeyframeUtils::GetKeyframesFromObject(aContext, mDocument, aKeyframes, aRv);
-  if (aRv.Failed()) {
-    return;
-  }
-
-  RefPtr<ComputedStyle> style = GetTargetComputedStyle();
-  SetKeyframes(Move(keyframes), style);
-}
-
-
-void
-KeyframeEffectReadOnly::SetKeyframes(
-  nsTArray<Keyframe>&& aKeyframes,
-  const ComputedStyle* aStyle)
-{
-  if (KeyframesEqualIgnoringComputedOffsets(aKeyframes, mKeyframes)) {
-    return;
-  }
-
-  mKeyframes = Move(aKeyframes);
-  KeyframeUtils::DistributeKeyframes(mKeyframes);
-
-  if (mAnimation && mAnimation->IsRelevant()) {
-    nsNodeUtils::AnimationChanged(mAnimation);
-  }
-
-  // We need to call UpdateProperties() unless the target element doesn't have
-  // style (e.g. the target element is not associated with any document).
-  if (aStyle) {
-    UpdateProperties(aStyle);
-    MaybeUpdateFrameForCompositor();
-  }
-}
-
-const AnimationProperty*
-KeyframeEffectReadOnly::GetEffectiveAnimationOfProperty(
-  nsCSSPropertyID aProperty) const
-{
-  EffectSet* effectSet =
-    EffectSet::GetEffectSet(mTarget->mElement, mTarget->mPseudoType);
-  for (size_t propIdx = 0, propEnd = mProperties.Length();
-       propIdx != propEnd; ++propIdx) {
-    if (aProperty == mProperties[propIdx].mProperty) {
-      const AnimationProperty* result = &mProperties[propIdx];
-      // Skip if there is a property of animation level that is overridden
-      // by !important rules.
-      if (effectSet &&
-          effectSet->PropertiesWithImportantRules()
-            .HasProperty(result->mProperty) &&
-          effectSet->PropertiesForAnimationsLevel()
-            .HasProperty(result->mProperty)) {
-        result = nullptr;
-      }
-      return result;
-    }
-  }
-  return nullptr;
-}
-
-bool
-KeyframeEffectReadOnly::HasAnimationOfProperty(nsCSSPropertyID aProperty) const
-{
-  for (const AnimationProperty& property : mProperties) {
-    if (property.mProperty == aProperty) {
-      return true;
-    }
-  }
-  return false;
-}
-
-#ifdef DEBUG
-bool
-SpecifiedKeyframeArraysAreEqual(const nsTArray<Keyframe>& aA,
-                                const nsTArray<Keyframe>& aB)
-{
-  if (aA.Length() != aB.Length()) {
-    return false;
-  }
-
-  for (size_t i = 0; i < aA.Length(); i++) {
-    const Keyframe& a = aA[i];
-    const Keyframe& b = aB[i];
-    if (a.mOffset         != b.mOffset ||
-        a.mTimingFunction != b.mTimingFunction ||
-        a.mPropertyValues != b.mPropertyValues) {
-      return false;
-    }
-  }
-
-  return true;
-}
-#endif
-
-void
-KeyframeEffectReadOnly::UpdateProperties(const ComputedStyle* aStyle)
-{
-  MOZ_ASSERT(aStyle);
-
-  nsTArray<AnimationProperty> properties = BuildProperties(aStyle);
-
-  // We need to update base styles even if any properties are not changed at all
-  // since base styles might have been changed due to parent style changes, etc.
-  EnsureBaseStyles(aStyle, properties);
-
-  if (mProperties == properties) {
-    return;
-  }
-
-  // Preserve the state of the mIsRunningOnCompositor flag.
-  nsCSSPropertyIDSet runningOnCompositorProperties;
-
-  for (const AnimationProperty& property : mProperties) {
-    if (property.mIsRunningOnCompositor) {
-      runningOnCompositorProperties.AddProperty(property.mProperty);
-    }
-  }
-
-  mProperties = Move(properties);
-  UpdateEffectSet();
-
-  for (AnimationProperty& property : mProperties) {
-    property.mIsRunningOnCompositor =
-      runningOnCompositorProperties.HasProperty(property.mProperty);
-  }
-
-  CalculateCumulativeChangeHint(aStyle);
-
-  MarkCascadeNeedsUpdate();
-
-  RequestRestyle(EffectCompositor::RestyleType::Layer);
-}
-
-
-void
-KeyframeEffectReadOnly::EnsureBaseStyles(
-  const ComputedStyle* aComputedValues,
-  const nsTArray<AnimationProperty>& aProperties)
-{
-  if (!mTarget) {
-    return;
-  }
-
-  mBaseStyleValuesForServo.Clear();
-
-  nsPresContext* presContext =
-    nsContentUtils::GetContextForContent(mTarget->mElement);
-  // If |aProperties| is empty we're not going to dereference |presContext| so
-  // we don't care if it is nullptr.
-  //
-  // We could just return early when |aProperties| is empty and save looking up
-  // the pres context, but that won't save any effort normally since we don't
-  // call this function if we have no keyframes to begin with. Furthermore, the
-  // case where |presContext| is nullptr is so rare (we've only ever seen in
-  // fuzzing, and even then we've never been able to reproduce it reliably)
-  // it's not worth the runtime cost of an extra branch.
-  MOZ_ASSERT(presContext || aProperties.IsEmpty(),
-             "Typically presContext should not be nullptr but if it is"
-             " we should have also failed to calculate the computed values"
-             " passed-in as aProperties");
-
-  RefPtr<ComputedStyle> baseComputedStyle;
-  for (const AnimationProperty& property : aProperties) {
-    EnsureBaseStyle(property,
-                    presContext,
-                    aComputedValues,
-                    baseComputedStyle);
-  }
-}
-
-void
-KeyframeEffectReadOnly::EnsureBaseStyle(
-  const AnimationProperty& aProperty,
-  nsPresContext* aPresContext,
-  const ComputedStyle* aComputedStyle,
- RefPtr<ComputedStyle>& aBaseComputedStyle)
-{
-  bool hasAdditiveValues = false;
-
-  for (const AnimationPropertySegment& segment : aProperty.mSegments) {
-    if (!segment.HasReplaceableValues()) {
-      hasAdditiveValues = true;
-      break;
-    }
-  }
-
-  if (!hasAdditiveValues) {
-    return;
-  }
-
-  if (!aBaseComputedStyle) {
-    Element* animatingElement =
-      EffectCompositor::GetElementToRestyle(mTarget->mElement,
-                                            mTarget->mPseudoType);
-    aBaseComputedStyle = aPresContext->StyleSet()->
-      GetBaseContextForElement(animatingElement, aComputedStyle);
-  }
-  RefPtr<RawServoAnimationValue> baseValue =
-    Servo_ComputedValues_ExtractAnimationValue(aBaseComputedStyle,
-                                               aProperty.mProperty).Consume();
-  mBaseStyleValuesForServo.Put(aProperty.mProperty, baseValue);
-}
-
-void
-KeyframeEffectReadOnly::WillComposeStyle()
-{
-  ComputedTiming computedTiming = GetComputedTiming();
-  mProgressOnLastCompose = computedTiming.mProgress;
-  mCurrentIterationOnLastCompose = computedTiming.mCurrentIteration;
-}
-
-
-void
-KeyframeEffectReadOnly::ComposeStyleRule(
-  RawServoAnimationValueMap& aAnimationValues,
-  const AnimationProperty& aProperty,
-  const AnimationPropertySegment& aSegment,
-  const ComputedTiming& aComputedTiming)
-{
-  Servo_AnimationCompose(&aAnimationValues,
-                         &mBaseStyleValuesForServo,
-                         aProperty.mProperty,
-                         &aSegment,
-                         &aProperty.mSegments.LastElement(),
-                         &aComputedTiming,
-                         mEffectOptions.mIterationComposite);
-}
-
-void
-KeyframeEffectReadOnly::ComposeStyle(
-  RawServoAnimationValueMap& aComposeResult,
-  const nsCSSPropertyIDSet& aPropertiesToSkip)
-{
-  ComputedTiming computedTiming = GetComputedTiming();
-
-  // If the progress is null, we don't have fill data for the current
-  // time so we shouldn't animate.
-  if (computedTiming.mProgress.IsNull()) {
-    return;
-  }
-
-  for (size_t propIdx = 0, propEnd = mProperties.Length();
-       propIdx != propEnd; ++propIdx)
-  {
-    const AnimationProperty& prop = mProperties[propIdx];
-
-    MOZ_ASSERT(prop.mSegments[0].mFromKey == 0.0, "incorrect first from key");
-    MOZ_ASSERT(prop.mSegments[prop.mSegments.Length() - 1].mToKey == 1.0,
-               "incorrect last to key");
-
-    if (aPropertiesToSkip.HasProperty(prop.mProperty)) {
-      continue;
-    }
-
-    MOZ_ASSERT(prop.mSegments.Length() > 0,
-               "property should not be in animations if it has no segments");
-
-    // FIXME: Maybe cache the current segment?
-    const AnimationPropertySegment *segment = prop.mSegments.Elements(),
-                                *segmentEnd = segment + prop.mSegments.Length();
-    while (segment->mToKey <= computedTiming.mProgress.Value()) {
-      MOZ_ASSERT(segment->mFromKey <= segment->mToKey, "incorrect keys");
-      if ((segment+1) == segmentEnd) {
-        break;
-      }
-      ++segment;
-      MOZ_ASSERT(segment->mFromKey == (segment-1)->mToKey, "incorrect keys");
-    }
-    MOZ_ASSERT(segment->mFromKey <= segment->mToKey, "incorrect keys");
-    MOZ_ASSERT(segment >= prop.mSegments.Elements() &&
-               size_t(segment - prop.mSegments.Elements()) <
-                 prop.mSegments.Length(),
-               "out of array bounds");
-
-    ComposeStyleRule(aComposeResult, prop, *segment, computedTiming);
-  }
-
-  // If the animation produces a transform change hint that affects the overflow
-  // region, we need to record the current time to unthrottle the animation
-  // periodically when the animation is being throttled because it's scrolled
-  // out of view.
-  if (HasTransformThatMightAffectOverflow()) {
-    nsPresContext* presContext =
-      nsContentUtils::GetContextForContent(mTarget->mElement);
-    if (presContext) {
-      TimeStamp now = presContext->RefreshDriver()->MostRecentRefresh();
-      EffectSet* effectSet =
-        EffectSet::GetEffectSet(mTarget->mElement, mTarget->mPseudoType);
-      MOZ_ASSERT(effectSet, "ComposeStyle should only be called on an effect "
-                            "that is part of an effect set");
-      effectSet->UpdateLastTransformSyncTime(now);
-    }
-  }
-}
-
-bool
-KeyframeEffectReadOnly::IsRunningOnCompositor() const
-{
-  // We consider animation is running on compositor if there is at least
-  // one property running on compositor.
-  // Animation.IsRunningOnCompotitor will return more fine grained
-  // information in bug 1196114.
-  for (const AnimationProperty& property : mProperties) {
-    if (property.mIsRunningOnCompositor) {
-      return true;
-    }
-  }
-  return false;
-}
-
-void
-KeyframeEffectReadOnly::SetIsRunningOnCompositor(nsCSSPropertyID aProperty,
-                                                 bool aIsRunning)
-{
-  MOZ_ASSERT(nsCSSProps::PropHasFlags(aProperty,
-                                      CSSPropFlags::CanAnimateOnCompositor),
-             "Property being animated on compositor is a recognized "
-             "compositor-animatable property");
-  for (AnimationProperty& property : mProperties) {
-    if (property.mProperty == aProperty) {
-      property.mIsRunningOnCompositor = aIsRunning;
-      // We currently only set a performance warning message when animations
-      // cannot be run on the compositor, so if this animation is running
-      // on the compositor we don't need a message.
-      if (aIsRunning) {
-        property.mPerformanceWarning.reset();
-      }
-      return;
-    }
-  }
-}
-
-void
-KeyframeEffectReadOnly::ResetIsRunningOnCompositor()
-{
-  for (AnimationProperty& property : mProperties) {
-    property.mIsRunningOnCompositor = false;
-  }
-}
-
-static const KeyframeEffectOptions&
-KeyframeEffectOptionsFromUnion(
-  const UnrestrictedDoubleOrKeyframeEffectOptions& aOptions)
-{
-  MOZ_ASSERT(aOptions.IsKeyframeEffectOptions());
-  return aOptions.GetAsKeyframeEffectOptions();
-}
-
-static const KeyframeEffectOptions&
-KeyframeEffectOptionsFromUnion(
-  const UnrestrictedDoubleOrKeyframeAnimationOptions& aOptions)
-{
-  MOZ_ASSERT(aOptions.IsKeyframeAnimationOptions());
-  return aOptions.GetAsKeyframeAnimationOptions();
-}
-
-template <class OptionsType>
-static KeyframeEffectParams
-KeyframeEffectParamsFromUnion(const OptionsType& aOptions,
-                              CallerType aCallerType)
-{
-  KeyframeEffectParams result;
-  if (aOptions.IsUnrestrictedDouble() ||
-      // Ignore iterationComposite if the Web Animations API is not enabled,
-      // then the default value 'Replace' will be used.
-      !nsDocument::IsWebAnimationsEnabled(aCallerType)) {
-    return result;
-  }
-
-  const KeyframeEffectOptions& options =
-    KeyframeEffectOptionsFromUnion(aOptions);
-  result.mIterationComposite = options.mIterationComposite;
-  result.mComposite = options.mComposite;
-  return result;
-}
-
-/* static */ Maybe<OwningAnimationTarget>
-KeyframeEffectReadOnly::ConvertTarget(
-  const Nullable<ElementOrCSSPseudoElement>& aTarget)
-{
-  // Return value optimization.
-  Maybe<OwningAnimationTarget> result;
-
-  if (aTarget.IsNull()) {
-    return result;
-  }
-
-  const ElementOrCSSPseudoElement& target = aTarget.Value();
-  MOZ_ASSERT(target.IsElement() || target.IsCSSPseudoElement(),
-             "Uninitialized target");
-
-  if (target.IsElement()) {
-    result.emplace(&target.GetAsElement());
-  } else {
-    RefPtr<Element> elem = target.GetAsCSSPseudoElement().ParentElement();
-    result.emplace(elem, target.GetAsCSSPseudoElement().GetType());
-  }
-  return result;
-}
-
-template <class KeyframeEffectType, class OptionsType>
-/* static */ already_AddRefed<KeyframeEffectType>
-KeyframeEffectReadOnly::ConstructKeyframeEffect(
-    const GlobalObject& aGlobal,
-    const Nullable<ElementOrCSSPseudoElement>& aTarget,
-    JS::Handle<JSObject*> aKeyframes,
-    const OptionsType& aOptions,
-    ErrorResult& aRv)
-{
-  // We should get the document from `aGlobal` instead of the current Realm
-  // to make this works in Xray case.
-  //
-  // In all non-Xray cases, `aGlobal` matches the current Realm, so this
-  // matches the spec behavior.
-  //
-  // In Xray case, the new objects should be created using the document of
-  // the target global, but KeyframeEffect and KeyframeEffectReadOnly
-  // constructors are called in the caller's compartment to access
-  // `aKeyframes` object.
-  nsIDocument* doc = AnimationUtils::GetDocumentFromGlobal(aGlobal.Get());
-  if (!doc) {
-    aRv.Throw(NS_ERROR_FAILURE);
-    return nullptr;
-  }
-
-  TimingParams timingParams =
-    TimingParams::FromOptionsUnion(aOptions, doc, aRv);
-  if (aRv.Failed()) {
-    return nullptr;
-  }
-
-  KeyframeEffectParams effectOptions =
-    KeyframeEffectParamsFromUnion(aOptions, aGlobal.CallerType());
-
-  Maybe<OwningAnimationTarget> target = ConvertTarget(aTarget);
-  RefPtr<KeyframeEffectType> effect =
-    new KeyframeEffectType(doc, target, timingParams, effectOptions);
-
-  effect->SetKeyframes(aGlobal.Context(), aKeyframes, aRv);
-  if (aRv.Failed()) {
-    return nullptr;
-  }
-
-  return effect.forget();
-}
-
-template<class KeyframeEffectType>
-/* static */ already_AddRefed<KeyframeEffectType>
-KeyframeEffectReadOnly::ConstructKeyframeEffect(const GlobalObject& aGlobal,
-                                                KeyframeEffectReadOnly& aSource,
-                                                ErrorResult& aRv)
-{
-  nsIDocument* doc = AnimationUtils::GetCurrentRealmDocument(aGlobal.Context());
-  if (!doc) {
-    aRv.Throw(NS_ERROR_FAILURE);
-    return nullptr;
-  }
-
-  // Create a new KeyframeEffectReadOnly object with aSource's target,
-  // iteration composite operation, composite operation, and spacing mode.
-  // The constructor creates a new AnimationEffect(ReadOnly) object by
-  // aSource's TimingParams.
-  // Note: we don't need to re-throw exceptions since the value specified on
-  //       aSource's timing object can be assumed valid.
-  RefPtr<KeyframeEffectType> effect =
-    new KeyframeEffectType(doc,
-                           aSource.mTarget,
-                           aSource.SpecifiedTiming(),
-                           aSource.mEffectOptions);
-  // Copy cumulative change hint. mCumulativeChangeHint should be the same as
-  // the source one because both of targets are the same.
-  effect->mCumulativeChangeHint = aSource.mCumulativeChangeHint;
-
-  // Copy aSource's keyframes and animation properties.
-  // Note: We don't call SetKeyframes directly, which might revise the
-  //       computed offsets and rebuild the animation properties.
-  effect->mKeyframes = aSource.mKeyframes;
-  effect->mProperties = aSource.mProperties;
-  return effect.forget();
-}
-
-nsTArray<AnimationProperty>
-KeyframeEffectReadOnly::BuildProperties(const ComputedStyle* aStyle)
-{
-
-  MOZ_ASSERT(aStyle);
-
-  nsTArray<AnimationProperty> result;
-  // If mTarget is null, return an empty property array.
-  if (!mTarget) {
-    return result;
-  }
-
-  // When GetComputedKeyframeValues or GetAnimationPropertiesFromKeyframes
-  // calculate computed values from |mKeyframes|, they could possibly
-  // trigger a subsequent restyle in which we rebuild animations. If that
-  // happens we could find that |mKeyframes| is overwritten while it is
-  // being iterated over. Normally that shouldn't happen but just in case we
-  // make a copy of |mKeyframes| first and iterate over that instead.
-  auto keyframesCopy(mKeyframes);
-
-  result =
-    KeyframeUtils::GetAnimationPropertiesFromKeyframes(
-      keyframesCopy,
-      mTarget->mElement,
-      aStyle,
-      mEffectOptions.mComposite);
-
-#ifdef DEBUG
-  MOZ_ASSERT(SpecifiedKeyframeArraysAreEqual(mKeyframes, keyframesCopy),
-             "Apart from the computed offset members, the keyframes array"
-             " should not be modified");
-#endif
-
-  mKeyframes.SwapElements(keyframesCopy);
-  return result;
-}
-
-void
-KeyframeEffectReadOnly::UpdateTargetRegistration()
-{
-  if (!mTarget) {
-    return;
-  }
-
-  bool isRelevant = mAnimation && mAnimation->IsRelevant();
-
-  // Animation::IsRelevant() returns a cached value. It only updates when
-  // something calls Animation::UpdateRelevance. Whenever our timing changes,
-  // we should be notifying our Animation before calling this, so
-  // Animation::IsRelevant() should be up-to-date by the time we get here.
-  MOZ_ASSERT(isRelevant == IsCurrent() || IsInEffect(),
-             "Out of date Animation::IsRelevant value");
-
-  if (isRelevant && !mInEffectSet) {
-    EffectSet* effectSet =
-      EffectSet::GetOrCreateEffectSet(mTarget->mElement, mTarget->mPseudoType);
-    effectSet->AddEffect(*this);
-    mInEffectSet = true;
-    UpdateEffectSet(effectSet);
-    nsIFrame* f = GetPrimaryFrame();
-    while (f) {
-      f->MarkNeedsDisplayItemRebuild();
-      f = f->GetNextContinuation();
-    }
-  } else if (!isRelevant && mInEffectSet) {
-    UnregisterTarget();
-  }
-}
-
-void
-KeyframeEffectReadOnly::UnregisterTarget()
-{
-  if (!mInEffectSet) {
-    return;
-  }
-
-  EffectSet* effectSet =
-    EffectSet::GetEffectSet(mTarget->mElement, mTarget->mPseudoType);
-  MOZ_ASSERT(effectSet, "If mInEffectSet is true, there must be an EffectSet"
-                        " on the target element");
-  mInEffectSet = false;
-  if (effectSet) {
-    effectSet->RemoveEffect(*this);
-
-    if (effectSet->IsEmpty()) {
-      EffectSet::DestroyEffectSet(mTarget->mElement, mTarget->mPseudoType);
-    }
-  }
-  nsIFrame* f = GetPrimaryFrame();
-  while (f) {
-    f->MarkNeedsDisplayItemRebuild();
-    f = f->GetNextContinuation();
-  }
-}
-
-void
-KeyframeEffectReadOnly::RequestRestyle(
-  EffectCompositor::RestyleType aRestyleType)
-{
-   if (!mTarget) {
-    return;
-  }
-  nsPresContext* presContext = nsContentUtils::GetContextForContent(mTarget->mElement);
-  if (presContext && mAnimation) {
-    presContext->EffectCompositor()->
-      RequestRestyle(mTarget->mElement, mTarget->mPseudoType,
-                     aRestyleType, mAnimation->CascadeLevel());
-  }
-}
-
-already_AddRefed<ComputedStyle>
-KeyframeEffectReadOnly::GetTargetComputedStyle()
-{
-  if (!GetRenderedDocument()) {
-    return nullptr;
-  }
-
-  MOZ_ASSERT(mTarget,
-             "Should only have a document when we have a target element");
-
-  nsAtom* pseudo = mTarget->mPseudoType < CSSPseudoElementType::Count
-                    ? nsCSSPseudoElements::GetPseudoAtom(mTarget->mPseudoType)
-                    : nullptr;
-
-  OwningAnimationTarget kungfuDeathGrip(mTarget->mElement,
-                                        mTarget->mPseudoType);
-
-  return nsComputedDOMStyle::GetComputedStyle(mTarget->mElement, pseudo);
-}
-
-#ifdef DEBUG
-void
-DumpAnimationProperties(nsTArray<AnimationProperty>& aAnimationProperties)
-{
-  for (auto& p : aAnimationProperties) {
-    printf("%s\n", nsCSSProps::GetStringValue(p.mProperty).get());
-    for (auto& s : p.mSegments) {
-      nsString fromValue, toValue;
-      s.mFromValue.SerializeSpecifiedValue(p.mProperty, fromValue);
-      s.mToValue.SerializeSpecifiedValue(p.mProperty, toValue);
-      printf("  %f..%f: %s..%s\n", s.mFromKey, s.mToKey,
-             NS_ConvertUTF16toUTF8(fromValue).get(),
-             NS_ConvertUTF16toUTF8(toValue).get());
-    }
-  }
-}
-#endif
-
-/* static */ already_AddRefed<KeyframeEffectReadOnly>
-KeyframeEffectReadOnly::Constructor(
-    const GlobalObject& aGlobal,
-    const Nullable<ElementOrCSSPseudoElement>& aTarget,
-    JS::Handle<JSObject*> aKeyframes,
-    const UnrestrictedDoubleOrKeyframeEffectOptions& aOptions,
-    ErrorResult& aRv)
-{
-  return ConstructKeyframeEffect<KeyframeEffectReadOnly>(aGlobal, aTarget,
-                                                         aKeyframes, aOptions,
-                                                         aRv);
-}
-
-/* static */ already_AddRefed<KeyframeEffectReadOnly>
-KeyframeEffectReadOnly::Constructor(const GlobalObject& aGlobal,
-                                    KeyframeEffectReadOnly& aSource,
-                                    ErrorResult& aRv)
-{
-  return ConstructKeyframeEffect<KeyframeEffectReadOnly>(aGlobal, aSource, aRv);
-}
-
-void
-KeyframeEffectReadOnly::GetTarget(
-    Nullable<OwningElementOrCSSPseudoElement>& aRv) const
-{
-  if (!mTarget) {
-    aRv.SetNull();
-    return;
-  }
-
-  switch (mTarget->mPseudoType) {
-    case CSSPseudoElementType::before:
-    case CSSPseudoElementType::after:
-      aRv.SetValue().SetAsCSSPseudoElement() =
-        CSSPseudoElement::GetCSSPseudoElement(mTarget->mElement,
-                                              mTarget->mPseudoType);
-      break;
-
-    case CSSPseudoElementType::NotPseudo:
-      aRv.SetValue().SetAsElement() = mTarget->mElement;
-      break;
-
-    default:
-      NS_NOTREACHED("Animation of unsupported pseudo-type");
-      aRv.SetNull();
-  }
-}
-
-static void
-CreatePropertyValue(nsCSSPropertyID aProperty,
-                    float aOffset,
-                    const Maybe<ComputedTimingFunction>& aTimingFunction,
-                    const AnimationValue& aValue,
-                    dom::CompositeOperation aComposite,
-                    AnimationPropertyValueDetails& aResult)
-{
-  aResult.mOffset = aOffset;
-
-  if (!aValue.IsNull()) {
-    nsString stringValue;
-    aValue.SerializeSpecifiedValue(aProperty, stringValue);
-    aResult.mValue.Construct(stringValue);
-  }
-
-  if (aTimingFunction) {
-    aResult.mEasing.Construct();
-    aTimingFunction->AppendToString(aResult.mEasing.Value());
-  } else {
-    aResult.mEasing.Construct(NS_LITERAL_STRING("linear"));
-  }
-
-  aResult.mComposite = aComposite;
-}
-
-void
-KeyframeEffectReadOnly::GetProperties(
-    nsTArray<AnimationPropertyDetails>& aProperties,
-    ErrorResult& aRv) const
-{
-  for (const AnimationProperty& property : mProperties) {
-    AnimationPropertyDetails propertyDetails;
-    propertyDetails.mProperty =
-      NS_ConvertASCIItoUTF16(nsCSSProps::GetStringValue(property.mProperty));
-    propertyDetails.mRunningOnCompositor = property.mIsRunningOnCompositor;
-
-    nsAutoString localizedString;
-    if (property.mPerformanceWarning &&
-        property.mPerformanceWarning->ToLocalizedString(localizedString)) {
-      propertyDetails.mWarning.Construct(localizedString);
-    }
-
-    if (!propertyDetails.mValues.SetCapacity(property.mSegments.Length(),
-                                             mozilla::fallible)) {
-      aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
-      return;
-    }
-
-    for (size_t segmentIdx = 0, segmentLen = property.mSegments.Length();
-         segmentIdx < segmentLen;
-         segmentIdx++)
-    {
-      const AnimationPropertySegment& segment = property.mSegments[segmentIdx];
-
-      binding_detail::FastAnimationPropertyValueDetails fromValue;
-      CreatePropertyValue(property.mProperty, segment.mFromKey,
-                          segment.mTimingFunction, segment.mFromValue,
-                          segment.mFromComposite, fromValue);
-      // We don't apply timing functions for zero-length segments, so
-      // don't return one here.
-      if (segment.mFromKey == segment.mToKey) {
-        fromValue.mEasing.Reset();
-      }
-      // The following won't fail since we have already allocated the capacity
-      // above.
-      propertyDetails.mValues.AppendElement(fromValue, mozilla::fallible);
-
-      // Normally we can ignore the to-value for this segment since it is
-      // identical to the from-value from the next segment. However, we need
-      // to add it if either:
-      // a) this is the last segment, or
-      // b) the next segment's from-value differs.
-      if (segmentIdx == segmentLen - 1 ||
-          property.mSegments[segmentIdx + 1].mFromValue != segment.mToValue) {
-        binding_detail::FastAnimationPropertyValueDetails toValue;
-        CreatePropertyValue(property.mProperty, segment.mToKey,
-                            Nothing(), segment.mToValue,
-                            segment.mToComposite, toValue);
-        // It doesn't really make sense to have a timing function on the
-        // last property value or before a sudden jump so we just drop the
-        // easing property altogether.
-        toValue.mEasing.Reset();
-        propertyDetails.mValues.AppendElement(toValue, mozilla::fallible);
-      }
-    }
-
-    aProperties.AppendElement(propertyDetails);
-  }
-}
-
-void
-KeyframeEffectReadOnly::GetKeyframes(JSContext*& aCx,
-                                     nsTArray<JSObject*>& aResult,
-                                     ErrorResult& aRv)
-{
-  MOZ_ASSERT(aResult.IsEmpty());
-  MOZ_ASSERT(!aRv.Failed());
-
-  if (!aResult.SetCapacity(mKeyframes.Length(), mozilla::fallible)) {
-    aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
-    return;
-  }
-
-  bool isCSSAnimation = mAnimation && mAnimation->AsCSSAnimation();
-
-  // For Servo, when we have CSS Animation @keyframes with variables, we convert
-  // shorthands to longhands if needed, and store a reference to the unparsed
-  // value. When it comes time to serialize, however, what do you serialize for
-  // a longhand that comes from a variable reference in a shorthand? Servo says,
-  // "an empty string" which is not particularly helpful.
-  //
-  // We should just store shorthands as-is (bug 1391537) and then return the
-  // variable references, but for now, since we don't do that, and in order to
-  // be consistent with Gecko, we just expand the variables (assuming we have
-  // enough context to do so). For that we need to grab the ComputedStyle so we
-  // know what custom property values to provide.
-  RefPtr<ComputedStyle> computedStyle;
-  if (isCSSAnimation) {
-    // The following will flush style but that's ok since if you update
-    // a variable's computed value, you expect to see that updated value in the
-    // result of getKeyframes().
-    //
-    // If we don't have a target, the following will return null. In that case
-    // we might end up returning variables as-is or empty string. That should be
-    // acceptable however, since such a case is rare and this is only
-    // short-term (and unshipped) behavior until bug 1391537 is fixed.
-    computedStyle = GetTargetComputedStyle();
-  }
-
-  for (const Keyframe& keyframe : mKeyframes) {
-    // Set up a dictionary object for the explicit members
-    BaseComputedKeyframe keyframeDict;
-    if (keyframe.mOffset) {
-      keyframeDict.mOffset.SetValue(keyframe.mOffset.value());
-    }
-    MOZ_ASSERT(keyframe.mComputedOffset != Keyframe::kComputedOffsetNotSet,
-               "Invalid computed offset");
-    keyframeDict.mComputedOffset.Construct(keyframe.mComputedOffset);
-    if (keyframe.mTimingFunction) {
-      keyframeDict.mEasing.Truncate();
-      keyframe.mTimingFunction.ref().AppendToString(keyframeDict.mEasing);
-    } // else if null, leave easing as its default "linear".
-
-    if (keyframe.mComposite) {
-      keyframeDict.mComposite.SetValue(keyframe.mComposite.value());
-    }
-
-    JS::Rooted<JS::Value> keyframeJSValue(aCx);
-    if (!ToJSValue(aCx, keyframeDict, &keyframeJSValue)) {
-      aRv.Throw(NS_ERROR_FAILURE);
-      return;
-    }
-
-    RefPtr<RawServoDeclarationBlock> customProperties;
-    // A workaround for CSS Animations in servo backend, custom properties in
-    // keyframe are stored in a servo's declaration block. Find the declaration
-    // block to resolve CSS variables in the keyframe.
-    // This workaround will be solved by bug 1391537.
-    if (isCSSAnimation) {
-      for (const PropertyValuePair& propertyValue : keyframe.mPropertyValues) {
-        if (propertyValue.mProperty ==
-              nsCSSPropertyID::eCSSPropertyExtra_variable) {
-          customProperties = propertyValue.mServoDeclarationBlock;
-          break;
-        }
-      }
-    }
-
-    JS::Rooted<JSObject*> keyframeObject(aCx, &keyframeJSValue.toObject());
-    for (const PropertyValuePair& propertyValue : keyframe.mPropertyValues) {
-      nsAutoString stringValue;
-      // Don't serialize the custom properties for this keyframe.
-      if (propertyValue.mProperty ==
-            nsCSSPropertyID::eCSSPropertyExtra_variable) {
-        continue;
-      }
-      if (propertyValue.mServoDeclarationBlock) {
-        Servo_DeclarationBlock_SerializeOneValue(
-          propertyValue.mServoDeclarationBlock,
-          propertyValue.mProperty,
-          &stringValue,
-          computedStyle,
-          customProperties);
-      } else {
-        RawServoAnimationValue* value =
-          mBaseStyleValuesForServo.GetWeak(propertyValue.mProperty);
-
-        if (value) {
-          Servo_AnimationValue_Serialize(value,
-                                         propertyValue.mProperty,
-                                         &stringValue);
-        }
-      }
-
-      const char* name = nsCSSProps::PropertyIDLName(propertyValue.mProperty);
-      JS::Rooted<JS::Value> value(aCx);
-      if (!ToJSValue(aCx, stringValue, &value) ||
-          !JS_DefineProperty(aCx, keyframeObject, name, value,
-                             JSPROP_ENUMERATE)) {
-        aRv.Throw(NS_ERROR_FAILURE);
-        return;
-      }
-    }
-
-    aResult.AppendElement(keyframeObject);
-  }
-}
-
-/* static */ const TimeDuration
-KeyframeEffectReadOnly::OverflowRegionRefreshInterval()
-{
-  // The amount of time we can wait between updating throttled animations
-  // on the main thread that influence the overflow region.
-  static const TimeDuration kOverflowRegionRefreshInterval =
-    TimeDuration::FromMilliseconds(200);
-
-  return kOverflowRegionRefreshInterval;
-}
-
-bool
-KeyframeEffectReadOnly::CanThrottle() const
-{
-  // Unthrottle if we are not in effect or current. This will be the case when
-  // our owning animation has finished, is idle, or when we are in the delay
-  // phase (but without a backwards fill). In each case the computed progress
-  // value produced on each tick will be the same so we will skip requesting
-  // unnecessary restyles in NotifyAnimationTimingUpdated. Any calls we *do* get
-  // here will be because of a change in state (e.g. we are newly finished or
-  // newly no longer in effect) in which case we shouldn't throttle the sample.
-  if (!IsInEffect() || !IsCurrent()) {
-    return false;
-  }
-
-  nsIFrame* frame = GetStyleFrame();
-  if (!frame) {
-    // There are two possible cases here.
-    // a) No target element
-    // b) The target element has no frame, e.g. because it is in a display:none
-    //    subtree.
-    // In either case we can throttle the animation because there is no
-    // need to update on the main thread.
-    return true;
-  }
-
-  // Unless we are newly in-effect, we can throttle the animation if the
-  // animation is paint only and the target frame is out of view or the document
-  // is in background tabs.
-  if (mInEffectOnLastAnimationTimingUpdate && CanIgnoreIfNotVisible()) {
-    nsIPresShell* presShell = GetPresShell();
-    if (presShell && !presShell->IsActive()) {
-      return true;
-    }
-
-    const bool isVisibilityHidden =
-      !frame->IsVisibleOrMayHaveVisibleDescendants();
-    if ((isVisibilityHidden && !HasVisibilityChange()) ||
-        frame->IsScrolledOutOfView()) {
-      // If there are transform change hints, unthrottle the animation
-      // periodically since it might affect the overflow region.
-      if (HasTransformThatMightAffectOverflow()) {
-        // Don't throttle finite transform animations since the animation might
-        // suddenly come into view and if it was throttled it will be
-        // out-of-sync.
-        if (HasFiniteActiveDuration()) {
-          return false;
-        }
-
-        return isVisibilityHidden
-          ? CanThrottleTransformChangesInScrollable(*frame)
-          : CanThrottleTransformChanges(*frame);
-      }
-      return true;
-    }
-  }
-
-  // First we need to check layer generation and transform overflow
-  // prior to the property.mIsRunningOnCompositor check because we should
-  // occasionally unthrottle these animations even if the animations are
-  // already running on compositor.
-  for (const LayerAnimationInfo::Record& record :
-        LayerAnimationInfo::sRecords) {
-    // Skip properties that are overridden by !important rules.
-    // (GetEffectiveAnimationOfProperty, as called by
-    // HasEffectiveAnimationOfProperty, only returns a property which is
-    // neither overridden by !important rules nor overridden by other
-    // animation.)
-    if (!HasEffectiveAnimationOfProperty(record.mProperty)) {
-      continue;
-    }
-
-    EffectSet* effectSet = EffectSet::GetEffectSet(mTarget->mElement,
-                                                   mTarget->mPseudoType);
-    MOZ_ASSERT(effectSet, "CanThrottle should be called on an effect "
-                          "associated with a target element");
-    // Note that AnimationInfo::GetGenarationFromFrame() is supposed to work
-    // with the primary frame instead of the style frame.
-    Maybe<uint64_t> generation = layers::AnimationInfo::GetGenerationFromFrame(
-        GetPrimaryFrame(), record.mLayerType);
-    // Unthrottle if the animation needs to be brought up to date
-    if (!generation || effectSet->GetAnimationGeneration() != *generation) {
-      return false;
-    }
-
-    // If this is a transform animation that affects the overflow region,
-    // we should unthrottle the animation periodically.
-    if (HasTransformThatMightAffectOverflow() &&
-        !CanThrottleTransformChangesInScrollable(*frame)) {
-      return false;
-    }
-  }
-
-  for (const AnimationProperty& property : mProperties) {
-    if (!property.mIsRunningOnCompositor) {
-      return false;
-    }
-  }
-
-  return true;
-}
-
-bool
-KeyframeEffectReadOnly::CanThrottleTransformChanges(const nsIFrame& aFrame) const
-{
-  TimeStamp now = aFrame.PresContext()->RefreshDriver()->MostRecentRefresh();
-
-  EffectSet* effectSet = EffectSet::GetEffectSet(mTarget->mElement,
-                                                 mTarget->mPseudoType);
-  MOZ_ASSERT(effectSet, "CanThrottleTransformChanges is expected to be called"
-                        " on an effect in an effect set");
-  MOZ_ASSERT(mAnimation, "CanThrottleTransformChanges is expected to be called"
-                         " on an effect with a parent animation");
-  TimeStamp lastSyncTime = effectSet->LastTransformSyncTime();
-  // If this animation can cause overflow, we can throttle some of the ticks.
-  return (!lastSyncTime.IsNull() &&
-    (now - lastSyncTime) < OverflowRegionRefreshInterval());
-}
-
-bool
-KeyframeEffectReadOnly::CanThrottleTransformChangesInScrollable(nsIFrame& aFrame) const
-{
-  // If the target element is not associated with any documents, we don't care
-  // it.
-  nsIDocument* doc = GetRenderedDocument();
-  if (!doc) {
-    return true;
-  }
-
-  bool hasIntersectionObservers = doc->HasIntersectionObservers();
-
-  // If we know that the animation cannot cause overflow,
-  // we can just disable flushes for this animation.
-
-  // If we don't show scrollbars and have no intersection observers, we don't
-  // care about overflow.
-  if (LookAndFeel::GetInt(LookAndFeel::eIntID_ShowHideScrollbars) == 0 &&
-      !hasIntersectionObservers) {
-    return true;
-  }
-
-  if (CanThrottleTransformChanges(aFrame)) {
-    return true;
-  }
-
-  // If we have any intersection observers, we unthrottle this transform
-  // animation periodically.
-  if (hasIntersectionObservers) {
-    return false;
-  }
-
-  // If the nearest scrollable ancestor has overflow:hidden,
-  // we don't care about overflow.
-  nsIScrollableFrame* scrollable =
-    nsLayoutUtils::GetNearestScrollableFrame(&aFrame);
-  if (!scrollable) {
-    return true;
-  }
-
-  ScrollbarStyles ss = scrollable->GetScrollbarStyles();
-  if (ss.mVertical == NS_STYLE_OVERFLOW_HIDDEN &&
-      ss.mHorizontal == NS_STYLE_OVERFLOW_HIDDEN &&
-      scrollable->GetLogicalScrollPosition() == nsPoint(0, 0)) {
-    return true;
-  }
-
-  return false;
-}
-
-nsIFrame*
-KeyframeEffectReadOnly::GetStyleFrame() const
-{
-  nsIFrame* frame = GetPrimaryFrame();
-  if (!frame) {
-    return nullptr;
-  }
-
-  return nsLayoutUtils::GetStyleFrame(frame);
-}
-
-nsIFrame*
-KeyframeEffectReadOnly::GetPrimaryFrame() const
-{
-  nsIFrame* frame = nullptr;
-  if (!mTarget) {
-    return frame;
-  }
-
-  if (mTarget->mPseudoType == CSSPseudoElementType::before) {
-    frame = nsLayoutUtils::GetBeforeFrame(mTarget->mElement);
-  } else if (mTarget->mPseudoType == CSSPseudoElementType::after) {
-    frame = nsLayoutUtils::GetAfterFrame(mTarget->mElement);
-  } else {
-    frame = mTarget->mElement->GetPrimaryFrame();
-    MOZ_ASSERT(mTarget->mPseudoType == CSSPseudoElementType::NotPseudo,
-               "unknown mTarget->mPseudoType");
-  }
-
-  return frame;
-}
-
-nsIDocument*
-KeyframeEffectReadOnly::GetRenderedDocument() const
-{
-  if (!mTarget) {
-    return nullptr;
-  }
-  return mTarget->mElement->GetComposedDoc();
-}
-
-nsIPresShell*
-KeyframeEffectReadOnly::GetPresShell() const
-{
-  nsIDocument* doc = GetRenderedDocument();
-  if (!doc) {
-    return nullptr;
-  }
-  return doc->GetShell();
-}
-
-/* static */ bool
-KeyframeEffectReadOnly::IsGeometricProperty(
-  const nsCSSPropertyID aProperty)
-{
-  MOZ_ASSERT(!nsCSSProps::IsShorthand(aProperty),
-             "Property should be a longhand property");
-
-  switch (aProperty) {
-    case eCSSProperty_bottom:
-    case eCSSProperty_height:
-    case eCSSProperty_left:
-    case eCSSProperty_margin_bottom:
-    case eCSSProperty_margin_left:
-    case eCSSProperty_margin_right:
-    case eCSSProperty_margin_top:
-    case eCSSProperty_padding_bottom:
-    case eCSSProperty_padding_left:
-    case eCSSProperty_padding_right:
-    case eCSSProperty_padding_top:
-    case eCSSProperty_right:
-    case eCSSProperty_top:
-    case eCSSProperty_width:
-      return true;
-    default:
-      return false;
-  }
-}
-
-/* static */ bool
-KeyframeEffectReadOnly::CanAnimateTransformOnCompositor(
-  const nsIFrame* aFrame,
-  AnimationPerformanceWarning::Type& aPerformanceWarning)
-{
-  // Disallow OMTA for preserve-3d transform. Note that we check the style property
-  // rather than Extend3DContext() since that can recurse back into this function
-  // via HasOpacity(). See bug 779598.
-  if (aFrame->Combines3DTransformWithAncestors() ||
-      aFrame->StyleDisplay()->mTransformStyle == NS_STYLE_TRANSFORM_STYLE_PRESERVE_3D) {
-    aPerformanceWarning = AnimationPerformanceWarning::Type::TransformPreserve3D;
-    return false;
-  }
-  // Note that testing BackfaceIsHidden() is not a sufficient test for
-  // what we need for animating backface-visibility correctly if we
-  // remove the above test for Extend3DContext(); that would require
-  // looking at backface-visibility on descendants as well. See bug 1186204.
-  if (aFrame->BackfaceIsHidden()) {
-    aPerformanceWarning =
-      AnimationPerformanceWarning::Type::TransformBackfaceVisibilityHidden;
-    return false;
-  }
-  // Async 'transform' animations of aFrames with SVG transforms is not
-  // supported.  See bug 779599.
-  if (aFrame->IsSVGTransformed()) {
-    aPerformanceWarning = AnimationPerformanceWarning::Type::TransformSVG;
-    return false;
-  }
-
-  return true;
-}
-
-bool
-KeyframeEffectReadOnly::ShouldBlockAsyncTransformAnimations(
-  const nsIFrame* aFrame,
-  AnimationPerformanceWarning::Type& aPerformanceWarning) const
-{
-  EffectSet* effectSet =
-    EffectSet::GetEffectSet(mTarget->mElement, mTarget->mPseudoType);
-  for (const AnimationProperty& property : mProperties) {
-    // If there is a property for animations level that is overridden by
-    // !important rules, it should not block other animations from running
-    // on the compositor.
-    // NOTE: We don't currently check for !important rules for properties that
-    // don't run on the compositor. As result such properties (e.g. margin-left)
-    // can still block async animations even if they are overridden by
-    // !important rules.
-    if (effectSet &&
-        effectSet->PropertiesWithImportantRules()
-          .HasProperty(property.mProperty) &&
-        effectSet->PropertiesForAnimationsLevel()
-          .HasProperty(property.mProperty)) {
-      continue;
-    }
-    // Check for geometric properties
-    if (IsGeometricProperty(property.mProperty)) {
-      aPerformanceWarning =
-        AnimationPerformanceWarning::Type::TransformWithGeometricProperties;
-      return true;
-    }
-
-    // Check for unsupported transform animations
-    if (property.mProperty == eCSSProperty_transform) {
-      if (!CanAnimateTransformOnCompositor(aFrame,
-                                           aPerformanceWarning)) {
-        return true;
-      }
-    }
-  }
-
-  // XXX cku temporarily disable async-animation when this frame has any
-  // individual transforms before bug 1425837 been fixed.
-  if (aFrame->StyleDisplay()->HasIndividualTransform()) {
-    return true;
-  }
-
-  return false;
-}
-
-bool
-KeyframeEffectReadOnly::HasGeometricProperties() const
-{
-  for (const AnimationProperty& property : mProperties) {
-    if (IsGeometricProperty(property.mProperty)) {
-      return true;
-    }
-  }
-
-  return false;
-}
-
-void
-KeyframeEffectReadOnly::SetPerformanceWarning(
-  nsCSSPropertyID aProperty,
-  const AnimationPerformanceWarning& aWarning)
-{
-  for (AnimationProperty& property : mProperties) {
-    if (property.mProperty == aProperty &&
-        (!property.mPerformanceWarning ||
-         *property.mPerformanceWarning != aWarning)) {
-      property.mPerformanceWarning = Some(aWarning);
-
-      nsAutoString localizedString;
-      if (nsLayoutUtils::IsAnimationLoggingEnabled() &&
-          property.mPerformanceWarning->ToLocalizedString(localizedString)) {
-        nsAutoCString logMessage = NS_ConvertUTF16toUTF8(localizedString);
-        AnimationUtils::LogAsyncAnimationFailure(logMessage, mTarget->mElement);
-      }
-      return;
-    }
-  }
-}
-
-
-already_AddRefed<ComputedStyle>
-KeyframeEffectReadOnly::CreateComputedStyleForAnimationValue(
-  nsCSSPropertyID aProperty,
-  const AnimationValue& aValue,
-  nsPresContext* aPresContext,
-  const ComputedStyle* aBaseComputedStyle)
-{
-  MOZ_ASSERT(aBaseComputedStyle,
-             "CreateComputedStyleForAnimationValue needs to be called "
-             "with a valid ComputedStyle");
-
-  ServoStyleSet* styleSet = aPresContext->StyleSet();
-  Element* elementForResolve =
-    EffectCompositor::GetElementToRestyle(mTarget->mElement,
-                                          mTarget->mPseudoType);
-  MOZ_ASSERT(elementForResolve, "The target element shouldn't be null");
-  return styleSet->ResolveServoStyleByAddingAnimation(elementForResolve,
-                                                      aBaseComputedStyle,
-                                                      aValue.mServo);
-}
-
-void
-KeyframeEffectReadOnly::CalculateCumulativeChangeHint(const ComputedStyle* aComputedStyle)
-{
-  mCumulativeChangeHint = nsChangeHint(0);
-
-  nsPresContext* presContext =
-    nsContentUtils::GetContextForContent(mTarget->mElement);
-  if (!presContext) {
-    // Change hints make no sense if we're not rendered.
-    //
-    // Actually, we cannot even post them anywhere.
-    return;
-  }
-
-  for (const AnimationProperty& property : mProperties) {
-    // For opacity property we don't produce any change hints that are not
-    // included in nsChangeHint_Hints_CanIgnoreIfNotVisible so we can throttle
-    // opacity animations regardless of the change they produce.  This
-    // optimization is particularly important since it allows us to throttle
-    // opacity animations with missing 0%/100% keyframes.
-    if (property.mProperty == eCSSProperty_opacity) {
-      continue;
-    }
-
-    for (const AnimationPropertySegment& segment : property.mSegments) {
-      // In case composite operation is not 'replace' or value is null,
-      // we can't throttle animations which will not cause any layout changes
-      // on invisible elements because we can't calculate the change hint for
-      // such properties until we compose it.
-      if (!segment.HasReplaceableValues()) {
-        mCumulativeChangeHint = ~nsChangeHint_Hints_CanIgnoreIfNotVisible;
-        return;
-      }
-      RefPtr<ComputedStyle> fromContext =
-        CreateComputedStyleForAnimationValue(property.mProperty,
-                                             segment.mFromValue,
-                                             presContext,
-                                             aComputedStyle);
-      if (!fromContext) {
-        mCumulativeChangeHint = ~nsChangeHint_Hints_CanIgnoreIfNotVisible;
-        return;
-      }
-
-      RefPtr<ComputedStyle> toContext =
-        CreateComputedStyleForAnimationValue(property.mProperty,
-                                             segment.mToValue,
-                                             presContext,
-                                             aComputedStyle);
-      if (!toContext) {
-        mCumulativeChangeHint = ~nsChangeHint_Hints_CanIgnoreIfNotVisible;
-        return;
-      }
-
-      uint32_t equalStructs = 0;
-      nsChangeHint changeHint =
-        fromContext->CalcStyleDifference(toContext, &equalStructs);
-
-      mCumulativeChangeHint |= changeHint;
-    }
-  }
-}
-
-void
-KeyframeEffectReadOnly::SetAnimation(Animation* aAnimation)
-{
-  if (mAnimation == aAnimation) {
-    return;
-  }
-
-  // Restyle for the old animation.
-  RequestRestyle(EffectCompositor::RestyleType::Layer);
-
-  mAnimation = aAnimation;
-
-  // The order of these function calls is important:
-  // NotifyAnimationTimingUpdated() need the updated mIsRelevant flag to check
-  // if it should create the effectSet or not, and MarkCascadeNeedsUpdate()
-  // needs a valid effectSet, so we should call them in this order.
-  if (mAnimation) {
-    mAnimation->UpdateRelevance();
-  }
-  NotifyAnimationTimingUpdated();
-  if (mAnimation) {
-    MarkCascadeNeedsUpdate();
-  }
-}
-
-bool
-KeyframeEffectReadOnly::CanIgnoreIfNotVisible() const
-{
-  if (!AnimationUtils::IsOffscreenThrottlingEnabled()) {
-    return false;
-  }
-
-  // FIXME: For further sophisticated optimization we need to check
-  // change hint on the segment corresponding to computedTiming.progress.
-  return NS_IsHintSubset(
-    mCumulativeChangeHint, nsChangeHint_Hints_CanIgnoreIfNotVisible);
-}
-
-void
-KeyframeEffectReadOnly::MaybeUpdateFrameForCompositor()
-{
-  nsIFrame* frame = GetStyleFrame();
-  if (!frame) {
-    return;
-  }
-
-  // FIXME: Bug 1272495: If this effect does not win in the cascade, the
-  // NS_FRAME_MAY_BE_TRANSFORMED flag should be removed when the animation
-  // will be removed from effect set or the transform keyframes are removed
-  // by setKeyframes. The latter case will be hard to solve though.
-  for (const AnimationProperty& property : mProperties) {
-    if (property.mProperty == eCSSProperty_transform) {
-      frame->AddStateBits(NS_FRAME_MAY_BE_TRANSFORMED);
-      return;
-    }
-  }
-}
-
-void
-KeyframeEffectReadOnly::MarkCascadeNeedsUpdate()
-{
-  if (!mTarget) {
-    return;
-  }
-
-  EffectSet* effectSet = EffectSet::GetEffectSet(mTarget->mElement,
-                                                 mTarget->mPseudoType);
-  if (!effectSet) {
-    return;
-  }
-  effectSet->MarkCascadeNeedsUpdate();
-}
-
-/* static */ bool
-KeyframeEffectReadOnly::HasComputedTimingChanged(
-  const ComputedTiming& aComputedTiming,
-  IterationCompositeOperation aIterationComposite,
-  const Nullable<double>& aProgressOnLastCompose,
-  uint64_t aCurrentIterationOnLastCompose)
-{
-  // Typically we don't need to request a restyle if the progress hasn't
-  // changed since the last call to ComposeStyle. The one exception is if the
-  // iteration composite mode is 'accumulate' and the current iteration has
-  // changed, since that will often produce a different result.
-  return aComputedTiming.mProgress != aProgressOnLastCompose ||
-         (aIterationComposite == IterationCompositeOperation::Accumulate &&
-          aComputedTiming.mCurrentIteration != aCurrentIterationOnLastCompose);
-}
-
-bool
-KeyframeEffectReadOnly::HasComputedTimingChanged() const
-{
-  ComputedTiming computedTiming = GetComputedTiming();
-  return HasComputedTimingChanged(computedTiming,
-                                  mEffectOptions.mIterationComposite,
-                                  mProgressOnLastCompose,
-                                  mCurrentIterationOnLastCompose);
-}
-
-bool
-KeyframeEffectReadOnly::ContainsAnimatedScale(const nsIFrame* aFrame) const
-{
-  if (!IsCurrent()) {
-    return false;
-  }
-
-  for (const AnimationProperty& prop : mProperties) {
-    if (prop.mProperty != eCSSProperty_transform) {
-      continue;
-    }
-
-    AnimationValue baseStyle = BaseStyle(prop.mProperty);
-    if (baseStyle.IsNull()) {
-      // If we failed to get the base style, we consider it has scale value
-      // here just to be safe.
-      return true;
-    }
-    gfx::Size size = baseStyle.GetScaleValue(aFrame);
-    if (size != gfx::Size(1.0f, 1.0f)) {
-      return true;
-    }
-
-    // This is actually overestimate because there are some cases that combining
-    // the base value and from/to value produces 1:1 scale. But it doesn't
-    // really matter.
-    for (const AnimationPropertySegment& segment : prop.mSegments) {
-      if (!segment.mFromValue.IsNull()) {
-        gfx::Size from = segment.mFromValue.GetScaleValue(aFrame);
-        if (from != gfx::Size(1.0f, 1.0f)) {
-          return true;
-        }
-      }
-      if (!segment.mToValue.IsNull()) {
-        gfx::Size to = segment.mToValue.GetScaleValue(aFrame);
-        if (to != gfx::Size(1.0f, 1.0f)) {
-          return true;
-        }
-      }
-    }
-  }
-
-  return false;
-}
-
-void
-KeyframeEffectReadOnly::UpdateEffectSet(EffectSet* aEffectSet) const
-{
-  if (!mInEffectSet) {
-    return;
-  }
-
-  EffectSet* effectSet =
-    aEffectSet ? aEffectSet
-               : EffectSet::GetEffectSet(mTarget->mElement,
-                                         mTarget->mPseudoType);
-  if (!effectSet) {
-    return;
-  }
-
-  nsIFrame* frame = GetStyleFrame();
-  if (HasAnimationOfProperty(eCSSProperty_opacity)) {
-    effectSet->SetMayHaveOpacityAnimation();
-    if (frame) {
-      frame->SetMayHaveOpacityAnimation();
-    }
-  }
-  if (HasAnimationOfProperty(eCSSProperty_transform)) {
-    effectSet->SetMayHaveTransformAnimation();
-    if (frame) {
-      frame->SetMayHaveTransformAnimation();
-    }
-  }
-}
-
-
-} // namespace dom
-} // namespace mozilla
deleted file mode 100644
--- a/dom/animation/KeyframeEffectReadOnly.h
+++ /dev/null
@@ -1,443 +0,0 @@
-/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
-/* vim: set ts=8 sts=2 et sw=2 tw=80: */
-/* 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/. */
-
-#ifndef mozilla_dom_KeyframeEffectReadOnly_h
-#define mozilla_dom_KeyframeEffectReadOnly_h
-
-#include "nsChangeHint.h"
-#include "nsCSSPropertyID.h"
-#include "nsCSSPropertyIDSet.h"
-#include "nsCSSValue.h"
-#include "nsCycleCollectionParticipant.h"
-#include "nsRefPtrHashtable.h"
-#include "nsTArray.h"
-#include "nsWrapperCache.h"
-#include "mozilla/AnimationPerformanceWarning.h"
-#include "mozilla/AnimationPropertySegment.h"
-#include "mozilla/AnimationTarget.h"
-#include "mozilla/Attributes.h"
-#include "mozilla/ComputedTimingFunction.h"
-#include "mozilla/EffectCompositor.h"
-#include "mozilla/Keyframe.h"
-#include "mozilla/KeyframeEffectParams.h"
-// RawServoDeclarationBlock and associated RefPtrTraits
-#include "mozilla/ServoBindingTypes.h"
-#include "mozilla/StyleAnimationValue.h"
-#include "mozilla/dom/AnimationEffectReadOnly.h"
-#include "mozilla/dom/BindingDeclarations.h"
-#include "mozilla/dom/Element.h"
-
-struct JSContext;
-class JSObject;
-class nsIContent;
-class nsIDocument;
-class nsIFrame;
-class nsIPresShell;
-
-namespace mozilla {
-
-class AnimValuesStyleRule;
-enum class CSSPseudoElementType : uint8_t;
-class ErrorResult;
-struct AnimationRule;
-struct TimingParams;
-class EffectSet;
-class ComputedStyle;
-
-namespace dom {
-class ElementOrCSSPseudoElement;
-class GlobalObject;
-class OwningElementOrCSSPseudoElement;
-class UnrestrictedDoubleOrKeyframeAnimationOptions;
-class UnrestrictedDoubleOrKeyframeEffectOptions;
-enum class IterationCompositeOperation : uint8_t;
-enum class CompositeOperation : uint8_t;
-struct AnimationPropertyDetails;
-}
-
-struct AnimationProperty
-{
-  nsCSSPropertyID mProperty = eCSSProperty_UNKNOWN;
-
-  // If true, the propery is currently being animated on the compositor.
-  //
-  // Note that when the owning Animation requests a non-throttled restyle, in
-  // between calling RequestRestyle on its EffectCompositor and when the
-  // restyle is performed, this member may temporarily become false even if
-  // the animation remains on the layer after the restyle.
-  //
-  // **NOTE**: This member is not included when comparing AnimationProperty
-  // objects for equality.
-  bool mIsRunningOnCompositor = false;
-
-  Maybe<AnimationPerformanceWarning> mPerformanceWarning;
-
-  InfallibleTArray<AnimationPropertySegment> mSegments;
-
-  // The copy constructor/assignment doesn't copy mIsRunningOnCompositor and
-  // mPerformanceWarning.
-  AnimationProperty() = default;
-  AnimationProperty(const AnimationProperty& aOther)
-    : mProperty(aOther.mProperty), mSegments(aOther.mSegments) { }
-  AnimationProperty& operator=(const AnimationProperty& aOther)
-  {
-    mProperty = aOther.mProperty;
-    mSegments = aOther.mSegments;
-    return *this;
-  }
-
-  // NOTE: This operator does *not* compare the mIsRunningOnCompositor member.
-  // This is because AnimationProperty objects are compared when recreating
-  // CSS animations to determine if mutation observer change records need to
-  // be created or not. However, at the point when these objects are compared
-  // the mIsRunningOnCompositor will not have been set on the new objects so
-  // we ignore this member to avoid generating spurious change records.
-  bool operator==(const AnimationProperty& aOther) const
-  {
-    return mProperty == aOther.mProperty &&
-           mSegments == aOther.mSegments;
-  }
-  bool operator!=(const AnimationProperty& aOther) const
-  {
-    return !(*this == aOther);
-  }
-};
-
-struct ElementPropertyTransition;
-
-namespace dom {
-
-class Animation;
-
-class KeyframeEffectReadOnly : public AnimationEffectReadOnly
-{
-public:
-  KeyframeEffectReadOnly(nsIDocument* aDocument,
-                         const Maybe<OwningAnimationTarget>& aTarget,
-                         const TimingParams& aTiming,
-                         const KeyframeEffectParams& aOptions);
-
-  NS_DECL_ISUPPORTS_INHERITED
-  NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(KeyframeEffectReadOnly,
-                                                        AnimationEffectReadOnly)
-
-  virtual JSObject* WrapObject(JSContext* aCx,
-                               JS::Handle<JSObject*> aGivenProto) override;
-
-  KeyframeEffectReadOnly* AsKeyframeEffect() override { return this; }
-
-  // KeyframeEffectReadOnly interface
-  static already_AddRefed<KeyframeEffectReadOnly>
-  Constructor(const GlobalObject& aGlobal,
-              const Nullable<ElementOrCSSPseudoElement>& aTarget,
-              JS::Handle<JSObject*> aKeyframes,
-              const UnrestrictedDoubleOrKeyframeEffectOptions& aOptions,
-              ErrorResult& aRv);
-
-  static already_AddRefed<KeyframeEffectReadOnly>
-  Constructor(const GlobalObject& aGlobal,
-              KeyframeEffectReadOnly& aSource,
-              ErrorResult& aRv);
-
-  void GetTarget(Nullable<OwningElementOrCSSPseudoElement>& aRv) const;
-  Maybe<NonOwningAnimationTarget> GetTarget() const
-  {
-    Maybe<NonOwningAnimationTarget> result;
-    if (mTarget) {
-      result.emplace(*mTarget);
-    }
-    return result;
-  }
-  void GetKeyframes(JSContext*& aCx,
-                    nsTArray<JSObject*>& aResult,
-                    ErrorResult& aRv);
-  void GetProperties(nsTArray<AnimationPropertyDetails>& aProperties,
-                     ErrorResult& aRv) const;
-
-  IterationCompositeOperation IterationComposite() const;
-  CompositeOperation Composite() const;
-  void NotifyAnimationTimingUpdated();
-  void RequestRestyle(EffectCompositor::RestyleType aRestyleType);
-  void SetAnimation(Animation* aAnimation) override;
-  void SetKeyframes(JSContext* aContext, JS::Handle<JSObject*> aKeyframes,
-                    ErrorResult& aRv);
-  void SetKeyframes(nsTArray<Keyframe>&& aKeyframes,
-                    const ComputedStyle* aStyle);
-
-  // Returns true if the effect includes |aProperty| regardless of whether the
-  // property is overridden by !important rule.
-  bool HasAnimationOfProperty(nsCSSPropertyID aProperty) const;
-
-  // GetEffectiveAnimationOfProperty returns AnimationProperty corresponding
-  // to a given CSS property if the effect includes the property and the
-  // property is not overridden by !important rules.
-  // Also EffectiveAnimationOfProperty returns true under the same condition.
-  //
-  // NOTE: We don't currently check for !important rules for properties that
-  // can't run on the compositor.
-  bool HasEffectiveAnimationOfProperty(nsCSSPropertyID aProperty) const
-  {
-    return GetEffectiveAnimationOfProperty(aProperty) != nullptr;
-  }
-  const AnimationProperty* GetEffectiveAnimationOfProperty(
-    nsCSSPropertyID aProperty) const;
-
-  const InfallibleTArray<AnimationProperty>& Properties() const
-  {
-    return mProperties;
-  }
-
-  // Update |mProperties| by recalculating from |mKeyframes| using
-  // |aComputedStyle| to resolve specified values.
-  void UpdateProperties(const ComputedStyle* aComputedValues);
-
-  // Update various bits of state related to running ComposeStyle().
-  // We need to update this outside ComposeStyle() because we should avoid
-  // mutating any state in ComposeStyle() since it might be called during
-  // parallel traversal.
-  void WillComposeStyle();
-
-  // Updates |aComposeResult| with the animation values produced by this
-  // AnimationEffect for the current time except any properties contained
-  // in |aPropertiesToSkip|.
-  void ComposeStyle(RawServoAnimationValueMap& aComposeResult,
-                    const nsCSSPropertyIDSet& aPropertiesToSkip);
-
-
-  // Returns true if at least one property is being animated on compositor.
-  bool IsRunningOnCompositor() const;
-  void SetIsRunningOnCompositor(nsCSSPropertyID aProperty, bool aIsRunning);
-  void ResetIsRunningOnCompositor();
-
-  // Returns true if this effect, applied to |aFrame|, contains properties
-  // that mean we shouldn't run transform compositor animations on this element.
-  //
-  // For example, if we have an animation of geometric properties like 'left'
-  // and 'top' on an element, we force all 'transform' animations running at
-  // the same time on the same element to run on the main thread.
-  //
-  // When returning true, |aPerformanceWarning| stores the reason why
-  // we shouldn't run the transform animations.
-  bool ShouldBlockAsyncTransformAnimations(
-    const nsIFrame* aFrame, AnimationPerformanceWarning::Type& aPerformanceWarning) const;
-  bool HasGeometricProperties() const;
-  bool AffectsGeometry() const override
-  {
-    return GetTarget() && HasGeometricProperties();
-  }
-
-  nsIDocument* GetRenderedDocument() const;
-  nsIPresShell* GetPresShell() const;
-
-  // Associates a warning with the animated property on the specified frame
-  // indicating why, for example, the property could not be animated on the
-  // compositor. |aParams| and |aParamsLength| are optional parameters which
-  // will be used to generate a localized message for devtools.
-  void SetPerformanceWarning(
-    nsCSSPropertyID aProperty,
-    const AnimationPerformanceWarning& aWarning);
-
-  // Cumulative change hint on each segment for each property.
-  // This is used for deciding the animation is paint-only.
-  void CalculateCumulativeChangeHint(const ComputedStyle* aStyle);
-
-  // Returns true if all of animation properties' change hints
-  // can ignore painting if the animation is not visible.
-  // See nsChangeHint_Hints_CanIgnoreIfNotVisible in nsChangeHint.h
-  // in detail which change hint can be ignored.
-  bool CanIgnoreIfNotVisible() const;
-
-  // Returns true if the effect is current state and has scale animation.
-  // |aFrame| is used for calculation of scale values.
-  bool ContainsAnimatedScale(const nsIFrame* aFrame) const;
-
-  AnimationValue BaseStyle(nsCSSPropertyID aProperty) const
-  {
-    AnimationValue result;
-    bool hasProperty = false;
-    // We cannot use getters_AddRefs on RawServoAnimationValue because it is
-    // an incomplete type, so Get() doesn't work. Instead, use GetWeak, and
-    // then assign the raw pointer to a RefPtr.
-    result.mServo = mBaseStyleValuesForServo.GetWeak(aProperty, &hasProperty);
-    MOZ_ASSERT(hasProperty || result.IsNull());
-    return result;
-  }
-
-  static bool HasComputedTimingChanged(
-    const ComputedTiming& aComputedTiming,
-    IterationCompositeOperation aIterationComposite,
-    const Nullable<double>& aProgressOnLastCompose,
-    uint64_t aCurrentIterationOnLastCompose);
-
-protected:
-  KeyframeEffectReadOnly(nsIDocument* aDocument,
-                         const Maybe<OwningAnimationTarget>& aTarget,
-                         AnimationEffectTimingReadOnly* aTiming,
-                         const KeyframeEffectParams& aOptions);
-
-  ~KeyframeEffectReadOnly() override = default;
-
-  static Maybe<OwningAnimationTarget>
-  ConvertTarget(const Nullable<ElementOrCSSPseudoElement>& aTarget);
-
-  template<class KeyframeEffectType, class OptionsType>
-  static already_AddRefed<KeyframeEffectType>
-  ConstructKeyframeEffect(const GlobalObject& aGlobal,
-                          const Nullable<ElementOrCSSPseudoElement>& aTarget,
-                          JS::Handle<JSObject*> aKeyframes,
-                          const OptionsType& aOptions,
-                          ErrorResult& aRv);
-
-  template<class KeyframeEffectType>
-  static already_AddRefed<KeyframeEffectType>
-  ConstructKeyframeEffect(const GlobalObject& aGlobal,
-                          KeyframeEffectReadOnly& aSource,
-                          ErrorResult& aRv);
-
-  // Build properties by recalculating from |mKeyframes| using |aComputedStyle|
-  // to resolve specified values. This function also applies paced spacing if
-  // needed.
-  nsTArray<AnimationProperty> BuildProperties(const ComputedStyle* aStyle);
-
-  // This effect is registered with its target element so long as:
-  //
-  // (a) It has a target element, and
-  // (b) It is "relevant" (i.e. yet to finish but not idle, or finished but
-  //     filling forwards)
-  //
-  // As a result, we need to make sure this gets called whenever anything
-  // changes with regards to this effects's timing including changes to the
-  // owning Animation's timing.
-  void UpdateTargetRegistration();
-
-  // Remove the current effect target from its EffectSet.
-  void UnregisterTarget();
-
-  // Update the associated frame state bits so that, if necessary, a stacking
-  // context will be created and the effect sent to the compositor.  We
-  // typically need to do this when the properties referenced by the keyframe
-  // have changed, or when the target frame might have changed.
-  void MaybeUpdateFrameForCompositor();
-
-  // Looks up the ComputedStyle associated with the target element, if any.
-  // We need to be careful to *not* call this when we are updating the style
-  // context. That's because calling GetComputedStyle when we are in the process
-  // of building a ComputedStyle may trigger various forms of infinite
-  // recursion.
-  already_AddRefed<ComputedStyle> GetTargetComputedStyle();
-
-  // A wrapper for marking cascade update according to the current
-  // target and its effectSet.
-  void MarkCascadeNeedsUpdate();
-
-  void EnsureBaseStyles(const ComputedStyle* aComputedValues,
-                        const nsTArray<AnimationProperty>& aProperties);
-
-  // Stylo version of the above function that also first checks for an additive
-  // value in |aProperty|'s list of segments.
-  void EnsureBaseStyle(const AnimationProperty& aProperty,
-                       nsPresContext* aPresContext,
-                       const ComputedStyle* aComputedValues,
-                       RefPtr<ComputedStyle>& aBaseComputedValues);
-
-  Maybe<OwningAnimationTarget> mTarget;
-
-  KeyframeEffectParams mEffectOptions;
-
-  // The specified keyframes.
-  nsTArray<Keyframe>          mKeyframes;
-
-  // A set of per-property value arrays, derived from |mKeyframes|.
-  nsTArray<AnimationProperty> mProperties;
-
-  // The computed progress last time we composed the style rule. This is
-  // used to detect when the progress is not changing (e.g. due to a step
-  // timing function) so we can avoid unnecessary style updates.
-  Nullable<double> mProgressOnLastCompose;
-
-  // The purpose of this value is the same as mProgressOnLastCompose but
-  // this is used to detect when the current iteration is not changing
-  // in the case when iterationComposite is accumulate.
-  uint64_t mCurrentIterationOnLastCompose = 0;
-
-  // We need to track when we go to or from being "in effect" since
-  // we need to re-evaluate the cascade of animations when that changes.
-  bool mInEffectOnLastAnimationTimingUpdate;
-
-  // The non-animated values for properties in this effect that contain at
-  // least one animation value that is composited with the underlying value
-  // (i.e. it uses the additive or accumulate composite mode).
-  nsRefPtrHashtable<nsUint32HashKey, RawServoAnimationValue>
-    mBaseStyleValuesForServo;
-
-  // True if this effect is in the EffectSet for its target element. This is
-  // used as an optimization to avoid unnecessary hashmap lookups on the
-  // EffectSet.
-  bool mInEffectSet = false;
-
-private:
-  nsChangeHint mCumulativeChangeHint;
-
-  void ComposeStyleRule(RawServoAnimationValueMap& aAnimationValues,
-                        const AnimationProperty& aProperty,
-                        const AnimationPropertySegment& aSegment,
-                        const ComputedTiming& aComputedTiming);
-
-
-  already_AddRefed<ComputedStyle> CreateComputedStyleForAnimationValue(
-    nsCSSPropertyID aProperty,
-    const AnimationValue& aValue,
-    nsPresContext* aPresContext,
-    const ComputedStyle* aBaseComputedStyle);
-
-  // Return the primary frame for the target (pseudo-)element.
-  nsIFrame* GetPrimaryFrame() const;
-  // Returns the frame which is used for styling.
-  nsIFrame* GetStyleFrame() const;
-
-  bool CanThrottle() const;
-  bool CanThrottleTransformChanges(const nsIFrame& aFrame) const;
-  bool CanThrottleTransformChangesInScrollable(nsIFrame& aFrame) const;
-
-  // Returns true if the computedTiming has changed since the last
-  // composition.
-  bool HasComputedTimingChanged() const;
-
-  // Returns true unless Gecko limitations prevent performing transform
-  // animations for |aFrame|. When returning true, the reason for the
-  // limitation is stored in |aOutPerformanceWarning|.
-  static bool CanAnimateTransformOnCompositor(
-    const nsIFrame* aFrame,
-    AnimationPerformanceWarning::Type& aPerformanceWarning);
-  static bool IsGeometricProperty(const nsCSSPropertyID aProperty);
-
-  static const TimeDuration OverflowRegionRefreshInterval();
-
-  void UpdateEffectSet(mozilla::EffectSet* aEffectSet = nullptr) const;
-
-  // Returns true if this effect has transform and the transform might affect
-  // the overflow region.
-  // This function is used for updating scroll bars or notifying intersection
-  // observers reflected by the transform.
-  bool HasTransformThatMightAffectOverflow() const
-  {
-    return mCumulativeChangeHint & (nsChangeHint_UpdatePostTransformOverflow |
-                                    nsChangeHint_AddOrRemoveTransform |
-                                    nsChangeHint_UpdateTransformLayer);
-  }
-
-  // Returns true if this effect causes visibility change.
-  // (i.e. 'visibility: hidden' -> 'visibility: visible' and vice versa.)
-  bool HasVisibilityChange() const
-  {
-    return mCumulativeChangeHint & nsChangeHint_VisibilityChange;
-  }
-};
-
-} // namespace dom
-} // namespace mozilla
-
-#endif // mozilla_dom_KeyframeEffectReadOnly_h
--- a/dom/animation/KeyframeUtils.cpp
+++ b/dom/animation/KeyframeUtils.cpp
@@ -13,17 +13,17 @@
 #include "mozilla/ServoBindings.h"
 #include "mozilla/ServoBindingTypes.h"
 #include "mozilla/ServoCSSParser.h"
 #include "mozilla/StyleAnimationValue.h"
 #include "mozilla/TimingParams.h"
 #include "mozilla/dom/BaseKeyframeTypesBinding.h" // For FastBaseKeyframe etc.
 #include "mozilla/dom/Element.h"
 #include "mozilla/dom/KeyframeEffectBinding.h"
-#include "mozilla/dom/KeyframeEffectReadOnly.h" // For PropertyValuesPair etc.
+#include "mozilla/dom/KeyframeEffect.h" // For PropertyValuesPair etc.
 #include "mozilla/dom/Nullable.h"
 #include "jsapi.h" // For ForOfIterator etc.
 #include "nsClassHashtable.h"
 #include "nsContentUtils.h" // For GetContextForContent, and
                             // AnimationsAPICoreEnabled
 #include "nsCSSPropertyIDSet.h"
 #include "nsCSSProps.h"
 #include "nsCSSPseudoElements.h" // For CSSPseudoElementType
--- a/dom/animation/TimingParams.cpp
+++ b/dom/animation/TimingParams.cpp
@@ -11,86 +11,59 @@
 #include "mozilla/dom/KeyframeAnimationOptionsBinding.h"
 #include "mozilla/dom/KeyframeEffectBinding.h"
 #include "mozilla/ServoCSSParser.h"
 #include "nsIDocument.h"
 
 namespace mozilla {
 
 template <class OptionsType>
-static const dom::AnimationEffectTimingProperties&
+static const dom::EffectTiming&
 GetTimingProperties(const OptionsType& aOptions);
 
 template <>
-/* static */ const dom::AnimationEffectTimingProperties&
+/* static */ const dom::EffectTiming&
 GetTimingProperties(
   const dom::UnrestrictedDoubleOrKeyframeEffectOptions& aOptions)
 {
   MOZ_ASSERT(aOptions.IsKeyframeEffectOptions());
   return aOptions.GetAsKeyframeEffectOptions();
 }
 
 template <>
-/* static */ const dom::AnimationEffectTimingProperties&
+/* static */ const dom::EffectTiming&
 GetTimingProperties(
   const dom::UnrestrictedDoubleOrKeyframeAnimationOptions& aOptions)
 {
   MOZ_ASSERT(aOptions.IsKeyframeAnimationOptions());
   return aOptions.GetAsKeyframeAnimationOptions();
 }
 
 template <class OptionsType>
 /* static */ TimingParams
 TimingParams::FromOptionsType(const OptionsType& aOptions,
                               nsIDocument* aDocument,
                               ErrorResult& aRv)
 {
   TimingParams result;
+
   if (aOptions.IsUnrestrictedDouble()) {
     double durationInMs = aOptions.GetAsUnrestrictedDouble();
     if (durationInMs >= 0) {
       result.mDuration.emplace(
         StickyTimeDuration::FromMilliseconds(durationInMs));
     } else {
       aRv.Throw(NS_ERROR_DOM_TYPE_ERR);
       return result;
     }
+    result.Update();
   } else {
-    const dom::AnimationEffectTimingProperties& timing =
-      GetTimingProperties(aOptions);
-
-    Maybe<StickyTimeDuration> duration =
-      TimingParams::ParseDuration(timing.mDuration, aRv);
-    if (aRv.Failed()) {
-      return result;
-    }
-    TimingParams::ValidateIterationStart(timing.mIterationStart, aRv);
-    if (aRv.Failed()) {
-      return result;
-    }
-    TimingParams::ValidateIterations(timing.mIterations, aRv);
-    if (aRv.Failed()) {
-      return result;
-    }
-    Maybe<ComputedTimingFunction> easing =
-      TimingParams::ParseEasing(timing.mEasing, aDocument, aRv);
-    if (aRv.Failed()) {
-      return result;
-    }
-
-    result.mDuration = duration;
-    result.mDelay = TimeDuration::FromMilliseconds(timing.mDelay);
-    result.mEndDelay = TimeDuration::FromMilliseconds(timing.mEndDelay);
-    result.mIterations = timing.mIterations;
-    result.mIterationStart = timing.mIterationStart;
-    result.mDirection = timing.mDirection;
-    result.mFill = timing.mFill;
-    result.mFunction = easing;
+    const dom::EffectTiming& timing = GetTimingProperties(aOptions);
+    result = FromEffectTiming(timing, aDocument, aRv);
   }
-  result.Update();
 
   return result;
 }
 
 /* static */ TimingParams
 TimingParams::FromOptionsUnion(
   const dom::UnrestrictedDoubleOrKeyframeEffectOptions& aOptions,
   nsIDocument* aDocument,
@@ -103,16 +76,136 @@ TimingParams::FromOptionsUnion(
 TimingParams::FromOptionsUnion(
   const dom::UnrestrictedDoubleOrKeyframeAnimationOptions& aOptions,
   nsIDocument* aDocument,
   ErrorResult& aRv)
 {
   return FromOptionsType(aOptions, aDocument, aRv);
 }
 
+/* static */ TimingParams
+TimingParams::FromEffectTiming(const dom::EffectTiming& aEffectTiming,
+                               nsIDocument* aDocument,
+                               ErrorResult& aRv)
+{
+  TimingParams result;
+
+  Maybe<StickyTimeDuration> duration =
+    TimingParams::ParseDuration(aEffectTiming.mDuration, aRv);
+  if (aRv.Failed()) {
+    return result;
+  }
+  TimingParams::ValidateIterationStart(aEffectTiming.mIterationStart, aRv);
+  if (aRv.Failed()) {
+    return result;
+  }
+  TimingParams::ValidateIterations(aEffectTiming.mIterations, aRv);
+  if (aRv.Failed()) {
+    return result;
+  }
+  Maybe<ComputedTimingFunction> easing =
+    TimingParams::ParseEasing(aEffectTiming.mEasing, aDocument, aRv);
+  if (aRv.Failed()) {
+    return result;
+  }
+
+  result.mDuration = duration;
+  result.mDelay = TimeDuration::FromMilliseconds(aEffectTiming.mDelay);
+  result.mEndDelay = TimeDuration::FromMilliseconds(aEffectTiming.mEndDelay);
+  result.mIterations = aEffectTiming.mIterations;
+  result.mIterationStart = aEffectTiming.mIterationStart;
+  result.mDirection = aEffectTiming.mDirection;
+  result.mFill = aEffectTiming.mFill;
+  result.mFunction = easing;
+
+  result.Update();
+
+  return result;
+}
+
+/* static */ TimingParams
+TimingParams::MergeOptionalEffectTiming(
+  const TimingParams& aSource,
+  const dom::OptionalEffectTiming& aEffectTiming,
+  nsIDocument* aDocument,
+  ErrorResult& aRv)
+{
+  MOZ_ASSERT(!aRv.Failed(), "Initially return value should be ok");
+
+  TimingParams result = aSource;
+
+  // Check for errors first
+
+  Maybe<StickyTimeDuration> duration;
+  if (aEffectTiming.mDuration.WasPassed()) {
+    duration =
+      TimingParams::ParseDuration(aEffectTiming.mDuration.Value(), aRv);
+    if (aRv.Failed()) {
+      return result;
+    }
+  }
+
+  if (aEffectTiming.mIterationStart.WasPassed()) {
+    TimingParams::ValidateIterationStart(aEffectTiming.mIterationStart.Value(),
+                                         aRv);
+    if (aRv.Failed()) {
+      return result;
+    }
+  }
+
+  if (aEffectTiming.mIterations.WasPassed()) {
+    TimingParams::ValidateIterations(aEffectTiming.mIterations.Value(), aRv);
+    if (aRv.Failed()) {
+      return result;
+    }
+  }
+
+  Maybe<ComputedTimingFunction> easing;
+  if (aEffectTiming.mEasing.WasPassed()) {
+    easing =
+      TimingParams::ParseEasing(aEffectTiming.mEasing.Value(), aDocument, aRv);
+    if (aRv.Failed()) {
+      return result;
+    }
+  }
+
+  // Assign values
+
+  if (aEffectTiming.mDuration.WasPassed()) {
+    result.mDuration = duration;
+  }
+  if (aEffectTiming.mDelay.WasPassed()) {
+    result.mDelay =
+      TimeDuration::FromMilliseconds(aEffectTiming.mDelay.Value());
+  }
+  if (aEffectTiming.mEndDelay.WasPassed()) {
+    result.mEndDelay =
+      TimeDuration::FromMilliseconds(aEffectTiming.mEndDelay.Value());
+  }
+  if (aEffectTiming.mIterations.WasPassed()) {
+    result.mIterations = aEffectTiming.mIterations.Value();
+  }
+  if (aEffectTiming.mIterationStart.WasPassed()) {
+    result.mIterationStart = aEffectTiming.mIterationStart.Value();
+  }
+  if (aEffectTiming.mDirection.WasPassed()) {
+    result.mDirection = aEffectTiming.mDirection.Value();
+  }
+  if (aEffectTiming.mFill.WasPassed()) {
+    result.mFill = aEffectTiming.mFill.Value();
+  }
+  if (aEffectTiming.mEasing.WasPassed()) {
+    result.mFunction = easing;
+  }
+
+  result.Update();
+
+  return result;
+}
+
 /* static */ Maybe<ComputedTimingFunction>
 TimingParams::ParseEasing(const nsAString& aEasing,
                           nsIDocument* aDocument,
                           ErrorResult& aRv)
 {
   MOZ_ASSERT(aDocument);
 
   nsTimingFunction timingFunction;
@@ -131,16 +224,17 @@ TimingParams::ParseEasing(const nsAStrin
 
 bool
 TimingParams::operator==(const TimingParams& aOther) const
 {
   // We don't compare mActiveDuration and mEndTime because they are calculated
   // from other timing parameters.
   return mDuration == aOther.mDuration &&
          mDelay == aOther.mDelay &&
+         mEndDelay == aOther.mEndDelay &&
          mIterations == aOther.mIterations &&
          mIterationStart == aOther.mIterationStart &&
          mDirection == aOther.mDirection &&
          mFill == aOther.mFill &&
          mFunction == aOther.mFunction;
 }
 
 } // namespace mozilla
--- a/dom/animation/TimingParams.h
+++ b/dom/animation/TimingParams.h
@@ -10,18 +10,18 @@
 #include "nsStringFwd.h"
 #include "mozilla/dom/Nullable.h"
 #include "mozilla/dom/UnionTypes.h" // For OwningUnrestrictedDoubleOrString
 #include "mozilla/ComputedTimingFunction.h"
 #include "mozilla/Maybe.h"
 #include "mozilla/StickyTimeDuration.h"
 #include "mozilla/TimeStamp.h" // for TimeDuration
 
-#include "mozilla/dom/AnimationEffectReadOnlyBinding.h" // for FillMode
-                                                        // and PlaybackDirection
+#include "mozilla/dom/AnimationEffectBinding.h" // for FillMode
+                                                // and PlaybackDirection
 
 class nsIDocument;
 
 namespace mozilla {
 
 namespace dom {
 class UnrestrictedDoubleOrKeyframeEffectOptions;
 class UnrestrictedDoubleOrKeyframeAnimationOptions;
@@ -70,16 +70,31 @@ struct TimingParams
     nsIDocument* aDocument,
     ErrorResult& aRv);
   static TimingParams FromOptionsUnion(
     const dom::UnrestrictedDoubleOrKeyframeEffectOptions& aOptions,
     nsIDocument* aDocument, ErrorResult& aRv);
   static TimingParams FromOptionsUnion(
     const dom::UnrestrictedDoubleOrKeyframeAnimationOptions& aOptions,
     nsIDocument* aDocument, ErrorResult& aRv);
+  static TimingParams FromEffectTiming(
+    const dom::EffectTiming& aEffectTiming,
+    nsIDocument* aDocument,
+    ErrorResult& aRv);
+  // Returns a copy of |aSource| where each timing property in |aSource| that
+  // is also specified in |aEffectTiming| is replaced with the value from
+  // |aEffectTiming|.
+  //
+  // If any of the values in |aEffectTiming| are invalid, |aRv.Failed()| will be
+  // true and an unmodified copy of |aSource| will be returned.
+  static TimingParams MergeOptionalEffectTiming(
+    const TimingParams& aSource,
+    const dom::OptionalEffectTiming& aEffectTiming,
+    nsIDocument* aDocument,
+    ErrorResult& aRv);
 
   // Range-checks and validates an UnrestrictedDoubleOrString or
   // OwningUnrestrictedDoubleOrString object and converts to a
   // StickyTimeDuration value or Nothing() if aDuration is "auto".
   // Caller must check aRv.Failed().
   template <class DoubleOrString>
   static Maybe<StickyTimeDuration> ParseDuration(DoubleOrString& aDuration,
                                                  ErrorResult& aRv)
--- a/dom/animation/moz.build
+++ b/dom/animation/moz.build
@@ -7,24 +7,21 @@
 with Files("**"):
     BUG_COMPONENT = ("Core", "DOM: Animation")
 
 MOCHITEST_MANIFESTS += ['test/mochitest.ini']
 MOCHITEST_CHROME_MANIFESTS += ['test/chrome.ini']
 
 EXPORTS.mozilla.dom += [
     'Animation.h',
-    'AnimationEffectReadOnly.h',
-    'AnimationEffectTiming.h',
-    'AnimationEffectTimingReadOnly.h',
+    'AnimationEffect.h',
     'AnimationTimeline.h',
     'CSSPseudoElement.h',
     'DocumentTimeline.h',
     'KeyframeEffect.h',
-    'KeyframeEffectReadOnly.h',
 ]
 
 EXPORTS.mozilla += [
     'AnimationComparator.h',
     'AnimationEventDispatcher.h',
     'AnimationPerformanceWarning.h',
     'AnimationPropertySegment.h',
     'AnimationTarget.h',
@@ -38,30 +35,27 @@ EXPORTS.mozilla += [
     'KeyframeUtils.h',
     'PendingAnimationTracker.h',
     'PseudoElementHashEntry.h',
     'TimingParams.h',
 ]
 
 UNIFIED_SOURCES += [
     'Animation.cpp',
-    'AnimationEffectReadOnly.cpp',
-    'AnimationEffectTiming.cpp',
-    'AnimationEffectTimingReadOnly.cpp',
+    'AnimationEffect.cpp',
     'AnimationEventDispatcher.cpp',
     'AnimationPerformanceWarning.cpp',
     'AnimationTimeline.cpp',
     'AnimationUtils.cpp',
     'ComputedTimingFunction.cpp',
     'CSSPseudoElement.cpp',
     'DocumentTimeline.cpp',
     'EffectCompositor.cpp',
     'EffectSet.cpp',
     'KeyframeEffect.cpp',
-    'KeyframeEffectReadOnly.cpp',
     'KeyframeUtils.cpp',
     'PendingAnimationTracker.cpp',
     'TimingParams.cpp',
 ]
 
 LOCAL_INCLUDES += [
     '/dom/base',
     '/layout/base',
--- a/dom/animation/test/chrome/file_animate_xrays.html
+++ b/dom/animation/test/chrome/file_animate_xrays.html
@@ -1,17 +1,17 @@
 <!doctype html>
 <html>
 <head>
 <meta charset=utf-8>
 <script>
 Element.prototype.animate = function() {
   throw 'Called animate() as defined in content document';
 }
-for (let name of ["KeyframeEffect", "KeyframeEffectReadOnly", "Animation"]) {
+for (let name of ["KeyframeEffect", "Animation"]) {
   this[name] = function() {
     throw `Called overridden ${name} constructor`;
   };
 }
 </script>
 <body>
 <div id="target"></div>
 </body>
--- a/dom/animation/test/chrome/test_animation_observers_sync.html
+++ b/dom/animation/test/chrome/test_animation_observers_sync.html
@@ -101,42 +101,44 @@ function runTest() {
                                  aOptions.subtree);
 
       var anim = div.animate({ opacity: [ 0, 1 ] }, 200 * MS_PER_SEC);
 
       assert_equals_records(observer.takeRecords(),
         [{ added: [anim], changed: [], removed: [] }],
         "records after animation is added");
 
-      anim.effect.timing.duration = 100 * MS_PER_SEC;
+      anim.effect.updateTiming({ duration: 100 * MS_PER_SEC });
       assert_equals_records(observer.takeRecords(),
         [{ added: [], changed: [anim], removed: [] }],
         "records after duration is changed");
 
-      anim.effect.timing.duration = 100 * MS_PER_SEC;
+      anim.effect.updateTiming({ duration: 100 * MS_PER_SEC });
       assert_equals_records(observer.takeRecords(),
         [], "records after assigning same value");
 
-      anim.currentTime = anim.effect.timing.duration * 2;
+      anim.currentTime = anim.effect.getComputedTiming().duration * 2;
       anim.finish();
       assert_equals_records(observer.takeRecords(),
         [{ added: [], changed: [], removed: [anim] }],
         "records after animation end");
 
-      anim.effect.timing.duration = anim.effect.timing.duration * 3;
+      anim.effect.updateTiming({
+        duration: anim.effect.getComputedTiming().duration * 3
+      });
       assert_equals_records(observer.takeRecords(),
         [{ added: [anim], changed: [], removed: [] }],
         "records after animation restarted");
 
-      anim.effect.timing.duration = "auto";
+      anim.effect.updateTiming({ duration: 'auto' });
       assert_equals_records(observer.takeRecords(),
         [{ added: [], changed: [], removed: [anim] }],
         "records after duration set \"auto\"");
 
-      anim.effect.timing.duration = "auto";
+      anim.effect.updateTiming({ duration: 'auto' });
       assert_equals_records(observer.takeRecords(),
         [], "records after assigning same value \"auto\"");
     }, "change_duration_and_currenttime");
 
     test(t => {
       var div = addDiv(t);
       var observer =
         setupSynchronousObserver(t,
@@ -144,31 +146,31 @@ function runTest() {
                                  aOptions.subtree);
 
       var anim = div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
 
       assert_equals_records(observer.takeRecords(),
         [{ added: [anim], changed: [], removed: [] }],
         "records after animation is added");
 
-      anim.effect.timing.endDelay = 10 * MS_PER_SEC;
+      anim.effect.updateTiming({ endDelay: 10 * MS_PER_SEC });
       assert_equals_records(observer.takeRecords(),
         [{ added: [], changed: [anim], removed: [] }],
         "records after endDelay is changed");
 
-      anim.effect.timing.endDelay = 10 * MS_PER_SEC;
+      anim.effect.updateTiming({ endDelay: 10 * MS_PER_SEC });
       assert_equals_records(observer.takeRecords(),
         [], "records after assigning same value");
 
       anim.currentTime = 109 * MS_PER_SEC;
       assert_equals_records(observer.takeRecords(),
         [{ added: [], changed: [], removed: [anim] }],
         "records after currentTime during endDelay");
 
-      anim.effect.timing.endDelay = -110 * MS_PER_SEC;
+      anim.effect.updateTiming({ endDelay: -110 * MS_PER_SEC });
       assert_equals_records(observer.takeRecords(),
         [], "records after assigning negative value");
     }, "change_enddelay_and_currenttime");
 
     test(t => {
       var div = addDiv(t);
       var observer =
         setupSynchronousObserver(t,
@@ -190,31 +192,31 @@ function runTest() {
                                  aOptions.subtree);
 
       var anim = div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
 
       assert_equals_records(observer.takeRecords(),
         [{ added: [anim], changed: [], removed: [] }],
         "records after animation is added");
 
-      anim.effect.timing.iterations = 2;
+      anim.effect.updateTiming({ iterations: 2 });
       assert_equals_records(observer.takeRecords(),
         [{ added: [], changed: [anim], removed: [] }],
         "records after iterations is changed");
 
-      anim.effect.timing.iterations = 2;
+      anim.effect.updateTiming({ iterations: 2 });
       assert_equals_records(observer.takeRecords(),
         [], "records after assigning same value");
 
-      anim.effect.timing.iterations = 0;
+      anim.effect.updateTiming({ iterations: 0 });
       assert_equals_records(observer.takeRecords(),
         [{ added: [], changed: [], removed: [anim] }],
         "records after animation end");
 
-      anim.effect.timing.iterations = Infinity;
+      anim.effect.updateTiming({ iterations: Infinity });
       assert_equals_records(observer.takeRecords(),
         [{ added: [anim], changed: [], removed: [] }],
         "records after animation restarted");
     }, "change_iterations");
 
     test(t => {
       var div = addDiv(t);
       var observer =
@@ -223,31 +225,31 @@ function runTest() {
                                  aOptions.subtree);
 
       var anim = div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
 
       assert_equals_records(observer.takeRecords(),
         [{ added: [anim], changed: [], removed: [] }],
         "records after animation is added");
 
-      anim.effect.timing.delay = 100;
+      anim.effect.updateTiming({ delay: 100 });
       assert_equals_records(observer.takeRecords(),
         [{ added: [], changed: [anim], removed: [] }],
         "records after delay is changed");
 
-      anim.effect.timing.delay = 100;
+      anim.effect.updateTiming({ delay: 100 });
       assert_equals_records(observer.takeRecords(),
         [], "records after assigning same value");
 
-      anim.effect.timing.delay = -100 * MS_PER_SEC;
+      anim.effect.updateTiming({ delay: -100 * MS_PER_SEC });
       assert_equals_records(observer.takeRecords(),
         [{ added: [], changed: [], removed: [anim] }],
         "records after animation end");
 
-      anim.effect.timing.delay = 0;
+      anim.effect.updateTiming({ delay: 0 });
       assert_equals_records(observer.takeRecords(),
         [{ added: [anim], changed: [], removed: [] }],
         "records after animation restarted");
     }, "change_delay");
 
     test(t => {
       var div = addDiv(t);
       var observer =
@@ -258,22 +260,22 @@ function runTest() {
       var anim = div.animate({ opacity: [ 0, 1 ] },
                              { duration: 100 * MS_PER_SEC,
                                easing: "steps(2, start)" });
 
       assert_equals_records(observer.takeRecords(),
         [{ added: [anim], changed: [], removed: [] }],
         "records after animation is added");
 
-      anim.effect.timing.easing = "steps(2, end)";
+      anim.effect.updateTiming({ easing: "steps(2, end)" });
       assert_equals_records(observer.takeRecords(),
         [{ added: [], changed: [anim], removed: [] }],
         "records after easing is changed");
 
-      anim.effect.timing.easing = "steps(2, end)";
+      anim.effect.updateTiming({ easing: "steps(2, end)" });
       assert_equals_records(observer.takeRecords(),
         [], "records after assigning same value");
     }, "change_easing");
 
     test(t => {
       var div = addDiv(t);
       var observer =
         setupSynchronousObserver(t,
@@ -288,19 +290,19 @@ function runTest() {
 
     test(t => {
       var div = addDiv(t);
       var observer =
         setupSynchronousObserver(t,
                                  aOptions.subtree ? div.parentNode : div,
                                  aOptions.subtree);
 
-      var effect = new KeyframeEffectReadOnly(null,
-                                              { opacity: [ 0, 1 ] },
-                                              { duration: 100 * MS_PER_SEC });
+      var effect = new KeyframeEffect(null,
+                                      { opacity: [ 0, 1 ] },
+                                      { duration: 100 * MS_PER_SEC });
       var anim = new Animation(effect, document.timeline);
       anim.play();
       assert_equals_records(observer.takeRecords(),
         [], "no records after animation is added");
     }, "create_animation_without_target");
 
     test(t => {
       var div = addDiv(t);
@@ -1520,42 +1522,44 @@ function runTest() {
     var observer = setupSynchronousObserver(t, div, true);
 
     var anim = pseudoTarget.animate({ opacity: [ 0, 1 ] }, 200 * MS_PER_SEC);
 
     assert_equals_records(observer.takeRecords(),
       [{ added: [anim], changed: [], removed: [] }],
       "records after animation is added");
 
-    anim.effect.timing.duration = 100 * MS_PER_SEC;
+    anim.effect.updateTiming({ duration: 100 * MS_PER_SEC });
     assert_equals_records(observer.takeRecords(),
       [{ added: [], changed: [anim], removed: [] }],
       "records after duration is changed");
 
-    anim.effect.timing.duration = 100 * MS_PER_SEC;
+    anim.effect.updateTiming({ duration: 100 * MS_PER_SEC });
     assert_equals_records(observer.takeRecords(),
       [], "records after assigning same value");
 
-    anim.currentTime = anim.effect.timing.duration * 2;
+    anim.currentTime = anim.effect.getComputedTiming().duration * 2;
     anim.finish();
     assert_equals_records(observer.takeRecords(),
       [{ added: [], changed: [], removed: [anim] }],
       "records after animation end");
 
-    anim.effect.timing.duration = anim.effect.timing.duration * 3;
+    anim.effect.updateTiming({
+      duration: anim.effect.getComputedTiming().duration * 3
+    });
     assert_equals_records(observer.takeRecords(),
       [{ added: [anim], changed: [], removed: [] }],
       "records after animation restarted");
 
-    anim.effect.timing.duration = "auto";
+    anim.effect.updateTiming({ duration: "auto" });
     assert_equals_records(observer.takeRecords(),
       [{ added: [], changed: [], removed: [anim] }],
       "records after duration set \"auto\"");
 
-    anim.effect.timing.duration = "auto";
+    anim.effect.updateTiming({ duration: "auto" });
     assert_equals_records(observer.takeRecords(),
       [], "records after assigning same value \"auto\"");
   }, "change_duration_and_currenttime_on_pseudo_elements");
 
   test(t => {
     var div = addDiv(t);
     var pseudoTarget = createPseudo(t, div, 'before');
     var observer = setupSynchronousObserver(t, div, false);
--- a/dom/animation/test/chrome/test_animation_properties.html
+++ b/dom/animation/test/chrome/test_animation_properties.html
@@ -1,13 +1,13 @@
 <!doctype html>
 <head>
 <meta charset=utf-8>
 <title>Bug 1254419 - Test the values returned by
-       KeyframeEffectReadOnly.getProperties()</title>
+       KeyframeEffect.getProperties()</title>
 <script type="application/javascript" src="../testharness.js"></script>
 <script type="application/javascript" src="../testharnessreport.js"></script>
 <script type="application/javascript" src="../testcommon.js"></script>
 </head>
 <body>
 <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1254419"
   target="_blank">Mozilla Bug 1254419</a>
 <div id="log"></div>
--- a/dom/animation/test/chrome/test_running_on_compositor.html
+++ b/dom/animation/test/chrome/test_running_on_compositor.html
@@ -315,23 +315,23 @@ promise_test(async t => {
                                    { opacity: [ 0, 1 ] }, 200 * MS_PER_SEC);
 
   await waitForPaints();
 
   assert_animation_is_running_on_compositor(animation,
     'Animation reports that it is running on the compositor');
 
   animation.currentTime = 150 * MS_PER_SEC;
-  animation.effect.timing.duration = 100 * MS_PER_SEC;
+  animation.effect.updateTiming({ duration: 100 * MS_PER_SEC });
 
   assert_animation_is_not_running_on_compositor(animation,
      'Animation reports that it is NOT running on the compositor'
      + ' when the animation is set a shorter duration than current time');
 }, 'animation is immediately removed from compositor' +
-   'when timing.duration is made shorter than the current time');
+   'when the duration is made shorter than the current time');
 
 promise_test(async t => {
   var animation = addDivAndAnimate(t,
                                    {},
                                    { opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
 
   await waitForPaints();
 
@@ -339,36 +339,36 @@ promise_test(async t => {
     'Animation reports that it is running on the compositor');
 
   animation.currentTime = 500 * MS_PER_SEC;
 
   assert_animation_is_not_running_on_compositor(animation,
     'Animation reports that it is NOT running on the compositor'
     + ' when finished');
 
-  animation.effect.timing.duration = 1000 * MS_PER_SEC;
+  animation.effect.updateTiming({ duration: 1000 * MS_PER_SEC });
   await waitForFrame();
 
   assert_animation_is_running_on_compositor(animation,
     'Animation reports that it is running on the compositor'
     + ' when restarted');
 }, 'animation is added to compositor' +
-   ' when timing.duration is made longer than the current time');
+   ' when the duration is made longer than the current time');
 
 promise_test(async t => {
   var animation = addDivAndAnimate(t,
                                    {},
                                    { opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
 
   await waitForPaints();
 
   assert_animation_is_running_on_compositor(animation,
     'Animation reports that it is running on the compositor');
 
-  animation.effect.timing.endDelay = 100 * MS_PER_SEC;
+  animation.effect.updateTiming({ endDelay: 100 * MS_PER_SEC });
 
   assert_animation_is_running_on_compositor(animation,
     'Animation reports that it is running on the compositor'
     + ' when endDelay is changed');
 
   animation.currentTime = 110 * MS_PER_SEC;
   await waitForFrame();
 
@@ -383,17 +383,17 @@ promise_test(async t => {
                                    {},
                                    { opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
 
   await waitForPaints();
 
   assert_animation_is_running_on_compositor(animation,
     'Animation reports that it is running on the compositor');
 
-  animation.effect.timing.endDelay = -200 * MS_PER_SEC;
+  animation.effect.updateTiming({ endDelay: -200 * MS_PER_SEC });
   await waitForFrame();
 
   assert_animation_is_not_running_on_compositor(animation,
     'Animation reports that it is NOT running on the compositor'
     + ' when endTime is negative value');
 }, 'animation is removed from compositor' +
    ' when endTime is negative value');
 
@@ -402,17 +402,17 @@ promise_test(async t => {
                                    {},
                                    { opacity: [ 0, 1 ] }, 200 * MS_PER_SEC);
 
   await waitForPaints();
 
   assert_animation_is_running_on_compositor(animation,
     'Animation reports that it is running on the compositor');
 
-  animation.effect.timing.endDelay = -100 * MS_PER_SEC;
+  animation.effect.updateTiming({ endDelay: -100 * MS_PER_SEC });
   await waitForFrame();
 
   assert_animation_is_running_on_compositor(animation,
     'Animation reports that it is running on the compositor'
     + ' when endTime is positive and endDelay is negative');
   animation.currentTime = 110 * MS_PER_SEC;
   await waitForFrame();
 
--- a/dom/animation/test/crashtests/1216842-1.html
+++ b/dom/animation/test/crashtests/1216842-1.html
@@ -10,17 +10,17 @@
     </style>
   </head>
   <body>
   <div id="target"></div>
   </body>
   <script>
     var target = document.getElementById("target");
     var effect =
-      new KeyframeEffectReadOnly(
+      new KeyframeEffect(
         target,
         { opacity: [0, 1] },
         {
           fill: "forwards",
           /* The function produces negative values in (0, 0.766312060) */
           easing: "cubic-bezier(0,-0.5,1,-0.5)",
           duration: 100,
           iterations: 0.75 /* To finish in the extraporation range */
--- a/dom/animation/test/crashtests/1216842-2.html
+++ b/dom/animation/test/crashtests/1216842-2.html
@@ -10,17 +10,17 @@
     </style>
   </head>
   <body>
   <div id="target"></div>
   </body>
   <script>
     var target = document.getElementById("target");
     var effect =
-      new KeyframeEffectReadOnly(
+      new KeyframeEffect(
         target,
         { opacity: [0, 1] },
         {
           fill: "forwards",
           /* The function produces values greater than 1 in (0.23368794, 1) */
           easing: "cubic-bezier(0,1.5,1,1.5)",
           duration: 100,
           iterations: 0.25 /* To finish in the extraporation range */
--- a/dom/animation/test/crashtests/1216842-3.html
+++ b/dom/animation/test/crashtests/1216842-3.html
@@ -4,17 +4,17 @@
     <title>Bug 1216842: effect-level easing function produces values greater than 1 (main-thread)</title>
   </head>
   <body>
   <div id="target"></div>
   </body>
   <script>
     var target = document.getElementById("target");
     var effect =
-      new KeyframeEffectReadOnly(
+      new KeyframeEffect(
         target,
         { color: ["red", "blue"] },
         {
           fill: "forwards",
           /* The function produces values greater than 1 in (0.23368794, 1) */
           easing: "cubic-bezier(0,1.5,1,1.5)",
           duration: 100
         }
--- a/dom/animation/test/crashtests/1216842-4.html
+++ b/dom/animation/test/crashtests/1216842-4.html
@@ -4,17 +4,17 @@
     <title>Bug 1216842: effect-level easing function produces negative values (main-thread)</title>
   </head>
   <body>
   <div id="target"></div>
   </body>
   <script>
     var target = document.getElementById("target");
     var effect =
-      new KeyframeEffectReadOnly(
+      new KeyframeEffect(
         target,
         { color: ["red", "blue"] },
         {
           fill: "forwards",
           /* The function produces negative values in (0, 0.766312060) */
           easing: "cubic-bezier(0,-0.5,1,-0.5)",
           duration: 100
         }
--- a/dom/animation/test/crashtests/1216842-5.html
+++ b/dom/animation/test/crashtests/1216842-5.html
@@ -13,17 +13,17 @@
     </style>
   </head>
   <body>
   <div id="target"></div>
   </body>
   <script>
     var target = document.getElementById("target");
     var effect =
-      new KeyframeEffectReadOnly(
+      new KeyframeEffect(
         target,
         { opacity: [0, 1], easing: "step-end" },
         {
           fill: "forwards",
           /* The function produces negative values in (0, 0.766312060) */
           easing: "cubic-bezier(0,-0.5,1,-0.5)",
           duration: 100,
           iterations: 0.75 /* To finish in the extraporation range */
--- a/dom/animation/test/crashtests/1216842-6.html
+++ b/dom/animation/test/crashtests/1216842-6.html
@@ -13,17 +13,17 @@
     </style>
   </head>
   <body>
   <div id="target"></div>
   </body>
   <script>
     var target = document.getElementById("target");
     var effect =
-      new KeyframeEffectReadOnly(
+      new KeyframeEffect(
         target,
         { opacity: [0, 1], easing: "step-end" },
         {
           fill: "forwards",
           /* The function produces values greater than 1 in (0.23368794, 1) */
           easing: "cubic-bezier(0,1.5,1,1.5)",
           duration: 100,
           iterations: 0.25 /* To finish in the extraporation range */
--- a/dom/animation/test/crashtests/1239889-1.html
+++ b/dom/animation/test/crashtests/1239889-1.html
@@ -2,15 +2,15 @@
 <html class="reftest-wait">
   <head>
     <title>Bug 1239889</title>
   </head>
   <body>
   </body>
   <script>
     var div = document.createElement('div');
-    var effect = new KeyframeEffectReadOnly(div, { opacity: [0, 1] });
+    var effect = new KeyframeEffect(div, { opacity: [0, 1] });
     requestAnimationFrame(() => {
       document.body.appendChild(div);
       document.documentElement.classList.remove("reftest-wait");
     });
   </script>
 </html>
--- a/dom/animation/test/crashtests/1359658-1.html
+++ b/dom/animation/test/crashtests/1359658-1.html
@@ -18,16 +18,16 @@
 const ancestor = document.getElementById('ancestor');
 const target   = document.getElementById('target');
 
 document.addEventListener('DOMContentLoaded', () => {
   const animation = target.animate({ color: [ 'red', 'lime' ] },
                                    { duration: 1000, iterations: Infinity });
   requestAnimationFrame(() => {
     // Tweak animation to cause animation dirty bit to be set
-    animation.effect.timing.duration = 2000;
+    animation.effect.updateTiming({ duration: 2000 });
     ancestor.style.display = "none";
     getComputedStyle(ancestor).display;
     document.documentElement.className = '';
   });
 });
   </script>
 </html>
--- a/dom/animation/test/css-animations/test_animation-computed-timing.html
+++ b/dom/animation/test/css-animations/test_animation-computed-timing.html
@@ -269,17 +269,17 @@ promise_test(function(t) {
   }).then(function() {
     assert_equals(anim.effect.getComputedTiming().localTime, anim.currentTime,
                   'localTime is equal to currentTime');
   });
 }, 'localTime reflects playbackRate immediately');
 
 test(function(t) {
   var div = addDiv(t);
-  var effect = new KeyframeEffectReadOnly(div, {left: ["0px", "100px"]});
+  var effect = new KeyframeEffect(div, {left: ["0px", "100px"]});
 
   assert_equals(effect.getComputedTiming().localTime, null,
                 'localTime for orphaned effect');
 }, 'localTime of an AnimationEffect without an Animation');
 
 
 // --------------------
 // progress
@@ -550,17 +550,17 @@ test(function(t) {
   // Finish
   anim.finish();
   assert_equals(anim.effect.getComputedTiming().currentIteration, 0,
                 'Value of currentIteration in after phase');
 }, 'currentIteration of an animation with a default iteration count');
 
 test(function(t) {
   var div = addDiv(t);
-  var effect = new KeyframeEffectReadOnly(div, {left: ["0px", "100px"]});
+  var effect = new KeyframeEffect(div, {left: ["0px", "100px"]});
 
   assert_equals(effect.getComputedTiming().currentIteration, null,
                 'currentIteration for orphaned effect');
 }, 'currentIteration of an AnimationEffect without an Animation');
 
 // TODO: If iteration duration is Infinity, currentIteration is 0.
 // However, we cannot set iteration duration to Infinity in CSS Animation now.
 
--- a/dom/animation/test/css-animations/test_effect-target.html
+++ b/dom/animation/test/css-animations/test_effect-target.html
@@ -34,29 +34,29 @@ test(function(t) {
   assert_equals(anims[0].effect.target, anims[1].effect.target,
                 'Both animations return the same target object');
 }, 'effect.target should return the same CSSPseudoElement object each time');
 
 test(function(t) {
   addStyle(t, { '.after::after': 'animation: anim 10s;' });
   var div = addDiv(t, { class: 'after' });
   var pseudoTarget = document.getAnimations()[0].effect.target;
-  var effect = new KeyframeEffectReadOnly(pseudoTarget,
-                                          { background: ["blue", "red"] },
-                                          3 * MS_PER_SEC);
+  var effect = new KeyframeEffect(pseudoTarget,
+                                  { background: ["blue", "red"] },
+                                  3 * MS_PER_SEC);
   var newAnim = new Animation(effect, document.timeline);
   newAnim.play();
   var anims = document.getAnimations();
   assert_equals(anims.length, 2,
                 'Got animations running on ::after pseudo element');
   assert_not_equals(anims[0], newAnim,
                     'The scriped-generated animation appears last');
   assert_equals(newAnim.effect.target, pseudoTarget,
                 'The effect.target of the scripted-generated animation is ' +
                 'the same as the one from the argument of ' +
-                'KeyframeEffectReadOnly constructor');
+                'KeyframeEffect constructor');
   assert_equals(anims[0].effect.target, newAnim.effect.target,
                 'Both animations return the same target object');
 }, 'effect.target from the script-generated animation should return the same ' +
    'CSSPseudoElement object as that from the CSS generated animation');
 
 </script>
 </body>
--- a/dom/animation/test/css-animations/test_keyframeeffect-getkeyframes.html
+++ b/dom/animation/test/css-animations/test_keyframeeffect-getkeyframes.html
@@ -210,17 +210,17 @@ test(function(t) {
   assert_equals(getKeyframes(div).length, 0,
                 "number of frames when @keyframes only has keyframes with " +
                 "animation-timing-function");
 
   div.style.animation = 'anim-only-non-animatable 100s';
   assert_equals(getKeyframes(div).length, 0,
                 "number of frames when @keyframes only has frames with " +
                 "non-animatable properties");
-}, 'KeyframeEffectReadOnly.getKeyframes() returns no frames for various kinds'
+}, 'KeyframeEffect.getKeyframes() returns no frames for various kinds'
    + ' of empty enimations');
 
 test(function(t) {
   var div = addDiv(t);
 
   div.style.animation = 'anim-simple 100s';
   var frames = getKeyframes(div);
 
@@ -231,67 +231,67 @@ test(function(t) {
       color: "rgb(0, 0, 0)", composite: null },
     { offset: 1, computedOffset: 1, easing: "ease",
       color: "rgb(255, 255, 255)", composite: null },
   ];
 
   for (var i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
-}, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for a simple'
+}, 'KeyframeEffect.getKeyframes() returns expected frames for a simple'
    + ' animation');
 
 test(function(t) {
   kTimingFunctionValues.forEach(function(easing) {
     var div = addDiv(t);
 
     div.style.animation = 'anim-simple-three 100s ' + easing;
     var frames = getKeyframes(div);
 
     assert_equals(frames.length, 3, "number of frames");
 
     for (var i = 0; i < frames.length; i++) {
       assert_equals(frames[i].easing, easing,
                     "value for 'easing' on ComputedKeyframe #" + i);
     }
   });
-}, 'KeyframeEffectReadOnly.getKeyframes() returns frames with expected easing'
+}, 'KeyframeEffect.getKeyframes() returns frames with expected easing'
    + ' values, when the easing comes from animation-timing-function on the'
    + ' element');
 
 test(function(t) {
   var div = addDiv(t);
 
   div.style.animation = 'anim-simple-timing 100s';
   var frames = getKeyframes(div);
 
   assert_equals(frames.length, 3, "number of frames");
   assert_equals(frames[0].easing, "linear",
                 "value of 'easing' on ComputedKeyframe #0");
   assert_equals(frames[1].easing, "ease-in-out",
                 "value of 'easing' on ComputedKeyframe #1");
   assert_equals(frames[2].easing, "steps(1)",
                 "value of 'easing' on ComputedKeyframe #2");
-}, 'KeyframeEffectReadOnly.getKeyframes() returns frames with expected easing'
+}, 'KeyframeEffect.getKeyframes() returns frames with expected easing'
    + ' values, when the easing is specified on each keyframe');
 
 test(function(t) {
   var div = addDiv(t);
 
   div.style.animation = 'anim-simple-timing-some 100s step-start';
   var frames = getKeyframes(div);
 
   assert_equals(frames.length, 3, "number of frames");
   assert_equals(frames[0].easing, "linear",
                 "value of 'easing' on ComputedKeyframe #0");
   assert_equals(frames[1].easing, "steps(1, start)",
                 "value of 'easing' on ComputedKeyframe #1");
   assert_equals(frames[2].easing, "steps(1, start)",
                 "value of 'easing' on ComputedKeyframe #2");
-}, 'KeyframeEffectReadOnly.getKeyframes() returns frames with expected easing'
+}, 'KeyframeEffect.getKeyframes() returns frames with expected easing'
    + ' values, when the easing is specified on some keyframes');
 
 test(function(t) {
   var div = addDiv(t);
 
   div.style.animation = 'anim-simple-shorthand 100s';
   var frames = getKeyframes(div);
 
@@ -304,17 +304,17 @@ test(function(t) {
     { offset: 1, computedOffset: 1, easing: "ease", composite: null,
       marginBottom: "16px", marginLeft: "16px",
       marginRight: "16px", marginTop: "16px" },
   ];
 
   for (var i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
-}, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for a simple'
+}, 'KeyframeEffect.getKeyframes() returns expected frames for a simple'
    + ' animation that specifies a single shorthand property');
 
 test(function(t) {
   var div = addDiv(t);
 
   div.style.animation = 'anim-omit-to 100s';
   div.style.color = 'rgb(255, 255, 255)';
   var frames = getKeyframes(div);
@@ -326,17 +326,17 @@ test(function(t) {
       color: "rgb(0, 0, 255)" },
     { offset: 1, computedOffset: 1, easing: "ease", composite: null,
       color: "rgb(255, 255, 255)" },
   ];
 
   for (var i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
-}, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for an ' +
+}, 'KeyframeEffect.getKeyframes() returns expected frames for an ' +
    'animation with a 0% keyframe and no 100% keyframe');
 
 test(function(t) {
   var div = addDiv(t);
 
   div.style.animation = 'anim-omit-from 100s';
   div.style.color = 'rgb(255, 255, 255)';
   var frames = getKeyframes(div);
@@ -348,17 +348,17 @@ test(function(t) {
       color: "rgb(255, 255, 255)" },
     { offset: 1, computedOffset: 1, easing: "ease", composite: null,
       color: "rgb(0, 0, 255)" },
   ];
 
   for (var i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
-}, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for an ' +
+}, 'KeyframeEffect.getKeyframes() returns expected frames for an ' +
    'animation with a 100% keyframe and no 0% keyframe');
 
 test(function(t) {
   var div = addDiv(t);
 
   div.style.animation = 'anim-omit-from-to 100s';
   div.style.color = 'rgb(255, 255, 255)';
   var frames = getKeyframes(div);
@@ -372,17 +372,17 @@ test(function(t) {
       color: "rgb(0, 0, 255)" },
     { offset: 1,   computedOffset: 1,   easing: "ease", composite: null,
       color: "rgb(255, 255, 255)" },
   ];
 
   for (var i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
-}, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for an ' +
+}, 'KeyframeEffect.getKeyframes() returns expected frames for an ' +
    'animation with no 0% or 100% keyframe but with a 50% keyframe');
 
 test(function(t) {
   var div = addDiv(t);
 
   div.style.animation = 'anim-partially-omit-to 100s';
   div.style.marginTop = '250px';
   var frames = getKeyframes(div);
@@ -394,17 +394,17 @@ test(function(t) {
       marginTop: '50px', marginBottom: '100px' },
     { offset: 1, computedOffset: 1, easing: "ease", composite: null,
       marginTop: '250px', marginBottom: '200px' },
   ];
 
   for (var i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
-}, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for an ' +
+}, 'KeyframeEffect.getKeyframes() returns expected frames for an ' +
    'animation with a partially complete 100% keyframe (because the ' +
    '!important rule is ignored)');
 
 test(function(t) {
   var div = addDiv(t);
 
   div.style.animation = 'anim-different-props 100s';
   var frames = getKeyframes(div);
@@ -420,17 +420,17 @@ test(function(t) {
       marginTop: "12px" },
     { offset: 1, computedOffset: 1, easing: "ease", composite: null,
       color: "rgb(255, 255, 255)", marginTop: "16px" },
   ];
 
   for (var i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
-}, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for an ' +
+}, 'KeyframeEffect.getKeyframes() returns expected frames for an ' +
    'animation with different properties on different keyframes, all ' +
    'with the same easing function');
 
 test(function(t) {
   var div = addDiv(t);
 
   div.style.animation = 'anim-different-props-and-easing 100s';
   var frames = getKeyframes(div);
@@ -446,17 +446,17 @@ test(function(t) {
       marginTop: "12px" },
     { offset: 1, computedOffset: 1, easing: "ease", composite: null,
       color: "rgb(255, 255, 255)", marginTop: "16px" },
   ];
 
   for (var i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
-}, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for an ' +
+}, 'KeyframeEffect.getKeyframes() returns expected frames for an ' +
    'animation with different properties on different keyframes, with ' +
    'a different easing function on each');
 
 test(function(t) {
   var div = addDiv(t);
 
   div.style.animation = 'anim-merge-offset 100s';
   var frames = getKeyframes(div);
@@ -468,17 +468,17 @@ test(function(t) {
       color: "rgb(0, 0, 0)", marginTop: "8px" },
     { offset: 1, computedOffset: 1, easing: "ease", composite: null,
       color: "rgb(255, 255, 255)", marginTop: "16px" },
   ];
 
   for (var i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
-}, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for an ' +
+}, 'KeyframeEffect.getKeyframes() returns expected frames for an ' +
    'animation with multiple keyframes for the same time, and all with ' +
    'the same easing function');
 
 test(function(t) {
   var div = addDiv(t);
 
   div.style.animation = 'anim-merge-offset-and-easing 100s';
   var frames = getKeyframes(div);
@@ -493,17 +493,17 @@ test(function(t) {
     { offset: 1, computedOffset: 1, easing: "ease", composite: null,
       color: "rgb(255, 255, 255)", fontSize: "32px", marginTop: "16px",
       paddingLeft: "4px" },
   ];
 
   for (var i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
-}, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for an ' +
+}, 'KeyframeEffect.getKeyframes() returns expected frames for an ' +
    'animation with multiple keyframes for the same time and with ' +
    'different easing functions');
 
 test(function(t) {
   var div = addDiv(t);
 
   div.style.animation = 'anim-no-merge-equiv-easing 100s';
   var frames = getKeyframes(div);
@@ -517,17 +517,17 @@ test(function(t) {
       marginTop: "10px", marginRight: "10px", marginBottom: "10px" },
     { offset: 1, computedOffset: 1, easing: "ease", composite: null,
       marginTop: "20px", marginRight: "20px", marginBottom: "20px" },
   ];
 
   for (var i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
-}, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for an ' +
+}, 'KeyframeEffect.getKeyframes() returns expected frames for an ' +
    'animation with multiple keyframes for the same time and with ' +
    'different but equivalent easing functions');
 
 test(function(t) {
   var div = addDiv(t);
 
   div.style.animation = 'anim-overriding 100s';
   var frames = getKeyframes(div);
@@ -547,17 +547,17 @@ test(function(t) {
       paddingTop: "60px" },
     { offset: 1, computedOffset: 1, easing: "ease", composite: null,
       paddingTop: "70px" },
   ];
 
   for (var i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
-}, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for ' +
+}, 'KeyframeEffect.getKeyframes() returns expected frames for ' +
    'overlapping keyframes');
 
 // Gecko-specific test case: We are specifically concerned here that the
 // computed value for filter, "none", is correctly represented.
 
 test(function(t) {
   var div = addDiv(t);
 
@@ -571,17 +571,17 @@ test(function(t) {
       filter: "none" },
     { offset: 1, computedOffset: 1, easing: "ease", composite: null,
       filter: "blur(5px) sepia(60%) saturate(30%)" },
   ];
 
   for (var i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
-}, 'KeyframeEffectReadOnly.getKeyframes() returns expected values for ' +
+}, 'KeyframeEffect.getKeyframes() returns expected values for ' +
    'animations with filter properties and missing keyframes');
 
 test(function(t) {
   var div = addDiv(t);
 
   div.style.animation = 'anim-filter-drop-shadow 100s';
   var frames = getKeyframes(div);
 
@@ -592,17 +592,17 @@ test(function(t) {
       filter: "drop-shadow(rgb(0, 255, 0) 10px 10px 10px)" },
     { offset: 1, computedOffset: 1, easing: "ease", composite: null,
       filter: "drop-shadow(rgb(255, 0, 0) 50px 30px 10px)" },
   ];
 
   for (var i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
-}, 'KeyframeEffectReadOnly.getKeyframes() returns expected values for ' +
+}, 'KeyframeEffect.getKeyframes() returns expected values for ' +
    'animation with drop-shadow of filter property');
 
 // Gecko-specific test case: We are specifically concerned here that the
 // computed value for text-shadow and a "none" specified on a keyframe
 // are correctly represented.
 
 test(function(t) {
   var div = addDiv(t);
@@ -622,17 +622,17 @@ test(function(t) {
                   + " rgb(0, 0, 255) 0px 0px 3.2px" },
     { offset: 1, computedOffset: 1, easing: "ease", composite: null,
       textShadow: "none" },
   ];
 
   for (var i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
-}, 'KeyframeEffectReadOnly.getKeyframes() returns expected values for ' +
+}, 'KeyframeEffect.getKeyframes() returns expected values for ' +
    'animations with text-shadow properties and missing keyframes');
 
 // Gecko-specific test case: We are specifically concerned here that the
 // initial value for background-size and the specified list are correctly
 // represented.
 
 test(function(t) {
   var div = addDiv(t);
@@ -658,17 +658,17 @@ test(function(t) {
   expected[0].backgroundSize = div.style.backgroundSize =
     "30px auto, 40% auto, auto auto";
   frames = getKeyframes(div);
 
   for (var i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i
                         + " after updating current style");
   }
-}, 'KeyframeEffectReadOnly.getKeyframes() returns expected values for ' +
+}, 'KeyframeEffect.getKeyframes() returns expected values for ' +
    'animations with background-size properties and missing keyframes');
 
 test(function(t) {
   var div = addDiv(t);
   div.style.animation = 'anim-variables 100s';
 
   var frames = getKeyframes(div);
 
@@ -678,17 +678,17 @@ test(function(t) {
     { offset: 0, computedOffset: 0, easing: "ease", composite: null,
       transform: "none" },
     { offset: 1, computedOffset: 1, easing: "ease", composite: null,
       transform: "translate(100px, 0px)" },
   ];
   for (var i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
-}, 'KeyframeEffectReadOnly.getKeyframes() returns expected values for ' +
+}, 'KeyframeEffect.getKeyframes() returns expected values for ' +
    'animations with CSS variables as keyframe values');
 
 test(function(t) {
   var div = addDiv(t);
   div.style.animation = 'anim-variables-shorthand 100s';
 
   var frames = getKeyframes(div);
 
@@ -704,17 +704,17 @@ test(function(t) {
       marginBottom: "100px",
       marginLeft: "100px",
       marginRight: "100px",
       marginTop: "100px" },
   ];
   for (var i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
-}, 'KeyframeEffectReadOnly.getKeyframes() returns expected values for ' +
+}, 'KeyframeEffect.getKeyframes() returns expected values for ' +
    'animations with CSS variables as keyframe values in a shorthand property');
 
 test(function(t) {
   var div = addDiv(t);
   div.style.animation = 'anim-custom-property-in-keyframe 100s';
 
   var frames = getKeyframes(div);
 
@@ -724,17 +724,17 @@ test(function(t) {
     { offset: 0, computedOffset: 0, easing: "ease", composite: null,
       color: "rgb(0, 0, 0)" },
     { offset: 1, computedOffset: 1, easing: "ease", composite: null,
       color: "rgb(0, 255, 0)" },
   ];
   for (var i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
-}, 'KeyframeEffectReadOnly.getKeyframes() returns expected values for ' +
+}, 'KeyframeEffect.getKeyframes() returns expected values for ' +
    'animations with a CSS variable which is overriden by the value in keyframe');
 
 test(function(t) {
   var div = addDiv(t);
   div.style.animation = 'anim-only-custom-property-in-keyframe 100s';
 
   var frames = getKeyframes(div);
 
@@ -744,13 +744,13 @@ test(function(t) {
     { offset: 0, computedOffset: 0, easing: "ease", composite: null,
       transform: "translate(100px, 0px)" },
     { offset: 1, computedOffset: 1, easing: "ease", composite: null,
       transform: "none" },
   ];
   for (var i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
-}, 'KeyframeEffectReadOnly.getKeyframes() returns expected values for ' +
+}, 'KeyframeEffect.getKeyframes() returns expected values for ' +
    'animations with only custom property in a keyframe');
 
 </script>
 </body>
--- a/dom/animation/test/css-animations/test_pseudoElement-get-animations.html
+++ b/dom/animation/test/css-animations/test_pseudoElement-get-animations.html
@@ -40,19 +40,19 @@ test(function(t) {
 test(function(t) {
   var div = addDiv(t, { class: 'after-with-mix-anims-trans' });
   // Trigger transitions
   flushComputedStyle(div);
   div.classList.add('after-change');
 
   // Create additional animation on the pseudo-element from script
   var pseudoTarget = document.getAnimations()[0].effect.target;
-  var effect = new KeyframeEffectReadOnly(pseudoTarget,
-                                          { background: ["blue", "red"] },
-                                          3 * MS_PER_SEC);
+  var effect = new KeyframeEffect(pseudoTarget,
+                                  { background: ["blue", "red"] },
+                                  3 * MS_PER_SEC);
   var newAnimation = new Animation(effect, document.timeline);
   newAnimation.id = 'scripted-anim';
   newAnimation.play();
 
   // Check order - the script-generated animation should appear later
   var anims = pseudoTarget.getAnimations();
   assert_equals(anims.length, 5,
                 'Got expected number of animations/trnasitions running on ' +
--- a/dom/animation/test/css-transitions/test_effect-target.html
+++ b/dom/animation/test/css-transitions/test_effect-target.html
@@ -38,31 +38,31 @@ test(function(t) {
 
 test(function(t) {
   addStyle(t, { '.init::after': 'content: ""; width: 0px; transition: all 10s;',
                 '.change::after': 'width: 100px;' });
   var div = addDiv(t, { class: 'init' });
   flushComputedStyle(div);
   div.classList.add('change');
   var pseudoTarget = document.getAnimations()[0].effect.target;
-  var effect = new KeyframeEffectReadOnly(pseudoTarget,
-                                          { background: ["blue", "red"] },
-                                          3000);
+  var effect = new KeyframeEffect(pseudoTarget,
+                                  { background: ["blue", "red"] },
+                                  3000);
   var newAnim = new Animation(effect, document.timeline);
   newAnim.play();
 
   var anims = document.getAnimations();
   assert_equals(anims.length, 2,
                 'Got animations running on ::after pseudo element');
   assert_not_equals(anims[0], newAnim,
                     'The scriped-generated animation appears last');
   assert_equals(newAnim.effect.target, pseudoTarget,
                 'The effect.target of the scripted-generated animation is ' +
                 'the same as the one from the argument of ' +
-                'KeyframeEffectReadOnly constructor');
+                'KeyframeEffect constructor');
   assert_equals(anims[0].effect.target, newAnim.effect.target,
                 'Both the transition and the scripted-generated animation ' +
                 'return the same target object');
 }, 'effect.target from the script-generated animation should return the same ' +
    'CSSPseudoElement object as that from the CSS generated transition');
 
 </script>
 </body>
--- a/dom/animation/test/css-transitions/test_event-dispatch.html
+++ b/dom/animation/test/css-transitions/test_event-dispatch.html
@@ -345,18 +345,18 @@ promise_test(t => {
   });
 }, 'Calculating the interval start and end time with negative start delay.');
 
 promise_test(t => {
   const { transition, watcher, div, handler } =
     setupTransition(t, 'margin-left 100s 100s');
 
   return watcher.wait_for('transitionrun').then(evt => {
-    // We can't set the end delay via generated effect timing.
-    // Because CSS-Transition use the AnimationEffectTimingReadOnly.
+    // We can't set the end delay via generated effect timing
+    // because mutating CSS transitions is not specced yet.
     transition.effect = new KeyframeEffect(div,
                                            { marginleft: [ '0px', '100px' ]},
                                            { duration: 100 * MS_PER_SEC,
                                              endDelay: -50 * MS_PER_SEC });
     // Seek to Before and play.
     transition.cancel();
     transition.play();
     return watcher.wait_for([ 'transitioncancel',
--- a/dom/animation/test/css-transitions/test_keyframeeffect-getkeyframes.html
+++ b/dom/animation/test/css-transitions/test_keyframeeffect-getkeyframes.html
@@ -43,17 +43,17 @@ test(function(t) {
       left: "0px" },
     { offset: 1, computedOffset: 1, easing: "linear", composite: null,
       left: "100px" },
   ];
 
   for (var i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
-}, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for a simple'
+}, 'KeyframeEffect.getKeyframes() returns expected frames for a simple'
    + ' transition');
 
 test(function(t) {
   var div = addDiv(t);
 
   div.style.left = '0px';
   getComputedStyle(div).transitionProperty;
   div.style.transition = 'left 100s steps(2,end)';
@@ -68,17 +68,17 @@ test(function(t) {
       left: "0px" },
     { offset: 1, computedOffset: 1, easing: "linear", composite: null,
       left: "100px" },
   ];
 
   for (var i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
-}, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for a simple'
+}, 'KeyframeEffect.getKeyframes() returns expected frames for a simple'
    + ' transition with a non-default easing function');
 
 test(function(t) {
   var div = addDiv(t);
   div.style.left = '0px';
   getComputedStyle(div).transitionProperty;
   div.style.transition = 'left 100s';
   div.style.left = 'var(--var-100px)';
@@ -91,13 +91,13 @@ test(function(t) {
     { offset: 0, computedOffset: 0, easing: 'ease', composite: null,
       left: '0px' },
     { offset: 1, computedOffset: 1, easing: 'linear', composite: null,
       left: '100px' },
   ];
   for (var i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
-}, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for a'
+}, 'KeyframeEffect.getKeyframes() returns expected frames for a'
    + ' transition with a CSS variable endpoint');
 
 </script>
 </body>
--- a/dom/animation/test/mozilla/file_restyles.html
+++ b/dom/animation/test/mozilla/file_restyles.html
@@ -1144,21 +1144,21 @@ waitForAllPaints(() => {
 
     // Set currentTime to a time longer than duration.
     animation.currentTime = 500 * MS_PER_SEC;
 
     // Now the animation immediately get back from compositor.
     ok(!SpecialPowers.wrap(animation).isRunningOnCompositor);
 
     // Extend the duration.
-    animation.effect.timing.duration = 800 * MS_PER_SEC;
+    animation.effect.updateTiming({ duration: 800 * MS_PER_SEC });
     var markers = await observeStyling(5);
     is(markers.length, 1,
        'Animations running on the compositor should update style ' +
-       'when timing.duration is made longer than the current time');
+       'when duration is made longer than the current time');
 
     await ensureElementRemoval(div);
   });
 
   add_task(async function script_animation_on_display_none_element() {
     var div = addDiv(null);
     var animation = div.animate({ backgroundColor: [ 'red', 'blue' ] },
                                 100 * MS_PER_SEC);
--- a/dom/animation/test/mozilla/test_cubic_bezier_limits.html
+++ b/dom/animation/test/mozilla/test_cubic_bezier_limits.html
@@ -20,33 +20,33 @@
 // We clamp +infinity or -inifinity value in floating point to
 // maximum floating point value or -maxinum floating point value.
 const max_float = 3.40282e+38;
 
 test(function(t) {
   var div = addDiv(t);
   var anim = div.animate({ }, 100 * MS_PER_SEC);
 
-  anim.effect.timing.easing = 'cubic-bezier(0, 1e+39, 0, 0)';
-  assert_equals(anim.effect.timing.easing,
+  anim.effect.updateTiming({ easing: 'cubic-bezier(0, 1e+39, 0, 0)' });
+  assert_equals(anim.effect.getComputedTiming().easing,
     'cubic-bezier(0, ' + max_float + ', 0, 0)',
     'y1 control point for effect easing is out of upper boundary');
 
-  anim.effect.timing.easing = 'cubic-bezier(0, 0, 0, 1e+39)';
-  assert_equals(anim.effect.timing.easing,
+  anim.effect.updateTiming({ easing: 'cubic-bezier(0, 0, 0, 1e+39)' });
+  assert_equals(anim.effect.getComputedTiming().easing,
     'cubic-bezier(0, 0, 0, ' + max_float + ')',
     'y2 control point for effect easing is out of upper boundary');
 
-  anim.effect.timing.easing = 'cubic-bezier(0, -1e+39, 0, 0)';
-  assert_equals(anim.effect.timing.easing,
+  anim.effect.updateTiming({ easing: 'cubic-bezier(0, -1e+39, 0, 0)' });
+  assert_equals(anim.effect.getComputedTiming().easing,
     'cubic-bezier(0, ' + -max_float + ', 0, 0)',
     'y1 control point for effect easing is out of lower boundary');
 
-  anim.effect.timing.easing = 'cubic-bezier(0, 0, 0, -1e+39)';
-  assert_equals(anim.effect.timing.easing,
+  anim.effect.updateTiming({ easing: 'cubic-bezier(0, 0, 0, -1e+39)' });
+  assert_equals(anim.effect.getComputedTiming().easing,
     'cubic-bezier(0, 0, 0, ' + -max_float + ')',
     'y2 control point for effect easing is out of lower boundary');
 
 }, 'Clamp y1 and y2 control point out of boundaries for effect easing' );
 
 test(function(t) {
   var div = addDiv(t);
   var anim = div.animate({ }, 100 * MS_PER_SEC);
@@ -139,26 +139,26 @@ test(function(t) {
 }, 'Clamp y1 and y2 control point out of boundaries for CSS transition' );
 
 test(function(t) {
   var div = addDiv(t);
   var anim = div.animate({ }, { duration: 100 * MS_PER_SEC, fill: 'forwards' });
 
   anim.pause();
   // The positive steepest function on both edges.
-  anim.effect.timing.easing = 'cubic-bezier(0, 1e+39, 0, 1e+39)';
+  anim.effect.updateTiming({ easing: 'cubic-bezier(0, 1e+39, 0, 1e+39)' });
   assert_equals(anim.effect.getComputedTiming().progress, 0.0,
     'progress on lower edge for the highest value of y1 and y2 control points');
 
   anim.finish();
   assert_equals(anim.effect.getComputedTiming().progress, 1.0,
     'progress on upper edge for the highest value of y1 and y2 control points');
 
   // The negative steepest function on both edges.
-  anim.effect.timing.easing = 'cubic-bezier(0, -1e+39, 0, -1e+39)';
+  anim.effect.updateTiming({ easing: 'cubic-bezier(0, -1e+39, 0, -1e+39)' });
   anim.currentTime = 0;
   assert_equals(anim.effect.getComputedTiming().progress, 0.0,
     'progress on lower edge for the lowest value of y1 and y2 control points');
 
   anim.finish();
   assert_equals(anim.effect.getComputedTiming().progress, 1.0,
     'progress on lower edge for the lowest value of y1 and y2 control points');
 
--- a/dom/animation/test/mozilla/test_set_easing.html
+++ b/dom/animation/test/mozilla/test_set_easing.html
@@ -13,17 +13,17 @@
 
 test(function(t) {
   const div = document.createElement("div");
   document.body.appendChild(div);
   div.animate({ opacity: [0, 1] }, 100000 );
 
   const contentScript = function() {
     try {
-      document.getAnimations()[0].effect.timing.easing = "linear";
+      document.getAnimations()[0].effect.updateTiming({ easing: 'linear' });
       assert_true(true, 'Setting easing should not throw in sandbox');
     } catch (e) {
       assert_unreached('Setting easing threw ' + e);
     }
   };
 
   const sandbox = new SpecialPowers.Cu.Sandbox(window);
   sandbox.importFunction(document, "document");
--- a/dom/animation/test/style/test_animation-setting-effect.html
+++ b/dom/animation/test/style/test_animation-setting-effect.html
@@ -11,19 +11,19 @@
     <div id="log"></div>
     <script type='text/javascript'>
 
 'use strict';
 
 test(function(t) {
   var target = addDiv(t);
   var anim = new Animation();
-  anim.effect = new KeyframeEffectReadOnly(target,
-                                           { marginLeft: [ '0px', '100px' ] },
-                                           100 * MS_PER_SEC);
+  anim.effect = new KeyframeEffect(target,
+                                   { marginLeft: [ '0px', '100px' ] },
+                                   100 * MS_PER_SEC);
   anim.currentTime = 50 * MS_PER_SEC;
   assert_equals(getComputedStyle(target).marginLeft, '50px');
 }, 'After setting target effect on an animation with null effect, the ' +
    'animation still works');
 
 test(function(t) {
   var target = addDiv(t);
   target.style.marginLeft = '10px';
--- a/dom/animation/test/style/test_missing-keyframe-on-compositor.html
+++ b/dom/animation/test/style/test_missing-keyframe-on-compositor.html
@@ -208,18 +208,20 @@ promise_test(t => {
   var lowerAnimation;
   return useTestRefreshMode(t).then(() => {
     div = addDiv(t);
     lowerAnimation = div.animate({ opacity: [ 1, 0.5 ] }, 100 * MS_PER_SEC);
     var higherAnimation = div.animate({ opacity: 1 }, 100 * MS_PER_SEC);
 
     return waitForPaintsFlushed();
   }).then(() => {
-    lowerAnimation.effect.timing.duration = 0;
-    lowerAnimation.effect.timing.fill = 'forwards';
+    lowerAnimation.effect.updateTiming({
+      duration: 0,
+      fill: 'forwards',
+    });
     return waitForPaintsFlushed();
   }).then(() => {
     SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC);
 
     var opacity =
       SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'opacity');
     // The underlying value is the value that is filling forwards state of the
     // lowerAnimation, that is 0.5.
--- a/dom/base/Element.cpp
+++ b/dom/base/Element.cpp
@@ -131,18 +131,18 @@
 #include "nsStyledElement.h"
 #include "nsXBLService.h"
 #include "nsITextControlElement.h"
 #include "nsITextControlFrame.h"
 #include "nsISupportsImpl.h"
 #include "mozilla/dom/CSSPseudoElement.h"
 #include "mozilla/dom/DocumentFragment.h"
 #include "mozilla/dom/ElementBinding.h"
+#include "mozilla/dom/KeyframeEffectBinding.h"
 #include "mozilla/dom/KeyframeEffect.h"
-#include "mozilla/dom/KeyframeEffectBinding.h"
 #include "mozilla/dom/MouseEventBinding.h"
 #include "mozilla/dom/WindowBinding.h"
 #include "mozilla/dom/VRDisplay.h"
 #include "mozilla/IntegerPrintfMacros.h"
 #include "mozilla/Preferences.h"
 #include "nsComputedDOMStyle.h"
 #include "nsDOMStringMap.h"
 #include "DOMIntersectionObserver.h"
@@ -3771,17 +3771,17 @@ Element::GetAnimationsUnsorted(Element* 
              "Unsupported pseudo type");
   MOZ_ASSERT(aElement, "Null element");
 
   EffectSet* effects = EffectSet::GetEffectSet(aElement, aPseudoType);
   if (!effects) {
     return;
   }
 
-  for (KeyframeEffectReadOnly* effect : *effects) {
+  for (KeyframeEffect* effect : *effects) {
     MOZ_ASSERT(effect && effect->GetAnimation(),
                "Only effects associated with an animation should be "
                "added to an element's effect set");
     Animation* animation = effect->GetAnimation();
 
     MOZ_ASSERT(animation->IsRelevant(),
                "Only relevant animations should be added to an element's "
                "effect set");
--- a/dom/base/nsDOMMutationObserver.cpp
+++ b/dom/base/nsDOMMutationObserver.cpp
@@ -6,17 +6,17 @@
 
 #include "nsDOMMutationObserver.h"
 
 #include "mozilla/AnimationTarget.h"
 #include "mozilla/Maybe.h"
 #include "mozilla/OwningNonNull.h"
 
 #include "mozilla/dom/Animation.h"
-#include "mozilla/dom/KeyframeEffectReadOnly.h"
+#include "mozilla/dom/KeyframeEffect.h"
 #include "mozilla/dom/DocGroup.h"
 
 #include "nsContentUtils.h"
 #include "nsCSSPseudoElements.h"
 #include "nsError.h"
 #include "nsIScriptGlobalObject.h"
 #include "nsServiceManagerUtils.h"
 #include "nsTextFragment.h"
@@ -375,23 +375,22 @@ void nsMutationReceiver::NodeWillBeDestr
   NS_ASSERTION(!mParent, "Shouldn't have mParent here!");
   Disconnect(true);
 }
 
 void
 nsAnimationReceiver::RecordAnimationMutation(Animation* aAnimation,
                                              AnimationMutation aMutationType)
 {
-  mozilla::dom::AnimationEffectReadOnly* effect = aAnimation->GetEffect();
+  mozilla::dom::AnimationEffect* effect = aAnimation->GetEffect();
   if (!effect) {
     return;
   }
 
-  mozilla::dom::KeyframeEffectReadOnly* keyframeEffect =
-    effect->AsKeyframeEffect();
+  mozilla::dom::KeyframeEffect* keyframeEffect = effect->AsKeyframeEffect();
   if (!keyframeEffect) {
     return;
   }
 
   Maybe<NonOwningAnimationTarget> animationTarget = keyframeEffect->GetTarget();
   if (!animationTarget) {
     return;
   }
--- a/dom/base/nsDocument.cpp
+++ b/dom/base/nsDocument.cpp
@@ -9962,20 +9962,23 @@ nsIDocument::CaretPositionFromPoint(floa
   }
 
   nsIFrame *ptFrame = nsLayoutUtils::GetFrameForPoint(rootFrame, pt,
       nsLayoutUtils::IGNORE_PAINT_SUPPRESSION | nsLayoutUtils::IGNORE_CROSS_DOC);
   if (!ptFrame) {
     return nullptr;
   }
 
-  // GetContentOffsetsFromPoint requires frame-relative coordinates, so we need
-  // to adjust to frame-relative coordinates before we can perform this call.
-  // It should also not take into account the padding of the frame.
-  nsPoint adjustedPoint = pt - ptFrame->GetOffsetTo(rootFrame);
+  // We require frame-relative coordinates for GetContentOffsetsFromPoint.
+  nsPoint aOffset;
+  nsCOMPtr<nsIWidget> widget = nsContentUtils::GetWidget(ps, &aOffset);
+  LayoutDeviceIntPoint refPoint =
+    nsContentUtils::ToWidgetPoint(CSSPoint(aX, aY), aOffset, GetPresContext());
+  nsPoint adjustedPoint =
+    nsLayoutUtils::GetEventCoordinatesRelativeTo(widget, refPoint, ptFrame);
 
   nsFrame::ContentOffsets offsets =
     ptFrame->GetContentOffsetsFromPoint(adjustedPoint);
 
   nsCOMPtr<nsIContent> node = offsets.content;
   uint32_t offset = offsets.offset;
   nsCOMPtr<nsIContent> anonNode = node;
   bool nodeIsAnonymous = node && node->IsInNativeAnonymousSubtree();
--- a/dom/base/nsNodeUtils.cpp
+++ b/dom/base/nsNodeUtils.cpp
@@ -25,17 +25,17 @@
 #include "nsBindingManager.h"
 #include "nsGenericHTMLElement.h"
 #include "mozilla/AnimationTarget.h"
 #include "mozilla/Assertions.h"
 #include "mozilla/ErrorResult.h"
 #include "mozilla/dom/Animation.h"
 #include "mozilla/dom/HTMLImageElement.h"
 #include "mozilla/dom/HTMLMediaElement.h"
-#include "mozilla/dom/KeyframeEffectReadOnly.h"
+#include "mozilla/dom/KeyframeEffect.h"
 #include "nsWrapperCacheInlines.h"
 #include "nsObjectLoadingContent.h"
 #include "nsDOMMutationObserver.h"
 #include "mozilla/dom/BindingUtils.h"
 #include "mozilla/dom/HTMLTemplateElement.h"
 #include "mozilla/dom/ShadowRoot.h"
 
 using namespace mozilla;
@@ -230,17 +230,17 @@ nsNodeUtils::ContentRemoved(nsINode* aCo
   IMPL_MUTATION_NOTIFICATION(ContentRemoved, aContainer,
                              (aChild, aPreviousSibling),
                              IsRemoveNotification::Yes);
 }
 
 Maybe<NonOwningAnimationTarget>
 nsNodeUtils::GetTargetForAnimation(const Animation* aAnimation)
 {
-  AnimationEffectReadOnly* effect = aAnimation->GetEffect();
+  AnimationEffect* effect = aAnimation->GetEffect();
   if (!effect || !effect->AsKeyframeEffect()) {
     return Nothing();
   }
   return effect->AsKeyframeEffect()->GetTarget();
 }
 
 void
 nsNodeUtils::AnimationMutated(Animation* aAnimation,
--- a/dom/base/test/test_caretPositionFromPoint.html
+++ b/dom/base/test/test_caretPositionFromPoint.html
@@ -100,24 +100,32 @@
     var test6Rect = test6Element.getBoundingClientRect();
     checkOffsetsFromPoint(Math.round(test6Rect.left + 4),
                           Math.round(test6Rect.top + (test6Rect.height / 2)),
                           0, "test6");
     checkOffsetsFromPoint(Math.round(test6Rect.left + test6Rect.width - 30),
                           Math.round(test6Rect.top + (test6Rect.height / 2)),
                           5, "test6");
 
+    // Check the first and last characters of the transformed div.
+    var test7Element = document.getElementById('test7');
+    var test7Rect = test7Element.getBoundingClientRect();
+    checkOffsetsFromPoint(Math.round(test7Rect.left + 1), Math.round(test7Rect.top + 1), 0, 'test7');
+    checkOffsetsFromPoint(Math.round(test7Rect.right - 1), Math.round(test7Rect.top + 1), 13, 'test7');
+
     SimpleTest.finish();
   }
 
   SimpleTest.waitForExplicitFinish();
 </script>
 </head>
 <body onload="doTesting();">
 <div id="a" contenteditable><span id="test1">abc, abc, abc</span><br>
 <span id="test2" style="color: blue;">abc, abc, abc</span><br>
 <textarea id="test3">abc</textarea><input id="test4" value="abcdef"><br><br>
 <marquee>marquee</marquee>
+<!-- Translate test7 while staying within confines of the test viewport -->
+<div id="test7" style="transform: translate(140px, -20px); display: inline-block;">abc, abc, abc</div>
 </div>
 <input id="test5" value="The rabbit-hole went straight on like a tunnel for some way, and then dipped suddenly down, so suddenly that Alice had not a moment to think about stopping herself before she found herself falling down a very deep well. Either the well was very deep, or she fell very slowly, for she had plenty of time as she went down to look about her and to wonder what was going to happen next. First, she tried to look down and make out what she was coming to, but it was too dark to see anything; then she looked at the sides of the well, and noticed that they were filled with cupboards and book-shelves; here and there she saw maps and pictures hung upon pegs. She took down a jar from one of the shelves as she passed; it was labelled `ORANGE MARMALADE', but to her great disappointment it was empty: she did not like to drop the jar for fear of killing somebody, so managed to put it into one of the cupboards as she fell past it." type="text">
 <input id="test6" type="number" style="width:150px; height:57px;" value="31415"><br>
 </body>
 </html>
--- a/dom/bindings/Bindings.conf
+++ b/dom/bindings/Bindings.conf
@@ -53,17 +53,17 @@ DOMInterfaces = {
     'concrete': False
 },
 
 'AddonManagerPermissions': {
     'wrapperCache': False,
     'concrete': False
 },
 
-'AnimationEffectReadOnly': {
+'AnimationEffect': {
     'concrete': False
 },
 
 'AnimationTimeline': {
     'concrete': False
 },
 
 'AnonymousContent': {
--- a/dom/events/EventStateManager.cpp
+++ b/dom/events/EventStateManager.cpp
@@ -98,18 +98,16 @@
 #ifdef XP_MACOSX
 #import <ApplicationServices/ApplicationServices.h>
 #endif
 
 namespace mozilla {
 
 using namespace dom;
 
-//#define DEBUG_DOCSHELL_FOCUS
-
 static const LayoutDeviceIntPoint kInvalidRefPoint = LayoutDeviceIntPoint(-1,-1);
 
 static uint32_t gMouseOrKeyboardEventCounter = 0;
 static nsITimer* gUserInteractionTimer = nullptr;
 static nsITimerCallback* gUserInteractionTimerCallback = nullptr;
 
 static const double kCursorLoadingTimeout = 1000; // ms
 static AutoWeakFrame gLastCursorSourceFrame;
@@ -117,76 +115,16 @@ static TimeStamp gLastCursorUpdateTime;
 
 static inline int32_t
 RoundDown(double aDouble)
 {
   return (aDouble > 0) ? static_cast<int32_t>(floor(aDouble)) :
                          static_cast<int32_t>(ceil(aDouble));
 }
 
-#ifdef DEBUG_DOCSHELL_FOCUS
-static void
-PrintDocTree(nsIDocShellTreeItem* aParentItem, int aLevel)
-{
-  for (int32_t i=0;i<aLevel;i++) printf("  ");
-
-  int32_t childWebshellCount;
-  aParentItem->GetChildCount(&childWebshellCount);
-  nsCOMPtr<nsIDocShell> parentAsDocShell(do_QueryInterface(aParentItem));
-  int32_t type = aParentItem->ItemType();
-  nsCOMPtr<nsIPresShell> presShell = parentAsDocShell->GetPresShell();
-  RefPtr<nsPresContext> presContext;
-  parentAsDocShell->GetPresContext(getter_AddRefs(presContext));
-  nsCOMPtr<nsIContentViewer> cv;
-  parentAsDocShell->GetContentViewer(getter_AddRefs(cv));
-  nsCOMPtr<nsIDOMDocument> domDoc;
-  if (cv)
-    cv->GetDOMDocument(getter_AddRefs(domDoc));
-  nsCOMPtr<nsIDocument> doc = do_QueryInterface(domDoc);
-  nsCOMPtr<nsIDOMWindow> domwin = doc ? doc->GetWindow() : nullptr;
-  nsIURI* uri = doc ? doc->GetDocumentURI() : nullptr;
-
-  printf("DS %p  Type %s  Cnt %d  Doc %p  DW %p  EM %p%c",
-    static_cast<void*>(parentAsDocShell.get()),
-    type==nsIDocShellTreeItem::typeChrome?"Chrome":"Content",
-    childWebshellCount, static_cast<void*>(doc.get()),
-    static_cast<void*>(domwin.get()),
-    static_cast<void*>(presContext ? presContext->EventStateManager() : nullptr),
-    uri ? ' ' : '\n');
-  if (uri) {
-    nsAutoCString spec;
-    uri->GetSpec(spec);
-    printf("\"%s\"\n", spec.get());
-  }
-
-  if (childWebshellCount > 0) {
-    for (int32_t i = 0; i < childWebshellCount; i++) {
-      nsCOMPtr<nsIDocShellTreeItem> child;
-      aParentItem->GetChildAt(i, getter_AddRefs(child));
-      PrintDocTree(child, aLevel + 1);
-    }
-  }
-}
-
-static void
-PrintDocTreeAll(nsIDocShellTreeItem* aItem)
-{
-  nsCOMPtr<nsIDocShellTreeItem> item = aItem;
-  for(;;) {
-    nsCOMPtr<nsIDocShellTreeItem> parent;
-    item->GetParent(getter_AddRefs(parent));
-    if (!parent)
-      break;
-    item = parent;
-  }
-
-  PrintDocTree(item, 0);
-}
-#endif
-
 /******************************************************************/
 /* mozilla::UITimerCallback                                       */
 /******************************************************************/
 
 class UITimerCallback final :
     public nsITimerCallback,
     public nsINamed
 {
--- a/dom/plugins/test/mochitest/browser_bug1196539.js
+++ b/dom/plugins/test/mochitest/browser_bug1196539.js
@@ -9,16 +9,20 @@ function checkPaintCount(aCount) {
 // animate so this should really be just 1, but operating systems can
 // occasionally fire a few of these so we give these tests a fudge factor.
 // A bad regression would either be 0, or 100+.
 const kMaxPaints = 10;
 
 add_task(async function() {
   let result, tabSwitchedPromise;
 
+  // We want to make sure that we will paint in cases where we need to. The
+  // tab layer cache just gets in the way of measuring that.
+  await SpecialPowers.pushPrefEnv({set: [["browser.tabs.remote.tabCacheSize", 0]]});
+
   setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED, "Test Plug-in");
 
   let testTab = gBrowser.selectedTab;
   let pluginTab = await BrowserTestUtils.openNewForegroundTab(gBrowser, gTestRoot + "plugin_test.html");
   let homeTab = await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:home");
 
   result = await ContentTask.spawn(pluginTab.linkedBrowser, null, async function() {
     let doc = content.document;
--- a/dom/plugins/test/mochitest/browser_tabswitchbetweenplugins.js
+++ b/dom/plugins/test/mochitest/browser_tabswitchbetweenplugins.js
@@ -1,10 +1,38 @@
 var gTestRoot = getRootDirectory(gTestPath).replace("chrome://mochitests/content/", "http://127.0.0.1:8888/");
 
+function waitForPluginVisibility(browser, shouldBeVisible, errorMessage) {
+  return new Promise((resolve, reject) => {
+    let windowUtils = window.QueryInterface(Ci.nsIInterfaceRequestor)
+                            .getInterface(Ci.nsIDOMWindowUtils);
+    let lastTransactionId = windowUtils.lastTransactionId;
+    let listener = async (event) => {
+      let visibility = await ContentTask.spawn(browser, null, async function() {
+        let doc = content.document;
+        let plugin = doc.getElementById("testplugin");
+        return XPCNativeWrapper.unwrap(plugin).nativeWidgetIsVisible();
+      });
+
+      if (visibility == shouldBeVisible) {
+        window.removeEventListener("MozAfterPaint", listener);
+        resolve();
+      } else if (event && event.transactionId > lastTransactionId) {
+        // We want to allow for one failed check since we call listener
+        // directly, but if we get a MozAfterPaint notification and we
+        // still don't have the correct visibility, that's likely a
+        // problem.
+        reject(new Error("MozAfterPaint had a mismatched plugin visibility"));
+      }
+    };
+    window.addEventListener("MozAfterPaint", listener);
+    listener(null);
+  });
+}
+
 // tests that we get plugin updates when we flip between tabs that
 // have the same plugin in the same position in the page.
 
 add_task(async function() {
   let result, tabSwitchedPromise;
 
   setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED, "Test Plug-in");
 
@@ -24,82 +52,50 @@ add_task(async function() {
     let plugin = doc.getElementById("testplugin");
     return !!plugin;
   });
   is(result, true, "plugin2 is loaded");
 
   // plugin tab 2 should be selected
   is(gBrowser.selectedTab == pluginTab2, true, "plugin2 is selected");
 
-  result = await ContentTask.spawn(pluginTab1.linkedBrowser, null, async function() {
-    let doc = content.document;
-    let plugin = doc.getElementById("testplugin");
-    return XPCNativeWrapper.unwrap(plugin).nativeWidgetIsVisible();
-  });
-  is(result, false, "plugin1 is hidden");
+  await waitForPluginVisibility(pluginTab1.linkedBrowser,
+                                false, "plugin1 should be hidden");
 
-  result = await ContentTask.spawn(pluginTab2.linkedBrowser, null, async function() {
-    let doc = content.document;
-    let plugin = doc.getElementById("testplugin");
-    return XPCNativeWrapper.unwrap(plugin).nativeWidgetIsVisible();
-  });
-  is(result, true, "plugin2 is visible");
+  await waitForPluginVisibility(pluginTab2.linkedBrowser,
+                                true, "plugin2 should be visible");
 
   // select plugin1 tab
   tabSwitchedPromise = waitTabSwitched();
   gBrowser.selectedTab = pluginTab1;
   await tabSwitchedPromise;
 
-  result = await ContentTask.spawn(pluginTab1.linkedBrowser, null, async function() {
-    let doc = content.document;
-    let plugin = doc.getElementById("testplugin");
-    return XPCNativeWrapper.unwrap(plugin).nativeWidgetIsVisible();
-  });
-  is(result, true, "plugin1 is visible");
+  await waitForPluginVisibility(pluginTab1.linkedBrowser,
+                                true, "plugin1 should be visible");
 
-  result = await ContentTask.spawn(pluginTab2.linkedBrowser, null, async function() {
-    let doc = content.document;
-    let plugin = doc.getElementById("testplugin");
-    return XPCNativeWrapper.unwrap(plugin).nativeWidgetIsVisible();
-  });
-  is(result, false, "plugin2 is hidden");
+  await waitForPluginVisibility(pluginTab2.linkedBrowser,
+                                false, "plugin2 should be hidden");
 
   // select plugin2 tab
   tabSwitchedPromise = waitTabSwitched();
   gBrowser.selectedTab = pluginTab2;
   await tabSwitchedPromise;
 
-  result = await ContentTask.spawn(pluginTab1.linkedBrowser, null, async function() {
-    let doc = content.document;
-    let plugin = doc.getElementById("testplugin");
-    return XPCNativeWrapper.unwrap(plugin).nativeWidgetIsVisible();
-  });
-  is(result, false, "plugin1 is hidden");
+  await waitForPluginVisibility(pluginTab1.linkedBrowser,
+                                false, "plugin1 should be hidden");
 
-  result = await ContentTask.spawn(pluginTab2.linkedBrowser, null, async function() {
-    let doc = content.document;
-    let plugin = doc.getElementById("testplugin");
-    return XPCNativeWrapper.unwrap(plugin).nativeWidgetIsVisible();
-  });
-  is(result, true, "plugin2 is visible");
+  await waitForPluginVisibility(pluginTab2.linkedBrowser,
+                                true, "plugin2 should be visible");
 
   // select test tab
   tabSwitchedPromise = waitTabSwitched();
   gBrowser.selectedTab = testTab;
   await tabSwitchedPromise;
 
-  result = await ContentTask.spawn(pluginTab1.linkedBrowser, null, async function() {
-    let doc = content.document;
-    let plugin = doc.getElementById("testplugin");
-    return XPCNativeWrapper.unwrap(plugin).nativeWidgetIsVisible();
-  });
-  is(result, false, "plugin1 is hidden");
+  await waitForPluginVisibility(pluginTab1.linkedBrowser,
+                                false, "plugin1 should be hidden");
 
-  result = await ContentTask.spawn(pluginTab2.linkedBrowser, null, async function() {
-    let doc = content.document;
-    let plugin = doc.getElementById("testplugin");
-    return XPCNativeWrapper.unwrap(plugin).nativeWidgetIsVisible();
-  });
-  is(result, false, "plugin2 is hidden");
+  await waitForPluginVisibility(pluginTab2.linkedBrowser,
+                                false, "plugin2 should be hidden");
 
   gBrowser.removeTab(pluginTab1);
   gBrowser.removeTab(pluginTab2);
 });
--- a/dom/tests/mochitest/general/test_interfaces.js
+++ b/dom/tests/mochitest/general/test_interfaces.js
@@ -120,21 +120,17 @@ var interfaceNamesInGlobalScope =
     {name: "AbortController", insecureContext: true},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "AbortSignal", insecureContext: true},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "AnalyserNode", insecureContext: true},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "Animation", insecureContext: true},
 // IMPORTANT: Do not change this list without review from a DOM peer!
-    {name: "AnimationEffectReadOnly", insecureContext: true, release: false},
-// IMPORTANT: Do not change this list without review from a DOM peer!
-    {name: "AnimationEffectTiming", insecureContext: true, release: false},
-// IMPORTANT: Do not change this list without review from a DOM peer!
-    {name: "AnimationEffectTimingReadOnly", insecureContext: true, release: false},
+    {name: "AnimationEffect", insecureContext: true, release: false},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "AnimationEvent", insecureContext: true},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "AnimationPlaybackEvent", insecureContext: true, release: false},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "AnimationTimeline", insecureContext: true, release: false},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "Attr", insecureContext: true},
@@ -610,18 +606,16 @@ var interfaceNamesInGlobalScope =
     {name: "IntersectionObserver", insecureContext: true},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "IntersectionObserverEntry", insecureContext: true},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "KeyEvent", insecureContext: true},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "KeyboardEvent", insecureContext: true},
 // IMPORTANT: Do not change this list without review from a DOM peer!
-    {name: "KeyframeEffectReadOnly", insecureContext: true, release: false},
-// IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "KeyframeEffect", insecureContext: true, release: false},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "LocalMediaStream", insecureContext: true},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "Location", insecureContext: true},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "MediaDeviceInfo", insecureContext: true},
 // IMPORTANT: Do not change this list without review from a DOM peer!
--- a/dom/webidl/Animation.webidl
+++ b/dom/webidl/Animation.webidl
@@ -8,22 +8,22 @@
  *
  * Copyright © 2015 W3C® (MIT, ERCIM, Keio), All Rights Reserved. W3C
  * liability, trademark and document use rules apply.
  */
 
 enum AnimationPlayState { "idle", "running", "paused", "finished" };
 
 [Func="nsDocument::IsElementAnimateEnabled",
- Constructor (optional AnimationEffectReadOnly? effect = null,
+ Constructor (optional AnimationEffect? effect = null,
               optional AnimationTimeline? timeline)]
 interface Animation : EventTarget {
   attribute DOMString id;
   [Func="nsDocument::IsWebAnimationsEnabled", Pure]
-  attribute AnimationEffectReadOnly? effect;
+  attribute AnimationEffect? effect;
   [Func="nsDocument::IsWebAnimationsEnabled"]
   attribute AnimationTimeline? timeline;
   [BinaryName="startTimeAsDouble"]
   attribute double? startTime;
   [SetterThrows, BinaryName="currentTimeAsDouble"]
   attribute double? currentTime;
 
            attribute double             playbackRate;
rename from dom/webidl/AnimationEffectReadOnly.webidl
rename to dom/webidl/AnimationEffect.webidl
--- a/dom/webidl/AnimationEffectReadOnly.webidl
+++ b/dom/webidl/AnimationEffect.webidl
@@ -20,34 +20,46 @@ enum FillMode {
 
 enum PlaybackDirection {
   "normal",
   "reverse",
   "alternate",
   "alternate-reverse"
 };
 
-dictionary AnimationEffectTimingProperties {
+dictionary EffectTiming {
   double                              delay = 0.0;
   double                              endDelay = 0.0;
   FillMode                            fill = "auto";
   double                              iterationStart = 0.0;
   unrestricted double                 iterations = 1.0;
   (unrestricted double or DOMString)  duration = "auto";
   PlaybackDirection                   direction = "normal";
   DOMString                           easing = "linear";
 };
 
-dictionary ComputedTimingProperties : AnimationEffectTimingProperties {
+dictionary OptionalEffectTiming {
+  double                              delay;
+  double                              endDelay;
+  FillMode                            fill;
+  double                              iterationStart;
+  unrestricted double                 iterations;
+  (unrestricted double or DOMString)  duration;
+  PlaybackDirection                   direction;
+  DOMString                           easing;
+};
+
+dictionary ComputedEffectTiming : EffectTiming {
   unrestricted double   endTime = 0.0;
   unrestricted double   activeDuration = 0.0;
   double?               localTime = null;
   double?               progress = null;
   unrestricted double?  currentIteration = null;
 };
 
 [Func="nsDocument::IsWebAnimationsEnabled"]
-interface AnimationEffectReadOnly {
-  [Cached, Constant]
-  readonly attribute AnimationEffectTimingReadOnly timing;
+interface AnimationEffect {
+  EffectTiming getTiming();
   [BinaryName="getComputedTimingAsDict"]
-  ComputedTimingProperties getComputedTiming();
+  ComputedEffectTiming getComputedTiming();
+  [Throws]
+  void updateTiming(optional OptionalEffectTiming timing);
 };
deleted file mode 100644
--- a/dom/webidl/AnimationEffectTiming.webidl
+++ /dev/null
@@ -1,27 +0,0 @@
-/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
-/* 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/.
- *
- * The origin of this IDL file is
- * https://drafts.csswg.org/web-animations/#animationeffecttiming
- *
- * Copyright © 2015 W3C® (MIT, ERCIM, Keio), All Rights Reserved. W3C
- * liability, trademark and document use rules apply.
- */
-
-[Func="nsDocument::IsWebAnimationsEnabled"]
-interface AnimationEffectTiming : AnimationEffectTimingReadOnly {
-  inherit attribute double                             delay;
-  inherit attribute double                             endDelay;
-  inherit attribute FillMode                           fill;
-  [SetterThrows]
-  inherit attribute double                             iterationStart;
-  [SetterThrows]
-  inherit attribute unrestricted double                iterations;
-  [SetterThrows]
-  inherit attribute (unrestricted double or DOMString) duration;
-  inherit attribute PlaybackDirection                  direction;
-  [SetterThrows]
-  inherit attribute DOMString                          easing;
-};
deleted file mode 100644
--- a/dom/webidl/AnimationEffectTimingReadOnly.webidl
+++ /dev/null
@@ -1,23 +0,0 @@
-/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
-/* 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/.
- *
- * The origin of this IDL file is
- * https://drafts.csswg.org/web-animations/#animationeffecttimingreadonly
- *
- * Copyright © 2015 W3C® (MIT, ERCIM, Keio), All Rights Reserved. W3C
- * liability, trademark and document use rules apply.
- */
-
-[Func="nsDocument::IsWebAnimationsEnabled"]
-interface AnimationEffectTimingReadOnly {
-  readonly attribute double                             delay;
-  readonly attribute double                             endDelay;
-  readonly attribute FillMode                           fill;
-  readonly attribute double                             iterationStart;
-  readonly attribute unrestricted double                iterations;
-  readonly attribute (unrestricted double or DOMString) duration;
-  readonly attribute PlaybackDirection                  direction;
-  readonly attribute DOMString                          easing;
-};
--- a/dom/webidl/BaseKeyframeTypes.webidl
+++ b/dom/webidl/BaseKeyframeTypes.webidl
@@ -11,18 +11,18 @@
  *
  * Copyright © 2016 W3C® (MIT, ERCIM, Keio), All Rights Reserved. W3C
  * liability, trademark and document use rules apply.
  */
 
 enum CompositeOperation { "replace", "add", "accumulate" };
 
 // The following dictionary types are not referred to by other .webidl files,
-// but we use it for manual JS->IDL and IDL->JS conversions in
-// KeyframeEffectReadOnly's implementation.
+// but we use it for manual JS->IDL and IDL->JS conversions in KeyframeEffect's
+// implementation.
 
 dictionary BasePropertyIndexedKeyframe {
   (double? or sequence<double?>) offset = [];
   (DOMString or sequence<DOMString>) easing = [];
   (CompositeOperation? or sequence<CompositeOperation?>) composite = [];
 };
 
 dictionary BaseKeyframe {
--- a/dom/webidl/KeyframeEffect.webidl
+++ b/dom/webidl/KeyframeEffect.webidl
@@ -10,37 +10,36 @@
  * liability, trademark and document use rules apply.
  */
 
 enum IterationCompositeOperation {
   "replace",
   "accumulate"
 };
 
-dictionary KeyframeEffectOptions : AnimationEffectTimingProperties {
+dictionary KeyframeEffectOptions : EffectTiming {
   IterationCompositeOperation iterationComposite = "replace";
   CompositeOperation          composite = "replace";
 };
 
-// KeyframeEffectReadOnly should run in the caller's compartment to do custom
+// KeyframeEffect should run in the caller's compartment to do custom
 // processing on the `keyframes` object.
 [Func="nsDocument::IsWebAnimationsEnabled",
  RunConstructorInCallerCompartment,
  Constructor ((Element or CSSPseudoElement)? target,
               object? keyframes,
               optional (unrestricted double or KeyframeEffectOptions) options),
- Constructor (KeyframeEffectReadOnly source)]
-interface KeyframeEffectReadOnly : AnimationEffectReadOnly {
-  readonly attribute (Element or CSSPseudoElement)?  target;
-  readonly attribute IterationCompositeOperation iterationComposite;
-  readonly attribute CompositeOperation          composite;
-
-  // We use object instead of ComputedKeyframe so that we can put the
-  // property-value pairs on the object.
-  [Throws] sequence<object> getKeyframes();
+ Constructor (KeyframeEffect source)]
+interface KeyframeEffect : AnimationEffect {
+  attribute (Element or CSSPseudoElement)?  target;
+  [NeedsCallerType]
+  attribute IterationCompositeOperation     iterationComposite;
+  attribute CompositeOperation              composite;
+  [Throws] sequence<object> getKeyframes ();
+  [Throws] void             setKeyframes (object? keyframes);
 };
 
 // Non-standard extensions
 dictionary AnimationPropertyValueDetails {
   required double             offset;
            DOMString          value;
            DOMString          easing;
   required CompositeOperation composite;
@@ -48,28 +47,11 @@ dictionary AnimationPropertyValueDetails
 
 dictionary AnimationPropertyDetails {
   required DOMString                               property;
   required boolean                                 runningOnCompositor;
            DOMString                               warning;
   required sequence<AnimationPropertyValueDetails> values;
 };
 
-partial interface KeyframeEffectReadOnly {
+partial interface KeyframeEffect {
   [ChromeOnly, Throws] sequence<AnimationPropertyDetails> getProperties();
 };
-
-// KeyframeEffect should run in the caller's compartment to do custom
-// processing on the `keyframes` object.
-[Func="nsDocument::IsWebAnimationsEnabled",
- RunConstructorInCallerCompartment,
- Constructor ((Element or CSSPseudoElement)? target,
-              object? keyframes,
-              optional (unrestricted double or KeyframeEffectOptions) options),
- Constructor (KeyframeEffectReadOnly source)]
-interface KeyframeEffect : KeyframeEffectReadOnly {
-  inherit attribute (Element or CSSPseudoElement)? target;
-  [NeedsCallerType]
-  inherit attribute IterationCompositeOperation    iterationComposite;
-  inherit attribute CompositeOperation          composite;
-  [Throws]
-  void setKeyframes (object? keyframes);
-};
--- a/dom/webidl/moz.build
+++ b/dom/webidl/moz.build
@@ -359,19 +359,17 @@ WEBIDL_FILES = [
     'AbortController.webidl',
     'AbortSignal.webidl',
     'AboutCapabilities.webidl',
     'AbstractWorker.webidl',
     'AddonManager.webidl',
     'AnalyserNode.webidl',
     'Animatable.webidl',
     'Animation.webidl',
-    'AnimationEffectReadOnly.webidl',
-    'AnimationEffectTiming.webidl',
-    'AnimationEffectTimingReadOnly.webidl',
+    'AnimationEffect.webidl',
     'AnimationEvent.webidl',
     'AnimationTimeline.webidl',
     'AnonymousContent.webidl',
     'AppInfo.webidl',
     'AppNotificationServiceOptions.webidl',
     'APZTestData.webidl',
     'Attr.webidl',
     'AudioBuffer.webidl',
--- a/gfx/layers/AnimationHelper.cpp
+++ b/gfx/layers/AnimationHelper.cpp
@@ -1,19 +1,19 @@
 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* 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/. */
 
 #include "AnimationHelper.h"
 #include "mozilla/ComputedTimingFunction.h" // for ComputedTimingFunction
-#include "mozilla/dom/AnimationEffectReadOnlyBinding.h" // for dom::FillMode
+#include "mozilla/dom/AnimationEffectBinding.h" // for dom::FillMode
 #include "mozilla/dom/KeyframeEffectBinding.h" // for dom::IterationComposite
-#include "mozilla/dom/KeyframeEffectReadOnly.h" // for dom::KeyFrameEffectReadOnly
+#include "mozilla/dom/KeyframeEffect.h" // for dom::KeyFrameEffectReadOnly
 #include "mozilla/dom/Nullable.h" // for dom::Nullable
 #include "mozilla/layers/CompositorThread.h" // for CompositorThreadHolder
 #include "mozilla/layers/LayerAnimationUtils.h" // for TimingFunctionToComputedTimingFunction
 #include "mozilla/ServoBindings.h" // for Servo_ComposeAnimationSegment, etc
 #include "mozilla/StyleAnimationValue.h" // for StyleAnimationValue, etc
 #include "nsDeviceContext.h"            // for AppUnitsPerCSSPixel
 #include "nsDisplayList.h"              // for nsDisplayTransform, etc
 
@@ -228,17 +228,17 @@ AnimationHelper::SampleAnimationForEachN
       animation.isNotPlaying() ||
       animation.startTime().type() != MaybeTimeDuration::TTimeDuration
       ? animation.holdTime()
       : (timeStamp - animation.originTime() -
          animation.startTime().get_TimeDuration())
         .MultDouble(animation.playbackRate());
 
     ComputedTiming computedTiming =
-      dom::AnimationEffectReadOnly::GetComputedTimingAt(
+      dom::AnimationEffect::GetComputedTimingAt(
         dom::Nullable<TimeDuration>(elapsedDuration), animData.mTiming,
         animation.playbackRate());
 
     if (computedTiming.mProgress.IsNull()) {
       continue;
     }
 
     dom::IterationCompositeOperation iterCompositeOperation =
@@ -249,17 +249,17 @@ AnimationHelper::SampleAnimationForEachN
     // calculation.
     // Note that we don't skip calculate this animation if there is another
     // animation since the other animation might be 'accumulate' or 'add', or
     // might have a missing keyframe (i.e. this animation value will be used in
     // the missing keyframe).
     // FIXME Bug 1455476: We should do this optimizations for the case where
     // the layer has multiple animations.
     if (iEnd == 1 &&
-        !dom::KeyframeEffectReadOnly::HasComputedTimingChanged(
+        !dom::KeyframeEffect::HasComputedTimingChanged(
           computedTiming,
           iterCompositeOperation,
           animData.mProgressOnLastCompose,
           animData.mCurrentIterationOnLastCompose)) {
 #ifdef DEBUG
       shouldBeSkipped = true;
 #else
       return SampleResult::Skipped;
--- a/gfx/src/nsDeviceContext.cpp
+++ b/gfx/src/nsDeviceContext.cpp
@@ -199,16 +199,17 @@ nsFontCache::Flush()
     mFontMetrics.Clear();
 }
 
 nsDeviceContext::nsDeviceContext()
     : mWidth(0), mHeight(0),
       mAppUnitsPerDevPixel(-1), mAppUnitsPerDevPixelAtUnitFullZoom(-1),
       mAppUnitsPerPhysicalInch(-1),
       mFullZoom(1.0f), mPrintingScale(1.0f),
+      mPrintingTranslate(gfxPoint(0, 0)),
       mIsCurrentlyPrintingDoc(false)
 #ifdef DEBUG
     , mIsInitialized(false)
 #endif
 {
     MOZ_ASSERT(NS_IsMainThread(), "nsDeviceContext created off main thread");
 }
 
@@ -271,16 +272,17 @@ void
 nsDeviceContext::SetDPI(double* aScale)
 {
     float dpi = -1.0f;
 
     // Use the printing DC to determine DPI values, if we have one.
     if (mDeviceContextSpec) {
         dpi = mDeviceContextSpec->GetDPI();
         mPrintingScale = mDeviceContextSpec->GetPrintingScale();
+        mPrintingTranslate = mDeviceContextSpec->GetPrintingTranslate();
         mAppUnitsPerDevPixelAtUnitFullZoom =
             NS_lround((AppUnitsPerCSSPixel() * 96) / dpi);
     } else {
         nsCOMPtr<nsIScreen> primaryScreen;
         ScreenManager& screenManager = ScreenManager::GetSingleton();
         screenManager.GetPrimaryScreen(getter_AddRefs(primaryScreen));
         MOZ_ASSERT(primaryScreen);
 
@@ -409,16 +411,17 @@ nsDeviceContext::CreateRenderingContextC
     dt->AddUserData(&gfxContext::sDontUseAsSourceKey, dt, nullptr);
 #endif
     dt->AddUserData(&sDisablePixelSnapping, (void*)0x1, nullptr);
 
     RefPtr<gfxContext> pContext = gfxContext::CreateOrNull(dt);
     MOZ_ASSERT(pContext); // already checked draw target above
 
     gfxMatrix transform;
+    transform.PreTranslate(mPrintingTranslate);
     if (mPrintTarget->RotateNeededForLandscape()) {
       // Rotate page 90 degrees to draw landscape page on portrait paper
       IntSize size = mPrintTarget->GetSize();
       transform.PreTranslate(gfxPoint(0, size.width));
       gfxMatrix rotate(0, -1,
                        1,  0,
                        0,  0);
       transform = rotate * transform;
--- a/gfx/src/nsDeviceContext.h
+++ b/gfx/src/nsDeviceContext.h
@@ -300,16 +300,17 @@ private:
 
     nscoord  mWidth;
     nscoord  mHeight;
     int32_t  mAppUnitsPerDevPixel;
     int32_t  mAppUnitsPerDevPixelAtUnitFullZoom;
     int32_t  mAppUnitsPerPhysicalInch;
     float    mFullZoom;
     float    mPrintingScale;
+    gfxPoint  mPrintingTranslate;
 
     RefPtr<nsFontCache>            mFontCache;
     nsCOMPtr<nsIWidget>            mWidget;
     nsCOMPtr<nsIScreenManager>     mScreenManager;
     nsCOMPtr<nsIDeviceContextSpec> mDeviceContextSpec;
     RefPtr<PrintTarget>            mPrintTarget;
     bool                           mIsCurrentlyPrintingDoc;
 #ifdef DEBUG
--- a/gfx/thebes/PrintTargetWindows.cpp
+++ b/gfx/thebes/PrintTargetWindows.cpp
@@ -21,24 +21,26 @@ PrintTargetWindows::PrintTargetWindows(c
 {
   // TODO: At least add basic memory reporting.
   // 4 * mSize.width * mSize.height + sizeof(PrintTargetWindows) ?
 }
 
 /* static */ already_AddRefed<PrintTargetWindows>
 PrintTargetWindows::CreateOrNull(HDC aDC)
 {
-  // Figure out the cairo surface size - Windows we need to use the printable
-  // area of the page.  Note: we only scale the printing using the LOGPIXELSY,
+  // Figure out the paper size, the actual surface size will be the printable
+  // area which is likely smaller, but the size here is later used to create the
+  // draw target where the full page size is needed.
+  // Note: we only scale the printing using the LOGPIXELSY,
   // so we use that when calculating the surface width as well as the height.
   int32_t heightDPI = ::GetDeviceCaps(aDC, LOGPIXELSY);
   float width =
-    (::GetDeviceCaps(aDC, HORZRES) * POINTS_PER_INCH_FLOAT) / heightDPI;
+    (::GetDeviceCaps(aDC, PHYSICALWIDTH) * POINTS_PER_INCH_FLOAT) / heightDPI;
   float height =
-    (::GetDeviceCaps(aDC, VERTRES) * POINTS_PER_INCH_FLOAT) / heightDPI;
+    (::GetDeviceCaps(aDC, PHYSICALHEIGHT) * POINTS_PER_INCH_FLOAT) / heightDPI;
   IntSize size = IntSize::Truncate(width, height);
 
   if (!Factory::CheckSurfaceSize(size)) {
     return nullptr;
   }
 
   cairo_surface_t* surface = cairo_win32_printing_surface_create(aDC);
 
--- a/gfx/thebes/gfxPlatform.cpp
+++ b/gfx/thebes/gfxPlatform.cpp
@@ -2501,18 +2501,26 @@ gfxPlatform::WebRenderEnvvarEnabled()
   const char* env = PR_GetEnv("MOZ_WEBRENDER");
   return (env && *env == '1');
 }
 
 void
 gfxPlatform::InitWebRenderConfig()
 {
   bool prefEnabled = WebRenderPrefEnabled();
-
-  ScopedGfxFeatureReporter reporter("WR", prefEnabled);
+  bool envvarEnabled = WebRenderEnvvarEnabled();
+
+  // On Nightly:
+  //   WR? WR+   => means WR was enabled via gfx.webrender.all.qualified
+  //   WR! WR+   => means WR was enabled via gfx.webrender.{all,enabled} or envvar
+  // On Beta/Release:
+  //   WR? WR+   => means WR was enabled via gfx.webrender.all.qualified on qualified hardware
+  //   WR! WR+   => means WR was enabled via envvar, possibly on unqualified hardware.
+  // In all cases WR- means WR was not enabled, for one of many possible reasons.
+  ScopedGfxFeatureReporter reporter("WR", prefEnabled || envvarEnabled);
   if (!XRE_IsParentProcess()) {
     // The parent process runs through all the real decision-making code
     // later in this function. For other processes we still want to report
     // the state of the feature for crash reports.
     if (gfxVars::UseWebRender()) {
       reporter.SetSuccessful();
     }
     return;
@@ -2520,20 +2528,29 @@ gfxPlatform::InitWebRenderConfig()
 
   FeatureState& featureWebRender = gfxConfig::GetFeature(Feature::WEBRENDER);
 
   featureWebRender.DisableByDefault(
       FeatureStatus::OptIn,
       "WebRender is an opt-in feature",
       NS_LITERAL_CSTRING("FEATURE_FAILURE_DEFAULT_OFF"));
 
-  if (prefEnabled) {
+  // envvar works everywhere; we need this for testing in CI. Sadly this allows
+  // beta/release to enable it on unqualified hardware, but at least this is
+  // harder for the average person than flipping a pref.
+  if (envvarEnabled) {
+    featureWebRender.UserEnable("Force enabled by envvar");
+
+  // gfx.webrender.enabled and gfx.webrender.all only work on nightly
+#ifdef NIGHTLY_BUILD
+  } else if (prefEnabled) {
     featureWebRender.UserEnable("Force enabled by pref");
-  } else if (WebRenderEnvvarEnabled()) {
-    featureWebRender.UserEnable("Force enabled by envvar");
+#endif
+
+  // gfx.webrender.all.qualified works on all channels
   } else if (gfxPrefs::WebRenderAllQualified()) {
     nsCOMPtr<nsIGfxInfo> gfxInfo = services::GetGfxInfo();
     nsCString discardFailureId;
     int32_t status;
     if (NS_SUCCEEDED(gfxInfo->GetFeatureStatus(nsIGfxInfo::FEATURE_WEBRENDER,
                                                discardFailureId, &status))) {
       if (status == nsIGfxInfo::FEATURE_STATUS_OK) {
         featureWebRender.UserEnable("Qualified enabled by pref ");
--- a/gfx/vr/gfxVROpenVR.cpp
+++ b/gfx/vr/gfxVROpenVR.cpp
@@ -589,33 +589,30 @@ VRControllerOpenVR::ShutdownVibrateHapti
   if (mVibrateThread) {
     mVibrateThread->Shutdown();
     mVibrateThread = nullptr;
   }
 }
 
 VRSystemManagerOpenVR::VRSystemManagerOpenVR()
   : mVRSystem(nullptr)
+  , mRuntimeCheckFailed(false)
   , mIsWindowsMR(false)
 {
 }
 
 /*static*/ already_AddRefed<VRSystemManagerOpenVR>
 VRSystemManagerOpenVR::Create()
 {
   MOZ_ASSERT(NS_IsMainThread());
 
   if (!gfxPrefs::VREnabled() || !gfxPrefs::VROpenVREnabled()) {
     return nullptr;
   }
 
-  if (!::vr::VR_IsRuntimeInstalled()) {
-    return nullptr;
-  }
-
   RefPtr<VRSystemManagerOpenVR> manager = new VRSystemManagerOpenVR();
   return manager.forget();
 }
 
 void
 VRSystemManagerOpenVR::Destroy()
 {
   Shutdown();
@@ -654,43 +651,61 @@ VRSystemManagerOpenVR::NotifyVSync()
       mVRSystem = nullptr;
     }
   }
 }
 
 void
 VRSystemManagerOpenVR::Enumerate()
 {
-  if (mOpenVRHMD == nullptr && ::vr::VR_IsHmdPresent()) {
-    ::vr::HmdError err;
+  if (mOpenVRHMD) {
+    // Already enumerated, nothing more to do
+    return;
+  }
+  if (mRuntimeCheckFailed) {
+    // We have already checked for a runtime and
+    // know that its not installed.
+    return;
+  }
+  if (!::vr::VR_IsRuntimeInstalled()) {
+    // Runtime is not installed, remember so we don't
+    // continue to scan for the files
+    mRuntimeCheckFailed = true;
+    return;
+  }
+  if (!::vr::VR_IsHmdPresent()) {
+    // Avoid initializing if no headset is connected
+    return;
+  }
 
-    ::vr::VR_Init(&err, ::vr::EVRApplicationType::VRApplication_Scene);
-    if (err) {
-      return;
-    }
+  ::vr::HmdError err;
 
-    ::vr::IVRSystem *system = (::vr::IVRSystem *)::vr::VR_GetGenericInterface(::vr::IVRSystem_Version, &err);
-    if (err || !system) {
-      ::vr::VR_Shutdown();
-      return;
-    }
-    ::vr::IVRChaperone *chaperone = (::vr::IVRChaperone *)::vr::VR_GetGenericInterface(::vr::IVRChaperone_Version, &err);
-    if (err || !chaperone) {
-      ::vr::VR_Shutdown();
-      return;
-    }
-    ::vr::IVRCompositor *compositor = (::vr::IVRCompositor*)::vr::VR_GetGenericInterface(::vr::IVRCompositor_Version, &err);
-    if (err || !compositor) {
-      ::vr::VR_Shutdown();
-      return;
-    }
+  ::vr::VR_Init(&err, ::vr::EVRApplicationType::VRApplication_Scene);
+  if (err) {
+    return;
+  }
 
-    mVRSystem = system;
-    mOpenVRHMD = new VRDisplayOpenVR(system, chaperone, compositor);
+  ::vr::IVRSystem *system = (::vr::IVRSystem *)::vr::VR_GetGenericInterface(::vr::IVRSystem_Version, &err);
+  if (err || !system) {
+    ::vr::VR_Shutdown();
+    return;
   }
+  ::vr::IVRChaperone *chaperone = (::vr::IVRChaperone *)::vr::VR_GetGenericInterface(::vr::IVRChaperone_Version, &err);
+  if (err || !chaperone) {
+    ::vr::VR_Shutdown();
+    return;
+  }
+  ::vr::IVRCompositor *compositor = (::vr::IVRCompositor*)::vr::VR_GetGenericInterface(::vr::IVRCompositor_Version, &err);
+  if (err || !compositor) {
+    ::vr::VR_Shutdown();
+    return;
+  }
+
+  mVRSystem = system;
+  mOpenVRHMD = new VRDisplayOpenVR(system, chaperone, compositor);
 }
 
 bool
 VRSystemManagerOpenVR::ShouldInhibitEnumeration()
 {
   if (VRSystemManager::ShouldInhibitEnumeration()) {
     return true;
   }
--- a/gfx/vr/gfxVROpenVR.h
+++ b/gfx/vr/gfxVROpenVR.h
@@ -165,16 +165,17 @@ private:
   void GetControllerDeviceId(::vr::ETrackedDeviceClass aDeviceType,
                              ::vr::TrackedDeviceIndex_t aDeviceIndex,
                              nsCString& aId);
 
   // there can only be one
   RefPtr<impl::VRDisplayOpenVR> mOpenVRHMD;
   nsTArray<RefPtr<impl::VRControllerOpenVR>> mOpenVRController;
   ::vr::IVRSystem *mVRSystem;
+  bool mRuntimeCheckFailed;
   bool mIsWindowsMR;
 };
 
 } // namespace gfx
 } // namespace mozilla
 
 
 #endif /* GFX_VR_OPENVR_H */
--- a/gfx/webrender_bindings/WebRenderAPI.cpp
+++ b/gfx/webrender_bindings/WebRenderAPI.cpp
@@ -129,26 +129,20 @@ public:
   }
 
 private:
   layers::SynchronousTask* mTask;
 };
 
 
 TransactionBuilder::TransactionBuilder()
+  : mUseSceneBuilderThread(gfxPrefs::WebRenderAsyncSceneBuild())
 {
-  // We need the if statement to avoid miscompilation on windows, see
-  // bug 1449982 comment 22.
-  if (gfxPrefs::WebRenderAsyncSceneBuild()) {
-    mTxn = wr_transaction_new(true);
-    mResourceUpdates = wr_resource_updates_new();
-  } else {
-    mResourceUpdates = wr_resource_updates_new();
-    mTxn = wr_transaction_new(false);
-  }
+  mTxn = wr_transaction_new(mUseSceneBuilderThread);
+  mResourceUpdates = wr_resource_updates_new();
 }
 
 TransactionBuilder::~TransactionBuilder()
 {
   wr_transaction_delete(mTxn);
   wr_resource_updates_delete(mResourceUpdates);
 }
 
@@ -361,17 +355,17 @@ WebRenderAPI::~WebRenderAPI()
 
   wr_api_delete(mDocHandle);
 }
 
 void
 WebRenderAPI::SendTransaction(TransactionBuilder& aTxn)
 {
   wr_transaction_update_resources(aTxn.Raw(), aTxn.RawUpdates());
-  wr_api_send_transaction(mDocHandle, aTxn.Raw());
+  wr_api_send_transaction(mDocHandle, aTxn.Raw(), aTxn.UseSceneBuilderThread());
 }
 
 bool
 WebRenderAPI::HitTest(const wr::WorldPoint& aPoint,
                       wr::WrPipelineId& aOutPipelineId,
                       layers::FrameMetrics::ViewID& aOutScrollId,
                       gfx::CompositorHitTestInfo& aOutHitInfo)
 {
--- a/gfx/webrender_bindings/WebRenderAPI.h
+++ b/gfx/webrender_bindings/WebRenderAPI.h
@@ -133,19 +133,21 @@ public:
                        const wr::FontInstanceOptions* aOptions,
                        const wr::FontInstancePlatformOptions* aPlatformOptions,
                        wr::Vec<uint8_t>& aVariations);
 
   void DeleteFontInstance(wr::FontInstanceKey aKey);
 
   void Clear();
 
+  bool UseSceneBuilderThread() const { return mUseSceneBuilderThread; }
   Transaction* Raw() { return mTxn; }
   wr::ResourceUpdates* RawUpdates() { return mResourceUpdates; }
 protected:
+  bool mUseSceneBuilderThread;
   Transaction* mTxn;
   wr::ResourceUpdates* mResourceUpdates;
 };
 
 class TransactionWrapper
 {
 public:
   explicit TransactionWrapper(Transaction* aTxn);
--- a/gfx/webrender_bindings/src/bindings.rs
+++ b/gfx/webrender_bindings/src/bindings.rs
@@ -976,28 +976,32 @@ pub unsafe extern "C" fn wr_api_delete(d
 }
 
 /// cbindgen:postfix=WR_DESTRUCTOR_SAFE_FUNC
 #[no_mangle]
 pub unsafe extern "C" fn wr_api_shut_down(dh: &mut DocumentHandle) {
     dh.api.shut_down();