Django视图函数性能分析(续)

前言

django.png

以前的文章中,我介绍了两种分析Django视图函数性能的工具:Django Debug ToolbarcProfile中间件。在实际开发过程中,前者的SQL执行分析是相当实用的功能。

但美中不足的是,Django Debug Toolbar只能在浏览器里分析响应类型为HTML的视图函数,这就导致响应类型为JSON的视图函数必须要借助Django Rest Framework框架中的HTMLRenderer等手段修改自身的响应类型,否则无法使用该工具。

那有没有更直接的手段,来为JSON视图函数分析SQL执行情况呢?这就是文本要探究的问题。

注:本文所使用的运行环境为Python 3.6+Django 2.2

Django中的SQL执行

在查阅了相关文档后,我发现事情比我想象中的简单。在Django官方文档中的FAQ: Databases and models有这样的描述:

document.webp

如此一来方法就很清晰了:写一个中间件,将connection.queries里的内容插入到JSON响应中。中间件代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# coding:utf-8
import json

from django.conf import settings
from django.core.exceptions import MiddlewareNotUsed
from django.db import connections
from django.http.response import HttpResponse


class ProfileMiddleware(object):
ignore_sql = []

def __init__(self, get_response):
if not settings.DEBUG:
raise MiddlewareNotUsed # 只在DEBUG模式下启用该中间件

self.get_response = get_response

def __call__(self, request):
response = self.get_response(request) # type: HttpResponse
# 根据url参数和返回类型判断是否应该插入SQL执行信息
if request.GET.get("profile") == "db" and "application/json" in repr(response):
try:
data = json.loads(response.content)
except (TypeError, ValueError):
return response # 解析json失败
# 插入sql执行信息
sql_queries = []
for alias in connections:
for query in connections[alias].queries:
if query["sql"] not in self.ignore_sql:
query["using"] = alias
sql_queries.append(query)
if isinstance(data, dict):
data["_debug_sql"] = sql_queries
elif isinstance(data, list):
data.append({"_debug_sql": sql_queries})
response.content = json.dumps(data)

return response

写个简单视图函数来验证一下:

1
2
3
4
5
6
7
8
#!/usr/bin/env python3
from django.contrib.auth.models import User
from django.http import JsonResponse


def user_count(request):
data = {"count": User.objects.count()}
return JsonResponse(data)
1
2
3
4
5
6
7
8
9
10
11
$ curl -s localhost:8000/user_count/\?profile=db | python3 -m json.tool
{
"count": 0,
"_debug_sql": [
{
"sql": "SELECT COUNT(*) AS \"__count\" FROM \"auth_user\"",
"time": "0.000",
"using": "default"
}
]
}

和cProfile中间件组合

以前的文章中我也介绍了使用cProfile中间件来分析Django视图函数的Python代码执行情况,这里就顺便组合起来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# coding:utf-8
import cProfile
import json
import pstats
from io import StringIO

from django.conf import settings
from django.core.exceptions import MiddlewareNotUsed
from django.db import connections
from django.http.response import HttpResponse


class ProfileMiddleware(object):
ignore_sql = []

def __init__(self, get_response):
if not settings.DEBUG:
raise MiddlewareNotUsed # 只在DEBUG模式下启用该中间件

self.get_response = get_response

def __call__(self, request):
if request.GET.get("profile") == "cprofile":
# 用cProfile的结果覆盖原始响应
profile = cProfile.Profile()
profile.enable()
self.get_response(request)
profile.disable()
ram_file = StringIO()
sort_by = 'tottime'
stats = pstats.Stats(profile, stream=ram_file)
stats.strip_dirs().sort_stats(sort_by).print_stats()
return HttpResponse(ram_file.getvalue().encode('utf-8'), 'text/plain')

response = self.get_response(request) # type: HttpResponse
# 根据url参数和返回类型判断是否应该插入SQL执行信息
if request.GET.get("profile") == "db" and "application/json" in repr(response):
try:
data = json.loads(response.content)
except (TypeError, ValueError):
return response # 解析json失败
# 插入sql执行信息
sql_queries = []
for alias in connections:
for query in connections[alias].queries:
if query["sql"] not in self.ignore_sql:
query["using"] = alias
sql_queries.append(query)
data["_debug_sql"] = sql_queries
response.content = json.dumps(data)

return response

同样验证一下:

1
2
3
4
5
6
7
8
9
10
11
12
$ curl -s localhost:8000/user_count/\?profile=cprofile
830 function calls (826 primitive calls) in 0.002 seconds

Ordered by: internal time

ncalls tottime percall cumtime percall filename:lineno(function)
1 0.000 0.000 0.000 0.000 base.py:379(execute)
1 0.000 0.000 0.000 0.000 {built-in method _sqlite3.connect}
4 0.000 0.000 0.000 0.000 base.py:53(list_aggregate)
20 0.000 0.000 0.000 0.000 functools.py:44(update_wrapper)
158 0.000 0.000 0.000 0.000 {built-in method builtins.getattr}
# 省略

总结

不得不感叹Django真是一个功能完善、生态丰富的Web框架,这篇文章只是站在巨人的肩膀上取得了一些微不足道的成就。Django Debug Toolbar虽然也是通过connection.queries的方式获取了SQL执行情况(panel.py#L58),但其所做的工作远不止如此,值得细看。


Django视图函数性能分析(续)
https://www.yooo.ltd/2020/04/19/Django视图函数性能分析(续)/
作者
OrangeWolf
发布于
2020年4月19日
许可协议