性能

性能分析

性能分析是分析程序执行并测量聚合数据的过程。这些数据可以是每个函数的经过时间,执行的 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在线数据库进行性能分析。

分析结果

要浏览性能分析结果,请确保在数据库上全局启用了 性能分析器,然后打开 开发者模式工具 并点击性能分析部分右上角的按钮。会打开一个按性能分析会话分组的 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[源代码]

在当前线程中保存所有执行的查询和调用堆栈。

定期收集器

此收集器在单独的线程中运行,并在每个间隔保存分析线程的堆栈跟踪。间隔(默认为10毫秒)可以通过用户界面中的 Interval 选项或 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,从而导致意外的长帧与 周期性收集器。这应该被分析器检测到并发出警告。如果需要,可以在此类调用之前手动触发分析器。

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

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

  • 性能分析结果可能会占用大量内存。在某些情况下(例如,对安装或长时间请求进行分析),可能会达到内存限制,特别是在渲染 speedscope 结果时,可能会导致 HTTP 500 错误。在这种情况下,您可能需要使用更高的内存限制启动服务器: --limit-memory-hard $((8 *1024** 3))

数据库填充

Odoo CLI 通过 CLI 命令 odoo-bin populate 提供了一个 数据库填充 功能。

不必进行繁琐的手动或编程式测试数据规范,可以使用此功能根据需要填充数据库中所需数量的测试数据。这可用于检测测试流程中的各种错误或性能问题。

为了填充给定的模型,可以定义以下方法和属性。

Model._populate_sizes

返回一个字典,将符号大小('small''medium''large')映射到整数,给出 _populate() 应该创建的最小记录数。

默认的种群大小为:

  • small : 10

  • medium : 100

  • large : 1000

Model._populate_dependencies

返回在当前模型之前需要填充的模型列表。

返回类型

list

Model._populate(size)[源代码]

创建记录以填充此模型。

参数

size (str) – 记录数量的符号大小: 'small''medium''large'

Model._populate_factories()[源代码]

为模型的不同字段生成工厂。

factory is a generator of values (dict of field values).

工厂骨架:

def generator(iterator, field_name, model_name):
    for counter, values in enumerate(iterator):
        # values.update(dict())
        yield values

参见 odoo.tools.populate 以获取人口工具和应用程序。

返回

一对对(字段名,工厂)的列表,其中 工厂 是一个生成器函数。

返回类型

list(tuple(str, generator))

注解

生成器有责任正确处理字段名称。生成器可以一起为多个字段生成值。在这种情况下,字段名称应更像是一个“字段组”(应以“_”开头),涵盖生成器更新的不同字段(例如,对于更新多个地址字段的生成器,应使用“_address”)。

注解

您必须在模型上至少定义 _populate()_populate_factories() 方法,以启用数据库填充。

Example

from odoo.tools import populate

class CustomModel(models.Model)
    _inherit = "custom.some_model"
    _populate_sizes = {"small": 100, "medium": 2000, "large": 10000}
    _populate_dependencies = ["custom.some_other_model"]

    def _populate_factories(self):
        # Record ids of previously populated models are accessible in the registry
        some_other_ids = self.env.registry.populated_models["custom.some_other_model"]

        def get_some_field(values=None, random=None, **kwargs):
            """ Choose a value for some_field depending on other fields values.

            :param dict values:
            :param random: seeded :class:`random.Random` object
            """
            field_1 = values['field_1']
            if field_1 in [value2, value3]:
                return random.choice(some_field_values)
            return False

        return [
            ("field_1", populate.randomize([value1, value2, value3])),
            ("field_2", populate.randomize([value_a, value_b], [0.5, 0.5])),
            ("some_other_id", populate.randomize(some_other_ids)),
            ("some_field", populate.compute(get_some_field, seed="some_field")),
            ('active', populate.cartesian([True, False])),
        ]

    def _populate(self, size):
        records = super()._populate(size)

        # If you want to update the generated records
        # E.g setting the parent-child relationships
        records.do_something()

        return records

人口工具

多种数据生成工具可用于轻松创建所需的数据生成器。

odoo.tools.populate.cartesian(vals, weights=None, seed=False, formatter=<function format_str>, then=None)[源代码]

返回一个工厂,用于生成一个值字典的迭代器,该迭代器将 vals 字段的所有值与输入中的其他字段值组合在一起。

参数
  • vals (list) – 根据 weights 来选择值的列表

  • weights (list) – 概率权重列表

  • seed – 随机数生成器的可选初始化

  • formatter (function) – (val, counter, values) –> 格式化后的值

  • then (function) – 如果定义了,则在vals被消耗时使用工厂。

返回

形如 (iterator, field_name, model_name) -> values 的函数

返回类型

function (iterator, str, str) -> dict

odoo.tools.populate.compute(function, seed=None)[源代码]

返回一个值字典的迭代器工厂,该工厂根据 function(values, counter, random) 计算字段值,其中 values 是其他字段的值, counter 是一个整数, random 是一个伪随机数生成器。

参数
  • function (callable) – (values, counter, random) –> 字段值

  • seed – 随机数生成器的可选初始化

返回

形如 (iterator, field_name, model_name) -> values 的函数

返回类型

function (iterator, str, str) -> dict

odoo.tools.populate.constant(val, formatter=<function format_str>)[源代码]

返回一个工厂,用于生成一个值字典的迭代器,该迭代器会在每个输入字典中将字段设置为给定的值。

返回

形如 (iterator, field_name, model_name) -> values 的函数

返回类型

function (iterator, str, str) -> dict

odoo.tools.populate.iterate(vals, weights=None, seed=False, formatter=<function format_str>, then=None)[源代码]

返回一个工厂,用于生成一个值字典的迭代器,该迭代器从 vals 中选择一个值作为每个输入的值。一旦所有的 vals 都被使用一次,就会继续作为 thenrandomize 生成器。

参数
  • vals (list) – 根据 weights 来选择值的列表

  • weights (list) – 概率权重列表

  • seed – 随机数生成器的可选初始化

  • formatter (function) – (val, counter, values) –> 格式化后的值

  • then (function) – 如果定义了,则在vals被消耗时使用工厂。

返回

形如 (iterator, field_name, model_name) -> values 的函数

返回类型

function (iterator, str, str) -> dict

odoo.tools.populate.randint(a, b, seed=None)[源代码]

返回一个工厂,用于生成一个值字典的迭代器,该迭代器将每个输入字典中的字段设置为介于a和b之间的随机整数(包括a和b)。

参数
  • a (int) – 最小随机值

  • b (int) – 最大随机值

  • seed (int) –

返回

形如 (iterator, field_name, model_name) -> values 的函数

返回类型

function (iterator, str, str) -> dict

odoo.tools.populate.randomize(vals, weights=None, seed=False, formatter=<function format_str>, counter_offset=0)[源代码]

返回一个工厂,用于生成一个值字典的迭代器,该字典的值是在字段的 vals 中伪随机选择的值。

参数
  • vals (list) – 根据 weights 来选择值的列表

  • weights (list) – 概率权重列表

  • seed – 随机数生成器的可选初始化

  • formatter (function) – (val, counter, values) –> 格式化后的值

  • counter_offset (int) –

返回

形如 (iterator, field_name, model_name) -> values 的函数

返回类型

function (iterator, str, str) -> dict

良好的实践

批量操作

当处理记录集时,批量操作几乎总是更好的选择。

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):
    if self.ids:
        domain = [('related_id', 'in', self.ids)]
        counts_data = other_model.read_group(domain, ['related_id'], ['related_id'])
        mapped_data = {
            count['related_id'][0]: count['related_id_count'] for count in counts_data
        }
    else:
        mapped_data = {}
    for record in self:
        record.count = mapped_data.get(record.id, 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 时的性能。