Bug 1498420 Convert talos tart extension to a webextension r=mconley a=Aryx
authorAndrew Swan <aswan@mozilla.com>
Mon, 15 Oct 2018 16:23:17 -0700
changeset 500742 fb6f3dea0beaa9e17e58f8967ccfcf604f4d95fb
parent 500741 345543cd804524463207a7c674e82e21e7dcffba
child 500743 380375ad1dbd2402b5d131dd5cc8bed9d78ba072
child 500754 a83370778faf43b69e9a3b3540d25ce8b51da6a8
push id1864
push userffxbld-merge
push dateMon, 03 Dec 2018 15:51:40 +0000
treeherdermozilla-release@f040763d99ad [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmconley, Aryx
bugs1498420
milestone64.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1498420 Convert talos tart extension to a webextension r=mconley a=Aryx The biggest change here is that the tart.html page that drives the test (not to be confused with the pages loaded in new tabs during the test) moves from a chrome: page inside the extension to a regular http: page. That also required revamping the communication between tart.html and the extension. The rest of the changes are just the packaging and startup mechanics for the test extension. Differential Revision: https://phabricator.services.mozilla.com/D8807
testing/talos/talos/tests/tart/addon/api.js
testing/talos/talos/tests/tart/addon/bootstrap.js
testing/talos/talos/tests/tart/addon/chrome.manifest
testing/talos/talos/tests/tart/addon/chrome/blank.icon.html
testing/talos/talos/tests/tart/addon/content/blank.icon.html
testing/talos/talos/tests/tart/addon/content/framescript.js
testing/talos/talos/tests/tart/addon/content/initialize_browser.js
testing/talos/talos/tests/tart/addon/content/tart.html
testing/talos/talos/tests/tart/addon/content/tart.ico
testing/talos/talos/tests/tart/addon/content/tart.js
testing/talos/talos/tests/tart/addon/install.rdf
testing/talos/talos/tests/tart/addon/manifest.json
testing/talos/talos/tests/tart/addon/schema.json
testing/talos/talos/tests/tart/tart.html
testing/talos/talos/tests/tart/tart.ico
testing/talos/talos/tests/tart/tart.manifest
rename from testing/talos/talos/tests/tart/addon/bootstrap.js
rename to testing/talos/talos/tests/tart/addon/api.js
--- a/testing/talos/talos/tests/tart/addon/bootstrap.js
+++ b/testing/talos/talos/tests/tart/addon/api.js
@@ -1,52 +1,91 @@
 "use strict";
 
-/* globals initializeBrowser */
+ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+  Services: "resource://gre/modules/Services.jsm",
+});
 
-// PLEASE NOTE:
-//
-// The canonical version of this file lives in testing/talos/talos, and
-// is duplicated in a number of test add-ons in directories below it.
-// Please do not update one withput updating all.
+XPCOMUtils.defineLazyServiceGetter(this, "aomStartup",
+                                   "@mozilla.org/addons/addon-manager-startup;1",
+                                   "amIAddonManagerStartup");
 
-// Reads the chrome.manifest from a legacy non-restartless extension and loads
-// its overlays into the appropriate top-level windows.
+XPCOMUtils.defineLazyServiceGetter(this, "clipboardHelper",
+                                   "@mozilla.org/widget/clipboardhelper;1",
+                                   "nsIClipboardHelper");
+
+/* globals ExtensionAPI */
+
+const PREFIX = "tart@mozilla.org";
 
-ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
-ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
-ChromeUtils.import("resource://gre/modules/Services.jsm");
+this.tart = class extends ExtensionAPI {
+  constructor(...args) {
+    super(...args);
+    this.loadedWindows = new WeakSet();
+  }
 
-const windowTracker = {
-  init() {
-    Services.ww.registerNotification(this);
-  },
+  onStartup() {
+    const manifestURI = Services.io.newURI("manifest.json", null, this.extension.rootURI);
+    this.chromeHandle = aomStartup.registerChrome(manifestURI, [
+      ["content", "tart", "chrome/"],
+    ]);
 
-  async observe(window, topic, data) {
-    if (topic === "domwindowopened") {
-      await new Promise(resolve =>
-        window.addEventListener("DOMWindowCreated", resolve, {once: true}));
+    this.framescriptURL = this.extension.baseURI.resolve("/content/framescript.js");
+    Services.mm.loadFrameScript(this.framescriptURL, true);
+    Services.mm.addMessageListener(`${PREFIX}:chrome-exec-message`, this);
+  }
+
+  onShutdown() {
+    Services.mm.removeMessageListener(`${PREFIX}:chrome-exec-message`, this);
+    Services.mm.removeDelayedFrameScript(this.framescriptURL);
+    this.chromeHandle.destruct();
+  }
 
-      let {document} = window;
-      let {documentURI} = document;
+  receiveMessage({target, data}) {
+    let win = target.ownerGlobal;
+    if (!this.loadedWindows.has(win)) {
+      let {baseURI} = this.extension;
+      Services.scriptloader.loadSubScript(baseURI.resolve("/content/Profiler.js"), win);
+      Services.scriptloader.loadSubScript(baseURI.resolve("/content/tart.js"), win);
+      this.loadedWindows.add(win);
+    }
 
-      if (documentURI !== AppConstants.BROWSER_CHROME_URL) {
-        return;
-      }
-      initializeBrowser(window);
+    function sendResult(result) {
+      target.messageManager.sendAsyncMessage(`${PREFIX}:chrome-exec-reply`,
+                                             {id: data.id, result});
     }
-  },
-};
+
+    let {command} = data;
+
+    switch (command.name) {
+      case "ping":
+        sendResult();
+        break;
 
-function readSync(uri) {
-  let channel = NetUtil.newChannel({uri, loadUsingSystemPrincipal: true});
-  let buffer = NetUtil.readInputStream(channel.open2());
-  return new TextDecoder().decode(buffer);
-}
+      case "runTest":
+        (new win.Tart()).startTest(sendResult, command.data);
+        break;
+
+      case "setASAP":
+        Services.prefs.setIntPref("layout.frame_rate", 0);
+        Services.prefs.setIntPref("docshell.event_starvation_delay_hint", 1);
+        sendResult();
+        break;
 
-function startup(data, reason) {
-  Services.scriptloader.loadSubScript(data.resourceURI.resolve("content/initialize_browser.js"));
-  windowTracker.init();
-}
+      case "unsetASAP":
+        Services.prefs.clearUserPref("layout.frame_rate");
+        Services.prefs.clearUserPref("docshell.event_starvation_delay_hint");
+        sendResult();
+        break;
 
-function shutdown(data, reason) {}
-function install(data, reason) {}
-function uninstall(data, reason) {}
+      case "toClipboard":
+        clipboardHelper.copyString(command.data);
+        sendResult();
+        break;
+
+      default:
+        Cu.reportError(`Unknown TART command ${command.name}\n`);
+        break;
+    }
+  }
+};
deleted file mode 100644
--- a/testing/talos/talos/tests/tart/addon/chrome.manifest
+++ /dev/null
@@ -1,1 +0,0 @@
-content tart content/
rename from testing/talos/talos/tests/tart/addon/content/blank.icon.html
rename to testing/talos/talos/tests/tart/addon/chrome/blank.icon.html
--- a/testing/talos/talos/tests/tart/addon/content/framescript.js
+++ b/testing/talos/talos/tests/tart/addon/content/framescript.js
@@ -1,25 +1,38 @@
 (function() {
   const TART_PREFIX = "tart@mozilla.org:";
 
   addEventListener(TART_PREFIX + "chrome-exec-event", function(e) {
-    if (content.document.documentURI.indexOf("chrome://tart/content/tart.html")) {
-      // Can have url fragment. Backward compatible version of !str.startsWidth("prefix")
-      throw new Error("Cannot be used outside of TART's launch page");
+    if (!content.location.pathname.endsWith("tart.html")) {
+      Cu.reportError(`Ignore chrome-exec-event on non-tart page ${content.location.href}`);
+      return;
+    }
+
+    function dispatchReply(result) {
+      let contentEvent = Cu.cloneInto({
+        bubbles: true,
+        detail: result,
+      }, content);
+      content.dispatchEvent(new content.CustomEvent(e.detail.replyEvent, contentEvent));
+    }
+
+    if (e.detail.command.name == "ping") {
+      dispatchReply();
+      return;
     }
 
     // eslint-disable-next-line mozilla/avoid-Date-timing
     var uniqueMessageId = TART_PREFIX + content.document.documentURI + Date.now() + Math.random();
 
     addMessageListener(TART_PREFIX + "chrome-exec-reply", function done(reply) {
       if (reply.data.id == uniqueMessageId) {
         removeMessageListener(TART_PREFIX + "chrome-exec-reply", done);
-        e.detail.doneCallback(reply.data.result);
+        dispatchReply(reply.data.result);
       }
     });
 
     sendAsyncMessage(TART_PREFIX + "chrome-exec-message", {
       command: e.detail.command,
       id: uniqueMessageId,
     });
-  }, false);
+  }, false, true);
 })();
deleted file mode 100644
--- a/testing/talos/talos/tests/tart/addon/content/initialize_browser.js
+++ /dev/null
@@ -1,54 +0,0 @@
-function initializeBrowser(win) {
-  Services.scriptloader.loadSubScript("chrome://tart/content/Profiler.js", win);
-  Services.scriptloader.loadSubScript("chrome://tart/content/tart.js", win);
-  var prefs = Services.prefs;
-
-  const TART_PREFIX = "tart@mozilla.org:";
-
-  // "services" which the framescript can execute at the chrome process
-  var proxiedServices = {
-    runTest(config, callback) {
-      (new win.Tart()).startTest(callback, config);
-    },
-
-    setASAP() {
-      prefs.setIntPref("layout.frame_rate", 0);
-      prefs.setIntPref("docshell.event_starvation_delay_hint", 1);
-    },
-
-    unsetASAP() {
-      prefs.clearUserPref("layout.frame_rate");
-      prefs.clearUserPref("docshell.event_starvation_delay_hint");
-    },
-
-    toClipboard(text) {
-      const gClipboardHelper = Cc["@mozilla.org/widget/clipboardhelper;1"]
-                               .getService(Ci.nsIClipboardHelper);
-      gClipboardHelper.copyString(text);
-    },
-  };
-
-  var groupMM = win.getGroupMessageManager("browsers");
-  groupMM.loadFrameScript("chrome://tart/content/framescript.js", true);
-
-  // listener/executor on the chrome process for tart.html
-  groupMM.addMessageListener(TART_PREFIX + "chrome-exec-message", function listener(m) {
-    function sendResult(result) {
-      groupMM.broadcastAsyncMessage(TART_PREFIX + "chrome-exec-reply", {
-        id: m.data.id,
-        result,
-      });
-    }
-
-    var command = m.data.command;
-    if (!proxiedServices.hasOwnProperty(command.name))
-      throw new Error("TART: service doesn't exist: '" + command.name + "'");
-
-    var service = proxiedServices[command.name];
-    if (command.name == "runTest") // Needs async execution
-      service(command.data, sendResult);
-    else
-      sendResult(service(command.data));
-
-  });
-}
--- a/testing/talos/talos/tests/tart/addon/content/tart.js
+++ b/testing/talos/talos/tests/tart/addon/content/tart.js
@@ -388,37 +388,23 @@ Tart.prototype = {
     window.dump(str);
   },
 
   _logLine(str) {
     return this._log(str + "\n");
   },
 
   _reportAllResults() {
-    var testNames = [];
-    var testResults = [];
-
     var out = "";
     for (var i in this._results) {
       res = this._results[i];
       var disp = [].concat(res.value).map(function(a) { return (isNaN(a) ? -1 : a.toFixed(1)); }).join(" ");
       out += res.name + ": " + disp + "\n";
-
-      if (!Array.isArray(res.value)) { // Waw intervals array is not reported to talos
-        testNames.push(res.name);
-        testResults.push(res.value);
-      }
     }
     this._log("\n" + out);
-
-    if (content && content.tpRecordTime) {
-      content.tpRecordTime(testResults.join(","), 0, testNames.join(","));
-    } else {
-      // alert(out);
-    }
   },
 
   _onTestComplete: null,
 
   _doneInternal() {
     this._logLine("TART_RESULTS_JSON=" + JSON.stringify(this._results));
     this._reportAllResults();
     this._win.gBrowser.selectedTab = this._tartTab;
rename from testing/talos/talos/tests/tart/addon/install.rdf
rename to testing/talos/talos/tests/tart/addon/manifest.json
--- a/testing/talos/talos/tests/tart/addon/install.rdf
+++ b/testing/talos/talos/tests/tart/addon/manifest.json
@@ -1,21 +1,22 @@
-<?xml version="1.0"?><RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"     xmlns:em="http://www.mozilla.org/2004/em-rdf#"><Description about="urn:mozilla:install-manifest">
+{
+  "manifest_version": 2,
+  "name": "TART - Tab Animation regression Test",
+  "version": "2.0",
 
-<!-- Required Items -->
-<em:id>bug848358@mozilla.org</em:id>
-<em:name>TART - Tab Animation regression Test</em:name>
-<em:version>1.6.2</em:version>
-<em:bootstrap>true</em:bootstrap>
+  "applications": {
+    "gecko": {
+      "id": "bug848358@mozilla.org"
+    }
+  },
 
-<em:targetApplication>
-    <Description>
-        <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
-        <em:minVersion>1.5</em:minVersion>
-        <em:maxVersion>*</em:maxVersion>
-    </Description>
-</em:targetApplication>
-
-<!-- Optional Items -->
-<em:creator>Avi Halachmi</em:creator>
-<em:description>Bug 848358, bug 956388. To run: navigate to chrome://tart/content/tart.html</em:description>
-<em:homepageURL>https://bugzilla.mozilla.org/show_bug.cgi?id=848358</em:homepageURL>
-</Description></RDF>
+  "experiment_apis": {
+    "tart": {
+      "schema": "schema.json",
+      "parent": {
+        "scopes": ["addon_parent"],
+        "script": "api.js",
+        "events": ["startup"]
+      }
+    }
+  }
+}
new file mode 100644
--- /dev/null
+++ b/testing/talos/talos/tests/tart/addon/schema.json
@@ -0,0 +1,1 @@
+[]
rename from testing/talos/talos/tests/tart/addon/content/tart.html
rename to testing/talos/talos/tests/tart/tart.html
--- a/testing/talos/talos/tests/tart/addon/content/tart.html
+++ b/testing/talos/talos/tests/tart/tart.html
@@ -1,9 +1,9 @@
-<html>
+<html>
 <head>
 
 <meta charset="UTF-8"/>
 <link id="tart-icon" rel="icon" href="tart.ico"/>
 <title>TART - Tab Animation Regression Test</title>
 
 <script type="application/x-javascript">
 
@@ -11,28 +11,33 @@ function $(id) {
   return document.getElementById(id);
 }
 
 // Executes command at the chrome process.
 // Limited to one argument (data), which is enough for TART.
 // doneCallback will be called once done and, if applicable, with the result as argument.
 // Execution might finish quickly (e.g. when setting prefs) or
 // take a while (e.g. when triggering the test run)
+let _replyId = 1;
 function chromeExec(commandName, data, doneCallback) {
-  // dispatch an event to the framescript which will take it from there.
-  doneCallback = doneCallback || function dummy() {};
+  let replyEvent = `tart@mozilla.org:chrome-exec-reply:${_replyId++}`;
+  if (doneCallback) {
+    addEventListener(replyEvent, e => { doneCallback(e.detail); },
+                     {once: true});
+  }
+
   dispatchEvent(
     new CustomEvent("tart@mozilla.org:chrome-exec-event", {
       bubbles: true,
       detail: {
         command: {
           name: commandName,
           data,
         },
-        doneCallback,
+        replyEvent,
       },
     })
   );
 }
 
 function setASAP() {
   chromeExec("setASAP");
 }
@@ -44,16 +49,32 @@ function unsetASAP() {
 function toClipboard(text) {
   chromeExec("toClipboard", text);
 }
 
 function runTest(config, doneCallback) {
   chromeExec("runTest", config, doneCallback);
 }
 
+// Returns a Promise that resolves when the test extension is loaded.
+function waitForLoad() {
+  async function tryPing() {
+    let pingPromise = new Promise(resolve => chromeExec("ping", null, resolve));
+    let timeoutPromise = new Promise((resolve, reject) => setTimeout(reject, 500));
+
+    try {
+      await Promise.race([pingPromise, timeoutPromise]);
+    } catch (e) {
+      return tryPing();
+    }
+    return null;
+  }
+  return tryPing();
+}
+
 
 function sum(values) {
   return values.reduce(function(a, b) { return a + b; });
 }
 
 function average(values) {
   return values.length ? sum(values) / values.length : 999999999;
 }
@@ -111,28 +132,39 @@ function doneTest(dispResult) {
         dispStats += s + "&nbsp;&nbsp;&nbsp;&nbsp;Average (" + stats[s].length + "): " + average(stats[s]).toFixed(2) + " stddev: " + stddev(stats[s]).toFixed(2) + "<br/>";
       }
 
       dispStats += "<hr/><b>Individual animations</b>:<br/>";
     }
 
     // eslint-disable-next-line no-unsanitized/property
     $("run-results").innerHTML = "<hr/><br/>Results <button onclick='toClipboard(lastResults)'>[ Copy to clipboard as JSON ]</button>:<br/>" + dispStats + dispResult.join("<br/>");
+
+    let testNames = [], testResults = [];
+    for (let result of JSON.parse(lastResults)) {
+      if (!Array.isArray(result.value)) {
+        testNames.push(result.name);
+        testResults.push(result.value);
+      }
+    }
+    window.tpRecordTime(testResults.join(","), 0, testNames.join(","));
   }
 }
 
 var config = {subtests: [], repeat: 1}; // Empty subtests interpreted as all subtests, since otherwise meaningless.
 
 function triggerStart() {
   updateConfig();
   $("hide-during-run").style.display = "none";
   $("show-during-run").style.display = "block";
   $("run-results").innerHTML = "";
 
-  runTest(config, doneTest);
+  waitForLoad().then(() => {
+    runTest(config, doneTest);
+  });
 }
 
 var defaultConfig = {
       repeat: 1,
       rest: 500,
       tickle: true,
       controlProfiler: true,  // If true, pause the profiler when not measuring. Else just add markers.
       subtests: {
rename from testing/talos/talos/tests/tart/addon/content/tart.ico
rename to testing/talos/talos/tests/tart/tart.ico
--- a/testing/talos/talos/tests/tart/tart.manifest
+++ b/testing/talos/talos/tests/tart/tart.manifest
@@ -1,1 +1,1 @@
-% chrome://tart/content/tart.html#auto
+% http://localhost/tests/tart/tart.html#auto