Skip to content

Force all deserialized objects to the oldest GC generation #19681

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Aug 19, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions mypy/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@
"abc",
}

# We are careful now, we can increase this in future if safe/useful.
MAX_GC_FREEZE_CYCLES = 1

Graph: _TypeAlias = dict[str, "State"]

Expand Down Expand Up @@ -707,6 +709,8 @@ def __init__(
# new file can be processed O(n**2) times. This cache
# avoids most of this redundant work.
self.ast_cache: dict[str, tuple[MypyFile, list[ErrorInfo]]] = {}
# Number of times we used GC optimization hack for fresh SCCs.
self.gc_freeze_cycles = 0

def dump_stats(self) -> None:
if self.options.dump_build_stats:
Expand Down Expand Up @@ -3326,8 +3330,29 @@ def process_graph(graph: Graph, manager: BuildManager) -> None:
#
# TODO: see if it's possible to determine if we need to process only a
# _subset_ of the past SCCs instead of having to process them all.
if (
platform.python_implementation() == "CPython"
and manager.gc_freeze_cycles < MAX_GC_FREEZE_CYCLES
):
# When deserializing cache we create huge amount of new objects, so even
# with our generous GC thresholds, GC is still doing a lot of pointless
# work searching for garbage. So, we temporarily disable it when
# processing fresh SCCs, and then move all the new objects to the oldest
# generation with the freeze()/unfreeze() trick below. This is arguably
# a hack, but it gives huge performance wins for large third-party
# libraries, like torch.
gc.collect()
gc.disable()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we get here multiple times, if there are multiple dirty sub-DAGs? If yes, do you think it'll be a problem?

A quick workaround would be to do this only at most N times per run (possibly N=1).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I was thinking about this. FWIW, I don't think it will be a problem, since freeze/unfreeze are quite fast. Also, we may accidentally get some objects from the stale SCCs previously processed in the oldest generation, but it is probably not so bad. But also I think it is fine to start with just one pass per run and increase the limit as we get more data for this.

(With mypy -c 'import torch' we enter here only once)

for prev_scc in fresh_scc_queue:
process_fresh_modules(graph, prev_scc, manager)
if (
platform.python_implementation() == "CPython"
and manager.gc_freeze_cycles < MAX_GC_FREEZE_CYCLES
):
manager.gc_freeze_cycles += 1
gc.freeze()
gc.unfreeze()
gc.enable()
fresh_scc_queue = []
size = len(scc)
if size == 1:
Expand Down
Loading