性能¶
性能分析¶
性能分析是分析程序执行并测量聚合数据的过程。这些数据可以是每个函数的经过时间,执行的 SQL 查询等等。
虽然性能分析本身并不能改善程序的性能,但它可以帮助找到性能问题并确定程序的哪个部分负责这些问题。
Odoo 提供了一个集成的性能分析工具,可以记录执行的所有查询和堆栈跟踪。它可以用于分析一组请求或用户会话的特定部分代码。性能分析结果可以使用集成的 speedscope 开源应用程序,用于可视化火焰图 视图进行检查,也可以通过首先将其保存为 JSON 文件或数据库中的自定义工具进行分析。
启用分析器¶
性能分析器可以从用户界面启用,这是最简单的方法,但只允许对Web请求进行分析;也可以从Python代码启用,这允许对任何代码片段进行分析,包括测试。
启用开发者模式 <developer-mode> 。
在开始分析会话之前,必须在数据库上全局启用分析器。这可以通过两种方式完成:
打开 开发者模式工具,然后切换到 启用性能分析 按钮。一个向导会建议一组性能分析的过期时间。点击 启用性能分析 来全局启用性能分析器。
Go to 设置 –> 通用设置 –> 性能 and set the desired time to the field 启用性能分析直到.
启用数据库上的分析器后,用户可以在其会话中启用它。要这样做,请再次切换到 启用分析 按钮在 开发者模式工具 中。默认情况下,推荐选项 记录 SQL 和 记录跟踪 已启用。要了解更多不同选项,请转到 性能/分析/收集器 。
当启用分析器时,所有发送到服务器的请求都会被分析并保存到 ir.profile
记录中。这些记录被分组到当前的分析会话中,该会话从启用分析器开始一直持续到禁用分析器。
注解
无法对Odoo在线数据库进行性能分析。
手动启动分析器可以方便地分析特定的方法或代码部分。这段代码可以是测试、计算方法、整个加载过程等。
要从Python代码中启动分析器,请将其作为上下文管理器调用。您可以通过参数指定要记录的内容。有一个快捷方式可用于对测试类进行分析:self.profile()
。有关`collectors`参数的更多信息,请参见:ref:performance/profiling/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)[源代码]¶
- 参数
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 会生成不同的视图模式,您可以从顶部菜单中访问这些模式。
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 |
|
|
|
记录跟踪 |
|
|
|
记录 qweb |
|
|
|
不 |
|
|
默认情况下,性能分析器启用 SQL 和周期性收集器。无论是从用户界面还是 Python 代码启用,都是如此。
SQL 收集器¶
SQL收集器会保存当前线程(所有游标)对数据库发出的所有SQL查询,以及堆栈跟踪信息。对于每个查询,收集器的开销都会被添加到分析线程中,这意味着在大量小查询上使用它可能会影响执行时间和其他分析工具。
它对于调试查询计数特别有用,或者在组合的 speedscope 视图中添加信息到 周期性收集器 。
定期收集器¶
此收集器在单独的线程中运行,并在每个间隔保存分析线程的堆栈跟踪。间隔(默认为10毫秒)可以通过用户界面中的 Interval 选项或 Python 代码中的 interval
参数进行定义。
警告
如果间隔时间设置得太低,对长时间请求进行分析将会产生内存问题。如果间隔时间设置得太高,将会丢失有关短函数执行的信息。
这是分析性能的最佳方式之一,因为它应该具有非常低的执行时间影响,这要归功于其独立的线程。
QWeb收集器¶
此收集器保存了所有指令的Python执行时间和查询。对于 SQL收集器 来说,在执行大量小指令时,开销可能很大。与其他收集器不同,收集的数据结果可以通过使用自定义小部件从 ir.profile
表单视图进行分析。
它主要用于优化视图。
同步收集器¶
该收集器保存每个函数调用和返回的堆栈,并在同一线程上运行,这会极大地影响性能。
它可以帮助调试和理解复杂的流程,并在代码中跟踪它们的执行。但是,不建议用于性能分析,因为开销很大。
性能陷阱¶
小心随机性。多次执行可能会导致不同的结果。例如,在执行期间触发垃圾收集器。
小心阻塞调用。在某些情况下,外部
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
: 10medium
: 100large
: 1000
- 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
以获取人口工具和应用程序。注解
生成器有责任正确处理字段名称。生成器可以一起为多个字段生成值。在这种情况下,字段名称应更像是一个“字段组”(应以“_”开头),涵盖生成器更新的不同字段(例如,对于更新多个地址字段的生成器,应使用“_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
字段的所有值与输入中的其他字段值组合在一起。
- odoo.tools.populate.compute(function, seed=None)[源代码]¶
返回一个值字典的迭代器工厂,该工厂根据
function(values, counter, random)
计算字段值,其中values
是其他字段的值,counter
是一个整数,random
是一个伪随机数生成器。
- odoo.tools.populate.constant(val, formatter=<function format_str>)[源代码]¶
返回一个工厂,用于生成一个值字典的迭代器,该迭代器会在每个输入字典中将字段设置为给定的值。
- odoo.tools.populate.iterate(vals, weights=None, seed=False, formatter=<function format_str>, then=None)[源代码]¶
返回一个工厂,用于生成一个值字典的迭代器,该迭代器从
vals
中选择一个值作为每个输入的值。一旦所有的vals
都被使用一次,就会继续作为then
或randomize
生成器。
- odoo.tools.populate.randint(a, b, seed=None)[源代码]¶
返回一个工厂,用于生成一个值字典的迭代器,该迭代器将每个输入字典中的字段设置为介于a和b之间的随机整数(包括a和b)。
良好的实践¶
批量操作¶
当处理记录集时,批量操作几乎总是更好的选择。
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)
警告
小心不要将每个字段都索引,因为索引会占用空间并影响执行 INSERT
、 UPDATE
和 DELETE
时的性能。