Bug 900784 part 1.5 - Encode JS bytecode when no more scripts are executed. r=mrbkap

This commit is contained in:
Nicolas B. Pierron 2017-04-21 16:57:58 +00:00
Родитель 5b59221f67
Коммит c6509f9e01
3 изменённых файлов: 317 добавлений и 16 удалений

Просмотреть файл

@ -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<JSScript*> script(aes.cx());
rv = exec.JoinAndExec(&aRequest->mOffThreadToken, &script);
{
nsJSUtils::ExecutionContext exec(aes.cx(), global);
JS::Rooted<JSScript*> 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<nsIRunnable> 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<nsIScriptGlobalObject> globalObject = GetScriptGlobalObject();
if (!globalObject) {
GiveUpBytecodeEncoding();
return;
}
nsCOMPtr<nsIScriptContext> context = globalObject->GetScriptContext();
if (!context) {
GiveUpBytecodeEncoding();
return;
}
AutoEntryScript aes(globalObject, "encode bytecode", true);
RefPtr<nsScriptLoadRequest> 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<nsIOutputStream> 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<char*>(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<nsIScriptGlobalObject> globalObject = GetScriptGlobalObject();
if (globalObject) {
nsCOMPtr<nsIScriptContext> context = globalObject->GetScriptContext();
if (context) {
AutoEntryScript aes(globalObject, "give-up bytecode encoding", true);
JS::RootedScript script(aes.cx());
while (!mBytecodeEncodingQueue.isEmpty()) {
RefPtr<nsScriptLoadRequest> 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<nsScriptLoadRequest> 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<nsIRunnable> 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

Просмотреть файл

@ -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<JSScript*> mScript;
// Holds script text for non-inline scripts. Don't use nsString so we can give
// ownership to jsapi.
mozilla::Vector<char16_t> 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<nsICacheInfoChannel> mCacheInfo;
};
class nsScriptLoadRequestList : private mozilla::LinkedList<nsScriptLoadRequest>
@ -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<nsIScriptGlobalObject> GetScriptGlobalObject();
nsresult FillCompileOptionsForRequest(const mozilla::dom::AutoJSAPI& jsapi,
nsScriptLoadRequest* aRequest,
@ -676,6 +722,10 @@ private:
nsScriptLoadRequestList mXSLTRequests;
RefPtr<nsScriptLoadRequest> 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<nsScriptLoadRequest> mRequest;
@ -709,6 +759,7 @@ private:
bool mDeferEnabled;
bool mDocumentParsingDone;
bool mBlockingDOMContentLoaded;
bool mLoadEventFired;
// Module map
nsRefPtrHashtable<nsURIHashKey, mozilla::GenericPromise::Private> mFetchingModules;

Просмотреть файл

@ -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.