Profiling Python Flask Web Apps with Pyinstrument

In the previous post, I talked about using cProfile (via Werkzeug Application Profiler Middleware) to profile parts of code or routes in Flask web apps. In this one, we’ll explore another tool called pyinstrument that does statistical profiling instead of deterministic profiling, that can be easily used with Flask or any other web framework like Django or FastAPI in Python.

Benefits of statistical profiling? It adds less overhead compared to something like cProfile because instead of tracking every function call (via hooks provided by Python C API), it records the call stack every 1ms (configurable). This means super fast functions that end up executing within 1ms will not get “traced” or profiled unless you configure it which is super easy.

Some of the advantages of using pyinstrument are:

  • Statistical profiling.
  • We can choose to profile behind flags, i.e., no need to profile each and every request. We’ll see how to do this below.
  • We don’t have to use another tool (like SnakeViz) to get a graphical representation of the profiled summary or report. pyinstrument by itself can generate a nice HTML report of where the code spent most of its time. This report is interactive and has a good user interface.

Usage

Installing pyinstrument is super simple:

$ pip install pyinstrument

To profile a python script all we have to do is:

$ pyinstrument script.py # Instead of python script.py

Let’s say our script.py had the following code:

def foo():
    a = 10

for _ in range(100000000):
    foo()

When you run the pyinstrument command above, at the end of the script execution you’ll see a coloured summary showing how the individual and cumulative time was spent across different parts of the code (functions, methods, etc.).

Pyinstrument CLI Report

You can also get a beautiful interactive HTML report by using the -r option:

$ pyinstrument -r html script.py
stdout is a terminal, so saved profile output to /tmp/tmpsgobuxw2.html

Open the file in your browser of choice and you’ll see a nice interactive user interface where you can carefully analyse the time taken by different parts of the code.

Pyinstrument HTML Report

Now all this is cool, but we wanted to primarily learn to how to use the profiler with our Flask web apps. We do this by using pyinstrument‘s Python API. Basically, if we wanted to profile just a portion of our source code, the tool allows us to do that like this:

from pyinstrument import Profiler

profiler = Profiler()
profiler.start()

# code you want to profile

profiler.stop()

profiler.print()

Now in the case of Flask, we can start() and stop() the profiler in before_request() and after_request() respectively and by the end of it, return the HTML output to the browser. Also, we can choose to run the profiler behind a flag.

from flask import Flask, g, make_response, request
app = Flask(__name__)

@app.before_request
def before_request():
    if "profile" in request.args:
        g.profiler = Profiler()
        g.profiler.start()

@app.after_request
def after_request(response):
    if not hasattr(g, "profiler"):
        return response
    g.profiler.stop()
    output_html = g.profiler.output_html()
    return make_response(output_html)

I’ve taken this piece of code from the pyinstrument documentation. Here’s what it does:

  • Start the profiler in before_request() only when the profile query parameter (?profile) is set. This query param acts like a flag to enable/disable profiling.
  • The profiler instance is saved in g which is a special global variable provided by Flask where you can manage resources during a request. The data on g is lost after the context ends, i.e., after the request ends.
  • Stop the profiler in after_request and generate the HTML output to be sent as response to the client.

Now you can interact with the profile report in your browser. In some cases, you may want to save the HTML output in a local file for future analysis.

Leave a Reply

Your email address will not be published.