性能?

性能分析?

性能分析是分析程序執行并測量聚合數據的過程。這些數據可以是每個函數的經過時間,執行的 SQL 查詢等等。

雖然性能分析本身并不能改善程序的性能,但它可以幫助找到性能問題并確定程序的哪個部分負責這些問題。

Odoo提供了一個集成的性能分析工具,可以記錄執行過的所有查詢和堆棧跟蹤。它可以用于分析一組請求或用戶會話的特定部分代碼。性能分析結果可以使用集成的 speedscope 應用程序進行查看,該應用程序允許可視化火焰圖視圖,也可以通過首先將其保存在JSON文件或數據庫中,然后使用自定義工具進行分析。

啟用分析器?

性能分析器可以從用戶界面啟用,這是最簡單的方法,但只允許對Web請求進行分析;也可以從Python代碼啟用,這允許對任何代碼片段進行分析,包括測試。

  1. 啟用開發者模式 <developer-mode> 。

  2. 在開始分析會話之前,必須在數據庫上全局啟用分析器。這可以通過兩種方式完成:

    • 打開 開發者模式工具 ,然后切換到 啟用性能分析 按鈕。一個向導會建議一組性能分析的過期時間。點擊 啟用性能分析 來全局啟用性能分析器。

      ../../../_images/enable_profiling_wizard.png
    • 前往 設置 –> 通用設置 –> 性能 ,并將所需時間設置到字段 啟用性能分析直至 。

  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
  • Combined 視圖顯示所有 SQL 查詢和跟蹤合并在一起。

  • 組合無上下文 視圖顯示相同的結果,但忽略了保存的執行上下文<performance/profiling/enable>`。

  • sql (無間隔) 視圖顯示所有 SQL 查詢,就像它們一個接一個地執行一樣,沒有任何 Python 邏輯。這對于僅優化 SQL 很有用。

  • sql (density) 視圖僅顯示所有 SQL 查詢,它們之間留有間隔。這對于發現是 SQL 還是 Python 代碼存在問題以及識別許多小查詢可以合并的區域非常有用。

  • frames 視圖僅顯示 周期性收集器 的結果。

重要

盡管分析器被設計為盡可能輕量級,但它仍可能影響性能,特別是在使用 同步收集器 時。在分析 speedscope 結果時請注意。

收集器?

雖然分析器關注的是分析的時間,但是收集器則關注分析的內容。

每個收集器都專門收集其自己格式和方式的性能分析數據。它們可以通過用戶界面中的專用切換按鈕在 開發者模式工具 中單獨啟用,也可以通過它們的鍵或類從Python代碼中啟用。

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

名稱

切換按鈕

Python鍵

Python類

SQL收集器

記錄SQL

sql

SqlCollector

定期收集器

記錄跟蹤

traces_async

PeriodicCollector

QWeb收集器

記錄 qweb

qweb

QwebCollector

同步收集器

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 是一個值生成器(字段值的字典)。

工廠骨架:

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 方法禁用批量預?。?/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 、 UPDATEDELETE 中影響性能。