Repository navigation
Conversation
jcristau
force-pushed
the
perf-generator-fork
branch
from
October 8, 2026 11:51
e9c453a to
bce06e6
Compare
Avoid surprising results (deadlocks or incomplete graphs) later.
jcristau
force-pushed
the
perf-generator-fork
branch
from
October 8, 2026 14:59
bce06e6 to
471fbb0
Compare
On Linux, kinds were loaded in a ProcessPoolExecutor whose workers are forked before any task exists, so the tasks of all the kinds a kind depends on were pickled and sent to it, and its own tasks pickled and sent back. In Firefox's taskgraph (~190 kinds, ~49,000 tasks) that meant 74,000 task pickles to the workers, all on the parent's threads under the GIL, with unrelated kinds waiting behind big transfers. Instead, fork a child for each kind once the kinds it depends on are loaded, up to the number of CPUs at once. The child shares every task loaded so far with the parent, so nothing is sent to it, and it sends its tasks back through a pipe of its own, read on the parent's main thread as children finish. Errors are reported as before, with the traceback from the child. A child exiting without sending its tasks, or tasks that can't be pickled, are reported as errors loading the kind, and the other children are killed on error. Kinds are still loaded by calling load_tasks on the Kind instances from _load_kinds, so subclasses keep working. TASKGRAPH_SERIAL, TASKGRAPH_USE_THREADS and other platforms are unchanged, except that the thread pool no longer needs Python 3.13's os.process_cpu_count. Part of https://bugzilla.mozilla.org/show_bug.cgi?id=2079635
Generating a task graph creates millions of objects that stay alive until the end, so each collection traverses an ever growing heap without freeing anything. Disable the garbage collector while running each phase of the generator, restoring its previous state before yielding to the caller, and while loading a TaskGraph from JSON. Children loading kinds inherit the disabled collector, which also keeps them from writing to every page they share with the parent. Re-enabling the collector in children, or only freezing the heap after loading kinds and each phase (gc.freeze), measured slower in Firefox's taskgraph, with peak memory the same in all cases. Part of https://bugzilla.mozilla.org/show_bug.cgi?id=2079640
jcristau
force-pushed
the
perf-generator-fork
branch
from
October 8, 2026 17:07
471fbb0 to
a8a2c83
Compare
jcristau
marked this pull request as ready for review
October 8, 2026 17:15
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
On Linux, kinds were loaded in a
ProcessPoolExecutor. Its workers are forked before any task exists, so for each kind the tasks of all the kinds it depends on were pickled and sent to a worker, and the kind's own tasks were pickled and sent back. In Firefox's taskgraph (~190 kinds, ~49,000 tasks) that meant 74,000 task pickles to the workers. All of the parent's side runs on its threads under the GIL, and results come back through a single pipe, so unrelated kinds waited behind big transfers. For example, ready kinds waited 1.6s to be sent to a worker while the largest kind's 72 MB result was being unpickled.Now a child is forked for each kind once the kinds it depends on are loaded, up to
os.process_cpu_count()at once. The child already shares every task loaded so far with the parent, so nothing is sent to it. It sends its tasks back through its own pipe, which the parent reads on its main thread as children finish.SchemaValidationErrorwithout a traceback, other exceptions with one), and the child's traceback is attached as the exception's cause.os._exit) is reported with the kind's name and the child's exit status or signal.KeyboardInterrupt, the other children are killed and reaped.os._exit. They never return into the caller's code or runatexithandlers.load_tasksis still called on theKindinstances from_load_kinds, with the same arguments, soKindsubclasses keep working.TASKGRAPH_SERIAL,TASKGRAPH_USE_THREADSand other platforms are unchanged. The thread pool no longer needs Python 3.13'sos.process_cpu_count.kind-dependenciesentry naming a kind that doesn't exist now raises "Could not find the kind" instead of silently skipping the kinds waiting on it.Don't collect garbage while generating the task graph
Generation creates millions of objects that stay alive until the end, so each collection traverses an ever-growing heap without freeing anything. The garbage collector is now disabled while each phase of the generator runs, and restored to its previous state before yielding to the caller. It is also disabled in
TaskGraph.from_json. Children loading kinds inherit the disabled collector, which also keeps them from writing to every page they share with the parent. The helper istaskgraph.util.memory.gc_disabled.GC strategies measured with fork per kind (time until the target task set is done, median of 3):
gc.freeze()after loading kinds and after each phasegc.freeze()Peak memory was the same for all of them.
Timings
Cold generation of Firefox's full and target task sets (49,203 tasks), with the generator driven from a script using Firefox's
tryvirtualenv (Python 3.13) and no GC handling on the caller's side. 28-core Linux machine, median of 3 alternating runs:The full task sets generated by main and by this branch are identical once each run's timestamps (build date, pushdate, index rank) are normalized.
Loading the full task set from JSON (
json.loads+TaskGraph.from_json): 3.10s → 2.09s, withfrom_jsonalone going from 1.42s to 0.43s.Tests
New tests cover:
Kindsubclass'sload_tasksand the right dependency tasksThe existing
SchemaValidationErrorand traceback logging tests now go through the forked path on Linux.