Skip to content

Commit a7e66a6

Browse files
authored
Speed up options snapshot calculation (#20797)
The `options_snapshot` function was using up to 19% CPU in small incremental runs in a very large codebase, when not using orjson. Micro-optimize it, and use binary serialization instead of json. We can still make this faster, but first let's see how much this helps.
1 parent d422b3d commit a7e66a6

2 files changed

Lines changed: 30 additions & 9 deletions

File tree

mypy/build.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
TextIO,
3838
TypeAlias as _TypeAlias,
3939
TypedDict,
40+
cast,
4041
)
4142

4243
from librt.base64 import b64encode
@@ -59,6 +60,7 @@
5960
LIST_GEN,
6061
LITERAL_NONE,
6162
CacheMeta,
63+
JsonValue,
6264
ReadBuffer,
6365
SerializedError,
6466
Tag,
@@ -74,6 +76,7 @@
7476
write_int,
7577
write_int_list,
7678
write_int_opt,
79+
write_json_value,
7780
write_str,
7881
write_str_list,
7982
write_str_opt,
@@ -108,6 +111,7 @@
108111
MypyFile,
109112
SymbolTable,
110113
)
114+
from mypy.options import OPTIONS_AFFECTING_CACHE_NO_PLATFORM
111115
from mypy.partially_defined import PossiblyUndefinedVariableVisitor
112116
from mypy.semanal import SemanticAnalyzer
113117
from mypy.semanal_pass1 import SemanticAnalyzerPreAnalysis
@@ -1616,11 +1620,17 @@ def options_snapshot(id: str, manager: BuildManager) -> dict[str, object]:
16161620
Separately store only the options we may compare individually, and take a hash
16171621
of everything else. If --debug-cache is specified, fall back to full snapshot.
16181622
"""
1619-
snapshot = manager.options.clone_for_module(id).select_options_affecting_cache()
1623+
platform_opt, values = manager.options.clone_for_module(id).select_options_affecting_cache()
16201624
if manager.options.debug_cache:
1621-
return snapshot
1622-
platform_opt = snapshot.pop("platform")
1623-
return {"platform": platform_opt, "other_options": hash_digest(json_dumps(snapshot))}
1625+
# Build full options snapshot for debugging purposes.
1626+
result: dict[str, object] = {"platform": platform_opt}
1627+
for key, val in zip(OPTIONS_AFFECTING_CACHE_NO_PLATFORM, values):
1628+
result[key] = val
1629+
return result
1630+
# Process most options quickly, since this is performance critical.
1631+
buf = WriteBuffer()
1632+
write_json_value(buf, cast(JsonValue, values))
1633+
return {"platform": platform_opt, "other_options": hash_digest(buf.getvalue())}
16241634

16251635

16261636
def find_cache_meta(

mypy/options.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ class BuildType:
7777
}
7878
) - {"debug_cache"}
7979

80+
# OPTIONS_AFFECTING_CACHE without "platform", as a sorted tuple for fast iteration.
81+
# "platform" is handled separately in options_snapshot().
82+
OPTIONS_AFFECTING_CACHE_NO_PLATFORM: Final = tuple(sorted(OPTIONS_AFFECTING_CACHE - {"platform"}))
83+
8084
# Features that are currently (or were recently) incomplete/experimental
8185
TYPE_VAR_TUPLE: Final = "TypeVarTuple"
8286
UNPACK: Final = "Unpack"
@@ -608,14 +612,21 @@ def compile_glob(self, s: str) -> Pattern[str]:
608612
expr += re.escape("." + part) if part != "*" else r"(\..*)?"
609613
return re.compile(expr + "\\Z")
610614

611-
def select_options_affecting_cache(self) -> dict[str, object]:
612-
result: dict[str, object] = {}
613-
for opt in OPTIONS_AFFECTING_CACHE:
615+
def select_options_affecting_cache(self) -> tuple[str, list[object]]:
616+
"""Return (platform, [values...]) for options that affect the cache.
617+
618+
The list contains values for OPTIONS_AFFECTING_CACHE_NO_PLATFORM
619+
in sorted attribute name order. Keys are omitted since the cache
620+
is invalidated when the mypy version changes, and keys are constant
621+
on any specific mypy version.
622+
"""
623+
result: list[object] = []
624+
for opt in OPTIONS_AFFECTING_CACHE_NO_PLATFORM:
614625
val = getattr(self, opt)
615626
if opt in ("disabled_error_codes", "enabled_error_codes"):
616627
val = sorted([code.code for code in val])
617-
result[opt] = val
618-
return result
628+
result.append(val)
629+
return self.platform, result
619630

620631
def dep_import_options(self) -> dict[str, object]:
621632
# These are options that can affect dependent modules as well.

0 commit comments

Comments
 (0)