##// END OF EJS Templates
perf: clear vfs audit_cache before each run...
marmoute -
r52485:24844407 default
parent child Browse files
Show More
@@ -1,4706 +1,4725 b''
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 ``pre-run``
19 19 number of run to perform before starting measurement.
20 20
21 21 ``profile-benchmark``
22 22 Enable profiling for the benchmarked section.
23 23 (by default, the first iteration is benchmarked)
24 24
25 25 ``profiled-runs``
26 26 list of iteration to profile (starting from 0)
27 27
28 28 ``run-limits``
29 29 Control the number of runs each benchmark will perform. The option value
30 30 should be a list of `<time>-<numberofrun>` pairs. After each run the
31 31 conditions are considered in order with the following logic:
32 32
33 33 If benchmark has been running for <time> seconds, and we have performed
34 34 <numberofrun> iterations, stop the benchmark,
35 35
36 36 The default value is: `3.0-100, 10.0-3`
37 37
38 38 ``stub``
39 39 When set, benchmarks will only be run once, useful for testing
40 40 (default: off)
41 41 '''
42 42
43 43 # "historical portability" policy of perf.py:
44 44 #
45 45 # We have to do:
46 46 # - make perf.py "loadable" with as wide Mercurial version as possible
47 47 # This doesn't mean that perf commands work correctly with that Mercurial.
48 48 # BTW, perf.py itself has been available since 1.1 (or eb240755386d).
49 49 # - make historical perf command work correctly with as wide Mercurial
50 50 # version as possible
51 51 #
52 52 # We have to do, if possible with reasonable cost:
53 53 # - make recent perf command for historical feature work correctly
54 54 # with early Mercurial
55 55 #
56 56 # We don't have to do:
57 57 # - make perf command for recent feature work correctly with early
58 58 # Mercurial
59 59
60 60 import contextlib
61 61 import functools
62 62 import gc
63 63 import os
64 64 import random
65 65 import shutil
66 66 import struct
67 67 import sys
68 68 import tempfile
69 69 import threading
70 70 import time
71 71
72 72 import mercurial.revlog
73 73 from mercurial import (
74 74 changegroup,
75 75 cmdutil,
76 76 commands,
77 77 copies,
78 78 error,
79 79 extensions,
80 80 hg,
81 81 mdiff,
82 82 merge,
83 83 util,
84 84 )
85 85
86 86 # for "historical portability":
87 87 # try to import modules separately (in dict order), and ignore
88 88 # failure, because these aren't available with early Mercurial
89 89 try:
90 90 from mercurial import branchmap # since 2.5 (or bcee63733aad)
91 91 except ImportError:
92 92 pass
93 93 try:
94 94 from mercurial import obsolete # since 2.3 (or ad0d6c2b3279)
95 95 except ImportError:
96 96 pass
97 97 try:
98 98 from mercurial import registrar # since 3.7 (or 37d50250b696)
99 99
100 100 dir(registrar) # forcibly load it
101 101 except ImportError:
102 102 registrar = None
103 103 try:
104 104 from mercurial import repoview # since 2.5 (or 3a6ddacb7198)
105 105 except ImportError:
106 106 pass
107 107 try:
108 108 from mercurial.utils import repoviewutil # since 5.0
109 109 except ImportError:
110 110 repoviewutil = None
111 111 try:
112 112 from mercurial import scmutil # since 1.9 (or 8b252e826c68)
113 113 except ImportError:
114 114 pass
115 115 try:
116 116 from mercurial import setdiscovery # since 1.9 (or cb98fed52495)
117 117 except ImportError:
118 118 pass
119 119
120 120 try:
121 121 from mercurial import profiling
122 122 except ImportError:
123 123 profiling = None
124 124
125 125 try:
126 126 from mercurial.revlogutils import constants as revlog_constants
127 127
128 128 perf_rl_kind = (revlog_constants.KIND_OTHER, b'created-by-perf')
129 129
130 130 def revlog(opener, *args, **kwargs):
131 131 return mercurial.revlog.revlog(opener, perf_rl_kind, *args, **kwargs)
132 132
133 133
134 134 except (ImportError, AttributeError):
135 135 perf_rl_kind = None
136 136
137 137 def revlog(opener, *args, **kwargs):
138 138 return mercurial.revlog.revlog(opener, *args, **kwargs)
139 139
140 140
141 141 def identity(a):
142 142 return a
143 143
144 144
145 145 try:
146 146 from mercurial import pycompat
147 147
148 148 getargspec = pycompat.getargspec # added to module after 4.5
149 149 _byteskwargs = pycompat.byteskwargs # since 4.1 (or fbc3f73dc802)
150 150 _sysstr = pycompat.sysstr # since 4.0 (or 2219f4f82ede)
151 151 _bytestr = pycompat.bytestr # since 4.2 (or b70407bd84d5)
152 152 _xrange = pycompat.xrange # since 4.8 (or 7eba8f83129b)
153 153 fsencode = pycompat.fsencode # since 3.9 (or f4a5e0e86a7e)
154 154 if pycompat.ispy3:
155 155 _maxint = sys.maxsize # per py3 docs for replacing maxint
156 156 else:
157 157 _maxint = sys.maxint
158 158 except (NameError, ImportError, AttributeError):
159 159 import inspect
160 160
161 161 getargspec = inspect.getargspec
162 162 _byteskwargs = identity
163 163 _bytestr = str
164 164 fsencode = identity # no py3 support
165 165 _maxint = sys.maxint # no py3 support
166 166 _sysstr = lambda x: x # no py3 support
167 167 _xrange = xrange
168 168
169 169 try:
170 170 # 4.7+
171 171 queue = pycompat.queue.Queue
172 172 except (NameError, AttributeError, ImportError):
173 173 # <4.7.
174 174 try:
175 175 queue = pycompat.queue
176 176 except (NameError, AttributeError, ImportError):
177 177 import Queue as queue
178 178
179 179 try:
180 180 from mercurial import logcmdutil
181 181
182 182 makelogtemplater = logcmdutil.maketemplater
183 183 except (AttributeError, ImportError):
184 184 try:
185 185 makelogtemplater = cmdutil.makelogtemplater
186 186 except (AttributeError, ImportError):
187 187 makelogtemplater = None
188 188
189 189 # for "historical portability":
190 190 # define util.safehasattr forcibly, because util.safehasattr has been
191 191 # available since 1.9.3 (or 94b200a11cf7)
192 192 _undefined = object()
193 193
194 194
195 195 def safehasattr(thing, attr):
196 196 return getattr(thing, _sysstr(attr), _undefined) is not _undefined
197 197
198 198
199 199 setattr(util, 'safehasattr', safehasattr)
200 200
201 201 # for "historical portability":
202 202 # define util.timer forcibly, because util.timer has been available
203 203 # since ae5d60bb70c9
204 204 if safehasattr(time, 'perf_counter'):
205 205 util.timer = time.perf_counter
206 206 elif os.name == b'nt':
207 207 util.timer = time.clock
208 208 else:
209 209 util.timer = time.time
210 210
211 211 # for "historical portability":
212 212 # use locally defined empty option list, if formatteropts isn't
213 213 # available, because commands.formatteropts has been available since
214 214 # 3.2 (or 7a7eed5176a4), even though formatting itself has been
215 215 # available since 2.2 (or ae5f92e154d3)
216 216 formatteropts = getattr(
217 217 cmdutil, "formatteropts", getattr(commands, "formatteropts", [])
218 218 )
219 219
220 220 # for "historical portability":
221 221 # use locally defined option list, if debugrevlogopts isn't available,
222 222 # because commands.debugrevlogopts has been available since 3.7 (or
223 223 # 5606f7d0d063), even though cmdutil.openrevlog() has been available
224 224 # since 1.9 (or a79fea6b3e77).
225 225 revlogopts = getattr(
226 226 cmdutil,
227 227 "debugrevlogopts",
228 228 getattr(
229 229 commands,
230 230 "debugrevlogopts",
231 231 [
232 232 (b'c', b'changelog', False, b'open changelog'),
233 233 (b'm', b'manifest', False, b'open manifest'),
234 234 (b'', b'dir', False, b'open directory manifest'),
235 235 ],
236 236 ),
237 237 )
238 238
239 239 cmdtable = {}
240 240
241 241
242 242 # for "historical portability":
243 243 # define parsealiases locally, because cmdutil.parsealiases has been
244 244 # available since 1.5 (or 6252852b4332)
245 245 def parsealiases(cmd):
246 246 return cmd.split(b"|")
247 247
248 248
249 249 if safehasattr(registrar, 'command'):
250 250 command = registrar.command(cmdtable)
251 251 elif safehasattr(cmdutil, 'command'):
252 252 command = cmdutil.command(cmdtable)
253 253 if 'norepo' not in getargspec(command).args:
254 254 # for "historical portability":
255 255 # wrap original cmdutil.command, because "norepo" option has
256 256 # been available since 3.1 (or 75a96326cecb)
257 257 _command = command
258 258
259 259 def command(name, options=(), synopsis=None, norepo=False):
260 260 if norepo:
261 261 commands.norepo += b' %s' % b' '.join(parsealiases(name))
262 262 return _command(name, list(options), synopsis)
263 263
264 264
265 265 else:
266 266 # for "historical portability":
267 267 # define "@command" annotation locally, because cmdutil.command
268 268 # has been available since 1.9 (or 2daa5179e73f)
269 269 def command(name, options=(), synopsis=None, norepo=False):
270 270 def decorator(func):
271 271 if synopsis:
272 272 cmdtable[name] = func, list(options), synopsis
273 273 else:
274 274 cmdtable[name] = func, list(options)
275 275 if norepo:
276 276 commands.norepo += b' %s' % b' '.join(parsealiases(name))
277 277 return func
278 278
279 279 return decorator
280 280
281 281
282 282 try:
283 283 import mercurial.registrar
284 284 import mercurial.configitems
285 285
286 286 configtable = {}
287 287 configitem = mercurial.registrar.configitem(configtable)
288 288 configitem(
289 289 b'perf',
290 290 b'presleep',
291 291 default=mercurial.configitems.dynamicdefault,
292 292 experimental=True,
293 293 )
294 294 configitem(
295 295 b'perf',
296 296 b'stub',
297 297 default=mercurial.configitems.dynamicdefault,
298 298 experimental=True,
299 299 )
300 300 configitem(
301 301 b'perf',
302 302 b'parentscount',
303 303 default=mercurial.configitems.dynamicdefault,
304 304 experimental=True,
305 305 )
306 306 configitem(
307 307 b'perf',
308 308 b'all-timing',
309 309 default=mercurial.configitems.dynamicdefault,
310 310 experimental=True,
311 311 )
312 312 configitem(
313 313 b'perf',
314 314 b'pre-run',
315 315 default=mercurial.configitems.dynamicdefault,
316 316 )
317 317 configitem(
318 318 b'perf',
319 319 b'profile-benchmark',
320 320 default=mercurial.configitems.dynamicdefault,
321 321 )
322 322 configitem(
323 323 b'perf',
324 324 b'profiled-runs',
325 325 default=mercurial.configitems.dynamicdefault,
326 326 )
327 327 configitem(
328 328 b'perf',
329 329 b'run-limits',
330 330 default=mercurial.configitems.dynamicdefault,
331 331 experimental=True,
332 332 )
333 333 except (ImportError, AttributeError):
334 334 pass
335 335 except TypeError:
336 336 # compatibility fix for a11fd395e83f
337 337 # hg version: 5.2
338 338 configitem(
339 339 b'perf',
340 340 b'presleep',
341 341 default=mercurial.configitems.dynamicdefault,
342 342 )
343 343 configitem(
344 344 b'perf',
345 345 b'stub',
346 346 default=mercurial.configitems.dynamicdefault,
347 347 )
348 348 configitem(
349 349 b'perf',
350 350 b'parentscount',
351 351 default=mercurial.configitems.dynamicdefault,
352 352 )
353 353 configitem(
354 354 b'perf',
355 355 b'all-timing',
356 356 default=mercurial.configitems.dynamicdefault,
357 357 )
358 358 configitem(
359 359 b'perf',
360 360 b'pre-run',
361 361 default=mercurial.configitems.dynamicdefault,
362 362 )
363 363 configitem(
364 364 b'perf',
365 365 b'profiled-runs',
366 366 default=mercurial.configitems.dynamicdefault,
367 367 )
368 368 configitem(
369 369 b'perf',
370 370 b'run-limits',
371 371 default=mercurial.configitems.dynamicdefault,
372 372 )
373 373
374 374
375 375 def getlen(ui):
376 376 if ui.configbool(b"perf", b"stub", False):
377 377 return lambda x: 1
378 378 return len
379 379
380 380
381 381 class noop:
382 382 """dummy context manager"""
383 383
384 384 def __enter__(self):
385 385 pass
386 386
387 387 def __exit__(self, *args):
388 388 pass
389 389
390 390
391 391 NOOPCTX = noop()
392 392
393 393
394 394 def gettimer(ui, opts=None):
395 395 """return a timer function and formatter: (timer, formatter)
396 396
397 397 This function exists to gather the creation of formatter in a single
398 398 place instead of duplicating it in all performance commands."""
399 399
400 400 # enforce an idle period before execution to counteract power management
401 401 # experimental config: perf.presleep
402 402 time.sleep(getint(ui, b"perf", b"presleep", 1))
403 403
404 404 if opts is None:
405 405 opts = {}
406 406 # redirect all to stderr unless buffer api is in use
407 407 if not ui._buffers:
408 408 ui = ui.copy()
409 409 uifout = safeattrsetter(ui, b'fout', ignoremissing=True)
410 410 if uifout:
411 411 # for "historical portability":
412 412 # ui.fout/ferr have been available since 1.9 (or 4e1ccd4c2b6d)
413 413 uifout.set(ui.ferr)
414 414
415 415 # get a formatter
416 416 uiformatter = getattr(ui, 'formatter', None)
417 417 if uiformatter:
418 418 fm = uiformatter(b'perf', opts)
419 419 else:
420 420 # for "historical portability":
421 421 # define formatter locally, because ui.formatter has been
422 422 # available since 2.2 (or ae5f92e154d3)
423 423 from mercurial import node
424 424
425 425 class defaultformatter:
426 426 """Minimized composition of baseformatter and plainformatter"""
427 427
428 428 def __init__(self, ui, topic, opts):
429 429 self._ui = ui
430 430 if ui.debugflag:
431 431 self.hexfunc = node.hex
432 432 else:
433 433 self.hexfunc = node.short
434 434
435 435 def __nonzero__(self):
436 436 return False
437 437
438 438 __bool__ = __nonzero__
439 439
440 440 def startitem(self):
441 441 pass
442 442
443 443 def data(self, **data):
444 444 pass
445 445
446 446 def write(self, fields, deftext, *fielddata, **opts):
447 447 self._ui.write(deftext % fielddata, **opts)
448 448
449 449 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
450 450 if cond:
451 451 self._ui.write(deftext % fielddata, **opts)
452 452
453 453 def plain(self, text, **opts):
454 454 self._ui.write(text, **opts)
455 455
456 456 def end(self):
457 457 pass
458 458
459 459 fm = defaultformatter(ui, b'perf', opts)
460 460
461 461 # stub function, runs code only once instead of in a loop
462 462 # experimental config: perf.stub
463 463 if ui.configbool(b"perf", b"stub", False):
464 464 return functools.partial(stub_timer, fm), fm
465 465
466 466 # experimental config: perf.all-timing
467 467 displayall = ui.configbool(b"perf", b"all-timing", True)
468 468
469 469 # experimental config: perf.run-limits
470 470 limitspec = ui.configlist(b"perf", b"run-limits", [])
471 471 limits = []
472 472 for item in limitspec:
473 473 parts = item.split(b'-', 1)
474 474 if len(parts) < 2:
475 475 ui.warn((b'malformatted run limit entry, missing "-": %s\n' % item))
476 476 continue
477 477 try:
478 478 time_limit = float(_sysstr(parts[0]))
479 479 except ValueError as e:
480 480 ui.warn(
481 481 (
482 482 b'malformatted run limit entry, %s: %s\n'
483 483 % (_bytestr(e), item)
484 484 )
485 485 )
486 486 continue
487 487 try:
488 488 run_limit = int(_sysstr(parts[1]))
489 489 except ValueError as e:
490 490 ui.warn(
491 491 (
492 492 b'malformatted run limit entry, %s: %s\n'
493 493 % (_bytestr(e), item)
494 494 )
495 495 )
496 496 continue
497 497 limits.append((time_limit, run_limit))
498 498 if not limits:
499 499 limits = DEFAULTLIMITS
500 500
501 501 profiler = None
502 502 profiled_runs = set()
503 503 if profiling is not None:
504 504 if ui.configbool(b"perf", b"profile-benchmark", False):
505 505 profiler = lambda: profiling.profile(ui)
506 506 for run in ui.configlist(b"perf", b"profiled-runs", [0]):
507 507 profiled_runs.add(int(run))
508 508
509 509 prerun = getint(ui, b"perf", b"pre-run", 0)
510 510 t = functools.partial(
511 511 _timer,
512 512 fm,
513 513 displayall=displayall,
514 514 limits=limits,
515 515 prerun=prerun,
516 516 profiler=profiler,
517 517 profiled_runs=profiled_runs,
518 518 )
519 519 return t, fm
520 520
521 521
522 522 def stub_timer(fm, func, setup=None, title=None):
523 523 if setup is not None:
524 524 setup()
525 525 func()
526 526
527 527
528 528 @contextlib.contextmanager
529 529 def timeone():
530 530 r = []
531 531 ostart = os.times()
532 532 cstart = util.timer()
533 533 yield r
534 534 cstop = util.timer()
535 535 ostop = os.times()
536 536 a, b = ostart, ostop
537 537 r.append((cstop - cstart, b[0] - a[0], b[1] - a[1]))
538 538
539 539
540 540 # list of stop condition (elapsed time, minimal run count)
541 541 DEFAULTLIMITS = (
542 542 (3.0, 100),
543 543 (10.0, 3),
544 544 )
545 545
546 546
547 547 @contextlib.contextmanager
548 548 def noop_context():
549 549 yield
550 550
551 551
552 552 def _timer(
553 553 fm,
554 554 func,
555 555 setup=None,
556 556 context=noop_context,
557 557 title=None,
558 558 displayall=False,
559 559 limits=DEFAULTLIMITS,
560 560 prerun=0,
561 561 profiler=None,
562 562 profiled_runs=(0,),
563 563 ):
564 564 gc.collect()
565 565 results = []
566 566 count = 0
567 567 if profiler is None:
568 568 profiler = lambda: NOOPCTX
569 569 for i in range(prerun):
570 570 if setup is not None:
571 571 setup()
572 572 with context():
573 573 func()
574 574 begin = util.timer()
575 575 keepgoing = True
576 576 while keepgoing:
577 577 if count in profiled_runs:
578 578 prof = profiler()
579 579 else:
580 580 prof = NOOPCTX
581 581 if setup is not None:
582 582 setup()
583 583 with context():
584 584 gc.collect()
585 585 with prof:
586 586 with timeone() as item:
587 587 r = func()
588 588 count += 1
589 589 results.append(item[0])
590 590 cstop = util.timer()
591 591 # Look for a stop condition.
592 592 elapsed = cstop - begin
593 593 for t, mincount in limits:
594 594 if elapsed >= t and count >= mincount:
595 595 keepgoing = False
596 596 break
597 597
598 598 formatone(fm, results, title=title, result=r, displayall=displayall)
599 599
600 600
601 601 def formatone(fm, timings, title=None, result=None, displayall=False):
602 602 count = len(timings)
603 603
604 604 fm.startitem()
605 605
606 606 if title:
607 607 fm.write(b'title', b'! %s\n', title)
608 608 if result:
609 609 fm.write(b'result', b'! result: %s\n', result)
610 610
611 611 def display(role, entry):
612 612 prefix = b''
613 613 if role != b'best':
614 614 prefix = b'%s.' % role
615 615 fm.plain(b'!')
616 616 fm.write(prefix + b'wall', b' wall %f', entry[0])
617 617 fm.write(prefix + b'comb', b' comb %f', entry[1] + entry[2])
618 618 fm.write(prefix + b'user', b' user %f', entry[1])
619 619 fm.write(prefix + b'sys', b' sys %f', entry[2])
620 620 fm.write(prefix + b'count', b' (%s of %%d)' % role, count)
621 621 fm.plain(b'\n')
622 622
623 623 timings.sort()
624 624 min_val = timings[0]
625 625 display(b'best', min_val)
626 626 if displayall:
627 627 max_val = timings[-1]
628 628 display(b'max', max_val)
629 629 avg = tuple([sum(x) / count for x in zip(*timings)])
630 630 display(b'avg', avg)
631 631 median = timings[len(timings) // 2]
632 632 display(b'median', median)
633 633
634 634
635 635 # utilities for historical portability
636 636
637 637
638 638 def getint(ui, section, name, default):
639 639 # for "historical portability":
640 640 # ui.configint has been available since 1.9 (or fa2b596db182)
641 641 v = ui.config(section, name, None)
642 642 if v is None:
643 643 return default
644 644 try:
645 645 return int(v)
646 646 except ValueError:
647 647 raise error.ConfigError(
648 648 b"%s.%s is not an integer ('%s')" % (section, name, v)
649 649 )
650 650
651 651
652 652 def safeattrsetter(obj, name, ignoremissing=False):
653 653 """Ensure that 'obj' has 'name' attribute before subsequent setattr
654 654
655 655 This function is aborted, if 'obj' doesn't have 'name' attribute
656 656 at runtime. This avoids overlooking removal of an attribute, which
657 657 breaks assumption of performance measurement, in the future.
658 658
659 659 This function returns the object to (1) assign a new value, and
660 660 (2) restore an original value to the attribute.
661 661
662 662 If 'ignoremissing' is true, missing 'name' attribute doesn't cause
663 663 abortion, and this function returns None. This is useful to
664 664 examine an attribute, which isn't ensured in all Mercurial
665 665 versions.
666 666 """
667 667 if not util.safehasattr(obj, name):
668 668 if ignoremissing:
669 669 return None
670 670 raise error.Abort(
671 671 (
672 672 b"missing attribute %s of %s might break assumption"
673 673 b" of performance measurement"
674 674 )
675 675 % (name, obj)
676 676 )
677 677
678 678 origvalue = getattr(obj, _sysstr(name))
679 679
680 680 class attrutil:
681 681 def set(self, newvalue):
682 682 setattr(obj, _sysstr(name), newvalue)
683 683
684 684 def restore(self):
685 685 setattr(obj, _sysstr(name), origvalue)
686 686
687 687 return attrutil()
688 688
689 689
690 690 # utilities to examine each internal API changes
691 691
692 692
693 693 def getbranchmapsubsettable():
694 694 # for "historical portability":
695 695 # subsettable is defined in:
696 696 # - branchmap since 2.9 (or 175c6fd8cacc)
697 697 # - repoview since 2.5 (or 59a9f18d4587)
698 698 # - repoviewutil since 5.0
699 699 for mod in (branchmap, repoview, repoviewutil):
700 700 subsettable = getattr(mod, 'subsettable', None)
701 701 if subsettable:
702 702 return subsettable
703 703
704 704 # bisecting in bcee63733aad::59a9f18d4587 can reach here (both
705 705 # branchmap and repoview modules exist, but subsettable attribute
706 706 # doesn't)
707 707 raise error.Abort(
708 708 b"perfbranchmap not available with this Mercurial",
709 709 hint=b"use 2.5 or later",
710 710 )
711 711
712 712
713 713 def getsvfs(repo):
714 714 """Return appropriate object to access files under .hg/store"""
715 715 # for "historical portability":
716 716 # repo.svfs has been available since 2.3 (or 7034365089bf)
717 717 svfs = getattr(repo, 'svfs', None)
718 718 if svfs:
719 719 return svfs
720 720 else:
721 721 return getattr(repo, 'sopener')
722 722
723 723
724 724 def getvfs(repo):
725 725 """Return appropriate object to access files under .hg"""
726 726 # for "historical portability":
727 727 # repo.vfs has been available since 2.3 (or 7034365089bf)
728 728 vfs = getattr(repo, 'vfs', None)
729 729 if vfs:
730 730 return vfs
731 731 else:
732 732 return getattr(repo, 'opener')
733 733
734 734
735 735 def repocleartagscachefunc(repo):
736 736 """Return the function to clear tags cache according to repo internal API"""
737 737 if util.safehasattr(repo, b'_tagscache'): # since 2.0 (or 9dca7653b525)
738 738 # in this case, setattr(repo, '_tagscache', None) or so isn't
739 739 # correct way to clear tags cache, because existing code paths
740 740 # expect _tagscache to be a structured object.
741 741 def clearcache():
742 742 # _tagscache has been filteredpropertycache since 2.5 (or
743 743 # 98c867ac1330), and delattr() can't work in such case
744 744 if '_tagscache' in vars(repo):
745 745 del repo.__dict__['_tagscache']
746 746
747 747 return clearcache
748 748
749 749 repotags = safeattrsetter(repo, b'_tags', ignoremissing=True)
750 750 if repotags: # since 1.4 (or 5614a628d173)
751 751 return lambda: repotags.set(None)
752 752
753 753 repotagscache = safeattrsetter(repo, b'tagscache', ignoremissing=True)
754 754 if repotagscache: # since 0.6 (or d7df759d0e97)
755 755 return lambda: repotagscache.set(None)
756 756
757 757 # Mercurial earlier than 0.6 (or d7df759d0e97) logically reaches
758 758 # this point, but it isn't so problematic, because:
759 759 # - repo.tags of such Mercurial isn't "callable", and repo.tags()
760 760 # in perftags() causes failure soon
761 761 # - perf.py itself has been available since 1.1 (or eb240755386d)
762 762 raise error.Abort(b"tags API of this hg command is unknown")
763 763
764 764
765 765 # utilities to clear cache
766 766
767 767
768 768 def clearfilecache(obj, attrname):
769 769 unfiltered = getattr(obj, 'unfiltered', None)
770 770 if unfiltered is not None:
771 771 obj = obj.unfiltered()
772 772 if attrname in vars(obj):
773 773 delattr(obj, attrname)
774 774 obj._filecache.pop(attrname, None)
775 775
776 776
777 777 def clearchangelog(repo):
778 778 if repo is not repo.unfiltered():
779 779 object.__setattr__(repo, '_clcachekey', None)
780 780 object.__setattr__(repo, '_clcache', None)
781 781 clearfilecache(repo.unfiltered(), 'changelog')
782 782
783 783
784 784 # perf commands
785 785
786 786
787 787 @command(b'perf::walk|perfwalk', formatteropts)
788 788 def perfwalk(ui, repo, *pats, **opts):
789 789 opts = _byteskwargs(opts)
790 790 timer, fm = gettimer(ui, opts)
791 791 m = scmutil.match(repo[None], pats, {})
792 792 timer(
793 793 lambda: len(
794 794 list(
795 795 repo.dirstate.walk(m, subrepos=[], unknown=True, ignored=False)
796 796 )
797 797 )
798 798 )
799 799 fm.end()
800 800
801 801
802 802 @command(b'perf::annotate|perfannotate', formatteropts)
803 803 def perfannotate(ui, repo, f, **opts):
804 804 opts = _byteskwargs(opts)
805 805 timer, fm = gettimer(ui, opts)
806 806 fc = repo[b'.'][f]
807 807 timer(lambda: len(fc.annotate(True)))
808 808 fm.end()
809 809
810 810
811 811 @command(
812 812 b'perf::status|perfstatus',
813 813 [
814 814 (b'u', b'unknown', False, b'ask status to look for unknown files'),
815 815 (b'', b'dirstate', False, b'benchmark the internal dirstate call'),
816 816 ]
817 817 + formatteropts,
818 818 )
819 819 def perfstatus(ui, repo, **opts):
820 820 """benchmark the performance of a single status call
821 821
822 822 The repository data are preserved between each call.
823 823
824 824 By default, only the status of the tracked file are requested. If
825 825 `--unknown` is passed, the "unknown" files are also tracked.
826 826 """
827 827 opts = _byteskwargs(opts)
828 828 # m = match.always(repo.root, repo.getcwd())
829 829 # timer(lambda: sum(map(len, repo.dirstate.status(m, [], False, False,
830 830 # False))))
831 831 timer, fm = gettimer(ui, opts)
832 832 if opts[b'dirstate']:
833 833 dirstate = repo.dirstate
834 834 m = scmutil.matchall(repo)
835 835 unknown = opts[b'unknown']
836 836
837 837 def status_dirstate():
838 838 s = dirstate.status(
839 839 m, subrepos=[], ignored=False, clean=False, unknown=unknown
840 840 )
841 841 sum(map(bool, s))
842 842
843 843 if util.safehasattr(dirstate, 'running_status'):
844 844 with dirstate.running_status(repo):
845 845 timer(status_dirstate)
846 846 dirstate.invalidate()
847 847 else:
848 848 timer(status_dirstate)
849 849 else:
850 850 timer(lambda: sum(map(len, repo.status(unknown=opts[b'unknown']))))
851 851 fm.end()
852 852
853 853
854 854 @command(b'perf::addremove|perfaddremove', formatteropts)
855 855 def perfaddremove(ui, repo, **opts):
856 856 opts = _byteskwargs(opts)
857 857 timer, fm = gettimer(ui, opts)
858 858 try:
859 859 oldquiet = repo.ui.quiet
860 860 repo.ui.quiet = True
861 861 matcher = scmutil.match(repo[None])
862 862 opts[b'dry_run'] = True
863 863 if 'uipathfn' in getargspec(scmutil.addremove).args:
864 864 uipathfn = scmutil.getuipathfn(repo)
865 865 timer(lambda: scmutil.addremove(repo, matcher, b"", uipathfn, opts))
866 866 else:
867 867 timer(lambda: scmutil.addremove(repo, matcher, b"", opts))
868 868 finally:
869 869 repo.ui.quiet = oldquiet
870 870 fm.end()
871 871
872 872
873 873 def clearcaches(cl):
874 874 # behave somewhat consistently across internal API changes
875 875 if util.safehasattr(cl, b'clearcaches'):
876 876 cl.clearcaches()
877 877 elif util.safehasattr(cl, b'_nodecache'):
878 878 # <= hg-5.2
879 879 from mercurial.node import nullid, nullrev
880 880
881 881 cl._nodecache = {nullid: nullrev}
882 882 cl._nodepos = None
883 883
884 884
885 885 @command(b'perf::heads|perfheads', formatteropts)
886 886 def perfheads(ui, repo, **opts):
887 887 """benchmark the computation of a changelog heads"""
888 888 opts = _byteskwargs(opts)
889 889 timer, fm = gettimer(ui, opts)
890 890 cl = repo.changelog
891 891
892 892 def s():
893 893 clearcaches(cl)
894 894
895 895 def d():
896 896 len(cl.headrevs())
897 897
898 898 timer(d, setup=s)
899 899 fm.end()
900 900
901 901
902 902 def _default_clear_on_disk_tags_cache(repo):
903 903 from mercurial import tags
904 904
905 905 repo.cachevfs.tryunlink(tags._filename(repo))
906 906
907 907
908 908 def _default_clear_on_disk_tags_fnodes_cache(repo):
909 909 from mercurial import tags
910 910
911 911 repo.cachevfs.tryunlink(tags._fnodescachefile)
912 912
913 913
914 914 def _default_forget_fnodes(repo, revs):
915 915 """function used by the perf extension to prune some entries from the
916 916 fnodes cache"""
917 917 from mercurial import tags
918 918
919 919 missing_1 = b'\xff' * 4
920 920 missing_2 = b'\xff' * 20
921 921 cache = tags.hgtagsfnodescache(repo.unfiltered())
922 922 for r in revs:
923 923 cache._writeentry(r * tags._fnodesrecsize, missing_1, missing_2)
924 924 cache.write()
925 925
926 926
927 927 @command(
928 928 b'perf::tags|perftags',
929 929 formatteropts
930 930 + [
931 931 (b'', b'clear-revlogs', False, b'refresh changelog and manifest'),
932 932 (
933 933 b'',
934 934 b'clear-on-disk-cache',
935 935 False,
936 936 b'clear on disk tags cache (DESTRUCTIVE)',
937 937 ),
938 938 (
939 939 b'',
940 940 b'clear-fnode-cache-all',
941 941 False,
942 942 b'clear on disk file node cache (DESTRUCTIVE),',
943 943 ),
944 944 (
945 945 b'',
946 946 b'clear-fnode-cache-rev',
947 947 [],
948 948 b'clear on disk file node cache (DESTRUCTIVE),',
949 949 b'REVS',
950 950 ),
951 951 (
952 952 b'',
953 953 b'update-last',
954 954 b'',
955 955 b'simulate an update over the last N revisions (DESTRUCTIVE),',
956 956 b'N',
957 957 ),
958 958 ],
959 959 )
960 960 def perftags(ui, repo, **opts):
961 961 """Benchmark tags retrieval in various situation
962 962
963 963 The option marked as (DESTRUCTIVE) will alter the on-disk cache, possibly
964 964 altering performance after the command was run. However, it does not
965 965 destroy any stored data.
966 966 """
967 967 from mercurial import tags
968 968
969 969 opts = _byteskwargs(opts)
970 970 timer, fm = gettimer(ui, opts)
971 971 repocleartagscache = repocleartagscachefunc(repo)
972 972 clearrevlogs = opts[b'clear_revlogs']
973 973 clear_disk = opts[b'clear_on_disk_cache']
974 974 clear_fnode = opts[b'clear_fnode_cache_all']
975 975
976 976 clear_fnode_revs = opts[b'clear_fnode_cache_rev']
977 977 update_last_str = opts[b'update_last']
978 978 update_last = None
979 979 if update_last_str:
980 980 try:
981 981 update_last = int(update_last_str)
982 982 except ValueError:
983 983 msg = b'could not parse value for update-last: "%s"'
984 984 msg %= update_last_str
985 985 hint = b'value should be an integer'
986 986 raise error.Abort(msg, hint=hint)
987 987
988 988 clear_disk_fn = getattr(
989 989 tags,
990 990 "clear_cache_on_disk",
991 991 _default_clear_on_disk_tags_cache,
992 992 )
993 993 if getattr(tags, 'clear_cache_fnodes_is_working', False):
994 994 clear_fnodes_fn = tags.clear_cache_fnodes
995 995 else:
996 996 clear_fnodes_fn = _default_clear_on_disk_tags_fnodes_cache
997 997 clear_fnodes_rev_fn = getattr(
998 998 tags,
999 999 "forget_fnodes",
1000 1000 _default_forget_fnodes,
1001 1001 )
1002 1002
1003 1003 clear_revs = []
1004 1004 if clear_fnode_revs:
1005 1005 clear_revs.extend(scmutil.revrange(repo, clear_fnode_revs))
1006 1006
1007 1007 if update_last:
1008 1008 revset = b'last(all(), %d)' % update_last
1009 1009 last_revs = repo.unfiltered().revs(revset)
1010 1010 clear_revs.extend(last_revs)
1011 1011
1012 1012 from mercurial import repoview
1013 1013
1014 1014 rev_filter = {(b'experimental', b'extra-filter-revs'): revset}
1015 1015 with repo.ui.configoverride(rev_filter, source=b"perf"):
1016 1016 filter_id = repoview.extrafilter(repo.ui)
1017 1017
1018 1018 filter_name = b'%s%%%s' % (repo.filtername, filter_id)
1019 1019 pre_repo = repo.filtered(filter_name)
1020 1020 pre_repo.tags() # warm the cache
1021 1021 old_tags_path = repo.cachevfs.join(tags._filename(pre_repo))
1022 1022 new_tags_path = repo.cachevfs.join(tags._filename(repo))
1023 1023
1024 1024 clear_revs = sorted(set(clear_revs))
1025 1025
1026 1026 def s():
1027 1027 if update_last:
1028 1028 util.copyfile(old_tags_path, new_tags_path)
1029 1029 if clearrevlogs:
1030 1030 clearchangelog(repo)
1031 1031 clearfilecache(repo.unfiltered(), 'manifest')
1032 1032 if clear_disk:
1033 1033 clear_disk_fn(repo)
1034 1034 if clear_fnode:
1035 1035 clear_fnodes_fn(repo)
1036 1036 elif clear_revs:
1037 1037 clear_fnodes_rev_fn(repo, clear_revs)
1038 1038 repocleartagscache()
1039 1039
1040 1040 def t():
1041 1041 len(repo.tags())
1042 1042
1043 1043 timer(t, setup=s)
1044 1044 fm.end()
1045 1045
1046 1046
1047 1047 @command(b'perf::ancestors|perfancestors', formatteropts)
1048 1048 def perfancestors(ui, repo, **opts):
1049 1049 opts = _byteskwargs(opts)
1050 1050 timer, fm = gettimer(ui, opts)
1051 1051 heads = repo.changelog.headrevs()
1052 1052
1053 1053 def d():
1054 1054 for a in repo.changelog.ancestors(heads):
1055 1055 pass
1056 1056
1057 1057 timer(d)
1058 1058 fm.end()
1059 1059
1060 1060
1061 1061 @command(b'perf::ancestorset|perfancestorset', formatteropts)
1062 1062 def perfancestorset(ui, repo, revset, **opts):
1063 1063 opts = _byteskwargs(opts)
1064 1064 timer, fm = gettimer(ui, opts)
1065 1065 revs = repo.revs(revset)
1066 1066 heads = repo.changelog.headrevs()
1067 1067
1068 1068 def d():
1069 1069 s = repo.changelog.ancestors(heads)
1070 1070 for rev in revs:
1071 1071 rev in s
1072 1072
1073 1073 timer(d)
1074 1074 fm.end()
1075 1075
1076 1076
1077 1077 @command(
1078 1078 b'perf::delta-find',
1079 1079 revlogopts + formatteropts,
1080 1080 b'-c|-m|FILE REV',
1081 1081 )
1082 1082 def perf_delta_find(ui, repo, arg_1, arg_2=None, **opts):
1083 1083 """benchmark the process of finding a valid delta for a revlog revision
1084 1084
1085 1085 When a revlog receives a new revision (e.g. from a commit, or from an
1086 1086 incoming bundle), it searches for a suitable delta-base to produce a delta.
1087 1087 This perf command measures how much time we spend in this process. It
1088 1088 operates on an already stored revision.
1089 1089
1090 1090 See `hg help debug-delta-find` for another related command.
1091 1091 """
1092 1092 from mercurial import revlogutils
1093 1093 import mercurial.revlogutils.deltas as deltautil
1094 1094
1095 1095 opts = _byteskwargs(opts)
1096 1096 if arg_2 is None:
1097 1097 file_ = None
1098 1098 rev = arg_1
1099 1099 else:
1100 1100 file_ = arg_1
1101 1101 rev = arg_2
1102 1102
1103 1103 repo = repo.unfiltered()
1104 1104
1105 1105 timer, fm = gettimer(ui, opts)
1106 1106
1107 1107 rev = int(rev)
1108 1108
1109 1109 revlog = cmdutil.openrevlog(repo, b'perf::delta-find', file_, opts)
1110 1110
1111 1111 deltacomputer = deltautil.deltacomputer(revlog)
1112 1112
1113 1113 node = revlog.node(rev)
1114 1114 p1r, p2r = revlog.parentrevs(rev)
1115 1115 p1 = revlog.node(p1r)
1116 1116 p2 = revlog.node(p2r)
1117 1117 full_text = revlog.revision(rev)
1118 1118 textlen = len(full_text)
1119 1119 cachedelta = None
1120 1120 flags = revlog.flags(rev)
1121 1121
1122 1122 revinfo = revlogutils.revisioninfo(
1123 1123 node,
1124 1124 p1,
1125 1125 p2,
1126 1126 [full_text], # btext
1127 1127 textlen,
1128 1128 cachedelta,
1129 1129 flags,
1130 1130 )
1131 1131
1132 1132 # Note: we should probably purge the potential caches (like the full
1133 1133 # manifest cache) between runs.
1134 1134 def find_one():
1135 1135 with revlog._datafp() as fh:
1136 1136 deltacomputer.finddeltainfo(revinfo, fh, target_rev=rev)
1137 1137
1138 1138 timer(find_one)
1139 1139 fm.end()
1140 1140
1141 1141
1142 1142 @command(b'perf::discovery|perfdiscovery', formatteropts, b'PATH')
1143 1143 def perfdiscovery(ui, repo, path, **opts):
1144 1144 """benchmark discovery between local repo and the peer at given path"""
1145 1145 repos = [repo, None]
1146 1146 timer, fm = gettimer(ui, opts)
1147 1147
1148 1148 try:
1149 1149 from mercurial.utils.urlutil import get_unique_pull_path_obj
1150 1150
1151 1151 path = get_unique_pull_path_obj(b'perfdiscovery', ui, path)
1152 1152 except ImportError:
1153 1153 try:
1154 1154 from mercurial.utils.urlutil import get_unique_pull_path
1155 1155
1156 1156 path = get_unique_pull_path(b'perfdiscovery', repo, ui, path)[0]
1157 1157 except ImportError:
1158 1158 path = ui.expandpath(path)
1159 1159
1160 1160 def s():
1161 1161 repos[1] = hg.peer(ui, opts, path)
1162 1162
1163 1163 def d():
1164 1164 setdiscovery.findcommonheads(ui, *repos)
1165 1165
1166 1166 timer(d, setup=s)
1167 1167 fm.end()
1168 1168
1169 1169
1170 1170 @command(
1171 1171 b'perf::bookmarks|perfbookmarks',
1172 1172 formatteropts
1173 1173 + [
1174 1174 (b'', b'clear-revlogs', False, b'refresh changelog and manifest'),
1175 1175 ],
1176 1176 )
1177 1177 def perfbookmarks(ui, repo, **opts):
1178 1178 """benchmark parsing bookmarks from disk to memory"""
1179 1179 opts = _byteskwargs(opts)
1180 1180 timer, fm = gettimer(ui, opts)
1181 1181
1182 1182 clearrevlogs = opts[b'clear_revlogs']
1183 1183
1184 1184 def s():
1185 1185 if clearrevlogs:
1186 1186 clearchangelog(repo)
1187 1187 clearfilecache(repo, b'_bookmarks')
1188 1188
1189 1189 def d():
1190 1190 repo._bookmarks
1191 1191
1192 1192 timer(d, setup=s)
1193 1193 fm.end()
1194 1194
1195 1195
1196 1196 @command(
1197 1197 b'perf::bundle',
1198 1198 [
1199 1199 (
1200 1200 b'r',
1201 1201 b'rev',
1202 1202 [],
1203 1203 b'changesets to bundle',
1204 1204 b'REV',
1205 1205 ),
1206 1206 (
1207 1207 b't',
1208 1208 b'type',
1209 1209 b'none',
1210 1210 b'bundlespec to use (see `hg help bundlespec`)',
1211 1211 b'TYPE',
1212 1212 ),
1213 1213 ]
1214 1214 + formatteropts,
1215 1215 b'REVS',
1216 1216 )
1217 1217 def perfbundle(ui, repo, *revs, **opts):
1218 1218 """benchmark the creation of a bundle from a repository
1219 1219
1220 1220 For now, this only supports "none" compression.
1221 1221 """
1222 1222 try:
1223 1223 from mercurial import bundlecaches
1224 1224
1225 1225 parsebundlespec = bundlecaches.parsebundlespec
1226 1226 except ImportError:
1227 1227 from mercurial import exchange
1228 1228
1229 1229 parsebundlespec = exchange.parsebundlespec
1230 1230
1231 1231 from mercurial import discovery
1232 1232 from mercurial import bundle2
1233 1233
1234 1234 opts = _byteskwargs(opts)
1235 1235 timer, fm = gettimer(ui, opts)
1236 1236
1237 1237 cl = repo.changelog
1238 1238 revs = list(revs)
1239 1239 revs.extend(opts.get(b'rev', ()))
1240 1240 revs = scmutil.revrange(repo, revs)
1241 1241 if not revs:
1242 1242 raise error.Abort(b"not revision specified")
1243 1243 # make it a consistent set (ie: without topological gaps)
1244 1244 old_len = len(revs)
1245 1245 revs = list(repo.revs(b"%ld::%ld", revs, revs))
1246 1246 if old_len != len(revs):
1247 1247 new_count = len(revs) - old_len
1248 1248 msg = b"add %d new revisions to make it a consistent set\n"
1249 1249 ui.write_err(msg % new_count)
1250 1250
1251 1251 targets = [cl.node(r) for r in repo.revs(b"heads(::%ld)", revs)]
1252 1252 bases = [cl.node(r) for r in repo.revs(b"heads(::%ld - %ld)", revs, revs)]
1253 1253 outgoing = discovery.outgoing(repo, bases, targets)
1254 1254
1255 1255 bundle_spec = opts.get(b'type')
1256 1256
1257 1257 bundle_spec = parsebundlespec(repo, bundle_spec, strict=False)
1258 1258
1259 1259 cgversion = bundle_spec.params.get(b"cg.version")
1260 1260 if cgversion is None:
1261 1261 if bundle_spec.version == b'v1':
1262 1262 cgversion = b'01'
1263 1263 if bundle_spec.version == b'v2':
1264 1264 cgversion = b'02'
1265 1265 if cgversion not in changegroup.supportedoutgoingversions(repo):
1266 1266 err = b"repository does not support bundle version %s"
1267 1267 raise error.Abort(err % cgversion)
1268 1268
1269 1269 if cgversion == b'01': # bundle1
1270 1270 bversion = b'HG10' + bundle_spec.wirecompression
1271 1271 bcompression = None
1272 1272 elif cgversion in (b'02', b'03'):
1273 1273 bversion = b'HG20'
1274 1274 bcompression = bundle_spec.wirecompression
1275 1275 else:
1276 1276 err = b'perf::bundle: unexpected changegroup version %s'
1277 1277 raise error.ProgrammingError(err % cgversion)
1278 1278
1279 1279 if bcompression is None:
1280 1280 bcompression = b'UN'
1281 1281
1282 1282 if bcompression != b'UN':
1283 1283 err = b'perf::bundle: compression currently unsupported: %s'
1284 1284 raise error.ProgrammingError(err % bcompression)
1285 1285
1286 1286 def do_bundle():
1287 1287 bundle2.writenewbundle(
1288 1288 ui,
1289 1289 repo,
1290 1290 b'perf::bundle',
1291 1291 os.devnull,
1292 1292 bversion,
1293 1293 outgoing,
1294 1294 bundle_spec.params,
1295 1295 )
1296 1296
1297 1297 timer(do_bundle)
1298 1298 fm.end()
1299 1299
1300 1300
1301 1301 @command(b'perf::bundleread|perfbundleread', formatteropts, b'BUNDLE')
1302 1302 def perfbundleread(ui, repo, bundlepath, **opts):
1303 1303 """Benchmark reading of bundle files.
1304 1304
1305 1305 This command is meant to isolate the I/O part of bundle reading as
1306 1306 much as possible.
1307 1307 """
1308 1308 from mercurial import (
1309 1309 bundle2,
1310 1310 exchange,
1311 1311 streamclone,
1312 1312 )
1313 1313
1314 1314 opts = _byteskwargs(opts)
1315 1315
1316 1316 def makebench(fn):
1317 1317 def run():
1318 1318 with open(bundlepath, b'rb') as fh:
1319 1319 bundle = exchange.readbundle(ui, fh, bundlepath)
1320 1320 fn(bundle)
1321 1321
1322 1322 return run
1323 1323
1324 1324 def makereadnbytes(size):
1325 1325 def run():
1326 1326 with open(bundlepath, b'rb') as fh:
1327 1327 bundle = exchange.readbundle(ui, fh, bundlepath)
1328 1328 while bundle.read(size):
1329 1329 pass
1330 1330
1331 1331 return run
1332 1332
1333 1333 def makestdioread(size):
1334 1334 def run():
1335 1335 with open(bundlepath, b'rb') as fh:
1336 1336 while fh.read(size):
1337 1337 pass
1338 1338
1339 1339 return run
1340 1340
1341 1341 # bundle1
1342 1342
1343 1343 def deltaiter(bundle):
1344 1344 for delta in bundle.deltaiter():
1345 1345 pass
1346 1346
1347 1347 def iterchunks(bundle):
1348 1348 for chunk in bundle.getchunks():
1349 1349 pass
1350 1350
1351 1351 # bundle2
1352 1352
1353 1353 def forwardchunks(bundle):
1354 1354 for chunk in bundle._forwardchunks():
1355 1355 pass
1356 1356
1357 1357 def iterparts(bundle):
1358 1358 for part in bundle.iterparts():
1359 1359 pass
1360 1360
1361 1361 def iterpartsseekable(bundle):
1362 1362 for part in bundle.iterparts(seekable=True):
1363 1363 pass
1364 1364
1365 1365 def seek(bundle):
1366 1366 for part in bundle.iterparts(seekable=True):
1367 1367 part.seek(0, os.SEEK_END)
1368 1368
1369 1369 def makepartreadnbytes(size):
1370 1370 def run():
1371 1371 with open(bundlepath, b'rb') as fh:
1372 1372 bundle = exchange.readbundle(ui, fh, bundlepath)
1373 1373 for part in bundle.iterparts():
1374 1374 while part.read(size):
1375 1375 pass
1376 1376
1377 1377 return run
1378 1378
1379 1379 benches = [
1380 1380 (makestdioread(8192), b'read(8k)'),
1381 1381 (makestdioread(16384), b'read(16k)'),
1382 1382 (makestdioread(32768), b'read(32k)'),
1383 1383 (makestdioread(131072), b'read(128k)'),
1384 1384 ]
1385 1385
1386 1386 with open(bundlepath, b'rb') as fh:
1387 1387 bundle = exchange.readbundle(ui, fh, bundlepath)
1388 1388
1389 1389 if isinstance(bundle, changegroup.cg1unpacker):
1390 1390 benches.extend(
1391 1391 [
1392 1392 (makebench(deltaiter), b'cg1 deltaiter()'),
1393 1393 (makebench(iterchunks), b'cg1 getchunks()'),
1394 1394 (makereadnbytes(8192), b'cg1 read(8k)'),
1395 1395 (makereadnbytes(16384), b'cg1 read(16k)'),
1396 1396 (makereadnbytes(32768), b'cg1 read(32k)'),
1397 1397 (makereadnbytes(131072), b'cg1 read(128k)'),
1398 1398 ]
1399 1399 )
1400 1400 elif isinstance(bundle, bundle2.unbundle20):
1401 1401 benches.extend(
1402 1402 [
1403 1403 (makebench(forwardchunks), b'bundle2 forwardchunks()'),
1404 1404 (makebench(iterparts), b'bundle2 iterparts()'),
1405 1405 (
1406 1406 makebench(iterpartsseekable),
1407 1407 b'bundle2 iterparts() seekable',
1408 1408 ),
1409 1409 (makebench(seek), b'bundle2 part seek()'),
1410 1410 (makepartreadnbytes(8192), b'bundle2 part read(8k)'),
1411 1411 (makepartreadnbytes(16384), b'bundle2 part read(16k)'),
1412 1412 (makepartreadnbytes(32768), b'bundle2 part read(32k)'),
1413 1413 (makepartreadnbytes(131072), b'bundle2 part read(128k)'),
1414 1414 ]
1415 1415 )
1416 1416 elif isinstance(bundle, streamclone.streamcloneapplier):
1417 1417 raise error.Abort(b'stream clone bundles not supported')
1418 1418 else:
1419 1419 raise error.Abort(b'unhandled bundle type: %s' % type(bundle))
1420 1420
1421 1421 for fn, title in benches:
1422 1422 timer, fm = gettimer(ui, opts)
1423 1423 timer(fn, title=title)
1424 1424 fm.end()
1425 1425
1426 1426
1427 1427 @command(
1428 1428 b'perf::changegroupchangelog|perfchangegroupchangelog',
1429 1429 formatteropts
1430 1430 + [
1431 1431 (b'', b'cgversion', b'02', b'changegroup version'),
1432 1432 (b'r', b'rev', b'', b'revisions to add to changegroup'),
1433 1433 ],
1434 1434 )
1435 1435 def perfchangegroupchangelog(ui, repo, cgversion=b'02', rev=None, **opts):
1436 1436 """Benchmark producing a changelog group for a changegroup.
1437 1437
1438 1438 This measures the time spent processing the changelog during a
1439 1439 bundle operation. This occurs during `hg bundle` and on a server
1440 1440 processing a `getbundle` wire protocol request (handles clones
1441 1441 and pull requests).
1442 1442
1443 1443 By default, all revisions are added to the changegroup.
1444 1444 """
1445 1445 opts = _byteskwargs(opts)
1446 1446 cl = repo.changelog
1447 1447 nodes = [cl.lookup(r) for r in repo.revs(rev or b'all()')]
1448 1448 bundler = changegroup.getbundler(cgversion, repo)
1449 1449
1450 1450 def d():
1451 1451 state, chunks = bundler._generatechangelog(cl, nodes)
1452 1452 for chunk in chunks:
1453 1453 pass
1454 1454
1455 1455 timer, fm = gettimer(ui, opts)
1456 1456
1457 1457 # Terminal printing can interfere with timing. So disable it.
1458 1458 with ui.configoverride({(b'progress', b'disable'): True}):
1459 1459 timer(d)
1460 1460
1461 1461 fm.end()
1462 1462
1463 1463
1464 1464 @command(b'perf::dirs|perfdirs', formatteropts)
1465 1465 def perfdirs(ui, repo, **opts):
1466 1466 opts = _byteskwargs(opts)
1467 1467 timer, fm = gettimer(ui, opts)
1468 1468 dirstate = repo.dirstate
1469 1469 b'a' in dirstate
1470 1470
1471 1471 def d():
1472 1472 dirstate.hasdir(b'a')
1473 1473 try:
1474 1474 del dirstate._map._dirs
1475 1475 except AttributeError:
1476 1476 pass
1477 1477
1478 1478 timer(d)
1479 1479 fm.end()
1480 1480
1481 1481
1482 1482 @command(
1483 1483 b'perf::dirstate|perfdirstate',
1484 1484 [
1485 1485 (
1486 1486 b'',
1487 1487 b'iteration',
1488 1488 None,
1489 1489 b'benchmark a full iteration for the dirstate',
1490 1490 ),
1491 1491 (
1492 1492 b'',
1493 1493 b'contains',
1494 1494 None,
1495 1495 b'benchmark a large amount of `nf in dirstate` calls',
1496 1496 ),
1497 1497 ]
1498 1498 + formatteropts,
1499 1499 )
1500 1500 def perfdirstate(ui, repo, **opts):
1501 1501 """benchmap the time of various distate operations
1502 1502
1503 1503 By default benchmark the time necessary to load a dirstate from scratch.
1504 1504 The dirstate is loaded to the point were a "contains" request can be
1505 1505 answered.
1506 1506 """
1507 1507 opts = _byteskwargs(opts)
1508 1508 timer, fm = gettimer(ui, opts)
1509 1509 b"a" in repo.dirstate
1510 1510
1511 1511 if opts[b'iteration'] and opts[b'contains']:
1512 1512 msg = b'only specify one of --iteration or --contains'
1513 1513 raise error.Abort(msg)
1514 1514
1515 1515 if opts[b'iteration']:
1516 1516 setup = None
1517 1517 dirstate = repo.dirstate
1518 1518
1519 1519 def d():
1520 1520 for f in dirstate:
1521 1521 pass
1522 1522
1523 1523 elif opts[b'contains']:
1524 1524 setup = None
1525 1525 dirstate = repo.dirstate
1526 1526 allfiles = list(dirstate)
1527 1527 # also add file path that will be "missing" from the dirstate
1528 1528 allfiles.extend([f[::-1] for f in allfiles])
1529 1529
1530 1530 def d():
1531 1531 for f in allfiles:
1532 1532 f in dirstate
1533 1533
1534 1534 else:
1535 1535
1536 1536 def setup():
1537 1537 repo.dirstate.invalidate()
1538 1538
1539 1539 def d():
1540 1540 b"a" in repo.dirstate
1541 1541
1542 1542 timer(d, setup=setup)
1543 1543 fm.end()
1544 1544
1545 1545
1546 1546 @command(b'perf::dirstatedirs|perfdirstatedirs', formatteropts)
1547 1547 def perfdirstatedirs(ui, repo, **opts):
1548 1548 """benchmap a 'dirstate.hasdir' call from an empty `dirs` cache"""
1549 1549 opts = _byteskwargs(opts)
1550 1550 timer, fm = gettimer(ui, opts)
1551 1551 repo.dirstate.hasdir(b"a")
1552 1552
1553 1553 def setup():
1554 1554 try:
1555 1555 del repo.dirstate._map._dirs
1556 1556 except AttributeError:
1557 1557 pass
1558 1558
1559 1559 def d():
1560 1560 repo.dirstate.hasdir(b"a")
1561 1561
1562 1562 timer(d, setup=setup)
1563 1563 fm.end()
1564 1564
1565 1565
1566 1566 @command(b'perf::dirstatefoldmap|perfdirstatefoldmap', formatteropts)
1567 1567 def perfdirstatefoldmap(ui, repo, **opts):
1568 1568 """benchmap a `dirstate._map.filefoldmap.get()` request
1569 1569
1570 1570 The dirstate filefoldmap cache is dropped between every request.
1571 1571 """
1572 1572 opts = _byteskwargs(opts)
1573 1573 timer, fm = gettimer(ui, opts)
1574 1574 dirstate = repo.dirstate
1575 1575 dirstate._map.filefoldmap.get(b'a')
1576 1576
1577 1577 def setup():
1578 1578 del dirstate._map.filefoldmap
1579 1579
1580 1580 def d():
1581 1581 dirstate._map.filefoldmap.get(b'a')
1582 1582
1583 1583 timer(d, setup=setup)
1584 1584 fm.end()
1585 1585
1586 1586
1587 1587 @command(b'perf::dirfoldmap|perfdirfoldmap', formatteropts)
1588 1588 def perfdirfoldmap(ui, repo, **opts):
1589 1589 """benchmap a `dirstate._map.dirfoldmap.get()` request
1590 1590
1591 1591 The dirstate dirfoldmap cache is dropped between every request.
1592 1592 """
1593 1593 opts = _byteskwargs(opts)
1594 1594 timer, fm = gettimer(ui, opts)
1595 1595 dirstate = repo.dirstate
1596 1596 dirstate._map.dirfoldmap.get(b'a')
1597 1597
1598 1598 def setup():
1599 1599 del dirstate._map.dirfoldmap
1600 1600 try:
1601 1601 del dirstate._map._dirs
1602 1602 except AttributeError:
1603 1603 pass
1604 1604
1605 1605 def d():
1606 1606 dirstate._map.dirfoldmap.get(b'a')
1607 1607
1608 1608 timer(d, setup=setup)
1609 1609 fm.end()
1610 1610
1611 1611
1612 1612 @command(b'perf::dirstatewrite|perfdirstatewrite', formatteropts)
1613 1613 def perfdirstatewrite(ui, repo, **opts):
1614 1614 """benchmap the time it take to write a dirstate on disk"""
1615 1615 opts = _byteskwargs(opts)
1616 1616 timer, fm = gettimer(ui, opts)
1617 1617 ds = repo.dirstate
1618 1618 b"a" in ds
1619 1619
1620 1620 def setup():
1621 1621 ds._dirty = True
1622 1622
1623 1623 def d():
1624 1624 ds.write(repo.currenttransaction())
1625 1625
1626 1626 with repo.wlock():
1627 1627 timer(d, setup=setup)
1628 1628 fm.end()
1629 1629
1630 1630
1631 1631 def _getmergerevs(repo, opts):
1632 1632 """parse command argument to return rev involved in merge
1633 1633
1634 1634 input: options dictionnary with `rev`, `from` and `bse`
1635 1635 output: (localctx, otherctx, basectx)
1636 1636 """
1637 1637 if opts[b'from']:
1638 1638 fromrev = scmutil.revsingle(repo, opts[b'from'])
1639 1639 wctx = repo[fromrev]
1640 1640 else:
1641 1641 wctx = repo[None]
1642 1642 # we don't want working dir files to be stat'd in the benchmark, so
1643 1643 # prime that cache
1644 1644 wctx.dirty()
1645 1645 rctx = scmutil.revsingle(repo, opts[b'rev'], opts[b'rev'])
1646 1646 if opts[b'base']:
1647 1647 fromrev = scmutil.revsingle(repo, opts[b'base'])
1648 1648 ancestor = repo[fromrev]
1649 1649 else:
1650 1650 ancestor = wctx.ancestor(rctx)
1651 1651 return (wctx, rctx, ancestor)
1652 1652
1653 1653
1654 1654 @command(
1655 1655 b'perf::mergecalculate|perfmergecalculate',
1656 1656 [
1657 1657 (b'r', b'rev', b'.', b'rev to merge against'),
1658 1658 (b'', b'from', b'', b'rev to merge from'),
1659 1659 (b'', b'base', b'', b'the revision to use as base'),
1660 1660 ]
1661 1661 + formatteropts,
1662 1662 )
1663 1663 def perfmergecalculate(ui, repo, **opts):
1664 1664 opts = _byteskwargs(opts)
1665 1665 timer, fm = gettimer(ui, opts)
1666 1666
1667 1667 wctx, rctx, ancestor = _getmergerevs(repo, opts)
1668 1668
1669 1669 def d():
1670 1670 # acceptremote is True because we don't want prompts in the middle of
1671 1671 # our benchmark
1672 1672 merge.calculateupdates(
1673 1673 repo,
1674 1674 wctx,
1675 1675 rctx,
1676 1676 [ancestor],
1677 1677 branchmerge=False,
1678 1678 force=False,
1679 1679 acceptremote=True,
1680 1680 followcopies=True,
1681 1681 )
1682 1682
1683 1683 timer(d)
1684 1684 fm.end()
1685 1685
1686 1686
1687 1687 @command(
1688 1688 b'perf::mergecopies|perfmergecopies',
1689 1689 [
1690 1690 (b'r', b'rev', b'.', b'rev to merge against'),
1691 1691 (b'', b'from', b'', b'rev to merge from'),
1692 1692 (b'', b'base', b'', b'the revision to use as base'),
1693 1693 ]
1694 1694 + formatteropts,
1695 1695 )
1696 1696 def perfmergecopies(ui, repo, **opts):
1697 1697 """measure runtime of `copies.mergecopies`"""
1698 1698 opts = _byteskwargs(opts)
1699 1699 timer, fm = gettimer(ui, opts)
1700 1700 wctx, rctx, ancestor = _getmergerevs(repo, opts)
1701 1701
1702 1702 def d():
1703 1703 # acceptremote is True because we don't want prompts in the middle of
1704 1704 # our benchmark
1705 1705 copies.mergecopies(repo, wctx, rctx, ancestor)
1706 1706
1707 1707 timer(d)
1708 1708 fm.end()
1709 1709
1710 1710
1711 1711 @command(b'perf::pathcopies|perfpathcopies', [], b"REV REV")
1712 1712 def perfpathcopies(ui, repo, rev1, rev2, **opts):
1713 1713 """benchmark the copy tracing logic"""
1714 1714 opts = _byteskwargs(opts)
1715 1715 timer, fm = gettimer(ui, opts)
1716 1716 ctx1 = scmutil.revsingle(repo, rev1, rev1)
1717 1717 ctx2 = scmutil.revsingle(repo, rev2, rev2)
1718 1718
1719 1719 def d():
1720 1720 copies.pathcopies(ctx1, ctx2)
1721 1721
1722 1722 timer(d)
1723 1723 fm.end()
1724 1724
1725 1725
1726 1726 @command(
1727 1727 b'perf::phases|perfphases',
1728 1728 [
1729 1729 (b'', b'full', False, b'include file reading time too'),
1730 1730 ]
1731 1731 + formatteropts,
1732 1732 b"",
1733 1733 )
1734 1734 def perfphases(ui, repo, **opts):
1735 1735 """benchmark phasesets computation"""
1736 1736 opts = _byteskwargs(opts)
1737 1737 timer, fm = gettimer(ui, opts)
1738 1738 _phases = repo._phasecache
1739 1739 full = opts.get(b'full')
1740 1740 tip_rev = repo.changelog.tiprev()
1741 1741
1742 1742 def d():
1743 1743 phases = _phases
1744 1744 if full:
1745 1745 clearfilecache(repo, b'_phasecache')
1746 1746 phases = repo._phasecache
1747 1747 phases.invalidate()
1748 1748 phases.phase(repo, tip_rev)
1749 1749
1750 1750 timer(d)
1751 1751 fm.end()
1752 1752
1753 1753
1754 1754 @command(b'perf::phasesremote|perfphasesremote', [], b"[DEST]")
1755 1755 def perfphasesremote(ui, repo, dest=None, **opts):
1756 1756 """benchmark time needed to analyse phases of the remote server"""
1757 1757 from mercurial.node import bin
1758 1758 from mercurial import (
1759 1759 exchange,
1760 1760 hg,
1761 1761 phases,
1762 1762 )
1763 1763
1764 1764 opts = _byteskwargs(opts)
1765 1765 timer, fm = gettimer(ui, opts)
1766 1766
1767 1767 path = ui.getpath(dest, default=(b'default-push', b'default'))
1768 1768 if not path:
1769 1769 raise error.Abort(
1770 1770 b'default repository not configured!',
1771 1771 hint=b"see 'hg help config.paths'",
1772 1772 )
1773 1773 if util.safehasattr(path, 'main_path'):
1774 1774 path = path.get_push_variant()
1775 1775 dest = path.loc
1776 1776 else:
1777 1777 dest = path.pushloc or path.loc
1778 1778 ui.statusnoi18n(b'analysing phase of %s\n' % util.hidepassword(dest))
1779 1779 other = hg.peer(repo, opts, dest)
1780 1780
1781 1781 # easier to perform discovery through the operation
1782 1782 op = exchange.pushoperation(repo, other)
1783 1783 exchange._pushdiscoverychangeset(op)
1784 1784
1785 1785 remotesubset = op.fallbackheads
1786 1786
1787 1787 with other.commandexecutor() as e:
1788 1788 remotephases = e.callcommand(
1789 1789 b'listkeys', {b'namespace': b'phases'}
1790 1790 ).result()
1791 1791 del other
1792 1792 publishing = remotephases.get(b'publishing', False)
1793 1793 if publishing:
1794 1794 ui.statusnoi18n(b'publishing: yes\n')
1795 1795 else:
1796 1796 ui.statusnoi18n(b'publishing: no\n')
1797 1797
1798 1798 has_node = getattr(repo.changelog.index, 'has_node', None)
1799 1799 if has_node is None:
1800 1800 has_node = repo.changelog.nodemap.__contains__
1801 1801 nonpublishroots = 0
1802 1802 for nhex, phase in remotephases.iteritems():
1803 1803 if nhex == b'publishing': # ignore data related to publish option
1804 1804 continue
1805 1805 node = bin(nhex)
1806 1806 if has_node(node) and int(phase):
1807 1807 nonpublishroots += 1
1808 1808 ui.statusnoi18n(b'number of roots: %d\n' % len(remotephases))
1809 1809 ui.statusnoi18n(b'number of known non public roots: %d\n' % nonpublishroots)
1810 1810
1811 1811 def d():
1812 1812 phases.remotephasessummary(repo, remotesubset, remotephases)
1813 1813
1814 1814 timer(d)
1815 1815 fm.end()
1816 1816
1817 1817
1818 1818 @command(
1819 1819 b'perf::manifest|perfmanifest',
1820 1820 [
1821 1821 (b'm', b'manifest-rev', False, b'Look up a manifest node revision'),
1822 1822 (b'', b'clear-disk', False, b'clear on-disk caches too'),
1823 1823 ]
1824 1824 + formatteropts,
1825 1825 b'REV|NODE',
1826 1826 )
1827 1827 def perfmanifest(ui, repo, rev, manifest_rev=False, clear_disk=False, **opts):
1828 1828 """benchmark the time to read a manifest from disk and return a usable
1829 1829 dict-like object
1830 1830
1831 1831 Manifest caches are cleared before retrieval."""
1832 1832 opts = _byteskwargs(opts)
1833 1833 timer, fm = gettimer(ui, opts)
1834 1834 if not manifest_rev:
1835 1835 ctx = scmutil.revsingle(repo, rev, rev)
1836 1836 t = ctx.manifestnode()
1837 1837 else:
1838 1838 from mercurial.node import bin
1839 1839
1840 1840 if len(rev) == 40:
1841 1841 t = bin(rev)
1842 1842 else:
1843 1843 try:
1844 1844 rev = int(rev)
1845 1845
1846 1846 if util.safehasattr(repo.manifestlog, b'getstorage'):
1847 1847 t = repo.manifestlog.getstorage(b'').node(rev)
1848 1848 else:
1849 1849 t = repo.manifestlog._revlog.lookup(rev)
1850 1850 except ValueError:
1851 1851 raise error.Abort(
1852 1852 b'manifest revision must be integer or full node'
1853 1853 )
1854 1854
1855 1855 def d():
1856 1856 repo.manifestlog.clearcaches(clear_persisted_data=clear_disk)
1857 1857 repo.manifestlog[t].read()
1858 1858
1859 1859 timer(d)
1860 1860 fm.end()
1861 1861
1862 1862
1863 1863 @command(b'perf::changeset|perfchangeset', formatteropts)
1864 1864 def perfchangeset(ui, repo, rev, **opts):
1865 1865 opts = _byteskwargs(opts)
1866 1866 timer, fm = gettimer(ui, opts)
1867 1867 n = scmutil.revsingle(repo, rev).node()
1868 1868
1869 1869 def d():
1870 1870 repo.changelog.read(n)
1871 1871 # repo.changelog._cache = None
1872 1872
1873 1873 timer(d)
1874 1874 fm.end()
1875 1875
1876 1876
1877 1877 @command(b'perf::ignore|perfignore', formatteropts)
1878 1878 def perfignore(ui, repo, **opts):
1879 1879 """benchmark operation related to computing ignore"""
1880 1880 opts = _byteskwargs(opts)
1881 1881 timer, fm = gettimer(ui, opts)
1882 1882 dirstate = repo.dirstate
1883 1883
1884 1884 def setupone():
1885 1885 dirstate.invalidate()
1886 1886 clearfilecache(dirstate, b'_ignore')
1887 1887
1888 1888 def runone():
1889 1889 dirstate._ignore
1890 1890
1891 1891 timer(runone, setup=setupone, title=b"load")
1892 1892 fm.end()
1893 1893
1894 1894
1895 1895 @command(
1896 1896 b'perf::index|perfindex',
1897 1897 [
1898 1898 (b'', b'rev', [], b'revision to be looked up (default tip)'),
1899 1899 (b'', b'no-lookup', None, b'do not revision lookup post creation'),
1900 1900 ]
1901 1901 + formatteropts,
1902 1902 )
1903 1903 def perfindex(ui, repo, **opts):
1904 1904 """benchmark index creation time followed by a lookup
1905 1905
1906 1906 The default is to look `tip` up. Depending on the index implementation,
1907 1907 the revision looked up can matters. For example, an implementation
1908 1908 scanning the index will have a faster lookup time for `--rev tip` than for
1909 1909 `--rev 0`. The number of looked up revisions and their order can also
1910 1910 matters.
1911 1911
1912 1912 Example of useful set to test:
1913 1913
1914 1914 * tip
1915 1915 * 0
1916 1916 * -10:
1917 1917 * :10
1918 1918 * -10: + :10
1919 1919 * :10: + -10:
1920 1920 * -10000:
1921 1921 * -10000: + 0
1922 1922
1923 1923 It is not currently possible to check for lookup of a missing node. For
1924 1924 deeper lookup benchmarking, checkout the `perfnodemap` command."""
1925 1925 import mercurial.revlog
1926 1926
1927 1927 opts = _byteskwargs(opts)
1928 1928 timer, fm = gettimer(ui, opts)
1929 1929 mercurial.revlog._prereadsize = 2 ** 24 # disable lazy parser in old hg
1930 1930 if opts[b'no_lookup']:
1931 1931 if opts['rev']:
1932 1932 raise error.Abort('--no-lookup and --rev are mutually exclusive')
1933 1933 nodes = []
1934 1934 elif not opts[b'rev']:
1935 1935 nodes = [repo[b"tip"].node()]
1936 1936 else:
1937 1937 revs = scmutil.revrange(repo, opts[b'rev'])
1938 1938 cl = repo.changelog
1939 1939 nodes = [cl.node(r) for r in revs]
1940 1940
1941 1941 unfi = repo.unfiltered()
1942 1942 # find the filecache func directly
1943 1943 # This avoid polluting the benchmark with the filecache logic
1944 1944 makecl = unfi.__class__.changelog.func
1945 1945
1946 1946 def setup():
1947 1947 # probably not necessary, but for good measure
1948 1948 clearchangelog(unfi)
1949 1949
1950 1950 def d():
1951 1951 cl = makecl(unfi)
1952 1952 for n in nodes:
1953 1953 cl.rev(n)
1954 1954
1955 1955 timer(d, setup=setup)
1956 1956 fm.end()
1957 1957
1958 1958
1959 1959 @command(
1960 1960 b'perf::nodemap|perfnodemap',
1961 1961 [
1962 1962 (b'', b'rev', [], b'revision to be looked up (default tip)'),
1963 1963 (b'', b'clear-caches', True, b'clear revlog cache between calls'),
1964 1964 ]
1965 1965 + formatteropts,
1966 1966 )
1967 1967 def perfnodemap(ui, repo, **opts):
1968 1968 """benchmark the time necessary to look up revision from a cold nodemap
1969 1969
1970 1970 Depending on the implementation, the amount and order of revision we look
1971 1971 up can varies. Example of useful set to test:
1972 1972 * tip
1973 1973 * 0
1974 1974 * -10:
1975 1975 * :10
1976 1976 * -10: + :10
1977 1977 * :10: + -10:
1978 1978 * -10000:
1979 1979 * -10000: + 0
1980 1980
1981 1981 The command currently focus on valid binary lookup. Benchmarking for
1982 1982 hexlookup, prefix lookup and missing lookup would also be valuable.
1983 1983 """
1984 1984 import mercurial.revlog
1985 1985
1986 1986 opts = _byteskwargs(opts)
1987 1987 timer, fm = gettimer(ui, opts)
1988 1988 mercurial.revlog._prereadsize = 2 ** 24 # disable lazy parser in old hg
1989 1989
1990 1990 unfi = repo.unfiltered()
1991 1991 clearcaches = opts[b'clear_caches']
1992 1992 # find the filecache func directly
1993 1993 # This avoid polluting the benchmark with the filecache logic
1994 1994 makecl = unfi.__class__.changelog.func
1995 1995 if not opts[b'rev']:
1996 1996 raise error.Abort(b'use --rev to specify revisions to look up')
1997 1997 revs = scmutil.revrange(repo, opts[b'rev'])
1998 1998 cl = repo.changelog
1999 1999 nodes = [cl.node(r) for r in revs]
2000 2000
2001 2001 # use a list to pass reference to a nodemap from one closure to the next
2002 2002 nodeget = [None]
2003 2003
2004 2004 def setnodeget():
2005 2005 # probably not necessary, but for good measure
2006 2006 clearchangelog(unfi)
2007 2007 cl = makecl(unfi)
2008 2008 if util.safehasattr(cl.index, 'get_rev'):
2009 2009 nodeget[0] = cl.index.get_rev
2010 2010 else:
2011 2011 nodeget[0] = cl.nodemap.get
2012 2012
2013 2013 def d():
2014 2014 get = nodeget[0]
2015 2015 for n in nodes:
2016 2016 get(n)
2017 2017
2018 2018 setup = None
2019 2019 if clearcaches:
2020 2020
2021 2021 def setup():
2022 2022 setnodeget()
2023 2023
2024 2024 else:
2025 2025 setnodeget()
2026 2026 d() # prewarm the data structure
2027 2027 timer(d, setup=setup)
2028 2028 fm.end()
2029 2029
2030 2030
2031 2031 @command(b'perf::startup|perfstartup', formatteropts)
2032 2032 def perfstartup(ui, repo, **opts):
2033 2033 opts = _byteskwargs(opts)
2034 2034 timer, fm = gettimer(ui, opts)
2035 2035
2036 2036 def d():
2037 2037 if os.name != 'nt':
2038 2038 os.system(
2039 2039 b"HGRCPATH= %s version -q > /dev/null" % fsencode(sys.argv[0])
2040 2040 )
2041 2041 else:
2042 2042 os.environ['HGRCPATH'] = r' '
2043 2043 os.system("%s version -q > NUL" % sys.argv[0])
2044 2044
2045 2045 timer(d)
2046 2046 fm.end()
2047 2047
2048 2048
2049 def _clear_store_audit_cache(repo):
2050 vfs = getsvfs(repo)
2051 # unwrap the fncache proxy
2052 if not hasattr(vfs, "audit"):
2053 vfs = getattr(vfs, "vfs", vfs)
2054 auditor = vfs.audit
2055 if hasattr(auditor, "clear_audit_cache"):
2056 auditor.clear_audit_cache()
2057 elif hasattr(auditor, "audited"):
2058 auditor.audited.clear()
2059 auditor.auditeddir.clear()
2060
2061
2049 2062 def _find_stream_generator(version):
2050 2063 """find the proper generator function for this stream version"""
2051 2064 import mercurial.streamclone
2052 2065
2053 2066 available = {}
2054 2067
2055 2068 # try to fetch a v1 generator
2056 2069 generatev1 = getattr(mercurial.streamclone, "generatev1", None)
2057 2070 if generatev1 is not None:
2058 2071
2059 2072 def generate(repo):
2060 2073 entries, bytes, data = generatev1(repo, None, None, True)
2061 2074 return data
2062 2075
2063 2076 available[b'v1'] = generatev1
2064 2077 # try to fetch a v2 generator
2065 2078 generatev2 = getattr(mercurial.streamclone, "generatev2", None)
2066 2079 if generatev2 is not None:
2067 2080
2068 2081 def generate(repo):
2069 2082 entries, bytes, data = generatev2(repo, None, None, True)
2070 2083 return data
2071 2084
2072 2085 available[b'v2'] = generate
2073 2086 # try to fetch a v3 generator
2074 2087 generatev3 = getattr(mercurial.streamclone, "generatev3", None)
2075 2088 if generatev3 is not None:
2076 2089
2077 2090 def generate(repo):
2078 2091 return generatev3(repo, None, None, True)
2079 2092
2080 2093 available[b'v3-exp'] = generate
2081 2094
2082 2095 # resolve the request
2083 2096 if version == b"latest":
2084 2097 # latest is the highest non experimental version
2085 2098 latest_key = max(v for v in available if b'-exp' not in v)
2086 2099 return available[latest_key]
2087 2100 elif version in available:
2088 2101 return available[version]
2089 2102 else:
2090 2103 msg = b"unkown or unavailable version: %s"
2091 2104 msg %= version
2092 2105 hint = b"available versions: %s"
2093 2106 hint %= b', '.join(sorted(available))
2094 2107 raise error.Abort(msg, hint=hint)
2095 2108
2096 2109
2097 2110 @command(
2098 2111 b'perf::stream-locked-section',
2099 2112 [
2100 2113 (
2101 2114 b'',
2102 2115 b'stream-version',
2103 2116 b'latest',
2104 2117 b'stream version to use ("v1", "v2", "v3-exp" '
2105 2118 b'or "latest", (the default))',
2106 2119 ),
2107 2120 ]
2108 2121 + formatteropts,
2109 2122 )
2110 2123 def perf_stream_clone_scan(ui, repo, stream_version, **opts):
2111 2124 """benchmark the initial, repo-locked, section of a stream-clone"""
2112 2125
2113 2126 opts = _byteskwargs(opts)
2114 2127 timer, fm = gettimer(ui, opts)
2115 2128
2116 2129 # deletion of the generator may trigger some cleanup that we do not want to
2117 2130 # measure
2118 2131 result_holder = [None]
2119 2132
2120 2133 def setupone():
2121 2134 result_holder[0] = None
2135 # This is important for the full generation, even if it does not
2136 # currently matters, it seems safer to also real it here.
2137 _clear_store_audit_cache(repo)
2122 2138
2123 2139 generate = _find_stream_generator(stream_version)
2124 2140
2125 2141 def runone():
2126 2142 # the lock is held for the duration the initialisation
2127 2143 result_holder[0] = generate(repo)
2128 2144
2129 2145 timer(runone, setup=setupone, title=b"load")
2130 2146 fm.end()
2131 2147
2132 2148
2133 2149 @command(
2134 2150 b'perf::stream-generate',
2135 2151 [
2136 2152 (
2137 2153 b'',
2138 2154 b'stream-version',
2139 2155 b'latest',
2140 2156 b'stream version to us ("v1", "v2", "v3-exp" '
2141 2157 b'or "latest", (the default))',
2142 2158 ),
2143 2159 ]
2144 2160 + formatteropts,
2145 2161 )
2146 2162 def perf_stream_clone_generate(ui, repo, stream_version, **opts):
2147 2163 """benchmark the full generation of a stream clone"""
2148 2164
2149 2165 opts = _byteskwargs(opts)
2150 2166 timer, fm = gettimer(ui, opts)
2151 2167
2152 2168 # deletion of the generator may trigger some cleanup that we do not want to
2153 2169 # measure
2154 2170
2155 2171 generate = _find_stream_generator(stream_version)
2156 2172
2173 def setup():
2174 _clear_store_audit_cache(repo)
2175
2157 2176 def runone():
2158 2177 # the lock is held for the duration the initialisation
2159 2178 for chunk in generate(repo):
2160 2179 pass
2161 2180
2162 timer(runone, title=b"generate")
2181 timer(runone, setup=setup, title=b"generate")
2163 2182 fm.end()
2164 2183
2165 2184
2166 2185 @command(
2167 2186 b'perf::stream-consume',
2168 2187 formatteropts,
2169 2188 )
2170 2189 def perf_stream_clone_consume(ui, repo, filename, **opts):
2171 2190 """benchmark the full application of a stream clone
2172 2191
2173 2192 This include the creation of the repository
2174 2193 """
2175 2194 # try except to appease check code
2176 2195 msg = b"mercurial too old, missing necessary module: %s"
2177 2196 try:
2178 2197 from mercurial import bundle2
2179 2198 except ImportError as exc:
2180 2199 msg %= _bytestr(exc)
2181 2200 raise error.Abort(msg)
2182 2201 try:
2183 2202 from mercurial import exchange
2184 2203 except ImportError as exc:
2185 2204 msg %= _bytestr(exc)
2186 2205 raise error.Abort(msg)
2187 2206 try:
2188 2207 from mercurial import hg
2189 2208 except ImportError as exc:
2190 2209 msg %= _bytestr(exc)
2191 2210 raise error.Abort(msg)
2192 2211 try:
2193 2212 from mercurial import localrepo
2194 2213 except ImportError as exc:
2195 2214 msg %= _bytestr(exc)
2196 2215 raise error.Abort(msg)
2197 2216
2198 2217 opts = _byteskwargs(opts)
2199 2218 timer, fm = gettimer(ui, opts)
2200 2219
2201 2220 # deletion of the generator may trigger some cleanup that we do not want to
2202 2221 # measure
2203 2222 if not (os.path.isfile(filename) and os.access(filename, os.R_OK)):
2204 2223 raise error.Abort("not a readable file: %s" % filename)
2205 2224
2206 2225 run_variables = [None, None]
2207 2226
2208 2227 # we create the new repository next to the other one for two reasons:
2209 2228 # - this way we use the same file system, which are relevant for benchmark
2210 2229 # - if /tmp/ is small, the operation could overfills it.
2211 2230 source_repo_dir = os.path.dirname(repo.root)
2212 2231
2213 2232 @contextlib.contextmanager
2214 2233 def context():
2215 2234 with open(filename, mode='rb') as bundle:
2216 2235 with tempfile.TemporaryDirectory(
2217 2236 prefix=b'hg-perf-stream-consume-',
2218 2237 dir=source_repo_dir,
2219 2238 ) as tmp_dir:
2220 2239 tmp_dir = fsencode(tmp_dir)
2221 2240 run_variables[0] = bundle
2222 2241 run_variables[1] = tmp_dir
2223 2242 yield
2224 2243 run_variables[0] = None
2225 2244 run_variables[1] = None
2226 2245
2227 2246 def runone():
2228 2247 bundle = run_variables[0]
2229 2248 tmp_dir = run_variables[1]
2230 2249
2231 2250 # we actually wants to copy all config to ensure the repo config is
2232 2251 # taken in account during the benchmark
2233 2252 new_ui = repo.ui.__class__(repo.ui)
2234 2253 # only pass ui when no srcrepo
2235 2254 localrepo.createrepository(
2236 2255 new_ui, tmp_dir, requirements=repo.requirements
2237 2256 )
2238 2257 target = hg.repository(new_ui, tmp_dir)
2239 2258 gen = exchange.readbundle(target.ui, bundle, bundle.name)
2240 2259 # stream v1
2241 2260 if util.safehasattr(gen, 'apply'):
2242 2261 gen.apply(target)
2243 2262 else:
2244 2263 with target.transaction(b"perf::stream-consume") as tr:
2245 2264 bundle2.applybundle(
2246 2265 target,
2247 2266 gen,
2248 2267 tr,
2249 2268 source=b'unbundle',
2250 2269 url=filename,
2251 2270 )
2252 2271
2253 2272 timer(runone, context=context, title=b"consume")
2254 2273 fm.end()
2255 2274
2256 2275
2257 2276 @command(b'perf::parents|perfparents', formatteropts)
2258 2277 def perfparents(ui, repo, **opts):
2259 2278 """benchmark the time necessary to fetch one changeset's parents.
2260 2279
2261 2280 The fetch is done using the `node identifier`, traversing all object layers
2262 2281 from the repository object. The first N revisions will be used for this
2263 2282 benchmark. N is controlled by the ``perf.parentscount`` config option
2264 2283 (default: 1000).
2265 2284 """
2266 2285 opts = _byteskwargs(opts)
2267 2286 timer, fm = gettimer(ui, opts)
2268 2287 # control the number of commits perfparents iterates over
2269 2288 # experimental config: perf.parentscount
2270 2289 count = getint(ui, b"perf", b"parentscount", 1000)
2271 2290 if len(repo.changelog) < count:
2272 2291 raise error.Abort(b"repo needs %d commits for this test" % count)
2273 2292 repo = repo.unfiltered()
2274 2293 nl = [repo.changelog.node(i) for i in _xrange(count)]
2275 2294
2276 2295 def d():
2277 2296 for n in nl:
2278 2297 repo.changelog.parents(n)
2279 2298
2280 2299 timer(d)
2281 2300 fm.end()
2282 2301
2283 2302
2284 2303 @command(b'perf::ctxfiles|perfctxfiles', formatteropts)
2285 2304 def perfctxfiles(ui, repo, x, **opts):
2286 2305 opts = _byteskwargs(opts)
2287 2306 x = int(x)
2288 2307 timer, fm = gettimer(ui, opts)
2289 2308
2290 2309 def d():
2291 2310 len(repo[x].files())
2292 2311
2293 2312 timer(d)
2294 2313 fm.end()
2295 2314
2296 2315
2297 2316 @command(b'perf::rawfiles|perfrawfiles', formatteropts)
2298 2317 def perfrawfiles(ui, repo, x, **opts):
2299 2318 opts = _byteskwargs(opts)
2300 2319 x = int(x)
2301 2320 timer, fm = gettimer(ui, opts)
2302 2321 cl = repo.changelog
2303 2322
2304 2323 def d():
2305 2324 len(cl.read(x)[3])
2306 2325
2307 2326 timer(d)
2308 2327 fm.end()
2309 2328
2310 2329
2311 2330 @command(b'perf::lookup|perflookup', formatteropts)
2312 2331 def perflookup(ui, repo, rev, **opts):
2313 2332 opts = _byteskwargs(opts)
2314 2333 timer, fm = gettimer(ui, opts)
2315 2334 timer(lambda: len(repo.lookup(rev)))
2316 2335 fm.end()
2317 2336
2318 2337
2319 2338 @command(
2320 2339 b'perf::linelogedits|perflinelogedits',
2321 2340 [
2322 2341 (b'n', b'edits', 10000, b'number of edits'),
2323 2342 (b'', b'max-hunk-lines', 10, b'max lines in a hunk'),
2324 2343 ],
2325 2344 norepo=True,
2326 2345 )
2327 2346 def perflinelogedits(ui, **opts):
2328 2347 from mercurial import linelog
2329 2348
2330 2349 opts = _byteskwargs(opts)
2331 2350
2332 2351 edits = opts[b'edits']
2333 2352 maxhunklines = opts[b'max_hunk_lines']
2334 2353
2335 2354 maxb1 = 100000
2336 2355 random.seed(0)
2337 2356 randint = random.randint
2338 2357 currentlines = 0
2339 2358 arglist = []
2340 2359 for rev in _xrange(edits):
2341 2360 a1 = randint(0, currentlines)
2342 2361 a2 = randint(a1, min(currentlines, a1 + maxhunklines))
2343 2362 b1 = randint(0, maxb1)
2344 2363 b2 = randint(b1, b1 + maxhunklines)
2345 2364 currentlines += (b2 - b1) - (a2 - a1)
2346 2365 arglist.append((rev, a1, a2, b1, b2))
2347 2366
2348 2367 def d():
2349 2368 ll = linelog.linelog()
2350 2369 for args in arglist:
2351 2370 ll.replacelines(*args)
2352 2371
2353 2372 timer, fm = gettimer(ui, opts)
2354 2373 timer(d)
2355 2374 fm.end()
2356 2375
2357 2376
2358 2377 @command(b'perf::revrange|perfrevrange', formatteropts)
2359 2378 def perfrevrange(ui, repo, *specs, **opts):
2360 2379 opts = _byteskwargs(opts)
2361 2380 timer, fm = gettimer(ui, opts)
2362 2381 revrange = scmutil.revrange
2363 2382 timer(lambda: len(revrange(repo, specs)))
2364 2383 fm.end()
2365 2384
2366 2385
2367 2386 @command(b'perf::nodelookup|perfnodelookup', formatteropts)
2368 2387 def perfnodelookup(ui, repo, rev, **opts):
2369 2388 opts = _byteskwargs(opts)
2370 2389 timer, fm = gettimer(ui, opts)
2371 2390 import mercurial.revlog
2372 2391
2373 2392 mercurial.revlog._prereadsize = 2 ** 24 # disable lazy parser in old hg
2374 2393 n = scmutil.revsingle(repo, rev).node()
2375 2394
2376 2395 try:
2377 2396 cl = revlog(getsvfs(repo), radix=b"00changelog")
2378 2397 except TypeError:
2379 2398 cl = revlog(getsvfs(repo), indexfile=b"00changelog.i")
2380 2399
2381 2400 def d():
2382 2401 cl.rev(n)
2383 2402 clearcaches(cl)
2384 2403
2385 2404 timer(d)
2386 2405 fm.end()
2387 2406
2388 2407
2389 2408 @command(
2390 2409 b'perf::log|perflog',
2391 2410 [(b'', b'rename', False, b'ask log to follow renames')] + formatteropts,
2392 2411 )
2393 2412 def perflog(ui, repo, rev=None, **opts):
2394 2413 opts = _byteskwargs(opts)
2395 2414 if rev is None:
2396 2415 rev = []
2397 2416 timer, fm = gettimer(ui, opts)
2398 2417 ui.pushbuffer()
2399 2418 timer(
2400 2419 lambda: commands.log(
2401 2420 ui, repo, rev=rev, date=b'', user=b'', copies=opts.get(b'rename')
2402 2421 )
2403 2422 )
2404 2423 ui.popbuffer()
2405 2424 fm.end()
2406 2425
2407 2426
2408 2427 @command(b'perf::moonwalk|perfmoonwalk', formatteropts)
2409 2428 def perfmoonwalk(ui, repo, **opts):
2410 2429 """benchmark walking the changelog backwards
2411 2430
2412 2431 This also loads the changelog data for each revision in the changelog.
2413 2432 """
2414 2433 opts = _byteskwargs(opts)
2415 2434 timer, fm = gettimer(ui, opts)
2416 2435
2417 2436 def moonwalk():
2418 2437 for i in repo.changelog.revs(start=(len(repo) - 1), stop=-1):
2419 2438 ctx = repo[i]
2420 2439 ctx.branch() # read changelog data (in addition to the index)
2421 2440
2422 2441 timer(moonwalk)
2423 2442 fm.end()
2424 2443
2425 2444
2426 2445 @command(
2427 2446 b'perf::templating|perftemplating',
2428 2447 [
2429 2448 (b'r', b'rev', [], b'revisions to run the template on'),
2430 2449 ]
2431 2450 + formatteropts,
2432 2451 )
2433 2452 def perftemplating(ui, repo, testedtemplate=None, **opts):
2434 2453 """test the rendering time of a given template"""
2435 2454 if makelogtemplater is None:
2436 2455 raise error.Abort(
2437 2456 b"perftemplating not available with this Mercurial",
2438 2457 hint=b"use 4.3 or later",
2439 2458 )
2440 2459
2441 2460 opts = _byteskwargs(opts)
2442 2461
2443 2462 nullui = ui.copy()
2444 2463 nullui.fout = open(os.devnull, 'wb')
2445 2464 nullui.disablepager()
2446 2465 revs = opts.get(b'rev')
2447 2466 if not revs:
2448 2467 revs = [b'all()']
2449 2468 revs = list(scmutil.revrange(repo, revs))
2450 2469
2451 2470 defaulttemplate = (
2452 2471 b'{date|shortdate} [{rev}:{node|short}]'
2453 2472 b' {author|person}: {desc|firstline}\n'
2454 2473 )
2455 2474 if testedtemplate is None:
2456 2475 testedtemplate = defaulttemplate
2457 2476 displayer = makelogtemplater(nullui, repo, testedtemplate)
2458 2477
2459 2478 def format():
2460 2479 for r in revs:
2461 2480 ctx = repo[r]
2462 2481 displayer.show(ctx)
2463 2482 displayer.flush(ctx)
2464 2483
2465 2484 timer, fm = gettimer(ui, opts)
2466 2485 timer(format)
2467 2486 fm.end()
2468 2487
2469 2488
2470 2489 def _displaystats(ui, opts, entries, data):
2471 2490 # use a second formatter because the data are quite different, not sure
2472 2491 # how it flies with the templater.
2473 2492 fm = ui.formatter(b'perf-stats', opts)
2474 2493 for key, title in entries:
2475 2494 values = data[key]
2476 2495 nbvalues = len(data)
2477 2496 values.sort()
2478 2497 stats = {
2479 2498 'key': key,
2480 2499 'title': title,
2481 2500 'nbitems': len(values),
2482 2501 'min': values[0][0],
2483 2502 '10%': values[(nbvalues * 10) // 100][0],
2484 2503 '25%': values[(nbvalues * 25) // 100][0],
2485 2504 '50%': values[(nbvalues * 50) // 100][0],
2486 2505 '75%': values[(nbvalues * 75) // 100][0],
2487 2506 '80%': values[(nbvalues * 80) // 100][0],
2488 2507 '85%': values[(nbvalues * 85) // 100][0],
2489 2508 '90%': values[(nbvalues * 90) // 100][0],
2490 2509 '95%': values[(nbvalues * 95) // 100][0],
2491 2510 '99%': values[(nbvalues * 99) // 100][0],
2492 2511 'max': values[-1][0],
2493 2512 }
2494 2513 fm.startitem()
2495 2514 fm.data(**stats)
2496 2515 # make node pretty for the human output
2497 2516 fm.plain('### %s (%d items)\n' % (title, len(values)))
2498 2517 lines = [
2499 2518 'min',
2500 2519 '10%',
2501 2520 '25%',
2502 2521 '50%',
2503 2522 '75%',
2504 2523 '80%',
2505 2524 '85%',
2506 2525 '90%',
2507 2526 '95%',
2508 2527 '99%',
2509 2528 'max',
2510 2529 ]
2511 2530 for l in lines:
2512 2531 fm.plain('%s: %s\n' % (l, stats[l]))
2513 2532 fm.end()
2514 2533
2515 2534
2516 2535 @command(
2517 2536 b'perf::helper-mergecopies|perfhelper-mergecopies',
2518 2537 formatteropts
2519 2538 + [
2520 2539 (b'r', b'revs', [], b'restrict search to these revisions'),
2521 2540 (b'', b'timing', False, b'provides extra data (costly)'),
2522 2541 (b'', b'stats', False, b'provides statistic about the measured data'),
2523 2542 ],
2524 2543 )
2525 2544 def perfhelpermergecopies(ui, repo, revs=[], **opts):
2526 2545 """find statistics about potential parameters for `perfmergecopies`
2527 2546
2528 2547 This command find (base, p1, p2) triplet relevant for copytracing
2529 2548 benchmarking in the context of a merge. It reports values for some of the
2530 2549 parameters that impact merge copy tracing time during merge.
2531 2550
2532 2551 If `--timing` is set, rename detection is run and the associated timing
2533 2552 will be reported. The extra details come at the cost of slower command
2534 2553 execution.
2535 2554
2536 2555 Since rename detection is only run once, other factors might easily
2537 2556 affect the precision of the timing. However it should give a good
2538 2557 approximation of which revision triplets are very costly.
2539 2558 """
2540 2559 opts = _byteskwargs(opts)
2541 2560 fm = ui.formatter(b'perf', opts)
2542 2561 dotiming = opts[b'timing']
2543 2562 dostats = opts[b'stats']
2544 2563
2545 2564 output_template = [
2546 2565 ("base", "%(base)12s"),
2547 2566 ("p1", "%(p1.node)12s"),
2548 2567 ("p2", "%(p2.node)12s"),
2549 2568 ("p1.nb-revs", "%(p1.nbrevs)12d"),
2550 2569 ("p1.nb-files", "%(p1.nbmissingfiles)12d"),
2551 2570 ("p1.renames", "%(p1.renamedfiles)12d"),
2552 2571 ("p1.time", "%(p1.time)12.3f"),
2553 2572 ("p2.nb-revs", "%(p2.nbrevs)12d"),
2554 2573 ("p2.nb-files", "%(p2.nbmissingfiles)12d"),
2555 2574 ("p2.renames", "%(p2.renamedfiles)12d"),
2556 2575 ("p2.time", "%(p2.time)12.3f"),
2557 2576 ("renames", "%(nbrenamedfiles)12d"),
2558 2577 ("total.time", "%(time)12.3f"),
2559 2578 ]
2560 2579 if not dotiming:
2561 2580 output_template = [
2562 2581 i
2563 2582 for i in output_template
2564 2583 if not ('time' in i[0] or 'renames' in i[0])
2565 2584 ]
2566 2585 header_names = [h for (h, v) in output_template]
2567 2586 output = ' '.join([v for (h, v) in output_template]) + '\n'
2568 2587 header = ' '.join(['%12s'] * len(header_names)) + '\n'
2569 2588 fm.plain(header % tuple(header_names))
2570 2589
2571 2590 if not revs:
2572 2591 revs = ['all()']
2573 2592 revs = scmutil.revrange(repo, revs)
2574 2593
2575 2594 if dostats:
2576 2595 alldata = {
2577 2596 'nbrevs': [],
2578 2597 'nbmissingfiles': [],
2579 2598 }
2580 2599 if dotiming:
2581 2600 alldata['parentnbrenames'] = []
2582 2601 alldata['totalnbrenames'] = []
2583 2602 alldata['parenttime'] = []
2584 2603 alldata['totaltime'] = []
2585 2604
2586 2605 roi = repo.revs('merge() and %ld', revs)
2587 2606 for r in roi:
2588 2607 ctx = repo[r]
2589 2608 p1 = ctx.p1()
2590 2609 p2 = ctx.p2()
2591 2610 bases = repo.changelog._commonancestorsheads(p1.rev(), p2.rev())
2592 2611 for b in bases:
2593 2612 b = repo[b]
2594 2613 p1missing = copies._computeforwardmissing(b, p1)
2595 2614 p2missing = copies._computeforwardmissing(b, p2)
2596 2615 data = {
2597 2616 b'base': b.hex(),
2598 2617 b'p1.node': p1.hex(),
2599 2618 b'p1.nbrevs': len(repo.revs('only(%d, %d)', p1.rev(), b.rev())),
2600 2619 b'p1.nbmissingfiles': len(p1missing),
2601 2620 b'p2.node': p2.hex(),
2602 2621 b'p2.nbrevs': len(repo.revs('only(%d, %d)', p2.rev(), b.rev())),
2603 2622 b'p2.nbmissingfiles': len(p2missing),
2604 2623 }
2605 2624 if dostats:
2606 2625 if p1missing:
2607 2626 alldata['nbrevs'].append(
2608 2627 (data['p1.nbrevs'], b.hex(), p1.hex())
2609 2628 )
2610 2629 alldata['nbmissingfiles'].append(
2611 2630 (data['p1.nbmissingfiles'], b.hex(), p1.hex())
2612 2631 )
2613 2632 if p2missing:
2614 2633 alldata['nbrevs'].append(
2615 2634 (data['p2.nbrevs'], b.hex(), p2.hex())
2616 2635 )
2617 2636 alldata['nbmissingfiles'].append(
2618 2637 (data['p2.nbmissingfiles'], b.hex(), p2.hex())
2619 2638 )
2620 2639 if dotiming:
2621 2640 begin = util.timer()
2622 2641 mergedata = copies.mergecopies(repo, p1, p2, b)
2623 2642 end = util.timer()
2624 2643 # not very stable timing since we did only one run
2625 2644 data['time'] = end - begin
2626 2645 # mergedata contains five dicts: "copy", "movewithdir",
2627 2646 # "diverge", "renamedelete" and "dirmove".
2628 2647 # The first 4 are about renamed file so lets count that.
2629 2648 renames = len(mergedata[0])
2630 2649 renames += len(mergedata[1])
2631 2650 renames += len(mergedata[2])
2632 2651 renames += len(mergedata[3])
2633 2652 data['nbrenamedfiles'] = renames
2634 2653 begin = util.timer()
2635 2654 p1renames = copies.pathcopies(b, p1)
2636 2655 end = util.timer()
2637 2656 data['p1.time'] = end - begin
2638 2657 begin = util.timer()
2639 2658 p2renames = copies.pathcopies(b, p2)
2640 2659 end = util.timer()
2641 2660 data['p2.time'] = end - begin
2642 2661 data['p1.renamedfiles'] = len(p1renames)
2643 2662 data['p2.renamedfiles'] = len(p2renames)
2644 2663
2645 2664 if dostats:
2646 2665 if p1missing:
2647 2666 alldata['parentnbrenames'].append(
2648 2667 (data['p1.renamedfiles'], b.hex(), p1.hex())
2649 2668 )
2650 2669 alldata['parenttime'].append(
2651 2670 (data['p1.time'], b.hex(), p1.hex())
2652 2671 )
2653 2672 if p2missing:
2654 2673 alldata['parentnbrenames'].append(
2655 2674 (data['p2.renamedfiles'], b.hex(), p2.hex())
2656 2675 )
2657 2676 alldata['parenttime'].append(
2658 2677 (data['p2.time'], b.hex(), p2.hex())
2659 2678 )
2660 2679 if p1missing or p2missing:
2661 2680 alldata['totalnbrenames'].append(
2662 2681 (
2663 2682 data['nbrenamedfiles'],
2664 2683 b.hex(),
2665 2684 p1.hex(),
2666 2685 p2.hex(),
2667 2686 )
2668 2687 )
2669 2688 alldata['totaltime'].append(
2670 2689 (data['time'], b.hex(), p1.hex(), p2.hex())
2671 2690 )
2672 2691 fm.startitem()
2673 2692 fm.data(**data)
2674 2693 # make node pretty for the human output
2675 2694 out = data.copy()
2676 2695 out['base'] = fm.hexfunc(b.node())
2677 2696 out['p1.node'] = fm.hexfunc(p1.node())
2678 2697 out['p2.node'] = fm.hexfunc(p2.node())
2679 2698 fm.plain(output % out)
2680 2699
2681 2700 fm.end()
2682 2701 if dostats:
2683 2702 # use a second formatter because the data are quite different, not sure
2684 2703 # how it flies with the templater.
2685 2704 entries = [
2686 2705 ('nbrevs', 'number of revision covered'),
2687 2706 ('nbmissingfiles', 'number of missing files at head'),
2688 2707 ]
2689 2708 if dotiming:
2690 2709 entries.append(
2691 2710 ('parentnbrenames', 'rename from one parent to base')
2692 2711 )
2693 2712 entries.append(('totalnbrenames', 'total number of renames'))
2694 2713 entries.append(('parenttime', 'time for one parent'))
2695 2714 entries.append(('totaltime', 'time for both parents'))
2696 2715 _displaystats(ui, opts, entries, alldata)
2697 2716
2698 2717
2699 2718 @command(
2700 2719 b'perf::helper-pathcopies|perfhelper-pathcopies',
2701 2720 formatteropts
2702 2721 + [
2703 2722 (b'r', b'revs', [], b'restrict search to these revisions'),
2704 2723 (b'', b'timing', False, b'provides extra data (costly)'),
2705 2724 (b'', b'stats', False, b'provides statistic about the measured data'),
2706 2725 ],
2707 2726 )
2708 2727 def perfhelperpathcopies(ui, repo, revs=[], **opts):
2709 2728 """find statistic about potential parameters for the `perftracecopies`
2710 2729
2711 2730 This command find source-destination pair relevant for copytracing testing.
2712 2731 It report value for some of the parameters that impact copy tracing time.
2713 2732
2714 2733 If `--timing` is set, rename detection is run and the associated timing
2715 2734 will be reported. The extra details comes at the cost of a slower command
2716 2735 execution.
2717 2736
2718 2737 Since the rename detection is only run once, other factors might easily
2719 2738 affect the precision of the timing. However it should give a good
2720 2739 approximation of which revision pairs are very costly.
2721 2740 """
2722 2741 opts = _byteskwargs(opts)
2723 2742 fm = ui.formatter(b'perf', opts)
2724 2743 dotiming = opts[b'timing']
2725 2744 dostats = opts[b'stats']
2726 2745
2727 2746 if dotiming:
2728 2747 header = '%12s %12s %12s %12s %12s %12s\n'
2729 2748 output = (
2730 2749 "%(source)12s %(destination)12s "
2731 2750 "%(nbrevs)12d %(nbmissingfiles)12d "
2732 2751 "%(nbrenamedfiles)12d %(time)18.5f\n"
2733 2752 )
2734 2753 header_names = (
2735 2754 "source",
2736 2755 "destination",
2737 2756 "nb-revs",
2738 2757 "nb-files",
2739 2758 "nb-renames",
2740 2759 "time",
2741 2760 )
2742 2761 fm.plain(header % header_names)
2743 2762 else:
2744 2763 header = '%12s %12s %12s %12s\n'
2745 2764 output = (
2746 2765 "%(source)12s %(destination)12s "
2747 2766 "%(nbrevs)12d %(nbmissingfiles)12d\n"
2748 2767 )
2749 2768 fm.plain(header % ("source", "destination", "nb-revs", "nb-files"))
2750 2769
2751 2770 if not revs:
2752 2771 revs = ['all()']
2753 2772 revs = scmutil.revrange(repo, revs)
2754 2773
2755 2774 if dostats:
2756 2775 alldata = {
2757 2776 'nbrevs': [],
2758 2777 'nbmissingfiles': [],
2759 2778 }
2760 2779 if dotiming:
2761 2780 alldata['nbrenames'] = []
2762 2781 alldata['time'] = []
2763 2782
2764 2783 roi = repo.revs('merge() and %ld', revs)
2765 2784 for r in roi:
2766 2785 ctx = repo[r]
2767 2786 p1 = ctx.p1().rev()
2768 2787 p2 = ctx.p2().rev()
2769 2788 bases = repo.changelog._commonancestorsheads(p1, p2)
2770 2789 for p in (p1, p2):
2771 2790 for b in bases:
2772 2791 base = repo[b]
2773 2792 parent = repo[p]
2774 2793 missing = copies._computeforwardmissing(base, parent)
2775 2794 if not missing:
2776 2795 continue
2777 2796 data = {
2778 2797 b'source': base.hex(),
2779 2798 b'destination': parent.hex(),
2780 2799 b'nbrevs': len(repo.revs('only(%d, %d)', p, b)),
2781 2800 b'nbmissingfiles': len(missing),
2782 2801 }
2783 2802 if dostats:
2784 2803 alldata['nbrevs'].append(
2785 2804 (
2786 2805 data['nbrevs'],
2787 2806 base.hex(),
2788 2807 parent.hex(),
2789 2808 )
2790 2809 )
2791 2810 alldata['nbmissingfiles'].append(
2792 2811 (
2793 2812 data['nbmissingfiles'],
2794 2813 base.hex(),
2795 2814 parent.hex(),
2796 2815 )
2797 2816 )
2798 2817 if dotiming:
2799 2818 begin = util.timer()
2800 2819 renames = copies.pathcopies(base, parent)
2801 2820 end = util.timer()
2802 2821 # not very stable timing since we did only one run
2803 2822 data['time'] = end - begin
2804 2823 data['nbrenamedfiles'] = len(renames)
2805 2824 if dostats:
2806 2825 alldata['time'].append(
2807 2826 (
2808 2827 data['time'],
2809 2828 base.hex(),
2810 2829 parent.hex(),
2811 2830 )
2812 2831 )
2813 2832 alldata['nbrenames'].append(
2814 2833 (
2815 2834 data['nbrenamedfiles'],
2816 2835 base.hex(),
2817 2836 parent.hex(),
2818 2837 )
2819 2838 )
2820 2839 fm.startitem()
2821 2840 fm.data(**data)
2822 2841 out = data.copy()
2823 2842 out['source'] = fm.hexfunc(base.node())
2824 2843 out['destination'] = fm.hexfunc(parent.node())
2825 2844 fm.plain(output % out)
2826 2845
2827 2846 fm.end()
2828 2847 if dostats:
2829 2848 entries = [
2830 2849 ('nbrevs', 'number of revision covered'),
2831 2850 ('nbmissingfiles', 'number of missing files at head'),
2832 2851 ]
2833 2852 if dotiming:
2834 2853 entries.append(('nbrenames', 'renamed files'))
2835 2854 entries.append(('time', 'time'))
2836 2855 _displaystats(ui, opts, entries, alldata)
2837 2856
2838 2857
2839 2858 @command(b'perf::cca|perfcca', formatteropts)
2840 2859 def perfcca(ui, repo, **opts):
2841 2860 opts = _byteskwargs(opts)
2842 2861 timer, fm = gettimer(ui, opts)
2843 2862 timer(lambda: scmutil.casecollisionauditor(ui, False, repo.dirstate))
2844 2863 fm.end()
2845 2864
2846 2865
2847 2866 @command(b'perf::fncacheload|perffncacheload', formatteropts)
2848 2867 def perffncacheload(ui, repo, **opts):
2849 2868 opts = _byteskwargs(opts)
2850 2869 timer, fm = gettimer(ui, opts)
2851 2870 s = repo.store
2852 2871
2853 2872 def d():
2854 2873 s.fncache._load()
2855 2874
2856 2875 timer(d)
2857 2876 fm.end()
2858 2877
2859 2878
2860 2879 @command(b'perf::fncachewrite|perffncachewrite', formatteropts)
2861 2880 def perffncachewrite(ui, repo, **opts):
2862 2881 opts = _byteskwargs(opts)
2863 2882 timer, fm = gettimer(ui, opts)
2864 2883 s = repo.store
2865 2884 lock = repo.lock()
2866 2885 s.fncache._load()
2867 2886 tr = repo.transaction(b'perffncachewrite')
2868 2887 tr.addbackup(b'fncache')
2869 2888
2870 2889 def d():
2871 2890 s.fncache._dirty = True
2872 2891 s.fncache.write(tr)
2873 2892
2874 2893 timer(d)
2875 2894 tr.close()
2876 2895 lock.release()
2877 2896 fm.end()
2878 2897
2879 2898
2880 2899 @command(b'perf::fncacheencode|perffncacheencode', formatteropts)
2881 2900 def perffncacheencode(ui, repo, **opts):
2882 2901 opts = _byteskwargs(opts)
2883 2902 timer, fm = gettimer(ui, opts)
2884 2903 s = repo.store
2885 2904 s.fncache._load()
2886 2905
2887 2906 def d():
2888 2907 for p in s.fncache.entries:
2889 2908 s.encode(p)
2890 2909
2891 2910 timer(d)
2892 2911 fm.end()
2893 2912
2894 2913
2895 2914 def _bdiffworker(q, blocks, xdiff, ready, done):
2896 2915 while not done.is_set():
2897 2916 pair = q.get()
2898 2917 while pair is not None:
2899 2918 if xdiff:
2900 2919 mdiff.bdiff.xdiffblocks(*pair)
2901 2920 elif blocks:
2902 2921 mdiff.bdiff.blocks(*pair)
2903 2922 else:
2904 2923 mdiff.textdiff(*pair)
2905 2924 q.task_done()
2906 2925 pair = q.get()
2907 2926 q.task_done() # for the None one
2908 2927 with ready:
2909 2928 ready.wait()
2910 2929
2911 2930
2912 2931 def _manifestrevision(repo, mnode):
2913 2932 ml = repo.manifestlog
2914 2933
2915 2934 if util.safehasattr(ml, b'getstorage'):
2916 2935 store = ml.getstorage(b'')
2917 2936 else:
2918 2937 store = ml._revlog
2919 2938
2920 2939 return store.revision(mnode)
2921 2940
2922 2941
2923 2942 @command(
2924 2943 b'perf::bdiff|perfbdiff',
2925 2944 revlogopts
2926 2945 + formatteropts
2927 2946 + [
2928 2947 (
2929 2948 b'',
2930 2949 b'count',
2931 2950 1,
2932 2951 b'number of revisions to test (when using --startrev)',
2933 2952 ),
2934 2953 (b'', b'alldata', False, b'test bdiffs for all associated revisions'),
2935 2954 (b'', b'threads', 0, b'number of thread to use (disable with 0)'),
2936 2955 (b'', b'blocks', False, b'test computing diffs into blocks'),
2937 2956 (b'', b'xdiff', False, b'use xdiff algorithm'),
2938 2957 ],
2939 2958 b'-c|-m|FILE REV',
2940 2959 )
2941 2960 def perfbdiff(ui, repo, file_, rev=None, count=None, threads=0, **opts):
2942 2961 """benchmark a bdiff between revisions
2943 2962
2944 2963 By default, benchmark a bdiff between its delta parent and itself.
2945 2964
2946 2965 With ``--count``, benchmark bdiffs between delta parents and self for N
2947 2966 revisions starting at the specified revision.
2948 2967
2949 2968 With ``--alldata``, assume the requested revision is a changeset and
2950 2969 measure bdiffs for all changes related to that changeset (manifest
2951 2970 and filelogs).
2952 2971 """
2953 2972 opts = _byteskwargs(opts)
2954 2973
2955 2974 if opts[b'xdiff'] and not opts[b'blocks']:
2956 2975 raise error.CommandError(b'perfbdiff', b'--xdiff requires --blocks')
2957 2976
2958 2977 if opts[b'alldata']:
2959 2978 opts[b'changelog'] = True
2960 2979
2961 2980 if opts.get(b'changelog') or opts.get(b'manifest'):
2962 2981 file_, rev = None, file_
2963 2982 elif rev is None:
2964 2983 raise error.CommandError(b'perfbdiff', b'invalid arguments')
2965 2984
2966 2985 blocks = opts[b'blocks']
2967 2986 xdiff = opts[b'xdiff']
2968 2987 textpairs = []
2969 2988
2970 2989 r = cmdutil.openrevlog(repo, b'perfbdiff', file_, opts)
2971 2990
2972 2991 startrev = r.rev(r.lookup(rev))
2973 2992 for rev in range(startrev, min(startrev + count, len(r) - 1)):
2974 2993 if opts[b'alldata']:
2975 2994 # Load revisions associated with changeset.
2976 2995 ctx = repo[rev]
2977 2996 mtext = _manifestrevision(repo, ctx.manifestnode())
2978 2997 for pctx in ctx.parents():
2979 2998 pman = _manifestrevision(repo, pctx.manifestnode())
2980 2999 textpairs.append((pman, mtext))
2981 3000
2982 3001 # Load filelog revisions by iterating manifest delta.
2983 3002 man = ctx.manifest()
2984 3003 pman = ctx.p1().manifest()
2985 3004 for filename, change in pman.diff(man).items():
2986 3005 fctx = repo.file(filename)
2987 3006 f1 = fctx.revision(change[0][0] or -1)
2988 3007 f2 = fctx.revision(change[1][0] or -1)
2989 3008 textpairs.append((f1, f2))
2990 3009 else:
2991 3010 dp = r.deltaparent(rev)
2992 3011 textpairs.append((r.revision(dp), r.revision(rev)))
2993 3012
2994 3013 withthreads = threads > 0
2995 3014 if not withthreads:
2996 3015
2997 3016 def d():
2998 3017 for pair in textpairs:
2999 3018 if xdiff:
3000 3019 mdiff.bdiff.xdiffblocks(*pair)
3001 3020 elif blocks:
3002 3021 mdiff.bdiff.blocks(*pair)
3003 3022 else:
3004 3023 mdiff.textdiff(*pair)
3005 3024
3006 3025 else:
3007 3026 q = queue()
3008 3027 for i in _xrange(threads):
3009 3028 q.put(None)
3010 3029 ready = threading.Condition()
3011 3030 done = threading.Event()
3012 3031 for i in _xrange(threads):
3013 3032 threading.Thread(
3014 3033 target=_bdiffworker, args=(q, blocks, xdiff, ready, done)
3015 3034 ).start()
3016 3035 q.join()
3017 3036
3018 3037 def d():
3019 3038 for pair in textpairs:
3020 3039 q.put(pair)
3021 3040 for i in _xrange(threads):
3022 3041 q.put(None)
3023 3042 with ready:
3024 3043 ready.notify_all()
3025 3044 q.join()
3026 3045
3027 3046 timer, fm = gettimer(ui, opts)
3028 3047 timer(d)
3029 3048 fm.end()
3030 3049
3031 3050 if withthreads:
3032 3051 done.set()
3033 3052 for i in _xrange(threads):
3034 3053 q.put(None)
3035 3054 with ready:
3036 3055 ready.notify_all()
3037 3056
3038 3057
3039 3058 @command(
3040 3059 b'perf::unbundle',
3041 3060 [
3042 3061 (b'', b'as-push', None, b'pretend the bundle comes from a push'),
3043 3062 ]
3044 3063 + formatteropts,
3045 3064 b'BUNDLE_FILE',
3046 3065 )
3047 3066 def perf_unbundle(ui, repo, fname, **opts):
3048 3067 """benchmark application of a bundle in a repository.
3049 3068
3050 3069 This does not include the final transaction processing
3051 3070
3052 3071 The --as-push option make the unbundle operation appears like it comes from
3053 3072 a client push. It change some aspect of the processing and associated
3054 3073 performance profile.
3055 3074 """
3056 3075
3057 3076 from mercurial import exchange
3058 3077 from mercurial import bundle2
3059 3078 from mercurial import transaction
3060 3079
3061 3080 opts = _byteskwargs(opts)
3062 3081
3063 3082 ### some compatibility hotfix
3064 3083 #
3065 3084 # the data attribute is dropped in 63edc384d3b7 a changeset introducing a
3066 3085 # critical regression that break transaction rollback for files that are
3067 3086 # de-inlined.
3068 3087 method = transaction.transaction._addentry
3069 3088 pre_63edc384d3b7 = "data" in getargspec(method).args
3070 3089 # the `detailed_exit_code` attribute is introduced in 33c0c25d0b0f
3071 3090 # a changeset that is a close descendant of 18415fc918a1, the changeset
3072 3091 # that conclude the fix run for the bug introduced in 63edc384d3b7.
3073 3092 args = getargspec(error.Abort.__init__).args
3074 3093 post_18415fc918a1 = "detailed_exit_code" in args
3075 3094
3076 3095 unbundle_source = b'perf::unbundle'
3077 3096 if opts[b'as_push']:
3078 3097 unbundle_source = b'push'
3079 3098
3080 3099 old_max_inline = None
3081 3100 try:
3082 3101 if not (pre_63edc384d3b7 or post_18415fc918a1):
3083 3102 # disable inlining
3084 3103 old_max_inline = mercurial.revlog._maxinline
3085 3104 # large enough to never happen
3086 3105 mercurial.revlog._maxinline = 2 ** 50
3087 3106
3088 3107 with repo.lock():
3089 3108 bundle = [None, None]
3090 3109 orig_quiet = repo.ui.quiet
3091 3110 try:
3092 3111 repo.ui.quiet = True
3093 3112 with open(fname, mode="rb") as f:
3094 3113
3095 3114 def noop_report(*args, **kwargs):
3096 3115 pass
3097 3116
3098 3117 def setup():
3099 3118 gen, tr = bundle
3100 3119 if tr is not None:
3101 3120 tr.abort()
3102 3121 bundle[:] = [None, None]
3103 3122 f.seek(0)
3104 3123 bundle[0] = exchange.readbundle(ui, f, fname)
3105 3124 bundle[1] = repo.transaction(b'perf::unbundle')
3106 3125 # silence the transaction
3107 3126 bundle[1]._report = noop_report
3108 3127
3109 3128 def apply():
3110 3129 gen, tr = bundle
3111 3130 bundle2.applybundle(
3112 3131 repo,
3113 3132 gen,
3114 3133 tr,
3115 3134 source=unbundle_source,
3116 3135 url=fname,
3117 3136 )
3118 3137
3119 3138 timer, fm = gettimer(ui, opts)
3120 3139 timer(apply, setup=setup)
3121 3140 fm.end()
3122 3141 finally:
3123 3142 repo.ui.quiet == orig_quiet
3124 3143 gen, tr = bundle
3125 3144 if tr is not None:
3126 3145 tr.abort()
3127 3146 finally:
3128 3147 if old_max_inline is not None:
3129 3148 mercurial.revlog._maxinline = old_max_inline
3130 3149
3131 3150
3132 3151 @command(
3133 3152 b'perf::unidiff|perfunidiff',
3134 3153 revlogopts
3135 3154 + formatteropts
3136 3155 + [
3137 3156 (
3138 3157 b'',
3139 3158 b'count',
3140 3159 1,
3141 3160 b'number of revisions to test (when using --startrev)',
3142 3161 ),
3143 3162 (b'', b'alldata', False, b'test unidiffs for all associated revisions'),
3144 3163 ],
3145 3164 b'-c|-m|FILE REV',
3146 3165 )
3147 3166 def perfunidiff(ui, repo, file_, rev=None, count=None, **opts):
3148 3167 """benchmark a unified diff between revisions
3149 3168
3150 3169 This doesn't include any copy tracing - it's just a unified diff
3151 3170 of the texts.
3152 3171
3153 3172 By default, benchmark a diff between its delta parent and itself.
3154 3173
3155 3174 With ``--count``, benchmark diffs between delta parents and self for N
3156 3175 revisions starting at the specified revision.
3157 3176
3158 3177 With ``--alldata``, assume the requested revision is a changeset and
3159 3178 measure diffs for all changes related to that changeset (manifest
3160 3179 and filelogs).
3161 3180 """
3162 3181 opts = _byteskwargs(opts)
3163 3182 if opts[b'alldata']:
3164 3183 opts[b'changelog'] = True
3165 3184
3166 3185 if opts.get(b'changelog') or opts.get(b'manifest'):
3167 3186 file_, rev = None, file_
3168 3187 elif rev is None:
3169 3188 raise error.CommandError(b'perfunidiff', b'invalid arguments')
3170 3189
3171 3190 textpairs = []
3172 3191
3173 3192 r = cmdutil.openrevlog(repo, b'perfunidiff', file_, opts)
3174 3193
3175 3194 startrev = r.rev(r.lookup(rev))
3176 3195 for rev in range(startrev, min(startrev + count, len(r) - 1)):
3177 3196 if opts[b'alldata']:
3178 3197 # Load revisions associated with changeset.
3179 3198 ctx = repo[rev]
3180 3199 mtext = _manifestrevision(repo, ctx.manifestnode())
3181 3200 for pctx in ctx.parents():
3182 3201 pman = _manifestrevision(repo, pctx.manifestnode())
3183 3202 textpairs.append((pman, mtext))
3184 3203
3185 3204 # Load filelog revisions by iterating manifest delta.
3186 3205 man = ctx.manifest()
3187 3206 pman = ctx.p1().manifest()
3188 3207 for filename, change in pman.diff(man).items():
3189 3208 fctx = repo.file(filename)
3190 3209 f1 = fctx.revision(change[0][0] or -1)
3191 3210 f2 = fctx.revision(change[1][0] or -1)
3192 3211 textpairs.append((f1, f2))
3193 3212 else:
3194 3213 dp = r.deltaparent(rev)
3195 3214 textpairs.append((r.revision(dp), r.revision(rev)))
3196 3215
3197 3216 def d():
3198 3217 for left, right in textpairs:
3199 3218 # The date strings don't matter, so we pass empty strings.
3200 3219 headerlines, hunks = mdiff.unidiff(
3201 3220 left, b'', right, b'', b'left', b'right', binary=False
3202 3221 )
3203 3222 # consume iterators in roughly the way patch.py does
3204 3223 b'\n'.join(headerlines)
3205 3224 b''.join(sum((list(hlines) for hrange, hlines in hunks), []))
3206 3225
3207 3226 timer, fm = gettimer(ui, opts)
3208 3227 timer(d)
3209 3228 fm.end()
3210 3229
3211 3230
3212 3231 @command(b'perf::diffwd|perfdiffwd', formatteropts)
3213 3232 def perfdiffwd(ui, repo, **opts):
3214 3233 """Profile diff of working directory changes"""
3215 3234 opts = _byteskwargs(opts)
3216 3235 timer, fm = gettimer(ui, opts)
3217 3236 options = {
3218 3237 'w': 'ignore_all_space',
3219 3238 'b': 'ignore_space_change',
3220 3239 'B': 'ignore_blank_lines',
3221 3240 }
3222 3241
3223 3242 for diffopt in ('', 'w', 'b', 'B', 'wB'):
3224 3243 opts = {options[c]: b'1' for c in diffopt}
3225 3244
3226 3245 def d():
3227 3246 ui.pushbuffer()
3228 3247 commands.diff(ui, repo, **opts)
3229 3248 ui.popbuffer()
3230 3249
3231 3250 diffopt = diffopt.encode('ascii')
3232 3251 title = b'diffopts: %s' % (diffopt and (b'-' + diffopt) or b'none')
3233 3252 timer(d, title=title)
3234 3253 fm.end()
3235 3254
3236 3255
3237 3256 @command(
3238 3257 b'perf::revlogindex|perfrevlogindex',
3239 3258 revlogopts + formatteropts,
3240 3259 b'-c|-m|FILE',
3241 3260 )
3242 3261 def perfrevlogindex(ui, repo, file_=None, **opts):
3243 3262 """Benchmark operations against a revlog index.
3244 3263
3245 3264 This tests constructing a revlog instance, reading index data,
3246 3265 parsing index data, and performing various operations related to
3247 3266 index data.
3248 3267 """
3249 3268
3250 3269 opts = _byteskwargs(opts)
3251 3270
3252 3271 rl = cmdutil.openrevlog(repo, b'perfrevlogindex', file_, opts)
3253 3272
3254 3273 opener = getattr(rl, 'opener') # trick linter
3255 3274 # compat with hg <= 5.8
3256 3275 radix = getattr(rl, 'radix', None)
3257 3276 indexfile = getattr(rl, '_indexfile', None)
3258 3277 if indexfile is None:
3259 3278 # compatibility with <= hg-5.8
3260 3279 indexfile = getattr(rl, 'indexfile')
3261 3280 data = opener.read(indexfile)
3262 3281
3263 3282 header = struct.unpack(b'>I', data[0:4])[0]
3264 3283 version = header & 0xFFFF
3265 3284 if version == 1:
3266 3285 inline = header & (1 << 16)
3267 3286 else:
3268 3287 raise error.Abort(b'unsupported revlog version: %d' % version)
3269 3288
3270 3289 parse_index_v1 = getattr(mercurial.revlog, 'parse_index_v1', None)
3271 3290 if parse_index_v1 is None:
3272 3291 parse_index_v1 = mercurial.revlog.revlogio().parseindex
3273 3292
3274 3293 rllen = len(rl)
3275 3294
3276 3295 node0 = rl.node(0)
3277 3296 node25 = rl.node(rllen // 4)
3278 3297 node50 = rl.node(rllen // 2)
3279 3298 node75 = rl.node(rllen // 4 * 3)
3280 3299 node100 = rl.node(rllen - 1)
3281 3300
3282 3301 allrevs = range(rllen)
3283 3302 allrevsrev = list(reversed(allrevs))
3284 3303 allnodes = [rl.node(rev) for rev in range(rllen)]
3285 3304 allnodesrev = list(reversed(allnodes))
3286 3305
3287 3306 def constructor():
3288 3307 if radix is not None:
3289 3308 revlog(opener, radix=radix)
3290 3309 else:
3291 3310 # hg <= 5.8
3292 3311 revlog(opener, indexfile=indexfile)
3293 3312
3294 3313 def read():
3295 3314 with opener(indexfile) as fh:
3296 3315 fh.read()
3297 3316
3298 3317 def parseindex():
3299 3318 parse_index_v1(data, inline)
3300 3319
3301 3320 def getentry(revornode):
3302 3321 index = parse_index_v1(data, inline)[0]
3303 3322 index[revornode]
3304 3323
3305 3324 def getentries(revs, count=1):
3306 3325 index = parse_index_v1(data, inline)[0]
3307 3326
3308 3327 for i in range(count):
3309 3328 for rev in revs:
3310 3329 index[rev]
3311 3330
3312 3331 def resolvenode(node):
3313 3332 index = parse_index_v1(data, inline)[0]
3314 3333 rev = getattr(index, 'rev', None)
3315 3334 if rev is None:
3316 3335 nodemap = getattr(parse_index_v1(data, inline)[0], 'nodemap', None)
3317 3336 # This only works for the C code.
3318 3337 if nodemap is None:
3319 3338 return
3320 3339 rev = nodemap.__getitem__
3321 3340
3322 3341 try:
3323 3342 rev(node)
3324 3343 except error.RevlogError:
3325 3344 pass
3326 3345
3327 3346 def resolvenodes(nodes, count=1):
3328 3347 index = parse_index_v1(data, inline)[0]
3329 3348 rev = getattr(index, 'rev', None)
3330 3349 if rev is None:
3331 3350 nodemap = getattr(parse_index_v1(data, inline)[0], 'nodemap', None)
3332 3351 # This only works for the C code.
3333 3352 if nodemap is None:
3334 3353 return
3335 3354 rev = nodemap.__getitem__
3336 3355
3337 3356 for i in range(count):
3338 3357 for node in nodes:
3339 3358 try:
3340 3359 rev(node)
3341 3360 except error.RevlogError:
3342 3361 pass
3343 3362
3344 3363 benches = [
3345 3364 (constructor, b'revlog constructor'),
3346 3365 (read, b'read'),
3347 3366 (parseindex, b'create index object'),
3348 3367 (lambda: getentry(0), b'retrieve index entry for rev 0'),
3349 3368 (lambda: resolvenode(b'a' * 20), b'look up missing node'),
3350 3369 (lambda: resolvenode(node0), b'look up node at rev 0'),
3351 3370 (lambda: resolvenode(node25), b'look up node at 1/4 len'),
3352 3371 (lambda: resolvenode(node50), b'look up node at 1/2 len'),
3353 3372 (lambda: resolvenode(node75), b'look up node at 3/4 len'),
3354 3373 (lambda: resolvenode(node100), b'look up node at tip'),
3355 3374 # 2x variation is to measure caching impact.
3356 3375 (lambda: resolvenodes(allnodes), b'look up all nodes (forward)'),
3357 3376 (lambda: resolvenodes(allnodes, 2), b'look up all nodes 2x (forward)'),
3358 3377 (lambda: resolvenodes(allnodesrev), b'look up all nodes (reverse)'),
3359 3378 (
3360 3379 lambda: resolvenodes(allnodesrev, 2),
3361 3380 b'look up all nodes 2x (reverse)',
3362 3381 ),
3363 3382 (lambda: getentries(allrevs), b'retrieve all index entries (forward)'),
3364 3383 (
3365 3384 lambda: getentries(allrevs, 2),
3366 3385 b'retrieve all index entries 2x (forward)',
3367 3386 ),
3368 3387 (
3369 3388 lambda: getentries(allrevsrev),
3370 3389 b'retrieve all index entries (reverse)',
3371 3390 ),
3372 3391 (
3373 3392 lambda: getentries(allrevsrev, 2),
3374 3393 b'retrieve all index entries 2x (reverse)',
3375 3394 ),
3376 3395 ]
3377 3396
3378 3397 for fn, title in benches:
3379 3398 timer, fm = gettimer(ui, opts)
3380 3399 timer(fn, title=title)
3381 3400 fm.end()
3382 3401
3383 3402
3384 3403 @command(
3385 3404 b'perf::revlogrevisions|perfrevlogrevisions',
3386 3405 revlogopts
3387 3406 + formatteropts
3388 3407 + [
3389 3408 (b'd', b'dist', 100, b'distance between the revisions'),
3390 3409 (b's', b'startrev', 0, b'revision to start reading at'),
3391 3410 (b'', b'reverse', False, b'read in reverse'),
3392 3411 ],
3393 3412 b'-c|-m|FILE',
3394 3413 )
3395 3414 def perfrevlogrevisions(
3396 3415 ui, repo, file_=None, startrev=0, reverse=False, **opts
3397 3416 ):
3398 3417 """Benchmark reading a series of revisions from a revlog.
3399 3418
3400 3419 By default, we read every ``-d/--dist`` revision from 0 to tip of
3401 3420 the specified revlog.
3402 3421
3403 3422 The start revision can be defined via ``-s/--startrev``.
3404 3423 """
3405 3424 opts = _byteskwargs(opts)
3406 3425
3407 3426 rl = cmdutil.openrevlog(repo, b'perfrevlogrevisions', file_, opts)
3408 3427 rllen = getlen(ui)(rl)
3409 3428
3410 3429 if startrev < 0:
3411 3430 startrev = rllen + startrev
3412 3431
3413 3432 def d():
3414 3433 rl.clearcaches()
3415 3434
3416 3435 beginrev = startrev
3417 3436 endrev = rllen
3418 3437 dist = opts[b'dist']
3419 3438
3420 3439 if reverse:
3421 3440 beginrev, endrev = endrev - 1, beginrev - 1
3422 3441 dist = -1 * dist
3423 3442
3424 3443 for x in _xrange(beginrev, endrev, dist):
3425 3444 # Old revisions don't support passing int.
3426 3445 n = rl.node(x)
3427 3446 rl.revision(n)
3428 3447
3429 3448 timer, fm = gettimer(ui, opts)
3430 3449 timer(d)
3431 3450 fm.end()
3432 3451
3433 3452
3434 3453 @command(
3435 3454 b'perf::revlogwrite|perfrevlogwrite',
3436 3455 revlogopts
3437 3456 + formatteropts
3438 3457 + [
3439 3458 (b's', b'startrev', 1000, b'revision to start writing at'),
3440 3459 (b'', b'stoprev', -1, b'last revision to write'),
3441 3460 (b'', b'count', 3, b'number of passes to perform'),
3442 3461 (b'', b'details', False, b'print timing for every revisions tested'),
3443 3462 (b'', b'source', b'full', b'the kind of data feed in the revlog'),
3444 3463 (b'', b'lazydeltabase', True, b'try the provided delta first'),
3445 3464 (b'', b'clear-caches', True, b'clear revlog cache between calls'),
3446 3465 ],
3447 3466 b'-c|-m|FILE',
3448 3467 )
3449 3468 def perfrevlogwrite(ui, repo, file_=None, startrev=1000, stoprev=-1, **opts):
3450 3469 """Benchmark writing a series of revisions to a revlog.
3451 3470
3452 3471 Possible source values are:
3453 3472 * `full`: add from a full text (default).
3454 3473 * `parent-1`: add from a delta to the first parent
3455 3474 * `parent-2`: add from a delta to the second parent if it exists
3456 3475 (use a delta from the first parent otherwise)
3457 3476 * `parent-smallest`: add from the smallest delta (either p1 or p2)
3458 3477 * `storage`: add from the existing precomputed deltas
3459 3478
3460 3479 Note: This performance command measures performance in a custom way. As a
3461 3480 result some of the global configuration of the 'perf' command does not
3462 3481 apply to it:
3463 3482
3464 3483 * ``pre-run``: disabled
3465 3484
3466 3485 * ``profile-benchmark``: disabled
3467 3486
3468 3487 * ``run-limits``: disabled use --count instead
3469 3488 """
3470 3489 opts = _byteskwargs(opts)
3471 3490
3472 3491 rl = cmdutil.openrevlog(repo, b'perfrevlogwrite', file_, opts)
3473 3492 rllen = getlen(ui)(rl)
3474 3493 if startrev < 0:
3475 3494 startrev = rllen + startrev
3476 3495 if stoprev < 0:
3477 3496 stoprev = rllen + stoprev
3478 3497
3479 3498 lazydeltabase = opts['lazydeltabase']
3480 3499 source = opts['source']
3481 3500 clearcaches = opts['clear_caches']
3482 3501 validsource = (
3483 3502 b'full',
3484 3503 b'parent-1',
3485 3504 b'parent-2',
3486 3505 b'parent-smallest',
3487 3506 b'storage',
3488 3507 )
3489 3508 if source not in validsource:
3490 3509 raise error.Abort('invalid source type: %s' % source)
3491 3510
3492 3511 ### actually gather results
3493 3512 count = opts['count']
3494 3513 if count <= 0:
3495 3514 raise error.Abort('invalide run count: %d' % count)
3496 3515 allresults = []
3497 3516 for c in range(count):
3498 3517 timing = _timeonewrite(
3499 3518 ui,
3500 3519 rl,
3501 3520 source,
3502 3521 startrev,
3503 3522 stoprev,
3504 3523 c + 1,
3505 3524 lazydeltabase=lazydeltabase,
3506 3525 clearcaches=clearcaches,
3507 3526 )
3508 3527 allresults.append(timing)
3509 3528
3510 3529 ### consolidate the results in a single list
3511 3530 results = []
3512 3531 for idx, (rev, t) in enumerate(allresults[0]):
3513 3532 ts = [t]
3514 3533 for other in allresults[1:]:
3515 3534 orev, ot = other[idx]
3516 3535 assert orev == rev
3517 3536 ts.append(ot)
3518 3537 results.append((rev, ts))
3519 3538 resultcount = len(results)
3520 3539
3521 3540 ### Compute and display relevant statistics
3522 3541
3523 3542 # get a formatter
3524 3543 fm = ui.formatter(b'perf', opts)
3525 3544 displayall = ui.configbool(b"perf", b"all-timing", True)
3526 3545
3527 3546 # print individual details if requested
3528 3547 if opts['details']:
3529 3548 for idx, item in enumerate(results, 1):
3530 3549 rev, data = item
3531 3550 title = 'revisions #%d of %d, rev %d' % (idx, resultcount, rev)
3532 3551 formatone(fm, data, title=title, displayall=displayall)
3533 3552
3534 3553 # sorts results by median time
3535 3554 results.sort(key=lambda x: sorted(x[1])[len(x[1]) // 2])
3536 3555 # list of (name, index) to display)
3537 3556 relevants = [
3538 3557 ("min", 0),
3539 3558 ("10%", resultcount * 10 // 100),
3540 3559 ("25%", resultcount * 25 // 100),
3541 3560 ("50%", resultcount * 70 // 100),
3542 3561 ("75%", resultcount * 75 // 100),
3543 3562 ("90%", resultcount * 90 // 100),
3544 3563 ("95%", resultcount * 95 // 100),
3545 3564 ("99%", resultcount * 99 // 100),
3546 3565 ("99.9%", resultcount * 999 // 1000),
3547 3566 ("99.99%", resultcount * 9999 // 10000),
3548 3567 ("99.999%", resultcount * 99999 // 100000),
3549 3568 ("max", -1),
3550 3569 ]
3551 3570 if not ui.quiet:
3552 3571 for name, idx in relevants:
3553 3572 data = results[idx]
3554 3573 title = '%s of %d, rev %d' % (name, resultcount, data[0])
3555 3574 formatone(fm, data[1], title=title, displayall=displayall)
3556 3575
3557 3576 # XXX summing that many float will not be very precise, we ignore this fact
3558 3577 # for now
3559 3578 totaltime = []
3560 3579 for item in allresults:
3561 3580 totaltime.append(
3562 3581 (
3563 3582 sum(x[1][0] for x in item),
3564 3583 sum(x[1][1] for x in item),
3565 3584 sum(x[1][2] for x in item),
3566 3585 )
3567 3586 )
3568 3587 formatone(
3569 3588 fm,
3570 3589 totaltime,
3571 3590 title="total time (%d revs)" % resultcount,
3572 3591 displayall=displayall,
3573 3592 )
3574 3593 fm.end()
3575 3594
3576 3595
3577 3596 class _faketr:
3578 3597 def add(s, x, y, z=None):
3579 3598 return None
3580 3599
3581 3600
3582 3601 def _timeonewrite(
3583 3602 ui,
3584 3603 orig,
3585 3604 source,
3586 3605 startrev,
3587 3606 stoprev,
3588 3607 runidx=None,
3589 3608 lazydeltabase=True,
3590 3609 clearcaches=True,
3591 3610 ):
3592 3611 timings = []
3593 3612 tr = _faketr()
3594 3613 with _temprevlog(ui, orig, startrev) as dest:
3595 3614 if hasattr(dest, "delta_config"):
3596 3615 dest.delta_config.lazy_delta_base = lazydeltabase
3597 3616 else:
3598 3617 dest._lazydeltabase = lazydeltabase
3599 3618 revs = list(orig.revs(startrev, stoprev))
3600 3619 total = len(revs)
3601 3620 topic = 'adding'
3602 3621 if runidx is not None:
3603 3622 topic += ' (run #%d)' % runidx
3604 3623 # Support both old and new progress API
3605 3624 if util.safehasattr(ui, 'makeprogress'):
3606 3625 progress = ui.makeprogress(topic, unit='revs', total=total)
3607 3626
3608 3627 def updateprogress(pos):
3609 3628 progress.update(pos)
3610 3629
3611 3630 def completeprogress():
3612 3631 progress.complete()
3613 3632
3614 3633 else:
3615 3634
3616 3635 def updateprogress(pos):
3617 3636 ui.progress(topic, pos, unit='revs', total=total)
3618 3637
3619 3638 def completeprogress():
3620 3639 ui.progress(topic, None, unit='revs', total=total)
3621 3640
3622 3641 for idx, rev in enumerate(revs):
3623 3642 updateprogress(idx)
3624 3643 addargs, addkwargs = _getrevisionseed(orig, rev, tr, source)
3625 3644 if clearcaches:
3626 3645 dest.index.clearcaches()
3627 3646 dest.clearcaches()
3628 3647 with timeone() as r:
3629 3648 dest.addrawrevision(*addargs, **addkwargs)
3630 3649 timings.append((rev, r[0]))
3631 3650 updateprogress(total)
3632 3651 completeprogress()
3633 3652 return timings
3634 3653
3635 3654
3636 3655 def _getrevisionseed(orig, rev, tr, source):
3637 3656 from mercurial.node import nullid
3638 3657
3639 3658 linkrev = orig.linkrev(rev)
3640 3659 node = orig.node(rev)
3641 3660 p1, p2 = orig.parents(node)
3642 3661 flags = orig.flags(rev)
3643 3662 cachedelta = None
3644 3663 text = None
3645 3664
3646 3665 if source == b'full':
3647 3666 text = orig.revision(rev)
3648 3667 elif source == b'parent-1':
3649 3668 baserev = orig.rev(p1)
3650 3669 cachedelta = (baserev, orig.revdiff(p1, rev))
3651 3670 elif source == b'parent-2':
3652 3671 parent = p2
3653 3672 if p2 == nullid:
3654 3673 parent = p1
3655 3674 baserev = orig.rev(parent)
3656 3675 cachedelta = (baserev, orig.revdiff(parent, rev))
3657 3676 elif source == b'parent-smallest':
3658 3677 p1diff = orig.revdiff(p1, rev)
3659 3678 parent = p1
3660 3679 diff = p1diff
3661 3680 if p2 != nullid:
3662 3681 p2diff = orig.revdiff(p2, rev)
3663 3682 if len(p1diff) > len(p2diff):
3664 3683 parent = p2
3665 3684 diff = p2diff
3666 3685 baserev = orig.rev(parent)
3667 3686 cachedelta = (baserev, diff)
3668 3687 elif source == b'storage':
3669 3688 baserev = orig.deltaparent(rev)
3670 3689 cachedelta = (baserev, orig.revdiff(orig.node(baserev), rev))
3671 3690
3672 3691 return (
3673 3692 (text, tr, linkrev, p1, p2),
3674 3693 {'node': node, 'flags': flags, 'cachedelta': cachedelta},
3675 3694 )
3676 3695
3677 3696
3678 3697 @contextlib.contextmanager
3679 3698 def _temprevlog(ui, orig, truncaterev):
3680 3699 from mercurial import vfs as vfsmod
3681 3700
3682 3701 if orig._inline:
3683 3702 raise error.Abort('not supporting inline revlog (yet)')
3684 3703 revlogkwargs = {}
3685 3704 k = 'upperboundcomp'
3686 3705 if util.safehasattr(orig, k):
3687 3706 revlogkwargs[k] = getattr(orig, k)
3688 3707
3689 3708 indexfile = getattr(orig, '_indexfile', None)
3690 3709 if indexfile is None:
3691 3710 # compatibility with <= hg-5.8
3692 3711 indexfile = getattr(orig, 'indexfile')
3693 3712 origindexpath = orig.opener.join(indexfile)
3694 3713
3695 3714 datafile = getattr(orig, '_datafile', getattr(orig, 'datafile'))
3696 3715 origdatapath = orig.opener.join(datafile)
3697 3716 radix = b'revlog'
3698 3717 indexname = b'revlog.i'
3699 3718 dataname = b'revlog.d'
3700 3719
3701 3720 tmpdir = tempfile.mkdtemp(prefix='tmp-hgperf-')
3702 3721 try:
3703 3722 # copy the data file in a temporary directory
3704 3723 ui.debug('copying data in %s\n' % tmpdir)
3705 3724 destindexpath = os.path.join(tmpdir, 'revlog.i')
3706 3725 destdatapath = os.path.join(tmpdir, 'revlog.d')
3707 3726 shutil.copyfile(origindexpath, destindexpath)
3708 3727 shutil.copyfile(origdatapath, destdatapath)
3709 3728
3710 3729 # remove the data we want to add again
3711 3730 ui.debug('truncating data to be rewritten\n')
3712 3731 with open(destindexpath, 'ab') as index:
3713 3732 index.seek(0)
3714 3733 index.truncate(truncaterev * orig._io.size)
3715 3734 with open(destdatapath, 'ab') as data:
3716 3735 data.seek(0)
3717 3736 data.truncate(orig.start(truncaterev))
3718 3737
3719 3738 # instantiate a new revlog from the temporary copy
3720 3739 ui.debug('truncating adding to be rewritten\n')
3721 3740 vfs = vfsmod.vfs(tmpdir)
3722 3741 vfs.options = getattr(orig.opener, 'options', None)
3723 3742
3724 3743 try:
3725 3744 dest = revlog(vfs, radix=radix, **revlogkwargs)
3726 3745 except TypeError:
3727 3746 dest = revlog(
3728 3747 vfs, indexfile=indexname, datafile=dataname, **revlogkwargs
3729 3748 )
3730 3749 if dest._inline:
3731 3750 raise error.Abort('not supporting inline revlog (yet)')
3732 3751 # make sure internals are initialized
3733 3752 dest.revision(len(dest) - 1)
3734 3753 yield dest
3735 3754 del dest, vfs
3736 3755 finally:
3737 3756 shutil.rmtree(tmpdir, True)
3738 3757
3739 3758
3740 3759 @command(
3741 3760 b'perf::revlogchunks|perfrevlogchunks',
3742 3761 revlogopts
3743 3762 + formatteropts
3744 3763 + [
3745 3764 (b'e', b'engines', b'', b'compression engines to use'),
3746 3765 (b's', b'startrev', 0, b'revision to start at'),
3747 3766 ],
3748 3767 b'-c|-m|FILE',
3749 3768 )
3750 3769 def perfrevlogchunks(ui, repo, file_=None, engines=None, startrev=0, **opts):
3751 3770 """Benchmark operations on revlog chunks.
3752 3771
3753 3772 Logically, each revlog is a collection of fulltext revisions. However,
3754 3773 stored within each revlog are "chunks" of possibly compressed data. This
3755 3774 data needs to be read and decompressed or compressed and written.
3756 3775
3757 3776 This command measures the time it takes to read+decompress and recompress
3758 3777 chunks in a revlog. It effectively isolates I/O and compression performance.
3759 3778 For measurements of higher-level operations like resolving revisions,
3760 3779 see ``perfrevlogrevisions`` and ``perfrevlogrevision``.
3761 3780 """
3762 3781 opts = _byteskwargs(opts)
3763 3782
3764 3783 rl = cmdutil.openrevlog(repo, b'perfrevlogchunks', file_, opts)
3765 3784
3766 3785 # - _chunkraw was renamed to _getsegmentforrevs
3767 3786 # - _getsegmentforrevs was moved on the inner object
3768 3787 try:
3769 3788 segmentforrevs = rl._inner.get_segment_for_revs
3770 3789 except AttributeError:
3771 3790 try:
3772 3791 segmentforrevs = rl._getsegmentforrevs
3773 3792 except AttributeError:
3774 3793 segmentforrevs = rl._chunkraw
3775 3794
3776 3795 # Verify engines argument.
3777 3796 if engines:
3778 3797 engines = {e.strip() for e in engines.split(b',')}
3779 3798 for engine in engines:
3780 3799 try:
3781 3800 util.compressionengines[engine]
3782 3801 except KeyError:
3783 3802 raise error.Abort(b'unknown compression engine: %s' % engine)
3784 3803 else:
3785 3804 engines = []
3786 3805 for e in util.compengines:
3787 3806 engine = util.compengines[e]
3788 3807 try:
3789 3808 if engine.available():
3790 3809 engine.revlogcompressor().compress(b'dummy')
3791 3810 engines.append(e)
3792 3811 except NotImplementedError:
3793 3812 pass
3794 3813
3795 3814 revs = list(rl.revs(startrev, len(rl) - 1))
3796 3815
3797 3816 @contextlib.contextmanager
3798 3817 def reading(rl):
3799 3818 if getattr(rl, 'reading', None) is not None:
3800 3819 with rl.reading():
3801 3820 yield None
3802 3821 elif rl._inline:
3803 3822 indexfile = getattr(rl, '_indexfile', None)
3804 3823 if indexfile is None:
3805 3824 # compatibility with <= hg-5.8
3806 3825 indexfile = getattr(rl, 'indexfile')
3807 3826 yield getsvfs(repo)(indexfile)
3808 3827 else:
3809 3828 datafile = getattr(rl, 'datafile', getattr(rl, 'datafile'))
3810 3829 yield getsvfs(repo)(datafile)
3811 3830
3812 3831 if getattr(rl, 'reading', None) is not None:
3813 3832
3814 3833 @contextlib.contextmanager
3815 3834 def lazy_reading(rl):
3816 3835 with rl.reading():
3817 3836 yield
3818 3837
3819 3838 else:
3820 3839
3821 3840 @contextlib.contextmanager
3822 3841 def lazy_reading(rl):
3823 3842 yield
3824 3843
3825 3844 def doread():
3826 3845 rl.clearcaches()
3827 3846 for rev in revs:
3828 3847 with lazy_reading(rl):
3829 3848 segmentforrevs(rev, rev)
3830 3849
3831 3850 def doreadcachedfh():
3832 3851 rl.clearcaches()
3833 3852 with reading(rl) as fh:
3834 3853 if fh is not None:
3835 3854 for rev in revs:
3836 3855 segmentforrevs(rev, rev, df=fh)
3837 3856 else:
3838 3857 for rev in revs:
3839 3858 segmentforrevs(rev, rev)
3840 3859
3841 3860 def doreadbatch():
3842 3861 rl.clearcaches()
3843 3862 with lazy_reading(rl):
3844 3863 segmentforrevs(revs[0], revs[-1])
3845 3864
3846 3865 def doreadbatchcachedfh():
3847 3866 rl.clearcaches()
3848 3867 with reading(rl) as fh:
3849 3868 if fh is not None:
3850 3869 segmentforrevs(revs[0], revs[-1], df=fh)
3851 3870 else:
3852 3871 segmentforrevs(revs[0], revs[-1])
3853 3872
3854 3873 def dochunk():
3855 3874 rl.clearcaches()
3856 3875 # chunk used to be available directly on the revlog
3857 3876 _chunk = getattr(rl, '_inner', rl)._chunk
3858 3877 with reading(rl) as fh:
3859 3878 if fh is not None:
3860 3879 for rev in revs:
3861 3880 _chunk(rev, df=fh)
3862 3881 else:
3863 3882 for rev in revs:
3864 3883 _chunk(rev)
3865 3884
3866 3885 chunks = [None]
3867 3886
3868 3887 def dochunkbatch():
3869 3888 rl.clearcaches()
3870 3889 _chunks = getattr(rl, '_inner', rl)._chunks
3871 3890 with reading(rl) as fh:
3872 3891 if fh is not None:
3873 3892 # Save chunks as a side-effect.
3874 3893 chunks[0] = _chunks(revs, df=fh)
3875 3894 else:
3876 3895 # Save chunks as a side-effect.
3877 3896 chunks[0] = _chunks(revs)
3878 3897
3879 3898 def docompress(compressor):
3880 3899 rl.clearcaches()
3881 3900
3882 3901 compressor_holder = getattr(rl, '_inner', rl)
3883 3902
3884 3903 try:
3885 3904 # Swap in the requested compression engine.
3886 3905 oldcompressor = compressor_holder._compressor
3887 3906 compressor_holder._compressor = compressor
3888 3907 for chunk in chunks[0]:
3889 3908 rl.compress(chunk)
3890 3909 finally:
3891 3910 compressor_holder._compressor = oldcompressor
3892 3911
3893 3912 benches = [
3894 3913 (lambda: doread(), b'read'),
3895 3914 (lambda: doreadcachedfh(), b'read w/ reused fd'),
3896 3915 (lambda: doreadbatch(), b'read batch'),
3897 3916 (lambda: doreadbatchcachedfh(), b'read batch w/ reused fd'),
3898 3917 (lambda: dochunk(), b'chunk'),
3899 3918 (lambda: dochunkbatch(), b'chunk batch'),
3900 3919 ]
3901 3920
3902 3921 for engine in sorted(engines):
3903 3922 compressor = util.compengines[engine].revlogcompressor()
3904 3923 benches.append(
3905 3924 (
3906 3925 functools.partial(docompress, compressor),
3907 3926 b'compress w/ %s' % engine,
3908 3927 )
3909 3928 )
3910 3929
3911 3930 for fn, title in benches:
3912 3931 timer, fm = gettimer(ui, opts)
3913 3932 timer(fn, title=title)
3914 3933 fm.end()
3915 3934
3916 3935
3917 3936 @command(
3918 3937 b'perf::revlogrevision|perfrevlogrevision',
3919 3938 revlogopts
3920 3939 + formatteropts
3921 3940 + [(b'', b'cache', False, b'use caches instead of clearing')],
3922 3941 b'-c|-m|FILE REV',
3923 3942 )
3924 3943 def perfrevlogrevision(ui, repo, file_, rev=None, cache=None, **opts):
3925 3944 """Benchmark obtaining a revlog revision.
3926 3945
3927 3946 Obtaining a revlog revision consists of roughly the following steps:
3928 3947
3929 3948 1. Compute the delta chain
3930 3949 2. Slice the delta chain if applicable
3931 3950 3. Obtain the raw chunks for that delta chain
3932 3951 4. Decompress each raw chunk
3933 3952 5. Apply binary patches to obtain fulltext
3934 3953 6. Verify hash of fulltext
3935 3954
3936 3955 This command measures the time spent in each of these phases.
3937 3956 """
3938 3957 opts = _byteskwargs(opts)
3939 3958
3940 3959 if opts.get(b'changelog') or opts.get(b'manifest'):
3941 3960 file_, rev = None, file_
3942 3961 elif rev is None:
3943 3962 raise error.CommandError(b'perfrevlogrevision', b'invalid arguments')
3944 3963
3945 3964 r = cmdutil.openrevlog(repo, b'perfrevlogrevision', file_, opts)
3946 3965
3947 3966 # _chunkraw was renamed to _getsegmentforrevs.
3948 3967 try:
3949 3968 segmentforrevs = r._inner.get_segment_for_revs
3950 3969 except AttributeError:
3951 3970 try:
3952 3971 segmentforrevs = r._getsegmentforrevs
3953 3972 except AttributeError:
3954 3973 segmentforrevs = r._chunkraw
3955 3974
3956 3975 node = r.lookup(rev)
3957 3976 rev = r.rev(node)
3958 3977
3959 3978 if getattr(r, 'reading', None) is not None:
3960 3979
3961 3980 @contextlib.contextmanager
3962 3981 def lazy_reading(r):
3963 3982 with r.reading():
3964 3983 yield
3965 3984
3966 3985 else:
3967 3986
3968 3987 @contextlib.contextmanager
3969 3988 def lazy_reading(r):
3970 3989 yield
3971 3990
3972 3991 def getrawchunks(data, chain):
3973 3992 start = r.start
3974 3993 length = r.length
3975 3994 inline = r._inline
3976 3995 try:
3977 3996 iosize = r.index.entry_size
3978 3997 except AttributeError:
3979 3998 iosize = r._io.size
3980 3999 buffer = util.buffer
3981 4000
3982 4001 chunks = []
3983 4002 ladd = chunks.append
3984 4003 for idx, item in enumerate(chain):
3985 4004 offset = start(item[0])
3986 4005 bits = data[idx]
3987 4006 for rev in item:
3988 4007 chunkstart = start(rev)
3989 4008 if inline:
3990 4009 chunkstart += (rev + 1) * iosize
3991 4010 chunklength = length(rev)
3992 4011 ladd(buffer(bits, chunkstart - offset, chunklength))
3993 4012
3994 4013 return chunks
3995 4014
3996 4015 def dodeltachain(rev):
3997 4016 if not cache:
3998 4017 r.clearcaches()
3999 4018 r._deltachain(rev)
4000 4019
4001 4020 def doread(chain):
4002 4021 if not cache:
4003 4022 r.clearcaches()
4004 4023 for item in slicedchain:
4005 4024 with lazy_reading(r):
4006 4025 segmentforrevs(item[0], item[-1])
4007 4026
4008 4027 def doslice(r, chain, size):
4009 4028 for s in slicechunk(r, chain, targetsize=size):
4010 4029 pass
4011 4030
4012 4031 def dorawchunks(data, chain):
4013 4032 if not cache:
4014 4033 r.clearcaches()
4015 4034 getrawchunks(data, chain)
4016 4035
4017 4036 def dodecompress(chunks):
4018 4037 decomp = r.decompress
4019 4038 for chunk in chunks:
4020 4039 decomp(chunk)
4021 4040
4022 4041 def dopatch(text, bins):
4023 4042 if not cache:
4024 4043 r.clearcaches()
4025 4044 mdiff.patches(text, bins)
4026 4045
4027 4046 def dohash(text):
4028 4047 if not cache:
4029 4048 r.clearcaches()
4030 4049 r.checkhash(text, node, rev=rev)
4031 4050
4032 4051 def dorevision():
4033 4052 if not cache:
4034 4053 r.clearcaches()
4035 4054 r.revision(node)
4036 4055
4037 4056 try:
4038 4057 from mercurial.revlogutils.deltas import slicechunk
4039 4058 except ImportError:
4040 4059 slicechunk = getattr(revlog, '_slicechunk', None)
4041 4060
4042 4061 size = r.length(rev)
4043 4062 chain = r._deltachain(rev)[0]
4044 4063
4045 4064 with_sparse_read = False
4046 4065 if hasattr(r, 'data_config'):
4047 4066 with_sparse_read = r.data_config.with_sparse_read
4048 4067 elif hasattr(r, '_withsparseread'):
4049 4068 with_sparse_read = r._withsparseread
4050 4069 if with_sparse_read:
4051 4070 slicedchain = (chain,)
4052 4071 else:
4053 4072 slicedchain = tuple(slicechunk(r, chain, targetsize=size))
4054 4073 data = [segmentforrevs(seg[0], seg[-1])[1] for seg in slicedchain]
4055 4074 rawchunks = getrawchunks(data, slicedchain)
4056 4075 bins = r._inner._chunks(chain)
4057 4076 text = bytes(bins[0])
4058 4077 bins = bins[1:]
4059 4078 text = mdiff.patches(text, bins)
4060 4079
4061 4080 benches = [
4062 4081 (lambda: dorevision(), b'full'),
4063 4082 (lambda: dodeltachain(rev), b'deltachain'),
4064 4083 (lambda: doread(chain), b'read'),
4065 4084 ]
4066 4085
4067 4086 if with_sparse_read:
4068 4087 slicing = (lambda: doslice(r, chain, size), b'slice-sparse-chain')
4069 4088 benches.append(slicing)
4070 4089
4071 4090 benches.extend(
4072 4091 [
4073 4092 (lambda: dorawchunks(data, slicedchain), b'rawchunks'),
4074 4093 (lambda: dodecompress(rawchunks), b'decompress'),
4075 4094 (lambda: dopatch(text, bins), b'patch'),
4076 4095 (lambda: dohash(text), b'hash'),
4077 4096 ]
4078 4097 )
4079 4098
4080 4099 timer, fm = gettimer(ui, opts)
4081 4100 for fn, title in benches:
4082 4101 timer(fn, title=title)
4083 4102 fm.end()
4084 4103
4085 4104
4086 4105 @command(
4087 4106 b'perf::revset|perfrevset',
4088 4107 [
4089 4108 (b'C', b'clear', False, b'clear volatile cache between each call.'),
4090 4109 (b'', b'contexts', False, b'obtain changectx for each revision'),
4091 4110 ]
4092 4111 + formatteropts,
4093 4112 b"REVSET",
4094 4113 )
4095 4114 def perfrevset(ui, repo, expr, clear=False, contexts=False, **opts):
4096 4115 """benchmark the execution time of a revset
4097 4116
4098 4117 Use the --clean option if need to evaluate the impact of build volatile
4099 4118 revisions set cache on the revset execution. Volatile cache hold filtered
4100 4119 and obsolete related cache."""
4101 4120 opts = _byteskwargs(opts)
4102 4121
4103 4122 timer, fm = gettimer(ui, opts)
4104 4123
4105 4124 def d():
4106 4125 if clear:
4107 4126 repo.invalidatevolatilesets()
4108 4127 if contexts:
4109 4128 for ctx in repo.set(expr):
4110 4129 pass
4111 4130 else:
4112 4131 for r in repo.revs(expr):
4113 4132 pass
4114 4133
4115 4134 timer(d)
4116 4135 fm.end()
4117 4136
4118 4137
4119 4138 @command(
4120 4139 b'perf::volatilesets|perfvolatilesets',
4121 4140 [
4122 4141 (b'', b'clear-obsstore', False, b'drop obsstore between each call.'),
4123 4142 ]
4124 4143 + formatteropts,
4125 4144 )
4126 4145 def perfvolatilesets(ui, repo, *names, **opts):
4127 4146 """benchmark the computation of various volatile set
4128 4147
4129 4148 Volatile set computes element related to filtering and obsolescence."""
4130 4149 opts = _byteskwargs(opts)
4131 4150 timer, fm = gettimer(ui, opts)
4132 4151 repo = repo.unfiltered()
4133 4152
4134 4153 def getobs(name):
4135 4154 def d():
4136 4155 repo.invalidatevolatilesets()
4137 4156 if opts[b'clear_obsstore']:
4138 4157 clearfilecache(repo, b'obsstore')
4139 4158 obsolete.getrevs(repo, name)
4140 4159
4141 4160 return d
4142 4161
4143 4162 allobs = sorted(obsolete.cachefuncs)
4144 4163 if names:
4145 4164 allobs = [n for n in allobs if n in names]
4146 4165
4147 4166 for name in allobs:
4148 4167 timer(getobs(name), title=name)
4149 4168
4150 4169 def getfiltered(name):
4151 4170 def d():
4152 4171 repo.invalidatevolatilesets()
4153 4172 if opts[b'clear_obsstore']:
4154 4173 clearfilecache(repo, b'obsstore')
4155 4174 repoview.filterrevs(repo, name)
4156 4175
4157 4176 return d
4158 4177
4159 4178 allfilter = sorted(repoview.filtertable)
4160 4179 if names:
4161 4180 allfilter = [n for n in allfilter if n in names]
4162 4181
4163 4182 for name in allfilter:
4164 4183 timer(getfiltered(name), title=name)
4165 4184 fm.end()
4166 4185
4167 4186
4168 4187 @command(
4169 4188 b'perf::branchmap|perfbranchmap',
4170 4189 [
4171 4190 (b'f', b'full', False, b'Includes build time of subset'),
4172 4191 (
4173 4192 b'',
4174 4193 b'clear-revbranch',
4175 4194 False,
4176 4195 b'purge the revbranch cache between computation',
4177 4196 ),
4178 4197 ]
4179 4198 + formatteropts,
4180 4199 )
4181 4200 def perfbranchmap(ui, repo, *filternames, **opts):
4182 4201 """benchmark the update of a branchmap
4183 4202
4184 4203 This benchmarks the full repo.branchmap() call with read and write disabled
4185 4204 """
4186 4205 opts = _byteskwargs(opts)
4187 4206 full = opts.get(b"full", False)
4188 4207 clear_revbranch = opts.get(b"clear_revbranch", False)
4189 4208 timer, fm = gettimer(ui, opts)
4190 4209
4191 4210 def getbranchmap(filtername):
4192 4211 """generate a benchmark function for the filtername"""
4193 4212 if filtername is None:
4194 4213 view = repo
4195 4214 else:
4196 4215 view = repo.filtered(filtername)
4197 4216 if util.safehasattr(view._branchcaches, '_per_filter'):
4198 4217 filtered = view._branchcaches._per_filter
4199 4218 else:
4200 4219 # older versions
4201 4220 filtered = view._branchcaches
4202 4221
4203 4222 def d():
4204 4223 if clear_revbranch:
4205 4224 repo.revbranchcache()._clear()
4206 4225 if full:
4207 4226 view._branchcaches.clear()
4208 4227 else:
4209 4228 filtered.pop(filtername, None)
4210 4229 view.branchmap()
4211 4230
4212 4231 return d
4213 4232
4214 4233 # add filter in smaller subset to bigger subset
4215 4234 possiblefilters = set(repoview.filtertable)
4216 4235 if filternames:
4217 4236 possiblefilters &= set(filternames)
4218 4237 subsettable = getbranchmapsubsettable()
4219 4238 allfilters = []
4220 4239 while possiblefilters:
4221 4240 for name in possiblefilters:
4222 4241 subset = subsettable.get(name)
4223 4242 if subset not in possiblefilters:
4224 4243 break
4225 4244 else:
4226 4245 assert False, b'subset cycle %s!' % possiblefilters
4227 4246 allfilters.append(name)
4228 4247 possiblefilters.remove(name)
4229 4248
4230 4249 # warm the cache
4231 4250 if not full:
4232 4251 for name in allfilters:
4233 4252 repo.filtered(name).branchmap()
4234 4253 if not filternames or b'unfiltered' in filternames:
4235 4254 # add unfiltered
4236 4255 allfilters.append(None)
4237 4256
4238 4257 old_branch_cache_from_file = None
4239 4258 branchcacheread = None
4240 4259 if util.safehasattr(branchmap, 'branch_cache_from_file'):
4241 4260 old_branch_cache_from_file = branchmap.branch_cache_from_file
4242 4261 branchmap.branch_cache_from_file = lambda *args: None
4243 4262 elif util.safehasattr(branchmap.branchcache, 'fromfile'):
4244 4263 branchcacheread = safeattrsetter(branchmap.branchcache, b'fromfile')
4245 4264 branchcacheread.set(classmethod(lambda *args: None))
4246 4265 else:
4247 4266 # older versions
4248 4267 branchcacheread = safeattrsetter(branchmap, b'read')
4249 4268 branchcacheread.set(lambda *args: None)
4250 4269 if util.safehasattr(branchmap, '_LocalBranchCache'):
4251 4270 branchcachewrite = safeattrsetter(branchmap._LocalBranchCache, b'write')
4252 4271 branchcachewrite.set(lambda *args: None)
4253 4272 else:
4254 4273 branchcachewrite = safeattrsetter(branchmap.branchcache, b'write')
4255 4274 branchcachewrite.set(lambda *args: None)
4256 4275 try:
4257 4276 for name in allfilters:
4258 4277 printname = name
4259 4278 if name is None:
4260 4279 printname = b'unfiltered'
4261 4280 timer(getbranchmap(name), title=printname)
4262 4281 finally:
4263 4282 if old_branch_cache_from_file is not None:
4264 4283 branchmap.branch_cache_from_file = old_branch_cache_from_file
4265 4284 if branchcacheread is not None:
4266 4285 branchcacheread.restore()
4267 4286 branchcachewrite.restore()
4268 4287 fm.end()
4269 4288
4270 4289
4271 4290 @command(
4272 4291 b'perf::branchmapupdate|perfbranchmapupdate',
4273 4292 [
4274 4293 (b'', b'base', [], b'subset of revision to start from'),
4275 4294 (b'', b'target', [], b'subset of revision to end with'),
4276 4295 (b'', b'clear-caches', False, b'clear cache between each runs'),
4277 4296 ]
4278 4297 + formatteropts,
4279 4298 )
4280 4299 def perfbranchmapupdate(ui, repo, base=(), target=(), **opts):
4281 4300 """benchmark branchmap update from for <base> revs to <target> revs
4282 4301
4283 4302 If `--clear-caches` is passed, the following items will be reset before
4284 4303 each update:
4285 4304 * the changelog instance and associated indexes
4286 4305 * the rev-branch-cache instance
4287 4306
4288 4307 Examples:
4289 4308
4290 4309 # update for the one last revision
4291 4310 $ hg perfbranchmapupdate --base 'not tip' --target 'tip'
4292 4311
4293 4312 $ update for change coming with a new branch
4294 4313 $ hg perfbranchmapupdate --base 'stable' --target 'default'
4295 4314 """
4296 4315 from mercurial import branchmap
4297 4316 from mercurial import repoview
4298 4317
4299 4318 opts = _byteskwargs(opts)
4300 4319 timer, fm = gettimer(ui, opts)
4301 4320 clearcaches = opts[b'clear_caches']
4302 4321 unfi = repo.unfiltered()
4303 4322 x = [None] # used to pass data between closure
4304 4323
4305 4324 # we use a `list` here to avoid possible side effect from smartset
4306 4325 baserevs = list(scmutil.revrange(repo, base))
4307 4326 targetrevs = list(scmutil.revrange(repo, target))
4308 4327 if not baserevs:
4309 4328 raise error.Abort(b'no revisions selected for --base')
4310 4329 if not targetrevs:
4311 4330 raise error.Abort(b'no revisions selected for --target')
4312 4331
4313 4332 # make sure the target branchmap also contains the one in the base
4314 4333 targetrevs = list(set(baserevs) | set(targetrevs))
4315 4334 targetrevs.sort()
4316 4335
4317 4336 cl = repo.changelog
4318 4337 allbaserevs = list(cl.ancestors(baserevs, inclusive=True))
4319 4338 allbaserevs.sort()
4320 4339 alltargetrevs = frozenset(cl.ancestors(targetrevs, inclusive=True))
4321 4340
4322 4341 newrevs = list(alltargetrevs.difference(allbaserevs))
4323 4342 newrevs.sort()
4324 4343
4325 4344 allrevs = frozenset(unfi.changelog.revs())
4326 4345 basefilterrevs = frozenset(allrevs.difference(allbaserevs))
4327 4346 targetfilterrevs = frozenset(allrevs.difference(alltargetrevs))
4328 4347
4329 4348 def basefilter(repo, visibilityexceptions=None):
4330 4349 return basefilterrevs
4331 4350
4332 4351 def targetfilter(repo, visibilityexceptions=None):
4333 4352 return targetfilterrevs
4334 4353
4335 4354 msg = b'benchmark of branchmap with %d revisions with %d new ones\n'
4336 4355 ui.status(msg % (len(allbaserevs), len(newrevs)))
4337 4356 if targetfilterrevs:
4338 4357 msg = b'(%d revisions still filtered)\n'
4339 4358 ui.status(msg % len(targetfilterrevs))
4340 4359
4341 4360 try:
4342 4361 repoview.filtertable[b'__perf_branchmap_update_base'] = basefilter
4343 4362 repoview.filtertable[b'__perf_branchmap_update_target'] = targetfilter
4344 4363
4345 4364 baserepo = repo.filtered(b'__perf_branchmap_update_base')
4346 4365 targetrepo = repo.filtered(b'__perf_branchmap_update_target')
4347 4366
4348 4367 bcache = repo.branchmap()
4349 4368 copy_method = 'copy'
4350 4369
4351 4370 copy_base_kwargs = copy_base_kwargs = {}
4352 4371 if hasattr(bcache, 'copy'):
4353 4372 if 'repo' in getargspec(bcache.copy).args:
4354 4373 copy_base_kwargs = {"repo": baserepo}
4355 4374 copy_target_kwargs = {"repo": targetrepo}
4356 4375 else:
4357 4376 copy_method = 'inherit_for'
4358 4377 copy_base_kwargs = {"repo": baserepo}
4359 4378 copy_target_kwargs = {"repo": targetrepo}
4360 4379
4361 4380 # try to find an existing branchmap to reuse
4362 4381 subsettable = getbranchmapsubsettable()
4363 4382 candidatefilter = subsettable.get(None)
4364 4383 while candidatefilter is not None:
4365 4384 candidatebm = repo.filtered(candidatefilter).branchmap()
4366 4385 if candidatebm.validfor(baserepo):
4367 4386 filtered = repoview.filterrevs(repo, candidatefilter)
4368 4387 missing = [r for r in allbaserevs if r in filtered]
4369 4388 base = getattr(candidatebm, copy_method)(**copy_base_kwargs)
4370 4389 base.update(baserepo, missing)
4371 4390 break
4372 4391 candidatefilter = subsettable.get(candidatefilter)
4373 4392 else:
4374 4393 # no suitable subset where found
4375 4394 base = branchmap.branchcache()
4376 4395 base.update(baserepo, allbaserevs)
4377 4396
4378 4397 def setup():
4379 4398 x[0] = getattr(base, copy_method)(**copy_target_kwargs)
4380 4399 if clearcaches:
4381 4400 unfi._revbranchcache = None
4382 4401 clearchangelog(repo)
4383 4402
4384 4403 def bench():
4385 4404 x[0].update(targetrepo, newrevs)
4386 4405
4387 4406 timer(bench, setup=setup)
4388 4407 fm.end()
4389 4408 finally:
4390 4409 repoview.filtertable.pop(b'__perf_branchmap_update_base', None)
4391 4410 repoview.filtertable.pop(b'__perf_branchmap_update_target', None)
4392 4411
4393 4412
4394 4413 @command(
4395 4414 b'perf::branchmapload|perfbranchmapload',
4396 4415 [
4397 4416 (b'f', b'filter', b'', b'Specify repoview filter'),
4398 4417 (b'', b'list', False, b'List brachmap filter caches'),
4399 4418 (b'', b'clear-revlogs', False, b'refresh changelog and manifest'),
4400 4419 ]
4401 4420 + formatteropts,
4402 4421 )
4403 4422 def perfbranchmapload(ui, repo, filter=b'', list=False, **opts):
4404 4423 """benchmark reading the branchmap"""
4405 4424 opts = _byteskwargs(opts)
4406 4425 clearrevlogs = opts[b'clear_revlogs']
4407 4426
4408 4427 if list:
4409 4428 for name, kind, st in repo.cachevfs.readdir(stat=True):
4410 4429 if name.startswith(b'branch2'):
4411 4430 filtername = name.partition(b'-')[2] or b'unfiltered'
4412 4431 ui.status(
4413 4432 b'%s - %s\n' % (filtername, util.bytecount(st.st_size))
4414 4433 )
4415 4434 return
4416 4435 if not filter:
4417 4436 filter = None
4418 4437 subsettable = getbranchmapsubsettable()
4419 4438 if filter is None:
4420 4439 repo = repo.unfiltered()
4421 4440 else:
4422 4441 repo = repoview.repoview(repo, filter)
4423 4442
4424 4443 repo.branchmap() # make sure we have a relevant, up to date branchmap
4425 4444
4426 4445 fromfile = getattr(branchmap, 'branch_cache_from_file', None)
4427 4446 if fromfile is None:
4428 4447 fromfile = getattr(branchmap.branchcache, 'fromfile', None)
4429 4448 if fromfile is None:
4430 4449 fromfile = branchmap.read
4431 4450
4432 4451 currentfilter = filter
4433 4452 # try once without timer, the filter may not be cached
4434 4453 while fromfile(repo) is None:
4435 4454 currentfilter = subsettable.get(currentfilter)
4436 4455 if currentfilter is None:
4437 4456 raise error.Abort(
4438 4457 b'No branchmap cached for %s repo' % (filter or b'unfiltered')
4439 4458 )
4440 4459 repo = repo.filtered(currentfilter)
4441 4460 timer, fm = gettimer(ui, opts)
4442 4461
4443 4462 def setup():
4444 4463 if clearrevlogs:
4445 4464 clearchangelog(repo)
4446 4465
4447 4466 def bench():
4448 4467 fromfile(repo)
4449 4468
4450 4469 timer(bench, setup=setup)
4451 4470 fm.end()
4452 4471
4453 4472
4454 4473 @command(b'perf::loadmarkers|perfloadmarkers')
4455 4474 def perfloadmarkers(ui, repo):
4456 4475 """benchmark the time to parse the on-disk markers for a repo
4457 4476
4458 4477 Result is the number of markers in the repo."""
4459 4478 timer, fm = gettimer(ui)
4460 4479 svfs = getsvfs(repo)
4461 4480 timer(lambda: len(obsolete.obsstore(repo, svfs)))
4462 4481 fm.end()
4463 4482
4464 4483
4465 4484 @command(
4466 4485 b'perf::lrucachedict|perflrucachedict',
4467 4486 formatteropts
4468 4487 + [
4469 4488 (b'', b'costlimit', 0, b'maximum total cost of items in cache'),
4470 4489 (b'', b'mincost', 0, b'smallest cost of items in cache'),
4471 4490 (b'', b'maxcost', 100, b'maximum cost of items in cache'),
4472 4491 (b'', b'size', 4, b'size of cache'),
4473 4492 (b'', b'gets', 10000, b'number of key lookups'),
4474 4493 (b'', b'sets', 10000, b'number of key sets'),
4475 4494 (b'', b'mixed', 10000, b'number of mixed mode operations'),
4476 4495 (
4477 4496 b'',
4478 4497 b'mixedgetfreq',
4479 4498 50,
4480 4499 b'frequency of get vs set ops in mixed mode',
4481 4500 ),
4482 4501 ],
4483 4502 norepo=True,
4484 4503 )
4485 4504 def perflrucache(
4486 4505 ui,
4487 4506 mincost=0,
4488 4507 maxcost=100,
4489 4508 costlimit=0,
4490 4509 size=4,
4491 4510 gets=10000,
4492 4511 sets=10000,
4493 4512 mixed=10000,
4494 4513 mixedgetfreq=50,
4495 4514 **opts
4496 4515 ):
4497 4516 opts = _byteskwargs(opts)
4498 4517
4499 4518 def doinit():
4500 4519 for i in _xrange(10000):
4501 4520 util.lrucachedict(size)
4502 4521
4503 4522 costrange = list(range(mincost, maxcost + 1))
4504 4523
4505 4524 values = []
4506 4525 for i in _xrange(size):
4507 4526 values.append(random.randint(0, _maxint))
4508 4527
4509 4528 # Get mode fills the cache and tests raw lookup performance with no
4510 4529 # eviction.
4511 4530 getseq = []
4512 4531 for i in _xrange(gets):
4513 4532 getseq.append(random.choice(values))
4514 4533
4515 4534 def dogets():
4516 4535 d = util.lrucachedict(size)
4517 4536 for v in values:
4518 4537 d[v] = v
4519 4538 for key in getseq:
4520 4539 value = d[key]
4521 4540 value # silence pyflakes warning
4522 4541
4523 4542 def dogetscost():
4524 4543 d = util.lrucachedict(size, maxcost=costlimit)
4525 4544 for i, v in enumerate(values):
4526 4545 d.insert(v, v, cost=costs[i])
4527 4546 for key in getseq:
4528 4547 try:
4529 4548 value = d[key]
4530 4549 value # silence pyflakes warning
4531 4550 except KeyError:
4532 4551 pass
4533 4552
4534 4553 # Set mode tests insertion speed with cache eviction.
4535 4554 setseq = []
4536 4555 costs = []
4537 4556 for i in _xrange(sets):
4538 4557 setseq.append(random.randint(0, _maxint))
4539 4558 costs.append(random.choice(costrange))
4540 4559
4541 4560 def doinserts():
4542 4561 d = util.lrucachedict(size)
4543 4562 for v in setseq:
4544 4563 d.insert(v, v)
4545 4564
4546 4565 def doinsertscost():
4547 4566 d = util.lrucachedict(size, maxcost=costlimit)
4548 4567 for i, v in enumerate(setseq):
4549 4568 d.insert(v, v, cost=costs[i])
4550 4569
4551 4570 def dosets():
4552 4571 d = util.lrucachedict(size)
4553 4572 for v in setseq:
4554 4573 d[v] = v
4555 4574
4556 4575 # Mixed mode randomly performs gets and sets with eviction.
4557 4576 mixedops = []
4558 4577 for i in _xrange(mixed):
4559 4578 r = random.randint(0, 100)
4560 4579 if r < mixedgetfreq:
4561 4580 op = 0
4562 4581 else:
4563 4582 op = 1
4564 4583
4565 4584 mixedops.append(
4566 4585 (op, random.randint(0, size * 2), random.choice(costrange))
4567 4586 )
4568 4587
4569 4588 def domixed():
4570 4589 d = util.lrucachedict(size)
4571 4590
4572 4591 for op, v, cost in mixedops:
4573 4592 if op == 0:
4574 4593 try:
4575 4594 d[v]
4576 4595 except KeyError:
4577 4596 pass
4578 4597 else:
4579 4598 d[v] = v
4580 4599
4581 4600 def domixedcost():
4582 4601 d = util.lrucachedict(size, maxcost=costlimit)
4583 4602
4584 4603 for op, v, cost in mixedops:
4585 4604 if op == 0:
4586 4605 try:
4587 4606 d[v]
4588 4607 except KeyError:
4589 4608 pass
4590 4609 else:
4591 4610 d.insert(v, v, cost=cost)
4592 4611
4593 4612 benches = [
4594 4613 (doinit, b'init'),
4595 4614 ]
4596 4615
4597 4616 if costlimit:
4598 4617 benches.extend(
4599 4618 [
4600 4619 (dogetscost, b'gets w/ cost limit'),
4601 4620 (doinsertscost, b'inserts w/ cost limit'),
4602 4621 (domixedcost, b'mixed w/ cost limit'),
4603 4622 ]
4604 4623 )
4605 4624 else:
4606 4625 benches.extend(
4607 4626 [
4608 4627 (dogets, b'gets'),
4609 4628 (doinserts, b'inserts'),
4610 4629 (dosets, b'sets'),
4611 4630 (domixed, b'mixed'),
4612 4631 ]
4613 4632 )
4614 4633
4615 4634 for fn, title in benches:
4616 4635 timer, fm = gettimer(ui, opts)
4617 4636 timer(fn, title=title)
4618 4637 fm.end()
4619 4638
4620 4639
4621 4640 @command(
4622 4641 b'perf::write|perfwrite',
4623 4642 formatteropts
4624 4643 + [
4625 4644 (b'', b'write-method', b'write', b'ui write method'),
4626 4645 (b'', b'nlines', 100, b'number of lines'),
4627 4646 (b'', b'nitems', 100, b'number of items (per line)'),
4628 4647 (b'', b'item', b'x', b'item that is written'),
4629 4648 (b'', b'batch-line', None, b'pass whole line to write method at once'),
4630 4649 (b'', b'flush-line', None, b'flush after each line'),
4631 4650 ],
4632 4651 )
4633 4652 def perfwrite(ui, repo, **opts):
4634 4653 """microbenchmark ui.write (and others)"""
4635 4654 opts = _byteskwargs(opts)
4636 4655
4637 4656 write = getattr(ui, _sysstr(opts[b'write_method']))
4638 4657 nlines = int(opts[b'nlines'])
4639 4658 nitems = int(opts[b'nitems'])
4640 4659 item = opts[b'item']
4641 4660 batch_line = opts.get(b'batch_line')
4642 4661 flush_line = opts.get(b'flush_line')
4643 4662
4644 4663 if batch_line:
4645 4664 line = item * nitems + b'\n'
4646 4665
4647 4666 def benchmark():
4648 4667 for i in pycompat.xrange(nlines):
4649 4668 if batch_line:
4650 4669 write(line)
4651 4670 else:
4652 4671 for i in pycompat.xrange(nitems):
4653 4672 write(item)
4654 4673 write(b'\n')
4655 4674 if flush_line:
4656 4675 ui.flush()
4657 4676 ui.flush()
4658 4677
4659 4678 timer, fm = gettimer(ui, opts)
4660 4679 timer(benchmark)
4661 4680 fm.end()
4662 4681
4663 4682
4664 4683 def uisetup(ui):
4665 4684 if util.safehasattr(cmdutil, b'openrevlog') and not util.safehasattr(
4666 4685 commands, b'debugrevlogopts'
4667 4686 ):
4668 4687 # for "historical portability":
4669 4688 # In this case, Mercurial should be 1.9 (or a79fea6b3e77) -
4670 4689 # 3.7 (or 5606f7d0d063). Therefore, '--dir' option for
4671 4690 # openrevlog() should cause failure, because it has been
4672 4691 # available since 3.5 (or 49c583ca48c4).
4673 4692 def openrevlog(orig, repo, cmd, file_, opts):
4674 4693 if opts.get(b'dir') and not util.safehasattr(repo, b'dirlog'):
4675 4694 raise error.Abort(
4676 4695 b"This version doesn't support --dir option",
4677 4696 hint=b"use 3.5 or later",
4678 4697 )
4679 4698 return orig(repo, cmd, file_, opts)
4680 4699
4681 4700 name = _sysstr(b'openrevlog')
4682 4701 extensions.wrapfunction(cmdutil, name, openrevlog)
4683 4702
4684 4703
4685 4704 @command(
4686 4705 b'perf::progress|perfprogress',
4687 4706 formatteropts
4688 4707 + [
4689 4708 (b'', b'topic', b'topic', b'topic for progress messages'),
4690 4709 (b'c', b'total', 1000000, b'total value we are progressing to'),
4691 4710 ],
4692 4711 norepo=True,
4693 4712 )
4694 4713 def perfprogress(ui, topic=None, total=None, **opts):
4695 4714 """printing of progress bars"""
4696 4715 opts = _byteskwargs(opts)
4697 4716
4698 4717 timer, fm = gettimer(ui, opts)
4699 4718
4700 4719 def doprogress():
4701 4720 with ui.makeprogress(topic, total=total) as progress:
4702 4721 for i in _xrange(total):
4703 4722 progress.increment()
4704 4723
4705 4724 timer(doprogress)
4706 4725 fm.end()
@@ -1,387 +1,394 b''
1 1 import contextlib
2 2 import errno
3 3 import os
4 4 import posixpath
5 5 import stat
6 6
7 7 from typing import (
8 8 Any,
9 9 Callable,
10 10 Iterator,
11 11 Optional,
12 12 )
13 13
14 14 from .i18n import _
15 15 from . import (
16 16 encoding,
17 17 error,
18 18 policy,
19 19 pycompat,
20 20 util,
21 21 )
22 22
23 23 rustdirs = policy.importrust('dirstate', 'Dirs')
24 24 parsers = policy.importmod('parsers')
25 25
26 26
27 27 def _lowerclean(s: bytes) -> bytes:
28 28 return encoding.hfsignoreclean(s.lower())
29 29
30 30
31 31 class pathauditor:
32 32 """ensure that a filesystem path contains no banned components.
33 33 the following properties of a path are checked:
34 34
35 35 - ends with a directory separator
36 36 - under top-level .hg
37 37 - starts at the root of a windows drive
38 38 - contains ".."
39 39
40 40 More check are also done about the file system states:
41 41 - traverses a symlink (e.g. a/symlink_here/b)
42 42 - inside a nested repository (a callback can be used to approve
43 43 some nested repositories, e.g., subrepositories)
44 44
45 45 The file system checks are only done when 'realfs' is set to True (the
46 46 default). They should be disable then we are auditing path for operation on
47 47 stored history.
48 48
49 49 If 'cached' is set to True, audited paths and sub-directories are cached.
50 50 Be careful to not keep the cache of unmanaged directories for long because
51 51 audited paths may be replaced with symlinks.
52 52 """
53 53
54 54 def __init__(self, root, callback=None, realfs=True, cached=False):
55 55 self.audited = set()
56 56 self.auditeddir = dict()
57 57 self.root = root
58 58 self._realfs = realfs
59 59 self._cached = cached
60 60 self.callback = callback
61 61 if os.path.lexists(root) and not util.fscasesensitive(root):
62 62 self.normcase = util.normcase
63 63 else:
64 64 self.normcase = lambda x: x
65 65
66 66 def __call__(self, path: bytes, mode: Optional[Any] = None) -> None:
67 67 """Check the relative path.
68 68 path may contain a pattern (e.g. foodir/**.txt)"""
69 69
70 70 path = util.localpath(path)
71 71 if path in self.audited:
72 72 return
73 73 # AIX ignores "/" at end of path, others raise EISDIR.
74 74 if util.endswithsep(path):
75 75 raise error.InputError(
76 76 _(b"path ends in directory separator: %s") % path
77 77 )
78 78 parts = util.splitpath(path)
79 79 if (
80 80 os.path.splitdrive(path)[0]
81 81 or _lowerclean(parts[0]) in (b'.hg', b'.hg.', b'')
82 82 or pycompat.ospardir in parts
83 83 ):
84 84 raise error.InputError(
85 85 _(b"path contains illegal component: %s") % path
86 86 )
87 87 # Windows shortname aliases
88 88 if b"~" in path:
89 89 for p in parts:
90 90 if b"~" in p:
91 91 first, last = p.split(b"~", 1)
92 92 if last.isdigit() and first.upper() in [b"HG", b"HG8B6C"]:
93 93 raise error.InputError(
94 94 _(b"path contains illegal component: %s") % path
95 95 )
96 96 if b'.hg' in _lowerclean(path):
97 97 lparts = [_lowerclean(p) for p in parts]
98 98 for p in b'.hg', b'.hg.':
99 99 if p in lparts[1:]:
100 100 pos = lparts.index(p)
101 101 base = os.path.join(*parts[:pos])
102 102 raise error.InputError(
103 103 _(b"path '%s' is inside nested repo %r")
104 104 % (path, pycompat.bytestr(base))
105 105 )
106 106
107 107 if self._realfs:
108 108 # It's important that we check the path parts starting from the root.
109 109 # We don't want to add "foo/bar/baz" to auditeddir before checking if
110 110 # there's a "foo/.hg" directory. This also means we won't accidentally
111 111 # traverse a symlink into some other filesystem (which is potentially
112 112 # expensive to access).
113 113 for prefix in finddirs_rev_noroot(path):
114 114 if prefix in self.auditeddir:
115 115 res = self.auditeddir[prefix]
116 116 else:
117 117 res = pathauditor._checkfs_exists(
118 118 self.root, prefix, path, self.callback
119 119 )
120 120 if self._cached:
121 121 self.auditeddir[prefix] = res
122 122 if not res:
123 123 break
124 124
125 125 if self._cached:
126 126 self.audited.add(path)
127 127
128 128 @staticmethod
129 129 def _checkfs_exists(
130 130 root,
131 131 prefix: bytes,
132 132 path: bytes,
133 133 callback: Optional[Callable[[bytes], bool]] = None,
134 134 ):
135 135 """raise exception if a file system backed check fails.
136 136
137 137 Return a bool that indicates that the directory (or file) exists."""
138 138 curpath = os.path.join(root, prefix)
139 139 try:
140 140 st = os.lstat(curpath)
141 141 except OSError as err:
142 142 if err.errno == errno.ENOENT:
143 143 return False
144 144 # EINVAL can be raised as invalid path syntax under win32.
145 145 # They must be ignored for patterns can be checked too.
146 146 if err.errno not in (errno.ENOENT, errno.ENOTDIR, errno.EINVAL):
147 147 raise
148 148 else:
149 149 if stat.S_ISLNK(st.st_mode):
150 150 msg = _(b'path %r traverses symbolic link %r') % (
151 151 pycompat.bytestr(path),
152 152 pycompat.bytestr(prefix),
153 153 )
154 154 raise error.Abort(msg)
155 155 elif stat.S_ISDIR(st.st_mode) and os.path.isdir(
156 156 os.path.join(curpath, b'.hg')
157 157 ):
158 158 if not callback or not callback(curpath):
159 159 msg = _(b"path '%s' is inside nested repo %r")
160 160 raise error.Abort(msg % (path, pycompat.bytestr(prefix)))
161 161 return True
162 162
163 163 def check(self, path: bytes) -> bool:
164 164 try:
165 165 self(path)
166 166 return True
167 167 except (OSError, error.Abort):
168 168 return False
169 169
170 170 @contextlib.contextmanager
171 171 def cached(self):
172 172 if self._cached:
173 173 yield
174 174 else:
175 175 try:
176 176 self._cached = True
177 177 yield
178 178 finally:
179 179 self.audited.clear()
180 180 self.auditeddir.clear()
181 181 self._cached = False
182 182
183 def clear_audit_cache(self):
184 """reset all audit cache
185
186 intended for debug and performance benchmark purposes"""
187 self.audited.clear()
188 self.auditeddir.clear()
189
183 190
184 191 def canonpath(
185 192 root: bytes,
186 193 cwd: bytes,
187 194 myname: bytes,
188 195 auditor: Optional[pathauditor] = None,
189 196 ) -> bytes:
190 197 """return the canonical path of myname, given cwd and root
191 198
192 199 >>> def check(root, cwd, myname):
193 200 ... a = pathauditor(root, realfs=False)
194 201 ... try:
195 202 ... return canonpath(root, cwd, myname, a)
196 203 ... except error.Abort:
197 204 ... return 'aborted'
198 205 >>> def unixonly(root, cwd, myname, expected='aborted'):
199 206 ... if pycompat.iswindows:
200 207 ... return expected
201 208 ... return check(root, cwd, myname)
202 209 >>> def winonly(root, cwd, myname, expected='aborted'):
203 210 ... if not pycompat.iswindows:
204 211 ... return expected
205 212 ... return check(root, cwd, myname)
206 213 >>> winonly(b'd:\\\\repo', b'c:\\\\dir', b'filename')
207 214 'aborted'
208 215 >>> winonly(b'c:\\\\repo', b'c:\\\\dir', b'filename')
209 216 'aborted'
210 217 >>> winonly(b'c:\\\\repo', b'c:\\\\', b'filename')
211 218 'aborted'
212 219 >>> winonly(b'c:\\\\repo', b'c:\\\\', b'repo\\\\filename',
213 220 ... b'filename')
214 221 'filename'
215 222 >>> winonly(b'c:\\\\repo', b'c:\\\\repo', b'filename', b'filename')
216 223 'filename'
217 224 >>> winonly(b'c:\\\\repo', b'c:\\\\repo\\\\subdir', b'filename',
218 225 ... b'subdir/filename')
219 226 'subdir/filename'
220 227 >>> unixonly(b'/repo', b'/dir', b'filename')
221 228 'aborted'
222 229 >>> unixonly(b'/repo', b'/', b'filename')
223 230 'aborted'
224 231 >>> unixonly(b'/repo', b'/', b'repo/filename', b'filename')
225 232 'filename'
226 233 >>> unixonly(b'/repo', b'/repo', b'filename', b'filename')
227 234 'filename'
228 235 >>> unixonly(b'/repo', b'/repo/subdir', b'filename', b'subdir/filename')
229 236 'subdir/filename'
230 237 """
231 238 if util.endswithsep(root):
232 239 rootsep = root
233 240 else:
234 241 rootsep = root + pycompat.ossep
235 242 name = myname
236 243 if not os.path.isabs(name):
237 244 name = os.path.join(root, cwd, name)
238 245 name = os.path.normpath(name)
239 246 if auditor is None:
240 247 auditor = pathauditor(root)
241 248 if name != rootsep and name.startswith(rootsep):
242 249 name = name[len(rootsep) :]
243 250 auditor(name)
244 251 return util.pconvert(name)
245 252 elif name == root:
246 253 return b''
247 254 else:
248 255 # Determine whether `name' is in the hierarchy at or beneath `root',
249 256 # by iterating name=dirname(name) until that causes no change (can't
250 257 # check name == '/', because that doesn't work on windows). The list
251 258 # `rel' holds the reversed list of components making up the relative
252 259 # file name we want.
253 260 rel = []
254 261 while True:
255 262 try:
256 263 s = util.samefile(name, root)
257 264 except OSError:
258 265 s = False
259 266 if s:
260 267 if not rel:
261 268 # name was actually the same as root (maybe a symlink)
262 269 return b''
263 270 rel.reverse()
264 271 name = os.path.join(*rel)
265 272 auditor(name)
266 273 return util.pconvert(name)
267 274 dirname, basename = util.split(name)
268 275 rel.append(basename)
269 276 if dirname == name:
270 277 break
271 278 name = dirname
272 279
273 280 # A common mistake is to use -R, but specify a file relative to the repo
274 281 # instead of cwd. Detect that case, and provide a hint to the user.
275 282 hint = None
276 283 try:
277 284 if cwd != root:
278 285 canonpath(root, root, myname, auditor)
279 286 relpath = util.pathto(root, cwd, b'')
280 287 if relpath.endswith(pycompat.ossep):
281 288 relpath = relpath[:-1]
282 289 hint = _(b"consider using '--cwd %s'") % relpath
283 290 except error.Abort:
284 291 pass
285 292
286 293 raise error.Abort(
287 294 _(b"%s not under root '%s'") % (myname, root), hint=hint
288 295 )
289 296
290 297
291 298 def normasprefix(path: bytes) -> bytes:
292 299 """normalize the specified path as path prefix
293 300
294 301 Returned value can be used safely for "p.startswith(prefix)",
295 302 "p[len(prefix):]", and so on.
296 303
297 304 For efficiency, this expects "path" argument to be already
298 305 normalized by "os.path.normpath", "os.path.realpath", and so on.
299 306
300 307 See also issue3033 for detail about need of this function.
301 308
302 309 >>> normasprefix(b'/foo/bar').replace(pycompat.ossep, b'/')
303 310 '/foo/bar/'
304 311 >>> normasprefix(b'/').replace(pycompat.ossep, b'/')
305 312 '/'
306 313 """
307 314 d, p = os.path.splitdrive(path)
308 315 if len(p) != len(pycompat.ossep):
309 316 return path + pycompat.ossep
310 317 else:
311 318 return path
312 319
313 320
314 321 def finddirs(path: bytes) -> Iterator[bytes]:
315 322 pos = path.rfind(b'/')
316 323 while pos != -1:
317 324 yield path[:pos]
318 325 pos = path.rfind(b'/', 0, pos)
319 326 yield b''
320 327
321 328
322 329 def finddirs_rev_noroot(path: bytes) -> Iterator[bytes]:
323 330 pos = path.find(pycompat.ossep)
324 331 while pos != -1:
325 332 yield path[:pos]
326 333 pos = path.find(pycompat.ossep, pos + 1)
327 334
328 335
329 336 class dirs:
330 337 '''a multiset of directory names from a set of file paths'''
331 338
332 339 def __init__(self, map, only_tracked=False):
333 340 """
334 341 a dict map indicates a dirstate while a list indicates a manifest
335 342 """
336 343 self._dirs = {}
337 344 addpath = self.addpath
338 345 if isinstance(map, dict) and only_tracked:
339 346 for f, s in map.items():
340 347 if s.state != b'r':
341 348 addpath(f)
342 349 elif only_tracked:
343 350 msg = b"`only_tracked` is only supported with a dict source"
344 351 raise error.ProgrammingError(msg)
345 352 else:
346 353 for f in map:
347 354 addpath(f)
348 355
349 356 def addpath(self, path: bytes) -> None:
350 357 dirs = self._dirs
351 358 for base in finddirs(path):
352 359 if base.endswith(b'/'):
353 360 raise ValueError(
354 361 "found invalid consecutive slashes in path: %r" % base
355 362 )
356 363 if base in dirs:
357 364 dirs[base] += 1
358 365 return
359 366 dirs[base] = 1
360 367
361 368 def delpath(self, path: bytes) -> None:
362 369 dirs = self._dirs
363 370 for base in finddirs(path):
364 371 if dirs[base] > 1:
365 372 dirs[base] -= 1
366 373 return
367 374 del dirs[base]
368 375
369 376 def __iter__(self):
370 377 return iter(self._dirs)
371 378
372 379 def __contains__(self, d: bytes) -> bool:
373 380 return d in self._dirs
374 381
375 382
376 383 if hasattr(parsers, 'dirs'):
377 384 dirs = parsers.dirs
378 385
379 386 if rustdirs is not None:
380 387 dirs = rustdirs
381 388
382 389
383 390 # forward two methods from posixpath that do what we need, but we'd
384 391 # rather not let our internals know that we're thinking in posix terms
385 392 # - instead we'll let them be oblivious.
386 393 join = posixpath.join
387 394 dirname: Callable[[bytes], bytes] = posixpath.dirname
General Comments 0
You need to be logged in to leave comments. Login now