django-devbar 0.5.0__tar.gz → 0.6.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {django_devbar-0.5.0 → django_devbar-0.6.0}/PKG-INFO +7 -6
- {django_devbar-0.5.0 → django_devbar-0.6.0}/README.md +5 -4
- {django_devbar-0.5.0 → django_devbar-0.6.0}/pyproject.toml +2 -2
- {django_devbar-0.5.0 → django_devbar-0.6.0}/src/django_devbar/middleware.py +25 -4
- {django_devbar-0.5.0 → django_devbar-0.6.0}/src/django_devbar/templates/django_devbar/devbar.html +22 -21
- django_devbar-0.6.0/src/django_devbar/templates/django_devbar/duplicates.html +1 -0
- django_devbar-0.6.0/src/django_devbar/templates/django_devbar/similar.html +1 -0
- {django_devbar-0.5.0 → django_devbar-0.6.0}/src/django_devbar/tracker.py +35 -18
- django_devbar-0.5.0/src/django_devbar/templates/django_devbar/duplicates.html +0 -1
- {django_devbar-0.5.0 → django_devbar-0.6.0}/src/django_devbar/__init__.py +0 -0
- {django_devbar-0.5.0 → django_devbar-0.6.0}/src/django_devbar/conf.py +0 -0
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: django-devbar
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Lightweight performance devbar for Django
|
|
5
5
|
Keywords: django,debug,performance,monitoring,development
|
|
6
6
|
Author: amureki
|
|
7
|
-
Author-email: amureki <oss@amureki.
|
|
7
|
+
Author-email: amureki <oss@amureki.me>
|
|
8
8
|
License-Expression: MIT
|
|
9
9
|
Classifier: Programming Language :: Python
|
|
10
10
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -34,7 +34,8 @@ Description-Content-Type: text/markdown
|
|
|
34
34
|
> [!WARNING]
|
|
35
35
|
> This package is experimental and may introduce breaking changes in minor versions.
|
|
36
36
|
|
|
37
|
-
Lightweight performance devbar for Django.
|
|
37
|
+
Lightweight performance devbar for Django.
|
|
38
|
+
Shows DB query count, query duration, application time, and detects similar and duplicate queries with visual severity indicators.
|
|
38
39
|
|
|
39
40
|
## Showcase
|
|
40
41
|
|
|
@@ -42,13 +43,13 @@ Normal operation – showing query count, duration, and application time:
|
|
|
42
43
|
|
|
43
44
|

|
|
44
45
|
|
|
45
|
-
|
|
46
|
+
Similar and duplicate queries detected – severity indicators and details shown:
|
|
46
47
|
|
|
47
48
|

|
|
48
49
|
|
|
49
50
|
[optional] Chrome extension - showing detailed metrics in DevTools panel:
|
|
50
51
|
|
|
51
|
-
[](#chrome-extension)
|
|
52
53
|
|
|
53
54
|
## Motivation
|
|
54
55
|
|
|
@@ -100,7 +101,7 @@ Django DevBar adds HTTP response headers with performance metrics:
|
|
|
100
101
|
|
|
101
102
|
An additional header is included by default in DEBUG mode:
|
|
102
103
|
|
|
103
|
-
- **DevBar-Data** - JSON header with comprehensive metrics including duplicate query details
|
|
104
|
+
- **DevBar-Data** - JSON header with comprehensive metrics including similar and duplicate query details
|
|
104
105
|
|
|
105
106
|
This is useful for:
|
|
106
107
|
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
> [!WARNING]
|
|
4
4
|
> This package is experimental and may introduce breaking changes in minor versions.
|
|
5
5
|
|
|
6
|
-
Lightweight performance devbar for Django.
|
|
6
|
+
Lightweight performance devbar for Django.
|
|
7
|
+
Shows DB query count, query duration, application time, and detects similar and duplicate queries with visual severity indicators.
|
|
7
8
|
|
|
8
9
|
## Showcase
|
|
9
10
|
|
|
@@ -11,13 +12,13 @@ Normal operation – showing query count, duration, and application time:
|
|
|
11
12
|
|
|
12
13
|

|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
Similar and duplicate queries detected – severity indicators and details shown:
|
|
15
16
|
|
|
16
17
|

|
|
17
18
|
|
|
18
19
|
[optional] Chrome extension - showing detailed metrics in DevTools panel:
|
|
19
20
|
|
|
20
|
-
[](#chrome-extension)
|
|
21
22
|
|
|
22
23
|
## Motivation
|
|
23
24
|
|
|
@@ -69,7 +70,7 @@ Django DevBar adds HTTP response headers with performance metrics:
|
|
|
69
70
|
|
|
70
71
|
An additional header is included by default in DEBUG mode:
|
|
71
72
|
|
|
72
|
-
- **DevBar-Data** - JSON header with comprehensive metrics including duplicate query details
|
|
73
|
+
- **DevBar-Data** - JSON header with comprehensive metrics including similar and duplicate query details
|
|
73
74
|
|
|
74
75
|
This is useful for:
|
|
75
76
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "django-devbar"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.6.0"
|
|
4
4
|
description = "Lightweight performance devbar for Django"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
7
7
|
authors = [
|
|
8
|
-
{ name = "amureki", email = "oss@amureki.
|
|
8
|
+
{ name = "amureki", email = "oss@amureki.me" },
|
|
9
9
|
]
|
|
10
10
|
requires-python = ">=3.10"
|
|
11
11
|
dependencies = ["django>=4.2"]
|
|
@@ -75,6 +75,7 @@ class DevBarMiddleware:
|
|
|
75
75
|
"s": truncate_sql(q["sql"]),
|
|
76
76
|
"dur": q["duration"],
|
|
77
77
|
"dup": 1 if q["is_duplicate"] else 0,
|
|
78
|
+
"sim": 1 if q.get("is_similar") else 0,
|
|
78
79
|
}
|
|
79
80
|
for q in all_queries
|
|
80
81
|
]
|
|
@@ -113,6 +114,7 @@ class DevBarMiddleware:
|
|
|
113
114
|
duplicates_html = self._build_duplicates_html(
|
|
114
115
|
stats.get("duplicate_queries", [])
|
|
115
116
|
)
|
|
117
|
+
similar_html = self._build_similar_html(stats.get("similar_queries", []))
|
|
116
118
|
|
|
117
119
|
template = _template_engine.get_template("django_devbar/devbar.html")
|
|
118
120
|
html = template.render(
|
|
@@ -123,6 +125,7 @@ class DevBarMiddleware:
|
|
|
123
125
|
"app_time": stats["python_time"],
|
|
124
126
|
"query_count": stats["count"],
|
|
125
127
|
"duplicates_html": duplicates_html,
|
|
128
|
+
"similar_html": similar_html,
|
|
126
129
|
}
|
|
127
130
|
)
|
|
128
131
|
)
|
|
@@ -133,11 +136,29 @@ class DevBarMiddleware:
|
|
|
133
136
|
response.content = content[:idx] + payload + content[idx:]
|
|
134
137
|
response["Content-Length"] = str(len(response.content))
|
|
135
138
|
|
|
139
|
+
def _deduplicate_queries(self, queries):
|
|
140
|
+
seen_sqls = set()
|
|
141
|
+
unique = []
|
|
142
|
+
for q in queries:
|
|
143
|
+
if q["sql"] not in seen_sqls:
|
|
144
|
+
seen_sqls.add(q["sql"])
|
|
145
|
+
unique.append({**q, "sql": format_sql(q["sql"])})
|
|
146
|
+
return unique
|
|
147
|
+
|
|
136
148
|
def _build_duplicates_html(self, duplicates):
|
|
137
149
|
if not duplicates:
|
|
138
150
|
return ""
|
|
139
|
-
|
|
140
|
-
{**dup, "sql": format_sql(dup["sql"])} for dup in duplicates
|
|
141
|
-
]
|
|
151
|
+
unique = self._deduplicate_queries(duplicates)
|
|
142
152
|
template = _template_engine.get_template("django_devbar/duplicates.html")
|
|
143
|
-
return template.render(
|
|
153
|
+
return template.render(
|
|
154
|
+
Context({"duplicates": unique, "total_count": len(duplicates)})
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
def _build_similar_html(self, similar):
|
|
158
|
+
if not similar:
|
|
159
|
+
return ""
|
|
160
|
+
unique = self._deduplicate_queries(similar)
|
|
161
|
+
template = _template_engine.get_template("django_devbar/similar.html")
|
|
162
|
+
return template.render(
|
|
163
|
+
Context({"similar": unique, "total_count": len(similar)})
|
|
164
|
+
)
|
{django_devbar-0.5.0 → django_devbar-0.6.0}/src/django_devbar/templates/django_devbar/devbar.html
RENAMED
|
@@ -13,44 +13,40 @@
|
|
|
13
13
|
background: rgba(20, 20, 20, 0.92);
|
|
14
14
|
color: #f5f5f5;
|
|
15
15
|
}
|
|
16
|
-
|
|
17
|
-
#django-devbar.has-duplicates { border-left: 3px solid #f59e0b; }
|
|
16
|
+
|
|
18
17
|
#django-devbar span { opacity: 0.7; }
|
|
19
18
|
#django-devbar strong { opacity: 1; font-weight: 600; }
|
|
20
19
|
#django-devbar details { display: inline; position: relative; }
|
|
21
20
|
#django-devbar summary {
|
|
22
21
|
all: revert;
|
|
23
|
-
|
|
22
|
+
font-weight: 600; cursor: pointer; list-style: none;
|
|
24
23
|
}
|
|
24
|
+
#django-devbar .dup-details > summary { color: #f87171; }
|
|
25
|
+
#django-devbar .sim-details > summary { color: #fb923c; }
|
|
25
26
|
#django-devbar summary::-webkit-details-marker { display: none; }
|
|
26
|
-
#django-devbar details[open] > .
|
|
27
|
+
#django-devbar details[open] > .query-list {
|
|
27
28
|
position: absolute; bottom: 100%; right: -8px; margin-bottom: 8px;
|
|
28
29
|
background: rgba(20, 20, 20, 0.92); border-radius: 4px; padding: 6px;
|
|
29
30
|
width: min(500px, 90vw); max-height: 200px; overflow-y: auto; overflow-x: hidden;
|
|
30
31
|
box-sizing: border-box;
|
|
31
32
|
box-shadow: 0 2px 8px rgba(0,0,0,0.15), 0 1px 2px rgba(0,0,0,0.2);
|
|
32
33
|
}
|
|
33
|
-
#django-devbar .
|
|
34
|
+
#django-devbar .query-list details {
|
|
34
35
|
display: block;
|
|
35
36
|
margin: 0 0 6px 0;
|
|
36
37
|
font-size: 11px;
|
|
37
38
|
color: #f5f5f5;
|
|
38
39
|
}
|
|
39
|
-
#django-devbar .
|
|
40
|
-
#django-devbar .
|
|
40
|
+
#django-devbar .query-list details:last-child { margin-bottom: 0; }
|
|
41
|
+
#django-devbar .query-list summary {
|
|
41
42
|
all: revert;
|
|
42
43
|
cursor: pointer;
|
|
43
44
|
list-style: none;
|
|
44
45
|
padding: 6px 8px;
|
|
45
|
-
background: rgba(251, 191, 36, 0.1);
|
|
46
46
|
border-radius: 3px;
|
|
47
|
-
border-left: 2px solid #fbbf24;
|
|
48
47
|
transition: background 0.15s ease;
|
|
49
48
|
}
|
|
50
|
-
#django-devbar .
|
|
51
|
-
background: rgba(251, 191, 36, 0.2);
|
|
52
|
-
}
|
|
53
|
-
#django-devbar .dup-list summary code {
|
|
49
|
+
#django-devbar .query-list summary code {
|
|
54
50
|
white-space: nowrap;
|
|
55
51
|
overflow: hidden;
|
|
56
52
|
text-overflow: ellipsis;
|
|
@@ -58,23 +54,22 @@
|
|
|
58
54
|
max-width: 420px;
|
|
59
55
|
vertical-align: bottom;
|
|
60
56
|
}
|
|
61
|
-
#django-devbar .
|
|
57
|
+
#django-devbar .query-list summary::before {
|
|
62
58
|
content: '▶ ';
|
|
63
59
|
display: inline-block;
|
|
64
60
|
transition: transform 0.15s ease;
|
|
65
|
-
color: #fbbf24;
|
|
66
61
|
font-size: 8px;
|
|
67
62
|
margin-right: 6px;
|
|
68
63
|
}
|
|
69
|
-
#django-devbar .
|
|
64
|
+
#django-devbar .query-list details[open] > summary::before {
|
|
70
65
|
transform: rotate(90deg);
|
|
71
66
|
}
|
|
72
|
-
#django-devbar .
|
|
67
|
+
#django-devbar .query-list code {
|
|
73
68
|
all: revert;
|
|
74
|
-
|
|
69
|
+
font-family: 'SF Mono', Consolas, 'Liberation Mono', Menlo, monospace;
|
|
75
70
|
overflow-wrap: break-word;
|
|
76
71
|
}
|
|
77
|
-
#django-devbar .
|
|
72
|
+
#django-devbar .query-list details[open] > code {
|
|
78
73
|
display: block;
|
|
79
74
|
padding: 8px;
|
|
80
75
|
background: rgba(0, 0, 0, 0.3);
|
|
@@ -84,10 +79,16 @@
|
|
|
84
79
|
line-height: 1.5;
|
|
85
80
|
font-size: 10px;
|
|
86
81
|
}
|
|
82
|
+
#django-devbar .dup-list summary { background: rgba(220, 38, 38, 0.1); border-left: 2px solid #dc2626; }
|
|
83
|
+
#django-devbar .dup-list summary:hover { background: rgba(220, 38, 38, 0.2); }
|
|
84
|
+
#django-devbar .dup-list summary::before, #django-devbar .dup-list code { color: #f87171; }
|
|
85
|
+
#django-devbar .sim-list summary { background: rgba(234, 88, 12, 0.1); border-left: 2px solid #ea580c; }
|
|
86
|
+
#django-devbar .sim-list summary:hover { background: rgba(234, 88, 12, 0.2); }
|
|
87
|
+
#django-devbar .sim-list summary::before, #django-devbar .sim-list code { color: #fb923c; }
|
|
87
88
|
@media (max-width: 640px) { #django-devbar { display: none; } }
|
|
88
89
|
</style>
|
|
89
|
-
<div id="django-devbar"{% if duplicates_html %}
|
|
90
|
+
<div id="django-devbar" class="{% if duplicates_html %}has-duplicates{% endif %}{% if similar_html %} has-similar{% endif %}">
|
|
90
91
|
<span>queries</span> <strong>{{ query_count }}</strong> <span>·</span>
|
|
91
92
|
<span>db</span> <strong>{{ db_time|floatformat:0 }}ms</strong> <span>·</span>
|
|
92
|
-
<span>app</span> <strong>{{ app_time|floatformat:0 }}ms</strong>{{ duplicates_html|safe }}
|
|
93
|
+
<span>app</span> <strong>{{ app_time|floatformat:0 }}ms</strong>{{ duplicates_html|safe }}{{ similar_html|safe }}
|
|
93
94
|
</div>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{% if duplicates %}<details class="dup-details"><summary>({{ total_count }}d)</summary><div class="query-list dup-list">{% for dup in duplicates %}<details><summary><code>{{ dup.sql }}</code></summary><code>{{ dup.sql }}</code></details>{% endfor %}</div></details>{% endif %}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{% if similar %}<details class="sim-details"><summary>({{ total_count }}s)</summary><div class="query-list sim-list">{% for q in similar %}<details><summary><code>{{ q.sql }}</code></summary><code>{{ q.sql }}</code></details>{% endfor %}</div></details>{% endif %}
|
|
@@ -14,31 +14,57 @@ SQL_CLAUSES_RE = re.compile(
|
|
|
14
14
|
|
|
15
15
|
_query_count: ContextVar[int] = ContextVar("query_count", default=0)
|
|
16
16
|
_query_duration: ContextVar[float] = ContextVar("query_duration", default=0.0)
|
|
17
|
-
_seen_queries: ContextVar[dict] = ContextVar("seen_queries", default={})
|
|
18
|
-
_duplicate_log: ContextVar[list] = ContextVar("duplicate_log", default=[])
|
|
19
17
|
_query_log: ContextVar[list] = ContextVar("query_log", default=[])
|
|
20
18
|
|
|
21
19
|
|
|
22
20
|
def reset():
|
|
23
21
|
_query_count.set(0)
|
|
24
22
|
_query_duration.set(0.0)
|
|
25
|
-
_seen_queries.set({})
|
|
26
|
-
_duplicate_log.set([])
|
|
27
23
|
_query_log.set([])
|
|
28
24
|
|
|
29
25
|
|
|
30
26
|
def get_stats():
|
|
31
|
-
duplicate_queries = _duplicate_log.get()
|
|
32
|
-
duplicate_sqls = {d["sql"] for d in duplicate_queries}
|
|
33
|
-
|
|
34
27
|
queries = _query_log.get()
|
|
28
|
+
|
|
29
|
+
# Count occurrences of each SQL template and each (SQL, params_hash) pair
|
|
30
|
+
sql_counts = {}
|
|
31
|
+
duplicate_keys = set()
|
|
32
|
+
for q in queries:
|
|
33
|
+
sql = q["sql"]
|
|
34
|
+
params_hash = q["params_hash"]
|
|
35
|
+
sql_counts[sql] = sql_counts.get(sql, 0) + 1
|
|
36
|
+
dup_key = (sql, params_hash)
|
|
37
|
+
if dup_key in duplicate_keys:
|
|
38
|
+
q["is_duplicate"] = True
|
|
39
|
+
else:
|
|
40
|
+
duplicate_keys.add(dup_key)
|
|
41
|
+
|
|
42
|
+
# Similar = SQL template appears 2+ times (matches DDT semantics)
|
|
43
|
+
similar_sqls = {sql for sql, count in sql_counts.items() if count > 1}
|
|
44
|
+
|
|
45
|
+
# Second pass: mark all queries in groups with 2+ members
|
|
46
|
+
dup_key_counts = {}
|
|
35
47
|
for q in queries:
|
|
36
|
-
q["
|
|
48
|
+
dup_key = (q["sql"], q["params_hash"])
|
|
49
|
+
dup_key_counts[dup_key] = dup_key_counts.get(dup_key, 0) + 1
|
|
50
|
+
|
|
51
|
+
duplicate_keys_final = {k for k, count in dup_key_counts.items() if count > 1}
|
|
52
|
+
|
|
53
|
+
similar_queries = []
|
|
54
|
+
duplicate_queries = []
|
|
55
|
+
for q in queries:
|
|
56
|
+
q["is_similar"] = q["sql"] in similar_sqls
|
|
57
|
+
q["is_duplicate"] = (q["sql"], q["params_hash"]) in duplicate_keys_final
|
|
58
|
+
if q["is_similar"]:
|
|
59
|
+
similar_queries.append(q)
|
|
60
|
+
if q["is_duplicate"]:
|
|
61
|
+
duplicate_queries.append(q)
|
|
37
62
|
|
|
38
63
|
return {
|
|
39
64
|
"count": _query_count.get(),
|
|
40
65
|
"duration": _query_duration.get(),
|
|
41
66
|
"duplicate_queries": duplicate_queries,
|
|
67
|
+
"similar_queries": similar_queries,
|
|
42
68
|
"queries": queries,
|
|
43
69
|
}
|
|
44
70
|
|
|
@@ -54,7 +80,6 @@ def _record(sql, params, duration):
|
|
|
54
80
|
_query_count.set(_query_count.get() + 1)
|
|
55
81
|
_query_duration.set(_query_duration.get() + duration)
|
|
56
82
|
|
|
57
|
-
seen = _seen_queries.get()
|
|
58
83
|
params_hash = _hash_params(params)
|
|
59
84
|
|
|
60
85
|
query_log = _query_log.get()
|
|
@@ -62,19 +87,11 @@ def _record(sql, params, duration):
|
|
|
62
87
|
{
|
|
63
88
|
"sql": sql,
|
|
64
89
|
"duration": round(duration, 2),
|
|
90
|
+
"params_hash": params_hash,
|
|
65
91
|
}
|
|
66
92
|
)
|
|
67
93
|
_query_log.set(query_log)
|
|
68
94
|
|
|
69
|
-
if sql in seen:
|
|
70
|
-
if params_hash in seen[sql]:
|
|
71
|
-
duplicates = _duplicate_log.get()
|
|
72
|
-
duplicates.append({"sql": sql, "duration": round(duration, 2)})
|
|
73
|
-
else:
|
|
74
|
-
seen[sql].add(params_hash)
|
|
75
|
-
else:
|
|
76
|
-
seen[sql] = {params_hash}
|
|
77
|
-
|
|
78
95
|
|
|
79
96
|
def format_sql(sql):
|
|
80
97
|
"""Highlight SQL keywords with HTML spans for visual formatting.
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{% if duplicates %}<details><summary>({{ duplicates|length }}d)</summary><div class="dup-list">{% for dup in duplicates %}<details><summary><code>{{ dup.sql }}</code></summary><code>{{ dup.sql }}</code></details>{% endfor %}</div></details>{% endif %}
|
|
File without changes
|
|
File without changes
|