##// END OF EJS Templates
subsettable: move from repoview to branchmap, the only place it's used...
Augie Fackler -
r20032:175c6fd8 default
parent child Browse files
Show More
@@ -1,412 +1,412 b''
1 1 # perf.py - performance test routines
2 2 '''helper extension to measure performance'''
3 3
4 4 from mercurial import cmdutil, scmutil, util, commands, obsolete
5 5 from mercurial import repoview, branchmap, merge, copies
6 6 import time, os, sys
7 7
8 8 cmdtable = {}
9 9 command = cmdutil.command(cmdtable)
10 10
11 11 def timer(func, title=None):
12 12 results = []
13 13 begin = time.time()
14 14 count = 0
15 15 while True:
16 16 ostart = os.times()
17 17 cstart = time.time()
18 18 r = func()
19 19 cstop = time.time()
20 20 ostop = os.times()
21 21 count += 1
22 22 a, b = ostart, ostop
23 23 results.append((cstop - cstart, b[0] - a[0], b[1]-a[1]))
24 24 if cstop - begin > 3 and count >= 100:
25 25 break
26 26 if cstop - begin > 10 and count >= 3:
27 27 break
28 28 if title:
29 29 sys.stderr.write("! %s\n" % title)
30 30 if r:
31 31 sys.stderr.write("! result: %s\n" % r)
32 32 m = min(results)
33 33 sys.stderr.write("! wall %f comb %f user %f sys %f (best of %d)\n"
34 34 % (m[0], m[1] + m[2], m[1], m[2], count))
35 35
36 36 @command('perfwalk')
37 37 def perfwalk(ui, repo, *pats):
38 38 try:
39 39 m = scmutil.match(repo[None], pats, {})
40 40 timer(lambda: len(list(repo.dirstate.walk(m, [], True, False))))
41 41 except Exception:
42 42 try:
43 43 m = scmutil.match(repo[None], pats, {})
44 44 timer(lambda: len([b for a, b, c in repo.dirstate.statwalk([], m)]))
45 45 except Exception:
46 46 timer(lambda: len(list(cmdutil.walk(repo, pats, {}))))
47 47
48 48 @command('perfannotate')
49 49 def perfannotate(ui, repo, f):
50 50 fc = repo['.'][f]
51 51 timer(lambda: len(fc.annotate(True)))
52 52
53 53 @command('perfstatus',
54 54 [('u', 'unknown', False,
55 55 'ask status to look for unknown files')])
56 56 def perfstatus(ui, repo, **opts):
57 57 #m = match.always(repo.root, repo.getcwd())
58 58 #timer(lambda: sum(map(len, repo.dirstate.status(m, [], False, False,
59 59 # False))))
60 60 timer(lambda: sum(map(len, repo.status(**opts))))
61 61
62 62 @command('perfaddremove')
63 63 def perfaddremove(ui, repo):
64 64 try:
65 65 oldquiet = repo.ui.quiet
66 66 repo.ui.quiet = True
67 67 timer(lambda: scmutil.addremove(repo, dry_run=True))
68 68 finally:
69 69 repo.ui.quiet = oldquiet
70 70
71 71 def clearcaches(cl):
72 72 # behave somewhat consistently across internal API changes
73 73 if util.safehasattr(cl, 'clearcaches'):
74 74 cl.clearcaches()
75 75 elif util.safehasattr(cl, '_nodecache'):
76 76 from mercurial.node import nullid, nullrev
77 77 cl._nodecache = {nullid: nullrev}
78 78 cl._nodepos = None
79 79
80 80 @command('perfheads')
81 81 def perfheads(ui, repo):
82 82 cl = repo.changelog
83 83 def d():
84 84 len(cl.headrevs())
85 85 clearcaches(cl)
86 86 timer(d)
87 87
88 88 @command('perftags')
89 89 def perftags(ui, repo):
90 90 import mercurial.changelog
91 91 import mercurial.manifest
92 92 def t():
93 93 repo.changelog = mercurial.changelog.changelog(repo.sopener)
94 94 repo.manifest = mercurial.manifest.manifest(repo.sopener)
95 95 repo._tags = None
96 96 return len(repo.tags())
97 97 timer(t)
98 98
99 99 @command('perfancestors')
100 100 def perfancestors(ui, repo):
101 101 heads = repo.changelog.headrevs()
102 102 def d():
103 103 for a in repo.changelog.ancestors(heads):
104 104 pass
105 105 timer(d)
106 106
107 107 @command('perfancestorset')
108 108 def perfancestorset(ui, repo, revset):
109 109 revs = repo.revs(revset)
110 110 heads = repo.changelog.headrevs()
111 111 def d():
112 112 s = repo.changelog.ancestors(heads)
113 113 for rev in revs:
114 114 rev in s
115 115 timer(d)
116 116
117 117 @command('perfdirs')
118 118 def perfdirs(ui, repo):
119 119 dirstate = repo.dirstate
120 120 'a' in dirstate
121 121 def d():
122 122 dirstate.dirs()
123 123 del dirstate._dirs
124 124 timer(d)
125 125
126 126 @command('perfdirstate')
127 127 def perfdirstate(ui, repo):
128 128 "a" in repo.dirstate
129 129 def d():
130 130 repo.dirstate.invalidate()
131 131 "a" in repo.dirstate
132 132 timer(d)
133 133
134 134 @command('perfdirstatedirs')
135 135 def perfdirstatedirs(ui, repo):
136 136 "a" in repo.dirstate
137 137 def d():
138 138 "a" in repo.dirstate._dirs
139 139 del repo.dirstate._dirs
140 140 timer(d)
141 141
142 142 @command('perfdirstatewrite')
143 143 def perfdirstatewrite(ui, repo):
144 144 ds = repo.dirstate
145 145 "a" in ds
146 146 def d():
147 147 ds._dirty = True
148 148 ds.write()
149 149 timer(d)
150 150
151 151 @command('perfmergecalculate',
152 152 [('r', 'rev', '.', 'rev to merge against')])
153 153 def perfmergecalculate(ui, repo, rev):
154 154 wctx = repo[None]
155 155 rctx = scmutil.revsingle(repo, rev, rev)
156 156 ancestor = wctx.ancestor(rctx)
157 157 # we don't want working dir files to be stat'd in the benchmark, so prime
158 158 # that cache
159 159 wctx.dirty()
160 160 def d():
161 161 # acceptremote is True because we don't want prompts in the middle of
162 162 # our benchmark
163 163 merge.calculateupdates(repo, wctx, rctx, ancestor, False, False, False,
164 164 acceptremote=True)
165 165 timer(d)
166 166
167 167 @command('perfpathcopies', [], "REV REV")
168 168 def perfpathcopies(ui, repo, rev1, rev2):
169 169 ctx1 = scmutil.revsingle(repo, rev1, rev1)
170 170 ctx2 = scmutil.revsingle(repo, rev2, rev2)
171 171 def d():
172 172 copies.pathcopies(ctx1, ctx2)
173 173 timer(d)
174 174
175 175 @command('perfmanifest', [], 'REV')
176 176 def perfmanifest(ui, repo, rev):
177 177 ctx = scmutil.revsingle(repo, rev, rev)
178 178 t = ctx.manifestnode()
179 179 def d():
180 180 repo.manifest._mancache.clear()
181 181 repo.manifest._cache = None
182 182 repo.manifest.read(t)
183 183 timer(d)
184 184
185 185 @command('perfchangeset')
186 186 def perfchangeset(ui, repo, rev):
187 187 n = repo[rev].node()
188 188 def d():
189 189 repo.changelog.read(n)
190 190 #repo.changelog._cache = None
191 191 timer(d)
192 192
193 193 @command('perfindex')
194 194 def perfindex(ui, repo):
195 195 import mercurial.revlog
196 196 mercurial.revlog._prereadsize = 2**24 # disable lazy parser in old hg
197 197 n = repo["tip"].node()
198 198 def d():
199 199 cl = mercurial.revlog.revlog(repo.sopener, "00changelog.i")
200 200 cl.rev(n)
201 201 timer(d)
202 202
203 203 @command('perfstartup')
204 204 def perfstartup(ui, repo):
205 205 cmd = sys.argv[0]
206 206 def d():
207 207 os.system("HGRCPATH= %s version -q > /dev/null" % cmd)
208 208 timer(d)
209 209
210 210 @command('perfparents')
211 211 def perfparents(ui, repo):
212 212 nl = [repo.changelog.node(i) for i in xrange(1000)]
213 213 def d():
214 214 for n in nl:
215 215 repo.changelog.parents(n)
216 216 timer(d)
217 217
218 218 @command('perflookup')
219 219 def perflookup(ui, repo, rev):
220 220 timer(lambda: len(repo.lookup(rev)))
221 221
222 222 @command('perfrevrange')
223 223 def perfrevrange(ui, repo, *specs):
224 224 revrange = scmutil.revrange
225 225 timer(lambda: len(revrange(repo, specs)))
226 226
227 227 @command('perfnodelookup')
228 228 def perfnodelookup(ui, repo, rev):
229 229 import mercurial.revlog
230 230 mercurial.revlog._prereadsize = 2**24 # disable lazy parser in old hg
231 231 n = repo[rev].node()
232 232 cl = mercurial.revlog.revlog(repo.sopener, "00changelog.i")
233 233 def d():
234 234 cl.rev(n)
235 235 clearcaches(cl)
236 236 timer(d)
237 237
238 238 @command('perflog',
239 239 [('', 'rename', False, 'ask log to follow renames')])
240 240 def perflog(ui, repo, **opts):
241 241 ui.pushbuffer()
242 242 timer(lambda: commands.log(ui, repo, rev=[], date='', user='',
243 243 copies=opts.get('rename')))
244 244 ui.popbuffer()
245 245
246 246 @command('perftemplating')
247 247 def perftemplating(ui, repo):
248 248 ui.pushbuffer()
249 249 timer(lambda: commands.log(ui, repo, rev=[], date='', user='',
250 250 template='{date|shortdate} [{rev}:{node|short}]'
251 251 ' {author|person}: {desc|firstline}\n'))
252 252 ui.popbuffer()
253 253
254 254 @command('perfcca')
255 255 def perfcca(ui, repo):
256 256 timer(lambda: scmutil.casecollisionauditor(ui, False, repo.dirstate))
257 257
258 258 @command('perffncacheload')
259 259 def perffncacheload(ui, repo):
260 260 s = repo.store
261 261 def d():
262 262 s.fncache._load()
263 263 timer(d)
264 264
265 265 @command('perffncachewrite')
266 266 def perffncachewrite(ui, repo):
267 267 s = repo.store
268 268 s.fncache._load()
269 269 def d():
270 270 s.fncache._dirty = True
271 271 s.fncache.write()
272 272 timer(d)
273 273
274 274 @command('perffncacheencode')
275 275 def perffncacheencode(ui, repo):
276 276 s = repo.store
277 277 s.fncache._load()
278 278 def d():
279 279 for p in s.fncache.entries:
280 280 s.encode(p)
281 281 timer(d)
282 282
283 283 @command('perfdiffwd')
284 284 def perfdiffwd(ui, repo):
285 285 """Profile diff of working directory changes"""
286 286 options = {
287 287 'w': 'ignore_all_space',
288 288 'b': 'ignore_space_change',
289 289 'B': 'ignore_blank_lines',
290 290 }
291 291
292 292 for diffopt in ('', 'w', 'b', 'B', 'wB'):
293 293 opts = dict((options[c], '1') for c in diffopt)
294 294 def d():
295 295 ui.pushbuffer()
296 296 commands.diff(ui, repo, **opts)
297 297 ui.popbuffer()
298 298 title = 'diffopts: %s' % (diffopt and ('-' + diffopt) or 'none')
299 299 timer(d, title)
300 300
301 301 @command('perfrevlog',
302 302 [('d', 'dist', 100, 'distance between the revisions')],
303 303 "[INDEXFILE]")
304 304 def perfrevlog(ui, repo, file_, **opts):
305 305 from mercurial import revlog
306 306 dist = opts['dist']
307 307 def d():
308 308 r = revlog.revlog(lambda fn: open(fn, 'rb'), file_)
309 309 for x in xrange(0, len(r), dist):
310 310 r.revision(r.node(x))
311 311
312 312 timer(d)
313 313
314 314 @command('perfrevset',
315 315 [('C', 'clear', False, 'clear volatile cache between each call.')],
316 316 "REVSET")
317 317 def perfrevset(ui, repo, expr, clear=False):
318 318 """benchmark the execution time of a revset
319 319
320 320 Use the --clean option if need to evaluate the impact of build volatile
321 321 revisions set cache on the revset execution. Volatile cache hold filtered
322 322 and obsolete related cache."""
323 323 def d():
324 324 if clear:
325 325 repo.invalidatevolatilesets()
326 326 repo.revs(expr)
327 327 timer(d)
328 328
329 329 @command('perfvolatilesets')
330 330 def perfvolatilesets(ui, repo, *names):
331 331 """benchmark the computation of various volatile set
332 332
333 333 Volatile set computes element related to filtering and obsolescence."""
334 334 repo = repo.unfiltered()
335 335
336 336 def getobs(name):
337 337 def d():
338 338 repo.invalidatevolatilesets()
339 339 obsolete.getrevs(repo, name)
340 340 return d
341 341
342 342 allobs = sorted(obsolete.cachefuncs)
343 343 if names:
344 344 allobs = [n for n in allobs if n in names]
345 345
346 346 for name in allobs:
347 347 timer(getobs(name), title=name)
348 348
349 349 def getfiltered(name):
350 350 def d():
351 351 repo.invalidatevolatilesets()
352 352 repoview.filteredrevs(repo, name)
353 353 return d
354 354
355 355 allfilter = sorted(repoview.filtertable)
356 356 if names:
357 357 allfilter = [n for n in allfilter if n in names]
358 358
359 359 for name in allfilter:
360 360 timer(getfiltered(name), title=name)
361 361
362 362 @command('perfbranchmap',
363 363 [('f', 'full', False,
364 364 'Includes build time of subset'),
365 365 ])
366 366 def perfbranchmap(ui, repo, full=False):
367 367 """benchmark the update of a branchmap
368 368
369 369 This benchmarks the full repo.branchmap() call with read and write disabled
370 370 """
371 371 def getbranchmap(filtername):
372 372 """generate a benchmark function for the filtername"""
373 373 if filtername is None:
374 374 view = repo
375 375 else:
376 376 view = repo.filtered(filtername)
377 377 def d():
378 378 if full:
379 379 view._branchcaches.clear()
380 380 else:
381 381 view._branchcaches.pop(filtername, None)
382 382 view.branchmap()
383 383 return d
384 384 # add filter in smaller subset to bigger subset
385 385 possiblefilters = set(repoview.filtertable)
386 386 allfilters = []
387 387 while possiblefilters:
388 388 for name in possiblefilters:
389 subset = repoview.subsettable.get(name)
389 subset = branchmap.subsettable.get(name)
390 390 if subset not in possiblefilters:
391 391 break
392 392 else:
393 393 assert False, 'subset cycle %s!' % possiblefilters
394 394 allfilters.append(name)
395 395 possiblefilters.remove(name)
396 396
397 397 # warm the cache
398 398 if not full:
399 399 for name in allfilters:
400 400 repo.filtered(name).branchmap()
401 401 # add unfiltered
402 402 allfilters.append(None)
403 403 oldread = branchmap.read
404 404 oldwrite = branchmap.branchcache.write
405 405 try:
406 406 branchmap.read = lambda repo: None
407 407 branchmap.write = lambda repo: None
408 408 for name in allfilters:
409 409 timer(getbranchmap(name), title=str(name))
410 410 finally:
411 411 branchmap.read = oldread
412 412 branchmap.branchcache.write = oldwrite
@@ -1,210 +1,221 b''
1 1 # branchmap.py - logic to computes, maintain and stores branchmap for local repo
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from node import bin, hex, nullid, nullrev
9 9 import encoding
10 import util, repoview
10 import util
11 11
12 12 def _filename(repo):
13 13 """name of a branchcache file for a given repo or repoview"""
14 14 filename = "cache/branchheads"
15 15 if repo.filtername:
16 16 filename = '%s-%s' % (filename, repo.filtername)
17 17 return filename
18 18
19 19 def read(repo):
20 20 try:
21 21 f = repo.opener(_filename(repo))
22 22 lines = f.read().split('\n')
23 23 f.close()
24 24 except (IOError, OSError):
25 25 return None
26 26
27 27 try:
28 28 cachekey = lines.pop(0).split(" ", 2)
29 29 last, lrev = cachekey[:2]
30 30 last, lrev = bin(last), int(lrev)
31 31 filteredhash = None
32 32 if len(cachekey) > 2:
33 33 filteredhash = bin(cachekey[2])
34 34 partial = branchcache(tipnode=last, tiprev=lrev,
35 35 filteredhash=filteredhash)
36 36 if not partial.validfor(repo):
37 37 # invalidate the cache
38 38 raise ValueError('tip differs')
39 39 for l in lines:
40 40 if not l:
41 41 continue
42 42 node, label = l.split(" ", 1)
43 43 label = encoding.tolocal(label.strip())
44 44 if not node in repo:
45 45 raise ValueError('node %s does not exist' % node)
46 46 partial.setdefault(label, []).append(bin(node))
47 47 except KeyboardInterrupt:
48 48 raise
49 49 except Exception, inst:
50 50 if repo.ui.debugflag:
51 51 msg = 'invalid branchheads cache'
52 52 if repo.filtername is not None:
53 53 msg += ' (%s)' % repo.filtername
54 54 msg += ': %s\n'
55 55 repo.ui.warn(msg % inst)
56 56 partial = None
57 57 return partial
58 58
59 59
60 60
61 ### Nearest subset relation
62 # Nearest subset of filter X is a filter Y so that:
63 # * Y is included in X,
64 # * X - Y is as small as possible.
65 # This create and ordering used for branchmap purpose.
66 # the ordering may be partial
67 subsettable = {None: 'visible',
68 'visible': 'served',
69 'served': 'immutable',
70 'immutable': 'base'}
71
61 72 def updatecache(repo):
62 73 cl = repo.changelog
63 74 filtername = repo.filtername
64 75 partial = repo._branchcaches.get(filtername)
65 76
66 77 revs = []
67 78 if partial is None or not partial.validfor(repo):
68 79 partial = read(repo)
69 80 if partial is None:
70 subsetname = repoview.subsettable.get(filtername)
81 subsetname = subsettable.get(filtername)
71 82 if subsetname is None:
72 83 partial = branchcache()
73 84 else:
74 85 subset = repo.filtered(subsetname)
75 86 partial = subset.branchmap().copy()
76 87 extrarevs = subset.changelog.filteredrevs - cl.filteredrevs
77 88 revs.extend(r for r in extrarevs if r <= partial.tiprev)
78 89 revs.extend(cl.revs(start=partial.tiprev + 1))
79 90 if revs:
80 91 partial.update(repo, revs)
81 92 partial.write(repo)
82 93 assert partial.validfor(repo), filtername
83 94 repo._branchcaches[repo.filtername] = partial
84 95
85 96 class branchcache(dict):
86 97 """A dict like object that hold branches heads cache"""
87 98
88 99 def __init__(self, entries=(), tipnode=nullid, tiprev=nullrev,
89 100 filteredhash=None):
90 101 super(branchcache, self).__init__(entries)
91 102 self.tipnode = tipnode
92 103 self.tiprev = tiprev
93 104 self.filteredhash = filteredhash
94 105
95 106 def _hashfiltered(self, repo):
96 107 """build hash of revision filtered in the current cache
97 108
98 109 Tracking tipnode and tiprev is not enough to ensure validity of the
99 110 cache as they do not help to distinct cache that ignored various
100 111 revision bellow tiprev.
101 112
102 113 To detect such difference, we build a cache of all ignored revisions.
103 114 """
104 115 cl = repo.changelog
105 116 if not cl.filteredrevs:
106 117 return None
107 118 key = None
108 119 revs = sorted(r for r in cl.filteredrevs if r <= self.tiprev)
109 120 if revs:
110 121 s = util.sha1()
111 122 for rev in revs:
112 123 s.update('%s;' % rev)
113 124 key = s.digest()
114 125 return key
115 126
116 127 def validfor(self, repo):
117 128 """Is the cache content valid regarding a repo
118 129
119 130 - False when cached tipnode is unknown or if we detect a strip.
120 131 - True when cache is up to date or a subset of current repo."""
121 132 try:
122 133 return ((self.tipnode == repo.changelog.node(self.tiprev))
123 134 and (self.filteredhash == self._hashfiltered(repo)))
124 135 except IndexError:
125 136 return False
126 137
127 138 def copy(self):
128 139 """return an deep copy of the branchcache object"""
129 140 return branchcache(self, self.tipnode, self.tiprev, self.filteredhash)
130 141
131 142 def write(self, repo):
132 143 try:
133 144 f = repo.opener(_filename(repo), "w", atomictemp=True)
134 145 cachekey = [hex(self.tipnode), str(self.tiprev)]
135 146 if self.filteredhash is not None:
136 147 cachekey.append(hex(self.filteredhash))
137 148 f.write(" ".join(cachekey) + '\n')
138 149 for label, nodes in sorted(self.iteritems()):
139 150 for node in nodes:
140 151 f.write("%s %s\n" % (hex(node), encoding.fromlocal(label)))
141 152 f.close()
142 153 except (IOError, OSError, util.Abort):
143 154 # Abort may be raise by read only opener
144 155 pass
145 156
146 157 def update(self, repo, revgen):
147 158 """Given a branchhead cache, self, that may have extra nodes or be
148 159 missing heads, and a generator of nodes that are at least a superset of
149 160 heads missing, this function updates self to be correct.
150 161 """
151 162 cl = repo.changelog
152 163 # collect new branch entries
153 164 newbranches = {}
154 165 getbranch = cl.branch
155 166 for r in revgen:
156 167 newbranches.setdefault(getbranch(r), []).append(cl.node(r))
157 168 # if older branchheads are reachable from new ones, they aren't
158 169 # really branchheads. Note checking parents is insufficient:
159 170 # 1 (branch a) -> 2 (branch b) -> 3 (branch a)
160 171 for branch, newnodes in newbranches.iteritems():
161 172 bheads = self.setdefault(branch, [])
162 173 # Remove candidate heads that no longer are in the repo (e.g., as
163 174 # the result of a strip that just happened). Avoid using 'node in
164 175 # self' here because that dives down into branchcache code somewhat
165 176 # recursively.
166 177 bheadrevs = [cl.rev(node) for node in bheads
167 178 if cl.hasnode(node)]
168 179 newheadrevs = [cl.rev(node) for node in newnodes
169 180 if cl.hasnode(node)]
170 181 ctxisnew = bheadrevs and min(newheadrevs) > max(bheadrevs)
171 182 # Remove duplicates - nodes that are in newheadrevs and are already
172 183 # in bheadrevs. This can happen if you strip a node whose parent
173 184 # was already a head (because they're on different branches).
174 185 bheadrevs = sorted(set(bheadrevs).union(newheadrevs))
175 186
176 187 # Starting from tip means fewer passes over reachable. If we know
177 188 # the new candidates are not ancestors of existing heads, we don't
178 189 # have to examine ancestors of existing heads
179 190 if ctxisnew:
180 191 iterrevs = sorted(newheadrevs)
181 192 else:
182 193 iterrevs = list(bheadrevs)
183 194
184 195 # This loop prunes out two kinds of heads - heads that are
185 196 # superseded by a head in newheadrevs, and newheadrevs that are not
186 197 # heads because an existing head is their descendant.
187 198 while iterrevs:
188 199 latest = iterrevs.pop()
189 200 if latest not in bheadrevs:
190 201 continue
191 202 ancestors = set(cl.ancestors([latest],
192 203 bheadrevs[0]))
193 204 if ancestors:
194 205 bheadrevs = [b for b in bheadrevs if b not in ancestors]
195 206 self[branch] = [cl.node(rev) for rev in bheadrevs]
196 207 tiprev = max(bheadrevs)
197 208 if tiprev > self.tiprev:
198 209 self.tipnode = cl.node(tiprev)
199 210 self.tiprev = tiprev
200 211
201 212 if not self.validfor(repo):
202 213 # cache key are not valid anymore
203 214 self.tipnode = nullid
204 215 self.tiprev = nullrev
205 216 for heads in self.values():
206 217 tiprev = max(cl.rev(node) for node in heads)
207 218 if tiprev > self.tiprev:
208 219 self.tipnode = cl.node(tiprev)
209 220 self.tiprev = tiprev
210 221 self.filteredhash = self._hashfiltered(repo)
@@ -1,218 +1,207 b''
1 1 # repoview.py - Filtered view of a localrepo object
2 2 #
3 3 # Copyright 2012 Pierre-Yves David <pierre-yves.david@ens-lyon.org>
4 4 # Logilab SA <contact@logilab.fr>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 import copy
10 10 import phases
11 11 import util
12 12 import obsolete, revset
13 13
14 14
15 15 def hideablerevs(repo):
16 16 """Revisions candidates to be hidden
17 17
18 18 This is a standalone function to help extensions to wrap it."""
19 19 return obsolete.getrevs(repo, 'obsolete')
20 20
21 21 def computehidden(repo):
22 22 """compute the set of hidden revision to filter
23 23
24 24 During most operation hidden should be filtered."""
25 25 assert not repo.changelog.filteredrevs
26 26 hideable = hideablerevs(repo)
27 27 if hideable:
28 28 cl = repo.changelog
29 29 firsthideable = min(hideable)
30 30 revs = cl.revs(start=firsthideable)
31 31 blockers = [r for r in revset._children(repo, revs, hideable)
32 32 if r not in hideable]
33 33 for par in repo[None].parents():
34 34 blockers.append(par.rev())
35 35 for bm in repo._bookmarks.values():
36 36 blockers.append(repo[bm].rev())
37 37 blocked = cl.ancestors(blockers, inclusive=True)
38 38 return frozenset(r for r in hideable if r not in blocked)
39 39 return frozenset()
40 40
41 41 def computeunserved(repo):
42 42 """compute the set of revision that should be filtered when used a server
43 43
44 44 Secret and hidden changeset should not pretend to be here."""
45 45 assert not repo.changelog.filteredrevs
46 46 # fast path in simple case to avoid impact of non optimised code
47 47 hiddens = filterrevs(repo, 'visible')
48 48 if phases.hassecret(repo):
49 49 cl = repo.changelog
50 50 secret = phases.secret
51 51 getphase = repo._phasecache.phase
52 52 first = min(cl.rev(n) for n in repo._phasecache.phaseroots[secret])
53 53 revs = cl.revs(start=first)
54 54 secrets = set(r for r in revs if getphase(repo, r) >= secret)
55 55 return frozenset(hiddens | secrets)
56 56 else:
57 57 return hiddens
58 58
59 59 def computemutable(repo):
60 60 """compute the set of revision that should be filtered when used a server
61 61
62 62 Secret and hidden changeset should not pretend to be here."""
63 63 assert not repo.changelog.filteredrevs
64 64 # fast check to avoid revset call on huge repo
65 65 if util.any(repo._phasecache.phaseroots[1:]):
66 66 getphase = repo._phasecache.phase
67 67 maymutable = filterrevs(repo, 'base')
68 68 return frozenset(r for r in maymutable if getphase(repo, r))
69 69 return frozenset()
70 70
71 71 def computeimpactable(repo):
72 72 """Everything impactable by mutable revision
73 73
74 74 The immutable filter still have some chance to get invalidated. This will
75 75 happen when:
76 76
77 77 - you garbage collect hidden changeset,
78 78 - public phase is moved backward,
79 79 - something is changed in the filtering (this could be fixed)
80 80
81 81 This filter out any mutable changeset and any public changeset that may be
82 82 impacted by something happening to a mutable revision.
83 83
84 84 This is achieved by filtered everything with a revision number egal or
85 85 higher than the first mutable changeset is filtered."""
86 86 assert not repo.changelog.filteredrevs
87 87 cl = repo.changelog
88 88 firstmutable = len(cl)
89 89 for roots in repo._phasecache.phaseroots[1:]:
90 90 if roots:
91 91 firstmutable = min(firstmutable, min(cl.rev(r) for r in roots))
92 92 # protect from nullrev root
93 93 firstmutable = max(0, firstmutable)
94 94 return frozenset(xrange(firstmutable, len(cl)))
95 95
96 96 # function to compute filtered set
97 97 filtertable = {'visible': computehidden,
98 98 'served': computeunserved,
99 99 'immutable': computemutable,
100 100 'base': computeimpactable}
101 ### Nearest subset relation
102 # Nearest subset of filter X is a filter Y so that:
103 # * Y is included in X,
104 # * X - Y is as small as possible.
105 # This create and ordering used for branchmap purpose.
106 # the ordering may be partial
107 subsettable = {None: 'visible',
108 'visible': 'served',
109 'served': 'immutable',
110 'immutable': 'base'}
111 101
112 102 def filterrevs(repo, filtername):
113 103 """returns set of filtered revision for this filter name"""
114 104 if filtername not in repo.filteredrevcache:
115 105 func = filtertable[filtername]
116 106 repo.filteredrevcache[filtername] = func(repo.unfiltered())
117 107 return repo.filteredrevcache[filtername]
118 108
119 109 class repoview(object):
120 110 """Provide a read/write view of a repo through a filtered changelog
121 111
122 112 This object is used to access a filtered version of a repository without
123 113 altering the original repository object itself. We can not alter the
124 114 original object for two main reasons:
125 115 - It prevents the use of a repo with multiple filters at the same time. In
126 116 particular when multiple threads are involved.
127 117 - It makes scope of the filtering harder to control.
128 118
129 119 This object behaves very closely to the original repository. All attribute
130 120 operations are done on the original repository:
131 121 - An access to `repoview.someattr` actually returns `repo.someattr`,
132 122 - A write to `repoview.someattr` actually sets value of `repo.someattr`,
133 123 - A deletion of `repoview.someattr` actually drops `someattr`
134 124 from `repo.__dict__`.
135 125
136 126 The only exception is the `changelog` property. It is overridden to return
137 127 a (surface) copy of `repo.changelog` with some revisions filtered. The
138 128 `filtername` attribute of the view control the revisions that need to be
139 129 filtered. (the fact the changelog is copied is an implementation detail).
140 130
141 131 Unlike attributes, this object intercepts all method calls. This means that
142 132 all methods are run on the `repoview` object with the filtered `changelog`
143 133 property. For this purpose the simple `repoview` class must be mixed with
144 134 the actual class of the repository. This ensures that the resulting
145 135 `repoview` object have the very same methods than the repo object. This
146 136 leads to the property below.
147 137
148 138 repoview.method() --> repo.__class__.method(repoview)
149 139
150 140 The inheritance has to be done dynamically because `repo` can be of any
151 141 subclasses of `localrepo`. Eg: `bundlerepo` or `statichttprepo`.
152 142 """
153 143
154 144 def __init__(self, repo, filtername):
155 145 object.__setattr__(self, '_unfilteredrepo', repo)
156 146 object.__setattr__(self, 'filtername', filtername)
157 147 object.__setattr__(self, '_clcachekey', None)
158 148 object.__setattr__(self, '_clcache', None)
159 149
160 150 # not a propertycache on purpose we shall implement a proper cache later
161 151 @property
162 152 def changelog(self):
163 153 """return a filtered version of the changeset
164 154
165 155 this changelog must not be used for writing"""
166 156 # some cache may be implemented later
167 157 unfi = self._unfilteredrepo
168 158 unfichangelog = unfi.changelog
169 159 revs = filterrevs(unfi, self.filtername)
170 160 cl = self._clcache
171 161 newkey = (len(unfichangelog), unfichangelog.tip(), hash(revs))
172 162 if cl is not None:
173 163 # we need to check curkey too for some obscure reason.
174 164 # MQ test show a corruption of the underlying repo (in _clcache)
175 165 # without change in the cachekey.
176 166 oldfilter = cl.filteredrevs
177 167 try:
178 168 cl.filterrevs = () # disable filtering for tip
179 169 curkey = (len(cl), cl.tip(), hash(oldfilter))
180 170 finally:
181 171 cl.filteredrevs = oldfilter
182 172 if newkey != self._clcachekey or newkey != curkey:
183 173 cl = None
184 174 # could have been made None by the previous if
185 175 if cl is None:
186 176 cl = copy.copy(unfichangelog)
187 177 cl.filteredrevs = revs
188 178 object.__setattr__(self, '_clcache', cl)
189 179 object.__setattr__(self, '_clcachekey', newkey)
190 180 return cl
191 181
192 182 def unfiltered(self):
193 183 """Return an unfiltered version of a repo"""
194 184 return self._unfilteredrepo
195 185
196 186 def filtered(self, name):
197 187 """Return a filtered version of a repository"""
198 188 if name == self.filtername:
199 189 return self
200 190 return self.unfiltered().filtered(name)
201 191
202 192 # everything access are forwarded to the proxied repo
203 193 def __getattr__(self, attr):
204 194 return getattr(self._unfilteredrepo, attr)
205 195
206 196 def __setattr__(self, attr, value):
207 197 return setattr(self._unfilteredrepo, attr, value)
208 198
209 199 def __delattr__(self, attr):
210 200 return delattr(self._unfilteredrepo, attr)
211 201
212 202 # The `requirements` attribute is initialized during __init__. But
213 203 # __getattr__ won't be called as it also exists on the class. We need
214 204 # explicit forwarding to main repo here
215 205 @property
216 206 def requirements(self):
217 207 return self._unfilteredrepo.requirements
218
General Comments 0
You need to be logged in to leave comments. Login now