Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit f8e106a

Browse files
authored
Improve daemon (#4169)
- Fix the bug where stale trees were used. - Add --log-file flag to daemon [re]start commands. - Refactored GC tracking code. - Make daemon testing in misc/incremental_checker.py more robust. - Add -x flag to misc/incremental_checker.py to exit on first error. - Other small cleanups.
1 parent 85c8fec commit f8e106a

4 files changed

Lines changed: 152 additions & 74 deletions

File tree

misc/incremental_checker.py

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ def get_commits_starting_at(repo_folder_path: str, start_commit: str) -> List[Tu
114114
return get_commits(repo_folder_path, '{0}^..HEAD'.format(start_commit))
115115

116116

117-
def get_nth_commit(repo_folder_path, n: int) -> Tuple[str, str]:
117+
def get_nth_commit(repo_folder_path: str, n: int) -> Tuple[str, str]:
118118
print("Fetching last {} commits (or all, if there are fewer commits than n)".format(n))
119119
return get_commits(repo_folder_path, '-{}'.format(n))[0]
120120

@@ -156,19 +156,13 @@ def run_mypy(target_file_path: Optional[str],
156156
return runtime, output
157157

158158

159-
def start_daemon(mypy_cache_path: str, verbose: bool) -> None:
160-
stdout, stderr, status = execute(DAEMON_CMD + ["status"], fail_on_error=False)
161-
if status:
162-
cmd = DAEMON_CMD + ["start", "--", "--cache-dir", mypy_cache_path]
163-
if verbose:
164-
cmd.extend(["-v", "-v"])
165-
execute(cmd)
159+
def start_daemon(mypy_cache_path: str) -> None:
160+
cmd = DAEMON_CMD + ["restart", "--", "--cache-dir", mypy_cache_path]
161+
execute(cmd)
166162

167163

168164
def stop_daemon() -> None:
169-
stdout, stderr, status = execute(DAEMON_CMD + ["status"], fail_on_error=False)
170-
if status == 0:
171-
execute(DAEMON_CMD + ["stop"])
165+
execute(DAEMON_CMD + ["stop"])
172166

173167

174168
def load_cache(incremental_cache_path: str = CACHE_PATH) -> JsonDict:
@@ -221,7 +215,8 @@ def test_incremental(commits: List[Tuple[str, str]],
221215
mypy_cache_path: str,
222216
*,
223217
mypy_script: Optional[str] = None,
224-
daemon: bool = False) -> None:
218+
daemon: bool = False,
219+
exit_on_error: bool = False) -> None:
225220
"""Runs incremental mode on all `commits` to verify the output matches the expected output.
226221
227222
This function runs mypy on the `target_file_path` inside the `temp_repo_path`. The
@@ -242,6 +237,8 @@ def test_incremental(commits: List[Tuple[str, str]],
242237
print_offset(expected_output, 8)
243238
print(" Actual output: ({:.3f} sec):".format(runtime))
244239
print_offset(output, 8)
240+
if exit_on_error:
241+
break
245242
else:
246243
print(" Output matches expected result!")
247244
print(" Incremental: {:.3f} sec".format(runtime))
@@ -257,7 +254,7 @@ def test_repo(target_repo_url: str, temp_repo_path: str,
257254
target_file_path: Optional[str],
258255
mypy_path: str, incremental_cache_path: str, mypy_cache_path: str,
259256
range_type: str, range_start: str, branch: str,
260-
params: Optional[Namespace] = None) -> None:
257+
params: Namespace) -> None:
261258
"""Tests incremental mode against the repo specified in `target_repo_url`.
262259
263260
This algorithm runs in five main stages:
@@ -290,7 +287,7 @@ def test_repo(target_repo_url: str, temp_repo_path: str,
290287
else:
291288
raise RuntimeError("Invalid option: {}".format(range_type))
292289
commits = get_commits_starting_at(temp_repo_path, start_commit)
293-
if params is not None and params.sample:
290+
if params.sample:
294291
seed = params.seed or base64.urlsafe_b64encode(os.urandom(15)).decode('ascii')
295292
random.seed(seed)
296293
commits = random.sample(commits, params.sample)
@@ -304,18 +301,21 @@ def test_repo(target_repo_url: str, temp_repo_path: str,
304301

305302
# Stage 4: Rewind and re-run mypy (with incremental mode enabled)
306303
if params.daemon:
307-
start_daemon(mypy_cache_path, False)
304+
print('Starting daemon')
305+
start_daemon(mypy_cache_path)
308306
test_incremental(commits, cache, temp_repo_path, target_file_path, mypy_cache_path,
309-
mypy_script=params.mypy_script, daemon=params.daemon)
307+
mypy_script=params.mypy_script, daemon=params.daemon,
308+
exit_on_error=params.exit_on_error)
310309

311310
# Stage 5: Remove temp files, stop daemon
312311
cleanup(temp_repo_path, mypy_cache_path)
313312
if params.daemon:
313+
print('Stopping daemon')
314314
stop_daemon()
315315

316316

317317
def main() -> None:
318-
help_factory = (lambda prog: RawDescriptionHelpFormatter(prog=prog, max_help_position=32))
318+
help_factory = (lambda prog: RawDescriptionHelpFormatter(prog=prog, max_help_position=32)) # type: Any
319319
parser = ArgumentParser(
320320
prog='incremental_checker',
321321
description=__doc__,
@@ -330,6 +330,8 @@ def main() -> None:
330330
help="the repo to clone and run tests on")
331331
parser.add_argument("-f", "--file-path", default=MYPY_TARGET_FILE, metavar="FILE",
332332
help="the name of the file or directory to typecheck")
333+
parser.add_argument("-x", "--exit-on-error", action='store_true',
334+
help="Exits as soon as an error occurs")
333335
parser.add_argument("--cache-path", default=CACHE_PATH, metavar="DIR",
334336
help="sets a custom location to store cache data")
335337
parser.add_argument("--branch", default=None, metavar="NAME",

mypy/build.py

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -357,7 +357,7 @@ def default_lib_path(data_dir: str,
357357

358358

359359
def cache_meta_from_dict(meta: Dict[str, Any], data_json: str) -> CacheMeta:
360-
sentinel = None # type: Any # the values will be post-validated below
360+
sentinel = None # type: Any # Values to be validated by the caller
361361
return CacheMeta(
362362
meta.get('id', sentinel),
363363
meta.get('path', sentinel),
@@ -1408,6 +1408,7 @@ class State:
14081408
meta = None # type: Optional[CacheMeta]
14091409
data = None # type: Optional[str]
14101410
tree = None # type: Optional[MypyFile]
1411+
is_from_saved_cache = False # True if the tree came from the in-memory cache
14111412
dependencies = None # type: List[str]
14121413
suppressed = None # type: List[str] # Suppressed/missing dependencies
14131414
priorities = None # type: Dict[str, int]
@@ -1442,9 +1443,6 @@ class State:
14421443
# Whether to ignore all errors
14431444
ignore_all = False
14441445

1445-
# Whether this module was found to have errors
1446-
has_errors = False
1447-
14481446
def __init__(self,
14491447
id: Optional[str],
14501448
path: Optional[str],
@@ -1621,7 +1619,6 @@ def mark_interface_stale(self, *, on_errors: bool = False) -> None:
16211619
"""Marks this module as having a stale public interface, and discards the cache data."""
16221620
self.meta = None
16231621
self.externally_same = False
1624-
self.has_errors = on_errors
16251622
if not on_errors:
16261623
self.manager.stale_modules.add(self.id)
16271624

@@ -1951,7 +1948,7 @@ def preserve_cache(graph: Graph) -> SavedCache:
19511948
saved_cache = {}
19521949
for id, state in graph.items():
19531950
assert state.id == id
1954-
if state.meta is not None and state.tree is not None and not state.has_errors:
1951+
if state.meta is not None and state.tree is not None:
19551952
saved_cache[id] = (state.meta, state.tree)
19561953
return saved_cache
19571954

@@ -2260,16 +2257,27 @@ def order_ascc(graph: Graph, ascc: AbstractSet[str], pri_max: int = PRI_ALL) ->
22602257

22612258

22622259
def process_fresh_scc(graph: Graph, scc: List[str], manager: BuildManager) -> None:
2263-
"""Process the modules in one SCC from their cached data."""
2264-
# TODO: Clean this up, it's ugly.
2260+
"""Process the modules in one SCC from their cached data.
2261+
2262+
This involves loading the tree from JSON and then doing various cleanups.
2263+
2264+
If the tree is loaded from memory ('saved_cache') it's even quicker.
2265+
"""
22652266
saved_cache = manager.saved_cache
2267+
# Check that all nodes are available for loading from memory.
22662268
if all(id in saved_cache for id in scc):
2267-
trees = {id: saved_cache[id][1] for id in scc}
2268-
if all(trees.values()):
2269+
deps = set(dep for id in scc for dep in graph[id].dependencies if dep in graph)
2270+
# Check that all dependencies were loaded from memory.
2271+
# If not, some dependency was reparsed but the interface hash
2272+
# wasn't changed -- in that case we can't reuse the tree.
2273+
if all(graph[dep].is_from_saved_cache for dep in deps):
2274+
trees = {id: saved_cache[id][1] for id in scc}
22692275
for id, tree in trees.items():
22702276
manager.add_stats(reused_trees=1)
22712277
manager.trace("Reusing saved tree %s" % id)
2272-
graph[id].tree = tree
2278+
st = graph[id]
2279+
st.tree = tree # This is never overwritten.
2280+
st.is_from_saved_cache = True
22732281
manager.modules[id] = tree
22742282
return
22752283
for id in scc:

mypy/dmypy.py

Lines changed: 68 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
subparsers = parser.add_subparsers()
3535

3636
start_parser = subparsers.add_parser('start', help="Start daemon")
37+
start_parser.add_argument('--log-file', metavar='FILE', type=str,
38+
help="Direct daemon stdout/stderr to FILE")
3739
start_parser.add_argument('flags', metavar='FLAG', nargs='*', type=str,
3840
help="Regular mypy flags (precede with --)")
3941

@@ -45,6 +47,8 @@
4547

4648
restart_parser = subparsers.add_parser('restart',
4749
help="Restart daemon (stop or kill followed by start)")
50+
restart_parser.add_argument('--log-file', metavar='FILE', type=str,
51+
help="Direct daemon stdout/stderr to FILE")
4852
restart_parser.add_argument('flags', metavar='FLAG', nargs='*', type=str,
4953
help="Regular mypy flags (precede with --)")
5054

@@ -103,7 +107,7 @@ def do_start(args: argparse.Namespace) -> None:
103107
try:
104108
pid, sockname = get_status()
105109
except SystemExit as err:
106-
if daemonize(Server(args.flags).serve):
110+
if daemonize(Server(args.flags).serve, args.log_file):
107111
sys.exit(1)
108112
wait_for_server()
109113
else:
@@ -169,7 +173,7 @@ def do_restart(args: argparse.Namespace) -> None:
169173
sys.exit("Status: %s" % str(response))
170174
else:
171175
print("Daemon stopped")
172-
if daemonize(Server(args.flags).serve):
176+
if daemonize(Server(args.flags).serve, args.log_file):
173177
sys.exit(1)
174178
wait_for_server()
175179

@@ -333,7 +337,7 @@ def read_status() -> Dict[str, object]:
333337
return data
334338

335339

336-
def daemonize(func: Callable[[], None]) -> int:
340+
def daemonize(func: Callable[[], None], log_file: Optional[str] = None) -> int:
337341
"""Arrange to call func() in a grandchild of the current process.
338342
339343
Return 0 for success, exit status for failure, negative if
@@ -368,6 +372,11 @@ def daemonize(func: Callable[[], None]) -> int:
368372
# Child is done, exit to parent.
369373
os._exit(0)
370374
# Grandchild: run the server.
375+
if log_file:
376+
sys.stdout = sys.stderr = open(log_file, 'a', buffering=1)
377+
fd = sys.stdout.fileno()
378+
os.dup2(fd, 2)
379+
os.dup2(fd, 1)
371380
func()
372381
finally:
373382
# Make sure we never get back into the caller.
@@ -490,43 +499,28 @@ def cmd_recheck(self) -> Dict[str, object]:
490499
return {'error': "Command 'recheck' is only valid after a 'check' command"}
491500
return self.check(self.last_sources)
492501

493-
last_mananager = None # type: Optional[mypy.build.BuildManager]
502+
# Needed by tests.
503+
last_manager = None # type: Optional[mypy.build.BuildManager]
494504

495505
def check(self, sources: List[mypy.build.BuildSource],
496506
alt_lib_path: Optional[str] = None) -> Dict[str, Any]:
497-
# TODO: Move stats handling code to make the logic here less cluttered.
498-
bound_gc_callback = self.gc_callback
499-
self.gc_start_time = None # type: Optional[float]
500-
self.gc_time = 0.0
501-
self.gc_calls = 0
502-
self.gc_collected = 0
503-
self.gc_uncollectable = 0
504-
t0 = time.time()
505-
try:
506-
gc.callbacks.append(bound_gc_callback)
507-
# saved_cache is mutated in place.
508-
res = mypy.build.build(sources, self.options,
509-
saved_cache=self.saved_cache,
510-
alt_lib_path=alt_lib_path)
511-
msgs = res.errors
512-
self.last_manager = res.manager # type: Optional[mypy.build.BuildManager]
513-
except mypy.errors.CompileError as err:
514-
msgs = err.messages
515-
self.last_manager = None
516-
finally:
517-
while bound_gc_callback in gc.callbacks:
518-
gc.callbacks.remove(bound_gc_callback)
519-
t1 = time.time()
507+
self.last_manager = None
508+
with GcLogger() as gc_result:
509+
try:
510+
# saved_cache is mutated in place.
511+
res = mypy.build.build(sources, self.options,
512+
saved_cache=self.saved_cache,
513+
alt_lib_path=alt_lib_path)
514+
msgs = res.errors
515+
self.last_manager = res.manager # type: Optional[mypy.build.BuildManager]
516+
except mypy.errors.CompileError as err:
517+
msgs = err.messages
520518
if msgs:
521519
msgs.append("")
522520
response = {'out': "\n".join(msgs), 'err': "", 'status': 1}
523521
else:
524522
response = {'out': "", 'err': "", 'status': 0}
525-
response['build_time'] = t1 - t0
526-
response['gc_time'] = self.gc_time
527-
response['gc_calls'] = self.gc_calls
528-
response['gc_collected'] = self.gc_collected
529-
response['gc_uncollectable'] = self.gc_uncollectable
523+
response.update(gc_result.get_stats())
530524
response.update(get_meminfo())
531525
if self.last_manager is not None:
532526
response.update(self.last_manager.stats_summary())
@@ -537,20 +531,6 @@ def cmd_hang(self) -> Dict[str, object]:
537531
time.sleep(100)
538532
return {}
539533

540-
def gc_callback(self, phase: str, info: Mapping[str, int]) -> None:
541-
if phase == 'start':
542-
assert self.gc_start_time is None, "Start phase out of sequence"
543-
self.gc_start_time = time.time()
544-
elif phase == 'stop':
545-
assert self.gc_start_time is not None, "Stop phase out of sequence"
546-
self.gc_calls += 1
547-
self.gc_time += time.time() - self.gc_start_time
548-
self.gc_start_time = None
549-
self.gc_collected += info['collected']
550-
self.gc_uncollectable += info['uncollectable']
551-
else:
552-
assert False, "Unrecognized gc phase (%r)" % (phase,)
553-
554534

555535
# Misc utilities.
556536

@@ -570,6 +550,48 @@ def receive(sock: socket.socket) -> Any:
570550
return data
571551

572552

553+
class GcLogger:
554+
"""Context manager to log GC stats and overall time."""
555+
556+
def __enter__(self) -> 'GcLogger':
557+
self.gc_start_time = None # type: Optional[float]
558+
self.gc_time = 0.0
559+
self.gc_calls = 0
560+
self.gc_collected = 0
561+
self.gc_uncollectable = 0
562+
gc.callbacks.append(self.gc_callback)
563+
self.start_time = time.time()
564+
return self
565+
566+
def gc_callback(self, phase: str, info: Mapping[str, int]) -> None:
567+
if phase == 'start':
568+
assert self.gc_start_time is None, "Start phase out of sequence"
569+
self.gc_start_time = time.time()
570+
elif phase == 'stop':
571+
assert self.gc_start_time is not None, "Stop phase out of sequence"
572+
self.gc_calls += 1
573+
self.gc_time += time.time() - self.gc_start_time
574+
self.gc_start_time = None
575+
self.gc_collected += info['collected']
576+
self.gc_uncollectable += info['uncollectable']
577+
else:
578+
assert False, "Unrecognized gc phase (%r)" % (phase,)
579+
580+
def __exit__(self, *args: object) -> None:
581+
while self.gc_callback in gc.callbacks:
582+
gc.callbacks.remove(self.gc_callback)
583+
584+
def get_stats(self) -> Dict[str, float]:
585+
end_time = time.time()
586+
result = {}
587+
result['gc_time'] = self.gc_time
588+
result['gc_calls'] = self.gc_calls
589+
result['gc_collected'] = self.gc_collected
590+
result['gc_uncollectable'] = self.gc_uncollectable
591+
result['build_time'] = end_time - self.start_time
592+
return result
593+
594+
573595
MiB = 2**20
574596

575597

0 commit comments

Comments
 (0)