RequestContextService.cpp (16641B)
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 ;*; */ 2 /* vim: set sw=2 ts=8 et tw=80 : */ 3 /* This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 #include "nsIDocShell.h" 8 #include "mozilla/dom/Document.h" 9 #include "nsComponentManagerUtils.h" 10 #include "nsIDocumentLoader.h" 11 #include "nsIObserverService.h" 12 #include "nsITimer.h" 13 #include "nsIXULRuntime.h" 14 #include "nsServiceManagerUtils.h" 15 #include "nsThreadUtils.h" 16 #include "RequestContextService.h" 17 18 #include "mozilla/Atomics.h" 19 #include "mozilla/ClearOnShutdown.h" 20 #include "mozilla/Logging.h" 21 #include "mozilla/Services.h" 22 #include "mozilla/StaticPtr.h" 23 #include "mozilla/TimeStamp.h" 24 25 #include "mozilla/net/NeckoChild.h" 26 #include "mozilla/net/NeckoCommon.h" 27 28 #include "../protocol/http/nsHttpHandler.h" 29 30 namespace mozilla { 31 namespace net { 32 33 LazyLogModule gRequestContextLog("RequestContext"); 34 #undef LOG 35 #define LOG(args) MOZ_LOG(gRequestContextLog, LogLevel::Info, args) 36 37 static StaticRefPtr<RequestContextService> gSingleton; 38 39 // This is used to prevent adding tail pending requests after shutdown 40 static bool sShutdown = false; 41 42 // nsIRequestContext 43 class RequestContext final : public nsIRequestContext, 44 public nsITimerCallback, 45 public nsINamed { 46 public: 47 NS_DECL_THREADSAFE_ISUPPORTS 48 NS_DECL_NSIREQUESTCONTEXT 49 NS_DECL_NSITIMERCALLBACK 50 NS_DECL_NSINAMED 51 52 explicit RequestContext(const uint64_t id); 53 54 private: 55 virtual ~RequestContext(); 56 57 void ProcessTailQueue(nsresult aResult); 58 // Reschedules the timer if needed 59 void ScheduleUnblock(); 60 // Hard-reschedules the timer 61 void RescheduleUntailTimer(TimeStamp const& now); 62 63 uint64_t mID; 64 Atomic<uint32_t> mBlockingTransactionCount; 65 66 using PendingTailRequest = nsCOMPtr<nsIRequestTailUnblockCallback>; 67 // Number of known opened non-tailed requets 68 uint32_t mNonTailRequests; 69 // Queue of requests that have been tailed, when conditions are met 70 // we call each of them to unblock and drop the reference 71 nsTArray<PendingTailRequest> mTailQueue; 72 // Loosly scheduled timer, never scheduled further to the future than 73 // mUntailAt time 74 nsCOMPtr<nsITimer> mUntailTimer; 75 // Timestamp when the timer is expected to fire, 76 // always less than or equal to mUntailAt 77 TimeStamp mTimerScheduledAt; 78 // Timestamp when we want to actually untail queued requets based on 79 // the number of request count change in the past; iff this timestamp 80 // is set, we tail requests 81 TimeStamp mUntailAt; 82 83 // Timestamp of the navigation start time, set to Now() in BeginLoad(). 84 // This is used to progressively lower the maximum delay time so that 85 // we can't get to a situation when a number of repetitive requests 86 // on the page causes forever tailing. 87 TimeStamp mBeginLoadTime; 88 89 // This member is true only between DOMContentLoaded notification and 90 // next document load beginning for this request context. 91 // Top level request contexts are recycled. 92 bool mAfterDOMContentLoaded; 93 }; 94 95 NS_IMPL_ISUPPORTS(RequestContext, nsIRequestContext, nsITimerCallback, nsINamed) 96 97 RequestContext::RequestContext(const uint64_t aID) 98 : mID(aID), 99 mBlockingTransactionCount(0), 100 mNonTailRequests(0), 101 mAfterDOMContentLoaded(false) { 102 LOG(("RequestContext::RequestContext this=%p id=%" PRIx64, this, mID)); 103 } 104 105 RequestContext::~RequestContext() { 106 MOZ_ASSERT(mTailQueue.Length() == 0); 107 108 LOG(("RequestContext::~RequestContext this=%p blockers=%u", this, 109 static_cast<uint32_t>(mBlockingTransactionCount))); 110 } 111 112 NS_IMETHODIMP 113 RequestContext::BeginLoad() { 114 MOZ_ASSERT(NS_IsMainThread()); 115 116 LOG(("RequestContext::BeginLoad %p", this)); 117 118 if (IsNeckoChild()) { 119 // Tailing is not supported on the child process 120 if (gNeckoChild) { 121 gNeckoChild->SendRequestContextLoadBegin(mID); 122 } 123 return NS_OK; 124 } 125 126 mAfterDOMContentLoaded = false; 127 mBeginLoadTime = TimeStamp::NowLoRes(); 128 return NS_OK; 129 } 130 131 NS_IMETHODIMP 132 RequestContext::DOMContentLoaded() { 133 MOZ_ASSERT(NS_IsMainThread()); 134 135 LOG(("RequestContext::DOMContentLoaded %p", this)); 136 137 if (IsNeckoChild()) { 138 // Tailing is not supported on the child process 139 if (gNeckoChild) { 140 gNeckoChild->SendRequestContextAfterDOMContentLoaded(mID); 141 } 142 return NS_OK; 143 } 144 145 if (mAfterDOMContentLoaded) { 146 // There is a possibility of a duplicate notification 147 return NS_OK; 148 } 149 150 mAfterDOMContentLoaded = true; 151 152 // Conditions for the delay calculation has changed. 153 ScheduleUnblock(); 154 return NS_OK; 155 } 156 157 NS_IMETHODIMP 158 RequestContext::GetBlockingTransactionCount( 159 uint32_t* aBlockingTransactionCount) { 160 NS_ENSURE_ARG_POINTER(aBlockingTransactionCount); 161 *aBlockingTransactionCount = mBlockingTransactionCount; 162 return NS_OK; 163 } 164 165 NS_IMETHODIMP 166 RequestContext::AddBlockingTransaction() { 167 mBlockingTransactionCount++; 168 LOG(("RequestContext::AddBlockingTransaction this=%p blockers=%u", this, 169 static_cast<uint32_t>(mBlockingTransactionCount))); 170 return NS_OK; 171 } 172 173 NS_IMETHODIMP 174 RequestContext::RemoveBlockingTransaction(uint32_t* outval) { 175 NS_ENSURE_ARG_POINTER(outval); 176 mBlockingTransactionCount--; 177 LOG(("RequestContext::RemoveBlockingTransaction this=%p blockers=%u", this, 178 static_cast<uint32_t>(mBlockingTransactionCount))); 179 *outval = mBlockingTransactionCount; 180 return NS_OK; 181 } 182 183 uint64_t RequestContext::GetID() { return mID; } 184 185 NS_IMETHODIMP 186 RequestContext::AddNonTailRequest() { 187 MOZ_ASSERT(NS_IsMainThread()); 188 189 ++mNonTailRequests; 190 LOG(("RequestContext::AddNonTailRequest this=%p, cnt=%u", this, 191 mNonTailRequests)); 192 193 ScheduleUnblock(); 194 return NS_OK; 195 } 196 197 NS_IMETHODIMP 198 RequestContext::RemoveNonTailRequest() { 199 MOZ_ASSERT(NS_IsMainThread()); 200 MOZ_ASSERT(mNonTailRequests > 0); 201 202 LOG(("RequestContext::RemoveNonTailRequest this=%p, cnt=%u", this, 203 mNonTailRequests - 1)); 204 205 --mNonTailRequests; 206 207 ScheduleUnblock(); 208 return NS_OK; 209 } 210 211 void RequestContext::ScheduleUnblock() { 212 MOZ_ASSERT(!IsNeckoChild()); 213 MOZ_ASSERT(NS_IsMainThread()); 214 215 if (!gHttpHandler) { 216 return; 217 } 218 219 uint32_t quantum = 220 gHttpHandler->TailBlockingDelayQuantum(mAfterDOMContentLoaded); 221 uint32_t delayMax = gHttpHandler->TailBlockingDelayMax(); 222 uint32_t totalMax = gHttpHandler->TailBlockingTotalMax(); 223 224 if (!mBeginLoadTime.IsNull()) { 225 // We decrease the maximum delay progressively with the time since the page 226 // load begin. This seems like a reasonable and clear heuristic allowing us 227 // to start loading tailed requests in a deterministic time after the load 228 // has started. 229 230 uint32_t sinceBeginLoad = static_cast<uint32_t>( 231 (TimeStamp::NowLoRes() - mBeginLoadTime).ToMilliseconds()); 232 uint32_t tillTotal = totalMax - std::min(sinceBeginLoad, totalMax); 233 uint32_t proportion = totalMax // values clamped between 0 and 60'000 234 ? (delayMax * tillTotal) / totalMax 235 : 0; 236 delayMax = std::min(delayMax, proportion); 237 } 238 239 CheckedInt<uint32_t> delay = quantum * mNonTailRequests; 240 241 if (!mAfterDOMContentLoaded) { 242 // Before DOMContentLoaded notification we want to make sure that tailed 243 // requests don't start when there is a short delay during which we may 244 // not have any active requests on the page happening. 245 delay += quantum; 246 } 247 248 if (!delay.isValid() || delay.value() > delayMax) { 249 delay = delayMax; 250 } 251 252 LOG( 253 ("RequestContext::ScheduleUnblock this=%p non-tails=%u tail-queue=%zu " 254 "delay=%u after-DCL=%d", 255 this, mNonTailRequests, mTailQueue.Length(), delay.value(), 256 mAfterDOMContentLoaded)); 257 258 TimeStamp now = TimeStamp::NowLoRes(); 259 mUntailAt = now + TimeDuration::FromMilliseconds(delay.value()); 260 261 if (mTimerScheduledAt.IsNull() || mUntailAt < mTimerScheduledAt) { 262 LOG(("RequestContext %p timer would fire too late, rescheduling", this)); 263 RescheduleUntailTimer(now); 264 } 265 } 266 267 void RequestContext::RescheduleUntailTimer(TimeStamp const& now) { 268 MOZ_ASSERT(mUntailAt >= now); 269 270 if (mUntailTimer) { 271 mUntailTimer->Cancel(); 272 } 273 274 if (!mTailQueue.Length()) { 275 mUntailTimer = nullptr; 276 mTimerScheduledAt = TimeStamp(); 277 return; 278 } 279 280 TimeDuration interval = mUntailAt - now; 281 if (!mTimerScheduledAt.IsNull() && mUntailAt < mTimerScheduledAt) { 282 // When the number of untailed requests goes down, 283 // let's half the interval, since it's likely we would 284 // reschedule for a shorter time again very soon. 285 // This will likely save rescheduling this timer. 286 interval = interval / int64_t(2); 287 mTimerScheduledAt = mUntailAt - interval; 288 } else { 289 mTimerScheduledAt = mUntailAt; 290 } 291 292 uint32_t delay = interval.ToMilliseconds(); 293 nsresult rv = 294 NS_NewTimerWithCallback(getter_AddRefs(mUntailTimer), this, delay, 295 nsITimer::TYPE_ONE_SHOT, nullptr); 296 if (NS_FAILED(rv)) { 297 NS_WARNING("Could not reschedule untail timer"); 298 } 299 300 LOG(("RequestContext::RescheduleUntailTimer %p in %d", this, delay)); 301 } 302 303 NS_IMETHODIMP 304 RequestContext::Notify(nsITimer* timer) { 305 MOZ_ASSERT(NS_IsMainThread()); 306 MOZ_ASSERT(timer == mUntailTimer); 307 MOZ_ASSERT(!mTimerScheduledAt.IsNull()); 308 MOZ_ASSERT(mTailQueue.Length()); 309 310 mUntailTimer = nullptr; 311 312 TimeStamp now = TimeStamp::NowLoRes(); 313 if (mUntailAt > mTimerScheduledAt && mUntailAt > now) { 314 LOG(("RequestContext %p timer fired too soon, rescheduling", this)); 315 RescheduleUntailTimer(now); 316 return NS_OK; 317 } 318 319 // Must drop to allow re-engage of the timer 320 mTimerScheduledAt = TimeStamp(); 321 322 ProcessTailQueue(NS_OK); 323 324 return NS_OK; 325 } 326 327 NS_IMETHODIMP 328 RequestContext::GetName(nsACString& aName) { 329 aName.AssignLiteral("RequestContext"); 330 return NS_OK; 331 } 332 333 NS_IMETHODIMP 334 RequestContext::IsContextTailBlocked(nsIRequestTailUnblockCallback* aRequest, 335 bool* aBlocked) { 336 MOZ_ASSERT(NS_IsMainThread()); 337 338 LOG(("RequestContext::IsContextTailBlocked this=%p, request=%p, queued=%zu", 339 this, aRequest, mTailQueue.Length())); 340 341 *aBlocked = false; 342 343 if (sShutdown) { 344 return NS_ERROR_ILLEGAL_DURING_SHUTDOWN; 345 } 346 347 if (mUntailAt.IsNull()) { 348 LOG((" untail time passed")); 349 return NS_OK; 350 } 351 352 if (mAfterDOMContentLoaded && !mNonTailRequests) { 353 LOG((" after DOMContentLoaded and no untailed requests")); 354 return NS_OK; 355 } 356 357 if (!gHttpHandler) { 358 // Xpcshell tests may not have http handler 359 LOG((" missing gHttpHandler?")); 360 return NS_OK; 361 } 362 363 *aBlocked = true; 364 mTailQueue.AppendElement(aRequest); 365 366 LOG((" request queued")); 367 368 if (!mUntailTimer) { 369 ScheduleUnblock(); 370 } 371 372 return NS_OK; 373 } 374 375 NS_IMETHODIMP 376 RequestContext::CancelTailedRequest(nsIRequestTailUnblockCallback* aRequest) { 377 MOZ_ASSERT(NS_IsMainThread()); 378 379 bool removed = mTailQueue.RemoveElement(aRequest); 380 381 LOG(("RequestContext::CancelTailedRequest %p req=%p removed=%d", this, 382 aRequest, removed)); 383 384 // Stop untail timer if all tail requests are canceled. 385 if (removed && mTailQueue.IsEmpty()) { 386 if (mUntailTimer) { 387 mUntailTimer->Cancel(); 388 mUntailTimer = nullptr; 389 } 390 391 // Must drop to allow re-engage of the timer 392 mTimerScheduledAt = TimeStamp(); 393 } 394 395 return NS_OK; 396 } 397 398 void RequestContext::ProcessTailQueue(nsresult aResult) { 399 LOG(("RequestContext::ProcessTailQueue this=%p, queued=%zu, rv=%" PRIx32, 400 this, mTailQueue.Length(), static_cast<uint32_t>(aResult))); 401 402 if (mUntailTimer) { 403 mUntailTimer->Cancel(); 404 mUntailTimer = nullptr; 405 } 406 407 // Must drop to stop tailing requests 408 mUntailAt = TimeStamp(); 409 410 nsTArray<PendingTailRequest> queue = std::move(mTailQueue); 411 412 for (const auto& request : queue) { 413 LOG((" untailing %p", request.get())); 414 request->OnTailUnblock(aResult); 415 } 416 } 417 418 NS_IMETHODIMP 419 RequestContext::CancelTailPendingRequests(nsresult aResult) { 420 MOZ_ASSERT(NS_IsMainThread()); 421 MOZ_ASSERT(NS_FAILED(aResult)); 422 423 ProcessTailQueue(aResult); 424 return NS_OK; 425 } 426 427 // nsIRequestContextService 428 RequestContextService* RequestContextService::sSelf = nullptr; 429 430 NS_IMPL_ISUPPORTS(RequestContextService, nsIRequestContextService, nsIObserver) 431 432 RequestContextService::RequestContextService() { 433 MOZ_ASSERT(!sSelf, "multiple rcs instances!"); 434 MOZ_ASSERT(NS_IsMainThread()); 435 sSelf = this; 436 437 nsCOMPtr<nsIXULRuntime> runtime = do_GetService("@mozilla.org/xre/runtime;1"); 438 runtime->GetProcessID(&mRCIDNamespace); 439 } 440 441 RequestContextService::~RequestContextService() { 442 MOZ_ASSERT(NS_IsMainThread()); 443 Shutdown(); 444 sSelf = nullptr; 445 } 446 447 nsresult RequestContextService::Init() { 448 nsresult rv; 449 450 MOZ_ASSERT(NS_IsMainThread()); 451 nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); 452 if (!obs) { 453 return NS_ERROR_NOT_AVAILABLE; 454 } 455 456 rv = obs->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false); 457 if (NS_FAILED(rv)) { 458 return rv; 459 } 460 obs->AddObserver(this, "content-document-interactive", false); 461 if (NS_FAILED(rv)) { 462 return rv; 463 } 464 465 return NS_OK; 466 } 467 468 void RequestContextService::Shutdown() { 469 MOZ_ASSERT(NS_IsMainThread()); 470 // We need to do this to prevent the requests from being scheduled after 471 // shutdown. 472 for (const auto& data : mTable.Values()) { 473 data->CancelTailPendingRequests(NS_ERROR_ABORT); 474 } 475 mTable.Clear(); 476 sShutdown = true; 477 } 478 479 /* static */ 480 already_AddRefed<nsIRequestContextService> 481 RequestContextService::GetOrCreate() { 482 MOZ_ASSERT(NS_IsMainThread()); 483 484 if (sShutdown) { 485 return nullptr; 486 } 487 488 RefPtr<RequestContextService> svc; 489 if (gSingleton) { 490 svc = gSingleton; 491 } else { 492 svc = new RequestContextService(); 493 nsresult rv = svc->Init(); 494 NS_ENSURE_SUCCESS(rv, nullptr); 495 gSingleton = svc; 496 ClearOnShutdown(&gSingleton); 497 } 498 499 return svc.forget(); 500 } 501 502 NS_IMETHODIMP 503 RequestContextService::GetRequestContext(const uint64_t rcID, 504 nsIRequestContext** rc) { 505 MOZ_ASSERT(NS_IsMainThread()); 506 NS_ENSURE_ARG_POINTER(rc); 507 *rc = nullptr; 508 509 if (sShutdown) { 510 return NS_ERROR_ILLEGAL_DURING_SHUTDOWN; 511 } 512 513 if (!rcID) { 514 return NS_ERROR_INVALID_ARG; 515 } 516 517 *rc = do_AddRef(mTable.LookupOrInsertWith(rcID, [&] { 518 nsCOMPtr<nsIRequestContext> newSC = new RequestContext(rcID); 519 return newSC; 520 })).take(); 521 522 return NS_OK; 523 } 524 525 NS_IMETHODIMP 526 RequestContextService::GetRequestContextFromLoadGroup(nsILoadGroup* aLoadGroup, 527 nsIRequestContext** rc) { 528 nsresult rv; 529 530 uint64_t rcID; 531 rv = aLoadGroup->GetRequestContextID(&rcID); 532 if (NS_FAILED(rv)) { 533 return rv; 534 } 535 536 return GetRequestContext(rcID, rc); 537 } 538 539 NS_IMETHODIMP 540 RequestContextService::NewRequestContext(nsIRequestContext** rc) { 541 MOZ_ASSERT(NS_IsMainThread()); 542 NS_ENSURE_ARG_POINTER(rc); 543 *rc = nullptr; 544 545 if (sShutdown) { 546 return NS_ERROR_ILLEGAL_DURING_SHUTDOWN; 547 } 548 549 uint64_t rcID = 550 ((static_cast<uint64_t>(mRCIDNamespace) << 32) & 0xFFFFFFFF00000000LL) | 551 mNextRCID++; 552 553 nsCOMPtr<nsIRequestContext> newSC = new RequestContext(rcID); 554 mTable.InsertOrUpdate(rcID, newSC); 555 newSC.swap(*rc); 556 557 return NS_OK; 558 } 559 560 NS_IMETHODIMP 561 RequestContextService::RemoveRequestContext(const uint64_t rcID) { 562 MOZ_ASSERT(NS_IsMainThread()); 563 mTable.Remove(rcID); 564 return NS_OK; 565 } 566 567 NS_IMETHODIMP 568 RequestContextService::Observe(nsISupports* subject, const char* topic, 569 const char16_t* data_unicode) { 570 MOZ_ASSERT(NS_IsMainThread()); 571 if (!strcmp(NS_XPCOM_SHUTDOWN_OBSERVER_ID, topic)) { 572 Shutdown(); 573 return NS_OK; 574 } 575 576 if (!strcmp("content-document-interactive", topic)) { 577 nsCOMPtr<dom::Document> document(do_QueryInterface(subject)); 578 MOZ_ASSERT(document); 579 // We want this be triggered also for iframes, since those track their 580 // own request context ids. 581 if (!document) { 582 return NS_OK; 583 } 584 nsIDocShell* ds = document->GetDocShell(); 585 // XML documents don't always have a docshell assigned 586 if (!ds) { 587 return NS_OK; 588 } 589 nsCOMPtr<nsIDocumentLoader> dl(do_QueryInterface(ds)); 590 if (!dl) { 591 return NS_OK; 592 } 593 nsCOMPtr<nsILoadGroup> lg; 594 dl->GetLoadGroup(getter_AddRefs(lg)); 595 if (!lg) { 596 return NS_OK; 597 } 598 nsCOMPtr<nsIRequestContext> rc; 599 GetRequestContextFromLoadGroup(lg, getter_AddRefs(rc)); 600 if (rc) { 601 rc->DOMContentLoaded(); 602 } 603 604 return NS_OK; 605 } 606 607 MOZ_ASSERT(false, "Unexpected observer topic"); 608 return NS_OK; 609 } 610 611 } // namespace net 612 } // namespace mozilla 613 614 #undef LOG