From 6578f0b8ea3b26022bb5161a40a77c6b15de21ae Mon Sep 17 00:00:00 2001 From: Matthew J Morrison Date: Wed, 8 Dec 2010 08:42:57 -0600 Subject: Added clean_params method to DatabaseStatTracker to scrub non-unicode data for displaying on the sql panel Signed-off-by: Rob Hudson --- debug_toolbar/panels/sql.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) (limited to 'debug_toolbar/panels') diff --git a/debug_toolbar/panels/sql.py b/debug_toolbar/panels/sql.py index e1e9bdf..299da66 100644 --- a/debug_toolbar/panels/sql.py +++ b/debug_toolbar/panels/sql.py @@ -12,7 +12,7 @@ from django.views.debug import linebreak_iter from django.template import Node from django.template.loader import render_to_string from django.utils import simplejson -from django.utils.encoding import force_unicode +from django.utils.encoding import force_unicode, DjangoUnicodeDecodeError from django.utils.hashcompat import sha_constructor from django.utils.translation import ugettext_lazy as _ @@ -85,12 +85,24 @@ class DatabaseStatTracker(util.CursorDebugWrapper): Replacement for CursorDebugWrapper which stores additional information in `connection.queries`. """ + def clean_params(self, params): + clean_params = () + for x in params: + try: + force_unicode(x, strings_only=True) + except DjangoUnicodeDecodeError: + clean_params += ("", ) + else: + clean_params += (x, ) + return clean_params + def execute(self, sql, params=()): start = datetime.now() try: return self.cursor.execute(sql, params) finally: stop = datetime.now() + params = self.clean_params(params) duration = ms_from_timedelta(stop - start) stacktrace = tidy_stacktrace(traceback.extract_stack()) _params = '' -- cgit v1.2.3 From 1d90d8e19c8f7a06a8f3e61535f4f83c12cb58c6 Mon Sep 17 00:00:00 2001 From: Vincent Driessen Date: Sun, 9 Jan 2011 10:06:25 -0800 Subject: Added support for LogBook. Thanks to Vincent Driessen for the idea and patch. Signed-off-by: Rob Hudson --- debug_toolbar/panels/logger.py | 78 +++++++++++++++++++++++++++++++----------- 1 file changed, 58 insertions(+), 20 deletions(-) (limited to 'debug_toolbar/panels') diff --git a/debug_toolbar/panels/logger.py b/debug_toolbar/panels/logger.py index 620102e..0ddbfb1 100644 --- a/debug_toolbar/panels/logger.py +++ b/debug_toolbar/panels/logger.py @@ -8,16 +8,16 @@ from django.template.loader import render_to_string from django.utils.translation import ugettext_lazy as _ from debug_toolbar.panels import DebugPanel -class ThreadTrackingHandler(logging.Handler): + +class LogCollector(object): def __init__(self): if threading is None: raise NotImplementedError("threading module is not available, \ the logging panel cannot be used without it") - logging.Handler.__init__(self) self.records = {} # a dictionary that maps threads to log records - def emit(self, record): - self.get_records().append(record) + def add_record(self, record, thread=None): + self.get_records(thread).append(record) def get_records(self, thread=None): """ @@ -36,20 +36,67 @@ class ThreadTrackingHandler(logging.Handler): if thread in self.records: del self.records[thread] -handler = ThreadTrackingHandler() + +class ThreadTrackingHandler(logging.Handler): + def __init__(self, collector): + logging.Handler.__init__(self) + self.collector = collector + + def emit(self, record): + record = { + 'message': record.getMessage(), + 'time': datetime.datetime.fromtimestamp(record.created), + 'level': record.levelname, + 'file': record.pathname, + 'line': record.lineno, + 'channel': record.name, + } + self.collector.add_record(record) + + +collector = LogCollector() +logging_handler = ThreadTrackingHandler(collector) logging.root.setLevel(logging.NOTSET) -logging.root.addHandler(handler) +logging.root.addHandler(logging_handler) # register with logging + +try: + import logbook + logbook_supported = True +except ImportError: + # logbook support is optional, so fail silently + logbook_supported = False + +if logbook_supported: + class LogbookThreadTrackingHandler(logbook.handlers.Handler): + def __init__(self, collector): + logbook.handlers.Handler.__init__(self, bubble=True) + self.collector = collector + + def emit(self, record): + record = { + 'message': record.message, + 'time': record.time, + 'level': record.level_name, + 'file': record.filename, + 'line': record.lineno, + 'channel': record.channel, + } + self.collector.add_record(record) + + + logbook_handler = LogbookThreadTrackingHandler(collector) + logbook_handler.push_application() # register with logbook class LoggingPanel(DebugPanel): name = 'Logging' has_content = True def process_request(self, request): - handler.clear_records() + collector.clear_records() def get_and_delete(self): - records = handler.get_records() - handler.clear_records() + records = collector.get_records() + collector.clear_records() return records def nav_title(self): @@ -57,7 +104,7 @@ class LoggingPanel(DebugPanel): def nav_subtitle(self): # FIXME l10n: use ngettext - return "%s message%s" % (len(handler.get_records()), (len(handler.get_records()) == 1) and '' or 's') + return "%s message%s" % (len(collector.get_records()), (len(collector.get_records()) == 1) and '' or 's') def title(self): return _('Log Messages') @@ -66,16 +113,7 @@ class LoggingPanel(DebugPanel): return '' def content(self): - records = [] - for record in self.get_and_delete(): - records.append({ - 'message': record.getMessage(), - 'time': datetime.datetime.fromtimestamp(record.created), - 'level': record.levelname, - 'file': record.pathname, - 'line': record.lineno, - }) - + records = self.get_and_delete() context = self.context.copy() context.update({'records': records}) -- cgit v1.2.3 From a2edb76e8dc900ab5f147e062ceee5ac3ac35a18 Mon Sep 17 00:00:00 2001 From: postal2600 Date: Thu, 25 Nov 2010 01:44:46 -0800 Subject: Added support for executemany. Fixes issue #120 Signed-off-by: Rob Hudson --- debug_toolbar/panels/sql.py | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) (limited to 'debug_toolbar/panels') diff --git a/debug_toolbar/panels/sql.py b/debug_toolbar/panels/sql.py index 299da66..a3eff49 100644 --- a/debug_toolbar/panels/sql.py +++ b/debug_toolbar/panels/sql.py @@ -125,6 +125,49 @@ class DatabaseStatTracker(util.CursorDebugWrapper): pass del cur_frame + # We keep `sql` to maintain backwards compatibility + self.db.queries.append({ + 'sql': self.db.ops.last_executed_query(self.cursor, sql, params), + '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), + 'is_select': sql.lower().strip().startswith('select'), + 'template_info': template_info, + }) + + def executemany(self, sql, params=()): + start = datetime.now() + try: + return self.cursor.executemany(sql, params) + finally: + stop = datetime.now() + duration = ms_from_timedelta(stop - start) + stacktrace = tidy_stacktrace(traceback.extract_stack()) + _params = '' + try: + _params = simplejson.dumps([force_unicode(x, strings_only=True) for x in params]) + except TypeError: + pass # object not JSON serializable + + template_info = None + cur_frame = sys._getframe().f_back + try: + while cur_frame is not None: + if cur_frame.f_code.co_name == 'render': + node = cur_frame.f_locals['self'] + if isinstance(node, Node): + template_info = get_template_info(node.source) + break + cur_frame = cur_frame.f_back + except: + pass + del cur_frame + # We keep `sql` to maintain backwards compatibility self.db.queries.append({ 'sql': self.db.ops.last_executed_query(self.cursor, sql, params), -- cgit v1.2.3 From 926643b1881422b2712a952a3c82697cbcad8285 Mon Sep 17 00:00:00 2001 From: David Cramer Date: Wed, 23 Mar 2011 18:02:06 -0700 Subject: Overhaul SQL panel to include better timeline, more compact queries, and better view of stacktrace --- debug_toolbar/panels/sql.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) (limited to 'debug_toolbar/panels') diff --git a/debug_toolbar/panels/sql.py b/debug_toolbar/panels/sql.py index e1e9bdf..86ecbbf 100644 --- a/debug_toolbar/panels/sql.py +++ b/debug_toolbar/panels/sql.py @@ -1,5 +1,6 @@ from datetime import datetime import os +import re import sys import SocketServer import traceback @@ -10,10 +11,12 @@ from django.db import connection from django.db.backends import util from django.views.debug import linebreak_iter from django.template import Node +from django.template.defaultfilters import escape from django.template.loader import render_to_string from django.utils import simplejson from django.utils.encoding import force_unicode from django.utils.hashcompat import sha_constructor +from django.utils.safestring import mark_safe from django.utils.translation import ugettext_lazy as _ from debug_toolbar.panels import DebugPanel @@ -172,7 +175,14 @@ class SQLDebugPanel(DebugPanel): except ZeroDivisionError: query['width_ratio'] = 0 query['start_offset'] = width_ratio_tally + query['end_offset'] = query['width_ratio'] + query['start_offset'] width_ratio_tally += query['width_ratio'] + + stacktrace = [] + for frame in query['stacktrace']: + params = map(escape, frame[0].rsplit('/', 1) + list(frame[1:])) + stacktrace.append('{0}/{1} in {3}({2})\n {4}"'.format(*params)) + query['stacktrace'] = mark_safe('\n'.join(stacktrace)) context = self.context.copy() context.update({ @@ -201,8 +211,11 @@ class BoldKeywordFilter(sqlparse.filters.Filter): if is_keyword: yield sqlparse.tokens.Text, '' +def swap_fields(sql): + return re.sub('SELECT (.*) FROM', 'SELECT \g<1> FROM', sql) + def reformat_sql(sql): stack = sqlparse.engine.FilterStack() stack.preprocess.append(BoldKeywordFilter()) # add our custom filter stack.postprocess.append(sqlparse.filters.SerializerUnicode()) # tokens -> strings - return ''.join(stack.run(sql)) + return swap_fields(''.join(stack.run(sql))) -- cgit v1.2.3 From d29f055713636c5629ddb7f48805536d39e550cb Mon Sep 17 00:00:00 2001 From: David Cramer Date: Thu, 24 Mar 2011 16:13:15 -0700 Subject: Add multi db support and summaries to SQL panel --- debug_toolbar/panels/sql.py | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) (limited to 'debug_toolbar/panels') diff --git a/debug_toolbar/panels/sql.py b/debug_toolbar/panels/sql.py index 86ecbbf..8fd07d3 100644 --- a/debug_toolbar/panels/sql.py +++ b/debug_toolbar/panels/sql.py @@ -7,7 +7,12 @@ import traceback import django from django.conf import settings -from django.db import connection +try: + from django.db import connections +except ImportError: + # Compat with < Django 1.2 + from django.db import connection + connections = {'default': connection} from django.db.backends import util from django.views.debug import linebreak_iter from django.template import Node @@ -119,6 +124,7 @@ 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': duration * 1000, 'duration': duration, 'raw_sql': sql, 'params': _params, @@ -142,16 +148,27 @@ class SQLDebugPanel(DebugPanel): def __init__(self, *args, **kwargs): super(self.__class__, self).__init__(*args, **kwargs) - self._offset = len(connection.queries) + self._offset = dict((k, len(connections[k].queries)) for k in connections) self._sql_time = 0 self._queries = [] + self._databases = {} def nav_title(self): return _('SQL') def nav_subtitle(self): - self._queries = connection.queries[self._offset:] - self._sql_time = sum([q['duration'] for q in self._queries]) + self._queries = [] + self._databases = {} + for alias in connections: + db_queries = connections[alias].queries[self._offset[alias]:] + self._databases[alias] = { + 'time_spent': sum(q['duration'] for q in db_queries), + 'queries': len(db_queries), + } + self._queries.extend([(alias, q) for q in db_queries]) + + self._queries.sort(key=lambda x: x[1]['start_time']) + self._sql_time = sum([d['time_spent'] for d in self._databases.itervalues()]) num_queries = len(self._queries) # TODO l10n: use ngettext return "%d %s in %.2fms" % ( @@ -168,7 +185,8 @@ class SQLDebugPanel(DebugPanel): def content(self): width_ratio_tally = 0 - for query in self._queries: + for alias, query in self._queries: + query['alias'] = alias query['sql'] = reformat_sql(query['sql']) try: query['width_ratio'] = (query['duration'] / self._sql_time) * 100 @@ -186,7 +204,8 @@ class SQLDebugPanel(DebugPanel): context = self.context.copy() context.update({ - 'queries': self._queries, + '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, 'is_mysql': settings.DATABASE_ENGINE == 'mysql', }) -- cgit v1.2.3 From 4bb644ad825ed9009176c7e78967b6ba07d7a681 Mon Sep 17 00:00:00 2001 From: David Cramer Date: Thu, 24 Mar 2011 16:50:09 -0700 Subject: Only show connections which executed queries. Show number of used connections in title --- debug_toolbar/panels/sql.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) (limited to 'debug_toolbar/panels') diff --git a/debug_toolbar/panels/sql.py b/debug_toolbar/panels/sql.py index 8fd07d3..15c6ff3 100644 --- a/debug_toolbar/panels/sql.py +++ b/debug_toolbar/panels/sql.py @@ -22,7 +22,7 @@ from django.utils import simplejson from django.utils.encoding import force_unicode from django.utils.hashcompat import sha_constructor from django.utils.safestring import mark_safe -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import ugettext_lazy as _, ungettext_lazy as __ from debug_toolbar.panels import DebugPanel from debug_toolbar.utils import sqlparse @@ -161,11 +161,13 @@ class SQLDebugPanel(DebugPanel): self._databases = {} for alias in connections: db_queries = connections[alias].queries[self._offset[alias]:] - self._databases[alias] = { - 'time_spent': sum(q['duration'] for q in db_queries), - 'queries': len(db_queries), - } - self._queries.extend([(alias, q) for q in db_queries]) + num_queries = len(db_queries) + if num_queries: + self._databases[alias] = { + 'time_spent': sum(q['duration'] for q in db_queries), + 'queries': num_queries, + } + self._queries.extend([(alias, q) for q in db_queries]) self._queries.sort(key=lambda x: x[1]['start_time']) self._sql_time = sum([d['time_spent'] for d in self._databases.itervalues()]) @@ -178,7 +180,11 @@ class SQLDebugPanel(DebugPanel): ) def title(self): - return _('SQL Queries') + count = len(self._databases) + + return __('SQL Queries from %(count)d connection', 'SQL Queries from %(count)d connections', count) % dict( + count=count, + ) def url(self): return '' -- cgit v1.2.3 From 79ddcefb629612da3fb05ccbb8b602d1f026b1f9 Mon Sep 17 00:00:00 2001 From: David Cramer Date: Thu, 24 Mar 2011 17:34:55 -0700 Subject: basic color code grouping of db aliases --- debug_toolbar/panels/sql.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) (limited to 'debug_toolbar/panels') diff --git a/debug_toolbar/panels/sql.py b/debug_toolbar/panels/sql.py index 15c6ff3..19a4767 100644 --- a/debug_toolbar/panels/sql.py +++ b/debug_toolbar/panels/sql.py @@ -1,4 +1,5 @@ from datetime import datetime +import itertools import os import re import sys @@ -191,9 +192,21 @@ class SQLDebugPanel(DebugPanel): def content(self): width_ratio_tally = 0 + colors = [ + (256, 0, 0), # red + (0, 256, 0), # blue + (0, 0, 256), # green + ] + for n, db in enumerate(self._databases.itervalues()): + rgb = [0, 0, 0] + color = n % 3 + rgb[color] = 256 - n/3*32 + db['rgb_color'] = rgb + for alias, query in self._queries: query['alias'] = alias query['sql'] = reformat_sql(query['sql']) + query['rgb_color'] = self._databases[alias]['rgb_color'] try: query['width_ratio'] = (query['duration'] / self._sql_time) * 100 except ZeroDivisionError: @@ -207,7 +220,7 @@ class SQLDebugPanel(DebugPanel): params = map(escape, frame[0].rsplit('/', 1) + list(frame[1:])) stacktrace.append('{0}/{1} in {3}({2})\n {4}"'.format(*params)) query['stacktrace'] = mark_safe('\n'.join(stacktrace)) - + context = self.context.copy() context.update({ 'databases': sorted(self._databases.items(), key=lambda x: -x[1]['time_spent']), -- cgit v1.2.3 From 1212fec3a0df762c2f63a34925e304c49fa7354d Mon Sep 17 00:00:00 2001 From: David Cramer Date: Thu, 24 Mar 2011 17:45:58 -0700 Subject: Much better coloring scale --- debug_toolbar/panels/sql.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) (limited to 'debug_toolbar/panels') diff --git a/debug_toolbar/panels/sql.py b/debug_toolbar/panels/sql.py index 19a4767..dc096c0 100644 --- a/debug_toolbar/panels/sql.py +++ b/debug_toolbar/panels/sql.py @@ -197,10 +197,20 @@ class SQLDebugPanel(DebugPanel): (0, 256, 0), # blue (0, 0, 256), # green ] + factor = int(256.0/len(self._databases)*2.5) for n, db in enumerate(self._databases.itervalues()): rgb = [0, 0, 0] color = n % 3 - rgb[color] = 256 - n/3*32 + 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 for alias, query in self._queries: -- cgit v1.2.3 From 85e9976164b3d7746385664506ce6c5409e7e9d8 Mon Sep 17 00:00:00 2001 From: David Cramer Date: Thu, 24 Mar 2011 17:48:59 -0700 Subject: Fix for infinite math fail :) --- debug_toolbar/panels/sql.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'debug_toolbar/panels') diff --git a/debug_toolbar/panels/sql.py b/debug_toolbar/panels/sql.py index dc096c0..258fab8 100644 --- a/debug_toolbar/panels/sql.py +++ b/debug_toolbar/panels/sql.py @@ -197,7 +197,8 @@ class SQLDebugPanel(DebugPanel): (0, 256, 0), # blue (0, 0, 256), # green ] - factor = int(256.0/len(self._databases)*2.5) + factor = int(256.0/(len(self._databases)*2.5)) + print factor for n, db in enumerate(self._databases.itervalues()): rgb = [0, 0, 0] color = n % 3 @@ -205,6 +206,7 @@ class SQLDebugPanel(DebugPanel): nn = color # XXX: pretty sure this is horrible after so many aliases while rgb[color] < factor: + print rgb[color], factor nc = min(256 - rgb[color], 256) rgb[color] += nc nn += 1 -- cgit v1.2.3 From 7a07b70db07b91be652e55eebee7b04dac01ee64 Mon Sep 17 00:00:00 2001 From: David Cramer Date: Thu, 24 Mar 2011 17:49:20 -0700 Subject: Remove debug print --- debug_toolbar/panels/sql.py | 1 - 1 file changed, 1 deletion(-) (limited to 'debug_toolbar/panels') diff --git a/debug_toolbar/panels/sql.py b/debug_toolbar/panels/sql.py index 258fab8..5769df6 100644 --- a/debug_toolbar/panels/sql.py +++ b/debug_toolbar/panels/sql.py @@ -198,7 +198,6 @@ class SQLDebugPanel(DebugPanel): (0, 0, 256), # green ] factor = int(256.0/(len(self._databases)*2.5)) - print factor for n, db in enumerate(self._databases.itervalues()): rgb = [0, 0, 0] color = n % 3 -- cgit v1.2.3 From 5aff6ee75f8af3dd46254953b0b0de7c8e19c8e2 Mon Sep 17 00:00:00 2001 From: David Cramer Date: Mon, 28 Mar 2011 17:25:21 -0700 Subject: SQL injection now happens without interfering with the underlying db.queries objects (and no longer requires DEBUG to be set) --- debug_toolbar/panels/sql.py | 61 ++++++++++++++++++++++++++------------------- 1 file changed, 36 insertions(+), 25 deletions(-) (limited to 'debug_toolbar/panels') diff --git a/debug_toolbar/panels/sql.py b/debug_toolbar/panels/sql.py index 5769df6..5efafbb 100644 --- a/debug_toolbar/panels/sql.py +++ b/debug_toolbar/panels/sql.py @@ -1,5 +1,4 @@ from datetime import datetime -import itertools import os import re import sys @@ -25,6 +24,7 @@ from django.utils.hashcompat import sha_constructor from django.utils.safestring import mark_safe from django.utils.translation import ugettext_lazy as _, ungettext_lazy as __ +from debug_toolbar.middleware import DebugToolbarMiddleware from debug_toolbar.panels import DebugPanel from debug_toolbar.utils import sqlparse @@ -89,15 +89,21 @@ def get_template_info(source, context_lines=3): 'context': context, } -class DatabaseStatTracker(util.CursorDebugWrapper): +def inject_sql_tracker(cls): """ - Replacement for CursorDebugWrapper which stores additional information - in `connection.queries`. + Injects a replacement execute method which records queries within the SQLPanel. """ + if getattr(cls.execute, 'is_tracked', False): + return def execute(self, sql, params=()): + djdt = DebugToolbarMiddleware.get_current() + if not djdt: + return cls.execute.__wraps(self, sql, params) + + panel = djdt.get_panel(SQLDebugPanel) start = datetime.now() try: - return self.cursor.execute(sql, params) + return cls.execute.__wraps(self, sql, params) finally: stop = datetime.now() duration = ms_from_timedelta(stop - start) @@ -123,9 +129,9 @@ class DatabaseStatTracker(util.CursorDebugWrapper): del cur_frame # We keep `sql` to maintain backwards compatibility - self.db.queries.append({ + panel.record(**{ + 'alias': getattr(self, 'alias', 'default'), 'sql': self.db.ops.last_executed_query(self.cursor, sql, params), - 'time': duration * 1000, 'duration': duration, 'raw_sql': sql, 'params': _params, @@ -137,7 +143,13 @@ class DatabaseStatTracker(util.CursorDebugWrapper): 'is_select': sql.lower().strip().startswith('select'), 'template_info': template_info, }) -util.CursorDebugWrapper = DatabaseStatTracker + execute.is_tracked = True + execute.__wraps = cls.execute + + cls.execute = execute + +inject_sql_tracker(util.CursorWrapper) +inject_sql_tracker(util.CursorDebugWrapper) class SQLDebugPanel(DebugPanel): """ @@ -151,32 +163,31 @@ class SQLDebugPanel(DebugPanel): super(self.__class__, 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 = {} + + 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 def nav_title(self): return _('SQL') def nav_subtitle(self): - self._queries = [] - self._databases = {} - for alias in connections: - db_queries = connections[alias].queries[self._offset[alias]:] - num_queries = len(db_queries) - if num_queries: - self._databases[alias] = { - 'time_spent': sum(q['duration'] for q in db_queries), - 'queries': num_queries, - } - self._queries.extend([(alias, q) for q in db_queries]) - - self._queries.sort(key=lambda x: x[1]['start_time']) - self._sql_time = sum([d['time_spent'] for d in self._databases.itervalues()]) - num_queries = len(self._queries) # TODO l10n: use ngettext return "%d %s in %.2fms" % ( - num_queries, - (num_queries == 1) and 'query' or 'queries', + self._num_queries, + (self._num_queries == 1) and 'query' or 'queries', self._sql_time ) -- cgit v1.2.3 From 4c7c43300ed0bfee6243cb5b59887837f2becb13 Mon Sep 17 00:00:00 2001 From: David Cramer Date: Mon, 28 Mar 2011 17:30:19 -0700 Subject: Fail gracefully when theres no queries on a page --- debug_toolbar/panels/sql.py | 77 +++++++++++++++++++++++---------------------- 1 file changed, 39 insertions(+), 38 deletions(-) (limited to 'debug_toolbar/panels') diff --git a/debug_toolbar/panels/sql.py b/debug_toolbar/panels/sql.py index 5efafbb..e9e2569 100644 --- a/debug_toolbar/panels/sql.py +++ b/debug_toolbar/panels/sql.py @@ -202,46 +202,47 @@ class SQLDebugPanel(DebugPanel): return '' def content(self): - width_ratio_tally = 0 - colors = [ - (256, 0, 0), # red - (0, 256, 0), # blue - (0, 0, 256), # green - ] - factor = int(256.0/(len(self._databases)*2.5)) - for n, db in enumerate(self._databases.itervalues()): - 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: - print 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 + if self._queries: + width_ratio_tally = 0 + colors = [ + (256, 0, 0), # red + (0, 256, 0), # blue + (0, 0, 256), # green + ] + factor = int(256.0/(len(self._databases)*2.5)) + for n, db in enumerate(self._databases.itervalues()): + 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: + print 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 - for alias, query in self._queries: - query['alias'] = alias - query['sql'] = reformat_sql(query['sql']) - query['rgb_color'] = self._databases[alias]['rgb_color'] - try: - query['width_ratio'] = (query['duration'] / self._sql_time) * 100 - except ZeroDivisionError: - query['width_ratio'] = 0 - query['start_offset'] = width_ratio_tally - query['end_offset'] = query['width_ratio'] + query['start_offset'] - width_ratio_tally += query['width_ratio'] + for alias, query in self._queries: + query['alias'] = alias + query['sql'] = reformat_sql(query['sql']) + query['rgb_color'] = self._databases[alias]['rgb_color'] + try: + query['width_ratio'] = (query['duration'] / self._sql_time) * 100 + except ZeroDivisionError: + query['width_ratio'] = 0 + query['start_offset'] = width_ratio_tally + query['end_offset'] = query['width_ratio'] + query['start_offset'] + width_ratio_tally += query['width_ratio'] - stacktrace = [] - for frame in query['stacktrace']: - params = map(escape, frame[0].rsplit('/', 1) + list(frame[1:])) - stacktrace.append('{0}/{1} in {3}({2})\n {4}"'.format(*params)) - query['stacktrace'] = mark_safe('\n'.join(stacktrace)) + stacktrace = [] + for frame in query['stacktrace']: + params = map(escape, frame[0].rsplit('/', 1) + list(frame[1:])) + stacktrace.append('{0}/{1} in {3}({2})\n {4}"'.format(*params)) + query['stacktrace'] = mark_safe('\n'.join(stacktrace)) context = self.context.copy() context.update({ -- cgit v1.2.3 From da85fc0a8b5e1855b6b6219076a34cd0df88bcc3 Mon Sep 17 00:00:00 2001 From: David Cramer Date: Mon, 28 Mar 2011 17:32:55 -0700 Subject: Store actual function as __wrapped__ instead of __wraps for Python 3.2 compatibility --- debug_toolbar/panels/sql.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'debug_toolbar/panels') diff --git a/debug_toolbar/panels/sql.py b/debug_toolbar/panels/sql.py index e9e2569..b3b44da 100644 --- a/debug_toolbar/panels/sql.py +++ b/debug_toolbar/panels/sql.py @@ -98,12 +98,12 @@ def inject_sql_tracker(cls): def execute(self, sql, params=()): djdt = DebugToolbarMiddleware.get_current() if not djdt: - return cls.execute.__wraps(self, sql, params) + return cls.execute.__wrapped__(self, sql, params) panel = djdt.get_panel(SQLDebugPanel) start = datetime.now() try: - return cls.execute.__wraps(self, sql, params) + return cls.execute.__wrapped__(self, sql, params) finally: stop = datetime.now() duration = ms_from_timedelta(stop - start) @@ -144,7 +144,7 @@ def inject_sql_tracker(cls): 'template_info': template_info, }) execute.is_tracked = True - execute.__wraps = cls.execute + execute.__wrapped__ = cls.execute cls.execute = execute -- cgit v1.2.3 From 48c8d0dd09c352532326b83a39800be047686356 Mon Sep 17 00:00:00 2001 From: David Cramer Date: Wed, 30 Mar 2011 15:31:27 -0700 Subject: Ensure we wrap all cursors --- debug_toolbar/panels/sql.py | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) (limited to 'debug_toolbar/panels') diff --git a/debug_toolbar/panels/sql.py b/debug_toolbar/panels/sql.py index b3b44da..ba40c34 100644 --- a/debug_toolbar/panels/sql.py +++ b/debug_toolbar/panels/sql.py @@ -13,7 +13,7 @@ except ImportError: # Compat with < Django 1.2 from django.db import connection connections = {'default': connection} -from django.db.backends import util +from django.db.backends import util, BaseDatabaseWrapper from django.views.debug import linebreak_iter from django.template import Node from django.template.defaultfilters import escape @@ -148,6 +148,39 @@ def inject_sql_tracker(cls): cls.execute = execute +class CursorWrapper(object): + def __init__(self, cursor, db): + self.cursor = cursor + self.db = db # Instance of a BaseDatabaseWrapper subclass + + def execute(self, sql, params=None): + return self.cursor.execute(sql, params) + + def executemany(self, sql, param_list): + return self.cursor.executemany(sql, param_list) + + def __getattr__(self, attr): + if attr in self.__dict__: + return self.__dict__[attr] + else: + return getattr(self.cursor, attr) + + def __iter__(self): + return iter(self.cursor) + +if not hasattr(util, 'CursorWrapper'): + # Inject our CursorWrapper class + util.CursorWrapper = CursorWrapper + +def cursor(self): + from django.conf import settings + cursor = self._cursor() + if settings.DEBUG: + return self.make_debug_cursor(cursor) + return util.CursorWrapper(cursor, self) + +BaseDatabaseWrapper.cursor = cursor + inject_sql_tracker(util.CursorWrapper) inject_sql_tracker(util.CursorDebugWrapper) -- cgit v1.2.3 From 65969f7777ce0bd6bba53540d960a93c27b346ce Mon Sep 17 00:00:00 2001 From: David Cramer Date: Wed, 30 Mar 2011 15:57:51 -0700 Subject: Inject our SQL tracker on BaseDatabaseWrapper.cursor rather than discovering different CursorWrapper's --- debug_toolbar/panels/sql.py | 63 ++++++++++++++++++++++----------------------- 1 file changed, 31 insertions(+), 32 deletions(-) (limited to 'debug_toolbar/panels') diff --git a/debug_toolbar/panels/sql.py b/debug_toolbar/panels/sql.py index ba40c34..a78a3e2 100644 --- a/debug_toolbar/panels/sql.py +++ b/debug_toolbar/panels/sql.py @@ -13,7 +13,7 @@ except ImportError: # Compat with < Django 1.2 from django.db import connection connections = {'default': connection} -from django.db.backends import util, BaseDatabaseWrapper +from django.db.backends import BaseDatabaseWrapper from django.views.debug import linebreak_iter from django.template import Node from django.template.defaultfilters import escape @@ -89,21 +89,25 @@ def get_template_info(source, context_lines=3): 'context': context, } -def inject_sql_tracker(cls): + +class CursorWrapper(object): """ - Injects a replacement execute method which records queries within the SQLPanel. + Wraps a cursor and logs queries. """ - if getattr(cls.execute, 'is_tracked', False): - return + + def __init__(self, cursor, db): + self.cursor = cursor + self.db = db # Instance of a BaseDatabaseWrapper subclass + def execute(self, sql, params=()): djdt = DebugToolbarMiddleware.get_current() if not djdt: - return cls.execute.__wrapped__(self, sql, params) + return self.cursor.execute(self, sql, params) panel = djdt.get_panel(SQLDebugPanel) start = datetime.now() try: - return cls.execute.__wrapped__(self, sql, params) + return self.cursor.execute(self, sql, params) finally: stop = datetime.now() duration = ms_from_timedelta(stop - start) @@ -143,18 +147,6 @@ def inject_sql_tracker(cls): 'is_select': sql.lower().strip().startswith('select'), 'template_info': template_info, }) - execute.is_tracked = True - execute.__wrapped__ = cls.execute - - cls.execute = execute - -class CursorWrapper(object): - def __init__(self, cursor, db): - self.cursor = cursor - self.db = db # Instance of a BaseDatabaseWrapper subclass - - def execute(self, sql, params=None): - return self.cursor.execute(sql, params) def executemany(self, sql, param_list): return self.cursor.executemany(sql, param_list) @@ -168,21 +160,29 @@ class CursorWrapper(object): def __iter__(self): return iter(self.cursor) -if not hasattr(util, 'CursorWrapper'): - # Inject our CursorWrapper class - util.CursorWrapper = CursorWrapper +def inject_sql_tracker(cls): + """ + Injects a replacement execute method which records queries within the SQLPanel. + """ + import warnings + + if not hasattr(cls, 'cursor'): + warnings.warn('Unable to patch %r: missing cursor method' % cls) + + if getattr(cls.cursor, 'djdt_tracked', False): + return + + def cursor(self): + result = cls.cursor.__wrapped__(self) + return CursorWrapper(result, self) -def cursor(self): - from django.conf import settings - cursor = self._cursor() - if settings.DEBUG: - return self.make_debug_cursor(cursor) - return util.CursorWrapper(cursor, self) + cursor.djdt_tracked = True + cursor.__wrapped__ = cls.cursor -BaseDatabaseWrapper.cursor = cursor + cls.cursor = cursor -inject_sql_tracker(util.CursorWrapper) -inject_sql_tracker(util.CursorDebugWrapper) +# Inject our tracking code into the existing CursorWrapper's +inject_sql_tracker(BaseDatabaseWrapper) class SQLDebugPanel(DebugPanel): """ @@ -250,7 +250,6 @@ class SQLDebugPanel(DebugPanel): nn = color # XXX: pretty sure this is horrible after so many aliases while rgb[color] < factor: - print rgb[color], factor nc = min(256 - rgb[color], 256) rgb[color] += nc nn += 1 -- cgit v1.2.3 From f492b56c8200eebb77b8023ab386c9ef412cc06b Mon Sep 17 00:00:00 2001 From: David Cramer Date: Wed, 30 Mar 2011 16:33:34 -0700 Subject: Some initial tests and fix for execution model --- debug_toolbar/panels/sql.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'debug_toolbar/panels') diff --git a/debug_toolbar/panels/sql.py b/debug_toolbar/panels/sql.py index a78a3e2..4c7b8a1 100644 --- a/debug_toolbar/panels/sql.py +++ b/debug_toolbar/panels/sql.py @@ -102,12 +102,12 @@ class CursorWrapper(object): def execute(self, sql, params=()): djdt = DebugToolbarMiddleware.get_current() if not djdt: - return self.cursor.execute(self, sql, params) + return self.cursor.execute(sql, params) panel = djdt.get_panel(SQLDebugPanel) start = datetime.now() try: - return self.cursor.execute(self, sql, params) + return self.cursor.execute(sql, params) finally: stop = datetime.now() duration = ms_from_timedelta(stop - start) -- cgit v1.2.3 From 3f578cf684b7e3b9a20d9c777950c28c44db074a Mon Sep 17 00:00:00 2001 From: David Cramer Date: Thu, 31 Mar 2011 11:42:23 -0700 Subject: Add utilities to inject and monitor functions. Change DB tracking to use new injection method on BaseDatabaseWrapper.cursor --- debug_toolbar/panels/sql.py | 196 ++++---------------------------------------- 1 file changed, 16 insertions(+), 180 deletions(-) (limited to 'debug_toolbar/panels') diff --git a/debug_toolbar/panels/sql.py b/debug_toolbar/panels/sql.py index 4c7b8a1..ba83455 100644 --- a/debug_toolbar/panels/sql.py +++ b/debug_toolbar/panels/sql.py @@ -1,188 +1,30 @@ -from datetime import datetime -import os import re -import sys -import SocketServer -import traceback -import django from django.conf import settings -try: - from django.db import connections -except ImportError: - # Compat with < Django 1.2 - from django.db import connection - connections = {'default': connection} from django.db.backends import BaseDatabaseWrapper -from django.views.debug import linebreak_iter -from django.template import Node -from django.template.defaultfilters import escape from django.template.loader import render_to_string -from django.utils import simplejson -from django.utils.encoding import force_unicode -from django.utils.hashcompat import sha_constructor +from django.utils.html import escape from django.utils.safestring import mark_safe from django.utils.translation import ugettext_lazy as _, ungettext_lazy as __ +from debug_toolbar.utils.compat.db import connections from debug_toolbar.middleware import DebugToolbarMiddleware from debug_toolbar.panels import DebugPanel from debug_toolbar.utils import sqlparse - -# Figure out some paths -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) - -def tidy_stacktrace(strace): - """ - Clean up stacktrace and remove all entries that: - 1. Are part of Django (except contrib apps) - 2. Are part of SocketServer (used by Django's dev server) - 3. Are the last entry (which is part of our stacktracing code) - """ - trace = [] - for s in strace[:-1]: - s_path = os.path.realpath(s[0]) - if getattr(settings, 'DEBUG_TOOLBAR_CONFIG', {}).get('HIDE_DJANGO_SQL', True) \ - and django_path in s_path and not 'django/contrib' in s_path: - continue - if socketserver_path in s_path: - continue - trace.append((s[0], s[1], s[2], s[3])) - return trace - -def get_template_info(source, context_lines=3): - line = 0 - upto = 0 - source_lines = [] - before = during = after = "" - - origin, (start, end) = source - template_source = origin.reload() - - for num, next in enumerate(linebreak_iter(template_source)): - if start >= upto and end <= next: - line = num - before = template_source[upto:start] - during = template_source[start:end] - after = template_source[end:next] - source_lines.append((num, template_source[upto:next])) - upto = next - - top = max(1, line - context_lines) - bottom = min(len(source_lines), line + 1 + context_lines) - - context = [] - for num, content in source_lines[top:bottom]: - context.append({ - 'num': num, - 'content': content, - 'highlight': (num == line), - }) - - return { - 'name': origin.name, - 'context': context, - } - - -class CursorWrapper(object): - """ - Wraps a cursor and logs queries. - """ - - def __init__(self, cursor, db): - self.cursor = cursor - self.db = db # Instance of a BaseDatabaseWrapper subclass - - def execute(self, sql, params=()): - djdt = DebugToolbarMiddleware.get_current() - if not djdt: - return self.cursor.execute(sql, params) - - panel = djdt.get_panel(SQLDebugPanel) - start = datetime.now() - try: - return self.cursor.execute(sql, params) - finally: - stop = datetime.now() - duration = ms_from_timedelta(stop - start) - stacktrace = tidy_stacktrace(traceback.extract_stack()) - _params = '' - try: - _params = simplejson.dumps([force_unicode(x, strings_only=True) for x in params]) - except TypeError: - pass # object not JSON serializable - - template_info = None - cur_frame = sys._getframe().f_back - try: - while cur_frame is not None: - if cur_frame.f_code.co_name == 'render': - node = cur_frame.f_locals['self'] - if isinstance(node, Node): - template_info = get_template_info(node.source) - break - cur_frame = cur_frame.f_back - except: - pass - del cur_frame - - # We keep `sql` to maintain backwards compatibility - panel.record(**{ - 'alias': getattr(self, 'alias', 'default'), - 'sql': self.db.ops.last_executed_query(self.cursor, sql, params), - '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), - 'is_select': sql.lower().strip().startswith('select'), - 'template_info': template_info, - }) - - def executemany(self, sql, param_list): - return self.cursor.executemany(sql, param_list) - - def __getattr__(self, attr): - if attr in self.__dict__: - return self.__dict__[attr] - else: - return getattr(self.cursor, attr) - - def __iter__(self): - return iter(self.cursor) - -def inject_sql_tracker(cls): - """ - Injects a replacement execute method which records queries within the SQLPanel. - """ - import warnings +from debug_toolbar.utils.tracking.db import CursorWrapper +from debug_toolbar.utils.tracking import replace_call + +# Inject our tracking cursor +@replace_call(BaseDatabaseWrapper.cursor) +def cursor(func, self): + result = func(self) + + djdt = DebugToolbarMiddleware.get_current() + if not djdt: + return result + logger = djdt.get_panel(SQLDebugPanel) - if not hasattr(cls, 'cursor'): - warnings.warn('Unable to patch %r: missing cursor method' % cls) - - if getattr(cls.cursor, 'djdt_tracked', False): - return - - def cursor(self): - result = cls.cursor.__wrapped__(self) - return CursorWrapper(result, self) - - cursor.djdt_tracked = True - cursor.__wrapped__ = cls.cursor - - cls.cursor = cursor - -# Inject our tracking code into the existing CursorWrapper's -inject_sql_tracker(BaseDatabaseWrapper) + return CursorWrapper(result, self, logger=logger) class SQLDebugPanel(DebugPanel): """ @@ -286,12 +128,6 @@ 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) - class BoldKeywordFilter(sqlparse.filters.Filter): """sqlparse filter to bold SQL keywords""" def process(self, stack, stream): @@ -300,7 +136,7 @@ class BoldKeywordFilter(sqlparse.filters.Filter): is_keyword = token_type in sqlparse.tokens.Keyword if is_keyword: yield sqlparse.tokens.Text, '' - yield token_type, django.utils.html.escape(value) + yield token_type, escape(value) if is_keyword: yield sqlparse.tokens.Text, '' -- cgit v1.2.3 From 72271115974463f30a2f9f8755e42acc69e204c3 Mon Sep 17 00:00:00 2001 From: David Cramer Date: Tue, 5 Apr 2011 14:02:47 -0700 Subject: Add support for engine-per-query as well as pulling out psycopg2's isolation level and transaction status --- debug_toolbar/panels/sql.py | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) (limited to 'debug_toolbar/panels') diff --git a/debug_toolbar/panels/sql.py b/debug_toolbar/panels/sql.py index ba83455..344f5d6 100644 --- a/debug_toolbar/panels/sql.py +++ b/debug_toolbar/panels/sql.py @@ -1,6 +1,5 @@ import re -from django.conf import settings from django.db.backends import BaseDatabaseWrapper from django.template.loader import render_to_string from django.utils.html import escape @@ -26,6 +25,36 @@ def cursor(func, self): return CursorWrapper(result, self, logger=logger) +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 @@ -102,6 +131,10 @@ class SQLDebugPanel(DebugPanel): for alias, query in self._queries: 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['sql'] = reformat_sql(query['sql']) query['rgb_color'] = self._databases[alias]['rgb_color'] try: @@ -123,7 +156,6 @@ class SQLDebugPanel(DebugPanel): '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, - 'is_mysql': settings.DATABASE_ENGINE == 'mysql', }) return render_to_string('debug_toolbar/panels/sql.html', context) -- cgit v1.2.3 From 634bd0d68cb30d25705a049d46aa72e9a64e9dbd Mon Sep 17 00:00:00 2001 From: Rob Hudson Date: Wed, 6 Apr 2011 07:52:45 -0700 Subject: Added basic support for multi-db. Thanks to David Cramer, Ionel Cristian Mărieș, noah256, and Sander Steffann, who all supplied patches. --- debug_toolbar/panels/sql.py | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) (limited to 'debug_toolbar/panels') diff --git a/debug_toolbar/panels/sql.py b/debug_toolbar/panels/sql.py index a3eff49..90a7b51 100644 --- a/debug_toolbar/panels/sql.py +++ b/debug_toolbar/panels/sql.py @@ -6,7 +6,14 @@ import traceback import django from django.conf import settings -from django.db import connection +try: + from django.db import connections +except ImportError: + # Compatibility with Django < 1.2 + from django.db import connection + connections = {'default': connection} + connection.alias = 'default' + from django.db.backends import util from django.views.debug import linebreak_iter from django.template import Node @@ -194,16 +201,27 @@ class SQLDebugPanel(DebugPanel): def __init__(self, *args, **kwargs): super(self.__class__, self).__init__(*args, **kwargs) - self._offset = len(connection.queries) + self._offset = dict((conn, len(connections[conn].queries)) for conn in connections) self._sql_time = 0 self._queries = [] + self._databases = {} def nav_title(self): return _('SQL') def nav_subtitle(self): - self._queries = connection.queries[self._offset:] - self._sql_time = sum([q['duration'] for q in self._queries]) + self._queries = [] + self._databases = {} + for alias in connections: + db_queries = connections[alias].queries[self._offset[alias]:] + self._databases[alias] = { + 'time_spent': sum(q['duration'] for q in db_queries), + 'queries': len(db_queries), + } + self._queries.extend([(alias, q) for q in db_queries]) + + self._queries.sort(key=lambda x: x[1]['start_time']) + self._sql_time = sum([d['time_spent'] for d in self._databases.itervalues()]) num_queries = len(self._queries) # TODO l10n: use ngettext return "%d %s in %.2fms" % ( @@ -220,7 +238,8 @@ class SQLDebugPanel(DebugPanel): def content(self): width_ratio_tally = 0 - for query in self._queries: + for alias, query in self._queries: + query['alias'] = alias query['sql'] = reformat_sql(query['sql']) try: query['width_ratio'] = (query['duration'] / self._sql_time) * 100 @@ -231,7 +250,8 @@ class SQLDebugPanel(DebugPanel): context = self.context.copy() context.update({ - 'queries': self._queries, + '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, 'is_mysql': settings.DATABASE_ENGINE == 'mysql', }) -- cgit v1.2.3 From a9b466b6672c72f498e3f011524d48726937d1d5 Mon Sep 17 00:00:00 2001 From: David Cramer Date: Wed, 6 Apr 2011 16:07:46 -0700 Subject: SQL panel now guesses at Psycopg2 transactions (when autocommit swaps to in trans and vice versa) --- debug_toolbar/panels/sql.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) (limited to 'debug_toolbar/panels') diff --git a/debug_toolbar/panels/sql.py b/debug_toolbar/panels/sql.py index 344f5d6..9f4e83c 100644 --- a/debug_toolbar/panels/sql.py +++ b/debug_toolbar/panels/sql.py @@ -70,6 +70,23 @@ class SQLDebugPanel(DebugPanel): self._num_queries = 0 self._queries = [] self._databases = {} + self._transaction_status = {} + + def get_transaction_status(self, alias, reset=False): + conn = connections[alias].connection + if not conn: + return None + + engine = conn.__class__.__module__.split('.', 1)[0] + + if reset or self._transaction_status.get(alias) is None: + if engine == 'psycopg2': + self._transaction_status[alias] = conn.get_transaction_status() + else: + raise ValueError(engine) + + return self._transaction_status[alias] + def record(self, alias, **kwargs): self._queries.append((alias, kwargs)) -- cgit v1.2.3 From 0556fd7b48c709b5ff803eff01e0dd7eb07ba3fc Mon Sep 17 00:00:00 2001 From: David Cramer Date: Wed, 6 Apr 2011 16:29:33 -0700 Subject: Generate fake transaction ids so we can match up transaction end state correctly --- debug_toolbar/panels/sql.py | 46 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 37 insertions(+), 9 deletions(-) (limited to 'debug_toolbar/panels') diff --git a/debug_toolbar/panels/sql.py b/debug_toolbar/panels/sql.py index 9f4e83c..0c36f48 100644 --- a/debug_toolbar/panels/sql.py +++ b/debug_toolbar/panels/sql.py @@ -1,4 +1,5 @@ import re +import uuid from django.db.backends import BaseDatabaseWrapper from django.template.loader import render_to_string @@ -71,22 +72,30 @@ class SQLDebugPanel(DebugPanel): self._queries = [] self._databases = {} self._transaction_status = {} + self._transaction_ids = {} - def get_transaction_status(self, alias, reset=False): + def get_transaction_id(self, alias): conn = connections[alias].connection if not conn: return None 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: + self._transaction_ids[alias] = uuid.uuid4().hex - if reset or self._transaction_status.get(alias) is None: - if engine == 'psycopg2': - self._transaction_status[alias] = conn.get_transaction_status() - else: - raise ValueError(engine) - - return self._transaction_status[alias] - + return self._transaction_ids[alias] def record(self, alias, **kwargs): self._queries.append((alias, kwargs)) @@ -146,7 +155,22 @@ class SQLDebugPanel(DebugPanel): 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) + + print trans_id, last_trans_id + if query['engine'] == 'psycopg2' and trans_id != last_trans_id: + if last_trans_id: + self._queries[i][1]['ends_trans'] = True + trans_ids[alias] = 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']) @@ -167,6 +191,10 @@ class SQLDebugPanel(DebugPanel): params = map(escape, frame[0].rsplit('/', 1) + list(frame[1:])) stacktrace.append('{0}/{1} in {3}({2})\n {4}"'.format(*params)) query['stacktrace'] = mark_safe('\n'.join(stacktrace)) + i += 1 + + if trans_id: + self._queries[i][1]['ends_trans'] = True context = self.context.copy() context.update({ -- cgit v1.2.3 From 5542f52d7ca2bfda8bd5135476eef1f93164ed50 Mon Sep 17 00:00:00 2001 From: David Cramer Date: Wed, 6 Apr 2011 16:34:34 -0700 Subject: Transaction id tracing is not specific to psycopg2 --- debug_toolbar/panels/sql.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'debug_toolbar/panels') diff --git a/debug_toolbar/panels/sql.py b/debug_toolbar/panels/sql.py index 0c36f48..7427101 100644 --- a/debug_toolbar/panels/sql.py +++ b/debug_toolbar/panels/sql.py @@ -162,8 +162,7 @@ class SQLDebugPanel(DebugPanel): trans_id = query.get('trans_id') last_trans_id = trans_ids.get(alias) - print trans_id, last_trans_id - if query['engine'] == 'psycopg2' and trans_id != last_trans_id: + if trans_id != last_trans_id: if last_trans_id: self._queries[i][1]['ends_trans'] = True trans_ids[alias] = trans_id -- cgit v1.2.3 From 42e693a94aa3a92f7b270df9fb22374da2bc5a05 Mon Sep 17 00:00:00 2001 From: David Cramer Date: Wed, 6 Apr 2011 16:52:25 -0700 Subject: Mark the correct query as ending the transaction --- debug_toolbar/panels/sql.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'debug_toolbar/panels') diff --git a/debug_toolbar/panels/sql.py b/debug_toolbar/panels/sql.py index 7427101..a3812de 100644 --- a/debug_toolbar/panels/sql.py +++ b/debug_toolbar/panels/sql.py @@ -164,7 +164,7 @@ class SQLDebugPanel(DebugPanel): if trans_id != last_trans_id: if last_trans_id: - self._queries[i][1]['ends_trans'] = True + self._queries[i-1][1]['ends_trans'] = True trans_ids[alias] = trans_id query['starts_trans'] = True if trans_id: -- cgit v1.2.3 From 34eff57a293ba456d87a04e5cc2a72056c1a2010 Mon Sep 17 00:00:00 2001 From: David Cramer Date: Wed, 6 Apr 2011 16:54:42 -0700 Subject: unset transaction id if we're not in a transaction --- debug_toolbar/panels/sql.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'debug_toolbar/panels') diff --git a/debug_toolbar/panels/sql.py b/debug_toolbar/panels/sql.py index a3812de..9dfee71 100644 --- a/debug_toolbar/panels/sql.py +++ b/debug_toolbar/panels/sql.py @@ -93,7 +93,10 @@ class SQLDebugPanel(DebugPanel): return None if cur_status != last_status: - self._transaction_ids[alias] = uuid.uuid4().hex + if cur_status: + self._transaction_ids[alias] = uuid.uuid4().hex + else: + self._transaction_ids[alias] = None return self._transaction_ids[alias] -- cgit v1.2.3 From 43a95e175c889064f580a8ab472ae45a16e2f738 Mon Sep 17 00:00:00 2001 From: David Cramer Date: Wed, 6 Apr 2011 16:56:30 -0700 Subject: Only mark starting a new transaction if new trans_id is set --- debug_toolbar/panels/sql.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'debug_toolbar/panels') diff --git a/debug_toolbar/panels/sql.py b/debug_toolbar/panels/sql.py index 9dfee71..0fac42b 100644 --- a/debug_toolbar/panels/sql.py +++ b/debug_toolbar/panels/sql.py @@ -169,7 +169,8 @@ class SQLDebugPanel(DebugPanel): if last_trans_id: self._queries[i-1][1]['ends_trans'] = True trans_ids[alias] = trans_id - query['starts_trans'] = True + if trans_id: + query['starts_trans'] = True if trans_id: query['in_trans'] = True -- cgit v1.2.3 From ed26be9548a4dedc5b3b21bf338e5a0a9bd2484b Mon Sep 17 00:00:00 2001 From: Rob Hudson Date: Fri, 8 Apr 2011 09:33:45 -0700 Subject: Updated sql panel to include logging support. This matches Django's added logging support to the debug cursor.--- debug_toolbar/panels/sql.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) (limited to 'debug_toolbar/panels') diff --git a/debug_toolbar/panels/sql.py b/debug_toolbar/panels/sql.py index 90a7b51..c6e73a0 100644 --- a/debug_toolbar/panels/sql.py +++ b/debug_toolbar/panels/sql.py @@ -21,6 +21,14 @@ from django.template.loader import render_to_string from django.utils import simplejson from django.utils.encoding import force_unicode, DjangoUnicodeDecodeError from django.utils.hashcompat import sha_constructor +try: + from django.utils.log import getLogger + logger = getLogger('django.db.backends') + has_logger = True +except ImportError: + # Compatibility with Django < 1.2 + has_logger = False + from django.utils.translation import ugettext_lazy as _ from debug_toolbar.panels import DebugPanel @@ -132,6 +140,12 @@ class DatabaseStatTracker(util.CursorDebugWrapper): pass del cur_frame + # Logging was added in Django 1.3 + if has_logger: + logger.debug('(%.3f) %s; args=%s' % (duration, sql, params), + extra={'duration':duration, 'sql':sql, 'params':params} + ) + # We keep `sql` to maintain backwards compatibility self.db.queries.append({ 'sql': self.db.ops.last_executed_query(self.cursor, sql, params), @@ -175,6 +189,11 @@ class DatabaseStatTracker(util.CursorDebugWrapper): pass del cur_frame + if has_logger: + logger.debug('(%.3f) %s; args=%s' % (duration, sql, params), + extra={'duration':duration, 'sql':sql, 'params':params} + ) + # We keep `sql` to maintain backwards compatibility self.db.queries.append({ 'sql': self.db.ops.last_executed_query(self.cursor, sql, params), -- cgit v1.2.3 From ad8071f6657b539570949bef6d4d82c89bfd19fe Mon Sep 17 00:00:00 2001 From: Rob Hudson Date: Fri, 8 Apr 2011 09:34:36 -0700 Subject: Stripped out logging of SQL queries from logging panel. --- debug_toolbar/panels/logger.py | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'debug_toolbar/panels') diff --git a/debug_toolbar/panels/logger.py b/debug_toolbar/panels/logger.py index 0ddbfb1..5e82a13 100644 --- a/debug_toolbar/panels/logger.py +++ b/debug_toolbar/panels/logger.py @@ -17,6 +17,11 @@ class LogCollector(object): self.records = {} # a dictionary that maps threads to log records def add_record(self, record, thread=None): + # Avoid logging SQL queries since they are already in the SQL panel + # TODO: Make this check whether SQL panel is enabled + if record.get('channel', '') == 'django.db.backends': + return + self.get_records(thread).append(record) def get_records(self, thread=None): -- cgit v1.2.3 From 95b1654028f74db4478da2214460ae8c363e8084 Mon Sep 17 00:00:00 2001 From: David Cramer Date: Wed, 13 Apr 2011 11:10:53 -0700 Subject: Fix a bug with marking the last query in a transaction as the end of chain --- debug_toolbar/panels/sql.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'debug_toolbar/panels') diff --git a/debug_toolbar/panels/sql.py b/debug_toolbar/panels/sql.py index 0fac42b..ff63b6f 100644 --- a/debug_toolbar/panels/sql.py +++ b/debug_toolbar/panels/sql.py @@ -197,7 +197,7 @@ class SQLDebugPanel(DebugPanel): i += 1 if trans_id: - self._queries[i][1]['ends_trans'] = True + self._queries[i-1][1]['ends_trans'] = True context = self.context.copy() context.update({ -- cgit v1.2.3 From a00f3c050ee2a1d37d14efa6bca402df6c466051 Mon Sep 17 00:00:00 2001 From: David Cramer Date: Wed, 13 Apr 2011 11:11:46 -0700 Subject: Ensure we are verifying that template.name is present before running a string check --- debug_toolbar/panels/template.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) (limited to 'debug_toolbar/panels') diff --git a/debug_toolbar/panels/template.py b/debug_toolbar/panels/template.py index d9cd071..44b8b3e 100644 --- a/debug_toolbar/panels/template.py +++ b/debug_toolbar/panels/template.py @@ -3,8 +3,6 @@ from pprint import pformat from django import http from django.conf import settings -from django.core.signals import request_started -from django.dispatch import Signal from django.template.context import get_standard_processors from django.template.loader import render_to_string from django.test.signals import template_rendered @@ -56,7 +54,7 @@ class TemplateDebugPanel(DebugPanel): def title(self): num_templates = len([t for t in self.templates - if not t['template'].name.startswith('debug_toolbar/')]) + if not (t['template'].name and t['template'].name.startswith('debug_toolbar/'))]) return _('Templates (%(num_templates)s rendered)') % {'num_templates': num_templates} def url(self): @@ -78,7 +76,7 @@ class TemplateDebugPanel(DebugPanel): # Clean up some info about templates template = template_data.get('template', None) # Skip templates that we are generating through the debug toolbar. - if template.name.startswith('debug_toolbar/'): + if template.name and template.name.startswith('debug_toolbar/'): continue if template.origin and template.origin.name: template.origin_name = template.origin.name -- cgit v1.2.3 From dfee3848866f2193576d2bdd44f0c8bcd10c63a4 Mon Sep 17 00:00:00 2001 From: Dave McLain Date: Thu, 21 Apr 2011 20:12:42 -0500 Subject: Creating a profiling panel from the version panel for basic stuff --- debug_toolbar/panels/profiling.py | 59 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 debug_toolbar/panels/profiling.py (limited to 'debug_toolbar/panels') diff --git a/debug_toolbar/panels/profiling.py b/debug_toolbar/panels/profiling.py new file mode 100644 index 0000000..302f444 --- /dev/null +++ b/debug_toolbar/panels/profiling.py @@ -0,0 +1,59 @@ +import sys + +import django +from django.conf import settings +from django.template.loader import render_to_string +from django.utils.translation import ugettext_lazy as _ + +import debug_toolbar +from debug_toolbar.panels import DebugPanel + + +class VersionDebugPanel(DebugPanel): + """ + Panel that displays the Django version. + """ + name = 'Profiling' + has_content = True + + def nav_title(self): + return _('Versions') + + def nav_subtitle(self): + return 'Django %s' % django.get_version() + + def url(self): + return '' + + def title(self): + return _('Versions') + + def content(self): + versions = {} + for app in settings.INSTALLED_APPS + ['django']: + name = app.split('.')[-1].replace('_', ' ').capitalize() + __import__(app) + app = sys.modules[app] + if hasattr(app, 'get_version'): + get_version = app.get_version + if callable(get_version): + version = get_version() + else: + version = get_version + elif hasattr(app, 'VERSION'): + version = app.VERSION + elif hasattr(app, '__version__'): + version = app.__version__ + else: + continue + if isinstance(version, (list, tuple)): + version = '.'.join(str(o) for o in version) + versions[name] = version + + context = self.context.copy() + context.update({ + 'versions': versions, + 'paths': sys.path, + }) + + return render_to_string('debug_toolbar/panels/versions.html', context) -- cgit v1.2.3 From 3929bf2066e108695546e2dd8db14579a8710126 Mon Sep 17 00:00:00 2001 From: Dave McLain Date: Fri, 22 Apr 2011 01:24:57 -0500 Subject: first pass at a profiler panel --- debug_toolbar/panels/profiling.py | 169 ++++++++++++++++++++++++++++++-------- 1 file changed, 136 insertions(+), 33 deletions(-) (limited to 'debug_toolbar/panels') diff --git a/debug_toolbar/panels/profiling.py b/debug_toolbar/panels/profiling.py index 302f444..af7137f 100644 --- a/debug_toolbar/panels/profiling.py +++ b/debug_toolbar/panels/profiling.py @@ -1,15 +1,120 @@ -import sys - import django from django.conf import settings from django.template.loader import render_to_string from django.utils.translation import ugettext_lazy as _ - +from django.utils.safestring import mark_safe import debug_toolbar from debug_toolbar.panels import DebugPanel +import sys +import cProfile +from pstats import Stats, f8, func_std_string +from cStringIO import StringIO +from django.conf import settings +from colorsys import hsv_to_rgb + +class DjangoDebugToolbarStats(Stats): + __root = None + + def get_root_func(self): + if self.__root is None: + for func, (cc, nc, tt, ct, callers) in self.stats.iteritems(): + if len(callers) == 0: + self.__root = func + break + return self.__root + + def print_call_tree_node(self, function, depth, max_depth, cum_filter=0.1): + self.print_line(function, depth=depth) + if depth < max_depth: + for called in self.all_callees[function].keys(): + if self.stats[called][3] >= cum_filter: + self.print_call_tree_node(called, depth+1, max_depth, cum_filter=cum_filter) -class VersionDebugPanel(DebugPanel): +class FunctionCall(object): + def __init__(self, statobj, func, depth=0, stats=None, css_id='djDebugProfileCall_0', hsv=(0,0.5,1)): + self.statobj = statobj + self.func = func + if stats: + self.stats = stats + else: + self.stats = statobj.stats[func][:4] + self.depth = depth + self.id = css_id + self.hsv=hsv + + def parent_classes(self): + return self.parent_classes + + def background(self): + print self.hsv + r,g,b = hsv_to_rgb(*self.hsv) + return 'rgb(%f%%,%f%%,%f%%)' %(r*100, g*100, b*100) + + def func_std_string(self): # match what old profile produced + func_name = self.func + if func_name[:2] == ('~', 0): + # special case for built-in functions + name = func_name[2] + if name.startswith('<') and name.endswith('>'): + return '{%s}' % name[1:-1] + else: + return name + else: + file_name, line_num, method = self.func + if file_name.startswith(settings.PROJECT_ROOT): + file_name='PROJECT' + file_name[len(settings.PROJECT_ROOT):] + idx = file_name.find('/site-packages/') + if idx > -1: + file_name=file_name[idx+14:] + return "%s:%d(%s)" % (file_name, line_num, method) + + def subfuncs(self): + i=0 + h,s,v = self.hsv + count = len(self.statobj.all_callees[self.func]) + for func, stats in self.statobj.all_callees[self.func].iteritems(): + i += 1 + h1 = h + (i/count)/(self.depth+1) + if stats[3] == 0: + s1 = 0 + else: + s1 = s*(stats[3]/self.stats[3]) + yield FunctionCall(self.statobj, + func, + self.depth+1, + stats=stats, + css_id=self.id + '_' + str(i), + hsv=(h1,s1,1)) + + def as_row(self): + cc, nc, tt, ct = self.stats + if nc != cc: + c = str(nc) + '/' + str(cc) + else: + c = str(nc) + + if nc != 0: + ttdivnc = tt/nc + else: + ttdivnc = 0 + + if cc == 0: + ctdivcc = 0 + else: + ctdivcc = ct/cc + indent = 5*self.depth + funcstr = self.func_std_string() + out = """ + %(c)s + %(tt)8.3f
(%(ttdivnc)8.3f) + %(ct)8.3f
(%(ctdivcc)8.3f) + %(funcstr)s + """ % locals() + return mark_safe(out) + + +class ProfilingDebugPanel(DebugPanel): """ Panel that displays the Django version. """ @@ -17,43 +122,41 @@ class VersionDebugPanel(DebugPanel): has_content = True def nav_title(self): - return _('Versions') - - def nav_subtitle(self): - return 'Django %s' % django.get_version() + return _('Profiling') def url(self): return '' def title(self): - return _('Versions') + return _('Profiling') - def content(self): - versions = {} - for app in settings.INSTALLED_APPS + ['django']: - name = app.split('.')[-1].replace('_', ' ').capitalize() - __import__(app) - app = sys.modules[app] - if hasattr(app, 'get_version'): - get_version = app.get_version - if callable(get_version): - version = get_version() - else: - version = get_version - elif hasattr(app, 'VERSION'): - version = app.VERSION - elif hasattr(app, '__version__'): - version = app.__version__ - else: - continue - if isinstance(version, (list, tuple)): - version = '.'.join(str(o) for o in version) - versions[name] = version + def process_view(self, request, view_func, view_args, view_kwargs): + self.profiler = cProfile.Profile() + args = (request,) + view_args + return self.profiler.runcall(view_func, *args, **view_kwargs) + + def process_response(self, request, response): + self.profiler.create_stats() + self.stats = DjangoDebugToolbarStats(self.profiler) + return response + def add_node(self, func_list, func, max_depth, cum_time=0.1): + func_list.append(func) + if func.depth < max_depth: + for subfunc in func.subfuncs(): + if subfunc.stats[3] >= cum_time: + self.add_node(func_list, subfunc, max_depth, cum_time=cum_time) + + def content(self): + + self.stats.calc_callees() + root = FunctionCall(self.stats, self.stats.get_root_func(), depth=0) + + func_list = [] + self.add_node(func_list, root, 10, root.stats[3]/8) context = self.context.copy() context.update({ - 'versions': versions, - 'paths': sys.path, + 'func_list': func_list, }) - return render_to_string('debug_toolbar/panels/versions.html', context) + return render_to_string('debug_toolbar/panels/profiling.html', context) -- cgit v1.2.3 From 38019ea2f5222ac97a7c5b67074a8a3b2ecf1ba0 Mon Sep 17 00:00:00 2001 From: David Cramer Date: Fri, 22 Apr 2011 13:07:41 -0700 Subject: Kill off requirement of PROJECT_ROOT and clean up lint violations in profiling panel --- debug_toolbar/panels/profiling.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) (limited to 'debug_toolbar/panels') diff --git a/debug_toolbar/panels/profiling.py b/debug_toolbar/panels/profiling.py index af7137f..465a0a3 100644 --- a/debug_toolbar/panels/profiling.py +++ b/debug_toolbar/panels/profiling.py @@ -1,16 +1,10 @@ -import django -from django.conf import settings from django.template.loader import render_to_string from django.utils.translation import ugettext_lazy as _ from django.utils.safestring import mark_safe -import debug_toolbar from debug_toolbar.panels import DebugPanel -import sys import cProfile -from pstats import Stats, f8, func_std_string -from cStringIO import StringIO -from django.conf import settings +from pstats import Stats from colorsys import hsv_to_rgb class DjangoDebugToolbarStats(Stats): @@ -62,8 +56,6 @@ class FunctionCall(object): return name else: file_name, line_num, method = self.func - if file_name.startswith(settings.PROJECT_ROOT): - file_name='PROJECT' + file_name[len(settings.PROJECT_ROOT):] idx = file_name.find('/site-packages/') if idx > -1: file_name=file_name[idx+14:] -- cgit v1.2.3 From 5fc8d35d17b691727c08e9c2b55aa655f516850f Mon Sep 17 00:00:00 2001 From: David Cramer Date: Fri, 22 Apr 2011 13:08:44 -0700 Subject: Remove print statement --- debug_toolbar/panels/profiling.py | 1 - 1 file changed, 1 deletion(-) (limited to 'debug_toolbar/panels') diff --git a/debug_toolbar/panels/profiling.py b/debug_toolbar/panels/profiling.py index 465a0a3..97f4865 100644 --- a/debug_toolbar/panels/profiling.py +++ b/debug_toolbar/panels/profiling.py @@ -41,7 +41,6 @@ class FunctionCall(object): return self.parent_classes def background(self): - print self.hsv r,g,b = hsv_to_rgb(*self.hsv) return 'rgb(%f%%,%f%%,%f%%)' %(r*100, g*100, b*100) -- cgit v1.2.3 From 249247f9c7a5027d444471526859ee86c06a1f1a Mon Sep 17 00:00:00 2001 From: David Cramer Date: Fri, 22 Apr 2011 14:47:36 -0700 Subject: Initial refactor of toggline so that profiling matches sql --- debug_toolbar/panels/profiling.py | 70 +++++++++++++++++++++++---------------- 1 file changed, 42 insertions(+), 28 deletions(-) (limited to 'debug_toolbar/panels') diff --git a/debug_toolbar/panels/profiling.py b/debug_toolbar/panels/profiling.py index 97f4865..d0ea2c1 100644 --- a/debug_toolbar/panels/profiling.py +++ b/debug_toolbar/panels/profiling.py @@ -1,3 +1,5 @@ +from __future__ import division + from django.template.loader import render_to_string from django.utils.translation import ugettext_lazy as _ from django.utils.safestring import mark_safe @@ -26,7 +28,7 @@ class DjangoDebugToolbarStats(Stats): self.print_call_tree_node(called, depth+1, max_depth, cum_filter=cum_filter) class FunctionCall(object): - def __init__(self, statobj, func, depth=0, stats=None, css_id='djDebugProfileCall_0', hsv=(0,0.5,1)): + def __init__(self, statobj, func, depth=0, stats=None, id=0, parent_ids=[], hsv=(0,0.5,1)): self.statobj = statobj self.func = func if stats: @@ -34,8 +36,9 @@ class FunctionCall(object): else: self.stats = statobj.stats[func][:4] self.depth = depth - self.id = css_id - self.hsv=hsv + self.id = id + self.parent_ids = parent_ids + self.hsv = hsv def parent_classes(self): return self.parent_classes @@ -58,7 +61,15 @@ class FunctionCall(object): idx = file_name.find('/site-packages/') if idx > -1: file_name=file_name[idx+14:] - return "%s:%d(%s)" % (file_name, line_num, method) + + file_path, file_name = file_name.rsplit('/', 1) + + return mark_safe('{0}/{1} in {3}({2})'.format( + file_path, + file_name, + line_num, + method, + )) def subfuncs(self): i=0 @@ -75,35 +86,38 @@ class FunctionCall(object): func, self.depth+1, stats=stats, - css_id=self.id + '_' + str(i), + id=str(self.id) + '_' + str(i), + parent_ids=self.parent_ids + [self.id], hsv=(h1,s1,1)) - def as_row(self): + def count(self): + return self.stats[1] + + def tottime(self): + return self.stats[2] + + def cumtime(self): cc, nc, tt, ct = self.stats - if nc != cc: - c = str(nc) + '/' + str(cc) - else: - c = str(nc) - - if nc != 0: - ttdivnc = tt/nc - else: - ttdivnc = 0 - + return self.stats[3] + + def tottime_per_call(self): + cc, nc, tt, ct = self.stats + + if nc == 0: + return 0 + + return tt/nc + + def cumtime_per_call(self): + cc, nc, tt, ct = self.stats + if cc == 0: - ctdivcc = 0 - else: - ctdivcc = ct/cc - indent = 5*self.depth - funcstr = self.func_std_string() - out = """ - %(c)s - %(tt)8.3f
(%(ttdivnc)8.3f) - %(ct)8.3f
(%(ctdivcc)8.3f) - %(funcstr)s - """ % locals() - return mark_safe(out) + return 0 + + return ct/cc + def indent(self): + return 16 * self.depth class ProfilingDebugPanel(DebugPanel): """ -- cgit v1.2.3 From a3e8ce8eb1c4e2d1482a22d3a7e0dba7f4ff3201 Mon Sep 17 00:00:00 2001 From: David Cramer Date: Fri, 22 Apr 2011 14:59:59 -0700 Subject: Basic support for toggling everywhere correctly --- debug_toolbar/panels/profiling.py | 2 ++ 1 file changed, 2 insertions(+) (limited to 'debug_toolbar/panels') diff --git a/debug_toolbar/panels/profiling.py b/debug_toolbar/panels/profiling.py index d0ea2c1..68aafb7 100644 --- a/debug_toolbar/panels/profiling.py +++ b/debug_toolbar/panels/profiling.py @@ -147,9 +147,11 @@ class ProfilingDebugPanel(DebugPanel): def add_node(self, func_list, func, max_depth, cum_time=0.1): func_list.append(func) + func.has_subfuncs = False if func.depth < max_depth: for subfunc in func.subfuncs(): if subfunc.stats[3] >= cum_time: + func.has_subfuncs = True self.add_node(func_list, subfunc, max_depth, cum_time=cum_time) def content(self): -- cgit v1.2.3