Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
13 changes: 7 additions & 6 deletions profiler/__init__.py
Original file line number Diff line number Diff line change
@@ -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
9 changes: 6 additions & 3 deletions profiler/instrument.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -19,17 +21,17 @@ 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()
try:
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


Expand All @@ -38,3 +40,4 @@ def execute_sql(self, *args, **kwargs):
SQLCompiler.__execute_sql = SQLCompiler.execute_sql
SQLCompiler.execute_sql = execute_sql
INSTRUMENTED = True

75 changes: 44 additions & 31 deletions profiler/middleware.py
Original file line number Diff line number Diff line change
@@ -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
20 changes: 11 additions & 9 deletions profiler/templates/profiler/base.html
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
{% load static %}

<!DOCTYPE html>
<html lang="en">
<head>
Expand All @@ -8,12 +10,12 @@
<meta name="author" content="">

<!-- Le styles -->
<link href="{{ STATIC_URL }}profiler/bootstrap.css" rel="stylesheet">
<link href="{{ STATIC_URL }}profiler/profiler.css" rel="stylesheet">
<link href="{% static 'profiler/bootstrap.css' %}" rel="stylesheet">
<link href="{% static 'profiler/profiler.css' %}" rel="stylesheet">


<script src="{{ STATIC_URL }}profiler/jquery-1.7.2.min.js" type="text/javascript"></script>
<script src="{{ STATIC_URL }}profiler/jquery.tablesorter.min.js" type="text/javascript"></script>
<script src="{% static 'profiler/jquery-1.7.2.min.js' %}" type="text/javascript"></script>
<script src="{% static 'profiler/jquery.tablesorter.min.js' %}" type="text/javascript"></script>
<script src="http://d3js.org/d3.v3.min.js"></script>
</head>

Expand All @@ -29,10 +31,10 @@
</a>
<div class="nav-collapse collapse">
<ul class="nav">
<li><a href="{% url "profiler_global_stats" %}">SQL global</a></li>
<li><a href="{% url "profiler_stats_by_view" %}">SQL by view</a></li>
<li><a href="{% url "profiler_python_stats" %}">Python code</a></li>
<li><a href="{% url "profiler_reset" %}?next={{ request.path }}">Reset</a></li>
<li><a href="{% url 'profiler_global_stats' %}">SQL global</a></li>
<li><a href="{% url 'profiler_stats_by_view' %}">SQL by view</a></li>
<li><a href="{% url 'profiler_python_stats' %}">Python code</a></li>
<li><a href="{% url 'profiler_reset' %}?next={{ request.path }}">Reset</a></li>
</ul>
</div><!--/.nav-collapse -->
</div>
Expand All @@ -44,7 +46,7 @@
{% endblock %}

</div> <!-- /container -->
<script src="{{ STATIC_URL }}profiler/profiler.js" type="text/javascript"></script>
<script src="{% static 'profiler/profiler.js' %}" type="text/javascript"></script>

</body>
</html>
22 changes: 14 additions & 8 deletions profiler/urls.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
from django.conf.urls.defaults import *
from django.urls import path

urlpatterns = patterns(
'profiler.views',
url(r'^$', 'global_stats', name='profiler_global_stats'),
url(r'^by_view/$', 'stats_by_view', name='profiler_stats_by_view'),
url(r'^code/$', 'python_stats', name='profiler_python_stats'),
url(r'^reset/$', 'reset', name='profiler_reset'),
)
from .views import (
global_stats,
stats_by_view,
python_stats,
reset,
)

urlpatterns = [
path('', global_stats, name='profiler_global_stats'),
path('by_view/', stats_by_view, name='profiler_stats_by_view'),
path('code/', python_stats, name='profiler_python_stats'),
path('reset/', reset, name='profiler_reset'),
]

31 changes: 13 additions & 18 deletions profiler/views.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import render_to_response
from django.template.context import RequestContext
from django.core.cache import cache
import json

from django.http import HttpResponseRedirect
from django.shortcuts import render
from django.contrib.auth.decorators import user_passes_test
from django.core.urlresolvers import reverse
from django.utils import simplejson
from django.urls import reverse

from aggregate.client import get_client

Expand All @@ -13,9 +12,8 @@ def global_stats(request):
stats = get_client().select(group_by=['query'], where={'type':'sql'})
for s in stats:
s['average_time'] = s['time'] / s['count']
return render_to_response('profiler/index.html',
{'queries' : stats},
context_instance=RequestContext(request))
return render(request, 'profiler/index.html',
{'queries' : stats})

@user_passes_test(lambda u:u.is_superuser)
def stats_by_view(request):
Expand All @@ -40,26 +38,23 @@ def stats_by_view(request):
for r in stats:
r['normtime'] = (0.0+r['average_time'])/maxtime

return render_to_response('profiler/by_view.html',
return render(request, 'profiler/by_view.html',
{'queries' : grouped,
'stats' :simplejson.dumps(stats)},
context_instance=RequestContext(request))
'stats' :json.dumps(stats)})

@user_passes_test(lambda u:u.is_superuser)
def reset(request):
next = request.GET.get('next') or request.POST.get('next') or request.META.get('HTTP_REFERER') or reverse('profiler_global_stats')
if request.method == 'POST':
get_client().clear()
return HttpResponseRedirect(next)
return render_to_response('profiler/reset.html',
{'next' : next},
context_instance=RequestContext(request))
return render(request, 'profiler/reset.html',
{'next' : next})



@user_passes_test(lambda u:u.is_superuser)
def python_stats(request):
stats = get_client().select(group_by=['file','lineno'], where={'type':'python'})
return render_to_response('profiler/code.html',
{'stats' : stats},
context_instance=RequestContext(request))
return render(request, 'profiler/code.html',
{'stats' : stats})