Skip to content

Commit 04d44c1

Browse files
authored
Add intenal flag for per-line type checking peformance (#14173)
This should help with the investigation of tricky performance regressions like #13821. I tried to implement in such a way that it will give minimal impact when not used (since I am touching a hot method).
1 parent 07139ef commit 04d44c1

File tree

6 files changed

+65
-16
lines changed

6 files changed

+65
-16
lines changed

mypy/build.py

+24-6
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
from __future__ import annotations
1414

15+
import collections
1516
import contextlib
1617
import errno
1718
import gc
@@ -278,6 +279,8 @@ def _build(
278279
TypeState.reset_all_subtype_caches()
279280
if options.timing_stats is not None:
280281
dump_timing_stats(options.timing_stats, graph)
282+
if options.line_checking_stats is not None:
283+
dump_line_checking_stats(options.line_checking_stats, graph)
281284
return BuildResult(manager, graph)
282285
finally:
283286
t0 = time.time()
@@ -1889,6 +1892,10 @@ class State:
18891892
# Cumulative time spent on this file, in microseconds (for profiling stats)
18901893
time_spent_us: int = 0
18911894

1895+
# Per-line type-checking time (cumulative time spent type-checking expressions
1896+
# on a given source code line).
1897+
per_line_checking_time_ns: dict[int, int]
1898+
18921899
def __init__(
18931900
self,
18941901
id: str | None,
@@ -1956,6 +1963,7 @@ def __init__(
19561963
source = ""
19571964
self.source = source
19581965
self.add_ancestors()
1966+
self.per_line_checking_time_ns = collections.defaultdict(int)
19591967
t0 = time.time()
19601968
self.meta = validate_meta(self.meta, self.id, self.path, self.ignore_all, manager)
19611969
self.manager.add_stats(validate_meta_time=time.time() - t0)
@@ -2320,6 +2328,7 @@ def type_checker(self) -> TypeChecker:
23202328
self.tree,
23212329
self.xpath,
23222330
manager.plugin,
2331+
self.per_line_checking_time_ns,
23232332
)
23242333
return self._type_checker
23252334

@@ -2945,13 +2954,22 @@ def dumps(self) -> str:
29452954

29462955

29472956
def dump_timing_stats(path: str, graph: Graph) -> None:
2948-
"""
2949-
Dump timing stats for each file in the given graph
2950-
"""
2957+
"""Dump timing stats for each file in the given graph."""
29512958
with open(path, "w") as f:
2952-
for k in sorted(graph.keys()):
2953-
v = graph[k]
2954-
f.write(f"{v.id} {v.time_spent_us}\n")
2959+
for id in sorted(graph):
2960+
f.write(f"{id} {graph[id].time_spent_us}\n")
2961+
2962+
2963+
def dump_line_checking_stats(path: str, graph: Graph) -> None:
2964+
"""Dump per-line expression type checking stats."""
2965+
with open(path, "w") as f:
2966+
for id in sorted(graph):
2967+
if not graph[id].per_line_checking_time_ns:
2968+
continue
2969+
f.write(f"{id}:\n")
2970+
for line in sorted(graph[id].per_line_checking_time_ns):
2971+
line_time = graph[id].per_line_checking_time_ns[line]
2972+
f.write(f"{line:>5} {line_time/1000:8.1f}\n")
29552973

29562974

29572975
def dump_graph(graph: Graph, stdout: TextIO | None = None) -> None:

mypy/checker.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,7 @@ def __init__(
364364
tree: MypyFile,
365365
path: str,
366366
plugin: Plugin,
367+
per_line_checking_time_ns: dict[int, int],
367368
) -> None:
368369
"""Construct a type checker.
369370
@@ -376,7 +377,9 @@ def __init__(
376377
self.path = path
377378
self.msg = MessageBuilder(errors, modules)
378379
self.plugin = plugin
379-
self.expr_checker = mypy.checkexpr.ExpressionChecker(self, self.msg, self.plugin)
380+
self.expr_checker = mypy.checkexpr.ExpressionChecker(
381+
self, self.msg, self.plugin, per_line_checking_time_ns
382+
)
380383
self.pattern_checker = PatternChecker(self, self.msg, self.plugin)
381384
self.tscope = Scope()
382385
self.scope = CheckerScope(tree)

mypy/checkexpr.py

+26-3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from __future__ import annotations
44

55
import itertools
6+
import time
67
from contextlib import contextmanager
78
from typing import Callable, ClassVar, Iterator, List, Optional, Sequence, cast
89
from typing_extensions import Final, TypeAlias as _TypeAlias, overload
@@ -263,11 +264,22 @@ class ExpressionChecker(ExpressionVisitor[Type]):
263264
strfrm_checker: StringFormatterChecker
264265
plugin: Plugin
265266

266-
def __init__(self, chk: mypy.checker.TypeChecker, msg: MessageBuilder, plugin: Plugin) -> None:
267+
def __init__(
268+
self,
269+
chk: mypy.checker.TypeChecker,
270+
msg: MessageBuilder,
271+
plugin: Plugin,
272+
per_line_checking_time_ns: dict[int, int],
273+
) -> None:
267274
"""Construct an expression type checker."""
268275
self.chk = chk
269276
self.msg = msg
270277
self.plugin = plugin
278+
self.per_line_checking_time_ns = per_line_checking_time_ns
279+
self.collect_line_checking_stats = self.chk.options.line_checking_stats is not None
280+
# Are we already visiting some expression? This is used to avoid double counting
281+
# time for nested expressions.
282+
self.in_expression = False
271283
self.type_context = [None]
272284

273285
# Temporary overrides for expression types. This is currently
@@ -4727,7 +4739,14 @@ def accept(
47274739
applies only to this expression and not any subexpressions.
47284740
"""
47294741
if node in self.type_overrides:
4742+
# This branch is very fast, there is no point timing it.
47304743
return self.type_overrides[node]
4744+
# We don't use context manager here to get most precise data (and avoid overhead).
4745+
record_time = False
4746+
if self.collect_line_checking_stats and not self.in_expression:
4747+
t0 = time.perf_counter_ns()
4748+
self.in_expression = True
4749+
record_time = True
47314750
self.type_context.append(type_context)
47324751
old_is_callee = self.is_callee
47334752
self.is_callee = is_callee
@@ -4762,9 +4781,13 @@ def accept(
47624781
self.msg.disallowed_any_type(typ, node)
47634782

47644783
if not self.chk.in_checked_function() or self.chk.current_node_deferred:
4765-
return AnyType(TypeOfAny.unannotated)
4784+
result: Type = AnyType(TypeOfAny.unannotated)
47664785
else:
4767-
return typ
4786+
result = typ
4787+
if record_time:
4788+
self.per_line_checking_time_ns[node.line] += time.perf_counter_ns() - t0
4789+
self.in_expression = False
4790+
return result
47684791

47694792
def named_type(self, name: str) -> Instance:
47704793
"""Return an instance type with type given by the name and no type

mypy/main.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -1082,8 +1082,14 @@ def add_invertible_flag(
10821082
"--inferstats", action="store_true", dest="dump_inference_stats", help=argparse.SUPPRESS
10831083
)
10841084
parser.add_argument("--dump-build-stats", action="store_true", help=argparse.SUPPRESS)
1085-
# dump timing stats for each processed file into the given output file
1085+
# Dump timing stats for each processed file into the given output file
10861086
parser.add_argument("--timing-stats", dest="timing_stats", help=argparse.SUPPRESS)
1087+
# Dump per line type checking timing stats for each processed file into the given
1088+
# output file. Only total time spent in each top level expression will be shown.
1089+
# Times are show in microseconds.
1090+
parser.add_argument(
1091+
"--line-checking-stats", dest="line_checking_stats", help=argparse.SUPPRESS
1092+
)
10871093
# --debug-cache will disable any cache-related compressions/optimizations,
10881094
# which will make the cache writing process output pretty-printed JSON (which
10891095
# is easier to debug).

mypy/options.py

+1
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,7 @@ def __init__(self) -> None:
283283
self.enable_incomplete_features = False # deprecated
284284
self.enable_incomplete_feature: list[str] = []
285285
self.timing_stats: str | None = None
286+
self.line_checking_stats: str | None = None
286287

287288
# -- test options --
288289
# Stop after the semantic analysis phase

mypy/util.py

+3-5
Original file line numberDiff line numberDiff line change
@@ -807,13 +807,11 @@ def unnamed_function(name: str | None) -> bool:
807807
return name is not None and name == "_"
808808

809809

810-
# TODO: replace with uses of perf_counter_ns when support for py3.6 is dropped
811-
# (or when mypy properly handles alternate definitions based on python version check
812-
time_ref = time.perf_counter
810+
time_ref = time.perf_counter_ns
813811

814812

815-
def time_spent_us(t0: float) -> int:
816-
return int((time.perf_counter() - t0) * 1e6)
813+
def time_spent_us(t0: int) -> int:
814+
return int((time.perf_counter_ns() - t0) / 1000)
817815

818816

819817
def plural_s(s: int | Sized) -> str:

0 commit comments

Comments
 (0)