| /* |
| * Copyright (C) 2010 Google, Inc. All Rights Reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY |
| * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
| * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR |
| * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
| * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
| * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
| * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY |
| * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| #include "core/html/parser/HTMLScriptRunner.h" |
| |
| #include "bindings/core/v8/Microtask.h" |
| #include "bindings/core/v8/ScriptSourceCode.h" |
| #include "bindings/core/v8/V8PerIsolateData.h" |
| #include "core/dom/DocumentParserTiming.h" |
| #include "core/dom/Element.h" |
| #include "core/dom/IgnoreDestructiveWriteCountIncrementer.h" |
| #include "core/dom/ScriptLoader.h" |
| #include "core/dom/TaskRunnerHelper.h" |
| #include "core/events/Event.h" |
| #include "core/fetch/MemoryCache.h" |
| #include "core/fetch/ScriptResource.h" |
| #include "core/frame/LocalFrame.h" |
| #include "core/html/parser/HTMLInputStream.h" |
| #include "core/html/parser/HTMLScriptRunnerHost.h" |
| #include "core/html/parser/NestingLevelIncrementer.h" |
| #include "platform/Histogram.h" |
| #include "platform/TraceEvent.h" |
| #include "platform/TracedValue.h" |
| #include "public/platform/Platform.h" |
| #include "public/platform/WebFrameScheduler.h" |
| #include <inttypes.h> |
| #include <memory> |
| |
| namespace blink { |
| |
| namespace { |
| |
| // TODO(bmcquade): move this to a shared location if we find ourselves wanting |
| // to trace similar data elsewhere in the codebase. |
| std::unique_ptr<TracedValue> getTraceArgsForScriptElement( |
| Element* element, |
| const TextPosition& textPosition) { |
| std::unique_ptr<TracedValue> value = TracedValue::create(); |
| ScriptLoader* scriptLoader = toScriptLoaderIfPossible(element); |
| if (scriptLoader && scriptLoader->resource()) |
| value->setString("url", scriptLoader->resource()->url().getString()); |
| if (element->ownerDocument() && element->ownerDocument()->frame()) |
| value->setString( |
| "frame", |
| String::format("0x%" PRIx64, |
| static_cast<uint64_t>(reinterpret_cast<intptr_t>( |
| element->ownerDocument()->frame())))); |
| if (textPosition.m_line.zeroBasedInt() > 0 || |
| textPosition.m_column.zeroBasedInt() > 0) { |
| value->setInteger("lineNumber", textPosition.m_line.oneBasedInt()); |
| value->setInteger("columnNumber", textPosition.m_column.oneBasedInt()); |
| } |
| return value; |
| } |
| |
| bool doExecuteScript(Element* scriptElement, |
| const ScriptSourceCode& sourceCode, |
| const TextPosition& textPosition) { |
| ScriptLoader* scriptLoader = toScriptLoaderIfPossible(scriptElement); |
| ASSERT(scriptLoader); |
| TRACE_EVENT_WITH_FLOW1( |
| "blink", "HTMLScriptRunner ExecuteScript", scriptElement, |
| TRACE_EVENT_FLAG_FLOW_IN, "data", |
| getTraceArgsForScriptElement(scriptElement, textPosition)); |
| return scriptLoader->executeScript(sourceCode); |
| } |
| |
| void traceParserBlockingScript(const PendingScript* pendingScript, |
| bool waitingForResources) { |
| // The HTML parser must yield before executing script in the following |
| // cases: |
| // * the script's execution is blocked on the completed load of the script |
| // resource |
| // (https://html.spec.whatwg.org/multipage/scripting.html#pending-parsing-blocking-script) |
| // * the script's execution is blocked on the load of a style sheet or other |
| // resources that are blocking scripts |
| // (https://html.spec.whatwg.org/multipage/semantics.html#a-style-sheet-that-is-blocking-scripts) |
| // |
| // Both of these cases can introduce significant latency when loading a |
| // web page, especially for users on slow connections, since the HTML parser |
| // must yield until the blocking resources finish loading. |
| // |
| // We trace these parser yields here using flow events, so we can track |
| // both when these yields occur, as well as how long the parser had |
| // to yield. The connecting flow events are traced once the parser becomes |
| // unblocked when the script actually executes, in doExecuteScript. |
| Element* element = pendingScript->element(); |
| if (!element) |
| return; |
| TextPosition scriptStartPosition = pendingScript->startingPosition(); |
| if (!pendingScript->isReady()) { |
| if (waitingForResources) { |
| TRACE_EVENT_WITH_FLOW1( |
| "blink", "YieldParserForScriptLoadAndBlockingResources", element, |
| TRACE_EVENT_FLAG_FLOW_OUT, "data", |
| getTraceArgsForScriptElement(element, scriptStartPosition)); |
| } else { |
| TRACE_EVENT_WITH_FLOW1( |
| "blink", "YieldParserForScriptLoad", element, |
| TRACE_EVENT_FLAG_FLOW_OUT, "data", |
| getTraceArgsForScriptElement(element, scriptStartPosition)); |
| } |
| } else if (waitingForResources) { |
| TRACE_EVENT_WITH_FLOW1( |
| "blink", "YieldParserForScriptBlockingResources", element, |
| TRACE_EVENT_FLAG_FLOW_OUT, "data", |
| getTraceArgsForScriptElement(element, scriptStartPosition)); |
| } |
| } |
| |
| static KURL documentURLForScriptExecution(Document* document) { |
| if (!document) |
| return KURL(); |
| |
| if (!document->frame()) { |
| if (document->importsController()) |
| return document->url(); |
| return KURL(); |
| } |
| |
| // Use the URL of the currently active document for this frame. |
| return document->frame()->document()->url(); |
| } |
| |
| } // namespace |
| |
| using namespace HTMLNames; |
| |
| HTMLScriptRunner::HTMLScriptRunner(HTMLParserReentryPermit* reentryPermit, |
| Document* document, |
| HTMLScriptRunnerHost* host) |
| : m_reentryPermit(reentryPermit), |
| m_document(document), |
| m_host(host), |
| m_parserBlockingScript(PendingScript::create(nullptr, nullptr)), |
| m_hasScriptsWaitingForResources(false) { |
| ASSERT(m_host); |
| ThreadState::current()->registerPreFinalizer(this); |
| } |
| |
| HTMLScriptRunner::~HTMLScriptRunner() { |
| // Verify that detach() has been called. |
| ASSERT(!m_document); |
| } |
| |
| void HTMLScriptRunner::detach() { |
| if (!m_document) |
| return; |
| |
| m_parserBlockingScript->stopWatchingForLoad(); |
| m_parserBlockingScript->releaseElementAndClear(); |
| |
| while (!m_scriptsToExecuteAfterParsing.isEmpty()) { |
| PendingScript* pendingScript = m_scriptsToExecuteAfterParsing.takeFirst(); |
| pendingScript->stopWatchingForLoad(); |
| pendingScript->releaseElementAndClear(); |
| } |
| m_document = nullptr; |
| // m_reentryPermit is not cleared here, because the script runner |
| // may continue to run pending scripts after the parser has |
| // detached. |
| } |
| |
| bool HTMLScriptRunner::isPendingScriptReady(const PendingScript* script) { |
| m_hasScriptsWaitingForResources = !m_document->isScriptExecutionReady(); |
| if (m_hasScriptsWaitingForResources) |
| return false; |
| return script->isReady(); |
| } |
| |
| void HTMLScriptRunner::executeParsingBlockingScript() { |
| ASSERT(m_document); |
| ASSERT(!isExecutingScript()); |
| ASSERT(m_document->isScriptExecutionReady()); |
| ASSERT(isPendingScriptReady(m_parserBlockingScript.get())); |
| |
| InsertionPointRecord insertionPointRecord(m_host->inputStream()); |
| executePendingScriptAndDispatchEvent(m_parserBlockingScript.get(), |
| ScriptStreamer::ParsingBlocking); |
| } |
| |
| void HTMLScriptRunner::executePendingScriptAndDispatchEvent( |
| PendingScript* pendingScript, |
| ScriptStreamer::Type pendingScriptType) { |
| bool errorOccurred = false; |
| ScriptSourceCode sourceCode = pendingScript->getSource( |
| documentURLForScriptExecution(m_document), errorOccurred); |
| |
| // Stop watching loads before executeScript to prevent recursion if the script |
| // reloads itself. |
| pendingScript->stopWatchingForLoad(); |
| |
| if (!isExecutingScript()) { |
| Microtask::performCheckpoint(V8PerIsolateData::mainThreadIsolate()); |
| if (pendingScriptType == ScriptStreamer::ParsingBlocking) { |
| m_hasScriptsWaitingForResources = !m_document->isScriptExecutionReady(); |
| // The parser cannot be unblocked as a microtask requested another |
| // resource |
| if (m_hasScriptsWaitingForResources) |
| return; |
| } |
| } |
| |
| TextPosition scriptStartPosition = pendingScript->startingPosition(); |
| double scriptParserBlockingTime = |
| pendingScript->parserBlockingLoadStartTime(); |
| // Clear the pending script before possible re-entrancy from executeScript() |
| Element* element = pendingScript->releaseElementAndClear(); |
| if (ScriptLoader* scriptLoader = toScriptLoaderIfPossible(element)) { |
| HTMLParserReentryPermit::ScriptNestingLevelIncrementer |
| nestingLevelIncrementer = |
| m_reentryPermit->incrementScriptNestingLevel(); |
| IgnoreDestructiveWriteCountIncrementer |
| ignoreDestructiveWriteCountIncrementer(m_document); |
| if (errorOccurred) { |
| TRACE_EVENT_WITH_FLOW1( |
| "blink", "HTMLScriptRunner ExecuteScriptFailed", element, |
| TRACE_EVENT_FLAG_FLOW_IN, "data", |
| getTraceArgsForScriptElement(element, scriptStartPosition)); |
| scriptLoader->dispatchErrorEvent(); |
| } else { |
| ASSERT(isExecutingScript()); |
| if (scriptParserBlockingTime > 0.0) { |
| DocumentParserTiming::from(*m_document) |
| .recordParserBlockedOnScriptLoadDuration( |
| monotonicallyIncreasingTime() - scriptParserBlockingTime, |
| scriptLoader->wasCreatedDuringDocumentWrite()); |
| } |
| if (!doExecuteScript(element, sourceCode, scriptStartPosition)) { |
| scriptLoader->dispatchErrorEvent(); |
| } else { |
| element->dispatchEvent(Event::create(EventTypeNames::load)); |
| } |
| } |
| } |
| |
| ASSERT(!isExecutingScript()); |
| } |
| |
| void HTMLScriptRunner::stopWatchingResourceForLoad(Resource* resource) { |
| if (m_parserBlockingScript->resource() == resource) { |
| m_parserBlockingScript->stopWatchingForLoad(); |
| m_parserBlockingScript->releaseElementAndClear(); |
| return; |
| } |
| for (auto& script : m_scriptsToExecuteAfterParsing) { |
| if (script->resource() == resource) { |
| script->stopWatchingForLoad(); |
| script->releaseElementAndClear(); |
| return; |
| } |
| } |
| } |
| |
| void fetchBlockedDocWriteScript(Element* script, |
| bool isParserInserted, |
| const TextPosition& scriptStartPosition) { |
| DCHECK(script); |
| |
| ScriptLoader* scriptLoader = |
| ScriptLoader::create(script, isParserInserted, false, false); |
| DCHECK(scriptLoader); |
| scriptLoader->setFetchDocWrittenScriptDeferIdle(); |
| scriptLoader->prepareScript(scriptStartPosition); |
| } |
| |
| void HTMLScriptRunner::possiblyFetchBlockedDocWriteScript(Resource* resource) { |
| // If the script was blocked as part of document.write intervention, |
| // then send an asynchronous GET request with an interventions header. |
| Element* element = nullptr; |
| TextPosition startingPosition; |
| bool isParserInserted = false; |
| |
| if (!resource->errorOccurred() || !m_parserBlockingScript || |
| !(m_parserBlockingScript->resource() == resource)) |
| return; |
| |
| // Due to dependency violation, not able to check the exact error to be |
| // ERR_CACHE_MISS but other errors are rare with |
| // WebCachePolicy::ReturnCacheDataDontLoad. |
| element = m_parserBlockingScript->element(); |
| |
| ScriptLoader* scriptLoader = nullptr; |
| if (element && (scriptLoader = toScriptLoaderIfPossible(element)) && |
| scriptLoader->disallowedFetchForDocWrittenScript()) { |
| startingPosition = m_parserBlockingScript->startingPosition(); |
| isParserInserted = scriptLoader->isParserInserted(); |
| // remove this resource entry from memory cache as the new request |
| // should not join onto this existing entry. |
| memoryCache()->remove(resource); |
| fetchBlockedDocWriteScript(element, isParserInserted, startingPosition); |
| } |
| } |
| |
| void HTMLScriptRunner::notifyFinished(Resource* cachedResource) { |
| // Handle cancellations of parser-blocking script loads without |
| // notifying the host (i.e., parser) if these were initiated by nested |
| // document.write()s. The cancellation may have been triggered by |
| // script execution to signal an abrupt stop (e.g., window.close().) |
| // |
| // The parser is unprepared to be told, and doesn't need to be. |
| if (isExecutingScript() && cachedResource->wasCanceled()) { |
| stopWatchingResourceForLoad(cachedResource); |
| return; |
| } |
| |
| // If the script was blocked as part of document.write intervention, |
| // then send an asynchronous GET request with an interventions header. |
| possiblyFetchBlockedDocWriteScript(cachedResource); |
| |
| m_host->notifyScriptLoaded(cachedResource); |
| } |
| |
| // Implements the steps for 'An end tag whose tag name is "script"' |
| // http://whatwg.org/html#scriptEndTag |
| // Script handling lives outside the tree builder to keep each class simple. |
| void HTMLScriptRunner::execute(Element* scriptElement, |
| const TextPosition& scriptStartPosition) { |
| ASSERT(scriptElement); |
| TRACE_EVENT1( |
| "blink", "HTMLScriptRunner::execute", "data", |
| getTraceArgsForScriptElement(scriptElement, scriptStartPosition)); |
| // FIXME: If scripting is disabled, always just return. |
| |
| bool hadPreloadScanner = m_host->hasPreloadScanner(); |
| |
| // Try to execute the script given to us. |
| runScript(scriptElement, scriptStartPosition); |
| |
| if (hasParserBlockingScript()) { |
| if (isExecutingScript()) { |
| // Unwind to the outermost HTMLScriptRunner::execute before continuing |
| // parsing. |
| return; |
| } |
| |
| traceParserBlockingScript(m_parserBlockingScript.get(), |
| !m_document->isScriptExecutionReady()); |
| m_parserBlockingScript->markParserBlockingLoadStartTime(); |
| |
| // If preload scanner got created, it is missing the source after the |
| // current insertion point. Append it and scan. |
| if (!hadPreloadScanner && m_host->hasPreloadScanner()) |
| m_host->appendCurrentInputStreamToPreloadScannerAndScan(); |
| executeParsingBlockingScripts(); |
| } |
| } |
| |
| bool HTMLScriptRunner::hasParserBlockingScript() const { |
| return !!m_parserBlockingScript->element(); |
| } |
| |
| void HTMLScriptRunner::executeParsingBlockingScripts() { |
| while (hasParserBlockingScript() && |
| isPendingScriptReady(m_parserBlockingScript.get())) |
| executeParsingBlockingScript(); |
| } |
| |
| void HTMLScriptRunner::executeScriptsWaitingForLoad(Resource* resource) { |
| TRACE_EVENT0("blink", "HTMLScriptRunner::executeScriptsWaitingForLoad"); |
| ASSERT(!isExecutingScript()); |
| ASSERT(hasParserBlockingScript()); |
| ASSERT_UNUSED(resource, m_parserBlockingScript->resource() == resource); |
| ASSERT(m_parserBlockingScript->isReady()); |
| executeParsingBlockingScripts(); |
| } |
| |
| void HTMLScriptRunner::executeScriptsWaitingForResources() { |
| TRACE_EVENT0("blink", "HTMLScriptRunner::executeScriptsWaitingForResources"); |
| ASSERT(m_document); |
| // Callers should check hasScriptsWaitingForResources() before calling |
| // to prevent parser or script re-entry during </style> parsing. |
| ASSERT(hasScriptsWaitingForResources()); |
| ASSERT(!isExecutingScript()); |
| ASSERT(m_document->isScriptExecutionReady()); |
| executeParsingBlockingScripts(); |
| } |
| |
| bool HTMLScriptRunner::executeScriptsWaitingForParsing() { |
| TRACE_EVENT0("blink", "HTMLScriptRunner::executeScriptsWaitingForParsing"); |
| while (!m_scriptsToExecuteAfterParsing.isEmpty()) { |
| ASSERT(!isExecutingScript()); |
| ASSERT(!hasParserBlockingScript()); |
| ASSERT(m_scriptsToExecuteAfterParsing.first()->resource()); |
| if (!m_scriptsToExecuteAfterParsing.first()->isReady()) { |
| m_scriptsToExecuteAfterParsing.first()->watchForLoad(this); |
| traceParserBlockingScript(m_scriptsToExecuteAfterParsing.first().get(), |
| !m_document->isScriptExecutionReady()); |
| m_scriptsToExecuteAfterParsing.first()->markParserBlockingLoadStartTime(); |
| return false; |
| } |
| PendingScript* first = m_scriptsToExecuteAfterParsing.takeFirst(); |
| executePendingScriptAndDispatchEvent(first, ScriptStreamer::Deferred); |
| // FIXME: What is this m_document check for? |
| if (!m_document) |
| return false; |
| } |
| return true; |
| } |
| |
| void HTMLScriptRunner::requestParsingBlockingScript(Element* element) { |
| if (!requestPendingScript(m_parserBlockingScript.get(), element)) |
| return; |
| |
| ASSERT(m_parserBlockingScript->resource()); |
| |
| // We only care about a load callback if resource is not already in the cache. |
| // Callers will attempt to run the m_parserBlockingScript if possible before |
| // returning control to the parser. |
| if (!m_parserBlockingScript->isReady()) { |
| if (m_document->frame()) { |
| ScriptState* scriptState = ScriptState::forMainWorld(m_document->frame()); |
| if (scriptState) |
| ScriptStreamer::startStreaming( |
| m_parserBlockingScript.get(), ScriptStreamer::ParsingBlocking, |
| m_document->frame()->settings(), scriptState, |
| TaskRunnerHelper::get(TaskType::Networking, m_document)); |
| } |
| |
| m_parserBlockingScript->watchForLoad(this); |
| } |
| } |
| |
| void HTMLScriptRunner::requestDeferredScript(Element* element) { |
| PendingScript* pendingScript = PendingScript::create(nullptr, nullptr); |
| if (!requestPendingScript(pendingScript, element)) |
| return; |
| |
| if (m_document->frame() && !pendingScript->isReady()) { |
| ScriptState* scriptState = ScriptState::forMainWorld(m_document->frame()); |
| if (scriptState) |
| ScriptStreamer::startStreaming( |
| pendingScript, ScriptStreamer::Deferred, |
| m_document->frame()->settings(), scriptState, |
| TaskRunnerHelper::get(TaskType::Networking, m_document)); |
| } |
| |
| ASSERT(pendingScript->resource()); |
| m_scriptsToExecuteAfterParsing.append(pendingScript); |
| } |
| |
| bool HTMLScriptRunner::requestPendingScript(PendingScript* pendingScript, |
| Element* script) const { |
| ASSERT(!pendingScript->element()); |
| pendingScript->setElement(script); |
| // This should correctly return 0 for empty or invalid srcValues. |
| ScriptResource* resource = toScriptLoaderIfPossible(script)->resource(); |
| if (!resource) { |
| DVLOG(1) << "Not implemented."; // Dispatch error event. |
| return false; |
| } |
| pendingScript->setScriptResource(resource); |
| return true; |
| } |
| |
| // Implements the initial steps for 'An end tag whose tag name is "script"' |
| // http://whatwg.org/html#scriptEndTag |
| void HTMLScriptRunner::runScript(Element* script, |
| const TextPosition& scriptStartPosition) { |
| ASSERT(m_document); |
| ASSERT(!hasParserBlockingScript()); |
| { |
| ScriptLoader* scriptLoader = toScriptLoaderIfPossible(script); |
| |
| // This contains both and ASSERTION and a null check since we should not |
| // be getting into the case of a null script element, but seem to be from |
| // time to time. The assertion is left in to help find those cases and |
| // is being tracked by <https://bugs.webkit.org/show_bug.cgi?id=60559>. |
| ASSERT(scriptLoader); |
| if (!scriptLoader) |
| return; |
| |
| ASSERT(scriptLoader->isParserInserted()); |
| |
| if (!isExecutingScript()) |
| Microtask::performCheckpoint(V8PerIsolateData::mainThreadIsolate()); |
| |
| InsertionPointRecord insertionPointRecord(m_host->inputStream()); |
| HTMLParserReentryPermit::ScriptNestingLevelIncrementer |
| nestingLevelIncrementer = |
| m_reentryPermit->incrementScriptNestingLevel(); |
| |
| scriptLoader->prepareScript(scriptStartPosition); |
| |
| if (!scriptLoader->willBeParserExecuted()) |
| return; |
| |
| if (scriptLoader->willExecuteWhenDocumentFinishedParsing()) { |
| requestDeferredScript(script); |
| } else if (scriptLoader->readyToBeParserExecuted()) { |
| if (m_reentryPermit->scriptNestingLevel() == 1u) { |
| m_parserBlockingScript->setElement(script); |
| m_parserBlockingScript->setStartingPosition(scriptStartPosition); |
| } else { |
| DCHECK_GT(m_reentryPermit->scriptNestingLevel(), 1u); |
| m_parserBlockingScript->releaseElementAndClear(); |
| ScriptSourceCode sourceCode(script->textContent(), |
| documentURLForScriptExecution(m_document), |
| scriptStartPosition); |
| doExecuteScript(script, sourceCode, scriptStartPosition); |
| } |
| } else { |
| requestParsingBlockingScript(script); |
| } |
| } |
| } |
| |
| DEFINE_TRACE(HTMLScriptRunner) { |
| visitor->trace(m_document); |
| visitor->trace(m_host); |
| visitor->trace(m_parserBlockingScript); |
| visitor->trace(m_scriptsToExecuteAfterParsing); |
| ScriptResourceClient::trace(visitor); |
| } |
| |
| } // namespace blink |