Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Serialize the panels #1826

Open
wants to merge 9 commits into
base: serializable
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -12,3 +12,4 @@ htmlcov
.tox
geckodriver.log
coverage.xml
venv
7 changes: 7 additions & 0 deletions Makefile
Expand Up @@ -6,6 +6,13 @@ example:
--noinput --username="$(USER)" --email="$(USER)@mailinator.com"
python example/manage.py runserver

example_async:
python example/manage.py migrate --noinput
-DJANGO_SUPERUSER_PASSWORD=p python example/manage.py createsuperuser \
--noinput --username="$(USER)" --email="$(USER)@mailinator.com"
daphne example.asgi:application


test:
DJANGO_SETTINGS_MODULE=tests.settings \
python -m django test $${TEST_ARGS:-tests}
Expand Down
26 changes: 23 additions & 3 deletions debug_toolbar/panels/__init__.py
@@ -1,4 +1,5 @@
from django.template.loader import render_to_string
from django.utils.functional import classproperty

from debug_toolbar import settings as dt_settings
from debug_toolbar.utils import get_name_from_obj
Expand All @@ -12,15 +13,22 @@ class Panel:
def __init__(self, toolbar, get_response):
self.toolbar = toolbar
self.get_response = get_response
self.from_store = False

# Private panel properties

@property
def panel_id(self):
return self.__class__.__name__
@classproperty
def panel_id(cls):
return cls.__name__

@property
def enabled(self) -> bool:
if self.from_store:
# If the toolbar was loaded from the store the existence of
# recorded data indicates whether it was enabled or not.
# We can't use the remainder of the logic since we don't have
# a request to work off of.
return bool(self.get_stats())
# The user's cookies should override the default value
cookie_value = self.toolbar.request.COOKIES.get("djdt" + self.panel_id)
if cookie_value is not None:
Expand Down Expand Up @@ -168,6 +176,9 @@ def record_stats(self, stats):
Each call to ``record_stats`` updates the statistics dictionary.
"""
self.toolbar.stats.setdefault(self.panel_id, {}).update(stats)
self.toolbar.store.save_panel(
self.toolbar.request_id, self.panel_id, self.toolbar.stats[self.panel_id]
)

def get_stats(self):
"""
Expand Down Expand Up @@ -251,6 +262,15 @@ def generate_server_timing(self, request, response):
Does not return a value.
"""

def load_stats_from_store(self, data):
"""
Instantiate the panel from serialized data.

Return the panel instance.
"""
self.toolbar.stats.setdefault(self.panel_id, {}).update(data)
self.from_store = True

@classmethod
def run_checks(cls):
"""
Expand Down
8 changes: 5 additions & 3 deletions debug_toolbar/panels/cache.py
Expand Up @@ -169,16 +169,17 @@ def _record_call(self, cache, name, original_method, args, kwargs):

@property
def nav_subtitle(self):
cache_calls = len(self.calls)
stats = self.get_stats()
cache_calls = len(stats.get("calls"))
return ngettext(
"%(cache_calls)d call in %(time).2fms",
"%(cache_calls)d calls in %(time).2fms",
cache_calls,
) % {"cache_calls": cache_calls, "time": self.total_time}
) % {"cache_calls": cache_calls, "time": stats.get("total_time")}

@property
def title(self):
count = len(getattr(settings, "CACHES", ["default"]))
count = self.get_stats().get("total_caches")
return ngettext(
"Cache calls from %(count)d backend",
"Cache calls from %(count)d backends",
Expand Down Expand Up @@ -214,6 +215,7 @@ def generate_stats(self, request, response):
"hits": self.hits,
"misses": self.misses,
"counts": self.counts,
"total_caches": len(getattr(settings, "CACHES", ["default"])),
}
)

Expand Down
2 changes: 1 addition & 1 deletion debug_toolbar/panels/history/__init__.py
@@ -1,3 +1,3 @@
from debug_toolbar.panels.history.panel import HistoryPanel

__all__ = ["HistoryPanel"]
__all__ = [HistoryPanel.panel_id]
4 changes: 2 additions & 2 deletions debug_toolbar/panels/history/forms.py
Expand Up @@ -5,8 +5,8 @@ class HistoryStoreForm(forms.Form):
"""
Validate params

store_id: The key for the store instance to be fetched.
request_id: The key for the store instance to be fetched.
"""

store_id = forms.CharField(widget=forms.HiddenInput())
request_id = forms.CharField(widget=forms.HiddenInput())
exclude_history = forms.BooleanField(widget=forms.HiddenInput(), required=False)
24 changes: 13 additions & 11 deletions debug_toolbar/panels/history/panel.py
Expand Up @@ -23,9 +23,9 @@ class HistoryPanel(Panel):
def get_headers(self, request):
headers = super().get_headers(request)
observe_request = self.toolbar.get_observe_request()
store_id = self.toolbar.store_id
if store_id and observe_request(request):
headers["djdt-store-id"] = store_id
request_id = self.toolbar.request_id
if request_id and observe_request(request):
headers["djdt-request-id"] = request_id
return headers

@property
Expand Down Expand Up @@ -86,23 +86,25 @@ def content(self):

Fetch every store for the toolbar and include it in the template.
"""
stores = {}
for id, toolbar in reversed(self.toolbar._store.items()):
stores[id] = {
"toolbar": toolbar,
toolbar_history = {}
for request_id in reversed(self.toolbar.store.request_ids()):
toolbar_history[request_id] = {
"history_stats": self.toolbar.store.panel(
request_id, HistoryPanel.panel_id
),
"form": HistoryStoreForm(
initial={"store_id": id, "exclude_history": True}
initial={"request_id": request_id, "exclude_history": True}
),
}

return render_to_string(
self.template,
{
"current_store_id": self.toolbar.store_id,
"stores": stores,
"current_request_id": self.toolbar.request_id,
"toolbar_history": toolbar_history,
"refresh_form": HistoryStoreForm(
initial={
"store_id": self.toolbar.store_id,
"request_id": self.toolbar.request_id,
"exclude_history": True,
}
),
Expand Down
32 changes: 18 additions & 14 deletions debug_toolbar/panels/history/views.py
Expand Up @@ -3,6 +3,7 @@

from debug_toolbar.decorators import render_with_toolbar_language, require_show_toolbar
from debug_toolbar.panels.history.forms import HistoryStoreForm
from debug_toolbar.store import get_store
from debug_toolbar.toolbar import DebugToolbar


Expand All @@ -13,12 +14,12 @@ def history_sidebar(request):
form = HistoryStoreForm(request.GET)

if form.is_valid():
store_id = form.cleaned_data["store_id"]
toolbar = DebugToolbar.fetch(store_id)
request_id = form.cleaned_data["request_id"]
toolbar = DebugToolbar.fetch(request_id)
exclude_history = form.cleaned_data["exclude_history"]
context = {}
if toolbar is None:
# When the store_id has been popped already due to
# When the request_id has been popped already due to
# RESULTS_CACHE_SIZE
return JsonResponse(context)
for panel in toolbar.panels:
Expand Down Expand Up @@ -46,23 +47,26 @@ def history_refresh(request):
if form.is_valid():
requests = []
# Convert to list to handle mutations happening in parallel
for id, toolbar in list(DebugToolbar._store.items()):
for request_id in get_store().request_ids():
toolbar = DebugToolbar.fetch(request_id)
requests.append(
{
"id": id,
"id": request_id,
"content": render_to_string(
"debug_toolbar/panels/history_tr.html",
{
"id": id,
"store_context": {
"toolbar": toolbar,
"form": HistoryStoreForm(
initial={
"store_id": id,
"exclude_history": True,
}
),
"request_id": request_id,
"history_context": {
"history_stats": toolbar.store.panel(
request_id, "HistoryPanel"
)
},
"form": HistoryStoreForm(
initial={
"request_id": request_id,
"exclude_history": True,
}
),
},
),
}
Expand Down
19 changes: 17 additions & 2 deletions debug_toolbar/panels/profiling.py
Expand Up @@ -25,6 +25,7 @@ def __init__(
self.id = id
self.parent_ids = parent_ids or []
self.hsv = hsv
self.has_subfuncs = False

def parent_classes(self):
return self.parent_classes
Expand Down Expand Up @@ -128,6 +129,21 @@ def cumtime_per_call(self):
def indent(self):
return 16 * self.depth

def serialize(self):
return {
"has_subfuncs": self.has_subfuncs,
"id": self.id,
"parent_ids": self.parent_ids,
"is_project_func": self.is_project_func(),
"indent": self.indent(),
"func_std_string": self.func_std_string(),
"cumtime": self.cumtime(),
"cumtime_per_call": self.cumtime_per_call(),
"tottime": self.tottime(),
"tottime_per_call": self.tottime_per_call(),
"count": self.count(),
}


class ProfilingPanel(Panel):
"""
Expand All @@ -145,7 +161,6 @@ def process_request(self, request):

def add_node(self, func_list, func, max_depth, cum_time):
func_list.append(func)
func.has_subfuncs = False
if func.depth < max_depth:
for subfunc in func.subfuncs():
# Always include the user's code
Expand Down Expand Up @@ -179,4 +194,4 @@ def generate_stats(self, request, response):
dt_settings.get_config()["PROFILER_MAX_DEPTH"],
cum_time_threshold,
)
self.record_stats({"func_list": func_list})
self.record_stats({"func_list": [func.serialize() for func in func_list]})
15 changes: 12 additions & 3 deletions debug_toolbar/panels/settings.py
@@ -1,4 +1,4 @@
from django.conf import settings
from django.utils.encoding import force_str
from django.utils.translation import gettext_lazy as _
from django.views.debug import get_default_exception_reporter_filter

Expand All @@ -17,7 +17,16 @@ class SettingsPanel(Panel):
nav_title = _("Settings")

def title(self):
return _("Settings from %s") % settings.SETTINGS_MODULE
return _("Settings from %s") % self.get_stats()["settings"].get(
"SETTINGS_MODULE"
)

def generate_stats(self, request, response):
self.record_stats({"settings": dict(sorted(get_safe_settings().items()))})
self.record_stats(
{
"settings": {
key: force_str(value)
for key, value in sorted(get_safe_settings().items())
}
}
)
2 changes: 1 addition & 1 deletion debug_toolbar/panels/sql/__init__.py
@@ -1,3 +1,3 @@
from debug_toolbar.panels.sql.panel import SQLPanel

__all__ = ["SQLPanel"]
__all__ = [SQLPanel.panel_id]