Продуктивність

Профілювання

Профілювання полягає в аналізі виконання програми та вимірюванні агрегованих даних. Ці дані можуть включати час, що минув для кожної функції, виконані SQL-запити…

Хоча профілювання саме по собі не покращує продуктивність програми, воно може виявитися дуже корисним для виявлення проблем із продуктивністю та визначення того, яка частина програми відповідає за них.

Odoo надає інтегрований інструмент профілювання, який дозволяє записувати всі виконані запити та відстеження стеку під час виконання. Його можна використовувати для профілювання як набору запитів користувацького сеансу, так і певної частини коду. Результати профілювання можна перевірити за допомогою вбудованого speedscope відкритого додатку, що дозволяє візуалізувати графік виконання, або проаналізувати за допомогою користувацьких інструментів, попередньо зберігши їх у JSON-файлі або в базі даних.

Увімкнути профайлер

Профілювальник можна ввімкнути або з інтерфейсу користувача, що є найпростішим способом, але дозволяє профілювати лише веб-запити, або з коду Python, що дозволяє профілювати будь-який фрагмент коду, включаючи тести.

  1. Увімкнути режим розробника.

  2. Перед початком сеансу профілювання профайлер має бути глобально ввімкнений у базі даних. Це можна зробити двома способами:

    • Відкрийте інструменти режиму розробника, потім увімкніть кнопку Увімкнути профайлер. Майстер запропонує набір термінів дії для профілювання. Натисніть УВІМКНУТИ ПРОФАЙЛЕР, щоб увімкнути профайлер глобально.

      ../../../_images/enable_profiling_wizard.png
    • Перейдіть до Налаштування –> Загальні налаштування –> Продуктивність та встановіть потрібний час у полі Увімкнути профайлер до.

  3. Після увімкнення профайлера для бази даних, користувачі можуть увімкнути його для свого сеансу. Для цього знову натисніть кнопку Увімкнути профайлер у інструменти режиму розробника. За замовчуванням увімкнено рекомендовані опції Запис sql та Запис відстеження. Щоб дізнатися більше про різні опції, перейдіть до Collectors.

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

Коли профайлер увімкнено, всі запити, зроблені до сервера, профілюються та зберігаються в записі ir.profile. Такі записи групуються в поточний сеанс профілювання, який охоплює період з моменту ввімкнення профайлера до моменту його вимкнення.

Примітка

Бази даних Odoo Online не можуть бути профілювані.

Проаналізуйте результати

Щоб переглянути результати профілювання, переконайтеся, що профайлеп увімкнено глобально на базі даних, потім відкрийте інструменти режиму розробника і натисніть на кнопку у верхньому правому куті розділу профілювання. Відкриється вікно списку записів ir.profile, згрупованих за сеансом профілювання.

../../../_images/profiling_web.png

Кожен запис має посилання, яке можна клікнути, і яке відкриває результати speedscope в новій вкладці.

../../../_images/flamegraph_example.png

Speedscope виходить за рамки цієї документації, але є багато інструментів, які можна спробувати: пошук, виділення схожих кадрів, масштабування кадру, часова шкала, переважування лівого краю, сендвіч-вид…

Залежно від активованих опцій профайлера, Odoo генерує різні режими перегляду, до яких можна отримати доступ з верхнього меню.

../../../_images/speedscope_modes.png
  • У режимі представлення Комбінований показано всі об’єднані SQL-запити та відстеження.

  • Представлення Комбіновано без контексту показує той самий результат, але ігнорує збережений контекст виконання <performance/profiling/enable>`.

  • Представлення sql (без пропусків) показує всі SQL-запити так, ніби вони виконувалися один за одним, без будь-якої логіки Python. Це корисно лише для оптимізації SQL.

  • У представленні sql (density) відображаються лише всі SQL-запити, залишаючи проміжки між ними. Це може бути корисним для визначення того, чи є проблема кодом SQL, чи Python, а також для визначення зон, у яких може бути об’єднано багато невеликих запитів.

  • У представленні кадри відображаються результати лише періодичний collector.

Важливо

Навіть попри те, що профайлер був розроблений якомога легшим, він все одно може впливати на продуктивність, особливо під час використання Синхронізація collector. Майте це на увазі під час аналізу результатів speedscope.

Collectors

У той час як профайлер займається тим, коли профілювання, collectors піклуються про що.

Кожен колектор спеціалізується на зборі даних профілювання у власному форматі та способі. Їх можна вмикати індивідуально з інтерфейсу користувача за допомогою спеціальної кнопки перемикача у інструменти режиму розробника, або з коду Python за допомогою ключа або класу.

Наразі в Odoo доступні чотири collectors:

Name

Кнопка перемикання

Ключ Python

Клас Python

SQL collector

Запис sql

sql

SqlCollector

Періодичний collector

Запис відстежень

traces_async

PeriodicCollector

QWeb collector

Запис qweb

qweb

QwebCollector

Sync collector

No

traces_sync

SyncCollector

За замовчуванням профайлер увімкнено collectors SQL та Періодичний. Обидва, коли він увімкнено з інтерфейсу користувача або коду Python.

SQL collector

Collector SQL зберігає всі SQL-запити, виконані до бази даних у поточному потоці (для всіх курсорів), а також відстеження стека. Накладні витрати collector додаються до аналізованого потоку для кожного запиту, а це означає, що його використання для великої кількості невеликих запитів може вплинути на час виконання та інші профайлери.

Це особливо корисно для налагодження кількості запитів або для додавання інформації до Періодичний collector у об’єднаному представленні speedscope.

class odoo.tools.profiler.SQLCollector[source]

Зберігає всі виконані запити в поточному потоці разом зі стеком викликів.

Періодичний collector

Цей collector працює в окремому потоці та зберігає выдстеження стека аналізованого потоку через кожен інтервал. Інтервал (за замовчуванням 10 мс) можна визначити за допомогою опції Інтервал в інтерфейсі користувача або параметра interval у коді Python.

Попередження

Якщо інтервал встановлено на дуже низькому значенні, профілювання довгих запитів призведе до проблем з пам’яттю. Якщо інтервал встановлено на дуже високому значенні, інформація про короткі виконання функцій буде втрачена.

Це один з найкращих способів аналізу продуктивності, оскільки він повинен мати дуже незначний вплив на час виконання завдяки окремому потоку.

class odoo.tools.profiler.PeriodicCollector(interval=0.01)[source]

Записувати кадри виконання асинхронно максимум кожні interval секунд.

Параметри

(float) (interval) – час очікування в секундах між двома вибірками.

Колектор QWeb

Цей collector економить час виконання Python та запити до всіх директив. Що стосується SQL collector, накладні витрати можуть бути значними під час виконання великої кількості невеликих директив. Результати відрізняються від інших collectors з точки зору зібраних даних і можуть бути проаналізовані з форми ir.profile за допомогою спеціального віджета.

Це головним чином корисно для оптимізації представлень.

class odoo.tools.profiler.QwebCollector[source]

Записати виконання qweb за допомогою выдстеження директив.

Ccollector синхронізації

Цей collector зберігає стек для кожного виклику та повернення функції та виконується в одному потоці, що значно впливає на продуктивність.

Це може бути корисним для налагодження та розуміння складних потоків, а також для відстеження їх виконання в коді. Однак це не рекомендується для аналізу продуктивності, оскільки накладні витрати високі.

class odoo.tools.profiler.SyncCollector[source]

Синхронно записувати повне виконання. Зверніть увагу, що під час запуску Odoo може знадобитися збільшити параметр –limit-memory-hard.

Підводні камені продуктивності

  • Будьте обережні з випадковістю. Кілька виконань можуть призвести до різних результатів. Наприклад, collector сміття може бути запущений під час виконання.

  • Будьте обережні з блокуванням викликів. У деяких випадках зовнішній c_call може зайняти деякий час перед звільненням GIL, що призводить до неочікувано довгих кадрів з Періодичний collector. Це має бути виявлено профайлером і видати попередження. За потреби можна запустити профайлер вручну перед такими викликами.

  • Зверніть увагу на кеш. Профілювання до того, як view/assets/… знаходяться в кеші, може призвести до різних результатів.

  • Зверніть увагу на накладні витрати профайлера. Накладні витрати SQL collector можуть бути значними, коли виконується багато невеликих запитів. Профілювання практичне для виявлення проблеми, але ви можете захотіти вимкнути профайлер, щоб виміряти реальний вплив зміни коду.

  • Результати профілювання можуть потребувати багато пам’яті. У деяких випадках (наприклад, профілювання встановлення або довгого запиту) можливо досягти ліміту пам’яті, особливо під час рендерингу результатів speedscope, що може призвести до помилки HTTP 500. У цьому випадку вам може знадобитися запустити сервер з вищим лімітом пам’яті: --limit-memory-hard $((8*1024**3)).

Заповнення бази даних

Інтерфейс командного рядка Odoo пропонує функцію заповнення бази даних за допомогою команди командного рядка odoo-bin populate.

Замість стомлюючої ручної або програмної специфікації тестових даних можна використовувати цю функцію, щоб заповнити базу даних на вимогу бажаною кількістю тестових даних. Це можна використовувати для виявлення різноманітних помилок або проблем із продуктивністю в перевірених потоках.

Для заповнення заданої моделі можна визначити такі методи та атрибути.

Model._populate_sizes

Повертає dict, що відображає символічні розміри ('small', 'medium', 'large') на цілі числа, надаючи мінімальну кількість записів, яку має створити _populate().

Стандартні розміри сукупності:

  • small : 10

  • medium : 100

  • large : 1000

Model._populate_dependencies

Повернути список моделей, які потрібно заповнити перед поточною.

Тип повернення

list

Model._populate(size)[source]

Створіть записи для заповнення цієї моделі.

Параметри

size (str) – символічний розмір для кількості записів: 'small', 'medium' або 'large'

Model._populate_factories()[source]

Генерує фабрику для різних полів моделі.

factory - це генератор значень (словник значень полів).

Factory skeleton:

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

Дивіться odoo.tools.populate для отримання інформації про інструменти та додатків популяції.

Повертає

список пар(field_name, factory), де фабрика - це функція-генератор.

Тип повернення

list(tuple(str, generator))

Примітка

Генератор відповідає за правильну обробку field_name. Генератор може генерувати значення для кількох полів одночасно. У цьому випадку field_name має бути радше «field_group» (повинно починатися з «_»), охоплюючи різні поля, оновлені генератором (наприклад, «_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.randomize(vals, weights=None, seed=False, formatter=<function format_str>, counter_offset=0)[source]

Возвращает фабрику для итератора словарей значений с псевдослучайно выбранными значениями (среди vals) для поля.

Параметри
  • vals (list) – список, в якому буде вибрано значення залежно від weights

  • weights (list) – список ймовірнісних ваг

  • seed – необов’язкова ініціалізація генератора випадкових чисел

  • formatter (function) – (val, counter, values) –> formatted_value

  • counter_offset (int) –

Повертає

функція виду (iterator, field_name, model_name) -> values

Тип повернення

function (iterator, str, str) -> dict

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

Возвращает фабрику для итератора словарей значений, которая объединяет все val для поля с другими значениями полей во входных данных.

Параметри
  • vals (list) – список, в якому буде вибрано значення залежно від weights

  • weights (list) – список ймовірнісних ваг

  • seed – необов’язкова ініціалізація генератора випадкових чисел

  • formatter (function) – (val, counter, values) –> formatted_value

  • then (function) – если определено, фабрика будет использоваться после использования vals.

Повертає

функція виду (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)[source]

Возвращает фабрику для итератора словарей значений, который выбирает значение среди val для каждого входного значения. После того, как все val будут использованы один раз, продолжить работу как then или как генератор randomize.

Параметри
  • vals (list) – список, в якому буде вибрано значення залежно від weights

  • weights (list) – список ймовірнісних ваг

  • seed – необов’язкова ініціалізація генератора випадкових чисел

  • formatter (function) – (val, counter, values) –> formatted_value

  • then (function) – если определено, фабрика будет использоваться после использования vals.

Повертає

функція виду (iterator, field_name, model_name) -> values

Тип повернення

function (iterator, str, str) -> dict

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

Возвращает фабрику для итератора словарей значений, которая устанавливает поле в заданное значение в каждом входном словаре.

Повертає

функція виду (iterator, field_name, model_name) -> values

Тип повернення

function (iterator, str, str) -> dict

odoo.tools.populate.compute(function, seed=None)[source]

Возвращает фабрику для итератора словарей значений, который вычисляет значение поля как function(values, counter, random), где values - другие значения поля, counter - целое число, а random - генератор псевдослучайных чисел.

Параметри
  • function (callable) – (values, counter, random) –> field_values

  • seed – необов’язкова ініціалізація генератора випадкових чисел

Повертає

функція виду (iterator, field_name, model_name) -> values

Тип повернення

function (iterator, str, str) -> dict

odoo.tools.populate.randint(a, b, seed=None)[source]

Возвращает фабрику для итератора словарей значений, которая устанавливает поле в случайное целое число от a до b, включенное в каждый входной словарь.

Параметри
  • a (int) – мінімальне випадкове значення

  • b (int) – максимальне випадкове значення

  • seed (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)

Натомість замініть search_count на _read_group, щоб виконати один 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 у пакеті. Це майже не впливає на результат і допомагає фреймворку оптимізувати обчислення полів.

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, яке містить кожен ідентифікатор запису. Перегляд усіх записів разом є непрактичним,

При необходимости можно использовать метод 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

Припускаючи, що всі результати мають різні ідентифікатори, ми можемо підготувати дані для зменшення складності.

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 на set.

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.