blob: ca737a21b99e23838b1da3350478635099a80d90 [file] [log] [blame]
/*
* 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 "third_party/blink/renderer/core/script/html_parser_script_runner.h"
#include <inttypes.h>
#include <memory>
#include "third_party/blink/public/platform/platform.h"
#include "third_party/blink/public/platform/task_type.h"
#include "third_party/blink/renderer/core/dom/document_parser_timing.h"
#include "third_party/blink/renderer/core/dom/element.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/html/parser/html_input_stream.h"
#include "third_party/blink/renderer/core/html/parser/nesting_level_incrementer.h"
#include "third_party/blink/renderer/core/script/html_parser_script_runner_host.h"
#include "third_party/blink/renderer/core/script/ignore_destructive_write_count_incrementer.h"
#include "third_party/blink/renderer/core/script/script_loader.h"
#include "third_party/blink/renderer/platform/bindings/microtask.h"
#include "third_party/blink/renderer/platform/bindings/v8_per_isolate_data.h"
#include "third_party/blink/renderer/platform/histogram.h"
#include "third_party/blink/renderer/platform/instrumentation/tracing/trace_event.h"
#include "third_party/blink/renderer/platform/instrumentation/tracing/traced_value.h"
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(
Document& document,
const TextPosition& text_position,
const KURL& url) {
std::unique_ptr<TracedValue> value = TracedValue::Create();
if (!url.IsNull())
value->SetString("url", url.GetString());
if (document.GetFrame()) {
value->SetString(
"frame",
String::Format("0x%" PRIx64,
static_cast<uint64_t>(
reinterpret_cast<intptr_t>(document.GetFrame()))));
}
if (text_position.line_.ZeroBasedInt() > 0 ||
text_position.column_.ZeroBasedInt() > 0) {
value->SetInteger("lineNumber", text_position.line_.OneBasedInt());
value->SetInteger("columnNumber", text_position.column_.OneBasedInt());
}
return value;
}
std::unique_ptr<TracedValue> GetTraceArgsForScriptElement(
const PendingScript* pending_script) {
DCHECK(pending_script);
return GetTraceArgsForScriptElement(
pending_script->GetElement()->GetDocument(),
pending_script->StartingPosition(), pending_script->UrlForTracing());
}
void DoExecuteScript(PendingScript* pending_script, const KURL& document_url) {
TRACE_EVENT_WITH_FLOW1("blink", "HTMLParserScriptRunner ExecuteScript",
pending_script->GetElement(), TRACE_EVENT_FLAG_FLOW_IN,
"data", GetTraceArgsForScriptElement(pending_script));
pending_script->ExecuteScriptBlock(document_url);
}
void TraceParserBlockingScript(const PendingScript* pending_script,
bool waiting_for_resources) {
// 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.
ScriptElementBase* element = pending_script->GetElement();
if (!element)
return;
if (!pending_script->IsReady()) {
if (waiting_for_resources) {
TRACE_EVENT_WITH_FLOW1("blink",
"YieldParserForScriptLoadAndBlockingResources",
element, TRACE_EVENT_FLAG_FLOW_OUT, "data",
GetTraceArgsForScriptElement(pending_script));
} else {
TRACE_EVENT_WITH_FLOW1("blink", "YieldParserForScriptLoad", element,
TRACE_EVENT_FLAG_FLOW_OUT, "data",
GetTraceArgsForScriptElement(pending_script));
}
} else if (waiting_for_resources) {
TRACE_EVENT_WITH_FLOW1("blink", "YieldParserForScriptBlockingResources",
element, TRACE_EVENT_FLAG_FLOW_OUT, "data",
GetTraceArgsForScriptElement(pending_script));
}
}
static KURL DocumentURLForScriptExecution(Document* document) {
if (!document)
return KURL();
if (!document->GetFrame()) {
if (document->ImportsController())
return document->Url();
return KURL();
}
// Use the URL of the currently active document for this frame.
return document->GetFrame()->GetDocument()->Url();
}
} // namespace
HTMLParserScriptRunner::HTMLParserScriptRunner(
HTMLParserReentryPermit* reentry_permit,
Document* document,
HTMLParserScriptRunnerHost* host)
: reentry_permit_(reentry_permit), document_(document), host_(host) {
DCHECK(host_);
}
HTMLParserScriptRunner::~HTMLParserScriptRunner() {}
void HTMLParserScriptRunner::Detach() {
if (!document_)
return;
if (parser_blocking_script_)
parser_blocking_script_->Dispose();
parser_blocking_script_ = nullptr;
while (!scripts_to_execute_after_parsing_.IsEmpty()) {
PendingScript* pending_script =
scripts_to_execute_after_parsing_.TakeFirst();
pending_script->Dispose();
}
document_ = nullptr;
// m_reentryPermit is not cleared here, because the script runner
// may continue to run pending scripts after the parser has
// detached.
}
bool HTMLParserScriptRunner::IsParserBlockingScriptReady() {
DCHECK(ParserBlockingScript());
if (!document_->IsScriptExecutionReady())
return false;
return ParserBlockingScript()->IsReady();
}
// Corresponds to some steps of the "Otherwise" Clause of 'An end tag whose
// tag name is "script"'
// <specdef href="https://html.spec.whatwg.org/#scriptEndTag">
void HTMLParserScriptRunner::
ExecutePendingParserBlockingScriptAndDispatchEvent() {
// Stop watching loads before executeScript to prevent recursion if the script
// reloads itself.
// TODO(kouhei): Consider merging this w/ pendingScript->dispose() after the
// if block.
// TODO(kouhei, hiroshige): Consider merging this w/ the code clearing
// |parser_blocking_script_| below.
PendingScript* pending_script = parser_blocking_script_;
pending_script->StopWatchingForLoad();
if (!IsExecutingScript()) {
// TODO(kouhei, hiroshige): Investigate why we need checkpoint here.
Microtask::PerformCheckpoint(V8PerIsolateData::MainThreadIsolate());
// The parser cannot be unblocked as a microtask requested another
// resource
if (!document_->IsScriptExecutionReady())
return;
}
// <spec step="B.1">Let the script be the pending parsing-blocking script.
// There is no longer a pending parsing-blocking script.</spec>
parser_blocking_script_ = nullptr;
{
// <spec step="B.7">Increment the parser's script nesting level by one (it
// should be zero before this step, so this sets it to one).</spec>
HTMLParserReentryPermit::ScriptNestingLevelIncrementer
nesting_level_incrementer =
reentry_permit_->IncrementScriptNestingLevel();
// TODO(hiroshige): Remove IgnoreDestructiveWriteCountIncrementer here,
// according to the spec. After https://crbug.com/721914 is resolved,
// |document_| is equal to the element's context document used in
// PendingScript::ExecuteScriptBlockInternal(), and thus this can be removed
// more easily.
IgnoreDestructiveWriteCountIncrementer
ignore_destructive_write_count_incrementer(document_);
// <spec step="B.8">Execute the script.</spec>
DCHECK(IsExecutingScript());
DoExecuteScript(pending_script, DocumentURLForScriptExecution(document_));
// <spec step="B.9">Decrement the parser's script nesting level by one. If
// the parser's script nesting level is zero (which it always should be at
// this point), then set the parser pause flag to false.</spec>
//
// This is implemented by ~ScriptNestingLevelIncrementer().
}
DCHECK(!IsExecutingScript());
}
// Should be correspond to
//
// <specdef
// href="https://html.spec.whatwg.org/multipage/scripting.html#execute-the-script-block">
//
// but currently does more than specced, because historically this and
// ExecutePendingParserBlockingScriptAndDispatchEvent() was the same method.
// TODO(hiroshige): Make this spec-conformant.
void HTMLParserScriptRunner::ExecutePendingDeferredScriptAndDispatchEvent(
PendingScript* pending_script) {
// Stop watching loads before executeScript to prevent recursion if the script
// reloads itself.
// TODO(kouhei): Consider merging this w/ pendingScript->dispose() after the
// if block.
pending_script->StopWatchingForLoad();
if (!IsExecutingScript()) {
// TODO(kouhei, hiroshige): Investigate why we need checkpoint here.
Microtask::PerformCheckpoint(V8PerIsolateData::MainThreadIsolate());
}
{
// The following code corresponds to:
//
// <spec href="https://html.spec.whatwg.org/#scriptEndTag"
// step="B.7">Increment the parser's script nesting level by one (it should
// be zero before this step, so this sets it to one).</spec>
//
// but this shouldn't be executed here according to the
// #execute-the-script-block spec.
HTMLParserReentryPermit::ScriptNestingLevelIncrementer
nesting_level_incrementer =
reentry_permit_->IncrementScriptNestingLevel();
// <spec step="3">... increment the ignore-destructive-writes counter of the
// script element's node document. ...</spec>
//
// TODO(hiroshige): This is duplicated (also done in ExecuteScriptBlock())).
IgnoreDestructiveWriteCountIncrementer
ignore_destructive_write_count_incrementer(document_);
DCHECK(IsExecutingScript());
DoExecuteScript(pending_script, DocumentURLForScriptExecution(document_));
}
DCHECK(!IsExecutingScript());
}
void HTMLParserScriptRunner::PendingScriptFinished(
PendingScript* pending_script) {
// 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() && pending_script->WasCanceled()) {
pending_script->Dispose();
DCHECK_EQ(pending_script, ParserBlockingScript());
parser_blocking_script_ = nullptr;
return;
}
host_->NotifyScriptLoaded(pending_script);
}
// <specdef href="https://html.spec.whatwg.org/#scriptEndTag">
//
// Script handling lives outside the tree builder to keep each class simple.
void HTMLParserScriptRunner::ProcessScriptElement(
Element* script_element,
const TextPosition& script_start_position) {
DCHECK(script_element);
// FIXME: If scripting is disabled, always just return.
bool had_preload_scanner = host_->HasPreloadScanner();
// <spec>An end tag whose tag name is "script" ...</spec>
//
// Try to execute the script given to us.
ProcessScriptElementInternal(script_element, script_start_position);
// <spec>... At this stage, if there is a pending parsing-blocking script,
// then:</spec>
if (HasParserBlockingScript()) {
// <spec step="A">If the script nesting level is not zero: ...</spec>
if (IsExecutingScript()) {
// <spec step="A">If the script nesting level is not zero:
//
// Set the parser pause flag to true, and abort the processing of any
// nested invocations of the tokenizer, yielding control back to the
// caller. (Tokenization will resume when the caller returns to the
// "outer" tree construction stage.)</spec>
//
// <spec>... set the parser pause flag to ...</spec>
// Unwind to the outermost HTMLParserScriptRunner::processScriptElement
// before continuing parsing.
return;
}
// - "Otherwise":
TraceParserBlockingScript(ParserBlockingScript(),
!document_->IsScriptExecutionReady());
parser_blocking_script_->MarkParserBlockingLoadStartTime();
// If preload scanner got created, it is missing the source after the
// current insertion point. Append it and scan.
if (!had_preload_scanner && host_->HasPreloadScanner())
host_->AppendCurrentInputStreamToPreloadScannerAndScan();
ExecuteParsingBlockingScripts();
}
}
bool HTMLParserScriptRunner::HasParserBlockingScript() const {
return ParserBlockingScript();
}
// <specdef href="https://html.spec.whatwg.org/#scriptEndTag">
//
// <spec>An end tag whose tag name is "script" ...</spec>
void HTMLParserScriptRunner::ExecuteParsingBlockingScripts() {
// <spec step="B.3">If the parser's Document has a style sheet that is
// blocking scripts or the script's "ready to be parser-executed" flag is not
// set: spin the event loop until the parser's Document has no style sheet
// that is blocking scripts and the script's "ready to be parser-executed"
// flag is set.</spec>
//
// These conditions correspond to isParserBlockingScriptReady() and
// if it is false, executeParsingBlockingScripts() will be called later
// when isParserBlockingScriptReady() becomes true:
// (1) from HTMLParserScriptRunner::executeScriptsWaitingForResources(), or
// (2) from HTMLParserScriptRunner::executeScriptsWaitingForLoad().
while (HasParserBlockingScript() && IsParserBlockingScriptReady()) {
DCHECK(document_);
DCHECK(!IsExecutingScript());
DCHECK(document_->IsScriptExecutionReady());
// <spec step="B.6">Let the insertion point be just before the next input
// character.</spec>
InsertionPointRecord insertion_point_record(host_->InputStream());
// 1., 7.--9.
ExecutePendingParserBlockingScriptAndDispatchEvent();
// <spec step="B.10">Let the insertion point be undefined again.</spec>
//
// Implemented as ~InsertionPointRecord().
// <spec step="B.11">If there is once again a pending parsing-blocking
// script, then repeat these steps from step 1.</spec>
}
}
void HTMLParserScriptRunner::ExecuteScriptsWaitingForLoad(
PendingScript* pending_script) {
TRACE_EVENT0("blink", "HTMLParserScriptRunner::executeScriptsWaitingForLoad");
DCHECK(!IsExecutingScript());
DCHECK(HasParserBlockingScript());
DCHECK_EQ(pending_script, ParserBlockingScript());
DCHECK(ParserBlockingScript()->IsReady());
ExecuteParsingBlockingScripts();
}
void HTMLParserScriptRunner::ExecuteScriptsWaitingForResources() {
TRACE_EVENT0("blink",
"HTMLParserScriptRunner::executeScriptsWaitingForResources");
DCHECK(document_);
DCHECK(!IsExecutingScript());
DCHECK(document_->IsScriptExecutionReady());
ExecuteParsingBlockingScripts();
}
// <specdef href="https://html.spec.whatwg.org/#stop-parsing">
//
// <spec step="3">If the list of scripts that will execute when the document has
// finished parsing is not empty, run these substeps:</spec>
bool HTMLParserScriptRunner::ExecuteScriptsWaitingForParsing() {
TRACE_EVENT0("blink",
"HTMLParserScriptRunner::executeScriptsWaitingForParsing");
while (!scripts_to_execute_after_parsing_.IsEmpty()) {
DCHECK(!IsExecutingScript());
DCHECK(!HasParserBlockingScript());
DCHECK(scripts_to_execute_after_parsing_.front()->IsExternalOrModule());
// <spec step="3.1">Spin the event loop until the first script in the list
// of scripts that will execute when the document has finished parsing has
// its "ready to be parser-executed" flag set and the parser's Document has
// no style sheet that is blocking scripts.</spec>
//
// TODO(hiroshige): Is the latter part checked anywhere?
if (!scripts_to_execute_after_parsing_.front()->IsReady()) {
scripts_to_execute_after_parsing_.front()->WatchForLoad(this);
TraceParserBlockingScript(scripts_to_execute_after_parsing_.front().Get(),
!document_->IsScriptExecutionReady());
scripts_to_execute_after_parsing_.front()
->MarkParserBlockingLoadStartTime();
return false;
}
// <spec step="3.3">Remove the first script element from the list of scripts
// that will execute when the document has finished parsing (i.e. shift out
// the first entry in the list).</spec>
PendingScript* first = scripts_to_execute_after_parsing_.TakeFirst();
// <spec step="3.2">Execute the first script in the list of scripts that
// will execute when the document has finished parsing.</spec>
ExecutePendingDeferredScriptAndDispatchEvent(first);
// FIXME: What is this m_document check for?
if (!document_)
return false;
// <spec step="3.4">If the list of scripts that will execute when the
// document has finished parsing is still not empty, repeat these substeps
// again from substep 1.</spec>
}
return true;
}
void HTMLParserScriptRunner::RequestParsingBlockingScript(
ScriptLoader* script_loader) {
// <spec href="https://html.spec.whatwg.org/#prepare-a-script" step="26.B">...
// The element is the pending parsing-blocking script of the Document of the
// parser that created the element. (There can only be one such script per
// Document at a time.) ...</spec>
CHECK(!ParserBlockingScript());
parser_blocking_script_ =
script_loader->TakePendingScript(ScriptSchedulingType::kParserBlocking);
if (!ParserBlockingScript())
return;
DCHECK(ParserBlockingScript()->IsExternal());
// 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 (!ParserBlockingScript()->IsReady()) {
parser_blocking_script_->StartStreamingIfPossible();
parser_blocking_script_->WatchForLoad(this);
}
}
void HTMLParserScriptRunner::RequestDeferredScript(
ScriptLoader* script_loader) {
PendingScript* pending_script =
script_loader->TakePendingScript(ScriptSchedulingType::kDefer);
if (!pending_script)
return;
if (!pending_script->IsReady()) {
pending_script->StartStreamingIfPossible();
}
DCHECK(pending_script->IsExternalOrModule());
// <spec href="https://html.spec.whatwg.org/#prepare-a-script" step="26.A">...
// Add the element to the end of the list of scripts that will execute when
// the document has finished parsing associated with the Document of the
// parser that created the element. ...</spec>
scripts_to_execute_after_parsing_.push_back(pending_script);
}
// The initial steps for 'An end tag whose tag name is "script"'
// <specdef href="https://html.spec.whatwg.org/#scriptEndTag">
// <specdef label="prepare-a-script"
// href="https://html.spec.whatwg.org/multipage/scripting.html#prepare-a-script">
void HTMLParserScriptRunner::ProcessScriptElementInternal(
Element* script,
const TextPosition& script_start_position) {
DCHECK(document_);
DCHECK(!HasParserBlockingScript());
{
ScriptLoader* script_loader = ScriptLoaderFromElement(script);
// FIXME: Align trace event name and function name.
TRACE_EVENT1("blink", "HTMLParserScriptRunner::execute", "data",
GetTraceArgsForScriptElement(*document_, script_start_position,
NullURL()));
DCHECK(script_loader->IsParserInserted());
if (!IsExecutingScript())
Microtask::PerformCheckpoint(V8PerIsolateData::MainThreadIsolate());
// <spec>... Let the old insertion point have the same value as the current
// insertion point. Let the insertion point be just before the next input
// character. ...</spec>
InsertionPointRecord insertion_point_record(host_->InputStream());
// <spec>... Increment the parser's script nesting level by one. ...</spec>
HTMLParserReentryPermit::ScriptNestingLevelIncrementer
nesting_level_incrementer =
reentry_permit_->IncrementScriptNestingLevel();
// <spec>... Prepare the script. This might cause some script to execute,
// which might cause new characters to be inserted into the tokenizer, and
// might cause the tokenizer to output more tokens, resulting in a reentrant
// invocation of the parser. ...</spec>
script_loader->PrepareScript(script_start_position);
if (!script_loader->WillBeParserExecuted())
return;
if (script_loader->WillExecuteWhenDocumentFinishedParsing()) {
RequestDeferredScript(script_loader);
} else if (script_loader->ReadyToBeParserExecuted()) {
// <spec label="prepare-a-script" step="26.E">... it's an HTML parser
// whose script nesting level is not greater than one, ...</spec>
if (reentry_permit_->ScriptNestingLevel() == 1u) {
// <spec label="prepare-a-script" step="26.E">... The element is the
// pending parsing-blocking script of the Document of the parser that
// created the element. (There can only be one such script per Document
// at a time.) ...</spec>
CHECK(!parser_blocking_script_);
parser_blocking_script_ = script_loader->TakePendingScript(
ScriptSchedulingType::kParserBlockingInline);
} else {
// <spec label="prepare-a-script" step="26.F">Otherwise
//
// Immediately execute the script block, even if other scripts are
// already executing.</spec>
//
// TODO(hiroshige): Merge the block into ScriptLoader::prepareScript().
DCHECK_GT(reentry_permit_->ScriptNestingLevel(), 1u);
if (parser_blocking_script_)
parser_blocking_script_->Dispose();
parser_blocking_script_ = nullptr;
DoExecuteScript(
script_loader->TakePendingScript(ScriptSchedulingType::kImmediate),
DocumentURLForScriptExecution(document_));
}
} else {
// [PS] Step 25.B.
RequestParsingBlockingScript(script_loader);
}
// <spec>... Decrement the parser's script nesting level by one. If the
// parser's script nesting level is zero, then set the parser pause flag to
// false. ...</spec>
//
// Implemented by ~ScriptNestingLevelIncrementer().
// <spec>... Let the insertion point have the value of the old insertion
// point. ...</spec>
//
// Implemented by ~InsertionPointRecord().
}
}
void HTMLParserScriptRunner::Trace(blink::Visitor* visitor) {
visitor->Trace(document_);
visitor->Trace(host_);
visitor->Trace(parser_blocking_script_);
visitor->Trace(scripts_to_execute_after_parsing_);
PendingScriptClient::Trace(visitor);
}
} // namespace blink