Skip to content

Commit 7682537

Browse files
authored
Add support for Python profiling of Server requests (#3502)
PBENCH-1228
1 parent fac8d5f commit 7682537

File tree

2 files changed

+81
-0
lines changed

2 files changed

+81
-0
lines changed

lib/pbench/cli/server/shell.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,17 @@ def run_gunicorn(server_config: PbenchServerConfig, logger: Logger) -> int:
169169
"pbench-server",
170170
]
171171

172+
# If PB_PROFILE_DUMP_FILE is defined, enable profiling of the server's
173+
# execution of requests. If defined to an empty string, profiling
174+
# results are dumped to the log; otherwise, the value is treated as the
175+
# name of a file to receive the data. (An excellent choice is
176+
# "/srv/pbench/public_html/pbench_server.prof", because this is writable
177+
# by the server and easily accessed by the user via the browser by hitting
178+
# "https://<server>/pbench_server.prof".) Note that the file is
179+
# overwritten for each request.
180+
if os.environ.get("PB_PROFILE_DUMP_FILE") is not None:
181+
cmd_line += ["--config", "/opt/pbench-server/lib/pbench/profiling.conf.py"]
182+
172183
# When installed via RPM, the shebang in the gunicorn script includes a -s
173184
# which prevents Python from implicitly including the user site packages in
174185
# the sys.path configuration. (Note that, when installed via Pip, the

lib/pbench/profiling.conf.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
"""Optional Gunicorn configuration file to enable profiling
2+
3+
This module provides definitions for the pre-request and post-request Gunicorn
4+
hooks, which allow us to get control before a request begins executing and
5+
after it completes. We use these to enable and disable profiling (so that the
6+
profile reflects only the single method call, with a minimum of extraneous
7+
execution), and, when the request is complete, we dump the profiling report to
8+
a file or to the log.
9+
10+
This file is specified via a command line switch on the Gunicorn invocation
11+
when profiling is requested, and it is read by Gunicorn to set the
12+
`pre_request` and `post_request` Gunicorn settings to the hook routines
13+
defined below.
14+
"""
15+
16+
import cProfile
17+
from io import StringIO
18+
import os
19+
import pstats
20+
import time
21+
22+
from gunicorn.http import Request
23+
from gunicorn.workers import base
24+
25+
26+
def pre_request(worker: base.Worker, req: Request):
27+
"""Gunicorn hook function called before a request is executed
28+
29+
We create a profile context, notify the log that we're profiling (so that
30+
no one is surprised), note the current time, and enable Python profiling.
31+
(We hang the profile context and time stamp off the existing Gunicorn
32+
worker object (ain't Python grand!?), and we use the worker's log.)
33+
"""
34+
worker.profile = cProfile.Profile()
35+
worker.log.info(f"PROFILING: {req.method} {req.uri}")
36+
worker.start_time = time.time()
37+
worker.profile.enable()
38+
39+
40+
def post_request(worker: base.Worker, req: Request, *_args):
41+
"""Gunicorn hook function called after a request is executed
42+
43+
Disable Python profiling; calculate the elapsed time for the request
44+
execution and log it; dump the statistics to a file, or create a pstats
45+
object which prints its output to a string-stream, sort the stats and print
46+
them to the stream, and dump the contents of the stream to the log. (We
47+
pull the profile context and start time from where we hung them off the
48+
Gunicorn worker object (ain't Python grand!?), and we use the worker's log.)
49+
"""
50+
worker.profile.disable()
51+
52+
total_time = time.time() - worker.start_time
53+
worker.log.info(f"Load Time: {total_time:.3}s {req.method} {req.uri}")
54+
55+
# If PB_PROFILE_DUMP_FILE is defined to a true-y value, assume that it is a
56+
# file into which to dump the profile data. (An excellent choice is
57+
# "/srv/pbench/public_html/pbench_server.prof", because this is writable
58+
# by the server and easily accessed by the user via the browser by hitting
59+
# "https://<server>/pbench_server.prof".) Otherwise, dump the top functions
60+
# in the profile to the log. (Note that, if PB_PROFILE_DUMP_FILE is
61+
# undefined, profiling will not have been enabled, and we'll never get here.)
62+
dump_file = os.environ.get("PB_PROFILE_DUMP_FILE")
63+
if dump_file:
64+
# Note that this will overwrite the previous request's data, if any.
65+
worker.profile.dump_stats(dump_file)
66+
else:
67+
s = StringIO()
68+
ps = pstats.Stats(worker.profile, stream=s).sort_stats("cumulative")
69+
ps.print_stats(30) # Limit the number of lines output
70+
worker.log.info(s.getvalue())

0 commit comments

Comments
 (0)