Bug 1809861 - Part 1: Simplify and clean up off-main thread script compilation r=smaug

The main change in this patch is to prevent access to main-thread objects
while off-thread. This is done by using nsMainThreadPtrHandle to wrap main
thread pointers in the runnable. This prevents access to their targets when
off thread and ensure they are only released on the main thread.

This means that mRunnable is now only accessed on the main thread and so it
doesn't need to be atomic and can be a normal RefPtr. We also don't need to
leak a reference to it in AttemptOffThreadScriptCompile.

This also requires that timing data is moved to the runnable.

Cancellation should always have happened by unlink or destruction of
ScriptLoadContext so handling for that is removed.

Differential Revision: https://phabricator.services.mozilla.com/D166667
This commit is contained in:
Jon Coppeard 2023-01-17 14:37:06 +00:00
Родитель 00daa17deb
Коммит 62c190d44c
4 изменённых файлов: 150 добавлений и 177 удалений

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

@ -37,9 +37,8 @@ NS_IMPL_CYCLE_COLLECTION_CLASS(ScriptLoadContext)
NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(ScriptLoadContext,
JS::loader::LoadContextBase)
if (Runnable* runnable = tmp->mRunnable.exchange(nullptr)) {
runnable->Release();
}
MOZ_ASSERT(!tmp->mOffThreadToken);
MOZ_ASSERT(!tmp->mRunnable);
tmp->MaybeUnblockOnload();
NS_IMPL_CYCLE_COLLECTION_UNLINK_END
@ -70,16 +69,12 @@ ScriptLoadContext::ScriptLoadContext()
mUnreportedPreloadError(NS_OK) {}
ScriptLoadContext::~ScriptLoadContext() {
// When speculative parsing is enabled, it is possible to off-main-thread
// compile scripts that are never executed. These should be cleaned up here
// if they exist.
mRequest = nullptr;
MOZ_ASSERT_IF(
!StaticPrefs::
dom_script_loader_external_scripts_speculative_omt_parse_enabled(),
!mOffThreadToken);
MOZ_ASSERT(NS_IsMainThread());
MaybeCancelOffThreadScript();
// Off-thread parsing must have completed by this point.
MOZ_DIAGNOSTIC_ASSERT(!mOffThreadToken && !mRunnable);
mRequest = nullptr;
MaybeUnblockOnload();
}
@ -104,17 +99,18 @@ void ScriptLoadContext::MaybeCancelOffThreadScript() {
return;
}
// Cancel parse if it hasn't been started yet or wait for it to finish and
// clean up finished parse data.
JSContext* cx = danger::GetJSContext();
JS::CancelOffThreadToken(cx, mOffThreadToken);
mOffThreadToken = nullptr;
// Cancellation request above should guarantee removal of the parse task, so
// releasing the runnable should be safe to do here.
if (Runnable* runnable = mRunnable.exchange(nullptr)) {
runnable->Release();
}
// Clear the pointer to the runnable. It may still run later if we didn't
// cancel in time. In this case the runnable is held live by the reference
// passed to Dispatch, which is dropped after it runs.
mRunnable = nullptr;
MaybeUnblockOnload();
mOffThreadToken = nullptr;
}
void ScriptLoadContext::SetScriptMode(bool aDeferAttr, bool aAsyncAttr,

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

@ -153,9 +153,6 @@ class ScriptLoadContext : public JS::loader::LoadContextBase,
void MaybeCancelOffThreadScript();
TimeStamp mOffThreadParseStartTime;
TimeStamp mOffThreadParseStopTime;
ScriptMode mScriptMode; // Whether this is a blocking, defer or async script.
bool mScriptFromHead; // Synchronous head script block loading of other non
// js/css content.
@ -172,11 +169,13 @@ class ScriptLoadContext : public JS::loader::LoadContextBase,
bool mWasCompiledOMT; // True if the script has been compiled off main
// thread.
JS::OffThreadToken* mOffThreadToken; // Off-thread parsing token.
// Off-thread parsing token. Set at the start of off-thread parsing and
// cleared when the result of the parse is used.
JS::OffThreadToken* mOffThreadToken;
Atomic<Runnable*> mRunnable; // Runnable created when dispatching off thread
// compile. Tracked here so that it can be
// properly released during cancellation.
// Runnable that is dispatched to the main thread when off-thread compilation
// completes.
RefPtr<Runnable> mRunnable;
int32_t mLineNo;

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

@ -1387,19 +1387,19 @@ ReferrerPolicy ScriptLoader::GetReferrerPolicy(nsIScriptElement* aElement) {
namespace {
class NotifyOffThreadScriptLoadCompletedRunnable : public Runnable {
RefPtr<ScriptLoadRequest> mRequest;
RefPtr<ScriptLoader> mLoader;
nsMainThreadPtrHandle<ScriptLoadRequest> mRequest;
nsMainThreadPtrHandle<ScriptLoader> mLoader;
nsCOMPtr<nsISerialEventTarget> mEventTarget;
JS::OffThreadToken* mToken;
TimeStamp mStartTime;
TimeStamp mStopTime;
public:
ScriptLoadRequest* GetScriptLoadRequest() { return mRequest; }
NotifyOffThreadScriptLoadCompletedRunnable(ScriptLoadRequest* aRequest,
ScriptLoader* aLoader)
: Runnable("dom::NotifyOffThreadScriptLoadCompletedRunnable"),
mRequest(aRequest),
mLoader(aLoader),
mRequest(new nsMainThreadPtrHolder("mRequest", aRequest)),
mLoader(new nsMainThreadPtrHolder("mLoader", aLoader)),
mToken(nullptr) {
MOZ_ASSERT(NS_IsMainThread());
if (DocGroup* docGroup = aLoader->GetDocGroup()) {
@ -1407,7 +1407,8 @@ class NotifyOffThreadScriptLoadCompletedRunnable : public Runnable {
}
}
virtual ~NotifyOffThreadScriptLoadCompletedRunnable();
void RecordStartTime() { mStartTime = TimeStamp::Now(); }
void RecordStopTime() { mStopTime = TimeStamp::Now(); }
void SetToken(JS::OffThreadToken* aToken) {
MOZ_ASSERT(aToken && !mToken);
@ -1466,7 +1467,7 @@ void ScriptLoader::CancelScriptLoadRequests() {
}
nsresult ScriptLoader::ProcessOffThreadRequest(ScriptLoadRequest* aRequest) {
MOZ_ASSERT(aRequest->mState == ScriptLoadRequest::State::Compiling);
MOZ_ASSERT(aRequest->IsCompiling());
MOZ_ASSERT(!aRequest->GetScriptLoadContext()->mWasCompiledOMT);
if (aRequest->IsCanceled()) {
@ -1526,81 +1527,56 @@ nsresult ScriptLoader::ProcessOffThreadRequest(ScriptLoadRequest* aRequest) {
return NS_OK;
}
NotifyOffThreadScriptLoadCompletedRunnable::
~NotifyOffThreadScriptLoadCompletedRunnable() {
if (MOZ_UNLIKELY(mRequest || mLoader) && !NS_IsMainThread()) {
NS_ReleaseOnMainThread(
"NotifyOffThreadScriptLoadCompletedRunnable::mRequest",
mRequest.forget());
NS_ReleaseOnMainThread(
"NotifyOffThreadScriptLoadCompletedRunnable::mLoader",
mLoader.forget());
}
}
NS_IMETHODIMP
NotifyOffThreadScriptLoadCompletedRunnable::Run() {
MOZ_ASSERT(NS_IsMainThread());
// We want these to be dropped on the main thread, once we return from this
// function.
RefPtr<ScriptLoadRequest> request = std::move(mRequest);
RefPtr<ScriptLoadContext> context = mRequest->GetScriptLoadContext();
MOZ_ASSERT_IF(context->mRunnable, context->mRunnable == this);
MOZ_ASSERT_IF(context->mOffThreadToken, context->mOffThreadToken == mToken);
// Runnable pointer should have been cleared in the offthread callback.
MOZ_ASSERT(!request->GetScriptLoadContext()->mRunnable);
// Clear the pointer to the runnable. The final reference will be released
// when this method returns.
context->mRunnable = nullptr;
if (!context->mOffThreadToken) {
// Request has been cancelled by MaybeCancelOffThreadScript.
return NS_OK;
}
if (profiler_is_active()) {
ProfilerString8View scriptSourceString;
if (request->IsTextSource()) {
if (mRequest->IsTextSource()) {
scriptSourceString = "ScriptCompileOffThread";
} else {
MOZ_ASSERT(request->IsBytecode());
MOZ_ASSERT(mRequest->IsBytecode());
scriptSourceString = "BytecodeDecodeOffThread";
}
nsAutoCString profilerLabelString;
request->GetScriptLoadContext()->GetProfilerLabel(profilerLabelString);
PROFILER_MARKER_TEXT(
scriptSourceString, JS,
MarkerTiming::Interval(
request->GetScriptLoadContext()->mOffThreadParseStartTime,
request->GetScriptLoadContext()->mOffThreadParseStopTime),
profilerLabelString);
mRequest->GetScriptLoadContext()->GetProfilerLabel(profilerLabelString);
PROFILER_MARKER_TEXT(scriptSourceString, JS,
MarkerTiming::Interval(mStartTime, mStopTime),
profilerLabelString);
}
RefPtr<ScriptLoader> loader = std::move(mLoader);
nsresult rv = mLoader->ProcessOffThreadRequest(mRequest);
// Request was already cancelled at some earlier point.
if (!request->GetScriptLoadContext()->mOffThreadToken) {
return NS_OK;
}
return loader->ProcessOffThreadRequest(request);
mRequest = nullptr;
mLoader = nullptr;
return rv;
}
static void OffThreadScriptLoaderCallback(JS::OffThreadToken* aToken,
void* aCallbackData) {
RefPtr<NotifyOffThreadScriptLoadCompletedRunnable> aRunnable = dont_AddRef(
static_cast<NotifyOffThreadScriptLoadCompletedRunnable*>(aCallbackData));
MOZ_ASSERT(
aRunnable.get() ==
aRunnable->GetScriptLoadRequest()->GetScriptLoadContext()->mRunnable);
aRunnable->GetScriptLoadRequest()
->GetScriptLoadContext()
->mOffThreadParseStopTime = TimeStamp::Now();
RefPtr<NotifyOffThreadScriptLoadCompletedRunnable> aRunnable =
static_cast<NotifyOffThreadScriptLoadCompletedRunnable*>(aCallbackData);
LogRunnable::Run run(aRunnable);
aRunnable->RecordStopTime();
aRunnable->SetToken(aToken);
// If mRunnable was cleared then request was canceled so do nothing.
if (!aRunnable->GetScriptLoadRequest()
->GetScriptLoadContext()
->mRunnable.exchange(nullptr)) {
return;
}
NotifyOffThreadScriptLoadCompletedRunnable::Dispatch(aRunnable.forget());
}
@ -1660,108 +1636,25 @@ nsresult ScriptLoader::AttemptAsyncScriptCompile(ScriptLoadRequest* aRequest,
RefPtr<NotifyOffThreadScriptLoadCompletedRunnable> runnable =
new NotifyOffThreadScriptLoadCompletedRunnable(aRequest, this);
// Emulate dispatch. CompileOffThreadModule will call
// Emulate dispatch. CompileOffThreadModule will call
// OffThreadScriptLoaderCallback were we will emulate run.
LogRunnable::LogDispatch(runnable);
aRequest->GetScriptLoadContext()->mOffThreadParseStartTime = TimeStamp::Now();
runnable->RecordStartTime();
// Save the runnable so it can be properly cleared during cancellation.
aRequest->GetScriptLoadContext()->mRunnable = runnable.get();
auto signalOOM = mozilla::MakeScopeExit(
[&aRequest]() { aRequest->GetScriptLoadContext()->mRunnable = nullptr; });
JS::OffThreadToken* token = nullptr;
rv = StartOffThreadCompilation(cx, aRequest, options, runnable, &token);
NS_ENSURE_SUCCESS(rv, rv);
MOZ_ASSERT(token);
// The conditions should match ScriptLoadContext::MaybeCancelOffThreadScript.
if (aRequest->IsBytecode()) {
JS::DecodeOptions decodeOptions(options);
aRequest->GetScriptLoadContext()->mOffThreadToken =
JS::DecodeStencilOffThread(cx, decodeOptions, aRequest->mScriptBytecode,
aRequest->mBytecodeOffset,
OffThreadScriptLoaderCallback,
static_cast<void*>(runnable));
if (!aRequest->GetScriptLoadContext()->mOffThreadToken) {
return NS_ERROR_OUT_OF_MEMORY;
}
} else if (aRequest->IsModuleRequest()) {
MOZ_ASSERT(aRequest->IsTextSource());
MaybeSourceText maybeSource;
nsresult rv = aRequest->GetScriptSource(cx, &maybeSource);
NS_ENSURE_SUCCESS(rv, rv);
auto compile = [&](auto& source) {
return JS::CompileModuleToStencilOffThread(
cx, options, source, OffThreadScriptLoaderCallback, runnable.get());
};
MOZ_ASSERT(!maybeSource.empty());
JS::OffThreadToken* token = maybeSource.mapNonEmpty(compile);
if (!token) {
return NS_ERROR_OUT_OF_MEMORY;
}
aRequest->GetScriptLoadContext()->mOffThreadToken = token;
} else {
MOZ_ASSERT(aRequest->IsTextSource());
if (ShouldApplyDelazifyStrategy(aRequest)) {
ApplyDelazifyStrategy(&options);
mTotalFullParseSize +=
aRequest->ScriptTextLength() > 0
? static_cast<uint32_t>(aRequest->ScriptTextLength())
: 0;
LOG(
("ScriptLoadRequest (%p): non-on-demand-only Parsing Enabled for "
"url=%s mTotalFullParseSize=%u",
aRequest, aRequest->mURI->GetSpecOrDefault().get(),
mTotalFullParseSize));
}
MaybeSourceText maybeSource;
nsresult rv = aRequest->GetScriptSource(cx, &maybeSource);
NS_ENSURE_SUCCESS(rv, rv);
if (StaticPrefs::dom_expose_test_interfaces()) {
switch (options.eagerDelazificationStrategy()) {
case JS::DelazificationOption::OnDemandOnly:
TRACE_FOR_TEST(aRequest->GetScriptLoadContext()->GetScriptElement(),
"delazification_on_demand_only");
break;
case JS::DelazificationOption::CheckConcurrentWithOnDemand:
case JS::DelazificationOption::ConcurrentDepthFirst:
TRACE_FOR_TEST(aRequest->GetScriptLoadContext()->GetScriptElement(),
"delazification_concurrent_depth_first");
break;
case JS::DelazificationOption::ConcurrentLargeFirst:
TRACE_FOR_TEST(aRequest->GetScriptLoadContext()->GetScriptElement(),
"delazification_concurrent_large_first");
break;
case JS::DelazificationOption::ParseEverythingEagerly:
TRACE_FOR_TEST(aRequest->GetScriptLoadContext()->GetScriptElement(),
"delazification_parse_everything_eagerly");
break;
}
}
auto compile = [&](auto& source) {
return JS::CompileToStencilOffThread(
cx, options, source, OffThreadScriptLoaderCallback, runnable.get());
};
MOZ_ASSERT(!maybeSource.empty());
JS::OffThreadToken* token = maybeSource.mapNonEmpty(compile);
if (!token) {
return NS_ERROR_OUT_OF_MEMORY;
}
aRequest->GetScriptLoadContext()->mOffThreadToken = token;
}
signalOOM.release();
aRequest->GetScriptLoadContext()->mOffThreadToken = token;
aRequest->GetScriptLoadContext()->mRunnable = runnable;
aRequest->GetScriptLoadContext()->BlockOnload(mDocument);
// Once the compilation is finished, an event would be added to the event loop
// to call ScriptLoader::ProcessOffThreadRequest with the same request.
// Once the compilation is finished, a callback will dispatch the runnable to
// the main thread to call ScriptLoader::ProcessOffThreadRequest for the
// request.
aRequest->mState = ScriptLoadRequest::State::Compiling;
// Requests that are not tracked elsewhere are added to a list while they are
@ -1776,10 +1669,88 @@ nsresult ScriptLoader::AttemptAsyncScriptCompile(ScriptLoadRequest* aRequest,
}
*aCouldCompileOut = true;
Unused << runnable.forget();
return NS_OK;
}
static inline nsresult CompileResultForToken(void* aToken) {
return aToken ? NS_OK : NS_ERROR_OUT_OF_MEMORY;
}
nsresult ScriptLoader::StartOffThreadCompilation(
JSContext* aCx, ScriptLoadRequest* aRequest, JS::CompileOptions& aOptions,
Runnable* aRunnable, JS::OffThreadToken** aTokenOut) {
const JS::OffThreadCompileCallback callback = OffThreadScriptLoaderCallback;
if (aRequest->IsBytecode()) {
JS::DecodeOptions decodeOptions(aOptions);
*aTokenOut = JS::DecodeStencilOffThread(
aCx, decodeOptions, aRequest->mScriptBytecode,
aRequest->mBytecodeOffset, callback, aRunnable);
return CompileResultForToken(*aTokenOut);
}
MaybeSourceText maybeSource;
nsresult rv = aRequest->GetScriptSource(aCx, &maybeSource);
NS_ENSURE_SUCCESS(rv, rv);
if (aRequest->IsModuleRequest()) {
auto compile = [&](auto& source) {
return JS::CompileModuleToStencilOffThread(aCx, aOptions, source,
callback, aRunnable);
};
MOZ_ASSERT(!maybeSource.empty());
*aTokenOut = maybeSource.mapNonEmpty(compile);
return CompileResultForToken(*aTokenOut);
}
if (ShouldApplyDelazifyStrategy(aRequest)) {
ApplyDelazifyStrategy(&aOptions);
mTotalFullParseSize +=
aRequest->ScriptTextLength() > 0
? static_cast<uint32_t>(aRequest->ScriptTextLength())
: 0;
LOG(
("ScriptLoadRequest (%p): non-on-demand-only Parsing Enabled for "
"url=%s mTotalFullParseSize=%u",
aRequest, aRequest->mURI->GetSpecOrDefault().get(),
mTotalFullParseSize));
}
if (StaticPrefs::dom_expose_test_interfaces()) {
switch (aOptions.eagerDelazificationStrategy()) {
case JS::DelazificationOption::OnDemandOnly:
TRACE_FOR_TEST(aRequest->GetScriptLoadContext()->GetScriptElement(),
"delazification_on_demand_only");
break;
case JS::DelazificationOption::CheckConcurrentWithOnDemand:
case JS::DelazificationOption::ConcurrentDepthFirst:
TRACE_FOR_TEST(aRequest->GetScriptLoadContext()->GetScriptElement(),
"delazification_concurrent_depth_first");
break;
case JS::DelazificationOption::ConcurrentLargeFirst:
TRACE_FOR_TEST(aRequest->GetScriptLoadContext()->GetScriptElement(),
"delazification_concurrent_large_first");
break;
case JS::DelazificationOption::ParseEverythingEagerly:
TRACE_FOR_TEST(aRequest->GetScriptLoadContext()->GetScriptElement(),
"delazification_parse_everything_eagerly");
break;
}
}
auto compile = [&](auto& source) {
return JS::CompileToStencilOffThread(aCx, aOptions, source, callback,
aRunnable);
};
MOZ_ASSERT(!maybeSource.empty());
*aTokenOut = maybeSource.mapNonEmpty(compile);
return CompileResultForToken(*aTokenOut);
}
nsresult ScriptLoader::CompileOffThreadOrProcessRequest(
ScriptLoadRequest* aRequest) {
NS_ASSERTION(nsContentUtils::IsSafeToRunScript(),

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

@ -578,6 +578,13 @@ class ScriptLoader final : public JS::loader::ScriptLoaderInterface {
nsresult AttemptAsyncScriptCompile(ScriptLoadRequest* aRequest,
bool* aCouldCompileOut);
nsresult StartOffThreadCompilation(JSContext* aCx,
ScriptLoadRequest* aRequest,
JS::CompileOptions& aOptions,
Runnable* aRunnable,
JS::OffThreadToken** aTokenOut);
nsresult ProcessRequest(ScriptLoadRequest* aRequest);
nsresult CompileOffThreadOrProcessRequest(ScriptLoadRequest* aRequest);
void FireScriptAvailable(nsresult aResult, ScriptLoadRequest* aRequest);