Profilando un sistema con decoratori ampiamente riutilizzati
Domanda
La nostra base di codice ha alcuni decoratori che vengono ampiamente utilizzati.
Quando creo un profilo di runtime, gran parte del grafico delle chiamate sembra un vetro di un'ora; Molte funzioni chiamano una funzione (il decoratore), che quindi chiama molte funzioni. Questo è un profilo meno utili di quanto vorrei.
C'è un modo per correggere questa situazione? Rimuovere il decoratore non è un'opzione; Fornisce funzionalità necessarie.
Dopo il fatto, abbiamo preso in considerazione di spogliarci manualmente del decoratore dai dati CProfile, ma non sembra possibile, perché i dati sono riassunti in relazioni chiamanti, che distrugge la relazione Caller-> Decorator-> Calee.
Soluzione
Usando qualcosa di simile al new
biblioteca (o types
In Python 2.6+), è possibile creare teoricamente in modo dinamico un oggetto di codice e quindi un oggetto di funzione basato su quell'oggetto di codice che aveva un nome incorporato che variava insieme alla funzione che stavi avvolgendo.
Ciò ti permetterebbe di manipolare cose profonde come <func>.__code__.co_name
(che normalmente è di sola lettura).
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
è ancora usato qui per consentire il pass-through di cose come documenti, nomi di moduli, ecc.)
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
Altri suggerimenti
Immagino che non è il decoratore stesso che si ingombra la tua profilazione, ma piuttosto il funzione wrapper Creato dal decoratore. E questo sta accadendo perché tutte le funzioni wrapper hanno lo stesso nome. Per affrontare questo, basta cambiare il decoratore il nome della funzione wrapper.
def decorator(func):
def wrapper(*args):
print "enter func", func.__name__
return func(*args)
wrapper.__name__ += "_" + func.__name__
return wrapper
Potresti anche usare functools.wraps()
, ma poi il nome della funzione wrapper corrisponderà al nome della funzione che sta avvolgendo. Immagino che sarebbe ok per la profilazione.
Ora, l'oggetto del codice della funzione ha anche un nome. Python non memorizza i riferimenti alle funzioni sullo stack, solo per codificare gli oggetti, quindi se il profiler sta ottenendo il nome della funzione wrapper da un frame stack, otterrà questo nome. I wrapper definiti nel solito modo condividono l'oggetto del codice (anche se l'oggetto funzione è diverso) a meno che non si ricostruisca esplicitamente l'oggetto del codice e l'oggetto funzione per ciascuna funzione wrapper. Questo è un po 'più di lavoro e molto specifico per Cpython (potrebbe anche essere specifico per la versione). Ma ecco come potresti farlo:
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
Sia il nome della funzione che il nome dell'oggetto del codice sono impostati qui a wrapper_originalfuncname
e dovrebbero quindi essere contati separatamente dalla funzione avvolta nel profiler. Potresti facilmente impostarli solo sul nome della funzione originale in modo che il loro tempo di esecuzione venga invece inserito con la funzione originale.