Last active
December 6, 2018 11:20
-
-
Save mskrajnowski/1ddc318b4124ed152f6d to your computer and use it in GitHub Desktop.
Flamegraph django middleware
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
Django middleware for generating request flame graphs. | |
Requires the flamegraph.pl perl script: | |
https://github.com/brendangregg/FlameGraph/blob/master/flamegraph.pl | |
Installation: | |
1. Create a directory for flame graphs | |
2. Copy the flamegraph.pl script to it | |
3. Add the FLAMES_DIR django setting | |
4. Add the flames.FlamesMiddleware to MIDDLEWARE_CLASSES | |
Usage: | |
To generate a flame graph just append ?flames to the requested url. | |
Middleware will create an svg in the FLAMES_DIR with the current timestamp. | |
Uncomment line 88 to automatically open the svg in a new google chrome tab. | |
""" | |
import os | |
import subprocess | |
import sys | |
import threading | |
import time | |
import traceback | |
from datetime import datetime | |
from xml.dom.minidom import Text | |
from django.conf import settings | |
FLAMES_DIR = os.path.abspath(settings.FLAMES_DIR) | |
FLAMEGRAPH_SCRIPT_PATH = os.path.join(FLAMES_DIR, 'flamegraph.pl') | |
def get_module_name(module_path): | |
for path in sys.path: | |
path = path or os.getcwd() | |
if module_path.startswith(path): | |
rel_path = module_path[len(path) + 1:] | |
return ( | |
rel_path | |
.replace(u'/__init__.py', u'') | |
.replace(u'/', '.') | |
.replace(u'.py', u'') | |
.strip() | |
) | |
return module_path | |
def write_samples(file, samples): | |
for _, frame in samples: | |
stack = traceback.extract_stack(frame) | |
frame_strings = ( | |
func_name + u'@' + get_module_name(module_path) | |
for module_path, _, func_name, _ in stack | |
) | |
stack_string = u';'.join(frame_strings) | |
file.write(u'{} {}\n'.format(stack_string, 1)) | |
def write_flames(logger, title='flames'): | |
base_path = os.path.join( | |
FLAMES_DIR, | |
datetime.now().strftime('%Y-%m-%dT%H:%M:%S'), | |
) | |
out_txt_path = base_path + '.txt' | |
out_svg_path = base_path + '.svg' | |
title_element = Text() | |
title_element.data = title | |
title_xml = title_element.toxml() | |
with open(out_txt_path, 'w') as out_txt: | |
write_samples(out_txt, logger.samples) | |
with open(out_svg_path, 'w') as out_svg: | |
subprocess.call( | |
[ | |
'perl', FLAMEGRAPH_SCRIPT_PATH, out_txt_path, | |
'--title', title_xml, | |
], | |
stdout=out_svg | |
) | |
# subprocess.call(['google-chrome', 'file://' + out_svg_path]) | |
class StackLogger(object): | |
def __init__(self, thread_id, interval=0.001): | |
super(StackLogger, self).__init__() | |
self.thread_id = thread_id | |
self.interval = interval | |
self.should_stop = False | |
self.samples = [] | |
self.started = threading.Event() | |
self.thread = threading.Thread(target=self.run) | |
def run(self): | |
start_time = time.time() | |
self.samples = [] | |
self.started.set() | |
while not self.should_stop: | |
frame_start = time.clock() | |
timestamp = time.time() - start_time | |
frame = sys._current_frames()[self.thread_id] | |
self.samples.append((timestamp, frame)) | |
frame_dt = time.clock() - frame_start | |
time.sleep(max(0, self.interval - frame_dt)) | |
def start(self): | |
self.started.clear() | |
self.should_stop = False | |
self.thread.start() | |
self.started.wait() | |
def stop(self): | |
self.should_stop = True | |
self.thread.join() | |
class FlamesMiddleware(object): | |
def process_request(self, request): | |
if settings.DEBUG and 'flames' in request.GET: | |
logger = StackLogger(threading.current_thread().ident) | |
logger.start() | |
request._stack_logger = logger | |
def process_response(self, request, response): | |
if settings.DEBUG and hasattr(request, '_stack_logger'): | |
logger = request._stack_logger | |
logger.stop() | |
url = request.build_absolute_uri() | |
write_flames(logger, title=url) | |
return response |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Use this fork for >= django 1.10.