commit fb321bdf9c63c3b21e158571d2f0349132155212
parent 9f310f512f1c0298b0464ed4bb10cfa06e472c05
Author: Tooru Fujisawa <arai_a@mac.com>
Date: Wed, 17 Dec 2025 18:38:05 +0000
Bug 2004064 - Yield to other tasks before processing long defer scripts if no FCP have happened. r=nbp
Differential Revision: https://phabricator.services.mozilla.com/D275741
Diffstat:
6 files changed, 77 insertions(+), 0 deletions(-)
diff --git a/dom/script/ScriptLoader.cpp b/dom/script/ScriptLoader.cpp
@@ -49,6 +49,7 @@
#include "mozilla/Utf8.h" // mozilla::Utf8Unit
#include "mozilla/dom/AutoEntryScript.h"
#include "mozilla/dom/DocGroup.h"
+#include "mozilla/dom/DocumentInlines.h" // Document::GetPresContext
#include "mozilla/dom/Element.h"
#include "mozilla/dom/FetchPriority.h"
#include "mozilla/dom/JSExecutionUtils.h" // mozilla::dom::Compile, mozilla::dom::InstantiateStencil, mozilla::dom::EvaluationExceptionToNSResult
@@ -93,6 +94,7 @@
#include "nsJSPrincipals.h"
#include "nsJSUtils.h"
#include "nsNetUtil.h"
+#include "nsPresContext.h" // nsPresContext
#include "nsProxyRelease.h"
#include "nsQueryObject.h"
#include "nsThreadUtils.h"
@@ -207,6 +209,7 @@ ScriptLoader::ScriptLoader(Document* aDocument)
mLoadEventFired(false),
mGiveUpDiskCaching(false),
mContinueParsingDocumentAfterCurrentScript(false),
+ mHadFCPDoNotUseDirectly(false),
mReporter(new ConsoleReportCollector()) {
LOG(("ScriptLoader::ScriptLoader %p", this));
@@ -3514,7 +3517,18 @@ nsresult ScriptLoader::EvaluateScript(nsIGlobalObject* aGlobalObject,
MOZ_ASSERT(options.noScriptRval);
TRACE_FOR_TEST(aRequest, "evaluate:classic");
+
+ auto start = TimeStamp::Now();
+
ExecuteCompiledScript(cx, classicScript, script, erv);
+
+ auto end = TimeStamp::Now();
+ auto duration = (end - start).ToMilliseconds();
+
+ static constexpr double LongScriptThresholdInMilliseconds = 1.0;
+ if (duration > LongScriptThresholdInMilliseconds) {
+ aRequest->SetTookLongInPreviousRuns();
+ }
}
rv = EvaluationExceptionToNSResult(erv);
@@ -3907,6 +3921,13 @@ void ScriptLoader::ProcessPendingRequests(bool aAllowBypassingParserBlocking) {
if (mDeferCheckpointReached && mXSLTRequests.isEmpty()) {
while (ReadyToExecuteScripts() && !mDeferRequests.isEmpty() &&
mDeferRequests.getFirst()->IsFinished()) {
+ if (mDeferRequests.getFirst()->TookLongInPreviousRuns() &&
+ !mDeferRequests.getFirst()->HadPostponed() && IsBeforeFCP()) {
+ mDeferRequests.getFirst()->SetHadPostponed();
+ ProcessPendingRequestsAsync();
+ return;
+ }
+
request = mDeferRequests.StealFirst();
ProcessRequest(request);
}
@@ -3938,6 +3959,32 @@ void ScriptLoader::ProcessPendingRequests(bool aAllowBypassingParserBlocking) {
}
}
+bool ScriptLoader::IsBeforeFCP() {
+ if (mHadFCPDoNotUseDirectly) {
+ return false;
+ }
+
+ if (mLoadEventFired) {
+ return false;
+ }
+
+ if (!mDocument) {
+ return false;
+ }
+
+ nsPresContext* context = mDocument->GetPresContext();
+ if (!context) {
+ return false;
+ }
+
+ if (context->HadFirstContentfulPaint()) {
+ mHadFCPDoNotUseDirectly = true;
+ return false;
+ }
+
+ return true;
+}
+
bool ScriptLoader::ReadyToExecuteParserBlockingScripts() {
// Make sure the SelfReadyToExecuteParserBlockingScripts check is first, so
// that we don't block twice on an ancestor.
diff --git a/dom/script/ScriptLoader.h b/dom/script/ScriptLoader.h
@@ -809,6 +809,8 @@ class ScriptLoader final : public JS::loader::ScriptLoaderInterface {
void MaybeMoveToLoadedList(ScriptLoadRequest* aRequest);
+ bool IsBeforeFCP();
+
public:
struct DiskCacheStrategy {
bool mIsDisabled = false;
@@ -924,6 +926,7 @@ class ScriptLoader final : public JS::loader::ScriptLoaderInterface {
bool mLoadEventFired;
bool mGiveUpDiskCaching;
bool mContinueParsingDocumentAfterCurrentScript;
+ bool mHadFCPDoNotUseDirectly;
TimeDuration mMainThreadParseTime;
diff --git a/js/loader/LoadedScript.cpp b/js/loader/LoadedScript.cpp
@@ -56,6 +56,7 @@ LoadedScript::LoadedScript(ScriptKind aKind,
mSerializedStencilOffset(0),
mCacheEntryId(InvalidCacheEntryId),
mIsDirty(false),
+ mTookLongInPreviousRuns(false),
mFetchOptions(aFetchOptions),
mURI(aURI),
mReceivedScriptTextLength(0) {
@@ -70,6 +71,7 @@ LoadedScript::LoadedScript(const LoadedScript& aOther)
mSerializedStencilOffset(0),
mCacheEntryId(aOther.mCacheEntryId),
mIsDirty(aOther.mIsDirty),
+ mTookLongInPreviousRuns(aOther.mTookLongInPreviousRuns),
mFetchOptions(aOther.mFetchOptions),
mURI(aOther.mURI),
mBaseURL(aOther.mBaseURL),
diff --git a/js/loader/LoadedScript.h b/js/loader/LoadedScript.h
@@ -315,6 +315,9 @@ class LoadedScript : public nsIMemoryReporter {
// ==== Other methods ====
+ void SetTookLongInPreviousRuns() { mTookLongInPreviousRuns = true; }
+ bool TookLongInPreviousRuns() const { return mTookLongInPreviousRuns; }
+
/*
* Set the mBaseURL, based on aChannel.
* aOriginalURI is the result of aChannel->GetOriginalURI.
@@ -405,6 +408,14 @@ class LoadedScript : public nsIMemoryReporter {
// this must be uint64_t.
uint64_t mIsDirty : 1;
+ // Set to true if executing the top-level script takes long.
+ // This can be used for scheduling the script execution in subsequent loads.
+ // The threshold of "takes long" is user-defined.
+ // See dom::ScriptLoader::EvaluateScript for the example case
+ //
+ // TODO: Move this into JS::Stencil, and save to the disk cache (bug 2005128)
+ uint64_t mTookLongInPreviousRuns : 1;
+
RefPtr<ScriptFetchOptions> mFetchOptions;
nsCOMPtr<nsIURI> mURI;
@@ -562,6 +573,13 @@ class LoadedScriptDelegate {
GetLoadedScript()->SetStencil(aStencil);
}
void ClearStencil() { GetLoadedScript()->ClearStencil(); }
+
+ void SetTookLongInPreviousRuns() {
+ GetLoadedScript()->SetTookLongInPreviousRuns();
+ }
+ bool TookLongInPreviousRuns() const {
+ return GetLoadedScript()->TookLongInPreviousRuns();
+ }
};
class ClassicScript final : public LoadedScript {
diff --git a/js/loader/ScriptLoadRequest.cpp b/js/loader/ScriptLoadRequest.cpp
@@ -94,6 +94,7 @@ ScriptLoadRequest::ScriptLoadRequest(ScriptKind aKind,
mFetchSourceOnly(false),
mHasSourceMapURL_(false),
mHasDirtyCache_(false),
+ mHadPostponed_(false),
mDiskCachingPlan(CachingPlan::Uninitialized),
mMemoryCachingPlan(CachingPlan::Uninitialized),
mIntegrity(aIntegrity),
diff --git a/js/loader/ScriptLoadRequest.h b/js/loader/ScriptLoadRequest.h
@@ -268,6 +268,9 @@ class ScriptLoadRequest : public nsISupports,
bool HasDirtyCache() const { return mHasDirtyCache_; }
void SetHasDirtyCache() { mHasDirtyCache_ = true; }
+ bool HadPostponed() const { return mHadPostponed_; }
+ void SetHadPostponed() { mHadPostponed_ = true; }
+
public:
// Fields.
@@ -293,6 +296,9 @@ class ScriptLoadRequest : public nsISupports,
// the cache should be either revived or evicted.
bool mHasDirtyCache_ : 1;
+ // Set to true if this script had already been postponed in the scheduling.
+ bool mHadPostponed_ : 1;
+
enum class CachingPlan : uint8_t {
// This is not yet considered for caching.
Uninitialized,