/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 ;*; */ /* vim: set sw=2 ts=8 et tw=80 : */ /* 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 "nsIDocShell.h" #include "mozilla/dom/Document.h" #include "nsComponentManagerUtils.h" #include "nsIDocumentLoader.h" #include "nsIObserverService.h" #include "nsIXULRuntime.h" #include "nsServiceManagerUtils.h" #include "nsThreadUtils.h" #include "RequestContextService.h" #include "mozilla/Atomics.h" #include "mozilla/ClearOnShutdown.h" #include "mozilla/Logging.h" #include "mozilla/Services.h" #include "mozilla/StaticPtr.h" #include "mozilla/TimeStamp.h" #include "mozilla/net/NeckoChild.h" #include "mozilla/net/NeckoCommon.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) static StaticRefPtr gSingleton; // This is used to prevent adding tail pending requests after shutdown static bool sShutdown = false; // 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 mBlockingTransactionCount; UniquePtr mSpdyCache; typedef nsCOMPtr 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 mTailQueue; // Loosly scheduled timer, never scheduled further to the future than // mUntailAt time nsCOMPtr 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; // Timestamp of the navigation start time, set to Now() in BeginLoad(). // This is used to progressively lower the maximum delay time so that // we can't get to a situation when a number of repetitive requests // on the page causes forever tailing. TimeStamp mBeginLoadTime; // 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, 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(mBlockingTransactionCount))); } NS_IMETHODIMP RequestContext::BeginLoad() { MOZ_ASSERT(NS_IsMainThread()); LOG(("RequestContext::BeginLoad %p", this)); if (IsNeckoChild()) { // Tailing is not supported on the child process if (gNeckoChild) { gNeckoChild->SendRequestContextLoadBegin(mID); } return NS_OK; } mAfterDOMContentLoaded = false; mBeginLoadTime = TimeStamp::NowLoRes(); return NS_OK; } NS_IMETHODIMP RequestContext::DOMContentLoaded() { MOZ_ASSERT(NS_IsMainThread()); LOG(("RequestContext::DOMContentLoaded %p", this)); if (IsNeckoChild()) { // Tailing is not supported on the child process if (gNeckoChild) { 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 RequestContext::AddBlockingTransaction() { mBlockingTransactionCount++; LOG(("RequestContext::AddBlockingTransaction this=%p blockers=%u", this, static_cast(mBlockingTransactionCount))); return NS_OK; } NS_IMETHODIMP RequestContext::RemoveBlockingTransaction(uint32_t* outval) { NS_ENSURE_ARG_POINTER(outval); mBlockingTransactionCount--; LOG(("RequestContext::RemoveBlockingTransaction this=%p blockers=%u", this, static_cast(mBlockingTransactionCount))); *outval = mBlockingTransactionCount; return NS_OK; } SpdyPushCache* RequestContext::GetSpdyPushCache() { return mSpdyCache.get(); } void RequestContext::SetSpdyPushCache(SpdyPushCache* aSpdyPushCache) { mSpdyCache = WrapUnique(aSpdyPushCache); } uint64_t RequestContext::GetID() { return mID; } 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(); uint32_t totalMax = gHttpHandler->TailBlockingTotalMax(); if (!mBeginLoadTime.IsNull()) { // We decrease the maximum delay progressively with the time since the page // load begin. This seems like a reasonable and clear heuristic allowing us // to start loading tailed requests in a deterministic time after the load // has started. uint32_t sinceBeginLoad = static_cast( (TimeStamp::NowLoRes() - mBeginLoadTime).ToMilliseconds()); uint32_t tillTotal = totalMax - std::min(sinceBeginLoad, totalMax); uint32_t proportion = totalMax // values clamped between 0 and 60'000 ? (delayMax * tillTotal) / totalMax : 0; delayMax = std::min(delayMax, proportion); } CheckedInt 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 (sShutdown) { return NS_ERROR_ILLEGAL_DURING_SHUTDOWN; } if (mUntailAt.IsNull()) { LOG((" untail time passed")); 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)); // Stop untail timer if all tail requests are canceled. if (removed && mTailQueue.IsEmpty()) { if (mUntailTimer) { mUntailTimer->Cancel(); mUntailTimer = nullptr; } // Must drop to allow re-engage of the timer mTimerScheduledAt = TimeStamp(); } return NS_OK; } void RequestContext::ProcessTailQueue(nsresult aResult) { LOG(("RequestContext::ProcessTailQueue this=%p, queued=%zu, rv=%" PRIx32, this, mTailQueue.Length(), static_cast(aResult))); if (mUntailTimer) { mUntailTimer->Cancel(); mUntailTimer = nullptr; } // Must drop to stop tailing requests mUntailAt = TimeStamp(); nsTArray queue = std::move(mTailQueue); for (const 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() : mRCIDNamespace(0), mNextRCID(1) { MOZ_ASSERT(!sSelf, "multiple rcs instances!"); MOZ_ASSERT(NS_IsMainThread()); sSelf = this; nsCOMPtr runtime = do_GetService("@mozilla.org/xre/runtime;1"); runtime->GetProcessID(&mRCIDNamespace); } RequestContextService::~RequestContextService() { MOZ_ASSERT(NS_IsMainThread()); Shutdown(); sSelf = nullptr; } nsresult RequestContextService::Init() { nsresult rv; MOZ_ASSERT(NS_IsMainThread()); nsCOMPtr obs = mozilla::services::GetObserverService(); if (!obs) { return NS_ERROR_NOT_AVAILABLE; } 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()); // We need to do this to prevent the requests from being scheduled after // shutdown. for (auto iter = mTable.Iter(); !iter.Done(); iter.Next()) { iter.Data()->CancelTailPendingRequests(NS_ERROR_ABORT); } mTable.Clear(); sShutdown = true; } /* static */ already_AddRefed RequestContextService::GetOrCreate() { MOZ_ASSERT(NS_IsMainThread()); if (sShutdown) { return nullptr; } RefPtr svc; if (gSingleton) { svc = gSingleton; } else { svc = new RequestContextService(); nsresult rv = svc->Init(); NS_ENSURE_SUCCESS(rv, nullptr); gSingleton = svc; ClearOnShutdown(&gSingleton); } return svc.forget(); } NS_IMETHODIMP RequestContextService::GetRequestContext(const uint64_t rcID, nsIRequestContext** rc) { MOZ_ASSERT(NS_IsMainThread()); NS_ENSURE_ARG_POINTER(rc); *rc = nullptr; if (sShutdown) { return NS_ERROR_ILLEGAL_DURING_SHUTDOWN; } if (!rcID) { return NS_ERROR_INVALID_ARG; } if (!mTable.Get(rcID, rc)) { nsCOMPtr newSC = new RequestContext(rcID); 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; if (sShutdown) { return NS_ERROR_ILLEGAL_DURING_SHUTDOWN; } uint64_t rcID = ((static_cast(mRCIDNamespace) << 32) & 0xFFFFFFFF00000000LL) | mNextRCID++; nsCOMPtr newSC = new RequestContext(rcID); mTable.Put(rcID, newSC); newSC.swap(*rc); return NS_OK; } NS_IMETHODIMP RequestContextService::RemoveRequestContext(const uint64_t 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 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 dl(do_QueryInterface(ds)); if (!dl) { return NS_OK; } nsCOMPtr lg; dl->GetLoadGroup(getter_AddRefs(lg)); if (!lg) { return NS_OK; } nsCOMPtr rc; GetRequestContextFromLoadGroup(lg, getter_AddRefs(rc)); if (rc) { rc->DOMContentLoaded(); } return NS_OK; } MOZ_ASSERT(false, "Unexpected observer topic"); return NS_OK; } } // namespace net } // namespace mozilla #undef LOG