性能¶
性能分析¶
性能分析是指分析程序的执行过程并测量汇总数据。这些数据可以是每个函数的耗时、执行的 SQL 查询等。
虽然性能分析本身并不能提高程序的性能,但它对于发现性能问题并确定程序中哪些部分导致这些问题非常有帮助。
Odoo 提供了一个集成的性能分析工具,可以记录执行过程中所有的查询和堆栈跟踪。它可以用于分析用户会话的一组请求或特定代码片段。分析结果可以通过集成的 speedscope 开源应用(用于可视化火焰图) 查看,也可以通过将结果保存为 JSON 文件或存储在数据库中后使用自定义工具进行分析。
启用性能分析工具¶
性能分析工具可以通过用户界面启用,这是最简单的方式,但只能分析 Web 请求;或者通过 Python 代码启用,这种方式可以分析任意代码片段,包括测试代码。
启用开发者模式 。
在启动性能分析会话之前,必须在数据库上全局启用性能分析器。这可以通过两种方式完成:
打开 开发者模式工具 ,然后切换 启用性能分析 按钮。向导会建议一组性能分析的有效期时间。单击 启用性能分析 以全局启用性能分析器。
转到 设置 –> 常规设置 –> 性能 并为字段 启用性能分析至 设置所需的时间。
在数据库上启用性能分析器后,用户可以在其会话中启用它。为此,请再次在 开发者模式工具 中切换 启用性能分析 按钮。默认情况下,推荐选项 记录 SQL 和 记录跟踪 已启用。要了解有关不同选项的更多信息,请参阅 收集器 。
启用性能分析器后,所有发送到服务器的请求都会被分析并保存到 ir.profile
记录中。这些记录会被分组到当前性能分析会话中,该会话从性能分析器启用开始,直到禁用为止。
注解
Odoo 在线数据库无法进行性能分析。
手动启动性能分析器可以方便地对特定方法或代码的一部分进行性能分析。该代码可以是测试、计算方法、整个加载过程等。
要从 Python 代码启动性能分析器,请将其作为上下文管理器调用。您可以通过参数指定要记录的内容。对于测试类的性能分析,提供了一个快捷方式:self.profile()
。有关 collectors
参数的更多信息,请参见 收集器 。
Example
with Profiler():
do_stuff()
Example
with Profiler(collectors=['sql', PeriodicCollector(interval=0.1)]):
do_stuff()
Example
with self.profile():
with self.assertQueryCount(__system__=1211):
do_stuff()
注解
性能分析器在 assertQueryCount
外部调用,以便捕获退出上下文管理器时发出的查询(例如,刷新)。
- class odoo.tools.profiler.Profiler[源代码]¶
用作上下文管理器以开始记录某些执行。默认情况下会保存 SQL 和异步堆栈跟踪。
- __init__(collectors=None, db=Ellipsis, profile_session=None, description=None, disable_gc=False, params=None, log=False)[源代码]¶
- 参数
db – 用于保存结果的数据库名称。默认情况下会尝试自动定义数据库。使用值
None
表示不在数据库中保存结果。collectors – 字符串和收集器对象的列表,例如:[‘sql’, PeriodicCollector(interval=0.2)]。使用
None
表示默认收集器。profile_session – 用于将多个分析结果分组的会话描述。使用 make_session(name) 获取默认格式。
description – 当前性能分析器的描述。建议内容:(路由名称/测试方法/加载模块,…)
disable_gc – 在性能分析期间禁用垃圾回收的标志(在性能分析期间避免垃圾回收非常有用,尤其是在执行 SQL 时)
params – 收集器可用的参数(例如帧间隔)
启用性能分析器后,测试方法的所有执行都会被分析并保存到 ir.profile
记录中。这些记录会被分组到一个性能分析会话中。这在使用 @warmup
和 @users
装饰器时特别有用。
小技巧
分析多次调用的方法的性能分析结果可能会很复杂,因为所有调用都会在堆栈跟踪中分组在一起。添加一个**执行上下文**作为上下文管理器,将结果分解为多个帧。
Example
for index in range(max_index):
with ExecutionContext(current_index=index): # Identify each call in speedscope results.
do_stuff()
分析结果¶
要浏览性能分析结果,请确保已在数据库上全局启用了 性能分析工具 ,然后打开 开发者模式工具 并单击性能分析部分右上角的按钮。这将打开按性能分析会话分组的 ir.profile
记录列表视图。

每条记录都有一个可点击的链接,可以在新标签页中打开 speedscope 结果。

Speedscope 超出了本文档的范围,但它提供了许多功能可供尝试:搜索、相似帧高亮、帧缩放、时间线、左重模式、夹层视图等。
根据所激活的性能分析选项,Odoo 会生成不同的视图模式,您可以通过顶部菜单访问它们。

合并 视图显示所有 SQL 查询和跟踪合并在一起的结果。
无上下文合并 视图显示相同的结果,但忽略已保存的执行上下文 <performance/profiling/enable>` 。
“SQL(无间隔)”视图显示所有 SQL 查询,就好像它们是一个接一个执行的,没有任何 Python 逻辑。这对于仅优化 SQL 非常有用。
SQL(密度) 视图仅显示所有 SQL 查询,并在它们之间留有间隔。这有助于发现问题是出在 SQL 还是 Python 代码上,并识别可以批量处理的小查询区域。
“帧”视图仅显示 周期性收集器 的结果。
重要
尽管性能分析工具被设计得尽可能轻量级,但它仍然可能影响性能,尤其是在使用 同步收集器 时。在分析 speedscope 结果时请记住这一点。
收集器¶
如果说性能分析工具关注的是 何时 分析,那么收集器则负责 什么 内容。
每个收集器专门以自己的格式和方式收集性能分析数据。它们可以通过用户界面中的专用切换按钮在 开发者模式工具 中单独启用,也可以通过其键或类从 Python 代码中启用。
目前 Odoo 中有四种收集器可用:
名称 |
切换按钮 |
Python 键 |
Python 类 |
---|---|---|---|
记录 SQL |
|
|
|
记录跟踪 |
|
|
|
记录 QWeb |
|
|
|
否 |
|
|
默认情况下,性能分析工具会启用 SQL 和周期性收集器。无论它是通过用户界面还是 Python 代码启用的。
SQL 收集器¶
SQL 收集器会保存当前线程中(针对所有游标)向数据库发出的所有 SQL 查询以及堆栈跟踪。收集器的开销会添加到每个查询的分析线程中,这意味着在大量小查询中使用它可能会影响执行时间和其他性能分析工具。
它对于调试查询计数或为组合的 speedscope 视图中的 周期性收集器 添加信息特别有用。
周期性收集器¶
该收集器在单独的线程中运行,并以固定的时间间隔保存被分析线程的堆栈跟踪。间隔时间(默认为 10 毫秒)可以通过用户界面中的 间隔 选项或 Python 代码中的 interval
参数定义。
警告
如果间隔设置得过低,分析长时间请求可能会导致内存问题;如果间隔设置得过高,则会丢失短时间函数执行的信息。
这是分析性能的最佳方法之一,因为它在独立线程中运行,对执行时间的影响非常小。
QWeb 收集器¶
该收集器会保存所有指令的 Python 执行时间和查询。与 SQL 收集器 类似,在执行大量小型指令时,其开销可能会较大。与其他收集器相比,它的收集数据有所不同,并且可以通过自定义小部件从 ir.profile
表单视图中进行分析。
它主要用于优化视图。
同步收集器¶
该收集器会保存每个函数调用和返回的堆栈,并在同一线程中运行,这对性能有很大影响。
它对于调试和理解复杂流程并在代码中跟踪其执行非常有用。但由于其开销较高,不建议用于性能分析。
性能陷阱¶
注意随机性问题。多次执行可能会导致不同的结果。例如,垃圾回收器可能在执行过程中被触发。
注意阻塞调用。在某些情况下,外部
c_call
可能在释放 GIL 之前花费一些时间,从而导致使用 周期性收集器 时出现意外的长时间帧。性能分析工具应检测到这种情况并发出警告。如有需要,可以在这些调用之前手动触发性能分析工具。注意缓存的影响。在
视图
/资源
/… 进入缓存之前进行性能分析可能导致不同的结果。注意性能分析工具的开销。当执行大量小查询时,SQL 收集器 的开销可能会较大。性能分析对于发现问题非常实用,但为了测量代码更改的实际影响,您可能需要禁用性能分析工具。
性能分析结果可能会占用大量内存。在某些情况下(例如分析安装或长请求),可能会达到内存限制,尤其是在渲染 speedscope 结果时,这可能导致 HTTP 500 错误。在这种情况下,您可能需要以更高的内存限制启动服务器:
--limit-memory-hard $((8*1024**3))
。
最佳实践¶
批量操作¶
在处理记录集时,几乎总是更优的方式是将操作批量化。
Example
不要在遍历记录集时调用运行 SQL 查询的方法,因为它会对集合中的每条记录都执行一次。
def _compute_count(self):
for record in self:
domain = [('related_id', '=', record.id)]
record.count = other_model.search_count(domain)
相反,可以用 _read_group
替代 search_count
,从而对整个批次的记录执行一条 SQL 查询。
def _compute_count(self):
domain = [('related_id', 'in', self.ids)]
counts_data = other_model._read_group(domain, ['related_id'], ['__count'])
mapped_data = dict(counts_data)
for record in self:
record.count = mapped_data.get(record, 0)
注解
此示例并非在所有情况下都是最优或正确的。它只是 search_count
的替代方案。另一种方法可能是预取并计算反向 One2many
字段。
Example
不要逐一创建记录。
for name in ['foo', 'bar']:
model.create({'name': name})
相反,应累积创建值并对整个批次调用 create
方法。这样做几乎没有影响,并有助于框架优化字段计算。
create_values = []
for name in ['foo', 'bar']:
create_values.append({'name': name})
records = model.create(create_values)
Example
在循环中浏览单个记录时未能预取记录集的字段。
for record_id in record_ids:
model.browse(record_id)
record.foo # One query is executed per record.
相反,应先浏览整个记录集。
records = model.browse(record_ids)
for record in records:
record.foo # One query is executed for the entire recordset.
我们可以通过读取包含每个记录 ID 的字段 prefetch_ids
来验证记录是否以批次方式预取。单独浏览所有记录是不切实际的,
如果需要,可以使用 with_prefetch
方法禁用批量预取:
for values in values_list:
message = self.browse(values['id']).with_prefetch(self.ids)
降低算法复杂度¶
算法复杂度是衡量算法完成所需时间相对于输入大小 n
的指标。当复杂度较高时,随着输入规模增大,执行时间可能会迅速增加。在某些情况下,通过正确准备输入数据可以降低算法复杂度。
Example
对于给定的问题,考虑一个由两个嵌套循环构成的朴素算法,其复杂度为 O(n²)。
for record in self:
for result in results:
if results['id'] == record.id:
record.foo = results['foo']
break
假设所有结果都有不同的 ID,我们可以准备数据以降低复杂度。
mapped_result = {result['id']: result['foo'] for result in results}
for record in self:
record.foo = mapped_result.get(record.id)
Example
选择错误的数据结构来存储输入可能导致二次复杂度。
invalid_ids = self.search(domain).ids
for record in self:
if record.id in invalid_ids:
...
如果 invalid_ids
是类似列表的数据结构,算法的复杂度可能是二次的。
相反,更倾向于使用集合操作,例如将 invalid_ids
转换为集合。
invalid_ids = set(invalid_ids)
for record in self:
if record.id in invalid_ids:
...
根据输入的不同,也可以使用记录集操作。
invalid_ids = self.search(domain)
for record in self - invalid_ids:
...
使用索引¶
数据库索引可以帮助加速搜索操作,无论是在后端搜索还是通过用户界面进行搜索。
name = fields.Char(string="Name", index=True)
警告
注意不要为每个字段都创建索引,因为索引会占用存储空间,并在执行 INSERT
、 UPDATE
和 DELETE
操作时对性能产生影响。