Skip to content

Commit 220373f

Browse files
committed
Support storing serialized panel stats directly into the store.
In order to support the toolbar persisting stats, we need to move away from calling record_stats with non-primitives.
1 parent e22ea43 commit 220373f

File tree

13 files changed

+117
-48
lines changed

13 files changed

+117
-48
lines changed

debug_toolbar/panels/__init__.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from django.template.loader import render_to_string
22

33
from debug_toolbar import settings as dt_settings
4+
from debug_toolbar.store import store
45
from debug_toolbar.utils import get_name_from_obj
56

67

@@ -146,19 +147,38 @@ def disable_instrumentation(self):
146147

147148
# Store and retrieve stats (shared between panels for no good reason)
148149

150+
def deserialize_stats(self, data):
151+
"""
152+
Deserialize stats coming from the store.
153+
154+
Provided to support future store mechanisms overriding a panel's content.
155+
"""
156+
return data
157+
158+
def serialize_stats(self, stats):
159+
"""
160+
Serialize stats for the store.
161+
162+
Provided to support future store mechanisms overriding a panel's content.
163+
"""
164+
return stats
165+
149166
def record_stats(self, stats):
150167
"""
151168
Store data gathered by the panel. ``stats`` is a :class:`dict`.
152169
153170
Each call to ``record_stats`` updates the statistics dictionary.
154171
"""
155172
self.toolbar.stats.setdefault(self.panel_id, {}).update(stats)
173+
store.save_panel(
174+
self.toolbar.store_id, self.panel_id, self.serialize_stats(stats)
175+
)
156176

157177
def get_stats(self):
158178
"""
159179
Access data stored by the panel. Returns a :class:`dict`.
160180
"""
161-
return self.toolbar.stats.get(self.panel_id, {})
181+
return self.deserialize_stats(store.panel(self.toolbar.store_id, self.panel_id))
162182

163183
def record_server_timing(self, key, title, value):
164184
"""

debug_toolbar/panels/cache.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ def _store_call_info(
208208
"kwargs": kwargs,
209209
"trace": render_stacktrace(trace),
210210
"template_info": template_info,
211-
"backend": backend,
211+
"backend": str(backend),
212212
}
213213
)
214214

debug_toolbar/panels/history/panel.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,9 @@ def nav_subtitle(self):
4040
def generate_stats(self, request, response):
4141
try:
4242
if request.method == "GET":
43-
data = request.GET.copy()
43+
data = dict(request.GET.copy())
4444
else:
45-
data = request.POST.copy()
45+
data = dict(request.POST.copy())
4646
# GraphQL tends to not be populated in POST. If the request seems
4747
# empty, check if it's a JSON request.
4848
if (

debug_toolbar/panels/profiling.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ def __init__(
5151
self.id = id
5252
self.parent_ids = parent_ids
5353
self.hsv = hsv
54+
self.has_subfuncs = False
5455

5556
def parent_classes(self):
5657
return self.parent_classes
@@ -141,6 +142,20 @@ def cumtime_per_call(self):
141142
def indent(self):
142143
return 16 * self.depth
143144

145+
def as_context(self):
146+
return {
147+
"id": self.id,
148+
"parent_ids": self.parent_ids,
149+
"func_std_string": self.func_std_string(),
150+
"has_subfuncs": self.has_subfuncs,
151+
"cumtime": self.cumtime(),
152+
"cumtime_per_call": self.cumtime_per_call(),
153+
"tottime": self.tottime(),
154+
"tottime_per_call": self.tottime_per_call(),
155+
"count": self.count(),
156+
"indent": self.indent(),
157+
}
158+
144159

145160
class ProfilingPanel(Panel):
146161
"""
@@ -157,7 +172,6 @@ def process_request(self, request):
157172

158173
def add_node(self, func_list, func, max_depth, cum_time=0.1):
159174
func_list.append(func)
160-
func.has_subfuncs = False
161175
if func.depth < max_depth:
162176
for subfunc in func.subfuncs():
163177
if subfunc.stats[3] >= cum_time:
@@ -183,4 +197,4 @@ def generate_stats(self, request, response):
183197
dt_settings.get_config()["PROFILER_MAX_DEPTH"],
184198
root.stats[3] / 8,
185199
)
186-
self.record_stats({"func_list": func_list})
200+
self.record_stats({"func_list": [func.as_context() for func in func_list]})

debug_toolbar/panels/request.py

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,11 @@ def nav_subtitle(self):
2424
return view_func.rsplit(".", 1)[-1]
2525

2626
def generate_stats(self, request, response):
27-
self.record_stats(
28-
{
29-
"get": get_sorted_request_variable(request.GET),
30-
"post": get_sorted_request_variable(request.POST),
31-
"cookies": get_sorted_request_variable(request.COOKIES),
32-
}
33-
)
27+
stats = {
28+
"get": get_sorted_request_variable(request.GET),
29+
"post": get_sorted_request_variable(request.POST),
30+
"cookies": get_sorted_request_variable(request.COOKIES),
31+
}
3432

3533
view_info = {
3634
"view_func": _("<no view>"),
@@ -47,14 +45,15 @@ def generate_stats(self, request, response):
4745
view_info["view_urlname"] = getattr(match, "url_name", _("<unavailable>"))
4846
except Http404:
4947
pass
50-
self.record_stats(view_info)
48+
stats.update(view_info)
5149

5250
if hasattr(request, "session"):
53-
self.record_stats(
51+
stats.update(
5452
{
5553
"session": [
5654
(k, request.session.get(k))
5755
for k in sorted(request.session.keys())
5856
]
5957
}
6058
)
59+
self.record_stats(stats)

debug_toolbar/panels/templates/panel.py

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -171,20 +171,24 @@ def disable_instrumentation(self):
171171
def generate_stats(self, request, response):
172172
template_context = []
173173
for template_data in self.templates:
174-
info = {}
175174
# Clean up some info about templates
176175
template = template_data["template"]
177176
if hasattr(template, "origin") and template.origin and template.origin.name:
178-
template.origin_name = template.origin.name
179-
template.origin_hash = signing.dumps(template.origin.name)
177+
origin_name = template.origin.name
178+
origin_hash = signing.dumps(template.origin.name)
180179
else:
181-
template.origin_name = _("No origin")
182-
template.origin_hash = ""
183-
info["template"] = template
184-
# Clean up context for better readability
185-
if self.toolbar.config["SHOW_TEMPLATE_CONTEXT"]:
186-
context_list = template_data.get("context", [])
187-
info["context"] = "\n".join(context_list)
180+
origin_name = _("No origin")
181+
origin_hash = ""
182+
info = {
183+
"template": {
184+
"origin_name": origin_name,
185+
"origin_hash": origin_hash,
186+
"name": template.name,
187+
},
188+
"context": "\n".join(template_data.get("context", []))
189+
if self.toolbar.config["SHOW_TEMPLATE_CONTEXT"]
190+
else "",
191+
}
188192
template_context.append(info)
189193

190194
# Fetch context_processors/template_dirs from any template

debug_toolbar/store.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,32 @@
1-
from collections import OrderedDict
1+
import json
2+
from collections import OrderedDict, defaultdict
23

4+
from django.core.serializers.json import DjangoJSONEncoder
35
from django.utils.module_loading import import_string
46

57
from debug_toolbar import settings as dt_settings
68

79

10+
class DebugToolbarJSONEncoder(DjangoJSONEncoder):
11+
def default(self, o):
12+
try:
13+
return super().default(o)
14+
except TypeError:
15+
return str(o)
16+
17+
18+
def serialize(data):
19+
return json.dumps(data, cls=DebugToolbarJSONEncoder)
20+
21+
22+
def deserialize(data):
23+
return json.loads(data)
24+
25+
26+
# Record stats in serialized fashion.
27+
# Remove use of fetching the toolbar as a whole from the store.
28+
29+
830
class BaseStore:
931
config = dt_settings.get_config().copy()
1032

@@ -24,9 +46,14 @@ def set(cls, store_id, toolbar):
2446
def delete(cls, store_id):
2547
raise NotImplementedError
2648

49+
@classmethod
50+
def record_stats(cls, store_id, panel_id, stats):
51+
raise NotImplementedError
52+
2753

2854
class MemoryStore(BaseStore):
2955
_store = OrderedDict()
56+
_stats = defaultdict(dict)
3057

3158
@classmethod
3259
def get(cls, store_id):
@@ -46,5 +73,17 @@ def set(cls, store_id, toolbar):
4673
def delete(cls, store_id):
4774
del cls._store[store_id]
4875

76+
@classmethod
77+
def save_panel(cls, store_id, panel_id, stats=None):
78+
cls._stats[store_id][panel_id] = serialize(stats)
79+
80+
@classmethod
81+
def panel(cls, store_id, panel_id):
82+
try:
83+
data = cls._stats[store_id][panel_id]
84+
except KeyError:
85+
data = None
86+
return {} if data is None else deserialize(data)
87+
4988

5089
store = import_string(dt_settings.get_config()["TOOLBAR_STORE_CLASS"])

debug_toolbar/templates/debug_toolbar/panels/cache.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@ <h4>{% trans "Calls" %}</h4>
6161
</tr>
6262
<tr class="djUnselected djToggleDetails_{{ forloop.counter }}" id="cacheDetails_{{ forloop.counter }}">
6363
<td colspan="1"></td>
64-
<td colspan="5"><pre class="djdt-stack">{{ call.trace }}</pre></td>
64+
{# The trace property is escaped when serialized into the store #}
65+
<td colspan="5"><pre class="djdt-stack">{{ call.trace|safe }}</pre></td>
6566
</tr>
6667
{% endfor %}
6768
</tbody>

debug_toolbar/templates/debug_toolbar/panels/profiling.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020
{% else %}
2121
<span class="djNoToggleSwitch"></span>
2222
{% endif %}
23-
<span class="djdt-stack">{{ call.func_std_string }}</span>
23+
{# The func_std_string property is escaped when serialized into the store #}
24+
<span class="djdt-stack">{{ call.func_std_string|safe }}</span>
2425
</div>
2526
</td>
2627
<td>{{ call.cumtime|floatformat:3 }}</td>

debug_toolbar/templates/debug_toolbar/panels/sql.html

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,8 @@
7777
{% if query.params %}
7878
{% if query.is_select %}
7979
<form method="post">
80-
{{ query.form }}
80+
{# The form is rendered when serialized into storage #}
81+
{{ query.form|safe }}
8182
<button formaction="{% url 'djdt:sql_select' %}" class="remoteCall">Sel</button>
8283
<button formaction="{% url 'djdt:sql_explain' %}" class="remoteCall">Expl</button>
8384
{% if query.vendor == 'mysql' %}
@@ -100,7 +101,8 @@
100101
<p><strong>{% trans "Transaction status:" %}</strong> {{ query.trans_status }}</p>
101102
{% endif %}
102103
{% if query.stacktrace %}
103-
<pre class="djdt-stack">{{ query.stacktrace }}</pre>
104+
{# The stacktrace property is a rendered template. It is escaped when serialized into the store #}
105+
<pre class="djdt-stack">{{ query.stacktrace|safe }}</pre>
104106
{% endif %}
105107
{% if query.template_info %}
106108
<table>

0 commit comments

Comments
 (0)