Bug 1312754 - Add a service to throttle certain HTTP channels. r=mayhemer
authorNicholas Hurley <hurley@todesschaf.org>
Tue, 10 Jan 2017 06:39:18 -0800
changeset 343145 145410f30206db4b6254b812c1a2a52fb90748e5
parent 343144 469857ed52b98880b892012b9210a5b999c19974
child 343146 9906e1364f049e794a741ca7ff93d16ded5bdff9
push id31371
push usercbook@mozilla.com
push dateThu, 16 Feb 2017 12:15:11 +0000
treeherdermozilla-central@8c8b54b13be7 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmayhemer
bugs1312754
milestone54.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 1312754 - Add a service to throttle certain HTTP channels. r=mayhemer This patch adds (and hooks up) a new service so that HTTP channels marked as "Throttleable" will periodically be Suspend()ed and Resume()d when more important operations are going (such as a page load). While this patch is not responsible for marking channels as "Throttleable", the general idea is that these would be less-important channels - background downloads, beacons, etc, and perhaps even resources known to be trackers. MozReview-Commit-ID: HEZsxS04rRK
docshell/base/nsDocShell.cpp
docshell/base/nsDocShell.h
modules/libpref/init/all.js
netwerk/base/ThrottlingService.cpp
netwerk/base/ThrottlingService.h
netwerk/base/moz.build
netwerk/base/nsIThrottlingService.idl
netwerk/build/nsNetCID.h
netwerk/build/nsNetModule.cpp
netwerk/ipc/NeckoParent.cpp
netwerk/ipc/NeckoParent.h
netwerk/ipc/PNecko.ipdl
netwerk/protocol/http/HttpBaseChannel.cpp
netwerk/protocol/http/nsHttpChannel.cpp
netwerk/protocol/http/nsHttpHandler.cpp
netwerk/protocol/http/nsHttpHandler.h
--- a/docshell/base/nsDocShell.cpp
+++ b/docshell/base/nsDocShell.cpp
@@ -7557,16 +7557,20 @@ nsDocShell::OnSecurityChange(nsIWebProgr
   NS_NOTREACHED("notification excluded in AddProgressListener(...)");
   return NS_OK;
 }
 
 nsresult
 nsDocShell::EndPageLoad(nsIWebProgress* aProgress,
                         nsIChannel* aChannel, nsresult aStatus)
 {
+  // We can release any pressure we may have had on the throttling service and
+  // let background channels continue.
+  mThrottler.reset();
+
   if (!aChannel) {
     return NS_ERROR_NULL_POINTER;
   }
 
   nsCOMPtr<nsIConsoleReportCollector> reporter = do_QueryInterface(aChannel);
   if (reporter) {
     nsCOMPtr<nsILoadGroup> loadGroup;
     aChannel->GetLoadGroup(getter_AddRefs(loadGroup));
@@ -10755,16 +10759,26 @@ nsDocShell::InternalLoad(nsIURI* aURI,
   attrs.Inherit(GetOriginAttributes());
   attrs.SetFirstPartyDomain(isTopLevelDoc, aURI);
 
   net::PredictorLearn(aURI, nullptr,
                       nsINetworkPredictor::LEARN_LOAD_TOPLEVEL, attrs);
   net::PredictorPredict(aURI, nullptr,
                         nsINetworkPredictor::PREDICT_LOAD, attrs, nullptr);
 
+  // Increase pressure on the throttling service so background channels will be
+  // appropriately de-prioritized. We need to explicitly check for http[s] here
+  // so that we don't throttle while loading, say, about:blank.
+  bool isHTTP, isHTTPS;
+  aURI->SchemeIs("http", &isHTTP);
+  aURI->SchemeIs("https", &isHTTPS);
+  if (isHTTP || isHTTPS) {
+    mThrottler.reset(new mozilla::net::Throttler());
+  }
+
   nsCOMPtr<nsIRequest> req;
   rv = DoURILoad(aURI, aOriginalURI, aLoadReplace, aReferrer,
                  !(aFlags & INTERNAL_LOAD_FLAGS_DONT_SEND_REFERRER),
                  aReferrerPolicy,
                  aTriggeringPrincipal, principalToInherit, aTypeHint,
                  aFileName, aPostData, aHeadersData,
                  aFirstParty, aDocShell, getter_AddRefs(req),
                  (aFlags & INTERNAL_LOAD_FLAGS_FIRST_LOAD) != 0,
--- a/docshell/base/nsDocShell.h
+++ b/docshell/base/nsDocShell.h
@@ -55,16 +55,17 @@
 #include "nsILinkHandler.h"
 #include "nsIClipboardCommands.h"
 #include "nsITabParent.h"
 #include "nsCRT.h"
 #include "prtime.h"
 #include "nsRect.h"
 #include "Units.h"
 #include "nsIDeprecationWarner.h"
+#include "nsIThrottlingService.h"
 
 namespace mozilla {
 namespace dom {
 class EventTarget;
 class PendingGlobalHistoryEntry;
 typedef uint32_t ScreenOrientationInternal;
 } // namespace dom
 } // namespace mozilla
@@ -1090,11 +1091,14 @@ public:
     NS_DECL_THREADSAFE_ISUPPORTS
     NS_DECL_NSIINTERFACEREQUESTOR
 
   protected:
     virtual ~InterfaceRequestorProxy();
     InterfaceRequestorProxy() {}
     nsWeakPtr mWeakPtr;
   };
+
+private:
+  mozilla::UniquePtr<mozilla::net::Throttler> mThrottler;
 };
 
 #endif /* nsDocShell_h__ */
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -1985,16 +1985,22 @@ pref("network.auth.subresource-http-auth
 // for credentials from the user explicitly.
 // If set to true, and a server URL conforms other conditions for sending default
 // credentials, those will be sent automatically in Private Browsing windows.
 //
 // This preference has no effect when the browser is set to "Never Remember History",
 // in that case default credentials will always be used.
 pref("network.auth.private-browsing-sso", false);
 
+// Control how the throttling service works - number of ms that each
+// suspend and resume period lasts (prefs named appropriately)
+pref("network.throttle.suspend-for", 2000);
+pref("network.throttle.resume-for", 2000);
+pref("network.throttle.enable", true);
+
 pref("permissions.default.image",           1); // 1-Accept, 2-Deny, 3-dontAcceptForeign
 
 pref("network.proxy.type",                  5);
 pref("network.proxy.ftp",                   "");
 pref("network.proxy.ftp_port",              0);
 pref("network.proxy.http",                  "");
 pref("network.proxy.http_port",             0);
 pref("network.proxy.ssl",                   "");
new file mode 100644
--- /dev/null
+++ b/netwerk/base/ThrottlingService.cpp
@@ -0,0 +1,445 @@
+/* vim: set ts=2 sts=2 et sw=2: */
+/* 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/. */
+
+
+ // Things to think about
+ //  * do we need to be multithreaded, or is mt-only ok?
+
+#include "ThrottlingService.h"
+
+#include "nsIHttpChannel.h"
+#include "nsIObserverService.h"
+
+#include "mozilla/Preferences.h"
+#include "mozilla/Services.h"
+
+#include "mozilla/net/NeckoChild.h"
+
+namespace mozilla {
+namespace net{
+
+static const char kEnabledPref[] = "network.throttle.enable";
+static const bool kDefaultEnabled = true;
+static const char kSuspendPeriodPref[] = "network.throttle.suspend-for";
+static const uint32_t kDefaultSuspendPeriod = 2000;
+static const char kResumePeriodPref[] = "network.throttle.resume-for";
+static const uint32_t kDefaultResumePeriod = 2000;
+
+NS_IMPL_ISUPPORTS(ThrottlingService, nsIThrottlingService, nsIObserver, nsITimerCallback)
+
+ThrottlingService::ThrottlingService()
+  :mEnabled(kDefaultEnabled)
+  ,mInitCalled(false)
+  ,mSuspended(false)
+  ,mPressureCount(0)
+  ,mSuspendPeriod(kDefaultSuspendPeriod)
+  ,mResumePeriod(kDefaultResumePeriod)
+  ,mIteratingHash(false)
+{
+}
+
+ThrottlingService::~ThrottlingService()
+{
+  Shutdown();
+}
+
+
+nsresult
+ThrottlingService::Init()
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  MOZ_ASSERT(!mInitCalled);
+
+  mInitCalled = true;
+
+  nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+  if (!obs) {
+    return NS_ERROR_UNEXPECTED;
+  }
+
+  nsresult rv = obs->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  mEnabled = Preferences::GetBool(kEnabledPref, kDefaultEnabled);
+  rv = Preferences::AddStrongObserver(this, kEnabledPref);
+  NS_ENSURE_SUCCESS(rv, rv);
+  Preferences::AddUintVarCache(&mSuspendPeriod, kSuspendPeriodPref, kDefaultSuspendPeriod);
+  Preferences::AddUintVarCache(&mResumePeriod, kResumePeriodPref, kDefaultResumePeriod);
+
+  mTimer = do_CreateInstance(NS_TIMER_CONTRACTID);
+
+  return NS_OK;
+}
+
+void
+ThrottlingService::Shutdown()
+{
+  if (!mInitCalled) {
+    return;
+  }
+
+  if (mTimer) {
+    mTimer->Cancel();
+  }
+
+  nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+  if (obs) {
+    obs->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID);
+  }
+
+  Preferences::RemoveObserver(this, kEnabledPref);
+
+  MaybeResumeAll();
+  mChannelHash.Clear();
+}
+
+nsresult
+ThrottlingService::Create(nsISupports *outer, const nsIID& iid, void **result)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+
+  if (outer != nullptr) {
+    return NS_ERROR_NO_AGGREGATION;
+  }
+
+  RefPtr<ThrottlingService> svc = new ThrottlingService();
+  if (!IsNeckoChild()) {
+    // We only need to do any work on the parent, so only bother initializing
+    // there. Child-side, we'll just error out since we only deal with parent
+    // channels.)
+    nsresult rv = svc->Init();
+    NS_ENSURE_SUCCESS(rv, rv);
+  }
+
+  return svc->QueryInterface(iid, result);
+}
+
+// nsIThrottlingService
+
+nsresult
+ThrottlingService::AddChannel(nsIHttpChannel *channel)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+
+  // We don't check mEnabled, because we always want to put channels in the hash
+  // to avoid potential inconsistencies in the case where the user changes the
+  // enabled pref at run-time.
+
+  if (IsNeckoChild()) {
+    return NS_ERROR_NOT_AVAILABLE;
+  }
+
+  nsAutoCString strKey;
+  nsresult rv = channel->GetChannelId(strKey);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  nsID key;
+  if (!key.Parse(strKey.get())) {
+    return NS_ERROR_UNEXPECTED;
+  }
+
+  if (mChannelHash.Get(key, nullptr)) {
+    // We already have this channel under our control, not adding it again.
+    MOZ_ASSERT(false, "Trying to throttle an already-throttled channel");
+    return NS_ERROR_ILLEGAL_VALUE;
+  }
+
+  if (!mIteratingHash) {
+    // This should be the common case, and as such is easy to handle
+    mChannelHash.Put(key, channel);
+
+    if (mSuspended) {
+      channel->Suspend();
+    }
+  } else {
+    // This gets tricky - we've somehow re-entrantly gotten here through the
+    // hash iteration in one of MaybeSuspendAll or MaybeResumeAll. Keep track
+    // of the fact that this add came in now, and once we're done iterating, we
+    // can add this into the hash. This avoids unexpectedly modifying the hash
+    // while it's being iterated over, which could lead to inconsistencies.
+    mChannelsToAddRemove.AppendElement(channel);
+    mChannelIsAdd.AppendElement(true);
+  }
+
+  return NS_OK;
+}
+
+nsresult
+ThrottlingService::RemoveChannel(nsIHttpChannel *channel)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+
+  // Just like above, don't worry about mEnabled to avoid inconsistencies when
+  // the pref changes at run-time
+
+  if (IsNeckoChild()) {
+    return NS_ERROR_NOT_AVAILABLE;
+  }
+
+  nsAutoCString strKey;
+  nsresult rv = channel->GetChannelId(strKey);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  nsID key;
+  if (!key.Parse(strKey.get())) {
+    return NS_ERROR_UNEXPECTED;
+  }
+
+  if (!mChannelHash.Get(key, nullptr)) {
+    // TODO - warn?
+    return NS_ERROR_ILLEGAL_VALUE;
+  }
+
+  if (!mIteratingHash) {
+    // This should be the common case, and easy to handle.
+    mChannelHash.Remove(key);
+
+    if (mSuspended) {
+      // This channel is no longer under our control for suspend/resume, but
+      // we've suspended it. Time to let it go.
+      channel->Resume();
+    }
+  } else {
+    // This gets tricky - we've somehow re-entrantly gotten here through the
+    // hash iteration in one of MaybeSuspendAll or MaybeResumeAll. Keep track
+    // of the fact that this add came in now, and once we're done iterating, we
+    // can add this into the hash. This avoids unexpectedly modifying the hash
+    // while it's being iterated over, which could lead to inconsistencies.
+    mChannelsToAddRemove.AppendElement(channel);
+    mChannelIsAdd.AppendElement(false);
+  }
+
+  return NS_OK;
+}
+
+nsresult
+ThrottlingService::IncreasePressure()
+{
+  MOZ_ASSERT(NS_IsMainThread());
+
+  // Just like add/removing channels, we don't check mEnabled here in order to
+  // avoid inconsistencies that could occur if the pref is flipped at runtime
+
+  if (IsNeckoChild()) {
+    return NS_ERROR_NOT_AVAILABLE;
+  }
+
+  if (mPressureCount++ == 0) {
+    MOZ_ASSERT(!mSuspended, "Suspended with 0 pressure?");
+    MaybeSuspendAll();
+    if (mSuspended) {
+      // MaybeSuspendAll() may not actually suspend things, and we only want to
+      // bother setting a timer to resume if we actually suspended.
+      mTimer->InitWithCallback(this, mSuspendPeriod, nsITimer::TYPE_ONE_SHOT);
+    }
+  }
+
+  return NS_OK;
+}
+
+nsresult
+ThrottlingService::DecreasePressure()
+{
+  MOZ_ASSERT(NS_IsMainThread());
+
+  // Just like add/removing channels, we don't check mEnabled here in order to
+  // avoid inconsistencies that could occur if the pref is flipped at runtime
+
+  if (IsNeckoChild()) {
+    return NS_ERROR_NOT_AVAILABLE;
+  }
+
+  MOZ_ASSERT(mPressureCount > 0, "Unbalanced throttle pressure");
+
+  if (--mPressureCount == 0) {
+    MaybeResumeAll();
+    mTimer->Cancel();
+  }
+
+  return NS_OK;
+}
+
+// nsIObserver
+
+nsresult
+ThrottlingService::Observe(nsISupports *subject, const char *topic,
+                           const char16_t *data_unicode)
+{
+  MOZ_ASSERT(!IsNeckoChild());
+  MOZ_ASSERT(NS_IsMainThread());
+
+  if (!strcmp(NS_XPCOM_SHUTDOWN_OBSERVER_ID, topic)) {
+    Shutdown();
+  } else if (!strcmp("nsPref:changed", topic)) {
+    mEnabled = Preferences::GetBool(kEnabledPref, mEnabled);
+    if (mEnabled && mPressureCount) {
+      // We weren't enabled, but we are now, AND we're under pressure. Go ahead
+      // and suspend things.
+      MaybeSuspendAll();
+      if (mSuspended) {
+        mTimer->InitWithCallback(this, mSuspendPeriod, nsITimer::TYPE_ONE_SHOT);
+      }
+    } else if (!mEnabled) {
+      // We were enabled, but we aren't any longer. Make sure we aren't
+      // suspending channels and that we don't have any timer that wants to
+      // change things unexpectedly.
+      mTimer->Cancel();
+      MaybeResumeAll();
+    }
+  }
+
+  return NS_OK;
+}
+
+// nsITimerCallback
+
+nsresult
+ThrottlingService::Notify(nsITimer *timer)
+{
+  MOZ_ASSERT(!IsNeckoChild());
+  MOZ_ASSERT(NS_IsMainThread());
+  MOZ_ASSERT(timer == mTimer);
+
+  if (mSuspended) {
+    MaybeResumeAll();
+    // Always try to resume if we were suspended, but only time-limit the
+    // resumption if we're under pressure and we're enabled. If either of those
+    // conditions is false, it doesn't make any sense to set a timer to suspend
+    // things when we don't want to be suspended anyway.
+    if (mPressureCount && mEnabled) {
+      mTimer->InitWithCallback(this, mResumePeriod, nsITimer::TYPE_ONE_SHOT);
+    }
+  } else if (mPressureCount) {
+    MaybeSuspendAll();
+    if (mSuspended) {
+      // MaybeSuspendAll() may not actually suspend, and it only makes sense to
+      // set a timer to resume if we actually suspended the channels.
+      mTimer->InitWithCallback(this, mSuspendPeriod, nsITimer::TYPE_ONE_SHOT);
+    }
+  }
+
+  return NS_OK;
+}
+
+// Internal methods
+
+void
+ThrottlingService::MaybeSuspendAll()
+{
+  MOZ_ASSERT(!IsNeckoChild());
+  MOZ_ASSERT(NS_IsMainThread());
+
+  if (!mEnabled) {
+    // We don't actually suspend when disabled, even though it's possible we get
+    // called in that state in order to avoid inconsistencies in the hash and
+    // the count if the pref changes at runtime.
+    return;
+  }
+
+  if (mSuspended) {
+    // Already suspended, nothing to do!
+    return;
+  }
+  mSuspended = true;
+
+  IterateHash([](ChannelHash::Iterator &iter) -> void {
+    const nsCOMPtr<nsIHttpChannel> channel = iter.UserData();
+    channel->Suspend();
+  });
+}
+
+void
+ThrottlingService::MaybeResumeAll()
+{
+  MOZ_ASSERT(!IsNeckoChild());
+  MOZ_ASSERT(NS_IsMainThread());
+
+  if (!mSuspended) {
+    // Already resumed, nothing to do!
+    return;
+  }
+  mSuspended = false;
+
+  IterateHash([](ChannelHash::Iterator &iter) -> void {
+    const nsCOMPtr<nsIHttpChannel> channel = iter.UserData();
+    channel->Resume();
+  });
+}
+
+void
+ThrottlingService::IterateHash(void (* callback)(ChannelHash::Iterator &iter))
+{
+  MOZ_ASSERT(!mIteratingHash);
+  mIteratingHash = true;
+  for (ChannelHash::Iterator iter = mChannelHash.ConstIter(); !iter.Done(); iter.Next()) {
+    callback(iter);
+  }
+  mIteratingHash = false;
+  HandleExtraAddRemove();
+}
+
+void
+ThrottlingService::HandleExtraAddRemove()
+{
+  MOZ_ASSERT(!mIteratingHash);
+  MOZ_ASSERT(mChannelsToAddRemove.Length() == mChannelIsAdd.Length());
+
+  nsCOMArray<nsIHttpChannel> channelsToAddRemove;
+  channelsToAddRemove.SwapElements(mChannelsToAddRemove);
+
+  nsTArray<bool> channelIsAdd;
+  channelIsAdd.SwapElements(mChannelIsAdd);
+
+  for (size_t i = 0; i < channelsToAddRemove.Length(); ++i) {
+    if (channelIsAdd[i]) {
+      AddChannel(channelsToAddRemove[i]);
+    } else {
+      RemoveChannel(channelsToAddRemove[i]);
+    }
+  }
+
+  channelsToAddRemove.Clear();
+  channelIsAdd.Clear();
+}
+
+// The publicly available way to throttle things
+
+Throttler::Throttler()
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  if (IsNeckoChild()) {
+    if (gNeckoChild) {
+      // The child object may have already gone away, so we need to guard
+      // guard against deref'ing a nullptr here. If that's what happened, then
+      // our pageload won't be continuing anyway, so what we do is pretty much
+      // irrelevant.
+      gNeckoChild->SendIncreaseThrottlePressure();
+    }
+  } else {
+    mThrottlingService = do_GetService("@mozilla.org/network/throttling-service;1");
+    mThrottlingService->IncreasePressure();
+  }
+}
+
+Throttler::~Throttler()
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  if (IsNeckoChild()) {
+    if (gNeckoChild) {
+      // The child object may have already gone away, so we need to guard
+      // guard against deref'ing a nullptr here. If that's what happened, then
+      // NeckoParent::ActorDestroy will take care of releasing the pressure we
+      // created.
+      gNeckoChild->SendDecreaseThrottlePressure();
+    }
+  } else {
+    MOZ_RELEASE_ASSERT(mThrottlingService);
+    mThrottlingService->DecreasePressure();
+    mThrottlingService = nullptr;
+  }
+}
+
+}
+}
new file mode 100644
--- /dev/null
+++ b/netwerk/base/ThrottlingService.h
@@ -0,0 +1,67 @@
+/* vim: set ts=2 sts=2 et sw=2: */
+/* 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/. */
+
+#ifndef mozilla__net__ThrottlingService_h
+#define mozilla__net__ThrottlingService_h
+
+#include "nsIThrottlingService.h"
+
+#include "nsCOMArray.h"
+#include "nsCOMPtr.h"
+#include "nsInterfaceHashtable.h"
+#include "nsIObserver.h"
+#include "nsITimer.h"
+
+class nsIHttpChannel;
+
+namespace mozilla {
+namespace net {
+
+class ThrottlingService : public nsIThrottlingService
+                        , public nsIObserver
+                        , public nsITimerCallback
+{
+public:
+  NS_DECL_ISUPPORTS
+  NS_DECL_NSITHROTTLINGSERVICE
+  NS_DECL_NSIOBSERVER
+  NS_DECL_NSITIMERCALLBACK
+
+  ThrottlingService();
+
+  nsresult Init();
+  void Shutdown();
+  static nsresult Create(nsISupports *outer, const nsIID& iid, void **result);
+
+private:
+  virtual ~ThrottlingService();
+
+  void MaybeSuspendAll();
+  void MaybeResumeAll();
+
+  void HandleExtraAddRemove();
+
+  bool mEnabled;
+  bool mInitCalled;
+  bool mSuspended;
+  uint32_t mPressureCount;
+  uint32_t mSuspendPeriod; // How long we should Suspend() channels for
+  uint32_t mResumePeriod; // How long we should Resume() channels for
+  nsCOMPtr<nsITimer> mTimer;
+  typedef nsInterfaceHashtable<nsIDHashKey, nsIHttpChannel> ChannelHash;
+  ChannelHash mChannelHash;
+
+  // Used to avoid inconsistencies in the hash and the suspend/resume count of
+  // channels. See comments in AddChannel and RemoveChannel for details.
+  void IterateHash(void (* callback)(ChannelHash::Iterator &iter));
+  bool mIteratingHash;
+  nsCOMArray<nsIHttpChannel> mChannelsToAddRemove;
+  nsTArray<bool> mChannelIsAdd;
+};
+
+} // ::mozilla::net
+} // ::mozilla
+
+#endif // mozilla__net__ThrottlingService_h
--- a/netwerk/base/moz.build
+++ b/netwerk/base/moz.build
@@ -115,16 +115,17 @@ XPIDL_SOURCES += [
     'nsIStreamListenerTee.idl',
     'nsIStreamLoader.idl',
     'nsIStreamTransportService.idl',
     'nsISyncStreamListener.idl',
     'nsISystemProxySettings.idl',
     'nsIThreadRetargetableRequest.idl',
     'nsIThreadRetargetableStreamListener.idl',
     'nsIThrottledInputChannel.idl',
+    'nsIThrottlingService.idl',
     'nsITimedChannel.idl',
     'nsITLSServerSocket.idl',
     'nsITraceableChannel.idl',
     'nsITransport.idl',
     'nsIUDPSocket.idl',
     'nsIUnicharStreamLoader.idl',
     'nsIUploadChannel.idl',
     'nsIUploadChannel2.idl',
@@ -249,16 +250,17 @@ UNIFIED_SOURCES += [
     'PollableEvent.cpp',
     'Predictor.cpp',
     'ProxyAutoConfig.cpp',
     'RedirectChannelRegistrar.cpp',
     'RequestContextService.cpp',
     'SimpleBuffer.cpp',
     'StreamingProtocolService.cpp',
     'ThrottleQueue.cpp',
+    'ThrottlingService.cpp',
     'Tickler.cpp',
     'TLSServerSocket.cpp',
 ]
 
 if CONFIG['MOZ_RUST'] and CONFIG['MOZ_RUST_URLPARSE']:
     EXPORTS.mozilla.net += [ 'RustURL.h' ]
     UNIFIED_SOURCES += [ 'RustURL.cpp' ]
 
new file mode 100644
--- /dev/null
+++ b/netwerk/base/nsIThrottlingService.idl
@@ -0,0 +1,37 @@
+/* vim: set ts=2 sts=2 et sw=2: */
+/* 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 "nsISupports.idl"
+
+interface nsIHttpChannel;
+
+[builtinclass, uuid(c755ef98-b749-4f30-a658-1e6110013a66)]
+interface nsIThrottlingService : nsISupports
+{
+  void addChannel(in nsIHttpChannel channel);
+  void removeChannel(in nsIHttpChannel channel);
+
+  /* Don't call these directly, use mozilla::net::Throttler instead! */
+  void increasePressure();
+  void decreasePressure();
+};
+
+%{C++
+namespace mozilla {
+namespace net {
+
+class Throttler
+{
+public:
+  Throttler();
+  ~Throttler();
+
+private:
+  nsCOMPtr<nsIThrottlingService> mThrottlingService;
+};
+
+} // ::mozilla::net
+} // ::mozilla
+%}
--- a/netwerk/build/nsNetCID.h
+++ b/netwerk/build/nsNetCID.h
@@ -499,16 +499,27 @@
 #define NS_REQUESTCONTEXTSERVICE_CID \
 { /* d5499fa7-7ba8-49ff-9e30-1858b99ace69 */ \
     0xd5499fa7, \
     0x7ba8, \
     0x49ff, \
     {0x93, 0x30, 0x18, 0x58, 0xb9, 0x9a, 0xce, 0x69} \
 }
 
+// service implementing nsIThrottlingService
+#define NS_THROTTLINGSERVICE_CONTRACTID \
+    "@mozilla.org/network/throttling-service;1"
+#define NS_THROTTLINGSERVICE_CID \
+{ /* c1c48f2b-cb9c-415e-b4f9-5e4c3476ca86 */ \
+    0xc1c48f2b, \
+    0xcb9c, \
+    0x415e, \
+    {0xb4, 0xf9, 0x5e, 0x4c, 0x34, 0x76, 0xca, 0x86} \
+}
+
 /******************************************************************************
  * netwerk/cache/ classes
  */
 
 // service implementing nsICacheService.
 #define NS_CACHESERVICE_CONTRACTID \
     "@mozilla.org/network/cache-service;1"
 #define NS_CACHESERVICE_CID                          \
--- a/netwerk/build/nsNetModule.cpp
+++ b/netwerk/build/nsNetModule.cpp
@@ -33,16 +33,17 @@
 #include "nsApplicationCacheService.h"
 #include "nsMimeTypes.h"
 #include "nsDNSPrefetch.h"
 #include "nsAboutProtocolHandler.h"
 #include "nsXULAppAPI.h"
 #include "nsCategoryCache.h"
 #include "nsIContentSniffer.h"
 #include "Predictor.h"
+#include "ThrottlingService.h"
 #include "nsIThreadPool.h"
 #include "mozilla/net/NeckoChild.h"
 
 #include "nsNetCID.h"
 
 #ifndef XP_MACOSX
 #define BUILD_BINHEX_DECODER 1
 #endif
@@ -870,16 +871,17 @@ NS_DEFINE_NAMED_CID(NS_REDIRECTCHANNELRE
 NS_DEFINE_NAMED_CID(NS_CACHE_STORAGE_SERVICE_CID);
 NS_DEFINE_NAMED_CID(NS_NSILOADCONTEXTINFOFACTORY_CID);
 NS_DEFINE_NAMED_CID(NS_NETWORKPREDICTOR_CID);
 NS_DEFINE_NAMED_CID(NS_CAPTIVEPORTAL_CID);
 NS_DEFINE_NAMED_CID(NS_REQUESTCONTEXTSERVICE_CID);
 #ifdef BUILD_NETWORK_INFO_SERVICE
 NS_DEFINE_NAMED_CID(NETWORKINFOSERVICE_CID);
 #endif // BUILD_NETWORK_INFO_SERVICE
+NS_DEFINE_NAMED_CID(NS_THROTTLINGSERVICE_CID);
 
 static const mozilla::Module::CIDEntry kNeckoCIDs[] = {
     { &kNS_IOSERVICE_CID, false, nullptr, nsIOServiceConstructor },
     { &kNS_STREAMTRANSPORTSERVICE_CID, false, nullptr, nsStreamTransportServiceConstructor },
     { &kNS_SOCKETTRANSPORTSERVICE_CID, false, nullptr, nsSocketTransportServiceConstructor },
     { &kNS_SERVERSOCKET_CID, false, nullptr, nsServerSocketConstructor },
     { &kNS_TLSSERVERSOCKET_CID, false, nullptr, TLSServerSocketConstructor },
     { &kNS_UDPSOCKET_CID, false, nullptr, nsUDPSocketConstructor },
@@ -1024,16 +1026,17 @@ static const mozilla::Module::CIDEntry k
     { &kNS_CACHE_STORAGE_SERVICE_CID, false, nullptr, CacheStorageServiceConstructor },
     { &kNS_NSILOADCONTEXTINFOFACTORY_CID, false, nullptr, LoadContextInfoFactoryConstructor },
     { &kNS_NETWORKPREDICTOR_CID, false, nullptr, mozilla::net::Predictor::Create },
     { &kNS_CAPTIVEPORTAL_CID, false, nullptr, mozilla::net::CaptivePortalServiceConstructor },
     { &kNS_REQUESTCONTEXTSERVICE_CID, false, nullptr, RequestContextServiceConstructor },
 #ifdef BUILD_NETWORK_INFO_SERVICE
     { &kNETWORKINFOSERVICE_CID, false, nullptr, nsNetworkInfoServiceConstructor },
 #endif
+    { &kNS_THROTTLINGSERVICE_CID, false, nullptr, mozilla::net::ThrottlingService::Create },
     { nullptr }
 };
 
 static const mozilla::Module::ContractIDEntry kNeckoContracts[] = {
     { NS_IOSERVICE_CONTRACTID, &kNS_IOSERVICE_CID },
     { NS_NETUTIL_CONTRACTID, &kNS_IOSERVICE_CID },
     { NS_STREAMTRANSPORTSERVICE_CONTRACTID, &kNS_STREAMTRANSPORTSERVICE_CID },
     { NS_SOCKETTRANSPORTSERVICE_CONTRACTID, &kNS_SOCKETTRANSPORTSERVICE_CID },
@@ -1183,16 +1186,17 @@ static const mozilla::Module::ContractID
     { NS_CACHE_STORAGE_SERVICE_CONTRACTID2, &kNS_CACHE_STORAGE_SERVICE_CID },
     { NS_NSILOADCONTEXTINFOFACTORY_CONTRACTID, &kNS_NSILOADCONTEXTINFOFACTORY_CID },
     { NS_NETWORKPREDICTOR_CONTRACTID, &kNS_NETWORKPREDICTOR_CID },
     { NS_CAPTIVEPORTAL_CONTRACTID, &kNS_CAPTIVEPORTAL_CID },
     { NS_REQUESTCONTEXTSERVICE_CONTRACTID, &kNS_REQUESTCONTEXTSERVICE_CID },
 #ifdef BUILD_NETWORK_INFO_SERVICE
     { NETWORKINFOSERVICE_CONTRACT_ID, &kNETWORKINFOSERVICE_CID },
 #endif
+    { NS_THROTTLINGSERVICE_CONTRACTID, &kNS_THROTTLINGSERVICE_CID },
     { nullptr }
 };
 
 static const mozilla::Module kNeckoModule = {
     mozilla::Module::kVersion,
     kNeckoCIDs,
     kNeckoContracts,
     kNeckoCategories,
--- a/netwerk/ipc/NeckoParent.cpp
+++ b/netwerk/ipc/NeckoParent.cpp
@@ -39,16 +39,17 @@
 #include "nsEscape.h"
 #include "SerializedLoadContext.h"
 #include "nsAuthInformationHolder.h"
 #include "nsIAuthPromptCallback.h"
 #include "nsPrincipal.h"
 #include "nsINetworkPredictor.h"
 #include "nsINetworkPredictorVerifier.h"
 #include "nsISpeculativeConnect.h"
+#include "nsIThrottlingService.h"
 
 using mozilla::OriginAttributes;
 using mozilla::dom::ChromeUtils;
 using mozilla::dom::ContentParent;
 using mozilla::dom::TabContext;
 using mozilla::dom::TabParent;
 using mozilla::net::PTCPSocketParent;
 using mozilla::dom::TCPSocketParent;
@@ -886,10 +887,27 @@ NeckoParent::RecvRemoveRequestContext(co
 
   nsID id;
   id.Parse(rcid.BeginReading());
   rcsvc->RemoveRequestContext(id);
 
   return IPC_OK();
 }
 
+mozilla::ipc::IPCResult
+NeckoParent::RecvIncreaseThrottlePressure()
+{
+  mThrottlers.AppendElement(mozilla::UniquePtr<mozilla::net::Throttler>(new mozilla::net::Throttler));
+  return IPC_OK();
+}
+
+mozilla::ipc::IPCResult
+NeckoParent::RecvDecreaseThrottlePressure()
+{
+  MOZ_ASSERT(!mThrottlers.IsEmpty());
+  // We do this because we don't actually care which throttler gets removed,
+  // just that one of them does.
+  mThrottlers.RemoveElementAt(0);
+  return IPC_OK();
+}
+
 } // namespace net
 } // namespace mozilla
--- a/netwerk/ipc/NeckoParent.h
+++ b/netwerk/ipc/NeckoParent.h
@@ -5,16 +5,17 @@
  * 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/BasePrincipal.h"
 #include "mozilla/net/PNeckoParent.h"
 #include "mozilla/net/NeckoCommon.h"
 #include "nsIAuthPrompt2.h"
 #include "nsINetworkPredictor.h"
+#include "nsIThrottlingService.h"
 #include "nsNetUtil.h"
 
 #ifndef mozilla_net_NeckoParent_h
 #define mozilla_net_NeckoParent_h
 
 namespace mozilla {
 namespace net {
 
@@ -214,14 +215,21 @@ protected:
 
   virtual mozilla::ipc::IPCResult RecvPredLearn(const ipc::URIParams& aTargetURI,
                                                 const ipc::OptionalURIParams& aSourceURI,
                                                 const PredictorPredictReason& aReason,
                                                 const OriginAttributes& aOriginAttributes) override;
   virtual mozilla::ipc::IPCResult RecvPredReset() override;
 
   virtual mozilla::ipc::IPCResult RecvRemoveRequestContext(const nsCString& rcid) override;
+
+  /* Throttler messages */
+  virtual mozilla::ipc::IPCResult RecvIncreaseThrottlePressure() override;
+  virtual mozilla::ipc::IPCResult RecvDecreaseThrottlePressure() override;
+
+private:
+  nsTArray<mozilla::UniquePtr<mozilla::net::Throttler>> mThrottlers;
 };
 
 } // namespace net
 } // namespace mozilla
 
 #endif // mozilla_net_NeckoParent_h
--- a/netwerk/ipc/PNecko.ipdl
+++ b/netwerk/ipc/PNecko.ipdl
@@ -110,16 +110,22 @@ parent:
   async OnAuthAvailable(uint64_t callbackId, nsString user,
                         nsString password, nsString domain);
   async OnAuthCancelled(uint64_t callbackId, bool userCancel);
 
   async RemoveRequestContext(nsCString rcid);
 
   async PAltDataOutputStream(nsCString type, PHttpChannel channel);
 
+  /**
+   * Throttling of channels
+   */
+  async IncreaseThrottlePressure();
+  async DecreaseThrottlePressure();
+
 child:
   /*
    * Bring up the http auth prompt for a nested remote mozbrowser.
    * NestedFrameId is the id corresponding to the PBrowser.  It is the same id
    * that was passed to the PBrowserOrId param in to the PHttpChannel constructor
    */
   async AsyncAuthPromptForNestedFrame(TabId nestedFrameId, nsCString uri,
                                       nsString realm, uint64_t callbackId);
--- a/netwerk/protocol/http/HttpBaseChannel.cpp
+++ b/netwerk/protocol/http/HttpBaseChannel.cpp
@@ -56,16 +56,17 @@
 #include "mozilla/BinarySearch.h"
 #include "mozilla/DebugOnly.h"
 #include "mozilla/Move.h"
 #include "nsIHttpHeaderVisitor.h"
 #include "nsIMIMEInputStream.h"
 #include "nsIXULRuntime.h"
 #include "nsICacheInfoChannel.h"
 #include "nsIDOMWindowUtils.h"
+#include "nsIThrottlingService.h"
 
 #include <algorithm>
 #include "HttpBaseChannel.h"
 
 namespace mozilla {
 namespace net {
 
 static
@@ -2951,16 +2952,23 @@ HttpBaseChannel::SetNewListener(nsIStrea
 // HttpBaseChannel helpers
 //-----------------------------------------------------------------------------
 
 void
 HttpBaseChannel::ReleaseListeners()
 {
   MOZ_ASSERT(NS_IsMainThread(), "Should only be called on the main thread.");
 
+  if (mClassOfService & nsIClassOfService::Throttleable) {
+    nsIThrottlingService *throttler = gHttpHandler->GetThrottlingService();
+    if (throttler) {
+      throttler->RemoveChannel(this);
+    }
+  }
+
   mListener = nullptr;
   mListenerContext = nullptr;
   mCallbacks = nullptr;
   mProgressSink = nullptr;
   mCompressListener = nullptr;
 }
 
 void
--- a/netwerk/protocol/http/nsHttpChannel.cpp
+++ b/netwerk/protocol/http/nsHttpChannel.cpp
@@ -100,16 +100,17 @@
 #include "nsISocketProvider.h"
 #include "mozilla/net/Predictor.h"
 #include "mozilla/MathAlgorithms.h"
 #include "CacheControlParser.h"
 #include "nsMixedContentBlocker.h"
 #include "HSTSPrimerListener.h"
 #include "CacheStorageService.h"
 #include "HttpChannelParent.h"
+#include "nsIThrottlingService.h"
 
 #ifdef MOZ_TASK_TRACER
 #include "GeckoTaskTracer.h"
 #endif
 
 namespace mozilla { namespace net {
 
 namespace {
@@ -1345,16 +1346,26 @@ nsHttpChannel::CallOnStartRequest()
         mOnStartRequestCalled = true;
         if (NS_FAILED(rv))
             return rv;
     } else {
         NS_WARNING("OnStartRequest skipped because of null listener");
         mOnStartRequestCalled = true;
     }
 
+    if (mClassOfService & nsIClassOfService::Throttleable) {
+        nsIThrottlingService *throttler = gHttpHandler->GetThrottlingService();
+        if (throttler) {
+            // This may immediately Suspend() this channel. We also may have
+            // done this already, during AsyncOpen. However, calling AddChannel
+            // twice doesn't hurt anything.
+            throttler->AddChannel(this);
+        }
+    }
+
     // Install stream converter if required.
     // If we use unknownDecoder, stream converters will be installed later (in
     // nsUnknownDecoder) after OnStartRequest is called for the real listener.
     if (!unknownDecoderStarted) {
       nsCOMPtr<nsIStreamListener> listener;
       nsISupports *ctxt = mListenerContext;
       rv = DoApplyContentConversions(mListener, getter_AddRefs(listener), ctxt);
       if (NS_FAILED(rv)) {
@@ -6114,16 +6125,24 @@ nsHttpChannel::BeginConnect()
 
     // We may have been cancelled already, either by on-modify-request
     // listeners or load group observers; in that case, we should not send the
     // request to the server
     if (mCanceled) {
         return mStatus;
     }
 
+    if (mClassOfService & nsIClassOfService::Throttleable) {
+        nsIThrottlingService *throttler = gHttpHandler->GetThrottlingService();
+        if (throttler) {
+            // This may immediately Suspend() this channel.
+            throttler->AddChannel(this);
+        }
+    }
+
     if (!(mLoadFlags & LOAD_CLASSIFY_URI)) {
         return ContinueBeginConnectWithResult();
     }
 
     // mLocalBlocklist is true only if tracking protection is enabled and the
     // URI is a tracking domain, it makes no guarantees about phishing or
     // malware, so if LOAD_CLASSIFY_URI is true we must call
     // nsChannelClassifier to catch phishing and malware URIs.
--- a/netwerk/protocol/http/nsHttpHandler.cpp
+++ b/netwerk/protocol/http/nsHttpHandler.cpp
@@ -48,16 +48,17 @@
 #include "nsPIDOMWindow.h"
 #include "nsINetworkLinkService.h"
 #include "nsHttpChannelAuthProvider.h"
 #include "nsServiceManagerUtils.h"
 #include "nsComponentManagerUtils.h"
 #include "nsSocketTransportService2.h"
 #include "nsIOService.h"
 #include "nsIUUIDGenerator.h"
+#include "nsIThrottlingService.h"
 
 #include "mozilla/net/NeckoChild.h"
 #include "mozilla/net/NeckoParent.h"
 #include "mozilla/ipc/URIUtils.h"
 #include "mozilla/Telemetry.h"
 #include "mozilla/Unused.h"
 #include "mozilla/BasePrincipal.h"
 
@@ -584,16 +585,27 @@ nsresult
 nsHttpHandler::GetIOService(nsIIOService** result)
 {
     NS_ENSURE_ARG_POINTER(result);
 
     NS_ADDREF(*result = mIOService);
     return NS_OK;
 }
 
+nsIThrottlingService *
+nsHttpHandler::GetThrottlingService()
+{
+    if (!mThrottlingService) {
+        nsCOMPtr<nsIThrottlingService> service = do_GetService(NS_THROTTLINGSERVICE_CONTRACTID);
+        mThrottlingService = new nsMainThreadPtrHolder<nsIThrottlingService>(service);
+    }
+
+    return mThrottlingService;
+}
+
 uint32_t
 nsHttpHandler::Get32BitsOfPseudoRandom()
 {
     // only confirm rand seeding on socket thread
     MOZ_ASSERT(PR_GetCurrentThread() == gSocketThread);
 
     // rand() provides different amounts of PRNG on different platforms.
     // 15 or 31 bits are common amounts.
--- a/netwerk/protocol/http/nsHttpHandler.h
+++ b/netwerk/protocol/http/nsHttpHandler.h
@@ -22,16 +22,17 @@
 class nsIHttpChannel;
 class nsIPrefBranch;
 class nsICancelable;
 class nsICookieService;
 class nsIIOService;
 class nsIRequestContextService;
 class nsISiteSecurityService;
 class nsIStreamConverterService;
+class nsIThrottlingService;
 class nsITimer;
 class nsIUUIDGenerator;
 
 
 namespace mozilla {
 namespace net {
 
 extern Atomic<PRThread*, Relaxed> gSocketThread;
@@ -260,16 +261,17 @@ public:
     //
     // The HTTP handler caches pointers to specific XPCOM services, and
     // provides the following helper routines for accessing those services:
     //
     nsresult GetStreamConverterService(nsIStreamConverterService **);
     nsresult GetIOService(nsIIOService** service);
     nsICookieService * GetCookieService(); // not addrefed
     nsISiteSecurityService * GetSSService();
+    nsIThrottlingService * GetThrottlingService();
 
     // callable from socket thread only
     uint32_t Get32BitsOfPseudoRandom();
 
     // Called by the channel synchronously during asyncOpen
     void OnOpeningRequest(nsIHttpChannel *chan)
     {
         NotifyObservers(chan, NS_HTTP_ON_OPENING_REQUEST_TOPIC);
@@ -401,16 +403,17 @@ private:
     static void TimerCallback(nsITimer * aTimer, void * aClosure);
 private:
 
     // cached services
     nsMainThreadPtrHandle<nsIIOService>              mIOService;
     nsMainThreadPtrHandle<nsIStreamConverterService> mStreamConvSvc;
     nsMainThreadPtrHandle<nsICookieService>          mCookieService;
     nsMainThreadPtrHandle<nsISiteSecurityService>    mSSService;
+    nsMainThreadPtrHandle<nsIThrottlingService>      mThrottlingService;
 
     // the authentication credentials cache
     nsHttpAuthCache mAuthCache;
     nsHttpAuthCache mPrivateAuthCache;
 
     // the connection manager
     RefPtr<nsHttpConnectionMgr> mConnMgr;