Bug 1376546 - Add instrumentation to browser-chrome tests to output the total set of elements in use in tests;r=bgrins
authorDave Townsend <dtownsend@oxymoronical.com>
Tue, 13 Jun 2017 15:49:33 -0700
changeset 443902 2d1bca2413eb88d1cdee6a5350ea1bd8410c0fd4
parent 443901 6be7ac2dffd2a4ab322a59827b406153a4f8b94f
child 443903 088804dba0059036979e7aa28344bc318b7238b4
push id1618
push userCallek@gmail.com
push dateThu, 11 Jan 2018 17:45:48 +0000
treeherdermozilla-release@882ca853e05a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbgrins
bugs1376546
milestone58.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 1376546 - Add instrumentation to browser-chrome tests to output the total set of elements in use in tests;r=bgrins MozReview-Commit-ID: DOpig6v4mtt
testing/mochitest/browser-test.js
--- a/testing/mochitest/browser-test.js
+++ b/testing/mochitest/browser-test.js
@@ -1,12 +1,13 @@
 /* -*- js-indent-level: 2; tab-width: 2; indent-tabs-mode: nil -*- */
 // Test timeout (seconds)
 var gTimeoutSeconds = 45;
 var gConfig;
+var gSaveInstrumentationData = null;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://gre/modules/AppConstants.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "ContentSearch",
   "resource:///modules/ContentSearch.jsm");
@@ -112,16 +113,265 @@ function testInit() {
     globalMM.loadFrameScript("chrome://mochikit/content/shutdown-leaks-collector.js", true);
   } else {
     // In non-e10s, only run the ShutdownLeaksCollector in the parent process.
     Components.utils.import("chrome://mochikit/content/ShutdownLeaksCollector.jsm");
   }
 
   let gmm = Cc["@mozilla.org/globalmessagemanager;1"].getService(Ci.nsIMessageListenerManager);
   gmm.loadFrameScript("chrome://mochikit/content/tests/SimpleTest/AsyncUtilsContent.js", true);
+
+  var testSuite = Cc["@mozilla.org/process/environment;1"].
+                    getService(Ci.nsIEnvironment).
+                    get("TEST_SUITE");
+  if (testSuite == "browser-chrome-instrumentation") {
+    takeInstrumentation();
+  }
+}
+
+function takeInstrumentation() {
+
+  let instrumentData = {
+    elements: {}
+  };
+
+  function pad(str, length) {
+    if (str.length >= length)
+      return str;
+
+    return str + " ".repeat(length - str.length);
+  }
+
+  function byCount(a, b) {
+    return b[1] - a[1];
+  }
+
+  function getSummaryText() {
+    let summary = [];
+    let allData = {};
+    for (let selector of Object.keys(instrumentData.elements)) {
+      allData[selector] = instrumentData.elements[selector];
+    }
+
+    let selectors = Object.keys(allData);
+    let elements = selectors.map(s => allData[s]);
+
+    let namespaceMap = new Map();
+    let bindingMap = new Map();
+
+    for (let element of elements) {
+      if (!bindingMap.has(element.binding)) {
+        bindingMap.set(element.binding, 1);
+      } else {
+        bindingMap.set(element.binding, bindingMap.get(element.binding) + 1);
+      }
+
+      if (!namespaceMap.has(element.namespaceURI)) {
+        namespaceMap.set(element.namespaceURI, new Map());
+      }
+
+      let localNameMap = namespaceMap.get(element.namespaceURI);
+      if (!localNameMap.has(element.localName)) {
+        localNameMap.set(element.localName, 1);
+      } else {
+        localNameMap.set(element.localName, localNameMap.get(element.localName) + 1);
+      }
+    }
+
+    for (let [namespace, localNameMap] of namespaceMap) {
+      summary.push(`Elements in namespace ${namespace}`);
+
+      let entries = Array.from(localNameMap);
+      entries.sort(byCount);
+      for (let entry of entries) {
+        summary.push(`  ${pad(entry[1] + "", 5)} ${entry[0]}`);
+      }
+    }
+
+    summary.push("XBL bindings");
+    let bindings = Array.from(bindingMap);
+    bindings.sort(byCount);
+    let bindingsJSON = {};
+    for (let binding of bindings) {
+      summary.push(`  ${pad(binding[1] + "", 5)} ${binding[0]}`);
+      if (binding[0]) {
+        bindingsJSON[binding[0].split("#")[1].split('"')[0]] = binding[1];
+      }
+    }
+
+    summary.push("XBL bindings as JSON");
+    summary.push(JSON.stringify(bindingsJSON, null, 2));
+
+    return summary.join("\n");
+  }
+
+  // Saves instrumantation data
+  function saveData() {
+    let path = Cc["@mozilla.org/process/environment;1"].
+               getService(Ci.nsIEnvironment).
+               get("MOZ_UPLOAD_DIR");
+    let encoder = new TextEncoder();
+
+    let instrumentPath = OS.Path.join(path, "xulinstrument.txt");
+    OS.File.writeAtomic(instrumentPath, encoder.encode(JSON.stringify(instrumentData, null, 2)));
+
+    let summaryPath = OS.Path.join(path, "xulsummary.txt");
+    OS.File.writeAtomic(summaryPath, encoder.encode(getSummaryText()));
+  }
+
+  // An iterator over an element and its ancestors
+  function* elementPath(element) {
+    yield element;
+    while ((element = element.parentNode) && (element instanceof Element)) {
+      yield element;
+    }
+  }
+
+  // Returns the information we care about for an element
+  function getElementInfo(element) {
+    let style = element.ownerGlobal.getComputedStyle(element);
+    let binding = style && style.getPropertyValue("-moz-binding");
+
+    return {
+      namespaceURI: element.namespaceURI,
+      localName: element.localName,
+      binding: (binding && binding != "none") ? binding : null,
+    }
+  }
+
+  // The selector for just this element
+  function immediateSelector(element) {
+    if (element.localName == "notificationbox" && element.parentNode &&
+        element.parentNode.classList.contains("tabbrowser-tabpanels")) {
+      // Don't do a full selector for a tabpanel's notificationbox
+      return element.localName;
+    }
+
+    if (element.localName == "tab" && element.classList.contains("tabbrowser-tab")) {
+      // Don't do a full selector for a tab
+      return element.localName;
+    }
+
+    if (element.id) {
+      return `#${element.id}`;
+    }
+
+    let selector = element.localName;
+
+    if (element.classList.length) {
+      selector += `.${Array.from(element.classList).join(".")}`;
+    }
+
+    for (let attr of ["src", "label"]) {
+      if (element.hasAttribute(attr)) {
+        selector += `[${attr}=${JSON.stringify(element.getAttribute(attr))}]`;
+      }
+    }
+
+    return selector;
+  }
+
+  // The selector chain for the element
+  function elementSelector(element) {
+    return Array.from(elementPath(element)).reverse().map(immediateSelector).join(" > ");
+  }
+
+  // An iterator over all elements in the window
+  function* windowElements(win) {
+    yield* elementDescendants(win.document.documentElement);
+  }
+
+  // An iterator over an element and all of its descendants
+  function* elementDescendants(element) {
+    let walker = Cc["@mozilla.org/inspector/deep-tree-walker;1"].
+                 createInstance(Ci.inIDeepTreeWalker);
+    walker.showAnonymousContent = true;
+    walker.showSubDocuments = false;
+    walker.showDocumentsAsNodes = false;
+    walker.init(element, Ci.nsIDOMNodeFilter.SHOW_ELEMENT);
+
+    yield element;
+    while (walker.nextNode()) {
+      if (walker.currentNode instanceof Element) {
+        yield walker.currentNode;
+      }
+    }
+  }
+
+  // Checks if we've seen an element and if not adds it to the instrumentation data
+  function instrumentElement(element) {
+    if (element.__instrumentSeen) {
+      return;
+    }
+
+    let selector = elementSelector(element);
+    element.__instrumentSeen = true;
+
+    if (selector in instrumentData.elements) {
+      return;
+    }
+
+    instrumentData.elements[selector] = getElementInfo(element);
+  }
+
+  // Instruments every element in a window
+  function scanWindow(win) {
+    Array.from(windowElements(win)).forEach(instrumentElement);
+  }
+
+  // Instruments every element in an element's descendants
+  function scanElement(element) {
+    Array.from(elementDescendants(element)).forEach(instrumentElement);
+  }
+
+  function handleMutation(mutation) {
+    if (mutation.type != "childList") {
+      return;
+    }
+
+    for (let node of mutation.addedNodes) {
+      if (node instanceof Element) {
+        scanElement(node);
+      }
+    }
+  }
+  // Watches a window for new elements to instrument
+  function observeWindow(win) {
+    let observer = new MutationObserver((mutations) => {
+      mutations.forEach(handleMutation);
+    });
+
+    observer.observe(win.document, {
+      childList: true,
+      subtree: true,
+    });
+
+    win.addEventListener("unload", () => {
+      observer.takeRecords().forEach(handleMutation);
+    }, { once: true });
+  }
+
+  scanWindow(window);
+  observeWindow(window);
+  gSaveInstrumentationData = saveData;
+
+  Services.ww.registerNotification((win, topic, data) => {
+    if (topic != "domwindowopened") {
+      return;
+    }
+
+    win.addEventListener("load", () => {
+      if (win.location.href != "chrome://browser/content/browser.xul") {
+        return;
+      }
+
+      scanWindow(win);
+      observeWindow(win);
+    }, { once: true });
+  });
 }
 
 function Tester(aTests, structuredLogger, aCallback) {
   this.structuredLogger = structuredLogger;
   this.tests = aTests;
   this.callback = aCallback;
 
   this._scriptLoader = Services.scriptloader;
@@ -359,16 +609,20 @@ Tester.prototype = {
       this.structuredLogger.error("browser-test.js | No tests to run. Did you pass invalid test_paths?");
     }
     this.structuredLogger.info("*** End BrowserChrome Test Results ***");
 
     // Tests complete, notify the callback and return
     this.callback(this.tests);
     this.callback = null;
     this.tests = null;
+
+    if (gSaveInstrumentationData) {
+      gSaveInstrumentationData();
+    }
   },
 
   haltTests: function Tester_haltTests() {
     // Do not run any further tests
     this.currentTestIndex = this.tests.length - 1;
     this.repeat = 0;
   },