Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 147 additions & 0 deletions base_field_trigger_warmup/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
.. image:: https://odoo-community.org/readme-banner-image
:target: https://odoo-community.org/get-involved?utm_source=readme
:alt: Odoo Community Association

=========================
Field Trigger Tree Warmup
=========================

..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:c32021bf286316f53f09e2a7ef5dce116ebfce5f52d8584ad1d093a4ca6a9b76
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fserver--tools-lightgray.png?logo=github
:target: https://github.com/OCA/server-tools/tree/16.0/base_field_trigger_warmup
:alt: OCA/server-tools
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/server-tools-16-0/server-tools-16-0-base_field_trigger_warmup
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
:target: https://runboat.odoo-community.org/builds?repo=OCA/server-tools&target_branch=16.0
:alt: Try me on Runboat

|badge1| |badge2| |badge3| |badge4| |badge5|

The ORM resolves the transitive closure of compute triggers lazily: the first
time a field is written, Odoo walks its ``@api.depends`` graph and caches the
resulting trigger tree in the registry. On models with many interdependent
stored computed fields, that first write pays for the whole closure, so the
first request served by each worker is noticeably slower than the ones that
follow, and every restart brings the penalty back.

This module builds those trees while the registry loads, where no user is
waiting. It has no user interface and no effect on behaviour: it only decides
*when* work the ORM would do anyway is done.

It was extracted from a production deployment whose invoice line model carries
around 180 interdependent stored fields: the first write on a fresh worker cost
about five times what every following write cost. Warming up the trees at boot
removed the difference.

**Table of contents**

.. contents::
:local:

Configuration
=============

By default every model in the registry is warmed up. On a large database that
costs a few seconds of boot time per worker, which is usually a good trade,
but it can be narrowed down.

To warm up only the models that matter, set the system parameter
``base_field_trigger_warmup.models`` to a comma separated list of model names::

base_field_trigger_warmup.models = account.move,account.move.line

Model names that do not exist in the registry are ignored with a warning.
Setting the parameter to ``*`` or leaving it empty restores the default of
warming up everything.

To disable the warmup entirely without uninstalling the module, for instance on
a development machine or in a CI pipeline where boot time matters more than the
first request, export::

ODOO_FIELD_TRIGGER_WARMUP=0

The warmup is always skipped while tests are running.

Usage
=====

There is nothing to do: installing the module is enough.

At the end of each registry load the module logs, at INFO level, how many
trigger trees it built and how long it took::

INFO odoo.addons.base_field_trigger_warmup: Warmed up 24868 field
trigger trees in 6.52s

Use that line to decide whether the default scope is worth its boot cost, and
narrow it down with the system parameter described in the configuration section
if it is not.

Known issues / Roadmap
======================

The module relies on ``Registry.get_field_trigger_tree``, which is private ORM
API. It is called defensively: if a future Odoo version renames or removes it,
the module logs the fact and does nothing instead of breaking the registry
load. Porting to a new version should start by checking that method.

Warming up the trees hides the symptom of a wide compute graph. When the first
request of a worker is slow enough to need this module, it is also worth
looking at whether the graph itself can be reduced.

Bug Tracker
===========

Bugs are tracked on `GitHub Issues <https://github.com/OCA/server-tools/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
`feedback <https://github.com/OCA/server-tools/issues/new?body=module:%20base_field_trigger_warmup%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.

Do not contact contributors directly about support or help with technical issues.

Credits
=======

Authors
~~~~~~~

* KMEE

Contributors
~~~~~~~~~~~~

* `KMEE <https://www.kmee.com.br>`_:

* Luis Felipe Mileo <mileo@kmee.com.br>

Maintainers
~~~~~~~~~~~

This module is maintained by the OCA.

.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org

OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.

This module is part of the `OCA/server-tools <https://github.com/OCA/server-tools/tree/16.0/base_field_trigger_warmup>`_ project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
1 change: 1 addition & 0 deletions base_field_trigger_warmup/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import models
15 changes: 15 additions & 0 deletions base_field_trigger_warmup/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Copyright 2026 KMEE INFORMATICA LTDA
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
{
"name": "Field Trigger Tree Warmup",
"summary": "Build the compute dependency trees at boot instead of on the "
"first request",
"version": "16.0.1.0.0",
"development_status": "Beta",
"category": "Tools",
"author": "KMEE, Odoo Community Association (OCA)",
"website": "https://github.com/OCA/server-tools",
"license": "AGPL-3",
"depends": ["base"],
"installable": True,
}
1 change: 1 addition & 0 deletions base_field_trigger_warmup/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import base_field_trigger_warmup
120 changes: 120 additions & 0 deletions base_field_trigger_warmup/models/base_field_trigger_warmup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# Copyright 2026 KMEE INFORMATICA LTDA
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

import logging
import os
import time

from odoo import api, models

_logger = logging.getLogger(__name__)

PARAM_MODELS = "base_field_trigger_warmup.models"
ENV_DISABLE = "ODOO_FIELD_TRIGGER_WARMUP"


class BaseFieldTriggerWarmup(models.AbstractModel):
"""Build the ORM compute dependency trees while the registry loads.

The ORM resolves the transitive closure of compute triggers lazily, the
first time a field is written. On models with many interdependent stored
computed fields, that first write pays for the whole closure, so the first
request served by each worker is much slower than the following ones. This
model moves that cost to the registry load, where no user is waiting.
"""

_name = "base.field.trigger.warmup"
_description = "Field Trigger Tree Warmup"

@api.model
def _warmup_is_enabled(self):
"""Warmup is skipped in tests and when the env var is set to 0."""
if os.environ.get(ENV_DISABLE, "1") == "0":
return False
return not self.env.registry.in_test_mode()

@api.model
def _warmup_model_names(self):
"""Model names to warm up.

Read from the ``base_field_trigger_warmup.models`` system parameter: a
comma separated list of model names, or ``*`` (the default) for every
model in the registry. Narrowing it down is useful when only a few
models are expensive and boot time matters.
"""
param = (
self.env["ir.config_parameter"].sudo().get_param(PARAM_MODELS, default="*")
or ""
).strip()
if param in ("", "*"):
return list(self.env.registry)
wanted = [name.strip() for name in param.split(",") if name.strip()]
known, unknown = [], []
for name in wanted:
(known if name in self.env.registry else unknown).append(name)
if unknown:
_logger.warning(
"%s lists unknown models, ignored: %s",
PARAM_MODELS,
", ".join(unknown),
)
return known

@api.model
def _warmup_field_trigger_trees(self, model_names=None):
"""Build the trigger tree of every field of ``model_names``.

Returns the number of fields whose tree was built. Failures on a single
field are logged at debug level and skipped: a warmup must never be able
to break the boot.
"""
registry = self.env.registry
# Private ORM API: guard it so an Odoo version that renames or drops it
# degrades to a no-op instead of breaking the registry load.
build_tree = getattr(registry, "get_field_trigger_tree", None)
if build_tree is None:
_logger.info(
"This Odoo build has no Registry.get_field_trigger_tree, "
"nothing to warm up"
)
return 0
if model_names is None:
model_names = self._warmup_model_names()
count = 0
for model_name in model_names:
model = self.env.get(model_name)
if model is None:
continue
for field in model._fields.values():
try:
build_tree(field)
except Exception: # pylint: disable=except-pass
_logger.debug(
"Could not build the trigger tree of %s.%s",
model_name,
field.name,
exc_info=True,
)
continue
count += 1
return count

def _register_hook(self):
res = super()._register_hook()
registry = self.env.registry
# _register_hook runs once per model that defines it, and the registry
# may be loaded more than once per process.
if getattr(registry, "_field_trigger_warmup_done", False):
return res
registry._field_trigger_warmup_done = True
if not self._warmup_is_enabled():
return res
start = time.time()
count = self._warmup_field_trigger_trees()
if count:
_logger.info(
"Warmed up %s field trigger trees in %.2fs",
count,
time.time() - start,
)
return res
20 changes: 20 additions & 0 deletions base_field_trigger_warmup/readme/CONFIGURE.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
By default every model in the registry is warmed up. On a large database that
costs a few seconds of boot time per worker, which is usually a good trade,
but it can be narrowed down.

To warm up only the models that matter, set the system parameter
``base_field_trigger_warmup.models`` to a comma separated list of model names::

base_field_trigger_warmup.models = account.move,account.move.line

Model names that do not exist in the registry are ignored with a warning.
Setting the parameter to ``*`` or leaving it empty restores the default of
warming up everything.

To disable the warmup entirely without uninstalling the module, for instance on
a development machine or in a CI pipeline where boot time matters more than the
first request, export::

ODOO_FIELD_TRIGGER_WARMUP=0

The warmup is always skipped while tests are running.
3 changes: 3 additions & 0 deletions base_field_trigger_warmup/readme/CONTRIBUTORS.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
* `KMEE <https://www.kmee.com.br>`_:

* Luis Felipe Mileo <mileo@kmee.com.br>
15 changes: 15 additions & 0 deletions base_field_trigger_warmup/readme/DESCRIPTION.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
The ORM resolves the transitive closure of compute triggers lazily: the first
time a field is written, Odoo walks its ``@api.depends`` graph and caches the
resulting trigger tree in the registry. On models with many interdependent
stored computed fields, that first write pays for the whole closure, so the
first request served by each worker is noticeably slower than the ones that
follow, and every restart brings the penalty back.

This module builds those trees while the registry loads, where no user is
waiting. It has no user interface and no effect on behaviour: it only decides
*when* work the ORM would do anyway is done.

It was extracted from a production deployment whose invoice line model carries
around 180 interdependent stored fields: the first write on a fresh worker cost
about five times what every following write cost. Warming up the trees at boot
removed the difference.
8 changes: 8 additions & 0 deletions base_field_trigger_warmup/readme/ROADMAP.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
The module relies on ``Registry.get_field_trigger_tree``, which is private ORM
API. It is called defensively: if a future Odoo version renames or removes it,
the module logs the fact and does nothing instead of breaking the registry
load. Porting to a new version should start by checking that method.

Warming up the trees hides the symptom of a wide compute graph. When the first
request of a worker is slow enough to need this module, it is also worth
looking at whether the graph itself can be reduced.
11 changes: 11 additions & 0 deletions base_field_trigger_warmup/readme/USAGE.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
There is nothing to do: installing the module is enough.

At the end of each registry load the module logs, at INFO level, how many
trigger trees it built and how long it took::

INFO odoo.addons.base_field_trigger_warmup: Warmed up 24868 field
trigger trees in 6.52s

Use that line to decide whether the default scope is worth its boot cost, and
narrow it down with the system parameter described in the configuration section
if it is not.
Loading
Loading