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 490574 fb6f3dea0beaa9e17e58f8967ccfcf604f4d95fb
parent 490573 345543cd804524463207a7c674e82e21e7dcffba
child 490575 a83370778faf43b69e9a3b3540d25ce8b51da6a8
child 490586 380375ad1dbd2402b5d131dd5cc8bed9d78ba072
push id247
push userfmarier@mozilla.com
push dateSat, 27 Oct 2018 01:06:44 +0000
reviewersmconley, Aryx
bugs1498420
milestone64.0a1
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