Index: Source/bindings/core/v8/V8ScriptStreamer.cpp |
diff --git a/Source/bindings/core/v8/V8ScriptStreamer.cpp b/Source/bindings/core/v8/V8ScriptStreamer.cpp |
new file mode 100644 |
index 0000000000000000000000000000000000000000..23a8acd665a198d483fc72cd4e865e9130a55803 |
--- /dev/null |
+++ b/Source/bindings/core/v8/V8ScriptStreamer.cpp |
@@ -0,0 +1,340 @@ |
+/* |
+ * Copyright (C) 2014 Google Inc. All rights reserved. |
haraken
2014/08/17 16:05:27
Nit: Use the 3-line copyright.
marja
2014/08/20 11:45:55
Done.
|
+ * |
+ * 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. AND ITS CONTRIBUTORS ``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 ITS 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 "config.h" |
+#include "bindings/core/v8/V8ScriptStreamer.h" |
+ |
+#include "bindings/core/v8/V8ScriptRunner.h" |
+#include "bindings/core/v8/V8ScriptStreamerThread.h" |
+#include "core/dom/Document.h" |
+#include "core/dom/Element.h" |
+#include "core/dom/PendingScript.h" |
+#include "core/fetch/ScriptResource.h" |
+#include "core/frame/LocalFrame.h" |
+#include "core/frame/Settings.h" |
+#include "core/html/parser/HTMLScriptRunner.h" |
+#include "platform/SharedBuffer.h" |
+#include "wtf/MainThread.h" |
+#include "wtf/text/TextEncodingRegistry.h" |
+ |
+#include <string.h> |
+ |
+namespace blink { |
+ |
+class LocalFrame; |
+ |
+class SourceStream : public v8::ExternalSourceStream { |
haraken
2014/08/17 16:05:26
(I haven't reviewed this class yet. I'll take a cl
marja
2014/08/20 11:45:55
Acknowledged.
|
+public: |
+ SourceStream(ScriptResource* resource) |
+ : m_resource(resource) |
+ , m_backgroundThreadWaitingForData(false) |
+ , m_dataPointer(0) |
+ , m_dataLength(0) |
+ , m_dataPosition(0) |
+ , m_finished(false) |
+ , m_cancelled(false) { } |
+ |
+ virtual ~SourceStream() { } |
+ |
+ // Called by V8 on a background thread. Should block until we can return |
+ // some data. |
+ virtual unsigned GetMoreData(const char** src) OVERRIDE |
+ { |
+ MutexLocker locker(m_mutex); |
+ if (m_cancelled) { |
+ return 0; |
+ } |
+ // The main thread might be modifying the data buffer of the Resource, |
+ // so we cannot read it here. Post a task to the main thread and wait |
+ // for it to complete. |
+ m_backgroundThreadWaitingForData = true; |
+ m_dataPointer = src; |
+ m_dataLength = 0; |
+ callOnMainThread(WTF::bind(&SourceStream::getMoreDataOnMainThread, this)); |
+ m_condition.wait(m_mutex); |
+ if (m_cancelled) { |
+ return 0; |
+ } |
+ return m_dataLength; |
+ } |
+ |
+ // Called on the main thread. |
+ void didFinishLoading() |
+ { |
+ if (m_resource->errorOccurred()) { |
+ // The background thread might be waiting for data. Wake it up. V8 |
+ // will perceive this as EOS and call the completion callback. At |
+ // that point we will resume the normal "script loaded" code path |
+ // (see HTMLScriptRunner::notifyFinished which doesn't differentiate |
+ // between scripts which loaded successfully and scripts which |
+ // failed to load). |
+ cancel(); |
+ return; |
+ } |
+ { |
+ MutexLocker locker(m_mutex); |
+ m_finished = true; |
+ if (!m_backgroundThreadWaitingForData) |
+ return; |
+ } |
+ getMoreDataOnMainThread(); |
+ } |
+ |
+ // Called on the main thread. |
+ void didReceiveData() |
+ { |
+ getMoreDataOnMainThread(); |
+ } |
+ |
+ // Called on the main thread. |
+ void cancel() |
+ { |
+ // The script is no longer needed. Stop streaming it. The next time |
+ // GetMoreData is called (or woken up), it will return 0, which will be |
+ // interpreted as EOS by V8 and the parsing will fail. Our "streaming |
+ // complete" callback will be called, and at that point we will release |
+ // the references to SourceStream. |
+ MutexLocker locker(m_mutex); |
+ m_cancelled = true; |
+ m_backgroundThreadWaitingForData = false; |
+ m_condition.signal(); |
+ } |
+ |
+private: |
+ // Called on the main thread. |
+ void getMoreDataOnMainThread() |
+ { |
+ MutexLocker locker(m_mutex); |
+ if (!m_backgroundThreadWaitingForData) { |
+ // It's possible that the background thread is not waiting. This |
+ // happens when 1) data arrives but V8 hasn't requested data yet, 2) |
+ // we have previously scheduled getMoreDataOnMainThread, but some |
+ // other event (load cancelled or finished) already retrieved the |
+ // data and set m_backgroundThreadWaitingForData to false. |
+ return; |
+ } |
+ if (!m_resourceBuffer) { |
+ // We don't have a buffer yet. Try to get it from the resource. |
+ SharedBuffer* buffer = m_resource->resourceBuffer(); |
+ if (!buffer) { |
+ // The Resource hasn't created its SharedBuffer yet. At some |
+ // point (at latest when the script loading finishes) this |
+ // function will be called again and we will have data. |
+ if (m_finished) { |
+ // The load finished and we never had a buffer. In normal |
+ // operations, this shouldn't happen, but it can probably |
+ // happen for empty scripts. |
+ m_backgroundThreadWaitingForData = false; |
+ m_condition.signal(); |
+ } |
+ return; |
+ } |
+ m_resourceBuffer = RefPtr<SharedBuffer>(buffer); |
+ } |
+ |
+ if (m_dataPosition == 0) { |
+ // This is the first time data is requested. Check the encoding. |
+ const char* encoding = WTF::atomicCanonicalTextEncodingName(m_resource->encoding()); |
+ |
+ // Here's a list of encodings we can use for streaming. These are |
+ // the canonical names. |
+ if (strcmp(encoding, "windows-1252") == 0 |
+ || strcmp(encoding, "ISO-8859-1") == 0 |
+ || strcmp(encoding, "US-ASCII") == 0) { |
+ SetEncoding(v8::ExternalSourceStream::ONE_BYTE); |
+ } else if (strcmp(encoding, "UTF-8") == 0) { |
+ SetEncoding(v8::ExternalSourceStream::UTF8); |
+ } else { |
+ // Otherwise, cancel the streaming. We don't stream two byte |
+ // scripts to avoid the handling of byte order marks. Most |
+ // scripts are Latin1 or UTF-8 anyway, so this should be enough |
+ // for most real world purposes. |
+ m_cancelled = true; |
+ m_backgroundThreadWaitingForData = false; |
+ m_condition.signal(); |
+ } |
+ } |
+ |
+ // Get as much data from the ResourceBuffer as we can. |
+ const char* data = 0; |
+ Vector<const char*> chunks; |
+ Vector<unsigned> chunkLengths; |
+ while (unsigned length = m_resourceBuffer->getSomeData(data, m_dataPosition)) { |
+ // TODO(marja): Here we can limit based on the total length, if it |
haraken
2014/08/17 16:05:27
Nit: TODO(marja) => FIXME
marja
2014/08/20 11:45:55
Done.
|
+ // turns out that we don't want to give all the data we have (memory |
+ // vs. speed). |
+ chunks.append(data); |
+ chunkLengths.append(length); |
+ m_dataLength += length; |
+ m_dataPosition += length; |
+ } |
+ // Copy the data chunks into a new buffer, since we're going to give the |
+ // data to a background thread. |
+ if (m_dataLength > 0) { |
+ char* copiedData = new char[m_dataLength]; |
+ unsigned offset = 0; |
+ for (size_t i = 0; i < chunks.size(); ++i) { |
+ memcpy(copiedData + offset, chunks[i], chunkLengths[i]); |
+ offset += chunkLengths[i]; |
+ } |
+ *m_dataPointer = copiedData; |
+ m_backgroundThreadWaitingForData = false; |
+ m_condition.signal(); |
+ } else if (m_finished) { |
+ // We need to return data length 0 to V8 to signal that the data |
+ // ends. |
+ m_condition.signal(); |
+ } |
+ // Otherwise, we had scheduled getMoreDataOnMainThread but we didn't |
+ // have any data to return. It will be scheduled again when the data |
+ // arrives. |
+ } |
+ |
+ ScriptResource* m_resource; |
+ |
+ // For coordinating between the main thread and background thread tasks. |
+ // Guarded by m_mutex. |
+ bool m_backgroundThreadWaitingForData; |
+ const char** m_dataPointer; |
+ unsigned m_dataLength; |
+ unsigned m_dataPosition; |
+ bool m_finished; |
+ bool m_cancelled; |
+ RefPtr<SharedBuffer> m_resourceBuffer; // Only used by the main thread. |
+ |
+ ThreadCondition m_condition; |
+ Mutex m_mutex; |
+}; |
+ |
+ |
+bool V8ScriptStreamer::startStreaming(PendingScript* script, HTMLScriptRunner* runner) |
+{ |
+ if (V8ScriptStreamerThread::shared()->isRunningTask()) { |
haraken
2014/08/17 16:05:26
Given that we currently support only parser-blocki
marja
2014/08/20 11:45:55
It can still be that we're already running a task.
|
+ // At the moment we only have one thread for running the tasks. A new |
+ // task shouldn't be queued before the running task completes, because |
+ // the running task can block and wait for data from the network. |
haraken
2014/08/17 16:05:26
Just to confirm: Is this the reason it's not easy
marja
2014/08/20 11:45:55
There are several modifications needed when we wan
haraken
2014/08/20 15:00:18
Understood.
|
+ return false; |
+ } |
+ ScriptResource* resource = script->resource(); |
+ if (!resource->url().protocolIsInHTTPFamily()) { |
+ return false; |
+ } |
+ // We cannot filter out short scripts, even if we wait for the HTTP headers |
+ // to arrive. In general, the web servers don't seem to send the |
+ // Content-Length HTTP header for scripts. |
+ |
+ LocalFrame* frame = script->element()->document().frame(); |
+ ScriptState* scriptState = ScriptState::forMainWorld(frame); |
haraken
2014/08/17 16:05:26
Just to confirm: Script streaming is used only in
marja
2014/08/20 11:45:55
Only for parser blocking scripts of HTMLScriptRunn
haraken
2014/08/20 15:00:18
Yes, I believe so. I also believe that non-parser-
|
+ if (scriptState->contextIsEmpty()) |
+ return false; |
+ ScriptState::Scope scope(scriptState); |
+ |
+ // script is a pointer to m_parserBlockingScript of HTMLScriptRunner, which |
+ // might go out of scope if HTMLScriptRunner is destroyed. We will soon call |
+ // PendingScript::setStreamer, which makes the PendingScript notify the |
+ // V8ScriptStreamer when it is destroyed. |
+ RefPtr<V8ScriptStreamer> streamer = adoptRef(new V8ScriptStreamer(script, runner)); |
+ |
+ v8::ScriptCompiler::CompileOptions compileOption = v8::ScriptCompiler::kNoCompileOptions; |
+ if (frame->settings()) { |
+ V8CacheOptions v8CacheOptions = frame->settings()->v8CacheOptions(); |
+ bool ignore; |
+ V8ScriptRunner::CacheDecider(0, resource, v8CacheOptions, &streamer->m_cachedDataDataType, &compileOption, &ignore); |
haraken
2014/08/17 16:05:25
I don't fully understand why you can decide whethe
marja
2014/08/20 11:45:55
(As discussed offline), this code path only decide
|
+ } |
+ v8::ScriptCompiler::ScriptStreamingTask* scriptStreamingTask = v8::ScriptCompiler::StartStreamingScript(scriptState->isolate(), &(streamer->m_source), compileOption); |
+ if (scriptStreamingTask) { |
+ // The background thread holds an implicit reference to streamer, |
+ // because it's passed as a callback parameter. The corresponding deref |
+ // is in V8ScriptStreamer::streamingComplete(). |
+ ScriptStreamingTask* task = new ScriptStreamingTask(scriptStreamingTask, streamer.get()); |
+ streamer->ref(); |
+ script->setStreamer(streamer.release()); |
+ V8ScriptStreamerThread::shared()->postTask(task); |
+ return true; |
+ } |
+ return false; |
+} |
+ |
+void V8ScriptStreamer::notifyAppendData() |
+{ |
+ m_stream->didReceiveData(); |
+} |
+ |
+void V8ScriptStreamer::notifyFinished() |
+{ |
+ m_stream->didFinishLoading(); |
+ m_loadingDone = true; |
+ // The load complete notification can come before or after V8 has processed |
+ // all the data. Send the notification after both loading and parsing have |
+ // completed. |
haraken
2014/08/17 16:05:26
Can we create a helper method like notifyFinishedT
haraken
2014/08/17 16:05:26
Help me understand: In what scenario is it possibl
marja
2014/08/20 11:45:55
Done.
marja
2014/08/20 11:45:55
The V8 side can finish before loading e.g., when V
|
+ if (m_v8Done) { |
+ m_runner->notifyFinished(m_script->resource()); |
+ } |
+} |
+ |
+void V8ScriptStreamer::streamingComplete() |
+{ |
+ // It's possible that the corresponding PendingScript was deleted before V8 |
+ // finished streaming. |
+ if (refCount() == 1) { |
haraken
2014/08/17 16:05:25
It's fragile to rely on the reference counter to j
marja
2014/08/20 11:45:55
Done (I ended up needing a more explicit way anywa
|
+ deref(); |
+ return; |
+ } |
+ |
+ // We have now streamed the whole script to V8 and it has parsed the |
+ // script. We're ready for the next step: compiling and executing the |
+ // script. |
+ m_v8Done = true; |
+ // V8 no longer holds an implicit reference. |
+ deref(); |
+ |
+ // The load complete notification can come before or after V8 has processed |
+ // all the data. Send the notification after both loading and parsing have |
+ // completed. |
haraken
2014/08/17 16:05:26
Ditto. Use notifyFinishedToHTMLScriptRunner().
marja
2014/08/20 11:45:55
Done.
|
+ if (m_loadingDone) { |
+ m_runner->notifyFinished(m_script->resource()); |
+ } |
+} |
+ |
+void V8ScriptStreamer::cancel() |
+{ |
+ m_script = 0; |
+ m_runner = 0; |
+ // The streaming might still be ongoing. Tell SourceStream to try to cancel |
+ // it whenever it gets the control the next time. It can also be that V8 has |
+ // already completed its operations and our callback will be called soon. |
+ m_stream->cancel(); |
+} |
+ |
+V8ScriptStreamer::V8ScriptStreamer(PendingScript* script, HTMLScriptRunner* runner) |
+ : m_script(script) |
+ , m_runner(runner) |
+ , m_stream(new SourceStream(script->resource())) |
+ , m_source(m_stream) // Source takes ownership of m_stream. |
haraken
2014/08/17 16:05:26
BTW, "StreamedSource" and "SourceStream" are confu
haraken
2014/08/17 16:05:26
Given that StreamedSource has an ownership of the
marja
2014/08/20 11:45:55
I could add an accessor for getting the ExternalSo
marja
2014/08/20 11:45:55
By StreamedSource you mean v8::ScriptCompiler::Str
haraken
2014/08/20 15:00:18
OK, then let's keep your current code as is.
haraken
2014/08/20 15:00:18
Got it; StreamedSource makes sense.
However, it's
marja
2014/09/09 17:00:26
I tried to think of the naming a bit, but didn't f
|
+ , m_loadingDone(false) |
+ , m_v8Done(false) |
+{ } |
haraken
2014/08/17 16:05:26
Nit: Write { and } in separate lines.
marja
2014/08/20 11:45:55
Done.
|
+ |
+} // namespace blink |