Skip to content

Commit e191a32

Browse files
committed
introduce per-file timing-stats
When profiling mypy over a large codebase, it can be useful to know which files are slowest to typecheck. Gather per-file timing stats and expose them through a new (hidden) command line switch
1 parent c56046c commit e191a32

File tree

3 files changed

+35
-0
lines changed

3 files changed

+35
-0
lines changed

mypy/build.py

+31
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,8 @@ def _build(sources: List[BuildSource],
256256
graph = dispatch(sources, manager, stdout)
257257
if not options.fine_grained_incremental:
258258
TypeState.reset_all_subtype_caches()
259+
if options.timing_stats is not None:
260+
dump_timing_stats(options.timing_stats, graph)
259261
return BuildResult(manager, graph)
260262
finally:
261263
t0 = time.time()
@@ -1808,6 +1810,9 @@ class State:
18081810

18091811
fine_grained_deps_loaded = False
18101812

1813+
# Cumulative time spent on this file (for profiling stats)
1814+
time_spent: int = 0
1815+
18111816
def __init__(self,
18121817
id: Optional[str],
18131818
path: Optional[str],
@@ -2034,6 +2039,8 @@ def parse_file(self) -> None:
20342039
else:
20352040
manager.log("Using cached AST for %s (%s)" % (self.xpath, self.id))
20362041

2042+
t0 = time.perf_counter_ns()
2043+
20372044
with self.wrap_context():
20382045
source = self.source
20392046
self.source = None # We won't need it again.
@@ -2079,6 +2086,8 @@ def parse_file(self) -> None:
20792086
self.tree.ignored_lines,
20802087
self.ignore_all or self.options.ignore_errors)
20812088

2089+
self.time_spent += time.perf_counter_ns() - t0
2090+
20822091
if not cached:
20832092
# Make a copy of any errors produced during parse time so that
20842093
# fine-grained mode can repeat them when the module is
@@ -2113,6 +2122,9 @@ def semantic_analysis_pass1(self) -> None:
21132122
"""
21142123
options = self.options
21152124
assert self.tree is not None
2125+
2126+
t0 = time.perf_counter_ns()
2127+
21162128
# Do the first pass of semantic analysis: analyze the reachability
21172129
# of blocks and import statements. We must do this before
21182130
# processing imports, since this may mark some import statements as
@@ -2131,6 +2143,7 @@ def semantic_analysis_pass1(self) -> None:
21312143
if options.allow_redefinition:
21322144
# Perform more renaming across the AST to allow variable redefinitions
21332145
self.tree.accept(VariableRenameVisitor())
2146+
self.time_spent += time.perf_counter_ns() - t0
21342147

21352148
def add_dependency(self, dep: str) -> None:
21362149
if dep not in self.dependencies_set:
@@ -2188,8 +2201,10 @@ def compute_dependencies(self) -> None:
21882201
def type_check_first_pass(self) -> None:
21892202
if self.options.semantic_analysis_only:
21902203
return
2204+
t0 = time.perf_counter_ns()
21912205
with self.wrap_context():
21922206
self.type_checker().check_first_pass()
2207+
self.time_spent += time.perf_counter_ns() - t0
21932208

21942209
def type_checker(self) -> TypeChecker:
21952210
if not self._type_checker:
@@ -2207,14 +2222,17 @@ def type_map(self) -> Dict[Expression, Type]:
22072222
def type_check_second_pass(self) -> bool:
22082223
if self.options.semantic_analysis_only:
22092224
return False
2225+
t0 = time.perf_counter_ns()
22102226
with self.wrap_context():
22112227
return self.type_checker().check_second_pass()
2228+
self.time_spent += time.perf_counter_ns() - t0
22122229

22132230
def finish_passes(self) -> None:
22142231
assert self.tree is not None, "Internal error: method must be called on parsed file only"
22152232
manager = self.manager
22162233
if self.options.semantic_analysis_only:
22172234
return
2235+
t0 = time.perf_counter_ns()
22182236
with self.wrap_context():
22192237
# Some tests (and tools) want to look at the set of all types.
22202238
options = manager.options
@@ -2237,6 +2255,7 @@ def finish_passes(self) -> None:
22372255
self.free_state()
22382256
if not manager.options.fine_grained_incremental and not manager.options.preserve_asts:
22392257
free_tree(self.tree)
2258+
self.time_spent += time.perf_counter_ns() - t0
22402259

22412260
def free_state(self) -> None:
22422261
if self._type_checker:
@@ -2771,6 +2790,16 @@ def dumps(self) -> str:
27712790
json.dumps(self.deps))
27722791

27732792

2793+
def dump_timing_stats(path: str, graph: Graph) -> None:
2794+
"""
2795+
Dump timing stats for each file in the given graph
2796+
"""
2797+
with open(path, 'w') as f:
2798+
for k in sorted(graph.keys()):
2799+
v = graph[k]
2800+
f.write('{} {}\n'.format(v.id, v.time_spent))
2801+
2802+
27742803
def dump_graph(graph: Graph, stdout: Optional[TextIO] = None) -> None:
27752804
"""Dump the graph as a JSON string to stdout.
27762805
@@ -3091,6 +3120,8 @@ def process_graph(graph: Graph, manager: BuildManager) -> None:
30913120
manager.log("No fresh SCCs left in queue")
30923121

30933122

3123+
3124+
30943125
def order_ascc(graph: Graph, ascc: AbstractSet[str], pri_max: int = PRI_ALL) -> List[str]:
30953126
"""Come up with the ideal processing order within an SCC.
30963127

mypy/main.py

+3
Original file line numberDiff line numberDiff line change
@@ -835,6 +835,9 @@ def add_invertible_flag(flag: str,
835835
parser.add_argument(
836836
'--dump-build-stats', action='store_true',
837837
help=argparse.SUPPRESS)
838+
# dump timing stats for each processed file into the given output file
839+
parser.add_argument(
840+
'--timing-stats', dest='timing_stats', help=argparse.SUPPRESS)
838841
# --debug-cache will disable any cache-related compressions/optimizations,
839842
# which will make the cache writing process output pretty-printed JSON (which
840843
# is easier to debug).

mypy/options.py

+1
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,7 @@ def __init__(self) -> None:
263263
self.dump_inference_stats = False
264264
self.dump_build_stats = False
265265
self.enable_incomplete_features = False
266+
self.timing_stats: Optional[str] = None
266267

267268
# -- test options --
268269
# Stop after the semantic analysis phase

0 commit comments

Comments
 (0)