Merge mozilla-central to mozilla-inbound. a=merge
authorDaniel Varga <dvarga@mozilla.com>
Thu, 06 Dec 2018 23:52:03 +0200
changeset 508804 5c25aa754f1e823ce9a6f0ad7c9a3cbc8908ce2d
parent 508803 1233b674e1cd073e2fb216b3ac0b61a9d70d9448 (current diff)
parent 508786 a52c254930e8fd4372d2a439b8e0e6cc10a351f8 (diff)
child 508805 bacd545336189e144e50de3e8e0872c02e0d3b12
push id1905
push userffxbld-merge
push dateMon, 21 Jan 2019 12:33:13 +0000
treeherdermozilla-release@c2fca1944d8c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone65.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 mozilla-inbound. a=merge
devtools/shared/adb/adb-devices-registry.js
devtools/shared/adb/adb-scanner.js
devtools/shared/adb/adb.js
devtools/shared/adb/addon-aware-adb-scanner.js
devtools/shared/adb/test/test_addon-aware-adb-scanner.js
gfx/wr/webrender/src/prim_store.rs
new file mode 100644
--- /dev/null
+++ b/browser/actors/SearchTelemetryChild.jsm
@@ -0,0 +1,165 @@
+/* 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";
+
+var EXPORTED_SYMBOLS = ["SearchTelemetryChild"];
+
+ChromeUtils.import("resource://gre/modules/ActorChild.jsm");
+ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+const SHARED_DATA_KEY = "SearchTelemetry:ProviderInfo";
+
+/**
+ * SearchProviders looks after keeping track of the search provider information
+ * received from the main process.
+ *
+ * It is separate to SearchTelemetryChild so that it is not constructed for each
+ * tab, but once per process.
+ */
+class SearchProviders {
+  constructor() {
+    this._searchProviderInfo = null;
+    Services.cpmm.sharedData.addEventListener("change", this);
+  }
+
+  /**
+   * Gets the search provider information for any provider with advert information.
+   * If there is nothing in the cache, it will obtain it from shared data.
+   *
+   * @returns {object} Returns the search provider information. @see SearchTelemetry.jsm
+   */
+  get info() {
+    if (this._searchProviderInfo) {
+      return this._searchProviderInfo;
+    }
+
+    this._searchProviderInfo = Services.cpmm.sharedData.get(SHARED_DATA_KEY);
+
+    if (!this._searchProviderInfo) {
+      return null;
+    }
+
+    // Filter-out non-ad providers so that we're not trying to match against
+    // those unnecessarily.
+    for (let [providerName, info] of Object.entries(this._searchProviderInfo)) {
+      if (!("extraAdServersRegexps" in info)) {
+        delete this._searchProviderInfo[providerName];
+      }
+    }
+
+    return this._searchProviderInfo;
+  }
+
+  /**
+   * Handles events received from sharedData notifications.
+   *
+   * @param {object} event The event details.
+   */
+  handleEvent(event) {
+    switch (event.type) {
+      case "change": {
+        if (event.changedKeys.includes(SHARED_DATA_KEY)) {
+          // Just null out the provider information for now, we'll fetch it next
+          // time we need it.
+          this._searchProviderInfo = null;
+        }
+        break;
+      }
+    }
+  }
+}
+
+const searchProviders = new SearchProviders();
+
+/**
+ * SearchTelemetryChild monitors for pages that are partner searches, and
+ * looks through them to find links which looks like adverts and sends back
+ * a notification to SearchTelemetry for possible telemetry reporting.
+ *
+ * Only the partner details and the fact that at least one ad was found on the
+ * page are returned to SearchTelemetry. If no ads are found, no notification is
+ * given.
+ */
+class SearchTelemetryChild extends ActorChild {
+  /**
+   * Determines if there is a provider that matches the supplied URL and returns
+   * the information associated with that provider.
+   *
+   * @param {string} url The url to check
+   * @returns {array|null} Returns null if there's no match, otherwise an array
+   *   of provider name and the provider information.
+   */
+  _getProviderInfoForUrl(url) {
+    return Object.entries(searchProviders.info || []).find(
+      ([_, info]) => info.regexp.test(url)
+    );
+  }
+
+  /**
+   * Checks to see if the page is a partner and has an ad link within it. If so,
+   * it will notify SearchTelemetry.
+   *
+   * @param {object} doc The document object to check.
+   */
+  _checkForAdLink(doc) {
+    let providerInfo = this._getProviderInfoForUrl(doc.documentURI);
+    if (!providerInfo) {
+      return;
+    }
+
+    let regexps = providerInfo[1].extraAdServersRegexps;
+
+    let anchors = doc.getElementsByTagName("a");
+    let hasAds = false;
+    for (let anchor of anchors) {
+      if (!anchor.href) {
+        continue;
+      }
+      for (let regexp of regexps) {
+        if (regexp.test(anchor.href)) {
+          hasAds = true;
+          break;
+        }
+      }
+      if (hasAds) {
+        break;
+      }
+    }
+    if (hasAds) {
+      this.sendAsyncMessage("SearchTelemetry:PageInfo", {
+        hasAds: true,
+        url: doc.documentURI,
+      });
+    }
+  }
+
+  /**
+   * Handles events received from the actor child notifications.
+   *
+   * @param {object} event The event details.
+   */
+  handleEvent(event) {
+    // We are only interested in the top-level frame.
+    if (event.target.ownerGlobal != this.content) {
+      return;
+    }
+
+    switch (event.type) {
+      case "pageshow": {
+        // If a page is loaded from the bfcache, we won't get a "DOMContentLoaded"
+        // event, so we need to rely on "pageshow" in this case. Note: we do this
+        // so that we remain consistent with the *.in-content:sap* count for the
+        // SEARCH_COUNTS histogram.
+        if (event.persisted) {
+          this._checkForAdLink(this.content.document);
+        }
+        break;
+      }
+      case "DOMContentLoaded": {
+        this._checkForAdLink(this.content.document);
+        break;
+      }
+    }
+  }
+}
--- a/browser/actors/moz.build
+++ b/browser/actors/moz.build
@@ -38,11 +38,12 @@ FINAL_TARGET_FILES.actors += [
     'LightWeightThemeInstallChild.jsm',
     'LinkHandlerChild.jsm',
     'NetErrorChild.jsm',
     'OfflineAppsChild.jsm',
     'PageInfoChild.jsm',
     'PageMetadataChild.jsm',
     'PageStyleChild.jsm',
     'PluginChild.jsm',
+    'SearchTelemetryChild.jsm',
     'URIFixupChild.jsm',
     'WebRTCChild.jsm',
 ]
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1270,23 +1270,28 @@ pref("browser.newtabpage.activity-stream
 #ifdef EARLY_BETA_OR_EARLIER
 pref("browser.newtabpage.activity-stream.improvesearch.topSiteSearchShortcuts", true);
 #else
 pref("browser.newtabpage.activity-stream.improvesearch.topSiteSearchShortcuts", false);
 #endif
 
 // ASRouter provider configuration
 #if defined(NIGHTLY_BUILD)
-pref("browser.newtabpage.activity-stream.asrouter.providers.snippets", "{\"id\":\"snippets\",\"enabled\":true,\"type\":\"remote\",\"url\":\"https://snippets.cdn.mozilla.net/%STARTPAGE_VERSION%/%NAME%/%VERSION%/%APPBUILDID%/%BUILD_TARGET%/%LOCALE%/%CHANNEL%/%OS_VERSION%/%DISTRIBUTION%/%DISTRIBUTION_VERSION%/\",\"updateCycleInMs\":14400000}");
 pref("browser.newtabpage.activity-stream.asrouter.providers.cfr", "{\"id\":\"cfr\",\"enabled\":true,\"type\":\"local\",\"localProvider\":\"CFRMessageProvider\",\"frequency\":{\"custom\":[{\"period\":\"daily\",\"cap\":1}]}}");
 #else
-pref("browser.newtabpage.activity-stream.asrouter.providers.snippets", "{\"id\":\"snippets\",\"enabled\":false,\"type\":\"remote\",\"url\":\"https://snippets.cdn.mozilla.net/%STARTPAGE_VERSION%/%NAME%/%VERSION%/%APPBUILDID%/%BUILD_TARGET%/%LOCALE%/%CHANNEL%/%OS_VERSION%/%DISTRIBUTION%/%DISTRIBUTION_VERSION%/\",\"updateCycleInMs\":14400000}");
 pref("browser.newtabpage.activity-stream.asrouter.providers.cfr", "{\"id\":\"cfr\",\"enabled\":false,\"type\":\"local\",\"localProvider\":\"CFRMessageProvider\",\"frequency\":{\"custom\":[{\"period\":\"daily\",\"cap\":1}]}}");
 #endif
 
+#ifdef EARLY_BETA_OR_EARLIER
+pref("browser.newtabpage.activity-stream.asrouter.providers.snippets", "{\"id\":\"snippets\",\"enabled\":true,\"type\":\"remote\",\"url\":\"https://snippets.cdn.mozilla.net/%STARTPAGE_VERSION%/%NAME%/%VERSION%/%APPBUILDID%/%BUILD_TARGET%/%LOCALE%/%CHANNEL%/%OS_VERSION%/%DISTRIBUTION%/%DISTRIBUTION_VERSION%/\",\"updateCycleInMs\":14400000}");
+#else
+pref("browser.newtabpage.activity-stream.asrouter.providers.snippets", "{\"id\":\"snippets\",\"enabled\":false,\"type\":\"remote\",\"url\":\"https://snippets.cdn.mozilla.net/%STARTPAGE_VERSION%/%NAME%/%VERSION%/%APPBUILDID%/%BUILD_TARGET%/%LOCALE%/%CHANNEL%/%OS_VERSION%/%DISTRIBUTION%/%DISTRIBUTION_VERSION%/\",\"updateCycleInMs\":14400000}");
+#endif
+
+
 
 // Enable the DOM fullscreen API.
 pref("full-screen-api.enabled", true);
 
 // Startup Crash Tracking
 // number of startup crashes that can occur before starting into safe mode automatically
 // (this pref has no effect if more than 6 hours have passed since the last crash)
 pref("toolkit.startup.max_resumed_crashes", 3);
--- a/browser/base/content/test/performance/browser_startup_content.js
+++ b/browser/base/content/test/performance/browser_startup_content.js
@@ -48,16 +48,17 @@ const whitelist = {
     "resource://formautofill/FormAutofillContent.jsm",
 
     // Browser front-end
     "resource:///actors/AboutReaderChild.jsm",
     "resource:///actors/BrowserTabChild.jsm",
     "resource:///modules/ContentMetaHandler.jsm",
     "resource:///actors/LinkHandlerChild.jsm",
     "resource:///actors/PageStyleChild.jsm",
+    "resource:///actors/SearchTelemetryChild.jsm",
     "resource://gre/modules/ActorChild.jsm",
     "resource://gre/modules/ActorManagerChild.jsm",
     "resource://gre/modules/E10SUtils.jsm",
     "resource://gre/modules/Readerable.jsm",
     "resource://gre/modules/WebProgressChild.jsm",
 
     // Pocket
     "chrome://pocket/content/AboutPocket.jsm",
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -247,16 +247,26 @@ let ACTORS = {
       ],
 
       observers: [
         "decoder-doctor-notification",
       ],
     },
   },
 
+  SearchTelemetry: {
+    child: {
+      module: "resource:///actors/SearchTelemetryChild.jsm",
+      events: {
+        DOMContentLoaded: {},
+        "pageshow": {mozSystemGroup: true},
+      },
+    },
+  },
+
   ShieldFrame: {
     child: {
       module: "resource://normandy-content/ShieldFrameChild.jsm",
       events: {
         "ShieldPageEvent": {wantUntrusted: true},
       },
       matches: ["about:studies"],
     },
@@ -421,16 +431,17 @@ XPCOMUtils.defineLazyModuleGetters(this,
   PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
   ProcessHangMonitor: "resource:///modules/ProcessHangMonitor.jsm",
   ReaderParent: "resource:///modules/ReaderParent.jsm",
   RemotePrompt: "resource:///modules/RemotePrompt.jsm",
   RemoteSettings: "resource://services-settings/remote-settings.js",
   SafeBrowsing: "resource://gre/modules/SafeBrowsing.jsm",
   Sanitizer: "resource:///modules/Sanitizer.jsm",
   SaveToPocket: "chrome://pocket/content/SaveToPocket.jsm",
+  SearchTelemetry: "resource:///modules/SearchTelemetry.jsm",
   SessionStartup: "resource:///modules/sessionstore/SessionStartup.jsm",
   SessionStore: "resource:///modules/sessionstore/SessionStore.jsm",
   ShellService: "resource:///modules/ShellService.jsm",
   TabCrashHandler: "resource:///modules/ContentCrashHandlers.jsm",
   UIState: "resource://services-sync/UIState.jsm",
   UITour: "resource:///modules/UITour.jsm",
   WebChannel: "resource://gre/modules/WebChannel.jsm",
   WindowsRegistry: "resource://gre/modules/WindowsRegistry.jsm",
@@ -1439,16 +1450,17 @@ BrowserGlue.prototype = {
 
     for (let mod of Object.values(initializedModules)) {
       if (mod.uninit) {
         mod.uninit();
       }
     }
 
     BrowserUsageTelemetry.uninit();
+    SearchTelemetry.uninit();
     // Only uninit PingCentre if the getter has initialized it
     if (Object.prototype.hasOwnProperty.call(this, "pingCentre")) {
       this.pingCentre.uninit();
     }
 
     PageThumbs.uninit();
     NewTabUtils.uninit();
     AboutPrivateBrowsingHandler.uninit();
@@ -1506,16 +1518,17 @@ BrowserGlue.prototype = {
 
     // Browser errors are only collected on Nightly, but telemetry for
     // them is collected on all channels.
     if (AppConstants.MOZ_DATA_REPORTING) {
       this.browserErrorReporter.init();
     }
 
     BrowserUsageTelemetry.init();
+    SearchTelemetry.init();
 
     // Show update notification, if needed.
     if (Services.prefs.prefHasUserValue("app.update.postupdate"))
       this._showUpdateNotification();
 
     ExtensionsUI.init();
 
     let signingRequired;
--- a/browser/components/search/SearchTelemetry.jsm
+++ b/browser/components/search/SearchTelemetry.jsm
@@ -7,83 +7,319 @@
 var EXPORTED_SYMBOLS = ["SearchTelemetry"];
 
 const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm", null);
 
 XPCOMUtils.defineLazyModuleGetters(this, {
   Services: "resource://gre/modules/Services.jsm",
 });
 
+// The various histograms and scalars that we report to.
 const SEARCH_COUNTS_HISTOGRAM_KEY = "SEARCH_COUNTS";
+const SEARCH_WITH_ADS_SCALAR = "browser.search.with_ads";
+const SEARCH_AD_CLICKS_SCALAR = "browser.search.ad_clicks";
 
-// Used to identify various parameters (query, code, etc.) in search URLS.
+/**
+ * Used to identify various parameters used with partner search providers. This
+ * consists of the following structure:
+ * - {<string>} name
+ *     Details for a particular provider with the string name.
+ * - {regexp} <string>.regexp
+ *     The regular expression used to match the url for the search providers main page.
+ * - {string} <string>.queryParam
+ *     The query parameter name that indicates a search has been made.
+ * - {string} [<string>.codeParam]
+ *     The query parameter name that indicates a search provider's code.
+ * - {array} [<string>.codePrefixes]
+ *     An array of the possible string prefixes for a codeParam, indicating a
+ *     partner search.
+ * - {array} [<string>.followOnParams]
+ *     An array of parameters name that indicates this is a follow-on search.
+ * - {array} [<string>.extraAdServersRegexps]
+ *     An array of regular expressions used to determine if a link on a search
+ *     page mightbe an advert.
+ */
 const SEARCH_PROVIDER_INFO = {
   "google": {
-    "regexp": /^https:\/\/www\.(google)\.(?:.+)\/search/,
+    "regexp": /^https:\/\/www\.google\.(?:.+)\/search/,
     "queryParam": "q",
     "codeParam": "client",
     "codePrefixes": ["firefox"],
     "followonParams": ["oq", "ved", "ei"],
+    "extraAdServersRegexps": [/^https:\/\/www\.googleadservices\.com\/(?:pagead\/)?aclk/],
   },
   "duckduckgo": {
-    "regexp": /^https:\/\/(duckduckgo)\.com\//,
+    "regexp": /^https:\/\/duckduckgo\.com\//,
     "queryParam": "q",
     "codeParam": "t",
     "codePrefixes": ["ff"],
   },
   "yahoo": {
-    "regexp": /^https:\/\/(?:.*)search\.(yahoo)\.com\/search/,
+    "regexp": /^https:\/\/(?:.*)search\.yahoo\.com\/search/,
     "queryParam": "p",
   },
   "baidu": {
-    "regexp": /^https:\/\/www\.(baidu)\.com\/(?:s|baidu)/,
+    "regexp": /^https:\/\/www\.baidu\.com\/(?:s|baidu)/,
     "queryParam": "wd",
     "codeParam": "tn",
     "codePrefixes": ["monline_dg"],
     "followonParams": ["oq"],
   },
   "bing": {
-    "regexp": /^https:\/\/www\.(bing)\.com\/search/,
+    "regexp": /^https:\/\/www\.bing\.com\/search/,
     "queryParam": "q",
     "codeParam": "pc",
     "codePrefixes": ["MOZ", "MZ"],
   },
 };
 
 const BROWSER_SEARCH_PREF = "browser.search.";
 
 XPCOMUtils.defineLazyPreferenceGetter(this, "loggingEnabled", BROWSER_SEARCH_PREF + "log", false);
 
+/**
+ * TelemetryHandler is the main class handling search telemetry. It primarily
+ * deals with tracking of what pages are loaded into tabs.
+ *
+ * It handles the *in-content:sap* keys of the SEARCH_COUNTS histogram.
+ */
 class TelemetryHandler {
   constructor() {
+    this._browserInfoByUrl = new Map();
+    this._initialized = false;
     this.__searchProviderInfo = null;
+    this._contentHandler = new ContentHandler({
+      browserInfoByUrl: this._browserInfoByUrl,
+      getProviderInfoForUrl: this._getProviderInfoForUrl.bind(this),
+    });
+  }
+
+  /**
+   * Initializes the TelemetryHandler and its ContentHandler. It will add
+   * appropriate listeners to the window so that window opening and closing
+   * can be tracked.
+   */
+  init() {
+    if (this._initialized) {
+      return;
+    }
+
+    this._contentHandler.init();
+
+    for (let win of Services.wm.getEnumerator("navigator:browser")) {
+      this._registerWindow(win);
+    }
+    Services.wm.addListener(this);
+
+    this._initialized = true;
   }
 
+  /**
+   * Uninitializes the TelemetryHandler and its ContentHandler.
+   */
+  uninit() {
+    if (!this._initialized) {
+      return;
+    }
+
+    this._contentHandler.uninit();
+
+    for (let win of Services.wm.getEnumerator("navigator:browser")) {
+      this._unregisterWindow(win);
+    }
+    Services.wm.removeListener(this);
+
+    this._initialized = false;
+  }
+
+  /**
+   * Handles the TabClose event received from the listeners.
+   *
+   * @param {object} event
+   */
+  handleEvent(event) {
+    if (event.type != "TabClose") {
+      Cu.reportError(`Received unexpected event type ${event.type}`);
+      return;
+    }
+
+    this.stopTrackingBrowser(event.target.linkedBrowser);
+  }
+
+  /**
+   * Test-only function, used to override the provider information, so that
+   * unit tests can set it to easy to test values.
+   *
+   * @param {object} infoByProvider @see SEARCH_PROVIDER_INFO for type information.
+   */
   overrideSearchTelemetryForTests(infoByProvider) {
     if (infoByProvider) {
       for (let info of Object.values(infoByProvider)) {
         info.regexp = new RegExp(info.regexp);
       }
       this.__searchProviderInfo = infoByProvider;
     } else {
       this.__searchProviderInfo = SEARCH_PROVIDER_INFO;
     }
+    this._contentHandler.overrideSearchTelemetryForTests(this.__searchProviderInfo);
+  }
+
+  /**
+   * This may start tracking a tab based on the URL. If the URL matches a search
+   * partner, and it has a code, then we'll start tracking it. This will aid
+   * determining if it is a page we should be tracking for adverts.
+   *
+   * @param {object} browser The browser associated with the page.
+   * @param {string} url The url that was loaded in the browser.
+   */
+  updateTrackingStatus(browser, url) {
+    let info = this._checkURLForSerpMatch(url);
+    if (!info) {
+      this.stopTrackingBrowser(browser);
+      return;
+    }
+
+    this._reportSerpPage(info, url);
+
+    // If we have a code, then we also track this for potential ad clicks.
+    if (info.code) {
+      let item = this._browserInfoByUrl.get(url);
+      if (item) {
+        item.browsers.add(browser);
+      } else {
+        this._browserInfoByUrl.set(url, {
+          browser: new WeakSet([browser]),
+          info,
+        });
+      }
+    }
+  }
+
+  /**
+   * Stops tracking of a tab, for example the tab has loaded a different URL.
+   *
+   * @param {object} browser The browser associated with the tab to stop being
+   *                         tracked.
+   */
+  stopTrackingBrowser(browser) {
+    for (let [url, item] of this._browserInfoByUrl) {
+      item.browser.delete(browser);
+
+      if (!item.browser.length) {
+        this._browserInfoByUrl.delete(url);
+      }
+    }
   }
 
-  recordSearchURLTelemetry(url) {
-    let entry = Object.entries(this._searchProviderInfo).find(
+  // nsIWindowMediatorListener
+
+  /**
+   * This is called when a new window is opened, and handles registration of
+   * that window if it is a browser window.
+   *
+   * @param {nsIXULWindow} xulWin The xul window that was opened.
+   */
+  onOpenWindow(xulWin) {
+    let win = xulWin.docShell.domWindow;
+    win.addEventListener("load", () => {
+      if (win.document.documentElement.getAttribute("windowtype") != "navigator:browser") {
+        return;
+      }
+
+      this._registerWindow(win);
+    }, {once: true});
+  }
+
+  /**
+   * Listener that is called when a window is closed, and handles deregistration of
+   * that window if it is a browser window.
+   *
+   * @param {nsIXULWindow} xulWin The xul window that was closed.
+   */
+  onCloseWindow(xulWin) {
+    let win = xulWin.docShell.domWindow;
+
+    if (win.document.documentElement.getAttribute("windowtype") != "navigator:browser") {
+      return;
+    }
+
+    this._unregisterWindow(win);
+  }
+
+  /**
+   * Adds event listeners for the window and registers it with the content handler.
+   *
+   * @param {object} win The window to register.
+   */
+  _registerWindow(win) {
+    this._contentHandler.registerWindow(win);
+    win.gBrowser.tabContainer.addEventListener("TabClose", this);
+  }
+
+  /**
+   * Removes event listeners for the window and unregisters it with the content
+   * handler.
+   *
+   * @param {object} win The window to unregister.
+   */
+  _unregisterWindow(win) {
+    for (let tab of win.gBrowser.tabs) {
+      this.stopTrackingBrowser(tab);
+    }
+
+    this._contentHandler.unregisterWindow(win);
+    win.gBrowser.tabContainer.removeEventListener("TabClose", this);
+  }
+
+  /**
+   * Searches for provider information for a given url.
+   *
+   * @param {string} url The url to match for a provider.
+   * @param {boolean} useOnlyExtraAdServers If true, this will use the extra
+   *   ad server regexp to match instead of the main regexp.
+   * @returns {array|null} Returns an array of provider name and the provider information.
+   */
+  _getProviderInfoForUrl(url, useOnlyExtraAdServers = false) {
+    if (useOnlyExtraAdServers) {
+      return Object.entries(this._searchProviderInfo).find(
+        ([_, info]) => {
+          if (info.extraAdServersRegexps) {
+            for (let regexp of info.extraAdServersRegexps) {
+              if (regexp.test(url)) {
+                return true;
+              }
+            }
+          }
+          return false;
+        }
+      );
+    }
+
+    return Object.entries(this._searchProviderInfo).find(
       ([_, info]) => info.regexp.test(url)
     );
-    if (!entry) {
-      return;
+  }
+
+  /**
+   * Checks to see if a url is a search partner location, and determines the
+   * provider and codes used.
+   *
+   * @param {string} url The url to match.
+   * @returns {null|object} Returns null if there is no match found. Otherwise,
+   *   returns an object of strings for provider, code and type.
+   */
+  _checkURLForSerpMatch(url) {
+    let info = this._getProviderInfoForUrl(url);
+    if (!info) {
+      return null;
     }
-    let [provider, searchProviderInfo] = entry;
+    let [provider, searchProviderInfo] = info;
     let queries = new URLSearchParams(url.split("#")[0].split("?")[1]);
     if (!queries.get(searchProviderInfo.queryParam)) {
-      return;
+      return null;
     }
     // Default to organic to simplify things.
     // We override type in the sap cases.
     let type = "organic";
     let code;
     if (searchProviderInfo.codeParam) {
       code = queries.get(searchProviderInfo.codeParam);
       if (code &&
@@ -109,34 +345,206 @@ class TelemetryHandler {
                 code = cookie.value.split("=")[1];
                 break;
               }
             }
           }
         }
       }
     }
+    return {provider, type, code};
+  }
 
-    let payload = `${provider}.in-content:${type}:${code || "none"}`;
+  /**
+   * Logs telemetry for a search provider visit.
+   *
+   * @param {object} info
+   * @param {string} info.provider The name of the provider.
+   * @param {string} info.type The type of search.
+   * @param {string} [info.code] The code for the provider.
+   * @param {string} url The url that was matched (for debug logging only).
+   */
+  _reportSerpPage(info, url) {
+    let payload = `${info.provider}.in-content:${info.type}:${info.code || "none"}`;
     let histogram = Services.telemetry.getKeyedHistogramById(SEARCH_COUNTS_HISTOGRAM_KEY);
     histogram.add(payload);
-    LOG("recordSearchURLTelemetry: " + payload);
+    LOG(`SearchTelemetry::recordSearchURLTelemetry: ${payload} for ${url}`);
   }
 
+  /**
+   * Returns the current search provider information in use.
+   * @see SEARCH_PROVIDER_INFO
+   */
   get _searchProviderInfo() {
     if (!this.__searchProviderInfo) {
       this.__searchProviderInfo = SEARCH_PROVIDER_INFO;
     }
     return this.__searchProviderInfo;
   }
 }
 
 /**
- * Outputs aText to the JavaScript console as well as to stdout.
+ * ContentHandler deals with handling telemetry of the content within a tab -
+ * when ads detected and when they are selected.
+ *
+ * It handles the "browser.search.with_ads" and "browser.search.ad_clicks"
+ * scalars.
  */
-function LOG(aText) {
+class ContentHandler {
+  /**
+   * Constructor.
+   *
+   * @param {object} options
+   * @param {Map} options.browserInfoByUrl The  map of urls from TelemetryHandler.
+   * @param {function} options.getProviderInfoForUrl A function that obtains
+   *   the provider information for a url.
+   */
+  constructor(options) {
+    this._browserInfoByUrl = options.browserInfoByUrl;
+    this._getProviderInfoForUrl = options.getProviderInfoForUrl;
+  }
+
+  /**
+   * Initializes the content handler. This will also set up the shared data that is
+   * shared with the SearchTelemetryChild actor.
+   */
+  init() {
+    Services.ppmm.sharedData.set("SearchTelemetry:ProviderInfo", SEARCH_PROVIDER_INFO);
+
+    Cc["@mozilla.org/network/http-activity-distributor;1"]
+      .getService(Ci.nsIHttpActivityDistributor)
+      .addObserver(this);
+  }
+
+  /**
+   * Uninitializes the content handler.
+   */
+  uninit() {
+    Cc["@mozilla.org/network/http-activity-distributor;1"]
+      .getService(Ci.nsIHttpActivityDistributor)
+      .removeObserver(this);
+  }
+
+  /**
+   * Receives a message from the SearchTelemetryChild actor.
+   *
+   * @param {object} msg
+   */
+  receiveMessage(msg) {
+    if (msg.name != "SearchTelemetry:PageInfo") {
+      LOG(`"Received unexpected message: ${msg.name}`);
+      return;
+    }
+
+    this._reportPageWithAds(msg.data);
+  }
+
+  /**
+   * Test-only function to override the search provider information for use
+   * with tests. Passes it to the SearchTelemetryChild actor.
+   *
+   * @param {object} providerInfo @see SEARCH_PROVIDER_INFO for type information.
+   */
+  overrideSearchTelemetryForTests(providerInfo) {
+    Services.ppmm.sharedData.set("SearchTelemetry:ProviderInfo", providerInfo);
+  }
+
+  /**
+   * Listener that observes network activity, so that we can determine if a link
+   * from a search provider page was followed, and if then if that link was an
+   * ad click or not.
+   *
+   * @param {nsISupports} httpChannel The channel that generated the activity.
+   * @param {number} activityType The type of activity.
+   * @param {number} activitySubtype The subtype for the activity.
+   * @param {PRTime} timestamp The time of the activity.
+   * @param {number} [extraSizeData] Any size data available for the activity.
+   * @param {string} [extraStringData] Any extra string data available for the
+   *   activity.
+   */
+  observeActivity(httpChannel, activityType, activitySubtype, timestamp, extraSizeData, extraStringData) {
+    if (!this._browserInfoByUrl.size ||
+        activityType != Ci.nsIHttpActivityObserver.ACTIVITY_TYPE_HTTP_TRANSACTION ||
+        activitySubtype != Ci.nsIHttpActivityObserver.ACTIVITY_SUBTYPE_TRANSACTION_CLOSE) {
+      return;
+    }
+
+    let channel = httpChannel.QueryInterface(Ci.nsIHttpChannel);
+    let loadInfo;
+    try {
+      loadInfo = channel.loadInfo;
+    } catch (e) {
+      // Channels without a loadInfo are not pertinent.
+      return;
+    }
+
+    try {
+      let uri = channel.URI;
+      let triggerURI = loadInfo.triggeringPrincipal.URI;
+
+      if (!triggerURI || !this._browserInfoByUrl.has(triggerURI.spec)) {
+        return;
+      }
+
+      let info = this._getProviderInfoForUrl(uri.spec, true);
+      if (!info) {
+        return;
+      }
+
+      Services.telemetry.keyedScalarAdd(SEARCH_AD_CLICKS_SCALAR, info[0], 1);
+      LOG(`SearchTelemetry::recordSearchURLTelemetry: Counting ad click in page for ${info[0]} ${triggerURI.spec}`);
+    } catch (e) {
+      Cu.reportError(e);
+    }
+  }
+
+  /**
+   * Adds a message listener for the window being registered to receive messages
+   * from SearchTelemetryChild.
+   *
+   * @param {object} win The window to register.
+   */
+  registerWindow(win) {
+    win.messageManager.addMessageListener("SearchTelemetry:PageInfo", this);
+  }
+
+  /**
+   * Removes the message listener for the window.
+   *
+   * @param {object} win The window to unregister.
+   */
+  unregisterWindow(win) {
+    win.messageManager.removeMessageListener("SearchTelemetry:PageInfo", this);
+  }
+
+  /**
+   * Logs telemetry for a page with adverts, if it is one of the partner search
+   * provider pages that we're tracking.
+   *
+   * @param {object} info
+   * @param {boolean} info.hasAds Whether or not the page has adverts.
+   * @param {string} info.url The url of the page.
+   */
+  _reportPageWithAds(info) {
+    let item = this._browserInfoByUrl.get(info.url);
+    if (!item) {
+      LOG(`Expected to report URI with ads but couldn't find the information`);
+      return;
+    }
+
+    Services.telemetry.keyedScalarAdd(SEARCH_WITH_ADS_SCALAR, item.info.provider, 1);
+    LOG(`SearchTelemetry::recordSearchURLTelemetry: Counting ads in page for ${item.info.provider} ${info.url}`);
+  }
+}
+
+/**
+ * Outputs the message to the JavaScript console as well as to stdout.
+ *
+ * @param {string} msg The message to output.
+ */
+function LOG(msg) {
   if (loggingEnabled) {
-    dump(`*** SearchTelemetry: ${aText}\n"`);
-    Services.console.logStringMessage(aText);
+    dump(`*** SearchTelemetry: ${msg}\n"`);
+    Services.console.logStringMessage(msg);
   }
 }
 
 var SearchTelemetry = new TelemetryHandler();
--- a/browser/components/search/test/browser/browser.ini
+++ b/browser/components/search/test/browser/browser.ini
@@ -41,10 +41,14 @@ skip-if = os == "mac" #1421238
 [browser_aboutSearchReset.js]
 disabled = bug 1488946 - Telemetry probe needs extension
 [browser_searchbar_openpopup.js]
 skip-if = os == "linux" # Linux has different focus behaviours.
 [browser_searchbar_keyboard_navigation.js]
 [browser_searchbar_smallpanel_keyboard_navigation.js]
 [browser_searchEngine_behaviors.js]
 skip-if = artifact # bug 1315953
+[browser_searchTelemetry.js]
+support-files =
+  searchTelemetry.html
+  searchTelemetryAd.html
 [browser_webapi.js]
 [browser_tooManyEnginesOffered.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/search/test/browser/browser_searchTelemetry.js
@@ -0,0 +1,221 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Main tests for SearchTelemetry - general engine visiting and link clicking.
+ */
+
+"use strict";
+
+const {SearchTelemetry} = ChromeUtils.import("resource:///modules/SearchTelemetry.jsm", null);
+
+const TEST_PROVIDER_INFO = {
+  "example": {
+    "regexp": /^http:\/\/mochi.test:.+\/browser\/browser\/components\/search\/test\/browser\/searchTelemetry(?:Ad)?.html/,
+    "queryParam": "s",
+    "codeParam": "abc",
+    "codePrefixes": ["ff"],
+    "followonParams": ["a"],
+    "extraAdServersRegexps": [/^https:\/\/example\.com\/ad2?/],
+  },
+};
+
+const SEARCH_AD_CLICK_SCALARS = [
+  "browser.search.with_ads",
+  "browser.search.ad_clicks",
+];
+
+function getPageUrl(useExample = false, useAdPage = false) {
+  let server = useExample ? "example.com" : "mochi.test:8888";
+  let page = useAdPage ? "searchTelemetryAd.html" : "searchTelemetry.html";
+  return `http://${server}/browser/browser/components/search/test/browser/${page}`;
+}
+
+function getSERPUrl(page) {
+  return page + "?s=test&abc=ff";
+}
+
+function getSERPFollowOnUrl(page) {
+  return page + "?s=test&abc=ff&a=foo";
+}
+
+const searchCounts = Services.telemetry.getKeyedHistogramById("SEARCH_COUNTS");
+
+async function assertTelemetry(expectedHistograms, expectedScalars) {
+  let histSnapshot = {};
+  let scalars = {};
+
+  await TestUtils.waitForCondition(() => {
+    histSnapshot = searchCounts.snapshot();
+    return Object.getOwnPropertyNames(histSnapshot).length ==
+      Object.getOwnPropertyNames(expectedHistograms).length;
+  });
+
+  if (Object.entries(expectedScalars).length > 0) {
+    await TestUtils.waitForCondition(() => {
+      scalars = Services.telemetry.getSnapshotForKeyedScalars(
+        "main", false).parent || {};
+      return Object.getOwnPropertyNames(expectedScalars)[0] in
+        scalars;
+    });
+  }
+
+  Assert.equal(Object.getOwnPropertyNames(histSnapshot).length,
+    Object.getOwnPropertyNames(expectedHistograms).length,
+    "Should only have one key");
+
+  for (let [key, value] of Object.entries(expectedHistograms)) {
+    Assert.ok(key in histSnapshot,
+      `Histogram should have the expected key: ${key}`);
+    Assert.equal(histSnapshot[key].sum, value,
+      `Should have counted the correct number of visits for ${key}`);
+  }
+
+  for (let [name, value] of Object.entries(expectedScalars)) {
+    Assert.ok(name in scalars,
+      `Scalar ${name} should have been added.`);
+    Assert.deepEqual(scalars[name], value,
+      `Should have counted the correct number of visits for ${name}`);
+  }
+
+  for (let name of SEARCH_AD_CLICK_SCALARS) {
+    Assert.equal(name in scalars, name in expectedScalars,
+      `Should have matched ${name} in scalars and expectedScalars`);
+  }
+}
+
+add_task(async function setup() {
+  SearchTelemetry.overrideSearchTelemetryForTests(TEST_PROVIDER_INFO);
+  // Enable local telemetry recording for the duration of the tests.
+  let oldCanRecord = Services.telemetry.canRecordExtended;
+  Services.telemetry.canRecordExtended = true;
+  Services.prefs.setBoolPref("browser.search.log", true);
+
+  registerCleanupFunction(async () => {
+    SearchTelemetry.overrideSearchTelemetryForTests();
+    Services.telemetry.canRecordExtended = oldCanRecord;
+    Services.telemetry.clearScalars();
+  });
+});
+
+add_task(async function test_simple_search_page_visit() {
+  searchCounts.clear();
+
+  await BrowserTestUtils.withNewTab({
+    gBrowser,
+    url: getSERPUrl(getPageUrl()),
+  }, async () => {
+    await assertTelemetry({"example.in-content:sap:ff": 1}, {});
+  });
+});
+
+add_task(async function test_follow_on_visit() {
+  await BrowserTestUtils.withNewTab({
+    gBrowser,
+    url: getSERPFollowOnUrl(getPageUrl()),
+  }, async () => {
+    await assertTelemetry({
+      "example.in-content:sap:ff": 1,
+      "example.in-content:sap-follow-on:ff": 1,
+    }, {});
+  });
+});
+
+add_task(async function test_track_ad() {
+  searchCounts.clear();
+
+  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser,
+    getSERPUrl(getPageUrl(false, true)));
+
+  await assertTelemetry({"example.in-content:sap:ff": 1}, {
+    "browser.search.with_ads": {"example": 1},
+  });
+
+  BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_track_ad_new_window() {
+  searchCounts.clear();
+  Services.telemetry.clearScalars();
+
+  let win = await BrowserTestUtils.openNewBrowserWindow();
+
+  let url = getSERPUrl(getPageUrl(false, true));
+  await BrowserTestUtils.loadURI(win.gBrowser.selectedBrowser, url);
+  await BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser, false, url);
+
+  await assertTelemetry({"example.in-content:sap:ff": 1}, {
+    "browser.search.with_ads": {"example": 1},
+  });
+
+  await BrowserTestUtils.closeWindow(win);
+});
+
+add_task(async function test_track_ad_pages_without_ads() {
+  // Note: the above tests have already checked a page with no ad-urls.
+  searchCounts.clear();
+  Services.telemetry.clearScalars();
+
+  let tabs = [];
+
+  tabs.push(await BrowserTestUtils.openNewForegroundTab(gBrowser,
+    getSERPUrl(getPageUrl(false, false))));
+  tabs.push(await BrowserTestUtils.openNewForegroundTab(gBrowser,
+    getSERPUrl(getPageUrl(false, true))));
+
+  await assertTelemetry({"example.in-content:sap:ff": 2}, {
+    "browser.search.with_ads": {"example": 1},
+  });
+
+  for (let tab of tabs) {
+    BrowserTestUtils.removeTab(tab);
+  }
+});
+
+add_task(async function test_track_ad_click() {
+  // Note: the above tests have already checked a page with no ad-urls.
+  searchCounts.clear();
+  Services.telemetry.clearScalars();
+
+  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser,
+    getSERPUrl(getPageUrl(false, true)));
+
+  await assertTelemetry({"example.in-content:sap:ff": 1}, {
+    "browser.search.with_ads": {"example": 1},
+  });
+
+  let pageLoadPromise = BrowserTestUtils.waitForLocationChange(gBrowser);
+  await ContentTask.spawn(tab.linkedBrowser, {}, () => {
+    content.document.getElementById("ad1").click();
+  });
+  await pageLoadPromise;
+
+  await assertTelemetry({"example.in-content:sap:ff": 1}, {
+    "browser.search.with_ads": {"example": 1},
+    "browser.search.ad_clicks": {"example": 1},
+  });
+
+  // Now go back, and click again.
+  pageLoadPromise = BrowserTestUtils.waitForLocationChange(gBrowser);
+  gBrowser.goBack();
+  await pageLoadPromise;
+
+  // We've gone back, so we register an extra display & if it is with ads or not.
+  await assertTelemetry({"example.in-content:sap:ff": 2}, {
+    "browser.search.with_ads": {"example": 2},
+    "browser.search.ad_clicks": {"example": 1},
+  });
+
+  pageLoadPromise = BrowserTestUtils.waitForLocationChange(gBrowser);
+  await ContentTask.spawn(tab.linkedBrowser, {}, () => {
+    content.document.getElementById("ad1").click();
+  });
+  await pageLoadPromise;
+
+  await assertTelemetry({"example.in-content:sap:ff": 2}, {
+    "browser.search.with_ads": {"example": 2},
+    "browser.search.ad_clicks": {"example": 2},
+  });
+
+  BrowserTestUtils.removeTab(tab);
+});
new file mode 100644
--- /dev/null
+++ b/browser/components/search/test/browser/searchTelemetry.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+<head>
+</head>
+<body>
+  <a href="https://example.com/otherpage">Non ad link</a>
+  <a href="https://example1.com/ad">Matching path prefix, different server</a>
+  <a href="https://mochi.test:8888/otherpage">Non ad link</a>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/components/search/test/browser/searchTelemetryAd.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+<head>
+</head>
+<body>
+  <a id="ad1" href="https://example.com/ad">Ad link</a>
+  <a id="ad2" href="https://example.com/ad2">Second Ad link</a>
+</body>
+</html>
--- a/browser/components/search/test/unit/test_urlTelemetry.js
+++ b/browser/components/search/test/unit/test_urlTelemetry.js
@@ -2,85 +2,85 @@
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource:///modules/SearchTelemetry.jsm");
 
 add_task(async function test_parsing_search_urls() {
   let hs;
   // Google search access point.
-  SearchTelemetry.recordSearchURLTelemetry("https://www.google.com/search?q=test&ie=utf-8&oe=utf-8&client=firefox-b-1-ab");
+  SearchTelemetry.updateTrackingStatus({}, "https://www.google.com/search?q=test&ie=utf-8&oe=utf-8&client=firefox-b-1-ab");
   hs = Services.telemetry.getKeyedHistogramById("SEARCH_COUNTS").snapshot();
   Assert.ok(hs);
   Assert.ok("google.in-content:sap:firefox-b-1-ab" in hs, "The histogram must contain the correct key");
 
   // Google search access point follow-on.
-  SearchTelemetry.recordSearchURLTelemetry("https://www.google.com/search?client=firefox-b-1-ab&ei=EI_VALUE&q=test2&oq=test2&gs_l=GS_L_VALUE");
+  SearchTelemetry.updateTrackingStatus({}, "https://www.google.com/search?client=firefox-b-1-ab&ei=EI_VALUE&q=test2&oq=test2&gs_l=GS_L_VALUE");
   hs = Services.telemetry.getKeyedHistogramById("SEARCH_COUNTS").snapshot();
   Assert.ok(hs);
   Assert.ok("google.in-content:sap-follow-on:firefox-b-1-ab" in hs, "The histogram must contain the correct key");
 
   // Google organic.
-  SearchTelemetry.recordSearchURLTelemetry("https://www.google.com/search?source=hp&ei=EI_VALUE&q=test&oq=test&gs_l=GS_L_VALUE");
+  SearchTelemetry.updateTrackingStatus({}, "https://www.google.com/search?source=hp&ei=EI_VALUE&q=test&oq=test&gs_l=GS_L_VALUE");
   hs = Services.telemetry.getKeyedHistogramById("SEARCH_COUNTS").snapshot();
   Assert.ok(hs);
   Assert.ok("google.in-content:organic:none" in hs, "The histogram must contain the correct key");
 
   // Google organic UK.
-  SearchTelemetry.recordSearchURLTelemetry("https://www.google.co.uk/search?source=hp&ei=EI_VALUE&q=test&oq=test&gs_l=GS_L_VALUE");
+  SearchTelemetry.updateTrackingStatus({}, "https://www.google.co.uk/search?source=hp&ei=EI_VALUE&q=test&oq=test&gs_l=GS_L_VALUE");
   hs = Services.telemetry.getKeyedHistogramById("SEARCH_COUNTS").snapshot();
   Assert.ok(hs);
   Assert.ok("google.in-content:organic:none" in hs, "The histogram must contain the correct key");
 
   // Yahoo organic.
-  SearchTelemetry.recordSearchURLTelemetry("https://search.yahoo.com/search?p=test&fr=yfp-t&fp=1&toggle=1&cop=mss&ei=UTF-8");
+  SearchTelemetry.updateTrackingStatus({}, "https://search.yahoo.com/search?p=test&fr=yfp-t&fp=1&toggle=1&cop=mss&ei=UTF-8");
   hs = Services.telemetry.getKeyedHistogramById("SEARCH_COUNTS").snapshot();
   Assert.ok(hs);
   Assert.ok("yahoo.in-content:organic:none" in hs, "The histogram must contain the correct key");
 
   // Yahoo organic UK.
-  SearchTelemetry.recordSearchURLTelemetry("https://uk.search.yahoo.com/search?p=test&fr=yfp-t&fp=1&toggle=1&cop=mss&ei=UTF-8");
+  SearchTelemetry.updateTrackingStatus({}, "https://uk.search.yahoo.com/search?p=test&fr=yfp-t&fp=1&toggle=1&cop=mss&ei=UTF-8");
   hs = Services.telemetry.getKeyedHistogramById("SEARCH_COUNTS").snapshot();
   Assert.ok(hs);
   Assert.ok("yahoo.in-content:organic:none" in hs, "The histogram must contain the correct key");
 
   // Bing search access point.
-  SearchTelemetry.recordSearchURLTelemetry("https://www.bing.com/search?q=test&pc=MOZI&form=MOZLBR");
+  SearchTelemetry.updateTrackingStatus({}, "https://www.bing.com/search?q=test&pc=MOZI&form=MOZLBR");
   hs = Services.telemetry.getKeyedHistogramById("SEARCH_COUNTS").snapshot();
   Assert.ok(hs);
   Assert.ok("bing.in-content:sap:MOZI" in hs, "The histogram must contain the correct key");
 
   // Bing organic.
-  SearchTelemetry.recordSearchURLTelemetry("https://www.bing.com/search?q=test&qs=n&form=QBLH&sp=-1&pq=&sc=0-0&sk=&cvid=CVID_VALUE");
+  SearchTelemetry.updateTrackingStatus({}, "https://www.bing.com/search?q=test&qs=n&form=QBLH&sp=-1&pq=&sc=0-0&sk=&cvid=CVID_VALUE");
   hs = Services.telemetry.getKeyedHistogramById("SEARCH_COUNTS").snapshot();
   Assert.ok(hs);
   Assert.ok("bing.in-content:organic:none" in hs, "The histogram must contain the correct key");
 
   // DuckDuckGo search access point.
-  SearchTelemetry.recordSearchURLTelemetry("https://duckduckgo.com/?q=test&t=ffab");
+  SearchTelemetry.updateTrackingStatus({}, "https://duckduckgo.com/?q=test&t=ffab");
   hs = Services.telemetry.getKeyedHistogramById("SEARCH_COUNTS").snapshot();
   Assert.ok(hs);
   Assert.ok("duckduckgo.in-content:sap:ffab" in hs, "The histogram must contain the correct key");
 
   // DuckDuckGo organic.
-  SearchTelemetry.recordSearchURLTelemetry("https://duckduckgo.com/?q=test&t=hi&ia=news");
+  SearchTelemetry.updateTrackingStatus({}, "https://duckduckgo.com/?q=test&t=hi&ia=news");
   hs = Services.telemetry.getKeyedHistogramById("SEARCH_COUNTS").snapshot();
   Assert.ok(hs);
   Assert.ok("duckduckgo.in-content:organic:hi" in hs, "The histogram must contain the correct key");
 
   // Baidu search access point.
-  SearchTelemetry.recordSearchURLTelemetry("https://www.baidu.com/baidu?wd=test&tn=monline_dg&ie=utf-8");
+  SearchTelemetry.updateTrackingStatus({}, "https://www.baidu.com/baidu?wd=test&tn=monline_dg&ie=utf-8");
   hs = Services.telemetry.getKeyedHistogramById("SEARCH_COUNTS").snapshot();
   Assert.ok(hs);
   Assert.ok("baidu.in-content:sap:monline_dg" in hs, "The histogram must contain the correct key");
 
   // Baidu search access point follow-on.
-  SearchTelemetry.recordSearchURLTelemetry("https://www.baidu.com/s?ie=utf-8&f=8&rsv_bp=1&tn=monline_dg&wd=test2&oq=test&rsv_pq=RSV_PQ_VALUE&rsv_t=RSV_T_VALUE&rqlang=cn&rsv_enter=1&rsv_sug3=2&rsv_sug2=0&inputT=227&rsv_sug4=397");
+  SearchTelemetry.updateTrackingStatus({}, "https://www.baidu.com/s?ie=utf-8&f=8&rsv_bp=1&tn=monline_dg&wd=test2&oq=test&rsv_pq=RSV_PQ_VALUE&rsv_t=RSV_T_VALUE&rqlang=cn&rsv_enter=1&rsv_sug3=2&rsv_sug2=0&inputT=227&rsv_sug4=397");
   hs = Services.telemetry.getKeyedHistogramById("SEARCH_COUNTS").snapshot();
   Assert.ok(hs);
   Assert.ok("baidu.in-content:sap-follow-on:monline_dg" in hs, "The histogram must contain the correct key");
 
   // Baidu organic.
-  SearchTelemetry.recordSearchURLTelemetry("https://www.baidu.com/s?ie=utf-8&f=8&rsv_bp=1&rsv_idx=1&ch=&tn=baidu&bar=&wd=test&rn=&oq=&rsv_pq=RSV_PQ_VALUE&rsv_t=RSV_T_VALUE&rqlang=cn");
+  SearchTelemetry.updateTrackingStatus({}, "https://www.baidu.com/s?ie=utf-8&f=8&rsv_bp=1&rsv_idx=1&ch=&tn=baidu&bar=&wd=test&rn=&oq=&rsv_pq=RSV_PQ_VALUE&rsv_t=RSV_T_VALUE&rqlang=cn");
   hs = Services.telemetry.getKeyedHistogramById("SEARCH_COUNTS").snapshot();
   Assert.ok(hs);
   Assert.ok("baidu.in-content:organic:baidu" in hs, "The histogram must contain the correct key");
 });
--- a/browser/modules/BrowserUsageTelemetry.jsm
+++ b/browser/modules/BrowserUsageTelemetry.jsm
@@ -149,16 +149,19 @@ let URICountListener = {
     if (!this.isHttpURI(uri)) {
       return;
     }
 
     this._restoredURIsMap.set(browser, uri.spec);
   },
 
   onLocationChange(browser, webProgress, request, uri, flags) {
+    // By default, assume we no longer need to track this tab.
+    SearchTelemetry.stopTrackingBrowser(browser);
+
     // Don't count this URI if it's an error page.
     if (flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) {
       return;
     }
 
     // We only care about top level loads.
     if (!webProgress.isTopLevel) {
       return;
@@ -214,17 +217,17 @@ let URICountListener = {
     }
 
     if (!this.isHttpURI(uri)) {
       return;
     }
 
     if (shouldRecordSearchCount(browser.getTabBrowser()) &&
         !(flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT)) {
-      SearchTelemetry.recordSearchURLTelemetry(uriSpec);
+      SearchTelemetry.updateTrackingStatus(browser, uriSpec);
     }
 
     if (!shouldCountURI) {
       return;
     }
 
     // Update the URI counts.
     Services.telemetry.scalarAdd(TOTAL_URI_COUNT_SCALAR_NAME, 1);
--- a/build/clang-plugin/LoadLibraryUsageChecker.cpp
+++ b/build/clang-plugin/LoadLibraryUsageChecker.cpp
@@ -21,14 +21,14 @@ void LoadLibraryUsageChecker::registerMa
           .bind("funcCall"),
       this);
 }
 
 void LoadLibraryUsageChecker::check(const MatchFinder::MatchResult &Result) {
   const CallExpr *FuncCall = Result.Nodes.getNodeAs<CallExpr>("funcCall");
 
   if (FuncCall) {
-    diag(FuncCall->getLocStart(),
+    diag(FuncCall->getBeginLoc(),
          "Usage of ASCII file functions (such as %0) is forbidden.",
          DiagnosticIDs::Error)
         << FuncCall->getDirectCallee()->getName();
   }
 }
--- a/devtools/client/aboutdebugging-new/aboutdebugging.js
+++ b/devtools/client/aboutdebugging-new/aboutdebugging.js
@@ -23,18 +23,16 @@ const { l10n } = require("./src/modules/
 
 const {
   addNetworkLocationsObserver,
   getNetworkLocations,
   removeNetworkLocationsObserver,
 } = require("./src/modules/network-locations");
 const {
   addUSBRuntimesObserver,
-  disableUSBRuntimes,
-  enableUSBRuntimes,
   getUSBRuntimes,
   removeUSBRuntimesObserver,
 } = require("./src/modules/usb-runtimes");
 
 loader.lazyRequireGetter(this, "adbAddon", "devtools/shared/adb/adb-addon", true);
 
 const Router = createFactory(require("devtools/client/shared/vendor/react-router-dom").HashRouter);
 const App = createFactory(require("./src/components/App"));
@@ -71,18 +69,20 @@ const AboutDebugging = {
         )
       ),
       this.mount
     );
 
     this.actions.updateNetworkLocations(getNetworkLocations());
 
     addNetworkLocationsObserver(this.onNetworkLocationsUpdated);
+
+    // Listen to USB runtime updates and retrieve the initial list of runtimes.
     addUSBRuntimesObserver(this.onUSBRuntimesUpdated);
-    await enableUSBRuntimes();
+    getUSBRuntimes();
 
     adbAddon.on("update", this.onAdbAddonUpdated);
     this.onAdbAddonUpdated();
 
     // Remove deprecated remote debugging extensions.
     await adbAddon.uninstallUnsupportedExtensions();
   },
 
@@ -108,17 +108,16 @@ const AboutDebugging = {
       await this.actions.unwatchRuntime(currentRuntimeId);
     }
 
     // Remove all client listeners.
     this.actions.removeRuntimeListeners();
 
     removeNetworkLocationsObserver(this.onNetworkLocationsUpdated);
     removeUSBRuntimesObserver(this.onUSBRuntimesUpdated);
-    disableUSBRuntimes();
     adbAddon.off("update", this.onAdbAddonUpdated);
     setDebugTargetCollapsibilities(state.ui.debugTargetCollapsibilities);
     unmountComponentAtNode(this.mount);
   },
 
   get mount() {
     return document.getElementById("mount");
   },
--- a/devtools/client/aboutdebugging-new/src/modules/runtime-client-factory.js
+++ b/devtools/client/aboutdebugging-new/src/modules/runtime-client-factory.js
@@ -1,15 +1,15 @@
 /* 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";
 
-const { ADB } = require("devtools/shared/adb/adb");
+const { prepareTCPConnection } = require("devtools/shared/adb/commands/index");
 const { DebuggerClient } = require("devtools/shared/client/debugger-client");
 const { DebuggerServer } = require("devtools/server/main");
 const { ClientWrapper } = require("./client-wrapper");
 const { remoteClientManager } =
   require("devtools/client/shared/remote-debugging/remote-client-manager");
 
 const { RUNTIMES } = require("../constants");
 
@@ -24,17 +24,17 @@ async function createLocalClient() {
 async function createNetworkClient(host, port) {
   const transport = await DebuggerClient.socketConnect({ host, port });
   const client = new DebuggerClient(transport);
   await client.connect();
   return new ClientWrapper(client);
 }
 
 async function createUSBClient(socketPath) {
-  const port = await ADB.prepareTCPConnection(socketPath);
+  const port = await prepareTCPConnection(socketPath);
   return createNetworkClient("localhost", port);
 }
 
 async function createClientForRuntime(runtime) {
   const { extra, id, type } = runtime;
 
   if (type === RUNTIMES.THIS_FIREFOX) {
     return createLocalClient();
--- a/devtools/client/aboutdebugging-new/src/modules/usb-runtimes.js
+++ b/devtools/client/aboutdebugging-new/src/modules/usb-runtimes.js
@@ -1,46 +1,33 @@
 /* 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";
 
-loader.lazyGetter(this, "adbScanner", () => {
-  const { AddonAwareADBScanner } = require("devtools/shared/adb/addon-aware-adb-scanner");
-  return new AddonAwareADBScanner();
-});
+loader.lazyRequireGetter(this, "adb", "devtools/shared/adb/adb", true);
 
 /**
  * This module provides a collection of helper methods to detect USB runtimes whom Firefox
  * is running on.
  */
 function addUSBRuntimesObserver(listener) {
-  adbScanner.on("runtime-list-updated", listener);
+  adb.registerListener(listener);
 }
 exports.addUSBRuntimesObserver = addUSBRuntimesObserver;
 
-function disableUSBRuntimes() {
-  adbScanner.disable();
-}
-exports.disableUSBRuntimes = disableUSBRuntimes;
-
-async function enableUSBRuntimes() {
-  adbScanner.enable();
-}
-exports.enableUSBRuntimes = enableUSBRuntimes;
-
 function getUSBRuntimes() {
-  return adbScanner.listRuntimes();
+  return adb.getRuntimes();
 }
 exports.getUSBRuntimes = getUSBRuntimes;
 
 function removeUSBRuntimesObserver(listener) {
-  adbScanner.off("runtime-list-updated", listener);
+  adb.unregisterListener(listener);
 }
 exports.removeUSBRuntimesObserver = removeUSBRuntimesObserver;
 
 function refreshUSBRuntimes() {
-  return adbScanner.scan();
+  return adb.updateRuntimes();
 }
 exports.refreshUSBRuntimes = refreshUSBRuntimes;
 
 require("./test-helper").enableMocks(module, "modules/usb-runtimes");
--- a/devtools/client/aboutdebugging-new/test/browser/browser.ini
+++ b/devtools/client/aboutdebugging-new/test/browser/browser.ini
@@ -34,12 +34,14 @@ skip-if = (os == 'linux' && bits == 32) 
 [browser_aboutdebugging_runtime_connection-prompt.js]
 [browser_aboutdebugging_select_network_runtime.js]
 [browser_aboutdebugging_sidebar_network_runtimes.js]
 [browser_aboutdebugging_sidebar_usb_runtime.js]
 [browser_aboutdebugging_sidebar_usb_runtime_connect.js]
 [browser_aboutdebugging_sidebar_usb_runtime_refresh.js]
 [browser_aboutdebugging_sidebar_usb_status.js]
 skip-if = (os == 'linux' && bits == 32) # ADB start() fails on linux 32, see Bug 1499638
+[browser_aboutdebugging_stop_adb.js]
+skip-if = (os == 'linux' && bits == 32) # ADB start() fails on linux 32, see Bug 1499638
 [browser_aboutdebugging_system_addons.js]
 [browser_aboutdebugging_tab_favicons.js]
 [browser_aboutdebugging_thisfirefox.js]
 [browser_aboutdebugging_thisfirefox_runtime_info.js]
--- a/devtools/client/aboutdebugging-new/test/browser/browser_aboutdebugging_connect_toggle_usb_devices.js
+++ b/devtools/client/aboutdebugging-new/test/browser/browser_aboutdebugging_connect_toggle_usb_devices.js
@@ -1,15 +1,15 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 const { AddonManager } = require("resource://gre/modules/AddonManager.jsm");
-const { ADB } = require("devtools/shared/adb/adb");
+const { adbProcess } = require("devtools/shared/adb/adb-process");
 
 /**
  * Check that USB Devices scanning can be enabled and disabled from the connect page.
  */
 add_task(async function() {
   await pushPref("devtools.remote.adb.extensionURL",
                  CHROME_URL_ROOT + "resources/test-adb-extension/adb-extension-#OS#.xpi");
 
@@ -43,17 +43,17 @@ add_task(async function() {
   Assert.deepEqual(addon.installTelemetryInfo, { source: "about:debugging" },
     "Got the expected addon.installTelemetryInfo");
 
   // Right now we are resuming as soon as "USB devices enabled" is displayed, but ADB
   // might still be starting up. If we move to uninstall directly, the ADB startup will
   // fail and we will have an unhandled promise rejection.
   // See Bug 1498469.
   info("Wait until ADB has started.");
-  await waitUntil(() => ADB.ready);
+  await waitUntil(() => adbProcess.ready);
 
   info("Click on the toggle button");
   usbToggleButton.click();
 
   info("Wait until the toggle button text is updated");
   await waitUntil(() => usbToggleButton.textContent.includes("Enable"));
   ok(document.querySelector(".js-connect-usb-disabled-message"),
     "The message about enabling USB devices is rendered again");
--- a/devtools/client/aboutdebugging-new/test/browser/browser_aboutdebugging_sidebar_usb_status.js
+++ b/devtools/client/aboutdebugging-new/test/browser/browser_aboutdebugging_sidebar_usb_status.js
@@ -1,15 +1,15 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 const { adbAddon } = require("devtools/shared/adb/adb-addon");
-const { ADB } = require("devtools/shared/adb/adb");
+const { adbProcess } = require("devtools/shared/adb/adb-process");
 
 /**
  * This test asserts that the sidebar shows a message describing the status of the USB
  * devices scanning.
  */
 add_task(async function() {
   await pushPref("devtools.remote.adb.extensionURL",
                  CHROME_URL_ROOT + "resources/test-adb-extension/adb-extension-#OS#.xpi");
@@ -26,16 +26,16 @@ add_task(async function() {
   adbAddon.install("internal");
   await waitUntil(() => usbStatusElement.textContent.includes("USB devices enabled"));
 
   // Right now we are resuming as soon as "USB devices enabled" is displayed, but ADB
   // might still be starting up. If we move to uninstall directly, the ADB startup will
   // fail and we will have an unhandled promise rejection.
   // See Bug 1498469.
   info("Wait until ADB has started.");
-  await waitUntil(() => ADB.ready);
+  await waitUntil(() => adbProcess.ready);
 
   info("Uninstall the adb extension and wait for the message to udpate");
   adbAddon.uninstall();
   await waitUntil(() => usbStatusElement.textContent.includes("USB devices disabled"));
 
   await removeTab(tab);
 });
new file mode 100644
--- /dev/null
+++ b/devtools/client/aboutdebugging-new/test/browser/browser_aboutdebugging_stop_adb.js
@@ -0,0 +1,79 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { adbAddon } = require("devtools/shared/adb/adb-addon");
+const { adbProcess } = require("devtools/shared/adb/adb-process");
+const { check } = require("devtools/shared/adb/adb-running-checker");
+
+/**
+ * Check that ADB is stopped:
+ * - when the adb extension is uninstalled
+ * - when no consumer is registered
+ */
+add_task(async function() {
+  await pushPref("devtools.remote.adb.extensionURL",
+                 CHROME_URL_ROOT + "resources/test-adb-extension/adb-extension-#OS#.xpi");
+
+  info("Check if ADB is already running before the test starts");
+  const isAdbAlreadyRunning = await check();
+  if (isAdbAlreadyRunning) {
+    ok(false, "The ADB process is already running on this machine, it should be " +
+      "stopped before running this test");
+    return;
+  }
+
+  const { tab } = await openAboutDebugging();
+
+  info("Install the adb extension and wait for ADB to start");
+  // Use "internal" as the install source to avoid triggering telemetry.
+  adbAddon.install("internal");
+  await waitForAdbStart();
+
+  info("Open a second about:debugging");
+  const { tab: secondTab } = await openAboutDebugging();
+
+  info("Close the second about:debugging and check that ADB is still running");
+  await removeTab(secondTab);
+  ok(await check(), "ADB is still running");
+
+  await removeTab(tab);
+
+  info("Check that the adb process stops after closing about:debugging");
+  await waitForAdbStop();
+
+  info("Open a third about:debugging, wait for the ADB to start again");
+  const { tab: thirdTab } = await openAboutDebugging();
+  await waitForAdbStart();
+
+  info("Uninstall the addon, this should stop ADB as well");
+  adbAddon.uninstall();
+  await waitForAdbStop();
+
+  info("Reinstall the addon, this should start ADB again");
+  adbAddon.install("internal");
+  await waitForAdbStart();
+
+  info("Close the last tab, this should stop ADB");
+  await removeTab(thirdTab);
+  await waitForAdbStop();
+});
+
+async function waitForAdbStart() {
+  info("Wait for ADB to start");
+  return asyncWaitUntil(async () => {
+    const isProcessReady = adbProcess.ready;
+    const isRunning = await check();
+    return isProcessReady && isRunning;
+  });
+}
+
+async function waitForAdbStop() {
+  info("Wait for ADB to stop");
+  return asyncWaitUntil(async () => {
+    const isProcessReady = adbProcess.ready;
+    const isRunning = await check();
+    return !isProcessReady && !isRunning;
+  });
+}
--- a/devtools/client/aboutdebugging-new/test/browser/head.js
+++ b/devtools/client/aboutdebugging-new/test/browser/head.js
@@ -25,18 +25,18 @@ Services.scriptloader.loadSubScript(
 // Make sure the ADB addon is removed and ADB is stopped when the test ends.
 registerCleanupFunction(async function() {
   try {
     const { adbAddon } = require("devtools/shared/adb/adb-addon");
     await adbAddon.uninstall();
   } catch (e) {
     // Will throw if the addon is already uninstalled, ignore exceptions here.
   }
-  const { ADB } = require("devtools/shared/adb/adb");
-  await ADB.kill();
+  const { adbProcess } = require("devtools/shared/adb/adb-process");
+  await adbProcess.kill();
 
   const { remoteClientManager } =
     require("devtools/client/shared/remote-debugging/remote-client-manager");
   await remoteClientManager.removeAllClients();
 });
 
 /**
  * Enable the new about:debugging panel.
--- a/devtools/client/webide/modules/runtimes.js
+++ b/devtools/client/webide/modules/runtimes.js
@@ -6,20 +6,17 @@
 
 const Services = require("Services");
 const {DebuggerServer} = require("devtools/server/main");
 const discovery = require("devtools/shared/discovery/discovery");
 const EventEmitter = require("devtools/shared/event-emitter");
 const {RuntimeTypes} = require("devtools/client/webide/modules/runtime-types");
 const promise = require("promise");
 
-loader.lazyGetter(this, "adbScanner", () => {
-  const { AddonAwareADBScanner } = require("devtools/shared/adb/addon-aware-adb-scanner");
-  return new AddonAwareADBScanner();
-});
+loader.lazyRequireGetter(this, "adb", "devtools/shared/adb/adb", true);
 
 loader.lazyRequireGetter(this, "AuthenticationResult",
   "devtools/shared/security/auth", true);
 loader.lazyRequireGetter(this, "DevToolsUtils",
   "devtools/shared/DevToolsUtils");
 
 const Strings = Services.strings.createBundle("chrome://devtools/locale/webide.properties");
 
@@ -193,19 +190,39 @@ var RuntimeScanners = {
 };
 
 EventEmitter.decorate(RuntimeScanners);
 
 exports.RuntimeScanners = RuntimeScanners;
 
 /* SCANNERS */
 
-// The adb-scanner will automatically start and stop when the ADB extension is installed
-// and uninstalled, so the scanner itself can always be used.
-RuntimeScanners.add(adbScanner);
+var UsbScanner = {
+  init() {
+    this._emitUpdated = this._emitUpdated.bind(this);
+  },
+  enable() {
+    adb.registerListener(this._emitUpdated);
+  },
+  disable() {
+    adb.unregisterListener(this._emitUpdated);
+  },
+  scan() {
+    return adb.updateRuntimes();
+  },
+  listRuntimes() {
+    return adb.getRuntimes();
+  },
+  _emitUpdated() {
+    this.emit("runtime-list-updated");
+  },
+};
+EventEmitter.decorate(UsbScanner);
+UsbScanner.init();
+RuntimeScanners.add(UsbScanner);
 
 var WiFiScanner = {
 
   _runtimes: [],
 
   init() {
     this.updateRegistration();
     Services.prefs.addObserver(this.ALLOWED_PREF, this);
--- a/devtools/shared/adb/adb-device.js
+++ b/devtools/shared/adb/adb-device.js
@@ -1,44 +1,44 @@
 /* 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";
 
-const { ADB } = require("devtools/shared/adb/adb");
+const { shell } = require("devtools/shared/adb/commands/index");
 
 /**
  * A Device instance is created and registered with the Devices module whenever
  * ADB notices a new device is connected.
  */
 class AdbDevice {
   constructor(id) {
     this.id = id;
   }
 
   async getModel() {
     if (this._model) {
       return this._model;
     }
-    const model = await ADB.shell("getprop ro.product.model");
+    const model = await shell("getprop ro.product.model");
     this._model = model.trim();
     return this._model;
   }
 
   // This method is not using any information from the instance, but in theory getting
   // runtime socket paths (as well as model) should be device specific. So we should use
   // information available on the instance when implementing multi device support.
   // See Bug 1507126.
   async getRuntimeSocketPaths() {
     // A matching entry looks like:
     // 00000000: 00000002 00000000 00010000 0001 01 6551588
     //  /data/data/org.mozilla.fennec/firefox-debugger-socket
     const query = "cat /proc/net/unix";
-    const rawSocketInfo = await ADB.shell(query);
+    const rawSocketInfo = await shell(query);
 
     // Filter to lines with "firefox-debugger-socket"
     let socketInfos = rawSocketInfo.split(/\r?\n/);
     socketInfos = socketInfos.filter(l => l.includes("firefox-debugger-socket"));
 
     // It's possible to have multiple lines with the same path, so de-dupe them
     const socketPaths = new Set();
     for (const socketInfo of socketInfos) {
deleted file mode 100644
--- a/devtools/shared/adb/adb-devices-registry.js
+++ /dev/null
@@ -1,80 +0,0 @@
-/* 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";
-
-const EventEmitter = require("devtools/shared/event-emitter");
-const { adbAddon, ADB_ADDON_STATES } = require("devtools/shared/adb/adb-addon");
-
-/**
- * Shared registry that will hold all the detected devices from ADB.
- * Extends EventEmitter and emits the following events:
- * - "register": a new device has been registered
- * - "unregister": a device has been unregistered
- */
-class AdbDevicesRegistry extends EventEmitter {
-  constructor() {
-    super();
-
-    // Internal object to store the discovered adb devices.
-    this._devices = {};
-
-    // When the addon is uninstalled, the repository should be emptied.
-    // TODO: This should also be done when ADB is stopped.
-    this._onAdbAddonUpdate = this._onAdbAddonUpdate.bind(this);
-    adbAddon.on("update", this._onAdbAddonUpdate);
-  }
-
-  /**
-   * Register a device (Device class defined in from adb-device.js) for the provided name.
-   *
-   * @param {String} name
-   *        Name of the device.
-   * @param {AdbDevice} device
-   *        The device to register.
-   */
-  register(name, device) {
-    this._devices[name] = device;
-    this.emit("register");
-  }
-
-  /**
-   * Unregister a device previously registered under the provided name.
-   *
-   * @param {String} name
-   *        Name of the device.
-   */
-  unregister(name) {
-    delete this._devices[name];
-    this.emit("unregister");
-  }
-
-  /**
-   * Returns an iterable containing the name of all the available devices, sorted by name.
-   */
-  available() {
-    return Object.keys(this._devices).sort();
-  }
-
-  /**
-   * Returns a device previously registered under the provided name.
-   *
-   * @param {String} name
-   *        Name of the device.
-   */
-  getByName(name) {
-    return this._devices[name];
-  }
-
-  _onAdbAddonUpdate() {
-    const installed = adbAddon.status === ADB_ADDON_STATES.INSTALLED;
-    if (!installed) {
-      for (const name in this._devices) {
-        this.unregister(name);
-      }
-    }
-  }
-}
-
-exports.adbDevicesRegistry = new AdbDevicesRegistry();
copy from devtools/shared/adb/adb.js
copy to devtools/shared/adb/adb-process.js
--- a/devtools/shared/adb/adb.js
+++ b/devtools/shared/adb/adb-process.js
@@ -1,424 +1,137 @@
 /* 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/. */
 
-// Wrapper around the ADB utility.
-
 "use strict";
 
 const { Cc, Ci } = require("chrome");
-const EventEmitter = require("devtools/shared/event-emitter");
-const client = require("./adb-client");
 const { dumpn } = require("devtools/shared/DevToolsUtils");
 const { getFileForBinary } = require("./adb-binary");
 const { setTimeout } = require("resource://gre/modules/Timer.jsm");
 const { Services } = require("resource://gre/modules/Services.jsm");
-const { ConnectionManager } = require("devtools/shared/client/connection-manager");
-loader.lazyRequireGetter(this, "check",
-                         "devtools/shared/adb/adb-running-checker", true);
 
-let ready = false;
-let didRunInitially = false;
+loader.lazyRequireGetter(this, "runCommand", "devtools/shared/adb/commands/index", true);
+loader.lazyRequireGetter(this, "check", "devtools/shared/adb/adb-running-checker", true);
 
-const OKAY = 0x59414b4f;
+// Waits until a predicate returns true or re-tries the predicate calls
+// |retry| times, we wait for 100ms between each calls.
+async function waitUntil(predicate, retry = 20) {
+  let count = 0;
+  while (count++ < retry) {
+    if (await predicate()) {
+      return true;
+    }
+    // Wait for 100 milliseconds.
+    await new Promise(resolve => setTimeout(resolve, 100));
+  }
+  // Timed out after trying too many times.
+  return false;
+}
 
-const ADB = {
-  get didRunInitially() {
-    return didRunInitially;
-  },
-  set didRunInitially(newVal) {
-    didRunInitially = newVal;
-  },
+// Class representing the ADB process.
+class AdbProcess {
+  constructor() {
+    this._ready = false;
+    this._didRunInitially = false;
+  }
 
   get ready() {
-    return ready;
-  },
-  set ready(newVal) {
-    ready = newVal;
-  },
+    return this._ready;
+  }
 
-  get adbFilePromise() {
+  _getAdbFile() {
     if (this._adbFilePromise) {
       return this._adbFilePromise;
     }
     this._adbFilePromise = getFileForBinary();
     return this._adbFilePromise;
-  },
+  }
 
   async _runProcess(process, params) {
     return new Promise((resolve, reject) => {
       process.runAsync(params, params.length, {
         observe(subject, topic, data) {
           switch (topic) {
             case "process-finished":
               resolve();
               break;
             case "process-failed":
               reject();
               break;
           }
         },
       }, false);
     });
-  },
-
-  // Waits until a predicate returns true or re-tries the predicate calls
-  // |retry| times, we wait for 100ms between each calls.
-  async _waitUntil(predicate, retry = 20) {
-    let count = 0;
-    while (count++ < retry) {
-      if (await predicate()) {
-        return true;
-      }
-      // Wait for 100 milliseconds.
-      await new Promise(resolve => setTimeout(resolve, 100));
-    }
-    // Timed out after trying too many times.
-    return false;
-  },
+  }
 
   // We startup by launching adb in server mode, and setting
   // the tcp socket preference to |true|
   async start() {
     return new Promise(async (resolve, reject) => {
       const onSuccessfulStart = () => {
         Services.obs.notifyObservers(null, "adb-ready");
-        this.ready = true;
+        this._ready = true;
         resolve();
       };
 
       const isAdbRunning = await check();
       if (isAdbRunning) {
         dumpn("Found ADB process running, not restarting");
         onSuccessfulStart();
         return;
       }
       dumpn("Didn't find ADB process running, restarting");
 
-      this.didRunInitially = true;
-      const process = Cc["@mozilla.org/process/util;1"]
-                      .createInstance(Ci.nsIProcess);
+      this._didRunInitially = true;
+      const process = Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess);
+
       // FIXME: Bug 1481691 - We should avoid extracting files every time.
-      const adbFile = await this.adbFilePromise;
+      const adbFile = await this._getAdbFile();
       process.init(adbFile);
       // Hide command prompt window on Windows
       process.startHidden = true;
       process.noShell = true;
       const params = ["start-server"];
       let isStarted = false;
       try {
         await this._runProcess(process, params);
-        isStarted = await this._waitUntil(check);
+        isStarted = await waitUntil(check);
       } catch (e) {
       }
 
       if (isStarted) {
         onSuccessfulStart();
       } else {
-        this.ready = false;
+        this._ready = false;
         reject();
       }
     });
-  },
+  }
 
   /**
    * Stop the ADB server, but only if we started it.  If it was started before
    * us, we return immediately.
    */
   async stop() {
-    if (!this.didRunInitially) {
+    if (!this._didRunInitially) {
       return; // We didn't start the server, nothing to do
     }
     await this.kill();
-  },
+  }
 
   /**
    * Kill the ADB server.
    */
   async kill() {
     try {
-      await this.runCommand("host:kill");
+      await runCommand("host:kill");
     } catch (e) {
       dumpn("Failed to send host:kill command");
     }
     dumpn("adb server was terminated by host:kill");
-    this.ready = false;
-    this.didRunInitially = false;
-  },
-
-  // Start tracking devices connecting and disconnecting from the host.
-  // We can't reuse runCommand here because we keep the socket alive.
-  // @return The socket used.
-  trackDevices() {
-    dumpn("trackDevices");
-    const socket = client.connect();
-    let waitForFirst = true;
-    const devices = {};
-
-    socket.s.onopen = function() {
-      dumpn("trackDevices onopen");
-      Services.obs.notifyObservers(null, "adb-track-devices-start");
-      const req = client.createRequest("host:track-devices");
-      socket.send(req);
-    };
-
-    socket.s.onerror = function(event) {
-      dumpn("trackDevices onerror: " + event);
-      Services.obs.notifyObservers(null, "adb-track-devices-stop");
-    };
-
-    socket.s.onclose = function() {
-      dumpn("trackDevices onclose");
-
-      // Report all devices as disconnected
-      for (const dev in devices) {
-        devices[dev] = false;
-        EventEmitter.emit(ADB, "device-disconnected", dev);
-      }
-
-      Services.obs.notifyObservers(null, "adb-track-devices-stop");
-
-      // When we lose connection to the server,
-      // and the adb is still on, we most likely got our server killed
-      // by local adb. So we do try to reconnect to it.
-      setTimeout(function() { // Give some time to the new adb to start
-        if (ADB.ready) { // Only try to reconnect/restart if the add-on is still enabled
-          ADB.start().then(function() { // try to connect to the new local adb server
-                                         // or, spawn a new one
-            ADB.trackDevices(); // Re-track devices
-          });
-        }
-      }, 2000);
-    };
-
-    socket.s.ondata = function(event) {
-      dumpn("trackDevices ondata");
-      const data = event.data;
-      dumpn("length=" + data.byteLength);
-      const dec = new TextDecoder();
-      dumpn(dec.decode(new Uint8Array(data)).trim());
-
-      // check the OKAY or FAIL on first packet.
-      if (waitForFirst) {
-        if (!client.checkResponse(data, OKAY)) {
-          socket.close();
-          return;
-        }
-      }
-
-      const packet = client.unpackPacket(data, !waitForFirst);
-      waitForFirst = false;
-
-      if (packet.data == "") {
-        // All devices got disconnected.
-        for (const dev in devices) {
-          devices[dev] = false;
-          EventEmitter.emit(ADB, "device-disconnected", dev);
-        }
-      } else {
-        // One line per device, each line being $DEVICE\t(offline|device)
-        const lines = packet.data.split("\n");
-        const newDev = {};
-        lines.forEach(function(line) {
-          if (line.length == 0) {
-            return;
-          }
-
-          const [dev, status] = line.split("\t");
-          newDev[dev] = status !== "offline";
-        });
-        // Check which device changed state.
-        for (const dev in newDev) {
-          if (devices[dev] != newDev[dev]) {
-            if (dev in devices || newDev[dev]) {
-              const topic = newDev[dev] ? "device-connected"
-                                        : "device-disconnected";
-              EventEmitter.emit(ADB, topic, dev);
-            }
-            devices[dev] = newDev[dev];
-          }
-        }
-      }
-    };
-  },
-
-  // Sends back an array of device names.
-  listDevices() {
-    dumpn("listDevices");
-
-    return this.runCommand("host:devices").then(
-      function onSuccess(data) {
-        const lines = data.split("\n");
-        const res = [];
-        lines.forEach(function(line) {
-          if (line.length == 0) {
-            return;
-          }
-          const [ device ] = line.split("\t");
-          res.push(device);
-        });
-        return res;
-      }
-    );
-  },
-
-  // sends adb forward localPort devicePort
-  forwardPort(localPort, devicePort) {
-    dumpn("forwardPort " + localPort + " -- " + devicePort);
-    // <host-prefix>:forward:<local>;<remote>
-
-    return this.runCommand("host:forward:" + localPort + ";" + devicePort)
-               .then(function onSuccess(data) {
-                 return data;
-               });
-  },
+    this._ready = false;
+    this._didRunInitially = false;
+  }
+}
 
-  // Prepare TCP connection for provided socket path.
-  // The returned value is a port number of localhost for the connection.
-  async prepareTCPConnection(socketPath) {
-    const port = ConnectionManager.getFreeTCPPort();
-    const local = `tcp:${ port }`;
-    const remote = socketPath.startsWith("@")
-                     ? `localabstract:${ socketPath.substring(1) }`
-                     : `localfilesystem:${ socketPath }`;
-    await this.forwardPort(local, remote);
-    return port;
-  },
-
-  // Run a shell command
-  async shell(command) {
-    let state;
-    let stdout = "";
-
-    dumpn("shell " + command);
-
-    return new Promise((resolve, reject) => {
-      const shutdown = function() {
-        dumpn("shell shutdown");
-        socket.close();
-        reject("BAD_RESPONSE");
-      };
-
-      const runFSM = function runFSM(data) {
-        dumpn("runFSM " + state);
-        let req;
-        let ignoreResponseCode = false;
-        switch (state) {
-          case "start":
-            state = "send-transport";
-            runFSM();
-            break;
-          case "send-transport":
-            req = client.createRequest("host:transport-any");
-            socket.send(req);
-            state = "wait-transport";
-            break;
-          case "wait-transport":
-            if (!client.checkResponse(data, OKAY)) {
-              shutdown();
-              return;
-            }
-            state = "send-shell";
-            runFSM();
-            break;
-          case "send-shell":
-            req = client.createRequest("shell:" + command);
-            socket.send(req);
-            state = "rec-shell";
-            break;
-          case "rec-shell":
-            if (!client.checkResponse(data, OKAY)) {
-              shutdown();
-              return;
-            }
-            state = "decode-shell";
-            if (client.getBuffer(data).byteLength == 4) {
-              break;
-            }
-            ignoreResponseCode = true;
-            // eslint-disable-next-lined no-fallthrough
-          case "decode-shell":
-            const decoder = new TextDecoder();
-            const text = new Uint8Array(client.getBuffer(data),
-                                        ignoreResponseCode ? 4 : 0);
-            stdout += decoder.decode(text);
-            break;
-          default:
-            dumpn("shell Unexpected State: " + state);
-            reject("UNEXPECTED_STATE");
-        }
-      };
-
-      const socket = client.connect();
-      socket.s.onerror = function(event) {
-        dumpn("shell onerror");
-        reject("SOCKET_ERROR");
-      };
-
-      socket.s.onopen = function(event) {
-        dumpn("shell onopen");
-        state = "start";
-        runFSM();
-      };
-
-      socket.s.onclose = function(event) {
-        resolve(stdout);
-        dumpn("shell onclose");
-      };
-
-      socket.s.ondata = function(event) {
-        dumpn("shell ondata");
-        runFSM(event.data);
-      };
-    });
-  },
-
-  // Asynchronously runs an adb command.
-  // @param command The command as documented in
-  // http://androidxref.com/4.0.4/xref/system/core/adb/SERVICES.TXT
-  runCommand(command) {
-    dumpn("runCommand " + command);
-    return new Promise((resolve, reject) => {
-      if (!this.ready) {
-        setTimeout(function() {
-          reject("ADB_NOT_READY");
-        });
-        return;
-      }
-
-      const socket = client.connect();
-
-      socket.s.onopen = function() {
-        dumpn("runCommand onopen");
-        const req = client.createRequest(command);
-        socket.send(req);
-      };
-
-      socket.s.onerror = function() {
-        dumpn("runCommand onerror");
-        reject("NETWORK_ERROR");
-      };
-
-      socket.s.onclose = function() {
-        dumpn("runCommand onclose");
-      };
-
-      socket.s.ondata = function(event) {
-        dumpn("runCommand ondata");
-        const data = event.data;
-
-        const packet = client.unpackPacket(data, false);
-        if (!client.checkResponse(data, OKAY)) {
-          socket.close();
-          dumpn("Error: " + packet.data);
-          reject("PROTOCOL_ERROR");
-          return;
-        }
-
-        resolve(packet.data);
-      };
-    });
-  },
-};
-
-exports.ADB = ADB;
+exports.adbProcess = new AdbProcess();
--- a/devtools/shared/adb/adb-runtime.js
+++ b/devtools/shared/adb/adb-runtime.js
@@ -1,16 +1,16 @@
 /* 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";
 
 const { RuntimeTypes } = require("devtools/client/webide/modules/runtime-types");
-const { ADB } = require("devtools/shared/adb/adb");
+const { prepareTCPConnection } = require("devtools/shared/adb/commands/index");
 
 class AdbRuntime {
   constructor(adbDevice, model, socketPath) {
     this.type = RuntimeTypes.USB;
 
     this._adbDevice = adbDevice;
     this._model = model;
     this._socketPath = socketPath;
@@ -28,17 +28,17 @@ class AdbRuntime {
     return `Firefox ${this._channel()}`;
   }
 
   get name() {
     return `Firefox ${this._channel()} on Android (${this.deviceName})`;
   }
 
   connect(connection) {
-    return ADB.prepareTCPConnection(this._socketPath).then(port => {
+    return prepareTCPConnection(this._socketPath).then(port => {
       connection.host = "localhost";
       connection.port = port;
       connection.connect();
     });
   }
 
   _channel() {
     const packageName = this._packageName();
deleted file mode 100644
--- a/devtools/shared/adb/adb-scanner.js
+++ /dev/null
@@ -1,98 +0,0 @@
-/* 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";
-
-const EventEmitter = require("devtools/shared/event-emitter");
-const { dumpn } = require("devtools/shared/DevToolsUtils");
-const { ADB } = require("devtools/shared/adb/adb");
-const { adbDevicesRegistry } = require("devtools/shared/adb/adb-devices-registry");
-const { AdbRuntime } = require("devtools/shared/adb/adb-runtime");
-
-loader.lazyRequireGetter(this, "AdbDevice", "devtools/shared/adb/adb-device");
-
-class ADBScanner extends EventEmitter {
-  constructor() {
-    super();
-    this._runtimes = [];
-
-    this._onDeviceConnected = this._onDeviceConnected.bind(this);
-    this._onDeviceDisconnected = this._onDeviceDisconnected.bind(this);
-    this._updateRuntimes = this._updateRuntimes.bind(this);
-  }
-
-  enable() {
-    EventEmitter.on(ADB, "device-connected", this._onDeviceConnected);
-    EventEmitter.on(ADB, "device-disconnected", this._onDeviceDisconnected);
-
-    adbDevicesRegistry.on("register", this._updateRuntimes);
-    adbDevicesRegistry.on("unregister", this._updateRuntimes);
-
-    ADB.start().then(() => {
-      ADB.trackDevices();
-    });
-    this._updateRuntimes();
-  }
-
-  disable() {
-    EventEmitter.off(ADB, "device-connected", this._onDeviceConnected);
-    EventEmitter.off(ADB, "device-disconnected", this._onDeviceDisconnected);
-    adbDevicesRegistry.off("register", this._updateRuntimes);
-    adbDevicesRegistry.off("unregister", this._updateRuntimes);
-    this._updateRuntimes();
-  }
-
-  _emitUpdated() {
-    this.emit("runtime-list-updated");
-  }
-
-  _onDeviceConnected(deviceId) {
-    const device = new AdbDevice(deviceId);
-    adbDevicesRegistry.register(deviceId, device);
-  }
-
-  _onDeviceDisconnected(deviceId) {
-    adbDevicesRegistry.unregister(deviceId);
-  }
-
-  _updateRuntimes() {
-    if (this._updatingPromise) {
-      return this._updatingPromise;
-    }
-    this._runtimes = [];
-    const promises = [];
-    for (const id of adbDevicesRegistry.available()) {
-      const device = adbDevicesRegistry.getByName(id);
-      promises.push(this._detectRuntimes(device));
-    }
-    this._updatingPromise = Promise.all(promises);
-    this._updatingPromise.then(() => {
-      this._emitUpdated();
-      this._updatingPromise = null;
-    }, () => {
-      this._updatingPromise = null;
-    });
-    return this._updatingPromise;
-  }
-
-  async _detectRuntimes(adbDevice) {
-    const model = await adbDevice.getModel();
-    const socketPaths = await adbDevice.getRuntimeSocketPaths();
-    for (const socketPath of socketPaths) {
-      const runtime = new AdbRuntime(adbDevice, model, socketPath);
-      dumpn("Found " + runtime.name);
-      this._runtimes.push(runtime);
-    }
-  }
-
-  scan() {
-    return this._updateRuntimes();
-  }
-
-  listRuntimes() {
-    return this._runtimes;
-  }
-}
-
-exports.ADBScanner = ADBScanner;
--- a/devtools/shared/adb/adb.js
+++ b/devtools/shared/adb/adb.js
@@ -1,424 +1,111 @@
 /* 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/. */
 
-// Wrapper around the ADB utility.
-
 "use strict";
 
-const { Cc, Ci } = require("chrome");
 const EventEmitter = require("devtools/shared/event-emitter");
-const client = require("./adb-client");
-const { dumpn } = require("devtools/shared/DevToolsUtils");
-const { getFileForBinary } = require("./adb-binary");
-const { setTimeout } = require("resource://gre/modules/Timer.jsm");
-const { Services } = require("resource://gre/modules/Services.jsm");
-const { ConnectionManager } = require("devtools/shared/client/connection-manager");
-loader.lazyRequireGetter(this, "check",
-                         "devtools/shared/adb/adb-running-checker", true);
-
-let ready = false;
-let didRunInitially = false;
-
-const OKAY = 0x59414b4f;
-
-const ADB = {
-  get didRunInitially() {
-    return didRunInitially;
-  },
-  set didRunInitially(newVal) {
-    didRunInitially = newVal;
-  },
-
-  get ready() {
-    return ready;
-  },
-  set ready(newVal) {
-    ready = newVal;
-  },
-
-  get adbFilePromise() {
-    if (this._adbFilePromise) {
-      return this._adbFilePromise;
-    }
-    this._adbFilePromise = getFileForBinary();
-    return this._adbFilePromise;
-  },
+const { adbProcess } = require("devtools/shared/adb/adb-process");
+const { adbAddon } = require("devtools/shared/adb/adb-addon");
+const AdbDevice = require("devtools/shared/adb/adb-device");
+const { AdbRuntime } = require("devtools/shared/adb/adb-runtime");
+const { TrackDevicesCommand } = require("devtools/shared/adb/commands/track-devices");
 
-  async _runProcess(process, params) {
-    return new Promise((resolve, reject) => {
-      process.runAsync(params, params.length, {
-        observe(subject, topic, data) {
-          switch (topic) {
-            case "process-finished":
-              resolve();
-              break;
-            case "process-failed":
-              reject();
-              break;
-          }
-        },
-      }, false);
-    });
-  },
+class Adb extends EventEmitter {
+  constructor() {
+    super();
+
+    this._trackDevicesCommand = new TrackDevicesCommand();
 
-  // Waits until a predicate returns true or re-tries the predicate calls
-  // |retry| times, we wait for 100ms between each calls.
-  async _waitUntil(predicate, retry = 20) {
-    let count = 0;
-    while (count++ < retry) {
-      if (await predicate()) {
-        return true;
-      }
-      // Wait for 100 milliseconds.
-      await new Promise(resolve => setTimeout(resolve, 100));
-    }
-    // Timed out after trying too many times.
-    return false;
-  },
+    this._isTrackingDevices = false;
+    this._isUpdatingRuntimes = false;
 
-  // We startup by launching adb in server mode, and setting
-  // the tcp socket preference to |true|
-  async start() {
-    return new Promise(async (resolve, reject) => {
-      const onSuccessfulStart = () => {
-        Services.obs.notifyObservers(null, "adb-ready");
-        this.ready = true;
-        resolve();
-      };
-
-      const isAdbRunning = await check();
-      if (isAdbRunning) {
-        dumpn("Found ADB process running, not restarting");
-        onSuccessfulStart();
-        return;
-      }
-      dumpn("Didn't find ADB process running, restarting");
+    this._listeners = new Set();
+    this._devices = new Map();
+    this._runtimes = [];
 
-      this.didRunInitially = true;
-      const process = Cc["@mozilla.org/process/util;1"]
-                      .createInstance(Ci.nsIProcess);
-      // FIXME: Bug 1481691 - We should avoid extracting files every time.
-      const adbFile = await this.adbFilePromise;
-      process.init(adbFile);
-      // Hide command prompt window on Windows
-      process.startHidden = true;
-      process.noShell = true;
-      const params = ["start-server"];
-      let isStarted = false;
-      try {
-        await this._runProcess(process, params);
-        isStarted = await this._waitUntil(check);
-      } catch (e) {
-      }
+    this._updateAdbProcess = this._updateAdbProcess.bind(this);
+    this._onDeviceConnected = this._onDeviceConnected.bind(this);
+    this._onDeviceDisconnected = this._onDeviceDisconnected.bind(this);
 
-      if (isStarted) {
-        onSuccessfulStart();
-      } else {
-        this.ready = false;
-        reject();
-      }
-    });
-  },
-
-  /**
-   * Stop the ADB server, but only if we started it.  If it was started before
-   * us, we return immediately.
-   */
-  async stop() {
-    if (!this.didRunInitially) {
-      return; // We didn't start the server, nothing to do
-    }
-    await this.kill();
-  },
-
-  /**
-   * Kill the ADB server.
-   */
-  async kill() {
-    try {
-      await this.runCommand("host:kill");
-    } catch (e) {
-      dumpn("Failed to send host:kill command");
-    }
-    dumpn("adb server was terminated by host:kill");
-    this.ready = false;
-    this.didRunInitially = false;
-  },
+    this._trackDevicesCommand.on("device-connected", this._onDeviceConnected);
+    this._trackDevicesCommand.on("device-disconnected", this._onDeviceDisconnected);
+    adbAddon.on("update", this._updateAdbProcess);
+  }
 
-  // Start tracking devices connecting and disconnecting from the host.
-  // We can't reuse runCommand here because we keep the socket alive.
-  // @return The socket used.
-  trackDevices() {
-    dumpn("trackDevices");
-    const socket = client.connect();
-    let waitForFirst = true;
-    const devices = {};
-
-    socket.s.onopen = function() {
-      dumpn("trackDevices onopen");
-      Services.obs.notifyObservers(null, "adb-track-devices-start");
-      const req = client.createRequest("host:track-devices");
-      socket.send(req);
-    };
-
-    socket.s.onerror = function(event) {
-      dumpn("trackDevices onerror: " + event);
-      Services.obs.notifyObservers(null, "adb-track-devices-stop");
-    };
-
-    socket.s.onclose = function() {
-      dumpn("trackDevices onclose");
-
-      // Report all devices as disconnected
-      for (const dev in devices) {
-        devices[dev] = false;
-        EventEmitter.emit(ADB, "device-disconnected", dev);
-      }
+  registerListener(listener) {
+    this._listeners.add(listener);
+    this.on("runtime-list-updated", listener);
+    this._updateAdbProcess();
+  }
 
-      Services.obs.notifyObservers(null, "adb-track-devices-stop");
-
-      // When we lose connection to the server,
-      // and the adb is still on, we most likely got our server killed
-      // by local adb. So we do try to reconnect to it.
-      setTimeout(function() { // Give some time to the new adb to start
-        if (ADB.ready) { // Only try to reconnect/restart if the add-on is still enabled
-          ADB.start().then(function() { // try to connect to the new local adb server
-                                         // or, spawn a new one
-            ADB.trackDevices(); // Re-track devices
-          });
-        }
-      }, 2000);
-    };
+  unregisterListener(listener) {
+    this._listeners.delete(listener);
+    this.off("runtime-list-updated", listener);
+    this._updateAdbProcess();
+  }
 
-    socket.s.ondata = function(event) {
-      dumpn("trackDevices ondata");
-      const data = event.data;
-      dumpn("length=" + data.byteLength);
-      const dec = new TextDecoder();
-      dumpn(dec.decode(new Uint8Array(data)).trim());
-
-      // check the OKAY or FAIL on first packet.
-      if (waitForFirst) {
-        if (!client.checkResponse(data, OKAY)) {
-          socket.close();
-          return;
-        }
-      }
-
-      const packet = client.unpackPacket(data, !waitForFirst);
-      waitForFirst = false;
+  async updateRuntimes() {
+    try {
+      const devices = [...this._devices.values()];
+      const promises = devices.map(d => this._getDeviceRuntimes(d));
+      const allRuntimes = await Promise.all(promises);
 
-      if (packet.data == "") {
-        // All devices got disconnected.
-        for (const dev in devices) {
-          devices[dev] = false;
-          EventEmitter.emit(ADB, "device-disconnected", dev);
-        }
-      } else {
-        // One line per device, each line being $DEVICE\t(offline|device)
-        const lines = packet.data.split("\n");
-        const newDev = {};
-        lines.forEach(function(line) {
-          if (line.length == 0) {
-            return;
-          }
+      this._runtimes = allRuntimes.flat();
+      this.emit("runtime-list-updated");
+    } catch (e) {
+      console.error(e);
+    }
+  }
 
-          const [dev, status] = line.split("\t");
-          newDev[dev] = status !== "offline";
-        });
-        // Check which device changed state.
-        for (const dev in newDev) {
-          if (devices[dev] != newDev[dev]) {
-            if (dev in devices || newDev[dev]) {
-              const topic = newDev[dev] ? "device-connected"
-                                        : "device-disconnected";
-              EventEmitter.emit(ADB, topic, dev);
-            }
-            devices[dev] = newDev[dev];
-          }
-        }
-      }
-    };
-  },
-
-  // Sends back an array of device names.
-  listDevices() {
-    dumpn("listDevices");
+  getRuntimes() {
+    return this._runtimes;
+  }
 
-    return this.runCommand("host:devices").then(
-      function onSuccess(data) {
-        const lines = data.split("\n");
-        const res = [];
-        lines.forEach(function(line) {
-          if (line.length == 0) {
-            return;
-          }
-          const [ device ] = line.split("\t");
-          res.push(device);
-        });
-        return res;
-      }
-    );
-  },
+  async _startAdb() {
+    this._isTrackingDevices = true;
+    await adbProcess.start();
 
-  // sends adb forward localPort devicePort
-  forwardPort(localPort, devicePort) {
-    dumpn("forwardPort " + localPort + " -- " + devicePort);
-    // <host-prefix>:forward:<local>;<remote>
+    this._trackDevicesCommand.run();
+  }
 
-    return this.runCommand("host:forward:" + localPort + ";" + devicePort)
-               .then(function onSuccess(data) {
-                 return data;
-               });
-  },
+  async _stopAdb() {
+    this._isTrackingDevices = false;
+    this._trackDevicesCommand.stop();
+    await adbProcess.stop();
 
-  // Prepare TCP connection for provided socket path.
-  // The returned value is a port number of localhost for the connection.
-  async prepareTCPConnection(socketPath) {
-    const port = ConnectionManager.getFreeTCPPort();
-    const local = `tcp:${ port }`;
-    const remote = socketPath.startsWith("@")
-                     ? `localabstract:${ socketPath.substring(1) }`
-                     : `localfilesystem:${ socketPath }`;
-    await this.forwardPort(local, remote);
-    return port;
-  },
-
-  // Run a shell command
-  async shell(command) {
-    let state;
-    let stdout = "";
-
-    dumpn("shell " + command);
-
-    return new Promise((resolve, reject) => {
-      const shutdown = function() {
-        dumpn("shell shutdown");
-        socket.close();
-        reject("BAD_RESPONSE");
-      };
+    this._devices = new Map();
+    this._runtimes = [];
+    this.updateRuntimes();
+  }
 
-      const runFSM = function runFSM(data) {
-        dumpn("runFSM " + state);
-        let req;
-        let ignoreResponseCode = false;
-        switch (state) {
-          case "start":
-            state = "send-transport";
-            runFSM();
-            break;
-          case "send-transport":
-            req = client.createRequest("host:transport-any");
-            socket.send(req);
-            state = "wait-transport";
-            break;
-          case "wait-transport":
-            if (!client.checkResponse(data, OKAY)) {
-              shutdown();
-              return;
-            }
-            state = "send-shell";
-            runFSM();
-            break;
-          case "send-shell":
-            req = client.createRequest("shell:" + command);
-            socket.send(req);
-            state = "rec-shell";
-            break;
-          case "rec-shell":
-            if (!client.checkResponse(data, OKAY)) {
-              shutdown();
-              return;
-            }
-            state = "decode-shell";
-            if (client.getBuffer(data).byteLength == 4) {
-              break;
-            }
-            ignoreResponseCode = true;
-            // eslint-disable-next-lined no-fallthrough
-          case "decode-shell":
-            const decoder = new TextDecoder();
-            const text = new Uint8Array(client.getBuffer(data),
-                                        ignoreResponseCode ? 4 : 0);
-            stdout += decoder.decode(text);
-            break;
-          default:
-            dumpn("shell Unexpected State: " + state);
-            reject("UNEXPECTED_STATE");
-        }
-      };
+  _shouldTrack() {
+    return adbAddon.status === "installed" && this._listeners.size > 0;
+  }
 
-      const socket = client.connect();
-      socket.s.onerror = function(event) {
-        dumpn("shell onerror");
-        reject("SOCKET_ERROR");
-      };
-
-      socket.s.onopen = function(event) {
-        dumpn("shell onopen");
-        state = "start";
-        runFSM();
-      };
+  _updateAdbProcess() {
+    if (!this._isTrackingDevices && this._shouldTrack()) {
+      this._startAdb();
+    } else if (this._isTrackingDevices && !this._shouldTrack()) {
+      this._stopAdb();
+    }
+  }
 
-      socket.s.onclose = function(event) {
-        resolve(stdout);
-        dumpn("shell onclose");
-      };
-
-      socket.s.ondata = function(event) {
-        dumpn("shell ondata");
-        runFSM(event.data);
-      };
-    });
-  },
-
-  // Asynchronously runs an adb command.
-  // @param command The command as documented in
-  // http://androidxref.com/4.0.4/xref/system/core/adb/SERVICES.TXT
-  runCommand(command) {
-    dumpn("runCommand " + command);
-    return new Promise((resolve, reject) => {
-      if (!this.ready) {
-        setTimeout(function() {
-          reject("ADB_NOT_READY");
-        });
-        return;
-      }
-
-      const socket = client.connect();
+  _onDeviceConnected(deviceId) {
+    this._devices.set(deviceId, new AdbDevice(deviceId));
+    this.updateRuntimes();
+  }
 
-      socket.s.onopen = function() {
-        dumpn("runCommand onopen");
-        const req = client.createRequest(command);
-        socket.send(req);
-      };
-
-      socket.s.onerror = function() {
-        dumpn("runCommand onerror");
-        reject("NETWORK_ERROR");
-      };
-
-      socket.s.onclose = function() {
-        dumpn("runCommand onclose");
-      };
+  _onDeviceDisconnected(deviceId) {
+    this._devices.delete(deviceId);
+    this.updateRuntimes();
+  }
 
-      socket.s.ondata = function(event) {
-        dumpn("runCommand ondata");
-        const data = event.data;
+  async _getDeviceRuntimes(device) {
+    const model = await device.getModel();
+    const socketPaths = await device.getRuntimeSocketPaths();
+    return [...socketPaths].map(socketPath => new AdbRuntime(device, model, socketPath));
+  }
+}
 
-        const packet = client.unpackPacket(data, false);
-        if (!client.checkResponse(data, OKAY)) {
-          socket.close();
-          dumpn("Error: " + packet.data);
-          reject("PROTOCOL_ERROR");
-          return;
-        }
-
-        resolve(packet.data);
-      };
-    });
-  },
-};
-
-exports.ADB = ADB;
+exports.adb = new Adb();
deleted file mode 100644
--- a/devtools/shared/adb/addon-aware-adb-scanner.js
+++ /dev/null
@@ -1,98 +0,0 @@
-/* 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";
-
-const EventEmitter = require("devtools/shared/event-emitter");
-
-loader.lazyRequireGetter(this, "adbAddon", "devtools/shared/adb/adb-addon", true);
-loader.lazyRequireGetter(this, "ADBScanner", "devtools/shared/adb/adb-scanner", true);
-
-/**
- * The AddonAwareADBScanner decorates an instance of ADBScanner. It will wait until the
- * ADB addon is installed to enable() the real scanner, and will automatically disable
- * it if the addon is uninstalled.
- *
- * It implements the following public API of ADBScanner:
- * - enable
- * - disable
- * - scan
- * - listRuntimes
- * - event "runtime-list-updated"
- */
-class AddonAwareADBScanner extends EventEmitter {
-  /**
-   * Parameters are provided only to allow tests to replace actual implementations with
-   * mocks.
-   *
-   * @param {ADBScanner} scanner
-   *        Only provided in tests for mocks
-   * @param {ADBAddon} addon
-   *        Only provided in tests for mocks
-   */
-  constructor(scanner = new ADBScanner(), addon = adbAddon) {
-    super();
-
-    this._onScannerListUpdated = this._onScannerListUpdated.bind(this);
-    this._onAddonUpdate = this._onAddonUpdate.bind(this);
-
-    this._scanner = scanner;
-    this._scanner.on("runtime-list-updated", this._onScannerListUpdated);
-
-    this._addon = addon;
-  }
-
-  /**
-   * Only forward the enable() call if the addon is installed, because ADBScanner::enable
-   * only works if the addon is installed.
-   */
-  enable() {
-    if (this._addon.status === "installed") {
-      this._scanner.enable();
-    }
-
-    // Remove any previous listener, to make sure we only add one listener if enable() is
-    // called several times.
-    this._addon.off("update", this._onAddonUpdate);
-
-    this._addon.on("update", this._onAddonUpdate);
-  }
-
-  disable() {
-    this._scanner.disable();
-
-    this._addon.off("update", this._onAddonUpdate);
-  }
-
-  /**
-   * Scan for USB devices.
-   *
-   * @return {Promise} Promise that will resolve when the scan is completed.
-   */
-  scan() {
-    return this._scanner.scan();
-  }
-
-  /**
-   * Get the list of currently detected runtimes.
-   *
-   * @return {Array} Array of currently detected runtimes.
-   */
-  listRuntimes() {
-    return this._scanner.listRuntimes();
-  }
-
-  _onAddonUpdate() {
-    if (this._addon.status === "installed") {
-      this._scanner.enable();
-    } else {
-      this._scanner.disable();
-    }
-  }
-
-  _onScannerListUpdated() {
-    this.emit("runtime-list-updated");
-  }
-}
-exports.AddonAwareADBScanner = AddonAwareADBScanner;
new file mode 100644
--- /dev/null
+++ b/devtools/shared/adb/commands/index.js
@@ -0,0 +1,19 @@
+/* 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";
+
+const { listDevices } = require("./list-devices");
+const { prepareTCPConnection } = require("./prepare-tcp-connection");
+const { runCommand } = require("./run-command");
+const { shell } = require("./shell");
+const { TrackDevicesCommand } = require("./track-devices");
+
+module.exports = {
+  listDevices,
+  prepareTCPConnection,
+  runCommand,
+  shell,
+  TrackDevicesCommand,
+};
copy from devtools/shared/adb/adb.js
copy to devtools/shared/adb/commands/list-devices.js
--- a/devtools/shared/adb/adb.js
+++ b/devtools/shared/adb/commands/list-devices.js
@@ -1,424 +1,31 @@
 /* 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/. */
 
-// Wrapper around the ADB utility.
-
 "use strict";
 
-const { Cc, Ci } = require("chrome");
-const EventEmitter = require("devtools/shared/event-emitter");
-const client = require("./adb-client");
 const { dumpn } = require("devtools/shared/DevToolsUtils");
-const { getFileForBinary } = require("./adb-binary");
-const { setTimeout } = require("resource://gre/modules/Timer.jsm");
-const { Services } = require("resource://gre/modules/Services.jsm");
-const { ConnectionManager } = require("devtools/shared/client/connection-manager");
-loader.lazyRequireGetter(this, "check",
-                         "devtools/shared/adb/adb-running-checker", true);
-
-let ready = false;
-let didRunInitially = false;
-
-const OKAY = 0x59414b4f;
-
-const ADB = {
-  get didRunInitially() {
-    return didRunInitially;
-  },
-  set didRunInitially(newVal) {
-    didRunInitially = newVal;
-  },
-
-  get ready() {
-    return ready;
-  },
-  set ready(newVal) {
-    ready = newVal;
-  },
-
-  get adbFilePromise() {
-    if (this._adbFilePromise) {
-      return this._adbFilePromise;
-    }
-    this._adbFilePromise = getFileForBinary();
-    return this._adbFilePromise;
-  },
-
-  async _runProcess(process, params) {
-    return new Promise((resolve, reject) => {
-      process.runAsync(params, params.length, {
-        observe(subject, topic, data) {
-          switch (topic) {
-            case "process-finished":
-              resolve();
-              break;
-            case "process-failed":
-              reject();
-              break;
-          }
-        },
-      }, false);
-    });
-  },
-
-  // Waits until a predicate returns true or re-tries the predicate calls
-  // |retry| times, we wait for 100ms between each calls.
-  async _waitUntil(predicate, retry = 20) {
-    let count = 0;
-    while (count++ < retry) {
-      if (await predicate()) {
-        return true;
-      }
-      // Wait for 100 milliseconds.
-      await new Promise(resolve => setTimeout(resolve, 100));
-    }
-    // Timed out after trying too many times.
-    return false;
-  },
-
-  // We startup by launching adb in server mode, and setting
-  // the tcp socket preference to |true|
-  async start() {
-    return new Promise(async (resolve, reject) => {
-      const onSuccessfulStart = () => {
-        Services.obs.notifyObservers(null, "adb-ready");
-        this.ready = true;
-        resolve();
-      };
-
-      const isAdbRunning = await check();
-      if (isAdbRunning) {
-        dumpn("Found ADB process running, not restarting");
-        onSuccessfulStart();
-        return;
-      }
-      dumpn("Didn't find ADB process running, restarting");
+/**
+ * The listDevices command is currently unused in DevTools. We are keeping it while
+ * working on RemoteDebugging NG, in case it becomes needed later. We will remove it from
+ * the codebase if unused at the end of the project. See Bug 1511779.
+ */
+const listDevices = function() {
+  dumpn("listDevices");
 
-      this.didRunInitially = true;
-      const process = Cc["@mozilla.org/process/util;1"]
-                      .createInstance(Ci.nsIProcess);
-      // FIXME: Bug 1481691 - We should avoid extracting files every time.
-      const adbFile = await this.adbFilePromise;
-      process.init(adbFile);
-      // Hide command prompt window on Windows
-      process.startHidden = true;
-      process.noShell = true;
-      const params = ["start-server"];
-      let isStarted = false;
-      try {
-        await this._runProcess(process, params);
-        isStarted = await this._waitUntil(check);
-      } catch (e) {
-      }
-
-      if (isStarted) {
-        onSuccessfulStart();
-      } else {
-        this.ready = false;
-        reject();
-      }
-    });
-  },
-
-  /**
-   * Stop the ADB server, but only if we started it.  If it was started before
-   * us, we return immediately.
-   */
-  async stop() {
-    if (!this.didRunInitially) {
-      return; // We didn't start the server, nothing to do
-    }
-    await this.kill();
-  },
-
-  /**
-   * Kill the ADB server.
-   */
-  async kill() {
-    try {
-      await this.runCommand("host:kill");
-    } catch (e) {
-      dumpn("Failed to send host:kill command");
-    }
-    dumpn("adb server was terminated by host:kill");
-    this.ready = false;
-    this.didRunInitially = false;
-  },
-
-  // Start tracking devices connecting and disconnecting from the host.
-  // We can't reuse runCommand here because we keep the socket alive.
-  // @return The socket used.
-  trackDevices() {
-    dumpn("trackDevices");
-    const socket = client.connect();
-    let waitForFirst = true;
-    const devices = {};
-
-    socket.s.onopen = function() {
-      dumpn("trackDevices onopen");
-      Services.obs.notifyObservers(null, "adb-track-devices-start");
-      const req = client.createRequest("host:track-devices");
-      socket.send(req);
-    };
-
-    socket.s.onerror = function(event) {
-      dumpn("trackDevices onerror: " + event);
-      Services.obs.notifyObservers(null, "adb-track-devices-stop");
-    };
-
-    socket.s.onclose = function() {
-      dumpn("trackDevices onclose");
-
-      // Report all devices as disconnected
-      for (const dev in devices) {
-        devices[dev] = false;
-        EventEmitter.emit(ADB, "device-disconnected", dev);
-      }
-
-      Services.obs.notifyObservers(null, "adb-track-devices-stop");
-
-      // When we lose connection to the server,
-      // and the adb is still on, we most likely got our server killed
-      // by local adb. So we do try to reconnect to it.
-      setTimeout(function() { // Give some time to the new adb to start
-        if (ADB.ready) { // Only try to reconnect/restart if the add-on is still enabled
-          ADB.start().then(function() { // try to connect to the new local adb server
-                                         // or, spawn a new one
-            ADB.trackDevices(); // Re-track devices
-          });
-        }
-      }, 2000);
-    };
-
-    socket.s.ondata = function(event) {
-      dumpn("trackDevices ondata");
-      const data = event.data;
-      dumpn("length=" + data.byteLength);
-      const dec = new TextDecoder();
-      dumpn(dec.decode(new Uint8Array(data)).trim());
-
-      // check the OKAY or FAIL on first packet.
-      if (waitForFirst) {
-        if (!client.checkResponse(data, OKAY)) {
-          socket.close();
+  return this.runCommand("host:devices").then(
+    function onSuccess(data) {
+      const lines = data.split("\n");
+      const res = [];
+      lines.forEach(function(line) {
+        if (line.length == 0) {
           return;
         }
-      }
-
-      const packet = client.unpackPacket(data, !waitForFirst);
-      waitForFirst = false;
-
-      if (packet.data == "") {
-        // All devices got disconnected.
-        for (const dev in devices) {
-          devices[dev] = false;
-          EventEmitter.emit(ADB, "device-disconnected", dev);
-        }
-      } else {
-        // One line per device, each line being $DEVICE\t(offline|device)
-        const lines = packet.data.split("\n");
-        const newDev = {};
-        lines.forEach(function(line) {
-          if (line.length == 0) {
-            return;
-          }
-
-          const [dev, status] = line.split("\t");
-          newDev[dev] = status !== "offline";
-        });
-        // Check which device changed state.
-        for (const dev in newDev) {
-          if (devices[dev] != newDev[dev]) {
-            if (dev in devices || newDev[dev]) {
-              const topic = newDev[dev] ? "device-connected"
-                                        : "device-disconnected";
-              EventEmitter.emit(ADB, topic, dev);
-            }
-            devices[dev] = newDev[dev];
-          }
-        }
-      }
-    };
-  },
-
-  // Sends back an array of device names.
-  listDevices() {
-    dumpn("listDevices");
-
-    return this.runCommand("host:devices").then(
-      function onSuccess(data) {
-        const lines = data.split("\n");
-        const res = [];
-        lines.forEach(function(line) {
-          if (line.length == 0) {
-            return;
-          }
-          const [ device ] = line.split("\t");
-          res.push(device);
-        });
-        return res;
-      }
-    );
-  },
-
-  // sends adb forward localPort devicePort
-  forwardPort(localPort, devicePort) {
-    dumpn("forwardPort " + localPort + " -- " + devicePort);
-    // <host-prefix>:forward:<local>;<remote>
-
-    return this.runCommand("host:forward:" + localPort + ";" + devicePort)
-               .then(function onSuccess(data) {
-                 return data;
-               });
-  },
-
-  // Prepare TCP connection for provided socket path.
-  // The returned value is a port number of localhost for the connection.
-  async prepareTCPConnection(socketPath) {
-    const port = ConnectionManager.getFreeTCPPort();
-    const local = `tcp:${ port }`;
-    const remote = socketPath.startsWith("@")
-                     ? `localabstract:${ socketPath.substring(1) }`
-                     : `localfilesystem:${ socketPath }`;
-    await this.forwardPort(local, remote);
-    return port;
-  },
-
-  // Run a shell command
-  async shell(command) {
-    let state;
-    let stdout = "";
-
-    dumpn("shell " + command);
-
-    return new Promise((resolve, reject) => {
-      const shutdown = function() {
-        dumpn("shell shutdown");
-        socket.close();
-        reject("BAD_RESPONSE");
-      };
-
-      const runFSM = function runFSM(data) {
-        dumpn("runFSM " + state);
-        let req;
-        let ignoreResponseCode = false;
-        switch (state) {
-          case "start":
-            state = "send-transport";
-            runFSM();
-            break;
-          case "send-transport":
-            req = client.createRequest("host:transport-any");
-            socket.send(req);
-            state = "wait-transport";
-            break;
-          case "wait-transport":
-            if (!client.checkResponse(data, OKAY)) {
-              shutdown();
-              return;
-            }
-            state = "send-shell";
-            runFSM();
-            break;
-          case "send-shell":
-            req = client.createRequest("shell:" + command);
-            socket.send(req);
-            state = "rec-shell";
-            break;
-          case "rec-shell":
-            if (!client.checkResponse(data, OKAY)) {
-              shutdown();
-              return;
-            }
-            state = "decode-shell";
-            if (client.getBuffer(data).byteLength == 4) {
-              break;
-            }
-            ignoreResponseCode = true;
-            // eslint-disable-next-lined no-fallthrough
-          case "decode-shell":
-            const decoder = new TextDecoder();
-            const text = new Uint8Array(client.getBuffer(data),
-                                        ignoreResponseCode ? 4 : 0);
-            stdout += decoder.decode(text);
-            break;
-          default:
-            dumpn("shell Unexpected State: " + state);
-            reject("UNEXPECTED_STATE");
-        }
-      };
-
-      const socket = client.connect();
-      socket.s.onerror = function(event) {
-        dumpn("shell onerror");
-        reject("SOCKET_ERROR");
-      };
-
-      socket.s.onopen = function(event) {
-        dumpn("shell onopen");
-        state = "start";
-        runFSM();
-      };
-
-      socket.s.onclose = function(event) {
-        resolve(stdout);
-        dumpn("shell onclose");
-      };
-
-      socket.s.ondata = function(event) {
-        dumpn("shell ondata");
-        runFSM(event.data);
-      };
-    });
-  },
-
-  // Asynchronously runs an adb command.
-  // @param command The command as documented in
-  // http://androidxref.com/4.0.4/xref/system/core/adb/SERVICES.TXT
-  runCommand(command) {
-    dumpn("runCommand " + command);
-    return new Promise((resolve, reject) => {
-      if (!this.ready) {
-        setTimeout(function() {
-          reject("ADB_NOT_READY");
-        });
-        return;
-      }
-
-      const socket = client.connect();
-
-      socket.s.onopen = function() {
-        dumpn("runCommand onopen");
-        const req = client.createRequest(command);
-        socket.send(req);
-      };
-
-      socket.s.onerror = function() {
-        dumpn("runCommand onerror");
-        reject("NETWORK_ERROR");
-      };
-
-      socket.s.onclose = function() {
-        dumpn("runCommand onclose");
-      };
-
-      socket.s.ondata = function(event) {
-        dumpn("runCommand ondata");
-        const data = event.data;
-
-        const packet = client.unpackPacket(data, false);
-        if (!client.checkResponse(data, OKAY)) {
-          socket.close();
-          dumpn("Error: " + packet.data);
-          reject("PROTOCOL_ERROR");
-          return;
-        }
-
-        resolve(packet.data);
-      };
-    });
-  },
+        const [ device ] = line.split("\t");
+        res.push(device);
+      });
+      return res;
+    }
+  );
 };
-
-exports.ADB = ADB;
+exports.listDevices = listDevices;
new file mode 100644
--- /dev/null
+++ b/devtools/shared/adb/commands/moz.build
@@ -0,0 +1,12 @@
+# 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/.
+
+DevToolsModules(
+    'index.js',
+    'list-devices.js',
+    'prepare-tcp-connection.js',
+    'run-command.js',
+    'shell.js',
+    'track-devices.js',
+)
copy from devtools/shared/adb/adb.js
copy to devtools/shared/adb/commands/prepare-tcp-connection.js
--- a/devtools/shared/adb/adb.js
+++ b/devtools/shared/adb/commands/prepare-tcp-connection.js
@@ -1,424 +1,33 @@
 /* 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/. */
 
-// Wrapper around the ADB utility.
-
 "use strict";
 
-const { Cc, Ci } = require("chrome");
-const EventEmitter = require("devtools/shared/event-emitter");
-const client = require("./adb-client");
 const { dumpn } = require("devtools/shared/DevToolsUtils");
-const { getFileForBinary } = require("./adb-binary");
-const { setTimeout } = require("resource://gre/modules/Timer.jsm");
-const { Services } = require("resource://gre/modules/Services.jsm");
 const { ConnectionManager } = require("devtools/shared/client/connection-manager");
-loader.lazyRequireGetter(this, "check",
-                         "devtools/shared/adb/adb-running-checker", true);
-
-let ready = false;
-let didRunInitially = false;
-
-const OKAY = 0x59414b4f;
-
-const ADB = {
-  get didRunInitially() {
-    return didRunInitially;
-  },
-  set didRunInitially(newVal) {
-    didRunInitially = newVal;
-  },
-
-  get ready() {
-    return ready;
-  },
-  set ready(newVal) {
-    ready = newVal;
-  },
-
-  get adbFilePromise() {
-    if (this._adbFilePromise) {
-      return this._adbFilePromise;
-    }
-    this._adbFilePromise = getFileForBinary();
-    return this._adbFilePromise;
-  },
-
-  async _runProcess(process, params) {
-    return new Promise((resolve, reject) => {
-      process.runAsync(params, params.length, {
-        observe(subject, topic, data) {
-          switch (topic) {
-            case "process-finished":
-              resolve();
-              break;
-            case "process-failed":
-              reject();
-              break;
-          }
-        },
-      }, false);
-    });
-  },
-
-  // Waits until a predicate returns true or re-tries the predicate calls
-  // |retry| times, we wait for 100ms between each calls.
-  async _waitUntil(predicate, retry = 20) {
-    let count = 0;
-    while (count++ < retry) {
-      if (await predicate()) {
-        return true;
-      }
-      // Wait for 100 milliseconds.
-      await new Promise(resolve => setTimeout(resolve, 100));
-    }
-    // Timed out after trying too many times.
-    return false;
-  },
-
-  // We startup by launching adb in server mode, and setting
-  // the tcp socket preference to |true|
-  async start() {
-    return new Promise(async (resolve, reject) => {
-      const onSuccessfulStart = () => {
-        Services.obs.notifyObservers(null, "adb-ready");
-        this.ready = true;
-        resolve();
-      };
-
-      const isAdbRunning = await check();
-      if (isAdbRunning) {
-        dumpn("Found ADB process running, not restarting");
-        onSuccessfulStart();
-        return;
-      }
-      dumpn("Didn't find ADB process running, restarting");
-
-      this.didRunInitially = true;
-      const process = Cc["@mozilla.org/process/util;1"]
-                      .createInstance(Ci.nsIProcess);
-      // FIXME: Bug 1481691 - We should avoid extracting files every time.
-      const adbFile = await this.adbFilePromise;
-      process.init(adbFile);
-      // Hide command prompt window on Windows
-      process.startHidden = true;
-      process.noShell = true;
-      const params = ["start-server"];
-      let isStarted = false;
-      try {
-        await this._runProcess(process, params);
-        isStarted = await this._waitUntil(check);
-      } catch (e) {
-      }
-
-      if (isStarted) {
-        onSuccessfulStart();
-      } else {
-        this.ready = false;
-        reject();
-      }
-    });
-  },
-
-  /**
-   * Stop the ADB server, but only if we started it.  If it was started before
-   * us, we return immediately.
-   */
-  async stop() {
-    if (!this.didRunInitially) {
-      return; // We didn't start the server, nothing to do
-    }
-    await this.kill();
-  },
-
-  /**
-   * Kill the ADB server.
-   */
-  async kill() {
-    try {
-      await this.runCommand("host:kill");
-    } catch (e) {
-      dumpn("Failed to send host:kill command");
-    }
-    dumpn("adb server was terminated by host:kill");
-    this.ready = false;
-    this.didRunInitially = false;
-  },
-
-  // Start tracking devices connecting and disconnecting from the host.
-  // We can't reuse runCommand here because we keep the socket alive.
-  // @return The socket used.
-  trackDevices() {
-    dumpn("trackDevices");
-    const socket = client.connect();
-    let waitForFirst = true;
-    const devices = {};
-
-    socket.s.onopen = function() {
-      dumpn("trackDevices onopen");
-      Services.obs.notifyObservers(null, "adb-track-devices-start");
-      const req = client.createRequest("host:track-devices");
-      socket.send(req);
-    };
-
-    socket.s.onerror = function(event) {
-      dumpn("trackDevices onerror: " + event);
-      Services.obs.notifyObservers(null, "adb-track-devices-stop");
-    };
-
-    socket.s.onclose = function() {
-      dumpn("trackDevices onclose");
-
-      // Report all devices as disconnected
-      for (const dev in devices) {
-        devices[dev] = false;
-        EventEmitter.emit(ADB, "device-disconnected", dev);
-      }
-
-      Services.obs.notifyObservers(null, "adb-track-devices-stop");
-
-      // When we lose connection to the server,
-      // and the adb is still on, we most likely got our server killed
-      // by local adb. So we do try to reconnect to it.
-      setTimeout(function() { // Give some time to the new adb to start
-        if (ADB.ready) { // Only try to reconnect/restart if the add-on is still enabled
-          ADB.start().then(function() { // try to connect to the new local adb server
-                                         // or, spawn a new one
-            ADB.trackDevices(); // Re-track devices
-          });
-        }
-      }, 2000);
-    };
-
-    socket.s.ondata = function(event) {
-      dumpn("trackDevices ondata");
-      const data = event.data;
-      dumpn("length=" + data.byteLength);
-      const dec = new TextDecoder();
-      dumpn(dec.decode(new Uint8Array(data)).trim());
-
-      // check the OKAY or FAIL on first packet.
-      if (waitForFirst) {
-        if (!client.checkResponse(data, OKAY)) {
-          socket.close();
-          return;
-        }
-      }
-
-      const packet = client.unpackPacket(data, !waitForFirst);
-      waitForFirst = false;
+const { runCommand } = require("./run-command");
 
-      if (packet.data == "") {
-        // All devices got disconnected.
-        for (const dev in devices) {
-          devices[dev] = false;
-          EventEmitter.emit(ADB, "device-disconnected", dev);
-        }
-      } else {
-        // One line per device, each line being $DEVICE\t(offline|device)
-        const lines = packet.data.split("\n");
-        const newDev = {};
-        lines.forEach(function(line) {
-          if (line.length == 0) {
-            return;
-          }
-
-          const [dev, status] = line.split("\t");
-          newDev[dev] = status !== "offline";
-        });
-        // Check which device changed state.
-        for (const dev in newDev) {
-          if (devices[dev] != newDev[dev]) {
-            if (dev in devices || newDev[dev]) {
-              const topic = newDev[dev] ? "device-connected"
-                                        : "device-disconnected";
-              EventEmitter.emit(ADB, topic, dev);
-            }
-            devices[dev] = newDev[dev];
-          }
-        }
-      }
-    };
-  },
-
-  // Sends back an array of device names.
-  listDevices() {
-    dumpn("listDevices");
-
-    return this.runCommand("host:devices").then(
-      function onSuccess(data) {
-        const lines = data.split("\n");
-        const res = [];
-        lines.forEach(function(line) {
-          if (line.length == 0) {
-            return;
-          }
-          const [ device ] = line.split("\t");
-          res.push(device);
-        });
-        return res;
-      }
-    );
-  },
-
-  // sends adb forward localPort devicePort
-  forwardPort(localPort, devicePort) {
-    dumpn("forwardPort " + localPort + " -- " + devicePort);
-    // <host-prefix>:forward:<local>;<remote>
-
-    return this.runCommand("host:forward:" + localPort + ";" + devicePort)
-               .then(function onSuccess(data) {
-                 return data;
-               });
-  },
-
-  // Prepare TCP connection for provided socket path.
-  // The returned value is a port number of localhost for the connection.
-  async prepareTCPConnection(socketPath) {
-    const port = ConnectionManager.getFreeTCPPort();
-    const local = `tcp:${ port }`;
-    const remote = socketPath.startsWith("@")
-                     ? `localabstract:${ socketPath.substring(1) }`
-                     : `localfilesystem:${ socketPath }`;
-    await this.forwardPort(local, remote);
-    return port;
-  },
-
-  // Run a shell command
-  async shell(command) {
-    let state;
-    let stdout = "";
-
-    dumpn("shell " + command);
-
-    return new Promise((resolve, reject) => {
-      const shutdown = function() {
-        dumpn("shell shutdown");
-        socket.close();
-        reject("BAD_RESPONSE");
-      };
+// sends adb forward localPort devicePort
+const forwardPort = function(localPort, devicePort) {
+  dumpn("forwardPort " + localPort + " -- " + devicePort);
+  // <host-prefix>:forward:<local>;<remote>
 
-      const runFSM = function runFSM(data) {
-        dumpn("runFSM " + state);
-        let req;
-        let ignoreResponseCode = false;
-        switch (state) {
-          case "start":
-            state = "send-transport";
-            runFSM();
-            break;
-          case "send-transport":
-            req = client.createRequest("host:transport-any");
-            socket.send(req);
-            state = "wait-transport";
-            break;
-          case "wait-transport":
-            if (!client.checkResponse(data, OKAY)) {
-              shutdown();
-              return;
-            }
-            state = "send-shell";
-            runFSM();
-            break;
-          case "send-shell":
-            req = client.createRequest("shell:" + command);
-            socket.send(req);
-            state = "rec-shell";
-            break;
-          case "rec-shell":
-            if (!client.checkResponse(data, OKAY)) {
-              shutdown();
-              return;
-            }
-            state = "decode-shell";
-            if (client.getBuffer(data).byteLength == 4) {
-              break;
-            }
-            ignoreResponseCode = true;
-            // eslint-disable-next-lined no-fallthrough
-          case "decode-shell":
-            const decoder = new TextDecoder();
-            const text = new Uint8Array(client.getBuffer(data),
-                                        ignoreResponseCode ? 4 : 0);
-            stdout += decoder.decode(text);
-            break;
-          default:
-            dumpn("shell Unexpected State: " + state);
-            reject("UNEXPECTED_STATE");
-        }
-      };
-
-      const socket = client.connect();
-      socket.s.onerror = function(event) {
-        dumpn("shell onerror");
-        reject("SOCKET_ERROR");
-      };
-
-      socket.s.onopen = function(event) {
-        dumpn("shell onopen");
-        state = "start";
-        runFSM();
-      };
-
-      socket.s.onclose = function(event) {
-        resolve(stdout);
-        dumpn("shell onclose");
-      };
-
-      socket.s.ondata = function(event) {
-        dumpn("shell ondata");
-        runFSM(event.data);
-      };
-    });
-  },
-
-  // Asynchronously runs an adb command.
-  // @param command The command as documented in
-  // http://androidxref.com/4.0.4/xref/system/core/adb/SERVICES.TXT
-  runCommand(command) {
-    dumpn("runCommand " + command);
-    return new Promise((resolve, reject) => {
-      if (!this.ready) {
-        setTimeout(function() {
-          reject("ADB_NOT_READY");
-        });
-        return;
-      }
-
-      const socket = client.connect();
-
-      socket.s.onopen = function() {
-        dumpn("runCommand onopen");
-        const req = client.createRequest(command);
-        socket.send(req);
-      };
-
-      socket.s.onerror = function() {
-        dumpn("runCommand onerror");
-        reject("NETWORK_ERROR");
-      };
-
-      socket.s.onclose = function() {
-        dumpn("runCommand onclose");
-      };
-
-      socket.s.ondata = function(event) {
-        dumpn("runCommand ondata");
-        const data = event.data;
-
-        const packet = client.unpackPacket(data, false);
-        if (!client.checkResponse(data, OKAY)) {
-          socket.close();
-          dumpn("Error: " + packet.data);
-          reject("PROTOCOL_ERROR");
-          return;
-        }
-
-        resolve(packet.data);
-      };
-    });
-  },
+  return runCommand("host:forward:" + localPort + ";" + devicePort)
+             .then(function onSuccess(data) {
+               return data;
+             });
 };
 
-exports.ADB = ADB;
+// Prepare TCP connection for provided socket path.
+// The returned value is a port number of localhost for the connection.
+const prepareTCPConnection = async function(socketPath) {
+  const port = ConnectionManager.getFreeTCPPort();
+  const local = `tcp:${ port }`;
+  const remote = socketPath.startsWith("@")
+                   ? `localabstract:${ socketPath.substring(1) }`
+                   : `localfilesystem:${ socketPath }`;
+  await forwardPort(local, remote);
+  return port;
+};
+exports.prepareTCPConnection = prepareTCPConnection;
copy from devtools/shared/adb/adb.js
copy to devtools/shared/adb/commands/run-command.js
--- a/devtools/shared/adb/adb.js
+++ b/devtools/shared/adb/commands/run-command.js
@@ -1,424 +1,62 @@
 /* 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/. */
 
 // Wrapper around the ADB utility.
 
 "use strict";
 
-const { Cc, Ci } = require("chrome");
-const EventEmitter = require("devtools/shared/event-emitter");
-const client = require("./adb-client");
 const { dumpn } = require("devtools/shared/DevToolsUtils");
-const { getFileForBinary } = require("./adb-binary");
 const { setTimeout } = require("resource://gre/modules/Timer.jsm");
-const { Services } = require("resource://gre/modules/Services.jsm");
-const { ConnectionManager } = require("devtools/shared/client/connection-manager");
-loader.lazyRequireGetter(this, "check",
-                         "devtools/shared/adb/adb-running-checker", true);
-
-let ready = false;
-let didRunInitially = false;
+const { adbProcess } = require("../adb-process");
+const client = require("../adb-client");
 
 const OKAY = 0x59414b4f;
 
-const ADB = {
-  get didRunInitially() {
-    return didRunInitially;
-  },
-  set didRunInitially(newVal) {
-    didRunInitially = newVal;
-  },
-
-  get ready() {
-    return ready;
-  },
-  set ready(newVal) {
-    ready = newVal;
-  },
-
-  get adbFilePromise() {
-    if (this._adbFilePromise) {
-      return this._adbFilePromise;
+// Asynchronously runs an adb command.
+// @param command The command as documented in
+// http://androidxref.com/4.0.4/xref/system/core/adb/SERVICES.TXT
+const runCommand = function(command) {
+  dumpn("runCommand " + command);
+  return new Promise((resolve, reject) => {
+    if (!adbProcess.ready) {
+      setTimeout(function() {
+        reject("ADB_NOT_READY");
+      });
+      return;
     }
-    this._adbFilePromise = getFileForBinary();
-    return this._adbFilePromise;
-  },
-
-  async _runProcess(process, params) {
-    return new Promise((resolve, reject) => {
-      process.runAsync(params, params.length, {
-        observe(subject, topic, data) {
-          switch (topic) {
-            case "process-finished":
-              resolve();
-              break;
-            case "process-failed":
-              reject();
-              break;
-          }
-        },
-      }, false);
-    });
-  },
-
-  // Waits until a predicate returns true or re-tries the predicate calls
-  // |retry| times, we wait for 100ms between each calls.
-  async _waitUntil(predicate, retry = 20) {
-    let count = 0;
-    while (count++ < retry) {
-      if (await predicate()) {
-        return true;
-      }
-      // Wait for 100 milliseconds.
-      await new Promise(resolve => setTimeout(resolve, 100));
-    }
-    // Timed out after trying too many times.
-    return false;
-  },
-
-  // We startup by launching adb in server mode, and setting
-  // the tcp socket preference to |true|
-  async start() {
-    return new Promise(async (resolve, reject) => {
-      const onSuccessfulStart = () => {
-        Services.obs.notifyObservers(null, "adb-ready");
-        this.ready = true;
-        resolve();
-      };
 
-      const isAdbRunning = await check();
-      if (isAdbRunning) {
-        dumpn("Found ADB process running, not restarting");
-        onSuccessfulStart();
-        return;
-      }
-      dumpn("Didn't find ADB process running, restarting");
-
-      this.didRunInitially = true;
-      const process = Cc["@mozilla.org/process/util;1"]
-                      .createInstance(Ci.nsIProcess);
-      // FIXME: Bug 1481691 - We should avoid extracting files every time.
-      const adbFile = await this.adbFilePromise;
-      process.init(adbFile);
-      // Hide command prompt window on Windows
-      process.startHidden = true;
-      process.noShell = true;
-      const params = ["start-server"];
-      let isStarted = false;
-      try {
-        await this._runProcess(process, params);
-        isStarted = await this._waitUntil(check);
-      } catch (e) {
-      }
-
-      if (isStarted) {
-        onSuccessfulStart();
-      } else {
-        this.ready = false;
-        reject();
-      }
-    });
-  },
-
-  /**
-   * Stop the ADB server, but only if we started it.  If it was started before
-   * us, we return immediately.
-   */
-  async stop() {
-    if (!this.didRunInitially) {
-      return; // We didn't start the server, nothing to do
-    }
-    await this.kill();
-  },
-
-  /**
-   * Kill the ADB server.
-   */
-  async kill() {
-    try {
-      await this.runCommand("host:kill");
-    } catch (e) {
-      dumpn("Failed to send host:kill command");
-    }
-    dumpn("adb server was terminated by host:kill");
-    this.ready = false;
-    this.didRunInitially = false;
-  },
-
-  // Start tracking devices connecting and disconnecting from the host.
-  // We can't reuse runCommand here because we keep the socket alive.
-  // @return The socket used.
-  trackDevices() {
-    dumpn("trackDevices");
     const socket = client.connect();
-    let waitForFirst = true;
-    const devices = {};
 
     socket.s.onopen = function() {
-      dumpn("trackDevices onopen");
-      Services.obs.notifyObservers(null, "adb-track-devices-start");
-      const req = client.createRequest("host:track-devices");
+      dumpn("runCommand onopen");
+      const req = client.createRequest(command);
       socket.send(req);
     };
 
-    socket.s.onerror = function(event) {
-      dumpn("trackDevices onerror: " + event);
-      Services.obs.notifyObservers(null, "adb-track-devices-stop");
+    socket.s.onerror = function() {
+      dumpn("runCommand onerror");
+      reject("NETWORK_ERROR");
     };
 
     socket.s.onclose = function() {
-      dumpn("trackDevices onclose");
-
-      // Report all devices as disconnected
-      for (const dev in devices) {
-        devices[dev] = false;
-        EventEmitter.emit(ADB, "device-disconnected", dev);
-      }
-
-      Services.obs.notifyObservers(null, "adb-track-devices-stop");
-
-      // When we lose connection to the server,
-      // and the adb is still on, we most likely got our server killed
-      // by local adb. So we do try to reconnect to it.
-      setTimeout(function() { // Give some time to the new adb to start
-        if (ADB.ready) { // Only try to reconnect/restart if the add-on is still enabled
-          ADB.start().then(function() { // try to connect to the new local adb server
-                                         // or, spawn a new one
-            ADB.trackDevices(); // Re-track devices
-          });
-        }
-      }, 2000);
+      dumpn("runCommand onclose");
     };
 
     socket.s.ondata = function(event) {
-      dumpn("trackDevices ondata");
+      dumpn("runCommand ondata");
       const data = event.data;
-      dumpn("length=" + data.byteLength);
-      const dec = new TextDecoder();
-      dumpn(dec.decode(new Uint8Array(data)).trim());
-
-      // check the OKAY or FAIL on first packet.
-      if (waitForFirst) {
-        if (!client.checkResponse(data, OKAY)) {
-          socket.close();
-          return;
-        }
-      }
-
-      const packet = client.unpackPacket(data, !waitForFirst);
-      waitForFirst = false;
-
-      if (packet.data == "") {
-        // All devices got disconnected.
-        for (const dev in devices) {
-          devices[dev] = false;
-          EventEmitter.emit(ADB, "device-disconnected", dev);
-        }
-      } else {
-        // One line per device, each line being $DEVICE\t(offline|device)
-        const lines = packet.data.split("\n");
-        const newDev = {};
-        lines.forEach(function(line) {
-          if (line.length == 0) {
-            return;
-          }
-
-          const [dev, status] = line.split("\t");
-          newDev[dev] = status !== "offline";
-        });
-        // Check which device changed state.
-        for (const dev in newDev) {
-          if (devices[dev] != newDev[dev]) {
-            if (dev in devices || newDev[dev]) {
-              const topic = newDev[dev] ? "device-connected"
-                                        : "device-disconnected";
-              EventEmitter.emit(ADB, topic, dev);
-            }
-            devices[dev] = newDev[dev];
-          }
-        }
-      }
-    };
-  },
-
-  // Sends back an array of device names.
-  listDevices() {
-    dumpn("listDevices");
-
-    return this.runCommand("host:devices").then(
-      function onSuccess(data) {
-        const lines = data.split("\n");
-        const res = [];
-        lines.forEach(function(line) {
-          if (line.length == 0) {
-            return;
-          }
-          const [ device ] = line.split("\t");
-          res.push(device);
-        });
-        return res;
-      }
-    );
-  },
-
-  // sends adb forward localPort devicePort
-  forwardPort(localPort, devicePort) {
-    dumpn("forwardPort " + localPort + " -- " + devicePort);
-    // <host-prefix>:forward:<local>;<remote>
-
-    return this.runCommand("host:forward:" + localPort + ";" + devicePort)
-               .then(function onSuccess(data) {
-                 return data;
-               });
-  },
-
-  // Prepare TCP connection for provided socket path.
-  // The returned value is a port number of localhost for the connection.
-  async prepareTCPConnection(socketPath) {
-    const port = ConnectionManager.getFreeTCPPort();
-    const local = `tcp:${ port }`;
-    const remote = socketPath.startsWith("@")
-                     ? `localabstract:${ socketPath.substring(1) }`
-                     : `localfilesystem:${ socketPath }`;
-    await this.forwardPort(local, remote);
-    return port;
-  },
 
-  // Run a shell command
-  async shell(command) {
-    let state;
-    let stdout = "";
-
-    dumpn("shell " + command);
-
-    return new Promise((resolve, reject) => {
-      const shutdown = function() {
-        dumpn("shell shutdown");
+      const packet = client.unpackPacket(data, false);
+      if (!client.checkResponse(data, OKAY)) {
         socket.close();
-        reject("BAD_RESPONSE");
-      };
-
-      const runFSM = function runFSM(data) {
-        dumpn("runFSM " + state);
-        let req;
-        let ignoreResponseCode = false;
-        switch (state) {
-          case "start":
-            state = "send-transport";
-            runFSM();
-            break;
-          case "send-transport":
-            req = client.createRequest("host:transport-any");
-            socket.send(req);
-            state = "wait-transport";
-            break;
-          case "wait-transport":
-            if (!client.checkResponse(data, OKAY)) {
-              shutdown();
-              return;
-            }
-            state = "send-shell";
-            runFSM();
-            break;
-          case "send-shell":
-            req = client.createRequest("shell:" + command);
-            socket.send(req);
-            state = "rec-shell";
-            break;
-          case "rec-shell":
-            if (!client.checkResponse(data, OKAY)) {
-              shutdown();
-              return;
-            }
-            state = "decode-shell";
-            if (client.getBuffer(data).byteLength == 4) {
-              break;
-            }
-            ignoreResponseCode = true;
-            // eslint-disable-next-lined no-fallthrough
-          case "decode-shell":
-            const decoder = new TextDecoder();
-            const text = new Uint8Array(client.getBuffer(data),
-                                        ignoreResponseCode ? 4 : 0);
-            stdout += decoder.decode(text);
-            break;
-          default:
-            dumpn("shell Unexpected State: " + state);
-            reject("UNEXPECTED_STATE");
-        }
-      };
-
-      const socket = client.connect();
-      socket.s.onerror = function(event) {
-        dumpn("shell onerror");
-        reject("SOCKET_ERROR");
-      };
-
-      socket.s.onopen = function(event) {
-        dumpn("shell onopen");
-        state = "start";
-        runFSM();
-      };
-
-      socket.s.onclose = function(event) {
-        resolve(stdout);
-        dumpn("shell onclose");
-      };
-
-      socket.s.ondata = function(event) {
-        dumpn("shell ondata");
-        runFSM(event.data);
-      };
-    });
-  },
-
-  // Asynchronously runs an adb command.
-  // @param command The command as documented in
-  // http://androidxref.com/4.0.4/xref/system/core/adb/SERVICES.TXT
-  runCommand(command) {
-    dumpn("runCommand " + command);
-    return new Promise((resolve, reject) => {
-      if (!this.ready) {
-        setTimeout(function() {
-          reject("ADB_NOT_READY");
-        });
+        dumpn("Error: " + packet.data);
+        reject("PROTOCOL_ERROR");
         return;
       }
 
-      const socket = client.connect();
-
-      socket.s.onopen = function() {
-        dumpn("runCommand onopen");
-        const req = client.createRequest(command);
-        socket.send(req);
-      };
-
-      socket.s.onerror = function() {
-        dumpn("runCommand onerror");
-        reject("NETWORK_ERROR");
-      };
-
-      socket.s.onclose = function() {
-        dumpn("runCommand onclose");
-      };
-
-      socket.s.ondata = function(event) {
-        dumpn("runCommand ondata");
-        const data = event.data;
-
-        const packet = client.unpackPacket(data, false);
-        if (!client.checkResponse(data, OKAY)) {
-          socket.close();
-          dumpn("Error: " + packet.data);
-          reject("PROTOCOL_ERROR");
-          return;
-        }
-
-        resolve(packet.data);
-      };
-    });
-  },
+      resolve(packet.data);
+    };
+  });
 };
-
-exports.ADB = ADB;
+exports.runCommand = runCommand;
copy from devtools/shared/adb/adb.js
copy to devtools/shared/adb/commands/shell.js
--- a/devtools/shared/adb/adb.js
+++ b/devtools/shared/adb/commands/shell.js
@@ -1,424 +1,101 @@
 /* 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/. */
 
 // Wrapper around the ADB utility.
 
 "use strict";
 
-const { Cc, Ci } = require("chrome");
-const EventEmitter = require("devtools/shared/event-emitter");
-const client = require("./adb-client");
 const { dumpn } = require("devtools/shared/DevToolsUtils");
-const { getFileForBinary } = require("./adb-binary");
-const { setTimeout } = require("resource://gre/modules/Timer.jsm");
-const { Services } = require("resource://gre/modules/Services.jsm");
-const { ConnectionManager } = require("devtools/shared/client/connection-manager");
-loader.lazyRequireGetter(this, "check",
-                         "devtools/shared/adb/adb-running-checker", true);
-
-let ready = false;
-let didRunInitially = false;
+const client = require("../adb-client");
 
 const OKAY = 0x59414b4f;
 
-const ADB = {
-  get didRunInitially() {
-    return didRunInitially;
-  },
-  set didRunInitially(newVal) {
-    didRunInitially = newVal;
-  },
-
-  get ready() {
-    return ready;
-  },
-  set ready(newVal) {
-    ready = newVal;
-  },
-
-  get adbFilePromise() {
-    if (this._adbFilePromise) {
-      return this._adbFilePromise;
-    }
-    this._adbFilePromise = getFileForBinary();
-    return this._adbFilePromise;
-  },
+const shell = async function(command) {
+  let state;
+  let stdout = "";
 
-  async _runProcess(process, params) {
-    return new Promise((resolve, reject) => {
-      process.runAsync(params, params.length, {
-        observe(subject, topic, data) {
-          switch (topic) {
-            case "process-finished":
-              resolve();
-              break;
-            case "process-failed":
-              reject();
-              break;
-          }
-        },
-      }, false);
-    });
-  },
-
-  // Waits until a predicate returns true or re-tries the predicate calls
-  // |retry| times, we wait for 100ms between each calls.
-  async _waitUntil(predicate, retry = 20) {
-    let count = 0;
-    while (count++ < retry) {
-      if (await predicate()) {
-        return true;
-      }
-      // Wait for 100 milliseconds.
-      await new Promise(resolve => setTimeout(resolve, 100));
-    }
-    // Timed out after trying too many times.
-    return false;
-  },
-
-  // We startup by launching adb in server mode, and setting
-  // the tcp socket preference to |true|
-  async start() {
-    return new Promise(async (resolve, reject) => {
-      const onSuccessfulStart = () => {
-        Services.obs.notifyObservers(null, "adb-ready");
-        this.ready = true;
-        resolve();
-      };
+  dumpn("shell " + command);
 
-      const isAdbRunning = await check();
-      if (isAdbRunning) {
-        dumpn("Found ADB process running, not restarting");
-        onSuccessfulStart();
-        return;
-      }
-      dumpn("Didn't find ADB process running, restarting");
-
-      this.didRunInitially = true;
-      const process = Cc["@mozilla.org/process/util;1"]
-                      .createInstance(Ci.nsIProcess);
-      // FIXME: Bug 1481691 - We should avoid extracting files every time.
-      const adbFile = await this.adbFilePromise;
-      process.init(adbFile);
-      // Hide command prompt window on Windows
-      process.startHidden = true;
-      process.noShell = true;
-      const params = ["start-server"];
-      let isStarted = false;
-      try {
-        await this._runProcess(process, params);
-        isStarted = await this._waitUntil(check);
-      } catch (e) {
-      }
-
-      if (isStarted) {
-        onSuccessfulStart();
-      } else {
-        this.ready = false;
-        reject();
-      }
-    });
-  },
-
-  /**
-   * Stop the ADB server, but only if we started it.  If it was started before
-   * us, we return immediately.
-   */
-  async stop() {
-    if (!this.didRunInitially) {
-      return; // We didn't start the server, nothing to do
-    }
-    await this.kill();
-  },
-
-  /**
-   * Kill the ADB server.
-   */
-  async kill() {
-    try {
-      await this.runCommand("host:kill");
-    } catch (e) {
-      dumpn("Failed to send host:kill command");
-    }
-    dumpn("adb server was terminated by host:kill");
-    this.ready = false;
-    this.didRunInitially = false;
-  },
-
-  // Start tracking devices connecting and disconnecting from the host.
-  // We can't reuse runCommand here because we keep the socket alive.
-  // @return The socket used.
-  trackDevices() {
-    dumpn("trackDevices");
-    const socket = client.connect();
-    let waitForFirst = true;
-    const devices = {};
-
-    socket.s.onopen = function() {
-      dumpn("trackDevices onopen");
-      Services.obs.notifyObservers(null, "adb-track-devices-start");
-      const req = client.createRequest("host:track-devices");
-      socket.send(req);
+  return new Promise((resolve, reject) => {
+    const shutdown = function() {
+      dumpn("shell shutdown");
+      socket.close();
+      reject("BAD_RESPONSE");
     };
 
-    socket.s.onerror = function(event) {
-      dumpn("trackDevices onerror: " + event);
-      Services.obs.notifyObservers(null, "adb-track-devices-stop");
+    const runFSM = function runFSM(data) {
+      dumpn("runFSM " + state);
+      let req;
+      let ignoreResponseCode = false;
+      switch (state) {
+        case "start":
+          state = "send-transport";
+          runFSM();
+          break;
+        case "send-transport":
+          req = client.createRequest("host:transport-any");
+          socket.send(req);
+          state = "wait-transport";
+          break;
+        case "wait-transport":
+          if (!client.checkResponse(data, OKAY)) {
+            shutdown();
+            return;
+          }
+          state = "send-shell";
+          runFSM();
+          break;
+        case "send-shell":
+          req = client.createRequest("shell:" + command);
+          socket.send(req);
+          state = "rec-shell";
+          break;
+        case "rec-shell":
+          if (!client.checkResponse(data, OKAY)) {
+            shutdown();
+            return;
+          }
+          state = "decode-shell";
+          if (client.getBuffer(data).byteLength == 4) {
+            break;
+          }
+          ignoreResponseCode = true;
+          // eslint-disable-next-lined no-fallthrough
+        case "decode-shell":
+          const decoder = new TextDecoder();
+          const text = new Uint8Array(client.getBuffer(data),
+                                      ignoreResponseCode ? 4 : 0);
+          stdout += decoder.decode(text);
+          break;
+        default:
+          dumpn("shell Unexpected State: " + state);
+          reject("UNEXPECTED_STATE");
+      }
     };
 
-    socket.s.onclose = function() {
-      dumpn("trackDevices onclose");
-
-      // Report all devices as disconnected
-      for (const dev in devices) {
-        devices[dev] = false;
-        EventEmitter.emit(ADB, "device-disconnected", dev);
-      }
-
-      Services.obs.notifyObservers(null, "adb-track-devices-stop");
+    const socket = client.connect();
+    socket.s.onerror = function(event) {
+      dumpn("shell onerror");
+      reject("SOCKET_ERROR");
+    };
 
-      // When we lose connection to the server,
-      // and the adb is still on, we most likely got our server killed
-      // by local adb. So we do try to reconnect to it.
-      setTimeout(function() { // Give some time to the new adb to start
-        if (ADB.ready) { // Only try to reconnect/restart if the add-on is still enabled
-          ADB.start().then(function() { // try to connect to the new local adb server
-                                         // or, spawn a new one
-            ADB.trackDevices(); // Re-track devices
-          });
-        }
-      }, 2000);
+    socket.s.onopen = function(event) {
+      dumpn("shell onopen");
+      state = "start";
+      runFSM();
+    };
+
+    socket.s.onclose = function(event) {
+      resolve(stdout);
+      dumpn("shell onclose");
     };
 
     socket.s.ondata = function(event) {
-      dumpn("trackDevices ondata");
-      const data = event.data;
-      dumpn("length=" + data.byteLength);
-      const dec = new TextDecoder();
-      dumpn(dec.decode(new Uint8Array(data)).trim());
-
-      // check the OKAY or FAIL on first packet.
-      if (waitForFirst) {
-        if (!client.checkResponse(data, OKAY)) {
-          socket.close();
-          return;
-        }
-      }
-
-      const packet = client.unpackPacket(data, !waitForFirst);
-      waitForFirst = false;
-
-      if (packet.data == "") {
-        // All devices got disconnected.
-        for (const dev in devices) {
-          devices[dev] = false;
-          EventEmitter.emit(ADB, "device-disconnected", dev);
-        }
-      } else {
-        // One line per device, each line being $DEVICE\t(offline|device)
-        const lines = packet.data.split("\n");
-        const newDev = {};
-        lines.forEach(function(line) {
-          if (line.length == 0) {
-            return;
-          }
-
-          const [dev, status] = line.split("\t");
-          newDev[dev] = status !== "offline";
-        });
-        // Check which device changed state.
-        for (const dev in newDev) {
-          if (devices[dev] != newDev[dev]) {
-            if (dev in devices || newDev[dev]) {
-              const topic = newDev[dev] ? "device-connected"
-                                        : "device-disconnected";
-              EventEmitter.emit(ADB, topic, dev);
-            }
-            devices[dev] = newDev[dev];
-          }
-        }
-      }
+      dumpn("shell ondata");
+      runFSM(event.data);
     };
-  },
-
-  // Sends back an array of device names.
-  listDevices() {
-    dumpn("listDevices");
-
-    return this.runCommand("host:devices").then(
-      function onSuccess(data) {
-        const lines = data.split("\n");
-        const res = [];
-        lines.forEach(function(line) {
-          if (line.length == 0) {
-            return;
-          }
-          const [ device ] = line.split("\t");
-          res.push(device);
-        });
-        return res;
-      }
-    );
-  },
-
-  // sends adb forward localPort devicePort
-  forwardPort(localPort, devicePort) {
-    dumpn("forwardPort " + localPort + " -- " + devicePort);
-    // <host-prefix>:forward:<local>;<remote>
-
-    return this.runCommand("host:forward:" + localPort + ";" + devicePort)
-               .then(function onSuccess(data) {
-                 return data;
-               });
-  },
-
-  // Prepare TCP connection for provided socket path.
-  // The returned value is a port number of localhost for the connection.
-  async prepareTCPConnection(socketPath) {
-    const port = ConnectionManager.getFreeTCPPort();
-    const local = `tcp:${ port }`;
-    const remote = socketPath.startsWith("@")
-                     ? `localabstract:${ socketPath.substring(1) }`
-                     : `localfilesystem:${ socketPath }`;
-    await this.forwardPort(local, remote);
-    return port;
-  },
-
-  // Run a shell command
-  async shell(command) {
-    let state;
-    let stdout = "";
-
-    dumpn("shell " + command);
-
-    return new Promise((resolve, reject) => {
-      const shutdown = function() {
-        dumpn("shell shutdown");
-        socket.close();
-        reject("BAD_RESPONSE");
-      };
-
-      const runFSM = function runFSM(data) {
-        dumpn("runFSM " + state);
-        let req;
-        let ignoreResponseCode = false;
-        switch (state) {
-          case "start":
-            state = "send-transport";
-            runFSM();
-            break;
-          case "send-transport":
-            req = client.createRequest("host:transport-any");
-            socket.send(req);
-            state = "wait-transport";
-            break;
-          case "wait-transport":
-            if (!client.checkResponse(data, OKAY)) {
-              shutdown();
-              return;
-            }
-            state = "send-shell";
-            runFSM();
-            break;
-          case "send-shell":
-            req = client.createRequest("shell:" + command);
-            socket.send(req);
-            state = "rec-shell";
-            break;
-          case "rec-shell":
-            if (!client.checkResponse(data, OKAY)) {
-              shutdown();
-              return;
-            }
-            state = "decode-shell";
-            if (client.getBuffer(data).byteLength == 4) {
-              break;
-            }
-            ignoreResponseCode = true;
-            // eslint-disable-next-lined no-fallthrough
-          case "decode-shell":
-            const decoder = new TextDecoder();
-            const text = new Uint8Array(client.getBuffer(data),
-                                        ignoreResponseCode ? 4 : 0);
-            stdout += decoder.decode(text);
-            break;
-          default:
-            dumpn("shell Unexpected State: " + state);
-            reject("UNEXPECTED_STATE");
-        }
-      };
-
-      const socket = client.connect();
-      socket.s.onerror = function(event) {
-        dumpn("shell onerror");
-        reject("SOCKET_ERROR");
-      };
-
-      socket.s.onopen = function(event) {
-        dumpn("shell onopen");
-        state = "start";
-        runFSM();
-      };
-
-      socket.s.onclose = function(event) {
-        resolve(stdout);
-        dumpn("shell onclose");
-      };
-
-      socket.s.ondata = function(event) {
-        dumpn("shell ondata");
-        runFSM(event.data);
-      };
-    });
-  },
-
-  // Asynchronously runs an adb command.
-  // @param command The command as documented in
-  // http://androidxref.com/4.0.4/xref/system/core/adb/SERVICES.TXT
-  runCommand(command) {
-    dumpn("runCommand " + command);
-    return new Promise((resolve, reject) => {
-      if (!this.ready) {
-        setTimeout(function() {
-          reject("ADB_NOT_READY");
-        });
-        return;
-      }
-
-      const socket = client.connect();
-
-      socket.s.onopen = function() {
-        dumpn("runCommand onopen");
-        const req = client.createRequest(command);
-        socket.send(req);
-      };
-
-      socket.s.onerror = function() {
-        dumpn("runCommand onerror");
-        reject("NETWORK_ERROR");
-      };
-
-      socket.s.onclose = function() {
-        dumpn("runCommand onclose");
-      };
-
-      socket.s.ondata = function(event) {
-        dumpn("runCommand ondata");
-        const data = event.data;
-
-        const packet = client.unpackPacket(data, false);
-        if (!client.checkResponse(data, OKAY)) {
-          socket.close();
-          dumpn("Error: " + packet.data);
-          reject("PROTOCOL_ERROR");
-          return;
-        }
-
-        resolve(packet.data);
-      };
-    });
-  },
+  });
 };
 
-exports.ADB = ADB;
+exports.shell = shell;
copy from devtools/shared/adb/adb.js
copy to devtools/shared/adb/commands/track-devices.js
--- a/devtools/shared/adb/adb.js
+++ b/devtools/shared/adb/commands/track-devices.js
@@ -1,424 +1,129 @@
 /* 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/. */
 
 // Wrapper around the ADB utility.
 
 "use strict";
 
-const { Cc, Ci } = require("chrome");
 const EventEmitter = require("devtools/shared/event-emitter");
-const client = require("./adb-client");
 const { dumpn } = require("devtools/shared/DevToolsUtils");
-const { getFileForBinary } = require("./adb-binary");
 const { setTimeout } = require("resource://gre/modules/Timer.jsm");
-const { Services } = require("resource://gre/modules/Services.jsm");
-const { ConnectionManager } = require("devtools/shared/client/connection-manager");
-loader.lazyRequireGetter(this, "check",
-                         "devtools/shared/adb/adb-running-checker", true);
 
-let ready = false;
-let didRunInitially = false;
+const { adbProcess } = require("../adb-process");
+const client = require("../adb-client");
 
 const OKAY = 0x59414b4f;
 
-const ADB = {
-  get didRunInitially() {
-    return didRunInitially;
-  },
-  set didRunInitially(newVal) {
-    didRunInitially = newVal;
-  },
+// Start tracking devices connecting and disconnecting from the host.
+// We can't reuse runCommand here because we keep the socket alive.
+class TrackDevicesCommand extends EventEmitter {
+  run() {
+    this._waitForFirst = true;
+    this._devices = {};
+    this._socket = client.connect();
+
+    this._socket.s.onopen = this._onOpen.bind(this);
+    this._socket.s.onerror = this._onError.bind(this);
+    this._socket.s.onclose = this._onClose.bind(this);
+    this._socket.s.ondata = this._onData.bind(this);
+  }
 
-  get ready() {
-    return ready;
-  },
-  set ready(newVal) {
-    ready = newVal;
-  },
+  stop() {
+    if (this._socket) {
+      this._socket.close();
 
-  get adbFilePromise() {
-    if (this._adbFilePromise) {
-      return this._adbFilePromise;
+      this._socket.s.onopen = null;
+      this._socket.s.onerror = null;
+      this._socket.s.onclose = null;
+      this._socket.s.ondata = null;
     }
-    this._adbFilePromise = getFileForBinary();
-    return this._adbFilePromise;
-  },
+  }
+
+  _onOpen() {
+    dumpn("trackDevices onopen");
+    const req = client.createRequest("host:track-devices");
+    this._socket.send(req);
+  }
+
+  _onError(event) {
+    dumpn("trackDevices onerror: " + event);
+  }
 
-  async _runProcess(process, params) {
-    return new Promise((resolve, reject) => {
-      process.runAsync(params, params.length, {
-        observe(subject, topic, data) {
-          switch (topic) {
-            case "process-finished":
-              resolve();
-              break;
-            case "process-failed":
-              reject();
-              break;
-          }
-        },
-      }, false);
-    });
-  },
+  _onClose() {
+    dumpn("trackDevices onclose");
+
+    // Report all devices as disconnected
+    for (const dev in this._devices) {
+      this._devices[dev] = false;
+      this.emit("device-disconnected", dev);
+    }
+
+    // When we lose connection to the server,
+    // and the adb is still on, we most likely got our server killed
+    // by local adb. So we do try to reconnect to it.
 
-  // Waits until a predicate returns true or re-tries the predicate calls
-  // |retry| times, we wait for 100ms between each calls.
-  async _waitUntil(predicate, retry = 20) {
-    let count = 0;
-    while (count++ < retry) {
-      if (await predicate()) {
-        return true;
+    // Give some time to the new adb to start
+    setTimeout(() => {
+      // Only try to reconnect/restart if the add-on is still enabled
+      if (adbProcess.ready) {
+        // try to connect to the new local adb server or spawn a new one
+        adbProcess.start().then(() => {
+          // Re-track devices
+          this.run();
+        });
       }
-      // Wait for 100 milliseconds.
-      await new Promise(resolve => setTimeout(resolve, 100));
-    }
-    // Timed out after trying too many times.
-    return false;
-  },
+    }, 2000);
+  }
 
-  // We startup by launching adb in server mode, and setting
-  // the tcp socket preference to |true|
-  async start() {
-    return new Promise(async (resolve, reject) => {
-      const onSuccessfulStart = () => {
-        Services.obs.notifyObservers(null, "adb-ready");
-        this.ready = true;
-        resolve();
-      };
+  _onData(event) {
+    dumpn("trackDevices ondata");
+    const data = event.data;
+    dumpn("length=" + data.byteLength);
+    const dec = new TextDecoder();
+    dumpn(dec.decode(new Uint8Array(data)).trim());
 
-      const isAdbRunning = await check();
-      if (isAdbRunning) {
-        dumpn("Found ADB process running, not restarting");
-        onSuccessfulStart();
+    // check the OKAY or FAIL on first packet.
+    if (this._waitForFirst) {
+      if (!client.checkResponse(data, OKAY)) {
+        this._socket.close();
         return;
       }
-      dumpn("Didn't find ADB process running, restarting");
-
-      this.didRunInitially = true;
-      const process = Cc["@mozilla.org/process/util;1"]
-                      .createInstance(Ci.nsIProcess);
-      // FIXME: Bug 1481691 - We should avoid extracting files every time.
-      const adbFile = await this.adbFilePromise;
-      process.init(adbFile);
-      // Hide command prompt window on Windows
-      process.startHidden = true;
-      process.noShell = true;
-      const params = ["start-server"];
-      let isStarted = false;
-      try {
-        await this._runProcess(process, params);
-        isStarted = await this._waitUntil(check);
-      } catch (e) {
-      }
-
-      if (isStarted) {
-        onSuccessfulStart();
-      } else {
-        this.ready = false;
-        reject();
-      }
-    });
-  },
-
-  /**
-   * Stop the ADB server, but only if we started it.  If it was started before
-   * us, we return immediately.
-   */
-  async stop() {
-    if (!this.didRunInitially) {
-      return; // We didn't start the server, nothing to do
     }
-    await this.kill();
-  },
-
-  /**
-   * Kill the ADB server.
-   */
-  async kill() {
-    try {
-      await this.runCommand("host:kill");
-    } catch (e) {
-      dumpn("Failed to send host:kill command");
-    }
-    dumpn("adb server was terminated by host:kill");
-    this.ready = false;
-    this.didRunInitially = false;
-  },
-
-  // Start tracking devices connecting and disconnecting from the host.
-  // We can't reuse runCommand here because we keep the socket alive.
-  // @return The socket used.
-  trackDevices() {
-    dumpn("trackDevices");
-    const socket = client.connect();
-    let waitForFirst = true;
-    const devices = {};
-
-    socket.s.onopen = function() {
-      dumpn("trackDevices onopen");
-      Services.obs.notifyObservers(null, "adb-track-devices-start");
-      const req = client.createRequest("host:track-devices");
-      socket.send(req);
-    };
-
-    socket.s.onerror = function(event) {
-      dumpn("trackDevices onerror: " + event);
-      Services.obs.notifyObservers(null, "adb-track-devices-stop");
-    };
 
-    socket.s.onclose = function() {
-      dumpn("trackDevices onclose");
-
-      // Report all devices as disconnected
-      for (const dev in devices) {
-        devices[dev] = false;
-        EventEmitter.emit(ADB, "device-disconnected", dev);
-      }
-
-      Services.obs.notifyObservers(null, "adb-track-devices-stop");
-
-      // When we lose connection to the server,
-      // and the adb is still on, we most likely got our server killed
-      // by local adb. So we do try to reconnect to it.
-      setTimeout(function() { // Give some time to the new adb to start
-        if (ADB.ready) { // Only try to reconnect/restart if the add-on is still enabled
-          ADB.start().then(function() { // try to connect to the new local adb server
-                                         // or, spawn a new one
-            ADB.trackDevices(); // Re-track devices
-          });
-        }
-      }, 2000);
-    };
-
-    socket.s.ondata = function(event) {
-      dumpn("trackDevices ondata");
-      const data = event.data;
-      dumpn("length=" + data.byteLength);
-      const dec = new TextDecoder();
-      dumpn(dec.decode(new Uint8Array(data)).trim());
-
-      // check the OKAY or FAIL on first packet.
-      if (waitForFirst) {
-        if (!client.checkResponse(data, OKAY)) {
-          socket.close();
-          return;
-        }
-      }
-
-      const packet = client.unpackPacket(data, !waitForFirst);
-      waitForFirst = false;
-
-      if (packet.data == "") {
-        // All devices got disconnected.
-        for (const dev in devices) {
-          devices[dev] = false;
-          EventEmitter.emit(ADB, "device-disconnected", dev);
-        }
-      } else {
-        // One line per device, each line being $DEVICE\t(offline|device)
-        const lines = packet.data.split("\n");
-        const newDev = {};
-        lines.forEach(function(line) {
-          if (line.length == 0) {
-            return;
-          }
-
-          const [dev, status] = line.split("\t");
-          newDev[dev] = status !== "offline";
-        });
-        // Check which device changed state.
-        for (const dev in newDev) {
-          if (devices[dev] != newDev[dev]) {
-            if (dev in devices || newDev[dev]) {
-              const topic = newDev[dev] ? "device-connected"
-                                        : "device-disconnected";
-              EventEmitter.emit(ADB, topic, dev);
-            }
-            devices[dev] = newDev[dev];
-          }
-        }
-      }
-    };
-  },
-
-  // Sends back an array of device names.
-  listDevices() {
-    dumpn("listDevices");
+    const packet = client.unpackPacket(data, !this._waitForFirst);
+    this._waitForFirst = false;
 
-    return this.runCommand("host:devices").then(
-      function onSuccess(data) {
-        const lines = data.split("\n");
-        const res = [];
-        lines.forEach(function(line) {
-          if (line.length == 0) {
-            return;
-          }
-          const [ device ] = line.split("\t");
-          res.push(device);
-        });
-        return res;
+    if (packet.data == "") {
+      // All devices got disconnected.
+      for (const dev in this._devices) {
+        this._devices[dev] = false;
+        this.emit("device-disconnected", dev);
       }
-    );
-  },
-
-  // sends adb forward localPort devicePort
-  forwardPort(localPort, devicePort) {
-    dumpn("forwardPort " + localPort + " -- " + devicePort);
-    // <host-prefix>:forward:<local>;<remote>
-
-    return this.runCommand("host:forward:" + localPort + ";" + devicePort)
-               .then(function onSuccess(data) {
-                 return data;
-               });
-  },
-
-  // Prepare TCP connection for provided socket path.
-  // The returned value is a port number of localhost for the connection.
-  async prepareTCPConnection(socketPath) {
-    const port = ConnectionManager.getFreeTCPPort();
-    const local = `tcp:${ port }`;
-    const remote = socketPath.startsWith("@")
-                     ? `localabstract:${ socketPath.substring(1) }`
-                     : `localfilesystem:${ socketPath }`;
-    await this.forwardPort(local, remote);
-    return port;
-  },
-
-  // Run a shell command
-  async shell(command) {
-    let state;
-    let stdout = "";
-
-    dumpn("shell " + command);
-
-    return new Promise((resolve, reject) => {
-      const shutdown = function() {
-        dumpn("shell shutdown");
-        socket.close();
-        reject("BAD_RESPONSE");
-      };
-
-      const runFSM = function runFSM(data) {
-        dumpn("runFSM " + state);
-        let req;
-        let ignoreResponseCode = false;
-        switch (state) {
-          case "start":
-            state = "send-transport";
-            runFSM();
-            break;
-          case "send-transport":
-            req = client.createRequest("host:transport-any");
-            socket.send(req);
-            state = "wait-transport";
-            break;
-          case "wait-transport":
-            if (!client.checkResponse(data, OKAY)) {
-              shutdown();
-              return;
-            }
-            state = "send-shell";
-            runFSM();
-            break;
-          case "send-shell":
-            req = client.createRequest("shell:" + command);
-            socket.send(req);
-            state = "rec-shell";
-            break;
-          case "rec-shell":
-            if (!client.checkResponse(data, OKAY)) {
-              shutdown();
-              return;
-            }
-            state = "decode-shell";
-            if (client.getBuffer(data).byteLength == 4) {
-              break;
-            }
-            ignoreResponseCode = true;
-            // eslint-disable-next-lined no-fallthrough
-          case "decode-shell":
-            const decoder = new TextDecoder();
-            const text = new Uint8Array(client.getBuffer(data),
-                                        ignoreResponseCode ? 4 : 0);
-            stdout += decoder.decode(text);
-            break;
-          default:
-            dumpn("shell Unexpected State: " + state);
-            reject("UNEXPECTED_STATE");
-        }
-      };
-
-      const socket = client.connect();
-      socket.s.onerror = function(event) {
-        dumpn("shell onerror");
-        reject("SOCKET_ERROR");
-      };
-
-      socket.s.onopen = function(event) {
-        dumpn("shell onopen");
-        state = "start";
-        runFSM();
-      };
-
-      socket.s.onclose = function(event) {
-        resolve(stdout);
-        dumpn("shell onclose");
-      };
-
-      socket.s.ondata = function(event) {
-        dumpn("shell ondata");
-        runFSM(event.data);
-      };
-    });
-  },
-
-  // Asynchronously runs an adb command.
-  // @param command The command as documented in
-  // http://androidxref.com/4.0.4/xref/system/core/adb/SERVICES.TXT
-  runCommand(command) {
-    dumpn("runCommand " + command);
-    return new Promise((resolve, reject) => {
-      if (!this.ready) {
-        setTimeout(function() {
-          reject("ADB_NOT_READY");
-        });
-        return;
-      }
-
-      const socket = client.connect();
-
-      socket.s.onopen = function() {
-        dumpn("runCommand onopen");
-        const req = client.createRequest(command);
-        socket.send(req);
-      };
-
-      socket.s.onerror = function() {
-        dumpn("runCommand onerror");
-        reject("NETWORK_ERROR");
-      };
-
-      socket.s.onclose = function() {
-        dumpn("runCommand onclose");
-      };
-
-      socket.s.ondata = function(event) {
-        dumpn("runCommand ondata");
-        const data = event.data;
-
-        const packet = client.unpackPacket(data, false);
-        if (!client.checkResponse(data, OKAY)) {
-          socket.close();
-          dumpn("Error: " + packet.data);
-          reject("PROTOCOL_ERROR");
+    } else {
+      // One line per device, each line being $DEVICE\t(offline|device)
+      const lines = packet.data.split("\n");
+      const newDev = {};
+      lines.forEach(function(line) {
+        if (line.length == 0) {
           return;
         }
 
-        resolve(packet.data);
-      };
-    });
-  },
-};
-
-exports.ADB = ADB;
+        const [dev, status] = line.split("\t");
+        newDev[dev] = status !== "offline";
+      });
+      // Check which device changed state.
+      for (const dev in newDev) {
+        if (this._devices[dev] != newDev[dev]) {
+          if (dev in this._devices || newDev[dev]) {
+            const topic = newDev[dev] ? "device-connected"
+                                      : "device-disconnected";
+            this.emit(topic, dev);
+          }
+          this._devices[dev] = newDev[dev];
+        }
+      }
+    }
+  }
+}
+exports.TrackDevicesCommand = TrackDevicesCommand;
--- a/devtools/shared/adb/moz.build
+++ b/devtools/shared/adb/moz.build
@@ -1,22 +1,24 @@
 # 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/.
 
+DIRS += [
+    'commands',
+]
+
 DevToolsModules(
     'adb-addon.js',
     'adb-binary.js',
     'adb-client.js',
     'adb-device.js',
-    'adb-devices-registry.js',
+    'adb-process.js',
     'adb-running-checker.js',
     'adb-runtime.js',
-    'adb-scanner.js',
     'adb-socket.js',
     'adb.js',
-    'addon-aware-adb-scanner.js',
 )
 
 with Files('**'):
     BUG_COMPONENT = ('DevTools', 'about:debugging')
 
 XPCSHELL_TESTS_MANIFESTS += ['test/xpcshell.ini']
--- a/devtools/shared/adb/test/test_adb.js
+++ b/devtools/shared/adb/test/test_adb.js
@@ -1,19 +1,19 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
-const EventEmitter = require("devtools/shared/event-emitter");
 const { ExtensionTestUtils } = ChromeUtils.import("resource://testing-common/ExtensionXPCShellUtils.jsm", {});
 const { NetUtil } = require("resource://gre/modules/NetUtil.jsm");
 const { getFileForBinary } = require("devtools/shared/adb/adb-binary");
 const { check } = require("devtools/shared/adb/adb-running-checker");
-const { ADB } = require("devtools/shared/adb/adb");
+const { adbProcess } = require("devtools/shared/adb/adb-process");
+const { TrackDevicesCommand } = require("devtools/shared/adb/commands/index");
 
 const ADB_JSON = {
   "Linux": {
     "x86": [
       "linux/adb",
     ],
     "x86_64": [
       "linux64/adb",
@@ -171,32 +171,32 @@ add_task({
       "win32/AdbWinApi.dll": "dummy",
       "win32/AdbWinUsbApi.dll": "dummy",
     },
   });
 
   await extension.startup();
 
   // Call start() once and call stop() afterwards.
-  await ADB.start();
-  ok(ADB.ready);
+  await adbProcess.start();
+  ok(adbProcess.ready);
   ok(await check(), "adb is now running");
 
-  await ADB.stop();
-  ok(!ADB.ready);
+  await adbProcess.stop();
+  ok(!adbProcess.ready);
   ok(!(await check()), "adb is no longer running");
 
   // Call start() twice and call stop() afterwards.
-  await ADB.start();
-  await ADB.start();
-  ok(ADB.ready);
+  await adbProcess.start();
+  await adbProcess.start();
+  ok(adbProcess.ready);
   ok(await check(), "adb is now running");
 
-  await ADB.stop();
-  ok(!ADB.ready);
+  await adbProcess.stop();
+  ok(!adbProcess.ready);
   ok(!(await check()), "adb is no longer running");
 
   await extension.unload();
 });
 
 add_task({
   skip_if: () => mozinfo.os == "win", // bug 1482008
 }, async function testTrackDevices() {
@@ -215,28 +215,29 @@ add_task({
       "win32/adb.exe": adbMock,
       "win32/AdbWinApi.dll": "dummy",
       "win32/AdbWinUsbApi.dll": "dummy",
     },
   });
 
   await extension.startup();
 
-  await ADB.start();
-  ok(ADB.ready);
+  await adbProcess.start();
+  ok(adbProcess.ready);
 
   ok(await check(), "adb is now running");
 
   const receivedDeviceId = await new Promise(resolve => {
-    EventEmitter.on(ADB, "device-connected", deviceId => {
+    const trackDevicesCommand = new TrackDevicesCommand();
+    trackDevicesCommand.on("device-connected", deviceId => {
       resolve(deviceId);
     });
-    ADB.trackDevices();
+    trackDevicesCommand.run();
   });
 
   equal(receivedDeviceId, "1234567890");
 
-  await ADB.stop();
-  ok(!ADB.ready);
+  await adbProcess.stop();
+  ok(!adbProcess.ready);
 
   await extension.unload();
 });
 
deleted file mode 100644
--- a/devtools/shared/adb/test/test_addon-aware-adb-scanner.js
+++ /dev/null
@@ -1,221 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
-   http://creativecommons.org/publicdomain/zero/1.0/ */
-
-"use strict";
-
-const EventEmitter = require("devtools/shared/event-emitter");
-const { AddonAwareADBScanner } = require("devtools/shared/adb/addon-aware-adb-scanner");
-
-/**
- * For the scanner mock, we create an object with spies for each of the public methods
- * used by the AddonAwareADBScanner, and the ability to emit events.
- */
-function prepareMockScanner() {
-  const mockScanner = {
-    enable: sinon.spy(),
-    disable: sinon.spy(),
-    scan: sinon.spy(),
-    listRuntimes: sinon.spy(),
-  };
-  EventEmitter.decorate(mockScanner);
-  return mockScanner;
-}
-
-/**
- * For the addon mock, we simply need an object that is able to emit events and has a
- * status.
- */
-function prepareMockAddon() {
-  const mockAddon = {
-    status: "unknown",
-  };
-  EventEmitter.decorate(mockAddon);
-  return mockAddon;
-}
-
-/**
- * Prepare all mocks needed for the scanner tests.
- */
-function prepareMocks() {
-  const mockScanner = prepareMockScanner();
-  const mockAddon = prepareMockAddon();
-  const addonAwareAdbScanner = new AddonAwareADBScanner(mockScanner, mockAddon);
-  return { addonAwareAdbScanner, mockAddon, mockScanner };
-}
-
-/**
- * This test covers basic usage of enable() on the AddonAwareADBScanner, and checks the
- * different behaviors based on the addon status.
- */
-add_task(async function testCallingEnable() {
-  const { mockScanner, mockAddon, addonAwareAdbScanner } = prepareMocks();
-
-  // Check that enable() is not called if the addon is uninstalled
-  mockAddon.status = "uninstalled";
-  addonAwareAdbScanner.enable();
-  ok(mockScanner.enable.notCalled, "enable() was not called");
-  mockScanner.enable.reset();
-
-  // Check that enable() is called if the addon is installed
-  mockAddon.status = "installed";
-  addonAwareAdbScanner.enable();
-  ok(mockScanner.enable.called, "enable() was called");
-  mockScanner.enable.reset();
-});
-
-/**
- * This test checks that enable()/disable() methods from the internal ADBScanner are
- * called when the addon is installed or uninstalled.
- */
-add_task(async function testUpdatingAddonEnablesDisablesScanner() {
-  const { mockScanner, mockAddon, addonAwareAdbScanner } = prepareMocks();
-
-  // Enable the addon aware scanner
-  addonAwareAdbScanner.enable();
-  ok(mockScanner.enable.notCalled, "enable() was not called initially");
-
-  // Check that enable() is called automatically when the addon is installed
-  mockAddon.status = "installed";
-  mockAddon.emit("update");
-  ok(mockScanner.enable.called, "enable() was called when installing the addon");
-  ok(mockScanner.disable.notCalled, "disable() was not called when installing the addon");
-  mockScanner.enable.reset();
-  mockScanner.disable.reset();
-
-  // Check that disabled() is called automatically when the addon is uninstalled
-  mockAddon.status = "uninstalled";
-  mockAddon.emit("update");
-  ok(mockScanner.enable.notCalled, "enable() was not called when uninstalling the addon");
-  ok(mockScanner.disable.called, "disable() was called when uninstalling the addon");
-  mockScanner.enable.reset();
-  mockScanner.disable.reset();
-
-  // Check that enable() is called again when the addon is reinstalled
-  mockAddon.status = "installed";
-  mockAddon.emit("update");
-  ok(mockScanner.enable.called, "enable() was called when installing the addon");
-  ok(mockScanner.disable.notCalled, "disable() was not called when installing the addon");
-  mockScanner.enable.reset();
-  mockScanner.disable.reset();
-});
-
-/**
- * This test checks that disable() is forwarded from the AddonAwareADBScanner to the real
- * scanner even if the addon is uninstalled. We might miss the addon uninstall
- * notification, so it is safer to always proceed with disabling.
- */
-add_task(async function testScannerIsDisabledWhenMissingAddonUpdate() {
-  const { mockScanner, mockAddon, addonAwareAdbScanner } = prepareMocks();
-
-  // Enable the addon aware scanner
-  mockAddon.status = "installed";
-  addonAwareAdbScanner.enable();
-  ok(mockScanner.enable.called, "enable() was called initially");
-  mockScanner.enable.reset();
-
-  // Uninstall the addon without firing any event
-  mockAddon.status = "uninstalled";
-
-  // Programmatically call disable, check that the scanner's disable is called even though
-  // the addon was uninstalled.
-  addonAwareAdbScanner.disable();
-  ok(mockScanner.disable.called, "disable() was called when uninstalling the addon");
-  mockScanner.disable.reset();
-});
-
-/**
- * This test checks that when the AddonAwareADBScanner is disabled, then enable/disable
- * are not called on the inner scanner when the addon status changes.
- */
-add_task(async function testInnerEnableIsNotCalledIfNotStarted() {
-  const { mockScanner, mockAddon, addonAwareAdbScanner } = prepareMocks();
-
-  // Check that enable() is not called on the inner scanner when the addon is installed
-  // if the AddonAwareADBScanner was not enabled
-  mockAddon.status = "installed";
-  mockAddon.emit("update");
-  ok(mockScanner.enable.notCalled, "enable() was not called");
-
-  // Same for disable() and "uninstall"
-  mockAddon.status = "uninstalled";
-  mockAddon.emit("update");
-  ok(mockScanner.disable.notCalled, "disable() was not called");
-
-  // Enable the addon aware scanner
-  addonAwareAdbScanner.enable();
-  ok(mockScanner.enable.notCalled, "enable() was not called");
-  ok(mockScanner.disable.notCalled, "disable() was not called");
-});
-
-/**
- * This test checks that when the AddonAwareADBScanner is disabled, installing the addon
- * no longer enables the internal ADBScanner.
- */
-add_task(async function testEnableIsNoLongerCalledAfterDisabling() {
-  const { mockScanner, mockAddon, addonAwareAdbScanner } = prepareMocks();
-
-  // Start with the addon installed
-  mockAddon.status = "installed";
-  addonAwareAdbScanner.enable();
-  ok(mockScanner.enable.called, "enable() was called since addon was already installed");
-  mockScanner.enable.reset();
-
-  // Here we call enable again to check that we will not add too many events.
-  // A single call to disable() should stop all listeners, even if we called enable()
-  // several times.
-  addonAwareAdbScanner.enable();
-  ok(mockScanner.enable.called, "enable() was called again");
-  mockScanner.enable.reset();
-
-  // Disable the scanner
-  addonAwareAdbScanner.disable();
-  ok(mockScanner.disable.called, "disable() was called");
-  mockScanner.disable.reset();
-
-  // Emit an addon update event
-  mockAddon.emit("update");
-  ok(mockScanner.enable.notCalled,
-    "enable() is not called since the main scanner is disabled");
-});
-
-/**
- * Basic check that the "runtime-list-updated" event is forwarded.
- */
-add_task(async function testListUpdatedEventForwarding() {
-  const { mockScanner, addonAwareAdbScanner } = prepareMocks();
-
-  const spy = sinon.spy();
-  addonAwareAdbScanner.on("runtime-list-updated", spy);
-  mockScanner.emit("runtime-list-updated");
-  ok(spy.called, "The runtime-list-updated event was forwarded from ADBScanner");
-  addonAwareAdbScanner.off("runtime-list-updated", spy);
-});
-
-/**
- * Basic check that calls to scan() are forwarded.
- */
-add_task(async function testScanCallForwarding() {
-  const { mockScanner, addonAwareAdbScanner } = prepareMocks();
-
-  ok(mockScanner.scan.notCalled, "ADBScanner scan() is not called initially");
-
-  addonAwareAdbScanner.scan();
-  mockScanner.emit("runtime-list-updated");
-  ok(mockScanner.scan.called, "ADBScanner scan() was called");
-  mockScanner.scan.reset();
-});
-
-/**
- * Basic check that calls to scan() are forwarded.
- */
-add_task(async function testListRuntimesCallForwarding() {
-  const { mockScanner, addonAwareAdbScanner } = prepareMocks();
-
-  ok(mockScanner.listRuntimes.notCalled,
-    "ADBScanner listRuntimes() is not called initially");
-
-  addonAwareAdbScanner.listRuntimes();
-  mockScanner.emit("runtime-list-updated");
-  ok(mockScanner.listRuntimes.called, "ADBScanner listRuntimes() was called");
-  mockScanner.scan.reset();
-});
--- a/devtools/shared/adb/test/xpcshell.ini
+++ b/devtools/shared/adb/test/xpcshell.ini
@@ -3,9 +3,8 @@ tags = devtools
 head = xpcshell-head.js
 firefox-appdir = browser
 skip-if = toolkit == 'android'
 support-files =
   adb.py
 
 [test_adb.js]
 run-sequentially = An extension having the same id is installed/uninstalled in different tests
-[test_addon-aware-adb-scanner.js]
--- a/gfx/webrender_bindings/revision.txt
+++ b/gfx/webrender_bindings/revision.txt
@@ -1,1 +1,1 @@
-8a6a53883bc6fe9522730e09b916f4023ee10d51
+6db2d6c9005cf5888c6b35ca6908449ec591527a
--- a/gfx/wr/.taskcluster.yml
+++ b/gfx/wr/.taskcluster.yml
@@ -13,29 +13,18 @@ metadata:
   source: '{{ event.head.repo.url }}'
 
 # This file triggers a set of tasks; the ones targeting Linux are run in a docker
 # container using docker-worker (which is a worker type provided by TaskCluster).
 # The OS X ones are run in a custom worker type, for which we have worker
 # instances configured and running.
 tasks:
   # For the docker-worker tasks, the Docker image used
-  # (staktrace/webrender-test:freetype28) was created using this Dockerfile:
-  # ---
-  #   FROM ubuntu:16.04
-  #   RUN apt-get -y update && apt-get install -y curl git python python-pip cmake pkg-config libx11-dev libgl1-mesa-dev libfontconfig1-dev software-properties-common
-  #   RUN add-apt-repository -y -u ppa:glasen/freetype2
-  #   RUN apt-get -y install freetype2-demos
-  #   RUN pip install mako voluptuous PyYAML servo-tidy
-  #   RUN useradd -d /home/worker -s /bin/bash -m worker
-  #   USER worker
-  #   WORKDIR /home/worker
-  #   ENV PATH $PATH:/home/worker/.cargo/bin
-  #   CMD /bin/bash
-  # ---
+  # (staktrace/webrender-test:debian) was created using the Dockerfile in
+  # ci-scripts/docker-image.
   #
   # The docker image may need to be updated over time if the set of required
   # packages increases. Note in particular that rust/cargo are not part of the
   # docker image, and are re-installed using rustup on each CI run. This ensures
   # the latest stable rust compiler is always used.
   # CI runs will be triggered on opening PRs, updating PRs, and pushes to the
   # repository.
   - metadata:
@@ -50,26 +39,27 @@ tasks:
         events:
           - pull_request.opened
           - pull_request.synchronize
           - push
         excludeBranches:
           - master
     payload:
       maxRunTime: 7200
-      image: 'staktrace/webrender-test:freetype28'
+      image: 'staktrace/webrender-test:debian'
       env:
         RUST_BACKTRACE: 'full'
         RUSTFLAGS: '--deny warnings'
       command:
         - /bin/bash
         - '--login'
         - '-c'
         - >-
           curl https://sh.rustup.rs -sSf | sh -s -- -y &&
+          source $HOME/.cargo/env &&
           git clone {{event.head.repo.url}} webrender && cd webrender &&
           git checkout {{event.head.sha}} &&
           servo-tidy &&
           ci-scripts/linux-release-tests.sh
     routes:
       - "index.garbage.webrender.ci.{{event.head.user.login}}.{{event.head.repo.branch}}.linux-release"
   - metadata:
       name: Linux debug tests
@@ -83,26 +73,27 @@ tasks:
         events:
           - pull_request.opened
           - pull_request.synchronize
           - push
         excludeBranches:
           - master
     payload:
       maxRunTime: 7200
-      image: 'staktrace/webrender-test:freetype28'
+      image: 'staktrace/webrender-test:debian'
       env:
         RUST_BACKTRACE: 'full'
         RUSTFLAGS: '--deny warnings'
       command:
         - /bin/bash
         - '--login'
         - '-c'
         - >-
           curl https://sh.rustup.rs -sSf | sh -s -- -y &&
+          source $HOME/.cargo/env &&
           git clone {{event.head.repo.url}} webrender && cd webrender &&
           git checkout {{event.head.sha}} &&
           servo-tidy &&
           ci-scripts/linux-debug-tests.sh
     routes:
       - "index.garbage.webrender.ci.{{event.head.user.login}}.{{event.head.repo.branch}}.linux-debug"
   # For the OS X jobs we use a pool of machines that we are managing, because
   # Mozilla releng doesn't have any spare OS X machines for us at this time.
new file mode 100644
--- /dev/null
+++ b/gfx/wr/ci-scripts/docker-image/Dockerfile
@@ -0,0 +1,9 @@
+FROM debian:stretch-20181112
+
+COPY setup.sh /root
+RUN cd /root && ./setup.sh
+
+RUN useradd -d /home/worker -s /bin/bash -m worker
+USER worker
+WORKDIR /home/worker
+CMD /bin/bash
new file mode 100755
--- /dev/null
+++ b/gfx/wr/ci-scripts/docker-image/setup.sh
@@ -0,0 +1,48 @@
+#!/usr/bin/env bash
+
+# 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/. */
+
+set -o errexit
+set -o nounset
+set -o pipefail
+set -o xtrace
+
+test "$(whoami)" == 'root'
+
+# Install stuff we need
+apt-get -y update
+apt-get install -y \
+    cmake \
+    curl \
+    git \
+    libfontconfig1-dev \
+    libgl1-mesa-dev \
+    libx11-dev \
+    pkg-config \
+    python \
+    python-pip \
+    software-properties-common
+
+# Build freetype with subpixel rendering enabled
+curl -sSfL -o ft.tar.bz2 \
+    https://download.savannah.gnu.org/releases/freetype/freetype-2.8.1.tar.bz2
+tar xjf ft.tar.bz2
+cd freetype-2.8.1
+# Need to respect 80-char line limit for servo-tidy, or this would be neater
+SUBPIXEL_OPTION="FT_CONFIG_OPTION_SUBPIXEL_RENDERING"
+sed --in-place="" \
+    -e "s/.*${SUBPIXEL_OPTION}.*/#define ${SUBPIXEL_OPTION}/" \
+    include/freetype/config/ftoption.h
+./configure
+make
+make install
+
+# Replace the system libfreetype with the one we just built
+cd /usr/lib/x86_64-linux-gnu/
+rm -f libfreetype.so.6
+ln -s /usr/local/lib/libfreetype.so.6
+
+# Other stuff we need
+pip install mako voluptuous PyYAML servo-tidy
--- a/gfx/wr/webrender/src/batch.rs
+++ b/gfx/wr/webrender/src/batch.rs
@@ -544,25 +544,19 @@ impl AlphaBatchBuilder {
         // Get the clip task address for the global primitive, if one was set.
         let clip_task_address = get_clip_task_address(
             &ctx.scratch.clip_mask_instances,
             prim_instance.clip_task_index,
             0,
             render_tasks,
         ).unwrap_or(OPAQUE_TASK_ADDRESS);
 
-        let prim_data = &ctx
-            .resources
-            .prim_data_store[prim_instance.prim_data_handle];
-
-        match (&prim_instance.kind, &prim_data.kind) {
-            (
-                PrimitiveInstanceKind::Clear,
-                PrimitiveTemplateKind::Clear,
-            ) => {
+        match prim_instance.kind {
+            PrimitiveInstanceKind::Clear { data_handle } => {
+                let prim_data = &ctx.resources.prim_data_store[data_handle];
                 let prim_cache_address = gpu_cache.get_address(&prim_data.gpu_cache_handle);
 
                 // TODO(gw): We can abstract some of the common code below into
                 //           helper methods, as we port more primitives to make
                 //           use of interning.
 
                 let prim_header = PrimitiveHeader {
                     local_rect: prim_data.prim_rect,
@@ -596,20 +590,18 @@ impl AlphaBatchBuilder {
 
                 self.batch_list.push_single_instance(
                     batch_key,
                     bounding_rect,
                     z_id,
                     PrimitiveInstanceData::from(instance),
                 );
             }
-            (
-                PrimitiveInstanceKind::NormalBorder { cache_handles, .. },
-                PrimitiveTemplateKind::NormalBorder { template, .. },
-            ) => {
+            PrimitiveInstanceKind::NormalBorder { data_handle, ref cache_handles, .. } => {
+                let prim_data = &ctx.resources.prim_data_store[data_handle];
                 let prim_cache_address = gpu_cache.get_address(&prim_data.gpu_cache_handle);
                 let cache_handles = &ctx.scratch.border_cache_handles[*cache_handles];
                 let specified_blend_mode = BlendMode::PremultipliedAlpha;
                 let mut segment_data: SmallVec<[SegmentInstanceData; 8]> = SmallVec::new();
 
                 // Collect the segment instance data from each render
                 // task for each valid edge / corner of the border.
 
@@ -655,42 +647,43 @@ impl AlphaBatchBuilder {
                 );
 
                 let prim_header_index = prim_headers.push(
                     &prim_header,
                     z_id,
                     batch_params.prim_user_data,
                 );
 
+                let template = match prim_data.kind {
+                    PrimitiveTemplateKind::NormalBorder { ref template, .. } => template,
+                    _ => unreachable!()
+                };
                 self.add_segmented_prim_to_batch(
                     Some(template.brush_segments.as_slice()),
                     prim_data.opacity,
                     &batch_params,
                     specified_blend_mode,
                     non_segmented_blend_mode,
                     prim_header_index,
                     clip_task_address,
                     bounding_rect,
                     transform_kind,
                     render_tasks,
                     z_id,
                     prim_instance.clip_task_index,
                     ctx,
                 );
             }
-            (
-                PrimitiveInstanceKind::TextRun { run_index, .. },
-                PrimitiveTemplateKind::TextRun { .. },
-            ) => {
-                let run = &ctx.prim_store.text_runs[*run_index];
+            PrimitiveInstanceKind::TextRun { data_handle, run_index, .. } => {
+                let run = &ctx.prim_store.text_runs[run_index];
                 let subpx_dir = run.used_font.get_subpx_dir();
 
                 // The GPU cache data is stored in the template and reused across
                 // frames and display lists.
-
+                let prim_data = &ctx.resources.text_run_data_store[data_handle];
                 let glyph_fetch_buffer = &mut self.glyph_fetch_buffer;
                 let alpha_batch_list = &mut self.batch_list.alpha_batch_list;
                 let prim_cache_address = gpu_cache.get_address(&prim_data.gpu_cache_handle);
 
                 let prim_header = PrimitiveHeader {
                     local_rect: prim_data.prim_rect,
                     local_clip_rect: prim_instance.combined_local_clip_rect,
                     task_address,
@@ -784,23 +777,21 @@ impl AlphaBatchBuilder {
                                 glyph.uv_rect_address.as_int(),
                                 (subpx_dir as u32 as i32) << 16 |
                                 (color_mode as u32 as i32),
                             ));
                         }
                     },
                 );
             }
-            (
-                PrimitiveInstanceKind::LineDecoration { ref cache_handle, .. },
-                PrimitiveTemplateKind::LineDecoration { .. },
-            ) => {
+            PrimitiveInstanceKind::LineDecoration { data_handle, ref cache_handle, .. } => {
                 // The GPU cache data is stored in the template and reused across
                 // frames and display lists.
 
+                let prim_data = &ctx.resources.prim_data_store[data_handle];
                 let prim_cache_address = gpu_cache.get_address(&prim_data.gpu_cache_handle);
 
                 let (batch_kind, textures, prim_user_data, segment_user_data) = match cache_handle {
                     Some(cache_handle) => {
                         let rt_cache_entry = ctx
                             .resource_cache
                             .get_cached_render_task(cache_handle);
                         let cache_item = ctx
@@ -872,20 +863,17 @@ impl AlphaBatchBuilder {
 
                 self.batch_list.push_single_instance(
                     batch_key,
                     bounding_rect,
                     z_id,
                     PrimitiveInstanceData::from(instance),
                 );
             }
-            (
-                PrimitiveInstanceKind::Picture { pic_index },
-                PrimitiveTemplateKind::Unused,
-            ) => {
+            PrimitiveInstanceKind::Picture { pic_index, .. } => {
                 let picture = &ctx.prim_store.pictures[pic_index.0];
                 let non_segmented_blend_mode = BlendMode::PremultipliedAlpha;
                 let prim_cache_address = gpu_cache.get_address(&picture.gpu_location);
 
                 let prim_header = PrimitiveHeader {
                     local_rect: picture.local_rect,
                     local_clip_rect: prim_instance.combined_local_clip_rect,
                     task_address,
@@ -895,27 +883,27 @@ impl AlphaBatchBuilder {
                 };
 
                 match picture.context_3d {
                     // Convert all children of the 3D hierarchy root into batches.
                     Picture3DContext::In { root_data: Some(ref list), .. } => {
                         for child in list {
                             let prim_instance = &picture.prim_list.prim_instances[child.anchor];
                             let pic_index = match prim_instance.kind {
-                                PrimitiveInstanceKind::Picture { pic_index } => pic_index,
+                                PrimitiveInstanceKind::Picture { pic_index, .. } => pic_index,
                                 PrimitiveInstanceKind::LineDecoration { .. } |
                                 PrimitiveInstanceKind::TextRun { .. } |
                                 PrimitiveInstanceKind::NormalBorder { .. } |
                                 PrimitiveInstanceKind::ImageBorder { .. } |
                                 PrimitiveInstanceKind::Rectangle { .. } |
                                 PrimitiveInstanceKind::YuvImage { .. } |
                                 PrimitiveInstanceKind::Image { .. } |
                                 PrimitiveInstanceKind::LinearGradient { .. } |
                                 PrimitiveInstanceKind::RadialGradient { .. } |
-                                PrimitiveInstanceKind::Clear => {
+                                PrimitiveInstanceKind::Clear { .. } => {
                                     unreachable!();
                                 }
                             };
                             let pic = &ctx.prim_store.pictures[pic_index.0];
 
                             // Get clip task, if set, for the picture primitive.
                             let clip_task_address = get_clip_task_address(
                                 &ctx.scratch.clip_mask_instances,
@@ -1396,20 +1384,25 @@ impl AlphaBatchBuilder {
                             prim_headers,
                             transforms,
                             root_spatial_node_index,
                             z_generator,
                         );
                     }
                 }
             }
-            (
-                PrimitiveInstanceKind::ImageBorder { .. },
-                PrimitiveTemplateKind::ImageBorder { request, brush_segments, .. }
-            ) => {
+            PrimitiveInstanceKind::ImageBorder { data_handle, .. } => {
+                let prim_data = &ctx.resources.prim_data_store[data_handle];
+                let (request, brush_segments) = match &prim_data.kind {
+                    PrimitiveTemplateKind::ImageBorder { request, brush_segments, .. } => {
+                        (request, brush_segments)
+                    }
+                    _ => unreachable!()
+                };
+
                 let cache_item = resolve_image(
                     *request,
                     ctx.resource_cache,
                     gpu_cache,
                     deferred_resolves,
                 );
                 if cache_item.texture_id == TextureSource::Invalid {
                     return;
@@ -1464,22 +1457,20 @@ impl AlphaBatchBuilder {
                     bounding_rect,
                     transform_kind,
                     render_tasks,
                     z_id,
                     prim_instance.clip_task_index,
                     ctx,
                 );
             }
-            (
-                PrimitiveInstanceKind::Rectangle { segment_instance_index, opacity_binding_index, .. },
-                PrimitiveTemplateKind::Rectangle { .. }
-            ) => {
+            PrimitiveInstanceKind::Rectangle { data_handle, segment_instance_index, opacity_binding_index, .. } => {
+                let prim_data = &ctx.resources.prim_data_store[data_handle];
                 let specified_blend_mode = BlendMode::PremultipliedAlpha;
-                let opacity_binding = ctx.prim_store.get_opacity_binding(*opacity_binding_index);
+                let opacity_binding = ctx.prim_store.get_opacity_binding(opacity_binding_index);
 
                 let opacity = PrimitiveOpacity::from_alpha(opacity_binding);
                 let opacity = opacity.combine(prim_data.opacity);
 
                 let non_segmented_blend_mode = if !opacity.is_opaque ||
                     prim_instance.clip_task_index != ClipTaskIndex::INVALID ||
                     transform_kind == TransformedRectKind::Complex
                 {
@@ -1490,20 +1481,20 @@ impl AlphaBatchBuilder {
 
                 let batch_params = BrushBatchParameters::shared(
                     BrushBatchKind::Solid,
                     BatchTextures::no_texture(),
                     [get_shader_opacity(opacity_binding), 0, 0],
                     0,
                 );
 
-                let (prim_cache_address, segments) = if *segment_instance_index == SegmentInstanceIndex::UNUSED {
+                let (prim_cache_address, segments) = if segment_instance_index == SegmentInstanceIndex::UNUSED {
                     (gpu_cache.get_address(&prim_data.gpu_cache_handle), None)
                 } else {
-                    let segment_instance = &ctx.scratch.segment_instances[*segment_instance_index];
+                    let segment_instance = &ctx.scratch.segment_instances[segment_instance_index];
                     let segments = Some(&ctx.scratch.segments[segment_instance.segments_range]);
                     (gpu_cache.get_address(&segment_instance.gpu_cache_handle), segments)
                 };
 
                 let prim_header = PrimitiveHeader {
                     local_rect: prim_data.prim_rect,
                     local_clip_rect: prim_instance.combined_local_clip_rect,
                     task_address,
@@ -1529,20 +1520,24 @@ impl AlphaBatchBuilder {
                     bounding_rect,
                     transform_kind,
                     render_tasks,
                     z_id,
                     prim_instance.clip_task_index,
                     ctx,
                 );
             }
-            (
-                PrimitiveInstanceKind::YuvImage { segment_instance_index, .. },
-                PrimitiveTemplateKind::YuvImage { format, yuv_key, image_rendering, color_depth, color_space, .. }
-            ) => {
+            PrimitiveInstanceKind::YuvImage { data_handle, segment_instance_index, .. } => {
+                let prim_data = &ctx.resources.prim_data_store[data_handle];
+                let (format, yuv_key, image_rendering, color_depth, color_space) = match prim_data.kind {
+                    PrimitiveTemplateKind::YuvImage { ref format, yuv_key, ref image_rendering, ref color_depth, ref color_space, .. } => {
+                        (format, yuv_key, image_rendering, color_depth, color_space)
+                    }
+                    _ => unreachable!()
+                };
                 let mut textures = BatchTextures::no_texture();
                 let mut uv_rect_addresses = [0; 3];
 
                 //yuv channel
                 let channel_count = format.get_plane_num();
                 debug_assert!(channel_count <= 3);
                 for channel in 0 .. channel_count {
                     let image_key = yuv_key[channel];
@@ -1599,21 +1594,21 @@ impl AlphaBatchBuilder {
                     prim_instance.clip_task_index != ClipTaskIndex::INVALID ||
                     transform_kind == TransformedRectKind::Complex
                 {
                     specified_blend_mode
                 } else {
                     BlendMode::None
                 };
 
-                debug_assert!(*segment_instance_index != SegmentInstanceIndex::INVALID);
-                let (prim_cache_address, segments) = if *segment_instance_index == SegmentInstanceIndex::UNUSED {
+                debug_assert!(segment_instance_index != SegmentInstanceIndex::INVALID);
+                let (prim_cache_address, segments) = if segment_instance_index == SegmentInstanceIndex::UNUSED {
                     (gpu_cache.get_address(&prim_data.gpu_cache_handle), None)
                 } else {
-                    let segment_instance = &ctx.scratch.segment_instances[*segment_instance_index];
+                    let segment_instance = &ctx.scratch.segment_instances[segment_instance_index];
                     let segments = Some(&ctx.scratch.segments[segment_instance.segments_range]);
                     (gpu_cache.get_address(&segment_instance.gpu_cache_handle), segments)
                 };
 
                 let prim_header = PrimitiveHeader {
                     local_rect: prim_data.prim_rect,
                     local_clip_rect: prim_instance.combined_local_clip_rect,
                     task_address,
@@ -1639,29 +1634,33 @@ impl AlphaBatchBuilder {
                     bounding_rect,
                     transform_kind,
                     render_tasks,
                     z_id,
                     prim_instance.clip_task_index,
                     ctx,
                 );
             }
-            (
-                PrimitiveInstanceKind::Image { image_instance_index, .. },
-                PrimitiveTemplateKind::Image { source, alpha_type, key, image_rendering, .. }
-            ) => {
-                let image_instance = &ctx.prim_store.images[*image_instance_index];
+            PrimitiveInstanceKind::Image { data_handle, image_instance_index, .. } => {
+                let prim_data = &ctx.resources.prim_data_store[data_handle];
+                let (source, alpha_type, key, image_rendering) = match prim_data.kind {
+                    PrimitiveTemplateKind::Image { ref source, alpha_type, key, image_rendering, .. } => {
+                        (source, alpha_type, key, image_rendering)
+                    }
+                    _ => unreachable!()
+                };
+                let image_instance = &ctx.prim_store.images[image_instance_index];
                 let opacity_binding = ctx.prim_store.get_opacity_binding(image_instance.opacity_binding_index);
                 let specified_blend_mode = match alpha_type {
                     AlphaType::PremultipliedAlpha => BlendMode::PremultipliedAlpha,
                     AlphaType::Alpha => BlendMode::Alpha,
                 };
                 let request = ImageRequest {
-                    key: *key,
-                    rendering: *image_rendering,
+                    key: key,
+                    rendering: image_rendering,
                     tile: None,
                 };
 
                 if image_instance.visible_tiles.is_empty() {
                     let cache_item = match *source {
                         ImageSource::Default => {
                             resolve_image(
                                 request,
@@ -1697,17 +1696,17 @@ impl AlphaBatchBuilder {
                     } else {
                         BlendMode::None
                     };
 
                     let batch_params = BrushBatchParameters::shared(
                         BrushBatchKind::Image(get_buffer_kind(cache_item.texture_id)),
                         textures,
                         [
-                            ShaderColorMode::Image as i32 | ((*alpha_type as i32) << 16),
+                            ShaderColorMode::Image as i32 | ((alpha_type as i32) << 16),
                             RasterizationSpace::Local as i32,
                             get_shader_opacity(opacity_binding),
                         ],
                         cache_item.uv_rect_handle.as_int(gpu_cache),
                     );
 
                     debug_assert!(image_instance.segment_instance_index != SegmentInstanceIndex::INVALID);
                     let (prim_cache_address, segments) = if image_instance.segment_instance_index == SegmentInstanceIndex::UNUSED {
@@ -1750,17 +1749,17 @@ impl AlphaBatchBuilder {
                     );
                 } else {
                     for tile in &image_instance.visible_tiles {
                         if let Some((batch_kind, textures, user_data, uv_rect_address)) = get_image_tile_params(
                             ctx.resource_cache,
                             gpu_cache,
                             deferred_resolves,
                             request.with_tile(tile.tile_offset),
-                            *alpha_type,
+                            alpha_type,
                             get_shader_opacity(opacity_binding),
                         ) {
                             let prim_cache_address = gpu_cache.get_address(&tile.handle);
                             let prim_header = PrimitiveHeader {
                                 specific_prim_address: prim_cache_address,
                                 local_rect: tile.local_rect,
                                 local_clip_rect: tile.local_clip_rect,
                                 task_address,
@@ -1779,20 +1778,24 @@ impl AlphaBatchBuilder {
                                 tile.edge_flags,
                                 uv_rect_address,
                                 z_id,
                             );
                         }
                     }
                 }
             }
-            (
-                PrimitiveInstanceKind::LinearGradient { visible_tiles_range, .. },
-                PrimitiveTemplateKind::LinearGradient { stops_handle, ref brush_segments, .. }
-            ) => {
+            PrimitiveInstanceKind::LinearGradient { data_handle, ref visible_tiles_range, .. } => {
+                let prim_data = &ctx.resources.prim_data_store[data_handle];
+                let (ref stops_handle, brush_segments) = match prim_data.kind {
+                    PrimitiveTemplateKind::LinearGradient { stops_handle, ref brush_segments, .. } => {
+                        (stops_handle, brush_segments)
+                    }
+                    _ => unreachable!()
+                };
                 let specified_blend_mode = BlendMode::PremultipliedAlpha;
 
                 let mut prim_header = PrimitiveHeader {
                     local_rect: prim_data.prim_rect,
                     local_clip_rect: prim_instance.combined_local_clip_rect,
                     task_address,
                     specific_prim_address: GpuCacheAddress::invalid(),
                     clip_task_address,
@@ -1862,20 +1865,24 @@ impl AlphaBatchBuilder {
                         gpu_cache,
                         &mut self.batch_list,
                         &prim_header,
                         prim_headers,
                         z_id,
                     );
                 }
             }
-            (
-                PrimitiveInstanceKind::RadialGradient { visible_tiles_range, .. },
-                PrimitiveTemplateKind::RadialGradient { stops_handle, ref brush_segments, .. }
-            ) => {
+            PrimitiveInstanceKind::RadialGradient { data_handle, ref visible_tiles_range, .. } => {
+                let prim_data = &ctx.resources.prim_data_store[data_handle];
+                let (stops_handle, brush_segments) = match prim_data.kind {
+                    PrimitiveTemplateKind::RadialGradient { ref stops_handle, ref brush_segments, .. } => {
+                        (stops_handle, brush_segments)
+                    }
+                    _ => unreachable!()
+                };
                 let specified_blend_mode = BlendMode::PremultipliedAlpha;
 
                 let mut prim_header = PrimitiveHeader {
                     local_rect: prim_data.prim_rect,
                     local_clip_rect: prim_instance.combined_local_clip_rect,
                     task_address,
                     specific_prim_address: GpuCacheAddress::invalid(),
                     clip_task_address,
@@ -1945,19 +1952,16 @@ impl AlphaBatchBuilder {
                         gpu_cache,
                         &mut self.batch_list,
                         &prim_header,
                         prim_headers,
                         z_id,
                     );
                 }
             }
-            _ => {
-                unreachable!();
-            }
         }
     }
 
     fn add_image_tile_to_batch(
         &mut self,
         batch_kind: BrushBatchKind,
         blend_mode: BlendMode,
         textures: BatchTextures,
@@ -2283,19 +2287,19 @@ impl BrushBatchParameters {
 
 impl PrimitiveInstance {
     pub fn is_cacheable(
         &self,
         prim_data_store: &PrimitiveDataStore,
         resource_cache: &ResourceCache,
     ) -> bool {
         let image_key = match self.kind {
-            PrimitiveInstanceKind::Image { .. } |
-            PrimitiveInstanceKind::YuvImage { .. } => {
-                let prim_data = &prim_data_store[self.prim_data_handle];
+            PrimitiveInstanceKind::Image { data_handle, .. } |
+            PrimitiveInstanceKind::YuvImage { data_handle, .. } => {
+                let prim_data = &prim_data_store[data_handle];
                 match prim_data.kind {
                     PrimitiveTemplateKind::YuvImage { ref yuv_key, .. } => {
                         yuv_key[0]
                     }
                     PrimitiveTemplateKind::Image { key, .. } => {
                         key
                     }
                     _ => unreachable!(),
@@ -2304,17 +2308,17 @@ impl PrimitiveInstance {
             PrimitiveInstanceKind::Picture { .. } |
             PrimitiveInstanceKind::TextRun { .. } |
             PrimitiveInstanceKind::LineDecoration { .. } |
             PrimitiveInstanceKind::NormalBorder { .. } |
             PrimitiveInstanceKind::ImageBorder { .. } |
             PrimitiveInstanceKind::Rectangle { .. } |
             PrimitiveInstanceKind::LinearGradient { .. } |
             PrimitiveInstanceKind::RadialGradient { .. } |
-            PrimitiveInstanceKind::Clear => {
+            PrimitiveInstanceKind::Clear { .. } => {
                 return true;
             }
         };
         match resource_cache.get_image_properties(image_key) {
             Some(ImageProperties { external_image: Some(_), .. }) => {
                 false
             }
             _ => true
--- a/gfx/wr/webrender/src/clip.rs
+++ b/gfx/wr/webrender/src/clip.rs
@@ -99,17 +99,16 @@ use util::{extract_inner_rect_safe, proj
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
 pub struct ClipDataMarker;
 
 pub type ClipDataStore = intern::DataStore<ClipItemKey, ClipNode, ClipDataMarker>;
 pub type ClipDataHandle = intern::Handle<ClipDataMarker>;
 pub type ClipDataUpdateList = intern::UpdateList<ClipItemKey>;
 pub type ClipDataInterner = intern::Interner<ClipItemKey, ClipItemSceneData, ClipDataMarker>;
-pub type ClipUid = intern::ItemUid<ClipDataMarker>;
 
 // Result of comparing a clip node instance against a local rect.
 #[derive(Debug)]
 enum ClipResult {
     // The clip does not affect the region at all.
     Accept,
     // The clip prevents the region from being drawn.
     Reject,
--- a/gfx/wr/webrender/src/display_list_flattener.rs
+++ b/gfx/wr/webrender/src/display_list_flattener.rs
@@ -1,9 +1,8 @@
-
 /* 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 api::{AlphaType, BorderDetails, BorderDisplayItem, BuiltDisplayListIter, ClipAndScrollInfo};
 use api::{ClipId, ColorF, ComplexClipRegion, DeviceIntPoint, DeviceIntRect, DeviceIntSize};
 use api::{DisplayItemRef, ExtendMode, ExternalScrollId, AuHelpers};
 use api::{FilterOp, FontInstanceKey, GlyphInstance, GlyphOptions, RasterSpace, GradientStop};
@@ -15,26 +14,28 @@ use api::{Shadow, SpecificDisplayItem, S
 use api::{ClipMode, TransformStyle, YuvColorSpace, YuvData};
 use app_units::Au;
 use clip::{ClipChainId, ClipRegion, ClipItemKey, ClipStore, ClipItemSceneData};
 use clip_scroll_tree::{ROOT_SPATIAL_NODE_INDEX, ClipScrollTree, SpatialNodeIndex};
 use frame_builder::{ChasePrimitive, FrameBuilder, FrameBuilderConfig};
 use glyph_rasterizer::FontInstance;
 use hit_test::{HitTestingItem, HitTestingRun};
 use image::simplify_repeated_primitive;
+use intern::{Handle, Internable};
 use internal_types::{FastHashMap, FastHashSet};
 use picture::{Picture3DContext, PictureCompositeMode, PicturePrimitive, PrimitiveList};
-use prim_store::{PrimitiveInstance, PrimitiveDataInterner, PrimitiveKeyKind, RadialGradientParams};
+use prim_store::{PrimitiveInstance, PrimitiveKeyKind, RadialGradientParams};
 use prim_store::{PrimitiveKey, PrimitiveSceneData, PrimitiveInstanceKind, GradientStopKey, NinePatchDescriptor};
 use prim_store::{PrimitiveDataHandle, PrimitiveStore, PrimitiveStoreStats, LineDecorationCacheKey};
 use prim_store::{ScrollNodeAndClipChain, PictureIndex, register_prim_chase_id, get_line_decoration_sizes};
+use prim_store::text_run::TextRun;
 use render_backend::{DocumentView};
 use resource_cache::{FontInstanceMap, ImageRequest};
 use scene::{Scene, ScenePipeline, StackingContextHelpers};
-use scene_builder::DocumentResources;
+use scene_builder::{DocumentResources, InternerMut};
 use spatial_node::{StickyFrameInfo, ScrollFrameKind, SpatialNodeType};
 use std::{f32, mem, usize};
 use std::collections::vec_deque::VecDeque;
 use tiling::{CompositeOps};
 use util::{MaxRect, VecHelper};
 
 #[derive(Debug, Copy, Clone)]
 struct ClipNode {
@@ -303,17 +304,17 @@ impl<'a> DisplayListFlattener<'a> {
                 scroll_root != ROOT_SPATIAL_NODE_INDEX
             }).unwrap_or(remaining_prims.len() - 1);
 
             let preceding_prims = old_prim_list.prim_instances;
             let trailing_prims = remaining_prims.split_off(last_index + 1);
 
             let prim_list = PrimitiveList::new(
                 remaining_prims,
-                &self.resources.prim_interner,
+                &self.resources,
             );
 
             // Now, create a picture with tile caching enabled that will hold all
             // of the primitives selected as belonging to the main scroll root.
             let prim_key = PrimitiveKey::new(
                 true,
                 LayoutRect::zero(),
                 LayoutRect::max_rect(),
@@ -339,32 +340,34 @@ impl<'a> DisplayListFlattener<'a> {
                 RasterSpace::Screen,
                 prim_list,
                 picture_cache_scroll_root,
                 LayoutRect::max_rect(),
                 &self.clip_store,
             ));
 
             let instance = PrimitiveInstance::new(
-                PrimitiveInstanceKind::Picture { pic_index: PictureIndex(pic_index) },
-                primitive_data_handle,
+                PrimitiveInstanceKind::Picture {
+                    data_handle: primitive_data_handle,
+                    pic_index: PictureIndex(pic_index)
+                },
                 ClipChainId::NONE,
                 picture_cache_scroll_root,
             );
 
             // This contains the tile caching picture, with preceding and
             // trailing primitives outside the main scroll root.
             let mut new_prim_list = preceding_prims;
             new_prim_list.push(instance);
             new_prim_list.extend(trailing_prims);
 
             // Finally, store the sliced primitive list in the root picture.
             self.prim_store.pictures[self.root_pic_index.0].prim_list = PrimitiveList::new(
                 new_prim_list,
-                &self.resources.prim_interner,
+                &self.resources,
             );
         }
     }
 
     /// Find the spatial node that is the scroll root for a given
     /// spatial node.
     fn find_scroll_root(
         &self,
@@ -1021,52 +1024,55 @@ impl<'a> DisplayListFlattener<'a> {
 
             clip_chain_id
         }
     }
 
     /// Create a primitive and add it to the prim store. This method doesn't
     /// add the primitive to the draw list, so can be used for creating
     /// sub-primitives.
-    pub fn create_primitive(
+    ///
+    /// TODO(djg): Can this inline into `add_interned_prim_to_draw_list`
+    fn create_primitive<P>(
         &mut self,
         info: &LayoutPrimitiveInfo,
         clip_chain_id: ClipChainId,
         spatial_node_index: SpatialNodeIndex,
-        prim_key_kind: PrimitiveKeyKind,
-    ) -> PrimitiveInstance {
+        prim: P,
+    ) -> PrimitiveInstance
+    where
+        P: Internable<InternData=PrimitiveSceneData>,
+        P::Source: AsInstanceKind<Handle<P::Marker>>,
+        DocumentResources: InternerMut<P>,
+    {
         // Build a primitive key.
-        let prim_key = PrimitiveKey::new(
-            info.is_backface_visible,
-            info.rect,
-            info.clip_rect,
-            prim_key_kind,
-        );
+        let prim_key = prim.build_key(info);
 
         // Get a tight bounding / culling rect for this primitive
         // from its local rect intersection with minimal local
         // clip rect.
         let culling_rect = info.clip_rect
             .intersection(&info.rect)
             .unwrap_or(LayoutRect::zero());
 
-        let prim_data_handle = self.resources
-            .prim_interner
+        let interner = self.resources.interner_mut();
+        let prim_data_handle =
+            interner
             .intern(&prim_key, || {
                 PrimitiveSceneData {
                     culling_rect,
                     is_backface_visible: info.is_backface_visible,
                 }
             });
 
-        let instance_kind = prim_key.to_instance_kind(&mut self.prim_store);
+        let instance_kind = prim_key.as_instance_kind(prim_data_handle,
+                                                      &mut self.prim_store);
 
         PrimitiveInstance::new(
             instance_kind,
-            prim_data_handle,
             clip_chain_id,
             spatial_node_index,
         )
     }
 
     pub fn add_primitive_to_hit_testing_list(
         &mut self,
         info: &LayoutPrimitiveInfo,
@@ -1100,58 +1106,84 @@ impl<'a> DisplayListFlattener<'a> {
             println!("\tadded to stacking context at {}", self.sc_stack.len());
         }
         let stacking_context = self.sc_stack.last_mut().unwrap();
         stacking_context.primitives.push(prim_instance);
     }
 
     /// Convenience interface that creates a primitive entry and adds it
     /// to the draw list.
-    pub fn add_primitive(
+    pub fn add_primitive<P>(
         &mut self,
         clip_and_scroll: ScrollNodeAndClipChain,
         info: &LayoutPrimitiveInfo,
         clip_items: Vec<(LayoutPoint, ClipItemKey)>,
-        key_kind: PrimitiveKeyKind,
-    ) {
+        prim: P,
+    )
+    where
+        P: Internable<InternData = PrimitiveSceneData> + IsVisible,
+        P::Source: AsInstanceKind<Handle<P::Marker>>,
+        DocumentResources: InternerMut<P>,
+        ShadowItem: From<PendingPrimitive<P>>
+    {
         // If a shadow context is not active, then add the primitive
         // directly to the parent picture.
         if self.pending_shadow_items.is_empty() {
-            if key_kind.is_visible() {
+            if prim.is_visible() {
                 let clip_chain_id = self.build_clip_chain(
                     clip_items,
                     clip_and_scroll.spatial_node_index,
                     clip_and_scroll.clip_chain_id,
                 );
-                let prim_instance = self.create_primitive(
+                self.add_prim_to_draw_list(
                     info,
                     clip_chain_id,
-                    clip_and_scroll.spatial_node_index,
-                    key_kind,
+                    clip_and_scroll,
+                    prim
                 );
-                self.register_chase_primitive_by_rect(
-                    &info.rect,
-                    &prim_instance,
-                );
-                self.add_primitive_to_hit_testing_list(info, clip_and_scroll);
-                self.add_primitive_to_draw_list(prim_instance);
             }
         } else {
             debug_assert!(clip_items.is_empty(), "No per-prim clips expected for shadowed primitives");
 
             // There is an active shadow context. Store as a pending primitive
             // for processing during pop_all_shadows.
-            self.pending_shadow_items.push_back(ShadowItem::Primitive(PendingPrimitive {
+            self.pending_shadow_items.push_back(PendingPrimitive {
                 clip_and_scroll,
                 info: *info,
-                key_kind,
-            }));
+                prim: prim.into(),
+            }.into());
         }
     }
 
+    fn add_prim_to_draw_list<P>(
+        &mut self,
+        info: &LayoutPrimitiveInfo,
+        clip_chain_id: ClipChainId,
+        clip_and_scroll: ScrollNodeAndClipChain,
+        prim: P,
+    )
+    where
+        P: Internable<InternData = PrimitiveSceneData>,
+        P::Source: AsInstanceKind<Handle<P::Marker>>,
+        DocumentResources: InternerMut<P>,
+    {
+        let prim_instance = self.create_primitive(
+            info,
+            clip_chain_id,
+            clip_and_scroll.spatial_node_index,
+            prim,
+        );
+        self.register_chase_primitive_by_rect(
+            &info.rect,
+            &prim_instance,
+        );
+        self.add_primitive_to_hit_testing_list(info, clip_and_scroll);
+        self.add_primitive_to_draw_list(prim_instance);
+    }
+
     pub fn push_stacking_context(
         &mut self,
         pipeline_id: PipelineId,
         composite_ops: CompositeOps,
         transform_style: TransformStyle,
         is_backface_visible: bool,
         is_pipeline_root: bool,
         spatial_node: ClipId,
@@ -1176,17 +1208,17 @@ impl<'a> DisplayListFlattener<'a> {
         // which determines if we *might* need to draw this on
         // an intermediate surface for plane splitting purposes.
         let (parent_is_3d, extra_3d_instance) = match self.sc_stack.last_mut() {
             Some(sc) => {
                 // Cut the sequence of flat children before starting a child stacking context,
                 // so that the relative order between them and our current SC is preserved.
                 let extra_instance = sc.cut_flat_item_sequence(
                     &mut self.prim_store,
-                    &self.resources.prim_interner,
+                    &self.resources,
                     &self.clip_store,
                 );
                 (sc.is_3d(), extra_instance)
             },
             None => (false, None),
         };
 
         if let Some(instance) = extra_3d_instance {
@@ -1327,31 +1359,34 @@ impl<'a> DisplayListFlattener<'a> {
                 leaf_composite_mode,
                 leaf_context_3d,
                 stacking_context.pipeline_id,
                 leaf_output_pipeline_id,
                 true,
                 stacking_context.requested_raster_space,
                 PrimitiveList::new(
                     stacking_context.primitives,
-                    &self.resources.prim_interner,
+                    &self.resources,
                 ),
                 stacking_context.spatial_node_index,
                 max_clip,
                 &self.clip_store,
             ))
         );
 
         // Create a chain of pictures based on presence of filters,
         // mix-blend-mode and/or 3d rendering context containers.
 
         let mut current_pic_index = leaf_pic_index;
+        let data_handle = stacking_context.primitive_data_handle;
         let mut cur_instance = PrimitiveInstance::new(
-            PrimitiveInstanceKind::Picture { pic_index: leaf_pic_index },
-            stacking_context.primitive_data_handle,
+            PrimitiveInstanceKind::Picture {
+                data_handle,
+                pic_index: leaf_pic_index
+            },
             stacking_context.clip_chain_id,
             stacking_context.spatial_node_index,
         );
 
         if cur_instance.is_chased() {
             println!("\tis a leaf primitive for a stacking context");
         }
 
@@ -1371,25 +1406,28 @@ impl<'a> DisplayListFlattener<'a> {
                         ancestor_index,
                     },
                     stacking_context.pipeline_id,
                     stacking_context.frame_output_pipeline_id,
                     true,
                     stacking_context.requested_raster_space,
                     PrimitiveList::new(
                         prims,
-                        &self.resources.prim_interner,
+                        &self.resources,
                     ),
                     stacking_context.spatial_node_index,
                     max_clip,
                     &self.clip_store,
                 ))
             );
 
-            cur_instance.kind = PrimitiveInstanceKind::Picture { pic_index: current_pic_index };
+            cur_instance.kind = PrimitiveInstanceKind::Picture {
+                data_handle,
+                pic_index: current_pic_index
+            };
         }
 
         // For each filter, create a new image with that composite mode.
         for filter in &stacking_context.composite_ops.filters {
             let filter = filter.sanitize();
 
             let filter_pic_index = PictureIndex(self.prim_store.pictures
                 .alloc()
@@ -1397,26 +1435,29 @@ impl<'a> DisplayListFlattener<'a> {
                     Some(PictureCompositeMode::Filter(filter)),
                     Picture3DContext::Out,
                     stacking_context.pipeline_id,
                     None,
                     true,
                     stacking_context.requested_raster_space,
                     PrimitiveList::new(
                         vec![cur_instance.clone()],
-                        &self.resources.prim_interner,
+                        &self.resources,
                     ),
                     stacking_context.spatial_node_index,
                     max_clip,
                     &self.clip_store,
                 ))
             );
 
             current_pic_index = filter_pic_index;
-            cur_instance.kind = PrimitiveInstanceKind::Picture { pic_index: current_pic_index };
+            cur_instance.kind = PrimitiveInstanceKind::Picture {
+                data_handle,
+                pic_index: current_pic_index
+            };
 
             if cur_instance.is_chased() {
                 println!("\tis a composite picture for a stacking context with {:?}", filter);
             }
 
             // Run the optimize pass on this picture, to see if we can
             // collapse opacity and avoid drawing to an off-screen surface.
             self.prim_store.optimize_picture_if_possible(current_pic_index);
@@ -1430,26 +1471,29 @@ impl<'a> DisplayListFlattener<'a> {
                     Some(PictureCompositeMode::MixBlend(mix_blend_mode)),
                     Picture3DContext::Out,
                     stacking_context.pipeline_id,
                     None,
                     true,
                     stacking_context.requested_raster_space,
                     PrimitiveList::new(
                         vec![cur_instance.clone()],
-                        &self.resources.prim_interner,
+                        &self.resources,
                     ),
                     stacking_context.spatial_node_index,
                     max_clip,
                     &self.clip_store,
                 ))
             );
 
             current_pic_index = blend_pic_index;
-            cur_instance.kind = PrimitiveInstanceKind::Picture { pic_index: blend_pic_index };
+            cur_instance.kind = PrimitiveInstanceKind::Picture {
+                data_handle,
+                pic_index: blend_pic_index
+            };
 
             if cur_instance.is_chased() {
                 println!("\tis a mix-blend picture for a stacking context with {:?}", mix_blend_mode);
             }
         }
 
         let has_mix_blend_on_secondary_framebuffer =
             stacking_context.composite_ops.mix_blend_mode.is_some() &&
@@ -1738,34 +1782,25 @@ impl<'a> DisplayListFlattener<'a> {
                         RasterSpace::Local(1.0)
                     };
 
                     // Add any primitives that come after this shadow in the item
                     // list to this shadow.
                     let mut prims = Vec::new();
 
                     for item in &items {
-                        if let ShadowItem::Primitive(ref pending_primitive) = item {
-                            // Offset the local rect and clip rect by the shadow offset.
-                            let mut info = pending_primitive.info.clone();
-                            info.rect = info.rect.translate(&pending_shadow.shadow.offset);
-                            info.clip_rect = info.clip_rect.translate(&pending_shadow.shadow.offset);
-
-                            // Construct and add a primitive for the given shadow.
-                            let shadow_prim_instance = self.create_primitive(
-                                &info,
-                                pending_primitive.clip_and_scroll.clip_chain_id,
-                                pending_primitive.clip_and_scroll.spatial_node_index,
-                                pending_primitive.key_kind.create_shadow(
-                                    &pending_shadow.shadow,
-                                ),
-                            );
-
-                            // Add the new primitive to the shadow picture.
-                            prims.push(shadow_prim_instance);
+                        match item {
+                            // TODO(djg): ugh. de-duplicate this code.
+                            ShadowItem::Primitive(ref pending_primitive) => {
+                                self.add_shadow_prim(&pending_shadow, pending_primitive, &mut prims)
+                            }
+                            ShadowItem::TextRun(ref pending_text_run) => {
+                                self.add_shadow_prim(&pending_shadow, pending_text_run, &mut prims)
+                            }
+                            _ => {}
                         }
                     }
 
                     // No point in adding a shadow here if there were no primitives
                     // added to the shadow.
                     if !prims.is_empty() {
                         // Create a picture that the shadow primitives will be added to. If the
                         // blur radius is 0, the code in Picture::prepare_for_render will
@@ -1780,17 +1815,17 @@ impl<'a> DisplayListFlattener<'a> {
                                 Some(PictureCompositeMode::Filter(blur_filter)),
                                 Picture3DContext::Out,
                                 pipeline_id,
                                 None,
                                 is_passthrough,
                                 raster_space,
                                 PrimitiveList::new(
                                     prims,
-                                    &self.resources.prim_interner,
+                                    &self.resources,
                                 ),
                                 pending_shadow.clip_and_scroll.spatial_node_index,
                                 max_clip,
                                 &self.clip_store,
                             ))
                         );
 
                         let shadow_prim_key = PrimitiveKey::new(
@@ -1806,52 +1841,90 @@ impl<'a> DisplayListFlattener<'a> {
                                 PrimitiveSceneData {
                                     culling_rect: LayoutRect::zero(),
                                     is_backface_visible: true,
                                 }
                             }
                         );
 
                         let shadow_prim_instance = PrimitiveInstance::new(
-                            PrimitiveInstanceKind::Picture { pic_index: shadow_pic_index },
-                            shadow_prim_data_handle,
+                            PrimitiveInstanceKind::Picture {
+                                data_handle: shadow_prim_data_handle,
+                                pic_index: shadow_pic_index
+                            },
                             pending_shadow.clip_and_scroll.clip_chain_id,
                             pending_shadow.clip_and_scroll.spatial_node_index,
                         );
 
                         // Add the shadow primitive. This must be done before pushing this
                         // picture on to the shadow stack, to avoid infinite recursion!
                         self.add_primitive_to_draw_list(shadow_prim_instance);
                     }
                 }
                 ShadowItem::Primitive(pending_primitive) => {
-                    // For a normal primitive, if it has alpha > 0, then we add this
-                    // as a normal primitive to the parent picture.
-                    if pending_primitive.key_kind.is_visible() {
-                        let prim_instance = self.create_primitive(
-                            &pending_primitive.info,
-                            pending_primitive.clip_and_scroll.clip_chain_id,
-                            pending_primitive.clip_and_scroll.spatial_node_index,
-                            pending_primitive.key_kind,
-                        );
-                        self.register_chase_primitive_by_rect(
-                            &pending_primitive.info.rect,
-                            &prim_instance,
-                        );
-                        self.add_primitive_to_hit_testing_list(&pending_primitive.info, pending_primitive.clip_and_scroll);
-                        self.add_primitive_to_draw_list(prim_instance);
-                    }
-                }
+                    self.add_shadow_prim_to_draw_list(pending_primitive)
+                },
+                ShadowItem::TextRun(pending_text_run) => {
+                    self.add_shadow_prim_to_draw_list(pending_text_run)
+                },
             }
         }
 
         debug_assert!(items.is_empty());
         self.pending_shadow_items = items;
     }
 
+    fn add_shadow_prim<P>(
+        &mut self,
+        pending_shadow: &PendingShadow,
+        pending_primitive: &PendingPrimitive<P>,
+        prims: &mut Vec<PrimitiveInstance>,
+    )
+    where
+        P: Internable<InternData=PrimitiveSceneData> + CreateShadow,
+        P::Source: AsInstanceKind<Handle<P::Marker>>,
+        DocumentResources: InternerMut<P>,
+    {
+        // Offset the local rect and clip rect by the shadow offset.
+        let mut info = pending_primitive.info.clone();
+        info.rect = info.rect.translate(&pending_shadow.shadow.offset);
+        info.clip_rect = info.clip_rect.translate(&pending_shadow.shadow.offset);
+
+        // Construct and add a primitive for the given shadow.
+        let shadow_prim_instance = self.create_primitive(
+            &info,
+            pending_primitive.clip_and_scroll.clip_chain_id,
+            pending_primitive.clip_and_scroll.spatial_node_index,
+            pending_primitive.prim.create_shadow(
+                &pending_shadow.shadow,
+            ),
+        );
+
+        // Add the new primitive to the shadow picture.
+        prims.push(shadow_prim_instance);
+    }
+
+    fn add_shadow_prim_to_draw_list<P>(&mut self, pending_primitive: PendingPrimitive<P>)
+    where
+        P: Internable<InternData = PrimitiveSceneData> + IsVisible,
+        P::Source: AsInstanceKind<Handle<P::Marker>>,
+        DocumentResources: InternerMut<P>,
+    {
+        // For a normal primitive, if it has alpha > 0, then we add this
+        // as a normal primitive to the parent picture.
+        if pending_primitive.prim.is_visible() {
+            self.add_prim_to_draw_list(
+                &pending_primitive.info,
+                pending_primitive.clip_and_scroll.clip_chain_id,
+                pending_primitive.clip_and_scroll,
+                pending_primitive.prim,
+            );
+        }
+    }
+
     #[cfg(debug_assertions)]
     fn register_chase_primitive_by_rect(
         &mut self,
         rect: &LayoutRect,
         prim_instance: &PrimitiveInstance,
     ) {
         if ChasePrimitive::LocalRect(*rect) == self.config.chase_primitive {
             println!("Chasing {:?} by local rect", prim_instance.id);
@@ -2175,17 +2248,17 @@ impl<'a> DisplayListFlattener<'a> {
         offset: LayoutVector2D,
         prim_info: &LayoutPrimitiveInfo,
         font_instance_key: &FontInstanceKey,
         text_color: &ColorF,
         glyph_range: ItemRange<GlyphInstance>,
         glyph_options: Option<GlyphOptions>,
         pipeline_id: PipelineId,
     ) {
-        let container = {
+        let text_run = {
             let instance_map = self.font_instances.read().unwrap();
             let font_instance = match instance_map.get(font_instance_key) {
                 Some(instance) => instance,
                 None => {
                     warn!("Unknown font instance key");
                     debug!("key={:?}", font_instance_key);
                     return;
                 }
@@ -2224,29 +2297,29 @@ impl<'a> DisplayListFlattener<'a> {
             let display_list = self.scene.get_display_list_for_pipeline(pipeline_id);
 
             // TODO(gw): It'd be nice not to have to allocate here for creating
             //           the primitive key, when the common case is that the
             //           hash will match and we won't end up creating a new
             //           primitive template.
             let glyphs = display_list.get(glyph_range).collect();
 
-            PrimitiveKeyKind::TextRun {
+            TextRun {
                 glyphs,
                 font,
                 offset: offset.to_au(),
                 shadow: false,
             }
         };
 
         self.add_primitive(
             clip_and_scroll,
             prim_info,
             Vec::new(),
-            container,
+            text_run,
         );
     }
 
     pub fn add_image(
         &mut self,
         clip_and_scroll: ScrollNodeAndClipChain,
         info: &LayoutPrimitiveInfo,
         stretch_size: LayoutSize,
@@ -2344,16 +2417,32 @@ impl<'a> DisplayListFlattener<'a> {
                 }
                 Picture3DContext::In { .. } => {}
                 Picture3DContext::Out => panic!("Unable to find 3D root"),
             }
         }
     }
 }
 
+pub trait AsInstanceKind<H> {
+    fn as_instance_kind(
+        &self,
+        data_handle: H,
+        prim_store: &mut PrimitiveStore,
+    ) -> PrimitiveInstanceKind;
+}
+
+pub trait CreateShadow {
+    fn create_shadow(&self, shadow: &Shadow) -> Self;
+}
+
+pub trait IsVisible {
+    fn is_visible(&self) -> bool;
+}
+
 /// Properties of a stacking context that are maintained
 /// during creation of the scene. These structures are
 /// not persisted after the initial scene build.
 struct FlattenedStackingContext {
     /// The list of primitive instances added to this stacking context.
     primitives: Vec<PrimitiveInstance>,
 
     /// The interned key for all the primitive instances associated with this
@@ -2443,17 +2532,17 @@ impl FlattenedStackingContext {
         true
     }
 
     /// For a Preserve3D context, cut the sequence of the immediate flat children
     /// recorded so far and generate a picture from them.
     pub fn cut_flat_item_sequence(
         &mut self,
         prim_store: &mut PrimitiveStore,
-        prim_interner: &PrimitiveDataInterner,
+        resources: &DocumentResources,
         clip_store: &ClipStore,
     ) -> Option<PrimitiveInstance> {
         if !self.is_3d() || self.primitives.is_empty() {
             return None
         }
         let flat_items_context_3d = match self.context_3d {
             Picture3DContext::In { ancestor_index, .. } => Picture3DContext::In {
                 root_data: None,
@@ -2468,45 +2557,60 @@ impl FlattenedStackingContext {
                 Some(PictureCompositeMode::Blit),
                 flat_items_context_3d,
                 self.pipeline_id,
                 None,
                 true,
                 self.requested_raster_space,
                 PrimitiveList::new(
                     mem::replace(&mut self.primitives, Vec::new()),
-                    prim_interner,
+                    resources,
                 ),
                 self.spatial_node_index,
                 LayoutRect::max_rect(),
                 clip_store,
             ))
         );
 
         Some(PrimitiveInstance::new(
-            PrimitiveInstanceKind::Picture { pic_index },
-            self.primitive_data_handle,
+            PrimitiveInstanceKind::Picture {
+                data_handle: self.primitive_data_handle,
+                pic_index
+            },
             self.clip_chain_id,
             self.spatial_node_index,
         ))
     }
 }
 
 /// A primitive that is added while a shadow context is
 /// active is stored as a pending primitive and only
 /// added to pictures during pop_all_shadows.
-struct PendingPrimitive {
+pub struct PendingPrimitive<T> {
     clip_and_scroll: ScrollNodeAndClipChain,
     info: LayoutPrimitiveInfo,
-    key_kind: PrimitiveKeyKind,
+    prim: T,
 }
 
 /// As shadows are pushed, they are stored as pending
 /// shadows, and handled at once during pop_all_shadows.
-struct PendingShadow {
+pub struct PendingShadow {
     shadow: Shadow,
     clip_and_scroll: ScrollNodeAndClipChain,
 }
 
-enum ShadowItem {
+pub enum ShadowItem {
     Shadow(PendingShadow),
-    Primitive(PendingPrimitive),
+    Primitive(PendingPrimitive<PrimitiveKeyKind>),
+    TextRun(PendingPrimitive<TextRun>),
 }
+
+impl From<PendingPrimitive<PrimitiveKeyKind>> for ShadowItem {
+    fn from(container: PendingPrimitive<PrimitiveKeyKind>) -> Self {
+        ShadowItem::Primitive(container)
+    }
+}
+
+impl From<PendingPrimitive<TextRun>> for ShadowItem {
+    fn from(text_run: PendingPrimitive<TextRun>) -> Self {
+        ShadowItem::TextRun(text_run)
+    }
+}
--- a/gfx/wr/webrender/src/frame_builder.rs
+++ b/gfx/wr/webrender/src/frame_builder.rs
@@ -270,17 +270,17 @@ impl FrameBuilder {
         // which surfaces have valid cached surfaces that don't need to
         // be rendered this frame.
         self.prim_store.update_picture(
             self.root_pic_index,
             &mut pic_update_state,
             &frame_context,
             resource_cache,
             gpu_cache,
-            &resources.prim_data_store,
+            resources,
             &self.clip_store,
             &mut retained_tiles,
         );
 
         // If we had any retained tiles from the last scene that were not picked
         // up by the new frame, then just discard them eagerly.
         // TODO(gw): Maybe it's worth keeping them around for a bit longer in
         //           some cases?
--- a/gfx/wr/webrender/src/intern.rs
+++ b/gfx/wr/webrender/src/intern.rs
@@ -1,17 +1,19 @@
 /* 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 api::LayoutPrimitiveInfo;
 use internal_types::FastHashMap;
 use std::fmt::Debug;
 use std::hash::Hash;
 use std::marker::PhantomData;
 use std::{mem, ops, u64};
+use std::sync::atomic::{AtomicUsize, Ordering};
 use util::VecHelper;
 
 /*
 
  The interning module provides a generic data structure
  interning container. It is similar in concept to a
  traditional string interning container, but it is
  specialized to the WR thread model.
@@ -59,36 +61,47 @@ pub struct UpdateList<S> {
     /// The current epoch of the scene builder.
     epoch: Epoch,
     /// The additions and removals to apply.
     updates: Vec<Update>,
     /// Actual new data to insert.
     data: Vec<S>,
 }
 
+lazy_static! {
+    static ref NEXT_UID: AtomicUsize = AtomicUsize::new(0);
+}
+
+/// A globally, unique identifier
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 #[derive(Debug, Copy, Clone, Eq, Hash, PartialEq)]
-pub struct ItemUid<T> {
+pub struct ItemUid {
     uid: usize,
-    _marker: PhantomData<T>,
+}
+
+impl ItemUid {
+    pub fn next_uid() -> ItemUid {
+        let uid = NEXT_UID.fetch_add(1, Ordering::Relaxed);
+        ItemUid { uid }
+    }
 }
 
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 #[derive(Debug, Copy, Clone)]
-pub struct Handle<T> {
+pub struct Handle<M: Copy> {
     index: u32,
     epoch: Epoch,
-    uid: ItemUid<T>,
-    _marker: PhantomData<T>,
+    uid: ItemUid,
+    _marker: PhantomData<M>,
 }
 
-impl <T> Handle<T> where T: Copy {
-    pub fn uid(&self) -> ItemUid<T> {
+impl <M> Handle<M> where M: Copy {
+    pub fn uid(&self) -> ItemUid {
         self.uid
     }
 }
 
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub enum UpdateKind {
     Insert,
@@ -117,26 +130,29 @@ struct Item<T> {
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub struct DataStore<S, T, M> {
     items: Vec<Item<T>>,
     _source: PhantomData<S>,
     _marker: PhantomData<M>,
 }
 
-impl<S, T, M> DataStore<S, T, M> where S: Debug, T: From<S>, M: Debug {
-    /// Construct a new data store
-    pub fn new() -> Self {
+impl<S, T, M> ::std::default::Default for DataStore<S, T, M> where S: Debug, T: From<S>, M: Debug
+{
+    fn default() -> Self {
         DataStore {
             items: Vec::new(),
             _source: PhantomData,
             _marker: PhantomData,
         }
     }
+}
 
+impl<S, T, M> DataStore<S, T, M> where S: Debug, T: From<S>, M: Debug
+{
     /// Apply any updates from the scene builder thread to
     /// this data store.
     pub fn apply_updates(
         &mut self,
         update_list: UpdateList<S>,
     ) {
         let mut data_iter = update_list.data.into_iter();
         for update in update_list.updates {
@@ -155,74 +171,89 @@ impl<S, T, M> DataStore<S, T, M> where S
                 }
             }
         }
         debug_assert!(data_iter.next().is_none());
     }
 }
 
 /// Retrieve an item from the store via handle
-impl<S, T, M> ops::Index<Handle<M>> for DataStore<S, T, M> {
+impl<S, T, M> ops::Index<Handle<M>> for DataStore<S, T, M>
+where M: Copy
+{
     type Output = T;
     fn index(&self, handle: Handle<M>) -> &T {
         let item = &self.items[handle.index as usize];
         assert_eq!(item.epoch, handle.epoch);
         &item.data
     }
 }
 
 /// Retrieve a mutable item from the store via handle
 /// Retrieve an item from the store via handle
-impl<S, T, M> ops::IndexMut<Handle<M>> for DataStore<S, T, M> {
+impl<S, T, M> ops::IndexMut<Handle<M>> for DataStore<S, T, M>
+where
+    M: Copy
+{
     fn index_mut(&mut self, handle: Handle<M>) -> &mut T {
         let item = &mut self.items[handle.index as usize];
         assert_eq!(item.epoch, handle.epoch);
         &mut item.data
     }
 }
 
 /// The main interning data structure. This lives in the
 /// scene builder thread, and handles hashing and interning
 /// unique data structures. It also manages a free-list for
 /// the items in the data store, which is synchronized via
 /// an update list of additions / removals.
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
-pub struct Interner<S : Eq + Hash + Clone + Debug, D, M> {
+pub struct Interner<S, D, M>
+where
+    S: Eq + Hash + Clone + Debug,
+    M: Copy
+{
     /// Uniquely map an interning key to a handle
     map: FastHashMap<S, Handle<M>>,
     /// List of free slots in the data store for re-use.
     free_list: Vec<usize>,
     /// Pending list of updates that need to be applied.
     updates: Vec<Update>,
     /// Pending new data to insert.
     update_data: Vec<S>,
     /// The current epoch for the interner.
     current_epoch: Epoch,
-    /// Incrementing counter for identifying stable values.
-    next_uid: usize,
     /// The information associated with each interned
     /// item that can be accessed by the interner.
     local_data: Vec<Item<D>>,
 }
 
-impl<S, D, M> Interner<S, D, M> where S: Eq + Hash + Clone + Debug, M: Copy + Debug {
-    /// Construct a new interner
-    pub fn new() -> Self {
+impl<S, D, M> ::std::default::Default for Interner<S, D, M>
+where
+    S: Eq + Hash + Clone + Debug,
+    M: Copy + Debug
+{
+    fn default() -> Self {
         Interner {
             map: FastHashMap::default(),
             free_list: Vec::new(),
             updates: Vec::new(),
             update_data: Vec::new(),
             current_epoch: Epoch(1),
-            next_uid: 0,
             local_data: Vec::new(),
         }
     }
+}
 
+impl<S, D, M> Interner<S, D, M>
+where
+    S: Eq + Hash + Clone + Debug,
+    M: Copy + Debug
+{
     /// Intern a data structure, and return a handle to
     /// that data. The handle can then be stored in the
     /// frame builder, and safely accessed via the data
     /// store that lives in the frame builder thread.
     /// The provided closure is invoked to build the
     /// local data about an interned structure if the
     /// key isn't already interned.
     pub fn intern<F>(
@@ -263,27 +294,23 @@ impl<S, D, M> Interner<S, D, M> where S:
             kind: UpdateKind::Insert,
         });
         self.update_data.alloc().init(data.clone());
 
         // Generate a handle for access via the data store.
         let handle = Handle {
             index: index as u32,
             epoch: self.current_epoch,
-            uid: ItemUid {
-                uid: self.next_uid,
-                _marker: PhantomData,
-            },
+            uid: ItemUid::next_uid(),
             _marker: PhantomData,
         };
 
         // Store this handle so the next time it is
         // interned, it gets re-used.
         self.map.insert(data.clone(), handle);
-        self.next_uid += 1;
 
         // Create the local data for this item that is
         // being interned.
         self.local_data.entry(index).set(Item {
             epoch: self.current_epoch,
             data: f(),
         });
 
@@ -333,16 +360,33 @@ impl<S, D, M> Interner<S, D, M> where S:
         // Begin the next epoch
         self.current_epoch = Epoch(self.current_epoch.0 + 1);
 
         updates
     }
 }
 
 /// Retrieve the local data for an item from the interner via handle
-impl<S, D, M> ops::Index<Handle<M>> for Interner<S, D, M> where S: Eq + Clone + Hash + Debug, M: Copy + Debug {
+impl<S, D, M> ops::Index<Handle<M>> for Interner<S, D, M>
+where
+    S: Eq + Clone + Hash + Debug,
+    M: Copy + Debug
+{
     type Output = D;
     fn index(&self, handle: Handle<M>) -> &D {
         let item = &self.local_data[handle.index as usize];
         assert_eq!(item.epoch, handle.epoch);
         &item.data
     }
 }
+
+/// Implement `Internable` for a type that wants participate in interning.
+///
+/// see DisplayListFlattener::add_interned_primitive<P>
+pub trait Internable {
+    type Marker: Copy + Debug;
+    type Source: Eq + Hash + Clone + Debug;
+    type StoreData: From<Self::Source>;
+    type InternData;
+
+    /// Build a new key from self with `info`.
+    fn build_key(self, info: &LayoutPrimitiveInfo) -> Self::Source;
+}
--- a/gfx/wr/webrender/src/lib.rs
+++ b/gfx/wr/webrender/src/lib.rs
@@ -59,19 +59,16 @@ extern crate lazy_static;
 #[macro_use]
 extern crate log;
 #[cfg(any(feature = "serde"))]
 #[macro_use]
 extern crate serde;
 #[macro_use]
 extern crate thread_profiler;
 
-#[macro_use]
-mod storage;
-
 mod batch;
 mod border;
 mod box_shadow;
 #[cfg(any(feature = "capture", feature = "replay"))]
 mod capture;
 mod clip;
 mod clip_scroll_tree;
 mod debug_colors;
@@ -107,16 +104,17 @@ mod render_backend;
 mod render_task;
 mod renderer;
 mod resource_cache;
 mod scene;
 mod scene_builder;
 mod segment;
 mod shade;
 mod spatial_node;
+mod storage;
 mod surface;
 mod texture_allocator;
 mod texture_cache;
 mod tiling;
 mod util;
 
 mod shader_source {
     include!(concat!(env!("OUT_DIR"), "/shaders.rs"));
--- a/gfx/wr/webrender/src/picture.rs
+++ b/gfx/wr/webrender/src/picture.rs
@@ -2,34 +2,37 @@
  * 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 api::{DeviceRect, FilterOp, MixBlendMode, PipelineId, PremultipliedColorF, PictureRect, PicturePoint};
 use api::{DeviceIntRect, DevicePoint, LayoutRect, PictureToRasterTransform, LayoutPixel, PropertyBinding, PropertyBindingId};
 use api::{DevicePixelScale, RasterRect, RasterSpace, PictureSize, DeviceIntPoint, ColorF, ImageKey, DirtyRect};
 use api::{PicturePixel, RasterPixel, WorldPixel, WorldRect, ImageFormat, ImageDescriptor};
 use box_shadow::{BLUR_SAMPLE_SCALE};
-use clip::{ClipNodeCollector, ClipStore, ClipChainId, ClipChainNode, ClipUid};
+use clip::{ClipNodeCollector, ClipStore, ClipChainId, ClipChainNode};
 use clip_scroll_tree::{ROOT_SPATIAL_NODE_INDEX, ClipScrollTree, SpatialNodeIndex};
 use device::TextureFilter;
 use euclid::{TypedScale, vec3, TypedRect, TypedPoint2D, TypedSize2D};
 use euclid::approxeq::ApproxEq;
+use intern::ItemUid;
 use internal_types::{FastHashMap, PlaneSplitter};
 use frame_builder::{FrameBuildingContext, FrameBuildingState, PictureState, PictureContext};
 use gpu_cache::{GpuCache, GpuCacheAddress, GpuCacheHandle};
 use gpu_types::{TransformPalette, TransformPaletteId, UvRectKind};
 use internal_types::FastHashSet;
 use plane_split::{Clipper, Polygon, Splitter};
-use prim_store::{PictureIndex, PrimitiveInstance, SpaceMapper, VisibleFace, PrimitiveInstanceKind, PrimitiveUid};
-use prim_store::{get_raster_rects, PrimitiveDataInterner, PrimitiveDataStore, CoordinateSpaceMapping};
+use prim_store::{PictureIndex, PrimitiveInstance, SpaceMapper, VisibleFace, PrimitiveInstanceKind};
+use prim_store::{get_raster_rects, CoordinateSpaceMapping};
 use prim_store::{OpacityBindingStorage, PrimitiveTemplateKind, ImageInstanceStorage, OpacityBindingIndex, SizeKey};
+use render_backend::FrameResources;
 use render_task::{ClearMode, RenderTask, RenderTaskCacheEntryHandle, TileBlit};
 use render_task::{RenderTaskCacheKey, RenderTaskCacheKeyKind, RenderTaskId, RenderTaskLocation};
 use resource_cache::ResourceCache;
 use scene::{FilterOpHelpers, SceneProperties};
+use scene_builder::DocumentResources;
 use smallvec::SmallVec;
 use surface::{SurfaceDescriptor, TransformKey};
 use std::{mem, ops};
 use texture_cache::{Eviction, TextureCacheHandle};
 use tiling::RenderTargetKind;
 use util::{TransformedRectKind, MatrixHelpers, MaxRect, RectHelpers};
 
 /*
@@ -231,21 +234,21 @@ impl Tile {
 pub struct TileTransformIndex(u32);
 
 /// Uniquely describes the content of this tile, in a way that can be
 /// (reasonably) efficiently hashed and compared.
 #[derive(Debug, Eq, PartialEq, Hash)]
 pub struct TileDescriptor {
     /// List of primitive unique identifiers. The uid is guaranteed
     /// to uniquely describe the content of the primitive.
-    pub prim_uids: Vec<PrimitiveUid>,
+    pub prim_uids: Vec<ItemUid>,
 
     /// List of clip node unique identifiers. The uid is guaranteed
     /// to uniquely describe the content of the clip node.
-    pub clip_uids: Vec<ClipUid>,
+    pub clip_uids: Vec<ItemUid>,
 
     /// List of local tile transform ids that are used to position
     /// the primitive and clip items above.
     pub transform_ids: Vec<TileTransformIndex>,
 
     /// List of transforms used by this tile, along with the current
     /// quantized value.
     pub transforms: Vec<TransformKey>,
@@ -595,29 +598,29 @@ impl TileCache {
     }
 
     /// Update the dependencies for each tile for a given primitive instance.
     pub fn update_prim_dependencies(
         &mut self,
         prim_instance: &PrimitiveInstance,
         surface_spatial_node_index: SpatialNodeIndex,
         clip_scroll_tree: &ClipScrollTree,
-        prim_data_store: &PrimitiveDataStore,
+        resources: &FrameResources,
         clip_chain_nodes: &[ClipChainNode],
         pictures: &[PicturePrimitive],
         resource_cache: &ResourceCache,
         opacity_binding_store: &OpacityBindingStorage,
         image_instances: &ImageInstanceStorage,
     ) {
         self.space_mapper.set_target_spatial_node(
             prim_instance.spatial_node_index,
             clip_scroll_tree,
         );
 
-        let prim_data = &prim_data_store[prim_instance.prim_data_handle];
+        let prim_data = &resources.as_common_data(&prim_instance);
 
         // Map the primitive local rect into the picture space.
         // TODO(gw): We should maybe store this in the primitive template
         //           during interning so that we never have to calculate
         //           it during frame building.
         let culling_rect = match prim_data.prim_rect.intersection(&prim_data.clip_rect) {
             Some(rect) => rect,
             None => return,
@@ -643,28 +646,28 @@ impl TileCache {
         let y1 = ((rect.origin.y + rect.size.height) / self.local_tile_size.height).ceil() as i32;
 
         // Update the tile array allocation if needed.
         self.reconfigure_tiles_if_required(x0, y0, x1, y1);
 
         // Build the list of resources that this primitive has dependencies on.
         let mut opacity_bindings: SmallVec<[PropertyBindingId; 4]> = SmallVec::new();
         let mut clip_chain_spatial_nodes: SmallVec<[SpatialNodeIndex; 8]> = SmallVec::new();
-        let mut clip_chain_uids: SmallVec<[ClipUid; 8]> = SmallVec::new();
+        let mut clip_chain_uids: SmallVec<[ItemUid; 8]> = SmallVec::new();
         let mut image_keys: SmallVec<[ImageKey; 8]> = SmallVec::new();
         let mut current_clip_chain_id = prim_instance.clip_chain_id;
 
         // Some primitives can not be cached (e.g. external video images)
         let is_cacheable = prim_instance.is_cacheable(
-            prim_data_store,
+            &resources.prim_data_store,
             resource_cache,
         );
 
         match prim_instance.kind {
-            PrimitiveInstanceKind::Picture { pic_index } => {
+            PrimitiveInstanceKind::Picture { pic_index,.. } => {
                 // Pictures can depend on animated opacity bindings.
                 let pic = &pictures[pic_index.0];
                 if let Some(PictureCompositeMode::Filter(FilterOp::Opacity(binding, _))) = pic.requested_composite_mode {
                     if let PropertyBinding::Binding(key, _) = binding {
                         opacity_bindings.push(key.id);
                     }
                 }
             }
@@ -673,17 +676,18 @@ impl TileCache {
                     let opacity_binding = &opacity_binding_store[opacity_binding_index];
                     for binding in &opacity_binding.bindings {
                         if let PropertyBinding::Binding(key, _) = binding {
                             opacity_bindings.push(key.id);
                         }
                     }
                 }
             }
-            PrimitiveInstanceKind::Image { image_instance_index, .. } => {
+            PrimitiveInstanceKind::Image { data_handle, image_instance_index, .. } => {
+                let prim_data = &resources.prim_data_store[data_handle];
                 let image_instance = &image_instances[image_instance_index];
                 let opacity_binding_index = image_instance.opacity_binding_index;
 
                 if opacity_binding_index != OpacityBindingIndex::INVALID {
                     let opacity_binding = &opacity_binding_store[opacity_binding_index];
                     for binding in &opacity_binding.bindings {
                         if let PropertyBinding::Binding(key, _) = binding {
                             opacity_bindings.push(key.id);
@@ -695,29 +699,30 @@ impl TileCache {
                     PrimitiveTemplateKind::Image { key, .. } => {
                         image_keys.push(key);
                     }
                     _ => {
                         unreachable!();
                     }
                 }
             }
-            PrimitiveInstanceKind::YuvImage { .. } => {
+            PrimitiveInstanceKind::YuvImage { data_handle, .. } => {
+                let prim_data = &resources.prim_data_store[data_handle];
                 match prim_data.kind {
                     PrimitiveTemplateKind::YuvImage { ref yuv_key, .. } => {
                         image_keys.extend_from_slice(yuv_key);
                     }
                     _ => {
                         unreachable!();
                     }
                 }
             }
             PrimitiveInstanceKind::TextRun { .. } |
             PrimitiveInstanceKind::LineDecoration { .. } |
-            PrimitiveInstanceKind::Clear |
+            PrimitiveInstanceKind::Clear { .. } |
             PrimitiveInstanceKind::NormalBorder { .. } |
             PrimitiveInstanceKind::LinearGradient { .. } |
             PrimitiveInstanceKind::RadialGradient { .. } |
             PrimitiveInstanceKind::ImageBorder { .. } => {
                 // These don't contribute dependencies
             }
         }
 
@@ -772,17 +777,17 @@ impl TileCache {
                 // Include any opacity bindings this primitive depends on.
                 for id in &opacity_bindings {
                     if tile.opacity_bindings.insert(*id) {
                         tile.descriptor.opacity_bindings.push(*id);
                     }
                 }
 
                 // Update the tile descriptor, used for tile comparison during scene swaps.
-                tile.descriptor.prim_uids.push(prim_instance.prim_data_handle.uid());
+                tile.descriptor.prim_uids.push(prim_instance.uid());
                 tile.descriptor.clip_uids.extend_from_slice(&clip_chain_uids);
             }
         }
     }
 
     /// Build the dirty region(s) for the tile cache after all primitive
     /// dependencies have been updated.
     pub fn build_dirty_regions(
@@ -1253,41 +1258,58 @@ impl PrimitiveList {
     }
 
     /// Construct a new prim list from a list of instances
     /// in render order. This does some work during scene
     /// building which makes the frame building traversals
     /// significantly faster.
     pub fn new(
         mut prim_instances: Vec<PrimitiveInstance>,
-        prim_interner: &PrimitiveDataInterner,
+        resources: &DocumentResources
     ) -> Self {
         let mut pictures = SmallVec::new();
         let mut clusters_map = FastHashMap::default();
         let mut clusters: SmallVec<[PrimitiveCluster; 4]> = SmallVec::new();
         let mut prim_cluster_map = Vec::new();
 
         // Walk the list of primitive instances and extract any that
         // are pictures.
         for prim_instance in &mut prim_instances {
             // Check if this primitive is a picture. In future we should
             // remove this match and embed this info directly in the primitive instance.
             let is_pic = match prim_instance.kind {
-                PrimitiveInstanceKind::Picture { pic_index } => {
+                PrimitiveInstanceKind::Picture { pic_index, .. } => {
                     pictures.push(pic_index);
                     true
                 }
                 _ => {
                     false
                 }
             };
 
+            let prim_data = match prim_instance.kind {
+                PrimitiveInstanceKind::Picture { data_handle, .. } |
+                PrimitiveInstanceKind::LineDecoration { data_handle, .. } |
+                PrimitiveInstanceKind::NormalBorder { data_handle, .. } |
+                PrimitiveInstanceKind::ImageBorder { data_handle, .. } |
+                PrimitiveInstanceKind::Rectangle { data_handle, .. } |
+                PrimitiveInstanceKind::YuvImage { data_handle, .. } |
+                PrimitiveInstanceKind::Image { data_handle, .. } |
+                PrimitiveInstanceKind::LinearGradient { data_handle, .. } |
+                PrimitiveInstanceKind::RadialGradient { data_handle, ..} |
+                PrimitiveInstanceKind::Clear {  data_handle, .. } => {
+                    &resources.prim_interner[data_handle]
+                }
+                PrimitiveInstanceKind::TextRun { data_handle, .. } => {
+                    &resources.text_run_interner[data_handle]
+                }
+            };
+
             // Get the key for the cluster that this primitive should
             // belong to.
-            let prim_data = &prim_interner[prim_instance.prim_data_handle];
             let key = PrimitiveClusterKey {
                 spatial_node_index: prim_instance.spatial_node_index,
                 is_backface_visible: prim_data.is_backface_visible,
             };
 
             // Find the cluster, or create a new one.
             let cluster_index = *clusters_map
                 .entry(key)
@@ -1903,17 +1925,17 @@ impl PicturePrimitive {
 
     /// Update the primitive dependencies for any active tile caches,
     /// but only *if* the transforms have made the mappings out of date.
     pub fn update_prim_dependencies(
         &self,
         state: &mut PictureUpdateState,
         frame_context: &FrameBuildingContext,
         resource_cache: &mut ResourceCache,
-        prim_data_store: &PrimitiveDataStore,
+        resources: &FrameResources,
         pictures: &[PicturePrimitive],
         clip_store: &ClipStore,
         opacity_binding_store: &OpacityBindingStorage,
         image_instances: &ImageInstanceStorage,
     ) {
         if state.tile_cache_update_count == 0 {
             return;
         }
@@ -1921,17 +1943,17 @@ impl PicturePrimitive {
         let surface_spatial_node_index = state.current_surface().surface_spatial_node_index;
 
         for prim_instance in &self.prim_list.prim_instances {
             for tile_cache in &mut state.tile_cache_stack {
                 tile_cache.update_prim_dependencies(
                     prim_instance,
                     surface_spatial_node_index,
                     &frame_context.clip_scroll_tree,
-                    prim_data_store,
+                    resources,
                     &clip_store.clip_chain_nodes,
                     pictures,
                     resource_cache,
                     opacity_binding_store,
                     image_instances,
                 );
             }
         }
deleted file mode 100644
--- a/gfx/wr/webrender/src/prim_store.rs
+++ /dev/null
@@ -1,4325 +0,0 @@
-/* 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 api::{AlphaType, BorderRadius, ClipMode, ColorF, PictureRect, ColorU, LayoutVector2D};
-use api::{DeviceIntRect, DeviceIntSize, DevicePixelScale, ExtendMode, DeviceRect, LayoutSideOffsetsAu};
-use api::{FilterOp, GlyphInstance, GradientStop, ImageKey, ImageRendering, TileOffset, RepeatMode};
-use api::{RasterSpace, LayoutPoint, LayoutRect, LayoutSideOffsets, LayoutSize, LayoutToWorldTransform};
-use api::{PremultipliedColorF, PropertyBinding, Shadow, YuvColorSpace, YuvFormat};
-use api::{DeviceIntSideOffsets, WorldPixel, BoxShadowClipMode, NormalBorder, WorldRect, LayoutToWorldScale};
-use api::{PicturePixel, RasterPixel, ColorDepth, LineStyle, LineOrientation, LayoutSizeAu, AuHelpers, LayoutVector2DAu};
-use app_units::Au;
-use border::{get_max_scale_for_border, build_border_instances, create_border_segments};
-use border::{BorderSegmentCacheKey, NormalBorderAu};
-use clip::{ClipStore};
-use clip_scroll_tree::{ClipScrollTree, SpatialNodeIndex};
-use clip::{ClipDataStore, ClipNodeFlags, ClipChainId, ClipChainInstance, ClipItem, ClipNodeCollector};
-use euclid::{SideOffsets2D, TypedTransform3D, TypedRect, TypedScale, TypedSize2D};
-use frame_builder::{FrameBuildingContext, FrameBuildingState, PictureContext, PictureState};
-use frame_builder::PrimitiveContext;
-use glyph_rasterizer::{FontInstance, FontTransform, GlyphKey, FONT_SIZE_LIMIT};
-use gpu_cache::{GpuCache, GpuCacheAddress, GpuCacheHandle, GpuDataRequest, ToGpuBlocks};
-use gpu_types::BrushFlags;
-use image::{self, Repetition};
-use intern;
-use internal_types::FastHashMap;
-use picture::{PictureCompositeMode, PicturePrimitive, PictureUpdateState};
-use picture::{ClusterRange, PrimitiveList, SurfaceIndex, TileDescriptor};
-#[cfg(debug_assertions)]
-use render_backend::{FrameId};
-use render_backend::FrameResources;
-use render_task::{BlitSource, RenderTask, RenderTaskCacheKey, RenderTaskTree, to_cache_size};
-use render_task::{RenderTaskCacheKeyKind, RenderTaskId, RenderTaskCacheEntryHandle};
-use renderer::{MAX_VERTEX_TEXTURE_WIDTH};
-use resource_cache::{ImageProperties, ImageRequest, ResourceCache};
-use scene::SceneProperties;
-use segment::SegmentBuilder;
-use std::{cmp, fmt, hash, mem, ops, u32, usize};
-#[cfg(debug_assertions)]
-use std::sync::atomic::{AtomicUsize, Ordering};
-use storage;
-use texture_cache::TextureCacheHandle;
-use tiling::SpecialRenderPasses;
-use util::{ScaleOffset, MatrixHelpers, MaxRect, recycle_vec};
-use util::{pack_as_float, project_rect, raster_rect_to_device_pixels};
-use smallvec::SmallVec;
-
-/// Counter for unique primitive IDs for debug tracing.
-#[cfg(debug_assertions)]
-static NEXT_PRIM_ID: AtomicUsize = AtomicUsize::new(0);
-
-#[cfg(debug_assertions)]
-static PRIM_CHASE_ID: AtomicUsize = AtomicUsize::new(usize::MAX);
-
-#[cfg(debug_assertions)]
-pub fn register_prim_chase_id(id: PrimitiveDebugId) {
-    PRIM_CHASE_ID.store(id.0, Ordering::SeqCst);
-}
-
-#[cfg(not(debug_assertions))]
-pub fn register_prim_chase_id(_: PrimitiveDebugId) {
-}
-
-const MIN_BRUSH_SPLIT_AREA: f32 = 256.0 * 256.0;
-pub const VECS_PER_SEGMENT: usize = 2;
-
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-pub struct ScrollNodeAndClipChain {
-    pub spatial_node_index: SpatialNodeIndex,
-    pub clip_chain_id: ClipChainId,
-}
-
-impl ScrollNodeAndClipChain {
-    pub fn new(
-        spatial_node_index: SpatialNodeIndex,
-        clip_chain_id: ClipChainId
-    ) -> Self {
-        ScrollNodeAndClipChain {
-            spatial_node_index,
-            clip_chain_id,
-        }
-    }
-}
-
-#[cfg_attr(feature = "capture", derive(Serialize))]
-#[cfg_attr(feature = "replay", derive(Deserialize))]
-#[derive(Debug, Copy, Clone)]
-pub struct PrimitiveOpacity {
-    pub is_opaque: bool,
-}
-
-impl PrimitiveOpacity {
-    pub fn opaque() -> PrimitiveOpacity {
-        PrimitiveOpacity { is_opaque: true }
-    }
-
-    pub fn translucent() -> PrimitiveOpacity {
-        PrimitiveOpacity { is_opaque: false }
-    }
-
-    pub fn from_alpha(alpha: f32) -> PrimitiveOpacity {
-        PrimitiveOpacity {
-            is_opaque: alpha >= 1.0,
-        }
-    }
-
-    pub fn combine(&self, other: PrimitiveOpacity) -> PrimitiveOpacity {
-        PrimitiveOpacity{
-            is_opaque: self.is_opaque && other.is_opaque
-        }
-    }
-}
-
-#[derive(Debug, Copy, Clone)]
-pub enum VisibleFace {
-    Front,
-    Back,
-}
-
-impl ops::Not for VisibleFace {
-    type Output = Self;
-    fn not(self) -> Self {
-        match self {
-            VisibleFace::Front => VisibleFace::Back,
-            VisibleFace::Back => VisibleFace::Front,
-        }
-    }
-}
-
-#[derive(Debug)]
-pub enum CoordinateSpaceMapping<F, T> {
-    Local,
-    ScaleOffset(ScaleOffset),
-    Transform(TypedTransform3D<f32, F, T>),
-}
-
-impl<F, T> CoordinateSpaceMapping<F, T> {
-    pub fn new(
-        ref_spatial_node_index: SpatialNodeIndex,
-        target_node_index: SpatialNodeIndex,
-        clip_scroll_tree: &ClipScrollTree,
-    ) -> Option<Self> {
-        let spatial_nodes = &clip_scroll_tree.spatial_nodes;
-        let ref_spatial_node = &spatial_nodes[ref_spatial_node_index.0];
-        let target_spatial_node = &spatial_nodes[target_node_index.0];
-
-        if ref_spatial_node_index == target_node_index {
-            Some(CoordinateSpaceMapping::Local)
-        } else if ref_spatial_node.coordinate_system_id == target_spatial_node.coordinate_system_id {
-            Some(CoordinateSpaceMapping::ScaleOffset(
-                ref_spatial_node.coordinate_system_relative_scale_offset
-                    .inverse()
-                    .accumulate(
-                        &target_spatial_node.coordinate_system_relative_scale_offset
-                    )
-            ))
-        } else {
-            let transform = clip_scroll_tree.get_relative_transform(
-                target_node_index,
-                ref_spatial_node_index,
-            );
-
-            transform.map(|transform| {
-                CoordinateSpaceMapping::Transform(
-                    transform.with_source::<F>().with_destination::<T>()
-                )
-            })
-        }
-    }
-}
-
-#[derive(Debug)]
-pub struct SpaceMapper<F, T> {
-    kind: CoordinateSpaceMapping<F, T>,
-    pub ref_spatial_node_index: SpatialNodeIndex,
-    pub current_target_spatial_node_index: SpatialNodeIndex,
-    pub bounds: TypedRect<f32, T>,
-}
-
-impl<F, T> SpaceMapper<F, T> where F: fmt::Debug {
-    pub fn new(
-        ref_spatial_node_index: SpatialNodeIndex,
-        bounds: TypedRect<f32, T>,
-    ) -> Self {
-        SpaceMapper {
-            kind: CoordinateSpaceMapping::Local,
-            ref_spatial_node_index,
-            current_target_spatial_node_index: ref_spatial_node_index,
-            bounds,
-        }
-    }
-
-    pub fn new_with_target(
-        ref_spatial_node_index: SpatialNodeIndex,
-        target_node_index: SpatialNodeIndex,
-        bounds: TypedRect<f32, T>,
-        clip_scroll_tree: &ClipScrollTree,
-    ) -> Self {
-        let mut mapper = SpaceMapper::new(ref_spatial_node_index, bounds);
-        mapper.set_target_spatial_node(target_node_index, clip_scroll_tree);
-        mapper
-    }
-
-    pub fn set_target_spatial_node(
-        &mut self,
-        target_node_index: SpatialNodeIndex,
-        clip_scroll_tree: &ClipScrollTree,
-    ) {
-        if target_node_index != self.current_target_spatial_node_index {
-            self.current_target_spatial_node_index = target_node_index;
-
-            self.kind = CoordinateSpaceMapping::new(
-                self.ref_spatial_node_index,
-                target_node_index,
-                clip_scroll_tree,
-            ).expect("bug: should have been culled by invalid node");
-        }
-    }
-
-    pub fn get_transform(&self) -> TypedTransform3D<f32, F, T> {
-        match self.kind {
-            CoordinateSpaceMapping::Local => {
-                TypedTransform3D::identity()
-            }
-            CoordinateSpaceMapping::ScaleOffset(ref scale_offset) => {
-                scale_offset.to_transform()
-            }
-            CoordinateSpaceMapping::Transform(transform) => {
-                transform
-            }
-        }
-    }
-
-    pub fn unmap(&self, rect: &TypedRect<f32, T>) -> Option<TypedRect<f32, F>> {
-        match self.kind {
-            CoordinateSpaceMapping::Local => {
-                Some(TypedRect::from_untyped(&rect.to_untyped()))
-            }
-            CoordinateSpaceMapping::ScaleOffset(ref scale_offset) => {
-                Some(scale_offset.unmap_rect(rect))
-            }
-            CoordinateSpaceMapping::Transform(ref transform) => {
-                transform.inverse_rect_footprint(rect)
-            }
-        }
-    }
-
-    pub fn map(&self, rect: &TypedRect<f32, F>) -> Option<TypedRect<f32, T>> {
-        match self.kind {
-            CoordinateSpaceMapping::Local => {
-                Some(TypedRect::from_untyped(&rect.to_untyped()))
-            }
-            CoordinateSpaceMapping::ScaleOffset(ref scale_offset) => {
-                Some(scale_offset.map_rect(rect))
-            }
-            CoordinateSpaceMapping::Transform(ref transform) => {
-                match project_rect(transform, rect, &self.bounds) {
-                    Some(bounds) => {
-                        Some(bounds)
-                    }
-                    None => {
-                        warn!("parent relative transform can't transform the primitive rect for {:?}", rect);
-                        None
-                    }
-                }
-            }
-        }
-    }
-
-    pub fn visible_face(&self) -> VisibleFace {
-        match self.kind {
-            CoordinateSpaceMapping::Local => VisibleFace::Front,
-            CoordinateSpaceMapping::ScaleOffset(_) => VisibleFace::Front,
-            CoordinateSpaceMapping::Transform(ref transform) => {
-                if transform.is_backface_visible() {
-                    VisibleFace::Back
-                } else {
-                    VisibleFace::Front
-                }
-            }
-        }
-    }
-}
-
-/// For external images, it's not possible to know the
-/// UV coords of the image (or the image data itself)
-/// until the render thread receives the frame and issues
-/// callbacks to the client application. For external
-/// images that are visible, a DeferredResolve is created
-/// that is stored in the frame. This allows the render
-/// thread to iterate this list and update any changed
-/// texture data and update the UV rect. Any filtering
-/// is handled externally for NativeTexture external
-/// images.
-#[cfg_attr(feature = "capture", derive(Serialize))]
-#[cfg_attr(feature = "replay", derive(Deserialize))]
-pub struct DeferredResolve {
-    pub address: GpuCacheAddress,
-    pub image_properties: ImageProperties,
-    pub rendering: ImageRendering,
-}
-
-#[derive(Debug, Copy, Clone, PartialEq)]
-pub struct ClipTaskIndex(pub u32);
-
-impl ClipTaskIndex {
-    pub const INVALID: ClipTaskIndex = ClipTaskIndex(0);
-}
-
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Ord, PartialOrd)]
-#[cfg_attr(feature = "capture", derive(Serialize))]
-#[cfg_attr(feature = "replay", derive(Deserialize))]
-pub struct PictureIndex(pub usize);
-
-impl GpuCacheHandle {
-    pub fn as_int(&self, gpu_cache: &GpuCache) -> i32 {
-        gpu_cache.get_address(self).as_int()
-    }
-}
-
-impl GpuCacheAddress {
-    pub fn as_int(&self) -> i32 {
-        // TODO(gw): Temporarily encode GPU Cache addresses as a single int.
-        //           In the future, we can change the PrimitiveInstanceData struct
-        //           to use 2x u16 for the vertex attribute instead of an i32.
-        self.v as i32 * MAX_VERTEX_TEXTURE_WIDTH as i32 + self.u as i32
-    }
-}
-
-/// The information about an interned primitive that
-/// is stored and available in the scene builder
-/// thread.
-#[cfg_attr(feature = "capture", derive(Serialize))]
-#[cfg_attr(feature = "replay", derive(Deserialize))]
-pub struct PrimitiveSceneData {
-    pub culling_rect: LayoutRect,
-    pub is_backface_visible: bool,
-}
-
-/// Information specific to a primitive type that
-/// uniquely identifies a primitive template by key.
-#[cfg_attr(feature = "capture", derive(Serialize))]
-#[cfg_attr(feature = "replay", derive(Deserialize))]
-#[derive(Debug, Clone, Eq, PartialEq, Hash)]
-pub enum PrimitiveKeyKind {
-    /// Pictures and old style (non-interned) primitives specify the
-    /// Unused primitive key kind. In the future it might make sense
-    /// to instead have Option<PrimitiveKeyKind>. It should become
-    /// clearer as we port more primitives to be interned.
-    Unused,
-    /// A run of glyphs, with associated font information.
-    TextRun {
-        font: FontInstance,
-        offset: LayoutVector2DAu,
-        glyphs: Vec<GlyphInstance>,
-        shadow: bool,
-    },
-    /// Identifying key for a line decoration.
-    LineDecoration {
-        // If the cache_key is Some(..) it is a line decoration
-        // that relies on a render task (e.g. wavy). If the
-        // cache key is None, it uses a fast path to draw the
-        // line decoration as a solid rect.
-        cache_key: Option<LineDecorationCacheKey>,
-        color: ColorU,
-    },
-    /// Clear an existing rect, used for special effects on some platforms.
-    Clear,
-    NormalBorder {
-        border: NormalBorderAu,
-        widths: LayoutSideOffsetsAu,
-    },
-    ImageBorder {
-        request: ImageRequest,
-        nine_patch: NinePatchDescriptor,
-    },
-    Rectangle {
-        color: ColorU,
-    },
-    YuvImage {
-        color_depth: ColorDepth,
-        yuv_key: [ImageKey; 3],
-        format: YuvFormat,
-        color_space: YuvColorSpace,
-        image_rendering: ImageRendering,
-    },
-    Image {
-        key: ImageKey,
-        stretch_size: SizeKey,
-        tile_spacing: SizeKey,
-        color: ColorU,
-        sub_rect: Option<DeviceIntRect>,
-        image_rendering: ImageRendering,
-        alpha_type: AlphaType,
-    },
-    LinearGradient {
-        extend_mode: ExtendMode,
-        start_point: PointKey,
-        end_point: PointKey,
-        stretch_size: SizeKey,
-        tile_spacing: SizeKey,
-        stops: Vec<GradientStopKey>,
-        reverse_stops: bool,
-        nine_patch: Option<Box<NinePatchDescriptor>>,
-    },
-    RadialGradient {
-        extend_mode: ExtendMode,
-        center: PointKey,
-        params: RadialGradientParams,
-        stretch_size: SizeKey,
-        stops: Vec<GradientStopKey>,
-        tile_spacing: SizeKey,
-        nine_patch: Option<Box<NinePatchDescriptor>>,
-    },
-}
-
-/// A hashable gradient stop that can be used in primitive keys.
-#[cfg_attr(feature = "capture", derive(Serialize))]
-#[cfg_attr(feature = "replay", derive(Deserialize))]
-#[derive(Debug, Clone, PartialEq)]
-pub struct GradientStopKey {
-    pub offset: f32,
-    pub color: ColorU,
-}
-
-impl Eq for GradientStopKey {}
-
-impl hash::Hash for GradientStopKey {
-    fn hash<H: hash::Hasher>(&self, state: &mut H) {
-        self.offset.to_bits().hash(state);
-        self.color.hash(state);
-    }
-}
-
-#[cfg_attr(feature = "capture", derive(Serialize))]
-#[cfg_attr(feature = "replay", derive(Deserialize))]
-#[derive(Debug, Clone, PartialEq)]
-pub struct RectangleKey {
-    x: f32,
-    y: f32,
-    w: f32,
-    h: f32,
-}
-
-impl Eq for RectangleKey {}
-
-impl hash::Hash for RectangleKey {
-    fn hash<H: hash::Hasher>(&self, state: &mut H) {
-        self.x.to_bits().hash(state);
-        self.y.to_bits().hash(state);
-        self.w.to_bits().hash(state);
-        self.h.to_bits().hash(state);
-    }
-}
-
-impl From<RectangleKey> for LayoutRect {
-    fn from(key: RectangleKey) -> LayoutRect {
-        LayoutRect {
-            origin: LayoutPoint::new(key.x, key.y),
-            size: LayoutSize::new(key.w, key.h),
-        }
-    }
-}
-
-impl From<LayoutRect> for RectangleKey {
-    fn from(rect: LayoutRect) -> RectangleKey {
-        RectangleKey {
-            x: rect.origin.x,
-            y: rect.origin.y,
-            w: rect.size.width,
-            h: rect.size.height,
-        }
-    }
-}
-
-/// A hashable SideOffset2D that can be used in primitive keys.
-#[cfg_attr(feature = "capture", derive(Serialize))]
-#[cfg_attr(feature = "replay", derive(Deserialize))]
-#[derive(Debug, Clone, PartialEq)]
-pub struct SideOffsetsKey {
-    pub top: f32,
-    pub right: f32,
-    pub bottom: f32,
-    pub left: f32,
-}
-
-impl Eq for SideOffsetsKey {}
-
-impl hash::Hash for SideOffsetsKey {
-    fn hash<H: hash::Hasher>(&self, state: &mut H) {
-        self.top.to_bits().hash(state);
-        self.right.to_bits().hash(state);
-        self.bottom.to_bits().hash(state);
-        self.left.to_bits().hash(state);
-    }
-}
-
-impl From<SideOffsetsKey> for LayoutSideOffsets {
-    fn from(key: SideOffsetsKey) -> LayoutSideOffsets {
-        LayoutSideOffsets::new(
-            key.top,
-            key.right,
-            key.bottom,
-            key.left,
-        )
-    }
-}
-
-impl From<LayoutSideOffsets> for SideOffsetsKey {
-    fn from(offsets: LayoutSideOffsets) -> SideOffsetsKey {
-        SideOffsetsKey {
-            top: offsets.top,
-            right: offsets.right,
-            bottom: offsets.bottom,
-            left: offsets.left,
-        }
-    }
-}
-
-impl From<SideOffsets2D<f32>> for SideOffsetsKey {
-    fn from(offsets: SideOffsets2D<f32>) -> SideOffsetsKey {
-        SideOffsetsKey {
-            top: offsets.top,
-            right: offsets.right,
-            bottom: offsets.bottom,
-            left: offsets.left,
-        }
-    }
-}
-
-/// A hashable size for using as a key during primitive interning.
-#[cfg_attr(feature = "capture", derive(Serialize))]
-#[cfg_attr(feature = "replay", derive(Deserialize))]
-#[derive(Copy, Debug, Clone, PartialEq)]
-pub struct SizeKey {
-    w: f32,
-    h: f32,
-}
-
-impl Eq for SizeKey {}
-
-impl hash::Hash for SizeKey {
-    fn hash<H: hash::Hasher>(&self, state: &mut H) {
-        self.w.to_bits().hash(state);
-        self.h.to_bits().hash(state);
-    }
-}
-
-impl From<SizeKey> for LayoutSize {
-    fn from(key: SizeKey) -> LayoutSize {
-        LayoutSize::new(key.w, key.h)
-    }
-}
-
-impl<U> From<TypedSize2D<f32, U>> for SizeKey {
-    fn from(size: TypedSize2D<f32, U>) -> SizeKey {
-        SizeKey {
-            w: size.width,
-            h: size.height,
-        }
-    }
-}
-
-impl SizeKey {
-    pub fn zero() -> SizeKey {
-        SizeKey {
-            w: 0.0,
-            h: 0.0,
-        }
-    }
-}
-
-/// Hashable radial gradient parameters, for use during prim interning.
-#[cfg_attr(feature = "capture", derive(Serialize))]
-#[cfg_attr(feature = "replay", derive(Deserialize))]
-#[derive(Debug, Clone, PartialEq)]
-pub struct RadialGradientParams {
-    pub start_radius: f32,
-    pub end_radius: f32,
-    pub ratio_xy: f32,
-}
-
-impl Eq for RadialGradientParams {}
-
-impl hash::Hash for RadialGradientParams {
-    fn hash<H: hash::Hasher>(&self, state: &mut H) {
-        self.start_radius.to_bits().hash(state);
-        self.end_radius.to_bits().hash(state);
-        self.ratio_xy.to_bits().hash(state);
-    }
-}
-
-/// A hashable point for using as a key during primitive interning.
-#[cfg_attr(feature = "capture", derive(Serialize))]
-#[cfg_attr(feature = "replay", derive(Deserialize))]
-#[derive(Debug, Clone, PartialEq)]
-pub struct PointKey {
-    x: f32,
-    y: f32,
-}
-
-impl Eq for PointKey {}
-
-impl hash::Hash for PointKey {
-    fn hash<H: hash::Hasher>(&self, state: &mut H) {
-        self.x.to_bits().hash(state);
-        self.y.to_bits().hash(state);
-    }
-}
-
-impl From<PointKey> for LayoutPoint {
-    fn from(key: PointKey) -> LayoutPoint {
-        LayoutPoint::new(key.x, key.y)
-    }
-}
-
-impl From<LayoutPoint> for PointKey {
-    fn from(p: LayoutPoint) -> PointKey {
-        PointKey {
-            x: p.x,
-            y: p.y,
-        }
-    }
-}
-
-#[cfg_attr(feature = "capture", derive(Serialize))]
-#[cfg_attr(feature = "replay", derive(Deserialize))]
-#[derive(Debug, Clone, Eq, PartialEq, Hash)]
-pub struct PrimitiveKey {
-    pub is_backface_visible: bool,
-    pub prim_rect: RectangleKey,
-    pub clip_rect: RectangleKey,
-    pub kind: PrimitiveKeyKind,
-}
-
-impl PrimitiveKey {
-    pub fn new(
-        is_backface_visible: bool,
-        prim_rect: LayoutRect,
-        clip_rect: LayoutRect,
-        kind: PrimitiveKeyKind,
-    ) -> Self {
-        PrimitiveKey {
-            is_backface_visible,
-            prim_rect: prim_rect.into(),
-            clip_rect: clip_rect.into(),
-            kind,
-        }
-    }
-
-    /// Construct a primitive instance that matches the type
-    /// of primitive key.
-    pub fn to_instance_kind(
-        &self,
-        prim_store: &mut PrimitiveStore,
-    ) -> PrimitiveInstanceKind {
-        match self.kind {
-            PrimitiveKeyKind::LineDecoration { .. } => {
-                PrimitiveInstanceKind::LineDecoration {
-                    cache_handle: None,
-                }
-            }
-            PrimitiveKeyKind::TextRun { ref font, shadow, .. } => {
-                let run_index = prim_store.text_runs.push(TextRunPrimitive {
-                    used_font: font.clone(),
-                    glyph_keys_range: storage::Range::empty(),
-                    shadow,
-                });
-
-                PrimitiveInstanceKind::TextRun {
-                    run_index
-                }
-            }
-            PrimitiveKeyKind::Clear => {
-                PrimitiveInstanceKind::Clear
-            }
-            PrimitiveKeyKind::NormalBorder { .. } => {
-                PrimitiveInstanceKind::NormalBorder {
-                    cache_handles: storage::Range::empty(),
-                }
-            }
-            PrimitiveKeyKind::ImageBorder { .. } => {
-                PrimitiveInstanceKind::ImageBorder {
-                }
-            }
-            PrimitiveKeyKind::Rectangle { .. } => {
-                PrimitiveInstanceKind::Rectangle {
-                    opacity_binding_index: OpacityBindingIndex::INVALID,
-                    segment_instance_index: SegmentInstanceIndex::INVALID,
-                }
-            }
-            PrimitiveKeyKind::YuvImage { .. } => {
-                PrimitiveInstanceKind::YuvImage {
-                    segment_instance_index: SegmentInstanceIndex::INVALID,
-                }
-            }
-            PrimitiveKeyKind::Image { .. } => {
-                // TODO(gw): Refactor this to not need a separate image
-                //           instance (see ImageInstance struct).
-                let image_instance_index = prim_store.images.push(ImageInstance {
-                    opacity_binding_index: OpacityBindingIndex::INVALID,
-                    segment_instance_index: SegmentInstanceIndex::INVALID,
-                    visible_tiles: Vec::new(),
-                });
-
-                PrimitiveInstanceKind::Image {
-                    image_instance_index,
-                }
-            }
-            PrimitiveKeyKind::LinearGradient { .. } => {
-                PrimitiveInstanceKind::LinearGradient {
-                    visible_tiles_range: GradientTileRange::empty(),
-                }
-            }
-            PrimitiveKeyKind::RadialGradient { .. } => {
-                PrimitiveInstanceKind::RadialGradient {
-                    visible_tiles_range: GradientTileRange::empty(),
-                }
-            }
-            PrimitiveKeyKind::Unused => {
-                // Should never be hit as this method should not be
-                // called for old style primitives.
-                unreachable!();
-            }
-        }
-    }
-}
-
-#[cfg_attr(feature = "capture", derive(Serialize))]
-#[cfg_attr(feature = "replay", derive(Deserialize))]
-pub struct NormalBorderTemplate {
-    pub brush_segments: Vec<BrushSegment>,
-    pub border_segments: Vec<BorderSegmentInfo>,
-    pub border: NormalBorder,
-    pub widths: LayoutSideOffsets,
-}
-
-/// The shared information for a given primitive. This is interned and retained
-/// both across frames and display lists, by comparing the matching PrimitiveKey.
-#[cfg_attr(feature = "capture", derive(Serialize))]
-#[cfg_attr(feature = "replay", derive(Deserialize))]
-pub enum PrimitiveTemplateKind {
-    LineDecoration {
-        cache_key: Option<LineDecorationCacheKey>,
-        color: ColorF,
-    },
-    TextRun {
-        font: FontInstance,
-        offset: LayoutVector2DAu,
-        glyphs: Vec<GlyphInstance>,
-    },
-    NormalBorder {
-        template: Box<NormalBorderTemplate>,
-    },
-    ImageBorder {
-        request: ImageRequest,
-        brush_segments: Vec<BrushSegment>,
-    },
-    Rectangle {
-        color: ColorF,
-    },
-    YuvImage {
-        color_depth: ColorDepth,
-        yuv_key: [ImageKey; 3],
-        format: YuvFormat,
-        color_space: YuvColorSpace,
-        image_rendering: ImageRendering,
-    },
-    Image {
-        key: ImageKey,
-        stretch_size: LayoutSize,
-        tile_spacing: LayoutSize,
-        color: ColorF,
-        source: ImageSource,
-        image_rendering: ImageRendering,
-        sub_rect: Option<DeviceIntRect>,
-        alpha_type: AlphaType,
-    },
-    LinearGradient {
-        extend_mode: ExtendMode,
-        start_point: LayoutPoint,
-        end_point: LayoutPoint,
-        stretch_size: LayoutSize,
-        tile_spacing: LayoutSize,
-        stops_opacity: PrimitiveOpacity,
-        stops: Vec<GradientStop>,
-        brush_segments: Vec<BrushSegment>,
-        reverse_stops: bool,
-        stops_handle: GpuCacheHandle,
-    },
-    RadialGradient {
-        extend_mode: ExtendMode,
-        center: LayoutPoint,
-        params: RadialGradientParams,
-        stretch_size: LayoutSize,
-        tile_spacing: LayoutSize,
-        brush_segments: Vec<BrushSegment>,
-        stops: Vec<GradientStop>,
-        stops_handle: GpuCacheHandle,
-    },
-    Clear,
-    Unused,
-}
-
-/// Construct the primitive template data from a primitive key. This
-/// is invoked when a primitive key is created and the interner
-/// doesn't currently contain a primitive with this key.
-impl PrimitiveKeyKind {
-    fn into_template(
-        self,
-        rect: &LayoutRect,
-    ) -> PrimitiveTemplateKind {
-        match self {
-            PrimitiveKeyKind::Unused => PrimitiveTemplateKind::Unused,
-            PrimitiveKeyKind::TextRun { glyphs, font, offset, .. } => {
-                PrimitiveTemplateKind::TextRun {
-                    font,
-                    offset,
-                    glyphs,
-                }
-            }
-            PrimitiveKeyKind::Clear => {
-                PrimitiveTemplateKind::Clear
-            }
-            PrimitiveKeyKind::NormalBorder { widths, border, .. } => {
-                let mut border: NormalBorder = border.into();
-                let widths = LayoutSideOffsets::from_au(widths);
-
-                // FIXME(emilio): Is this the best place to do this?
-                border.normalize(&widths);
-
-                let mut brush_segments = Vec::new();
-                let mut border_segments = Vec::new();
-
-                create_border_segments(
-                    rect.size,
-                    &border,
-                    &widths,
-                    &mut border_segments,
-                    &mut brush_segments,
-                );
-
-                PrimitiveTemplateKind::NormalBorder {
-                    template: Box::new(NormalBorderTemplate {
-                        border,
-                        widths,
-                        border_segments,
-                        brush_segments,
-                    })
-                }
-            }
-            PrimitiveKeyKind::ImageBorder {
-                request,
-                ref nine_patch,
-                ..
-            } => {
-                let brush_segments = nine_patch.create_segments(rect.size);
-
-                PrimitiveTemplateKind::ImageBorder {
-                    request,
-                    brush_segments,
-                }
-            }
-            PrimitiveKeyKind::Rectangle { color, .. } => {
-                PrimitiveTemplateKind::Rectangle {
-                    color: color.into(),
-                }
-            }
-            PrimitiveKeyKind::YuvImage { color_depth, yuv_key, format, color_space, image_rendering, .. } => {
-                PrimitiveTemplateKind::YuvImage {
-                    color_depth,
-                    yuv_key,
-                    format,
-                    color_space,
-                    image_rendering,
-                }
-            }
-            PrimitiveKeyKind::Image { alpha_type, key, color, stretch_size, tile_spacing, image_rendering, sub_rect, .. } => {
-                PrimitiveTemplateKind::Image {
-                    key,
-                    color: color.into(),
-                    stretch_size: stretch_size.into(),
-                    tile_spacing: tile_spacing.into(),
-                    source: ImageSource::Default,
-                    sub_rect,
-                    image_rendering,
-                    alpha_type,
-                }
-            }
-            PrimitiveKeyKind::LineDecoration { cache_key, color } => {
-                PrimitiveTemplateKind::LineDecoration {
-                    cache_key,
-                    color: color.into(),
-                }
-            }
-            PrimitiveKeyKind::LinearGradient {
-                extend_mode,
-                tile_spacing,
-                start_point,
-                end_point,
-                stretch_size,
-                stops,
-                reverse_stops,
-                nine_patch,
-                ..
-            } => {
-                let mut min_alpha: f32 = 1.0;
-
-                // Convert the stops to more convenient representation
-                // for the current gradient builder.
-                let stops = stops.iter().map(|stop| {
-                    let color: ColorF = stop.color.into();
-                    min_alpha = min_alpha.min(color.a);
-
-                    GradientStop {
-                        offset: stop.offset,
-                        color,
-                    }
-                }).collect();
-
-                let mut brush_segments = Vec::new();
-
-                if let Some(ref nine_patch) = nine_patch {
-                    brush_segments = nine_patch.create_segments(rect.size);
-                }
-
-                // Save opacity of the stops for use in
-                // selecting which pass this gradient
-                // should be drawn in.
-                let stops_opacity = PrimitiveOpacity::from_alpha(min_alpha);
-
-                PrimitiveTemplateKind::LinearGradient {
-                    extend_mode,
-                    start_point: start_point.into(),
-                    end_point: end_point.into(),
-                    stretch_size: stretch_size.into(),
-                    tile_spacing: tile_spacing.into(),
-                    stops_opacity,
-                    stops,
-                    brush_segments,
-                    reverse_stops,
-                    stops_handle: GpuCacheHandle::new(),
-                }
-            }
-            PrimitiveKeyKind::RadialGradient {
-                extend_mode,
-                params,
-                stretch_size,
-                tile_spacing,
-                nine_patch,
-                center,
-                stops,
-                ..
-            } => {
-                let mut brush_segments = Vec::new();
-
-                if let Some(ref nine_patch) = nine_patch {
-                    brush_segments = nine_patch.create_segments(rect.size);
-                }
-
-                let stops = stops.iter().map(|stop| {
-                    GradientStop {
-                        offset: stop.offset,
-                        color: stop.color.into(),
-                    }
-                }).collect();
-
-                PrimitiveTemplateKind::RadialGradient {
-                    center: center.into(),
-                    extend_mode,
-                    params,
-                    stretch_size: stretch_size.into(),
-                    tile_spacing: tile_spacing.into(),
-                    brush_segments,
-                    stops_handle: GpuCacheHandle::new(),
-                    stops,
-                }
-            }
-        }
-    }
-}
-
-#[cfg_attr(feature = "capture", derive(Serialize))]
-#[cfg_attr(feature = "replay", derive(Deserialize))]
-pub struct PrimitiveTemplate {
-    pub is_backface_visible: bool,
-    pub prim_rect: LayoutRect,
-    pub clip_rect: LayoutRect,
-    pub kind: PrimitiveTemplateKind,
-    pub opacity: PrimitiveOpacity,
-    /// The GPU cache handle for a primitive template. Since this structure
-    /// is retained across display lists by interning, this GPU cache handle
-    /// also remains valid, which reduces the number of updates to the GPU
-    /// cache when a new display list is processed.
-    pub gpu_cache_handle: GpuCacheHandle,
-}
-
-impl From<PrimitiveKey> for PrimitiveTemplate {
-    fn from(item: PrimitiveKey) -> Self {
-        let prim_rect = item.prim_rect.into();
-        let clip_rect = item.clip_rect.into();
-        let kind = item.kind.into_template(&prim_rect);
-
-        PrimitiveTemplate {
-            is_backface_visible: item.is_backface_visible,
-            prim_rect,
-            clip_rect,
-            kind,
-            gpu_cache_handle: GpuCacheHandle::new(),
-            opacity: PrimitiveOpacity::translucent(),
-        }
-    }
-}
-
-impl PrimitiveTemplateKind {
-    /// Write any GPU blocks for the primitive template to the given request object.
-    fn write_prim_gpu_blocks(
-        &self,
-        request: &mut GpuDataRequest,
-        prim_size: LayoutSize,
-    ) {
-        match *self {
-            PrimitiveTemplateKind::Clear => {
-                // Opaque black with operator dest out
-                request.push(PremultipliedColorF::BLACK);
-            }
-            PrimitiveTemplateKind::Rectangle { ref color, .. } => {
-                request.push(color.premultiplied());
-            }
-            PrimitiveTemplateKind::NormalBorder { .. } => {
-                // Border primitives currently used for
-                // image borders, and run through the
-                // normal brush_image shader.
-                request.push(PremultipliedColorF::WHITE);
-                request.push(PremultipliedColorF::WHITE);
-                request.push([
-                    prim_size.width,
-                    prim_size.height,
-                    0.0,
-                    0.0,
-                ]);
-            }
-            PrimitiveTemplateKind::ImageBorder { .. } => {
-                // Border primitives currently used for
-                // image borders, and run through the
-                // normal brush_image shader.
-                request.push(PremultipliedColorF::WHITE);
-                request.push(PremultipliedColorF::WHITE);
-                request.push([
-                    prim_size.width,
-                    prim_size.height,
-                    0.0,
-                    0.0,
-                ]);
-            }
-            PrimitiveTemplateKind::LineDecoration { ref cache_key, ref color } => {
-                match cache_key {
-                    Some(cache_key) => {
-                        request.push(color.premultiplied());
-                        request.push(PremultipliedColorF::WHITE);
-                        request.push([
-                            cache_key.size.width.to_f32_px(),
-                            cache_key.size.height.to_f32_px(),
-                            0.0,
-                            0.0,
-                        ]);
-                    }
-                    None => {
-                        request.push(color.premultiplied());
-                    }
-                }
-            }
-            PrimitiveTemplateKind::TextRun { ref glyphs, ref font, ref offset, .. } => {
-                request.push(ColorF::from(font.color).premultiplied());
-                // this is the only case where we need to provide plain color to GPU
-                let bg_color = ColorF::from(font.bg_color);
-                request.push([bg_color.r, bg_color.g, bg_color.b, 1.0]);
-                request.push([
-                    offset.x.to_f32_px(),
-                    offset.y.to_f32_px(),
-                    0.0,
-                    0.0,
-                ]);
-
-                let mut gpu_block = [0.0; 4];
-                for (i, src) in glyphs.iter().enumerate() {
-                    // Two glyphs are packed per GPU block.
-
-                    if (i & 1) == 0 {
-                        gpu_block[0] = src.point.x;
-                        gpu_block[1] = src.point.y;
-                    } else {
-                        gpu_block[2] = src.point.x;
-                        gpu_block[3] = src.point.y;
-                        request.push(gpu_block);
-                    }
-                }
-
-                // Ensure the last block is added in the case
-                // of an odd number of glyphs.
-                if (glyphs.len() & 1) != 0 {
-                    request.push(gpu_block);
-                }
-
-                assert!(request.current_used_block_num() <= MAX_VERTEX_TEXTURE_WIDTH);
-            }
-            PrimitiveTemplateKind::YuvImage { color_depth, .. } => {
-                request.push([
-                    color_depth.rescaling_factor(),
-                    0.0,
-                    0.0,
-                    0.0
-                ]);
-            }
-            PrimitiveTemplateKind::Image { stretch_size, tile_spacing, color, .. } => {
-                // Images are drawn as a white color, modulated by the total
-                // opacity coming from any collapsed property bindings.
-                request.push(color.premultiplied());
-                request.push(PremultipliedColorF::WHITE);
-                request.push([
-                    stretch_size.width + tile_spacing.width,
-                    stretch_size.height + tile_spacing.height,
-                    0.0,
-                    0.0,
-                ]);
-            }
-            PrimitiveTemplateKind::LinearGradient { stretch_size, start_point, end_point, extend_mode, .. } => {
-                request.push([
-                    start_point.x,
-                    start_point.y,
-                    end_point.x,
-                    end_point.y,
-                ]);
-                request.push([
-                    pack_as_float(extend_mode as u32),
-                    stretch_size.width,
-                    stretch_size.height,
-                    0.0,
-                ]);
-            }
-            PrimitiveTemplateKind::RadialGradient {
-                center,
-                ref params,
-                extend_mode,
-                stretch_size,
-                ..
-            } => {
-                request.push([
-                    center.x,
-                    center.y,
-                    params.start_radius,
-                    params.end_radius,
-                ]);
-                request.push([
-                    params.ratio_xy,
-                    pack_as_float(extend_mode as u32),
-                    stretch_size.width,
-                    stretch_size.height,
-                ]);
-            }
-            PrimitiveTemplateKind::Unused => {}
-        }
-    }
-
-    fn write_segment_gpu_blocks(
-        &self,
-        request: &mut GpuDataRequest,
-    ) {
-        match *self {
-            PrimitiveTemplateKind::NormalBorder { ref template, .. } => {
-                for segment in &template.brush_segments {
-                    // has to match VECS_PER_SEGMENT
-                    request.write_segment(
-                        segment.local_rect,
-                        segment.extra_data,
-                    );
-                }
-            }
-            PrimitiveTemplateKind::ImageBorder { ref brush_segments, .. } => {
-                for segment in brush_segments {
-                    // has to match VECS_PER_SEGMENT
-                    request.write_segment(
-                        segment.local_rect,
-                        segment.extra_data,
-                    );
-                }
-            }
-            PrimitiveTemplateKind::LinearGradient { ref brush_segments, .. } |
-            PrimitiveTemplateKind::RadialGradient { ref brush_segments, .. } => {
-                for segment in brush_segments {
-                    // has to match VECS_PER_SEGMENT
-                    request.write_segment(
-                        segment.local_rect,
-                        segment.extra_data,
-                    );
-                }
-            }
-            PrimitiveTemplateKind::Clear |
-            PrimitiveTemplateKind::LineDecoration { .. } |
-            PrimitiveTemplateKind::Image { .. } |
-            PrimitiveTemplateKind::Rectangle { .. } |
-            PrimitiveTemplateKind::TextRun { .. } |
-            PrimitiveTemplateKind::YuvImage { .. } |
-            PrimitiveTemplateKind::Unused => {}
-        }
-    }
-}
-
-impl PrimitiveTemplate {
-    /// Update the GPU cache for a given primitive template. This may be called multiple
-    /// times per frame, by each primitive reference that refers to this interned
-    /// template. The initial request call to the GPU cache ensures that work is only
-    /// done if the cache entry is invalid (due to first use or eviction).
-    pub fn update(
-        &mut self,
-        // TODO(gw): Passing in surface_index here is not ideal. The primitive template
-        //           code shouldn't depend on current surface state. This is due to a
-        //           limitation in how render task caching works. We should fix this by
-        //           allowing render task caching to assign to surfaces implicitly
-        //           during pass allocation.
-        surface_index: SurfaceIndex,
-        frame_state: &mut FrameBuildingState,
-    ) {
-        if let Some(mut request) = frame_state.gpu_cache.request(&mut self.gpu_cache_handle) {
-            self.kind.write_prim_gpu_blocks(
-                &mut request,
-                self.prim_rect.size,
-            );
-            self.kind.write_segment_gpu_blocks(&mut request);
-        }
-
-        self.opacity = match self.kind {
-            PrimitiveTemplateKind::Clear => {
-                PrimitiveOpacity::translucent()
-            }
-            PrimitiveTemplateKind::Rectangle { ref color, .. } => {
-                PrimitiveOpacity::from_alpha(color.a)
-            }
-            PrimitiveTemplateKind::NormalBorder { .. } => {
-                // Shouldn't matter, since the segment opacity is used instead
-                PrimitiveOpacity::translucent()
-            }
-            PrimitiveTemplateKind::LinearGradient {
-                stretch_size,
-                tile_spacing,
-                stops_opacity,
-                ref mut stops_handle,
-                reverse_stops,
-                ref stops,
-                ..
-            } => {
-                if let Some(mut request) = frame_state.gpu_cache.request(stops_handle) {
-                    GradientGpuBlockBuilder::build(
-                        reverse_stops,
-                        &mut request,
-                        stops,
-                    );
-                }
-
-                // If the coverage of the gradient extends to or beyond
-                // the primitive rect, then the opacity can be determined
-                // by the colors of the stops. If we have tiling / spacing
-                // then we just assume the gradient is translucent for now.
-                // (In the future we could consider segmenting in some cases).
-                let stride = stretch_size + tile_spacing;
-                if stride.width >= self.prim_rect.size.width &&
-                   stride.height >= self.prim_rect.size.height {
-                    stops_opacity
-                } else {
-                    PrimitiveOpacity::translucent()
-                }
-            }
-            PrimitiveTemplateKind::RadialGradient { ref mut stops_handle, ref stops, .. } => {
-                if let Some(mut request) = frame_state.gpu_cache.request(stops_handle) {
-                    GradientGpuBlockBuilder::build(
-                        false,
-                        &mut request,
-                        stops,
-                    );
-                }
-
-                //TODO: can we make it opaque in some cases?
-                PrimitiveOpacity::translucent()
-            }
-            PrimitiveTemplateKind::ImageBorder { request, .. } => {
-                let image_properties = frame_state
-                    .resource_cache
-                    .get_image_properties(request.key);
-
-                if let Some(image_properties) = image_properties {
-                    frame_state.resource_cache.request_image(
-                        request,
-                        frame_state.gpu_cache,
-                    );
-                    PrimitiveOpacity {
-                        is_opaque: image_properties.descriptor.is_opaque,
-                    }
-                } else {
-                    PrimitiveOpacity::opaque()
-                }
-            }
-            PrimitiveTemplateKind::LineDecoration { ref cache_key, ref color } => {
-                match cache_key {
-                    Some(..) => PrimitiveOpacity::translucent(),
-                    None => PrimitiveOpacity::from_alpha(color.a),
-                }
-            }
-            PrimitiveTemplateKind::TextRun { .. } => {
-                PrimitiveOpacity::translucent()
-            }
-            PrimitiveTemplateKind::YuvImage { format, yuv_key, image_rendering, .. } => {
-                let channel_num = format.get_plane_num();
-                debug_assert!(channel_num <= 3);
-                for channel in 0 .. channel_num {
-                    frame_state.resource_cache.request_image(
-                        ImageRequest {
-                            key: yuv_key[channel],
-                            rendering: image_rendering,
-                            tile: None,
-                        },
-                        frame_state.gpu_cache,
-                    );
-                }
-
-                PrimitiveOpacity::translucent()
-            }
-            PrimitiveTemplateKind::Image { key, stretch_size, ref color, tile_spacing, ref mut source, sub_rect, image_rendering, .. } => {
-                let image_properties = frame_state
-                    .resource_cache
-                    .get_image_properties(key);
-
-                match image_properties {
-                    Some(image_properties) => {
-                        let is_tiled = image_properties.tiling.is_some();
-
-                        if tile_spacing != LayoutSize::zero() && !is_tiled {
-                            *source = ImageSource::Cache {
-                                // Size in device-pixels we need to allocate in render task cache.
-                                size: image_properties.descriptor.size.to_i32(),
-                                handle: None,
-                            };
-                        }
-
-                        // Work out whether this image is a normal / simple type, or if
-                        // we need to pre-render it to the render task cache.
-                        if let Some(rect) = sub_rect {
-                            // We don't properly support this right now.
-                            debug_assert!(!is_tiled);
-                            *source = ImageSource::Cache {
-                                // Size in device-pixels we need to allocate in render task cache.
-                                size: rect.size,
-                                handle: None,
-                            };
-                        }
-
-                        let mut request_source_image = false;
-                        let mut is_opaque = image_properties.descriptor.is_opaque;
-                        let request = ImageRequest {
-                            key,
-                            rendering: image_rendering,
-                            tile: None,
-                        };
-
-                        // Every frame, for cached items, we need to request the render
-                        // task cache item. The closure will be invoked on the first
-                        // time through, and any time the render task output has been
-                        // evicted from the texture cache.
-                        match *source {
-                            ImageSource::Cache { ref mut size, ref mut handle } => {
-                                let padding = DeviceIntSideOffsets::new(
-                                    0,
-                                    (tile_spacing.width * size.width as f32 / stretch_size.width) as i32,
-                                    (tile_spacing.height * size.height as f32 / stretch_size.height) as i32,
-                                    0,
-                                );
-
-                                let inner_size = *size;
-                                size.width += padding.horizontal();
-                                size.height += padding.vertical();
-
-                                is_opaque &= padding == DeviceIntSideOffsets::zero();
-
-                                let image_cache_key = ImageCacheKey {
-                                    request,
-                                    texel_rect: sub_rect,
-                                };
-                                let surfaces = &mut frame_state.surfaces;
-
-                                // Request a pre-rendered image task.
-                                *handle = Some(frame_state.resource_cache.request_render_task(
-                                    RenderTaskCacheKey {
-                                        size: *size,
-                                        kind: RenderTaskCacheKeyKind::Image(image_cache_key),
-                                    },
-                                    frame_state.gpu_cache,
-                                    frame_state.render_tasks,
-                                    None,
-                                    image_properties.descriptor.is_opaque,
-                                    |render_tasks| {
-                                        // We need to render the image cache this frame,
-                                        // so will need access to the source texture.
-                                        request_source_image = true;
-
-                                        // Create a task to blit from the texture cache to
-                                        // a normal transient render task surface. This will
-                                        // copy only the sub-rect, if specified.
-                                        let cache_to_target_task = RenderTask::new_blit_with_padding(
-                                            inner_size,
-                                            &padding,
-                                            BlitSource::Image { key: image_cache_key },
-                                        );
-                                        let cache_to_target_task_id = render_tasks.add(cache_to_target_task);
-
-                                        // Create a task to blit the rect from the child render
-                                        // task above back into the right spot in the persistent
-                                        // render target cache.
-                                        let target_to_cache_task = RenderTask::new_blit(
-                                            *size,
-                                            BlitSource::RenderTask {
-                                                task_id: cache_to_target_task_id,
-                                            },
-                                        );
-                                        let target_to_cache_task_id = render_tasks.add(target_to_cache_task);
-
-                                        // Hook this into the render task tree at the right spot.
-                                        surfaces[surface_index.0].tasks.push(target_to_cache_task_id);
-
-                                        // Pass the image opacity, so that the cached render task
-                                        // item inherits the same opacity properties.
-                                        target_to_cache_task_id
-                                    }
-                                ));
-                            }
-                            ImageSource::Default => {
-                                // Normal images just reference the source texture each frame.
-                                request_source_image = true;
-                            }
-                        }
-
-                        if request_source_image && !is_tiled {
-                            frame_state.resource_cache.request_image(
-                                request,
-                                frame_state.gpu_cache,
-                            );
-                        }
-
-                        if is_opaque {
-                            PrimitiveOpacity::from_alpha(color.a)
-                        } else {
-                            PrimitiveOpacity::translucent()
-                        }
-                    }
-                    None => {
-                        PrimitiveOpacity::opaque()
-                    }
-                }
-            }
-            PrimitiveTemplateKind::Unused => {
-                PrimitiveOpacity::translucent()
-            }
-        };
-    }
-}
-
-// Type definitions for interning primitives.
-#[cfg_attr(feature = "capture", derive(Serialize))]
-#[cfg_attr(feature = "replay", derive(Deserialize))]
-#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq)]
-pub struct PrimitiveDataMarker;
-
-pub type PrimitiveDataStore = intern::DataStore<PrimitiveKey, PrimitiveTemplate, PrimitiveDataMarker>;
-pub type PrimitiveDataHandle = intern::Handle<PrimitiveDataMarker>;
-pub type PrimitiveDataUpdateList = intern::UpdateList<PrimitiveKey>;
-pub type PrimitiveDataInterner = intern::Interner<PrimitiveKey, PrimitiveSceneData, PrimitiveDataMarker>;
-pub type PrimitiveUid = intern::ItemUid<PrimitiveDataMarker>;
-
-// Maintains a list of opacity bindings that have been collapsed into
-// the color of a single primitive. This is an important optimization
-// that avoids allocating an intermediate surface for most common
-// uses of opacity filters.
-#[derive(Debug)]
-pub struct OpacityBinding {
-    pub bindings: Vec<PropertyBinding<f32>>,
-    pub current: f32,
-}
-
-impl OpacityBinding {
-    pub fn new() -> OpacityBinding {
-        OpacityBinding {
-            bindings: Vec::new(),
-            current: 1.0,
-        }
-    }
-
-    // Add a new opacity value / binding to the list
-    pub fn push(&mut self, binding: PropertyBinding<f32>) {
-        self.bindings.push(binding);
-    }
-
-    // Resolve the current value of each opacity binding, and
-    // store that as a single combined opacity. Returns true
-    // if the opacity value changed from last time.
-    pub fn update(&mut self, scene_properties: &SceneProperties) {
-        let mut new_opacity = 1.0;
-
-        for binding in &self.bindings {
-            let opacity = scene_properties.resolve_float(binding);
-            new_opacity = new_opacity * opacity;
-        }
-
-        self.current = new_opacity;
-    }
-}
-
-#[derive(Debug)]
-#[cfg_attr(feature = "capture", derive(Serialize))]
-#[cfg_attr(feature = "replay", derive(Deserialize))]
-pub struct VisibleImageTile {
-    pub tile_offset: TileOffset,
-    pub handle: GpuCacheHandle,
-    pub edge_flags: EdgeAaSegmentMask,
-    pub local_rect: LayoutRect,
-    pub local_clip_rect: LayoutRect,
-}
-
-#[derive(Debug)]
-#[cfg_attr(feature = "capture", derive(Serialize))]
-#[cfg_attr(feature = "replay", derive(Deserialize))]
-pub struct VisibleMaskImageTile {
-    pub tile_offset: TileOffset,
-    pub tile_rect: LayoutRect,
-}
-
-#[derive(Debug)]
-pub struct VisibleGradientTile {
-    pub handle: GpuCacheHandle,
-    pub local_rect: LayoutRect,
-    pub local_clip_rect: LayoutRect,
-}
-
-/// Information about how to cache a border segment,
-/// along with the current render task cache entry.
-#[cfg_attr(feature = "capture", derive(Serialize))]
-#[cfg_attr(feature = "replay", derive(Deserialize))]
-#[derive(Debug)]
-pub struct BorderSegmentInfo {
-    pub local_task_size: LayoutSize,
-    pub cache_key: BorderSegmentCacheKey,
-}
-
-bitflags! {
-    /// Each bit of the edge AA mask is:
-    /// 0, when the edge of the primitive needs to be considered for AA
-    /// 1, when the edge of the segment needs to be considered for AA
-    ///
-    /// *Note*: the bit values have to match the shader logic in
-    /// `write_transform_vertex()` function.
-    #[cfg_attr(feature = "capture", derive(Serialize))]
-    #[cfg_attr(feature = "replay", derive(Deserialize))]
-    pub struct EdgeAaSegmentMask: u8 {
-        const LEFT = 0x1;
-        const TOP = 0x2;
-        const RIGHT = 0x4;
-        const BOTTOM = 0x8;
-    }
-}
-
-/// Represents the visibility state of a segment (wrt clip masks).
-#[derive(Debug, Clone)]
-pub enum ClipMaskKind {
-    /// The segment has a clip mask, specified by the render task.
-    Mask(RenderTaskId),
-    /// The segment has no clip mask.
-    None,
-    /// The segment is made invisible / clipped completely.
-    Clipped,
-}
-
-#[cfg_attr(feature = "capture", derive(Serialize))]
-#[cfg_attr(feature = "replay", derive(Deserialize))]
-#[derive(Debug, Clone)]
-pub struct BrushSegment {
-    pub local_rect: LayoutRect,
-    pub may_need_clip_mask: bool,
-    pub edge_flags: EdgeAaSegmentMask,
-    pub extra_data: [f32; 4],
-    pub brush_flags: BrushFlags,
-}
-
-impl BrushSegment {
-    pub fn new(
-        local_rect: LayoutRect,
-        may_need_clip_mask: bool,
-        edge_flags: EdgeAaSegmentMask,
-        extra_data: [f32; 4],
-        brush_flags: BrushFlags,
-    ) -> Self {
-        Self {
-            local_rect,
-            may_need_clip_mask,
-            edge_flags,
-            extra_data,
-            brush_flags,
-        }
-    }
-
-    /// Write out to the clip mask instances array the correct clip mask
-    /// config for this segment.
-    pub fn update_clip_task(
-        &self,
-        clip_chain: Option<&ClipChainInstance>,
-        prim_bounding_rect: WorldRect,
-        root_spatial_node_index: SpatialNodeIndex,
-        surface_index: SurfaceIndex,
-        pic_state: &mut PictureState,
-        frame_context: &FrameBuildingContext,
-        frame_state: &mut FrameBuildingState,
-        clip_data_store: &mut ClipDataStore,
-    ) -> ClipMaskKind {
-        match clip_chain {
-            Some(clip_chain) => {
-                if !clip_chain.needs_mask ||
-                   (!self.may_need_clip_mask && !clip_chain.has_non_local_clips) {
-                    return ClipMaskKind::None;
-                }
-
-                let (device_rect, _) = match get_raster_rects(
-                    clip_chain.pic_clip_rect,
-                    &pic_state.map_pic_to_raster,
-                    &pic_state.map_raster_to_world,
-                    prim_bounding_rect,
-                    frame_context.device_pixel_scale,
-                ) {
-                    Some(info) => info,
-                    None => {
-                        return ClipMaskKind::Clipped;
-                    }
-                };
-
-                let clip_task = RenderTask::new_mask(
-                    device_rect.to_i32(),
-                    clip_chain.clips_range,
-                    root_spatial_node_index,
-                    frame_state.clip_store,
-                    frame_state.gpu_cache,
-                    frame_state.resource_cache,
-                    frame_state.render_tasks,
-                    clip_data_store,
-                );
-
-                let clip_task_id = frame_state.render_tasks.add(clip_task);
-                frame_state.surfaces[surface_index.0].tasks.push(clip_task_id);
-                ClipMaskKind::Mask(clip_task_id)
-            }
-            None => {
-                ClipMaskKind::Clipped
-            }
-        }
-    }
-}
-
-// Key that identifies a unique (partial) image that is being
-// stored in the render task cache.
-#[derive(Debug, Copy, Clone, Eq, Hash, PartialEq)]
-#[cfg_attr(feature = "capture", derive(Serialize))]
-#[cfg_attr(feature = "replay", derive(Deserialize))]
-pub struct ImageCacheKey {
-    pub request: ImageRequest,
-    pub texel_rect: Option<DeviceIntRect>,
-}
-
-#[derive(Clone, Debug, Hash, PartialEq, Eq)]
-#[cfg_attr(feature = "capture", derive(Serialize))]
-#[cfg_attr(feature = "replay", derive(Deserialize))]
-pub struct LineDecorationCacheKey {
-    pub style: LineStyle,
-    pub orientation: LineOrientation,
-    pub wavy_line_thickness: Au,
-    pub size: LayoutSizeAu,
-}
-
-// Where to find the texture data for an image primitive.
-#[cfg_attr(feature = "capture", derive(Serialize))]
-#[cfg_attr(feature = "replay", derive(Deserialize))]
-#[derive(Debug)]
-pub enum ImageSource {
-    // A normal image - just reference the texture cache.
-    Default,
-    // An image that is pre-rendered into the texture cache
-    // via a render task.
-    Cache {
-        size: DeviceIntSize,
-        handle: Option<RenderTaskCacheEntryHandle>,
-    },
-}
-
-// The gradient entry index for the first color stop
-pub const GRADIENT_DATA_FIRST_STOP: usize = 0;
-// The gradient entry index for the last color stop
-pub const GRADIENT_DATA_LAST_STOP: usize = GRADIENT_DATA_SIZE - 1;
-
-// The start of the gradient data table
-pub const GRADIENT_DATA_TABLE_BEGIN: usize = GRADIENT_DATA_FIRST_STOP + 1;
-// The exclusive bound of the gradient data table
-pub const GRADIENT_DATA_TABLE_END: usize = GRADIENT_DATA_LAST_STOP;
-// The number of entries in the gradient data table.
-pub const GRADIENT_DATA_TABLE_SIZE: usize = 128;
-
-// The number of entries in a gradient data: GRADIENT_DATA_TABLE_SIZE + first stop entry + last stop entry
-pub const GRADIENT_DATA_SIZE: usize = GRADIENT_DATA_TABLE_SIZE + 2;
-
-#[derive(Debug)]
-#[repr(C)]
-// An entry in a gradient data table representing a segment of the gradient color space.
-pub struct GradientDataEntry {
-    pub start_color: PremultipliedColorF,
-    pub end_color: PremultipliedColorF,
-}
-
-// TODO(gw): Tidy this up to be a free function / module?
-struct GradientGpuBlockBuilder {}
-
-impl GradientGpuBlockBuilder {
-    /// Generate a color ramp filling the indices in [start_idx, end_idx) and interpolating
-    /// from start_color to end_color.
-    fn fill_colors(
-        start_idx: usize,
-        end_idx: usize,
-        start_color: &PremultipliedColorF,
-        end_color: &PremultipliedColorF,
-        entries: &mut [GradientDataEntry; GRADIENT_DATA_SIZE],
-    ) {
-        // Calculate the color difference for individual steps in the ramp.
-        let inv_steps = 1.0 / (end_idx - start_idx) as f32;
-        let step_r = (end_color.r - start_color.r) * inv_steps;
-        let step_g = (end_color.g - start_color.g) * inv_steps;
-        let step_b = (end_color.b - start_color.b) * inv_steps;
-        let step_a = (end_color.a - start_color.a) * inv_steps;
-
-        let mut cur_color = *start_color;
-
-        // Walk the ramp writing start and end colors for each entry.
-        for index in start_idx .. end_idx {
-            let entry = &mut entries[index];
-            entry.start_color = cur_color;
-            cur_color.r += step_r;
-            cur_color.g += step_g;
-            cur_color.b += step_b;
-            cur_color.a += step_a;
-            entry.end_color = cur_color;
-        }
-    }
-
-    /// Compute an index into the gradient entry table based on a gradient stop offset. This
-    /// function maps offsets from [0, 1] to indices in [GRADIENT_DATA_TABLE_BEGIN, GRADIENT_DATA_TABLE_END].
-    #[inline]
-    fn get_index(offset: f32) -> usize {
-        (offset.max(0.0).min(1.0) * GRADIENT_DATA_TABLE_SIZE as f32 +
-            GRADIENT_DATA_TABLE_BEGIN as f32)
-            .round() as usize
-    }
-
-    // Build the gradient data from the supplied stops, reversing them if necessary.
-    fn build(
-        reverse_stops: bool,
-        request: &mut GpuDataRequest,
-        src_stops: &[GradientStop],
-    ) {
-        // Preconditions (should be ensured by DisplayListBuilder):
-        // * we have at least two stops
-        // * first stop has offset 0.0
-        // * last stop has offset 1.0
-        let mut src_stops = src_stops.into_iter();
-        let mut cur_color = match src_stops.next() {
-            Some(stop) => {
-                debug_assert_eq!(stop.offset, 0.0);
-                stop.color.premultiplied()
-            }
-            None => {
-                error!("Zero gradient stops found!");
-                PremultipliedColorF::BLACK
-            }
-        };
-
-        // A table of gradient entries, with two colors per entry, that specify the start and end color
-        // within the segment of the gradient space represented by that entry. To lookup a gradient result,
-        // first the entry index is calculated to determine which two colors to interpolate between, then
-        // the offset within that entry bucket is used to interpolate between the two colors in that entry.
-        // This layout preserves hard stops, as the end color for a given entry can differ from the start
-        // color for the following entry, despite them being adjacent. Colors are stored within in BGRA8
-        // format for texture upload. This table requires the gradient color stops to be normalized to the
-        // range [0, 1]. The first and last entries hold the first and last color stop colors respectively,
-        // while the entries in between hold the interpolated color stop values for the range [0, 1].
-        let mut entries: [GradientDataEntry; GRADIENT_DATA_SIZE] = unsafe { mem::uninitialized() };
-
-        if reverse_stops {
-            // Fill in the first entry (for reversed stops) with the first color stop
-            GradientGpuBlockBuilder::fill_colors(
-                GRADIENT_DATA_LAST_STOP,
-                GRADIENT_DATA_LAST_STOP + 1,
-                &cur_color,
-                &cur_color,
-                &mut entries,
-            );
-
-            // Fill in the center of the gradient table, generating a color ramp between each consecutive pair
-            // of gradient stops. Each iteration of a loop will fill the indices in [next_idx, cur_idx). The
-            // loop will then fill indices in [GRADIENT_DATA_TABLE_BEGIN, GRADIENT_DATA_TABLE_END).
-            let mut cur_idx = GRADIENT_DATA_TABLE_END;
-            for next in src_stops {
-                let next_color = next.color.premultiplied();
-                let next_idx = Self::get_index(1.0 - next.offset);
-
-                if next_idx < cur_idx {
-                    GradientGpuBlockBuilder::fill_colors(
-                        next_idx,
-                        cur_idx,
-                        &next_color,
-                        &cur_color,
-                        &mut entries,
-                    );
-                    cur_idx = next_idx;
-                }
-
-                cur_color = next_color;
-            }
-            if cur_idx != GRADIENT_DATA_TABLE_BEGIN {
-                error!("Gradient stops abruptly at {}, auto-completing to white", cur_idx);
-                GradientGpuBlockBuilder::fill_colors(
-                    GRADIENT_DATA_TABLE_BEGIN,
-                    cur_idx,
-                    &PremultipliedColorF::WHITE,
-                    &cur_color,
-                    &mut entries,
-                );
-            }
-
-            // Fill in the last entry (for reversed stops) with the last color stop
-            GradientGpuBlockBuilder::fill_colors(
-                GRADIENT_DATA_FIRST_STOP,
-                GRADIENT_DATA_FIRST_STOP + 1,
-                &cur_color,
-                &cur_color,
-                &mut entries,
-            );
-        } else {
-            // Fill in the first entry with the first color stop
-            GradientGpuBlockBuilder::fill_colors(
-                GRADIENT_DATA_FIRST_STOP,
-                GRADIENT_DATA_FIRST_STOP + 1,
-                &cur_color,
-                &cur_color,
-                &mut entries,
-            );
-
-            // Fill in the center of the gradient table, generating a color ramp between each consecutive pair
-            // of gradient stops. Each iteration of a loop will fill the indices in [cur_idx, next_idx). The
-            // loop will then fill indices in [GRADIENT_DATA_TABLE_BEGIN, GRADIENT_DATA_TABLE_END).
-            let mut cur_idx = GRADIENT_DATA_TABLE_BEGIN;
-            for next in src_stops {
-                let next_color = next.color.premultiplied();
-                let next_idx = Self::get_index(next.offset);
-
-                if next_idx > cur_idx {
-                    GradientGpuBlockBuilder::fill_colors(
-                        cur_idx,
-                        next_idx,
-                        &cur_color,
-                        &next_color,
-                        &mut entries,
-                    );
-                    cur_idx = next_idx;
-                }
-
-                cur_color = next_color;
-            }
-            if cur_idx != GRADIENT_DATA_TABLE_END {
-                error!("Gradient stops abruptly at {}, auto-completing to white", cur_idx);
-                GradientGpuBlockBuilder::fill_colors(
-                    cur_idx,
-                    GRADIENT_DATA_TABLE_END,
-                    &PremultipliedColorF::WHITE,
-                    &cur_color,
-                    &mut entries,
-                );
-            }
-
-            // Fill in the last entry with the last color stop
-            GradientGpuBlockBuilder::fill_colors(
-                GRADIENT_DATA_LAST_STOP,
-                GRADIENT_DATA_LAST_STOP + 1,
-                &cur_color,
-                &cur_color,
-                &mut entries,
-            );
-        }
-
-        for entry in entries.iter() {
-            request.push(entry.start_color);
-            request.push(entry.end_color);
-        }
-    }
-}
-
-#[derive(Debug)]
-pub struct TextRunPrimitive {
-    pub used_font: FontInstance,
-    pub glyph_keys_range: storage::Range<GlyphKey>,
-    pub shadow: bool,
-}
-
-impl TextRunPrimitive {
-    pub fn update_font_instance(
-        &mut self,
-        specified_font: &FontInstance,
-        device_pixel_scale: DevicePixelScale,
-        transform: &LayoutToWorldTransform,
-        allow_subpixel_aa: bool,
-        raster_space: RasterSpace,
-    ) -> bool {
-        // Get the current font size in device pixels
-        let device_font_size = specified_font.size.scale_by(device_pixel_scale.0);
-
-        // Determine if rasterizing glyphs in local or screen space.
-        // Only support transforms that can be coerced to simple 2D transforms.
-        let transform_glyphs = if transform.has_perspective_component() ||
-           !transform.has_2d_inverse() ||
-           // Font sizes larger than the limit need to be scaled, thus can't use subpixels.
-           transform.exceeds_2d_scale(FONT_SIZE_LIMIT / device_font_size.to_f64_px()) ||
-           // Otherwise, ensure the font is rasterized in screen-space.
-           raster_space != RasterSpace::Screen {
-            false
-        } else {
-            true
-        };
-
-        // Get the font transform matrix (skew / scale) from the complete transform.
-        let font_transform = if transform_glyphs {
-            // Quantize the transform to minimize thrashing of the glyph cache.
-            FontTransform::from(transform).quantize()
-        } else {
-            FontTransform::identity()
-        };
-
-        // If the transform or device size is different, then the caller of
-        // this method needs to know to rebuild the glyphs.
-        let cache_dirty =
-            self.used_font.transform != font_transform ||
-            self.used_font.size != device_font_size;
-
-        // Construct used font instance from the specified font instance
-        self.used_font = FontInstance {
-            transform: font_transform,
-            size: device_font_size,
-            ..specified_font.clone()
-        };
-
-        // If subpixel AA is disabled due to the backing surface the glyphs
-        // are being drawn onto, disable it (unless we are using the
-        // specifial subpixel mode that estimates background color).
-        if (!allow_subpixel_aa && self.used_font.bg_color.a == 0) ||
-            // If using local space glyphs, we don't want subpixel AA.
-            !transform_glyphs {
-            self.used_font.disable_subpixel_aa();
-        }
-
-        cache_dirty
-    }
-
-    fn prepare_for_render(
-        &mut self,
-        specified_font: &FontInstance,
-        glyphs: &[GlyphInstance],
-        device_pixel_scale: DevicePixelScale,
-        transform: &LayoutToWorldTransform,
-        pic_context: &PictureContext,
-        resource_cache: &mut ResourceCache,
-        gpu_cache: &mut GpuCache,
-        render_tasks: &mut RenderTaskTree,
-        special_render_passes: &mut SpecialRenderPasses,
-        scratch: &mut PrimitiveScratchBuffer,
-    ) {
-        let cache_dirty = self.update_font_instance(
-            specified_font,
-            device_pixel_scale,
-            transform,
-            pic_context.allow_subpixel_aa,
-            pic_context.raster_space,
-        );
-
-        if self.glyph_keys_range.is_empty() || cache_dirty {
-            let subpx_dir = self.used_font.get_subpx_dir();
-
-            self.glyph_keys_range = scratch.glyph_keys.extend(
-                glyphs.iter().map(|src| {
-                    let world_offset = self.used_font.transform.transform(&src.point);
-                    let device_offset = device_pixel_scale.transform_point(&world_offset);
-                    GlyphKey::new(src.index, device_offset, subpx_dir)
-                }));
-        }
-
-        resource_cache.request_glyphs(
-            self.used_font.clone(),
-            &scratch.glyph_keys[self.glyph_keys_range],
-            gpu_cache,
-            render_tasks,
-            special_render_passes,
-        );
-    }
-}
-
-#[derive(Debug)]
-#[repr(C)]
-struct ClipRect {
-    rect: LayoutRect,
-    mode: f32,
-}
-
-#[derive(Debug)]
-#[repr(C)]
-struct ClipCorner {
-    rect: LayoutRect,
-    outer_radius_x: f32,
-    outer_radius_y: f32,
-    inner_radius_x: f32,
-    inner_radius_y: f32,
-}
-
-impl ToGpuBlocks for ClipCorner {
-    fn write_gpu_blocks(&self, mut request: GpuDataRequest) {
-        self.write(&mut request)
-    }
-}
-
-impl ClipCorner {
-    fn write(&self, request: &mut GpuDataRequest) {
-        request.push(self.rect);
-        request.push([
-            self.outer_radius_x,
-            self.outer_radius_y,
-            self.inner_radius_x,
-            self.inner_radius_y,
-        ]);
-    }
-
-    fn uniform(rect: LayoutRect, outer_radius: f32, inner_radius: f32) -> ClipCorner {
-        ClipCorner {
-            rect,
-            outer_radius_x: outer_radius,
-            outer_radius_y: outer_radius,
-            inner_radius_x: inner_radius,
-            inner_radius_y: inner_radius,
-        }
-    }
-}
-
-#[derive(Debug)]
-#[repr(C)]
-pub struct ImageMaskData {
-    /// The local size of the whole masked area.
-    pub local_mask_size: LayoutSize,
-}
-
-impl ToGpuBlocks for ImageMaskData {
-    fn write_gpu_blocks(&self, mut request: GpuDataRequest) {
-        request.push([
-            self.local_mask_size.width,
-            self.local_mask_size.height,
-            0.0,
-            0.0,
-        ]);
-    }
-}
-
-#[derive(Debug)]
-pub struct ClipData {
-    rect: ClipRect,
-    top_left: ClipCorner,
-    top_right: ClipCorner,
-    bottom_left: ClipCorner,
-    bottom_right: ClipCorner,
-}
-
-impl ClipData {
-    pub fn rounded_rect(size: LayoutSize, radii: &BorderRadius, mode: ClipMode) -> ClipData {
-        // TODO(gw): For simplicity, keep most of the clip GPU structs the
-        //           same as they were, even though the origin is now always
-        //           zero, since they are in the clip's local space. In future,
-        //           we could reduce the GPU cache size of ClipData.
-        let rect = LayoutRect::new(
-            LayoutPoint::zero(),
-            size,
-        );
-
-        ClipData {
-            rect: ClipRect {
-                rect,
-                mode: mode as u32 as f32,
-            },
-            top_left: ClipCorner {
-                rect: LayoutRect::new(
-                    LayoutPoint::new(rect.origin.x, rect.origin.y),
-                    LayoutSize::new(radii.top_left.width, radii.top_left.height),
-                ),
-                outer_radius_x: radii.top_left.width,
-                outer_radius_y: radii.top_left.height,
-                inner_radius_x: 0.0,
-                inner_radius_y: 0.0,
-            },
-            top_right: ClipCorner {
-                rect: LayoutRect::new(
-                    LayoutPoint::new(
-                        rect.origin.x + rect.size.width - radii.top_right.width,
-                        rect.origin.y,
-                    ),
-                    LayoutSize::new(radii.top_right.width, radii.top_right.height),
-                ),
-                outer_radius_x: radii.top_right.width,
-                outer_radius_y: radii.top_right.height,
-                inner_radius_x: 0.0,
-                inner_radius_y: 0.0,
-            },
-            bottom_left: ClipCorner {
-                rect: Layou