aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--debug_toolbar/media/debug_toolbar/toolbar.css23
-rw-r--r--debug_toolbar/panels/sql.py64
-rw-r--r--debug_toolbar/templates/debug_toolbar/panels/sql.html15
-rw-r--r--debug_toolbar/templates/debug_toolbar/panels/sql_explain.html4
-rw-r--r--debug_toolbar/templates/debug_toolbar/panels/sql_profile.html51
-rw-r--r--debug_toolbar/templates/debug_toolbar/panels/sql_select.html4
-rw-r--r--debug_toolbar/views.py35
7 files changed, 127 insertions, 69 deletions
diff --git a/debug_toolbar/media/debug_toolbar/toolbar.css b/debug_toolbar/media/debug_toolbar/toolbar.css
index b12d86a..316f4df 100644
--- a/debug_toolbar/media/debug_toolbar/toolbar.css
+++ b/debug_toolbar/media/debug_toolbar/toolbar.css
@@ -146,7 +146,7 @@
left:0px;
background-color:#eee;
color:#666;
- z-index:1000000;
+ z-index:100000000;
}
#djDebug .panelContent > div {
@@ -336,6 +336,27 @@
cursor:pointer;
}
+#djDebug .djDebugSqlWrap {
+ position:relative;
+}
+
+#djDebug .djDebugSql {
+ z-index:100000002;
+}
+
+#djDebug span.djDebugLineChart {
+ border-top:3px solid #777;
+ position:absolute;
+ bottom:0;
+ top:0;
+ left:0;
+ display:block;
+ z-index:1000000001;
+}
+#djDebug span.djDebugLineChartWarning {
+ border-top-color:#900;
+}
+
#djDebug .highlight { color:#000; }
#djDebug .highlight .err { color:#000; } /* Error */
#djDebug .highlight .g { color:#000; } /* Generic */
diff --git a/debug_toolbar/panels/sql.py b/debug_toolbar/panels/sql.py
index f12939e..a53a8d4 100644
--- a/debug_toolbar/panels/sql.py
+++ b/debug_toolbar/panels/sql.py
@@ -1,6 +1,6 @@
import os
import SocketServer
-import time
+from datetime import datetime
import traceback
import django
from django.conf import settings
@@ -16,6 +16,21 @@ from debug_toolbar.panels import DebugPanel
django_path = os.path.realpath(os.path.dirname(django.__file__))
socketserver_path = os.path.realpath(os.path.dirname(SocketServer.__file__))
+# TODO:This should be set in the toolbar loader as a default and panels should
+# get a copy of the toolbar object with access to its config dictionary
+SQL_WARNING_THRESHOLD = getattr(settings, 'DEBUG_TOOLBAR_CONFIG', {}).get('SQL_WARNING_THRESHOLD', 500)
+
+SQL_KEYWORDS = (
+ 'SELECT',
+ 'FROM',
+ 'WHERE',
+ 'INNER JOIN',
+ 'LEFT OUTER JOIN',
+ 'ORDER BY',
+ 'HAVING',
+ 'GROUP BY',
+)
+
def tidy_stacktrace(strace):
"""
Clean up stacktrace and remove all entries that:
@@ -39,11 +54,12 @@ class DatabaseStatTracker(util.CursorDebugWrapper):
in `connection.queries`.
"""
def execute(self, sql, params=()):
- start = time.time()
+ start = datetime.now()
try:
return self.cursor.execute(sql, params)
finally:
- stop = time.time()
+ stop = datetime.now()
+ duration = ms_from_timedelta(stop - start)
stacktrace = tidy_stacktrace(traceback.extract_stack())
_params = ''
try:
@@ -53,11 +69,14 @@ class DatabaseStatTracker(util.CursorDebugWrapper):
# We keep `sql` to maintain backwards compatibility
self.db.queries.append({
'sql': self.db.ops.last_executed_query(self.cursor, sql, params),
- 'time': (stop - start) * 1000, # convert to ms
+ 'duration': duration,
'raw_sql': sql,
'params': _params,
'hash': sha_constructor(settings.SECRET_KEY + sql + _params).hexdigest(),
'stacktrace': stacktrace,
+ 'start_time': start,
+ 'stop_time': stop,
+ 'is_slow': (duration > SQL_WARNING_THRESHOLD)
})
util.CursorDebugWrapper = DatabaseStatTracker
@@ -77,14 +96,14 @@ class SQLDebugPanel(DebugPanel):
return 'SQL'
def nav_subtitle(self):
- self._sql_time = sum(map(lambda q: float(q['time']), connection.queries))
+ self._sql_time = sum([q['duration'] for q in connection.queries[self._offset:]])
num_queries = len(connection.queries) - self._offset
return "%d %s in %.2fms" % (
num_queries,
(num_queries == 1) and 'query' or 'queries',
self._sql_time
)
-
+
def title(self):
return 'SQL Queries'
@@ -93,8 +112,15 @@ class SQLDebugPanel(DebugPanel):
def content(self):
sql_queries = connection.queries[self._offset:]
+ width_ratio_tally = 0
for query in sql_queries:
query['sql'] = reformat_sql(query['sql'])
+ try:
+ query['width_ratio'] = (query['duration'] / self._sql_time) * 100
+ except ZeroDivisionError:
+ query['width_ratio'] = 0
+ query['start_offset'] = width_ratio_tally
+ width_ratio_tally += query['width_ratio']
context = {
'queries': sql_queries,
@@ -103,22 +129,14 @@ class SQLDebugPanel(DebugPanel):
}
return render_to_string('debug_toolbar/panels/sql.html', context)
+def ms_from_timedelta(td):
+ """
+ Given a timedelta object, returns a float representing milliseconds
+ """
+ return (td.seconds * 1000) + (td.microseconds / 1000.0)
+
def reformat_sql(sql):
- sql = sql.replace(',', ', ')
- sql = sql.replace('SELECT ', 'SELECT\n\t')
- sql = sql.replace(' FROM ', '\nFROM\n\t')
- sql = sql.replace(' WHERE ', '\nWHERE\n\t')
- sql = sql.replace(' INNER JOIN', '\n\tINNER JOIN')
- sql = sql.replace(' LEFT OUTER JOIN' , '\n\tLEFT OUTER JOIN')
- sql = sql.replace(' ORDER BY ', '\nORDER BY\n\t')
- sql = sql.replace(' HAVING ', '\nHAVING\n\t')
- sql = sql.replace(' GROUP BY ', '\nGROUP BY\n\t')
- # Use Pygments to highlight SQL if it's available
- try:
- from pygments import highlight
- from pygments.lexers import SqlLexer
- from pygments.formatters import HtmlFormatter
- sql = highlight(sql, SqlLexer(), HtmlFormatter())
- except ImportError:
- pass
+ for kwd in SQL_KEYWORDS:
+ sql = sql.replace(kwd, '<strong>%s</strong>' % (kwd,))
return sql
+
diff --git a/debug_toolbar/templates/debug_toolbar/panels/sql.html b/debug_toolbar/templates/debug_toolbar/panels/sql.html
index 7aa4c2d..9c8bfa6 100644
--- a/debug_toolbar/templates/debug_toolbar/panels/sql.html
+++ b/debug_toolbar/templates/debug_toolbar/panels/sql.html
@@ -11,13 +11,13 @@
<tbody>
{% for query in queries %}
<tr class="{% cycle 'djDebugOdd' 'djDebugEven' %}">
- <td>{{ query.time|floatformat:"2" }}</td>
+ <td>{{ query.duration|floatformat:"2" }}</td>
<td>
{% if query.params %}
- <a class="remoteCall" href="/__debug__/sql_select/?sql={{ query.raw_sql|urlencode }}&params={{ query.params|urlencode }}&time={{ query.time|floatformat:"2"|urlencode }}&hash={{ query.hash }}">SELECT</a>
- <a class="remoteCall" href="/__debug__/sql_explain/?sql={{ query.raw_sql|urlencode }}&params={{ query.params|urlencode }}&time={{ query.time|floatformat:"2"|urlencode }}&hash={{ query.hash }}">EXPLAIN</a>
+ <a class="remoteCall" href="/__debug__/sql_select/?sql={{ query.raw_sql|urlencode }}&params={{ query.params|urlencode }}&duration={{ query.duration|floatformat:"2"|urlencode }}&hash={{ query.hash }}">SELECT</a>
+ <a class="remoteCall" href="/__debug__/sql_explain/?sql={{ query.raw_sql|urlencode }}&params={{ query.params|urlencode }}&duration={{ query.duration|floatformat:"2"|urlencode }}&hash={{ query.hash }}">EXPLAIN</a>
{% if is_mysql %}
- <a class="remoteCall" href="/__debug__/sql_profile/?sql={{ query.raw_sql|urlencode }}&params={{ query.params|urlencode }}&time={{ query.time|floatformat:"2"|urlencode }}&hash={{ query.hash }}">PROFILE</a>
+ <a class="remoteCall" href="/__debug__/sql_profile/?sql={{ query.raw_sql|urlencode }}&params={{ query.params|urlencode }}&duration={{ query.duration|floatformat:"2"|urlencode }}&hash={{ query.hash }}">PROFILE</a>
{% endif %}
{% endif %}
</td>
@@ -42,7 +42,12 @@
</div>
{% endif %}
</td>
- <td class="syntax">{{ query.sql|safe }}</td>
+ <td class="syntax">
+ <div class="djDebugSqlWrap">
+ <div class="djDebugSql">{{ query.sql|safe }}</div>
+ <span class="djDebugLineChart{% if query.is_slow %} djDebugLineChartWarning{% endif %}" style="width:{{ query.width_ratio }}%; left:{{ query.start_offset }}%;"</span>
+ </div>
+ </td>
</tr>
{% endfor %}
</tbody>
diff --git a/debug_toolbar/templates/debug_toolbar/panels/sql_explain.html b/debug_toolbar/templates/debug_toolbar/panels/sql_explain.html
index 38249b3..a21ec59 100644
--- a/debug_toolbar/templates/debug_toolbar/panels/sql_explain.html
+++ b/debug_toolbar/templates/debug_toolbar/panels/sql_explain.html
@@ -6,9 +6,9 @@
<div class="djDebugPanelContent">
<dl>
<dt>{% trans "Executed SQL" %}</dt>
- <dd><pre>{{ sql|safe }}</pre></dd>
+ <dd>{{ sql|safe }}</dd>
<dt>{% trans "Time" %}</dt>
- <dd>{{ time }} ms</dd>
+ <dd>{{ duration }} ms</dd>
</dl>
<table>
<thead>
diff --git a/debug_toolbar/templates/debug_toolbar/panels/sql_profile.html b/debug_toolbar/templates/debug_toolbar/panels/sql_profile.html
index 7df65da..a15574e 100644
--- a/debug_toolbar/templates/debug_toolbar/panels/sql_profile.html
+++ b/debug_toolbar/templates/debug_toolbar/panels/sql_profile.html
@@ -4,28 +4,35 @@
<h3>SQL Profiled</h3>
</div>
<div class="djDebugPanelContent">
- <dl>
- <dt>{% trans "Executed SQL" %}</dt>
- <dd><pre>{{ sql|safe }}</pre></dd>
- <dt>{% trans "Time" %}</dt>
- <dd>{{ time }} ms</dd>
- </dl>
- <table>
- <thead>
- <tr>
- {% for h in headers %}
- <th>{{ h|upper }}</th>
- {% endfor %}
- </tr>
- </thead>
- <tbody>
- {% for row in result %}
- <tr class="{% cycle 'djDebugOdd' 'djDebugEven' %}">
- {% for column in row %}
- <td>{{ column|escape }}</td>
+ {% if result %}
+ <dl>
+ <dt>{% trans "Executed SQL" %}</dt>
+ <dd>{{ sql|safe }}</dd>
+ <dt>{% trans "Time" %}</dt>
+ <dd>{{ duration }} ms</dd>
+ </dl>
+ <table>
+ <thead>
+ <tr>
+ {% for h in headers %}
+ <th>{{ h|upper }}</th>
{% endfor %}
</tr>
- {% endfor %}
- </tbody>
- </table>
+ </thead>
+ <tbody>
+ {% for row in result %}
+ <tr class="{% cycle 'djDebugOdd' 'djDebugEven' %}">
+ {% for column in row %}
+ <td>{{ column|escape }}</td>
+ {% endfor %}
+ </tr>
+ {% endfor %}
+ </tbody>
+ </table>
+ {% else %}
+ <dl>
+ <dt>{% trans 'Error' %}</dt>
+ <dd>{{ result_error }}</dd>
+ </dl>
+ {% endif %}
</div>
diff --git a/debug_toolbar/templates/debug_toolbar/panels/sql_select.html b/debug_toolbar/templates/debug_toolbar/panels/sql_select.html
index e4d6027..66835c8 100644
--- a/debug_toolbar/templates/debug_toolbar/panels/sql_select.html
+++ b/debug_toolbar/templates/debug_toolbar/panels/sql_select.html
@@ -6,9 +6,9 @@
<div class="djDebugPanelContent">
<dl>
<dt>{% trans "Executed SQL" %}</dt>
- <dd><pre>{{ sql|safe }}</pre></dd>
+ <dd>{{ sql|safe }}</dd>
<dt>{% trans "Time" %}</dt>
- <dd>{{ time }} ms</dd>
+ <dd>{{ duration }} ms</dd>
</dl>
{% if result %}
<table>
diff --git a/debug_toolbar/views.py b/debug_toolbar/views.py
index 9123a00..3a8cb79 100644
--- a/debug_toolbar/views.py
+++ b/debug_toolbar/views.py
@@ -33,7 +33,7 @@ def sql_select(request):
Expected GET variables:
sql: urlencoded sql with positional arguments
params: JSON encoded parameter values
- time: time for SQL to execute passed in from toolbar just for redisplay
+ duration: time for SQL to execute passed in from toolbar just for redisplay
hash: the hash of (secret + sql + params) for tamper checking
"""
from debug_toolbar.panels.sql import reformat_sql
@@ -52,7 +52,7 @@ def sql_select(request):
context = {
'result': result,
'sql': reformat_sql(cursor.db.ops.last_executed_query(cursor, sql, params)),
- 'time': request.GET.get('time', 0.0),
+ 'duration': request.GET.get('duration', 0.0),
'headers': headers,
}
return render_to_response('debug_toolbar/panels/sql_select.html', context)
@@ -65,7 +65,7 @@ def sql_explain(request):
Expected GET variables:
sql: urlencoded sql with positional arguments
params: JSON encoded parameter values
- time: time for SQL to execute passed in from toolbar just for redisplay
+ duration: time for SQL to execute passed in from toolbar just for redisplay
hash: the hash of (secret + sql + params) for tamper checking
"""
from debug_toolbar.panels.sql import reformat_sql
@@ -84,7 +84,7 @@ def sql_explain(request):
context = {
'result': result,
'sql': reformat_sql(cursor.db.ops.last_executed_query(cursor, sql, params)),
- 'time': request.GET.get('time', 0.0),
+ 'duration': request.GET.get('duration', 0.0),
'headers': headers,
}
return render_to_response('debug_toolbar/panels/sql_explain.html', context)
@@ -97,7 +97,7 @@ def sql_profile(request):
Expected GET variables:
sql: urlencoded sql with positional arguments
params: JSON encoded parameter values
- time: time for SQL to execute passed in from toolbar just for redisplay
+ duration: time for SQL to execute passed in from toolbar just for redisplay
hash: the hash of (secret + sql + params) for tamper checking
"""
from debug_toolbar.panels.sql import reformat_sql
@@ -109,21 +109,28 @@ def sql_profile(request):
if sql.lower().strip().startswith('select'):
params = simplejson.loads(params)
cursor = connection.cursor()
- cursor.execute("SET PROFILING=1") # Enable profiling
- cursor.execute(sql, params) # Execute SELECT
- cursor.execute("SET PROFILING=0") # Disable profiling
- # The Query ID should always be 1 here but I'll subselect to get the last one just in case...
- cursor.execute("SELECT * FROM information_schema.profiling WHERE query_id=(SELECT query_id FROM information_schema.profiling ORDER BY query_id DESC LIMIT 1)")
- headers = [d[0] for d in cursor.description]
- result = cursor.fetchall()
+ result = None
+ headers = None
+ result_error = None
+ try:
+ cursor.execute("SET PROFILING=1") # Enable profiling
+ cursor.execute(sql, params) # Execute SELECT
+ cursor.execute("SET PROFILING=0") # Disable profiling
+ # The Query ID should always be 1 here but I'll subselect to get the last one just in case...
+ cursor.execute("SELECT * FROM information_schema.profiling WHERE query_id=(SELECT query_id FROM information_schema.profiling ORDER BY query_id DESC LIMIT 1)")
+ headers = [d[0] for d in cursor.description]
+ result = cursor.fetchall()
+ except:
+ result_error = "Profiling is either not available or not supported by your database."
cursor.close()
context = {
'result': result,
+ 'result_error': result_error,
'sql': reformat_sql(cursor.db.ops.last_executed_query(cursor, sql, params)),
- 'time': request.GET.get('time', 0.0),
+ 'duration': request.GET.get('duration', 0.0),
'headers': headers,
}
- return render_to_response('debug_toolbar/panels/sql_explain.html', context)
+ return render_to_response('debug_toolbar/panels/sql_profile.html', context)
raise InvalidSQLError("Only 'select' queries are allowed.")
def template_source(request):