性能?
性能分析?
性能分析是分析程序執行并測量聚合數據的過程。這些數據可以是每個函數的經過時間,執行的 SQL 查詢等等。
雖然性能分析本身并不能改善程序的性能,但它可以幫助找到性能問題并確定程序的哪個部分負責這些問題。
Odoo提供了一個集成的性能分析工具,可以記錄執行過的所有查詢和堆棧跟蹤。它可以用于分析一組請求或用戶會話的特定部分代碼。性能分析結果可以使用集成的 speedscope 應用程序進行查看,該應用程序允許可視化火焰圖視圖,也可以通過首先將其保存在JSON文件或數據庫中,然后使用自定義工具進行分析。
啟用分析器?
性能分析器可以從用戶界面啟用,這是最簡單的方法,但只允許對Web請求進行分析;也可以從Python代碼啟用,這允許對任何代碼片段進行分析,包括測試。
啟用開發者模式 <developer-mode> 。
在開始分析會話之前,必須在數據庫上全局啟用分析器。這可以通過兩種方式完成:
打開 開發者模式工具 ,然后切換到 啟用性能分析 按鈕。一個向導會建議一組性能分析的過期時間。點擊 啟用性能分析 來全局啟用性能分析器。
前往 設置 –> 通用設置 –> 性能 ,并將所需時間設置到字段 啟用性能分析直至 。
啟用數據庫分析器后,用戶可以在其會話中啟用它。為此,請再次切換到 開發者模式工具 中的 啟用分析 按鈕。默認情況下,推薦選項 記錄 SQL 和 記錄跟蹤 已啟用。要了解更多不同選項的信息,請轉到 收集器 。
當啟用分析器時,所有發送到服務器的請求都會被分析并保存到 ir.profile
記錄中。這些記錄被分組到當前的分析會話中,該會話從啟用分析器開始一直持續到禁用分析器。
注解
無法對Odoo在線數據庫進行性能分析。
手動啟動分析器可以方便地分析特定的方法或代碼部分。這段代碼可以是測試、計算方法、整個加載過程等。
要從Python代碼啟動分析器,請將其作為上下文管理器調用。您可以通過參數指定要記錄的內容。對于測試類的分析,有一個快捷方式: self.profile()
。有關 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 會生成不同的視圖模式,您可以從頂部菜單中訪問這些模式。

Combined 視圖顯示所有 SQL 查詢和跟蹤合并在一起。
組合無上下文 視圖顯示相同的結果,但忽略了保存的執行上下文<performance/profiling/enable>`。
sql (無間隔) 視圖顯示所有 SQL 查詢,就像它們一個接一個地執行一樣,沒有任何 Python 邏輯。這對于僅優化 SQL 很有用。
sql (density) 視圖僅顯示所有 SQL 查詢,它們之間留有間隔。這對于發現是 SQL 還是 Python 代碼存在問題以及識別許多小查詢可以合并的區域非常有用。
frames 視圖僅顯示 周期性收集器 的結果。
重要
盡管分析器被設計為盡可能輕量級,但它仍可能影響性能,特別是在使用 同步收集器 時。在分析 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
是一個值生成器(字段值的字典)。工廠骨架:
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
方法禁用批量預?。?/p>
for values in values_list:
message = self.browse(values['id']).with_prefetch(self.ids)
減少算法復雜度?
算法復雜度是衡量算法完成所需時間與輸入大小 n
的度量。當復雜度高時,隨著輸入規模的增大,執行時間會迅速增長。在某些情況下,通過正確準備輸入數據可以降低算法復雜度。
Example
對于一個給定的問題,考慮一個用兩個嵌套循環編寫的樸素算法,其復雜度為O(n2)。
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
中影響性能。