Bug 1312754 - Add a service to throttle certain HTTP channels. r?mayhemer draft
authorNicholas Hurley <hurley@todesschaf.org>
Tue, 10 Jan 2017 06:39:18 -0800
changeset 484870 c2e9e14a78932ec259f779a382a1b4da4714513e
parent 484869 43cd2f9009ca666be903f2b3670e7611c56bd29e
child 545876 a3da691ed4b9750fdbc6e836e7b9326707fcca54
push id45575
push userbmo:hurley@mozilla.com
push dateWed, 15 Feb 2017 22:05:34 +0000
reviewersmayhemer
bugs1312754
milestone54.0a1
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
@@ -7561,16 +7561,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));
@@ -10744,16 +10748,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
@@ -1089,11 +1090,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
@@ -1979,16 +1979,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
@@ -871,16 +872,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 },
@@ -1025,16 +1027,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 },
@@ -1184,16 +1187,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
@@ -54,16 +54,17 @@
 #include "nsIConsoleService.h"
 #include "mozilla/BinarySearch.h"
 #include "mozilla/DebugOnly.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
@@ -2906,16 +2907,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
@@ -98,16 +98,17 @@
 #include "nsCORSListenerProxy.h"
 #include "nsISocketProvider.h"
 #include "mozilla/net/Predictor.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 {
@@ -1322,16 +1323,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)) {
@@ -6076,16 +6087,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;