Bug 1358060 - Allow postponing of unimportant resources opening during page load, class-of-service Tail flag. r=dragana
authorHonza Bambas <honzab.moz@firemni.cz>
Wed, 30 Aug 2017 09:32:00 -0400
changeset 377925 b37a0bd71bbb1f3e5b4f58f1936d9cc0a38851d2
parent 377924 10d42af6319d08daca29d5d163f301696b5ff1fd
child 377926 72b3c90a042b044d8e38d8bb71653e908d4c6baf
push id32417
push userarchaeopteryx@coole-files.de
push dateThu, 31 Aug 2017 12:37:11 +0000
treeherdermozilla-central@fb22415719a9 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdragana
bugs1358060
milestone57.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 1358060 - Allow postponing of unimportant resources opening during page load, class-of-service Tail flag. r=dragana
dom/base/nsDocument.cpp
dom/fetch/FetchDriver.cpp
dom/script/ScriptLoader.cpp
dom/xhr/XMLHttpRequestMainThread.cpp
modules/libpref/init/all.js
netwerk/base/RequestContextService.cpp
netwerk/base/nsChannelClassifier.cpp
netwerk/base/nsIClassOfService.idl
netwerk/base/nsIRequestContext.idl
netwerk/base/nsLoadGroup.cpp
netwerk/ipc/NeckoParent.cpp
netwerk/ipc/NeckoParent.h
netwerk/ipc/PNecko.ipdl
netwerk/protocol/http/HttpBaseChannel.cpp
netwerk/protocol/http/HttpBaseChannel.h
netwerk/protocol/http/nsHttpChannel.cpp
netwerk/protocol/http/nsHttpChannel.h
netwerk/protocol/http/nsHttpHandler.cpp
netwerk/protocol/http/nsHttpHandler.h
toolkit/components/places/FaviconHelpers.cpp
--- a/dom/base/nsDocument.cpp
+++ b/dom/base/nsDocument.cpp
@@ -94,16 +94,17 @@
 #include "nsThreadUtils.h"
 #include "nsNodeInfoManager.h"
 #include "nsIFileChannel.h"
 #include "nsIMultiPartChannel.h"
 #include "nsIRefreshURI.h"
 #include "nsIWebNavigation.h"
 #include "nsIScriptError.h"
 #include "nsISimpleEnumerator.h"
+#include "nsIRequestContext.h"
 #include "nsStyleSheetService.h"
 
 #include "nsNetUtil.h"     // for NS_NewURI
 #include "nsIInputStreamChannel.h"
 #include "nsIAuthPrompt.h"
 #include "nsIAuthPrompt2.h"
 
 #include "nsIScriptSecurityManager.h"
@@ -2325,16 +2326,28 @@ nsDocument::ResetToURI(nsIURI *aURI, nsI
   if (aLoadGroup) {
     mDocumentLoadGroup = do_GetWeakReference(aLoadGroup);
     // there was an assertion here that aLoadGroup was not null.  This
     // is no longer valid: nsDocShell::SetDocument does not create a
     // load group, and it works just fine
 
     // XXXbz what does "just fine" mean exactly?  And given that there
     // is no nsDocShell::SetDocument, what is this talking about?
+
+    // Inform the associated request context about this load start so
+    // any of its internal load progress flags gets reset.
+    nsCOMPtr<nsIRequestContextService> rcsvc =
+      do_GetService("@mozilla.org/network/request-context-service;1");
+    if (rcsvc) {
+      nsCOMPtr<nsIRequestContext> rc;
+      rcsvc->GetRequestContextFromLoadGroup(aLoadGroup, getter_AddRefs(rc));
+      if (rc) {
+        rc->BeginLoad();
+      }
+    }
   }
 
   mLastModified.Truncate();
   // XXXbz I guess we're assuming that the caller will either pass in
   // a channel with a useful type or call SetContentType?
   SetContentTypeInternal(EmptyCString());
   mContentLanguage.Truncate();
   mBaseTarget.Truncate();
--- a/dom/fetch/FetchDriver.cpp
+++ b/dom/fetch/FetchDriver.cpp
@@ -374,16 +374,19 @@ FetchDriver::HttpFetch()
     mRequest->Headers()->GetUnsafeHeaders(unsafeHeaders);
     nsCOMPtr<nsILoadInfo> loadInfo = chan->GetLoadInfo();
     if (loadInfo) {
       loadInfo->SetCorsPreflightInfo(unsafeHeaders, false);
     }
   }
 
   if (mIsTrackingFetch && nsContentUtils::IsLowerNetworkPriority()) {
+    cos->AddClassFlags(nsIClassOfService::Throttleable |
+                       nsIClassOfService::Tail);
+
     nsCOMPtr<nsISupportsPriority> p = do_QueryInterface(chan);
     if (p) {
       p->SetPriority(nsISupportsPriority::PRIORITY_LOWEST);
     }
   }
 
   rv = chan->AsyncOpen2(this);
   NS_ENSURE_SUCCESS(rv, rv);
--- a/dom/script/ScriptLoader.cpp
+++ b/dom/script/ScriptLoader.cpp
@@ -132,20 +132,23 @@ ScriptLoader::ScriptLoader(nsIDocument *
     mEnabled(true),
     mDeferEnabled(false),
     mDocumentParsingDone(false),
     mBlockingDOMContentLoaded(false),
     mLoadEventFired(false),
     mGiveUpEncoding(false),
     mReporter(new ConsoleReportCollector())
 {
+  LOG(("ScriptLoader::ScriptLoader %p", this));
 }
 
 ScriptLoader::~ScriptLoader()
 {
+  LOG(("ScriptLoader::~ScriptLoader %p", this));
+
   mObservers.Clear();
 
   if (mParserBlockingRequest) {
     mParserBlockingRequest->FireScriptAvailable(NS_ERROR_ABORT);
   }
 
   for (ScriptLoadRequest* req = mXSLTRequests.getFirst(); req;
        req = req->getNext()) {
@@ -1050,25 +1053,42 @@ ScriptLoader::StartLoad(ScriptLoadReques
       cic->PreferAlternativeDataType(kNullMimeType);
     }
   }
 
   nsIScriptElement* script = aRequest->mElement;
   bool async = script ? script->GetScriptAsync() : aRequest->mPreloadAsAsync;
   bool defer = script ? script->GetScriptDeferred() : aRequest->mPreloadAsDefer;
 
+  LOG(("ScriptLoadRequest (%p): async=%d defer=%d tracking=%d",
+       aRequest, async, defer, aRequest->IsTracking()));
+
   nsCOMPtr<nsIClassOfService> cos(do_QueryInterface(channel));
   if (cos) {
     if (aRequest->mScriptFromHead && !async && !defer) {
       // synchronous head scripts block loading of most other non js/css
-      // content such as images
+      // content such as images, Leader implicitely disallows tailing
       cos->AddClassFlags(nsIClassOfService::Leader);
-    } else if (!defer) {
-      // other scripts are neither blocked nor prioritized unless marked deferred
+    } else if (defer && !async) {
+      // head/body deferred scripts are blocked by leaders but are not
+      // allowed tailing because they block DOMContentLoaded
+      cos->AddClassFlags(nsIClassOfService::TailForbidden);
+    } else {
+      // other scripts (=body sync or head/body async) are neither blocked
+      // nor prioritized
       cos->AddClassFlags(nsIClassOfService::Unblocked);
+
+      if (async) {
+        // async scripts are allowed tailing, since those and only those
+        // don't block DOMContentLoaded; this flag doesn't enforce tailing,
+        // just overweights the Unblocked flag when the channel is found
+        // to be a thrird-party tracker and thus set the Tail flag to engage
+        // tailing.
+        cos->AddClassFlags(nsIClassOfService::TailAllowed);
+      }
     }
   }
 
   nsCOMPtr<nsIHttpChannel> httpChannel(do_QueryInterface(channel));
   if (httpChannel) {
     // HTTP content negotation has little value in this context.
     rv = httpChannel->SetRequestHeader(NS_LITERAL_CSTRING("Accept"),
                                        NS_LITERAL_CSTRING("*/*"),
@@ -1201,18 +1221,21 @@ CSPAllowsInlineScript(nsIScriptElement* 
 
 ScriptLoadRequest*
 ScriptLoader::CreateLoadRequest(ScriptKind aKind,
                                 nsIScriptElement* aElement,
                                 uint32_t aVersion, CORSMode aCORSMode,
                                 const SRIMetadata& aIntegrity)
 {
   if (aKind == ScriptKind::Classic) {
-    return new ScriptLoadRequest(aKind, aElement, aVersion, aCORSMode,
+    ScriptLoadRequest* slr = new ScriptLoadRequest(aKind, aElement, aVersion, aCORSMode,
                                  aIntegrity);
+
+    LOG(("ScriptLoader %p creates ScriptLoadRequest %p", this, slr));
+    return slr;
   }
 
   MOZ_ASSERT(aKind == ScriptKind::Module);
   return new ModuleLoadRequest(aElement, aVersion, aCORSMode, aIntegrity, this);
 }
 
 bool
 ScriptLoader::ProcessScriptElement(nsIScriptElement* aElement)
--- a/dom/xhr/XMLHttpRequestMainThread.cpp
+++ b/dom/xhr/XMLHttpRequestMainThread.cpp
@@ -40,16 +40,17 @@
 #include "nsReadableUtils.h"
 
 #include "nsIURI.h"
 #include "nsILoadGroup.h"
 #include "nsNetUtil.h"
 #include "nsStringStream.h"
 #include "nsIAuthPrompt.h"
 #include "nsIAuthPrompt2.h"
+#include "nsIClassOfService.h"
 #include "nsIOutputStream.h"
 #include "nsISupportsPrimitives.h"
 #include "nsISupportsPriority.h"
 #include "nsIInterfaceRequestorUtils.h"
 #include "nsStreamUtils.h"
 #include "nsThreadUtils.h"
 #include "nsIUploadChannel.h"
 #include "nsIUploadChannel2.h"
@@ -2615,22 +2616,29 @@ XMLHttpRequestMainThread::MaybeLowerChan
   if (!nsJSUtils::GetCallingLocation(cx, fileNameString)) {
     return;
   }
 
   if (!doc->IsScriptTracking(fileNameString)) {
     return;
   }
 
+  nsCOMPtr<nsIClassOfService> cos = do_QueryInterface(mChannel);
+  if (cos) {
+    // Adding TailAllowed to overrule the Unblocked flag, but to preserve
+    // the effect of Unblocked when tailing is off.
+    cos->AddClassFlags(nsIClassOfService::Throttleable |
+                       nsIClassOfService::Tail |
+                       nsIClassOfService::TailAllowed);
+  }
+
   nsCOMPtr<nsISupportsPriority> p = do_QueryInterface(mChannel);
-  if (!p) {
-    return;
-  }
-
-  p->SetPriority(nsISupportsPriority::PRIORITY_LOWEST);
+  if (p) {
+    p->SetPriority(nsISupportsPriority::PRIORITY_LOWEST);
+  }
 }
 
 nsresult
 XMLHttpRequestMainThread::InitiateFetch(nsIInputStream* aUploadStream,
                                         int64_t aUploadLength,
                                         nsACString& aUploadContentType)
 {
   nsresult rv;
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -2235,16 +2235,29 @@ pref("network.http.throttle.resume-backg
 // throttle for this period of time.  This prevents comet and unresponsive
 // http requests to engage long-standing throttling.
 pref("network.http.throttle.time-window", 3000);
 
 // Give higher priority to requests resulting from a user interaction event
 // like click-to-play, image fancy-box zoom, navigation.
 pref("network.http.on_click_priority", true);
 
+// Some requests during a page load are marked as "tail", mainly trackers, but not only.
+// This pref controls whether such requests are put to the tail, behind other requests
+// emerging during page loading process.
+pref("network.http.tailing.enabled", true);
+// When the page load has not yet reached DOMContentLoaded point, tail requestes are delayed
+// by (non-tailed requests count + 1) * delay-quantum milliseconds.
+pref("network.http.tailing.delay-quantum", 600);
+// The same as above, but applied after the document load reached DOMContentLoaded event.
+pref("network.http.tailing.delay-quantum-after-domcontentloaded", 100);
+// Upper limit for the calculated delay, prevents long standing and comet-like requests
+// tail forever.  This is in milliseconds as well.
+pref("network.http.tailing.delay-max", 6000);
+
 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",                   "");
--- a/netwerk/base/RequestContextService.cpp
+++ b/netwerk/base/RequestContextService.cpp
@@ -9,59 +9,138 @@
 #include "nsIXULRuntime.h"
 #include "nsServiceManagerUtils.h"
 #include "nsThreadUtils.h"
 #include "RequestContextService.h"
 
 #include "mozilla/Atomics.h"
 #include "mozilla/Logging.h"
 #include "mozilla/Services.h"
+#include "mozilla/TimeStamp.h"
 
 #include "mozilla/net/PSpdyPush.h"
 
+#include "../protocol/http/nsHttpHandler.h"
+
 namespace mozilla {
 namespace net {
 
 LazyLogModule gRequestContextLog("RequestContext");
 #undef LOG
 #define LOG(args) MOZ_LOG(gRequestContextLog, LogLevel::Info, args)
 
 // nsIRequestContext
 class RequestContext final : public nsIRequestContext
+                           , public nsITimerCallback
 {
 public:
   NS_DECL_THREADSAFE_ISUPPORTS
   NS_DECL_NSIREQUESTCONTEXT
+  NS_DECL_NSITIMERCALLBACK
 
   explicit RequestContext(const uint64_t id);
 private:
   virtual ~RequestContext();
 
+  void ProcessTailQueue(nsresult aResult);
+  // Reschedules the timer if needed
+  void ScheduleUnblock();
+  // Hard-reschedules the timer
+  void RescheduleUntailTimer(TimeStamp const& now);
+
   uint64_t mID;
   Atomic<uint32_t>       mBlockingTransactionCount;
   nsAutoPtr<SpdyPushCache> mSpdyCache;
   nsCString mUserAgentOverride;
+
+  typedef nsCOMPtr<nsIRequestTailUnblockCallback> PendingTailRequest;
+  // Number of known opened non-tailed requets
+  uint32_t mNonTailRequests;
+  // Queue of requests that have been tailed, when conditions are met
+  // we call each of them to unblock and drop the reference
+  nsTArray<PendingTailRequest> mTailQueue;
+  // Loosly scheduled timer, never scheduled further to the future than
+  // mUntailAt time
+  nsCOMPtr<nsITimer> mUntailTimer;
+  // Timestamp when the timer is expected to fire,
+  // always less than or equal to mUntailAt
+  TimeStamp mTimerScheduledAt;
+  // Timestamp when we want to actually untail queued requets based on
+  // the number of request count change in the past; iff this timestamp
+  // is set, we tail requests
+  TimeStamp mUntailAt;
+
+  // This member is true only between DOMContentLoaded notification and
+  // next document load beginning for this request context.
+  // Top level request contexts are recycled.
+  bool mAfterDOMContentLoaded;
 };
 
-NS_IMPL_ISUPPORTS(RequestContext, nsIRequestContext)
+NS_IMPL_ISUPPORTS(RequestContext, nsIRequestContext, nsITimerCallback)
 
 RequestContext::RequestContext(const uint64_t aID)
   : mID(aID)
   , mBlockingTransactionCount(0)
+  , mNonTailRequests(0)
+  , mAfterDOMContentLoaded(false)
 {
   LOG(("RequestContext::RequestContext this=%p id=%" PRIx64, this, mID));
 }
 
 RequestContext::~RequestContext()
 {
+  MOZ_ASSERT(mTailQueue.Length() == 0);
+
   LOG(("RequestContext::~RequestContext this=%p blockers=%u",
        this, static_cast<uint32_t>(mBlockingTransactionCount)));
 }
 
 NS_IMETHODIMP
+RequestContext::BeginLoad()
+{
+  MOZ_ASSERT(NS_IsMainThread());
+
+  LOG(("RequestContext::BeginLoad %p", this));
+
+  if (IsNeckoChild() && gNeckoChild) {
+    // Tailing is not supported on the child process
+    gNeckoChild->SendRequestContextLoadBegin(mID);
+    return NS_OK;
+  }
+
+  mAfterDOMContentLoaded = false;
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+RequestContext::DOMContentLoaded()
+{
+  MOZ_ASSERT(NS_IsMainThread());
+
+  LOG(("RequestContext::DOMContentLoaded %p", this));
+
+  if (IsNeckoChild() && gNeckoChild) {
+    // Tailing is not supported on the child process
+    gNeckoChild->SendRequestContextAfterDOMContentLoaded(mID);
+    return NS_OK;
+  }
+
+  if (mAfterDOMContentLoaded) {
+    // There is a possibility of a duplicate notification
+    return NS_OK;
+  }
+
+  mAfterDOMContentLoaded = true;
+
+  // Conditions for the delay calculation has changed.
+  ScheduleUnblock();
+  return NS_OK;
+}
+
+NS_IMETHODIMP
 RequestContext::GetBlockingTransactionCount(uint32_t *aBlockingTransactionCount)
 {
   NS_ENSURE_ARG_POINTER(aBlockingTransactionCount);
   *aBlockingTransactionCount = mBlockingTransactionCount;
   return NS_OK;
 }
 
 NS_IMETHODIMP
@@ -115,16 +194,227 @@ RequestContext::GetUserAgentOverride(nsA
 
 NS_IMETHODIMP
 RequestContext::SetUserAgentOverride(const nsACString& aUserAgentOverride)
 {
   mUserAgentOverride = aUserAgentOverride;
   return NS_OK;
 }
 
+NS_IMETHODIMP
+RequestContext::AddNonTailRequest()
+{
+  MOZ_ASSERT(NS_IsMainThread());
+
+  ++mNonTailRequests;
+  LOG(("RequestContext::AddNonTailRequest this=%p, cnt=%u",
+       this, mNonTailRequests));
+
+  ScheduleUnblock();
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+RequestContext::RemoveNonTailRequest()
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  MOZ_ASSERT(mNonTailRequests > 0);
+
+  LOG(("RequestContext::RemoveNonTailRequest this=%p, cnt=%u",
+       this, mNonTailRequests - 1));
+
+  --mNonTailRequests;
+
+  ScheduleUnblock();
+  return NS_OK;
+}
+
+void
+RequestContext::ScheduleUnblock()
+{
+  MOZ_ASSERT(!IsNeckoChild());
+  MOZ_ASSERT(NS_IsMainThread());
+
+  if (!gHttpHandler) {
+    return;
+  }
+
+  uint32_t quantum = gHttpHandler->TailBlockingDelayQuantum(mAfterDOMContentLoaded);
+  uint32_t delayMax = gHttpHandler->TailBlockingDelayMax();
+
+  CheckedInt<uint32_t> delay = quantum * mNonTailRequests;
+
+  if (!mAfterDOMContentLoaded) {
+    // Before DOMContentLoaded notification we want to make sure that tailed
+    // requests don't start when there is a short delay during which we may
+    // not have any active requests on the page happening.
+    delay += quantum;
+  }
+
+  if (!delay.isValid() || delay.value() > delayMax) {
+    delay = delayMax;
+  }
+
+  LOG(("RequestContext::ScheduleUnblock this=%p non-tails=%u tail-queue=%zu delay=%u after-DCL=%d",
+       this, mNonTailRequests, mTailQueue.Length(), delay.value(), mAfterDOMContentLoaded));
+
+  TimeStamp now = TimeStamp::NowLoRes();
+  mUntailAt = now + TimeDuration::FromMilliseconds(delay.value());
+
+  if (mTimerScheduledAt.IsNull() || mUntailAt < mTimerScheduledAt) {
+    LOG(("RequestContext %p timer would fire too late, rescheduling", this));
+    RescheduleUntailTimer(now);
+  }
+}
+
+void
+RequestContext::RescheduleUntailTimer(TimeStamp const& now)
+{
+  MOZ_ASSERT(mUntailAt >= now);
+
+  if (mUntailTimer) {
+    mUntailTimer->Cancel();
+  }
+
+  if (!mTailQueue.Length()) {
+    mUntailTimer = nullptr;
+    mTimerScheduledAt = TimeStamp();
+    return;
+  }
+
+  TimeDuration interval = mUntailAt - now;
+  if (!mTimerScheduledAt.IsNull() && mUntailAt < mTimerScheduledAt) {
+    // When the number of untailed requests goes down,
+    // let's half the interval, since it's likely we would
+    // reschedule for a shorter time again very soon.
+    // This will likely save rescheduling this timer.
+    interval = interval / int64_t(2);
+    mTimerScheduledAt = mUntailAt - interval;
+  } else {
+    mTimerScheduledAt = mUntailAt;
+  }
+
+  uint32_t delay = interval.ToMilliseconds();
+  mUntailTimer = do_CreateInstance("@mozilla.org/timer;1");
+  mUntailTimer->InitWithCallback(this, delay, nsITimer::TYPE_ONE_SHOT);
+
+  LOG(("RequestContext::RescheduleUntailTimer %p in %d", this, delay));
+}
+
+NS_IMETHODIMP
+RequestContext::Notify(nsITimer *timer)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  MOZ_ASSERT(timer == mUntailTimer);
+  MOZ_ASSERT(!mTimerScheduledAt.IsNull());
+  MOZ_ASSERT(mTailQueue.Length());
+
+  mUntailTimer = nullptr;
+
+  TimeStamp now = TimeStamp::NowLoRes();
+  if (mUntailAt > mTimerScheduledAt && mUntailAt > now) {
+    LOG(("RequestContext %p timer fired too soon, rescheduling", this));
+    RescheduleUntailTimer(now);
+    return NS_OK;
+  }
+
+  // Must drop to allow re-engage of the timer
+  mTimerScheduledAt = TimeStamp();
+
+  ProcessTailQueue(NS_OK);
+
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+RequestContext::IsContextTailBlocked(nsIRequestTailUnblockCallback * aRequest,
+                                     bool *aBlocked)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+
+  LOG(("RequestContext::IsContextTailBlocked this=%p, request=%p, queued=%zu",
+       this, aRequest, mTailQueue.Length()));
+
+  *aBlocked = false;
+
+  if (mUntailAt.IsNull() || mUntailAt <= TimeStamp::NowLoRes()) {
+    LOG(("  untail time passed"));
+    // To save the expansive compare to now next time
+    mUntailAt = TimeStamp();
+    return NS_OK;
+  }
+
+  if (mAfterDOMContentLoaded && !mNonTailRequests) {
+    LOG(("  after DOMContentLoaded and no untailed requests"));
+    return NS_OK;
+  }
+
+  if (!gHttpHandler) {
+    // Xpcshell tests may not have http handler
+    LOG(("  missing gHttpHandler?"));
+    return NS_OK;
+  }
+
+  *aBlocked = true;
+  mTailQueue.AppendElement(aRequest);
+
+  LOG(("  request queued"));
+
+  if (!mUntailTimer) {
+    ScheduleUnblock();
+  }
+
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+RequestContext::CancelTailedRequest(nsIRequestTailUnblockCallback * aRequest)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+
+  bool removed = mTailQueue.RemoveElement(aRequest);
+
+  LOG(("RequestContext::CancelTailedRequest %p req=%p removed=%d",
+       this, aRequest, removed));
+
+  return NS_OK;
+}
+
+void
+RequestContext::ProcessTailQueue(nsresult aResult)
+{
+  LOG(("RequestContext::ProcessTailQueue this=%p, queued=%zu, rv=%" PRIx32,
+       this, mTailQueue.Length(), static_cast<uint32_t>(aResult)));
+
+  if (mUntailTimer) {
+    mUntailTimer->Cancel();
+    mUntailTimer = nullptr;
+  }
+
+  // Must drop to stop tailing requests
+  mUntailAt = TimeStamp();
+
+  nsTArray<PendingTailRequest> queue;
+  queue.SwapElements(mTailQueue);
+
+  for (auto request : queue) {
+    LOG(("  untailing %p", request.get()));
+    request->OnTailUnblock(aResult);
+  }
+}
+
+NS_IMETHODIMP
+RequestContext::CancelTailPendingRequests(nsresult aResult)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  MOZ_ASSERT(NS_FAILED(aResult));
+
+  ProcessTailQueue(aResult);
+  return NS_OK;
+}
 
 //nsIRequestContextService
 RequestContextService *RequestContextService::sSelf = nullptr;
 
 NS_IMPL_ISUPPORTS(RequestContextService, nsIRequestContextService, nsIObserver)
 
 RequestContextService::RequestContextService()
   : mNextRCID(1)
@@ -142,23 +432,34 @@ RequestContextService::~RequestContextSe
   MOZ_ASSERT(NS_IsMainThread());
   Shutdown();
   sSelf = nullptr;
 }
 
 nsresult
 RequestContextService::Init()
 {
+  nsresult rv;
+
   MOZ_ASSERT(NS_IsMainThread());
   nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
   if (!obs) {
     return NS_ERROR_NOT_AVAILABLE;
   }
 
-  return obs->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false);
+  rv = obs->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false);
+  if (NS_FAILED(rv)) {
+    return rv;
+  }
+  obs->AddObserver(this, "content-document-interactive", false);
+  if (NS_FAILED(rv)) {
+    return rv;
+  }
+
+  return NS_OK;
 }
 
 void
 RequestContextService::Shutdown()
 {
   MOZ_ASSERT(NS_IsMainThread());
   mTable.Clear();
 }
@@ -190,16 +491,30 @@ RequestContextService::GetRequestContext
     mTable.Put(rcID, newSC);
     newSC.swap(*rc);
   }
 
   return NS_OK;
 }
 
 NS_IMETHODIMP
+RequestContextService::GetRequestContextFromLoadGroup(nsILoadGroup *aLoadGroup, nsIRequestContext **rc)
+{
+  nsresult rv;
+
+  uint64_t rcID;
+  rv = aLoadGroup->GetRequestContextID(&rcID);
+  if (NS_FAILED(rv)) {
+    return rv;
+  }
+
+  return GetRequestContext(rcID, rc);
+}
+
+NS_IMETHODIMP
 RequestContextService::NewRequestContext(nsIRequestContext **rc)
 {
   MOZ_ASSERT(NS_IsMainThread());
   NS_ENSURE_ARG_POINTER(rc);
   *rc = nullptr;
 
   uint64_t rcID = ((static_cast<uint64_t>(mRCIDNamespace) << 32) & 0xFFFFFFFF00000000LL) | mNextRCID++;
 
@@ -208,29 +523,66 @@ RequestContextService::NewRequestContext
   newSC.swap(*rc);
 
   return NS_OK;
 }
 
 NS_IMETHODIMP
 RequestContextService::RemoveRequestContext(const uint64_t rcID)
 {
+  if (IsNeckoChild() && gNeckoChild) {
+    gNeckoChild->SendRemoveRequestContext(rcID);
+  }
+
   MOZ_ASSERT(NS_IsMainThread());
   mTable.Remove(rcID);
   return NS_OK;
 }
 
 NS_IMETHODIMP
 RequestContextService::Observe(nsISupports *subject, const char *topic,
                                   const char16_t *data_unicode)
 {
   MOZ_ASSERT(NS_IsMainThread());
   if (!strcmp(NS_XPCOM_SHUTDOWN_OBSERVER_ID, topic)) {
     Shutdown();
+    return NS_OK;
   }
 
+  if (!strcmp("content-document-interactive", topic)) {
+    nsCOMPtr<nsIDocument> document(do_QueryInterface(subject));
+    MOZ_ASSERT(document);
+    // We want this be triggered also for iframes, since those track their
+    // own request context ids.
+    if (!document) {
+      return NS_OK;
+    }
+    nsIDocShell* ds = document->GetDocShell();
+    // XML documents don't always have a docshell assigned
+    if (!ds) {
+      return NS_OK;
+    }
+    nsCOMPtr<nsIDocumentLoader> dl(do_QueryInterface(ds));
+    if (!dl) {
+      return NS_OK;
+    }
+    nsCOMPtr<nsILoadGroup> lg;
+    dl->GetLoadGroup(getter_AddRefs(lg));
+    if (!lg) {
+      return NS_OK;
+    }
+    nsCOMPtr<nsIRequestContext> rc;
+    GetRequestContextFromLoadGroup(lg, getter_AddRefs(rc));
+    if (rc) {
+      rc->DOMContentLoaded();
+    }
+
+    return NS_OK;
+  }
+
+  MOZ_ASSERT(false, "Unexpected observer topic");
   return NS_OK;
 }
 
 } // ::mozilla::net
 } // ::mozilla
 
 #undef LOG
--- a/netwerk/base/nsChannelClassifier.cpp
+++ b/netwerk/base/nsChannelClassifier.cpp
@@ -201,30 +201,39 @@ SetIsTrackingResourceHelper(nsIChannel* 
   }
 }
 
 static void
 LowerPriorityHelper(nsIChannel* aChannel)
 {
   MOZ_ASSERT(aChannel);
 
-  nsCOMPtr<nsISupportsPriority> p = do_QueryInterface(aChannel);
-  if (p) {
-    p->SetPriority(nsISupportsPriority::PRIORITY_LOWEST);
-  }
-}
-
-static void
-SetThrottleableHelper(nsIChannel* aChannel)
-{
-  MOZ_ASSERT(aChannel);
+  bool isBlockingResource = false;
 
   nsCOMPtr<nsIClassOfService> cos(do_QueryInterface(aChannel));
   if (cos) {
-    cos->AddClassFlags(nsIClassOfService::Throttleable);
+    uint32_t cosFlags = 0;
+    cos->GetClassFlags(&cosFlags);
+    isBlockingResource = cosFlags & (nsIClassOfService::UrgentStart |
+                                     nsIClassOfService::Leader |
+                                     nsIClassOfService::Unblocked);
+
+    // Requests not allowed to be tailed are usually those with higher
+    // prioritization.  That overweights being a tracker: don't throttle
+    // them when not in background.
+    if (!(cosFlags & nsIClassOfService::TailForbidden)) {
+      cos->AddClassFlags(nsIClassOfService::Throttleable);
+    }
+  }
+
+  if (!isBlockingResource) {
+    nsCOMPtr<nsISupportsPriority> p = do_QueryInterface(aChannel);
+    if (p) {
+      p->SetPriority(nsISupportsPriority::PRIORITY_LOWEST);
+    }
   }
 }
 
 NS_IMPL_ISUPPORTS(nsChannelClassifier,
                   nsIURIClassifierCallback,
                   nsIObserver)
 
 nsChannelClassifier::nsChannelClassifier(nsIChannel *aChannel)
@@ -1020,17 +1029,16 @@ IsTrackerBlacklistedCallback::OnClassify
          mChannelClassifier.get(), channel.get()));
 
     MOZ_ASSERT(mChannelClassifier->ShouldEnableTrackingAnnotation());
 
     SetIsTrackingResourceHelper(channel);
     if (CachedPrefs::GetInstance()->IsLowerNetworkPriority()) {
       LowerPriorityHelper(channel);
     }
-    SetThrottleableHelper(channel);
 
     // We don't want to disable speculative connection when tracking protection
     // is disabled. So, change the status to NS_OK.
     status = NS_OK;
 
     return mChannelCallback->OnClassifyComplete(
       status, aLists, aProvider, aPrefix);
   }
@@ -1067,17 +1075,16 @@ IsTrackerBlacklistedCallback::OnClassify
          mChannelClassifier.get(), channel.get(),
          uri->GetSpecOrDefault().get()));
   }
 
   SetIsTrackingResourceHelper(channel);
   if (CachedPrefs::GetInstance()->IsLowerNetworkPriority()) {
     LowerPriorityHelper(channel);
   }
-  SetThrottleableHelper(channel);
 
   return mChannelCallback->OnClassifyComplete(
       NS_OK, aLists, aProvider, aPrefix);
 }
 
 } // end of unnamed namespace/
 
 already_AddRefed<nsIURI>
--- a/netwerk/base/nsIClassOfService.idl
+++ b/netwerk/base/nsIClassOfService.idl
@@ -40,9 +40,18 @@ interface nsIClassOfService : nsISupport
   const unsigned long Leader = 1 << 0;
   const unsigned long Follower = 1 << 1;
   const unsigned long Speculative = 1 << 2;
   const unsigned long Background = 1 << 3;
   const unsigned long Unblocked = 1 << 4;
   const unsigned long Throttleable = 1 << 5;
   const unsigned long UrgentStart = 1 << 6;
   const unsigned long DontThrottle = 1 << 7;
+  // Enforce tailing on this load; any of Leader, Unblocked, UrgentStart, TailForbidden
+  // overrule this flag (disable tailing.)
+  const unsigned long Tail = 1 << 8;
+  // Tailing may be engaged regardless if the load is marked Unblocked when
+  // some other conditions are met later, like when the load is found to be
+  // a tracker.
+  const unsigned long TailAllowed = 1 << 9;
+  // Tailing not allowed under any circumstances or combination of flags.
+  const unsigned long TailForbidden = 1 << 10;
 };
--- a/netwerk/base/nsIRequestContext.idl
+++ b/netwerk/base/nsIRequestContext.idl
@@ -9,35 +9,70 @@
 // Forward-declare mozilla::net::SpdyPushCache
 namespace mozilla {
 namespace net {
 class SpdyPushCache;
 }
 }
 %}
 
+interface nsILoadGroup;
+interface nsIChannel;
+interface nsIStreamListener;
+
 [ptr] native SpdyPushCachePtr(mozilla::net::SpdyPushCache);
 
 /**
+ * Requests capable of tail-blocking must implement this
+ * interfaces (typically channels).
+ * If the request is tail-blocked, it will be held in its request
+ * context queue until unblocked.
+ */
+[scriptable, uuid(7EB361D4-37A5-42C9-AFAE-F6C88FE7C394)]
+interface nsIRequestTailUnblockCallback : nsISupports
+{
+  /**
+   * Called when the requests is unblocked and proceed.
+   * @param result
+   *    NS_OK - the request is OK to go, unblocking is not
+   *            caused by cancelation of the request.
+   *    any error - the request must behave as it were canceled
+   *                with the result as status.
+   */
+  void onTailUnblock(in nsresult aResult);
+};
+
+/**
  * The nsIRequestContext is used to maintain state about connections
  * that are in some way associated with each other (often by being part
  * of the same load group) and how they interact with blocking items like
  * HEAD css/js loads.
  *
  * This used to be known as nsILoadGroupConnectionInfo and nsISchedulingContext.
  */
 [scriptable, uuid(658e3e6e-8633-4b1a-8d66-fa9f72293e63)]
 interface nsIRequestContext : nsISupports
 {
   /**
    * A unique identifier for this request context
    */
   [noscript] readonly attribute unsigned long long ID;
 
   /**
+   * Called by the associated document when its load starts.  This resets
+   * context's internal states.
+   */
+  void beginLoad();
+
+  /**
+  * Called when the associated document notified the DOMContentLoaded event.
+  */
+  void DOMContentLoaded();
+
+  /**
    * Number of active blocking transactions associated with this context
    */
   readonly attribute unsigned long blockingTransactionCount;
 
   /**
    * Increase the number of active blocking transactions associated
    * with this context by one.
    */
@@ -57,16 +92,45 @@ interface nsIRequestContext : nsISupport
    * ends.
    */
   [noscript] attribute SpdyPushCachePtr spdyPushCache;
 
   /**
    * This holds a cached value of the user agent override.
    */
   [noscript] attribute ACString userAgentOverride;
+
+  /**
+   * Increases/decrease the number of non-tailed requests in this context.
+   * If the count drops to zero, all tail-blocked callbacks are notified
+   * shortly after that to be unblocked.
+   */
+  void addNonTailRequest();
+  void removeNonTailRequest();
+
+  /**
+   * If the request context is in tail-blocked state, the callback
+   * is queued and result is true.  The callback will be notified
+   * about tail-unblocking or when the request context is canceled.
+   */
+  [must_use] boolean isContextTailBlocked(in nsIRequestTailUnblockCallback callback);
+
+  /**
+   * Called when the request is sitting in the tail queue but has been
+   * canceled before untailing.  This just removes the request from the
+   * queue so that it is not notified on untail and not referenced.
+   */
+  void cancelTailedRequest(in nsIRequestTailUnblockCallback request);
+
+  /**
+   * This notifies all queued tail-blocked requests, they will be notified
+   * aResult and released afterwards.  Called by the load group when
+   * it's canceled.
+   */
+  void cancelTailPendingRequests(in nsresult aResult);
 };
 
 /**
  * The nsIRequestContextService is how anyone gets access to a request
  * context when they haven't been explicitly given a strong reference to an
  * existing one. It is responsible for creating and handing out strong
  * references to nsIRequestContexts, but only keeps weak references itself.
  * The shared request context will go away once no one else is keeping a
@@ -77,16 +141,20 @@ interface nsIRequestContext : nsISupport
  */
 [uuid(7fcbf4da-d828-4acc-b144-e5435198f727)]
 interface nsIRequestContextService : nsISupports
 {
   /**
    * Get an existing request context from its ID
    */
   nsIRequestContext getRequestContext(in unsigned long long id);
+  /**
+   * Shorthand to get request context from a load group
+   */
+  nsIRequestContext getRequestContextFromLoadGroup(in nsILoadGroup lg);
 
   /**
    * Create a new request context
    */
   nsIRequestContext newRequestContext();
 
   /**
    * Remove an existing request context from its ID
--- a/netwerk/base/nsLoadGroup.cpp
+++ b/netwerk/base/nsLoadGroup.cpp
@@ -18,18 +18,16 @@
 #include "nsITimedChannel.h"
 #include "nsIInterfaceRequestor.h"
 #include "nsIRequestObserver.h"
 #include "nsIRequestContext.h"
 #include "CacheObserver.h"
 #include "MainThreadUtils.h"
 #include "mozilla/Unused.h"
 
-#include "mozilla/net/NeckoChild.h"
-
 namespace mozilla {
 namespace net {
 
 //
 // Log module for nsILoadGroup logging...
 //
 // To enable logging (see prlog.h for full details):
 //
@@ -124,22 +122,17 @@ nsLoadGroup::~nsLoadGroup()
     DebugOnly<nsresult> rv = Cancel(NS_BINDING_ABORTED);
     NS_ASSERTION(NS_SUCCEEDED(rv), "Cancel failed");
 
     mDefaultLoadRequest = nullptr;
 
     if (mRequestContext) {
         uint64_t rcid;
         mRequestContext->GetID(&rcid);
-
-        if (IsNeckoChild() && gNeckoChild) {
-            gNeckoChild->SendRemoveRequestContext(rcid);
-        } else {
-            mRequestContextService->RemoveRequestContext(rcid);
-        }
+        mRequestContextService->RemoveRequestContext(rcid);
     }
 
     LOG(("LOADGROUP [%p]: Destroyed.\n", this));
 }
 
 
 ////////////////////////////////////////////////////////////////////////////////
 // nsISupports methods:
@@ -269,16 +262,20 @@ nsLoadGroup::Cancel(nsresult status)
 
         // Remember the first failure and return it...
         if (NS_FAILED(rv) && NS_SUCCEEDED(firstError))
             firstError = rv;
 
         NS_RELEASE(request);
     }
 
+    if (mRequestContext) {
+        Unused << mRequestContext->CancelTailPendingRequests(status);
+    }
+
 #if defined(DEBUG)
     NS_ASSERTION(mRequests.EntryCount() == 0, "Request list is not empty.");
     NS_ASSERTION(mForegroundCount == 0, "Foreground URLs are active.");
 #endif
 
     mStatus = NS_OK;
     mIsCanceling = false;
 
--- a/netwerk/ipc/NeckoParent.cpp
+++ b/netwerk/ipc/NeckoParent.cpp
@@ -894,16 +894,50 @@ NeckoParent::RecvPredReset()
     do_GetService("@mozilla.org/network/predictor;1", &rv);
   NS_ENSURE_SUCCESS(rv, IPC_FAIL_NO_REASON(this));
 
   predictor->Reset();
   return IPC_OK();
 }
 
 mozilla::ipc::IPCResult
+NeckoParent::RecvRequestContextLoadBegin(const uint64_t& rcid)
+{
+  nsCOMPtr<nsIRequestContextService> rcsvc =
+    do_GetService("@mozilla.org/network/request-context-service;1");
+  if (!rcsvc) {
+    return IPC_OK();
+  }
+  nsCOMPtr<nsIRequestContext> rc;
+  rcsvc->GetRequestContext(rcid, getter_AddRefs(rc));
+  if (rc) {
+    rc->BeginLoad();
+  }
+
+  return IPC_OK();
+}
+
+mozilla::ipc::IPCResult
+NeckoParent::RecvRequestContextAfterDOMContentLoaded(const uint64_t& rcid)
+{
+  nsCOMPtr<nsIRequestContextService> rcsvc =
+    do_GetService("@mozilla.org/network/request-context-service;1");
+  if (!rcsvc) {
+    return IPC_OK();
+  }
+  nsCOMPtr<nsIRequestContext> rc;
+  rcsvc->GetRequestContext(rcid, getter_AddRefs(rc));
+  if (rc) {
+    rc->DOMContentLoaded();
+  }
+
+  return IPC_OK();
+}
+
+mozilla::ipc::IPCResult
 NeckoParent::RecvRemoveRequestContext(const uint64_t& rcid)
 {
   nsCOMPtr<nsIRequestContextService> rcsvc =
     do_GetService("@mozilla.org/network/request-context-service;1");
   if (!rcsvc) {
     return IPC_OK();
   }
 
--- a/netwerk/ipc/NeckoParent.h
+++ b/netwerk/ipc/NeckoParent.h
@@ -223,16 +223,18 @@ protected:
                                                   const bool& hasVerifier) override;
 
   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 RecvRequestContextLoadBegin(const uint64_t& rcid) override;
+  virtual mozilla::ipc::IPCResult RecvRequestContextAfterDOMContentLoaded(const uint64_t& rcid) override;
   virtual mozilla::ipc::IPCResult RecvRemoveRequestContext(const uint64_t& rcid) override;
 
   /* WebExtensions */
   virtual mozilla::ipc::IPCResult
     RecvGetExtensionStream(const URIParams& aURI,
                            GetExtensionStreamResolver&& aResolve) override;
 
   virtual mozilla::ipc::IPCResult
--- a/netwerk/ipc/PNecko.ipdl
+++ b/netwerk/ipc/PNecko.ipdl
@@ -111,16 +111,18 @@ parent:
    * These are called from the child with the results of the auth prompt.
    * callbackId is the id that was passed in PBrowser::AsyncAuthPrompt,
    * corresponding to an nsIAuthPromptCallback
    */
   async OnAuthAvailable(uint64_t callbackId, nsString user,
                         nsString password, nsString domain);
   async OnAuthCancelled(uint64_t callbackId, bool userCancel);
 
+  async RequestContextLoadBegin(uint64_t rcid);
+  async RequestContextAfterDOMContentLoaded(uint64_t rcid);
   async RemoveRequestContext(uint64_t rcid);
 
   async PAltDataOutputStream(nsCString type, PHttpChannel channel);
 
   async PStunAddrsRequest();
 
   /**
    * WebExtension-specific remote resource loading
--- a/netwerk/protocol/http/HttpBaseChannel.cpp
+++ b/netwerk/protocol/http/HttpBaseChannel.cpp
@@ -178,16 +178,17 @@ HttpBaseChannel::HttpBaseChannel()
   , mAllowAltSvc(true)
   , mBeConservative(false)
   , mResponseTimeoutEnabled(true)
   , mAllRedirectsSameOrigin(true)
   , mAllRedirectsPassTimingAllowCheck(true)
   , mResponseCouldBeSynthesized(false)
   , mBlockAuthPrompt(false)
   , mAllowStaleCacheContent(false)
+  , mAddedAsNonTailRequest(false)
   , mTlsFlags(0)
   , mSuspendCount(0)
   , mInitialRwin(0)
   , mProxyResolveFlags(0)
   , mContentDispositionHint(UINT32_MAX)
   , mHttpHandler(gHttpHandler)
   , mReferrerPolicy(NS_GetDefaultReferrerPolicy())
   , mRedirectCount(0)
@@ -230,22 +231,48 @@ HttpBaseChannel::~HttpBaseChannel()
   LOG(("Destroying HttpBaseChannel @%p\n", this));
 
   // Make sure we don't leak
   CleanRedirectCacheChainIfNecessary();
 
   ReleaseMainThreadOnlyReferences();
 }
 
+namespace { // anon
+
+class NonTailRemover : public nsISupports
+{
+  NS_DECL_THREADSAFE_ISUPPORTS
+
+  explicit NonTailRemover(nsIRequestContext* rc)
+    : mRequestContext(rc)
+  {
+  }
+
+private:
+  virtual ~NonTailRemover()
+  {
+    MOZ_ASSERT(NS_IsMainThread());
+    mRequestContext->RemoveNonTailRequest();
+  }
+
+  nsCOMPtr<nsIRequestContext> mRequestContext;
+};
+
+NS_IMPL_ISUPPORTS0(NonTailRemover)
+
+} // anon
+
 void
 HttpBaseChannel::ReleaseMainThreadOnlyReferences()
 {
   if (NS_IsMainThread()) {
     // Already on main thread, let dtor to
     // take care of releasing references
+    RemoveAsNonTailRequest();
     return;
   }
 
   nsTArray<nsCOMPtr<nsISupports>> arrayToRelease;
   arrayToRelease.AppendElement(mURI.forget());
   arrayToRelease.AppendElement(mOriginalURI.forget());
   arrayToRelease.AppendElement(mDocumentURI.forget());
   arrayToRelease.AppendElement(mLoadGroup.forget());
@@ -257,16 +284,24 @@ HttpBaseChannel::ReleaseMainThreadOnlyRe
   arrayToRelease.AppendElement(mAPIRedirectToURI.forget());
   arrayToRelease.AppendElement(mProxyURI.forget());
   arrayToRelease.AppendElement(mPrincipal.forget());
   arrayToRelease.AppendElement(mTopWindowURI.forget());
   arrayToRelease.AppendElement(mListener.forget());
   arrayToRelease.AppendElement(mListenerContext.forget());
   arrayToRelease.AppendElement(mCompressListener.forget());
 
+  if (mAddedAsNonTailRequest) {
+    // RemoveNonTailRequest() on our request context must be called on the main thread
+    MOZ_RELEASE_ASSERT(mRequestContext, "Someone released rc or set flags w/o having it?");
+
+    nsCOMPtr<nsISupports> nonTailRemover(new NonTailRemover(mRequestContext));
+    arrayToRelease.AppendElement(nonTailRemover.forget());
+  }
+
   NS_DispatchToMainThread(new ProxyReleaseRunnable(Move(arrayToRelease)));
 }
 
 void
 HttpBaseChannel::SetIsTrackingResource()
 {
   LOG(("HttpBaseChannel::SetIsTrackingResource %p", this));
   mIsTrackingResource = true;
@@ -3009,16 +3044,48 @@ HttpBaseChannel::ShouldIntercept(nsIURI*
                                                         &shouldIntercept);
     if (NS_FAILED(rv)) {
       return false;
     }
   }
   return shouldIntercept;
 }
 
+void
+HttpBaseChannel::AddAsNonTailRequest()
+{
+  MOZ_ASSERT(NS_IsMainThread());
+
+  if (EnsureRequestContext()) {
+    LOG(("HttpBaseChannel::AddAsNonTailRequest this=%p, rc=%p, already added=%d",
+         this, mRequestContext.get(), (bool)mAddedAsNonTailRequest));
+
+    if (!mAddedAsNonTailRequest) {
+      mRequestContext->AddNonTailRequest();
+      mAddedAsNonTailRequest = true;
+    }
+  }
+}
+
+void
+HttpBaseChannel::RemoveAsNonTailRequest()
+{
+  MOZ_ASSERT(NS_IsMainThread());
+
+  if (mRequestContext) {
+    LOG(("HttpBaseChannel::RemoveAsNonTailRequest this=%p, rc=%p, already added=%d",
+         this, mRequestContext.get(), (bool)mAddedAsNonTailRequest));
+
+    if (mAddedAsNonTailRequest) {
+      mRequestContext->RemoveNonTailRequest();
+      mAddedAsNonTailRequest = false;
+    }
+  }
+}
+
 #ifdef DEBUG
 void HttpBaseChannel::AssertPrivateBrowsingId()
 {
   nsCOMPtr<nsILoadContext> loadContext;
   NS_QueryNotificationCallbacks(this, loadContext);
   // For addons it's possible that mLoadInfo is null.
   if (!mLoadInfo) {
     return;
@@ -3170,18 +3237,22 @@ HttpBaseChannel::DoNotifyListener()
                "We should not call OnStopRequest twice");
 
     nsCOMPtr<nsIStreamListener> listener = mListener;
     listener->OnStopRequest(this, mListenerContext, mStatus);
 
     mOnStopRequestCalled = true;
   }
 
+  // This channel has finished its job, potentially release any tail-blocked
+  // requests with this.
+  RemoveAsNonTailRequest();
+
   // We have to make sure to drop the references to listeners and callbacks
-  // no longer  needed
+  // no longer needed.
   ReleaseListeners();
 
   DoNotifyListenerCleanup();
 
   // If this is a navigation, then we must let the docshell flush the reports
   // to the console later.  The LoadDocument() is pointing at the detached
   // document that started the navigation.  We want to show the reports on the
   // new document.  Otherwise the console is wiped and the user never sees
@@ -4089,25 +4160,50 @@ HttpBaseChannel::EnsureRequestContextID(
     }
 
     nsCOMPtr<nsILoadGroup> rootLoadGroup;
     childLoadGroup->GetRootLoadGroup(getter_AddRefs(rootLoadGroup));
     if (!rootLoadGroup) {
         return false;
     }
 
-    // Set the load group connection scope on the transaction
+    // Set the load group connection scope on this channel and its transaction
     rootLoadGroup->GetRequestContextID(&mRequestContextID);
 
     LOG(("HttpBaseChannel::EnsureRequestContextID this=%p id=%" PRIx64,
          this, mRequestContextID));
 
     return true;
 }
 
+bool
+HttpBaseChannel::EnsureRequestContext()
+{
+    if (mRequestContext) {
+        // Already have a request context, no need to do the rest of this work
+        return true;
+    }
+
+    if (!EnsureRequestContextID()) {
+        return false;
+    }
+
+    nsIRequestContextService* rcsvc = gHttpHandler->GetRequestContextService();
+    if (!rcsvc) {
+        return false;
+    }
+
+    rcsvc->GetRequestContext(mRequestContextID, getter_AddRefs(mRequestContext));
+    if (!mRequestContext) {
+        return false;
+    }
+
+    return true;
+}
+
 void
 HttpBaseChannel::EnsureTopLevelOuterContentWindowId()
 {
   if (mTopLevelOuterContentWindowId) {
     return;
   }
 
   nsCOMPtr<nsILoadContext> loadContext;
--- a/netwerk/protocol/http/HttpBaseChannel.h
+++ b/netwerk/protocol/http/HttpBaseChannel.h
@@ -535,16 +535,20 @@ protected:
   uint32_t                          mResponseCouldBeSynthesized : 1;
 
   uint32_t                          mBlockAuthPrompt : 1;
 
   // If true, we behave as if the LOAD_FROM_CACHE flag has been set.
   // Used to enforce that flag's behavior but not expose it externally.
   uint32_t                          mAllowStaleCacheContent : 1;
 
+  // True iff this request has been calculated in its request context as
+  // a non tail request.  We must remove it again when this channel is done.
+  uint32_t                          mAddedAsNonTailRequest : 1;
+
   // An opaque flags for non-standard behavior of the TLS system.
   // It is unlikely this will need to be set outside of telemetry studies
   // relating to the TLS implementation.
   uint32_t                          mTlsFlags;
 
   // Current suspension depth for this channel object
   uint32_t                          mSuspendCount;
 
@@ -610,16 +614,24 @@ protected:
   uint64_t mDecodedBodySize;
   uint64_t mEncodedBodySize;
 
   // The network interface id that's associated with this channel.
   nsCString mNetworkInterfaceId;
 
   uint64_t mRequestContextID;
   bool EnsureRequestContextID();
+  nsCOMPtr<nsIRequestContext> mRequestContext;
+  bool EnsureRequestContext();
+
+  // Adds/removes this channel as a non-tailed request in its request context
+  // these helpers ensure we add it only once and remove it only when added
+  // via mAddedAsNonTailRequest member tracking.
+  void AddAsNonTailRequest();
+  void RemoveAsNonTailRequest();
 
   // ID of the top-level document's inner window this channel is being
   // originated from.
   uint64_t mContentWindowId;
 
   uint64_t mTopLevelOuterContentWindowId;
   void EnsureTopLevelOuterContentWindowId();
 
--- a/netwerk/protocol/http/nsHttpChannel.cpp
+++ b/netwerk/protocol/http/nsHttpChannel.cpp
@@ -271,16 +271,20 @@ private:
 void
 AutoRedirectVetoNotifier::ReportRedirectResult(bool succeeded)
 {
     if (!mChannel)
         return;
 
     mChannel->mRedirectChannel = nullptr;
 
+    if (succeeded) {
+        mChannel->RemoveAsNonTailRequest();
+    }
+
     nsCOMPtr<nsIRedirectResultListener> vetoHook;
     NS_QueryNotificationCallbacks(mChannel,
                                   NS_GET_IID(nsIRedirectResultListener),
                                   getter_AddRefs(vetoHook));
 
     nsHttpChannel* channel = mChannel;
     mChannel = nullptr;
 
@@ -331,16 +335,17 @@ nsHttpChannel::nsHttpChannel()
     , mIsCorsPreflightDone(0)
     , mStronglyFramed(false)
     , mUsedNetwork(0)
     , mAuthConnectionRestartable(0)
     , mReqContentLengthDetermined(0)
     , mReqContentLength(0U)
     , mPushedStream(nullptr)
     , mLocalBlocklist(false)
+    , mOnTailUnblock(nullptr)
     , mWarningReporter(nullptr)
     , mIsReadingFromCache(false)
     , mFirstResponseSource(RESPONSE_PENDING)
     , mOnCacheAvailableCalled(false)
     , mRaceCacheWithNetwork(false)
     , mRaceDelay(0)
     , mCacheAsyncOpenCalled(false)
     , mIgnoreCacheEntry(false)
@@ -531,27 +536,50 @@ nsHttpChannel::OnBeforeConnectContinue()
     }
 }
 
 nsresult
 nsHttpChannel::Connect()
 {
     LOG(("nsHttpChannel::Connect [this=%p]\n", this));
 
-    // Consider opening a TCP connection right away.
-    SpeculativeConnect();
-
     // Don't allow resuming when cache must be used
     if (mResuming && (mLoadFlags & LOAD_ONLY_FROM_CACHE)) {
         LOG(("Resuming from cache is not supported yet"));
         return NS_ERROR_DOCUMENT_NOT_CACHED;
     }
 
+    bool isTrackingResource = mIsTrackingResource; // is atomic
+    LOG(("nsHttpChannel %p tracking resource=%d, local blocklist=%d, cos=%u",
+          this, isTrackingResource, mLocalBlocklist, mClassOfService));
+
+    if (isTrackingResource || mLocalBlocklist) {
+        AddClassFlags(nsIClassOfService::Tail);
+    }
+
+    if (WaitingForTailUnblock()) {
+        MOZ_DIAGNOSTIC_ASSERT(!mOnTailUnblock);
+        mOnTailUnblock = &nsHttpChannel::ConnectOnTailUnblock;
+        return NS_OK;
+    }
+
+    return ConnectOnTailUnblock();
+}
+
+nsresult
+nsHttpChannel::ConnectOnTailUnblock()
+{
+    nsresult rv;
+
+    LOG(("nsHttpChannel::ConnectOnTailUnblock [this=%p]\n", this));
+
+    // Consider opening a TCP connection right away.
+    SpeculativeConnect();
+
     // open a cache entry for this channel...
-    nsresult rv;
     bool isHttps = false;
     rv = mURI->SchemeIs("https", &isHttps);
     NS_ENSURE_SUCCESS(rv,rv);
     rv = OpenCacheEntry(isHttps);
 
     // do not continue if asyncOpenCacheEntry is in progress
     if (AwaitingCacheCallbacks()) {
         LOG(("nsHttpChannel::Connect %p AwaitingCacheCallbacks forces async\n", this));
@@ -1059,40 +1087,16 @@ nsHttpChannel::ContinueHandleAsyncFallba
     mIsPending = false;
 
     if (mLoadGroup)
         mLoadGroup->RemoveRequest(this, nullptr, mStatus);
 
     return rv;
 }
 
-void
-nsHttpChannel::SetupTransactionRequestContext()
-{
-    if (!EnsureRequestContextID()) {
-        return;
-    }
-
-    nsIRequestContextService *rcsvc =
-        gHttpHandler->GetRequestContextService();
-    if (!rcsvc) {
-        return;
-    }
-
-    nsCOMPtr<nsIRequestContext> rc;
-    nsresult rv = rcsvc->GetRequestContext(mRequestContextID,
-                                           getter_AddRefs(rc));
-
-    if (NS_FAILED(rv)) {
-        return;
-    }
-
-    mTransaction->SetRequestContext(rc);
-}
-
 nsresult
 nsHttpChannel::SetupTransaction()
 {
     LOG(("nsHttpChannel::SetupTransaction [this=%p, cos=%u, prio=%d]\n",
          this, mClassOfService, mPriority));
 
     NS_ENSURE_TRUE(!mTransaction, NS_ERROR_ALREADY_INITIALIZED);
 
@@ -1312,17 +1316,19 @@ nsHttpChannel::SetupTransaction()
                             mTopLevelOuterContentWindowId,
                             getter_AddRefs(responseStream));
     if (NS_FAILED(rv)) {
         mTransaction = nullptr;
         return rv;
     }
 
     mTransaction->SetClassOfService(mClassOfService);
-    SetupTransactionRequestContext();
+    if (EnsureRequestContext()) {
+        mTransaction->SetRequestContext(mRequestContext);
+    }
 
     rv = nsInputStreamPump::Create(getter_AddRefs(mTransactionPump),
                                    responseStream);
     return rv;
 }
 
 // NOTE: This function duplicates code from nsBaseChannel. This will go away
 // once HTTP uses nsBaseChannel (part of bug 312760)
@@ -5962,16 +5968,17 @@ NS_INTERFACE_MAP_BEGIN(nsHttpChannel)
     NS_INTERFACE_MAP_ENTRY(nsIThreadRetargetableStreamListener)
     NS_INTERFACE_MAP_ENTRY(nsIDNSListener)
     NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
     NS_INTERFACE_MAP_ENTRY(nsICorsPreflightCallback)
     NS_INTERFACE_MAP_ENTRY(nsIRaceCacheWithNetwork)
     NS_INTERFACE_MAP_ENTRY(nsITimerCallback)
     NS_INTERFACE_MAP_ENTRY(nsIHstsPrimingCallback)
     NS_INTERFACE_MAP_ENTRY(nsIChannelWithDivertableParentListener)
+    NS_INTERFACE_MAP_ENTRY(nsIRequestTailUnblockCallback)
     // we have no macro that covers this case.
     if (aIID.Equals(NS_GET_IID(nsHttpChannel)) ) {
         AddRef();
         *aInstancePtr = this;
         return NS_OK;
     } else
 NS_INTERFACE_MAP_END_INHERITING(HttpBaseChannel)
 
@@ -6003,16 +6010,20 @@ nsHttpChannel::Cancel(nsresult status)
     CancelNetworkRequest(status);
     mCacheInputStream.CloseAndRelease();
     if (mCachePump)
         mCachePump->Cancel(status);
     if (mAuthProvider)
         mAuthProvider->Cancel(status);
     if (mPreflightChannel)
         mPreflightChannel->Cancel(status);
+    if (mRequestContext && mOnTailUnblock) {
+        mOnTailUnblock = nullptr;
+        mRequestContext->CancelTailedRequest(this);
+    }
     return NS_OK;
 }
 
 void
 nsHttpChannel::CancelNetworkRequest(nsresult aStatus)
 {
     if (mTransaction) {
         nsresult rv = gHttpHandler->CancelTransaction(mTransaction, aStatus);
@@ -6124,16 +6135,31 @@ nsHttpChannel::AsyncOpen(nsIStreamListen
     }
 
     rv = NS_CheckPortSafety(mURI);
     if (NS_FAILED(rv)) {
         ReleaseListeners();
         return rv;
     }
 
+    if (WaitingForTailUnblock()) {
+        // This channel is marked as Tail and is part of a request context
+        // that has positive number of non-tailed requestst, hence this channel
+        // has been put to a queue.
+        // When tail is unblocked, OnTailUnblock on this channel will be called
+        // to continue AsyncOpen.
+        mListener = listener;
+        mListenerContext = context;
+        MOZ_DIAGNOSTIC_ASSERT(!mOnTailUnblock);
+        mOnTailUnblock = &nsHttpChannel::AsyncOpenOnTailUnblock;
+
+        LOG(("  put on hold until tail is unblocked"));
+        return NS_OK;
+    }
+
     if (mInterceptCache != INTERCEPTED && ShouldIntercept()) {
         mInterceptCache = MAYBE_INTERCEPT;
         SetCouldBeSynthesized();
     }
 
     // Remember the cookie header that was set, if any
     nsAutoCString cookieHeader;
     if (NS_SUCCEEDED(mRequestHead.GetHeader(nsHttp::Cookie, cookieHeader))) {
@@ -6188,16 +6214,22 @@ nsHttpChannel::AsyncOpen(nsIStreamListen
     if (NS_FAILED(rv)) {
         CloseCacheEntry(false);
         Unused << AsyncAbort(rv);
     }
 
     return NS_OK;
 }
 
+nsresult
+nsHttpChannel::AsyncOpenOnTailUnblock()
+{
+    return AsyncOpen(mListener, mListenerContext);
+}
+
 namespace {
 
 class InitLocalBlockListXpcCallback final : public nsIURIClassifierCallback {
 public:
   using CallbackType = nsHttpChannel::InitLocalBlockListCallback;
 
   explicit InitLocalBlockListXpcCallback(const CallbackType& aCallback)
     : mCallback(aCallback)
@@ -6528,17 +6560,22 @@ nsHttpChannel::BeginConnectContinue()
     }
 
     // We are about to do a async lookup to check if the URI is a
     // tracker. The result will be delivered along with the callback.
     // Chances are the lookup is not needed so InitLocalBlockList()
     // will return false and then we can BeginConnectActual() right away.
     RefPtr<nsHttpChannel> self = this;
     bool willCallback = InitLocalBlockList([self](bool aLocalBlockList) -> void  {
+        MOZ_ASSERT(self->mLocalBlocklist <= aLocalBlockList, "Unmarking local block-list flag?");
+
         self->mLocalBlocklist = aLocalBlockList;
+
+        LOG(("nsHttpChannel %p on-local-blacklist=%d", self.get(), aLocalBlockList));
+
         nsresult rv = self->BeginConnectActual();
         if (NS_FAILED(rv)) {
             // Since this error is thrown asynchronously so that the caller
             // of BeginConnect() will not do clean up for us. We have to do
             // it on our own.
             self->CloseCacheEntry(false);
             Unused << self->AsyncAbort(rv);
         }
@@ -6732,33 +6769,43 @@ nsHttpChannel::ContinueBeginConnectWithR
          " mCanceled=%u]\n",
          this, static_cast<uint32_t>(rv), static_cast<bool>(mCanceled)));
     return rv;
 }
 
 void
 nsHttpChannel::ContinueBeginConnect()
 {
+    LOG(("nsHttpChannel::ContinueBeginConnect this=%p", this));
+
     nsresult rv = ContinueBeginConnectWithResult();
     if (NS_FAILED(rv)) {
         CloseCacheEntry(false);
         Unused << AsyncAbort(rv);
     }
 }
 
 //-----------------------------------------------------------------------------
 // HttpChannel::nsIClassOfService
 //-----------------------------------------------------------------------------
 
 void
 nsHttpChannel::OnClassOfServiceUpdated()
 {
+    LOG(("nsHttpChannel::OnClassOfServiceUpdated this=%p, cos=%u",
+         this, mClassOfService));
+
     if (mTransaction) {
         gHttpHandler->UpdateClassOfServiceOnTransaction(mTransaction, mClassOfService);
     }
+    if (EligibleForTailing()) {
+        RemoveAsNonTailRequest();
+    } else {
+        AddAsNonTailRequest();
+    }
 }
 
 NS_IMETHODIMP
 nsHttpChannel::SetClassFlags(uint32_t inFlags)
 {
     uint32_t previous = mClassOfService;
     mClassOfService = inFlags;
     if (previous != mClassOfService) {
@@ -7520,17 +7567,20 @@ nsHttpChannel::OnStopRequest(nsIRequest 
     if (mListener) {
         LOG(("nsHttpChannel %p calling OnStopRequest\n", this));
         MOZ_ASSERT(mOnStartRequestCalled,
                    "OnStartRequest should be called before OnStopRequest");
         MOZ_ASSERT(!mOnStopRequestCalled,
                    "We should not call OnStopRequest twice");
         mListener->OnStopRequest(this, mListenerContext, status);
         mOnStopRequestCalled = true;
-    }
+
+    }
+
+    RemoveAsNonTailRequest();
 
     // If a preferred alt-data type was set, this signals the consumer is
     // interested in reading and/or writing the alt-data representation.
     // We need to hold a reference to the cache entry in case the listener calls
     // openAlternativeOutputStream() after CloseCacheEntry() clears mCacheEntry.
     if (!mPreferredCachedAltDataType.IsEmpty()) {
         mAltDataCacheEntry = mCacheEntry;
     }
@@ -9327,16 +9377,108 @@ nsHttpChannel::Notify(nsITimer *aTimer)
         return TriggerNetwork(0);
     } else {
         MOZ_CRASH("Unknown timer");
     }
 
     return NS_OK;
 }
 
+bool
+nsHttpChannel::EligibleForTailing()
+{
+  if (!(mClassOfService & nsIClassOfService::Tail)) {
+      return false;
+  }
+
+  if (mClassOfService & (nsIClassOfService::UrgentStart |
+                         nsIClassOfService::Leader |
+                         nsIClassOfService::TailForbidden)) {
+      return false;
+  }
+
+  if (mClassOfService & nsIClassOfService::Unblocked &&
+      !(mClassOfService & nsIClassOfService::TailAllowed)) {
+      return false;
+  }
+
+  if (IsNavigation()) {
+      return false;
+  }
+
+  return true;
+}
+
+bool
+nsHttpChannel::WaitingForTailUnblock()
+{
+  nsresult rv;
+
+  if (!gHttpHandler->IsTailBlockingEnabled()) {
+    LOG(("nsHttpChannel %p tail-blocking disabled", this));
+    return false;
+  }
+
+  if (!EligibleForTailing()) {
+    LOG(("nsHttpChannel %p not eligible for tail-blocking", this));
+    AddAsNonTailRequest();
+    return false;
+  }
+
+  if (!EnsureRequestContext()) {
+    LOG(("nsHttpChannel %p no request context", this));
+    return false;
+  }
+
+  LOG(("nsHttpChannel::WaitingForTailUnblock this=%p, rc=%p",
+       this, mRequestContext.get()));
+
+  bool blocked;
+  rv = mRequestContext->IsContextTailBlocked(this, &blocked);
+  if (NS_FAILED(rv)) {
+    return false;
+  }
+
+  LOG(("  blocked=%d", blocked));
+
+  return blocked;
+}
+
+//-----------------------------------------------------------------------------
+// nsHttpChannel::nsIRequestTailUnblockCallback
+//-----------------------------------------------------------------------------
+
+// Must be implemented in the leaf class because we don't have
+// AsyncAbort in HttpBaseChannel.
+NS_IMETHODIMP
+nsHttpChannel::OnTailUnblock(nsresult rv)
+{
+    LOG(("nsHttpChannel::OnTailUnblock this=%p rv=%" PRIx32 " rc=%p",
+         this, static_cast<uint32_t>(rv), mRequestContext.get()));
+
+    MOZ_RELEASE_ASSERT(mOnTailUnblock);
+
+    if (NS_FAILED(mStatus)) {
+        rv = mStatus;
+    }
+
+    if (NS_SUCCEEDED(rv)) {
+        auto callback = mOnTailUnblock;
+        mOnTailUnblock = nullptr;
+        rv = (this->*callback)();
+    }
+
+    if (NS_FAILED(rv)) {
+        CloseCacheEntry(false);
+        return AsyncAbort(rv);
+    }
+
+    return NS_OK;
+}
+
 void
 nsHttpChannel::SetWarningReporter(HttpChannelSecurityWarningReporter *aReporter)
 {
     LOG(("nsHttpChannel [this=%p] SetWarningReporter [%p]", this, aReporter));
     mWarningReporter = aReporter;
 }
 
 HttpChannelSecurityWarningReporter*
--- a/netwerk/protocol/http/nsHttpChannel.h
+++ b/netwerk/protocol/http/nsHttpChannel.h
@@ -80,16 +80,17 @@ class nsHttpChannel final : public HttpB
                           , public nsIThreadRetargetableRequest
                           , public nsIThreadRetargetableStreamListener
                           , public nsIDNSListener
                           , public nsSupportsWeakReference
                           , public nsICorsPreflightCallback
                           , public nsIChannelWithDivertableParentListener
                           , public nsIHstsPrimingCallback
                           , public nsIRaceCacheWithNetwork
+                          , public nsIRequestTailUnblockCallback
                           , public nsITimerCallback
 {
 public:
     NS_DECL_ISUPPORTS_INHERITED
     NS_DECL_NSIREQUESTOBSERVER
     NS_DECL_NSISTREAMLISTENER
     NS_DECL_NSITHREADRETARGETABLESTREAMLISTENER
     NS_DECL_NSICACHEINFOCHANNEL
@@ -104,16 +105,17 @@ public:
     NS_DECL_NSIASYNCVERIFYREDIRECTCALLBACK
     NS_DECL_NSIHSTSPRIMINGCALLBACK
     NS_DECL_NSITHREADRETARGETABLEREQUEST
     NS_DECL_NSIDNSLISTENER
     NS_DECL_NSICHANNELWITHDIVERTABLEPARENTLISTENER
     NS_DECLARE_STATIC_IID_ACCESSOR(NS_HTTPCHANNEL_IID)
     NS_DECL_NSIRACECACHEWITHNETWORK
     NS_DECL_NSITIMERCALLBACK
+    NS_DECL_NSIREQUESTTAILUNBLOCKCALLBACK
 
     // nsIHttpAuthenticableChannel. We can't use
     // NS_DECL_NSIHTTPAUTHENTICABLECHANNEL because it duplicates cancel() and
     // others.
     NS_IMETHOD GetIsSSL(bool *aIsSSL) override;
     NS_IMETHOD GetProxyMethodIsConnect(bool *aProxyMethodIsConnect) override;
     NS_IMETHOD GetServerResponseHeader(nsACString & aServerResponseHeader) override;
     NS_IMETHOD GetProxyChallenges(nsACString & aChallenges) override;
@@ -320,17 +322,16 @@ private:
     MOZ_MUST_USE nsresult BeginConnectContinue();
     MOZ_MUST_USE nsresult ContinueBeginConnectWithResult();
     void     ContinueBeginConnect();
     MOZ_MUST_USE nsresult OnBeforeConnect();
     void     OnBeforeConnectContinue();
     MOZ_MUST_USE nsresult Connect();
     void     SpeculativeConnect();
     MOZ_MUST_USE nsresult SetupTransaction();
-    void     SetupTransactionRequestContext();
     MOZ_MUST_USE nsresult CallOnStartRequest();
     MOZ_MUST_USE nsresult ProcessResponse();
     void                  AsyncContinueProcessResponse();
     MOZ_MUST_USE nsresult ContinueProcessResponse1();
     MOZ_MUST_USE nsresult ContinueProcessResponse2(nsresult);
     MOZ_MUST_USE nsresult ContinueProcessResponse3(nsresult);
     MOZ_MUST_USE nsresult ProcessNormal();
     MOZ_MUST_USE nsresult ContinueProcessNormal(nsresult);
@@ -668,16 +669,35 @@ private:
     // True if the channel's principal was found on a phishing, malware, or
     // tracking (if tracking protection is enabled) blocklist
     bool                              mLocalBlocklist;
 
     MOZ_MUST_USE nsresult WaitForRedirectCallback();
     void PushRedirectAsyncFunc(nsContinueRedirectionFunc func);
     void PopRedirectAsyncFunc(nsContinueRedirectionFunc func);
 
+    // If this resource is eligible for tailing based on class-of-service flags
+    // and load flags.  We don't tail Leaders/Unblocked/UrgentStart and top-level
+    // loads.
+    bool EligibleForTailing();
+
+    // Called exclusively only from AsyncOpen or after all classification callbacks.
+    // If this channel is 1) Tail, 2) assigned a request context, 3) the context is
+    // still in the tail-blocked phase, then the method will queue this channel.
+    // OnTailUnblock will be called after the context is tail-unblocked or canceled.
+    bool WaitingForTailUnblock();
+
+    // A function we trigger when untail callback is triggered by our request
+    // context in case this channel was tail-blocked.
+    nsresult (nsHttpChannel::*mOnTailUnblock)();
+    // Called on untail when tailed during AsyncOpen execution.
+    nsresult AsyncOpenOnTailUnblock();
+    // Called on untail when tailed because of being a tracking resource.
+    nsresult ConnectOnTailUnblock();
+
     nsCString mUsername;
 
     // If non-null, warnings should be reported to this object.
     RefPtr<HttpChannelSecurityWarningReporter> mWarningReporter;
 
     RefPtr<ADivertableParentChannel> mParentChannel;
 
     // True if the channel is reading from cache.
--- a/netwerk/protocol/http/nsHttpHandler.cpp
+++ b/netwerk/protocol/http/nsHttpHandler.cpp
@@ -193,16 +193,20 @@ nsHttpHandler::nsHttpHandler()
     , mMaxPersistentConnectionsPerServer(2)
     , mMaxPersistentConnectionsPerProxy(4)
     , mThrottleEnabled(true)
     , mThrottleSuspendFor(3000)
     , mThrottleResumeFor(200)
     , mThrottleResumeIn(400)
     , mThrottleTimeWindow(3000)
     , mUrgentStartEnabled(true)
+    , mTailBlockingEnabled(true)
+    , mTailDelayQuantum(600)
+    , mTailDelayQuantumAfterDCL(100)
+    , mTailDelayMax(6000)
     , mRedirectionLimit(10)
     , mPhishyUserPassLength(1)
     , mQoSBits(0x00)
     , mEnforceAssocReq(false)
     , mLastUniqueID(NowInSeconds())
     , mSessionStartTime(0)
     , mLegacyAppName("Mozilla")
     , mLegacyAppVersion("5.0")
@@ -1648,16 +1652,32 @@ nsHttpHandler::PrefsChanged(nsIPrefBranc
                                         mThrottleTimeWindow);
       }
     }
 
     if (PREF_CHANGED(HTTP_PREF("on_click_priority"))) {
         Unused << prefs->GetBoolPref(HTTP_PREF("on_click_priority"), &mUrgentStartEnabled);
     }
 
+    if (PREF_CHANGED(HTTP_PREF("tailing.enabled"))) {
+        Unused << prefs->GetBoolPref(HTTP_PREF("tailing.enabled"), &mTailBlockingEnabled);
+    }
+    if (PREF_CHANGED(HTTP_PREF("tailing.delay-quantum"))) {
+        Unused << prefs->GetIntPref(HTTP_PREF("tailing.delay-quantum"), &val);
+        mTailDelayQuantum = (uint32_t)clamped(val, 0, 60000);
+    }
+    if (PREF_CHANGED(HTTP_PREF("tailing.delay-quantum-after-domcontentloaded"))) {
+        Unused << prefs->GetIntPref(HTTP_PREF("tailing.delay-quantum-after-domcontentloaded"), &val);
+        mTailDelayQuantumAfterDCL = (uint32_t)clamped(val, 0, 60000);
+    }
+    if (PREF_CHANGED(HTTP_PREF("tailing.delay-max"))) {
+        Unused << prefs->GetIntPref(HTTP_PREF("tailing.delay-max"), &val);
+        mTailDelayMax = (uint32_t)clamped(val, 0, 60000);
+    }
+
     if (PREF_CHANGED(HTTP_PREF("focused_window_transaction_ratio"))) {
         float ratio = 0;
         rv = prefs->GetFloatPref(HTTP_PREF("focused_window_transaction_ratio"), &ratio);
         if (NS_SUCCEEDED(rv)) {
             if (ratio > 0 && ratio < 1) {
                 mFocusedWindowTransactionRatio = ratio;
             } else {
                 NS_WARNING("Wrong value for focused_window_transaction_ratio");
--- a/netwerk/protocol/http/nsHttpHandler.h
+++ b/netwerk/protocol/http/nsHttpHandler.h
@@ -131,16 +131,21 @@ public:
     uint32_t       MaxConnectionsPerOrigin() { return mMaxPersistentConnectionsPerServer; }
     bool           UseRequestTokenBucket() { return mRequestTokenBucketEnabled; }
     uint16_t       RequestTokenBucketMinParallelism() { return mRequestTokenBucketMinParallelism; }
     uint32_t       RequestTokenBucketHz() { return mRequestTokenBucketHz; }
     uint32_t       RequestTokenBucketBurst() {return mRequestTokenBucketBurst; }
 
     bool           PromptTempRedirect()      { return mPromptTempRedirect; }
     bool           IsUrgentStartEnabled() { return mUrgentStartEnabled; }
+    bool           IsTailBlockingEnabled() { return mTailBlockingEnabled; }
+    uint32_t       TailBlockingDelayQuantum(bool aAfterDOMContentLoaded) {
+      return aAfterDOMContentLoaded ? mTailDelayQuantumAfterDCL : mTailDelayQuantum;
+    }
+    uint32_t       TailBlockingDelayMax() { return mTailDelayMax; }
 
     // TCP Keepalive configuration values.
 
     // Returns true if TCP keepalive should be enabled for short-lived conns.
     bool TCPKeepaliveEnabledForShortLivedConns() {
       return mTCPKeepaliveShortLivedEnabled;
     }
     // Return time (secs) that a connection is consider short lived (for TCP
@@ -459,16 +464,20 @@ private:
 
     bool mThrottleEnabled;
     uint32_t mThrottleSuspendFor;
     uint32_t mThrottleResumeFor;
     uint32_t mThrottleResumeIn;
     uint32_t mThrottleTimeWindow;
 
     bool mUrgentStartEnabled;
+    bool mTailBlockingEnabled;
+    uint32_t mTailDelayQuantum;
+    uint32_t mTailDelayQuantumAfterDCL;
+    uint32_t mTailDelayMax;
 
     uint8_t  mRedirectionLimit;
 
     // we'll warn the user if we load an URL containing a userpass field
     // unless its length is less than this threshold.  this warning is
     // intended to protect the user against spoofing attempts that use
     // the userpass field of the URL to obscure the actual origin server.
     uint8_t  mPhishyUserPassLength;
--- a/toolkit/components/places/FaviconHelpers.cpp
+++ b/toolkit/components/places/FaviconHelpers.cpp
@@ -3,16 +3,17 @@
  * 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 "FaviconHelpers.h"
 
 #include "nsICacheEntry.h"
 #include "nsICachingChannel.h"
+#include "nsIClassOfService.h"
 #include "nsIAsyncVerifyRedirectCallback.h"
 #include "nsIPrincipal.h"
 
 #include "nsNavHistory.h"
 #include "nsFaviconService.h"
 #include "mozilla/storage.h"
 #include "mozilla/Telemetry.h"
 #include "nsNetUtil.h"
@@ -588,16 +589,22 @@ AsyncFetchAndSetIconForPage::FetchFromNe
     NS_ENSURE_SUCCESS(rv, rv);
   }
 
   nsCOMPtr<nsISupportsPriority> priorityChannel = do_QueryInterface(channel);
   if (priorityChannel) {
     priorityChannel->AdjustPriority(nsISupportsPriority::PRIORITY_LOWEST);
   }
 
+  nsCOMPtr<nsIClassOfService> cos = do_QueryInterface(channel);
+  if (cos) {
+    cos->AddClassFlags(nsIClassOfService::Tail |
+                       nsIClassOfService::Throttleable);
+  }
+
   rv = channel->AsyncOpen2(this);
   if (NS_SUCCEEDED(rv)) {
     mRequest = channel;
   }
   return rv;
 }
 
 NS_IMETHODIMP