Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
tim-schilling committed Sep 11, 2021
1 parent 54ff730 commit 3fb2745
Show file tree
Hide file tree
Showing 20 changed files with 208 additions and 44 deletions.
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()
10 changes: 6 additions & 4 deletions debug_toolbar/panels/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from django.template.loader import render_to_string

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


Expand Down Expand Up @@ -44,7 +44,7 @@ def enabled(self):
== "on"
)
else:
return bool(store.panel(self.toolbar.store_id, self.panel_id))
return bool(get_store().panel(self.toolbar.store_id, self.panel_id))

# Titles and content

Expand Down Expand Up @@ -182,15 +182,17 @@ def record_stats(self, stats):
Each call to ``record_stats`` updates the statistics dictionary.
"""
self.toolbar.stats.setdefault(self.panel_id, {}).update(stats)
store.save_panel(
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.deserialize_stats(store.panel(self.toolbar.store_id, 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
5 changes: 3 additions & 2 deletions debug_toolbar/panels/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
18 changes: 10 additions & 8 deletions debug_toolbar/panels/history/panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +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 store
from debug_toolbar.store import get_store


class HistoryPanel(Panel):
Expand Down Expand Up @@ -82,13 +82,15 @@ def content(self):
Fetch every store for the toolbar and include it in the template.
"""
histories = OrderedDict()
for id in reversed(store.ids()):
histories[id] = {
"stats": self.deserialize_stats(store.panel(id, self.panel_id)),
"form": SignedDataForm(
initial=HistoryStoreForm(initial={"store_id": id}).initial
),
}
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,
Expand Down
4 changes: 2 additions & 2 deletions debug_toolbar/panels/history/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
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.store import store
from debug_toolbar.store import get_store
from debug_toolbar.toolbar import stats_only_toolbar


Expand Down Expand Up @@ -48,7 +48,7 @@ def history_refresh(request, verified_data):

if form.is_valid():
requests = []
for id in reversed(store.ids()):
for id in reversed(get_store().ids()):
toolbar = stats_only_toolbar(id)
requests.append(
{
Expand Down
14 changes: 10 additions & 4 deletions debug_toolbar/panels/sql/panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,18 +110,20 @@ def record(self, alias, **kwargs):

@property
def nav_subtitle(self):
stats = self.get_stats()
num_queries = len(stats["queries"])
return ngettext(
"%(query_count)d query in %(sql_time).2fms",
"%(query_count)d queries in %(sql_time).2fms",
self._num_queries,
num_queries,
) % {
"query_count": self._num_queries,
"sql_time": self._sql_time,
"query_count": num_queries,
"sql_time": stats["sql_time"],
}

@property
def title(self):
count = len(self._databases)
count = len(self.get_stats()["databases"])
return (
ngettext(
"SQL queries from %(count)d connection",
Expand All @@ -144,10 +146,14 @@ def get_urls(cls):
def enable_instrumentation(self):
# This is thread-safe because database connections are thread-local.
for connection in connections.all():
if connection.alias == "debug_toolbar":
continue
wrap_cursor(connection, self)

def disable_instrumentation(self):
for connection in connections.all():
if connection.alias == "debug_toolbar":
continue
unwrap_cursor(connection)

def generate_stats(self, request, response):
Expand Down
7 changes: 4 additions & 3 deletions debug_toolbar/panels/staticfiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,10 @@ class StaticFilesPanel(panels.Panel):

@property
def title(self):
stats = self.get_stats()
return _("Static files (%(num_found)s found, %(num_used)s used)") % {
"num_found": self.num_found,
"num_used": self.num_used,
"num_found": stats["num_found"],
"num_used": stats["num_used"],
}

def __init__(self, *args, **kwargs):
Expand All @@ -107,7 +108,7 @@ def num_used(self):

@property
def nav_subtitle(self):
num_used = self.num_used
num_used = self.get_stats()["num_used"]
return ngettext(
"%(num_used)s file used", "%(num_used)s files used", num_used
) % {"num_used": num_used}
Expand Down
6 changes: 2 additions & 4 deletions debug_toolbar/panels/timer.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,9 @@ class TimerPanel(Panel):

def nav_subtitle(self):
stats = self.get_stats()
if hasattr(self, "_start_rusage"):
utime = self._end_rusage.ru_utime - self._start_rusage.ru_utime
stime = self._end_rusage.ru_stime - self._start_rusage.ru_stime
if stats.get("utime"):
return _("CPU: %(cum)0.2fms (%(total)0.2fms)") % {
"cum": (utime + stime) * 1000.0,
"cum": stats["utime"],
"total": stats["total_time"],
}
elif "total_time" in stats:
Expand Down
5 changes: 3 additions & 2 deletions debug_toolbar/store.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def exists(cls, store_id):
def set(cls, store_id):
if store_id not in cls._ids:
cls._ids.append(store_id)
if len(cls._ids) > cls.config["RESULTS_CACHE_SIZE"]:
for _ in range(len(cls._ids) - cls.config["RESULTS_CACHE_SIZE"]):
cls.delete(cls._ids[0])

@classmethod
Expand All @@ -86,4 +86,5 @@ def panel(cls, store_id, panel_id):
return {} if data is None else deserialize(data)


store = import_string(dt_settings.get_config()["TOOLBAR_STORE_CLASS"])
def get_store():
return import_string(dt_settings.get_config()["TOOLBAR_STORE_CLASS"])
4 changes: 2 additions & 2 deletions debug_toolbar/toolbar.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from django.utils.module_loading import import_string

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


class DebugToolbar:
Expand Down Expand Up @@ -89,7 +89,7 @@ def should_render_panels(self):
return render_panels

def store(self):
store.set(self.store_id)
get_store().set(self.store_id)

# Manually implement class-level caching of panel classes and url patterns
# because it's more obvious than going through an abstraction.
Expand Down
4 changes: 2 additions & 2 deletions debug_toolbar/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@
from django.utils.translation import gettext as _

from debug_toolbar.decorators import require_show_toolbar
from debug_toolbar.store import store
from debug_toolbar.store import get_store
from debug_toolbar.toolbar import stats_only_toolbar


@require_show_toolbar
def render_panel(request):
"""Render the contents of a panel"""
store_id = request.GET["store_id"]
if not store.exists(store_id):
if not get_store().exists(store_id):
content = _(
"Data for this panel isn't available anymore. "
"Please reload the page and retry."
Expand Down
9 changes: 6 additions & 3 deletions tests/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@
from django.http import HttpResponse
from django.test import RequestFactory, TestCase

from debug_toolbar.store import store
from debug_toolbar.store import get_store
from debug_toolbar.toolbar import DebugToolbar

rf = RequestFactory()


class BaseTestCase(TestCase):
databases = {"default", "debug_toolbar"}
panel_id = None

def setUp(self):
Expand Down Expand Up @@ -49,10 +50,12 @@ def assertValidHTML(self, content, msg=None):
class IntegrationTestCase(TestCase):
"""Base TestCase for tests involving clients making requests."""

databases = {"default", "debug_toolbar"}

def setUp(self):
# The HistoryPanel keeps track of previous stores in memory.
# This bleeds into other tests and violates their idempotency.
# Clear the store before each test.
for key in list(store.ids()):
store.delete(key)
for key in list(get_store().ids()):
get_store().delete(key)
super().setUp()
Loading

0 comments on commit 3fb2745

Please sign in to comment.