diff --git a/README.rst b/README.rst index c4d494b..c00f827 100644 --- a/README.rst +++ b/README.rst @@ -9,8 +9,8 @@ Installation ------------ 1. Run `pip install django-live-profiler` 2. Add `'profiler'` app to `INSTALLED_APPS` -3. Add `'profiler.middleware.ProfilerMiddleware'` to `MIDDLEWARE_CLASSES` -4. Optionally add `'profiler.middleware.StatProfMiddleware'` to `MIDDLEWARE_CLASSES` to enable Python code statistical profiling (using statprof_). WARNING: this is an experimental feature, beware of possible incorrect output. +3. Add `'profiler.middleware.ProfilerMiddleware'` to `MIDDLEWARE` +4. Optionally add `'profiler.middleware.StatProfMiddleware'` to `MIDDLEWARE` to enable Python code statistical profiling (using statprof_). WARNING: this is an experimental feature, beware of possible incorrect output. 5. Add `url(r'^profiler/', include('profiler.urls'))` to your urlconf .. _statprof: https://github.com/bos/statprof.py @@ -23,5 +23,18 @@ In order to start gathering data you need to start the aggregation server:: $ aggregated --host 127.0.0.1 --port 5556 +Note, you must run Django with threading disabled in order for statprof to work! + + $ ./manage runserver --noreload --nothreading + +You may experience issues with staticfiles loading in chrome when `--nothreading` is passed. + +This is because chrome opens two initial connections, which blocks when Django is only able to respond to one of them. To fix this, you must serve staticfiles via separate staticfile server, such as nginx with a reverse_proxy to your Django runserver. Visit http://yoursite.com/profiler/ for results. + + +Note: you must be logged in as a superuser to view the profiler page. +You can create a superuser account with: + + $ ./manage.py createsuperuser diff --git a/profiler/__init__.py b/profiler/__init__.py index 33f96ff..c233dc8 100644 --- a/profiler/__init__.py +++ b/profiler/__init__.py @@ -1,9 +1,10 @@ -import threading +current_view = None -_local = threading.local() - -def _set_current_view(view): - _local.current_view = view +def _set_current_view(view_name): + global current_view + assert view_name is not None + current_view = view_name def _get_current_view(): - return getattr(_local, 'current_view', None) + global current_view + return current_view diff --git a/profiler/instrument.py b/profiler/instrument.py index 36a8791..5c12acc 100644 --- a/profiler/instrument.py +++ b/profiler/instrument.py @@ -3,12 +3,14 @@ from django.db.models.sql.compiler import SQLCompiler from django.db.models.sql.datastructures import EmptyResultSet from django.db.models.sql.constants import MULTI +from django.db.models.query import QuerySet from django.db import connection from aggregate.client import get_client from profiler import _get_current_view + def execute_sql(self, *args, **kwargs): client = get_client() if client is None: @@ -19,7 +21,7 @@ def execute_sql(self, *args, **kwargs): raise EmptyResultSet except EmptyResultSet: if kwargs.get('result_type', MULTI) == MULTI: - return iter([]) + return QuerySet.none() else: return start = datetime.now() @@ -27,9 +29,9 @@ def execute_sql(self, *args, **kwargs): return self.__execute_sql(*args, **kwargs) finally: d = (datetime.now() - start) - client.insert({'query' : q, 'view' : _get_current_view(), 'type' : 'sql'}, + client.insert({'query' : q, 'view' : _get_current_view(), 'type' : 'sql'}, {'time' : 0.0 + d.seconds * 1000 + d.microseconds/1000, 'count' : 1}) - + INSTRUMENTED = False @@ -38,3 +40,4 @@ def execute_sql(self, *args, **kwargs): SQLCompiler.__execute_sql = SQLCompiler.execute_sql SQLCompiler.execute_sql = execute_sql INSTRUMENTED = True + diff --git a/profiler/middleware.py b/profiler/middleware.py index 7e8f01a..617292d 100644 --- a/profiler/middleware.py +++ b/profiler/middleware.py @@ -1,60 +1,73 @@ -from datetime import datetime import inspect import statprof -from django.db import connection -from django.core.cache import cache from django.conf import settings - +from django.urls import resolve from aggregate.client import get_client from profiler import _set_current_view -class ProfilerMiddleware(object): - def process_view(self, request, view_func, view_args, view_kwargs): - if inspect.ismethod(view_func): - view_name = view_func.im_class.__module__+ '.' + view_func.im_class.__name__ + view_func.__name__ +def ProfilerMiddleware(get_response): + def middleware(request): + if request.path.startswith('/profiler'): + return get_response(request) + + view = resolve(request.path).func + if inspect.ismethod(view): + view_name = view.__class__.__module__+ '.' + view.__class__.__name__ else: - view_name = view_func.__module__ + '.' + view_func.__name__ + view_name = view.__module__ + '.' + view.__name__ _set_current_view(view_name) - - def process_response(self, request, response): - _set_current_view(None) - return response + return get_response(request) + + return middleware + -class StatProfMiddleware(object): +def StatProfMiddleware(get_response): + def middleware(request): + if request.path.startswith('/profiler'): + return get_response(request) - def process_request(self, request): + # print(f'[i] Starting sampling on {request.path}..') statprof.reset(getattr(settings, 'LIVEPROFILER_STATPROF_FREQUENCY', 100)) statprof.start() - def process_response(self, request, response): + response = get_response(request) + statprof.stop() - client = get_client() total_samples = statprof.state.sample_count if total_samples == 0: return response secs_per_sample = statprof.state.accumulated_time / total_samples - client.insert_all([( - {'file' : c.key.filename, - 'lineno' : c.key.lineno, - 'function' : c.key.name, - 'type' : 'python'}, - {'self_nsamples' : c.self_sample_count, - 'cum_nsamples' : c.cum_sample_count, - 'tot_nsamples' : total_samples, - 'cum_time' : c.cum_sample_count * secs_per_sample, - 'self_time' : c.self_sample_count * secs_per_sample - }) - for c in statprof.CallData.all_calls.itervalues()]) - - + # print('[i] Getting ZQM client...') + client = get_client() + client.insert_all([ + ( + { + 'file': c.key.filename, + 'lineno': c.key.lineno, + 'function': c.key.name, + 'type': 'python', + }, + { + 'self_nsamples': c.self_sample_count, + 'cum_nsamples': c.cum_sample_count, + 'tot_nsamples': total_samples, + 'cum_time': c.cum_sample_count * secs_per_sample, + 'self_time': c.self_sample_count * secs_per_sample, + } + ) + for c in statprof.CallData.all_calls.values() + ]) + # print(f'[i] Saved {statprof.state.sample_count} samples for {request.path}.') return response + + return middleware diff --git a/profiler/templates/profiler/base.html b/profiler/templates/profiler/base.html index 953bcee..6dee746 100644 --- a/profiler/templates/profiler/base.html +++ b/profiler/templates/profiler/base.html @@ -1,3 +1,5 @@ +{% load static %} +
@@ -8,12 +10,12 @@ - - + + - - + + @@ -29,10 +31,10 @@ @@ -44,7 +46,7 @@ {% endblock %} - +