Mercurial > hg
view contrib/import-checker.py @ 20182:04036798ebed
branches: avoid unnecessary changectx.branch() calls
This requires reading from the changelog, which can be costly over NFS.
Note that this does not totally remove reading from the changelog; we
still do that when calling changectx.closesbranch(). That call will be
removed in a later patch.
Running hg branches on the PyPy repo (with 996) over a busy NFS server,
before this change:
$ time hg --profile branches > /dev/null
CallCount Recursive Total(s) Inline(s) module:lineno(function)
2042 0 2.2827 2.2827 <open>
2036 0 0.9840 0.9840 <method 'close' of 'file' objects>
2036 0 0.0464 0.0464 <method 'read' of 'file' objects>
5233 0 0.1985 0.0453 mercurial.repoview:161(changelog)
10462 0 0.0791 0.0314 mercurial.changelog:133(tip)
5233 0 0.0388 0.0176 mercurial.localrepo:26(__get__)
10462 0 0.0250 0.0126 <len>
5233 0 0.0059 0.0039 mercurial.repoview:112(filterrevs)
10462 0 0.0029 0.0029 <hash>
2034 0 0.0444 0.0444 <method 'seek' of 'file' objects>
5340 0 0.0390 0.0390 mercurial.revlog:296(rev)
2582 0 0.0371 0.0371 <zlib.decompress>
3155 0 0.1963 0.0366 mercurial.context:202(__init__)
3155 0 0.1238 0.0306 mercurial.repoview:161(changelog)
3155 0 0.0261 0.0080 mercurial.changelog:183(rev)
9465 0 0.0061 0.0061 <isinstance>
1096 0 0.0023 0.0023 <binascii.unhexlify>
4251 0 0.0014 0.0014 <len>
2059 0 3.7341 0.0332 mercurial.changelog:270(read)
2059 0 3.6304 0.0307 mercurial.revlog:907(revision)
2057 0 0.0262 0.0137 mercurial.changelog:28(decodeextra)
4118 0 0.0094 0.0094 <method 'split' of 'str' objects>
4118 0 0.0270 0.0048 mercurial.encoding:61(tolocal)
2059 0 0.0040 0.0040 <method 'index' of 'str' objects>
10462 0 0.0791 0.0314 mercurial.changelog:133(tip)
10462 0 0.0289 0.0207 mercurial.changelog:190(node)
10462 0 0.0188 0.0091 <len>
52433 20932 0.0478 0.0310 <len>
20932 0 0.0221 0.0168 mercurial.revlog:262(__len__)
2059 0 3.6304 0.0307 mercurial.revlog:907(revision)
real 0m4.361s
user 0m0.986s
sys 0m0.237s
After this change:
$ time hg --profile branches > /dev/null
CallCount Recursive Total(s) Inline(s) module:lineno(function)
1069 0 1.1098 1.1098 <open>
1063 0 0.4865 0.4865 <method 'close' of 'file' objects>
4122 0 0.1811 0.0404 mercurial.repoview:161(changelog)
8240 0 0.0712 0.0272 mercurial.changelog:133(tip)
4122 0 0.0378 0.0177 mercurial.localrepo:26(__get__)
8240 0 0.0221 0.0115 <len>
4122 0 0.0057 0.0033 mercurial.repoview:112(filterrevs)
8240 0 0.0025 0.0025 <hash>
3029 0 0.1979 0.0371 mercurial.context:202(__init__)
3029 0 0.1278 0.0310 mercurial.repoview:161(changelog)
3029 0 0.0230 0.0081 mercurial.changelog:183(rev)
9087 0 0.0061 0.0061 <isinstance>
1096 0 0.0026 0.0026 <binascii.unhexlify>
4125 0 0.0014 0.0014 <len>
4229 0 0.0337 0.0337 mercurial.revlog:296(rev)
1061 0 0.0296 0.0296 <method 'seek' of 'file' objects>
1063 0 0.0292 0.0292 <method 'read' of 'file' objects>
8240 0 0.0712 0.0272 mercurial.changelog:133(tip)
8240 0 0.0271 0.0196 mercurial.changelog:190(node)
8240 0 0.0169 0.0083 <len>
40476 16488 0.0422 0.0271 <len>
16488 0 0.0193 0.0152 mercurial.revlog:262(__len__)
1342 0 0.0241 0.0241 <zlib.decompress>
9445 0 0.0336 0.0224 mercurial.changelog:190(node)
9445 0 0.0112 0.0112 mercurial.revlog:317(node)
1074 0 1.9102 0.0224 mercurial.changelog:270(read)
1074 0 1.8397 0.0202 mercurial.revlog:907(revision)
1073 0 0.0187 0.0099 mercurial.changelog:28(decodeextra)
2148 0 0.0061 0.0061 <method 'split' of 'str' objects>
2148 0 0.0184 0.0034 mercurial.encoding:61(tolocal)
real 0m2.402s
user 0m0.735s
sys 0m0.177s
author | Brodie Rao <brodie@sf.io> |
---|---|
date | Fri, 15 Nov 2013 23:18:08 -0500 |
parents | c65a6937b828 |
children | 761f2929a6ad |
line wrap: on
line source
import ast import os import sys def dotted_name_of_path(path): """Given a relative path to a source file, return its dotted module name. >>> dotted_name_of_path('mercurial/error.py') 'mercurial.error' """ parts = path.split('/') parts[-1] = parts[-1][:-3] # remove .py return '.'.join(parts) def list_stdlib_modules(): """List the modules present in the stdlib. >>> mods = set(list_stdlib_modules()) >>> 'BaseHTTPServer' in mods True os.path isn't really a module, so it's missing: >>> 'os.path' in mods False sys requires special treatment, because it's baked into the interpreter, but it should still appear: >>> 'sys' in mods True >>> 'collections' in mods True >>> 'cStringIO' in mods True """ for m in sys.builtin_module_names: yield m # These modules only exist on windows, but we should always # consider them stdlib. for m in ['msvcrt', '_winreg']: yield m # These get missed too for m in 'ctypes', 'email': yield m yield 'builtins' # python3 only for libpath in sys.path: # We want to walk everything in sys.path that starts with # either sys.prefix or sys.exec_prefix. if not (libpath.startswith(sys.prefix) or libpath.startswith(sys.exec_prefix)): continue if 'site-packages' in libpath: continue for top, dirs, files in os.walk(libpath): for name in files: if name == '__init__.py': continue if not (name.endswith('.py') or name.endswith('.so')): continue full_path = os.path.join(top, name) if 'site-packages' in full_path: continue rel_path = full_path[len(libpath) + 1:] mod = dotted_name_of_path(rel_path) yield mod stdlib_modules = set(list_stdlib_modules()) def imported_modules(source, ignore_nested=False): """Given the source of a file as a string, yield the names imported by that file. Args: source: The python source to examine as a string. ignore_nested: If true, import statements that do not start in column zero will be ignored. Returns: A list of module names imported by the given source. >>> sorted(imported_modules( ... 'import foo ; from baz import bar; import foo.qux')) ['baz.bar', 'foo', 'foo.qux'] >>> sorted(imported_modules( ... '''import foo ... def wat(): ... import bar ... ''', ignore_nested=True)) ['foo'] """ for node in ast.walk(ast.parse(source)): if ignore_nested and getattr(node, 'col_offset', 0) > 0: continue if isinstance(node, ast.Import): for n in node.names: yield n.name elif isinstance(node, ast.ImportFrom): prefix = node.module + '.' for n in node.names: yield prefix + n.name def verify_stdlib_on_own_line(source): """Given some python source, verify that stdlib imports are done in separate statements from relative local module imports. Observing this limitation is important as it works around an annoying lib2to3 bug in relative import rewrites: http://bugs.python.org/issue19510. >>> list(verify_stdlib_on_own_line('import sys, foo')) ['mixed stdlib and relative imports:\\n foo, sys'] >>> list(verify_stdlib_on_own_line('import sys, os')) [] >>> list(verify_stdlib_on_own_line('import foo, bar')) [] """ for node in ast.walk(ast.parse(source)): if isinstance(node, ast.Import): from_stdlib = {} for n in node.names: from_stdlib[n.name] = n.name in stdlib_modules num_std = len([x for x in from_stdlib.values() if x]) if num_std not in (len(from_stdlib.values()), 0): yield ('mixed stdlib and relative imports:\n %s' % ', '.join(sorted(from_stdlib.iterkeys()))) class CircularImport(Exception): pass def cyclekey(names): return tuple(sorted(set(names))) def check_one_mod(mod, imports, path=None, ignore=None): if path is None: path = [] if ignore is None: ignore = [] path = path + [mod] for i in sorted(imports.get(mod, [])): if i not in stdlib_modules: i = mod.rsplit('.', 1)[0] + '.' + i if i in path: firstspot = path.index(i) cycle = path[firstspot:] + [i] if cyclekey(cycle) not in ignore: raise CircularImport(cycle) continue check_one_mod(i, imports, path=path, ignore=ignore) def rotatecycle(cycle): """arrange a cycle so that the lexicographically first module listed first >>> rotatecycle(['foo', 'bar', 'foo']) ['bar', 'foo', 'bar'] """ lowest = min(cycle) idx = cycle.index(lowest) return cycle[idx:] + cycle[1:idx] + [lowest] def find_cycles(imports): """Find cycles in an already-loaded import graph. >>> imports = {'top.foo': ['bar', 'os.path', 'qux'], ... 'top.bar': ['baz', 'sys'], ... 'top.baz': ['foo'], ... 'top.qux': ['foo']} >>> print '\\n'.join(sorted(find_cycles(imports))) top.bar -> top.baz -> top.foo -> top.bar -> top.bar top.foo -> top.qux -> top.foo -> top.foo """ cycles = {} for mod in sorted(imports.iterkeys()): try: check_one_mod(mod, imports, ignore=cycles) except CircularImport, e: cycle = e.args[0] cycles[cyclekey(cycle)] = ' -> '.join(rotatecycle(cycle)) return cycles.values() def _cycle_sortkey(c): return len(c), c def main(argv): if len(argv) < 2: print 'Usage: %s file [file] [file] ...' return 1 used_imports = {} any_errors = False for source_path in argv[1:]: f = open(source_path) modname = dotted_name_of_path(source_path) src = f.read() used_imports[modname] = sorted( imported_modules(src, ignore_nested=True)) for error in verify_stdlib_on_own_line(src): any_errors = True print source_path, error f.close() cycles = find_cycles(used_imports) if cycles: firstmods = set() for c in sorted(cycles, key=_cycle_sortkey): first = c.split()[0] # As a rough cut, ignore any cycle that starts with the # same module as some other cycle. Otherwise we see lots # of cycles that are effectively duplicates. if first in firstmods: continue print 'Import cycle:', c firstmods.add(first) any_errors = True return not any_errors if __name__ == '__main__': sys.exit(int(main(sys.argv)))