Commit f7f811be by Jesse Zoldak Committed by Matt Drayer

Refactor profiling middleware

parent 2a9e527a
"""
Helper methods for the profiler middleware
"""
import os
import re
import shutil
import subprocess
from textwrap import dedent
import time
def generate_help():
"""
Provide some useful operational info to the caller
"""
help = dedent("""\
########## PROFILER HELP ##########
Profiler Options (query string params):
profiler_type: hotshot (default), cprofile
profiler_mode: normal (default), help
profiler_sort: time (default) calls, cumulative, file, module, ncalls
profiler_format: console (default), text, html, pdf, svg, raw
More info:
https://docs.python.org/2/library/hotshot.html
https://docs.python.org/2/library/profile.html#module-cProfile
""")
return help
def generate_console_response(stats_str, stats_summary):
"""
Output directly to the console -- helpful during unit testing or
for viewing code executions in devstack
"""
print stats_str
print stats_summary
def generate_text_response(stats_str, stats_summary, response):
"""
Output the call stats to the browser as plain text
"""
response['Content-Type'] = 'text/plain'
response.content = stats_str
response.content = "\n".join(response.content.split("\n")[:40])
response.content += "\n\n"
response.content += stats_summary
def generate_html_response(stats_str, stats_summary, response):
"""
Output the call stats to the browser wrapped in HTML tags
Feel free to improve the HTML structure!!!
"""
response['Content-Type'] = 'text/html'
response.content = '<pre>{}{}</pre>'.format(stats_str, stats_summary)
def generate_pdf_response(filename, response):
"""
Output a pretty picture of the call tree (boxes and arrows)
"""
def which(program):
"""
Helper method to return the path of the named program in the PATH,
or None if no such executable program can be found.
"""
def is_exe(fpath):
"""
Internal helper to confirm that this is an executable program
"""
return os.path.isfile(fpath) and os.access(fpath, os.X_OK)
fpath, _fname = os.path.split(program)
if fpath:
if is_exe(program):
return program
else:
for path in os.environ["PATH"].split(os.pathsep):
path = path.strip('"')
exe_file = os.path.join(path, program)
if is_exe(exe_file):
return exe_file
return None
if not which('dot'):
raise Exception('Could not find "dot" from Graphviz; please install Graphviz to enable call graph generation')
if not which('gprof2dot'):
raise Exception('Could not find gprof2dot; have you updated your dependencies recently?')
command = ('gprof2dot -f pstats {} | dot -Tpdf'.format(filename))
process = subprocess.Popen(
command,
shell=True,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE)
output = process.communicate()[0]
return_code = process.poll()
if return_code:
raise Exception('gprof2dot/dot exited with {}'.format(return_code))
response['Content-Type'] = 'application/pdf'
response.content = output
def generate_svg_response(filename, profiler_type, response):
"""
Output a pretty picture of the call tree (boxes and arrows)
"""
# Set up the data file
profile_name = '{}_{}'.format(profiler_type, time.time())
profile_data = '/tmp/{}.dat'.format(profile_name)
shutil.copy(filename, profile_data)
os.chmod(profile_data, 0666)
# Create the output file
profile_svg = '/tmp/{}.svg'.format(profile_name)
old = os.path.abspath('.')
os.chdir('/tmp')
command = 'gprof2dot -f pstats {} | dot -Tsvg -o {}'.format(profile_data, profile_svg)
try:
output = subprocess.call(command, shell=True)
except Exception: # pylint: disable=W0703
output = 'Error during call to gprof2dot/dot'
os.chdir(old)
if os.path.exists(profile_svg):
response['Content-Type'] = 'image/svg+xml'
f = open(profile_svg)
response.content = f.read()
f.close()
else:
response['Content-Type'] = 'text/plain'
response.content = output
def generate_raw_response(profiler_type, filename, response):
"""
Output the raw stats data to the browser -- the caller can then
save the information to a local file and do something else with it
Could be used as an integration point in the future for real-time
diagrams, charts, reports, etc.
"""
# Set up the data faile (this is all we do in this particular case)
profile_name = '{}_{}'.format(profiler_type, time.time())
profile_data = '/tmp/{}.dat'.format(profile_name)
shutil.copy(filename, profile_data)
os.chmod(profile_data, 0666)
# Return the raw data directly to the caller/browser (useful for API scenarios)
f = open(profile_data)
response.content = f.read()
f.close()
def _get_group(file_name):
"""
Finds a matching group for a given line (statistic) in the file
"""
group_prefix_re = [
re.compile("^.*/django/[^/]+"),
re.compile("^(.*)/[^/]+$"), # extract module path
re.compile(".*"), # catch strange entries
]
for prefix in group_prefix_re:
name = prefix.findall(file_name)
if name:
return name[0]
def _get_summary(results_dict, total):
"""
Does the actual rolling up of stats info into a group
"""
results = [(item[1], item[0]) for item in results_dict.items()]
results.sort(reverse=True)
result = results[:40]
res = " tottime\n"
for item in result:
res += "%4.1f%% %7.3f %s\n" % (100 * item[0] / total if total else 0, item[0], item[1])
return res
def summary_for_files(stats_str):
"""
Rolls up the statistics generated by the profiler into some
useful aggregates (by file and by group)
"""
stats_str = stats_str.split("\n")[5:]
mystats = {}
mygroups = {}
total = 0
iteration = 0
for stat in stats_str:
iteration = iteration + 1
if iteration > 2:
words_re = re.compile(r'\s+')
fields = words_re.split(stat)
if len(fields) == 7:
stat_time = float(fields[2])
total += stat_time
file_name = fields[6].split(":")[0]
if not file_name in mystats:
mystats[file_name] = 0
mystats[file_name] += stat_time
group = _get_group(file_name)
if not group in mygroups:
mygroups[group] = 0
mygroups[group] += stat_time
summary_string = " ---- By file ----\n\n" + _get_summary(mystats, total) + "\n" + \
" ---- By group ---\n\n" + _get_summary(mygroups, total)
return summary_string
...@@ -12,25 +12,28 @@ Hotshot/CProfile Profiler Middleware ...@@ -12,25 +12,28 @@ Hotshot/CProfile Profiler Middleware
* http://www.jeffknupp.com/blog/2012/02/14/profiling-django-applications-a-journey-from-1300-to-2-queries/ * http://www.jeffknupp.com/blog/2012/02/14/profiling-django-applications-a-journey-from-1300-to-2-queries/
- The profiler is enabled via feature flag in settings.py -- see devstack.py and test.py - The profiler is enabled via feature flag in settings.py -- see devstack.py and test.py
- Once enabled, simply add "prof=1" to the query string to profile your view - Once enabled, simply add "prof=1" to the query string to profile your view
- Include "&profile_mode=help" for more information (see generate_help below) - Include "&profiler_mode=help" for more information
- e.g. http://localhost:8000/about?prof=1&profiler_mode=help
""" """
from abc import ABCMeta, abstractmethod, abstractproperty
import hotshot import hotshot
import hotshot.stats import hotshot.stats
import os import logging
import pstats import pstats
import re
import shutil
import subprocess
import sys import sys
import tempfile import tempfile
import time from threading import local
import threading
from django.conf import settings from django.conf import settings
from django.core.exceptions import MiddlewareNotUsed from django.core.exceptions import MiddlewareNotUsed
from helpers import (
generate_help, generate_console_response, generate_text_response,
generate_html_response, generate_pdf_response, generate_svg_response,
generate_raw_response, summary_for_files
)
try: try:
import cProfile import cProfile
HAS_CPROFILE = True HAS_CPROFILE = True
...@@ -42,346 +45,247 @@ try: ...@@ -42,346 +45,247 @@ try:
except ImportError: except ImportError:
import StringIO import StringIO
THREAD_LOCAL = threading.local() log = logging.getLogger(__name__)
# Initialize the thread local storage
TLS = local()
def which(program):
class Profile(object):
""" """
Helper method to return the path of the named program in the PATH, Profile metadata
or None if no such executable program can be found.
""" """
def is_exe(fpath): def __init__(self):
""" self.data_file = None
Internal helper to confirm that this is an executable program self.profiler = None
"""
return os.path.isfile(fpath) and os.access(fpath, os.X_OK)
fpath, _fname = os.path.split(program)
if fpath:
if is_exe(program):
return program
else:
for path in os.environ["PATH"].split(os.pathsep):
path = path.strip('"')
exe_file = os.path.join(path, program)
if is_exe(exe_file):
return exe_file
return None
class BaseProfilerMiddleware(object): class BaseProfilerMiddleware(object):
""" """
Abstract base classs for profiler middleware.
This class performs the actual work of profiling and generating the This class performs the actual work of profiling and generating the
report output. The child classes defined below address some report output.
implementation-specific idiosyncrasies for each profiler. The child classes address implementation-specific idiosyncrasies for each profiler.
""" """
def process_request(self, request): __metaclass__ = ABCMeta
@abstractproperty
def profiler_type(self):
""" """
Set up the profiler for use Which profiler this is
""" """
print 'process_request' raise NotImplementedError('Subclasses must implement profiler_type')
# Capture some values/references to use across the operations
THREAD_LOCAL.profiler_requested = request.GET.get('prof', False)
# Ensure we're allowed to use the profiler @abstractproperty
if THREAD_LOCAL.profiler_requested and not settings.DEBUG and not request.user.is_superuser: def is_profiler_installed(self):
raise MiddlewareNotUsed() """
Is this profiler installed?
"""
return False
# Ensure the profiler being requested is actually installed/available @abstractmethod
if not hasattr(THREAD_LOCAL, 'profiler_type') or THREAD_LOCAL.profiler_type is None: def profiler_start(self):
THREAD_LOCAL.profiler_type = request.GET.get('profiler_type', 'hotshot') """
if self.profiler_type() == THREAD_LOCAL.profiler_type: Method for starting
if not self.profiler_installed(): """
return MiddlewareNotUsed() raise NotImplementedError('Subclasses must implement profiler_start')
# Create the container we'll be using to store the raw profiler data @abstractmethod
THREAD_LOCAL.data_file = tempfile.NamedTemporaryFile() def profiler_stop(self, _stream):
"""
Parent method
"""
raise NotImplementedError('Subclasses must implement profiler_stop')
def process_view(self, request, callback, callback_args, callback_kwargs): @staticmethod
def _do_cleanup(ptype):
""" """
Enable the profiler and begin collecting data about the view Clean up the stuff we stored in the thread local storage
Note that this misses the rest of Django's request processing (other middleware, etc.)
""" """
# Ensure the profiler being requested is actually installed/available TLS.is_requested = False
if THREAD_LOCAL.profiler_type is None: TLS.ptype = None
THREAD_LOCAL.profiler_type = request.GET.get('profiler_type', 'hotshot')
if self.profiler_type() == THREAD_LOCAL.profiler_type:
if not self.profiler_installed():
return MiddlewareNotUsed()
THREAD_LOCAL.profiler = self.profiler_start()
return THREAD_LOCAL.profiler.runcall(callback, request, *callback_args, **callback_kwargs)
def _generate_console_response(self, stats_str, stats_summary):
"""
Output directly to the console -- helpful during unit testing or
for viewing code executions in devstack
"""
print stats_str
print stats_summary
def _generate_text_response(self, stats_str, stats_summary, response):
"""
Output the call stats to the browser as plain text
"""
response['Content-Type'] = 'text/plain'
response.content = stats_str
response.content = "\n".join(response.content.split("\n")[:40])
response.content += "\n\n"
response.content += stats_summary
def _generate_html_response(self, stats_str, stats_summary, response):
"""
Output the call stats to the browser wrapped in HTML tags
Feel free to improve the HTML structure!!!
"""
response['Content-Type'] = 'text/html'
response.content = '<pre>{}{}</pre>'.format(stats_str, stats_summary)
def _generate_pdf_response(self, response):
"""
Output a pretty picture of the call tree (boxes and arrows)
"""
if not which('dot'):
raise Exception('Could not find "dot" from Graphviz; please install Graphviz to enable call graph generation')
if not which('gprof2dot'):
raise Exception('Could not find gprof2dot; have you updated your dependencies recently?')
command = ('gprof2dot -f pstats {} | dot -Tpdf'.format(THREAD_LOCAL.data_file.name))
process = subprocess.Popen(
command,
shell=True,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE)
output = process.communicate()[0]
return_code = process.poll()
if return_code:
raise Exception('gprof2dot/dot exited with {}'.format(return_code))
response['Content-Type'] = 'application/pdf'
response.content = output
def _generate_svg_response(self, response):
"""
Output a pretty picture of the call tree (boxes and arrows)
"""
# Set up the data file
profile_name = '{}_{}'.format(self.profiler_type(), time.time())
profile_data = '/tmp/{}.dat'.format(profile_name)
shutil.copy(THREAD_LOCAL.data_file.name, profile_data)
os.chmod(profile_data, 0666)
# Create the output file
profile_svg = '/tmp/{}.svg'.format(profile_name)
old = os.path.abspath('.')
os.chdir('/tmp')
command = 'gprof2dot -f pstats {} | dot -Tsvg -o {}'.format(profile_data, profile_svg)
try:
output = subprocess.call(command, shell=True)
except Exception: # pylint: disable=W0703
output = 'Error during call to gprof2dot/dot'
os.chdir(old)
if os.path.exists(profile_svg):
response['Content-Type'] = 'image/svg+xml'
f = open(profile_svg)
response.content = f.read()
f.close()
else:
response['Content-Type'] = 'text/plain'
response.content = output
def _generate_raw_response(self, response): if TLS.prof[ptype].data_file:
TLS.prof[ptype].data_file.close() # Closing the temp file will automatically delete it.
TLS.prof[ptype].data_file = None
@staticmethod
def _profiler_to_use(request):
""" """
Output the raw stats data to the browser -- the caller can then Determine which profiler to use, store that info in the TLS,
save the information to a local file and do something else with it and return its name.
Could be used as an integration point in the future for real-time
diagrams, charts, reports, etc.
""" """
# Set up the data faile (this is all we do in this particular case) # Set the profiler type to the requested (or default) one
profile_name = '{}_{}'.format(self.profiler_type(), time.time()) if not hasattr(TLS, 'ptype') or TLS.ptype is None:
profile_data = '/tmp/{}.dat'.format(profile_name) TLS.ptype = unicode(request.GET.get('profiler_type', 'hotshot'))
shutil.copy(THREAD_LOCAL.data_file.name, profile_data)
os.chmod(profile_data, 0666)
# Return the raw data directly to the caller/browser (useful for API scenarios)
f = open(profile_data)
response.content = f.read()
f.close()
def process_response(self, request, response): return TLS.ptype
def _verify_available(self, request):
""" """
Most of the heavy lifting takes place in this base process_response operation Verify that the requested profiler is installed and also that either
It seems process_response can be invoked without a prior invocation the django app is in debug mode or the user is a superuser.
of process request and/or process view, so we need to put in a guard
"""
if not hasattr(THREAD_LOCAL, 'profiler_type') or THREAD_LOCAL.profiler_type is None:
THREAD_LOCAL.profiler_type = request.GET.get('profiler_type', 'hotshot')
if self.profiler_type() == THREAD_LOCAL.profiler_type and THREAD_LOCAL.profiler is not None:
if not self.profiler_installed():
return MiddlewareNotUsed()
# The caller may want to view the runtime help documentation
profiler_mode = request.GET.get('profiler_mode', 'normal')
if profiler_mode == 'help':
response['Content-Type'] = 'text/plain'
response.content = self.generate_help()
return response
# Set up a redirected stdout location (hides output from console)
old_stdout = sys.stdout
temp_stdout = StringIO.StringIO()
sys.stdout = temp_stdout
# Load the statistics collected by the profiler
stats = self.profiler_stop(temp_stdout)
# Sort the statistics according to the caller's wishes
# See # http://docs.python.org/2/library/profile.html#pstats.Stats.sort_stats
# for the all of the fields you can sort on (some in generate_help below)
profiler_sort = request.GET.get('profiler_sort', 'time')
if profiler_sort == 'time':
profiler_sort = 'time,calls'
stats.sort_stats(*profiler_sort.split(','))
# Strip out the directories from the report, if so desired
strip_dirs = request.GET.get('profiler_strip', False)
if strip_dirs:
stats.strip_dirs()
# Pare down the statistics data further, if specified
restrictions = []
# Regex filter
if request.GET.get('profile_filter'):
restrictions.append(request.GET['profile_filter'])
# Cut the list down to either a fraction of the set or a specific line count
if request.GET.get('profile_fraction'):
restrictions.append(float(request.GET['profile_fraction']))
elif request.GET.get('profile_lines'):
restrictions.append(int(request.GET['profile_lines']))
# If no size restriction and we're not filtering, trim stats to a reasonable amount
elif not request.GET.get('filter'):
restrictions.append(.1)
# Send the statistics data to the redirected stdout location,
# then put stdout back to its original configuration
stats.print_stats(*restrictions)
stats_str = temp_stdout.getvalue()
sys.stdout.close()
sys.stdout = old_stdout
# Format the response
if response and response.content and stats_str:
stats_summary = self.summary_for_files(stats_str)
response_format = request.GET.get('profiler_format', 'console')
# Console format sends the profiler result to stdout, preserving current response content
# All other formats replace response content with the profiler result
if response_format == 'console':
self._generate_console_response(stats_str, stats_summary)
elif response_format == 'text':
self._generate_text_response(stats_str, stats_summary, response)
elif response_format == 'html':
self._generate_html_response(stats_str, stats_summary, response)
elif response_format == 'pdf':
self._generate_pdf_response(response)
elif response_format == 'svg':
self._generate_svg_response(response)
elif response_format == 'raw':
self._generate_raw_response(response)
# Clean up the stuff we stuffed into thread_local and then return the response to the caller
THREAD_LOCAL.profiler_type = None
THREAD_LOCAL.profiler_requested = None
return response
def profiler_type(self): Raise an error if it is not.
""" """
Parent method -- should be overridden by child if not self.is_profiler_installed:
msg = '{} profiler is not installed'.format(self.profiler_type)
log.error(msg)
raise MiddlewareNotUsed(msg)
if not settings.DEBUG and not request.user.is_superuser:
msg = '{}{}'.format(
'The DEBUG environment parameter must be set to True, or the ',
'authenticated user must be configured as a superuser to use the profiler middleware.'
)
log.error(msg)
raise MiddlewareNotUsed(msg)
def process_request(self, request):
"""
Set up the profiler for use if requested
""" """
return 'undefined' my_ptype = self.profiler_type
log.info('Entering process_request for {} profiler for path: {}'.format(my_ptype, request.path))
# Capture some values/references into thread local storage to use across the operations
TLS.is_requested = request.GET.get('prof', False)
# Do nothing if not asked to profile
if not TLS.is_requested:
return
ptype = self._profiler_to_use(request)
# Do nothing if this is not the profiler that was requested
if my_ptype != ptype:
return
# Verify that the profiler being requested is actually installed/available
self._verify_available(request)
# Initialize the object we use for storing data across operations
if not hasattr(TLS, 'prof'):
TLS.prof = {}
TLS.prof[ptype] = Profile()
def profiler_installed(self): # Create the container we'll be using to store the raw profiler data
TLS.prof[ptype].data_file = tempfile.NamedTemporaryFile(delete=True)
def process_view(self, request, callback, callback_args, callback_kwargs):
""" """
Parent method -- should be overridden by child Enable the profiler and begin collecting data about the view
Note that this misses the rest of Django's request processing (other middleware, etc.)
""" """
return False my_ptype = self.profiler_type
log.info('Entering process_view for {} profiler for path: {}'.format(my_ptype, request.path))
def profiler_start(self): # Do nothing if not asked to profile
if not TLS.is_requested:
return
# Check if this is the profiler that was requested
ptype = TLS.ptype
if my_ptype != ptype:
return
# This is the profiler to use. Do it!
TLS.prof[ptype].profiler = self.profiler_start()
return TLS.prof[ptype].profiler.runcall(callback, request, *callback_args, **callback_kwargs)
def process_response(self, request, response):
""" """
Parent method -- should be overridden by child Process the response.
""" Most of the heavy lifting takes place in this operation
return MiddlewareNotUsed() """
my_ptype = self.profiler_type
def profiler_stop(self, stream): # pylint: disable=W0613 log.info('Entering process_response for {} profiler for path: {}'.format(my_ptype, request.path))
"""
Parent method -- should be overridden by child # Check if the request asked to be profiled
""" if not TLS.is_requested:
return MiddlewareNotUsed() return response
def get_group(self, file_name): ptype = self._profiler_to_use(request)
""" # Only process if this is the profiler that you wanted to use
Finds a matching group for a given line (statistic) in the file if my_ptype != ptype:
""" return response
group_prefix_re = [
re.compile("^.*/django/[^/]+"), # Verify that the profiler being requested is actually installed/available
re.compile("^(.*)/[^/]+$"), # extract module path self._verify_available(request)
re.compile(".*"), # catch strange entries
] # The caller may want to view the runtime help documentation
for prefix in group_prefix_re: profiler_mode = request.GET.get('profiler_mode', 'normal')
name = prefix.findall(file_name) if profiler_mode == 'help':
if name: response['Content-Type'] = 'text/plain'
return name[0] response.content = generate_help()
self._do_cleanup(TLS.ptype)
def get_summary(self, results_dict, total): return response
"""
Does the actual rolling up of stats info into a group # Set up a redirected stdout location (hides output from console)
""" old_stdout = sys.stdout
results = [(item[1], item[0]) for item in results_dict.items()] temp_stdout = StringIO.StringIO()
results.sort(reverse=True) sys.stdout = temp_stdout
result = results[:40]
res = " tottime\n" # Load the statistics collected by the profiler
for item in result: stats = self.profiler_stop(temp_stdout)
res += "%4.1f%% %7.3f %s\n" % (100 * item[0] / total if total else 0, item[0], item[1])
return res # Sort the statistics according to the caller's wishes
# See http://docs.python.org/2/library/profile.html#pstats.Stats.sort_stats
def summary_for_files(self, stats_str): # for the all of the fields you can sort on
""" profiler_sort = request.GET.get('profiler_sort', 'time')
Rolls up the statistics generated by the profiler into some if profiler_sort == 'time':
useful aggregates (by file and by group) profiler_sort = 'time,calls'
""" stats.sort_stats(*profiler_sort.split(','))
stats_str = stats_str.split("\n")[5:]
mystats = {} # Strip out the directories from the report, if so desired
mygroups = {} strip_dirs = request.GET.get('profiler_strip', False)
total = 0 if strip_dirs:
iteration = 0 stats.strip_dirs()
for stat in stats_str:
iteration = iteration + 1 # Pare down the statistics data further, if specified
if iteration > 2: restrictions = []
words_re = re.compile(r'\s+') # Regex filter
fields = words_re.split(stat) if request.GET.get('profile_filter'):
if len(fields) == 7: restrictions.append(request.GET['profile_filter'])
stat_time = float(fields[2]) # Cut the list down to either a fraction of the set or a specific line count
total += stat_time if request.GET.get('profile_fraction'):
file_name = fields[6].split(":")[0] restrictions.append(float(request.GET['profile_fraction']))
if not file_name in mystats: elif request.GET.get('profile_lines'):
mystats[file_name] = 0 restrictions.append(int(request.GET['profile_lines']))
mystats[file_name] += stat_time # If no size restriction and we're not filtering, trim stats to a reasonable amount
group = self.get_group(file_name) elif not request.GET.get('filter'):
if not group in mygroups: restrictions.append(.1)
mygroups[group] = 0
mygroups[group] += stat_time # Send the statistics data to the redirected stdout location,
summary_string = " ---- By file ----\n\n" + self.get_summary(mystats, total) + "\n" + \ # then put stdout back to its original configuration
" ---- By group ---\n\n" + self.get_summary(mygroups, total) stats.print_stats(*restrictions)
return summary_string stats_str = temp_stdout.getvalue()
sys.stdout.close()
def generate_help(self): sys.stdout = old_stdout
"""
Provide some useful operational info to the caller # Format the response
""" if response and response.content and stats_str:
return "########## PROFILER HELP ##########\n\n\n" + \ stats_summary = summary_for_files(stats_str)
"Profiler Options (query string params):\n\n" + \ response_format = request.GET.get('profiler_format', 'console')
"profiler_type: hotshot (default), cprofile \n" + \ # Console format sends the profiler result to stdout, preserving current response content
"profiler_mode: normal (default), help \n" + \ # All other formats replace response content with the profiler result
"profiler_sort: time (default) calls, cumulative, file, module, ncalls \n" + \ if response_format == 'console':
"profiler_format: console (default), text, html, pdf, svg, raw \n\n\n" + \ generate_console_response(stats_str, stats_summary)
"More info: \n\n" + \ elif response_format == 'text':
"https://docs.python.org/2/library/hotshot.html \n" + \ generate_text_response(stats_str, stats_summary, response)
"https://docs.python.org/2/library/profile.html#module-cProfile \n" elif response_format == 'html':
generate_html_response(stats_str, stats_summary, response)
elif response_format == 'pdf':
generate_pdf_response(TLS.data_file.name, response)
elif response_format == 'svg':
generate_svg_response(ptype, TLS.prof[ptype].data_file.name, response)
elif response_format == 'raw':
generate_raw_response(ptype, TLS.prof[ptype].data_file.name, response)
self._do_cleanup(ptype)
return response
class HotshotProfilerMiddleware(BaseProfilerMiddleware): class HotshotProfilerMiddleware(BaseProfilerMiddleware):
...@@ -390,16 +294,15 @@ class HotshotProfilerMiddleware(BaseProfilerMiddleware): ...@@ -390,16 +294,15 @@ class HotshotProfilerMiddleware(BaseProfilerMiddleware):
See https://docs.python.org/2/library/hotshot.html for more info See https://docs.python.org/2/library/hotshot.html for more info
WARNING: The Hotshot profiler is not thread safe. WARNING: The Hotshot profiler is not thread safe.
""" """
def __init__(self, *args, **kwargs): @property
super(HotshotProfilerMiddleware, self).__init__(*args, **kwargs)
def profiler_type(self): def profiler_type(self):
""" """
Use this value to select the profiler via query string Use this value to select the profiler via query string
""" """
return 'hotshot' return u'hotshot'
def profiler_installed(self): @property
def is_profiler_installed(self):
""" """
Hotshot is native and available for use Hotshot is native and available for use
""" """
...@@ -409,14 +312,14 @@ class HotshotProfilerMiddleware(BaseProfilerMiddleware): ...@@ -409,14 +312,14 @@ class HotshotProfilerMiddleware(BaseProfilerMiddleware):
""" """
Turn on the profiler and begin collecting data Turn on the profiler and begin collecting data
""" """
return hotshot.Profile(THREAD_LOCAL.data_file.name) return hotshot.Profile(TLS.prof[self.profiler_type].data_file.name)
def profiler_stop(self, stream): # pylint: disable=W0221 def profiler_stop(self, _stream):
""" """
Store profiler data in file and return statistics to caller Store profiler data in file and return statistics to caller
""" """
THREAD_LOCAL.profiler.close() TLS.prof[self.profiler_type].profiler.close()
return hotshot.stats.load(THREAD_LOCAL.data_file.name) return hotshot.stats.load(TLS.prof[self.profiler_type].data_file.name)
class CProfileProfilerMiddleware(BaseProfilerMiddleware): class CProfileProfilerMiddleware(BaseProfilerMiddleware):
...@@ -424,19 +327,18 @@ class CProfileProfilerMiddleware(BaseProfilerMiddleware): ...@@ -424,19 +327,18 @@ class CProfileProfilerMiddleware(BaseProfilerMiddleware):
CProfile is a runtime profiler available natively in Python CProfile is a runtime profiler available natively in Python
See https://docs.python.org/2/library/profile.html#module-cProfile for more info See https://docs.python.org/2/library/profile.html#module-cProfile for more info
""" """
def __init__(self): @property
super(CProfileProfilerMiddleware, self).__init__()
def profiler_type(self): def profiler_type(self):
""" """
Use this value to select the profiler via query string Use this value to select the profiler via query string
""" """
return 'cprofile' return u'cprofile'
def profiler_installed(self): @property
def is_profiler_installed(self):
""" """
Apparently CProfile is not native, and many examples simply Apparently CProfile is not native, and many examples simply
failover to the regular 'profile' module. Maybe we should, too failover to the regular 'profile' module. Maybe we should, too.
""" """
return HAS_CPROFILE return HAS_CPROFILE
...@@ -446,10 +348,10 @@ class CProfileProfilerMiddleware(BaseProfilerMiddleware): ...@@ -446,10 +348,10 @@ class CProfileProfilerMiddleware(BaseProfilerMiddleware):
""" """
return cProfile.Profile() return cProfile.Profile()
def profiler_stop(self, stream): def profiler_stop(self, _stream):
""" """
Store profiler data in file and return statistics to caller Store profiler data in file and return statistics to caller
""" """
THREAD_LOCAL.profiler.create_stats() TLS.prof[self.profiler_type].profiler.create_stats()
THREAD_LOCAL.profiler.dump_stats(THREAD_LOCAL.data_file.name) TLS.prof[self.profiler_type].profiler.dump_stats(TLS.prof[TLS.ptype].data_file.name)
return pstats.Stats(THREAD_LOCAL.profiler, stream=stream) return pstats.Stats(TLS.prof[TLS.ptype].profiler, stream=_stream)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment