性能

性能分析

性能分析是指分析程序的执行过程并测量汇总数据。这些数据可以是每个函数的耗时、执行的 SQL 查询等。

虽然性能分析本身并不能提高程序的性能,但它对于发现性能问题并确定程序中哪些部分导致这些问题非常有帮助。

Odoo 提供了一个集成的性能分析工具,可以记录执行过程中所有的查询和堆栈跟踪。它可以用于分析用户会话的一组请求或特定代码片段。分析结果可以通过集成的 speedscope 开源应用(用于可视化火焰图) 查看,也可以通过将结果保存为 JSON 文件或存储在数据库中后使用自定义工具进行分析。

启用性能分析工具

性能分析工具可以通过用户界面启用,这是最简单的方式,但只能分析 Web 请求;或者通过 Python 代码启用,这种方式可以分析任意代码片段,包括测试代码。

  1. 启用开发者模式

  2. 在启动性能分析会话之前,必须在数据库上全局启用性能分析器。这可以通过两种方式完成:

    • 打开 开发者模式工具 ,然后切换 启用性能分析 按钮。向导会建议一组性能分析的有效期时间。单击 启用性能分析 以全局启用性能分析器。

      ../../../_images/enable_profiling_wizard.png
    • 转到 设置 –> 常规设置 –> 性能 并为字段 启用性能分析至 设置所需的时间。

  3. 在数据库上启用性能分析器后,用户可以在其会话中启用它。为此,请再次在 开发者模式工具 中切换 启用性能分析 按钮。默认情况下,推荐选项 记录 SQL记录跟踪 已启用。要了解有关不同选项的更多信息,请参阅 收集器

    ../../../_images/profiling_debug_menu.png

启用性能分析器后,所有发送到服务器的请求都会被分析并保存到 ir.profile 记录中。这些记录会被分组到当前性能分析会话中,该会话从性能分析器启用开始,直到禁用为止。

注解

Odoo 在线数据库无法进行性能分析。

分析结果

要浏览性能分析结果,请确保已在数据库上全局启用了 性能分析工具 ,然后打开 开发者模式工具 并单击性能分析部分右上角的按钮。这将打开按性能分析会话分组的 ir.profile 记录列表视图。

../../../_images/profiling_web.png

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

../../../_images/flamegraph_example.png

Speedscope 超出了本文档的范围,但它提供了许多功能可供尝试:搜索、相似帧高亮、帧缩放、时间线、左重模式、夹层视图等。

根据所激活的性能分析选项,Odoo 会生成不同的视图模式,您可以通过顶部菜单访问它们。

../../../_images/speedscope_modes.png
  • 合并 视图显示所有 SQL 查询和跟踪合并在一起的结果。

  • 无上下文合并 视图显示相同的结果,但忽略已保存的执行上下文 <performance/profiling/enable>` 。

  • “SQL(无间隔)”视图显示所有 SQL 查询,就好像它们是一个接一个执行的,没有任何 Python 逻辑。这对于仅优化 SQL 非常有用。

  • SQL(密度) 视图仅显示所有 SQL 查询,并在它们之间留有间隔。这有助于发现问题是出在 SQL 还是 Python 代码上,并识别可以批量处理的小查询区域。

  • “帧”视图仅显示 周期性收集器 的结果。

重要

尽管性能分析工具被设计得尽可能轻量级,但它仍然可能影响性能,尤其是在使用 同步收集器 时。在分析 speedscope 结果时请记住这一点。

收集器

如果说性能分析工具关注的是 何时 分析,那么收集器则负责 什么 内容。

每个收集器专门以自己的格式和方式收集性能分析数据。它们可以通过用户界面中的专用切换按钮在 开发者模式工具 中单独启用,也可以通过其键或类从 Python 代码中启用。

目前 Odoo 中有四种收集器可用:

名称

切换按钮

Python 键

Python 类

SQL 收集器

记录 SQL

sql

SqlCollector

周期性收集器

记录跟踪

traces_async

PeriodicCollector

QWeb 收集器

记录 QWeb

qweb

QwebCollector

同步收集器

traces_sync

SyncCollector

默认情况下,性能分析工具会启用 SQL 和周期性收集器。无论它是通过用户界面还是 Python 代码启用的。

SQL 收集器

SQL 收集器会保存当前线程中(针对所有游标)向数据库发出的所有 SQL 查询以及堆栈跟踪。收集器的开销会添加到每个查询的分析线程中,这意味着在大量小查询中使用它可能会影响执行时间和其他性能分析工具。

它对于调试查询计数或为组合的 speedscope 视图中的 周期性收集器 添加信息特别有用。

class odoo.tools.profiler.SQLCollector[源代码]

保存当前线程中执行的所有查询及其调用栈。

周期性收集器

该收集器在单独的线程中运行,并以固定的时间间隔保存被分析线程的堆栈跟踪。间隔时间(默认为 10 毫秒)可以通过用户界面中的 间隔 选项或 Python 代码中的 interval 参数定义。

警告

如果间隔设置得过低,分析长时间请求可能会导致内存问题;如果间隔设置得过高,则会丢失短时间函数执行的信息。

这是分析性能的最佳方法之一,因为它在独立线程中运行,对执行时间的影响非常小。

class odoo.tools.profiler.PeriodicCollector(interval=0.01)[源代码]

每隔最多 interval 秒异步记录执行帧。

参数

(float) (interval) – 两次采样之间等待的时间(以秒为单位)。

QWeb 收集器

该收集器会保存所有指令的 Python 执行时间和查询。与 SQL 收集器 类似,在执行大量小型指令时,其开销可能会较大。与其他收集器相比,它的收集数据有所不同,并且可以通过自定义小部件从 ir.profile 表单视图中进行分析。

它主要用于优化视图。

class odoo.tools.profiler.QwebCollector[源代码]

记录带有指令跟踪的 QWeb 执行。

同步收集器

该收集器会保存每个函数调用和返回的堆栈,并在同一线程中运行,这对性能有很大影响。

它对于调试和理解复杂流程并在代码中跟踪其执行非常有用。但由于其开销较高,不建议用于性能分析。

class odoo.tools.profiler.SyncCollector[源代码]

同步记录完整执行过程。注意,启动 Odoo 时可能需要增加 –limit-memory-hard 参数值。

性能陷阱

  • 注意随机性问题。多次执行可能会导致不同的结果。例如,垃圾回收器可能在执行过程中被触发。

  • 注意阻塞调用。在某些情况下,外部 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)

警告

注意不要为每个字段都创建索引,因为索引会占用存储空间,并在执行 INSERTUPDATEDELETE 操作时对性能产生影响。