Permalink
Cannot retrieve contributors at this time
Name already in use
A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
django-debug-toolbar-template-timings/template_timings_panel/panels/TemplateTimings.py
Go to fileThis commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
248 lines (189 sloc)
8.87 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from debug_toolbar.panels import Panel | |
from django.conf import settings | |
from django.template import base as template_base | |
from django.template import Template, Library | |
from django.template.loader_tags import BlockNode | |
from debug_toolbar.panels import sql | |
from django.core.exceptions import ImproperlyConfigured | |
import threading | |
import functools | |
import time | |
import re | |
import collections | |
import logging | |
logger = logging.getLogger(__name__) | |
# A set of nodes found by the tag_compiler. Used to patch during next request | |
# and to unpatch the nodes. | |
FOUND_GENERIC_NODES = set() | |
if not "debug_toolbar.panels.sql.SQLPanel" in settings.DEBUG_TOOLBAR_PANELS: | |
raise ImproperlyConfigured("debug_toolbar.panels.sql.SQLPanel must be present in DEBUG_TOOLBAR_PANELS") | |
def replace_method(klass, method_name): | |
original = getattr(klass, method_name) | |
def inner(callback): | |
def wrapped(*args, **kwargs): | |
return callback(original, *args, **kwargs) | |
actual = getattr(original, '__wrapped__', original) | |
wrapped.__wrapped__ = actual | |
wrapped.__doc__ = getattr(actual, '__doc__', None) | |
wrapped.__name__ = actual.__name__ | |
setattr(klass, method_name, wrapped) | |
return wrapped | |
return inner | |
def record_query(**kwargs): | |
if hasattr(results, "_current_template"): | |
if not results._current_key in results.timings or \ | |
not results._current_template in results.timings[results._current_key]: | |
return | |
part = results.timings[results._current_key][results._current_template] | |
part["queries"] += 1 | |
part["query_duration"] += kwargs["duration"] | |
logger.debug("Template: %s executed query %s" % (results._current_template, kwargs["raw_sql"])) | |
@replace_method(sql.SQLPanel, "record") | |
def record(func, self, **kwargs): | |
record_query(**kwargs) | |
return func(self, **kwargs) | |
results = threading.local() | |
TEMPLATE_TIMINGS_SETTINGS = { | |
'IGNORED_TEMPLATES': ["debug_toolbar/*"] | |
} | |
for k in TEMPLATE_TIMINGS_SETTINGS.keys(): | |
if hasattr(settings, k): | |
TEMPLATE_TIMINGS_SETTINGS[k] = getattr(settings, k) | |
def _tag_compiler(func): | |
@functools.wraps(func) | |
def wrapper(*args, **kwargs): | |
if 'node_class' in kwargs: | |
wrap_generic_node(kwargs['node_class'], kwargs['name']) | |
FOUND_GENERIC_NODES.add((kwargs['node_class'], kwargs['name'])) | |
return func(*args, **kwargs) | |
return wrapper | |
def wrap_generic_node(node, name): | |
if not hasattr(node.render, 'original'): | |
node.render = _template_render_wrapper( | |
node.render, node.__name__, name=lambda unused_: name) | |
try: | |
template_base.generic_tag_compiler = _tag_compiler(template_base.generic_tag_compiler) | |
except: | |
Library.simple_tag = _tag_compiler(Library.simple_tag) | |
def _template_render_wrapper(func, key, should_add=lambda n: True, name=lambda s: s.name if s.name else ''): | |
@functools.wraps(func) | |
def timing_hook(self, *args, **kwargs): | |
# Set up our thread-local results cache | |
if not hasattr(results, "timings"): | |
results.timings = collections.defaultdict(dict) | |
# This is like a stack. It gets incremented before a template is rendered and decremented when | |
# its finished rendering, because this function is recursive. So if it is 1 then this is the first | |
# template being rendered, and its total execution time encompasses all of the sub-templates rendering | |
# time. We use this to display the rendering time on the sidebar | |
results._count = 0 | |
# These two values are used with the SQL counter. We store the current key and template name | |
# so that the _logger can get to the current template being rendered. | |
results._current_template = name(self) | |
results._current_key = key | |
name_self = name(self) | |
# Issue #11, sometimes for some reason accessing results.timings causes a KeyError. | |
if key not in results.timings: | |
results.timings[key] = {} | |
if name_self not in results.timings[key] and should_add(name_self): | |
results.timings.setdefault(key, {})[name_self] = { | |
'count': 0, | |
'min': 0, | |
'max': 0, | |
'total': 0, | |
'avg': 0, | |
'is_base': False, | |
'queries': 0, | |
'query_duration': 0 | |
} | |
results._count += 1 | |
_old_current = results._current_template | |
_old_key = results._current_key | |
results._current_template = name_self | |
results._current_key = key | |
start_time = time.time() | |
result = func(self, *args, **kwargs) | |
time_taken = (time.time() - start_time) * 1000.0 | |
results._current_template = _old_current | |
results._current_key = _old_key | |
if should_add(name_self): | |
results_part = results.timings[key][name_self] | |
results_part["min"] = min(time_taken, results_part["min"]) | |
results_part["max"] = max(time_taken, results_part["max"]) | |
results_part['count'] += 1 | |
results_part['total'] += time_taken | |
results_part['avg'] = results_part['total'] / results_part['count'] | |
results_part["is_base"] = results._count == 1 | |
if results_part["queries"] > 0: | |
try: | |
results_part["sql_percentage"] = "%.2f%%" % ((float(results_part["query_duration"]) / | |
float(results_part["total"])) * 100) | |
except ZeroDivisionError: | |
results_part["sql_percentage"] = "0%" | |
logger.debug("%s %s took %.1f" % (key, name_self, time_taken)) | |
results._count -= 1 | |
return result | |
timing_hook.original = func | |
return timing_hook | |
class TemplateTimings(Panel): | |
name = "TemplateTimingPanel" | |
template = "debug_toolbar_template_timings.html" | |
title = 'Template Timings' | |
has_content = True | |
def _get_timings(self): | |
return getattr(results, "timings", collections.defaultdict(dict)) | |
def _results_to_list(self, results): | |
returner = {} | |
for key, value in results.items(): | |
returner[key] = [] | |
for name, timings in value.items(): | |
new_timings = {"name": name} | |
new_timings.update(timings) | |
returner[key].append(new_timings) | |
return returner | |
@property | |
def nav_title(self): | |
return 'Template Timings' | |
def enable_instrumentation(self): | |
Template.render = _template_render_wrapper(Template.render, "templates", | |
lambda n: not any([re.match(pattern, n) | |
for pattern in TEMPLATE_TIMINGS_SETTINGS["IGNORED_TEMPLATES"]])) | |
BlockNode.render = _template_render_wrapper(BlockNode.render, "blocks") | |
# Wrap the nodes which were found by earlier requests | |
for node, name in FOUND_GENERIC_NODES: | |
wrap_generic_node(node, name) | |
def disable_instrumentation(self): | |
if hasattr(Template.render, "original"): | |
Template.render = Template.render.original | |
BlockNode.render = BlockNode.render.original | |
for node, name in FOUND_GENERIC_NODES: | |
if hasattr(node.render, 'original'): | |
node.render = node.render.original | |
@property | |
def nav_subtitle(self): | |
results = self._get_timings() | |
total_template_queries = sum( | |
sum(results[name][template_name]["queries"] for template_name in results[name]) | |
for name in results | |
) | |
total_template_query_time = sum( | |
sum(results[name][template_name]["query_duration"] for template_name in results[name]) | |
for name in results | |
) | |
base_template = list(filter(lambda i: results["templates"][i]["is_base"] == True, results["templates"].keys())) | |
if not len(base_template) == 1: | |
logger.info("Found more than one base template: %s" % str(base_template)) | |
else: | |
base_template = base_template[0] | |
base_time = results["templates"][base_template]["total"] | |
query_percentage_time = "" | |
if total_template_query_time > 0 and base_time > 0: | |
query_percentage_time = "(%.2f%% SQL)" % ((float(total_template_query_time) / float(base_time)) * 100) | |
return "%.0f ms with %s queries %s" % (base_time, total_template_queries, query_percentage_time) | |
def process_response(self, request, response): | |
timings = self._get_timings() | |
# Setting default_factory to None allows us to access | |
# template_timings.iteritems in the template. | |
if timings is not None: | |
timings.default_factory = None | |
timings = self._results_to_list(timings) | |
self.record_stats({"template_timings": timings}) |