##// END OF EJS Templates
repoview: move subsettable in a dedicated module...
marmoute -
r42309:890f450f default draft
parent child Browse files
Show More
@@ -0,0 +1,22
1 # repoviewutil.py - constaints data relevant to repoview.py and other module
2 #
3 # Copyright 2012 Pierre-Yves David <pierre-yves.david@ens-lyon.org>
4 # Logilab SA <contact@logilab.fr>
5 #
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
8
9 from __future__ import absolute_import
10
11 ### Nearest subset relation
12 # Nearest subset of filter X is a filter Y so that:
13 # * Y is included in X,
14 # * X - Y is as small as possible.
15 # This create and ordering used for branchmap purpose.
16 # the ordering may be partial
17 subsettable = {None: 'visible',
18 'visible-hidden': 'visible',
19 'visible': 'served',
20 'served.hidden': 'served',
21 'served': 'immutable',
22 'immutable': 'base'}
@@ -1,2858 +1,2863
1 1 # perf.py - performance test routines
2 2 '''helper extension to measure performance
3 3
4 4 Configurations
5 5 ==============
6 6
7 7 ``perf``
8 8 --------
9 9
10 10 ``all-timing``
11 11 When set, additional statistics will be reported for each benchmark: best,
12 12 worst, median average. If not set only the best timing is reported
13 13 (default: off).
14 14
15 15 ``presleep``
16 16 number of second to wait before any group of runs (default: 1)
17 17
18 18 ``run-limits``
19 19 Control the number of runs each benchmark will perform. The option value
20 20 should be a list of `<time>-<numberofrun>` pairs. After each run the
21 21 conditions are considered in order with the following logic:
22 22
23 23 If benchmark has been running for <time> seconds, and we have performed
24 24 <numberofrun> iterations, stop the benchmark,
25 25
26 26 The default value is: `3.0-100, 10.0-3`
27 27
28 28 ``stub``
29 29 When set, benchmarks will only be run once, useful for testing
30 30 (default: off)
31 31 '''
32 32
33 33 # "historical portability" policy of perf.py:
34 34 #
35 35 # We have to do:
36 36 # - make perf.py "loadable" with as wide Mercurial version as possible
37 37 # This doesn't mean that perf commands work correctly with that Mercurial.
38 38 # BTW, perf.py itself has been available since 1.1 (or eb240755386d).
39 39 # - make historical perf command work correctly with as wide Mercurial
40 40 # version as possible
41 41 #
42 42 # We have to do, if possible with reasonable cost:
43 43 # - make recent perf command for historical feature work correctly
44 44 # with early Mercurial
45 45 #
46 46 # We don't have to do:
47 47 # - make perf command for recent feature work correctly with early
48 48 # Mercurial
49 49
50 50 from __future__ import absolute_import
51 51 import contextlib
52 52 import functools
53 53 import gc
54 54 import os
55 55 import random
56 56 import shutil
57 57 import struct
58 58 import sys
59 59 import tempfile
60 60 import threading
61 61 import time
62 62 from mercurial import (
63 63 changegroup,
64 64 cmdutil,
65 65 commands,
66 66 copies,
67 67 error,
68 68 extensions,
69 69 hg,
70 70 mdiff,
71 71 merge,
72 72 revlog,
73 73 util,
74 74 )
75 75
76 76 # for "historical portability":
77 77 # try to import modules separately (in dict order), and ignore
78 78 # failure, because these aren't available with early Mercurial
79 79 try:
80 80 from mercurial import branchmap # since 2.5 (or bcee63733aad)
81 81 except ImportError:
82 82 pass
83 83 try:
84 84 from mercurial import obsolete # since 2.3 (or ad0d6c2b3279)
85 85 except ImportError:
86 86 pass
87 87 try:
88 88 from mercurial import registrar # since 3.7 (or 37d50250b696)
89 89 dir(registrar) # forcibly load it
90 90 except ImportError:
91 91 registrar = None
92 92 try:
93 93 from mercurial import repoview # since 2.5 (or 3a6ddacb7198)
94 94 except ImportError:
95 95 pass
96 96 try:
97 from mercurial.utils import repoviewutil # since 5.0
98 except ImportError:
99 repoviewutil = None
100 try:
97 101 from mercurial import scmutil # since 1.9 (or 8b252e826c68)
98 102 except ImportError:
99 103 pass
100 104 try:
101 105 from mercurial import setdiscovery # since 1.9 (or cb98fed52495)
102 106 except ImportError:
103 107 pass
104 108
105 109
106 110 def identity(a):
107 111 return a
108 112
109 113 try:
110 114 from mercurial import pycompat
111 115 getargspec = pycompat.getargspec # added to module after 4.5
112 116 _byteskwargs = pycompat.byteskwargs # since 4.1 (or fbc3f73dc802)
113 117 _sysstr = pycompat.sysstr # since 4.0 (or 2219f4f82ede)
114 118 _xrange = pycompat.xrange # since 4.8 (or 7eba8f83129b)
115 119 fsencode = pycompat.fsencode # since 3.9 (or f4a5e0e86a7e)
116 120 if pycompat.ispy3:
117 121 _maxint = sys.maxsize # per py3 docs for replacing maxint
118 122 else:
119 123 _maxint = sys.maxint
120 124 except (ImportError, AttributeError):
121 125 import inspect
122 126 getargspec = inspect.getargspec
123 127 _byteskwargs = identity
124 128 fsencode = identity # no py3 support
125 129 _maxint = sys.maxint # no py3 support
126 130 _sysstr = lambda x: x # no py3 support
127 131 _xrange = xrange
128 132
129 133 try:
130 134 # 4.7+
131 135 queue = pycompat.queue.Queue
132 136 except (AttributeError, ImportError):
133 137 # <4.7.
134 138 try:
135 139 queue = pycompat.queue
136 140 except (AttributeError, ImportError):
137 141 queue = util.queue
138 142
139 143 try:
140 144 from mercurial import logcmdutil
141 145 makelogtemplater = logcmdutil.maketemplater
142 146 except (AttributeError, ImportError):
143 147 try:
144 148 makelogtemplater = cmdutil.makelogtemplater
145 149 except (AttributeError, ImportError):
146 150 makelogtemplater = None
147 151
148 152 # for "historical portability":
149 153 # define util.safehasattr forcibly, because util.safehasattr has been
150 154 # available since 1.9.3 (or 94b200a11cf7)
151 155 _undefined = object()
152 156 def safehasattr(thing, attr):
153 157 return getattr(thing, _sysstr(attr), _undefined) is not _undefined
154 158 setattr(util, 'safehasattr', safehasattr)
155 159
156 160 # for "historical portability":
157 161 # define util.timer forcibly, because util.timer has been available
158 162 # since ae5d60bb70c9
159 163 if safehasattr(time, 'perf_counter'):
160 164 util.timer = time.perf_counter
161 165 elif os.name == b'nt':
162 166 util.timer = time.clock
163 167 else:
164 168 util.timer = time.time
165 169
166 170 # for "historical portability":
167 171 # use locally defined empty option list, if formatteropts isn't
168 172 # available, because commands.formatteropts has been available since
169 173 # 3.2 (or 7a7eed5176a4), even though formatting itself has been
170 174 # available since 2.2 (or ae5f92e154d3)
171 175 formatteropts = getattr(cmdutil, "formatteropts",
172 176 getattr(commands, "formatteropts", []))
173 177
174 178 # for "historical portability":
175 179 # use locally defined option list, if debugrevlogopts isn't available,
176 180 # because commands.debugrevlogopts has been available since 3.7 (or
177 181 # 5606f7d0d063), even though cmdutil.openrevlog() has been available
178 182 # since 1.9 (or a79fea6b3e77).
179 183 revlogopts = getattr(cmdutil, "debugrevlogopts",
180 184 getattr(commands, "debugrevlogopts", [
181 185 (b'c', b'changelog', False, (b'open changelog')),
182 186 (b'm', b'manifest', False, (b'open manifest')),
183 187 (b'', b'dir', False, (b'open directory manifest')),
184 188 ]))
185 189
186 190 cmdtable = {}
187 191
188 192 # for "historical portability":
189 193 # define parsealiases locally, because cmdutil.parsealiases has been
190 194 # available since 1.5 (or 6252852b4332)
191 195 def parsealiases(cmd):
192 196 return cmd.split(b"|")
193 197
194 198 if safehasattr(registrar, 'command'):
195 199 command = registrar.command(cmdtable)
196 200 elif safehasattr(cmdutil, 'command'):
197 201 command = cmdutil.command(cmdtable)
198 202 if b'norepo' not in getargspec(command).args:
199 203 # for "historical portability":
200 204 # wrap original cmdutil.command, because "norepo" option has
201 205 # been available since 3.1 (or 75a96326cecb)
202 206 _command = command
203 207 def command(name, options=(), synopsis=None, norepo=False):
204 208 if norepo:
205 209 commands.norepo += b' %s' % b' '.join(parsealiases(name))
206 210 return _command(name, list(options), synopsis)
207 211 else:
208 212 # for "historical portability":
209 213 # define "@command" annotation locally, because cmdutil.command
210 214 # has been available since 1.9 (or 2daa5179e73f)
211 215 def command(name, options=(), synopsis=None, norepo=False):
212 216 def decorator(func):
213 217 if synopsis:
214 218 cmdtable[name] = func, list(options), synopsis
215 219 else:
216 220 cmdtable[name] = func, list(options)
217 221 if norepo:
218 222 commands.norepo += b' %s' % b' '.join(parsealiases(name))
219 223 return func
220 224 return decorator
221 225
222 226 try:
223 227 import mercurial.registrar
224 228 import mercurial.configitems
225 229 configtable = {}
226 230 configitem = mercurial.registrar.configitem(configtable)
227 231 configitem(b'perf', b'presleep',
228 232 default=mercurial.configitems.dynamicdefault,
229 233 )
230 234 configitem(b'perf', b'stub',
231 235 default=mercurial.configitems.dynamicdefault,
232 236 )
233 237 configitem(b'perf', b'parentscount',
234 238 default=mercurial.configitems.dynamicdefault,
235 239 )
236 240 configitem(b'perf', b'all-timing',
237 241 default=mercurial.configitems.dynamicdefault,
238 242 )
239 243 configitem(b'perf', b'run-limits',
240 244 default=mercurial.configitems.dynamicdefault,
241 245 )
242 246 except (ImportError, AttributeError):
243 247 pass
244 248
245 249 def getlen(ui):
246 250 if ui.configbool(b"perf", b"stub", False):
247 251 return lambda x: 1
248 252 return len
249 253
250 254 def gettimer(ui, opts=None):
251 255 """return a timer function and formatter: (timer, formatter)
252 256
253 257 This function exists to gather the creation of formatter in a single
254 258 place instead of duplicating it in all performance commands."""
255 259
256 260 # enforce an idle period before execution to counteract power management
257 261 # experimental config: perf.presleep
258 262 time.sleep(getint(ui, b"perf", b"presleep", 1))
259 263
260 264 if opts is None:
261 265 opts = {}
262 266 # redirect all to stderr unless buffer api is in use
263 267 if not ui._buffers:
264 268 ui = ui.copy()
265 269 uifout = safeattrsetter(ui, b'fout', ignoremissing=True)
266 270 if uifout:
267 271 # for "historical portability":
268 272 # ui.fout/ferr have been available since 1.9 (or 4e1ccd4c2b6d)
269 273 uifout.set(ui.ferr)
270 274
271 275 # get a formatter
272 276 uiformatter = getattr(ui, 'formatter', None)
273 277 if uiformatter:
274 278 fm = uiformatter(b'perf', opts)
275 279 else:
276 280 # for "historical portability":
277 281 # define formatter locally, because ui.formatter has been
278 282 # available since 2.2 (or ae5f92e154d3)
279 283 from mercurial import node
280 284 class defaultformatter(object):
281 285 """Minimized composition of baseformatter and plainformatter
282 286 """
283 287 def __init__(self, ui, topic, opts):
284 288 self._ui = ui
285 289 if ui.debugflag:
286 290 self.hexfunc = node.hex
287 291 else:
288 292 self.hexfunc = node.short
289 293 def __nonzero__(self):
290 294 return False
291 295 __bool__ = __nonzero__
292 296 def startitem(self):
293 297 pass
294 298 def data(self, **data):
295 299 pass
296 300 def write(self, fields, deftext, *fielddata, **opts):
297 301 self._ui.write(deftext % fielddata, **opts)
298 302 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
299 303 if cond:
300 304 self._ui.write(deftext % fielddata, **opts)
301 305 def plain(self, text, **opts):
302 306 self._ui.write(text, **opts)
303 307 def end(self):
304 308 pass
305 309 fm = defaultformatter(ui, b'perf', opts)
306 310
307 311 # stub function, runs code only once instead of in a loop
308 312 # experimental config: perf.stub
309 313 if ui.configbool(b"perf", b"stub", False):
310 314 return functools.partial(stub_timer, fm), fm
311 315
312 316 # experimental config: perf.all-timing
313 317 displayall = ui.configbool(b"perf", b"all-timing", False)
314 318
315 319 # experimental config: perf.run-limits
316 320 limitspec = ui.configlist(b"perf", b"run-limits", [])
317 321 limits = []
318 322 for item in limitspec:
319 323 parts = item.split(b'-', 1)
320 324 if len(parts) < 2:
321 325 ui.warn((b'malformatted run limit entry, missing "-": %s\n'
322 326 % item))
323 327 continue
324 328 try:
325 329 time_limit = float(pycompat.sysstr(parts[0]))
326 330 except ValueError as e:
327 331 ui.warn((b'malformatted run limit entry, %s: %s\n'
328 332 % (pycompat.bytestr(e), item)))
329 333 continue
330 334 try:
331 335 run_limit = int(pycompat.sysstr(parts[1]))
332 336 except ValueError as e:
333 337 ui.warn((b'malformatted run limit entry, %s: %s\n'
334 338 % (pycompat.bytestr(e), item)))
335 339 continue
336 340 limits.append((time_limit, run_limit))
337 341 if not limits:
338 342 limits = DEFAULTLIMITS
339 343
340 344 t = functools.partial(_timer, fm, displayall=displayall, limits=limits)
341 345 return t, fm
342 346
343 347 def stub_timer(fm, func, setup=None, title=None):
344 348 if setup is not None:
345 349 setup()
346 350 func()
347 351
348 352 @contextlib.contextmanager
349 353 def timeone():
350 354 r = []
351 355 ostart = os.times()
352 356 cstart = util.timer()
353 357 yield r
354 358 cstop = util.timer()
355 359 ostop = os.times()
356 360 a, b = ostart, ostop
357 361 r.append((cstop - cstart, b[0] - a[0], b[1]-a[1]))
358 362
359 363
360 364 # list of stop condition (elapsed time, minimal run count)
361 365 DEFAULTLIMITS = (
362 366 (3.0, 100),
363 367 (10.0, 3),
364 368 )
365 369
366 370 def _timer(fm, func, setup=None, title=None, displayall=False,
367 371 limits=DEFAULTLIMITS):
368 372 gc.collect()
369 373 results = []
370 374 begin = util.timer()
371 375 count = 0
372 376 keepgoing = True
373 377 while keepgoing:
374 378 if setup is not None:
375 379 setup()
376 380 with timeone() as item:
377 381 r = func()
378 382 count += 1
379 383 results.append(item[0])
380 384 cstop = util.timer()
381 385 # Look for a stop condition.
382 386 elapsed = cstop - begin
383 387 for t, mincount in limits:
384 388 if elapsed >= t and count >= mincount:
385 389 keepgoing = False
386 390 break
387 391
388 392 formatone(fm, results, title=title, result=r,
389 393 displayall=displayall)
390 394
391 395 def formatone(fm, timings, title=None, result=None, displayall=False):
392 396
393 397 count = len(timings)
394 398
395 399 fm.startitem()
396 400
397 401 if title:
398 402 fm.write(b'title', b'! %s\n', title)
399 403 if result:
400 404 fm.write(b'result', b'! result: %s\n', result)
401 405 def display(role, entry):
402 406 prefix = b''
403 407 if role != b'best':
404 408 prefix = b'%s.' % role
405 409 fm.plain(b'!')
406 410 fm.write(prefix + b'wall', b' wall %f', entry[0])
407 411 fm.write(prefix + b'comb', b' comb %f', entry[1] + entry[2])
408 412 fm.write(prefix + b'user', b' user %f', entry[1])
409 413 fm.write(prefix + b'sys', b' sys %f', entry[2])
410 414 fm.write(prefix + b'count', b' (%s of %%d)' % role, count)
411 415 fm.plain(b'\n')
412 416 timings.sort()
413 417 min_val = timings[0]
414 418 display(b'best', min_val)
415 419 if displayall:
416 420 max_val = timings[-1]
417 421 display(b'max', max_val)
418 422 avg = tuple([sum(x) / count for x in zip(*timings)])
419 423 display(b'avg', avg)
420 424 median = timings[len(timings) // 2]
421 425 display(b'median', median)
422 426
423 427 # utilities for historical portability
424 428
425 429 def getint(ui, section, name, default):
426 430 # for "historical portability":
427 431 # ui.configint has been available since 1.9 (or fa2b596db182)
428 432 v = ui.config(section, name, None)
429 433 if v is None:
430 434 return default
431 435 try:
432 436 return int(v)
433 437 except ValueError:
434 438 raise error.ConfigError((b"%s.%s is not an integer ('%s')")
435 439 % (section, name, v))
436 440
437 441 def safeattrsetter(obj, name, ignoremissing=False):
438 442 """Ensure that 'obj' has 'name' attribute before subsequent setattr
439 443
440 444 This function is aborted, if 'obj' doesn't have 'name' attribute
441 445 at runtime. This avoids overlooking removal of an attribute, which
442 446 breaks assumption of performance measurement, in the future.
443 447
444 448 This function returns the object to (1) assign a new value, and
445 449 (2) restore an original value to the attribute.
446 450
447 451 If 'ignoremissing' is true, missing 'name' attribute doesn't cause
448 452 abortion, and this function returns None. This is useful to
449 453 examine an attribute, which isn't ensured in all Mercurial
450 454 versions.
451 455 """
452 456 if not util.safehasattr(obj, name):
453 457 if ignoremissing:
454 458 return None
455 459 raise error.Abort((b"missing attribute %s of %s might break assumption"
456 460 b" of performance measurement") % (name, obj))
457 461
458 462 origvalue = getattr(obj, _sysstr(name))
459 463 class attrutil(object):
460 464 def set(self, newvalue):
461 465 setattr(obj, _sysstr(name), newvalue)
462 466 def restore(self):
463 467 setattr(obj, _sysstr(name), origvalue)
464 468
465 469 return attrutil()
466 470
467 471 # utilities to examine each internal API changes
468 472
469 473 def getbranchmapsubsettable():
470 474 # for "historical portability":
471 475 # subsettable is defined in:
472 476 # - branchmap since 2.9 (or 175c6fd8cacc)
473 477 # - repoview since 2.5 (or 59a9f18d4587)
474 for mod in (branchmap, repoview):
478 # - repoviewutil since 5.0
479 for mod in (branchmap, repoview, repoviewutil):
475 480 subsettable = getattr(mod, 'subsettable', None)
476 481 if subsettable:
477 482 return subsettable
478 483
479 484 # bisecting in bcee63733aad::59a9f18d4587 can reach here (both
480 485 # branchmap and repoview modules exist, but subsettable attribute
481 486 # doesn't)
482 487 raise error.Abort((b"perfbranchmap not available with this Mercurial"),
483 488 hint=b"use 2.5 or later")
484 489
485 490 def getsvfs(repo):
486 491 """Return appropriate object to access files under .hg/store
487 492 """
488 493 # for "historical portability":
489 494 # repo.svfs has been available since 2.3 (or 7034365089bf)
490 495 svfs = getattr(repo, 'svfs', None)
491 496 if svfs:
492 497 return svfs
493 498 else:
494 499 return getattr(repo, 'sopener')
495 500
496 501 def getvfs(repo):
497 502 """Return appropriate object to access files under .hg
498 503 """
499 504 # for "historical portability":
500 505 # repo.vfs has been available since 2.3 (or 7034365089bf)
501 506 vfs = getattr(repo, 'vfs', None)
502 507 if vfs:
503 508 return vfs
504 509 else:
505 510 return getattr(repo, 'opener')
506 511
507 512 def repocleartagscachefunc(repo):
508 513 """Return the function to clear tags cache according to repo internal API
509 514 """
510 515 if util.safehasattr(repo, b'_tagscache'): # since 2.0 (or 9dca7653b525)
511 516 # in this case, setattr(repo, '_tagscache', None) or so isn't
512 517 # correct way to clear tags cache, because existing code paths
513 518 # expect _tagscache to be a structured object.
514 519 def clearcache():
515 520 # _tagscache has been filteredpropertycache since 2.5 (or
516 521 # 98c867ac1330), and delattr() can't work in such case
517 522 if b'_tagscache' in vars(repo):
518 523 del repo.__dict__[b'_tagscache']
519 524 return clearcache
520 525
521 526 repotags = safeattrsetter(repo, b'_tags', ignoremissing=True)
522 527 if repotags: # since 1.4 (or 5614a628d173)
523 528 return lambda : repotags.set(None)
524 529
525 530 repotagscache = safeattrsetter(repo, b'tagscache', ignoremissing=True)
526 531 if repotagscache: # since 0.6 (or d7df759d0e97)
527 532 return lambda : repotagscache.set(None)
528 533
529 534 # Mercurial earlier than 0.6 (or d7df759d0e97) logically reaches
530 535 # this point, but it isn't so problematic, because:
531 536 # - repo.tags of such Mercurial isn't "callable", and repo.tags()
532 537 # in perftags() causes failure soon
533 538 # - perf.py itself has been available since 1.1 (or eb240755386d)
534 539 raise error.Abort((b"tags API of this hg command is unknown"))
535 540
536 541 # utilities to clear cache
537 542
538 543 def clearfilecache(obj, attrname):
539 544 unfiltered = getattr(obj, 'unfiltered', None)
540 545 if unfiltered is not None:
541 546 obj = obj.unfiltered()
542 547 if attrname in vars(obj):
543 548 delattr(obj, attrname)
544 549 obj._filecache.pop(attrname, None)
545 550
546 551 def clearchangelog(repo):
547 552 if repo is not repo.unfiltered():
548 553 object.__setattr__(repo, r'_clcachekey', None)
549 554 object.__setattr__(repo, r'_clcache', None)
550 555 clearfilecache(repo.unfiltered(), 'changelog')
551 556
552 557 # perf commands
553 558
554 559 @command(b'perfwalk', formatteropts)
555 560 def perfwalk(ui, repo, *pats, **opts):
556 561 opts = _byteskwargs(opts)
557 562 timer, fm = gettimer(ui, opts)
558 563 m = scmutil.match(repo[None], pats, {})
559 564 timer(lambda: len(list(repo.dirstate.walk(m, subrepos=[], unknown=True,
560 565 ignored=False))))
561 566 fm.end()
562 567
563 568 @command(b'perfannotate', formatteropts)
564 569 def perfannotate(ui, repo, f, **opts):
565 570 opts = _byteskwargs(opts)
566 571 timer, fm = gettimer(ui, opts)
567 572 fc = repo[b'.'][f]
568 573 timer(lambda: len(fc.annotate(True)))
569 574 fm.end()
570 575
571 576 @command(b'perfstatus',
572 577 [(b'u', b'unknown', False,
573 578 b'ask status to look for unknown files')] + formatteropts)
574 579 def perfstatus(ui, repo, **opts):
575 580 opts = _byteskwargs(opts)
576 581 #m = match.always(repo.root, repo.getcwd())
577 582 #timer(lambda: sum(map(len, repo.dirstate.status(m, [], False, False,
578 583 # False))))
579 584 timer, fm = gettimer(ui, opts)
580 585 timer(lambda: sum(map(len, repo.status(unknown=opts[b'unknown']))))
581 586 fm.end()
582 587
583 588 @command(b'perfaddremove', formatteropts)
584 589 def perfaddremove(ui, repo, **opts):
585 590 opts = _byteskwargs(opts)
586 591 timer, fm = gettimer(ui, opts)
587 592 try:
588 593 oldquiet = repo.ui.quiet
589 594 repo.ui.quiet = True
590 595 matcher = scmutil.match(repo[None])
591 596 opts[b'dry_run'] = True
592 597 if b'uipathfn' in getargspec(scmutil.addremove).args:
593 598 uipathfn = scmutil.getuipathfn(repo)
594 599 timer(lambda: scmutil.addremove(repo, matcher, b"", uipathfn, opts))
595 600 else:
596 601 timer(lambda: scmutil.addremove(repo, matcher, b"", opts))
597 602 finally:
598 603 repo.ui.quiet = oldquiet
599 604 fm.end()
600 605
601 606 def clearcaches(cl):
602 607 # behave somewhat consistently across internal API changes
603 608 if util.safehasattr(cl, b'clearcaches'):
604 609 cl.clearcaches()
605 610 elif util.safehasattr(cl, b'_nodecache'):
606 611 from mercurial.node import nullid, nullrev
607 612 cl._nodecache = {nullid: nullrev}
608 613 cl._nodepos = None
609 614
610 615 @command(b'perfheads', formatteropts)
611 616 def perfheads(ui, repo, **opts):
612 617 """benchmark the computation of a changelog heads"""
613 618 opts = _byteskwargs(opts)
614 619 timer, fm = gettimer(ui, opts)
615 620 cl = repo.changelog
616 621 def s():
617 622 clearcaches(cl)
618 623 def d():
619 624 len(cl.headrevs())
620 625 timer(d, setup=s)
621 626 fm.end()
622 627
623 628 @command(b'perftags', formatteropts+
624 629 [
625 630 (b'', b'clear-revlogs', False, b'refresh changelog and manifest'),
626 631 ])
627 632 def perftags(ui, repo, **opts):
628 633 opts = _byteskwargs(opts)
629 634 timer, fm = gettimer(ui, opts)
630 635 repocleartagscache = repocleartagscachefunc(repo)
631 636 clearrevlogs = opts[b'clear_revlogs']
632 637 def s():
633 638 if clearrevlogs:
634 639 clearchangelog(repo)
635 640 clearfilecache(repo.unfiltered(), 'manifest')
636 641 repocleartagscache()
637 642 def t():
638 643 return len(repo.tags())
639 644 timer(t, setup=s)
640 645 fm.end()
641 646
642 647 @command(b'perfancestors', formatteropts)
643 648 def perfancestors(ui, repo, **opts):
644 649 opts = _byteskwargs(opts)
645 650 timer, fm = gettimer(ui, opts)
646 651 heads = repo.changelog.headrevs()
647 652 def d():
648 653 for a in repo.changelog.ancestors(heads):
649 654 pass
650 655 timer(d)
651 656 fm.end()
652 657
653 658 @command(b'perfancestorset', formatteropts)
654 659 def perfancestorset(ui, repo, revset, **opts):
655 660 opts = _byteskwargs(opts)
656 661 timer, fm = gettimer(ui, opts)
657 662 revs = repo.revs(revset)
658 663 heads = repo.changelog.headrevs()
659 664 def d():
660 665 s = repo.changelog.ancestors(heads)
661 666 for rev in revs:
662 667 rev in s
663 668 timer(d)
664 669 fm.end()
665 670
666 671 @command(b'perfdiscovery', formatteropts, b'PATH')
667 672 def perfdiscovery(ui, repo, path, **opts):
668 673 """benchmark discovery between local repo and the peer at given path
669 674 """
670 675 repos = [repo, None]
671 676 timer, fm = gettimer(ui, opts)
672 677 path = ui.expandpath(path)
673 678
674 679 def s():
675 680 repos[1] = hg.peer(ui, opts, path)
676 681 def d():
677 682 setdiscovery.findcommonheads(ui, *repos)
678 683 timer(d, setup=s)
679 684 fm.end()
680 685
681 686 @command(b'perfbookmarks', formatteropts +
682 687 [
683 688 (b'', b'clear-revlogs', False, b'refresh changelog and manifest'),
684 689 ])
685 690 def perfbookmarks(ui, repo, **opts):
686 691 """benchmark parsing bookmarks from disk to memory"""
687 692 opts = _byteskwargs(opts)
688 693 timer, fm = gettimer(ui, opts)
689 694
690 695 clearrevlogs = opts[b'clear_revlogs']
691 696 def s():
692 697 if clearrevlogs:
693 698 clearchangelog(repo)
694 699 clearfilecache(repo, b'_bookmarks')
695 700 def d():
696 701 repo._bookmarks
697 702 timer(d, setup=s)
698 703 fm.end()
699 704
700 705 @command(b'perfbundleread', formatteropts, b'BUNDLE')
701 706 def perfbundleread(ui, repo, bundlepath, **opts):
702 707 """Benchmark reading of bundle files.
703 708
704 709 This command is meant to isolate the I/O part of bundle reading as
705 710 much as possible.
706 711 """
707 712 from mercurial import (
708 713 bundle2,
709 714 exchange,
710 715 streamclone,
711 716 )
712 717
713 718 opts = _byteskwargs(opts)
714 719
715 720 def makebench(fn):
716 721 def run():
717 722 with open(bundlepath, b'rb') as fh:
718 723 bundle = exchange.readbundle(ui, fh, bundlepath)
719 724 fn(bundle)
720 725
721 726 return run
722 727
723 728 def makereadnbytes(size):
724 729 def run():
725 730 with open(bundlepath, b'rb') as fh:
726 731 bundle = exchange.readbundle(ui, fh, bundlepath)
727 732 while bundle.read(size):
728 733 pass
729 734
730 735 return run
731 736
732 737 def makestdioread(size):
733 738 def run():
734 739 with open(bundlepath, b'rb') as fh:
735 740 while fh.read(size):
736 741 pass
737 742
738 743 return run
739 744
740 745 # bundle1
741 746
742 747 def deltaiter(bundle):
743 748 for delta in bundle.deltaiter():
744 749 pass
745 750
746 751 def iterchunks(bundle):
747 752 for chunk in bundle.getchunks():
748 753 pass
749 754
750 755 # bundle2
751 756
752 757 def forwardchunks(bundle):
753 758 for chunk in bundle._forwardchunks():
754 759 pass
755 760
756 761 def iterparts(bundle):
757 762 for part in bundle.iterparts():
758 763 pass
759 764
760 765 def iterpartsseekable(bundle):
761 766 for part in bundle.iterparts(seekable=True):
762 767 pass
763 768
764 769 def seek(bundle):
765 770 for part in bundle.iterparts(seekable=True):
766 771 part.seek(0, os.SEEK_END)
767 772
768 773 def makepartreadnbytes(size):
769 774 def run():
770 775 with open(bundlepath, b'rb') as fh:
771 776 bundle = exchange.readbundle(ui, fh, bundlepath)
772 777 for part in bundle.iterparts():
773 778 while part.read(size):
774 779 pass
775 780
776 781 return run
777 782
778 783 benches = [
779 784 (makestdioread(8192), b'read(8k)'),
780 785 (makestdioread(16384), b'read(16k)'),
781 786 (makestdioread(32768), b'read(32k)'),
782 787 (makestdioread(131072), b'read(128k)'),
783 788 ]
784 789
785 790 with open(bundlepath, b'rb') as fh:
786 791 bundle = exchange.readbundle(ui, fh, bundlepath)
787 792
788 793 if isinstance(bundle, changegroup.cg1unpacker):
789 794 benches.extend([
790 795 (makebench(deltaiter), b'cg1 deltaiter()'),
791 796 (makebench(iterchunks), b'cg1 getchunks()'),
792 797 (makereadnbytes(8192), b'cg1 read(8k)'),
793 798 (makereadnbytes(16384), b'cg1 read(16k)'),
794 799 (makereadnbytes(32768), b'cg1 read(32k)'),
795 800 (makereadnbytes(131072), b'cg1 read(128k)'),
796 801 ])
797 802 elif isinstance(bundle, bundle2.unbundle20):
798 803 benches.extend([
799 804 (makebench(forwardchunks), b'bundle2 forwardchunks()'),
800 805 (makebench(iterparts), b'bundle2 iterparts()'),
801 806 (makebench(iterpartsseekable), b'bundle2 iterparts() seekable'),
802 807 (makebench(seek), b'bundle2 part seek()'),
803 808 (makepartreadnbytes(8192), b'bundle2 part read(8k)'),
804 809 (makepartreadnbytes(16384), b'bundle2 part read(16k)'),
805 810 (makepartreadnbytes(32768), b'bundle2 part read(32k)'),
806 811 (makepartreadnbytes(131072), b'bundle2 part read(128k)'),
807 812 ])
808 813 elif isinstance(bundle, streamclone.streamcloneapplier):
809 814 raise error.Abort(b'stream clone bundles not supported')
810 815 else:
811 816 raise error.Abort(b'unhandled bundle type: %s' % type(bundle))
812 817
813 818 for fn, title in benches:
814 819 timer, fm = gettimer(ui, opts)
815 820 timer(fn, title=title)
816 821 fm.end()
817 822
818 823 @command(b'perfchangegroupchangelog', formatteropts +
819 824 [(b'', b'cgversion', b'02', b'changegroup version'),
820 825 (b'r', b'rev', b'', b'revisions to add to changegroup')])
821 826 def perfchangegroupchangelog(ui, repo, cgversion=b'02', rev=None, **opts):
822 827 """Benchmark producing a changelog group for a changegroup.
823 828
824 829 This measures the time spent processing the changelog during a
825 830 bundle operation. This occurs during `hg bundle` and on a server
826 831 processing a `getbundle` wire protocol request (handles clones
827 832 and pull requests).
828 833
829 834 By default, all revisions are added to the changegroup.
830 835 """
831 836 opts = _byteskwargs(opts)
832 837 cl = repo.changelog
833 838 nodes = [cl.lookup(r) for r in repo.revs(rev or b'all()')]
834 839 bundler = changegroup.getbundler(cgversion, repo)
835 840
836 841 def d():
837 842 state, chunks = bundler._generatechangelog(cl, nodes)
838 843 for chunk in chunks:
839 844 pass
840 845
841 846 timer, fm = gettimer(ui, opts)
842 847
843 848 # Terminal printing can interfere with timing. So disable it.
844 849 with ui.configoverride({(b'progress', b'disable'): True}):
845 850 timer(d)
846 851
847 852 fm.end()
848 853
849 854 @command(b'perfdirs', formatteropts)
850 855 def perfdirs(ui, repo, **opts):
851 856 opts = _byteskwargs(opts)
852 857 timer, fm = gettimer(ui, opts)
853 858 dirstate = repo.dirstate
854 859 b'a' in dirstate
855 860 def d():
856 861 dirstate.hasdir(b'a')
857 862 del dirstate._map._dirs
858 863 timer(d)
859 864 fm.end()
860 865
861 866 @command(b'perfdirstate', formatteropts)
862 867 def perfdirstate(ui, repo, **opts):
863 868 opts = _byteskwargs(opts)
864 869 timer, fm = gettimer(ui, opts)
865 870 b"a" in repo.dirstate
866 871 def d():
867 872 repo.dirstate.invalidate()
868 873 b"a" in repo.dirstate
869 874 timer(d)
870 875 fm.end()
871 876
872 877 @command(b'perfdirstatedirs', formatteropts)
873 878 def perfdirstatedirs(ui, repo, **opts):
874 879 opts = _byteskwargs(opts)
875 880 timer, fm = gettimer(ui, opts)
876 881 b"a" in repo.dirstate
877 882 def d():
878 883 repo.dirstate.hasdir(b"a")
879 884 del repo.dirstate._map._dirs
880 885 timer(d)
881 886 fm.end()
882 887
883 888 @command(b'perfdirstatefoldmap', formatteropts)
884 889 def perfdirstatefoldmap(ui, repo, **opts):
885 890 opts = _byteskwargs(opts)
886 891 timer, fm = gettimer(ui, opts)
887 892 dirstate = repo.dirstate
888 893 b'a' in dirstate
889 894 def d():
890 895 dirstate._map.filefoldmap.get(b'a')
891 896 del dirstate._map.filefoldmap
892 897 timer(d)
893 898 fm.end()
894 899
895 900 @command(b'perfdirfoldmap', formatteropts)
896 901 def perfdirfoldmap(ui, repo, **opts):
897 902 opts = _byteskwargs(opts)
898 903 timer, fm = gettimer(ui, opts)
899 904 dirstate = repo.dirstate
900 905 b'a' in dirstate
901 906 def d():
902 907 dirstate._map.dirfoldmap.get(b'a')
903 908 del dirstate._map.dirfoldmap
904 909 del dirstate._map._dirs
905 910 timer(d)
906 911 fm.end()
907 912
908 913 @command(b'perfdirstatewrite', formatteropts)
909 914 def perfdirstatewrite(ui, repo, **opts):
910 915 opts = _byteskwargs(opts)
911 916 timer, fm = gettimer(ui, opts)
912 917 ds = repo.dirstate
913 918 b"a" in ds
914 919 def d():
915 920 ds._dirty = True
916 921 ds.write(repo.currenttransaction())
917 922 timer(d)
918 923 fm.end()
919 924
920 925 @command(b'perfmergecalculate',
921 926 [(b'r', b'rev', b'.', b'rev to merge against')] + formatteropts)
922 927 def perfmergecalculate(ui, repo, rev, **opts):
923 928 opts = _byteskwargs(opts)
924 929 timer, fm = gettimer(ui, opts)
925 930 wctx = repo[None]
926 931 rctx = scmutil.revsingle(repo, rev, rev)
927 932 ancestor = wctx.ancestor(rctx)
928 933 # we don't want working dir files to be stat'd in the benchmark, so prime
929 934 # that cache
930 935 wctx.dirty()
931 936 def d():
932 937 # acceptremote is True because we don't want prompts in the middle of
933 938 # our benchmark
934 939 merge.calculateupdates(repo, wctx, rctx, [ancestor], False, False,
935 940 acceptremote=True, followcopies=True)
936 941 timer(d)
937 942 fm.end()
938 943
939 944 @command(b'perfpathcopies', [], b"REV REV")
940 945 def perfpathcopies(ui, repo, rev1, rev2, **opts):
941 946 """benchmark the copy tracing logic"""
942 947 opts = _byteskwargs(opts)
943 948 timer, fm = gettimer(ui, opts)
944 949 ctx1 = scmutil.revsingle(repo, rev1, rev1)
945 950 ctx2 = scmutil.revsingle(repo, rev2, rev2)
946 951 def d():
947 952 copies.pathcopies(ctx1, ctx2)
948 953 timer(d)
949 954 fm.end()
950 955
951 956 @command(b'perfphases',
952 957 [(b'', b'full', False, b'include file reading time too'),
953 958 ], b"")
954 959 def perfphases(ui, repo, **opts):
955 960 """benchmark phasesets computation"""
956 961 opts = _byteskwargs(opts)
957 962 timer, fm = gettimer(ui, opts)
958 963 _phases = repo._phasecache
959 964 full = opts.get(b'full')
960 965 def d():
961 966 phases = _phases
962 967 if full:
963 968 clearfilecache(repo, b'_phasecache')
964 969 phases = repo._phasecache
965 970 phases.invalidate()
966 971 phases.loadphaserevs(repo)
967 972 timer(d)
968 973 fm.end()
969 974
970 975 @command(b'perfphasesremote',
971 976 [], b"[DEST]")
972 977 def perfphasesremote(ui, repo, dest=None, **opts):
973 978 """benchmark time needed to analyse phases of the remote server"""
974 979 from mercurial.node import (
975 980 bin,
976 981 )
977 982 from mercurial import (
978 983 exchange,
979 984 hg,
980 985 phases,
981 986 )
982 987 opts = _byteskwargs(opts)
983 988 timer, fm = gettimer(ui, opts)
984 989
985 990 path = ui.paths.getpath(dest, default=(b'default-push', b'default'))
986 991 if not path:
987 992 raise error.Abort((b'default repository not configured!'),
988 993 hint=(b"see 'hg help config.paths'"))
989 994 dest = path.pushloc or path.loc
990 995 ui.status((b'analysing phase of %s\n') % util.hidepassword(dest))
991 996 other = hg.peer(repo, opts, dest)
992 997
993 998 # easier to perform discovery through the operation
994 999 op = exchange.pushoperation(repo, other)
995 1000 exchange._pushdiscoverychangeset(op)
996 1001
997 1002 remotesubset = op.fallbackheads
998 1003
999 1004 with other.commandexecutor() as e:
1000 1005 remotephases = e.callcommand(b'listkeys',
1001 1006 {b'namespace': b'phases'}).result()
1002 1007 del other
1003 1008 publishing = remotephases.get(b'publishing', False)
1004 1009 if publishing:
1005 1010 ui.status((b'publishing: yes\n'))
1006 1011 else:
1007 1012 ui.status((b'publishing: no\n'))
1008 1013
1009 1014 nodemap = repo.changelog.nodemap
1010 1015 nonpublishroots = 0
1011 1016 for nhex, phase in remotephases.iteritems():
1012 1017 if nhex == b'publishing': # ignore data related to publish option
1013 1018 continue
1014 1019 node = bin(nhex)
1015 1020 if node in nodemap and int(phase):
1016 1021 nonpublishroots += 1
1017 1022 ui.status((b'number of roots: %d\n') % len(remotephases))
1018 1023 ui.status((b'number of known non public roots: %d\n') % nonpublishroots)
1019 1024 def d():
1020 1025 phases.remotephasessummary(repo,
1021 1026 remotesubset,
1022 1027 remotephases)
1023 1028 timer(d)
1024 1029 fm.end()
1025 1030
1026 1031 @command(b'perfmanifest',[
1027 1032 (b'm', b'manifest-rev', False, b'Look up a manifest node revision'),
1028 1033 (b'', b'clear-disk', False, b'clear on-disk caches too'),
1029 1034 ] + formatteropts, b'REV|NODE')
1030 1035 def perfmanifest(ui, repo, rev, manifest_rev=False, clear_disk=False, **opts):
1031 1036 """benchmark the time to read a manifest from disk and return a usable
1032 1037 dict-like object
1033 1038
1034 1039 Manifest caches are cleared before retrieval."""
1035 1040 opts = _byteskwargs(opts)
1036 1041 timer, fm = gettimer(ui, opts)
1037 1042 if not manifest_rev:
1038 1043 ctx = scmutil.revsingle(repo, rev, rev)
1039 1044 t = ctx.manifestnode()
1040 1045 else:
1041 1046 from mercurial.node import bin
1042 1047
1043 1048 if len(rev) == 40:
1044 1049 t = bin(rev)
1045 1050 else:
1046 1051 try:
1047 1052 rev = int(rev)
1048 1053
1049 1054 if util.safehasattr(repo.manifestlog, b'getstorage'):
1050 1055 t = repo.manifestlog.getstorage(b'').node(rev)
1051 1056 else:
1052 1057 t = repo.manifestlog._revlog.lookup(rev)
1053 1058 except ValueError:
1054 1059 raise error.Abort(b'manifest revision must be integer or full '
1055 1060 b'node')
1056 1061 def d():
1057 1062 repo.manifestlog.clearcaches(clear_persisted_data=clear_disk)
1058 1063 repo.manifestlog[t].read()
1059 1064 timer(d)
1060 1065 fm.end()
1061 1066
1062 1067 @command(b'perfchangeset', formatteropts)
1063 1068 def perfchangeset(ui, repo, rev, **opts):
1064 1069 opts = _byteskwargs(opts)
1065 1070 timer, fm = gettimer(ui, opts)
1066 1071 n = scmutil.revsingle(repo, rev).node()
1067 1072 def d():
1068 1073 repo.changelog.read(n)
1069 1074 #repo.changelog._cache = None
1070 1075 timer(d)
1071 1076 fm.end()
1072 1077
1073 1078 @command(b'perfignore', formatteropts)
1074 1079 def perfignore(ui, repo, **opts):
1075 1080 """benchmark operation related to computing ignore"""
1076 1081 opts = _byteskwargs(opts)
1077 1082 timer, fm = gettimer(ui, opts)
1078 1083 dirstate = repo.dirstate
1079 1084
1080 1085 def setupone():
1081 1086 dirstate.invalidate()
1082 1087 clearfilecache(dirstate, b'_ignore')
1083 1088
1084 1089 def runone():
1085 1090 dirstate._ignore
1086 1091
1087 1092 timer(runone, setup=setupone, title=b"load")
1088 1093 fm.end()
1089 1094
1090 1095 @command(b'perfindex', [
1091 1096 (b'', b'rev', [], b'revision to be looked up (default tip)'),
1092 1097 (b'', b'no-lookup', None, b'do not revision lookup post creation'),
1093 1098 ] + formatteropts)
1094 1099 def perfindex(ui, repo, **opts):
1095 1100 """benchmark index creation time followed by a lookup
1096 1101
1097 1102 The default is to look `tip` up. Depending on the index implementation,
1098 1103 the revision looked up can matters. For example, an implementation
1099 1104 scanning the index will have a faster lookup time for `--rev tip` than for
1100 1105 `--rev 0`. The number of looked up revisions and their order can also
1101 1106 matters.
1102 1107
1103 1108 Example of useful set to test:
1104 1109 * tip
1105 1110 * 0
1106 1111 * -10:
1107 1112 * :10
1108 1113 * -10: + :10
1109 1114 * :10: + -10:
1110 1115 * -10000:
1111 1116 * -10000: + 0
1112 1117
1113 1118 It is not currently possible to check for lookup of a missing node. For
1114 1119 deeper lookup benchmarking, checkout the `perfnodemap` command."""
1115 1120 import mercurial.revlog
1116 1121 opts = _byteskwargs(opts)
1117 1122 timer, fm = gettimer(ui, opts)
1118 1123 mercurial.revlog._prereadsize = 2**24 # disable lazy parser in old hg
1119 1124 if opts[b'no_lookup']:
1120 1125 if opts['rev']:
1121 1126 raise error.Abort('--no-lookup and --rev are mutually exclusive')
1122 1127 nodes = []
1123 1128 elif not opts[b'rev']:
1124 1129 nodes = [repo[b"tip"].node()]
1125 1130 else:
1126 1131 revs = scmutil.revrange(repo, opts[b'rev'])
1127 1132 cl = repo.changelog
1128 1133 nodes = [cl.node(r) for r in revs]
1129 1134
1130 1135 unfi = repo.unfiltered()
1131 1136 # find the filecache func directly
1132 1137 # This avoid polluting the benchmark with the filecache logic
1133 1138 makecl = unfi.__class__.changelog.func
1134 1139 def setup():
1135 1140 # probably not necessary, but for good measure
1136 1141 clearchangelog(unfi)
1137 1142 def d():
1138 1143 cl = makecl(unfi)
1139 1144 for n in nodes:
1140 1145 cl.rev(n)
1141 1146 timer(d, setup=setup)
1142 1147 fm.end()
1143 1148
1144 1149 @command(b'perfnodemap', [
1145 1150 (b'', b'rev', [], b'revision to be looked up (default tip)'),
1146 1151 (b'', b'clear-caches', True, b'clear revlog cache between calls'),
1147 1152 ] + formatteropts)
1148 1153 def perfnodemap(ui, repo, **opts):
1149 1154 """benchmark the time necessary to look up revision from a cold nodemap
1150 1155
1151 1156 Depending on the implementation, the amount and order of revision we look
1152 1157 up can varies. Example of useful set to test:
1153 1158 * tip
1154 1159 * 0
1155 1160 * -10:
1156 1161 * :10
1157 1162 * -10: + :10
1158 1163 * :10: + -10:
1159 1164 * -10000:
1160 1165 * -10000: + 0
1161 1166
1162 1167 The command currently focus on valid binary lookup. Benchmarking for
1163 1168 hexlookup, prefix lookup and missing lookup would also be valuable.
1164 1169 """
1165 1170 import mercurial.revlog
1166 1171 opts = _byteskwargs(opts)
1167 1172 timer, fm = gettimer(ui, opts)
1168 1173 mercurial.revlog._prereadsize = 2**24 # disable lazy parser in old hg
1169 1174
1170 1175 unfi = repo.unfiltered()
1171 1176 clearcaches = opts['clear_caches']
1172 1177 # find the filecache func directly
1173 1178 # This avoid polluting the benchmark with the filecache logic
1174 1179 makecl = unfi.__class__.changelog.func
1175 1180 if not opts[b'rev']:
1176 1181 raise error.Abort('use --rev to specify revisions to look up')
1177 1182 revs = scmutil.revrange(repo, opts[b'rev'])
1178 1183 cl = repo.changelog
1179 1184 nodes = [cl.node(r) for r in revs]
1180 1185
1181 1186 # use a list to pass reference to a nodemap from one closure to the next
1182 1187 nodeget = [None]
1183 1188 def setnodeget():
1184 1189 # probably not necessary, but for good measure
1185 1190 clearchangelog(unfi)
1186 1191 nodeget[0] = makecl(unfi).nodemap.get
1187 1192
1188 1193 def d():
1189 1194 get = nodeget[0]
1190 1195 for n in nodes:
1191 1196 get(n)
1192 1197
1193 1198 setup = None
1194 1199 if clearcaches:
1195 1200 def setup():
1196 1201 setnodeget()
1197 1202 else:
1198 1203 setnodeget()
1199 1204 d() # prewarm the data structure
1200 1205 timer(d, setup=setup)
1201 1206 fm.end()
1202 1207
1203 1208 @command(b'perfstartup', formatteropts)
1204 1209 def perfstartup(ui, repo, **opts):
1205 1210 opts = _byteskwargs(opts)
1206 1211 timer, fm = gettimer(ui, opts)
1207 1212 def d():
1208 1213 if os.name != r'nt':
1209 1214 os.system(b"HGRCPATH= %s version -q > /dev/null" %
1210 1215 fsencode(sys.argv[0]))
1211 1216 else:
1212 1217 os.environ[r'HGRCPATH'] = r' '
1213 1218 os.system(r"%s version -q > NUL" % sys.argv[0])
1214 1219 timer(d)
1215 1220 fm.end()
1216 1221
1217 1222 @command(b'perfparents', formatteropts)
1218 1223 def perfparents(ui, repo, **opts):
1219 1224 """benchmark the time necessary to fetch one changeset's parents.
1220 1225
1221 1226 The fetch is done using the `node identifier`, traversing all object layers
1222 1227 from the repository object. The first N revisions will be used for this
1223 1228 benchmark. N is controlled by the ``perf.parentscount`` config option
1224 1229 (default: 1000).
1225 1230 """
1226 1231 opts = _byteskwargs(opts)
1227 1232 timer, fm = gettimer(ui, opts)
1228 1233 # control the number of commits perfparents iterates over
1229 1234 # experimental config: perf.parentscount
1230 1235 count = getint(ui, b"perf", b"parentscount", 1000)
1231 1236 if len(repo.changelog) < count:
1232 1237 raise error.Abort(b"repo needs %d commits for this test" % count)
1233 1238 repo = repo.unfiltered()
1234 1239 nl = [repo.changelog.node(i) for i in _xrange(count)]
1235 1240 def d():
1236 1241 for n in nl:
1237 1242 repo.changelog.parents(n)
1238 1243 timer(d)
1239 1244 fm.end()
1240 1245
1241 1246 @command(b'perfctxfiles', formatteropts)
1242 1247 def perfctxfiles(ui, repo, x, **opts):
1243 1248 opts = _byteskwargs(opts)
1244 1249 x = int(x)
1245 1250 timer, fm = gettimer(ui, opts)
1246 1251 def d():
1247 1252 len(repo[x].files())
1248 1253 timer(d)
1249 1254 fm.end()
1250 1255
1251 1256 @command(b'perfrawfiles', formatteropts)
1252 1257 def perfrawfiles(ui, repo, x, **opts):
1253 1258 opts = _byteskwargs(opts)
1254 1259 x = int(x)
1255 1260 timer, fm = gettimer(ui, opts)
1256 1261 cl = repo.changelog
1257 1262 def d():
1258 1263 len(cl.read(x)[3])
1259 1264 timer(d)
1260 1265 fm.end()
1261 1266
1262 1267 @command(b'perflookup', formatteropts)
1263 1268 def perflookup(ui, repo, rev, **opts):
1264 1269 opts = _byteskwargs(opts)
1265 1270 timer, fm = gettimer(ui, opts)
1266 1271 timer(lambda: len(repo.lookup(rev)))
1267 1272 fm.end()
1268 1273
1269 1274 @command(b'perflinelogedits',
1270 1275 [(b'n', b'edits', 10000, b'number of edits'),
1271 1276 (b'', b'max-hunk-lines', 10, b'max lines in a hunk'),
1272 1277 ], norepo=True)
1273 1278 def perflinelogedits(ui, **opts):
1274 1279 from mercurial import linelog
1275 1280
1276 1281 opts = _byteskwargs(opts)
1277 1282
1278 1283 edits = opts[b'edits']
1279 1284 maxhunklines = opts[b'max_hunk_lines']
1280 1285
1281 1286 maxb1 = 100000
1282 1287 random.seed(0)
1283 1288 randint = random.randint
1284 1289 currentlines = 0
1285 1290 arglist = []
1286 1291 for rev in _xrange(edits):
1287 1292 a1 = randint(0, currentlines)
1288 1293 a2 = randint(a1, min(currentlines, a1 + maxhunklines))
1289 1294 b1 = randint(0, maxb1)
1290 1295 b2 = randint(b1, b1 + maxhunklines)
1291 1296 currentlines += (b2 - b1) - (a2 - a1)
1292 1297 arglist.append((rev, a1, a2, b1, b2))
1293 1298
1294 1299 def d():
1295 1300 ll = linelog.linelog()
1296 1301 for args in arglist:
1297 1302 ll.replacelines(*args)
1298 1303
1299 1304 timer, fm = gettimer(ui, opts)
1300 1305 timer(d)
1301 1306 fm.end()
1302 1307
1303 1308 @command(b'perfrevrange', formatteropts)
1304 1309 def perfrevrange(ui, repo, *specs, **opts):
1305 1310 opts = _byteskwargs(opts)
1306 1311 timer, fm = gettimer(ui, opts)
1307 1312 revrange = scmutil.revrange
1308 1313 timer(lambda: len(revrange(repo, specs)))
1309 1314 fm.end()
1310 1315
1311 1316 @command(b'perfnodelookup', formatteropts)
1312 1317 def perfnodelookup(ui, repo, rev, **opts):
1313 1318 opts = _byteskwargs(opts)
1314 1319 timer, fm = gettimer(ui, opts)
1315 1320 import mercurial.revlog
1316 1321 mercurial.revlog._prereadsize = 2**24 # disable lazy parser in old hg
1317 1322 n = scmutil.revsingle(repo, rev).node()
1318 1323 cl = mercurial.revlog.revlog(getsvfs(repo), b"00changelog.i")
1319 1324 def d():
1320 1325 cl.rev(n)
1321 1326 clearcaches(cl)
1322 1327 timer(d)
1323 1328 fm.end()
1324 1329
1325 1330 @command(b'perflog',
1326 1331 [(b'', b'rename', False, b'ask log to follow renames')
1327 1332 ] + formatteropts)
1328 1333 def perflog(ui, repo, rev=None, **opts):
1329 1334 opts = _byteskwargs(opts)
1330 1335 if rev is None:
1331 1336 rev=[]
1332 1337 timer, fm = gettimer(ui, opts)
1333 1338 ui.pushbuffer()
1334 1339 timer(lambda: commands.log(ui, repo, rev=rev, date=b'', user=b'',
1335 1340 copies=opts.get(b'rename')))
1336 1341 ui.popbuffer()
1337 1342 fm.end()
1338 1343
1339 1344 @command(b'perfmoonwalk', formatteropts)
1340 1345 def perfmoonwalk(ui, repo, **opts):
1341 1346 """benchmark walking the changelog backwards
1342 1347
1343 1348 This also loads the changelog data for each revision in the changelog.
1344 1349 """
1345 1350 opts = _byteskwargs(opts)
1346 1351 timer, fm = gettimer(ui, opts)
1347 1352 def moonwalk():
1348 1353 for i in repo.changelog.revs(start=(len(repo) - 1), stop=-1):
1349 1354 ctx = repo[i]
1350 1355 ctx.branch() # read changelog data (in addition to the index)
1351 1356 timer(moonwalk)
1352 1357 fm.end()
1353 1358
1354 1359 @command(b'perftemplating',
1355 1360 [(b'r', b'rev', [], b'revisions to run the template on'),
1356 1361 ] + formatteropts)
1357 1362 def perftemplating(ui, repo, testedtemplate=None, **opts):
1358 1363 """test the rendering time of a given template"""
1359 1364 if makelogtemplater is None:
1360 1365 raise error.Abort((b"perftemplating not available with this Mercurial"),
1361 1366 hint=b"use 4.3 or later")
1362 1367
1363 1368 opts = _byteskwargs(opts)
1364 1369
1365 1370 nullui = ui.copy()
1366 1371 nullui.fout = open(os.devnull, r'wb')
1367 1372 nullui.disablepager()
1368 1373 revs = opts.get(b'rev')
1369 1374 if not revs:
1370 1375 revs = [b'all()']
1371 1376 revs = list(scmutil.revrange(repo, revs))
1372 1377
1373 1378 defaulttemplate = (b'{date|shortdate} [{rev}:{node|short}]'
1374 1379 b' {author|person}: {desc|firstline}\n')
1375 1380 if testedtemplate is None:
1376 1381 testedtemplate = defaulttemplate
1377 1382 displayer = makelogtemplater(nullui, repo, testedtemplate)
1378 1383 def format():
1379 1384 for r in revs:
1380 1385 ctx = repo[r]
1381 1386 displayer.show(ctx)
1382 1387 displayer.flush(ctx)
1383 1388
1384 1389 timer, fm = gettimer(ui, opts)
1385 1390 timer(format)
1386 1391 fm.end()
1387 1392
1388 1393 @command(b'perfhelper-pathcopies', formatteropts +
1389 1394 [
1390 1395 (b'r', b'revs', [], b'restrict search to these revisions'),
1391 1396 (b'', b'timing', False, b'provides extra data (costly)'),
1392 1397 ])
1393 1398 def perfhelperpathcopies(ui, repo, revs=[], **opts):
1394 1399 """find statistic about potential parameters for the `perftracecopies`
1395 1400
1396 1401 This command find source-destination pair relevant for copytracing testing.
1397 1402 It report value for some of the parameters that impact copy tracing time.
1398 1403
1399 1404 If `--timing` is set, rename detection is run and the associated timing
1400 1405 will be reported. The extra details comes at the cost of a slower command
1401 1406 execution.
1402 1407
1403 1408 Since the rename detection is only run once, other factors might easily
1404 1409 affect the precision of the timing. However it should give a good
1405 1410 approximation of which revision pairs are very costly.
1406 1411 """
1407 1412 opts = _byteskwargs(opts)
1408 1413 fm = ui.formatter(b'perf', opts)
1409 1414 dotiming = opts[b'timing']
1410 1415
1411 1416 if dotiming:
1412 1417 header = '%12s %12s %12s %12s %12s %12s\n'
1413 1418 output = ("%(source)12s %(destination)12s "
1414 1419 "%(nbrevs)12d %(nbmissingfiles)12d "
1415 1420 "%(nbrenamedfiles)12d %(time)18.5f\n")
1416 1421 header_names = ("source", "destination", "nb-revs", "nb-files",
1417 1422 "nb-renames", "time")
1418 1423 fm.plain(header % header_names)
1419 1424 else:
1420 1425 header = '%12s %12s %12s %12s\n'
1421 1426 output = ("%(source)12s %(destination)12s "
1422 1427 "%(nbrevs)12d %(nbmissingfiles)12d\n")
1423 1428 fm.plain(header % ("source", "destination", "nb-revs", "nb-files"))
1424 1429
1425 1430 if not revs:
1426 1431 revs = ['all()']
1427 1432 revs = scmutil.revrange(repo, revs)
1428 1433
1429 1434 roi = repo.revs('merge() and %ld', revs)
1430 1435 for r in roi:
1431 1436 ctx = repo[r]
1432 1437 p1 = ctx.p1().rev()
1433 1438 p2 = ctx.p2().rev()
1434 1439 bases = repo.changelog._commonancestorsheads(p1, p2)
1435 1440 for p in (p1, p2):
1436 1441 for b in bases:
1437 1442 base = repo[b]
1438 1443 parent = repo[p]
1439 1444 missing = copies._computeforwardmissing(base, parent)
1440 1445 if not missing:
1441 1446 continue
1442 1447 data = {
1443 1448 b'source': base.hex(),
1444 1449 b'destination': parent.hex(),
1445 1450 b'nbrevs': len(repo.revs('%d::%d', b, p)),
1446 1451 b'nbmissingfiles': len(missing),
1447 1452 }
1448 1453 if dotiming:
1449 1454 begin = util.timer()
1450 1455 renames = copies.pathcopies(base, parent)
1451 1456 end = util.timer()
1452 1457 # not very stable timing since we did only one run
1453 1458 data['time'] = end - begin
1454 1459 data['nbrenamedfiles'] = len(renames)
1455 1460 fm.startitem()
1456 1461 fm.data(**data)
1457 1462 out = data.copy()
1458 1463 out['source'] = fm.hexfunc(base.node())
1459 1464 out['destination'] = fm.hexfunc(parent.node())
1460 1465 fm.plain(output % out)
1461 1466
1462 1467 fm.end()
1463 1468
1464 1469 @command(b'perfcca', formatteropts)
1465 1470 def perfcca(ui, repo, **opts):
1466 1471 opts = _byteskwargs(opts)
1467 1472 timer, fm = gettimer(ui, opts)
1468 1473 timer(lambda: scmutil.casecollisionauditor(ui, False, repo.dirstate))
1469 1474 fm.end()
1470 1475
1471 1476 @command(b'perffncacheload', formatteropts)
1472 1477 def perffncacheload(ui, repo, **opts):
1473 1478 opts = _byteskwargs(opts)
1474 1479 timer, fm = gettimer(ui, opts)
1475 1480 s = repo.store
1476 1481 def d():
1477 1482 s.fncache._load()
1478 1483 timer(d)
1479 1484 fm.end()
1480 1485
1481 1486 @command(b'perffncachewrite', formatteropts)
1482 1487 def perffncachewrite(ui, repo, **opts):
1483 1488 opts = _byteskwargs(opts)
1484 1489 timer, fm = gettimer(ui, opts)
1485 1490 s = repo.store
1486 1491 lock = repo.lock()
1487 1492 s.fncache._load()
1488 1493 tr = repo.transaction(b'perffncachewrite')
1489 1494 tr.addbackup(b'fncache')
1490 1495 def d():
1491 1496 s.fncache._dirty = True
1492 1497 s.fncache.write(tr)
1493 1498 timer(d)
1494 1499 tr.close()
1495 1500 lock.release()
1496 1501 fm.end()
1497 1502
1498 1503 @command(b'perffncacheencode', formatteropts)
1499 1504 def perffncacheencode(ui, repo, **opts):
1500 1505 opts = _byteskwargs(opts)
1501 1506 timer, fm = gettimer(ui, opts)
1502 1507 s = repo.store
1503 1508 s.fncache._load()
1504 1509 def d():
1505 1510 for p in s.fncache.entries:
1506 1511 s.encode(p)
1507 1512 timer(d)
1508 1513 fm.end()
1509 1514
1510 1515 def _bdiffworker(q, blocks, xdiff, ready, done):
1511 1516 while not done.is_set():
1512 1517 pair = q.get()
1513 1518 while pair is not None:
1514 1519 if xdiff:
1515 1520 mdiff.bdiff.xdiffblocks(*pair)
1516 1521 elif blocks:
1517 1522 mdiff.bdiff.blocks(*pair)
1518 1523 else:
1519 1524 mdiff.textdiff(*pair)
1520 1525 q.task_done()
1521 1526 pair = q.get()
1522 1527 q.task_done() # for the None one
1523 1528 with ready:
1524 1529 ready.wait()
1525 1530
1526 1531 def _manifestrevision(repo, mnode):
1527 1532 ml = repo.manifestlog
1528 1533
1529 1534 if util.safehasattr(ml, b'getstorage'):
1530 1535 store = ml.getstorage(b'')
1531 1536 else:
1532 1537 store = ml._revlog
1533 1538
1534 1539 return store.revision(mnode)
1535 1540
1536 1541 @command(b'perfbdiff', revlogopts + formatteropts + [
1537 1542 (b'', b'count', 1, b'number of revisions to test (when using --startrev)'),
1538 1543 (b'', b'alldata', False, b'test bdiffs for all associated revisions'),
1539 1544 (b'', b'threads', 0, b'number of thread to use (disable with 0)'),
1540 1545 (b'', b'blocks', False, b'test computing diffs into blocks'),
1541 1546 (b'', b'xdiff', False, b'use xdiff algorithm'),
1542 1547 ],
1543 1548
1544 1549 b'-c|-m|FILE REV')
1545 1550 def perfbdiff(ui, repo, file_, rev=None, count=None, threads=0, **opts):
1546 1551 """benchmark a bdiff between revisions
1547 1552
1548 1553 By default, benchmark a bdiff between its delta parent and itself.
1549 1554
1550 1555 With ``--count``, benchmark bdiffs between delta parents and self for N
1551 1556 revisions starting at the specified revision.
1552 1557
1553 1558 With ``--alldata``, assume the requested revision is a changeset and
1554 1559 measure bdiffs for all changes related to that changeset (manifest
1555 1560 and filelogs).
1556 1561 """
1557 1562 opts = _byteskwargs(opts)
1558 1563
1559 1564 if opts[b'xdiff'] and not opts[b'blocks']:
1560 1565 raise error.CommandError(b'perfbdiff', b'--xdiff requires --blocks')
1561 1566
1562 1567 if opts[b'alldata']:
1563 1568 opts[b'changelog'] = True
1564 1569
1565 1570 if opts.get(b'changelog') or opts.get(b'manifest'):
1566 1571 file_, rev = None, file_
1567 1572 elif rev is None:
1568 1573 raise error.CommandError(b'perfbdiff', b'invalid arguments')
1569 1574
1570 1575 blocks = opts[b'blocks']
1571 1576 xdiff = opts[b'xdiff']
1572 1577 textpairs = []
1573 1578
1574 1579 r = cmdutil.openrevlog(repo, b'perfbdiff', file_, opts)
1575 1580
1576 1581 startrev = r.rev(r.lookup(rev))
1577 1582 for rev in range(startrev, min(startrev + count, len(r) - 1)):
1578 1583 if opts[b'alldata']:
1579 1584 # Load revisions associated with changeset.
1580 1585 ctx = repo[rev]
1581 1586 mtext = _manifestrevision(repo, ctx.manifestnode())
1582 1587 for pctx in ctx.parents():
1583 1588 pman = _manifestrevision(repo, pctx.manifestnode())
1584 1589 textpairs.append((pman, mtext))
1585 1590
1586 1591 # Load filelog revisions by iterating manifest delta.
1587 1592 man = ctx.manifest()
1588 1593 pman = ctx.p1().manifest()
1589 1594 for filename, change in pman.diff(man).items():
1590 1595 fctx = repo.file(filename)
1591 1596 f1 = fctx.revision(change[0][0] or -1)
1592 1597 f2 = fctx.revision(change[1][0] or -1)
1593 1598 textpairs.append((f1, f2))
1594 1599 else:
1595 1600 dp = r.deltaparent(rev)
1596 1601 textpairs.append((r.revision(dp), r.revision(rev)))
1597 1602
1598 1603 withthreads = threads > 0
1599 1604 if not withthreads:
1600 1605 def d():
1601 1606 for pair in textpairs:
1602 1607 if xdiff:
1603 1608 mdiff.bdiff.xdiffblocks(*pair)
1604 1609 elif blocks:
1605 1610 mdiff.bdiff.blocks(*pair)
1606 1611 else:
1607 1612 mdiff.textdiff(*pair)
1608 1613 else:
1609 1614 q = queue()
1610 1615 for i in _xrange(threads):
1611 1616 q.put(None)
1612 1617 ready = threading.Condition()
1613 1618 done = threading.Event()
1614 1619 for i in _xrange(threads):
1615 1620 threading.Thread(target=_bdiffworker,
1616 1621 args=(q, blocks, xdiff, ready, done)).start()
1617 1622 q.join()
1618 1623 def d():
1619 1624 for pair in textpairs:
1620 1625 q.put(pair)
1621 1626 for i in _xrange(threads):
1622 1627 q.put(None)
1623 1628 with ready:
1624 1629 ready.notify_all()
1625 1630 q.join()
1626 1631 timer, fm = gettimer(ui, opts)
1627 1632 timer(d)
1628 1633 fm.end()
1629 1634
1630 1635 if withthreads:
1631 1636 done.set()
1632 1637 for i in _xrange(threads):
1633 1638 q.put(None)
1634 1639 with ready:
1635 1640 ready.notify_all()
1636 1641
1637 1642 @command(b'perfunidiff', revlogopts + formatteropts + [
1638 1643 (b'', b'count', 1, b'number of revisions to test (when using --startrev)'),
1639 1644 (b'', b'alldata', False, b'test unidiffs for all associated revisions'),
1640 1645 ], b'-c|-m|FILE REV')
1641 1646 def perfunidiff(ui, repo, file_, rev=None, count=None, **opts):
1642 1647 """benchmark a unified diff between revisions
1643 1648
1644 1649 This doesn't include any copy tracing - it's just a unified diff
1645 1650 of the texts.
1646 1651
1647 1652 By default, benchmark a diff between its delta parent and itself.
1648 1653
1649 1654 With ``--count``, benchmark diffs between delta parents and self for N
1650 1655 revisions starting at the specified revision.
1651 1656
1652 1657 With ``--alldata``, assume the requested revision is a changeset and
1653 1658 measure diffs for all changes related to that changeset (manifest
1654 1659 and filelogs).
1655 1660 """
1656 1661 opts = _byteskwargs(opts)
1657 1662 if opts[b'alldata']:
1658 1663 opts[b'changelog'] = True
1659 1664
1660 1665 if opts.get(b'changelog') or opts.get(b'manifest'):
1661 1666 file_, rev = None, file_
1662 1667 elif rev is None:
1663 1668 raise error.CommandError(b'perfunidiff', b'invalid arguments')
1664 1669
1665 1670 textpairs = []
1666 1671
1667 1672 r = cmdutil.openrevlog(repo, b'perfunidiff', file_, opts)
1668 1673
1669 1674 startrev = r.rev(r.lookup(rev))
1670 1675 for rev in range(startrev, min(startrev + count, len(r) - 1)):
1671 1676 if opts[b'alldata']:
1672 1677 # Load revisions associated with changeset.
1673 1678 ctx = repo[rev]
1674 1679 mtext = _manifestrevision(repo, ctx.manifestnode())
1675 1680 for pctx in ctx.parents():
1676 1681 pman = _manifestrevision(repo, pctx.manifestnode())
1677 1682 textpairs.append((pman, mtext))
1678 1683
1679 1684 # Load filelog revisions by iterating manifest delta.
1680 1685 man = ctx.manifest()
1681 1686 pman = ctx.p1().manifest()
1682 1687 for filename, change in pman.diff(man).items():
1683 1688 fctx = repo.file(filename)
1684 1689 f1 = fctx.revision(change[0][0] or -1)
1685 1690 f2 = fctx.revision(change[1][0] or -1)
1686 1691 textpairs.append((f1, f2))
1687 1692 else:
1688 1693 dp = r.deltaparent(rev)
1689 1694 textpairs.append((r.revision(dp), r.revision(rev)))
1690 1695
1691 1696 def d():
1692 1697 for left, right in textpairs:
1693 1698 # The date strings don't matter, so we pass empty strings.
1694 1699 headerlines, hunks = mdiff.unidiff(
1695 1700 left, b'', right, b'', b'left', b'right', binary=False)
1696 1701 # consume iterators in roughly the way patch.py does
1697 1702 b'\n'.join(headerlines)
1698 1703 b''.join(sum((list(hlines) for hrange, hlines in hunks), []))
1699 1704 timer, fm = gettimer(ui, opts)
1700 1705 timer(d)
1701 1706 fm.end()
1702 1707
1703 1708 @command(b'perfdiffwd', formatteropts)
1704 1709 def perfdiffwd(ui, repo, **opts):
1705 1710 """Profile diff of working directory changes"""
1706 1711 opts = _byteskwargs(opts)
1707 1712 timer, fm = gettimer(ui, opts)
1708 1713 options = {
1709 1714 'w': 'ignore_all_space',
1710 1715 'b': 'ignore_space_change',
1711 1716 'B': 'ignore_blank_lines',
1712 1717 }
1713 1718
1714 1719 for diffopt in ('', 'w', 'b', 'B', 'wB'):
1715 1720 opts = dict((options[c], b'1') for c in diffopt)
1716 1721 def d():
1717 1722 ui.pushbuffer()
1718 1723 commands.diff(ui, repo, **opts)
1719 1724 ui.popbuffer()
1720 1725 diffopt = diffopt.encode('ascii')
1721 1726 title = b'diffopts: %s' % (diffopt and (b'-' + diffopt) or b'none')
1722 1727 timer(d, title=title)
1723 1728 fm.end()
1724 1729
1725 1730 @command(b'perfrevlogindex', revlogopts + formatteropts,
1726 1731 b'-c|-m|FILE')
1727 1732 def perfrevlogindex(ui, repo, file_=None, **opts):
1728 1733 """Benchmark operations against a revlog index.
1729 1734
1730 1735 This tests constructing a revlog instance, reading index data,
1731 1736 parsing index data, and performing various operations related to
1732 1737 index data.
1733 1738 """
1734 1739
1735 1740 opts = _byteskwargs(opts)
1736 1741
1737 1742 rl = cmdutil.openrevlog(repo, b'perfrevlogindex', file_, opts)
1738 1743
1739 1744 opener = getattr(rl, 'opener') # trick linter
1740 1745 indexfile = rl.indexfile
1741 1746 data = opener.read(indexfile)
1742 1747
1743 1748 header = struct.unpack(b'>I', data[0:4])[0]
1744 1749 version = header & 0xFFFF
1745 1750 if version == 1:
1746 1751 revlogio = revlog.revlogio()
1747 1752 inline = header & (1 << 16)
1748 1753 else:
1749 1754 raise error.Abort((b'unsupported revlog version: %d') % version)
1750 1755
1751 1756 rllen = len(rl)
1752 1757
1753 1758 node0 = rl.node(0)
1754 1759 node25 = rl.node(rllen // 4)
1755 1760 node50 = rl.node(rllen // 2)
1756 1761 node75 = rl.node(rllen // 4 * 3)
1757 1762 node100 = rl.node(rllen - 1)
1758 1763
1759 1764 allrevs = range(rllen)
1760 1765 allrevsrev = list(reversed(allrevs))
1761 1766 allnodes = [rl.node(rev) for rev in range(rllen)]
1762 1767 allnodesrev = list(reversed(allnodes))
1763 1768
1764 1769 def constructor():
1765 1770 revlog.revlog(opener, indexfile)
1766 1771
1767 1772 def read():
1768 1773 with opener(indexfile) as fh:
1769 1774 fh.read()
1770 1775
1771 1776 def parseindex():
1772 1777 revlogio.parseindex(data, inline)
1773 1778
1774 1779 def getentry(revornode):
1775 1780 index = revlogio.parseindex(data, inline)[0]
1776 1781 index[revornode]
1777 1782
1778 1783 def getentries(revs, count=1):
1779 1784 index = revlogio.parseindex(data, inline)[0]
1780 1785
1781 1786 for i in range(count):
1782 1787 for rev in revs:
1783 1788 index[rev]
1784 1789
1785 1790 def resolvenode(node):
1786 1791 nodemap = revlogio.parseindex(data, inline)[1]
1787 1792 # This only works for the C code.
1788 1793 if nodemap is None:
1789 1794 return
1790 1795
1791 1796 try:
1792 1797 nodemap[node]
1793 1798 except error.RevlogError:
1794 1799 pass
1795 1800
1796 1801 def resolvenodes(nodes, count=1):
1797 1802 nodemap = revlogio.parseindex(data, inline)[1]
1798 1803 if nodemap is None:
1799 1804 return
1800 1805
1801 1806 for i in range(count):
1802 1807 for node in nodes:
1803 1808 try:
1804 1809 nodemap[node]
1805 1810 except error.RevlogError:
1806 1811 pass
1807 1812
1808 1813 benches = [
1809 1814 (constructor, b'revlog constructor'),
1810 1815 (read, b'read'),
1811 1816 (parseindex, b'create index object'),
1812 1817 (lambda: getentry(0), b'retrieve index entry for rev 0'),
1813 1818 (lambda: resolvenode(b'a' * 20), b'look up missing node'),
1814 1819 (lambda: resolvenode(node0), b'look up node at rev 0'),
1815 1820 (lambda: resolvenode(node25), b'look up node at 1/4 len'),
1816 1821 (lambda: resolvenode(node50), b'look up node at 1/2 len'),
1817 1822 (lambda: resolvenode(node75), b'look up node at 3/4 len'),
1818 1823 (lambda: resolvenode(node100), b'look up node at tip'),
1819 1824 # 2x variation is to measure caching impact.
1820 1825 (lambda: resolvenodes(allnodes),
1821 1826 b'look up all nodes (forward)'),
1822 1827 (lambda: resolvenodes(allnodes, 2),
1823 1828 b'look up all nodes 2x (forward)'),
1824 1829 (lambda: resolvenodes(allnodesrev),
1825 1830 b'look up all nodes (reverse)'),
1826 1831 (lambda: resolvenodes(allnodesrev, 2),
1827 1832 b'look up all nodes 2x (reverse)'),
1828 1833 (lambda: getentries(allrevs),
1829 1834 b'retrieve all index entries (forward)'),
1830 1835 (lambda: getentries(allrevs, 2),
1831 1836 b'retrieve all index entries 2x (forward)'),
1832 1837 (lambda: getentries(allrevsrev),
1833 1838 b'retrieve all index entries (reverse)'),
1834 1839 (lambda: getentries(allrevsrev, 2),
1835 1840 b'retrieve all index entries 2x (reverse)'),
1836 1841 ]
1837 1842
1838 1843 for fn, title in benches:
1839 1844 timer, fm = gettimer(ui, opts)
1840 1845 timer(fn, title=title)
1841 1846 fm.end()
1842 1847
1843 1848 @command(b'perfrevlogrevisions', revlogopts + formatteropts +
1844 1849 [(b'd', b'dist', 100, b'distance between the revisions'),
1845 1850 (b's', b'startrev', 0, b'revision to start reading at'),
1846 1851 (b'', b'reverse', False, b'read in reverse')],
1847 1852 b'-c|-m|FILE')
1848 1853 def perfrevlogrevisions(ui, repo, file_=None, startrev=0, reverse=False,
1849 1854 **opts):
1850 1855 """Benchmark reading a series of revisions from a revlog.
1851 1856
1852 1857 By default, we read every ``-d/--dist`` revision from 0 to tip of
1853 1858 the specified revlog.
1854 1859
1855 1860 The start revision can be defined via ``-s/--startrev``.
1856 1861 """
1857 1862 opts = _byteskwargs(opts)
1858 1863
1859 1864 rl = cmdutil.openrevlog(repo, b'perfrevlogrevisions', file_, opts)
1860 1865 rllen = getlen(ui)(rl)
1861 1866
1862 1867 if startrev < 0:
1863 1868 startrev = rllen + startrev
1864 1869
1865 1870 def d():
1866 1871 rl.clearcaches()
1867 1872
1868 1873 beginrev = startrev
1869 1874 endrev = rllen
1870 1875 dist = opts[b'dist']
1871 1876
1872 1877 if reverse:
1873 1878 beginrev, endrev = endrev - 1, beginrev - 1
1874 1879 dist = -1 * dist
1875 1880
1876 1881 for x in _xrange(beginrev, endrev, dist):
1877 1882 # Old revisions don't support passing int.
1878 1883 n = rl.node(x)
1879 1884 rl.revision(n)
1880 1885
1881 1886 timer, fm = gettimer(ui, opts)
1882 1887 timer(d)
1883 1888 fm.end()
1884 1889
1885 1890 @command(b'perfrevlogwrite', revlogopts + formatteropts +
1886 1891 [(b's', b'startrev', 1000, b'revision to start writing at'),
1887 1892 (b'', b'stoprev', -1, b'last revision to write'),
1888 1893 (b'', b'count', 3, b'last revision to write'),
1889 1894 (b'', b'details', False, b'print timing for every revisions tested'),
1890 1895 (b'', b'source', b'full', b'the kind of data feed in the revlog'),
1891 1896 (b'', b'lazydeltabase', True, b'try the provided delta first'),
1892 1897 (b'', b'clear-caches', True, b'clear revlog cache between calls'),
1893 1898 ],
1894 1899 b'-c|-m|FILE')
1895 1900 def perfrevlogwrite(ui, repo, file_=None, startrev=1000, stoprev=-1, **opts):
1896 1901 """Benchmark writing a series of revisions to a revlog.
1897 1902
1898 1903 Possible source values are:
1899 1904 * `full`: add from a full text (default).
1900 1905 * `parent-1`: add from a delta to the first parent
1901 1906 * `parent-2`: add from a delta to the second parent if it exists
1902 1907 (use a delta from the first parent otherwise)
1903 1908 * `parent-smallest`: add from the smallest delta (either p1 or p2)
1904 1909 * `storage`: add from the existing precomputed deltas
1905 1910 """
1906 1911 opts = _byteskwargs(opts)
1907 1912
1908 1913 rl = cmdutil.openrevlog(repo, b'perfrevlogwrite', file_, opts)
1909 1914 rllen = getlen(ui)(rl)
1910 1915 if startrev < 0:
1911 1916 startrev = rllen + startrev
1912 1917 if stoprev < 0:
1913 1918 stoprev = rllen + stoprev
1914 1919
1915 1920 lazydeltabase = opts['lazydeltabase']
1916 1921 source = opts['source']
1917 1922 clearcaches = opts['clear_caches']
1918 1923 validsource = (b'full', b'parent-1', b'parent-2', b'parent-smallest',
1919 1924 b'storage')
1920 1925 if source not in validsource:
1921 1926 raise error.Abort('invalid source type: %s' % source)
1922 1927
1923 1928 ### actually gather results
1924 1929 count = opts['count']
1925 1930 if count <= 0:
1926 1931 raise error.Abort('invalide run count: %d' % count)
1927 1932 allresults = []
1928 1933 for c in range(count):
1929 1934 timing = _timeonewrite(ui, rl, source, startrev, stoprev, c + 1,
1930 1935 lazydeltabase=lazydeltabase,
1931 1936 clearcaches=clearcaches)
1932 1937 allresults.append(timing)
1933 1938
1934 1939 ### consolidate the results in a single list
1935 1940 results = []
1936 1941 for idx, (rev, t) in enumerate(allresults[0]):
1937 1942 ts = [t]
1938 1943 for other in allresults[1:]:
1939 1944 orev, ot = other[idx]
1940 1945 assert orev == rev
1941 1946 ts.append(ot)
1942 1947 results.append((rev, ts))
1943 1948 resultcount = len(results)
1944 1949
1945 1950 ### Compute and display relevant statistics
1946 1951
1947 1952 # get a formatter
1948 1953 fm = ui.formatter(b'perf', opts)
1949 1954 displayall = ui.configbool(b"perf", b"all-timing", False)
1950 1955
1951 1956 # print individual details if requested
1952 1957 if opts['details']:
1953 1958 for idx, item in enumerate(results, 1):
1954 1959 rev, data = item
1955 1960 title = 'revisions #%d of %d, rev %d' % (idx, resultcount, rev)
1956 1961 formatone(fm, data, title=title, displayall=displayall)
1957 1962
1958 1963 # sorts results by median time
1959 1964 results.sort(key=lambda x: sorted(x[1])[len(x[1]) // 2])
1960 1965 # list of (name, index) to display)
1961 1966 relevants = [
1962 1967 ("min", 0),
1963 1968 ("10%", resultcount * 10 // 100),
1964 1969 ("25%", resultcount * 25 // 100),
1965 1970 ("50%", resultcount * 70 // 100),
1966 1971 ("75%", resultcount * 75 // 100),
1967 1972 ("90%", resultcount * 90 // 100),
1968 1973 ("95%", resultcount * 95 // 100),
1969 1974 ("99%", resultcount * 99 // 100),
1970 1975 ("99.9%", resultcount * 999 // 1000),
1971 1976 ("99.99%", resultcount * 9999 // 10000),
1972 1977 ("99.999%", resultcount * 99999 // 100000),
1973 1978 ("max", -1),
1974 1979 ]
1975 1980 if not ui.quiet:
1976 1981 for name, idx in relevants:
1977 1982 data = results[idx]
1978 1983 title = '%s of %d, rev %d' % (name, resultcount, data[0])
1979 1984 formatone(fm, data[1], title=title, displayall=displayall)
1980 1985
1981 1986 # XXX summing that many float will not be very precise, we ignore this fact
1982 1987 # for now
1983 1988 totaltime = []
1984 1989 for item in allresults:
1985 1990 totaltime.append((sum(x[1][0] for x in item),
1986 1991 sum(x[1][1] for x in item),
1987 1992 sum(x[1][2] for x in item),)
1988 1993 )
1989 1994 formatone(fm, totaltime, title="total time (%d revs)" % resultcount,
1990 1995 displayall=displayall)
1991 1996 fm.end()
1992 1997
1993 1998 class _faketr(object):
1994 1999 def add(s, x, y, z=None):
1995 2000 return None
1996 2001
1997 2002 def _timeonewrite(ui, orig, source, startrev, stoprev, runidx=None,
1998 2003 lazydeltabase=True, clearcaches=True):
1999 2004 timings = []
2000 2005 tr = _faketr()
2001 2006 with _temprevlog(ui, orig, startrev) as dest:
2002 2007 dest._lazydeltabase = lazydeltabase
2003 2008 revs = list(orig.revs(startrev, stoprev))
2004 2009 total = len(revs)
2005 2010 topic = 'adding'
2006 2011 if runidx is not None:
2007 2012 topic += ' (run #%d)' % runidx
2008 2013 # Support both old and new progress API
2009 2014 if util.safehasattr(ui, 'makeprogress'):
2010 2015 progress = ui.makeprogress(topic, unit='revs', total=total)
2011 2016 def updateprogress(pos):
2012 2017 progress.update(pos)
2013 2018 def completeprogress():
2014 2019 progress.complete()
2015 2020 else:
2016 2021 def updateprogress(pos):
2017 2022 ui.progress(topic, pos, unit='revs', total=total)
2018 2023 def completeprogress():
2019 2024 ui.progress(topic, None, unit='revs', total=total)
2020 2025
2021 2026 for idx, rev in enumerate(revs):
2022 2027 updateprogress(idx)
2023 2028 addargs, addkwargs = _getrevisionseed(orig, rev, tr, source)
2024 2029 if clearcaches:
2025 2030 dest.index.clearcaches()
2026 2031 dest.clearcaches()
2027 2032 with timeone() as r:
2028 2033 dest.addrawrevision(*addargs, **addkwargs)
2029 2034 timings.append((rev, r[0]))
2030 2035 updateprogress(total)
2031 2036 completeprogress()
2032 2037 return timings
2033 2038
2034 2039 def _getrevisionseed(orig, rev, tr, source):
2035 2040 from mercurial.node import nullid
2036 2041
2037 2042 linkrev = orig.linkrev(rev)
2038 2043 node = orig.node(rev)
2039 2044 p1, p2 = orig.parents(node)
2040 2045 flags = orig.flags(rev)
2041 2046 cachedelta = None
2042 2047 text = None
2043 2048
2044 2049 if source == b'full':
2045 2050 text = orig.revision(rev)
2046 2051 elif source == b'parent-1':
2047 2052 baserev = orig.rev(p1)
2048 2053 cachedelta = (baserev, orig.revdiff(p1, rev))
2049 2054 elif source == b'parent-2':
2050 2055 parent = p2
2051 2056 if p2 == nullid:
2052 2057 parent = p1
2053 2058 baserev = orig.rev(parent)
2054 2059 cachedelta = (baserev, orig.revdiff(parent, rev))
2055 2060 elif source == b'parent-smallest':
2056 2061 p1diff = orig.revdiff(p1, rev)
2057 2062 parent = p1
2058 2063 diff = p1diff
2059 2064 if p2 != nullid:
2060 2065 p2diff = orig.revdiff(p2, rev)
2061 2066 if len(p1diff) > len(p2diff):
2062 2067 parent = p2
2063 2068 diff = p2diff
2064 2069 baserev = orig.rev(parent)
2065 2070 cachedelta = (baserev, diff)
2066 2071 elif source == b'storage':
2067 2072 baserev = orig.deltaparent(rev)
2068 2073 cachedelta = (baserev, orig.revdiff(orig.node(baserev), rev))
2069 2074
2070 2075 return ((text, tr, linkrev, p1, p2),
2071 2076 {'node': node, 'flags': flags, 'cachedelta': cachedelta})
2072 2077
2073 2078 @contextlib.contextmanager
2074 2079 def _temprevlog(ui, orig, truncaterev):
2075 2080 from mercurial import vfs as vfsmod
2076 2081
2077 2082 if orig._inline:
2078 2083 raise error.Abort('not supporting inline revlog (yet)')
2079 2084
2080 2085 origindexpath = orig.opener.join(orig.indexfile)
2081 2086 origdatapath = orig.opener.join(orig.datafile)
2082 2087 indexname = 'revlog.i'
2083 2088 dataname = 'revlog.d'
2084 2089
2085 2090 tmpdir = tempfile.mkdtemp(prefix='tmp-hgperf-')
2086 2091 try:
2087 2092 # copy the data file in a temporary directory
2088 2093 ui.debug('copying data in %s\n' % tmpdir)
2089 2094 destindexpath = os.path.join(tmpdir, 'revlog.i')
2090 2095 destdatapath = os.path.join(tmpdir, 'revlog.d')
2091 2096 shutil.copyfile(origindexpath, destindexpath)
2092 2097 shutil.copyfile(origdatapath, destdatapath)
2093 2098
2094 2099 # remove the data we want to add again
2095 2100 ui.debug('truncating data to be rewritten\n')
2096 2101 with open(destindexpath, 'ab') as index:
2097 2102 index.seek(0)
2098 2103 index.truncate(truncaterev * orig._io.size)
2099 2104 with open(destdatapath, 'ab') as data:
2100 2105 data.seek(0)
2101 2106 data.truncate(orig.start(truncaterev))
2102 2107
2103 2108 # instantiate a new revlog from the temporary copy
2104 2109 ui.debug('truncating adding to be rewritten\n')
2105 2110 vfs = vfsmod.vfs(tmpdir)
2106 2111 vfs.options = getattr(orig.opener, 'options', None)
2107 2112
2108 2113 dest = revlog.revlog(vfs,
2109 2114 indexfile=indexname,
2110 2115 datafile=dataname)
2111 2116 if dest._inline:
2112 2117 raise error.Abort('not supporting inline revlog (yet)')
2113 2118 # make sure internals are initialized
2114 2119 dest.revision(len(dest) - 1)
2115 2120 yield dest
2116 2121 del dest, vfs
2117 2122 finally:
2118 2123 shutil.rmtree(tmpdir, True)
2119 2124
2120 2125 @command(b'perfrevlogchunks', revlogopts + formatteropts +
2121 2126 [(b'e', b'engines', b'', b'compression engines to use'),
2122 2127 (b's', b'startrev', 0, b'revision to start at')],
2123 2128 b'-c|-m|FILE')
2124 2129 def perfrevlogchunks(ui, repo, file_=None, engines=None, startrev=0, **opts):
2125 2130 """Benchmark operations on revlog chunks.
2126 2131
2127 2132 Logically, each revlog is a collection of fulltext revisions. However,
2128 2133 stored within each revlog are "chunks" of possibly compressed data. This
2129 2134 data needs to be read and decompressed or compressed and written.
2130 2135
2131 2136 This command measures the time it takes to read+decompress and recompress
2132 2137 chunks in a revlog. It effectively isolates I/O and compression performance.
2133 2138 For measurements of higher-level operations like resolving revisions,
2134 2139 see ``perfrevlogrevisions`` and ``perfrevlogrevision``.
2135 2140 """
2136 2141 opts = _byteskwargs(opts)
2137 2142
2138 2143 rl = cmdutil.openrevlog(repo, b'perfrevlogchunks', file_, opts)
2139 2144
2140 2145 # _chunkraw was renamed to _getsegmentforrevs.
2141 2146 try:
2142 2147 segmentforrevs = rl._getsegmentforrevs
2143 2148 except AttributeError:
2144 2149 segmentforrevs = rl._chunkraw
2145 2150
2146 2151 # Verify engines argument.
2147 2152 if engines:
2148 2153 engines = set(e.strip() for e in engines.split(b','))
2149 2154 for engine in engines:
2150 2155 try:
2151 2156 util.compressionengines[engine]
2152 2157 except KeyError:
2153 2158 raise error.Abort(b'unknown compression engine: %s' % engine)
2154 2159 else:
2155 2160 engines = []
2156 2161 for e in util.compengines:
2157 2162 engine = util.compengines[e]
2158 2163 try:
2159 2164 if engine.available():
2160 2165 engine.revlogcompressor().compress(b'dummy')
2161 2166 engines.append(e)
2162 2167 except NotImplementedError:
2163 2168 pass
2164 2169
2165 2170 revs = list(rl.revs(startrev, len(rl) - 1))
2166 2171
2167 2172 def rlfh(rl):
2168 2173 if rl._inline:
2169 2174 return getsvfs(repo)(rl.indexfile)
2170 2175 else:
2171 2176 return getsvfs(repo)(rl.datafile)
2172 2177
2173 2178 def doread():
2174 2179 rl.clearcaches()
2175 2180 for rev in revs:
2176 2181 segmentforrevs(rev, rev)
2177 2182
2178 2183 def doreadcachedfh():
2179 2184 rl.clearcaches()
2180 2185 fh = rlfh(rl)
2181 2186 for rev in revs:
2182 2187 segmentforrevs(rev, rev, df=fh)
2183 2188
2184 2189 def doreadbatch():
2185 2190 rl.clearcaches()
2186 2191 segmentforrevs(revs[0], revs[-1])
2187 2192
2188 2193 def doreadbatchcachedfh():
2189 2194 rl.clearcaches()
2190 2195 fh = rlfh(rl)
2191 2196 segmentforrevs(revs[0], revs[-1], df=fh)
2192 2197
2193 2198 def dochunk():
2194 2199 rl.clearcaches()
2195 2200 fh = rlfh(rl)
2196 2201 for rev in revs:
2197 2202 rl._chunk(rev, df=fh)
2198 2203
2199 2204 chunks = [None]
2200 2205
2201 2206 def dochunkbatch():
2202 2207 rl.clearcaches()
2203 2208 fh = rlfh(rl)
2204 2209 # Save chunks as a side-effect.
2205 2210 chunks[0] = rl._chunks(revs, df=fh)
2206 2211
2207 2212 def docompress(compressor):
2208 2213 rl.clearcaches()
2209 2214
2210 2215 try:
2211 2216 # Swap in the requested compression engine.
2212 2217 oldcompressor = rl._compressor
2213 2218 rl._compressor = compressor
2214 2219 for chunk in chunks[0]:
2215 2220 rl.compress(chunk)
2216 2221 finally:
2217 2222 rl._compressor = oldcompressor
2218 2223
2219 2224 benches = [
2220 2225 (lambda: doread(), b'read'),
2221 2226 (lambda: doreadcachedfh(), b'read w/ reused fd'),
2222 2227 (lambda: doreadbatch(), b'read batch'),
2223 2228 (lambda: doreadbatchcachedfh(), b'read batch w/ reused fd'),
2224 2229 (lambda: dochunk(), b'chunk'),
2225 2230 (lambda: dochunkbatch(), b'chunk batch'),
2226 2231 ]
2227 2232
2228 2233 for engine in sorted(engines):
2229 2234 compressor = util.compengines[engine].revlogcompressor()
2230 2235 benches.append((functools.partial(docompress, compressor),
2231 2236 b'compress w/ %s' % engine))
2232 2237
2233 2238 for fn, title in benches:
2234 2239 timer, fm = gettimer(ui, opts)
2235 2240 timer(fn, title=title)
2236 2241 fm.end()
2237 2242
2238 2243 @command(b'perfrevlogrevision', revlogopts + formatteropts +
2239 2244 [(b'', b'cache', False, b'use caches instead of clearing')],
2240 2245 b'-c|-m|FILE REV')
2241 2246 def perfrevlogrevision(ui, repo, file_, rev=None, cache=None, **opts):
2242 2247 """Benchmark obtaining a revlog revision.
2243 2248
2244 2249 Obtaining a revlog revision consists of roughly the following steps:
2245 2250
2246 2251 1. Compute the delta chain
2247 2252 2. Slice the delta chain if applicable
2248 2253 3. Obtain the raw chunks for that delta chain
2249 2254 4. Decompress each raw chunk
2250 2255 5. Apply binary patches to obtain fulltext
2251 2256 6. Verify hash of fulltext
2252 2257
2253 2258 This command measures the time spent in each of these phases.
2254 2259 """
2255 2260 opts = _byteskwargs(opts)
2256 2261
2257 2262 if opts.get(b'changelog') or opts.get(b'manifest'):
2258 2263 file_, rev = None, file_
2259 2264 elif rev is None:
2260 2265 raise error.CommandError(b'perfrevlogrevision', b'invalid arguments')
2261 2266
2262 2267 r = cmdutil.openrevlog(repo, b'perfrevlogrevision', file_, opts)
2263 2268
2264 2269 # _chunkraw was renamed to _getsegmentforrevs.
2265 2270 try:
2266 2271 segmentforrevs = r._getsegmentforrevs
2267 2272 except AttributeError:
2268 2273 segmentforrevs = r._chunkraw
2269 2274
2270 2275 node = r.lookup(rev)
2271 2276 rev = r.rev(node)
2272 2277
2273 2278 def getrawchunks(data, chain):
2274 2279 start = r.start
2275 2280 length = r.length
2276 2281 inline = r._inline
2277 2282 iosize = r._io.size
2278 2283 buffer = util.buffer
2279 2284
2280 2285 chunks = []
2281 2286 ladd = chunks.append
2282 2287 for idx, item in enumerate(chain):
2283 2288 offset = start(item[0])
2284 2289 bits = data[idx]
2285 2290 for rev in item:
2286 2291 chunkstart = start(rev)
2287 2292 if inline:
2288 2293 chunkstart += (rev + 1) * iosize
2289 2294 chunklength = length(rev)
2290 2295 ladd(buffer(bits, chunkstart - offset, chunklength))
2291 2296
2292 2297 return chunks
2293 2298
2294 2299 def dodeltachain(rev):
2295 2300 if not cache:
2296 2301 r.clearcaches()
2297 2302 r._deltachain(rev)
2298 2303
2299 2304 def doread(chain):
2300 2305 if not cache:
2301 2306 r.clearcaches()
2302 2307 for item in slicedchain:
2303 2308 segmentforrevs(item[0], item[-1])
2304 2309
2305 2310 def doslice(r, chain, size):
2306 2311 for s in slicechunk(r, chain, targetsize=size):
2307 2312 pass
2308 2313
2309 2314 def dorawchunks(data, chain):
2310 2315 if not cache:
2311 2316 r.clearcaches()
2312 2317 getrawchunks(data, chain)
2313 2318
2314 2319 def dodecompress(chunks):
2315 2320 decomp = r.decompress
2316 2321 for chunk in chunks:
2317 2322 decomp(chunk)
2318 2323
2319 2324 def dopatch(text, bins):
2320 2325 if not cache:
2321 2326 r.clearcaches()
2322 2327 mdiff.patches(text, bins)
2323 2328
2324 2329 def dohash(text):
2325 2330 if not cache:
2326 2331 r.clearcaches()
2327 2332 r.checkhash(text, node, rev=rev)
2328 2333
2329 2334 def dorevision():
2330 2335 if not cache:
2331 2336 r.clearcaches()
2332 2337 r.revision(node)
2333 2338
2334 2339 try:
2335 2340 from mercurial.revlogutils.deltas import slicechunk
2336 2341 except ImportError:
2337 2342 slicechunk = getattr(revlog, '_slicechunk', None)
2338 2343
2339 2344 size = r.length(rev)
2340 2345 chain = r._deltachain(rev)[0]
2341 2346 if not getattr(r, '_withsparseread', False):
2342 2347 slicedchain = (chain,)
2343 2348 else:
2344 2349 slicedchain = tuple(slicechunk(r, chain, targetsize=size))
2345 2350 data = [segmentforrevs(seg[0], seg[-1])[1] for seg in slicedchain]
2346 2351 rawchunks = getrawchunks(data, slicedchain)
2347 2352 bins = r._chunks(chain)
2348 2353 text = bytes(bins[0])
2349 2354 bins = bins[1:]
2350 2355 text = mdiff.patches(text, bins)
2351 2356
2352 2357 benches = [
2353 2358 (lambda: dorevision(), b'full'),
2354 2359 (lambda: dodeltachain(rev), b'deltachain'),
2355 2360 (lambda: doread(chain), b'read'),
2356 2361 ]
2357 2362
2358 2363 if getattr(r, '_withsparseread', False):
2359 2364 slicing = (lambda: doslice(r, chain, size), b'slice-sparse-chain')
2360 2365 benches.append(slicing)
2361 2366
2362 2367 benches.extend([
2363 2368 (lambda: dorawchunks(data, slicedchain), b'rawchunks'),
2364 2369 (lambda: dodecompress(rawchunks), b'decompress'),
2365 2370 (lambda: dopatch(text, bins), b'patch'),
2366 2371 (lambda: dohash(text), b'hash'),
2367 2372 ])
2368 2373
2369 2374 timer, fm = gettimer(ui, opts)
2370 2375 for fn, title in benches:
2371 2376 timer(fn, title=title)
2372 2377 fm.end()
2373 2378
2374 2379 @command(b'perfrevset',
2375 2380 [(b'C', b'clear', False, b'clear volatile cache between each call.'),
2376 2381 (b'', b'contexts', False, b'obtain changectx for each revision')]
2377 2382 + formatteropts, b"REVSET")
2378 2383 def perfrevset(ui, repo, expr, clear=False, contexts=False, **opts):
2379 2384 """benchmark the execution time of a revset
2380 2385
2381 2386 Use the --clean option if need to evaluate the impact of build volatile
2382 2387 revisions set cache on the revset execution. Volatile cache hold filtered
2383 2388 and obsolete related cache."""
2384 2389 opts = _byteskwargs(opts)
2385 2390
2386 2391 timer, fm = gettimer(ui, opts)
2387 2392 def d():
2388 2393 if clear:
2389 2394 repo.invalidatevolatilesets()
2390 2395 if contexts:
2391 2396 for ctx in repo.set(expr): pass
2392 2397 else:
2393 2398 for r in repo.revs(expr): pass
2394 2399 timer(d)
2395 2400 fm.end()
2396 2401
2397 2402 @command(b'perfvolatilesets',
2398 2403 [(b'', b'clear-obsstore', False, b'drop obsstore between each call.'),
2399 2404 ] + formatteropts)
2400 2405 def perfvolatilesets(ui, repo, *names, **opts):
2401 2406 """benchmark the computation of various volatile set
2402 2407
2403 2408 Volatile set computes element related to filtering and obsolescence."""
2404 2409 opts = _byteskwargs(opts)
2405 2410 timer, fm = gettimer(ui, opts)
2406 2411 repo = repo.unfiltered()
2407 2412
2408 2413 def getobs(name):
2409 2414 def d():
2410 2415 repo.invalidatevolatilesets()
2411 2416 if opts[b'clear_obsstore']:
2412 2417 clearfilecache(repo, b'obsstore')
2413 2418 obsolete.getrevs(repo, name)
2414 2419 return d
2415 2420
2416 2421 allobs = sorted(obsolete.cachefuncs)
2417 2422 if names:
2418 2423 allobs = [n for n in allobs if n in names]
2419 2424
2420 2425 for name in allobs:
2421 2426 timer(getobs(name), title=name)
2422 2427
2423 2428 def getfiltered(name):
2424 2429 def d():
2425 2430 repo.invalidatevolatilesets()
2426 2431 if opts[b'clear_obsstore']:
2427 2432 clearfilecache(repo, b'obsstore')
2428 2433 repoview.filterrevs(repo, name)
2429 2434 return d
2430 2435
2431 2436 allfilter = sorted(repoview.filtertable)
2432 2437 if names:
2433 2438 allfilter = [n for n in allfilter if n in names]
2434 2439
2435 2440 for name in allfilter:
2436 2441 timer(getfiltered(name), title=name)
2437 2442 fm.end()
2438 2443
2439 2444 @command(b'perfbranchmap',
2440 2445 [(b'f', b'full', False,
2441 2446 b'Includes build time of subset'),
2442 2447 (b'', b'clear-revbranch', False,
2443 2448 b'purge the revbranch cache between computation'),
2444 2449 ] + formatteropts)
2445 2450 def perfbranchmap(ui, repo, *filternames, **opts):
2446 2451 """benchmark the update of a branchmap
2447 2452
2448 2453 This benchmarks the full repo.branchmap() call with read and write disabled
2449 2454 """
2450 2455 opts = _byteskwargs(opts)
2451 2456 full = opts.get(b"full", False)
2452 2457 clear_revbranch = opts.get(b"clear_revbranch", False)
2453 2458 timer, fm = gettimer(ui, opts)
2454 2459 def getbranchmap(filtername):
2455 2460 """generate a benchmark function for the filtername"""
2456 2461 if filtername is None:
2457 2462 view = repo
2458 2463 else:
2459 2464 view = repo.filtered(filtername)
2460 2465 if util.safehasattr(view._branchcaches, '_per_filter'):
2461 2466 filtered = view._branchcaches._per_filter
2462 2467 else:
2463 2468 # older versions
2464 2469 filtered = view._branchcaches
2465 2470 def d():
2466 2471 if clear_revbranch:
2467 2472 repo.revbranchcache()._clear()
2468 2473 if full:
2469 2474 view._branchcaches.clear()
2470 2475 else:
2471 2476 filtered.pop(filtername, None)
2472 2477 view.branchmap()
2473 2478 return d
2474 2479 # add filter in smaller subset to bigger subset
2475 2480 possiblefilters = set(repoview.filtertable)
2476 2481 if filternames:
2477 2482 possiblefilters &= set(filternames)
2478 2483 subsettable = getbranchmapsubsettable()
2479 2484 allfilters = []
2480 2485 while possiblefilters:
2481 2486 for name in possiblefilters:
2482 2487 subset = subsettable.get(name)
2483 2488 if subset not in possiblefilters:
2484 2489 break
2485 2490 else:
2486 2491 assert False, b'subset cycle %s!' % possiblefilters
2487 2492 allfilters.append(name)
2488 2493 possiblefilters.remove(name)
2489 2494
2490 2495 # warm the cache
2491 2496 if not full:
2492 2497 for name in allfilters:
2493 2498 repo.filtered(name).branchmap()
2494 2499 if not filternames or b'unfiltered' in filternames:
2495 2500 # add unfiltered
2496 2501 allfilters.append(None)
2497 2502
2498 2503 if util.safehasattr(branchmap.branchcache, 'fromfile'):
2499 2504 branchcacheread = safeattrsetter(branchmap.branchcache, b'fromfile')
2500 2505 branchcacheread.set(classmethod(lambda *args: None))
2501 2506 else:
2502 2507 # older versions
2503 2508 branchcacheread = safeattrsetter(branchmap, b'read')
2504 2509 branchcacheread.set(lambda *args: None)
2505 2510 branchcachewrite = safeattrsetter(branchmap.branchcache, b'write')
2506 2511 branchcachewrite.set(lambda *args: None)
2507 2512 try:
2508 2513 for name in allfilters:
2509 2514 printname = name
2510 2515 if name is None:
2511 2516 printname = b'unfiltered'
2512 2517 timer(getbranchmap(name), title=str(printname))
2513 2518 finally:
2514 2519 branchcacheread.restore()
2515 2520 branchcachewrite.restore()
2516 2521 fm.end()
2517 2522
2518 2523 @command(b'perfbranchmapupdate', [
2519 2524 (b'', b'base', [], b'subset of revision to start from'),
2520 2525 (b'', b'target', [], b'subset of revision to end with'),
2521 2526 (b'', b'clear-caches', False, b'clear cache between each runs')
2522 2527 ] + formatteropts)
2523 2528 def perfbranchmapupdate(ui, repo, base=(), target=(), **opts):
2524 2529 """benchmark branchmap update from for <base> revs to <target> revs
2525 2530
2526 2531 If `--clear-caches` is passed, the following items will be reset before
2527 2532 each update:
2528 2533 * the changelog instance and associated indexes
2529 2534 * the rev-branch-cache instance
2530 2535
2531 2536 Examples:
2532 2537
2533 2538 # update for the one last revision
2534 2539 $ hg perfbranchmapupdate --base 'not tip' --target 'tip'
2535 2540
2536 2541 $ update for change coming with a new branch
2537 2542 $ hg perfbranchmapupdate --base 'stable' --target 'default'
2538 2543 """
2539 2544 from mercurial import branchmap
2540 2545 from mercurial import repoview
2541 2546 opts = _byteskwargs(opts)
2542 2547 timer, fm = gettimer(ui, opts)
2543 2548 clearcaches = opts[b'clear_caches']
2544 2549 unfi = repo.unfiltered()
2545 2550 x = [None] # used to pass data between closure
2546 2551
2547 2552 # we use a `list` here to avoid possible side effect from smartset
2548 2553 baserevs = list(scmutil.revrange(repo, base))
2549 2554 targetrevs = list(scmutil.revrange(repo, target))
2550 2555 if not baserevs:
2551 2556 raise error.Abort(b'no revisions selected for --base')
2552 2557 if not targetrevs:
2553 2558 raise error.Abort(b'no revisions selected for --target')
2554 2559
2555 2560 # make sure the target branchmap also contains the one in the base
2556 2561 targetrevs = list(set(baserevs) | set(targetrevs))
2557 2562 targetrevs.sort()
2558 2563
2559 2564 cl = repo.changelog
2560 2565 allbaserevs = list(cl.ancestors(baserevs, inclusive=True))
2561 2566 allbaserevs.sort()
2562 2567 alltargetrevs = frozenset(cl.ancestors(targetrevs, inclusive=True))
2563 2568
2564 2569 newrevs = list(alltargetrevs.difference(allbaserevs))
2565 2570 newrevs.sort()
2566 2571
2567 2572 allrevs = frozenset(unfi.changelog.revs())
2568 2573 basefilterrevs = frozenset(allrevs.difference(allbaserevs))
2569 2574 targetfilterrevs = frozenset(allrevs.difference(alltargetrevs))
2570 2575
2571 2576 def basefilter(repo, visibilityexceptions=None):
2572 2577 return basefilterrevs
2573 2578
2574 2579 def targetfilter(repo, visibilityexceptions=None):
2575 2580 return targetfilterrevs
2576 2581
2577 2582 msg = b'benchmark of branchmap with %d revisions with %d new ones\n'
2578 2583 ui.status(msg % (len(allbaserevs), len(newrevs)))
2579 2584 if targetfilterrevs:
2580 2585 msg = b'(%d revisions still filtered)\n'
2581 2586 ui.status(msg % len(targetfilterrevs))
2582 2587
2583 2588 try:
2584 2589 repoview.filtertable[b'__perf_branchmap_update_base'] = basefilter
2585 2590 repoview.filtertable[b'__perf_branchmap_update_target'] = targetfilter
2586 2591
2587 2592 baserepo = repo.filtered(b'__perf_branchmap_update_base')
2588 2593 targetrepo = repo.filtered(b'__perf_branchmap_update_target')
2589 2594
2590 2595 # try to find an existing branchmap to reuse
2591 2596 subsettable = getbranchmapsubsettable()
2592 2597 candidatefilter = subsettable.get(None)
2593 2598 while candidatefilter is not None:
2594 2599 candidatebm = repo.filtered(candidatefilter).branchmap()
2595 2600 if candidatebm.validfor(baserepo):
2596 2601 filtered = repoview.filterrevs(repo, candidatefilter)
2597 2602 missing = [r for r in allbaserevs if r in filtered]
2598 2603 base = candidatebm.copy()
2599 2604 base.update(baserepo, missing)
2600 2605 break
2601 2606 candidatefilter = subsettable.get(candidatefilter)
2602 2607 else:
2603 2608 # no suitable subset where found
2604 2609 base = branchmap.branchcache()
2605 2610 base.update(baserepo, allbaserevs)
2606 2611
2607 2612 def setup():
2608 2613 x[0] = base.copy()
2609 2614 if clearcaches:
2610 2615 unfi._revbranchcache = None
2611 2616 clearchangelog(repo)
2612 2617
2613 2618 def bench():
2614 2619 x[0].update(targetrepo, newrevs)
2615 2620
2616 2621 timer(bench, setup=setup)
2617 2622 fm.end()
2618 2623 finally:
2619 2624 repoview.filtertable.pop(b'__perf_branchmap_update_base', None)
2620 2625 repoview.filtertable.pop(b'__perf_branchmap_update_target', None)
2621 2626
2622 2627 @command(b'perfbranchmapload', [
2623 2628 (b'f', b'filter', b'', b'Specify repoview filter'),
2624 2629 (b'', b'list', False, b'List brachmap filter caches'),
2625 2630 (b'', b'clear-revlogs', False, b'refresh changelog and manifest'),
2626 2631
2627 2632 ] + formatteropts)
2628 2633 def perfbranchmapload(ui, repo, filter=b'', list=False, **opts):
2629 2634 """benchmark reading the branchmap"""
2630 2635 opts = _byteskwargs(opts)
2631 2636 clearrevlogs = opts[b'clear_revlogs']
2632 2637
2633 2638 if list:
2634 2639 for name, kind, st in repo.cachevfs.readdir(stat=True):
2635 2640 if name.startswith(b'branch2'):
2636 2641 filtername = name.partition(b'-')[2] or b'unfiltered'
2637 2642 ui.status(b'%s - %s\n'
2638 2643 % (filtername, util.bytecount(st.st_size)))
2639 2644 return
2640 2645 if not filter:
2641 2646 filter = None
2642 2647 subsettable = getbranchmapsubsettable()
2643 2648 if filter is None:
2644 2649 repo = repo.unfiltered()
2645 2650 else:
2646 2651 repo = repoview.repoview(repo, filter)
2647 2652
2648 2653 repo.branchmap() # make sure we have a relevant, up to date branchmap
2649 2654
2650 2655 try:
2651 2656 fromfile = branchmap.branchcache.fromfile
2652 2657 except AttributeError:
2653 2658 # older versions
2654 2659 fromfile = branchmap.read
2655 2660
2656 2661 currentfilter = filter
2657 2662 # try once without timer, the filter may not be cached
2658 2663 while fromfile(repo) is None:
2659 2664 currentfilter = subsettable.get(currentfilter)
2660 2665 if currentfilter is None:
2661 2666 raise error.Abort(b'No branchmap cached for %s repo'
2662 2667 % (filter or b'unfiltered'))
2663 2668 repo = repo.filtered(currentfilter)
2664 2669 timer, fm = gettimer(ui, opts)
2665 2670 def setup():
2666 2671 if clearrevlogs:
2667 2672 clearchangelog(repo)
2668 2673 def bench():
2669 2674 fromfile(repo)
2670 2675 timer(bench, setup=setup)
2671 2676 fm.end()
2672 2677
2673 2678 @command(b'perfloadmarkers')
2674 2679 def perfloadmarkers(ui, repo):
2675 2680 """benchmark the time to parse the on-disk markers for a repo
2676 2681
2677 2682 Result is the number of markers in the repo."""
2678 2683 timer, fm = gettimer(ui)
2679 2684 svfs = getsvfs(repo)
2680 2685 timer(lambda: len(obsolete.obsstore(svfs)))
2681 2686 fm.end()
2682 2687
2683 2688 @command(b'perflrucachedict', formatteropts +
2684 2689 [(b'', b'costlimit', 0, b'maximum total cost of items in cache'),
2685 2690 (b'', b'mincost', 0, b'smallest cost of items in cache'),
2686 2691 (b'', b'maxcost', 100, b'maximum cost of items in cache'),
2687 2692 (b'', b'size', 4, b'size of cache'),
2688 2693 (b'', b'gets', 10000, b'number of key lookups'),
2689 2694 (b'', b'sets', 10000, b'number of key sets'),
2690 2695 (b'', b'mixed', 10000, b'number of mixed mode operations'),
2691 2696 (b'', b'mixedgetfreq', 50, b'frequency of get vs set ops in mixed mode')],
2692 2697 norepo=True)
2693 2698 def perflrucache(ui, mincost=0, maxcost=100, costlimit=0, size=4,
2694 2699 gets=10000, sets=10000, mixed=10000, mixedgetfreq=50, **opts):
2695 2700 opts = _byteskwargs(opts)
2696 2701
2697 2702 def doinit():
2698 2703 for i in _xrange(10000):
2699 2704 util.lrucachedict(size)
2700 2705
2701 2706 costrange = list(range(mincost, maxcost + 1))
2702 2707
2703 2708 values = []
2704 2709 for i in _xrange(size):
2705 2710 values.append(random.randint(0, _maxint))
2706 2711
2707 2712 # Get mode fills the cache and tests raw lookup performance with no
2708 2713 # eviction.
2709 2714 getseq = []
2710 2715 for i in _xrange(gets):
2711 2716 getseq.append(random.choice(values))
2712 2717
2713 2718 def dogets():
2714 2719 d = util.lrucachedict(size)
2715 2720 for v in values:
2716 2721 d[v] = v
2717 2722 for key in getseq:
2718 2723 value = d[key]
2719 2724 value # silence pyflakes warning
2720 2725
2721 2726 def dogetscost():
2722 2727 d = util.lrucachedict(size, maxcost=costlimit)
2723 2728 for i, v in enumerate(values):
2724 2729 d.insert(v, v, cost=costs[i])
2725 2730 for key in getseq:
2726 2731 try:
2727 2732 value = d[key]
2728 2733 value # silence pyflakes warning
2729 2734 except KeyError:
2730 2735 pass
2731 2736
2732 2737 # Set mode tests insertion speed with cache eviction.
2733 2738 setseq = []
2734 2739 costs = []
2735 2740 for i in _xrange(sets):
2736 2741 setseq.append(random.randint(0, _maxint))
2737 2742 costs.append(random.choice(costrange))
2738 2743
2739 2744 def doinserts():
2740 2745 d = util.lrucachedict(size)
2741 2746 for v in setseq:
2742 2747 d.insert(v, v)
2743 2748
2744 2749 def doinsertscost():
2745 2750 d = util.lrucachedict(size, maxcost=costlimit)
2746 2751 for i, v in enumerate(setseq):
2747 2752 d.insert(v, v, cost=costs[i])
2748 2753
2749 2754 def dosets():
2750 2755 d = util.lrucachedict(size)
2751 2756 for v in setseq:
2752 2757 d[v] = v
2753 2758
2754 2759 # Mixed mode randomly performs gets and sets with eviction.
2755 2760 mixedops = []
2756 2761 for i in _xrange(mixed):
2757 2762 r = random.randint(0, 100)
2758 2763 if r < mixedgetfreq:
2759 2764 op = 0
2760 2765 else:
2761 2766 op = 1
2762 2767
2763 2768 mixedops.append((op,
2764 2769 random.randint(0, size * 2),
2765 2770 random.choice(costrange)))
2766 2771
2767 2772 def domixed():
2768 2773 d = util.lrucachedict(size)
2769 2774
2770 2775 for op, v, cost in mixedops:
2771 2776 if op == 0:
2772 2777 try:
2773 2778 d[v]
2774 2779 except KeyError:
2775 2780 pass
2776 2781 else:
2777 2782 d[v] = v
2778 2783
2779 2784 def domixedcost():
2780 2785 d = util.lrucachedict(size, maxcost=costlimit)
2781 2786
2782 2787 for op, v, cost in mixedops:
2783 2788 if op == 0:
2784 2789 try:
2785 2790 d[v]
2786 2791 except KeyError:
2787 2792 pass
2788 2793 else:
2789 2794 d.insert(v, v, cost=cost)
2790 2795
2791 2796 benches = [
2792 2797 (doinit, b'init'),
2793 2798 ]
2794 2799
2795 2800 if costlimit:
2796 2801 benches.extend([
2797 2802 (dogetscost, b'gets w/ cost limit'),
2798 2803 (doinsertscost, b'inserts w/ cost limit'),
2799 2804 (domixedcost, b'mixed w/ cost limit'),
2800 2805 ])
2801 2806 else:
2802 2807 benches.extend([
2803 2808 (dogets, b'gets'),
2804 2809 (doinserts, b'inserts'),
2805 2810 (dosets, b'sets'),
2806 2811 (domixed, b'mixed')
2807 2812 ])
2808 2813
2809 2814 for fn, title in benches:
2810 2815 timer, fm = gettimer(ui, opts)
2811 2816 timer(fn, title=title)
2812 2817 fm.end()
2813 2818
2814 2819 @command(b'perfwrite', formatteropts)
2815 2820 def perfwrite(ui, repo, **opts):
2816 2821 """microbenchmark ui.write
2817 2822 """
2818 2823 opts = _byteskwargs(opts)
2819 2824
2820 2825 timer, fm = gettimer(ui, opts)
2821 2826 def write():
2822 2827 for i in range(100000):
2823 2828 ui.write((b'Testing write performance\n'))
2824 2829 timer(write)
2825 2830 fm.end()
2826 2831
2827 2832 def uisetup(ui):
2828 2833 if (util.safehasattr(cmdutil, b'openrevlog') and
2829 2834 not util.safehasattr(commands, b'debugrevlogopts')):
2830 2835 # for "historical portability":
2831 2836 # In this case, Mercurial should be 1.9 (or a79fea6b3e77) -
2832 2837 # 3.7 (or 5606f7d0d063). Therefore, '--dir' option for
2833 2838 # openrevlog() should cause failure, because it has been
2834 2839 # available since 3.5 (or 49c583ca48c4).
2835 2840 def openrevlog(orig, repo, cmd, file_, opts):
2836 2841 if opts.get(b'dir') and not util.safehasattr(repo, b'dirlog'):
2837 2842 raise error.Abort(b"This version doesn't support --dir option",
2838 2843 hint=b"use 3.5 or later")
2839 2844 return orig(repo, cmd, file_, opts)
2840 2845 extensions.wrapfunction(cmdutil, b'openrevlog', openrevlog)
2841 2846
2842 2847 @command(b'perfprogress', formatteropts + [
2843 2848 (b'', b'topic', b'topic', b'topic for progress messages'),
2844 2849 (b'c', b'total', 1000000, b'total value we are progressing to'),
2845 2850 ], norepo=True)
2846 2851 def perfprogress(ui, topic=None, total=None, **opts):
2847 2852 """printing of progress bars"""
2848 2853 opts = _byteskwargs(opts)
2849 2854
2850 2855 timer, fm = gettimer(ui, opts)
2851 2856
2852 2857 def doprogress():
2853 2858 with ui.makeprogress(topic, total=total) as progress:
2854 2859 for i in pycompat.xrange(total):
2855 2860 progress.increment()
2856 2861
2857 2862 timer(doprogress)
2858 2863 fm.end()
@@ -1,669 +1,658
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 __future__ import absolute_import
9 9
10 10 import struct
11 11
12 12 from .node import (
13 13 bin,
14 14 hex,
15 15 nullid,
16 16 nullrev,
17 17 )
18 18 from . import (
19 19 encoding,
20 20 error,
21 21 pycompat,
22 22 scmutil,
23 23 util,
24 24 )
25 25 from .utils import (
26 repoviewutil,
26 27 stringutil,
27 28 )
28 29
30 subsettable = repoviewutil. subsettable
31
29 32 calcsize = struct.calcsize
30 33 pack_into = struct.pack_into
31 34 unpack_from = struct.unpack_from
32 35
33 36
34 ### Nearest subset relation
35 # Nearest subset of filter X is a filter Y so that:
36 # * Y is included in X,
37 # * X - Y is as small as possible.
38 # This create and ordering used for branchmap purpose.
39 # the ordering may be partial
40 subsettable = {None: 'visible',
41 'visible-hidden': 'visible',
42 'visible': 'served',
43 'served.hidden': 'served',
44 'served': 'immutable',
45 'immutable': 'base'}
46
47
48 37 class BranchMapCache(object):
49 38 """mapping of filtered views of repo with their branchcache"""
50 39 def __init__(self):
51 40 self._per_filter = {}
52 41
53 42 def __getitem__(self, repo):
54 43 self.updatecache(repo)
55 44 return self._per_filter[repo.filtername]
56 45
57 46 def updatecache(self, repo):
58 47 """Update the cache for the given filtered view on a repository"""
59 48 # This can trigger updates for the caches for subsets of the filtered
60 49 # view, e.g. when there is no cache for this filtered view or the cache
61 50 # is stale.
62 51
63 52 cl = repo.changelog
64 53 filtername = repo.filtername
65 54 bcache = self._per_filter.get(filtername)
66 55 if bcache is None or not bcache.validfor(repo):
67 56 # cache object missing or cache object stale? Read from disk
68 57 bcache = branchcache.fromfile(repo)
69 58
70 59 revs = []
71 60 if bcache is None:
72 61 # no (fresh) cache available anymore, perhaps we can re-use
73 62 # the cache for a subset, then extend that to add info on missing
74 63 # revisions.
75 64 subsetname = subsettable.get(filtername)
76 65 if subsetname is not None:
77 66 subset = repo.filtered(subsetname)
78 67 bcache = self[subset].copy()
79 68 extrarevs = subset.changelog.filteredrevs - cl.filteredrevs
80 69 revs.extend(r for r in extrarevs if r <= bcache.tiprev)
81 70 else:
82 71 # nothing to fall back on, start empty.
83 72 bcache = branchcache()
84 73
85 74 revs.extend(cl.revs(start=bcache.tiprev + 1))
86 75 if revs:
87 76 bcache.update(repo, revs)
88 77
89 78 assert bcache.validfor(repo), filtername
90 79 self._per_filter[repo.filtername] = bcache
91 80
92 81 def replace(self, repo, remotebranchmap):
93 82 """Replace the branchmap cache for a repo with a branch mapping.
94 83
95 84 This is likely only called during clone with a branch map from a
96 85 remote.
97 86
98 87 """
99 88 cl = repo.changelog
100 89 clrev = cl.rev
101 90 clbranchinfo = cl.branchinfo
102 91 rbheads = []
103 92 closed = []
104 93 for bheads in remotebranchmap.itervalues():
105 94 rbheads += bheads
106 95 for h in bheads:
107 96 r = clrev(h)
108 97 b, c = clbranchinfo(r)
109 98 if c:
110 99 closed.append(h)
111 100
112 101 if rbheads:
113 102 rtiprev = max((int(clrev(node)) for node in rbheads))
114 103 cache = branchcache(
115 104 remotebranchmap, repo[rtiprev].node(), rtiprev,
116 105 closednodes=closed)
117 106
118 107 # Try to stick it as low as possible
119 108 # filter above served are unlikely to be fetch from a clone
120 109 for candidate in ('base', 'immutable', 'served'):
121 110 rview = repo.filtered(candidate)
122 111 if cache.validfor(rview):
123 112 self._per_filter[candidate] = cache
124 113 cache.write(rview)
125 114 return
126 115
127 116 def clear(self):
128 117 self._per_filter.clear()
129 118
130 119 def _unknownnode(node):
131 120 """ raises ValueError when branchcache found a node which does not exists
132 121 """
133 122 raise ValueError(r'node %s does not exist' % pycompat.sysstr(hex(node)))
134 123
135 124 class branchcache(object):
136 125 """A dict like object that hold branches heads cache.
137 126
138 127 This cache is used to avoid costly computations to determine all the
139 128 branch heads of a repo.
140 129
141 130 The cache is serialized on disk in the following format:
142 131
143 132 <tip hex node> <tip rev number> [optional filtered repo hex hash]
144 133 <branch head hex node> <open/closed state> <branch name>
145 134 <branch head hex node> <open/closed state> <branch name>
146 135 ...
147 136
148 137 The first line is used to check if the cache is still valid. If the
149 138 branch cache is for a filtered repo view, an optional third hash is
150 139 included that hashes the hashes of all filtered revisions.
151 140
152 141 The open/closed state is represented by a single letter 'o' or 'c'.
153 142 This field can be used to avoid changelog reads when determining if a
154 143 branch head closes a branch or not.
155 144 """
156 145
157 146 def __init__(self, entries=(), tipnode=nullid, tiprev=nullrev,
158 147 filteredhash=None, closednodes=None, hasnode=None):
159 148 """ hasnode is a function which can be used to verify whether changelog
160 149 has a given node or not. If it's not provided, we assume that every node
161 150 we have exists in changelog """
162 151 self.tipnode = tipnode
163 152 self.tiprev = tiprev
164 153 self.filteredhash = filteredhash
165 154 # closednodes is a set of nodes that close their branch. If the branch
166 155 # cache has been updated, it may contain nodes that are no longer
167 156 # heads.
168 157 if closednodes is None:
169 158 self._closednodes = set()
170 159 else:
171 160 self._closednodes = closednodes
172 161 self._entries = dict(entries)
173 162 # whether closed nodes are verified or not
174 163 self._closedverified = False
175 164 # branches for which nodes are verified
176 165 self._verifiedbranches = set()
177 166 self._hasnode = hasnode
178 167 if self._hasnode is None:
179 168 self._hasnode = lambda x: True
180 169
181 170 def _verifyclosed(self):
182 171 """ verify the closed nodes we have """
183 172 if self._closedverified:
184 173 return
185 174 for node in self._closednodes:
186 175 if not self._hasnode(node):
187 176 _unknownnode(node)
188 177
189 178 self._closedverified = True
190 179
191 180 def _verifybranch(self, branch):
192 181 """ verify head nodes for the given branch. """
193 182 if branch not in self._entries or branch in self._verifiedbranches:
194 183 return
195 184 for n in self._entries[branch]:
196 185 if not self._hasnode(n):
197 186 _unknownnode(n)
198 187
199 188 self._verifiedbranches.add(branch)
200 189
201 190 def _verifyall(self):
202 191 """ verifies nodes of all the branches """
203 192 needverification = set(self._entries.keys()) - self._verifiedbranches
204 193 for b in needverification:
205 194 self._verifybranch(b)
206 195
207 196 def __iter__(self):
208 197 return iter(self._entries)
209 198
210 199 def __setitem__(self, key, value):
211 200 self._entries[key] = value
212 201
213 202 def __getitem__(self, key):
214 203 self._verifybranch(key)
215 204 return self._entries[key]
216 205
217 206 def __contains__(self, key):
218 207 self._verifybranch(key)
219 208 return key in self._entries
220 209
221 210 def iteritems(self):
222 211 for k, v in self._entries.iteritems():
223 212 self._verifybranch(k)
224 213 yield k, v
225 214
226 215 def hasbranch(self, label):
227 216 """ checks whether a branch of this name exists or not """
228 217 self._verifybranch(label)
229 218 return label in self._entries
230 219
231 220 @classmethod
232 221 def fromfile(cls, repo):
233 222 f = None
234 223 try:
235 224 f = repo.cachevfs(cls._filename(repo))
236 225 lineiter = iter(f)
237 226 cachekey = next(lineiter).rstrip('\n').split(" ", 2)
238 227 last, lrev = cachekey[:2]
239 228 last, lrev = bin(last), int(lrev)
240 229 filteredhash = None
241 230 hasnode = repo.changelog.hasnode
242 231 if len(cachekey) > 2:
243 232 filteredhash = bin(cachekey[2])
244 233 bcache = cls(tipnode=last, tiprev=lrev, filteredhash=filteredhash,
245 234 hasnode=hasnode)
246 235 if not bcache.validfor(repo):
247 236 # invalidate the cache
248 237 raise ValueError(r'tip differs')
249 238 bcache.load(repo, lineiter)
250 239 except (IOError, OSError):
251 240 return None
252 241
253 242 except Exception as inst:
254 243 if repo.ui.debugflag:
255 244 msg = 'invalid branchheads cache'
256 245 if repo.filtername is not None:
257 246 msg += ' (%s)' % repo.filtername
258 247 msg += ': %s\n'
259 248 repo.ui.debug(msg % pycompat.bytestr(inst))
260 249 bcache = None
261 250
262 251 finally:
263 252 if f:
264 253 f.close()
265 254
266 255 return bcache
267 256
268 257 def load(self, repo, lineiter):
269 258 """ fully loads the branchcache by reading from the file using the line
270 259 iterator passed"""
271 260 for line in lineiter:
272 261 line = line.rstrip('\n')
273 262 if not line:
274 263 continue
275 264 node, state, label = line.split(" ", 2)
276 265 if state not in 'oc':
277 266 raise ValueError(r'invalid branch state')
278 267 label = encoding.tolocal(label.strip())
279 268 node = bin(node)
280 269 self._entries.setdefault(label, []).append(node)
281 270 if state == 'c':
282 271 self._closednodes.add(node)
283 272
284 273 @staticmethod
285 274 def _filename(repo):
286 275 """name of a branchcache file for a given repo or repoview"""
287 276 filename = "branch2"
288 277 if repo.filtername:
289 278 filename = '%s-%s' % (filename, repo.filtername)
290 279 return filename
291 280
292 281 def validfor(self, repo):
293 282 """Is the cache content valid regarding a repo
294 283
295 284 - False when cached tipnode is unknown or if we detect a strip.
296 285 - True when cache is up to date or a subset of current repo."""
297 286 try:
298 287 return ((self.tipnode == repo.changelog.node(self.tiprev))
299 288 and (self.filteredhash ==
300 289 scmutil.filteredhash(repo, self.tiprev)))
301 290 except IndexError:
302 291 return False
303 292
304 293 def _branchtip(self, heads):
305 294 '''Return tuple with last open head in heads and false,
306 295 otherwise return last closed head and true.'''
307 296 tip = heads[-1]
308 297 closed = True
309 298 for h in reversed(heads):
310 299 if h not in self._closednodes:
311 300 tip = h
312 301 closed = False
313 302 break
314 303 return tip, closed
315 304
316 305 def branchtip(self, branch):
317 306 '''Return the tipmost open head on branch head, otherwise return the
318 307 tipmost closed head on branch.
319 308 Raise KeyError for unknown branch.'''
320 309 return self._branchtip(self[branch])[0]
321 310
322 311 def iteropen(self, nodes):
323 312 return (n for n in nodes if n not in self._closednodes)
324 313
325 314 def branchheads(self, branch, closed=False):
326 315 self._verifybranch(branch)
327 316 heads = self._entries[branch]
328 317 if not closed:
329 318 heads = list(self.iteropen(heads))
330 319 return heads
331 320
332 321 def iterbranches(self):
333 322 for bn, heads in self.iteritems():
334 323 yield (bn, heads) + self._branchtip(heads)
335 324
336 325 def iterheads(self):
337 326 """ returns all the heads """
338 327 self._verifyall()
339 328 return self._entries.itervalues()
340 329
341 330 def copy(self):
342 331 """return an deep copy of the branchcache object"""
343 332 return type(self)(
344 333 self._entries, self.tipnode, self.tiprev, self.filteredhash,
345 334 self._closednodes)
346 335
347 336 def write(self, repo):
348 337 try:
349 338 f = repo.cachevfs(self._filename(repo), "w", atomictemp=True)
350 339 cachekey = [hex(self.tipnode), '%d' % self.tiprev]
351 340 if self.filteredhash is not None:
352 341 cachekey.append(hex(self.filteredhash))
353 342 f.write(" ".join(cachekey) + '\n')
354 343 nodecount = 0
355 344 for label, nodes in sorted(self.iteritems()):
356 345 label = encoding.fromlocal(label)
357 346 for node in nodes:
358 347 nodecount += 1
359 348 if node in self._closednodes:
360 349 state = 'c'
361 350 else:
362 351 state = 'o'
363 352 f.write("%s %s %s\n" % (hex(node), state, label))
364 353 f.close()
365 354 repo.ui.log('branchcache',
366 355 'wrote %s branch cache with %d labels and %d nodes\n',
367 356 repo.filtername, len(self._entries), nodecount)
368 357 except (IOError, OSError, error.Abort) as inst:
369 358 # Abort may be raised by read only opener, so log and continue
370 359 repo.ui.debug("couldn't write branch cache: %s\n" %
371 360 stringutil.forcebytestr(inst))
372 361
373 362 def update(self, repo, revgen):
374 363 """Given a branchhead cache, self, that may have extra nodes or be
375 364 missing heads, and a generator of nodes that are strictly a superset of
376 365 heads missing, this function updates self to be correct.
377 366 """
378 367 starttime = util.timer()
379 368 cl = repo.changelog
380 369 # collect new branch entries
381 370 newbranches = {}
382 371 getbranchinfo = repo.revbranchcache().branchinfo
383 372 for r in revgen:
384 373 branch, closesbranch = getbranchinfo(r)
385 374 newbranches.setdefault(branch, []).append(r)
386 375 if closesbranch:
387 376 self._closednodes.add(cl.node(r))
388 377
389 378 # fetch current topological heads to speed up filtering
390 379 topoheads = set(cl.headrevs())
391 380
392 381 # if older branchheads are reachable from new ones, they aren't
393 382 # really branchheads. Note checking parents is insufficient:
394 383 # 1 (branch a) -> 2 (branch b) -> 3 (branch a)
395 384 for branch, newheadrevs in newbranches.iteritems():
396 385 bheads = self._entries.setdefault(branch, [])
397 386 bheadset = set(cl.rev(node) for node in bheads)
398 387
399 388 # This have been tested True on all internal usage of this function.
400 389 # run it again in case of doubt
401 390 # assert not (set(bheadrevs) & set(newheadrevs))
402 391 bheadset.update(newheadrevs)
403 392
404 393 # This prunes out two kinds of heads - heads that are superseded by
405 394 # a head in newheadrevs, and newheadrevs that are not heads because
406 395 # an existing head is their descendant.
407 396 uncertain = bheadset - topoheads
408 397 if uncertain:
409 398 floorrev = min(uncertain)
410 399 ancestors = set(cl.ancestors(newheadrevs, floorrev))
411 400 bheadset -= ancestors
412 401 bheadrevs = sorted(bheadset)
413 402 self[branch] = [cl.node(rev) for rev in bheadrevs]
414 403 tiprev = bheadrevs[-1]
415 404 if tiprev > self.tiprev:
416 405 self.tipnode = cl.node(tiprev)
417 406 self.tiprev = tiprev
418 407
419 408 if not self.validfor(repo):
420 409 # cache key are not valid anymore
421 410 self.tipnode = nullid
422 411 self.tiprev = nullrev
423 412 for heads in self.iterheads():
424 413 tiprev = max(cl.rev(node) for node in heads)
425 414 if tiprev > self.tiprev:
426 415 self.tipnode = cl.node(tiprev)
427 416 self.tiprev = tiprev
428 417 self.filteredhash = scmutil.filteredhash(repo, self.tiprev)
429 418
430 419 duration = util.timer() - starttime
431 420 repo.ui.log('branchcache', 'updated %s branch cache in %.4f seconds\n',
432 421 repo.filtername or b'None', duration)
433 422
434 423 self.write(repo)
435 424
436 425
437 426 class remotebranchcache(branchcache):
438 427 """Branchmap info for a remote connection, should not write locally"""
439 428 def write(self, repo):
440 429 pass
441 430
442 431
443 432 # Revision branch info cache
444 433
445 434 _rbcversion = '-v1'
446 435 _rbcnames = 'rbc-names' + _rbcversion
447 436 _rbcrevs = 'rbc-revs' + _rbcversion
448 437 # [4 byte hash prefix][4 byte branch name number with sign bit indicating open]
449 438 _rbcrecfmt = '>4sI'
450 439 _rbcrecsize = calcsize(_rbcrecfmt)
451 440 _rbcnodelen = 4
452 441 _rbcbranchidxmask = 0x7fffffff
453 442 _rbccloseflag = 0x80000000
454 443
455 444 class revbranchcache(object):
456 445 """Persistent cache, mapping from revision number to branch name and close.
457 446 This is a low level cache, independent of filtering.
458 447
459 448 Branch names are stored in rbc-names in internal encoding separated by 0.
460 449 rbc-names is append-only, and each branch name is only stored once and will
461 450 thus have a unique index.
462 451
463 452 The branch info for each revision is stored in rbc-revs as constant size
464 453 records. The whole file is read into memory, but it is only 'parsed' on
465 454 demand. The file is usually append-only but will be truncated if repo
466 455 modification is detected.
467 456 The record for each revision contains the first 4 bytes of the
468 457 corresponding node hash, and the record is only used if it still matches.
469 458 Even a completely trashed rbc-revs fill thus still give the right result
470 459 while converging towards full recovery ... assuming no incorrectly matching
471 460 node hashes.
472 461 The record also contains 4 bytes where 31 bits contains the index of the
473 462 branch and the last bit indicate that it is a branch close commit.
474 463 The usage pattern for rbc-revs is thus somewhat similar to 00changelog.i
475 464 and will grow with it but be 1/8th of its size.
476 465 """
477 466
478 467 def __init__(self, repo, readonly=True):
479 468 assert repo.filtername is None
480 469 self._repo = repo
481 470 self._names = [] # branch names in local encoding with static index
482 471 self._rbcrevs = bytearray()
483 472 self._rbcsnameslen = 0 # length of names read at _rbcsnameslen
484 473 try:
485 474 bndata = repo.cachevfs.read(_rbcnames)
486 475 self._rbcsnameslen = len(bndata) # for verification before writing
487 476 if bndata:
488 477 self._names = [encoding.tolocal(bn)
489 478 for bn in bndata.split('\0')]
490 479 except (IOError, OSError):
491 480 if readonly:
492 481 # don't try to use cache - fall back to the slow path
493 482 self.branchinfo = self._branchinfo
494 483
495 484 if self._names:
496 485 try:
497 486 data = repo.cachevfs.read(_rbcrevs)
498 487 self._rbcrevs[:] = data
499 488 except (IOError, OSError) as inst:
500 489 repo.ui.debug("couldn't read revision branch cache: %s\n" %
501 490 stringutil.forcebytestr(inst))
502 491 # remember number of good records on disk
503 492 self._rbcrevslen = min(len(self._rbcrevs) // _rbcrecsize,
504 493 len(repo.changelog))
505 494 if self._rbcrevslen == 0:
506 495 self._names = []
507 496 self._rbcnamescount = len(self._names) # number of names read at
508 497 # _rbcsnameslen
509 498
510 499 def _clear(self):
511 500 self._rbcsnameslen = 0
512 501 del self._names[:]
513 502 self._rbcnamescount = 0
514 503 self._rbcrevslen = len(self._repo.changelog)
515 504 self._rbcrevs = bytearray(self._rbcrevslen * _rbcrecsize)
516 505 util.clearcachedproperty(self, '_namesreverse')
517 506
518 507 @util.propertycache
519 508 def _namesreverse(self):
520 509 return dict((b, r) for r, b in enumerate(self._names))
521 510
522 511 def branchinfo(self, rev):
523 512 """Return branch name and close flag for rev, using and updating
524 513 persistent cache."""
525 514 changelog = self._repo.changelog
526 515 rbcrevidx = rev * _rbcrecsize
527 516
528 517 # avoid negative index, changelog.read(nullrev) is fast without cache
529 518 if rev == nullrev:
530 519 return changelog.branchinfo(rev)
531 520
532 521 # if requested rev isn't allocated, grow and cache the rev info
533 522 if len(self._rbcrevs) < rbcrevidx + _rbcrecsize:
534 523 return self._branchinfo(rev)
535 524
536 525 # fast path: extract data from cache, use it if node is matching
537 526 reponode = changelog.node(rev)[:_rbcnodelen]
538 527 cachenode, branchidx = unpack_from(
539 528 _rbcrecfmt, util.buffer(self._rbcrevs), rbcrevidx)
540 529 close = bool(branchidx & _rbccloseflag)
541 530 if close:
542 531 branchidx &= _rbcbranchidxmask
543 532 if cachenode == '\0\0\0\0':
544 533 pass
545 534 elif cachenode == reponode:
546 535 try:
547 536 return self._names[branchidx], close
548 537 except IndexError:
549 538 # recover from invalid reference to unknown branch
550 539 self._repo.ui.debug("referenced branch names not found"
551 540 " - rebuilding revision branch cache from scratch\n")
552 541 self._clear()
553 542 else:
554 543 # rev/node map has changed, invalidate the cache from here up
555 544 self._repo.ui.debug("history modification detected - truncating "
556 545 "revision branch cache to revision %d\n" % rev)
557 546 truncate = rbcrevidx + _rbcrecsize
558 547 del self._rbcrevs[truncate:]
559 548 self._rbcrevslen = min(self._rbcrevslen, truncate)
560 549
561 550 # fall back to slow path and make sure it will be written to disk
562 551 return self._branchinfo(rev)
563 552
564 553 def _branchinfo(self, rev):
565 554 """Retrieve branch info from changelog and update _rbcrevs"""
566 555 changelog = self._repo.changelog
567 556 b, close = changelog.branchinfo(rev)
568 557 if b in self._namesreverse:
569 558 branchidx = self._namesreverse[b]
570 559 else:
571 560 branchidx = len(self._names)
572 561 self._names.append(b)
573 562 self._namesreverse[b] = branchidx
574 563 reponode = changelog.node(rev)
575 564 if close:
576 565 branchidx |= _rbccloseflag
577 566 self._setcachedata(rev, reponode, branchidx)
578 567 return b, close
579 568
580 569 def setdata(self, branch, rev, node, close):
581 570 """add new data information to the cache"""
582 571 if branch in self._namesreverse:
583 572 branchidx = self._namesreverse[branch]
584 573 else:
585 574 branchidx = len(self._names)
586 575 self._names.append(branch)
587 576 self._namesreverse[branch] = branchidx
588 577 if close:
589 578 branchidx |= _rbccloseflag
590 579 self._setcachedata(rev, node, branchidx)
591 580 # If no cache data were readable (non exists, bad permission, etc)
592 581 # the cache was bypassing itself by setting:
593 582 #
594 583 # self.branchinfo = self._branchinfo
595 584 #
596 585 # Since we now have data in the cache, we need to drop this bypassing.
597 586 if r'branchinfo' in vars(self):
598 587 del self.branchinfo
599 588
600 589 def _setcachedata(self, rev, node, branchidx):
601 590 """Writes the node's branch data to the in-memory cache data."""
602 591 if rev == nullrev:
603 592 return
604 593 rbcrevidx = rev * _rbcrecsize
605 594 if len(self._rbcrevs) < rbcrevidx + _rbcrecsize:
606 595 self._rbcrevs.extend('\0' *
607 596 (len(self._repo.changelog) * _rbcrecsize -
608 597 len(self._rbcrevs)))
609 598 pack_into(_rbcrecfmt, self._rbcrevs, rbcrevidx, node, branchidx)
610 599 self._rbcrevslen = min(self._rbcrevslen, rev)
611 600
612 601 tr = self._repo.currenttransaction()
613 602 if tr:
614 603 tr.addfinalize('write-revbranchcache', self.write)
615 604
616 605 def write(self, tr=None):
617 606 """Save branch cache if it is dirty."""
618 607 repo = self._repo
619 608 wlock = None
620 609 step = ''
621 610 try:
622 611 if self._rbcnamescount < len(self._names):
623 612 step = ' names'
624 613 wlock = repo.wlock(wait=False)
625 614 if self._rbcnamescount != 0:
626 615 f = repo.cachevfs.open(_rbcnames, 'ab')
627 616 if f.tell() == self._rbcsnameslen:
628 617 f.write('\0')
629 618 else:
630 619 f.close()
631 620 repo.ui.debug("%s changed - rewriting it\n" % _rbcnames)
632 621 self._rbcnamescount = 0
633 622 self._rbcrevslen = 0
634 623 if self._rbcnamescount == 0:
635 624 # before rewriting names, make sure references are removed
636 625 repo.cachevfs.unlinkpath(_rbcrevs, ignoremissing=True)
637 626 f = repo.cachevfs.open(_rbcnames, 'wb')
638 627 f.write('\0'.join(encoding.fromlocal(b)
639 628 for b in self._names[self._rbcnamescount:]))
640 629 self._rbcsnameslen = f.tell()
641 630 f.close()
642 631 self._rbcnamescount = len(self._names)
643 632
644 633 start = self._rbcrevslen * _rbcrecsize
645 634 if start != len(self._rbcrevs):
646 635 step = ''
647 636 if wlock is None:
648 637 wlock = repo.wlock(wait=False)
649 638 revs = min(len(repo.changelog),
650 639 len(self._rbcrevs) // _rbcrecsize)
651 640 f = repo.cachevfs.open(_rbcrevs, 'ab')
652 641 if f.tell() != start:
653 642 repo.ui.debug("truncating cache/%s to %d\n"
654 643 % (_rbcrevs, start))
655 644 f.seek(start)
656 645 if f.tell() != start:
657 646 start = 0
658 647 f.seek(start)
659 648 f.truncate()
660 649 end = revs * _rbcrecsize
661 650 f.write(self._rbcrevs[start:end])
662 651 f.close()
663 652 self._rbcrevslen = revs
664 653 except (IOError, OSError, error.Abort, error.LockError) as inst:
665 654 repo.ui.debug("couldn't write revision branch cache%s: %s\n"
666 655 % (step, stringutil.forcebytestr(inst)))
667 656 finally:
668 657 if wlock is not None:
669 658 wlock.release()
@@ -1,280 +1,280
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 from __future__ import absolute_import
10 10
11 11 import copy
12 12 import weakref
13 13
14 14 from .node import nullrev
15 15 from . import (
16 16 obsolete,
17 17 phases,
18 18 pycompat,
19 19 tags as tagsmod,
20 20 )
21 21
22 22 def hideablerevs(repo):
23 23 """Revision candidates to be hidden
24 24
25 25 This is a standalone function to allow extensions to wrap it.
26 26
27 27 Because we use the set of immutable changesets as a fallback subset in
28 branchmap (see mercurial.branchmap.subsettable), you cannot set "public"
29 changesets as "hideable". Doing so would break multiple code assertions and
30 lead to crashes."""
28 branchmap (see mercurial.utils.repoviewutils.subsettable), you cannot set
29 "public" changesets as "hideable". Doing so would break multiple code
30 assertions and lead to crashes."""
31 31 obsoletes = obsolete.getrevs(repo, 'obsolete')
32 32 internals = repo._phasecache.getrevset(repo, phases.localhiddenphases)
33 33 internals = frozenset(internals)
34 34 return obsoletes | internals
35 35
36 36 def pinnedrevs(repo):
37 37 """revisions blocking hidden changesets from being filtered
38 38 """
39 39
40 40 cl = repo.changelog
41 41 pinned = set()
42 42 pinned.update([par.rev() for par in repo[None].parents()])
43 43 pinned.update([cl.rev(bm) for bm in repo._bookmarks.values()])
44 44
45 45 tags = {}
46 46 tagsmod.readlocaltags(repo.ui, repo, tags, {})
47 47 if tags:
48 48 rev, nodemap = cl.rev, cl.nodemap
49 49 pinned.update(rev(t[0]) for t in tags.values() if t[0] in nodemap)
50 50 return pinned
51 51
52 52
53 53 def _revealancestors(pfunc, hidden, revs):
54 54 """reveals contiguous chains of hidden ancestors of 'revs' by removing them
55 55 from 'hidden'
56 56
57 57 - pfunc(r): a funtion returning parent of 'r',
58 58 - hidden: the (preliminary) hidden revisions, to be updated
59 59 - revs: iterable of revnum,
60 60
61 61 (Ancestors are revealed exclusively, i.e. the elements in 'revs' are
62 62 *not* revealed)
63 63 """
64 64 stack = list(revs)
65 65 while stack:
66 66 for p in pfunc(stack.pop()):
67 67 if p != nullrev and p in hidden:
68 68 hidden.remove(p)
69 69 stack.append(p)
70 70
71 71 def computehidden(repo, visibilityexceptions=None):
72 72 """compute the set of hidden revision to filter
73 73
74 74 During most operation hidden should be filtered."""
75 75 assert not repo.changelog.filteredrevs
76 76
77 77 hidden = hideablerevs(repo)
78 78 if hidden:
79 79 hidden = set(hidden - pinnedrevs(repo))
80 80 if visibilityexceptions:
81 81 hidden -= visibilityexceptions
82 82 pfunc = repo.changelog.parentrevs
83 83 mutable = repo._phasecache.getrevset(repo, phases.mutablephases)
84 84
85 85 visible = mutable - hidden
86 86 _revealancestors(pfunc, hidden, visible)
87 87 return frozenset(hidden)
88 88
89 89 def computesecret(repo, visibilityexceptions=None):
90 90 """compute the set of revision that can never be exposed through hgweb
91 91
92 92 Changeset in the secret phase (or above) should stay unaccessible."""
93 93 assert not repo.changelog.filteredrevs
94 94 secrets = repo._phasecache.getrevset(repo, phases.remotehiddenphases)
95 95 return frozenset(secrets)
96 96
97 97 def computeunserved(repo, visibilityexceptions=None):
98 98 """compute the set of revision that should be filtered when used a server
99 99
100 100 Secret and hidden changeset should not pretend to be here."""
101 101 assert not repo.changelog.filteredrevs
102 102 # fast path in simple case to avoid impact of non optimised code
103 103 hiddens = filterrevs(repo, 'visible')
104 104 secrets = filterrevs(repo, 'served.hidden')
105 105 if secrets:
106 106 return frozenset(hiddens | secrets)
107 107 else:
108 108 return hiddens
109 109
110 110 def computemutable(repo, visibilityexceptions=None):
111 111 assert not repo.changelog.filteredrevs
112 112 # fast check to avoid revset call on huge repo
113 113 if any(repo._phasecache.phaseroots[1:]):
114 114 getphase = repo._phasecache.phase
115 115 maymutable = filterrevs(repo, 'base')
116 116 return frozenset(r for r in maymutable if getphase(repo, r))
117 117 return frozenset()
118 118
119 119 def computeimpactable(repo, visibilityexceptions=None):
120 120 """Everything impactable by mutable revision
121 121
122 122 The immutable filter still have some chance to get invalidated. This will
123 123 happen when:
124 124
125 125 - you garbage collect hidden changeset,
126 126 - public phase is moved backward,
127 127 - something is changed in the filtering (this could be fixed)
128 128
129 129 This filter out any mutable changeset and any public changeset that may be
130 130 impacted by something happening to a mutable revision.
131 131
132 132 This is achieved by filtered everything with a revision number egal or
133 133 higher than the first mutable changeset is filtered."""
134 134 assert not repo.changelog.filteredrevs
135 135 cl = repo.changelog
136 136 firstmutable = len(cl)
137 137 for roots in repo._phasecache.phaseroots[1:]:
138 138 if roots:
139 139 firstmutable = min(firstmutable, min(cl.rev(r) for r in roots))
140 140 # protect from nullrev root
141 141 firstmutable = max(0, firstmutable)
142 142 return frozenset(pycompat.xrange(firstmutable, len(cl)))
143 143
144 144 # function to compute filtered set
145 145 #
146 146 # When adding a new filter you MUST update the table at:
147 # mercurial.branchmap.subsettable
147 # mercurial.utils.repoviewutil.subsettable
148 148 # Otherwise your filter will have to recompute all its branches cache
149 149 # from scratch (very slow).
150 150 filtertable = {'visible': computehidden,
151 151 'visible-hidden': computehidden,
152 152 'served.hidden': computesecret,
153 153 'served': computeunserved,
154 154 'immutable': computemutable,
155 155 'base': computeimpactable}
156 156
157 157 def filterrevs(repo, filtername, visibilityexceptions=None):
158 158 """returns set of filtered revision for this filter name
159 159
160 160 visibilityexceptions is a set of revs which must are exceptions for
161 161 hidden-state and must be visible. They are dynamic and hence we should not
162 162 cache it's result"""
163 163 if filtername not in repo.filteredrevcache:
164 164 func = filtertable[filtername]
165 165 if visibilityexceptions:
166 166 return func(repo.unfiltered, visibilityexceptions)
167 167 repo.filteredrevcache[filtername] = func(repo.unfiltered())
168 168 return repo.filteredrevcache[filtername]
169 169
170 170 class repoview(object):
171 171 """Provide a read/write view of a repo through a filtered changelog
172 172
173 173 This object is used to access a filtered version of a repository without
174 174 altering the original repository object itself. We can not alter the
175 175 original object for two main reasons:
176 176 - It prevents the use of a repo with multiple filters at the same time. In
177 177 particular when multiple threads are involved.
178 178 - It makes scope of the filtering harder to control.
179 179
180 180 This object behaves very closely to the original repository. All attribute
181 181 operations are done on the original repository:
182 182 - An access to `repoview.someattr` actually returns `repo.someattr`,
183 183 - A write to `repoview.someattr` actually sets value of `repo.someattr`,
184 184 - A deletion of `repoview.someattr` actually drops `someattr`
185 185 from `repo.__dict__`.
186 186
187 187 The only exception is the `changelog` property. It is overridden to return
188 188 a (surface) copy of `repo.changelog` with some revisions filtered. The
189 189 `filtername` attribute of the view control the revisions that need to be
190 190 filtered. (the fact the changelog is copied is an implementation detail).
191 191
192 192 Unlike attributes, this object intercepts all method calls. This means that
193 193 all methods are run on the `repoview` object with the filtered `changelog`
194 194 property. For this purpose the simple `repoview` class must be mixed with
195 195 the actual class of the repository. This ensures that the resulting
196 196 `repoview` object have the very same methods than the repo object. This
197 197 leads to the property below.
198 198
199 199 repoview.method() --> repo.__class__.method(repoview)
200 200
201 201 The inheritance has to be done dynamically because `repo` can be of any
202 202 subclasses of `localrepo`. Eg: `bundlerepo` or `statichttprepo`.
203 203 """
204 204
205 205 def __init__(self, repo, filtername, visibilityexceptions=None):
206 206 object.__setattr__(self, r'_unfilteredrepo', repo)
207 207 object.__setattr__(self, r'filtername', filtername)
208 208 object.__setattr__(self, r'_clcachekey', None)
209 209 object.__setattr__(self, r'_clcache', None)
210 210 # revs which are exceptions and must not be hidden
211 211 object.__setattr__(self, r'_visibilityexceptions',
212 212 visibilityexceptions)
213 213
214 214 # not a propertycache on purpose we shall implement a proper cache later
215 215 @property
216 216 def changelog(self):
217 217 """return a filtered version of the changeset
218 218
219 219 this changelog must not be used for writing"""
220 220 # some cache may be implemented later
221 221 unfi = self._unfilteredrepo
222 222 unfichangelog = unfi.changelog
223 223 # bypass call to changelog.method
224 224 unfiindex = unfichangelog.index
225 225 unfilen = len(unfiindex)
226 226 unfinode = unfiindex[unfilen - 1][7]
227 227
228 228 revs = filterrevs(unfi, self.filtername, self._visibilityexceptions)
229 229 cl = self._clcache
230 230 newkey = (unfilen, unfinode, hash(revs), unfichangelog._delayed)
231 231 # if cl.index is not unfiindex, unfi.changelog would be
232 232 # recreated, and our clcache refers to garbage object
233 233 if (cl is not None and
234 234 (cl.index is not unfiindex or newkey != self._clcachekey)):
235 235 cl = None
236 236 # could have been made None by the previous if
237 237 if cl is None:
238 238 cl = copy.copy(unfichangelog)
239 239 cl.filteredrevs = revs
240 240 object.__setattr__(self, r'_clcache', cl)
241 241 object.__setattr__(self, r'_clcachekey', newkey)
242 242 return cl
243 243
244 244 def unfiltered(self):
245 245 """Return an unfiltered version of a repo"""
246 246 return self._unfilteredrepo
247 247
248 248 def filtered(self, name, visibilityexceptions=None):
249 249 """Return a filtered version of a repository"""
250 250 if name == self.filtername and not visibilityexceptions:
251 251 return self
252 252 return self.unfiltered().filtered(name, visibilityexceptions)
253 253
254 254 def __repr__(self):
255 255 return r'<%s:%s %r>' % (self.__class__.__name__,
256 256 pycompat.sysstr(self.filtername),
257 257 self.unfiltered())
258 258
259 259 # everything access are forwarded to the proxied repo
260 260 def __getattr__(self, attr):
261 261 return getattr(self._unfilteredrepo, attr)
262 262
263 263 def __setattr__(self, attr, value):
264 264 return setattr(self._unfilteredrepo, attr, value)
265 265
266 266 def __delattr__(self, attr):
267 267 return delattr(self._unfilteredrepo, attr)
268 268
269 269 # Python <3.4 easily leaks types via __mro__. See
270 270 # https://bugs.python.org/issue17950. We cache dynamically created types
271 271 # so they won't be leaked on every invocation of repo.filtered().
272 272 _filteredrepotypes = weakref.WeakKeyDictionary()
273 273
274 274 def newtype(base):
275 275 """Create a new type with the repoview mixin and the given base class"""
276 276 if base not in _filteredrepotypes:
277 277 class filteredrepo(repoview, base):
278 278 pass
279 279 _filteredrepotypes[base] = filteredrepo
280 280 return _filteredrepotypes[base]
@@ -1,79 +1,79
1 1 #!/usr/bin/env python
2 2 #
3 3 # check-perf-code - (historical) portability checker for contrib/perf.py
4 4
5 5 from __future__ import absolute_import
6 6
7 7 import os
8 8 import sys
9 9
10 10 # write static check patterns here
11 11 perfpypats = [
12 12 [
13 (r'(branchmap|repoview)\.subsettable',
13 (r'(branchmap|repoview|repoviewutil)\.subsettable',
14 14 "use getbranchmapsubsettable() for early Mercurial"),
15 15 (r'\.(vfs|svfs|opener|sopener)',
16 16 "use getvfs()/getsvfs() for early Mercurial"),
17 17 (r'ui\.configint',
18 18 "use getint() instead of ui.configint() for early Mercurial"),
19 19 ],
20 20 # warnings
21 21 [
22 22 ]
23 23 ]
24 24
25 25 def modulewhitelist(names):
26 26 replacement = [('.py', ''), ('.c', ''), # trim suffix
27 27 ('mercurial%s' % ('/'), ''), # trim "mercurial/" path
28 28 ]
29 29 ignored = {'__init__'}
30 30 modules = {}
31 31
32 32 # convert from file name to module name, and count # of appearances
33 33 for name in names:
34 34 name = name.strip()
35 35 for old, new in replacement:
36 36 name = name.replace(old, new)
37 37 if name not in ignored:
38 38 modules[name] = modules.get(name, 0) + 1
39 39
40 40 # list up module names, which appear multiple times
41 41 whitelist = []
42 42 for name, count in modules.items():
43 43 if count > 1:
44 44 whitelist.append(name)
45 45
46 46 return whitelist
47 47
48 48 if __name__ == "__main__":
49 49 # in this case, it is assumed that result of "hg files" at
50 50 # multiple revisions is given via stdin
51 51 whitelist = modulewhitelist(sys.stdin)
52 52 assert whitelist, "module whitelist is empty"
53 53
54 54 # build up module whitelist check from file names given at runtime
55 55 perfpypats[0].append(
56 56 # this matching pattern assumes importing modules from
57 57 # "mercurial" package in the current style below, for simplicity
58 58 #
59 59 # from mercurial import (
60 60 # foo,
61 61 # bar,
62 62 # baz
63 63 # )
64 64 ((r'from mercurial import [(][a-z0-9, \n#]*\n(?! *%s,|^[ #]*\n|[)])'
65 65 % ',| *'.join(whitelist)),
66 66 "import newer module separately in try clause for early Mercurial"
67 67 ))
68 68
69 69 # import contrib/check-code.py as checkcode
70 70 assert 'RUNTESTDIR' in os.environ, "use check-perf-code.py in *.t script"
71 71 contribpath = os.path.join(os.environ['RUNTESTDIR'], '..', 'contrib')
72 72 sys.path.insert(0, contribpath)
73 73 checkcode = __import__('check-code')
74 74
75 75 # register perf.py specific entry with "checks" in check-code.py
76 76 checkcode.checks.append(('perf.py', r'contrib/perf.py$', '',
77 77 checkcode.pyfilters, perfpypats))
78 78
79 79 sys.exit(checkcode.main())
General Comments 0
You need to be logged in to leave comments. Login now