From c6509f9e0122b3b73192fc53b99cc4860a1fe4fa Mon Sep 17 00:00:00 2001 From: "Nicolas B. Pierron" Date: Fri, 21 Apr 2017 16:57:58 +0000 Subject: [PATCH] Bug 900784 part 1.5 - Encode JS bytecode when no more scripts are executed. r=mrbkap --- dom/base/nsScriptLoader.cpp | 274 +++++++++++++++++++++++++++++-- dom/base/nsScriptLoader.h | 53 +++++- layout/base/nsDocumentViewer.cpp | 6 + 3 files changed, 317 insertions(+), 16 deletions(-) diff --git a/dom/base/nsScriptLoader.cpp b/dom/base/nsScriptLoader.cpp index b32d5b5131a7..88d7eefa6b3f 100644 --- a/dom/base/nsScriptLoader.cpp +++ b/dom/base/nsScriptLoader.cpp @@ -57,9 +57,11 @@ #include "mozilla/dom/EncodingUtils.h" #include "mozilla/ConsoleReportCollector.h" +#include "mozilla/AsyncEventDispatcher.h" #include "mozilla/Attributes.h" #include "mozilla/Unused.h" #include "nsIScriptError.h" +#include "nsIOutputStream.h" using namespace mozilla; using namespace mozilla::dom; @@ -102,11 +104,24 @@ ImplCycleCollectionTraverse(nsCycleCollectionTraversalCallback& aCallback, NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsScriptLoadRequest) NS_INTERFACE_MAP_END -NS_IMPL_CYCLE_COLLECTION_0(nsScriptLoadRequest) - NS_IMPL_CYCLE_COLLECTING_ADDREF(nsScriptLoadRequest) NS_IMPL_CYCLE_COLLECTING_RELEASE(nsScriptLoadRequest) +NS_IMPL_CYCLE_COLLECTION_CLASS(nsScriptLoadRequest) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(nsScriptLoadRequest) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mCacheInfo) + tmp->DropBytecodeCacheReferences(); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(nsScriptLoadRequest) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mCacheInfo) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(nsScriptLoadRequest) + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mScript) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + nsScriptLoadRequest::~nsScriptLoadRequest() { // We should always clean up any off-thread script parsing resources. @@ -115,6 +130,10 @@ nsScriptLoadRequest::~nsScriptLoadRequest() // But play it safe in release builds and try to clean them up here // as a fail safe. MaybeCancelOffThreadScript(); + + if (mScript) { + DropBytecodeCacheReferences(); + } } void @@ -543,6 +562,7 @@ nsScriptLoader::nsScriptLoader(nsIDocument *aDocument) mDeferEnabled(false), mDocumentParsingDone(false), mBlockingDOMContentLoaded(false), + mLoadEventFired(false), mReporter(new ConsoleReportCollector()) { } @@ -2162,9 +2182,9 @@ nsScriptLoader::ProcessRequest(nsScriptLoadRequest* aRequest) aRequest->MaybeCancelOffThreadScript(); } - // Free any source data. + // Free any source data, but keep the bytecode content as we might have to + // save it later. aRequest->mScriptText.clearAndFree(); - aRequest->mScriptBytecode.clearAndFree(); return rv; } @@ -2348,6 +2368,7 @@ nsScriptLoader::EvaluateScript(nsScriptLoadRequest* aRequest) MOZ_ASSERT(module); rv = nsJSUtils::ModuleEvaluation(aes.cx(), module); } + aRequest->mCacheInfo = nullptr; } else { JS::CompileOptions options(aes.cx()); rv = FillCompileOptionsForRequest(aes, aRequest, global, &options); @@ -2363,41 +2384,249 @@ nsScriptLoader::EvaluateScript(nsScriptLoadRequest* aRequest) rv = exec.DecodeAndExec(options, aRequest->mScriptBytecode, aRequest->mBytecodeOffset); } + // We do not expect to be saving anything when we already have some + // bytecode. + MOZ_ASSERT(!aRequest->mCacheInfo); } else { - nsJSUtils::ExecutionContext exec(aes.cx(), global); MOZ_ASSERT(aRequest->IsSource()); if (aRequest->mOffThreadToken) { // Off-main-thread parsing. LOG(("ScriptLoadRequest (%p): Join (off-thread parsing) and Execute", aRequest)); - JS::Rooted script(aes.cx()); - rv = exec.JoinAndExec(&aRequest->mOffThreadToken, &script); + { + nsJSUtils::ExecutionContext exec(aes.cx(), global); + JS::Rooted script(aes.cx()); + if (!aRequest->mCacheInfo) { + rv = exec.JoinAndExec(&aRequest->mOffThreadToken, &script); + LOG(("ScriptLoadRequest (%p): Cannot cache anything (cacheInfo = nullptr)", + aRequest)); + } else { + MOZ_ASSERT(aRequest->mBytecodeOffset == + aRequest->mScriptBytecode.length()); + rv = exec.JoinEncodeAndExec(&aRequest->mOffThreadToken, + aRequest->mScriptBytecode, + &script); + // Queue the current script load request to later save the bytecode. + if (NS_SUCCEEDED(rv)) { + aRequest->mScript = script; + HoldJSObjects(aRequest); + RegisterForBytecodeEncoding(aRequest); + } else { + LOG(("ScriptLoadRequest (%p): Cannot cache anything (rv = %X, script = %p, cacheInfo = %p)", + aRequest, unsigned(rv), script.get(), aRequest->mCacheInfo.get())); + aRequest->mCacheInfo = nullptr; + } + } + } } else { // Main thread parsing (inline and small scripts) LOG(("ScriptLoadRequest (%p): Compile And Exec", aRequest)); + nsJSUtils::ExecutionContext exec(aes.cx(), global); nsAutoString inlineData; SourceBufferHolder srcBuf = GetScriptSource(aRequest, inlineData); rv = exec.CompileAndExec(options, srcBuf); + aRequest->mCacheInfo = nullptr; } } } } + + // Even if we are not saving the bytecode of the current script, we have + // to trigger the encoding of the bytecode, as the current script can + // call functions of a script for which we are recording the bytecode. + MaybeTriggerBytecodeEncoding(); } context->SetProcessingScriptTag(oldProcessingScriptTag); return rv; } +void +nsScriptLoader::RegisterForBytecodeEncoding(nsScriptLoadRequest* aRequest) +{ + MOZ_ASSERT(aRequest->mCacheInfo); + MOZ_ASSERT(aRequest->mScript); + mBytecodeEncodingQueue.AppendElement(aRequest); +} + +void +nsScriptLoader::LoadEventFired() +{ + mLoadEventFired = true; + MaybeTriggerBytecodeEncoding(); +} + +void +nsScriptLoader::MaybeTriggerBytecodeEncoding() +{ + // We wait for the load event to be fired before saving the bytecode of + // any script to the cache. It is quite common to have load event + // listeners trigger more JavaScript execution, that we want to save as + // part of this start-up bytecode cache. + if (!mLoadEventFired) { + return; + } + + // No need to fire any event if there is no bytecode to be saved. + if (mBytecodeEncodingQueue.isEmpty()) { + return; + } + + // Wait until all scripts are loaded before saving the bytecode, such that + // we capture most of the intialization of the page. + if (HasPendingRequests()) { + return; + } + + // Create a new runnable dedicated to encoding the content of the bytecode + // of all enqueued scripts. In case of failure, we give-up on encoding the + // bytecode. + nsCOMPtr encoder = + NewRunnableMethod("nsScriptLoader::EncodeBytecode", + this, &nsScriptLoader::EncodeBytecode); + if (NS_FAILED(NS_DispatchToCurrentThread(encoder))) { + GiveUpBytecodeEncoding(); + } +} + +void +nsScriptLoader::EncodeBytecode() +{ + // If any script got added in the previous loop cycle, wait until all + // remaining script executions are completed, such that we capture most of + // the initialization. + if (HasPendingRequests()) { + return; + } + + nsCOMPtr globalObject = GetScriptGlobalObject(); + if (!globalObject) { + GiveUpBytecodeEncoding(); + return; + } + + nsCOMPtr context = globalObject->GetScriptContext(); + if (!context) { + GiveUpBytecodeEncoding(); + return; + } + + AutoEntryScript aes(globalObject, "encode bytecode", true); + RefPtr request; + while (!mBytecodeEncodingQueue.isEmpty()) { + request = mBytecodeEncodingQueue.StealFirst(); + EncodeRequestBytecode(aes.cx(), request); + request->mScriptBytecode.clearAndFree(); + request->DropBytecodeCacheReferences(); + } +} + +void +nsScriptLoader::EncodeRequestBytecode(JSContext* aCx, nsScriptLoadRequest* aRequest) +{ + nsresult rv = NS_OK; + MOZ_ASSERT(aRequest->mCacheInfo); + + JS::RootedScript script(aCx, aRequest->mScript); + if (!JS::FinishIncrementalEncoding(aCx, script)) { + LOG(("ScriptLoadRequest (%p): Cannot serialize bytecode", + aRequest)); + return; + } + + if (aRequest->mScriptBytecode.length() >= UINT32_MAX) { + LOG(("ScriptLoadRequest (%p): Bytecode cache is too large to be decoded correctly.", + aRequest)); + return; + } + + // Open the output stream to the cache entry alternate data storage. This + // might fail if the stream is already open by another request, in which + // case, we just ignore the current one. + nsCOMPtr output; + rv = aRequest->mCacheInfo->OpenAlternativeOutputStream(kBytecodeMimeType, + getter_AddRefs(output)); + if (NS_FAILED(rv)) { + LOG(("ScriptLoadRequest (%p): Cannot open bytecode cache (rv = %X, output = %p)", + aRequest, unsigned(rv), output.get())); + return; + } + MOZ_ASSERT(output); + auto closeOutStream = mozilla::MakeScopeExit([&]() { + nsresult rv = output->Close(); + LOG(("ScriptLoadRequest (%p): Closing (rv = %X)", + aRequest, unsigned(rv))); + }); + + uint32_t n; + rv = output->Write(reinterpret_cast(aRequest->mScriptBytecode.begin()), + aRequest->mScriptBytecode.length(), &n); + LOG(("ScriptLoadRequest (%p): Write bytecode cache (rv = %X, length = %u, written = %u)", + aRequest, unsigned(rv), unsigned(aRequest->mScriptBytecode.length()), n)); + if (NS_FAILED(rv)) { + return; + } +} + +void +nsScriptLoadRequest::DropBytecodeCacheReferences() +{ + mCacheInfo = nullptr; + mScript = nullptr; + DropJSObjects(this); +} + +void +nsScriptLoader::GiveUpBytecodeEncoding() +{ + // Ideally we prefer to properly end the incremental encoder, such that we + // would not keep a large buffer around. If we cannot, we fallback on the + // removal of all request from the current list. + nsCOMPtr globalObject = GetScriptGlobalObject(); + if (globalObject) { + nsCOMPtr context = globalObject->GetScriptContext(); + if (context) { + AutoEntryScript aes(globalObject, "give-up bytecode encoding", true); + JS::RootedScript script(aes.cx()); + while (!mBytecodeEncodingQueue.isEmpty()) { + RefPtr request = mBytecodeEncodingQueue.StealFirst(); + LOG(("ScriptLoadRequest (%p): Cannot serialize bytecode", request.get())); + script.set(request->mScript); + Unused << JS::FinishIncrementalEncoding(aes.cx(), script); + request->mScriptBytecode.clearAndFree(); + request->DropBytecodeCacheReferences(); + } + return; + } + } + + while (!mBytecodeEncodingQueue.isEmpty()) { + RefPtr request = mBytecodeEncodingQueue.StealFirst(); + LOG(("ScriptLoadRequest (%p): Cannot serialize bytecode", request.get())); + // Note: Do not clear the mScriptBytecode buffer, because the incremental + // encoder owned by the ScriptSource object still has a reference to this + // buffer. This reference would be removed as soon as the ScriptSource + // object would be GC. + request->mCacheInfo = nullptr; + } +} + +bool +nsScriptLoader::HasPendingRequests() +{ + return mParserBlockingRequest || + !mXSLTRequests.isEmpty() || + !mLoadedAsyncRequests.isEmpty() || + !mNonAsyncExternalScriptInsertedRequests.isEmpty() || + !mDeferRequests.isEmpty() || + !mPendingChildLoaders.IsEmpty(); +} + void nsScriptLoader::ProcessPendingRequestsAsync() { - if (mParserBlockingRequest || - !mXSLTRequests.isEmpty() || - !mLoadedAsyncRequests.isEmpty() || - !mNonAsyncExternalScriptInsertedRequests.isEmpty() || - !mDeferRequests.isEmpty() || - !mPendingChildLoaders.IsEmpty()) { + if (HasPendingRequests()) { nsCOMPtr task = NewRunnableMethod(this, &nsScriptLoader::ProcessPendingRequests); if (mDocument) { @@ -3425,9 +3654,24 @@ nsScriptLoadHandler::OnStreamComplete(nsIIncrementalStreamLoader* aLoader, } } + // Everything went well, keep the CacheInfoChannel alive such that we can + // later save the bytecode on the cache entry. + if (NS_SUCCEEDED(rv) && mRequest->IsSource() && IsBytecodeCacheEnabled()) { + mRequest->mCacheInfo = do_QueryInterface(channelRequest); + LOG(("ScriptLoadRequest (%p): nsICacheInfoChannel = %p", + mRequest.get(), mRequest->mCacheInfo.get())); + } + // we have to mediate and use mRequest. - return mScriptLoader->OnStreamComplete(aLoader, mRequest, aStatus, mSRIStatus, - mSRIDataVerifier); + rv = mScriptLoader->OnStreamComplete(aLoader, mRequest, aStatus, mSRIStatus, + mSRIDataVerifier); + + // In case of failure, clear the mCacheInfoChannel to avoid keeping it alive. + if (NS_FAILED(rv)) { + mRequest->mCacheInfo = nullptr; + } + + return rv; } #undef LOG_ENABLED diff --git a/dom/base/nsScriptLoader.h b/dom/base/nsScriptLoader.h index 4c6a389cf413..672f143dbe4d 100644 --- a/dom/base/nsScriptLoader.h +++ b/dom/base/nsScriptLoader.h @@ -19,6 +19,7 @@ #include "nsCycleCollectionParticipant.h" #include "nsTArray.h" #include "nsAutoPtr.h" +#include "nsICacheInfoChannel.h" #include "nsIDocument.h" #include "nsIIncrementalStreamLoader.h" #include "nsURIHashKey.h" @@ -99,7 +100,7 @@ public: } NS_DECL_CYCLE_COLLECTING_ISUPPORTS - NS_DECL_CYCLE_COLLECTION_CLASS(nsScriptLoadRequest) + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(nsScriptLoadRequest) bool IsModuleRequest() const { @@ -187,6 +188,7 @@ public: } void MaybeCancelOffThreadScript(); + void DropBytecodeCacheReferences(); using super::getNext; using super::isInList; @@ -208,6 +210,10 @@ public: void* mOffThreadToken; // Off-thread parsing token. nsString mSourceMapURL; // Holds source map url for loaded scripts + // Holds the top-level JSScript that corresponds to the current source, once + // it is parsed, and planned to be saved in the bytecode cache. + JS::Heap mScript; + // Holds script text for non-inline scripts. Don't use nsString so we can give // ownership to jsapi. mozilla::Vector mScriptText; @@ -225,6 +231,10 @@ public: const mozilla::CORSMode mCORSMode; const mozilla::dom::SRIMetadata mIntegrity; mozilla::net::ReferrerPolicy mReferrerPolicy; + + // Holds the Cache information, which is used to register the bytecode + // on the cache entry, such that we can load it the next time. + nsCOMPtr mCacheInfo; }; class nsScriptLoadRequestList : private mozilla::LinkedList @@ -468,6 +478,11 @@ public: nsresult aSRIStatus, mozilla::dom::SRICheckDataVerifier* aSRIDataVerifier); + /** + * Returns wether any request is queued, and not executed yet. + */ + bool HasPendingRequests(); + /** * Processes any pending requests that are ready for processing. */ @@ -537,6 +552,13 @@ public: return mDocument->GetDocGroup(); } + /** + * Register the fact that we saw the load event, and that we need to save the + * bytecode at the next loop cycle unless new scripts are waiting in the + * pipeline. + */ + void LoadEventFired(); + private: virtual ~nsScriptLoader(); @@ -622,6 +644,30 @@ private: nsScriptLoadRequest* aRequest); nsresult EvaluateScript(nsScriptLoadRequest* aRequest); + /** + * Queue the current script load request to be saved, when the page + * initialization ends. The page initialization end is defined as being the + * time when the load event got received, and when no more scripts are waiting + * to be executed. + */ + void RegisterForBytecodeEncoding(nsScriptLoadRequest* aRequest); + + /** + * Check if all conditions are met, i-e that the onLoad event fired and that + * no more script have to be processed. If all conditions are met, queue an + * event to encode all the bytecode and save them on the cache. + */ + void MaybeTriggerBytecodeEncoding(); + + /** + * Iterate over all script load request and save the bytecode of executed + * functions on the cache provided by the channel. + */ + void EncodeBytecode(); + void EncodeRequestBytecode(JSContext* aCx, nsScriptLoadRequest* aRequest); + + void GiveUpBytecodeEncoding(); + already_AddRefed GetScriptGlobalObject(); nsresult FillCompileOptionsForRequest(const mozilla::dom::AutoJSAPI& jsapi, nsScriptLoadRequest* aRequest, @@ -676,6 +722,10 @@ private: nsScriptLoadRequestList mXSLTRequests; RefPtr mParserBlockingRequest; + // List of script load request that are holding a buffer which has to be saved + // on the cache. + nsScriptLoadRequestList mBytecodeEncodingQueue; + // In mRequests, the additional information here is stored by the element. struct PreloadInfo { RefPtr mRequest; @@ -709,6 +759,7 @@ private: bool mDeferEnabled; bool mDocumentParsingDone; bool mBlockingDOMContentLoaded; + bool mLoadEventFired; // Module map nsRefPtrHashtable mFetchingModules; diff --git a/layout/base/nsDocumentViewer.cpp b/layout/base/nsDocumentViewer.cpp index 102e0968790f..67aa1dd07aa8 100644 --- a/layout/base/nsDocumentViewer.cpp +++ b/layout/base/nsDocumentViewer.cpp @@ -1082,6 +1082,12 @@ nsDocumentViewer::LoadComplete(nsresult aStatus) } } + // Release the JS bytecode cache from its wait on the load event, and + // potentially dispatch the encoding of the bytecode. + if (mDocument && mDocument->ScriptLoader()) { + mDocument->ScriptLoader()->LoadEventFired(); + } + nsJSContext::LoadEnd(); // It's probably a good idea to GC soon since we have finished loading.