|
| 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