Mercurial > hg
view hgext/inotify/server.py @ 16719:e7bf09acd410
localrepo: add branchtip() method for faster single-branch lookups
For the PyPy repo with 744 branches and 843 branch heads, this brings
hg log -r default over NFS from:
CallCount Recursive Total(ms) Inline(ms) module:lineno(function)
3249 0 1.3222 1.3222 <open>
3244 0 0.6211 0.6211 <method 'close' of 'file' objects>
3243 0 0.0800 0.0800 <method 'read' of 'file' objects>
3241 0 0.0660 0.0660 <method 'seek' of 'file' objects>
3905 0 0.0476 0.0476 <zlib.decompress>
3281 0 2.6756 0.0472 mercurial.changelog:182(read)
+3281 0 2.5256 0.0453 +mercurial.revlog:881(revision)
+3276 0 0.0389 0.0196 +mercurial.changelog:28(decodeextra)
+6562 0 0.0123 0.0123 +<method 'split' of 'str' objects>
+6562 0 0.0408 0.0073 +mercurial.encoding:61(tolocal)
+3281 0 0.0054 0.0054 +<method 'index' of 'str' objects>
3241 0 2.2464 0.0456 mercurial.revlog:818(_loadchunk)
+3241 0 0.6205 0.6205 +<method 'close' of 'file' objects>
+3241 0 0.0765 0.0765 +<method 'read' of 'file' objects>
+3241 0 0.0660 0.0660 +<method 'seek' of 'file' objects>
+3241 0 1.4209 0.0135 +mercurial.store:374(__call__)
+3241 0 0.0122 0.0107 +mercurial.revlog:810(_addchunk)
3281 0 2.5256 0.0453 mercurial.revlog:881(revision)
+3280 0 0.0175 0.0175 +mercurial.revlog:305(rev)
+3281 0 2.2819 0.0119 +mercurial.revlog:847(_chunkraw)
+3281 0 0.0603 0.0083 +mercurial.revlog:945(_checkhash)
+3281 0 0.0051 0.0051 +mercurial.revlog:349(flags)
+3281 0 0.0040 0.0040 +<mercurial.mpatch.patches>
13682 0 0.0479 0.0248 <method 'decode' of 'str' objects>
+7418 0 0.0228 0.0076 +encodings.utf_8:15(decode)
+1 0 0.0003 0.0000 +encodings:71(search_function)
3248 0 1.3995 0.0246 mercurial.scmutil:218(__call__)
+3248 0 1.3222 1.3222 +<open>
+3248 0 0.0235 0.0184 +os.path:80(split)
+3248 0 0.0084 0.0068 +mercurial.scmutil:92(__call__)
Time: real 2.750 secs (user 0.680+0.000 sys 0.360+0.000)
down to:
CallCount Recursive Total(ms) Inline(ms) module:lineno(function)
55 31 0.0197 0.0163 <__import__>
+1 0 0.0006 0.0002 +mercurial.context:8(<module>)
+1 0 0.0042 0.0001 +mercurial.revlog:12(<module>)
+1 0 0.0002 0.0001 +mercurial.match:8(<module>)
+1 0 0.0003 0.0001 +mercurial.dirstate:7(<module>)
+1 0 0.0057 0.0001 +mercurial.changelog:8(<module>)
1 0 0.0117 0.0032 mercurial.localrepo:525(_readbranchcache)
+844 0 0.0015 0.0015 +<binascii.unhexlify>
+845 0 0.0010 0.0010 +<method 'split' of 'str' objects>
+843 0 0.0045 0.0009 +mercurial.encoding:61(tolocal)
+843 0 0.0004 0.0004 +<method 'setdefault' of 'dict' objects>
+1 0 0.0003 0.0003 +<method 'close' of 'file' objects>
3 0 0.0029 0.0029 <method 'read' of 'file' objects>
9 0 0.0018 0.0018 <open>
990 0 0.0017 0.0017 <binascii.unhexlify>
53 0 0.0016 0.0016 mercurial.demandimport:43(__init__)
862 0 0.0015 0.0015 <_codecs.utf_8_decode>
862 0 0.0037 0.0014 <method 'decode' of 'str' objects>
+862 0 0.0023 0.0008 +encodings.utf_8:15(decode)
981 0 0.0011 0.0011 <method 'split' of 'str' objects>
861 0 0.0046 0.0009 mercurial.encoding:61(tolocal)
+861 0 0.0037 0.0014 +<method 'decode' of 'str' objects>
862 0 0.0023 0.0008 encodings.utf_8:15(decode)
+862 0 0.0015 0.0015 +<_codecs.utf_8_decode>
4 0 0.0008 0.0008 <method 'close' of 'file' objects>
179 154 0.0202 0.0004 mercurial.demandimport:83(__getattribute__)
+36 11 0.0199 0.0003 +mercurial.demandimport:55(_load)
+72 0 0.0001 0.0001 +mercurial.demandimport:83(__getattribute__)
+36 0 0.0000 0.0000 +<getattr>
1 0 0.0015 0.0004 mercurial.tags:148(_readtagcache)
Time: real 0.060 secs (user 0.030+0.000 sys 0.010+0.000)
author | Brodie Rao <brodie@sf.io> |
---|---|
date | Sun, 13 May 2012 14:04:04 +0200 |
parents | cfb6682961b8 |
children | cd53e40ab0e2 |
line wrap: on
line source
# server.py - common entry point for inotify status server # # Copyright 2009 Nicolas Dumazet <nicdumz@gmail.com> # # This software may be used and distributed according to the terms of the # GNU General Public License version 2 or any later version. from mercurial.i18n import _ from mercurial import cmdutil, osutil, util import common import errno import os import socket import stat import struct import sys import tempfile class AlreadyStartedException(Exception): pass class TimeoutException(Exception): pass def join(a, b): if a: if a[-1] == '/': return a + b return a + '/' + b return b def split(path): c = path.rfind('/') if c == -1: return '', path return path[:c], path[c + 1:] walk_ignored_errors = (errno.ENOENT, errno.ENAMETOOLONG) def walk(dirstate, absroot, root): '''Like os.walk, but only yields regular files.''' # This function is critical to performance during startup. def walkit(root, reporoot): files, dirs = [], [] try: fullpath = join(absroot, root) for name, kind in osutil.listdir(fullpath): if kind == stat.S_IFDIR: if name == '.hg': if not reporoot: return else: dirs.append(name) path = join(root, name) if dirstate._ignore(path): continue for result in walkit(path, False): yield result elif kind in (stat.S_IFREG, stat.S_IFLNK): files.append(name) yield fullpath, dirs, files except OSError, err: if err.errno == errno.ENOTDIR: # fullpath was a directory, but has since been replaced # by a file. yield fullpath, dirs, files elif err.errno not in walk_ignored_errors: raise return walkit(root, root == '') class directory(object): """ Representing a directory * path is the relative path from repo root to this directory * files is a dict listing the files in this directory - keys are file names - values are file status * dirs is a dict listing the subdirectories - key are subdirectories names - values are directory objects """ def __init__(self, relpath=''): self.path = relpath self.files = {} self.dirs = {} def dir(self, relpath): """ Returns the directory contained at the relative path relpath. Creates the intermediate directories if necessary. """ if not relpath: return self l = relpath.split('/') ret = self while l: next = l.pop(0) try: ret = ret.dirs[next] except KeyError: d = directory(join(ret.path, next)) ret.dirs[next] = d ret = d return ret def walk(self, states, visited=None): """ yield (filename, status) pairs for items in the trees that have status in states. filenames are relative to the repo root """ for file, st in self.files.iteritems(): if st in states: yield join(self.path, file), st for dir in self.dirs.itervalues(): if visited is not None: visited.add(dir.path) for e in dir.walk(states): yield e def lookup(self, states, path, visited): """ yield root-relative filenames that match path, and whose status are in states: * if path is a file, yield path * if path is a directory, yield directory files * if path is not tracked, yield nothing """ if path[-1] == '/': path = path[:-1] paths = path.split('/') # we need to check separately for last node last = paths.pop() tree = self try: for dir in paths: tree = tree.dirs[dir] except KeyError: # path is not tracked visited.add(tree.path) return try: # if path is a directory, walk it target = tree.dirs[last] visited.add(target.path) for file, st in target.walk(states, visited): yield file except KeyError: try: if tree.files[last] in states: # path is a file visited.add(tree.path) yield path except KeyError: # path is not tracked pass class repowatcher(object): """ Watches inotify events """ statuskeys = 'almr!?' def __init__(self, ui, dirstate, root): self.ui = ui self.dirstate = dirstate self.wprefix = join(root, '') self.prefixlen = len(self.wprefix) self.tree = directory() self.statcache = {} self.statustrees = dict([(s, directory()) for s in self.statuskeys]) self.ds_info = self.dirstate_info() self.last_event = None def handle_timeout(self): pass def dirstate_info(self): try: st = os.lstat(self.wprefix + '.hg/dirstate') return st.st_mtime, st.st_ino except OSError, err: if err.errno != errno.ENOENT: raise return 0, 0 def filestatus(self, fn, st): try: type_, mode, size, time = self.dirstate._map[fn][:4] except KeyError: type_ = '?' if type_ == 'n': st_mode, st_size, st_mtime = st if size == -1: return 'l' if size and (size != st_size or (mode ^ st_mode) & 0100): return 'm' if time != int(st_mtime): return 'l' return 'n' if type_ == '?' and self.dirstate._dirignore(fn): # we must check not only if the file is ignored, but if any part # of its path match an ignore pattern return 'i' return type_ def updatefile(self, wfn, osstat): ''' update the file entry of an existing file. osstat: (mode, size, time) tuple, as returned by os.lstat(wfn) ''' self._updatestatus(wfn, self.filestatus(wfn, osstat)) def deletefile(self, wfn, oldstatus): ''' update the entry of a file which has been deleted. oldstatus: char in statuskeys, status of the file before deletion ''' if oldstatus == 'r': newstatus = 'r' elif oldstatus in 'almn': newstatus = '!' else: newstatus = None self.statcache.pop(wfn, None) self._updatestatus(wfn, newstatus) def _updatestatus(self, wfn, newstatus): ''' Update the stored status of a file. newstatus: - char in (statuskeys + 'ni'), new status to apply. - or None, to stop tracking wfn ''' root, fn = split(wfn) d = self.tree.dir(root) oldstatus = d.files.get(fn) # oldstatus can be either: # - None : fn is new # - a char in statuskeys: fn is a (tracked) file if self.ui.debugflag and oldstatus != newstatus: self.ui.note(_('status: %r %s -> %s\n') % (wfn, oldstatus, newstatus)) if oldstatus and oldstatus in self.statuskeys \ and oldstatus != newstatus: del self.statustrees[oldstatus].dir(root).files[fn] if newstatus in (None, 'i'): d.files.pop(fn, None) elif oldstatus != newstatus: d.files[fn] = newstatus if newstatus != 'n': self.statustrees[newstatus].dir(root).files[fn] = newstatus def check_deleted(self, key): # Files that had been deleted but were present in the dirstate # may have vanished from the dirstate; we must clean them up. nuke = [] for wfn, ignore in self.statustrees[key].walk(key): if wfn not in self.dirstate: nuke.append(wfn) for wfn in nuke: root, fn = split(wfn) del self.statustrees[key].dir(root).files[fn] del self.tree.dir(root).files[fn] def update_hgignore(self): # An update of the ignore file can potentially change the # states of all unknown and ignored files. # XXX If the user has other ignore files outside the repo, or # changes their list of ignore files at run time, we'll # potentially never see changes to them. We could get the # client to report to us what ignore data they're using. # But it's easier to do nothing than to open that can of # worms. if '_ignore' in self.dirstate.__dict__: delattr(self.dirstate, '_ignore') self.ui.note(_('rescanning due to .hgignore change\n')) self.handle_timeout() self.scan() def getstat(self, wpath): try: return self.statcache[wpath] except KeyError: try: return self.stat(wpath) except OSError, err: if err.errno != errno.ENOENT: raise def stat(self, wpath): try: st = os.lstat(join(self.wprefix, wpath)) ret = st.st_mode, st.st_size, st.st_mtime self.statcache[wpath] = ret return ret except OSError: self.statcache.pop(wpath, None) raise class socketlistener(object): """ Listens for client queries on unix socket inotify.sock """ def __init__(self, ui, root, repowatcher, timeout): self.ui = ui self.repowatcher = repowatcher self.sock = socket.socket(socket.AF_UNIX) self.sockpath = join(root, '.hg/inotify.sock') self.realsockpath = self.sockpath if os.path.islink(self.sockpath): if os.path.exists(self.sockpath): self.realsockpath = os.readlink(self.sockpath) else: raise util.Abort('inotify-server: cannot start: ' '.hg/inotify.sock is a broken symlink') try: self.sock.bind(self.realsockpath) except socket.error, err: if err.args[0] == errno.EADDRINUSE: raise AlreadyStartedException(_('cannot start: socket is ' 'already bound')) if err.args[0] == "AF_UNIX path too long": tempdir = tempfile.mkdtemp(prefix="hg-inotify-") self.realsockpath = os.path.join(tempdir, "inotify.sock") try: self.sock.bind(self.realsockpath) os.symlink(self.realsockpath, self.sockpath) except (OSError, socket.error), inst: try: os.unlink(self.realsockpath) except OSError: pass os.rmdir(tempdir) if inst.errno == errno.EEXIST: raise AlreadyStartedException(_('cannot start: tried ' 'linking .hg/inotify.sock to a temporary socket but' ' .hg/inotify.sock already exists')) raise else: raise self.sock.listen(5) self.fileno = self.sock.fileno def answer_stat_query(self, cs): names = cs.read().split('\0') states = names.pop() self.ui.note(_('answering query for %r\n') % states) visited = set() if not names: def genresult(states, tree): for fn, state in tree.walk(states): yield fn else: def genresult(states, tree): for fn in names: for f in tree.lookup(states, fn, visited): yield f return ['\0'.join(r) for r in [ genresult('l', self.repowatcher.statustrees['l']), genresult('m', self.repowatcher.statustrees['m']), genresult('a', self.repowatcher.statustrees['a']), genresult('r', self.repowatcher.statustrees['r']), genresult('!', self.repowatcher.statustrees['!']), '?' in states and genresult('?', self.repowatcher.statustrees['?']) or [], [], 'c' in states and genresult('n', self.repowatcher.tree) or [], visited ]] def answer_dbug_query(self): return ['\0'.join(self.repowatcher.debug())] def accept_connection(self): sock, addr = self.sock.accept() cs = common.recvcs(sock) version = ord(cs.read(1)) if version != common.version: self.ui.warn(_('received query from incompatible client ' 'version %d\n') % version) try: # try to send back our version to the client # this way, the client too is informed of the mismatch sock.sendall(chr(common.version)) except socket.error: pass return type = cs.read(4) if type == 'STAT': results = self.answer_stat_query(cs) elif type == 'DBUG': results = self.answer_dbug_query() else: self.ui.warn(_('unrecognized query type: %s\n') % type) return try: try: v = chr(common.version) sock.sendall(v + type + struct.pack(common.resphdrfmts[type], *map(len, results))) sock.sendall(''.join(results)) finally: sock.shutdown(socket.SHUT_WR) except socket.error, err: if err.args[0] != errno.EPIPE: raise if sys.platform.startswith('linux'): import linuxserver as _server else: raise ImportError master = _server.master def start(ui, dirstate, root, opts): timeout = opts.get('idle_timeout') if timeout: timeout = float(timeout) * 60000 else: timeout = None class service(object): def init(self): try: self.master = master(ui, dirstate, root, timeout) except AlreadyStartedException, inst: raise util.Abort("inotify-server: %s" % inst) def run(self): try: try: self.master.run() except TimeoutException: pass finally: self.master.shutdown() if 'inserve' not in sys.argv: runargs = util.hgcmd() + ['inserve', '-R', root] else: runargs = util.hgcmd() + sys.argv[1:] pidfile = ui.config('inotify', 'pidfile') if opts['daemon'] and pidfile is not None and 'pid-file' not in runargs: runargs.append("--pid-file=%s" % pidfile) service = service() logfile = ui.config('inotify', 'log') appendpid = ui.configbool('inotify', 'appendpid', False) ui.debug('starting inotify server: %s\n' % ' '.join(runargs)) cmdutil.service(opts, initfn=service.init, runfn=service.run, logfile=logfile, runargs=runargs, appendpid=appendpid)