blob: 9cf829f3b1644fafa12cb1d8c7dc1d971ee23f36 [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "third_party/blink/renderer/bindings/core/v8/script_streamer.h"
#include <memory>
#include <utility>
#include "base/single_thread_task_runner.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/mojom/fetch/fetch_api_request.mojom-blink.h"
#include "third_party/blink/public/platform/platform.h"
#include "third_party/blink/public/platform/scheduler/test/renderer_scheduler_test_support.h"
#include "third_party/blink/public/platform/web_back_forward_cache_loader_helper.h"
#include "third_party/blink/public/platform/web_url_loader.h"
#include "third_party/blink/public/platform/web_url_loader_mock_factory.h"
#include "third_party/blink/public/platform/web_url_request_extra_data.h"
#include "third_party/blink/renderer/bindings/core/v8/referrer_script_info.h"
#include "third_party/blink/renderer/bindings/core/v8/script_source_code.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_core.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_testing.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_code_cache.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_script_runner.h"
#include "third_party/blink/renderer/core/frame/settings.h"
#include "third_party/blink/renderer/core/script/classic_pending_script.h"
#include "third_party/blink/renderer/core/script/classic_script.h"
#include "third_party/blink/renderer/core/script/mock_script_element_base.h"
#include "third_party/blink/renderer/core/testing/dummy_page_holder.h"
#include "third_party/blink/renderer/platform/exported/wrapped_resource_response.h"
#include "third_party/blink/renderer/platform/heap/handle.h"
#include "third_party/blink/renderer/platform/loader/fetch/cross_origin_attribute_value.h"
#include "third_party/blink/renderer/platform/loader/fetch/resource_fetcher.h"
#include "third_party/blink/renderer/platform/loader/fetch/resource_loader.h"
#include "third_party/blink/renderer/platform/loader/fetch/response_body_loader.h"
#include "third_party/blink/renderer/platform/loader/fetch/script_fetch_options.h"
#include "third_party/blink/renderer/platform/loader/testing/mock_fetch_context.h"
#include "third_party/blink/renderer/platform/loader/testing/test_resource_fetcher_properties.h"
#include "third_party/blink/renderer/platform/scheduler/public/thread_scheduler.h"
#include "third_party/blink/renderer/platform/testing/mock_context_lifecycle_notifier.h"
#include "third_party/blink/renderer/platform/testing/testing_platform_support_with_mock_scheduler.h"
#include "third_party/blink/renderer/platform/testing/unit_test_helpers.h"
#include "third_party/blink/renderer/platform/wtf/text/text_encoding.h"
#include "v8/include/v8.h"
namespace blink {
namespace {
class TestResourceClient final : public GarbageCollected<TestResourceClient>,
public ResourceClient {
public:
TestResourceClient() : finished_(false) {}
bool Finished() const { return finished_; }
void DataReceived(Resource*,
const char* /* data */,
size_t /* length */) override {}
void NotifyFinished(Resource*) override { finished_ = true; }
// Name for debugging, e.g. shown in memory-infra.
String DebugName() const override { return "TestResourceClient"; }
private:
bool finished_;
};
// TODO(leszeks): This class has a similar class in resource_loader_test.cc,
// the two should probably share the same class.
class NoopLoaderFactory final : public ResourceFetcher::LoaderFactory {
std::unique_ptr<WebURLLoader> CreateURLLoader(
const ResourceRequest& request,
const ResourceLoaderOptions& options,
scoped_refptr<base::SingleThreadTaskRunner>,
scoped_refptr<base::SingleThreadTaskRunner>,
WebBackForwardCacheLoaderHelper) override {
return std::make_unique<NoopWebURLLoader>();
}
std::unique_ptr<WebCodeCacheLoader> CreateCodeCacheLoader() override {
return Platform::Current()->CreateCodeCacheLoader();
}
class NoopWebURLLoader final : public WebURLLoader {
public:
~NoopWebURLLoader() override = default;
void LoadSynchronously(
std::unique_ptr<network::ResourceRequest> request,
scoped_refptr<WebURLRequestExtraData> url_request_extra_data,
int requestor_id,
bool pass_response_pipe_to_client,
bool no_mime_sniffing,
base::TimeDelta timeout_interval,
WebURLLoaderClient*,
WebURLResponse&,
base::Optional<WebURLError>&,
WebData&,
int64_t& encoded_data_length,
int64_t& encoded_body_length,
WebBlobInfo& downloaded_blob,
std::unique_ptr<blink::ResourceLoadInfoNotifierWrapper>
resource_load_info_notifier_wrapper) override {
NOTREACHED();
}
void LoadAsynchronously(
std::unique_ptr<network::ResourceRequest> request,
scoped_refptr<WebURLRequestExtraData> url_request_extra_data,
int requestor_id,
bool no_mime_sniffing,
std::unique_ptr<blink::ResourceLoadInfoNotifierWrapper>
resource_load_info_notifier_wrapper,
WebURLLoaderClient*) override {}
void SetDefersLoading(WebURLLoader::DeferType) override {}
void DidChangePriority(WebURLRequest::Priority, int) override {
NOTREACHED();
}
scoped_refptr<base::SingleThreadTaskRunner> GetTaskRunnerForBodyLoader()
override {
return base::MakeRefCounted<scheduler::FakeTaskRunner>();
}
};
};
class ScriptStreamingTest : public testing::Test {
public:
ScriptStreamingTest()
: url_("http://www.streaming-test.com/"),
freezable_task_runner_(platform_->test_task_runner()),
unfreezable_task_runner_(platform_->test_task_runner()) {
auto* properties = MakeGarbageCollected<TestResourceFetcherProperties>();
FetchContext* context = MakeGarbageCollected<MockFetchContext>();
auto* fetcher = MakeGarbageCollected<ResourceFetcher>(ResourceFetcherInit(
properties->MakeDetachable(), context, freezable_task_runner_,
unfreezable_task_runner_, MakeGarbageCollected<NoopLoaderFactory>(),
MakeGarbageCollected<MockContextLifecycleNotifier>(),
nullptr /* back_forward_cache_loader_helper */));
EXPECT_EQ(mojo::CreateDataPipe(nullptr, producer_handle_, consumer_handle_),
MOJO_RESULT_OK);
ResourceRequest request(url_);
request.SetRequestContext(mojom::blink::RequestContextType::SCRIPT);
resource_client_ = MakeGarbageCollected<TestResourceClient>();
FetchParameters params = FetchParameters::CreateForTest(std::move(request));
resource_ = ScriptResource::Fetch(params, fetcher, resource_client_,
ScriptResource::kAllowStreaming);
resource_->AddClient(resource_client_, freezable_task_runner_.get());
ScriptStreamer::SetSmallScriptThresholdForTesting(0);
ResourceResponse response(url_);
response.SetHttpStatusCode(200);
resource_->SetResponse(response);
resource_->Loader()->DidReceiveResponse(WrappedResourceResponse(response));
resource_->Loader()->DidStartLoadingResponseBody(
std::move(consumer_handle_));
}
ScriptSourceCode GetScriptSourceCode() const {
ScriptStreamer* streamer = resource_->TakeStreamer();
if (streamer) {
if (streamer->IsStreamingSuppressed()) {
return ScriptSourceCode(nullptr, resource_,
streamer->StreamingSuppressedReason());
}
return ScriptSourceCode(streamer, resource_,
ScriptStreamer::NotStreamingReason::kInvalid);
}
return ScriptSourceCode(nullptr, resource_, resource_->NoStreamerReason());
}
Settings* GetSettings() const {
return &dummy_page_holder_->GetPage().GetSettings();
}
protected:
void AppendData(const char* data) {
uint32_t data_len = strlen(data);
MojoResult result = producer_handle_->WriteData(
data, &data_len, MOJO_WRITE_DATA_FLAG_ALL_OR_NONE);
EXPECT_EQ(result, MOJO_RESULT_OK);
// Yield control to the background thread, so that V8 gets a chance to
// process the data before the main thread adds more. Note that we
// cannot fully control in what kind of chunks the data is passed to V8
// (if V8 is not requesting more data between two appendData calls, it
// will get both chunks together).
test::YieldCurrentThread();
}
void AppendPadding() {
for (int i = 0; i < 10; ++i) {
AppendData(
" /* this is padding to make the script long enough, so "
"that V8's buffer gets filled and it starts processing "
"the data */ ");
}
}
void Finish() {
resource_->Loader()->DidFinishLoading(base::TimeTicks(), 0, 0, 0, false);
producer_handle_.reset();
resource_->SetStatus(ResourceStatus::kCached);
}
void ProcessTasksUntilStreamingComplete() { platform_->RunUntilIdle(); }
ScopedTestingPlatformSupport<TestingPlatformSupportWithMockScheduler>
platform_;
KURL url_;
scoped_refptr<base::SingleThreadTaskRunner> freezable_task_runner_;
scoped_refptr<base::SingleThreadTaskRunner> unfreezable_task_runner_;
Persistent<TestResourceClient> resource_client_;
Persistent<ScriptResource> resource_;
mojo::ScopedDataPipeProducerHandle producer_handle_;
mojo::ScopedDataPipeConsumerHandle consumer_handle_;
std::unique_ptr<DummyPageHolder> dummy_page_holder_;
};
// TODO(crbug.com/939054): Tests are disabled due to flakiness caused by being
// currently unable to block and wait for the script streaming thread.
TEST_F(ScriptStreamingTest, DISABLED_CompilingStreamedScript) {
// Test that we can successfully compile a streamed script.
V8TestingScope scope;
AppendData("function foo() {");
AppendPadding();
AppendData("return 5; }");
AppendPadding();
AppendData("foo();");
EXPECT_FALSE(resource_client_->Finished());
Finish();
// Process tasks on the main thread until the streaming background thread
// has completed its tasks.
ProcessTasksUntilStreamingComplete();
EXPECT_TRUE(resource_client_->Finished());
ScriptSourceCode source_code = GetScriptSourceCode();
EXPECT_TRUE(source_code.Streamer());
v8::TryCatch try_catch(scope.GetIsolate());
v8::Local<v8::Script> script;
v8::ScriptCompiler::CompileOptions compile_options;
V8CodeCache::ProduceCacheOptions produce_cache_options;
v8::ScriptCompiler::NoCacheReason no_cache_reason;
std::tie(compile_options, produce_cache_options, no_cache_reason) =
V8CodeCache::GetCompileOptions(mojom::blink::V8CacheOptions::kDefault,
source_code);
EXPECT_TRUE(V8ScriptRunner::CompileScript(
scope.GetScriptState(), source_code,
SanitizeScriptErrors::kDoNotSanitize, compile_options,
no_cache_reason, ReferrerScriptInfo())
.ToLocal(&script));
EXPECT_FALSE(try_catch.HasCaught());
}
// TODO(crbug.com/939054): Tests are disabled due to flakiness caused by being
// currently unable to block and wait for the script streaming thread.
TEST_F(ScriptStreamingTest, DISABLED_CompilingStreamedScriptWithParseError) {
// Test that scripts with parse errors are handled properly. In those cases,
// V8 stops reading the network stream: make sure we handle it gracefully.
V8TestingScope scope;
AppendData("function foo() {");
AppendData("this is the part which will be a parse error");
// V8 won't realize the parse error until it actually starts parsing the
// script, and this happens only when its buffer is filled.
AppendPadding();
EXPECT_FALSE(resource_client_->Finished());
Finish();
// Process tasks on the main thread until the streaming background thread
// has completed its tasks.
ProcessTasksUntilStreamingComplete();
EXPECT_TRUE(resource_client_->Finished());
ScriptSourceCode source_code = GetScriptSourceCode();
EXPECT_TRUE(source_code.Streamer());
v8::TryCatch try_catch(scope.GetIsolate());
v8::Local<v8::Script> script;
v8::ScriptCompiler::CompileOptions compile_options;
V8CodeCache::ProduceCacheOptions produce_cache_options;
v8::ScriptCompiler::NoCacheReason no_cache_reason;
std::tie(compile_options, produce_cache_options, no_cache_reason) =
V8CodeCache::GetCompileOptions(mojom::blink::V8CacheOptions::kDefault,
source_code);
EXPECT_FALSE(V8ScriptRunner::CompileScript(
scope.GetScriptState(), source_code,
SanitizeScriptErrors::kDoNotSanitize, compile_options,
no_cache_reason, ReferrerScriptInfo())
.ToLocal(&script));
EXPECT_TRUE(try_catch.HasCaught());
}
// TODO(crbug.com/939054): Tests are disabled due to flakiness caused by being
// currently unable to block and wait for the script streaming thread.
TEST_F(ScriptStreamingTest, DISABLED_CancellingStreaming) {
// Test that the upper layers (PendingScript and up) can be ramped down
// while streaming is ongoing, and ScriptStreamer handles it gracefully.
V8TestingScope scope;
AppendData("function foo() {");
// In general, we cannot control what the background thread is doing
// (whether it's parsing or waiting for more data). In this test, we have
// given it so little data that it's surely waiting for more.
// Simulate cancelling the network load (e.g., because the user navigated
// away).
EXPECT_FALSE(resource_client_->Finished());
resource_ = nullptr;
// The V8 side will complete too. This should not crash. We don't receive
// any results from the streaming and the client doesn't get notified.
ProcessTasksUntilStreamingComplete();
EXPECT_FALSE(resource_client_->Finished());
}
// TODO(crbug.com/939054): Tests are disabled due to flakiness caused by being
// currently unable to block and wait for the script streaming thread.
TEST_F(ScriptStreamingTest, DISABLED_DataAfterDisposingPendingScript) {
// Test that the upper layers (PendingScript and up) can be ramped down
// before streaming is started, and ScriptStreamer handles it gracefully.
V8TestingScope scope;
// In general, we cannot control what the background thread is doing
// (whether it's parsing or waiting for more data). In this test, we have
// given it so little data that it's surely waiting for more.
EXPECT_FALSE(resource_client_->Finished());
// Keep the resource alive
Persistent<ScriptResource> resource = resource_;
// Simulate cancelling the network load (e.g., because the user navigated
// away).
resource_ = nullptr;
// Make sure the streaming starts.
AppendData("function foo() {");
AppendPadding();
resource.Clear();
// The V8 side will complete too. This should not crash. We don't receive
// any results from the streaming and the client doesn't get notified.
ProcessTasksUntilStreamingComplete();
EXPECT_FALSE(resource_client_->Finished());
}
// TODO(crbug.com/939054): Tests are disabled due to flakiness caused by being
// currently unable to block and wait for the script streaming thread.
TEST_F(ScriptStreamingTest, DISABLED_SuppressingStreaming) {
// If we notice before streaming that there is a code cache, streaming
// is suppressed (V8 doesn't parse while the script is loading), and the
// upper layer (ScriptResourceClient) should get a notification when the
// script is loaded.
V8TestingScope scope;
SingleCachedMetadataHandler* cache_handler = resource_->CacheHandler();
EXPECT_TRUE(cache_handler);
cache_handler->DisableSendToPlatformForTesting();
cache_handler->SetCachedMetadata(V8CodeCache::TagForCodeCache(cache_handler),
reinterpret_cast<const uint8_t*>("X"), 1);
AppendData("function foo() {");
AppendPadding();
Finish();
ProcessTasksUntilStreamingComplete();
EXPECT_TRUE(resource_client_->Finished());
ScriptSourceCode source_code = GetScriptSourceCode();
// ScriptSourceCode doesn't refer to the streamer, since we have suppressed
// the streaming and resumed the non-streaming code path for script
// compilation.
EXPECT_FALSE(source_code.Streamer());
}
// TODO(crbug.com/939054): Tests are disabled due to flakiness caused by being
// currently unable to block and wait for the script streaming thread.
TEST_F(ScriptStreamingTest, DISABLED_EmptyScripts) {
// Empty scripts should also be streamed properly, that is, the upper layer
// (ScriptResourceClient) should be notified when an empty script has been
// loaded.
V8TestingScope scope;
// Finish the script without sending any data.
Finish();
ProcessTasksUntilStreamingComplete();
EXPECT_TRUE(resource_client_->Finished());
ScriptSourceCode source_code = GetScriptSourceCode();
EXPECT_FALSE(source_code.Streamer());
}
// TODO(crbug.com/939054): Tests are disabled due to flakiness caused by being
// currently unable to block and wait for the script streaming thread.
TEST_F(ScriptStreamingTest, DISABLED_SmallScripts) {
// Small scripts shouldn't be streamed.
V8TestingScope scope;
ScriptStreamer::SetSmallScriptThresholdForTesting(100);
AppendData("function foo() { }");
Finish();
ProcessTasksUntilStreamingComplete();
EXPECT_TRUE(resource_client_->Finished());
ScriptSourceCode source_code = GetScriptSourceCode();
EXPECT_FALSE(source_code.Streamer());
}
// TODO(crbug.com/939054): Tests are disabled due to flakiness caused by being
// currently unable to block and wait for the script streaming thread.
TEST_F(ScriptStreamingTest, DISABLED_ScriptsWithSmallFirstChunk) {
// If a script is long enough, if should be streamed, even if the first data
// chunk is small.
V8TestingScope scope;
ScriptStreamer::SetSmallScriptThresholdForTesting(100);
// This is the first data chunk which is small.
AppendData("function foo() { }");
AppendPadding();
AppendPadding();
AppendPadding();
Finish();
ProcessTasksUntilStreamingComplete();
EXPECT_TRUE(resource_client_->Finished());
ScriptSourceCode source_code = GetScriptSourceCode();
EXPECT_TRUE(source_code.Streamer());
v8::TryCatch try_catch(scope.GetIsolate());
v8::Local<v8::Script> script;
v8::ScriptCompiler::CompileOptions compile_options;
V8CodeCache::ProduceCacheOptions produce_cache_options;
v8::ScriptCompiler::NoCacheReason no_cache_reason;
std::tie(compile_options, produce_cache_options, no_cache_reason) =
V8CodeCache::GetCompileOptions(mojom::blink::V8CacheOptions::kDefault,
source_code);
EXPECT_TRUE(V8ScriptRunner::CompileScript(
scope.GetScriptState(), source_code,
SanitizeScriptErrors::kDoNotSanitize, compile_options,
no_cache_reason, ReferrerScriptInfo())
.ToLocal(&script));
EXPECT_FALSE(try_catch.HasCaught());
}
// TODO(crbug.com/939054): Tests are disabled due to flakiness caused by being
// currently unable to block and wait for the script streaming thread.
TEST_F(ScriptStreamingTest, DISABLED_EncodingChanges) {
// It's possible that the encoding of the Resource changes after we start
// loading it.
V8TestingScope scope;
resource_->SetEncodingForTest("windows-1252");
resource_->SetEncodingForTest("UTF-8");
// \xec\x92\x81 are the raw bytes for \uc481.
AppendData(
"function foo() { var foob\xec\x92\x81r = 13; return foob\xec\x92\x81r; "
"} foo();");
Finish();
ProcessTasksUntilStreamingComplete();
EXPECT_TRUE(resource_client_->Finished());
ScriptSourceCode source_code = GetScriptSourceCode();
EXPECT_TRUE(source_code.Streamer());
v8::TryCatch try_catch(scope.GetIsolate());
v8::Local<v8::Script> script;
v8::ScriptCompiler::CompileOptions compile_options;
V8CodeCache::ProduceCacheOptions produce_cache_options;
v8::ScriptCompiler::NoCacheReason no_cache_reason;
std::tie(compile_options, produce_cache_options, no_cache_reason) =
V8CodeCache::GetCompileOptions(mojom::blink::V8CacheOptions::kDefault,
source_code);
EXPECT_TRUE(V8ScriptRunner::CompileScript(
scope.GetScriptState(), source_code,
SanitizeScriptErrors::kDoNotSanitize, compile_options,
no_cache_reason, ReferrerScriptInfo())
.ToLocal(&script));
EXPECT_FALSE(try_catch.HasCaught());
}
// TODO(crbug.com/939054): Tests are disabled due to flakiness caused by being
// currently unable to block and wait for the script streaming thread.
TEST_F(ScriptStreamingTest, DISABLED_EncodingFromBOM) {
// Byte order marks should be removed before giving the data to V8. They
// will also affect encoding detection.
V8TestingScope scope;
// This encoding is wrong on purpose.
resource_->SetEncodingForTest("windows-1252");
// \xef\xbb\xbf is the UTF-8 byte order mark. \xec\x92\x81 are the raw bytes
// for \uc481.
AppendData(
"\xef\xbb\xbf function foo() { var foob\xec\x92\x81r = 13; return "
"foob\xec\x92\x81r; } foo();");
Finish();
ProcessTasksUntilStreamingComplete();
EXPECT_TRUE(resource_client_->Finished());
ScriptSourceCode source_code = GetScriptSourceCode();
EXPECT_TRUE(source_code.Streamer());
v8::TryCatch try_catch(scope.GetIsolate());
v8::Local<v8::Script> script;
v8::ScriptCompiler::CompileOptions compile_options;
V8CodeCache::ProduceCacheOptions produce_cache_options;
v8::ScriptCompiler::NoCacheReason no_cache_reason;
std::tie(compile_options, produce_cache_options, no_cache_reason) =
V8CodeCache::GetCompileOptions(mojom::blink::V8CacheOptions::kDefault,
source_code);
EXPECT_TRUE(V8ScriptRunner::CompileScript(
scope.GetScriptState(), source_code,
SanitizeScriptErrors::kDoNotSanitize, compile_options,
no_cache_reason, ReferrerScriptInfo())
.ToLocal(&script));
EXPECT_FALSE(try_catch.HasCaught());
}
// TODO(crbug.com/939054): Tests are disabled due to flakiness caused by being
// currently unable to block and wait for the script streaming thread.
// A test for crbug.com/711703. Should not crash.
TEST_F(ScriptStreamingTest, DISABLED_GarbageCollectDuringStreaming) {
V8TestingScope scope;
EXPECT_FALSE(resource_client_->Finished());
resource_ = nullptr;
ThreadState::Current()->CollectAllGarbageForTesting(
BlinkGC::kNoHeapPointersOnStack);
}
// TODO(crbug.com/939054): Tests are disabled due to flakiness caused by being
// currently unable to block and wait for the script streaming thread.
TEST_F(ScriptStreamingTest, DISABLED_ResourceSetRevalidatingRequest) {
V8TestingScope scope;
// Kick the streaming off.
AppendData("function foo() {");
AppendPadding();
AppendData("}");
Finish();
ProcessTasksUntilStreamingComplete();
// Should be done streaming by now.
EXPECT_TRUE(resource_->HasStreamer());
EXPECT_FALSE(resource_->HasRunningStreamer());
ResourceRequest request(resource_->Url());
resource_->SetRevalidatingRequest(request);
// Now there shouldn't be a streamer at all, and the reason should be
// "kRevalidate".
EXPECT_FALSE(resource_->HasStreamer());
EXPECT_EQ(resource_->NoStreamerReason(),
ScriptStreamer::NotStreamingReason::kRevalidate);
}
} // namespace
} // namespace blink