表现

分析

分析是关于分析程序的执行并测量聚合数据。这些数据可以是每个函数的运行时间、执行的 SQL 查询……

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

Odoo 提供了一个集成的分析工具,允许在执行期间记录所有执行的查询和堆栈跟踪。它可用于分析用户会话的一组请求或代码的特定部分。分析结果可以使用集成的 speedscope open source app allowing to visualize a flamegraph 视图进行检查,也可以使用自定义工具进行分析,方法是首先将其保存在 JSON 文件或数据库中。

启用分析器

探查器可以从用户界面启用,这是最简单的方法,但只允许分析 Web 请求,也可以从 Python 代码启用,它允许分析任何代码段,包括测试。

  1. Enable the developer mode

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

    • 打开 developer mode tools,然后切换 Enable profiling 按钮。向导会建议一组分析的到期时间。单击 ENABLE PROFILING 以全局启用探查器。

      ../../../_images/enable_profiling_wizard.png
    • 转至 Settings –> General Settings –> Performance 并在字段 Enable profiling until 中设置所需时间。

  3. 在数据库上启用探查器后,用户可以在其会话上启用它。为此,请再次切换 developer mode tools 中的 Enable profiling 按钮。默认情况下,启用推荐选项 Record sqlRecord traces。要了解有关不同选项的更多信息,请前往:ref:performance/profiling/collectors

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

启用分析器后,将对服务器发出的所有请求进行分析并保存到 ir.profile 记录中。此类记录被分组到当前分析会话中,该会话从启用分析器到禁用分析器。

注解

无法分析 Odoo Online 数据库。

分析结果

要浏览分析结果,请确保 profiler is enabled globally on the database,然后打开 developer mode tools 并单击分析部分右上角的按钮。将打开按分析会话分组的 ir.profile 记录的列表视图。

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

每条记录都有一个可点击的链接,可在新选项卡中打开 speedscope 结果。

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

Speedscope 不属于本文档的范围,但有很多工具可以尝试:搜索、突出显示相似帧、缩放帧、时间轴、左重、三明治视图…

根据激活的分析选项,Odoo 会生成不同的视图模式,您可以从顶部菜单访问这些模式。

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

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

  • sql (no gap) 视图显示所有 SQL 查询,就好像它们是一个接一个执行的,没有任何 Python 逻辑。这仅对优化 SQL 有用。

  • sql (density) 视图仅显示所有 SQL 查询,在它们之间留有间隙。这对于发现问题是否是 SQL 或 Python 代码以及识别可以批处理许多小型查询的区域非常有用。

  • frames 视图仅显示 periodic collector 的结果。

重要

尽管探查器被设计得尽可能轻,但它仍然会影响性能,尤其是在使用 Sync collector 时。分析 Speedscope 结果时请记住这一点。

收藏家

分析器关注的是分析的“何时”,而收集器则关注“分析的内容”。

每个收集器专门以自己的格式和方式收集分析数据。它们可以通过 developer mode tools 中的专用切换按钮从用户界面单独启用,或者通过其键或类从 Python 代码单独启用。

Odoo 目前有四种收集器:

姓名

切换按钮

Python 密钥

Python类

SQL collector

Record sql

sql

SqlCollector

Periodic collector

Record traces

traces_async

PeriodicCollector

QWeb collector

Record qweb

qweb

QwebCollector

Sync collector

traces_sync

SyncCollector

默认情况下,探查器启用 SQL 和定期收集器。当从用户界面或 Python 代码启用它时。

SQL收集器

SQL 收集器保存当前线程(对于所有游标)中对数据库进行的所有 SQL 查询以及堆栈跟踪。收集器的开销会添加到每个查询的分析线程中,​​这意味着在许多小型查询上使用它可能会影响执行时间和其他分析器。

它对于调试查询计数或向组合 speedscope 视图中的 Periodic collector 添加信息特别有用。

定期收集器

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

警告

如果间隔设置为非常低的值,分析长请求将产生内存问题。如果间隔设置为非常高的值,则短函数执行的信息将会丢失。

它是分析性能的最佳方法之一,因为由于其单独的线程,它对执行时间的影响非常低。

QWeb收集器

该收集器节省了Python执行时间和所有指令的查询。至于 SQL collector,在执行大量小指令时,开销可能很重要。结果在收集的数据方面与其他收集器不同,并且可以使用自定义小部件从 ir.profile 表单视图进行分析。

它主要用于优化视图。

同步收集器

该收集器为每个函数的调用和返回保存堆栈,并在同一线程上运行,这极大地影响了性能。

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

性能陷阱

  • 小心随机性。多次执行可能会导致不同的结果。例如,在执行期间触发垃圾收集器。

  • 小心阻止呼叫。在某些情况下,外部 c_call 可能需要一些时间才能释放 GIL,从而导致 Periodic collector 出现意外的长帧。这应该由分析器检测到并发出警告。如果需要,可以在此类调用之前手动触发分析器。

  • 注意缓存。在 view/assets/… 位于缓存中之前进行分析可能会导致不同的结果。

  • 请注意分析器的开销。当执行大量小查询时,SQL collector 的开销可能很重要。分析对于发现问题很实用,但您可能需要禁用分析器以衡量代码更改的实际影响。

  • 分析结果可能会占用大量内存。在某些情况下(例如,分析安装或长请求),您可能会达到内存限制,尤其是在渲染 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)

相反,请将 search_count 替换为 _read_group,以对整批记录执行一个 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 之一时,索引会消耗空间并影响性能。