/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set sw=2 ts=2 et tw=79: */ /* 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 "mozilla/DebugOnly.h" #include "mozilla/Likely.h" #include "mozilla/dom/ScriptLoader.h" #include "mozilla/dom/nsCSPService.h" #include "GeckoProfiler.h" #include "mozAutoDocUpdate.h" #include "mozilla/IdleTaskRunner.h" #include "mozilla/Preferences.h" #include "mozilla/StaticPrefs.h" #include "mozilla/css/Loader.h" #include "nsContentUtils.h" #include "nsDocShell.h" #include "nsError.h" #include "nsHtml5AutoPauseUpdate.h" #include "nsHtml5Parser.h" #include "nsHtml5StreamParser.h" #include "nsHtml5Tokenizer.h" #include "nsHtml5TreeBuilder.h" #include "nsHtml5TreeOpExecutor.h" #include "nsIContentSecurityPolicy.h" #include "nsIContentViewer.h" #include "nsIDocShell.h" #include "nsIDocShellTreeItem.h" #include "nsIHTMLDocument.h" #include "nsINestedURI.h" #include "nsIScriptContext.h" #include "nsIScriptError.h" #include "nsIScriptGlobalObject.h" #include "nsIViewSourceChannel.h" #include "nsNetUtil.h" #include "xpcpublic.h" using namespace mozilla; NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED(nsHtml5TreeOpExecutor, nsHtml5DocumentBuilder, nsIContentSink) class nsHtml5ExecutorReflusher : public Runnable { private: RefPtr mExecutor; public: explicit nsHtml5ExecutorReflusher(nsHtml5TreeOpExecutor* aExecutor) : mozilla::Runnable("nsHtml5ExecutorReflusher") , mExecutor(aExecutor) { } NS_IMETHOD Run() override { mExecutor->RunFlushLoop(); return NS_OK; } }; class MOZ_RAII nsHtml5AutoFlush final { private: RefPtr mExecutor; size_t mOpsToRemove; public: explicit nsHtml5AutoFlush(nsHtml5TreeOpExecutor* aExecutor) : mExecutor(aExecutor) , mOpsToRemove(aExecutor->OpQueueLength()) { mExecutor->BeginFlush(); mExecutor->BeginDocUpdate(); } ~nsHtml5AutoFlush() { if (mExecutor->IsInDocUpdate()) { mExecutor->EndDocUpdate(); } else { // We aren't in an update if nsHtml5AutoPauseUpdate // caused something to terminate the parser. MOZ_RELEASE_ASSERT( mExecutor->IsComplete(), "How do we have mParser but the doc update isn't open?"); } mExecutor->EndFlush(); mExecutor->RemoveFromStartOfOpQueue(mOpsToRemove); } void SetNumberOfOpsToRemove(size_t aOpsToRemove) { MOZ_ASSERT(aOpsToRemove < mOpsToRemove, "Requested partial clearing of op queue but the number to clear " "wasn't less than the length of the queue."); mOpsToRemove = aOpsToRemove; } }; static mozilla::LinkedList* gBackgroundFlushList = nullptr; StaticRefPtr gBackgroundFlushRunner; nsHtml5TreeOpExecutor::nsHtml5TreeOpExecutor() : nsHtml5DocumentBuilder(false) , mSuppressEOF(false) , mReadingFromStage(false) , mStreamParser(nullptr) , mPreloadedURLs(23) // Mean # of preloadable resources per page on dmoz , mSpeculationReferrerPolicy(mozilla::net::RP_Unset) , mStarted(false) , mRunFlushLoopOnStack(false) , mCallContinueInterruptedParsingIfEnabled(false) , mAlreadyComplainedAboutCharset(false) { } nsHtml5TreeOpExecutor::~nsHtml5TreeOpExecutor() { if (gBackgroundFlushList && isInList()) { ClearOpQueue(); removeFrom(*gBackgroundFlushList); if (gBackgroundFlushList->isEmpty()) { delete gBackgroundFlushList; gBackgroundFlushList = nullptr; if (gBackgroundFlushRunner) { gBackgroundFlushRunner->Cancel(); gBackgroundFlushRunner = nullptr; } } } NS_ASSERTION(mOpQueue.IsEmpty(), "Somehow there's stuff in the op queue."); } // nsIContentSink NS_IMETHODIMP nsHtml5TreeOpExecutor::WillParse() { MOZ_ASSERT_UNREACHABLE("No one should call this"); return NS_ERROR_NOT_IMPLEMENTED; } NS_IMETHODIMP nsHtml5TreeOpExecutor::WillBuildModel(nsDTDMode aDTDMode) { mDocument->AddObserver(this); WillBuildModelImpl(); GetDocument()->BeginLoad(); if (mDocShell && !GetDocument()->GetWindow() && !IsExternalViewSource()) { // Not loading as data but script global object not ready return MarkAsBroken(NS_ERROR_DOM_INVALID_STATE_ERR); } return NS_OK; } // This is called when the tree construction has ended NS_IMETHODIMP nsHtml5TreeOpExecutor::DidBuildModel(bool aTerminated) { if (mRunsToCompletion) { return NS_OK; } MOZ_RELEASE_ASSERT(!IsInDocUpdate(), "DidBuildModel from inside a doc update."); // This comes from nsXMLContentSink and nsHTMLContentSink // If this parser has been marked as broken, treat the end of parse as // forced termination. DidBuildModelImpl(aTerminated || NS_FAILED(IsBroken())); if (!mLayoutStarted) { // We never saw the body, and layout never got started. Force // layout *now*, to get an initial reflow. // NOTE: only force the layout if we are NOT destroying the // docshell. If we are destroying it, then starting layout will // likely cause us to crash, or at best waste a lot of time as we // are just going to tear it down anyway. bool destroying = true; if (mDocShell) { mDocShell->IsBeingDestroyed(&destroying); } if (!destroying) { nsContentSink::StartLayout(false); } } ScrollToRef(); mDocument->RemoveObserver(this); if (!mParser) { // DidBuildModelImpl may cause mParser to be nulled out // Return early to avoid unblocking the onload event too many times. return NS_OK; } // We may not have called BeginLoad() if loading is terminated before // OnStartRequest call. if (mStarted) { mDocument->EndLoad(); } GetParser()->DropStreamParser(); DropParserAndPerfHint(); #ifdef GATHER_DOCWRITE_STATISTICS printf("UNSAFE SCRIPTS: %d\n", sUnsafeDocWrites); printf("TOKENIZER-SAFE SCRIPTS: %d\n", sTokenSafeDocWrites); printf("TREEBUILDER-SAFE SCRIPTS: %d\n", sTreeSafeDocWrites); #endif #ifdef DEBUG_NS_HTML5_TREE_OP_EXECUTOR_FLUSH printf("MAX NOTIFICATION BATCH LEN: %d\n", sAppendBatchMaxSize); if (sAppendBatchExaminations != 0) { printf("AVERAGE SLOTS EXAMINED: %d\n", sAppendBatchSlotsExamined / sAppendBatchExaminations); } #endif return NS_OK; } NS_IMETHODIMP nsHtml5TreeOpExecutor::WillInterrupt() { MOZ_ASSERT_UNREACHABLE("Don't call. For interface compat only."); return NS_ERROR_NOT_IMPLEMENTED; } NS_IMETHODIMP nsHtml5TreeOpExecutor::WillResume() { MOZ_ASSERT_UNREACHABLE("Don't call. For interface compat only."); return NS_ERROR_NOT_IMPLEMENTED; } NS_IMETHODIMP nsHtml5TreeOpExecutor::SetParser(nsParserBase* aParser) { mParser = aParser; return NS_OK; } void nsHtml5TreeOpExecutor::FlushPendingNotifications(FlushType aType) { if (aType >= FlushType::EnsurePresShellInitAndFrames) { // Bug 577508 / 253951 nsContentSink::StartLayout(true); } } nsISupports* nsHtml5TreeOpExecutor::GetTarget() { return mDocument; } nsresult nsHtml5TreeOpExecutor::MarkAsBroken(nsresult aReason) { NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); mBroken = aReason; if (mStreamParser) { mStreamParser->Terminate(); } // We are under memory pressure, but let's hope the following allocation // works out so that we get to terminate and clean up the parser from // a safer point. if (mParser && mDocument) { // can mParser ever be null here? nsCOMPtr terminator = NewRunnableMethod( "nsHtml5Parser::Terminate", GetParser(), &nsHtml5Parser::Terminate); if (NS_FAILED( mDocument->Dispatch(TaskCategory::Network, terminator.forget()))) { NS_WARNING("failed to dispatch executor flush event"); } } return aReason; } static bool BackgroundFlushCallback(TimeStamp /*aDeadline*/) { RefPtr ex = gBackgroundFlushList->popFirst(); if (ex) { ex->RunFlushLoop(); } if (gBackgroundFlushList && gBackgroundFlushList->isEmpty()) { delete gBackgroundFlushList; gBackgroundFlushList = nullptr; gBackgroundFlushRunner->Cancel(); gBackgroundFlushRunner = nullptr; return true; } return true; } void nsHtml5TreeOpExecutor::ContinueInterruptedParsingAsync() { if (!mDocument || !mDocument->IsInBackgroundWindow()) { nsCOMPtr flusher = new nsHtml5ExecutorReflusher(this); if (NS_FAILED( mDocument->Dispatch(TaskCategory::Network, flusher.forget()))) { NS_WARNING("failed to dispatch executor flush event"); } } else { if (!gBackgroundFlushList) { gBackgroundFlushList = new mozilla::LinkedList(); } if (!isInList()) { gBackgroundFlushList->insertBack(this); } if (gBackgroundFlushRunner) { return; } // Now we set up a repetitive idle scheduler for flushing background list. gBackgroundFlushRunner = IdleTaskRunner::Create( &BackgroundFlushCallback, "nsHtml5TreeOpExecutor::BackgroundFlushCallback", 250, // The hard deadline: 250ms. nsContentSink::sInteractiveParseTime / 1000, // Required budget. true, // repeating [] { return false; }); // MayStopProcessing } } void nsHtml5TreeOpExecutor::FlushSpeculativeLoads() { nsTArray speculativeLoadQueue; mStage.MoveSpeculativeLoadsTo(speculativeLoadQueue); nsHtml5SpeculativeLoad* start = speculativeLoadQueue.Elements(); nsHtml5SpeculativeLoad* end = start + speculativeLoadQueue.Length(); for (nsHtml5SpeculativeLoad* iter = start; iter < end; ++iter) { if (MOZ_UNLIKELY(!mParser)) { // An extension terminated the parser from a HTTP observer. return; } iter->Perform(this); } } class nsHtml5FlushLoopGuard { private: RefPtr mExecutor; #ifdef DEBUG_NS_HTML5_TREE_OP_EXECUTOR_FLUSH uint32_t mStartTime; #endif public: explicit nsHtml5FlushLoopGuard(nsHtml5TreeOpExecutor* aExecutor) : mExecutor(aExecutor) #ifdef DEBUG_NS_HTML5_TREE_OP_EXECUTOR_FLUSH , mStartTime(PR_IntervalToMilliseconds(PR_IntervalNow())) #endif { mExecutor->mRunFlushLoopOnStack = true; } ~nsHtml5FlushLoopGuard() { #ifdef DEBUG_NS_HTML5_TREE_OP_EXECUTOR_FLUSH uint32_t timeOffTheEventLoop = PR_IntervalToMilliseconds(PR_IntervalNow()) - mStartTime; if (timeOffTheEventLoop > nsHtml5TreeOpExecutor::sLongestTimeOffTheEventLoop) { nsHtml5TreeOpExecutor::sLongestTimeOffTheEventLoop = timeOffTheEventLoop; } printf("Longest time off the event loop: %d\n", nsHtml5TreeOpExecutor::sLongestTimeOffTheEventLoop); #endif mExecutor->mRunFlushLoopOnStack = false; } }; /** * The purpose of the loop here is to avoid returning to the main event loop */ void nsHtml5TreeOpExecutor::RunFlushLoop() { AUTO_PROFILER_LABEL("nsHtml5TreeOpExecutor::RunFlushLoop", OTHER); if (mRunFlushLoopOnStack) { // There's already a RunFlushLoop() on the call stack. return; } nsHtml5FlushLoopGuard guard(this); // this is also the self-kungfu! RefPtr parserKungFuDeathGrip(mParser); // Remember the entry time (void)nsContentSink::WillParseImpl(); for (;;) { if (!mParser) { // Parse has terminated. ClearOpQueue(); // clear in order to be able to assert in destructor return; } if (NS_FAILED(IsBroken())) { return; } if (!parserKungFuDeathGrip->IsParserEnabled()) { // The parser is blocked. return; } if (mFlushState != eNotFlushing) { // XXX Can this happen? In case it can, let's avoid crashing. return; } // If there are scripts executing, then the content sink is jumping the gun // (probably due to a synchronous XMLHttpRequest) and will re-enable us // later, see bug 460706. if (IsScriptExecuting()) { return; } if (mReadingFromStage) { nsTArray speculativeLoadQueue; MOZ_RELEASE_ASSERT(mFlushState == eNotFlushing, "mOpQueue modified during flush."); mStage.MoveOpsAndSpeculativeLoadsTo(mOpQueue, speculativeLoadQueue); // Make sure speculative loads never start after the corresponding // normal loads for the same URLs. nsHtml5SpeculativeLoad* start = speculativeLoadQueue.Elements(); nsHtml5SpeculativeLoad* end = start + speculativeLoadQueue.Length(); for (nsHtml5SpeculativeLoad* iter = start; iter < end; ++iter) { iter->Perform(this); if (MOZ_UNLIKELY(!mParser)) { // An extension terminated the parser from a HTTP observer. ClearOpQueue(); // clear in order to be able to assert in destructor return; } } } else { FlushSpeculativeLoads(); // Make sure speculative loads never start after // the corresponding normal loads for the same // URLs. if (MOZ_UNLIKELY(!mParser)) { // An extension terminated the parser from a HTTP observer. ClearOpQueue(); // clear in order to be able to assert in destructor return; } // Not sure if this grip is still needed, but previously, the code // gripped before calling ParseUntilBlocked(); RefPtr streamKungFuDeathGrip = GetParser()->GetStreamParser(); mozilla::Unused << streamKungFuDeathGrip; // Not used within function // Now parse content left in the document.write() buffer queue if any. // This may generate tree ops on its own or dequeue a speculation. nsresult rv = GetParser()->ParseUntilBlocked(); if (NS_FAILED(rv)) { MarkAsBroken(rv); return; } } if (mOpQueue.IsEmpty()) { // Avoid bothering the rest of the engine with a doc update if there's // nothing to do. return; } nsIContent* scriptElement = nullptr; bool interrupted = false; bool streamEnded = false; { // autoFlush clears mOpQueue in its destructor unless // SetNumberOfOpsToRemove is called first, in which case only // some ops from the start of the queue are cleared. nsHtml5AutoFlush autoFlush(this); nsHtml5TreeOperation* first = mOpQueue.Elements(); nsHtml5TreeOperation* last = first + mOpQueue.Length() - 1; for (nsHtml5TreeOperation* iter = first;; ++iter) { if (MOZ_UNLIKELY(!mParser)) { // The previous tree op caused a call to nsIParser::Terminate(). return; } MOZ_ASSERT(IsInDocUpdate(), "Tried to perform tree op outside update batch."); nsresult rv = iter->Perform(this, &scriptElement, &interrupted, &streamEnded); if (NS_FAILED(rv)) { MarkAsBroken(rv); break; } // Be sure not to check the deadline if the last op was just performed. if (MOZ_UNLIKELY(iter == last)) { break; } else if (MOZ_UNLIKELY(interrupted) || MOZ_UNLIKELY(nsContentSink::DidProcessATokenImpl() == NS_ERROR_HTMLPARSER_INTERRUPTED)) { autoFlush.SetNumberOfOpsToRemove((iter - first) + 1); nsHtml5TreeOpExecutor::ContinueInterruptedParsingAsync(); return; } } if (MOZ_UNLIKELY(!mParser)) { // The parse ended during an update pause. return; } if (streamEnded) { GetParser()->PermanentlyUndefineInsertionPoint(); } } // end autoFlush if (MOZ_UNLIKELY(!mParser)) { // Ending the doc update caused a call to nsIParser::Terminate(). return; } if (streamEnded) { DidBuildModel(false); #ifdef DEBUG if (scriptElement) { nsCOMPtr sele = do_QueryInterface(scriptElement); if (!sele) { MOZ_ASSERT(nsNameSpaceManager::GetInstance()->mSVGDisabled, "Node didn't QI to script, but SVG wasn't disabled."); } MOZ_ASSERT(sele->IsMalformed(), "Script wasn't marked as malformed."); } #endif } else if (scriptElement) { // must be tail call when mFlushState is eNotFlushing RunScript(scriptElement); // Always check the clock in nsContentSink right after a script StopDeflecting(); if (nsContentSink::DidProcessATokenImpl() == NS_ERROR_HTMLPARSER_INTERRUPTED) { #ifdef DEBUG_NS_HTML5_TREE_OP_EXECUTOR_FLUSH printf("REFLUSH SCHEDULED (after script): %d\n", ++sTimesFlushLoopInterrupted); #endif nsHtml5TreeOpExecutor::ContinueInterruptedParsingAsync(); return; } } } } nsresult nsHtml5TreeOpExecutor::FlushDocumentWrite() { nsresult rv = IsBroken(); NS_ENSURE_SUCCESS(rv, rv); FlushSpeculativeLoads(); // Make sure speculative loads never start after the // corresponding normal loads for the same URLs. if (MOZ_UNLIKELY(!mParser)) { // The parse has ended. ClearOpQueue(); // clear in order to be able to assert in destructor return rv; } if (mFlushState != eNotFlushing) { // XXX Can this happen? In case it can, let's avoid crashing. return rv; } // avoid crashing near EOF RefPtr kungFuDeathGrip(this); RefPtr parserKungFuDeathGrip(mParser); mozilla::Unused << parserKungFuDeathGrip; // Intentionally not used within function MOZ_RELEASE_ASSERT(!mReadingFromStage, "Got doc write flush when reading from stage"); #ifdef DEBUG mStage.AssertEmpty(); #endif nsIContent* scriptElement = nullptr; bool interrupted = false; bool streamEnded = false; { // autoFlush clears mOpQueue in its destructor. nsHtml5AutoFlush autoFlush(this); nsHtml5TreeOperation* start = mOpQueue.Elements(); nsHtml5TreeOperation* end = start + mOpQueue.Length(); for (nsHtml5TreeOperation* iter = start; iter < end; ++iter) { if (MOZ_UNLIKELY(!mParser)) { // The previous tree op caused a call to nsIParser::Terminate(). return rv; } NS_ASSERTION(IsInDocUpdate(), "Tried to perform tree op outside update batch."); rv = iter->Perform(this, &scriptElement, &interrupted, &streamEnded); if (NS_FAILED(rv)) { MarkAsBroken(rv); break; } } if (MOZ_UNLIKELY(!mParser)) { // The parse ended during an update pause. return rv; } if (streamEnded) { // This should be redundant but let's do it just in case. GetParser()->PermanentlyUndefineInsertionPoint(); } } // autoFlush if (MOZ_UNLIKELY(!mParser)) { // Ending the doc update caused a call to nsIParser::Terminate(). return rv; } if (streamEnded) { DidBuildModel(false); #ifdef DEBUG if (scriptElement) { nsCOMPtr sele = do_QueryInterface(scriptElement); if (!sele) { MOZ_ASSERT(nsNameSpaceManager::GetInstance()->mSVGDisabled, "Node didn't QI to script, but SVG wasn't disabled."); } MOZ_ASSERT(sele->IsMalformed(), "Script wasn't marked as malformed."); } #endif } else if (scriptElement) { // must be tail call when mFlushState is eNotFlushing RunScript(scriptElement); } return rv; } // copied from HTML content sink bool nsHtml5TreeOpExecutor::IsScriptEnabled() { // Note that if we have no document or no docshell or no global or whatnot we // want to claim script _is_ enabled, so we don't parse the contents of //