Edgewall Software

Ticket #8507: 8507-pluggable-profiling-r8466.patch

File 8507-pluggable-profiling-r8466.patch, 7.7 KB (added by Remy Blank, 8 years ago)

Pluggable profiling infrastructure

  • trac/env.py

    diff --git a/trac/env.py b/trac/env.py
    a b  
    3030from trac.core import Component, ComponentManager, implements, Interface, \
    3131                      ExtensionPoint, TracError
    3232from trac.db import DatabaseManager
     33from trac.profile import IProfiler
    3334from trac.util import copytree, get_pkginfo, makedirs
    3435from trac.util.text import exception_to_unicode, printerr, printout
    3536from trac.util.translation import _
     
    7475     * wiki and ticket attachments.
    7576    """   
    7677    setup_participants = ExtensionPoint(IEnvironmentSetupParticipant)
     78   
     79    profilers = ExtensionPoint(IProfiler)
    7780
    7881    shared_plugins_dir = PathOption('inherit', 'plugins_dir', '',
    7982        """Path of the directory containing additional plugins.
     
    174177
    175178         (since 0.10.5)""")
    176179
     180    profiler = Option('profiling', 'profiler', 'CProfileProfiler',
     181        """Name of the component used for profiling the application.
     182        (''since 0.12'')""")
     183
     184    profile_main = BoolOption('profiling', 'profile_main', False,
     185        """Profile Trac's main request entry point. (''since 0.12'')""")
     186
    177187    def __init__(self, path, create=False, options=[]):
    178188        """Initialize the Trac environment.
    179189       
     
    286296            hdlr.close()
    287297            del self.log._trac_handler
    288298
     299    def get_profiler(self):
     300        """Return the configured profiler component."""
     301        try:
     302            return self._profiler
     303        except AttributeError:
     304            name = self.profiler
     305            for profiler in self.profilers:
     306                if profiler.__class__.__name__ == name:
     307                    break
     308            else:
     309                profiler = None
     310            self._profiler = profiler
     311            return profiler
     312
    289313    def get_repository(self, authname=None):
    290314        """Return the version control repository configured for this
    291315        environment.
  • new file trac/profile.py

    diff --git a/trac/profile.py b/trac/profile.py
    new file mode 100644
    - +  
     1# -*- coding: utf-8 -*-
     2#
     3# Copyright (C) 2009 Edgewall Software
     4# All rights reserved.
     5#
     6# This software is licensed as described in the file COPYING, which
     7# you should have received as part of this distribution. The terms
     8# are also available at http://trac.edgewall.com/license.html.
     9#
     10# This software consists of voluntary contributions made by many
     11# individuals. For the exact contribution history, see the revision
     12# history and logs, available at http://trac.edgewall.org/.
     13
     14import marshal
     15import os.path
     16import threading
     17import time
     18try:
     19    import cProfile
     20    import pstats
     21except ImportError:
     22    cProfile = None
     23    pstats = None
     24
     25from trac.admin.api import IAdminCommandProvider
     26from trac.config import Option
     27from trac.core import *
     28
     29
     30def IProfiler(Interface):
     31    """Extension point interface for profiling Trac and plugins."""
     32   
     33    def profile(self, *args, **kwargs):
     34        """Profile the callable in the first argument, passing the remaining
     35        arguments to the callable.
     36        """
     37
     38def profile(f):
     39    """Method decorator that enables profiling for a Component method."""
     40    def wrapper(*args, **kwargs):
     41        profiler = args[0].env.get_profiler()
     42        if profiler is not None:
     43            return profiler.profile(f, *args, **kwargs)
     44        return f(*args, **kwargs)
     45    return wrapper
     46
     47
     48class CProfileProfiler(Component):
     49    """A profiler based on the cProfile module."""
     50   
     51    abstract = cProfile is None
     52   
     53    implements(IProfiler, IAdminCommandProvider)
     54   
     55    log_dir = Option('profiling', 'log_dir', 'profiling',
     56        """Directory where profiling data should be stored.""")
     57
     58    def __init__(self):
     59        self.local = threading.local()
     60        self.lock = threading.Lock()
     61        self._log_file = None
     62   
     63    @property
     64    def log_file(self):
     65        if self._log_file is None:
     66            log_dir = self.log_dir
     67            if not os.path.isabs(log_dir):
     68                log_dir = os.path.join(self.env.path, log_dir)
     69            if not os.path.exists(log_dir):
     70                os.makedirs(log_dir)
     71            self._log_file = os.path.join(log_dir, "%s-%d.prof" % (
     72                time.strftime("%Y%m%d-%H%M%S"), os.getpid()))
     73        return self._log_file
     74
     75    # IProfiler methods
     76   
     77    def profile(self, *args, **kwargs):
     78        try:
     79            profile = self.local.profile
     80        except AttributeError:
     81            profile = None
     82        if profile is not None:
     83            return args[0](*args[1:], **kwargs)
     84       
     85        log_file = self.log_file
     86        profile = self.local.profile = Profile(subcalls=True, builtins=True)
     87        profile.enable()
     88        try:
     89            return args[0](*args[1:], **kwargs)
     90        finally:
     91            profile.disable()
     92            self.local.profile = None
     93            self._write_profile_data(profile, log_file)
     94   
     95    def _write_profile_data(self, profile, log_file):
     96        try:
     97            self.lock.acquire()
     98            try:
     99                profile.dump_stats(log_file)
     100            finally:
     101                self.lock.release()
     102        except Exception, e:
     103            self.log.error("Unable to write to profile log: %s"
     104                           % exception_to_unicode(e))
     105
     106    # IAdminCommandProvider methods
     107   
     108    def get_admin_commands(self):
     109        yield ('profile view', '<path> [...]',
     110               'View profile data',
     111               self._complete_view, self._do_view)
     112   
     113    def _complete_view(self, args):
     114        return get_dir_list(args[-1])
     115   
     116    def _do_view(self, *paths):
     117        stats = Stats()
     118        for path in paths:
     119            stats.add_multiple(path)
     120        stats.sort_stats('time')
     121        stats.print_stats(30)
     122
     123
     124if cProfile is not None:
     125    class Profile(cProfile.Profile):
     126        """A Profile subclass that appends its stats to an existing file."""
     127        def dump_stats(self, file):
     128            self.create_stats()
     129            f = open(file, "ab")
     130            try:
     131                marshal.dump(self.stats, f)
     132            finally:
     133                f.close()
     134   
     135   
     136    class Stats(pstats.Stats):
     137        """A Stats subclass that can read several statistics from a file."""
     138        def load_stats(self, arg):
     139            if arg is None:
     140                self.stats = {}
     141                self.files = []
     142            elif hasattr(arg, "read"):
     143                self.stats = marshal.load(arg)
     144                self.files = []
     145            else:
     146                _Stats.load_stats(self, arg)
     147       
     148        def add_multiple(self, path):
     149            f = open(path, "rb")
     150            try:
     151                try:
     152                    while True:
     153                        self.add(Stats(f))
     154                except (EOFError, ValueError, TypeError):
     155                    pass
     156            finally:
     157                f.close()
     158   
     159    # Monkey-patch original Stats class, because it instatiates itself
     160    _Stats = pstats.Stats
     161    pstats.Stats = Stats
  • trac/web/main.py

    diff --git a/trac/web/main.py b/trac/web/main.py
    a b  
    430430                                       environ['trac.web.version']))
    431431    except Exception, e:
    432432        env_error = e
     433   
     434    if env and env.profile_main:
     435        profiler = env.get_profiler()
     436        if profiler is not None:
     437            return profiler.profile(do_dispatch_request, environ,
     438                                    start_response, env, env_error, run_once)
     439    return do_dispatch_request(environ, start_response, env, env_error,
     440                               run_once)
    433441
     442def do_dispatch_request(environ, start_response, env, env_error, run_once):
    434443    req = Request(environ, start_response)
    435444    try:
    436445        return _dispatch_request(req, env, env_error)