commit 17d6d5813a804ff2d5226d298cbab2e01eefc8ea
parent ea44e6826a9435364e3f0322fabfe189d466cbfc
Author: Jan de Mooij <jdemooij@mozilla.com>
Date: Fri, 19 Dec 2025 08:40:39 +0000
Bug 2004893 part 1 - Stop allocating SourceCompressionTasks eagerly for pending compressions. r=jonco
This patch adds `PendingSourceCompressionEntry` and stores a vector of these in
the runtime. This replaces the current process-wide `compressionPendingList`
and eliminates some locking overhead.
The `SourceCompressionTask`s are now allocated in `startHandlingCompressionTasks`.
This will let us batch multiple script sources in a single compression task in later
patches.
Differential Revision: https://phabricator.services.mozilla.com/D276484
Diffstat:
8 files changed, 122 insertions(+), 107 deletions(-)
diff --git a/js/src/gc/GC.cpp b/js/src/gc/GC.cpp
@@ -2895,7 +2895,7 @@ bool GCRuntime::beginPreparePhase(JS::GCReason reason, AutoGCSession& session) {
* GCReason::XPCONNECT_SHUTDOWN GCs we can remove the extra check.
*/
if (!isShutdownGC() && reason != JS::GCReason::XPCONNECT_SHUTDOWN) {
- StartHandlingCompressionsOnGC(rt);
+ StartOffThreadCompressionsOnGC(rt);
}
return true;
diff --git a/js/src/gc/Sweeping.cpp b/js/src/gc/Sweeping.cpp
@@ -1391,12 +1391,14 @@ void GCRuntime::sweepMisc() {
}
void GCRuntime::sweepCompressionTasks() {
- JSRuntime* runtime = rt;
+ // Discard pending compression entries for ScriptSources that have no
+ // other references.
+ rt->pendingCompressions().eraseIf(
+ [&](const auto& entry) { return entry.shouldCancel(); });
// Attach finished compression tasks.
AutoLockHelperThreadState lock;
- AttachFinishedCompressions(runtime, lock);
- SweepPendingCompressions(lock);
+ AttachFinishedCompressions(rt, lock);
}
void GCRuntime::sweepWeakMaps() {
diff --git a/js/src/vm/HelperThreadState.h b/js/src/vm/HelperThreadState.h
@@ -179,9 +179,6 @@ class GlobalHelperThreadState {
// risk.
FreeDelazifyTaskVector freeDelazifyTaskVector_;
- // Source compression worklist of tasks that we do not yet know can start.
- SourceCompressionTaskVector compressionPendingList_;
-
// Source compression worklist of tasks that can start.
SourceCompressionTaskVector compressionWorklist_;
@@ -352,11 +349,6 @@ class GlobalHelperThreadState {
return freeDelazifyTaskVector_;
}
- SourceCompressionTaskVector& compressionPendingList(
- const AutoLockHelperThreadState&) {
- return compressionPendingList_;
- }
-
SourceCompressionTaskVector& compressionWorklist(
const AutoLockHelperThreadState&) {
return compressionWorklist_;
@@ -436,14 +428,12 @@ class GlobalHelperThreadState {
bool canStartTasks(const AutoLockHelperThreadState& locked);
public:
- // Used by a major GC to signal processing enqueued compression tasks.
+ // Used by a major GC to create and enqueue compression tasks.
enum class ScheduleCompressionTask { GC, API };
- void startHandlingCompressionTasks(ScheduleCompressionTask schedule,
- JSRuntime* maybeRuntime,
- const AutoLockHelperThreadState& lock);
+ void createAndSubmitCompressionTasks(ScheduleCompressionTask schedule,
+ JSRuntime* rt);
- void runPendingSourceCompressions(JSRuntime* runtime,
- AutoLockHelperThreadState& lock);
+ void runPendingSourceCompressions(JSRuntime* runtime);
void trace(JSTracer* trc);
@@ -586,17 +576,11 @@ struct FreeDelazifyTask : public HelperThreadTask {
const char* getName() override { return "FreeDelazifyTask"; }
};
-// 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.
+// Off-thread task for compressing one or more script sources.
//
-// To this end, compression tasks are heap allocated and enqueued in a pending
-// list by ScriptSource::setSourceCopy. When a major GC occurs, we schedule
-// pending compression tasks and move the ones that are ready to be compressed
-// to the worklist. Currently, a compression task is considered ready 2 major
-// GCs after being enqueued. Completed tasks are handled during the sweeping
-// phase by AttachCompressedSourcesTask, which runs in parallel with other GC
-// sweeping tasks.
+// Completed tasks are handled during the sweeping phase by
+// AttachFinishedCompressions, which runs in parallel with other GC sweeping
+// tasks.
class SourceCompressionTask : public HelperThreadTask {
friend class HelperThread;
friend class ScriptSource;
@@ -605,9 +589,6 @@ class SourceCompressionTask : public HelperThreadTask {
// it uses the runtime's immutable string cache.
JSRuntime* runtime_;
- // The major GC number of the runtime when the task was enqueued.
- uint64_t majorGCNumber_;
-
// The source to be compressed.
RefPtr<ScriptSource> source_;
@@ -618,20 +599,13 @@ class SourceCompressionTask : public HelperThreadTask {
SharedImmutableString resultString_;
public:
- // The majorGCNumber is used for scheduling tasks.
SourceCompressionTask(JSRuntime* rt, ScriptSource* source)
- : runtime_(rt), majorGCNumber_(rt->gc.majorGCCount()), source_(source) {
+ : runtime_(rt), source_(source) {
source->noteSourceCompressionTask();
}
virtual ~SourceCompressionTask() = default;
bool runtimeMatches(JSRuntime* runtime) const { return runtime == runtime_; }
- bool shouldStart() const {
- // We wait 2 major GCs to start compressing, in order to avoid immediate
- // compression. If the script source has no other references then don't
- // compress it and let SweepPendingCompressions remove this task.
- return !shouldCancel() && runtime_->gc.majorGCCount() > majorGCNumber_ + 1;
- }
bool shouldCancel() const {
// If the refcount is exactly 1, then nothing else is holding on to the
diff --git a/js/src/vm/HelperThreads.cpp b/js/src/vm/HelperThreads.cpp
@@ -407,7 +407,6 @@ void GlobalHelperThreadState::addSizeOfIncludingThis(
wasmCompleteTier2GeneratorWorklist_.sizeOfExcludingThis(mallocSizeOf) +
wasmPartialTier2CompileWorklist_.sizeOfExcludingThis(mallocSizeOf) +
promiseHelperTasks_.sizeOfExcludingThis(mallocSizeOf) +
- compressionPendingList_.sizeOfExcludingThis(mallocSizeOf) +
compressionWorklist_.sizeOfExcludingThis(mallocSizeOf) +
compressionFinishedList_.sizeOfExcludingThis(mallocSizeOf) +
gcParallelWorklist_.sizeOfExcludingThis(mallocSizeOf, lock) +
@@ -1610,23 +1609,49 @@ bool GlobalHelperThreadState::submitTask(
return true;
}
-void GlobalHelperThreadState::startHandlingCompressionTasks(
- ScheduleCompressionTask schedule, JSRuntime* maybeRuntime,
- const AutoLockHelperThreadState& lock) {
- MOZ_ASSERT((schedule == ScheduleCompressionTask::GC) ==
- (maybeRuntime != nullptr));
-
- auto& pending = compressionPendingList(lock);
-
- for (size_t i = 0; i < pending.length(); i++) {
- UniquePtr<SourceCompressionTask>& task = pending[i];
- if (schedule == ScheduleCompressionTask::API ||
- (task->runtimeMatches(maybeRuntime) && task->shouldStart())) {
- // OOMing during appending results in the task not being scheduled
- // and deleted.
- (void)submitTask(std::move(task), lock);
- remove(pending, &i);
+void GlobalHelperThreadState::createAndSubmitCompressionTasks(
+ ScheduleCompressionTask schedule, JSRuntime* rt) {
+ // First create the SourceCompressionTasks and add them to a Vector.
+ Vector<UniquePtr<SourceCompressionTask>, 8, SystemAllocPolicy> tasksToSubmit;
+
+ rt->pendingCompressions().eraseIf([&](const auto& entry) {
+ MOZ_ASSERT(entry.source()->hasUncompressedSource());
+
+ // If the script source has no other references then remove it from the
+ // vector and don't compress it.
+ if (entry.shouldCancel()) {
+ return true;
+ }
+
+ // If we're starting tasks on GC, we wait 2 major GCs to start compressing
+ // in order to avoid immediate compression.
+ if (schedule == ScheduleCompressionTask::GC &&
+ rt->gc.majorGCCount() <= entry.majorGCNumber() + 1) {
+ return false;
+ }
+
+ // Heap allocate the task. It will be freed upon compression completing in
+ // AttachFinishedCompressedSources. On OOM we leave the pending compression
+ // in the vector.
+ auto ownedTask = MakeUnique<SourceCompressionTask>(rt, entry.source());
+ if (!ownedTask || !tasksToSubmit.append(std::move(ownedTask))) {
+ return false;
}
+ return true;
+ });
+ if (rt->pendingCompressions().empty()) {
+ rt->pendingCompressions().clearAndFree();
+ }
+
+ if (tasksToSubmit.empty()) {
+ return;
+ }
+
+ AutoLockHelperThreadState lock;
+ for (auto& task : tasksToSubmit) {
+ // OOMing during appending results in the task not being scheduled and
+ // deleted.
+ (void)submitTask(std::move(task), lock);
}
}
@@ -1642,34 +1667,20 @@ void js::AttachFinishedCompressions(JSRuntime* runtime,
}
}
-void js::SweepPendingCompressions(AutoLockHelperThreadState& lock) {
- auto& pending = HelperThreadState().compressionPendingList(lock);
- for (size_t i = 0; i < pending.length(); i++) {
- if (pending[i]->shouldCancel()) {
- HelperThreadState().remove(pending, &i);
- }
- }
-}
-
void js::RunPendingSourceCompressions(JSRuntime* runtime) {
if (!CanUseExtraThreads()) {
return;
}
- AutoLockHelperThreadState lock;
- HelperThreadState().runPendingSourceCompressions(runtime, lock);
+ HelperThreadState().runPendingSourceCompressions(runtime);
}
-void GlobalHelperThreadState::runPendingSourceCompressions(
- JSRuntime* runtime, AutoLockHelperThreadState& lock) {
- startHandlingCompressionTasks(
- GlobalHelperThreadState::ScheduleCompressionTask::API, nullptr, lock);
- {
- // Dispatch tasks.
- AutoUnlockHelperThreadState unlock(lock);
- }
+void GlobalHelperThreadState::runPendingSourceCompressions(JSRuntime* runtime) {
+ createAndSubmitCompressionTasks(
+ GlobalHelperThreadState::ScheduleCompressionTask::API, runtime);
// Wait until all tasks have started compression.
+ AutoLockHelperThreadState lock;
while (!compressionWorklist(lock).empty()) {
wait(lock);
}
@@ -1680,23 +1691,9 @@ void GlobalHelperThreadState::runPendingSourceCompressions(
AttachFinishedCompressions(runtime, lock);
}
-bool js::EnqueueOffThreadCompression(JSContext* cx,
- UniquePtr<SourceCompressionTask> task) {
- AutoLockHelperThreadState lock;
-
- auto& pending = HelperThreadState().compressionPendingList(lock);
- if (!pending.append(std::move(task))) {
- ReportOutOfMemory(cx);
- return false;
- }
-
- return true;
-}
-
-void js::StartHandlingCompressionsOnGC(JSRuntime* runtime) {
- AutoLockHelperThreadState lock;
- HelperThreadState().startHandlingCompressionTasks(
- GlobalHelperThreadState::ScheduleCompressionTask::GC, runtime, lock);
+void js::StartOffThreadCompressionsOnGC(JSRuntime* runtime) {
+ HelperThreadState().createAndSubmitCompressionTasks(
+ GlobalHelperThreadState::ScheduleCompressionTask::GC, runtime);
}
template <typename T>
@@ -1711,7 +1708,7 @@ static void ClearCompressionTaskList(T& list, JSRuntime* runtime) {
void GlobalHelperThreadState::cancelOffThreadCompressions(
JSRuntime* runtime, AutoLockHelperThreadState& lock) {
// Cancel all pending compression tasks.
- ClearCompressionTaskList(compressionPendingList(lock), runtime);
+ runtime->pendingCompressions().clearAndFree();
ClearCompressionTaskList(compressionWorklist(lock), runtime);
// Cancel all in-process compression tasks and wait for them to join so we
diff --git a/js/src/vm/HelperThreads.h b/js/src/vm/HelperThreads.h
@@ -306,14 +306,9 @@ void StartOffThreadDelazification(
void WaitForAllHelperThreads();
void WaitForAllHelperThreads(AutoLockHelperThreadState& lock);
-// Enqueue a compression job to be processed later. These are started at the
-// start of the major GC after the next one.
-bool EnqueueOffThreadCompression(JSContext* cx,
- UniquePtr<SourceCompressionTask> task);
-
// Start handling any compression tasks for this runtime. Called at the start of
// major GC.
-void StartHandlingCompressionsOnGC(JSRuntime* rt);
+void StartOffThreadCompressionsOnGC(JSRuntime* rt);
// Cancel all scheduled, in progress, or finished compression tasks for
// runtime.
@@ -322,9 +317,6 @@ void CancelOffThreadCompressions(JSRuntime* runtime);
void AttachFinishedCompressions(JSRuntime* runtime,
AutoLockHelperThreadState& lock);
-// Sweep pending tasks that are holding onto should-be-dead ScriptSources.
-void SweepPendingCompressions(AutoLockHelperThreadState& lock);
-
// Run all pending source compression tasks synchronously, for testing purposes
void RunPendingSourceCompressions(JSRuntime* runtime);
diff --git a/js/src/vm/JSScript.cpp b/js/src/vm/JSScript.cpp
@@ -1594,14 +1594,11 @@ bool ScriptSource::tryCompressOffThread(JSContext* cx) {
return true;
}
- // Heap allocate the task. It will be freed upon compression
- // completing in AttachFinishedCompressedSources.
- auto task = MakeUnique<SourceCompressionTask>(cx->runtime(), this);
- if (!task) {
+ if (!cx->runtime()->addPendingCompressionEntry(this)) {
ReportOutOfMemory(cx);
return false;
}
- return EnqueueOffThreadCompression(cx, std::move(task));
+ return true;
}
template <typename Unit>
@@ -1797,6 +1794,12 @@ void SourceCompressionTask::workEncodingSpecific() {
resultString_ = strings.getOrCreate(std::move(compressed), totalBytes);
}
+PendingSourceCompressionEntry::PendingSourceCompressionEntry(
+ JSRuntime* rt, ScriptSource* source)
+ : majorGCNumber_(rt->gc.majorGCCount()), source_(source) {
+ source->noteSourceCompressionTask();
+}
+
struct SourceCompressionTask::PerformTaskWork {
SourceCompressionTask* const task_;
diff --git a/js/src/vm/JSScript.h b/js/src/vm/JSScript.h
@@ -392,6 +392,7 @@ class ScriptSource {
// modified by the main thread, and all members are always safe to access
// on the main thread.
+ friend class PendingSourceCompressionEntry;
friend class SourceCompressionTask;
friend bool SynchronouslyCompressSource(JSContext* cx,
JS::Handle<BaseScript*> script);
@@ -1443,6 +1444,37 @@ class alignas(uintptr_t) PrivateScriptData final
PrivateScriptData& operator=(const PrivateScriptData&) = delete;
};
+// An entry in the runtime's pendingCompressions_ list for a single
+// ScriptSource.
+//
+// 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.
+//
+// To this end, script sources are enqueued in a pending list by
+// ScriptSource::tryCompressOffThread. When a major GC occurs, we allocate and
+// submit SourceCompressionTasks for them. Currently, a script source is
+// considered ready 2 major GCs after being enqueued.
+class PendingSourceCompressionEntry {
+ // The major GC number of the runtime when the entry was enqueued.
+ uint64_t majorGCNumber_;
+
+ // The source to be compressed.
+ RefPtr<ScriptSource> source_;
+
+ public:
+ PendingSourceCompressionEntry(JSRuntime* rt, ScriptSource* source);
+
+ ScriptSource* source() const { return source_.get(); }
+ uint64_t majorGCNumber() const { return majorGCNumber_; }
+ bool shouldCancel() const {
+ // If the refcount is exactly 1, then nothing else is holding on to the
+ // ScriptSource, so no reason to compress it and we should cancel the
+ // compression.
+ return source_->refs == 1;
+ }
+};
+
// [SMDOC] Script Representation (js::BaseScript)
//
// A "script" corresponds to a JavaScript function or a top-level (global, eval,
diff --git a/js/src/vm/Runtime.h b/js/src/vm/Runtime.h
@@ -585,6 +585,21 @@ struct JSRuntime {
js::MainThreadData<JS::CTypesActivityCallback> ctypesActivityCallback;
private:
+ // Script sources to compress off-thread. Only accessed by the main thread or
+ // off-thread GC sweeping (GCRuntime::sweepCompressionTasks).
+ using PendingCompressions =
+ js::Vector<js::PendingSourceCompressionEntry, 4, js::SystemAllocPolicy>;
+ js::MainThreadOrGCTaskData<PendingCompressions> pendingCompressions_;
+
+ public:
+ [[nodiscard]] bool addPendingCompressionEntry(js::ScriptSource* source) {
+ return pendingCompressions().emplaceBack(this, source);
+ }
+ PendingCompressions& pendingCompressions() {
+ return pendingCompressions_.ref();
+ }
+
+ private:
js::WriteOnceData<const JSClass*> windowProxyClass_;
public: