Bug 1364974: Part 3 - Support decoding multiple scripts in a single parse tasks. r?shu
The per-operation overhead for off-thread parse tasks makes them unsuitable
for use with small scripts, since the setup overhead outweighs the benefits of
off-thread decoding. Decoding multiple scripts in a single operation for the
same global means we don't need to pay that cost per-script, and can save a
significant amount of startup overhead by decoding more scripts off-thread.
MozReview-Commit-ID: 91SmvIfYvGs
--- a/js/src/jsapi.cpp
+++ b/js/src/jsapi.cpp
@@ -4218,16 +4218,31 @@ JS_PUBLIC_API(bool)
JS::DecodeOffThreadScript(JSContext* cx, const ReadOnlyCompileOptions& options,
const mozilla::Range<uint8_t>& range /* TranscodeRange& */,
OffThreadCompileCallback callback, void* callbackData)
{
MOZ_ASSERT(CanCompileOffThread(cx, options, range.length()));
return StartOffThreadDecodeScript(cx, options, range, callback, callbackData);
}
+JS_PUBLIC_API(bool)
+JS::DecodeOffThreadScripts(JSContext* cx, const ReadOnlyCompileOptions& options,
+ TranscodeSources& sources,
+ OffThreadCompileCallback callback, void* callbackData)
+{
+#ifdef DEBUG
+ size_t length = 0;
+ for (auto& source : sources) {
+ length += source.range.length();
+ }
+ MOZ_ASSERT(CanCompileOffThread(cx, options, length));
+#endif
+ return StartOffThreadDecodeScripts(cx, options, sources, callback, callbackData);
+}
+
JS_PUBLIC_API(JSScript*)
JS::FinishOffThreadScriptDecoder(JSContext* cx, void* token)
{
MOZ_ASSERT(cx);
MOZ_ASSERT(CurrentThreadCanAccessRuntime(cx->runtime()));
return HelperThreadState().finishScriptDecodeTask(cx, token);
}
@@ -4235,16 +4250,32 @@ JS_PUBLIC_API(void)
JS::CancelOffThreadScriptDecoder(JSContext* cx, void* token)
{
MOZ_ASSERT(cx);
MOZ_ASSERT(CurrentThreadCanAccessRuntime(cx->runtime()));
HelperThreadState().cancelParseTask(cx->runtime(), ParseTaskKind::ScriptDecode, token);
}
JS_PUBLIC_API(bool)
+JS::FinishOffThreadScriptsDecoder(JSContext* cx, void* token, MutableHandle<ScriptVector> scripts)
+{
+ MOZ_ASSERT(cx);
+ MOZ_ASSERT(CurrentThreadCanAccessRuntime(cx->runtime()));
+ return HelperThreadState().finishScriptsDecodeTask(cx, token, scripts);
+}
+
+JS_PUBLIC_API(void)
+JS::CancelOffThreadScriptsDecoder(JSContext* cx, void* token)
+{
+ MOZ_ASSERT(cx);
+ MOZ_ASSERT(CurrentThreadCanAccessRuntime(cx->runtime()));
+ HelperThreadState().cancelParseTask(cx->runtime(), ParseTaskKind::ScriptsDecode, token);
+}
+
+JS_PUBLIC_API(bool)
JS_CompileScript(JSContext* cx, const char* ascii, size_t length,
const JS::CompileOptions& options, MutableHandleScript script)
{
return Compile(cx, options, ascii, length, script);
}
JS_PUBLIC_API(bool)
JS_CompileUCScript(JSContext* cx, const char16_t* chars, size_t length,
--- a/js/src/jsapi.h
+++ b/js/src/jsapi.h
@@ -836,16 +836,18 @@ class MOZ_STACK_CLASS SourceBufferHolder
SourceBufferHolder(SourceBufferHolder&) = delete;
SourceBufferHolder& operator=(SourceBufferHolder&) = delete;
const char16_t* data_;
size_t length_;
bool ownsChars_;
};
+struct TranscodeSource;
+
} /* namespace JS */
/************************************************************************/
/* Property attributes, set in JSPropertySpec and passed to API functions.
*
* NB: The data structure in which some of these values are stored only uses
* a uint8_t to store the relevant information. Proceed with caution if
@@ -4284,16 +4286,27 @@ DecodeOffThreadScript(JSContext* cx, con
OffThreadCompileCallback callback, void* callbackData);
extern JS_PUBLIC_API(JSScript*)
FinishOffThreadScriptDecoder(JSContext* cx, void* token);
extern JS_PUBLIC_API(void)
CancelOffThreadScriptDecoder(JSContext* cx, void* token);
+extern JS_PUBLIC_API(bool)
+DecodeOffThreadScripts(JSContext* cx, const ReadOnlyCompileOptions& options,
+ mozilla::Vector<TranscodeSource>& sources,
+ OffThreadCompileCallback callback, void* callbackData);
+
+extern JS_PUBLIC_API(bool)
+FinishOffThreadScriptsDecoder(JSContext* cx, void* token, JS::MutableHandle<JS::ScriptVector> scripts);
+
+extern JS_PUBLIC_API(void)
+CancelOffThreadScriptsDecoder(JSContext* cx, void* token);
+
/**
* Compile a function with envChain plus the global as its scope chain.
* envChain must contain objects in the current compartment of cx. The actual
* scope chain used for the function will consist of With wrappers for those
* objects, followed by the current global of the compartment cx is in. This
* global must not be explicitly included in the scope chain.
*/
extern JS_PUBLIC_API(bool)
@@ -6170,16 +6183,29 @@ class MOZ_RAII AutoHideScriptedCaller
/*
* Encode/Decode interpreted scripts and functions to/from memory.
*/
typedef mozilla::Vector<uint8_t> TranscodeBuffer;
typedef mozilla::Range<uint8_t> TranscodeRange;
+struct TranscodeSource
+{
+ TranscodeSource(const TranscodeRange& range_, const char* file, unsigned line)
+ : range(range_), filename(file), lineno(line)
+ {}
+
+ const TranscodeRange range;
+ const char* filename;
+ const int lineno;
+};
+
+typedef mozilla::Vector<JS::TranscodeSource> TranscodeSources;
+
enum TranscodeResult
{
// Successful encoding / decoding.
TranscodeResult_Ok = 0,
// A warning message, is set to the message out-param.
TranscodeResult_Failure = 0x100,
TranscodeResult_Failure_BadBuildId = TranscodeResult_Failure | 0x1,
--- a/js/src/vm/HelperThreads.cpp
+++ b/js/src/vm/HelperThreads.cpp
@@ -318,16 +318,29 @@ ParseTask::ParseTask(ParseTaskKind kind,
parseGlobal(parseGlobal),
callback(callback), callbackData(callbackData),
overRecursed(false), outOfMemory(false)
{
Unused << scripts.reserve(scripts.capacity());
Unused << sourceObjects.reserve(sourceObjects.capacity());
}
+ParseTask::ParseTask(ParseTaskKind kind, JSContext* cx, JSObject* parseGlobal,
+ JS::TranscodeSources& sources,
+ JS::OffThreadCompileCallback callback, void* callbackData)
+ : kind(kind), options(cx), data(AsVariant(&sources)),
+ alloc(JSContext::TEMP_LIFO_ALLOC_PRIMARY_CHUNK_SIZE),
+ parseGlobal(parseGlobal),
+ callback(callback), callbackData(callbackData),
+ overRecursed(false), outOfMemory(false)
+{
+ Unused << scripts.reserve(scripts.capacity());
+ Unused << sourceObjects.reserve(sourceObjects.capacity());
+}
+
bool
ParseTask::init(JSContext* cx, const ReadOnlyCompileOptions& options)
{
if (!this->options.copy(cx, options))
return false;
return true;
}
@@ -443,16 +456,52 @@ ScriptDecodeTask::parse(JSContext* cx)
MOZ_ASSERT(bool(resultScript) == (decoder.resultCode() == JS::TranscodeResult_Ok));
if (decoder.resultCode() == JS::TranscodeResult_Ok) {
scripts.infallibleAppend(resultScript);
if (sourceObject)
sourceObjects.infallibleAppend(sourceObject);
}
}
+ScriptsDecodeTask::ScriptsDecodeTask(JSContext* cx, JSObject* parseGlobal,
+ JS::TranscodeSources& sources,
+ JS::OffThreadCompileCallback callback, void* callbackData)
+ : ParseTask(ParseTaskKind::ScriptsDecode, cx, parseGlobal,
+ sources, callback, callbackData)
+{
+}
+
+void
+ScriptsDecodeTask::parse(JSContext* cx)
+{
+ auto sources = data.as<JS::TranscodeSources*>();
+
+ if (!scripts.reserve(sources->length()) ||
+ !sourceObjects.reserve(sources->length()))
+ return;
+
+ for (auto& source : *sources) {
+ CompileOptions opts(cx, options);
+ opts.setFileAndLine(source.filename, source.lineno);
+
+ RootedScript resultScript(cx);
+ Rooted<ScriptSourceObject*> sourceObject(cx);
+
+ XDROffThreadDecoder decoder(cx, alloc, &opts, &sourceObject.get(), source.range);
+ decoder.codeScript(&resultScript);
+ MOZ_ASSERT(bool(resultScript) == (decoder.resultCode() == JS::TranscodeResult_Ok));
+
+ if (decoder.resultCode() != JS::TranscodeResult_Ok)
+ break;
+ MOZ_ASSERT(resultScript);
+ scripts.infallibleAppend(resultScript);
+ sourceObjects.infallibleAppend(sourceObject);
+ }
+}
+
void
js::CancelOffThreadParses(JSRuntime* rt)
{
AutoLockHelperThreadState lock;
if (!HelperThreadState().threads)
return;
@@ -674,16 +723,27 @@ js::StartOffThreadDecodeScript(JSContext
JS::OffThreadCompileCallback callback, void* callbackData)
{
auto functor = [&](JSObject* global) -> ScriptDecodeTask* {
return cx->new_<ScriptDecodeTask>(cx, global, range, callback, callbackData);
};
return StartOffThreadParseTask(cx, options, ParseTaskKind::ScriptDecode, functor);
}
+bool
+js::StartOffThreadDecodeScripts(JSContext* cx, const ReadOnlyCompileOptions& options,
+ JS::TranscodeSources& sources,
+ JS::OffThreadCompileCallback callback, void* callbackData)
+{
+ auto functor = [&](JSObject* global) -> ScriptsDecodeTask* {
+ return cx->new_<ScriptsDecodeTask>(cx, global, sources, callback, callbackData);
+ };
+ return StartOffThreadParseTask(cx, options, ParseTaskKind::ScriptsDecode, functor);
+}
+
void
js::EnqueuePendingParseTasksAfterGC(JSRuntime* rt)
{
MOZ_ASSERT(!OffThreadParsingMustWaitForGC(rt));
GlobalHelperThreadState::ParseTaskVector newTasks;
{
AutoLockHelperThreadState lock;
@@ -1308,89 +1368,147 @@ GlobalHelperThreadState::removeFinishedP
MOZ_ASSERT(parseTask->kind == kind);
return parseTask;
}
}
MOZ_CRASH("Invalid ParseTask token");
}
-JSScript*
-GlobalHelperThreadState::finishParseTask(JSContext* cx, ParseTaskKind kind, void* token)
+template <typename F, typename>
+bool
+GlobalHelperThreadState::finishParseTask(JSContext* cx, ParseTaskKind kind, void* token, F finish)
{
MOZ_ASSERT(cx->compartment());
ScopedJSDeletePtr<ParseTask> parseTask(removeFinishedParseTask(kind, token));
// Make sure we have all the constructors we need for the prototype
// remapping below, since we can't GC while that's happening.
Rooted<GlobalObject*> global(cx, &cx->global()->as<GlobalObject>());
if (!EnsureParserCreatedClasses(cx, kind)) {
LeaveParseTaskZone(cx->runtime(), parseTask);
- return nullptr;
+ return false;
}
mergeParseTaskCompartment(cx, parseTask, global, cx->compartment());
- MOZ_RELEASE_ASSERT(parseTask->scripts.length() <= 1);
-
- JS::RootedScript script(cx);
- if (parseTask->scripts.length() > 0)
- script = parseTask->scripts[0];
+ bool ok = finish(parseTask);
- for (auto& script : parseTask->scripts)
- releaseAssertSameCompartment(cx, script);
-
- if (!parseTask->finish(cx))
- return nullptr;
+ if (!parseTask->finish(cx) || !ok)
+ return false;
// Report out of memory errors eagerly, or errors could be malformed.
if (parseTask->outOfMemory) {
ReportOutOfMemory(cx);
- return nullptr;
+ return false;
}
// Report any error or warnings generated during the parse, and inform the
// debugger about the compiled scripts.
for (size_t i = 0; i < parseTask->errors.length(); i++)
parseTask->errors[i]->throwError(cx);
if (parseTask->overRecursed)
ReportOverRecursed(cx);
if (cx->isExceptionPending())
+ return false;
+
+ return true;
+}
+
+JSScript*
+GlobalHelperThreadState::finishParseTask(JSContext* cx, ParseTaskKind kind, void* token)
+{
+ JS::RootedScript script(cx);
+
+ bool ok = finishParseTask(cx, kind, token, [&] (ParseTask* parseTask) {
+ MOZ_RELEASE_ASSERT(parseTask->scripts.length() <= 1);
+
+ if (parseTask->scripts.length() > 0)
+ script = parseTask->scripts[0];
+
+ return true;
+ });
+
+ if (!ok)
return nullptr;
if (!script) {
// No error was reported, but no script produced. Assume we hit out of
// memory.
ReportOutOfMemory(cx);
return nullptr;
}
// The Debugger only needs to be told about the topmost script that was compiled.
Debugger::onNewScript(cx, script);
return script;
}
+bool
+GlobalHelperThreadState::finishParseTask(JSContext* cx, ParseTaskKind kind, void* token,
+ MutableHandle<ScriptVector> scripts)
+{
+ size_t expectedLength = 0;
+
+ bool ok = finishParseTask(cx, kind, token, [&] (ParseTask* parseTask) {
+ expectedLength = parseTask->data.as<JS::TranscodeSources*>()->length();
+
+ if (!scripts.reserve(parseTask->scripts.length()))
+ return false;
+
+ for (auto& script : parseTask->scripts)
+ scripts.infallibleAppend(script);
+ return true;
+ });
+
+ if (!ok)
+ return false;
+
+ if (scripts.length() != expectedLength) {
+ // No error was reported, but no script produced. Assume we hit out of
+ // memory.
+ ReportOutOfMemory(cx);
+ return false;
+ }
+
+ // The Debugger only needs to be told about the topmost script that was compiled.
+ for (auto& script : scripts) {
+ MOZ_ASSERT(script->isGlobalCode());
+
+ JS::RootedScript rooted(cx, script);
+ Debugger::onNewScript(cx, rooted);
+ }
+
+ return true;
+}
+
JSScript*
GlobalHelperThreadState::finishScriptParseTask(JSContext* cx, void* token)
{
JSScript* script = finishParseTask(cx, ParseTaskKind::Script, token);
MOZ_ASSERT_IF(script, script->isGlobalCode());
return script;
}
JSScript*
GlobalHelperThreadState::finishScriptDecodeTask(JSContext* cx, void* token)
{
JSScript* script = finishParseTask(cx, ParseTaskKind::ScriptDecode, token);
MOZ_ASSERT_IF(script, script->isGlobalCode());
return script;
}
+bool
+GlobalHelperThreadState::finishScriptsDecodeTask(JSContext* cx, void* token, MutableHandle<ScriptVector> scripts)
+{
+ return finishParseTask(cx, ParseTaskKind::ScriptsDecode, token, scripts);
+}
+
JSObject*
GlobalHelperThreadState::finishModuleParseTask(JSContext* cx, void* token)
{
JSScript* script = finishParseTask(cx, ParseTaskKind::Module, token);
if (!script)
return nullptr;
MOZ_ASSERT(script->module());
--- a/js/src/vm/HelperThreads.h
+++ b/js/src/vm/HelperThreads.h
@@ -12,16 +12,17 @@
#ifndef vm_HelperThreads_h
#define vm_HelperThreads_h
#include "mozilla/Attributes.h"
#include "mozilla/GuardObjects.h"
#include "mozilla/PodOperations.h"
#include "mozilla/TimeStamp.h"
+#include "mozilla/TypeTraits.h"
#include "mozilla/Variant.h"
#include "jsapi.h"
#include "jscntxt.h"
#include "jit/Ion.h"
#include "threading/ConditionVariable.h"
#include "vm/MutexIDs.h"
@@ -47,17 +48,18 @@ namespace wasm {
class CompileTask;
typedef Vector<CompileTask*, 0, SystemAllocPolicy> CompileTaskPtrVector;
} // namespace wasm
enum class ParseTaskKind
{
Script,
Module,
- ScriptDecode
+ ScriptDecode,
+ ScriptsDecode
};
// Per-process state for off thread work items.
class GlobalHelperThreadState
{
friend class AutoLockHelperThreadState;
friend class AutoUnlockHelperThreadState;
@@ -256,17 +258,28 @@ class GlobalHelperThreadState
void setWasmError(const AutoLockHelperThreadState&, UniqueChars error) {
if (!firstWasmError)
firstWasmError = Move(error);
}
bool wasmFailed(const AutoLockHelperThreadState&) {
return bool(numWasmFailedJobs);
}
+ template <
+ typename F,
+ typename = typename mozilla::EnableIf<
+ mozilla::IsSame<bool, decltype((*(F*)nullptr)((ParseTask*)nullptr))>::value
+ >::Type
+ >
+ bool finishParseTask(JSContext* cx, ParseTaskKind kind, void* token, F finish);
+
JSScript* finishParseTask(JSContext* cx, ParseTaskKind kind, void* token);
+
+ bool finishParseTask(JSContext* cx, ParseTaskKind kind, void* token, MutableHandle<ScriptVector> scripts);
+
void cancelParseTask(JSRuntime* rt, ParseTaskKind kind, void* token);
void mergeParseTaskCompartment(JSContext* cx, ParseTask* parseTask,
Handle<GlobalObject*> global,
JSCompartment* dest);
void trace(JSTracer* trc);
@@ -280,16 +293,17 @@ class GlobalHelperThreadState
* Error string from wasm validation. Arbitrarily choose to keep the first one that gets
* reported. Nondeterministic if multiple threads have errors.
*/
UniqueChars firstWasmError;
public:
JSScript* finishScriptParseTask(JSContext* cx, void* token);
JSScript* finishScriptDecodeTask(JSContext* cx, void* token);
+ bool finishScriptsDecodeTask(JSContext* cx, void* token, MutableHandle<ScriptVector> scripts);
JSObject* finishModuleParseTask(JSContext* cx, void* token);
bool hasActiveThreads(const AutoLockHelperThreadState&);
void waitForAllThreads();
template <typename T>
bool checkTaskThreadLimit(size_t maxThreads) const;
@@ -532,16 +546,21 @@ StartOffThreadParseModule(JSContext* cx,
const char16_t* chars, size_t length,
JS::OffThreadCompileCallback callback, void* callbackData);
bool
StartOffThreadDecodeScript(JSContext* cx, const ReadOnlyCompileOptions& options,
const JS::TranscodeRange& range,
JS::OffThreadCompileCallback callback, void* callbackData);
+bool
+StartOffThreadDecodeScripts(JSContext* cx, const ReadOnlyCompileOptions& options,
+ JS::TranscodeSources& sources,
+ JS::OffThreadCompileCallback callback, void* callbackData);
+
/*
* Called at the end of GC to enqueue any Parse tasks that were waiting on an
* atoms-zone GC to finish.
*/
void
EnqueuePendingParseTasksAfterGC(JSRuntime* rt);
struct AutoEnqueuePendingParseTasksAfterGC {
@@ -589,17 +608,19 @@ class MOZ_RAII AutoUnlockHelperThreadSta
}
};
struct ParseTask
{
ParseTaskKind kind;
OwningCompileOptions options;
- mozilla::Variant<const JS::TranscodeRange, JS::TwoByteChars> data;
+ mozilla::Variant<const JS::TranscodeRange,
+ JS::TwoByteChars,
+ JS::TranscodeSources*> data;
LifoAlloc alloc;
// Rooted pointer to the global object to use while parsing.
JSObject* parseGlobal;
// Callback invoked off thread when the parse finishes.
JS::OffThreadCompileCallback callback;
@@ -620,16 +641,19 @@ struct ParseTask
bool outOfMemory;
ParseTask(ParseTaskKind kind, JSContext* cx, JSObject* parseGlobal,
const char16_t* chars, size_t length,
JS::OffThreadCompileCallback callback, void* callbackData);
ParseTask(ParseTaskKind kind, JSContext* cx, JSObject* parseGlobal,
const JS::TranscodeRange& range,
JS::OffThreadCompileCallback callback, void* callbackData);
+ ParseTask(ParseTaskKind kind, JSContext* cx, JSObject* parseGlobal,
+ JS::TranscodeSources& sources,
+ JS::OffThreadCompileCallback callback, void* callbackData);
bool init(JSContext* cx, const ReadOnlyCompileOptions& options);
void activate(JSRuntime* rt);
virtual void parse(JSContext* cx) = 0;
bool finish(JSContext* cx);
bool runtimeMatches(JSRuntime* rt) {
return parseGlobal->runtimeFromAnyThread() == rt;
@@ -659,16 +683,24 @@ struct ModuleParseTask : public ParseTas
struct ScriptDecodeTask : public ParseTask
{
ScriptDecodeTask(JSContext* cx, JSObject* parseGlobal,
const JS::TranscodeRange& range,
JS::OffThreadCompileCallback callback, void* callbackData);
void parse(JSContext* cx) override;
};
+struct ScriptsDecodeTask : public ParseTask
+{
+ ScriptsDecodeTask(JSContext* cx, JSObject* parseGlobal,
+ JS::TranscodeSources& sources,
+ JS::OffThreadCompileCallback callback, void* callbackData);
+ void parse(JSContext* cx) override;
+};
+
// Return whether, if a new parse task was started, it would need to wait for
// an in-progress GC to complete before starting.
extern bool
OffThreadParsingMustWaitForGC(JSRuntime* rt);
// It is not desirable to eagerly compress: if lazy functions that are tied to
// the ScriptSource were to be executed relatively soon after parsing, they
// would need to block on decompression, which hurts responsiveness.