blob: 3f7f9157f4c918b850c506ad6a2201d5b957022f [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 "config.h"
#include "bindings/core/v8/ScriptStreamer.h"
#include "bindings/core/v8/ScriptSourceCode.h"
#include "bindings/core/v8/ScriptStreamerThread.h"
#include "bindings/core/v8/ScriptStreamingMode.h"
#include "bindings/core/v8/V8Binding.h"
#include "bindings/core/v8/V8ScriptRunner.h"
#include "core/dom/PendingScript.h"
#include "core/frame/Settings.h"
#include "platform/Task.h"
#include "platform/heap/Handle.h"
#include "public/platform/Platform.h"
#include <gtest/gtest.h>
#include <v8.h>
namespace blink {
namespace {
// For the benefit of Oilpan, put the part object PendingScript inside
// a wrapper that's on the Oilpan heap and hold a reference to that wrapper
// from ScriptStreamingTest.
class PendingScriptWrapper : public NoBaseWillBeGarbageCollectedFinalized<PendingScriptWrapper> {
public:
static PassOwnPtrWillBeRawPtr<PendingScriptWrapper> create()
{
return adoptPtrWillBeNoop(new PendingScriptWrapper());
}
static PassOwnPtrWillBeRawPtr<PendingScriptWrapper> create(Element* element, ScriptResource* resource)
{
return adoptPtrWillBeNoop(new PendingScriptWrapper(element, resource));
}
PendingScript& get() { return m_pendingScript; }
void trace(Visitor* visitor)
{
visitor->trace(m_pendingScript);
}
private:
PendingScriptWrapper()
{
}
PendingScriptWrapper(Element* element, ScriptResource* resource)
: m_pendingScript(PendingScript(element, resource))
{
}
PendingScript m_pendingScript;
};
// The bool param for ScriptStreamingTest controls whether to make the main
// thread block and wait for parsing.
class ScriptStreamingTest : public testing::TestWithParam<bool> {
public:
ScriptStreamingTest()
: m_scope(v8::Isolate::GetCurrent())
, m_settings(Settings::create())
, m_resourceRequest("http://www.streaming-test.com/")
, m_resource(new ScriptResource(m_resourceRequest, "text/utf-8"))
, m_pendingScript(PendingScriptWrapper::create(0, m_resource)) // Takes ownership of m_resource.
{
m_settings->setV8ScriptStreamingEnabled(true);
if (GetParam())
m_settings->setV8ScriptStreamingMode(ScriptStreamingModeAllPlusBlockParsingBlocking);
m_resource->setLoading(true);
ScriptStreamer::setSmallScriptThresholdForTesting(0);
}
ScriptState* scriptState() const { return m_scope.scriptState(); }
v8::Isolate* isolate() const { return m_scope.isolate(); }
PendingScript& pendingScript() const { return m_pendingScript->get(); }
protected:
void appendData(const char* data)
{
m_resource->appendData(data, strlen(data));
// Yield control to the background thread, so that V8 gets a change 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 the V8 is not requesting more data between two appendData calls,
// V8 will get both chunks together).
Platform::current()->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()
{
m_resource->finish();
m_resource->setLoading(false);
}
void processTasksUntilStreamingComplete()
{
WebThread* currentThread = blink::Platform::current()->currentThread();
while (ScriptStreamerThread::shared()->isRunningTask()) {
currentThread->postTask(new Task(WTF::bind(&WebThread::exitRunLoop, currentThread)));
currentThread->enterRunLoop();
}
// Once more, because the "streaming complete" notification might only
// now be in the task queue.
currentThread->postTask(new Task(WTF::bind(&WebThread::exitRunLoop, currentThread)));
currentThread->enterRunLoop();
}
V8TestingScope m_scope;
OwnPtr<Settings> m_settings;
// The Resource and PendingScript where we stream from. These don't really
// fetch any data outside the test; the test controls the data by calling
// ScriptResource::appendData.
ResourceRequest m_resourceRequest;
ScriptResource* m_resource;
OwnPtrWillBePersistent<PendingScriptWrapper> m_pendingScript;
};
class TestScriptResourceClient : public ScriptResourceClient {
public:
TestScriptResourceClient()
: m_finished(false) { }
virtual void notifyFinished(Resource*) override { m_finished = true; }
bool finished() const { return m_finished; }
private:
bool m_finished;
};
TEST_P(ScriptStreamingTest, CompilingStreamedScript)
{
// Test that we can successfully compile a streamed script.
ScriptStreamer::startStreaming(pendingScript(), m_settings.get(), m_scope.scriptState(), PendingScript::ParsingBlocking);
TestScriptResourceClient client;
pendingScript().watchForLoad(&client);
appendData("function foo() {");
appendPadding();
appendData("return 5; }");
appendPadding();
appendData("foo();");
EXPECT_FALSE(client.finished());
finish();
// Process tasks on the main thread until the streaming background thread
// has completed its tasks.
processTasksUntilStreamingComplete();
EXPECT_TRUE(client.finished());
bool errorOccurred = false;
ScriptSourceCode sourceCode = pendingScript().getSource(KURL(), errorOccurred);
EXPECT_FALSE(errorOccurred);
EXPECT_TRUE(sourceCode.streamer());
v8::TryCatch tryCatch;
v8::Handle<v8::Script> script = V8ScriptRunner::compileScript(sourceCode, isolate());
EXPECT_FALSE(script.IsEmpty());
EXPECT_FALSE(tryCatch.HasCaught());
}
TEST_P(ScriptStreamingTest, CompilingStreamedScriptWithParseError)
{
// Test that scripts with parse errors are handled properly. In those cases,
// the V8 side typically finished before loading finishes: make sure we
// handle it gracefully.
ScriptStreamer::startStreaming(pendingScript(), m_settings.get(), m_scope.scriptState(), PendingScript::ParsingBlocking);
TestScriptResourceClient client;
pendingScript().watchForLoad(&client);
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(client.finished());
// Force the V8 side to finish before the loading.
processTasksUntilStreamingComplete();
EXPECT_FALSE(client.finished());
finish();
EXPECT_TRUE(client.finished());
bool errorOccurred = false;
ScriptSourceCode sourceCode = pendingScript().getSource(KURL(), errorOccurred);
EXPECT_FALSE(errorOccurred);
EXPECT_TRUE(sourceCode.streamer());
v8::TryCatch tryCatch;
v8::Handle<v8::Script> script = V8ScriptRunner::compileScript(sourceCode, isolate());
EXPECT_TRUE(script.IsEmpty());
EXPECT_TRUE(tryCatch.HasCaught());
}
TEST_P(ScriptStreamingTest, CancellingStreaming)
{
// Test that the upper layers (PendingScript and up) can be ramped down
// while streaming is ongoing, and ScriptStreamer handles it gracefully.
ScriptStreamer::startStreaming(pendingScript(), m_settings.get(), m_scope.scriptState(), PendingScript::ParsingBlocking);
TestScriptResourceClient client;
pendingScript().watchForLoad(&client);
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(client.finished());
pendingScript().stopWatchingForLoad(&client);
pendingScript().releaseElementAndClear();
m_pendingScript = PendingScriptWrapper::create(); // This will destroy m_resource.
m_resource = 0;
// 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(client.finished());
}
TEST_P(ScriptStreamingTest, SuppressingStreaming)
{
// If we notice during 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.
ScriptStreamer::startStreaming(pendingScript(), m_settings.get(), m_scope.scriptState(), PendingScript::ParsingBlocking);
TestScriptResourceClient client;
pendingScript().watchForLoad(&client);
appendData("function foo() {");
appendPadding();
m_resource->setCachedMetadata(V8ScriptRunner::tagForCodeCache(), "X", 1, Resource::CacheLocally);
appendPadding();
finish();
processTasksUntilStreamingComplete();
EXPECT_TRUE(client.finished());
bool errorOccurred = false;
ScriptSourceCode sourceCode = pendingScript().getSource(KURL(), errorOccurred);
EXPECT_FALSE(errorOccurred);
// 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(sourceCode.streamer());
}
TEST_P(ScriptStreamingTest, EmptyScripts)
{
// Empty scripts should also be streamed properly, that is, the upper layer
// (ScriptResourceClient) should be notified when an empty script has been
// loaded.
ScriptStreamer::startStreaming(pendingScript(), m_settings.get(), m_scope.scriptState(), PendingScript::ParsingBlocking);
TestScriptResourceClient client;
pendingScript().watchForLoad(&client);
// Finish the script without sending any data.
finish();
// The finished notification should arrive immediately and not be cycled
// through a background thread.
EXPECT_TRUE(client.finished());
bool errorOccurred = false;
ScriptSourceCode sourceCode = pendingScript().getSource(KURL(), errorOccurred);
EXPECT_FALSE(errorOccurred);
EXPECT_FALSE(sourceCode.streamer());
}
TEST_P(ScriptStreamingTest, SmallScripts)
{
// Small scripts shouldn't be streamed.
ScriptStreamer::setSmallScriptThresholdForTesting(100);
ScriptStreamer::startStreaming(pendingScript(), m_settings.get(), m_scope.scriptState(), PendingScript::ParsingBlocking);
TestScriptResourceClient client;
pendingScript().watchForLoad(&client);
appendData("function foo() { }");
finish();
// The finished notification should arrive immediately and not be cycled
// through a background thread.
EXPECT_TRUE(client.finished());
bool errorOccurred = false;
ScriptSourceCode sourceCode = pendingScript().getSource(KURL(), errorOccurred);
EXPECT_FALSE(errorOccurred);
EXPECT_FALSE(sourceCode.streamer());
}
TEST_P(ScriptStreamingTest, ScriptsWithSmallFirstChunk)
{
// If a script is long enough, if should be streamed, even if the first data
// chunk is small.
ScriptStreamer::setSmallScriptThresholdForTesting(100);
ScriptStreamer::startStreaming(pendingScript(), m_settings.get(), m_scope.scriptState(), PendingScript::ParsingBlocking);
TestScriptResourceClient client;
pendingScript().watchForLoad(&client);
// This is the first data chunk which is small.
appendData("function foo() { }");
appendPadding();
appendPadding();
appendPadding();
finish();
processTasksUntilStreamingComplete();
EXPECT_TRUE(client.finished());
bool errorOccurred = false;
ScriptSourceCode sourceCode = pendingScript().getSource(KURL(), errorOccurred);
EXPECT_FALSE(errorOccurred);
EXPECT_TRUE(sourceCode.streamer());
v8::TryCatch tryCatch;
v8::Handle<v8::Script> script = V8ScriptRunner::compileScript(sourceCode, isolate());
EXPECT_FALSE(script.IsEmpty());
EXPECT_FALSE(tryCatch.HasCaught());
}
INSTANTIATE_TEST_CASE_P(ScriptStreamingInstantiation, ScriptStreamingTest, ::testing::Values(false, true));
} // namespace
} // namespace blink