Skip to content

[16.0][ADD] base_field_trigger_warmup: build compute dependency trees at boot - #3687

Open
mileo wants to merge 1 commit into
OCA:16.0from
kmee:16.0-base-field-trigger-warmup
Open

[16.0][ADD] base_field_trigger_warmup: build compute dependency trees at boot#3687
mileo wants to merge 1 commit into
OCA:16.0from
kmee:16.0-base-field-trigger-warmup

Conversation

@mileo

@mileo mileo commented Jul 29, 2026

Copy link
Copy Markdown
Member

The problem

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 (Registry.get_field_trigger_tree).

On models with many interdependent stored computed fields, that first write pays
for the whole closure. The practical effect is that the first request served by
each worker is much slower than the ones that follow, and every restart, deploy or
autoscaling event brings the penalty back.

Where this was found: a production deployment of a localization whose invoice line
model carries around 180 interdependent stored fields. Writing the first invoice on a
fresh worker took about 5N seconds, where every following invoice took N.
Profiling attributed roughly 90% of that difference to the trigger tree construction,
not to the business computation itself.

What the module does

It builds those trees while the registry loads, where nobody is waiting. Behaviour
is unchanged: it only decides when work the ORM would do anyway is done. There is
no user interface, no data, and the only dependency is base.

It logs what it did, so the trade can be evaluated on any database:

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

After that, the first write costs the same as the following ones.

Design decisions worth reviewing

Scope is configurable. Warming up every model costs a few seconds of boot per
worker, which is usually a good trade but not always. The
base_field_trigger_warmup.models system parameter takes a comma separated list of
models; * (the default) means everything. Unknown model names are ignored with a
warning rather than raising.

It can be disabled without uninstalling. ODOO_FIELD_TRIGGER_WARMUP=0 skips it,
which is useful on development machines and in CI where boot time matters more than
the first request. It is always skipped while tests run.

The private ORM API is called defensively. Registry.get_field_trigger_tree is
not public API. It is fetched with getattr and, if a future version renames or
drops it, the module logs the fact and does nothing instead of breaking the registry
load. A failure on an individual field is logged at debug level and skipped, for the
same reason: a warmup must never be able to break a boot.

Honest scope

This module hides the symptom of a wide compute graph, it does not reduce the graph.
When the first request of a worker is slow enough to need it, the underlying model
is probably worth looking at too. That is written in the ROADMAP so nobody installs
it expecting a general speed-up: it moves a cost, it does not remove one. Warm
requests are exactly as fast as before.

Tests

9 tests, run against a real Odoo 16 database: scope defaults and narrowing, unknown
models ignored, a failing field not breaking the run, the missing-API path being a
no-op, and the enable/disable conditions.

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 the first request served by each worker pays for the whole closure, and
every restart brings the penalty back.

This module builds those trees while the registry loads, where nobody is
waiting. Behaviour is unchanged: it only decides when work the ORM would do
anyway is done.

The scope defaults to every model and can be narrowed with the
base_field_trigger_warmup.models system parameter; ODOO_FIELD_TRIGGER_WARMUP=0
disables it without uninstalling. Registry.get_field_trigger_tree is private ORM
API, so it is called defensively: a version that renames or drops it degrades to
a no-op instead of breaking the registry load.

Extracted from a production deployment whose invoice line model carries around
180 interdependent stored fields, where the first write on a fresh worker cost
about five times what every following write cost.
@mileo
mileo force-pushed the 16.0-base-field-trigger-warmup branch from 2bf0728 to 68f074c Compare July 29, 2026 04:14
@OCA-git-bot OCA-git-bot added series:16.0 mod:base_field_trigger_warmup Module base_field_trigger_warmup labels Jul 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

mod:base_field_trigger_warmup Module base_field_trigger_warmup series:16.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants