性能

性能分析

性能分析是分析程序执行并测量聚合数据的过程。这些数据可以是每个函数的经过时间,执行的 SQL 查询等等。

虽然性能分析本身并不能改善程序的性能,但它可以帮助找到性能问题并确定程序的哪个部分负责这些问题。

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

启用分析器

性能分析器可以从用户界面启用,这是最简单的方法,但只允许对Web请求进行分析;也可以从Python代码启用,这允许对任何代码片段进行分析,包括测试。

  1. 启用开发者模式 <developer-mode>

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

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

      ../../../_images/enable_profiling_wizard.png
    • Go to 设置 –> 通用设置 –> 性能 and set the desired time to the field 启用性能分析直到.

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

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

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

注解

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

分析结果

要浏览性能分析结果,请确保 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
  • The Combined view shows all the SQL queries and traces merged togethers.

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

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

  • sql (density) 视图中,只显示所有的 SQL 查询,它们之间有间隔。这对于确定是 SQL 还是 Python 代码有问题,并且识别出可以批量处理的多个小查询的区域非常有用。

  • The frames view shows the results of only the periodic collector.

重要

即使性能分析器已经被设计得尽可能轻量,它仍然会对性能产生影响,特别是在使用 同步收集器 时。在分析 speedscope 结果时请牢记这一点。

收集器

分析器关注的是分析的 时间,而收集器则负责处理的 内容

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

Odoo 目前有四个可用的收集器:

名称

切换按钮

Python键

Python类

SQL collector

记录sql

sql

SqlCollector

Periodic collector

记录跟踪

traces_async

PeriodicCollector

QWeb collector

记录 qweb

qweb

QwebCollector

Sync collector

traces_sync

SyncCollector

默认情况下,性能分析器启用 SQL 和周期性收集器。无论是从用户界面还是 Python 代码启用,都是如此。

SQL 收集器

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

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

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

Saves all executed queries in the current thread with the call stack.

定期收集器

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

警告

如果间隔时间设置得太低,对长时间请求进行分析将会产生内存问题。如果间隔时间设置得太高,将会丢失有关短函数执行的信息。

这是分析性能的最佳方式之一,因为它应该具有非常低的执行时间影响,这要归功于其独立的线程。

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

Record execution frames asynchronously at most every interval seconds.

参数

(float) (interval) – time to wait in seconds between two samples.

QWeb收集器

此收集器保存了所有指令的Python执行时间和查询。对于 SQL收集器 来说,在执行大量小指令时,开销可能很大。与其他收集器不同,收集的数据结果可以通过使用自定义小部件从 ir.profile 表单视图进行分析。

它主要用于优化视图。

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

Record qweb execution with directive trace.

同步收集器

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

它可以帮助调试和理解复杂的流程,并在代码中跟踪它们的执行。但是,不建议用于性能分析,因为开销很大。

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

Record complete execution synchronously. Note that –limit-memory-hard may need to be increased when launching Odoo.

性能陷阱

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

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

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

  • 请注意性能分析器的开销。当执行大量小查询时, 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.

我们可以通过读取 prefetch_ids 字段来验证记录是否批量预取,该字段包括每个记录的ID。一起浏览所有记录是不切实际的。

如果需要,可以使用 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 时的性能。