Bug 1603185 - Collect per tab unique site origin telemetry r=Dexter,Gijs,nika
authorBarret Rennie <barret@brennie.ca>
Fri, 22 May 2020 00:34:17 +0000
changeset 531563 13c6ff0fdb29eec127d89c9c42293608bc088e58
parent 531562 55761a2d367b46b7a85e9cb0b34f075cd8e5dc78
child 531564 57f8b5fcbbdae3b691642052d101b9804582eb94
push id37440
push userabutkovits@mozilla.com
push dateFri, 22 May 2020 09:43:16 +0000
treeherdermozilla-central@fbf71e4d2e21 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersDexter, Gijs, nika
bugs1603185
milestone78.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 1603185 - Collect per tab unique site origin telemetry r=Dexter,Gijs,nika Top-level content WindowGlobalParents now keep track of the site origins of the documents in their document tree. When the WindowGlobalParent is torn down, the maximum of the number of unique site origins is submitted for telemetry. Differential Revision: https://phabricator.services.mozilla.com/D71493
browser/modules/test/browser/blank_iframe.html
browser/modules/test/browser/browser.ini
browser/modules/test/browser/browser_Telemetry_numberOfSiteOriginsPerDocument.js
docshell/base/BrowsingContext.cpp
dom/ipc/WindowGlobalParent.cpp
dom/ipc/WindowGlobalParent.h
toolkit/components/telemetry/Histograms.json
toolkit/components/telemetry/geckoview/streaming/metrics.yaml
new file mode 100644
--- /dev/null
+++ b/browser/modules/test/browser/blank_iframe.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8"/>
+  </head>
+  <body><iframe></iframe></body>
+</html>
--- a/browser/modules/test/browser/browser.ini
+++ b/browser/modules/test/browser/browser.ini
@@ -58,8 +58,12 @@ support-files =
   usageTelemetrySearchSuggestions.sjs
   usageTelemetrySearchSuggestions.xml
 [browser_UsageTelemetry_content.js]
 [browser_UsageTelemetry_content_aboutHome.js]
 [browser_UsageTelemetry_content_aboutRestartRequired.js]
 [browser_Telemetry_numberOfSiteOrigins.js]
 support-files =
   contain_iframe.html
+[browser_Telemetry_numberOfSiteOriginsPerDocument.js]
+support-files =
+  contain_iframe.html
+  blank_iframe.html
new file mode 100644
--- /dev/null
+++ b/browser/modules/test/browser/browser_Telemetry_numberOfSiteOriginsPerDocument.js
@@ -0,0 +1,137 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+const { Assert } = ChromeUtils.import("resource://testing-common/Assert.jsm");
+
+ChromeUtils.defineModuleGetter(
+  this,
+  "TelemetryTestUtils",
+  "resource://testing-common/TelemetryTestUtils.jsm"
+);
+
+const histogramName = "FX_NUMBER_OF_UNIQUE_SITE_ORIGINS_PER_DOCUMENT";
+const testRoot = getRootDirectory(gTestPath).replace(
+  "chrome://mochitests/content",
+  "http://mochi.test:8888"
+);
+
+function windowGlobalDestroyed(id) {
+  return BrowserUtils.promiseObserved(
+    "window-global-destroyed",
+    aWGP => aWGP.innerWindowId == id
+  );
+}
+
+async function openAndCloseTab(uri) {
+  const tab = await BrowserTestUtils.openNewForegroundTab({
+    gBrowser,
+    opening: uri,
+    waitForStateStop: true,
+  });
+  const innerWindowId = tab.linkedBrowser.innerWindowID;
+
+  const wgpDestroyed = windowGlobalDestroyed(innerWindowId);
+  BrowserTestUtils.removeTab(tab);
+  await wgpDestroyed;
+}
+
+add_task(async function test_numberOfSiteOriginsAfterTabClose() {
+  const histogram = TelemetryTestUtils.getAndClearHistogram(histogramName);
+  const testPage = `${testRoot}contain_iframe.html`;
+
+  await openAndCloseTab(testPage);
+
+  // testPage contains two origins: mochi.test:8888 and example.com.
+  TelemetryTestUtils.assertHistogram(histogram, 2, 1);
+});
+
+add_task(async function test_numberOfSiteOriginsAboutBlank() {
+  const histogram = TelemetryTestUtils.getAndClearHistogram(histogramName);
+
+  await openAndCloseTab("about:blank");
+
+  const { values } = histogram.snapshot();
+  Assert.deepEqual(
+    values,
+    {},
+    `Histogram should have no values; had ${JSON.stringify(values)}`
+  );
+});
+
+add_task(async function test_numberOfSiteOriginsMultipleNavigations() {
+  const histogram = TelemetryTestUtils.getAndClearHistogram(histogramName);
+  const testPage = `${testRoot}contain_iframe.html`;
+
+  const tab = await BrowserTestUtils.openNewForegroundTab({
+    gBrowser,
+    opening: testPage,
+    waitForStateStop: true,
+  });
+
+  const wgpDestroyedPromises = [
+    windowGlobalDestroyed(tab.linkedBrowser.innerWindowID),
+  ];
+
+  // Navigate to an interstitial page.
+  await BrowserTestUtils.loadURI(tab.linkedBrowser, "about:blank");
+  await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+
+  // Navigate to another test page.
+  await BrowserTestUtils.loadURI(tab.linkedBrowser, testPage);
+  await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+
+  wgpDestroyedPromises.push(
+    windowGlobalDestroyed(tab.linkedBrowser.innerWindowID)
+  );
+
+  BrowserTestUtils.removeTab(tab);
+  await Promise.all(wgpDestroyedPromises);
+
+  // testPage has been loaded twice and contains two origins: mochi.test:8888
+  // and example.com.
+  TelemetryTestUtils.assertHistogram(histogram, 2, 2);
+});
+
+add_task(async function test_numberOfSiteOriginsAddAndRemove() {
+  const histogram = TelemetryTestUtils.getAndClearHistogram(histogramName);
+  const testPage = `${testRoot}blank_iframe.html`;
+
+  const tab = await BrowserTestUtils.openNewForegroundTab({
+    gBrowser,
+    opening: testPage,
+    waitForStateStop: true,
+  });
+
+  // Load a subdocument in the page's iframe.
+  await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+    const iframe = content.window.document.querySelector("iframe");
+    const loaded = new Promise(resolve => {
+      iframe.addEventListener("load", () => resolve(), { once: true });
+    });
+    iframe.src = "http://example.com";
+
+    await loaded;
+  });
+
+  // Load a *new* subdocument in the page's iframe. This will result in the page
+  // having had three different origins, but only two at any one time.
+  await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+    const iframe = content.window.document.querySelector("iframe");
+    const loaded = new Promise(resolve => {
+      iframe.addEventListener("load", () => resolve(), { once: true });
+    });
+    iframe.src = "http://example.org";
+
+    await loaded;
+  });
+
+  const wgpDestroyed = windowGlobalDestroyed(tab.linkedBrowser.innerWindowID);
+  BrowserTestUtils.removeTab(tab);
+  await wgpDestroyed;
+
+  // The page only ever had two origins at once.
+  TelemetryTestUtils.assertHistogram(histogram, 2, 1);
+});
--- a/docshell/base/BrowsingContext.cpp
+++ b/docshell/base/BrowsingContext.cpp
@@ -2118,26 +2118,35 @@ bool BrowsingContext::CanSet(FieldIndex<
   }
 
   // We must have access to the specified context.
   RefPtr<WindowContext> window = WindowContext::GetById(aValue);
   return window && window->GetBrowsingContext() == this;
 }
 
 void BrowsingContext::DidSet(FieldIndex<IDX_CurrentInnerWindowId>) {
+  RefPtr<WindowContext> prevWindowContext = mCurrentWindowContext.forget();
   mCurrentWindowContext = WindowContext::GetById(GetCurrentInnerWindowId());
   MOZ_ASSERT(
       !mCurrentWindowContext || mWindowContexts.Contains(mCurrentWindowContext),
       "WindowContext not registered?");
 
   // Clear our cached `children` value, to ensure that JS sees the up-to-date
   // value.
   BrowsingContext_Binding::ClearCachedChildrenValue(this);
 
   if (XRE_IsParentProcess()) {
+    if (prevWindowContext != mCurrentWindowContext) {
+      if (prevWindowContext) {
+        prevWindowContext->Canonical()->DidBecomeCurrentWindowGlobal(false);
+      }
+      if (mCurrentWindowContext) {
+        mCurrentWindowContext->Canonical()->DidBecomeCurrentWindowGlobal(true);
+      }
+    }
     BrowserParent::UpdateFocusFromBrowsingContext();
   }
 }
 
 bool BrowsingContext::CanSet(FieldIndex<IDX_IsPopupSpam>, const bool& aValue,
                              ContentParent* aSource) {
   // Ensure that we only mark a browsing context as popup spam once and never
   // unmark it.
--- a/dom/ipc/WindowGlobalParent.cpp
+++ b/dom/ipc/WindowGlobalParent.cpp
@@ -1,16 +1,18 @@
 /* -*- Mode: C++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 8 -*- */
 /* vim: set sw=2 ts=8 et tw=80 ft=cpp : */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "mozilla/dom/WindowGlobalParent.h"
 
+#include <algorithm>
+
 #include "mozilla/AsyncEventDispatcher.h"
 #include "mozilla/ClearOnShutdown.h"
 #include "mozilla/ipc/InProcessParent.h"
 #include "mozilla/dom/BrowserBridgeParent.h"
 #include "mozilla/dom/CanonicalBrowsingContext.h"
 #include "mozilla/dom/ClientInfo.h"
 #include "mozilla/dom/ClientIPCTypes.h"
 #include "mozilla/dom/ContentParent.h"
@@ -20,16 +22,17 @@
 #include "mozilla/dom/WindowGlobalActorsBinding.h"
 #include "mozilla/dom/WindowGlobalChild.h"
 #include "mozilla/dom/ChromeUtils.h"
 #include "mozilla/dom/ipc/IdType.h"
 #include "mozilla/dom/ipc/StructuredCloneData.h"
 #include "mozilla/ServoCSSParser.h"
 #include "mozilla/ServoStyleSet.h"
 #include "mozilla/StaticPrefs_network.h"
+#include "mozilla/Telemetry.h"
 #include "mozJSComponentLoader.h"
 #include "nsContentUtils.h"
 #include "nsDocShell.h"
 #include "nsError.h"
 #include "nsFrameLoader.h"
 #include "nsFrameLoaderOwner.h"
 #include "nsGlobalWindowInner.h"
 #include "nsQueryObject.h"
@@ -133,16 +136,53 @@ void WindowGlobalParent::Init() {
   if (!BrowsingContext()->IsDiscarded()) {
     BrowsingContext()->SetCurrentInnerWindowId(InnerWindowId());
   }
 
   nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
   if (obs) {
     obs->NotifyObservers(ToSupports(this), "window-global-created", nullptr);
   }
+
+  if (!BrowsingContext()->IsDiscarded() && ShouldTrackSiteOriginTelemetry()) {
+    mOriginCounter.emplace();
+    mOriginCounter->UpdateSiteOriginsFrom(this, /* aIncrease = */ true);
+  }
+}
+
+void WindowGlobalParent::OriginCounter::UpdateSiteOriginsFrom(
+    WindowGlobalParent* aParent, bool aIncrease) {
+  MOZ_RELEASE_ASSERT(aParent);
+
+  if (aParent->DocumentPrincipal()->GetIsContentPrincipal()) {
+    nsAutoCString origin;
+    aParent->DocumentPrincipal()->GetSiteOrigin(origin);
+
+    if (aIncrease) {
+      int32_t& count = mOriginMap.GetOrInsert(origin);
+      count += 1;
+      mMaxOrigins = std::max(mMaxOrigins, mOriginMap.Count());
+    } else if (auto entry = mOriginMap.Lookup(origin)) {
+      entry.Data() -= 1;
+
+      if (entry.Data() == 0) {
+        entry.Remove();
+      }
+    }
+  }
+}
+
+void WindowGlobalParent::OriginCounter::Accumulate() {
+  mozilla::Telemetry::Accumulate(
+      mozilla::Telemetry::HistogramID::
+          FX_NUMBER_OF_UNIQUE_SITE_ORIGINS_PER_DOCUMENT,
+      mMaxOrigins);
+
+  mMaxOrigins = 0;
+  mOriginMap.Clear();
 }
 
 /* static */
 already_AddRefed<WindowGlobalParent> WindowGlobalParent::GetByInnerWindowId(
     uint64_t aInnerWindowId) {
   if (!XRE_IsParentProcess()) {
     return nullptr;
   }
@@ -720,16 +760,20 @@ void WindowGlobalParent::ActorDestroy(Ac
     iter.Data()->AfterDestroy();
   }
   windowActors.Clear();
 
   nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
   if (obs) {
     obs->NotifyObservers(ToSupports(this), "window-global-destroyed", nullptr);
   }
+
+  if (mOriginCounter) {
+    mOriginCounter->Accumulate();
+  }
 }
 
 WindowGlobalParent::~WindowGlobalParent() {
   MOZ_ASSERT(!mWindowActors.Count());
 }
 
 JSObject* WindowGlobalParent::WrapObject(JSContext* aCx,
                                          JS::Handle<JSObject*> aGivenProto) {
@@ -744,16 +788,40 @@ nsIContentParent* WindowGlobalParent::Ge
   RefPtr<BrowserParent> browserParent = GetBrowserParent();
   if (!browserParent) {
     return nullptr;
   }
 
   return browserParent->Manager();
 }
 
+void WindowGlobalParent::DidBecomeCurrentWindowGlobal(bool aCurrent) {
+  WindowGlobalParent* top = BrowsingContext()->GetTopWindowContext();
+  if (top && top->mOriginCounter) {
+    top->mOriginCounter->UpdateSiteOriginsFrom(this,
+                                               /* aIncrease = */ aCurrent);
+  }
+}
+
+bool WindowGlobalParent::ShouldTrackSiteOriginTelemetry() {
+  CanonicalBrowsingContext* bc = BrowsingContext();
+
+  if (!bc->IsTopContent()) {
+    return false;
+  }
+
+  RefPtr<BrowserParent> browserParent = GetBrowserParent();
+  if (!browserParent ||
+      !IsWebRemoteType(browserParent->Manager()->GetRemoteType())) {
+    return false;
+  }
+
+  return DocumentPrincipal()->GetIsContentPrincipal();
+}
+
 NS_IMPL_CYCLE_COLLECTION_INHERITED(WindowGlobalParent, WindowContext,
                                    mWindowActors)
 
 NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(WindowGlobalParent,
                                                WindowContext)
 NS_IMPL_CYCLE_COLLECTION_TRACE_END
 
 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(WindowGlobalParent)
--- a/dom/ipc/WindowGlobalParent.h
+++ b/dom/ipc/WindowGlobalParent.h
@@ -4,23 +4,25 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #ifndef mozilla_dom_WindowGlobalParent_h
 #define mozilla_dom_WindowGlobalParent_h
 
 #include "mozilla/ContentBlockingLog.h"
 #include "mozilla/ContentBlockingNotifier.h"
+#include "mozilla/Maybe.h"
 #include "mozilla/RefPtr.h"
 #include "mozilla/dom/ClientInfo.h"
 #include "mozilla/dom/ClientIPCTypes.h"
 #include "mozilla/dom/DOMRect.h"
 #include "mozilla/dom/PWindowGlobalParent.h"
 #include "mozilla/dom/BrowserParent.h"
 #include "mozilla/dom/WindowContext.h"
+#include "nsDataHashtable.h"
 #include "nsRefPtrHashtable.h"
 #include "nsWrapperCache.h"
 #include "nsISupports.h"
 #include "nsIContentParent.h"
 #include "mozilla/dom/WindowGlobalActor.h"
 #include "mozilla/dom/CanonicalBrowsingContext.h"
 #include "mozilla/net/CookieJarSettings.h"
 
@@ -179,16 +181,18 @@ class WindowGlobalParent final : public 
   bool DocumentHasUserInteracted() { return mDocumentHasUserInteracted; }
 
   uint32_t SandboxFlags() { return mSandboxFlags; }
 
   bool GetDocumentBlockAllMixedContent() { return mBlockAllMixedContent; }
 
   bool GetDocumentUpgradeInsecureRequests() { return mUpgradeInsecureRequests; }
 
+  void DidBecomeCurrentWindowGlobal(bool aCurrent);
+
  protected:
   const nsAString& GetRemoteType() override;
   JSActor::Type GetSide() override { return JSActor::Type::Parent; }
 
   // IPC messages
   mozilla::ipc::IPCResult RecvLoadURI(
       const MaybeDiscarded<dom::BrowsingContext>& aTargetBC,
       nsDocShellLoadState* aLoadState, bool aSetNavigating);
@@ -234,16 +238,18 @@ class WindowGlobalParent final : public 
 
  private:
   WindowGlobalParent(CanonicalBrowsingContext* aBrowsingContext,
                      uint64_t aInnerWindowId, uint64_t aOuterWindowId,
                      bool aInProcess, FieldTuple&& aFields);
 
   ~WindowGlobalParent();
 
+  bool ShouldTrackSiteOriginTelemetry();
+
   // NOTE: This document principal doesn't reflect possible |document.domain|
   // mutations which may have been made in the actual document.
   nsCOMPtr<nsIPrincipal> mDocumentPrincipal;
   nsCOMPtr<nsIPrincipal> mDocContentBlockingAllowListPrincipal;
   nsCOMPtr<nsIURI> mDocumentURI;
   nsString mDocumentTitle;
 
   nsRefPtrHashtable<nsCStringHashKey, JSWindowActorParent> mWindowActors;
@@ -256,16 +262,30 @@ class WindowGlobalParent final : public 
   // this WindowGlobalParent. This is only stored on top-level documents and
   // includes the activity log for all of the nested subdocuments as well.
   ContentBlockingLog mContentBlockingLog;
 
   Maybe<ClientInfo> mClientInfo;
   // Fields being mirrored from the corresponding document
   nsCOMPtr<nsICookieJarSettings> mCookieJarSettings;
   uint32_t mSandboxFlags;
+
+  struct OriginCounter {
+    void UpdateSiteOriginsFrom(WindowGlobalParent* aParent, bool aIncrease);
+    void Accumulate();
+
+    nsDataHashtable<nsCStringHashKey, int32_t> mOriginMap;
+    uint32_t mMaxOrigins = 0;
+  };
+
+  // Used to collect unique site origin telemetry.
+  //
+  // Is only Some() on top-level content windows.
+  Maybe<OriginCounter> mOriginCounter;
+
   bool mDocumentHasLoaded;
   bool mDocumentHasUserInteracted;
   bool mBlockAllMixedContent;
   bool mUpgradeInsecureRequests;
 };
 
 }  // namespace dom
 }  // namespace mozilla
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -6974,16 +6974,28 @@
     "kind": "exponential",
     "high": 100,
     "n_buckets": 50,
     "description": "When a document is loaded, report the number of unique site origins across the browser(all tabs) if it has been at least 5 minutes since last time we collect this data",
     "bug_numbers": [1589700],
     "alert_emails": ["sefeng@mozilla.com", "perfteam@mozilla.com"],
     "releaseChannelCollection": "opt-out"
   },
+  "FX_NUMBER_OF_UNIQUE_SITE_ORIGINS_PER_DOCUMENT": {
+    "record_in_processes": ["main"],
+    "products": ["firefox", "geckoview_streaming"],
+    "expires_in_version": "never",
+    "kind": "exponential",
+    "high": 100,
+    "n_buckets": 50,
+    "description": "When a document is unloaded, report the highest number of site origins loaded simultaneously in that document.",
+    "bug_numbers": [1603185],
+    "alert_emails": ["barret@mozilla.com", "perfteam@mozilla.com"],
+    "releaseChannelCollection": "opt-out"
+  },
   "FX_TAB_SWITCH_REQUEST_TAB_WARMING_STATE": {
     "record_in_processes": ["main"],
     "products": ["firefox"],
     "expires_in_version": "81",
     "alert_emails": ["mconley@mozilla.com"],
     "releaseChannelCollection": "opt-out",
     "kind": "categorical",
     "labels": [
--- a/toolkit/components/telemetry/geckoview/streaming/metrics.yaml
+++ b/toolkit/components/telemetry/geckoview/streaming/metrics.yaml
@@ -135,16 +135,40 @@ geckoview:
       - https://bugzilla.mozilla.org/show_bug.cgi?id=1589700
     data_reviews:
       - https://bugzilla.mozilla.org/show_bug.cgi?id=1589700#c5
     notification_emails:
       - sefeng@mozilla.com
       - perfteam@mozilla.com
     expires: never
 
+  per_document_site_origins:
+    type: custom_distribution
+    description: >
+      When a document is unloaded, report the highest number of
+      [site origins](https://searchfox.org/
+      mozilla-central/rev/
+      3300072e993ae05d50d5c63d815260367eaf9179/
+      caps/nsIPrincipal.idl#264) loaded simultaneously in that
+      document.
+    range_min: 0
+    range_max: 100
+    bucket_count: 50
+    histogram_type: exponential
+    unit: number of site origins per document
+    gecko_datapoint: FX_NUMBER_OF_UNIQUE_SITE_ORIGINS_PER_DOCUMENT
+    bugs:
+      - https://bugzilla.mozilla.org/show_bug.cgi?id=1603185
+    data_reviews:
+      - https://bugzilla.mozilla.org/show_bug.cgi?id=1603185#c13
+    notification_emails:
+      - barret@mozilla.com
+      - perfteam@mozilla.com
+    expires: never
+
 gfx:
   composite_time:
     type: timing_distribution
     time_unit: millisecond
     gecko_datapoint: COMPOSITE_TIME
     description: >
       The time taken to composite a frame.
       On non-webrender this is the time taken in