Skip to content

Commit eaa8ee0

Browse files
authored
Merge pull request #17 from PetrDlouhy/trace_nodes
Break temlate parsing timeline by separate nodes
2 parents 5d4e06b + 5a7cb13 commit eaa8ee0

File tree

5 files changed

+173
-12
lines changed

5 files changed

+173
-12
lines changed

template_profiler_panel/panels/template.py

Lines changed: 104 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,49 @@
33
from time import time
44

55
import wrapt
6-
from django.dispatch import Signal
7-
from django.utils.translation import ugettext_lazy as _
8-
96
from debug_toolbar.panels import Panel
107
from debug_toolbar.panels.sql.utils import contrasting_color_generator
8+
from django.dispatch import Signal
9+
from django.utils.translation import ugettext_lazy as _
1110

12-
13-
template_rendered = Signal(providing_args=['instance', 'start', 'end', 'level'])
11+
template_rendered = Signal(providing_args=[
12+
'instance', 'start', 'end', 'level', 'processing_timeline',
13+
])
14+
15+
16+
node_element_colors = {}
17+
18+
19+
def get_nodelist_timeline(nodelist, level):
20+
timeline = []
21+
for node in nodelist:
22+
timeline += get_node_timeline(node, level)
23+
return timeline
24+
25+
26+
def get_node_timeline(node, level):
27+
"""
28+
Get timeline for node and it's children
29+
"""
30+
timeline = []
31+
child_nodelists = getattr(node, "child_nodelists", None)
32+
if child_nodelists:
33+
for child_nodelist_str in child_nodelists:
34+
child_nodelist = getattr(node, child_nodelist_str, None)
35+
if child_nodelist:
36+
timeline += get_nodelist_timeline(child_nodelist, level + 1)
37+
38+
if hasattr(node, "_node_end"):
39+
timeline.append(
40+
{
41+
"node": node,
42+
"name": node,
43+
"start": node._node_start,
44+
"end": node._node_end,
45+
"level": level,
46+
},
47+
)
48+
return timeline
1449

1550

1651
class TemplateProfilerPanel(Panel):
@@ -19,6 +54,7 @@ class TemplateProfilerPanel(Panel):
1954
'''
2055

2156
template = 'template_profiler_panel/template.html'
57+
scripts = ["static/js/template_profiler.js"]
2258

2359
def __init__(self, *args, **kwargs):
2460
self.colors = {}
@@ -49,6 +85,9 @@ def monkey_patch_template_classes(cls):
4985
else:
5086
template_classes.append(Jinja2Template)
5187

88+
from django.template import Node as DjangoNode
89+
node_classes = [DjangoNode]
90+
5291
@wrapt.decorator
5392
def render_wrapper(wrapped, instance, args, kwargs):
5493
start = time()
@@ -63,18 +102,34 @@ def render_wrapper(wrapped, instance, args, kwargs):
63102
break
64103
stack_depth += 1
65104

105+
timeline = []
106+
if hasattr(instance, 'nodelist'):
107+
timeline = get_nodelist_timeline(instance.nodelist, 0)
108+
66109
template_rendered.send(
67110
sender=instance.__class__,
68111
instance=instance,
69112
start=start,
70113
end=end,
114+
processing_timeline=timeline,
71115
level=stack_depth,
72116
)
73117
return result
74118

75119
for template_class in template_classes:
76120
template_class.render = render_wrapper(template_class.render)
77121

122+
@wrapt.decorator
123+
def render_node_wrapper(wrapped, instance, args, kwargs):
124+
instance._node_start = time()
125+
result = wrapped(*args, **kwargs)
126+
instance._node_end = time()
127+
return result
128+
129+
for node_class in node_classes:
130+
node_class.render_annotated = render_node_wrapper(
131+
node_class.render_annotated)
132+
78133
cls.have_monkey_patched_template_classes = True
79134

80135
@property
@@ -93,7 +148,8 @@ def title(self):
93148
def _get_color(self, level):
94149
return self.colors.setdefault(level, next(self.color_generator))
95150

96-
def record(self, instance, start, end, level, **kwargs):
151+
def record(self, instance, start, end, level,
152+
processing_timeline, **kwargs):
97153
if not self.enabled:
98154
return
99155

@@ -118,6 +174,7 @@ def record(self, instance, start, end, level, **kwargs):
118174
'end': end,
119175
'time': (end - start) * 1000.0,
120176
'level': level,
177+
'processing_timeline': processing_timeline,
121178
'name': template_name,
122179
'color': color,
123180
})
@@ -132,7 +189,7 @@ def _calc_p(self, part, whole):
132189
# return the percentage of part or 100% if whole is zero
133190
return (part / whole) * 100.0 if whole else 100.0
134191

135-
def _calc_timeline(self, start, end):
192+
def _calc_timeline(self, start, end, processing_timeline):
136193
result = {}
137194
result['offset_p'] = self._calc_p(
138195
start - self.t_min, self.t_max - self.t_min)
@@ -143,6 +200,43 @@ def _calc_timeline(self, start, end):
143200
result['rel_duration_p'] = self._calc_p(
144201
result['duration_p'], 100 - result['offset_p'])
145202

203+
result['relative_start'] = (start - self.t_min) * 1000.0
204+
result['relative_end'] = (end - self.t_min) * 1000.0
205+
206+
result['processing_timeline'] = []
207+
max_level = 0
208+
for time_item in processing_timeline:
209+
if 'node' in time_item:
210+
class_name = time_item['node'].__class__.__name__
211+
else:
212+
class_name = time_item['type']
213+
if class_name not in node_element_colors:
214+
node_element_colors[class_name] = next(self.color_generator)
215+
bg_color = node_element_colors[class_name]
216+
if 'node' in time_item:
217+
position = time_item['node'].token.position
218+
else:
219+
position = False
220+
level = time_item['level'] if 'level' in time_item else 0
221+
if level > max_level:
222+
max_level = level
223+
result['processing_timeline'].append({
224+
'name': time_item['name'],
225+
'position': position,
226+
'relative_start': (time_item['start'] - self.t_min) * 1000.0,
227+
'relative_end': (time_item['end'] - self.t_min) * 1000.0,
228+
'duration': (time_item['end'] - time_item['start']) * 1000.0,
229+
'rel_duration_p': self._calc_p(
230+
time_item['end'] - time_item['start'],
231+
self.t_max - self.t_min),
232+
'offset_p': self._calc_p(
233+
time_item['start']-self.t_min,
234+
self.t_max - self.t_min),
235+
'bg_color': bg_color,
236+
'level': level,
237+
})
238+
result['max_level'] = max_level
239+
146240
return result
147241

148242
def process_request(self, request):
@@ -165,7 +259,9 @@ def process_request(self, request):
165259
# Calc timelines
166260
for template in self.templates:
167261
template.update(
168-
self._calc_timeline(template['start'], template['end']))
262+
self._calc_timeline(
263+
template['start'], template['end'],
264+
template['processing_timeline']))
169265

170266
self.total = len(self.templates)
171267

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
2+
$(".timeline_item").mouseover(function() {
3+
$(this).css("border", "red 0.1px solid");
4+
}).mouseout(function() {
5+
$(this).css("border", "none");
6+
});

template_profiler_panel/templates/template_profiler_panel/template.html

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@ <h4>{{ templates|length }} {% trans 'calls to Template.render()' %}</h4>
33
<div>
44
<table>
55
<colgroup>
6+
<col style="width:1%" />
67
<col style="width:5%" />
7-
<col style="width:30%" />
8+
<col style="width:10%" />
89
<col style="width:60%" />
910
<col style="width:5%" />
1011
</colgroup>
1112
<thead>
1213
<tr>
14+
<th></th>
1315
<th>{% trans 'Stack Level' %}</th>
1416
<th>{% trans 'Template Name' %}</th>
1517
<th>{% trans 'Timeline' %}</th>
@@ -18,20 +20,52 @@ <h4>{{ templates|length }} {% trans 'calls to Template.render()' %}</h4>
1820
</thead>
1921
<tbody>
2022
{% for template in templates %}
21-
<tr class="{% cycle 'djDebugOdd' 'djDebugEven' %}">
23+
<tr class="{% cycle 'djDebugOdd' 'djDebugEven' %}" id="profileMain_{{ forloop.counter }}">
24+
<td class="djdt-toggle">
25+
<button type="button" class="djToggleSwitch" data-toggle-name="profileMain" data-toggle-id="{{ forloop.counter }}">+</button>
26+
</td>
2227
<td>
2328
<div style="background-color: {{ template.color.bg }}; color: {{ template.color.text }}; width: {{ template.level }}px; margin-right: 4px; padding-left: 2px;">{{ template.level }}</div>
2429
</td>
2530
<td>{{ template.name }}</td>
2631
<td class="timeline">
27-
<div class="djDebugTimeline">
32+
<div class="djDebugTimeline" style="position:relative;">
2833
<div class="djDebugLineChart" style="margin-left: {{ template.offset_p|stringformat:'f' }}%;">
29-
<div title="Start {{ template.start }}" style="min-width: 1px; width: {{ template.rel_duration_p|stringformat:'f' }}%; background-color: {{ template.color.bg }};">&nbsp;</div>
34+
<div title="Runtime {{ template.relative_start|floatformat:2 }} - {{ template.relative_end|floatformat:2 }} ms" style="min-width: 1px; width: {{ template.rel_duration_p|stringformat:'f' }}%; background-color: {{ template.color.bg }};">
35+
&nbsp;</div>
3036
</div>
3137
</div>
3238
</td>
3339
<td>{{ template.time|floatformat }}</td>
3440
</tr>
41+
<tr class="djToggleDetails_{{ forloop.counter }} djUnselected" id="sqlDetails_{{ forloop.counter }}">
42+
<td></td>
43+
<td></td>
44+
<td></td>
45+
<td>
46+
<div style="height: calc({{ template.max_level|add:1 }}px * 18); position:relative;" class="timeline_toggle_{{ forloop.counter }} djToggleDetails_{{ forloop.counter }} djSelected" id="sqlDetails_{{ forloop.counter }}">
47+
{% for time_item in template.processing_timeline %}
48+
<span title="{{ time_item.name }}
49+
Runtime {{ time_item.relative_start|floatformat:2 }} - {{ time_item.relative_end|floatformat:2 }} ms
50+
Duration {{ time_item.duration|floatformat:2 }} ms
51+
{% if time_item.position %}Lines {{ time_item.position.0 }} - {{ time_item.position.1 }}{% endif %}
52+
Depth {{ time_item.level|add:1 }}
53+
"
54+
class="timeline_item"
55+
style="
56+
min-width: 1px;
57+
margin-left: {{ time_item.offset_p|stringformat:'f' }}%;
58+
width: {{ time_item.rel_duration_p|stringformat:'f' }}%;
59+
background-color: {{ time_item.bg_color }};
60+
top: calc({{ time_item.level }}px * 18);
61+
position: absolute;
62+
opacity: 0.9;
63+
">&nbsp;</span>
64+
{% endfor %}
65+
</div>
66+
</td>
67+
<td></td>
68+
</tr>
3569
{% endfor %}
3670
</tbody>
3771
</table>

template_profiler_panel/templatetags/__init__.py

Whitespace-only changes.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from django import template
2+
from django.template.base import Node
3+
4+
5+
register = template.Library()
6+
7+
8+
@register.tag
9+
def profile(parser, tags):
10+
nodelist = parser.parse(('endprofile',))
11+
parser.delete_first_token()
12+
return ProfileNode(nodelist, tags)
13+
14+
15+
class ProfileNode(Node):
16+
def __init__(self, nodelist, tags):
17+
self.nodelist = nodelist
18+
self.block_name = tags.contents.split(' ')[1].strip("'")
19+
20+
def __str__(self):
21+
return f"Profile {self.block_name}"
22+
23+
def render(self, context):
24+
result = self.nodelist.render(context)
25+
return result

0 commit comments

Comments
 (0)