Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Standalone #27

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ Or with a contextmanager : ::
with statprof.profile():
my_questionable_function()

Or as a separate executable: ::

statprof my_questionable_script

The profiler can be invoked at more than one place inside your code and will
report its findings for all of them at once at the end: ::

Expand Down
3 changes: 3 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,8 @@ def read(fname):
"Operating System :: Unix",
"Topic :: Utilities",
],
entry_points={
'console_scripts': ['statprof=statprof:main']
},
**extra
)
149 changes: 121 additions & 28 deletions statprof.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,8 @@
import signal
import sys

from collections import defaultdict
from argparse import ArgumentParser
from collections import defaultdict, namedtuple
from contextlib import contextmanager


Expand Down Expand Up @@ -285,15 +286,33 @@ def reset(frequency=None):
state.reset(frequency)


class DisplayFormats:
ByLine = 0
ByMethod = 1
CSV = 2


class SortOrders:
BySelf = 0
ByCumulative = 1


def sort_key(ordering):
if ordering == SortOrders.BySelf:
return lambda x: x.self_secs_in_proc
elif ordering == SortOrders.ByCumulative:
return lambda x: x.cum_secs_in_proc


@contextmanager
def profile(verbose=True):
def profile(verbose=True, fp=None, format=DisplayFormats.ByLine, sort_order=SortOrders.BySelf):
start()
try:
yield
finally:
stop()
if verbose:
display()
display(fp, format, sort_order)


###########################################################################
Expand All @@ -311,10 +330,11 @@ def __init__(self, call_data):
self.filepath = call_data.key.filename
self.filename = basename
self.function = call_data.key.name
self.name = '%s:%d:%s' % (self.filename, self.lineno, self.function)
self.name = '%s:%d:%s' % (self.filepath, self.lineno, self.function)
self.pcnt_time_in_proc = self_samples / nsamples * 100
self.cum_secs_in_proc = cum_samples * secs_per_sample
self.self_secs_in_proc = self_samples * secs_per_sample
self.cum_time_in_proc = cum_samples / nsamples * 100
self.cum_secs_in_proc = cum_samples * secs_per_sample
self.num_calls = None
self.self_secs_per_call = None
self.cum_secs_per_call = None
Expand All @@ -326,12 +346,7 @@ def display(self, fp):
self.name))


class DisplayFormats:
ByLine = 0
ByMethod = 1


def display(fp=None, format=0):
def display(fp=None, format=DisplayFormats.ByLine, sort_order=SortOrders.BySelf):
'''Print statistics, either to stdout or the given file object.'''

if fp is None:
Expand All @@ -342,9 +357,11 @@ def display(fp=None, format=0):
return

if format == DisplayFormats.ByLine:
display_by_line(fp)
display_by_line(fp, sort_order=sort_order)
elif format == DisplayFormats.ByMethod:
display_by_method(fp)
display_by_method(fp, sort_order=sort_order)
elif format == DisplayFormats.CSV:
display_by_csv(fp, sort_order=sort_order)
else:
raise Exception("Invalid display format")

Expand All @@ -353,11 +370,11 @@ def display(fp=None, format=0):
fp.write('Total time: %f seconds\n' % state.accumulated_time)


def display_by_line(fp):
def display_by_line(fp, sort_order):
'''Print the profiler data with each sample line represented
as one row in a table. Sorted by self-time per line.'''
l = [CallStats(x) for x in _itervalues(CallData.all_calls)]
l.sort(reverse=True, key=lambda x: x.self_secs_in_proc)
l.sort(reverse=True, key=sort_key(sort_order))

fp.write('%5.5s %10.10s %7.7s %-8.8s\n' %
('% ', 'cumulative', 'self', ''))
Expand All @@ -384,7 +401,11 @@ def get_line_source(filename, lineno):

return ""

def display_by_method(fp):

FunctionData = namedtuple('FunctionData', 'fname, cum_secs_in_proc, self_secs_in_proc, pcnt_time_in_proc, samples')


def display_by_method(fp, sort_order):
'''Print the profiler data with each sample function represented
as one row in a table. Important lines within that function are
output as nested rows. Sorted by self-time per line.'''
Expand All @@ -397,7 +418,7 @@ def display_by_method(fp):

grouped = defaultdict(list)
for call in calldata:
grouped[call.filename + ":" + call.function].append(call)
grouped[call.filepath + ":" + call.function].append(call)

# compute sums for each function
functiondata = []
Expand All @@ -409,21 +430,23 @@ def display_by_method(fp):
total_cum_sec += sample.cum_secs_in_proc
total_self_sec += sample.self_secs_in_proc
total_percent += sample.pcnt_time_in_proc
functiondata.append((fname,
total_cum_sec,
total_self_sec,
total_percent,
samples))
functiondata.append(FunctionData(
fname,
total_cum_sec,
total_self_sec,
total_percent,
samples
))

# sort by total self sec
functiondata.sort(reverse=True, key=lambda x: x[2])
functiondata.sort(reverse=True, key=sort_key(sort_order))

for function in functiondata:
fp.write('%6.2f %9.2f %9.2f %s\n' % (function[3], # total percent
function[1], # total cum sec
function[2], # total self sec
function[0])) # file:function
function[4].sort(reverse=True, key=lambda i: i.self_secs_in_proc)
fp.write('%6.2f %9.2f %9.2f %s\n' % (function.pcnt_time_in_proc,
function.cum_secs_in_proc,
function.self_secs_in_proc,
function.fname)) # file:function
function[4].sort(reverse=True, key=sort_key(sort_order))
for call in function[4]:
# only show line numbers for significant locations ( > 1% time spent)
if call.pcnt_time_in_proc > 1:
Expand All @@ -435,3 +458,73 @@ def display_by_method(fp):
call.self_secs_in_proc,
call.lineno,
source))

def display_by_csv(fp, sort_order):
import csv
writer = csv.writer(fp)
writer.writerow((
"File Path",
"Line Number",
"Function",
"Self (%)",
"Self (sec)",
"Cumulative (%)",
"Cumulative (sec)",
))
for row in sorted(
(CallStats(x) for x in CallData.all_calls.itervalues()),
key=sort_key(sort_order),
reverse=True
):
writer.writerow((
row.filepath,
row.lineno,
row.function,
row.pcnt_time_in_proc,
row.self_secs_in_proc,
row.cum_time_in_proc,
row.cum_secs_in_proc,
))


def main(args=sys.argv):
parser = ArgumentParser()
parser.add_argument('progname', type=str)

formats = {
'line': DisplayFormats.ByLine,
'method': DisplayFormats.ByMethod,
'csv': DisplayFormats.CSV,
}
parser.add_argument('--format', type=str, choices=formats, default='line')
parser.add_argument('--outfile', type=str, default=None)

sorts = {
'cum': SortOrders.ByCumulative,
'self': SortOrders.BySelf,
}
parser.add_argument('--sort', type=str, choices=sorts, default='self')
parser.add_argument('--quiet', action='store_true')
opts, rest = parser.parse_known_args(args[1:])

sys.path.insert(0, os.path.dirname(opts.progname))
with open(opts.progname, 'rb') as fp:
code = compile(fp.read(), opts.progname, 'exec')
globs = {
'__file__': opts.progname,
'__name__': '__main__',
'__package__': None,
}

if opts.outfile is None:
outfile = sys.stdout
else:
outfile = open(opts.outfile('w'))
Copy link

@simaoafonso-pwt simaoafonso-pwt Oct 30, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an error, I think you meant:

outfile = open(opts.outfile, 'w')


with profile(not opts.quiet, outfile, formats[opts.format], sorts[opts.sort]):
sys.argv = [opts.progname] + rest
exec code in globs, None


if __name__ == "__main__":
sys.exit(main(sys.argv))