Commit Graph

30 Commits

Author SHA1 Message Date
Boris Feld
533f8e99c5 configitems: register the 'profiling.type' config 2017-06-30 03:44:00 +02:00
Boris Feld
c2d0e63222 configitems: register the 'profiling.showmax' config 2017-06-30 03:43:56 +02:00
Jun Wu
e47f7dc2fa codemod: register core configitems using a script
This is done by a script [2] using RedBaron [1], a tool designed for doing
code refactoring. All "default" values are decided by the script and are
strongly consistent with the existing code.

There are 2 changes done manually to fix tests:

  [warn] mercurial/exchange.py: experimental.bundle2-output-capture: default needs manual removal
  [warn] mercurial/localrepo.py: experimental.hook-track-tags: default needs manual removal

Since RedBaron is not confident about how to indent things [2].

[1]: https://github.com/PyCQA/redbaron
[2]: https://github.com/PyCQA/redbaron/issues/100
[3]:

#!/usr/bin/env python
# codemod_configitems.py - codemod tool to fill configitems
#
# Copyright 2017 Facebook, Inc.
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2 or any later version.
from __future__ import absolute_import, print_function

import os
import sys

import redbaron

def readpath(path):
    with open(path) as f:
        return f.read()

def writepath(path, content):
    with open(path, 'w') as f:
        f.write(content)

_configmethods = {'config', 'configbool', 'configint', 'configbytes',
                  'configlist', 'configdate'}

def extractstring(rnode):
    """get the string from a RedBaron string or call_argument node"""
    while rnode.type != 'string':
        rnode = rnode.value
    return rnode.value[1:-1]  # unquote, "'str'" -> "str"

def uiconfigitems(red):
    """match *.ui.config* pattern, yield (node, method, args, section, name)"""
    for node in red.find_all('atomtrailers'):
        entry = None
        try:
            obj = node[-3].value
            method = node[-2].value
            args = node[-1]
            section = args[0].value
            name = args[1].value
            if (obj in ('ui', 'self') and method in _configmethods
                and section.type == 'string' and name.type == 'string'):
                entry = (node, method, args, extractstring(section),
                         extractstring(name))
        except Exception:
            pass
        else:
            if entry:
                yield entry

def coreconfigitems(red):
    """match coreconfigitem(...) pattern, yield (node, args, section, name)"""
    for node in red.find_all('atomtrailers'):
        entry = None
        try:
            args = node[1]
            section = args[0].value
            name = args[1].value
            if (node[0].value == 'coreconfigitem' and section.type == 'string'
                and name.type == 'string'):
                entry = (node, args, extractstring(section),
                         extractstring(name))
        except Exception:
            pass
        else:
            if entry:
                yield entry

def registercoreconfig(cfgred, section, name, defaultrepr):
    """insert coreconfigitem to cfgred AST

    section and name are plain string, defaultrepr is a string
    """
    # find a place to insert the "coreconfigitem" item
    entries = list(coreconfigitems(cfgred))
    for node, args, nodesection, nodename in reversed(entries):
        if (nodesection, nodename) < (section, name):
            # insert after this entry
            node.insert_after(
                'coreconfigitem(%r, %r,\n'
                '    default=%s,\n'
                ')' % (section, name, defaultrepr))
            return

def main(argv):
    if not argv:
        print('Usage: codemod_configitems.py FILES\n'
              'For example, FILES could be "{hgext,mercurial}/*/**.py"')
    dirname = os.path.dirname
    reporoot = dirname(dirname(dirname(os.path.abspath(__file__))))

    # register configitems to this destination
    cfgpath = os.path.join(reporoot, 'mercurial', 'configitems.py')
    cfgred = redbaron.RedBaron(readpath(cfgpath))

    # state about what to do
    registered = set((s, n) for n, a, s, n in coreconfigitems(cfgred))
    toregister = {} # {(section, name): defaultrepr}
    coreconfigs = set() # {(section, name)}, whether it's used in core

    # first loop: scan all files before taking any action
    for i, path in enumerate(argv):
        print('(%d/%d) scanning %s' % (i + 1, len(argv), path))
        iscore = ('mercurial' in path) and ('hgext' not in path)
        red = redbaron.RedBaron(readpath(path))
        # find all repo.ui.config* and ui.config* calls, and collect their
        # section, name and default value information.
        for node, method, args, section, name in uiconfigitems(red):
            if section == 'web':
                # [web] section has some weirdness, ignore them for now
                continue
            defaultrepr = None
            key = (section, name)
            if len(args) == 2:
                if key in registered:
                    continue
                if method == 'configlist':
                    defaultrepr = 'list'
                elif method == 'configbool':
                    defaultrepr = 'False'
                else:
                    defaultrepr = 'None'
            elif len(args) >= 3 and (args[2].target is None or
                                     args[2].target.value == 'default'):
                # try to understand the "default" value
                dnode = args[2].value
                if dnode.type == 'name':
                    if dnode.value in {'None', 'True', 'False'}:
                        defaultrepr = dnode.value
                elif dnode.type == 'string':
                    defaultrepr = repr(dnode.value[1:-1])
                elif dnode.type in ('int', 'float'):
                    defaultrepr = dnode.value
            # inconsistent default
            if key in toregister and toregister[key] != defaultrepr:
                defaultrepr = None
            # interesting to rewrite
            if key not in registered:
                if defaultrepr is None:
                    print('[note] %s: %s.%s: unsupported default'
                          % (path, section, name))
                    registered.add(key) # skip checking it again
                else:
                    toregister[key] = defaultrepr
                    if iscore:
                        coreconfigs.add(key)

    # second loop: rewrite files given "toregister" result
    for path in argv:
        # reconstruct redbaron - trade CPU for memory
        red = redbaron.RedBaron(readpath(path))
        changed = False
        for node, method, args, section, name in uiconfigitems(red):
            key = (section, name)
            defaultrepr = toregister.get(key)
            if defaultrepr is None or key not in coreconfigs:
                continue
            if len(args) >= 3 and (args[2].target is None or
                                   args[2].target.value == 'default'):
                try:
                    del args[2]
                    changed = True
                except Exception:
                    # redbaron fails to do the rewrite due to indentation
                    # see https://github.com/PyCQA/redbaron/issues/100
                    print('[warn] %s: %s.%s: default needs manual removal'
                          % (path, section, name))
            if key not in registered:
                print('registering %s.%s' % (section, name))
                registercoreconfig(cfgred, section, name, defaultrepr)
                registered.add(key)
        if changed:
            print('updating %s' % path)
            writepath(path, red.dumps())

    if toregister:
        print('updating configitems.py')
        writepath(cfgpath, cfgred.dumps())

if __name__ == "__main__":
    sys.exit(main(sys.argv[1:]))
2017-07-14 14:22:40 -07:00
Gregory Szorc
e4b6d44784 check-config: syntax to allow inconsistent config values
The ignore regular expression has been updated to detect
"inconsistent config." If present, we track which configs have
that set and we suppress the conflicting defaults error for those
options.

I also added named groups to the regexp to aid readability.

A comment was added to profiling.py to make a desired inconsistent
value error go away.
2017-07-01 20:34:27 -07:00
Pierre-Yves David
447cbfab6f profiling: cope with configwith default value handling changes
Changeset 3d003a7a1a87 change 'configwith' behavior so that the default value is
run through the conversion function. In parallel a new user of 'configwith' got
introduced unaware of this coming behavior change. This broke profiling.

We resolve the situation by having the new conversion function cope with a
default value already using the right type.
2017-06-21 10:46:18 +02:00
Gregory Szorc
768455e422 profiling: allow configuring minimum display threshold for hotpath
statprof.display_hotpath() accepts a "limit" function to choose the
minimum threshold for samples to display. The default is 0.05, which
means you don't need individual items contributing less than 5%.

I had a need to adjust this threshold. We already have a config
option for it. So let's reuse it.

check-config.py doesn't like having multiple defaults for the
ui.configwith() calls. The behavior is obviously correct. I'm
not sure if it is worth teaching check-config.py how to ignore
this. So I've just accepted the new output.
2017-06-15 11:10:51 -07:00
Pierre-Yves David
d38cc2c694 profile: properly propagate exception from the sub-context manager
Context manager has a mechanism to control extension propagation. It is not
used by profiling right now, but making the code correct will help prevent bug
in the future.
2017-06-12 17:24:10 +02:00
Pierre-Yves David
0235063e7c profile: close 'fp' in all cases
There are no way for this to happen today, but better be safe than sorry, no
one know how the code will evolve. We now make sure the file pointer is closed
even is profiler is None.
2017-06-12 17:25:37 +02:00
Pierre-Yves David
cbbef9a3be profile: close 'fp' on error within '__enter__'
Previously, error when initialying the profiler would forgot to explicitly
close the file. Thank goes to Yuya Nishihara for spotting this.
2017-06-12 17:21:41 +02:00
Pierre-Yves David
f122fd3d17 profile: indent part of '__enter__'
We are about to introduce a try except. We start with adding the indent to make
the next patch clearer.
2017-06-12 17:20:48 +02:00
Pierre-Yves David
5cc8016d8b profile: remove now useless indent
We no longer rely on the value of '_output' so we can remove this conditional.
2017-06-12 17:15:43 +02:00
Pierre-Yves David
8e927eb83d profile: use explicit logic to control file closing
We make the decision to close 'fp' more explicit instead of relying on the
implication of other variable. This makes the overall logic more robust.
2017-06-12 17:14:56 +02:00
Pierre-Yves David
cb625340da profiling: move 'fp' closing logic into its own function
We are about to make the logic more robust and reuse it in more place, we start
by isolating what we have.
2017-06-12 17:13:35 +02:00
Pierre-Yves David
2fdfd87513 profile: drop maybeprofile
It seems sufficiently simple to use "profile(enabled=X)" to not justify having
a dedicated context manager just to read the config.

(I do not have a too strong opinion about this).
2017-06-09 12:29:29 +01:00
Pierre-Yves David
4dacb46437 profile: make the contextmanager object available to the callers
This will allow calling methods on the object in the code using the context
manager.
2017-06-09 11:42:45 +01:00
Pierre-Yves David
39ee1a16c0 profile: introduce a knob to control if the context is actually profiling
This is a step toward allowing context where the profiling in enabled
withing the context range.

This also open the way to kill the dedicated "maybeprofile" context manager
and keep only one of 'profile' and 'maybeprofile'.
2017-06-09 11:41:47 +01:00
Pierre-Yves David
669f0c9a69 profile: introduce a "start" method to the profile context
The start method is doing all profiler setup and activation. It is currently
unconditionally called by '__init__' but this will be made more flexible in
later changesets.
2017-06-09 11:39:53 +01:00
Pierre-Yves David
1999d809be profile: upgrade the "profile" context manager to a full class
So far we have been able to use a simple decorator for this. However using the
current context manager makes the scope of the profiling in dispatch
constrainted and the time frame to decide to enable profiling quite limited
(using "maybeprofile")

This is the first step toward the ability to enable the profiling from within
the profiling scope. eg::

  with maybeprofiling(ui) as profiler:
      ...
      bar.foo():
      ...
      if options['profile']:
          profiler.start()
      ...
      fooz()
      ...

My target usecase is adding support for  "--profile" to alias definitions with
effect.  These are to be used with "profiling.output=blackbox" to gather data
about operation that get slow from time to time (eg: pull being minutes instead
of seconds from time to time).

Of course, in such case, the scope of the profiling would be smaller since
profiler would be started after running extensions 'reposetup' (and other
potentially costly logic), but these are not relevant for my target usecase
(multiple second commits, multiple tens of seconds pull).

Currently adding '--profile' to a command through alias requires to re-spin a
Mercurial binary (using "!$HG" in alias), which as a significant performance
impact, especially in context where startup performance is being worked on...

An alternative approach would be to stop using the context manager in dispatch
and move back to a try/finally setup.
2017-06-08 01:38:48 +01:00
Jun Wu
610c5f87a9 profiling: allow loading profiling extension before everything else
459366b580cf makes profiler start early without loading extensions. That
makes it impossible for an extension to add customized profilers.

This patch adds a special case: if a profiler is not found but an extension
with the same name could be loaded, load that extension first, and expect it
to have a "profile" contextmanager method. This allows customized profilers
and extension setup time is still profiled.
2017-05-22 01:17:49 -07:00
Simon Farnsworth
e0b70e4f7f mercurial: switch to util.timer for all interval timings
util.timer is now the best available interval timer, at the expense of not
having a known epoch. Let's use it whenever the epoch is irrelevant.
2017-02-15 13:17:39 -08:00
Bryan O'Sullivan
4b315061e0 profiling: add statprof support for Chrome trace viewer rendering
We synthesize function call begin/end events from snapshots, and
try (configurably) to eliminate "noisy" stack frames.

Example invocation:

hg --config profiling.output=$HOME/Desktop/clone.json \
   --config profiling.statformat=chrome \
   --profile clone https://www.mercurial-scm.org/repo/hg
2017-02-12 22:28:09 -08:00
Pulkit Goyal
3c7388da12 py3: replace pycompat.getenv with encoding.environ.get
pycompat.getenv returns os.getenvb on py3 which is not available on Windows.
This patch replaces them with encoding.environ.get and checks to ensure no
new instances of os.getenv or os.setenv are introduced.
2017-01-15 13:17:05 +05:30
Pulkit Goyal
770a0e2938 py3: replace os.getenv with pycompat.osgetenv
os.getenv deals with unicodes on Python 3, so we have pycompat.osgetenv to
deal with bytes. This patch replaces occurrences on os.getenv with
pycompat.osgetenv
2016-12-19 02:54:49 +05:30
Yuya Nishihara
e23e6c94ff profiling: obtain stderr from ui
This will help Python 3 porting.
2016-10-20 22:07:03 +09:00
Gregory Szorc
d1e86f5ae3 profiling: make statprof the default profiler (BC)
The statprof sampling profiler runs with significantly less overhead.
Its data is therefore more useful. Furthermore, its default output
shows the hotpath by default, which I've found to be way more useful
than the default profiler's function time table.

There is one behavioral regression with this change worth noting:
the statprof profiler currently doesn't profile individual hgweb
requests like lsprof does. This is because the current implementation
of statprof only profiles the thread that started profiling.

The ability for lsprof to profile individual hgweb requests is
relatively new and likely not widely used. Furthermore, I have plans
to modify statprof to support profiling multiple threads. I expect
that change to go through several iterations. I'm submitting this
patch first so there is more time to test statprof. Perfect is the
enemy of good.
2016-11-04 21:44:25 -07:00
Gregory Szorc
c60af834ba profiling: use vendored statprof and upstream enhancements (BC)
Now that the statprof module is vendored and suitable for use, we
switch our statprof profiler to use it. This required some minor
changes because of drift between the official statprof profiler
and the vendored copy.

We also incorporate Facebook's improvements from the "statprofext"
extension at
https://bitbucket.org/facebook/hg-experimental, notably support for
different display formats.

Because statprof output is different, this is marked as BC. Although
most users likely won't notice since most users don't profile.
2016-11-04 20:50:38 -07:00
Gregory Szorc
e6e0818daa profiling: don't error with statprof when profiling has already started
statprof.reset() asserts if profiling has already started. So don't
call if it profiling is already running.
2016-08-14 18:28:43 -07:00
Gregory Szorc
9ac5776ef7 profiling: add a context manager that no-ops if profiling isn't enabled
And refactor dispatch.py to use it. As you can see, the resulting code
is much simpler.

I was tempted to inline _runcommand as part of writing this series.
However, a number of extensions wrap _runcommand. So keeping it around
is necessary (extensions can't easily wrap runcommand because it calls
hooks before and after command execution).
2016-08-14 17:51:12 -07:00
Gregory Szorc
fbd4d1a639 profiling: make profiling functions context managers (API)
This makes profiling more flexible since we can now call multiple
functions when a profiler is active. But the real reason for this
is to enable a future consumer to profile a function that returns
a generator. We can't do this from the profiling function itself
because functions can either be generators or have return values:
they can't be both. So therefore it isn't possible to have a generic
profiling function that can both consume and re-emit a generator
and return a value.
2016-08-14 18:25:22 -07:00
Gregory Szorc
bdb3786ca0 profiling: move profiling code from dispatch.py (API)
Currently, profiling code lives in dispatch.py, which is a low-level
module centered around command dispatch. Furthermore, dispatch.py
imports a lot of other modules, meaning that importing dispatch.py
to get at profiling functionality would often result in a module import
cycle.

Profiling is a generic activity. It shouldn't be limited to command
dispatch. This patch moves profiling code from dispatch.py to the
new profiling.py. The low-level "run a profiler against a function"
functions have been moved verbatim. The code for determining how to
invoke the profiler has been extracted to its own function.

I decided to create a new module rather than stick this code
elsewhere (such as util.py) because util.py is already quite large.
And, I foresee this file growing larger once Facebook's profiling
enhancements get added to it.
2016-08-14 16:30:44 -07:00