Skip to content

Add daemon mode -- highly experimental #4130

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 42 commits into from
Oct 20, 2017
Merged

Add daemon mode -- highly experimental #4130

merged 42 commits into from
Oct 20, 2017

Conversation

gvanrossum
Copy link
Member

Many things probably need to change still, but I'd like to get this in before the merge conflicts get out of hand (there are a fair number of small changes to build.py, and 1-2 to main.py).

Guido van Rossum added 30 commits October 13, 2017 11:24
- Don't die if client closed commection.
- Support `@file` to read arguments from a file.
- Call build.build() directly.
orig_stat = os.stat


def stat_proxy(path: str) -> os.stat_result:
Copy link
Member Author

Choose a reason for hiding this comment

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

Hm, we probably don't need this function. (It was an experiment to count stat() calls, and while the numbers are useful, the implementation feels pretty wrong.)

@@ -0,0 +1,308 @@
"""Type checker test cases"""
Copy link
Member Author

Choose a reason for hiding this comment

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

This started out as a clone of testcheck.py, but there are many small changes.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'd really like to be able to refactor the duplications in the test files in order to eventually clean up the test logic and improve the usability (#3973 being a first step). A comment about the nature of these differences may help with that.

Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks.

@@ -3020,7 +3020,6 @@ tmp/mod.py:7: error: Revealed type is 'builtins.bytes'
# cmd: mypy -m a
# cmd2: mypy -m b
# flags: --follow-imports=silent
# flags2: --follow-imports=silent
Copy link
Member Author

Choose a reason for hiding this comment

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

This was just redundant, and its presence thwarted my logic for skipping test that vary flags across runs.

data_files=data_files,
classifiers=classifiers,
cmdclass={'build_py': CustomPythonBuild},
install_requires = ['typed-ast >= 1.1.0, < 1.2.0'],
install_requires = ['typed-ast >= 1.1.0, < 1.2.0',
'psutil >= 5.4.0, < 5.5.0',
Copy link
Member Author

Choose a reason for hiding this comment

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

This is used by dmypy only, to report memory usage.

@@ -0,0 +1,20 @@
#!/usr/bin/env python3
Copy link
Member Author

Choose a reason for hiding this comment

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

Cloned from scripts/stubgen.

Copy link
Member

@emmatyping emmatyping Oct 18, 2017

Choose a reason for hiding this comment

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

You might want to also add a entry_point in setup.py if you want something runnable from an installed mypy distribution.

Copy link
Member Author

Choose a reason for hiding this comment

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

Copy link
Member

Choose a reason for hiding this comment

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

Ah! Sorry, I missed that.

Copy link
Member Author

@gvanrossum gvanrossum left a comment

Choose a reason for hiding this comment

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

@elazarg I've added some comments. In addition you should just diff testcheck.py and testdmypy.py to see some details (I scrubbed a lot of stuff because incremental is always true and the step is always >= 1 in testdmypy.py).


@classmethod
def cases(cls) -> List[DataDrivenTestCase]:
if sys.platform == 'win32':
Copy link
Member Author

Choose a reason for hiding this comment

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

This is new (dmypy is not supported on Windows, and doesn't work there).

if cls.has_stable_flags(case) and cls.is_incremental(case)]
return c

def run_case(self, testcase: DataDrivenTestCase) -> None:
Copy link
Member Author

Choose a reason for hiding this comment

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

This method is specialized for incremental==True.

tc = parse_test_cases(os.path.join(test_data_prefix, f),
None, test_temp_dir, True)
c += [case for case in tc
if cls.has_stable_flags(case) and cls.is_incremental(case)]
Copy link
Member Author

Choose a reason for hiding this comment

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

These filters were added.

raise ValueError(
'Output file {} exists though test case only has {} runs'.format(
file, num_steps))
self.server = None # type: Optional[dmypy.Server]
Copy link
Member Author

Choose a reason for hiding this comment

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

Reset the server here.

for step in range(1, num_steps + 1):
self.run_case_once(testcase, step)

@classmethod
Copy link
Member Author

Choose a reason for hiding this comment

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

Next two methods are new.

if os.path.exists(dn):
shutil.rmtree(dn)

def run_case_once(self, testcase: DataDrivenTestCase, incremental_step: int) -> None:
Copy link
Member Author

Choose a reason for hiding this comment

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

No default for incremental_step, and assert it's >= 1 below.


# Parse options after moving files (in case mypy.ini is being moved).
options = self.parse_options(original_program_text, testcase, incremental_step)
if incremental_step == 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.

Initialize server here.

for module_name, program_path, program_text in module_data:
# Always set to none so we're forced to reread the module in incremental mode
sources.append(build.BuildSource(program_path, module_name, None))
response = self.server.check(sources, alt_lib_path=test_temp_dir)
Copy link
Member Author

Choose a reason for hiding this comment

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

Call server.check() instead of build.build().

update_testcase_output(testcase, a)
assert_string_arrays_equal(output, a, msg.format(testcase.file, testcase.line))

manager = self.server.last_manager
Copy link
Member Author

Choose a reason for hiding this comment

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

Get manager from server instead of from BuildResult.

all(flag not in flag_list for flag in ['--python-version', '-2', '--py2'])):
options.python_version = testcase_pyversion(testcase.file, testcase.name)

options.use_builtins_fixtures = True
Copy link
Member Author

Choose a reason for hiding this comment

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

Hardcoding these flags here (rather than way up there).

Copy link
Collaborator

@JukkaL JukkaL left a comment

Choose a reason for hiding this comment

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

This looks like a good starting point for the daemon mode. Left a bunch of minor comments.

mypy/dmypy.py Outdated
# Argument parser. Subparsers are tied to action functions by the
# @action(subparse) decorator.

parser = argparse.ArgumentParser(description="Client for mymy daemon mode",
Copy link
Collaborator

Choose a reason for hiding this comment

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

Typo: mymy

mypy/dmypy.py Outdated
sys.stderr.flush()
pid = os.fork()
if pid:
npid, sts = os.waitpid(pid, 0)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Add comment saying that this happens in the original/current process.

mypy/dmypy.py Outdated
# Misc utilities.

def receive(sock: socket.socket) -> Any:
"""Receive data from a socket until EOF."""
Copy link
Collaborator

Choose a reason for hiding this comment

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

Document that this reads JSON data.

if method is None:
return {'error': "Unrecognized command '%s'" % command}
else:
return method(self, **data)
Copy link
Collaborator

Choose a reason for hiding this comment

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

The contents of data are not verified here, though elsewhere JSON is checked pretty carefully. At least add a comment about this here.

Copy link
Collaborator

Choose a reason for hiding this comment

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

This is still pending, but this can be merged anyway.

mypy/build.py Outdated
@@ -122,7 +123,9 @@ def is_source(self, file: MypyFile) -> bool:
def build(sources: List[BuildSource],
options: Options,
alt_lib_path: Optional[str] = None,
bin_dir: Optional[str] = None) -> BuildResult:
bin_dir: Optional[str] = None,
saved_cache: Optional['SavedCache'] = None,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Document saved_cache. Are we making some assumptions about it? Does it get mutated?

mypy/dmypy.py Outdated

import mypy.build
import mypy.errors
import mypy.main
Copy link
Collaborator

Choose a reason for hiding this comment

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

Eventually we could import most mypy modules lazily so that the client would run quicker in case the server is already running.

"""
try:
pid, sockname = get_status()
except SystemExit as err:
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'd prefer having a separate exception class for daemon errors, and a top-level try/except that will translate it to SystemExit.

mypy/dmypy.py Outdated
def check(self, sources: List[mypy.build.BuildSource],
alt_lib_path: Optional[str] = None) -> Dict[str, Any]:
bound_gc_callback = lambda phase, info: self.gc_callback(phase, info)
self.gc_start_time = None # type: Optional[float]
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'd recommend moving the stats handling code to separate methods to keep the logic of this method less cluttered.

@JukkaL
Copy link
Collaborator

JukkaL commented Oct 19, 2017

I encountered something that looks like a performance problem. I did these steps:

  • Remove mypy cache.
  • Create a tiny new file mypy/x.py with a single function that generates a type check error.
  • Run scripts/dmypy check mypy (takes about 13 seconds). It generates an error as expected.
  • Run scripts/dmypy recheck. It still takes about 13 seconds but I'd expect it to only take a fraction of a second, since only mypy.x needs to be processed.

@gvanrossum
Copy link
Member Author

The performance problem is apparently not new -- I can reproduce this with standard -i mode as well. No cache entries for the mypy package are written somehow -- perhaps because mypy.x precedes all other submodules of mypy. This could be due to an edge case in the topsort.

@JukkaL
Copy link
Collaborator

JukkaL commented Oct 19, 2017

In that case I'll create a new issue to track it.

@JukkaL
Copy link
Collaborator

JukkaL commented Oct 19, 2017

Created #4135 to track the performance issue.

@JukkaL JukkaL merged commit 2dbeaf7 into python:master Oct 20, 2017
JelleZijlstra added a commit that referenced this pull request Oct 20, 2017
This is causing typeshed's build to fail (e.g. python/typeshed#1680). Followup from #4130.
gvanrossum pushed a commit that referenced this pull request Oct 20, 2017
This is causing typeshed's build to fail (e.g. python/typeshed#1680). Followup from #4130.
@@ -339,6 +350,27 @@ def default_lib_path(data_dir: str,
# silent mode or simply not found.


def cache_meta_from_dict(meta: Dict[str, Any], data_json: str) -> CacheMeta:
sentinel = None # type: Any # the values will be post-validated below
Copy link
Member

Choose a reason for hiding this comment

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

This comment should be now "validated by the caller" or similar.

gvanrossum pushed a commit to gvanrossum/mypy that referenced this pull request Oct 20, 2017
gvanrossum pushed a commit to gvanrossum/mypy that referenced this pull request Oct 24, 2017
gvanrossum pushed a commit to gvanrossum/mypy that referenced this pull request Oct 25, 2017
gvanrossum pushed a commit to gvanrossum/mypy that referenced this pull request Oct 27, 2017
JukkaL pushed a commit that referenced this pull request Oct 31, 2017
This is causing typeshed's build to fail (e.g. python/typeshed#1680). Followup from #4130.
gvanrossum pushed a commit to gvanrossum/mypy that referenced this pull request Nov 1, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants