tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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:
Mjs/src/gc/GC.cpp | 2+-
Mjs/src/gc/Sweeping.cpp | 8+++++---
Mjs/src/vm/HelperThreadState.h | 44+++++++++-----------------------------------
Mjs/src/vm/HelperThreads.cpp | 105++++++++++++++++++++++++++++++++++++++-----------------------------------------
Mjs/src/vm/HelperThreads.h | 10+---------
Mjs/src/vm/JSScript.cpp | 13++++++++-----
Mjs/src/vm/JSScript.h | 32++++++++++++++++++++++++++++++++
Mjs/src/vm/Runtime.h | 15+++++++++++++++
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: