Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-platform
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
edx-platform
Commits
f7f811be
Commit
f7f811be
authored
Dec 03, 2014
by
Jesse Zoldak
Committed by
Matt Drayer
Aug 01, 2016
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Refactor profiling middleware
parent
2a9e527a
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
451 additions
and
336 deletions
+451
-336
common/djangoapps/profiler/helpers.py
+213
-0
common/djangoapps/profiler/middleware.py
+238
-336
No files found.
common/djangoapps/profiler/helpers.py
0 → 100644
View file @
f7f811be
"""
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.1
f
%% %7.3
f
%
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
common/djangoapps/profiler/middleware.py
View file @
f7f811be
...
...
@@ -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/
- 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
- 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.stats
import
os
import
logging
import
pstats
import
re
import
shutil
import
subprocess
import
sys
import
tempfile
import
time
import
threading
from
threading
import
local
from
django.conf
import
settings
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
:
import
cProfile
HAS_CPROFILE
=
True
...
...
@@ -42,346 +45,247 @@ try:
except
ImportError
:
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,
or None if no such executable program can be found.
Profile metadata
"""
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
def
__init__
(
self
):
self
.
data_file
=
None
self
.
profiler
=
None
class
BaseProfilerMiddleware
(
object
):
"""
Abstract base classs for profiler middleware.
This class performs the actual work of profiling and generating the
report output.
The child classes defined below address some
implementation-specific idiosyncrasies for each profiler.
report output.
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'
# Capture some values/references to use across the operations
THREAD_LOCAL
.
profiler_requested
=
request
.
GET
.
get
(
'prof'
,
False
)
raise
NotImplementedError
(
'Subclasses must implement profiler_type'
)
# Ensure we're allowed to use the profiler
if
THREAD_LOCAL
.
profiler_requested
and
not
settings
.
DEBUG
and
not
request
.
user
.
is_superuser
:
raise
MiddlewareNotUsed
()
@abstractproperty
def
is_profiler_installed
(
self
):
"""
Is this profiler installed?
"""
return
False
# Ensure the profiler being requested is actually installed/available
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
:
if
not
self
.
profiler_installed
():
return
MiddlewareNotUsed
(
)
@abstractmethod
def
profiler_start
(
self
)
:
"""
Method for starting
"""
raise
NotImplementedError
(
'Subclasses must implement profiler_start'
)
# Create the container we'll be using to store the raw profiler data
THREAD_LOCAL
.
data_file
=
tempfile
.
NamedTemporaryFile
()
@abstractmethod
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
Note that this misses the rest of Django's request processing (other middleware, etc.)
Clean up the stuff we stored in the thread local storage
"""
# Ensure the profiler being requested is actually installed/available
if
THREAD_LOCAL
.
profiler_type
is
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
TLS
.
is_requested
=
False
TLS
.
ptype
=
None
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
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.
Determine which profiler to use, store that info in the TLS,
and return its name.
"""
# Set up the data faile (this is all we do in this particular case)
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
)
# Return the raw data directly to the caller/browser (useful for API scenarios)
f
=
open
(
profile_data
)
response
.
content
=
f
.
read
()
f
.
close
()
# Set the profiler type to the requested (or default) one
if
not
hasattr
(
TLS
,
'ptype'
)
or
TLS
.
ptype
is
None
:
TLS
.
ptype
=
unicode
(
request
.
GET
.
get
(
'profiler_type'
,
'hotshot'
))
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
It seems process_response can be invoked without a prior invocation
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
Verify that the requested profiler is installed and also that either
the django app is in debug mode or the user is a superuser.
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
"""
return
MiddlewareNotUsed
()
def
profiler_stop
(
self
,
stream
):
# pylint: disable=W0613
"""
Parent method -- should be overridden by child
"""
return
MiddlewareNotUsed
()
def
get_group
(
self
,
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
(
self
,
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.1
f
%% %7.3
f
%
s
\n
"
%
(
100
*
item
[
0
]
/
total
if
total
else
0
,
item
[
0
],
item
[
1
])
return
res
def
summary_for_files
(
self
,
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
=
self
.
get_group
(
file_name
)
if
not
group
in
mygroups
:
mygroups
[
group
]
=
0
mygroups
[
group
]
+=
stat_time
summary_string
=
" ---- By file ----
\n\n
"
+
self
.
get_summary
(
mystats
,
total
)
+
"
\n
"
+
\
" ---- By group ---
\n\n
"
+
self
.
get_summary
(
mygroups
,
total
)
return
summary_string
def
generate_help
(
self
):
"""
Provide some useful operational info to the caller
"""
return
"########## PROFILER HELP ##########
\n\n\n
"
+
\
"Profiler Options (query string params):
\n\n
"
+
\
"profiler_type: hotshot (default), cprofile
\n
"
+
\
"profiler_mode: normal (default), help
\n
"
+
\
"profiler_sort: time (default) calls, cumulative, file, module, ncalls
\n
"
+
\
"profiler_format: console (default), text, html, pdf, svg, raw
\n\n\n
"
+
\
"More info:
\n\n
"
+
\
"https://docs.python.org/2/library/hotshot.html
\n
"
+
\
"https://docs.python.org/2/library/profile.html#module-cProfile
\n
"
Process the response.
Most of the heavy lifting takes place in this operation
"""
my_ptype
=
self
.
profiler_type
log
.
info
(
'Entering process_response for {} profiler for path: {}'
.
format
(
my_ptype
,
request
.
path
))
# Check if the request asked to be profiled
if
not
TLS
.
is_requested
:
return
response
ptype
=
self
.
_profiler_to_use
(
request
)
# Only process if this is the profiler that you wanted to use
if
my_ptype
!=
ptype
:
return
response
# Verify that the profiler being requested is actually installed/available
self
.
_verify_available
(
request
)
# 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
=
generate_help
()
self
.
_do_cleanup
(
TLS
.
ptype
)
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
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
=
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'
:
generate_console_response
(
stats_str
,
stats_summary
)
elif
response_format
==
'text'
:
generate_text_response
(
stats_str
,
stats_summary
,
response
)
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
):
...
...
@@ -390,16 +294,15 @@ class HotshotProfilerMiddleware(BaseProfilerMiddleware):
See https://docs.python.org/2/library/hotshot.html for more info
WARNING: The Hotshot profiler is not thread safe.
"""
def
__init__
(
self
,
*
args
,
**
kwargs
):
super
(
HotshotProfilerMiddleware
,
self
)
.
__init__
(
*
args
,
**
kwargs
)
@property
def
profiler_type
(
self
):
"""
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
"""
...
...
@@ -409,14 +312,14 @@ class HotshotProfilerMiddleware(BaseProfilerMiddleware):
"""
Turn on the profiler and begin collecting data
"""
return
hotshot
.
Profile
(
T
HREAD_LOCAL
.
data_file
.
name
)
return
hotshot
.
Profile
(
T
LS
.
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
"""
T
HREAD_LOCAL
.
profiler
.
close
()
return
hotshot
.
stats
.
load
(
T
HREAD_LOCAL
.
data_file
.
name
)
T
LS
.
prof
[
self
.
profiler_type
]
.
profiler
.
close
()
return
hotshot
.
stats
.
load
(
T
LS
.
prof
[
self
.
profiler_type
]
.
data_file
.
name
)
class
CProfileProfilerMiddleware
(
BaseProfilerMiddleware
):
...
...
@@ -424,19 +327,18 @@ class CProfileProfilerMiddleware(BaseProfilerMiddleware):
CProfile is a runtime profiler available natively in Python
See https://docs.python.org/2/library/profile.html#module-cProfile for more info
"""
def
__init__
(
self
):
super
(
CProfileProfilerMiddleware
,
self
)
.
__init__
()
@property
def
profiler_type
(
self
):
"""
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
failover to the regular 'profile' module. Maybe we should, too
failover to the regular 'profile' module. Maybe we should, too
.
"""
return
HAS_CPROFILE
...
...
@@ -446,10 +348,10 @@ class CProfileProfilerMiddleware(BaseProfilerMiddleware):
"""
return
cProfile
.
Profile
()
def
profiler_stop
(
self
,
stream
):
def
profiler_stop
(
self
,
_
stream
):
"""
Store profiler data in file and return statistics to caller
"""
T
HREAD_LOCAL
.
profiler
.
create_stats
()
T
HREAD_LOCAL
.
profiler
.
dump_stats
(
THREAD_LOCAL
.
data_file
.
name
)
return
pstats
.
Stats
(
T
HREAD_LOCAL
.
profiler
,
stream
=
stream
)
T
LS
.
prof
[
self
.
profiler_type
]
.
profiler
.
create_stats
()
T
LS
.
prof
[
self
.
profiler_type
]
.
profiler
.
dump_stats
(
TLS
.
prof
[
TLS
.
ptype
]
.
data_file
.
name
)
return
pstats
.
Stats
(
T
LS
.
prof
[
TLS
.
ptype
]
.
profiler
,
stream
=
_
stream
)
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment