Профилирование системы с использованием многократно используемых декораторов

В нашей базе кода есть несколько декораторов, которые широко используются.

Когда я создаю профиль времени выполнения, большая часть графика вызовов выглядит как часовое стекло; многие функции вызывают одну функцию (декоратор), которая затем вызывает много функций. Это менее полезный профиль, чем хотелось бы.

Есть ли способ исправить эту ситуацию? Удаление декоратора не является вариантом; он обеспечивает необходимую функциональность.

Мы рассмотрели возможность вручную удалить декоратор из данных cProfile после факта, но это не представляется возможным, поскольку данные суммируются в отношениях вызывающего абонентa → вызываемого абонента, что разрушает отношения caller- > decorator- > callee.

Ответ 1

Используя что-то вроде new (или types в Python 2.6+), вы могли бы теоретически динамически создать объект кода, а затем объект функции, основанный на этом объекте кода, который имел встроенное имя, которое варьировалось вместе с функцией, которую вы обертывали.

Это позволит вам манипулировать вещами так же глубоко, как <func>.__code__.co_name (который обычно доступен только для чтения).


import functools
import types

def metadec(func):

    @functools.wraps(func)
    def wrapper(*args, **kwargs):   
        # do stuff
        return func(*args, **kwargs)

    c = wrapper.func_code
    fname = "%s__%s" % (func.__name__, wrapper.__name__)

    code = types.CodeType(
                c.co_argcount, 
                c.co_nlocals,
                c.co_stacksize,
                c.co_flags,  
                c.co_code,        
                c.co_consts,         
                c.co_names,
                c.co_varnames,
                c.co_filename,
                fname, # change the name
                c.co_firstlineno,
                c.co_lnotab,
                c.co_freevars,
                c.co_cellvars,
            )

    return types.FunctionType(
            code, # Use our updated code object
            wrapper.func_globals,
            fname, # Use the updated name
            wrapper.func_defaults,
            wrapper.func_closure,
        )

(functools.wraps все еще используется здесь для того, чтобы разрешить прохождение таких вещей, как docstrings, имена модулей и т.д.)


In [1]: from metadec import metadec

In [2]: @metadec
   ...: def foobar(x):
   ...:     print(x)
   ...:     
   ...:     

In [3]: foobar.__name__
Out[3]: 'foobar__wrapper'

In [4]: foobar(1)
1

Ответ 2

Я собираюсь предположить, что это не сам декоратор, который загромождает профилирование, а скорее функцию обертки, созданную декоратором. И это происходит потому, что все функции обертки имеют одно и то же имя. Чтобы решить эту проблему, просто измените имя декоратора на имя обертки.

def decorator(func):

    def wrapper(*args):
        print "enter func", func.__name__
        return func(*args)

    wrapper.__name__ += "_" + func.__name__
    return wrapper

Вы также можете использовать functools.wraps(), но тогда имя функции-обертки будет соответствовать имени функции, которую она обертывает. Я думаю, это было бы хорошо для профилирования.

Теперь объект кода функции также имеет имя. Python не хранит ссылки на функции в стеке, а только на объекты кода, поэтому, если профилировщик получает имя функции-обертки из фрейма стека, он получит это имя. Обертки, определенные обычным способом, совместно используют объект кода (даже если объект функции отличается), если вы явно не перестраиваете объект кода и объект функции для каждой функции-обертки. Это довольно большая работа и очень специфичная для CPython (может даже быть специфичной для версии). Но вот как вы можете это сделать:

from types import FunctionType, CodeType    

def decorator(func):

    def wrapper(*args):
        print "enter func", func.__name__
        return func(*args)

    name = wrapper.__name__ + "_" + func.__name__

    func_code = wrapper.func_code
    new_code  = CodeType(
            func_code.co_argcount, func_code.co_nlocals, func_code.co_stacksize,
            func_code.co_flags, func_code.co_code, func_code.co_consts,
            func_code.co_names, func_code.co_varnames, func_code.co_filename,
            name, func_code.co_firstlineno, func_code.co_lnotab,
            func_code.co_freevars, func_code.co_cellvars)
    wrapper   = FunctionType(
            new_code, wrapper.func_globals, name, wrapper.func_defaults,
            wrapper.func_closure)

    return wrapper

И имя функции, и имя объекта кода здесь установлены на wrapper_originalfuncname, и поэтому их следует учитывать отдельно от обернутой функции в профилировщике. Вы можете легко установить их только в исходное имя функции, чтобы их время выполнения было включено в исходную функцию.