diff options
| author | Aymeric Augustin | 2013-11-15 22:43:59 +0100 |
|---|---|---|
| committer | Aymeric Augustin | 2013-11-15 22:43:59 +0100 |
| commit | 6334983458abd4380c21275d1229527778cf93a6 (patch) | |
| tree | 1b3e9304f93600fa7b38bfa8a6cc20b3857d7375 /debug_toolbar/panels/sql/panel.py | |
| parent | f0d0ddbada065ec0ff4fc64aed9d2f9ba48ba5a3 (diff) | |
| download | django-debug-toolbar-6334983458abd4380c21275d1229527778cf93a6.tar.bz2 | |
Continue moving panel-specific code within panels.
Structure the SQL and template panels as packages as they're growing.
Diffstat (limited to 'debug_toolbar/panels/sql/panel.py')
| -rw-r--r-- | debug_toolbar/panels/sql/panel.py | 210 |
1 files changed, 210 insertions, 0 deletions
diff --git a/debug_toolbar/panels/sql/panel.py b/debug_toolbar/panels/sql/panel.py new file mode 100644 index 0000000..cb80901 --- /dev/null +++ b/debug_toolbar/panels/sql/panel.py @@ -0,0 +1,210 @@ +from __future__ import unicode_literals + +import uuid +from copy import copy + +from django.conf.urls import patterns, url +from django.db import connections +from django.utils.translation import ugettext_lazy as _, ungettext_lazy as __ + +from debug_toolbar.panels import DebugPanel +from debug_toolbar.panels.sql.forms import SQLSelectForm +from debug_toolbar.utils import render_stacktrace +from debug_toolbar.panels.sql.utils import reformat_sql +from debug_toolbar.panels.sql.tracking import CursorWrapper + + +def get_isolation_level_display(engine, level): + if engine == 'psycopg2': + import psycopg2.extensions + choices = { + psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT: _('Autocommit'), + psycopg2.extensions.ISOLATION_LEVEL_READ_UNCOMMITTED: _('Read uncommitted'), + psycopg2.extensions.ISOLATION_LEVEL_READ_COMMITTED: _('Read committed'), + psycopg2.extensions.ISOLATION_LEVEL_REPEATABLE_READ: _('Repeatable read'), + psycopg2.extensions.ISOLATION_LEVEL_SERIALIZABLE: _('Serializable'), + } + else: + raise ValueError(engine) + return choices.get(level) + + +def get_transaction_status_display(engine, level): + if engine == 'psycopg2': + import psycopg2.extensions + choices = { + psycopg2.extensions.TRANSACTION_STATUS_IDLE: _('Idle'), + psycopg2.extensions.TRANSACTION_STATUS_ACTIVE: _('Active'), + psycopg2.extensions.TRANSACTION_STATUS_INTRANS: _('In transaction'), + psycopg2.extensions.TRANSACTION_STATUS_INERROR: _('In error'), + psycopg2.extensions.TRANSACTION_STATUS_UNKNOWN: _('Unknown'), + } + else: + raise ValueError(engine) + return choices.get(level) + + +class SQLDebugPanel(DebugPanel): + """ + Panel that displays information about the SQL queries run while processing + the request. + """ + name = 'SQL' + template = 'debug_toolbar/panels/sql.html' + has_content = True + + def __init__(self, *args, **kwargs): + super(SQLDebugPanel, self).__init__(*args, **kwargs) + self._offset = dict((k, len(connections[k].queries)) for k in connections) + self._sql_time = 0 + self._num_queries = 0 + self._queries = [] + self._databases = {} + self._transaction_status = {} + self._transaction_ids = {} + + def get_transaction_id(self, alias): + if alias not in connections: + return + conn = connections[alias].connection + if not conn: + return + + engine = conn.__class__.__module__.split('.', 1)[0] + if engine == 'psycopg2': + cur_status = conn.get_transaction_status() + else: + raise ValueError(engine) + + last_status = self._transaction_status.get(alias) + self._transaction_status[alias] = cur_status + + if not cur_status: + # No available state + return None + + if cur_status != last_status: + if cur_status: + self._transaction_ids[alias] = uuid.uuid4().hex + else: + self._transaction_ids[alias] = None + + return self._transaction_ids[alias] + + def record(self, alias, **kwargs): + self._queries.append((alias, kwargs)) + if alias not in self._databases: + self._databases[alias] = { + 'time_spent': kwargs['duration'], + 'num_queries': 1, + } + else: + self._databases[alias]['time_spent'] += kwargs['duration'] + self._databases[alias]['num_queries'] += 1 + self._sql_time += kwargs['duration'] + self._num_queries += 1 + + @classmethod + def get_urls(cls): + return patterns('debug_toolbar.panels.sql.views', # noqa + url(r'^sql_select/$', 'sql_select', name='sql_select'), + url(r'^sql_explain/$', 'sql_explain', name='sql_explain'), + url(r'^sql_profile/$', 'sql_profile', name='sql_profile'), + ) + + def nav_title(self): + return _('SQL') + + def nav_subtitle(self): + return __("%d query in %.2fms", "%d queries in %.2fms", + self._num_queries) % (self._num_queries, self._sql_time) + + def title(self): + count = len(self._databases) + return __('SQL Queries from %(count)d connection', + 'SQL Queries from %(count)d connections', + count) % dict(count=count) + + def enable_instrumentation(self): + # This is thread-safe because database connections are thread-local. + for connection in connections.all(): + if not hasattr(connection, '_djdt_cursor'): + connection._djdt_cursor = connection.cursor + connection.cursor = lambda: CursorWrapper( + connection._djdt_cursor(), connection, self) + + def disable_instrumentation(self): + for connection in connections.all(): + if hasattr(connection, '_djdt_cursor'): + del connection._djdt_cursor + del connection.cursor + + def process_response(self, request, response): + if self._queries: + width_ratio_tally = 0 + factor = int(256.0 / (len(self._databases) * 2.5)) + for n, db in enumerate(self._databases.values()): + rgb = [0, 0, 0] + color = n % 3 + rgb[color] = 256 - n / 3 * factor + nn = color + # XXX: pretty sure this is horrible after so many aliases + while rgb[color] < factor: + nc = min(256 - rgb[color], 256) + rgb[color] += nc + nn += 1 + if nn > 2: + nn = 0 + rgb[nn] = nc + db['rgb_color'] = rgb + + trans_ids = {} + trans_id = None + i = 0 + for alias, query in self._queries: + trans_id = query.get('trans_id') + last_trans_id = trans_ids.get(alias) + + if trans_id != last_trans_id: + if last_trans_id: + self._queries[(i - 1)][1]['ends_trans'] = True + trans_ids[alias] = trans_id + if trans_id: + query['starts_trans'] = True + if trans_id: + query['in_trans'] = True + + query['alias'] = alias + if 'iso_level' in query: + query['iso_level'] = get_isolation_level_display(query['engine'], + query['iso_level']) + if 'trans_status' in query: + query['trans_status'] = get_transaction_status_display(query['engine'], + query['trans_status']) + + query['form'] = SQLSelectForm(auto_id=None, initial=copy(query)) + + if query['sql']: + query['sql'] = reformat_sql(query['sql']) + query['rgb_color'] = self._databases[alias]['rgb_color'] + try: + query['width_ratio'] = (query['duration'] / self._sql_time) * 100 + query['width_ratio_relative'] = ( + 100.0 * query['width_ratio'] / (100.0 - width_ratio_tally)) + except ZeroDivisionError: + query['width_ratio'] = 0 + query['width_ratio_relative'] = 0 + query['start_offset'] = width_ratio_tally + query['end_offset'] = query['width_ratio'] + query['start_offset'] + width_ratio_tally += query['width_ratio'] + query['stacktrace'] = render_stacktrace(query['stacktrace']) + i += 1 + + if trans_id: + self._queries[(i - 1)][1]['ends_trans'] = True + + self.record_stats({ + 'databases': sorted(self._databases.items(), key=lambda x: -x[1]['time_spent']), + 'queries': [q for a, q in self._queries], + 'sql_time': self._sql_time, + }) |
