##// END OF EJS Templates
profiling: allow loading profiling extension before everything else...
Jun Wu -
r32417:f40dc6f7 default
parent child Browse files
Show More
@@ -1,578 +1,578
1 1 # extensions.py - extension handling for mercurial
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import imp
11 11 import inspect
12 12 import os
13 13
14 14 from .i18n import (
15 15 _,
16 16 gettext,
17 17 )
18 18
19 19 from . import (
20 20 cmdutil,
21 21 encoding,
22 22 error,
23 23 pycompat,
24 24 util,
25 25 )
26 26
27 27 _extensions = {}
28 28 _disabledextensions = {}
29 29 _aftercallbacks = {}
30 30 _order = []
31 31 _builtin = {'hbisect', 'bookmarks', 'parentrevspec', 'progress', 'interhg',
32 32 'inotify', 'hgcia'}
33 33
34 34 def extensions(ui=None):
35 35 if ui:
36 36 def enabled(name):
37 37 for format in ['%s', 'hgext.%s']:
38 38 conf = ui.config('extensions', format % name)
39 39 if conf is not None and not conf.startswith('!'):
40 40 return True
41 41 else:
42 42 enabled = lambda name: True
43 43 for name in _order:
44 44 module = _extensions[name]
45 45 if module and enabled(name):
46 46 yield name, module
47 47
48 48 def find(name):
49 49 '''return module with given extension name'''
50 50 mod = None
51 51 try:
52 52 mod = _extensions[name]
53 53 except KeyError:
54 54 for k, v in _extensions.iteritems():
55 55 if k.endswith('.' + name) or k.endswith('/' + name):
56 56 mod = v
57 57 break
58 58 if not mod:
59 59 raise KeyError(name)
60 60 return mod
61 61
62 62 def loadpath(path, module_name):
63 63 module_name = module_name.replace('.', '_')
64 64 path = util.normpath(util.expandpath(path))
65 65 module_name = pycompat.fsdecode(module_name)
66 66 path = pycompat.fsdecode(path)
67 67 if os.path.isdir(path):
68 68 # module/__init__.py style
69 69 d, f = os.path.split(path)
70 70 fd, fpath, desc = imp.find_module(f, [d])
71 71 return imp.load_module(module_name, fd, fpath, desc)
72 72 else:
73 73 try:
74 74 return imp.load_source(module_name, path)
75 75 except IOError as exc:
76 76 if not exc.filename:
77 77 exc.filename = path # python does not fill this
78 78 raise
79 79
80 80 def _importh(name):
81 81 """import and return the <name> module"""
82 82 mod = __import__(pycompat.sysstr(name))
83 83 components = name.split('.')
84 84 for comp in components[1:]:
85 85 mod = getattr(mod, comp)
86 86 return mod
87 87
88 88 def _importext(name, path=None, reportfunc=None):
89 89 if path:
90 90 # the module will be loaded in sys.modules
91 91 # choose an unique name so that it doesn't
92 92 # conflicts with other modules
93 93 mod = loadpath(path, 'hgext.%s' % name)
94 94 else:
95 95 try:
96 96 mod = _importh("hgext.%s" % name)
97 97 except ImportError as err:
98 98 if reportfunc:
99 99 reportfunc(err, "hgext.%s" % name, "hgext3rd.%s" % name)
100 100 try:
101 101 mod = _importh("hgext3rd.%s" % name)
102 102 except ImportError as err:
103 103 if reportfunc:
104 104 reportfunc(err, "hgext3rd.%s" % name, name)
105 105 mod = _importh(name)
106 106 return mod
107 107
108 108 def _forbytes(inst):
109 109 """Portably format an import error into a form suitable for
110 110 %-formatting into bytestrings."""
111 111 return encoding.strtolocal(str(inst))
112 112
113 113 def _reportimporterror(ui, err, failed, next):
114 114 # note: this ui.debug happens before --debug is processed,
115 115 # Use --config ui.debug=1 to see them.
116 116 ui.debug('could not import %s (%s): trying %s\n'
117 117 % (failed, _forbytes(err), next))
118 118 if ui.debugflag:
119 119 ui.traceback()
120 120
121 121 # attributes set by registrar.command
122 122 _cmdfuncattrs = ('norepo', 'optionalrepo', 'inferrepo')
123 123
124 124 def _validatecmdtable(ui, cmdtable):
125 125 """Check if extension commands have required attributes"""
126 126 for c, e in cmdtable.iteritems():
127 127 f = e[0]
128 128 if getattr(f, '_deprecatedregistrar', False):
129 129 ui.deprecwarn("cmdutil.command is deprecated, use "
130 130 "registrar.command to register '%s'" % c, '4.6')
131 131 missing = [a for a in _cmdfuncattrs if not util.safehasattr(f, a)]
132 132 if not missing:
133 133 continue
134 134 raise error.ProgrammingError(
135 135 'missing attributes: %s' % ', '.join(missing),
136 136 hint="use @command decorator to register '%s'" % c)
137 137
138 138 def load(ui, name, path):
139 139 if name.startswith('hgext.') or name.startswith('hgext/'):
140 140 shortname = name[6:]
141 141 else:
142 142 shortname = name
143 143 if shortname in _builtin:
144 144 return None
145 145 if shortname in _extensions:
146 146 return _extensions[shortname]
147 147 _extensions[shortname] = None
148 148 mod = _importext(name, path, bind(_reportimporterror, ui))
149 149
150 150 # Before we do anything with the extension, check against minimum stated
151 151 # compatibility. This gives extension authors a mechanism to have their
152 152 # extensions short circuit when loaded with a known incompatible version
153 153 # of Mercurial.
154 154 minver = getattr(mod, 'minimumhgversion', None)
155 155 if minver and util.versiontuple(minver, 2) > util.versiontuple(n=2):
156 156 ui.warn(_('(third party extension %s requires version %s or newer '
157 157 'of Mercurial; disabling)\n') % (shortname, minver))
158 158 return
159 159 _validatecmdtable(ui, getattr(mod, 'cmdtable', {}))
160 160
161 161 _extensions[shortname] = mod
162 162 _order.append(shortname)
163 163 for fn in _aftercallbacks.get(shortname, []):
164 164 fn(loaded=True)
165 165 return mod
166 166
167 167 def _runuisetup(name, ui):
168 168 uisetup = getattr(_extensions[name], 'uisetup', None)
169 169 if uisetup:
170 170 uisetup(ui)
171 171
172 172 def _runextsetup(name, ui):
173 173 extsetup = getattr(_extensions[name], 'extsetup', None)
174 174 if extsetup:
175 175 try:
176 176 extsetup(ui)
177 177 except TypeError:
178 178 if inspect.getargspec(extsetup).args:
179 179 raise
180 180 extsetup() # old extsetup with no ui argument
181 181
182 182 def loadall(ui, whitelist=None):
183 183 result = ui.configitems("extensions")
184 if whitelist:
184 if whitelist is not None:
185 185 result = [(k, v) for (k, v) in result if k in whitelist]
186 186 newindex = len(_order)
187 187 for (name, path) in result:
188 188 if path:
189 189 if path[0:1] == '!':
190 190 _disabledextensions[name] = path[1:]
191 191 continue
192 192 try:
193 193 load(ui, name, path)
194 194 except Exception as inst:
195 195 msg = _forbytes(inst)
196 196 if path:
197 197 ui.warn(_("*** failed to import extension %s from %s: %s\n")
198 198 % (name, path, msg))
199 199 else:
200 200 ui.warn(_("*** failed to import extension %s: %s\n")
201 201 % (name, msg))
202 202 if isinstance(inst, error.Hint) and inst.hint:
203 203 ui.warn(_("*** (%s)\n") % inst.hint)
204 204 ui.traceback()
205 205
206 206 for name in _order[newindex:]:
207 207 _runuisetup(name, ui)
208 208
209 209 for name in _order[newindex:]:
210 210 _runextsetup(name, ui)
211 211
212 212 # Call aftercallbacks that were never met.
213 213 for shortname in _aftercallbacks:
214 214 if shortname in _extensions:
215 215 continue
216 216
217 217 for fn in _aftercallbacks[shortname]:
218 218 fn(loaded=False)
219 219
220 220 # loadall() is called multiple times and lingering _aftercallbacks
221 221 # entries could result in double execution. See issue4646.
222 222 _aftercallbacks.clear()
223 223
224 224 def afterloaded(extension, callback):
225 225 '''Run the specified function after a named extension is loaded.
226 226
227 227 If the named extension is already loaded, the callback will be called
228 228 immediately.
229 229
230 230 If the named extension never loads, the callback will be called after
231 231 all extensions have been loaded.
232 232
233 233 The callback receives the named argument ``loaded``, which is a boolean
234 234 indicating whether the dependent extension actually loaded.
235 235 '''
236 236
237 237 if extension in _extensions:
238 238 callback(loaded=True)
239 239 else:
240 240 _aftercallbacks.setdefault(extension, []).append(callback)
241 241
242 242 def bind(func, *args):
243 243 '''Partial function application
244 244
245 245 Returns a new function that is the partial application of args and kwargs
246 246 to func. For example,
247 247
248 248 f(1, 2, bar=3) === bind(f, 1)(2, bar=3)'''
249 249 assert callable(func)
250 250 def closure(*a, **kw):
251 251 return func(*(args + a), **kw)
252 252 return closure
253 253
254 254 def _updatewrapper(wrap, origfn, unboundwrapper):
255 255 '''Copy and add some useful attributes to wrapper'''
256 256 wrap.__module__ = getattr(origfn, '__module__')
257 257 wrap.__doc__ = getattr(origfn, '__doc__')
258 258 wrap.__dict__.update(getattr(origfn, '__dict__', {}))
259 259 wrap._origfunc = origfn
260 260 wrap._unboundwrapper = unboundwrapper
261 261
262 262 def wrapcommand(table, command, wrapper, synopsis=None, docstring=None):
263 263 '''Wrap the command named `command' in table
264 264
265 265 Replace command in the command table with wrapper. The wrapped command will
266 266 be inserted into the command table specified by the table argument.
267 267
268 268 The wrapper will be called like
269 269
270 270 wrapper(orig, *args, **kwargs)
271 271
272 272 where orig is the original (wrapped) function, and *args, **kwargs
273 273 are the arguments passed to it.
274 274
275 275 Optionally append to the command synopsis and docstring, used for help.
276 276 For example, if your extension wraps the ``bookmarks`` command to add the
277 277 flags ``--remote`` and ``--all`` you might call this function like so:
278 278
279 279 synopsis = ' [-a] [--remote]'
280 280 docstring = """
281 281
282 282 The ``remotenames`` extension adds the ``--remote`` and ``--all`` (``-a``)
283 283 flags to the bookmarks command. Either flag will show the remote bookmarks
284 284 known to the repository; ``--remote`` will also suppress the output of the
285 285 local bookmarks.
286 286 """
287 287
288 288 extensions.wrapcommand(commands.table, 'bookmarks', exbookmarks,
289 289 synopsis, docstring)
290 290 '''
291 291 assert callable(wrapper)
292 292 aliases, entry = cmdutil.findcmd(command, table)
293 293 for alias, e in table.iteritems():
294 294 if e is entry:
295 295 key = alias
296 296 break
297 297
298 298 origfn = entry[0]
299 299 wrap = bind(util.checksignature(wrapper), util.checksignature(origfn))
300 300 _updatewrapper(wrap, origfn, wrapper)
301 301 if docstring is not None:
302 302 wrap.__doc__ += docstring
303 303
304 304 newentry = list(entry)
305 305 newentry[0] = wrap
306 306 if synopsis is not None:
307 307 newentry[2] += synopsis
308 308 table[key] = tuple(newentry)
309 309 return entry
310 310
311 311 def wrapfunction(container, funcname, wrapper):
312 312 '''Wrap the function named funcname in container
313 313
314 314 Replace the funcname member in the given container with the specified
315 315 wrapper. The container is typically a module, class, or instance.
316 316
317 317 The wrapper will be called like
318 318
319 319 wrapper(orig, *args, **kwargs)
320 320
321 321 where orig is the original (wrapped) function, and *args, **kwargs
322 322 are the arguments passed to it.
323 323
324 324 Wrapping methods of the repository object is not recommended since
325 325 it conflicts with extensions that extend the repository by
326 326 subclassing. All extensions that need to extend methods of
327 327 localrepository should use this subclassing trick: namely,
328 328 reposetup() should look like
329 329
330 330 def reposetup(ui, repo):
331 331 class myrepo(repo.__class__):
332 332 def whatever(self, *args, **kwargs):
333 333 [...extension stuff...]
334 334 super(myrepo, self).whatever(*args, **kwargs)
335 335 [...extension stuff...]
336 336
337 337 repo.__class__ = myrepo
338 338
339 339 In general, combining wrapfunction() with subclassing does not
340 340 work. Since you cannot control what other extensions are loaded by
341 341 your end users, you should play nicely with others by using the
342 342 subclass trick.
343 343 '''
344 344 assert callable(wrapper)
345 345
346 346 origfn = getattr(container, funcname)
347 347 assert callable(origfn)
348 348 wrap = bind(wrapper, origfn)
349 349 _updatewrapper(wrap, origfn, wrapper)
350 350 setattr(container, funcname, wrap)
351 351 return origfn
352 352
353 353 def unwrapfunction(container, funcname, wrapper=None):
354 354 '''undo wrapfunction
355 355
356 356 If wrappers is None, undo the last wrap. Otherwise removes the wrapper
357 357 from the chain of wrappers.
358 358
359 359 Return the removed wrapper.
360 360 Raise IndexError if wrapper is None and nothing to unwrap; ValueError if
361 361 wrapper is not None but is not found in the wrapper chain.
362 362 '''
363 363 chain = getwrapperchain(container, funcname)
364 364 origfn = chain.pop()
365 365 if wrapper is None:
366 366 wrapper = chain[0]
367 367 chain.remove(wrapper)
368 368 setattr(container, funcname, origfn)
369 369 for w in reversed(chain):
370 370 wrapfunction(container, funcname, w)
371 371 return wrapper
372 372
373 373 def getwrapperchain(container, funcname):
374 374 '''get a chain of wrappers of a function
375 375
376 376 Return a list of functions: [newest wrapper, ..., oldest wrapper, origfunc]
377 377
378 378 The wrapper functions are the ones passed to wrapfunction, whose first
379 379 argument is origfunc.
380 380 '''
381 381 result = []
382 382 fn = getattr(container, funcname)
383 383 while fn:
384 384 assert callable(fn)
385 385 result.append(getattr(fn, '_unboundwrapper', fn))
386 386 fn = getattr(fn, '_origfunc', None)
387 387 return result
388 388
389 389 def _disabledpaths(strip_init=False):
390 390 '''find paths of disabled extensions. returns a dict of {name: path}
391 391 removes /__init__.py from packages if strip_init is True'''
392 392 import hgext
393 393 extpath = os.path.dirname(
394 394 os.path.abspath(pycompat.fsencode(hgext.__file__)))
395 395 try: # might not be a filesystem path
396 396 files = os.listdir(extpath)
397 397 except OSError:
398 398 return {}
399 399
400 400 exts = {}
401 401 for e in files:
402 402 if e.endswith('.py'):
403 403 name = e.rsplit('.', 1)[0]
404 404 path = os.path.join(extpath, e)
405 405 else:
406 406 name = e
407 407 path = os.path.join(extpath, e, '__init__.py')
408 408 if not os.path.exists(path):
409 409 continue
410 410 if strip_init:
411 411 path = os.path.dirname(path)
412 412 if name in exts or name in _order or name == '__init__':
413 413 continue
414 414 exts[name] = path
415 415 exts.update(_disabledextensions)
416 416 return exts
417 417
418 418 def _moduledoc(file):
419 419 '''return the top-level python documentation for the given file
420 420
421 421 Loosely inspired by pydoc.source_synopsis(), but rewritten to
422 422 handle triple quotes and to return the whole text instead of just
423 423 the synopsis'''
424 424 result = []
425 425
426 426 line = file.readline()
427 427 while line[:1] == '#' or not line.strip():
428 428 line = file.readline()
429 429 if not line:
430 430 break
431 431
432 432 start = line[:3]
433 433 if start == '"""' or start == "'''":
434 434 line = line[3:]
435 435 while line:
436 436 if line.rstrip().endswith(start):
437 437 line = line.split(start)[0]
438 438 if line:
439 439 result.append(line)
440 440 break
441 441 elif not line:
442 442 return None # unmatched delimiter
443 443 result.append(line)
444 444 line = file.readline()
445 445 else:
446 446 return None
447 447
448 448 return ''.join(result)
449 449
450 450 def _disabledhelp(path):
451 451 '''retrieve help synopsis of a disabled extension (without importing)'''
452 452 try:
453 453 file = open(path)
454 454 except IOError:
455 455 return
456 456 else:
457 457 doc = _moduledoc(file)
458 458 file.close()
459 459
460 460 if doc: # extracting localized synopsis
461 461 return gettext(doc)
462 462 else:
463 463 return _('(no help text available)')
464 464
465 465 def disabled():
466 466 '''find disabled extensions from hgext. returns a dict of {name: desc}'''
467 467 try:
468 468 from hgext import __index__
469 469 return dict((name, gettext(desc))
470 470 for name, desc in __index__.docs.iteritems()
471 471 if name not in _order)
472 472 except (ImportError, AttributeError):
473 473 pass
474 474
475 475 paths = _disabledpaths()
476 476 if not paths:
477 477 return {}
478 478
479 479 exts = {}
480 480 for name, path in paths.iteritems():
481 481 doc = _disabledhelp(path)
482 482 if doc:
483 483 exts[name] = doc.splitlines()[0]
484 484
485 485 return exts
486 486
487 487 def disabledext(name):
488 488 '''find a specific disabled extension from hgext. returns desc'''
489 489 try:
490 490 from hgext import __index__
491 491 if name in _order: # enabled
492 492 return
493 493 else:
494 494 return gettext(__index__.docs.get(name))
495 495 except (ImportError, AttributeError):
496 496 pass
497 497
498 498 paths = _disabledpaths()
499 499 if name in paths:
500 500 return _disabledhelp(paths[name])
501 501
502 502 def disabledcmd(ui, cmd, strict=False):
503 503 '''import disabled extensions until cmd is found.
504 504 returns (cmdname, extname, module)'''
505 505
506 506 paths = _disabledpaths(strip_init=True)
507 507 if not paths:
508 508 raise error.UnknownCommand(cmd)
509 509
510 510 def findcmd(cmd, name, path):
511 511 try:
512 512 mod = loadpath(path, 'hgext.%s' % name)
513 513 except Exception:
514 514 return
515 515 try:
516 516 aliases, entry = cmdutil.findcmd(cmd,
517 517 getattr(mod, 'cmdtable', {}), strict)
518 518 except (error.AmbiguousCommand, error.UnknownCommand):
519 519 return
520 520 except Exception:
521 521 ui.warn(_('warning: error finding commands in %s\n') % path)
522 522 ui.traceback()
523 523 return
524 524 for c in aliases:
525 525 if c.startswith(cmd):
526 526 cmd = c
527 527 break
528 528 else:
529 529 cmd = aliases[0]
530 530 return (cmd, name, mod)
531 531
532 532 ext = None
533 533 # first, search for an extension with the same name as the command
534 534 path = paths.pop(cmd, None)
535 535 if path:
536 536 ext = findcmd(cmd, cmd, path)
537 537 if not ext:
538 538 # otherwise, interrogate each extension until there's a match
539 539 for name, path in paths.iteritems():
540 540 ext = findcmd(cmd, name, path)
541 541 if ext:
542 542 break
543 543 if ext and 'DEPRECATED' not in ext.__doc__:
544 544 return ext
545 545
546 546 raise error.UnknownCommand(cmd)
547 547
548 548 def enabled(shortname=True):
549 549 '''return a dict of {name: desc} of extensions'''
550 550 exts = {}
551 551 for ename, ext in extensions():
552 552 doc = (gettext(ext.__doc__) or _('(no help text available)'))
553 553 if shortname:
554 554 ename = ename.split('.')[-1]
555 555 exts[ename] = doc.splitlines()[0].strip()
556 556
557 557 return exts
558 558
559 559 def notloaded():
560 560 '''return short names of extensions that failed to load'''
561 561 return [name for name, mod in _extensions.iteritems() if mod is None]
562 562
563 563 def moduleversion(module):
564 564 '''return version information from given module as a string'''
565 565 if (util.safehasattr(module, 'getversion')
566 566 and callable(module.getversion)):
567 567 version = module.getversion()
568 568 elif util.safehasattr(module, '__version__'):
569 569 version = module.__version__
570 570 else:
571 571 version = ''
572 572 if isinstance(version, (list, tuple)):
573 573 version = '.'.join(str(o) for o in version)
574 574 return version
575 575
576 576 def ismoduleinternal(module):
577 577 exttestedwith = getattr(module, 'testedwith', None)
578 578 return exttestedwith == "ships-with-hg-core"
@@ -1,192 +1,210
1 1 # profiling.py - profiling functions
2 2 #
3 3 # Copyright 2016 Gregory Szorc <gregory.szorc@gmail.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import, print_function
9 9
10 10 import contextlib
11 11
12 12 from .i18n import _
13 13 from . import (
14 14 encoding,
15 15 error,
16 extensions,
16 17 util,
17 18 )
18 19
20 def _loadprofiler(ui, profiler):
21 """load profiler extension. return profile method, or None on failure"""
22 extname = profiler
23 extensions.loadall(ui, whitelist=[extname])
24 try:
25 mod = extensions.find(extname)
26 except KeyError:
27 return None
28 else:
29 return getattr(mod, 'profile', None)
30
19 31 @contextlib.contextmanager
20 32 def lsprofile(ui, fp):
21 33 format = ui.config('profiling', 'format', default='text')
22 34 field = ui.config('profiling', 'sort', default='inlinetime')
23 35 limit = ui.configint('profiling', 'limit', default=30)
24 36 climit = ui.configint('profiling', 'nested', default=0)
25 37
26 38 if format not in ['text', 'kcachegrind']:
27 39 ui.warn(_("unrecognized profiling format '%s'"
28 40 " - Ignored\n") % format)
29 41 format = 'text'
30 42
31 43 try:
32 44 from . import lsprof
33 45 except ImportError:
34 46 raise error.Abort(_(
35 47 'lsprof not available - install from '
36 48 'http://codespeak.net/svn/user/arigo/hack/misc/lsprof/'))
37 49 p = lsprof.Profiler()
38 50 p.enable(subcalls=True)
39 51 try:
40 52 yield
41 53 finally:
42 54 p.disable()
43 55
44 56 if format == 'kcachegrind':
45 57 from . import lsprofcalltree
46 58 calltree = lsprofcalltree.KCacheGrind(p)
47 59 calltree.output(fp)
48 60 else:
49 61 # format == 'text'
50 62 stats = lsprof.Stats(p.getstats())
51 63 stats.sort(field)
52 64 stats.pprint(limit=limit, file=fp, climit=climit)
53 65
54 66 @contextlib.contextmanager
55 67 def flameprofile(ui, fp):
56 68 try:
57 69 from flamegraph import flamegraph
58 70 except ImportError:
59 71 raise error.Abort(_(
60 72 'flamegraph not available - install from '
61 73 'https://github.com/evanhempel/python-flamegraph'))
62 74 # developer config: profiling.freq
63 75 freq = ui.configint('profiling', 'freq', default=1000)
64 76 filter_ = None
65 77 collapse_recursion = True
66 78 thread = flamegraph.ProfileThread(fp, 1.0 / freq,
67 79 filter_, collapse_recursion)
68 80 start_time = util.timer()
69 81 try:
70 82 thread.start()
71 83 yield
72 84 finally:
73 85 thread.stop()
74 86 thread.join()
75 87 print('Collected %d stack frames (%d unique) in %2.2f seconds.' % (
76 88 util.timer() - start_time, thread.num_frames(),
77 89 thread.num_frames(unique=True)))
78 90
79 91 @contextlib.contextmanager
80 92 def statprofile(ui, fp):
81 93 from . import statprof
82 94
83 95 freq = ui.configint('profiling', 'freq', default=1000)
84 96 if freq > 0:
85 97 # Cannot reset when profiler is already active. So silently no-op.
86 98 if statprof.state.profile_level == 0:
87 99 statprof.reset(freq)
88 100 else:
89 101 ui.warn(_("invalid sampling frequency '%s' - ignoring\n") % freq)
90 102
91 103 statprof.start(mechanism='thread')
92 104
93 105 try:
94 106 yield
95 107 finally:
96 108 data = statprof.stop()
97 109
98 110 profformat = ui.config('profiling', 'statformat', 'hotpath')
99 111
100 112 formats = {
101 113 'byline': statprof.DisplayFormats.ByLine,
102 114 'bymethod': statprof.DisplayFormats.ByMethod,
103 115 'hotpath': statprof.DisplayFormats.Hotpath,
104 116 'json': statprof.DisplayFormats.Json,
105 117 'chrome': statprof.DisplayFormats.Chrome,
106 118 }
107 119
108 120 if profformat in formats:
109 121 displayformat = formats[profformat]
110 122 else:
111 123 ui.warn(_('unknown profiler output format: %s\n') % profformat)
112 124 displayformat = statprof.DisplayFormats.Hotpath
113 125
114 126 kwargs = {}
115 127
116 128 def fraction(s):
117 129 if s.endswith('%'):
118 130 v = float(s[:-1]) / 100
119 131 else:
120 132 v = float(s)
121 133 if 0 <= v <= 1:
122 134 return v
123 135 raise ValueError(s)
124 136
125 137 if profformat == 'chrome':
126 138 showmin = ui.configwith(fraction, 'profiling', 'showmin', 0.005)
127 139 showmax = ui.configwith(fraction, 'profiling', 'showmax', 0.999)
128 140 kwargs.update(minthreshold=showmin, maxthreshold=showmax)
129 141
130 142 statprof.display(fp, data=data, format=displayformat, **kwargs)
131 143
132 144 @contextlib.contextmanager
133 145 def profile(ui):
134 146 """Start profiling.
135 147
136 148 Profiling is active when the context manager is active. When the context
137 149 manager exits, profiling results will be written to the configured output.
138 150 """
139 151 profiler = encoding.environ.get('HGPROF')
152 proffn = None
140 153 if profiler is None:
141 154 profiler = ui.config('profiling', 'type', default='stat')
142 155 if profiler not in ('ls', 'stat', 'flame'):
143 ui.warn(_("unrecognized profiler '%s' - ignored\n") % profiler)
144 profiler = 'stat'
156 # try load profiler from extension with the same name
157 proffn = _loadprofiler(ui, profiler)
158 if proffn is None:
159 ui.warn(_("unrecognized profiler '%s' - ignored\n") % profiler)
160 profiler = 'stat'
145 161
146 162 output = ui.config('profiling', 'output')
147 163
148 164 if output == 'blackbox':
149 165 fp = util.stringio()
150 166 elif output:
151 167 path = ui.expandpath(output)
152 168 fp = open(path, 'wb')
153 169 else:
154 170 fp = ui.ferr
155 171
156 172 try:
157 if profiler == 'ls':
173 if proffn is not None:
174 pass
175 elif profiler == 'ls':
158 176 proffn = lsprofile
159 177 elif profiler == 'flame':
160 178 proffn = flameprofile
161 179 else:
162 180 proffn = statprofile
163 181
164 182 with proffn(ui, fp):
165 183 yield
166 184
167 185 finally:
168 186 if output:
169 187 if output == 'blackbox':
170 188 val = 'Profile:\n%s' % fp.getvalue()
171 189 # ui.log treats the input as a format string,
172 190 # so we need to escape any % signs.
173 191 val = val.replace('%', '%%')
174 192 ui.log('profile', val)
175 193 fp.close()
176 194
177 195 @contextlib.contextmanager
178 196 def maybeprofile(ui):
179 197 """Profile if enabled, else do nothing.
180 198
181 199 This context manager can be used to optionally profile if profiling
182 200 is enabled. Otherwise, it does nothing.
183 201
184 202 The purpose of this context manager is to make calling code simpler:
185 203 just use a single code path for calling into code you may want to profile
186 204 and this function determines whether to start profiling.
187 205 """
188 206 if ui.configbool('profiling', 'enabled'):
189 207 with profile(ui):
190 208 yield
191 209 else:
192 210 yield
@@ -1,101 +1,149
1 1 test --time
2 2
3 3 $ hg --time help -q help 2>&1 | grep time > /dev/null
4 4 $ hg init a
5 5 $ cd a
6 6
7 7 #if lsprof
8 8
9 9 test --profile
10 10
11 11 $ prof='hg --config profiling.type=ls --profile'
12 12
13 13 $ $prof st 2>../out
14 14 $ grep CallCount ../out > /dev/null || cat ../out
15 15
16 16 $ $prof --config profiling.output=../out st
17 17 $ grep CallCount ../out > /dev/null || cat ../out
18 18
19 19 $ $prof --config profiling.output=blackbox --config extensions.blackbox= st
20 20 $ grep CallCount .hg/blackbox.log > /dev/null || cat .hg/blackbox.log
21 21
22 22 $ $prof --config profiling.format=text st 2>../out
23 23 $ grep CallCount ../out > /dev/null || cat ../out
24 24
25 25 $ echo "[profiling]" >> $HGRCPATH
26 26 $ echo "format=kcachegrind" >> $HGRCPATH
27 27
28 28 $ $prof st 2>../out
29 29 $ grep 'events: Ticks' ../out > /dev/null || cat ../out
30 30
31 31 $ $prof --config profiling.output=../out st
32 32 $ grep 'events: Ticks' ../out > /dev/null || cat ../out
33 33
34 34 #endif
35 35
36 36 #if lsprof serve
37 37
38 38 Profiling of HTTP requests works
39 39
40 40 $ $prof --config profiling.format=text --config profiling.output=../profile.log serve -d -p $HGPORT --pid-file ../hg.pid -A ../access.log
41 41 $ cat ../hg.pid >> $DAEMON_PIDS
42 42 $ hg -q clone -U http://localhost:$HGPORT ../clone
43 43
44 44 A single profile is logged because file logging doesn't append
45 45 $ grep CallCount ../profile.log | wc -l
46 46 \s*1 (re)
47 47
48 48 #endif
49 49
50 50 Install an extension that can sleep and guarantee a profiler has time to run
51 51
52 52 $ cat >> sleepext.py << EOF
53 53 > import time
54 54 > from mercurial import registrar, commands
55 55 > cmdtable = {}
56 56 > command = registrar.command(cmdtable)
57 57 > @command('sleep', [], 'hg sleep')
58 58 > def sleep(ui, *args, **kwargs):
59 59 > time.sleep(0.1)
60 60 > EOF
61 61
62 62 $ cat >> $HGRCPATH << EOF
63 63 > [extensions]
64 64 > sleep = `pwd`/sleepext.py
65 65 > EOF
66 66
67 67 statistical profiler works
68 68
69 69 $ hg --profile sleep 2>../out
70 70 $ grep Sample ../out
71 71 Sample count: \d+ (re)
72 72
73 73 Various statprof formatters work
74 74
75 75 $ hg --profile --config profiling.statformat=byline sleep 2>../out
76 76 $ head -n 1 ../out
77 77 % cumulative self
78 78 $ grep Sample ../out
79 79 Sample count: \d+ (re)
80 80
81 81 $ hg --profile --config profiling.statformat=bymethod sleep 2>../out
82 82 $ head -n 1 ../out
83 83 % cumulative self
84 84 $ grep Sample ../out
85 85 Sample count: \d+ (re)
86 86
87 87 $ hg --profile --config profiling.statformat=hotpath sleep 2>../out
88 88 $ grep Sample ../out
89 89 Sample count: \d+ (re)
90 90
91 91 $ hg --profile --config profiling.statformat=json sleep 2>../out
92 92 $ cat ../out
93 93 \[\[-?\d+.* (re)
94 94
95 95 statprof can be used as a standalone module
96 96
97 97 $ $PYTHON -m mercurial.statprof hotpath
98 98 must specify --file to load
99 99 [1]
100 100
101 101 $ cd ..
102
103 profiler extension could be loaded before other extensions
104
105 $ cat > fooprof.py <<EOF
106 > from __future__ import absolute_import
107 > import contextlib
108 > @contextlib.contextmanager
109 > def profile(ui, fp):
110 > print('fooprof: start profile')
111 > yield
112 > print('fooprof: end profile')
113 > def extsetup(ui):
114 > ui.write('fooprof: loaded\n')
115 > EOF
116
117 $ cat > otherextension.py <<EOF
118 > from __future__ import absolute_import
119 > def extsetup(ui):
120 > ui.write('otherextension: loaded\n')
121 > EOF
122
123 $ hg init b
124 $ cd b
125 $ cat >> .hg/hgrc <<EOF
126 > [extensions]
127 > other = $TESTTMP/otherextension.py
128 > fooprof = $TESTTMP/fooprof.py
129 > EOF
130
131 $ hg root
132 otherextension: loaded
133 fooprof: loaded
134 $TESTTMP/b (glob)
135 $ HGPROF=fooprof hg root --profile
136 fooprof: loaded
137 fooprof: start profile
138 otherextension: loaded
139 $TESTTMP/b (glob)
140 fooprof: end profile
141
142 $ HGPROF=other hg root --profile 2>&1 | head -n 2
143 otherextension: loaded
144 unrecognized profiler 'other' - ignored
145
146 $ HGPROF=unknown hg root --profile 2>&1 | head -n 1
147 unrecognized profiler 'unknown' - ignored
148
149 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now