Mercurial > hg
view hgext/hgk.py @ 29830:92ac2baaea86
revlog: use an LRU cache for delta chain bases
Profiling using statprof revealed a hotspot during changegroup
application calculating delta chain bases on generaldelta repos.
Essentially, revlog._addrevision() was performing a lot of redundant
work tracing the delta chain as part of determining when the chain
distance was acceptable. This was most pronounced when adding
revisions to manifests, which can have delta chains thousands of
revisions long.
There was a delta chain base cache on revlogs before, but it only
captured a single revision. This was acceptable before generaldelta,
when _addrevision would build deltas from the previous revision and
thus we'd pretty much guarantee a cache hit when resolving the delta
chain base on a subsequent _addrevision call. However, it isn't
suitable for generaldelta because parent revisions aren't necessarily
the last processed revision.
This patch converts the delta chain base cache to an LRU dict cache.
The cache can hold multiple entries, so generaldelta repos have a
higher chance of getting a cache hit.
The impact of this change when processing changegroup additions is
significant. On a generaldelta conversion of the "mozilla-unified"
repo (which contains heads of the main Firefox repositories in
chronological order - this means there are lots of transitions between
heads in revlog order), this change has the following impact when
performing an `hg unbundle` of an uncompressed bundle of the repo:
before: 5:42 CPU time
after: 4:34 CPU time
Most of this time is saved when applying the changelog and manifest
revlogs:
before: 2:30 CPU time
after: 1:17 CPU time
That nearly a 50% reduction in CPU time applying changesets and
manifests!
Applying a gzipped bundle of the same repo (effectively simulating a
`hg clone` over HTTP) showed a similar speedup:
before: 5:53 CPU time
after: 4:46 CPU time
Wall time improvements were basically the same as CPU time.
I didn't measure explicitly, but it feels like most of the time
is saved when processing manifests. This makes sense, as large
manifests tend to have very long delta chains and thus benefit the
most from this cache.
So, this change effectively makes changegroup application (which is
used by `hg unbundle`, `hg clone`, `hg pull`, `hg unshelve`, and
various other commands) significantly faster when delta chains are
long (which can happen on repos with large numbers of files and thus
large manifests).
In theory, this change can result in more memory utilization. However,
we're caching a dict of ints. At most we have 200 ints + Python object
overhead per revlog. And, the cache is really only populated when
performing read-heavy operations, such as adding changegroups or
scanning an individual revlog. For memory bloat to be an issue, we'd
need to scan/read several revisions from several revlogs all while
having active references to several revlogs. I don't think there are
many operations that do this, so I don't think memory bloat from the
cache will be an issue.
author | Gregory Szorc <gregory.szorc@gmail.com> |
---|---|
date | Mon, 22 Aug 2016 21:48:50 -0700 |
parents | 844f72885fb9 |
children | d5883fd055c6 |
line wrap: on
line source
# Minimal support for git commands on an hg repository # # Copyright 2005, 2006 Chris Mason <mason@suse.com> # # This software may be used and distributed according to the terms of the # GNU General Public License version 2 or any later version. '''browse the repository in a graphical way The hgk extension allows browsing the history of a repository in a graphical way. It requires Tcl/Tk version 8.4 or later. (Tcl/Tk is not distributed with Mercurial.) hgk consists of two parts: a Tcl script that does the displaying and querying of information, and an extension to Mercurial named hgk.py, which provides hooks for hgk to get information. hgk can be found in the contrib directory, and the extension is shipped in the hgext repository, and needs to be enabled. The :hg:`view` command will launch the hgk Tcl script. For this command to work, hgk must be in your search path. Alternately, you can specify the path to hgk in your configuration file:: [hgk] path = /location/of/hgk hgk can make use of the extdiff extension to visualize revisions. Assuming you had already configured extdiff vdiff command, just add:: [hgk] vdiff=vdiff Revisions context menu will now display additional entries to fire vdiff on hovered and selected revisions. ''' from __future__ import absolute_import import os from mercurial.i18n import _ from mercurial.node import ( nullid, nullrev, short, ) from mercurial import ( cmdutil, commands, obsolete, patch, scmutil, ) cmdtable = {} command = cmdutil.command(cmdtable) # Note for extension authors: ONLY specify testedwith = 'internal' for # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should # be specifying the version(s) of Mercurial they are tested with, or # leave the attribute unspecified. testedwith = 'internal' @command('debug-diff-tree', [('p', 'patch', None, _('generate patch')), ('r', 'recursive', None, _('recursive')), ('P', 'pretty', None, _('pretty')), ('s', 'stdin', None, _('stdin')), ('C', 'copy', None, _('detect copies')), ('S', 'search', "", _('search'))], ('[OPTION]... NODE1 NODE2 [FILE]...'), inferrepo=True) def difftree(ui, repo, node1=None, node2=None, *files, **opts): """diff trees from two commits""" def __difftree(repo, node1, node2, files=[]): assert node2 is not None mmap = repo[node1].manifest() mmap2 = repo[node2].manifest() m = scmutil.match(repo[node1], files) modified, added, removed = repo.status(node1, node2, m)[:3] empty = short(nullid) for f in modified: # TODO get file permissions ui.write((":100664 100664 %s %s M\t%s\t%s\n") % (short(mmap[f]), short(mmap2[f]), f, f)) for f in added: ui.write((":000000 100664 %s %s N\t%s\t%s\n") % (empty, short(mmap2[f]), f, f)) for f in removed: ui.write((":100664 000000 %s %s D\t%s\t%s\n") % (short(mmap[f]), empty, f, f)) ## while True: if opts['stdin']: try: line = raw_input().split(' ') node1 = line[0] if len(line) > 1: node2 = line[1] else: node2 = None except EOFError: break node1 = repo.lookup(node1) if node2: node2 = repo.lookup(node2) else: node2 = node1 node1 = repo.changelog.parents(node1)[0] if opts['patch']: if opts['pretty']: catcommit(ui, repo, node2, "") m = scmutil.match(repo[node1], files) diffopts = patch.difffeatureopts(ui) diffopts.git = True chunks = patch.diff(repo, node1, node2, match=m, opts=diffopts) for chunk in chunks: ui.write(chunk) else: __difftree(repo, node1, node2, files=files) if not opts['stdin']: break def catcommit(ui, repo, n, prefix, ctx=None): nlprefix = '\n' + prefix if ctx is None: ctx = repo[n] # use ctx.node() instead ?? ui.write(("tree %s\n" % short(ctx.changeset()[0]))) for p in ctx.parents(): ui.write(("parent %s\n" % p)) date = ctx.date() description = ctx.description().replace("\0", "") ui.write(("author %s %s %s\n" % (ctx.user(), int(date[0]), date[1]))) if 'committer' in ctx.extra(): ui.write(("committer %s\n" % ctx.extra()['committer'])) ui.write(("revision %d\n" % ctx.rev())) ui.write(("branch %s\n" % ctx.branch())) if obsolete.isenabled(repo, obsolete.createmarkersopt): if ctx.obsolete(): ui.write(("obsolete\n")) ui.write(("phase %s\n\n" % ctx.phasestr())) if prefix != "": ui.write("%s%s\n" % (prefix, description.replace('\n', nlprefix).strip())) else: ui.write(description + "\n") if prefix: ui.write('\0') @command('debug-merge-base', [], _('REV REV')) def base(ui, repo, node1, node2): """output common ancestor information""" node1 = repo.lookup(node1) node2 = repo.lookup(node2) n = repo.changelog.ancestor(node1, node2) ui.write(short(n) + "\n") @command('debug-cat-file', [('s', 'stdin', None, _('stdin'))], _('[OPTION]... TYPE FILE'), inferrepo=True) def catfile(ui, repo, type=None, r=None, **opts): """cat a specific revision""" # in stdin mode, every line except the commit is prefixed with two # spaces. This way the our caller can find the commit without magic # strings # prefix = "" if opts['stdin']: try: (type, r) = raw_input().split(' ') prefix = " " except EOFError: return else: if not type or not r: ui.warn(_("cat-file: type or revision not supplied\n")) commands.help_(ui, 'cat-file') while r: if type != "commit": ui.warn(_("aborting hg cat-file only understands commits\n")) return 1 n = repo.lookup(r) catcommit(ui, repo, n, prefix) if opts['stdin']: try: (type, r) = raw_input().split(' ') except EOFError: break else: break # git rev-tree is a confusing thing. You can supply a number of # commit sha1s on the command line, and it walks the commit history # telling you which commits are reachable from the supplied ones via # a bitmask based on arg position. # you can specify a commit to stop at by starting the sha1 with ^ def revtree(ui, args, repo, full="tree", maxnr=0, parents=False): def chlogwalk(): count = len(repo) i = count l = [0] * 100 chunk = 100 while True: if chunk > i: chunk = i i = 0 else: i -= chunk for x in xrange(chunk): if i + x >= count: l[chunk - x:] = [0] * (chunk - x) break if full is not None: if (i + x) in repo: l[x] = repo[i + x] l[x].changeset() # force reading else: if (i + x) in repo: l[x] = 1 for x in xrange(chunk - 1, -1, -1): if l[x] != 0: yield (i + x, full is not None and l[x] or None) if i == 0: break # calculate and return the reachability bitmask for sha def is_reachable(ar, reachable, sha): if len(ar) == 0: return 1 mask = 0 for i in xrange(len(ar)): if sha in reachable[i]: mask |= 1 << i return mask reachable = [] stop_sha1 = [] want_sha1 = [] count = 0 # figure out which commits they are asking for and which ones they # want us to stop on for i, arg in enumerate(args): if arg.startswith('^'): s = repo.lookup(arg[1:]) stop_sha1.append(s) want_sha1.append(s) elif arg != 'HEAD': want_sha1.append(repo.lookup(arg)) # calculate the graph for the supplied commits for i, n in enumerate(want_sha1): reachable.append(set()) visit = [n] reachable[i].add(n) while visit: n = visit.pop(0) if n in stop_sha1: continue for p in repo.changelog.parents(n): if p not in reachable[i]: reachable[i].add(p) visit.append(p) if p in stop_sha1: continue # walk the repository looking for commits that are in our # reachability graph for i, ctx in chlogwalk(): if i not in repo: continue n = repo.changelog.node(i) mask = is_reachable(want_sha1, reachable, n) if mask: parentstr = "" if parents: pp = repo.changelog.parents(n) if pp[0] != nullid: parentstr += " " + short(pp[0]) if pp[1] != nullid: parentstr += " " + short(pp[1]) if not full: ui.write("%s%s\n" % (short(n), parentstr)) elif full == "commit": ui.write("%s%s\n" % (short(n), parentstr)) catcommit(ui, repo, n, ' ', ctx) else: (p1, p2) = repo.changelog.parents(n) (h, h1, h2) = map(short, (n, p1, p2)) (i1, i2) = map(repo.changelog.rev, (p1, p2)) date = ctx.date()[0] ui.write("%s %s:%s" % (date, h, mask)) mask = is_reachable(want_sha1, reachable, p1) if i1 != nullrev and mask > 0: ui.write("%s:%s " % (h1, mask)), mask = is_reachable(want_sha1, reachable, p2) if i2 != nullrev and mask > 0: ui.write("%s:%s " % (h2, mask)) ui.write("\n") if maxnr and count >= maxnr: break count += 1 # git rev-list tries to order things by date, and has the ability to stop # at a given commit without walking the whole repo. TODO add the stop # parameter @command('debug-rev-list', [('H', 'header', None, _('header')), ('t', 'topo-order', None, _('topo-order')), ('p', 'parents', None, _('parents')), ('n', 'max-count', 0, _('max-count'))], ('[OPTION]... REV...')) def revlist(ui, repo, *revs, **opts): """print revisions""" if opts['header']: full = "commit" else: full = None copy = [x for x in revs] revtree(ui, copy, repo, full, opts['max_count'], opts['parents']) @command('view', [('l', 'limit', '', _('limit number of changes displayed'), _('NUM'))], _('[-l LIMIT] [REVRANGE]')) def view(ui, repo, *etc, **opts): "start interactive history viewer" os.chdir(repo.root) optstr = ' '.join(['--%s %s' % (k, v) for k, v in opts.iteritems() if v]) if repo.filtername is None: optstr += '--hidden' cmd = ui.config("hgk", "path", "hgk") + " %s %s" % (optstr, " ".join(etc)) ui.debug("running %s\n" % cmd) ui.system(cmd)