Bug 1333990: Part 1a - Add an async script pre-loading utility. r=billm,shu

MozReview-Commit-ID: 4vJF2drLeHS

--HG--
extra : rebase_source : 8adaa22d6d91ac6ef1804cf4c8d5698cf304b141
This commit is contained in:
Kris Maglione 2017-03-16 19:10:40 -07:00
parent 48125abbbe
commit 437f4e37d4
9 changed files with 508 additions and 0 deletions

View File

@ -21,6 +21,8 @@ class HeapSnapshot;
namespace dom {
class ArrayBufferViewOrArrayBuffer;
class PrecompiledScript;
class Promise;
class ThreadSafeChromeUtils
{
@ -101,6 +103,13 @@ public:
aA.mUserContextId == aB.mUserContextId &&
aA.mPrivateBrowsingId == aB.mPrivateBrowsingId;
}
// Implemented in js/xpconnect/loader/ChromeScriptLoader.cpp
static already_AddRefed<Promise>
CompileScript(GlobalObject& aGlobal,
const nsAString& aUrl,
const dom::CompileScriptOptionsDictionary& aOptions,
ErrorResult& aRv);
};
} // namespace dom

View File

@ -13,6 +13,7 @@
#include "prsystem.h"
#include "jsapi.h"
#include "jsfriendapi.h"
#include "js/Utility.h"
#include "xpcpublic.h"
#include "nsCycleCollectionParticipant.h"
#include "nsIContent.h"

View File

@ -407,6 +407,21 @@ public:
nsIDocument* aDocument,
char16_t*& aBufOut, size_t& aLengthOut);
static inline nsresult
ConvertToUTF16(nsIChannel* aChannel, const uint8_t* aData,
uint32_t aLength, const nsAString& aHintCharset,
nsIDocument* aDocument,
JS::UniqueTwoByteChars& aBufOut, size_t& aLengthOut)
{
char16_t* bufOut;
nsresult rv = ConvertToUTF16(aChannel, aData, aLength, aHintCharset, aDocument,
bufOut, aLengthOut);
if (NS_SUCCEEDED(rv)) {
aBufOut.reset(bufOut);
}
return rv;
};
/**
* Handle the completion of a stream. This is called by the
* nsScriptLoadHandler object which observes the IncrementalStreamLoader

View File

@ -60,6 +60,15 @@ interface ChromeUtils : ThreadSafeChromeUtils {
static boolean
isOriginAttributesEqual(optional OriginAttributesDictionary aA,
optional OriginAttributesDictionary aB);
/**
* Loads and compiles the script at the given URL and returns an object
* which may be used to execute it repeatedly, in different globals, without
* re-parsing.
*/
[NewObject, Throws]
static Promise<PrecompiledScript>
compileScript(DOMString url, optional CompileScriptOptionsDictionary options);
};
/**
@ -88,3 +97,24 @@ dictionary OriginAttributesPatternDictionary {
unsigned long privateBrowsingId;
DOMString firstPartyDomain;
};
dictionary CompileScriptOptionsDictionary {
/**
* The character set from which to decode the script.
*/
DOMString charset = "utf-8";
/**
* If true, certain parts of the script may be parsed lazily, the first time
* they are used, rather than eagerly parsed at load time.
*/
boolean lazilyParse = false;
/**
* If true, the script will be compiled so that its last expression will be
* returned as the value of its execution. This makes certain types of
* optimization impossible, and disables the JIT in many circumstances, so
* should not be used when not absolutely necessary.
*/
boolean hasReturnValue = false;
};

View File

@ -0,0 +1,32 @@
/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/**
* Represents a pre-compiled JS script, which can be repeatedly exeuted in
* different globals without being re-parsed.
*/
[ChromeOnly, Exposed=(Window,System)]
interface PrecompiledScript {
/**
* Executes the script in the global of the given object, and returns the
* value of its last expression, if compiled with a return value.
*/
[Throws]
any executeInGlobal(object global);
/**
* The URL that the script was loaded from.
*/
[Pure]
readonly attribute DOMString url;
/**
* True if the script was compiled with a return value, and will return the
* value of its last expression when executed.
*/
[Pure]
readonly attribute boolean hasReturnValue;
};

View File

@ -737,6 +737,7 @@ WEBIDL_FILES = [
'PopupBoxObject.webidl',
'Position.webidl',
'PositionError.webidl',
'PrecompiledScript.webidl',
'Presentation.webidl',
'PresentationAvailability.webidl',
'PresentationConnection.webidl',

View File

@ -0,0 +1,359 @@
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/* vim: set ts=8 sts=4 et sw=4 tw=99: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "PrecompiledScript.h"
#include "nsIURI.h"
#include "nsIChannel.h"
#include "nsNetUtil.h"
#include "nsScriptLoader.h"
#include "nsThreadUtils.h"
#include "jsapi.h"
#include "jsfriendapi.h"
#include "js/Utility.h"
#include "mozilla/dom/ChromeUtils.h"
#include "mozilla/dom/Promise.h"
#include "mozilla/HoldDropJSObjects.h"
#include "mozilla/SystemGroup.h"
#include "nsCycleCollectionParticipant.h"
using namespace JS;
using namespace mozilla;
using namespace mozilla::dom;
class AsyncScriptCompiler final : public nsIIncrementalStreamLoaderObserver
, public Runnable
{
public:
NS_DECL_ISUPPORTS_INHERITED
NS_DECL_NSIINCREMENTALSTREAMLOADEROBSERVER
NS_DECL_NSIRUNNABLE
AsyncScriptCompiler(JSContext* aCx, nsIGlobalObject* aGlobal,
const nsACString& aURL,
const CompileScriptOptionsDictionary& aOptions,
Promise* aPromise)
: mOptions(aCx)
, mURL(aURL)
, mGlobalObject(aGlobal)
, mPromise(aPromise)
, mCharset(aOptions.mCharset)
{
mOptions.setVersion(JSVERSION_DEFAULT)
.setNoScriptRval(!aOptions.mHasReturnValue)
.setCanLazilyParse(aOptions.mLazilyParse)
.setFile(aCx, mURL.get());
}
nsresult Start(nsIPrincipal* aPrincipal);
inline void
SetToken(void* aToken)
{
mToken = aToken;
}
protected:
virtual ~AsyncScriptCompiler() {
if (mPromise->State() == Promise::PromiseState::Pending) {
mPromise->MaybeReject(NS_ERROR_FAILURE);
}
}
private:
void Reject(JSContext* aCx);
void Reject(JSContext* aCx, const char* aMxg);
bool StartCompile(JSContext* aCx);
void FinishCompile(JSContext* aCx);
void Finish(JSContext* aCx, Handle<JSScript*> script);
OwningCompileOptions mOptions;
nsCString mURL;
nsCOMPtr<nsIGlobalObject> mGlobalObject;
RefPtr<Promise> mPromise;
nsString mCharset;
void* mToken;
UniqueTwoByteChars mScriptText;
size_t mScriptLength;
};
NS_IMPL_QUERY_INTERFACE_INHERITED(AsyncScriptCompiler, Runnable, nsIIncrementalStreamLoaderObserver)
NS_IMPL_ADDREF_INHERITED(AsyncScriptCompiler, Runnable)
NS_IMPL_RELEASE_INHERITED(AsyncScriptCompiler, Runnable)
nsresult
AsyncScriptCompiler::Start(nsIPrincipal* aPrincipal)
{
nsCOMPtr<nsIURI> uri;
nsresult rv = NS_NewURI(getter_AddRefs(uri), mURL);
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<nsIChannel> channel;
rv = NS_NewChannel(getter_AddRefs(channel),
uri, aPrincipal,
nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
nsIContentPolicy::TYPE_OTHER);
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<nsIIncrementalStreamLoader> loader;
rv = NS_NewIncrementalStreamLoader(getter_AddRefs(loader), this);
NS_ENSURE_SUCCESS(rv, rv);
return channel->AsyncOpen2(loader);
}
static void
OffThreadScriptLoaderCallback(void* aToken, void* aCallbackData)
{
RefPtr<AsyncScriptCompiler> scriptCompiler = dont_AddRef(
static_cast<AsyncScriptCompiler*>(aCallbackData));
scriptCompiler->SetToken(aToken);
SystemGroup::Dispatch("ScriptLoader::FinishCompile",
TaskCategory::Other,
scriptCompiler.forget());
}
bool
AsyncScriptCompiler::StartCompile(JSContext* aCx)
{
Rooted<JSObject*> global(aCx, mGlobalObject->GetGlobalJSObject());
if (JS::CanCompileOffThread(aCx, mOptions, mScriptLength)) {
if (!JS::CompileOffThread(aCx, mOptions, mScriptText.get(), mScriptLength,
OffThreadScriptLoaderCallback,
static_cast<void*>(this))) {
return false;
}
NS_ADDREF(this);
return true;
}
Rooted<JSScript*> script(aCx);
if (!JS::Compile(aCx, mOptions, mScriptText.get(), mScriptLength, &script)) {
return false;
}
Finish(aCx, script);
return true;
}
NS_IMETHODIMP
AsyncScriptCompiler::Run()
{
AutoJSAPI jsapi;
if (jsapi.Init(mGlobalObject)) {
FinishCompile(jsapi.cx());
} else {
jsapi.Init();
JS::CancelOffThreadScript(jsapi.cx(), mToken);
mPromise->MaybeReject(NS_ERROR_FAILURE);
}
return NS_OK;
}
void
AsyncScriptCompiler::FinishCompile(JSContext* aCx)
{
Rooted<JSScript*> script(aCx, JS::FinishOffThreadScript(aCx, mToken));
Finish(aCx, script);
}
void
AsyncScriptCompiler::Finish(JSContext* aCx, Handle<JSScript*> aScript)
{
RefPtr<PrecompiledScript> result = new PrecompiledScript(mGlobalObject, aScript, mOptions);
mPromise->MaybeResolve(result);
}
void
AsyncScriptCompiler::Reject(JSContext* aCx)
{
RootedValue value(aCx, JS::UndefinedValue());
if (JS_GetPendingException(aCx, &value)) {
JS_ClearPendingException(aCx);
}
mPromise->MaybeReject(aCx, value);
}
void
AsyncScriptCompiler::Reject(JSContext* aCx, const char* aMsg)
{
nsAutoCString msg(aMsg);
msg.Append(": ");
msg.Append(mURL);
RootedValue exn(aCx, StringValue(JS_NewStringCopyZ(aCx, msg.get())));
JS_SetPendingException(aCx, exn);
Reject(aCx);
}
NS_IMETHODIMP
AsyncScriptCompiler::OnIncrementalData(nsIIncrementalStreamLoader* aLoader,
nsISupports* aContext,
uint32_t aDataLength,
const uint8_t* aData,
uint32_t *aConsumedData)
{
return NS_OK;
}
NS_IMETHODIMP
AsyncScriptCompiler::OnStreamComplete(nsIIncrementalStreamLoader* aLoader,
nsISupports* aContext,
nsresult aStatus,
uint32_t aLength,
const uint8_t* aBuf)
{
AutoJSAPI jsapi;
if (!jsapi.Init(mGlobalObject)) {
mPromise->MaybeReject(NS_ERROR_FAILURE);
return NS_OK;
}
JSContext* cx = jsapi.cx();
if (NS_FAILED(aStatus)) {
Reject(cx, "Unable to load script");
return NS_OK;
}
nsresult rv = nsScriptLoader::ConvertToUTF16(
nullptr, aBuf, aLength, mCharset, nullptr, mScriptText, mScriptLength);
if (NS_FAILED(rv)) {
Reject(cx, "Unable to decode script");
return NS_OK;
}
if (!StartCompile(cx)) {
Reject(cx);
}
return NS_OK;
}
namespace mozilla {
namespace dom {
/* static */ already_AddRefed<Promise>
ChromeUtils::CompileScript(GlobalObject& aGlobal,
const nsAString& aURL,
const CompileScriptOptionsDictionary& aOptions,
ErrorResult& aRv)
{
nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
MOZ_ASSERT(global);
RefPtr<Promise> promise = Promise::Create(global, aRv);
if (aRv.Failed()) {
return nullptr;
}
NS_ConvertUTF16toUTF8 url(aURL);
RefPtr<AsyncScriptCompiler> compiler = new AsyncScriptCompiler(aGlobal.Context(), global, url, aOptions, promise);
nsresult rv = compiler->Start(aGlobal.GetSubjectPrincipal());
if (NS_FAILED(rv)) {
promise->MaybeReject(rv);
}
return promise.forget();
}
PrecompiledScript::PrecompiledScript(nsISupports* aParent, Handle<JSScript*> aScript,
JS::ReadOnlyCompileOptions& aOptions)
: mParent(aParent)
, mScript(aScript)
, mURL(aOptions.filename())
, mHasReturnValue(!aOptions.noScriptRval)
{
MOZ_ASSERT(aParent);
MOZ_ASSERT(aScript);
mozilla::HoldJSObjects(this);
};
PrecompiledScript::~PrecompiledScript()
{
mozilla::DropJSObjects(this);
}
void
PrecompiledScript::ExecuteInGlobal(JSContext* aCx, HandleObject aGlobal,
MutableHandleValue aRval,
ErrorResult& aRv)
{
{
RootedObject targetObj(aCx, JS_FindCompilationScope(aCx, aGlobal));
JSAutoCompartment ac(aCx, targetObj);
Rooted<JSScript*> script(aCx, mScript);
if (!JS::CloneAndExecuteScript(aCx, script, aRval)) {
aRv.NoteJSContextException(aCx);
}
}
JS_WrapValue(aCx, aRval);
}
void
PrecompiledScript::GetUrl(nsAString& aUrl)
{
CopyUTF8toUTF16(mURL, aUrl);
}
bool
PrecompiledScript::HasReturnValue()
{
return mHasReturnValue;
}
JSObject*
PrecompiledScript::WrapObject(JSContext* aCx, HandleObject aGivenProto)
{
return PrecompiledScriptBinding::Wrap(aCx, this, aGivenProto);
}
NS_IMPL_CYCLE_COLLECTION_CLASS(PrecompiledScript)
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PrecompiledScript)
NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
NS_INTERFACE_MAP_ENTRY(nsISupports)
NS_INTERFACE_MAP_END
NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(PrecompiledScript)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mParent)
NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
tmp->mScript = nullptr;
NS_IMPL_CYCLE_COLLECTION_UNLINK_END
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(PrecompiledScript)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mParent)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(PrecompiledScript)
NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mScript)
NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER
NS_IMPL_CYCLE_COLLECTION_TRACE_END
NS_IMPL_CYCLE_COLLECTING_ADDREF(PrecompiledScript)
NS_IMPL_CYCLE_COLLECTING_RELEASE(PrecompiledScript)
} // namespace dom
} // namespace mozilla

View File

@ -0,0 +1,56 @@
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#ifndef mozilla_dom_PrecompiledScript_h
#define mozilla_dom_PrecompiledScript_h
#include "mozilla/dom/BindingDeclarations.h"
#include "mozilla/dom/PrecompiledScriptBinding.h"
#include "jsapi.h"
#include "nsCOMPtr.h"
#include "nsCycleCollectionParticipant.h"
#include "nsISupports.h"
#include "nsWrapperCache.h"
namespace mozilla {
namespace dom {
class PrecompiledScript : public nsISupports
, public nsWrapperCache
{
NS_DECL_CYCLE_COLLECTING_ISUPPORTS
NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(PrecompiledScript)
explicit PrecompiledScript(nsISupports* aParent, JS::Handle<JSScript*> aScript, JS::ReadOnlyCompileOptions& aOptions);
void ExecuteInGlobal(JSContext* aCx, JS::HandleObject aGlobal,
JS::MutableHandleValue aRval,
ErrorResult& aRv);
void GetUrl(nsAString& aUrl);
bool HasReturnValue();
nsISupports* GetParentObject() const { return mParent; }
virtual JSObject* WrapObject(JSContext* aCx,
JS::Handle<JSObject*> aGivenProto) override;
protected:
virtual ~PrecompiledScript();
private:
nsCOMPtr<nsISupports> mParent;
JS::Heap<JSScript*> mScript;
nsCString mURL;
const bool mHasReturnValue;
};
} // namespace dom
} // namespace mozilla
#endif // mozilla_dom_PrecompiledScript_h

View File

@ -6,11 +6,16 @@
# These files cannot be built in unified mode because they rely on plarena.h
SOURCES += [
'ChromeScriptLoader.cpp',
'mozJSComponentLoader.cpp',
'mozJSLoaderUtils.cpp',
'mozJSSubScriptLoader.cpp',
]
EXPORTS.mozilla.dom += [
'PrecompiledScript.h',
]
EXTRA_JS_MODULES += [
'ISO8601DateUtils.jsm',
'XPCOMUtils.jsm',