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

Support serializable toolbar #1432

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ package-lock.json: package.json
touch $@

test:
DJANGO_SETTINGS_MODULE=tests.settings \
DB_BACKEND=sqlite3 DB_NAME=":memory:" DJANGO_SETTINGS_MODULE=tests.settings \
python -m django test $${TEST_ARGS:-tests}

test_selenium:
Expand Down
50 changes: 50 additions & 0 deletions debug_toolbar/db_store.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
from debug_toolbar import store
from debug_toolbar.models import PanelStore, ToolbarStore


class DBStore(store.BaseStore):
@classmethod
def ids(cls):
return (
ToolbarStore.objects.using("debug_toolbar")
.values_list("key", flat=True)
.order_by("created")
)

@classmethod
def exists(cls, store_id):
return ToolbarStore.objects.using("debug_toolbar").filter(key=store_id).exists()

@classmethod
def set(cls, store_id):
_, created = ToolbarStore.objects.using("debug_toolbar").get_or_create(
key=store_id
)
if (
created
and ToolbarStore.objects.using("debug_toolbar").all().count()
> cls.config["RESULTS_CACHE_SIZE"]
):
ToolbarStore.objects.using("debug_toolbar").earliest("created").delete()

@classmethod
def delete(cls, store_id):
ToolbarStore.objects.using("debug_toolbar").filter(key=store_id).delete()

@classmethod
def save_panel(cls, store_id, panel_id, stats=None):
toolbar, _ = ToolbarStore.objects.using("debug_toolbar").get_or_create(
key=store_id
)
toolbar.panelstore_set.update_or_create(
panel=panel_id, defaults={"data": store.serialize(stats)}
)

@classmethod
def panel(cls, store_id, panel_id):
panel = (
PanelStore.objects.using("debug_toolbar")
.filter(toolbar__key=store_id, panel=panel_id)
.first()
)
return {} if not panel else store.deserialize(panel.data)
58 changes: 58 additions & 0 deletions debug_toolbar/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Generated by Django 3.1.5 on 2021-01-09 17:02
import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):
initial = True

dependencies = []

operations = [
migrations.CreateModel(
name="ToolbarStore",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("created", models.DateTimeField(auto_now_add=True)),
("key", models.CharField(max_length=64, unique=True)),
],
),
migrations.CreateModel(
name="PanelStore",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("created", models.DateTimeField(auto_now_add=True)),
("panel", models.CharField(max_length=128)),
("data", models.TextField()),
(
"toolbar",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="debug_toolbar.toolbarstore",
),
),
],
),
migrations.AddConstraint(
model_name="panelstore",
constraint=models.UniqueConstraint(
fields=("toolbar", "panel"), name="unique_toolbar_panel"
),
),
]
Empty file.
13 changes: 13 additions & 0 deletions debug_toolbar/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from django.db import models


class ToolbarStore(models.Model):
created = models.DateTimeField(auto_now_add=True)
key = models.CharField(max_length=64, unique=True)


class PanelStore(models.Model):
created = models.DateTimeField(auto_now_add=True)
toolbar = models.ForeignKey(ToolbarStore, on_delete=models.CASCADE)
panel = models.CharField(max_length=128)
data = models.TextField()
34 changes: 32 additions & 2 deletions debug_toolbar/panels/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from django.template.loader import render_to_string

from debug_toolbar import settings as dt_settings
from debug_toolbar.store import get_store
from debug_toolbar.utils import get_name_from_obj


Expand Down Expand Up @@ -37,7 +38,13 @@ def enabled(self):
else:
default = "on"
# The user's cookies should override the default value
return self.toolbar.request.COOKIES.get("djdt" + self.panel_id, default) == "on"
if self.toolbar.request is not None:
return (
self.toolbar.request.COOKIES.get("djdt" + self.panel_id, default)
== "on"
)
else:
return bool(get_store().panel(self.toolbar.store_id, self.panel_id))

# Titles and content

Expand Down Expand Up @@ -150,19 +157,42 @@ def disable_instrumentation(self):

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

@classmethod
def deserialize_stats(cls, data):
"""
Deserialize stats coming from the store.

Provided to support future store mechanisms overriding a panel's content.
"""
return data

@classmethod
def serialize_stats(cls, stats):
"""
Serialize stats for the store.

Provided to support future store mechanisms overriding a panel's content.
"""
return stats

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are these two methods complete?

  1. deserialize_stats
  2. serialize_stats


def record_stats(self, stats):
"""
Store data gathered by the panel. ``stats`` is a :class:`dict`.

Each call to ``record_stats`` updates the statistics dictionary.
"""
self.toolbar.stats.setdefault(self.panel_id, {}).update(stats)
get_store().save_panel(
self.toolbar.store_id, self.panel_id, self.serialize_stats(stats)
)

def get_stats(self):
"""
Access data stored by the panel. Returns a :class:`dict`.
"""
return self.toolbar.stats.get(self.panel_id, {})
return self.deserialize_stats(
get_store().panel(self.toolbar.store_id, self.panel_id)
)

def record_server_timing(self, key, title, value):
"""
Expand Down
7 changes: 4 additions & 3 deletions debug_toolbar/panels/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ def _store_call_info(
"kwargs": kwargs,
"trace": render_stacktrace(trace),
"template_info": template_info,
"backend": backend,
"backend": str(backend),
}
)

Expand All @@ -246,14 +246,15 @@ def _store_call_info(

@property
def nav_subtitle(self):
cache_calls = len(self.calls)
stats = self.get_stats()
cache_calls = len(stats["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["total_time"]}
)

@property
Expand Down
27 changes: 15 additions & 12 deletions debug_toolbar/panels/history/panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from debug_toolbar.panels import Panel
from debug_toolbar.panels.history import views
from debug_toolbar.panels.history.forms import HistoryStoreForm
from debug_toolbar.store import get_store


class HistoryPanel(Panel):
Expand Down Expand Up @@ -46,9 +47,9 @@ def nav_subtitle(self):
def generate_stats(self, request, response):
try:
if request.method == "GET":
data = request.GET.copy()
data = dict(request.GET.copy())
else:
data = request.POST.copy()
data = dict(request.POST.copy())
# GraphQL tends to not be populated in POST. If the request seems
# empty, check if it's a JSON request.
if (
Expand Down Expand Up @@ -80,23 +81,25 @@ def content(self):

Fetch every store for the toolbar and include it in the template.
"""
stores = OrderedDict()
for id, toolbar in reversed(self.toolbar._store.items()):
stores[id] = {
"toolbar": toolbar,
"form": SignedDataForm(
initial=HistoryStoreForm(initial={"store_id": id}).initial
),
}
histories = OrderedDict()
for id in reversed(get_store().ids()):
stats = self.deserialize_stats(get_store().panel(id, self.panel_id))
if stats:
histories[id] = {
"stats": stats,
"form": SignedDataForm(
initial=HistoryStoreForm(initial={"store_id": str(id)}).initial
),
}

return render_to_string(
self.template,
{
"current_store_id": self.toolbar.store_id,
"stores": stores,
"histories": histories,
"refresh_form": SignedDataForm(
initial=HistoryStoreForm(
initial={"store_id": self.toolbar.store_id}
initial={"store_id": str(self.toolbar.store_id)}
).initial
),
},
Expand Down
17 changes: 11 additions & 6 deletions debug_toolbar/panels/history/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
from debug_toolbar.decorators import require_show_toolbar, signed_data_view
from debug_toolbar.forms import SignedDataForm
from debug_toolbar.panels.history.forms import HistoryStoreForm
from debug_toolbar.toolbar import DebugToolbar
from debug_toolbar.store import get_store
from debug_toolbar.toolbar import stats_only_toolbar


@require_show_toolbar
Expand All @@ -15,7 +16,8 @@ def history_sidebar(request, verified_data):

if form.is_valid():
store_id = form.cleaned_data["store_id"]
toolbar = DebugToolbar.fetch(store_id)
toolbar = stats_only_toolbar(store_id)

context = {}
if toolbar is None:
# When the store_id has been popped already due to
Expand All @@ -25,6 +27,7 @@ def history_sidebar(request, verified_data):
if not panel.is_historical:
continue
panel_context = {"panel": panel}

context[panel.panel_id] = {
"button": render_to_string(
"debug_toolbar/includes/panel_button.html", panel_context
Expand All @@ -45,17 +48,19 @@ def history_refresh(request, verified_data):

if form.is_valid():
requests = []
# Convert to list to handle mutations happenening in parallel
for id, toolbar in list(DebugToolbar._store.items())[::-1]:
for id in reversed(get_store().ids()):
toolbar = stats_only_toolbar(id)
requests.append(
{
"id": id,
"content": render_to_string(
"debug_toolbar/panels/history_tr.html",
{
"id": id,
"store_context": {
"toolbar": toolbar,
"history": {
"stats": toolbar.get_panel_by_id(
"HistoryPanel"
).get_stats(),
"form": SignedDataForm(
initial=HistoryStoreForm(
initial={"store_id": id}
Expand Down
18 changes: 16 additions & 2 deletions debug_toolbar/panels/profiling.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ def __init__(
self.id = id
self.parent_ids = parent_ids
self.hsv = hsv
self.has_subfuncs = False

def parent_classes(self):
return self.parent_classes
Expand Down Expand Up @@ -141,6 +142,20 @@ def cumtime_per_call(self):
def indent(self):
return 16 * self.depth

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


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

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:
Expand All @@ -183,4 +197,4 @@ def generate_stats(self, request, response):
dt_settings.get_config()["PROFILER_MAX_DEPTH"],
root.stats[3] / 8,
)
self.record_stats({"func_list": func_list})
self.record_stats({"func_list": [func.as_context() for func in func_list]})
Loading