Skip to content

perf: precompute field.relation_type and slim RelationsManager init#1651

Merged
collerek merged 1 commit intoperf/relationproxy-getattributefrom
perf/precompute-relation-type
May 5, 2026
Merged

perf: precompute field.relation_type and slim RelationsManager init#1651
collerek merged 1 commit intoperf/relationproxy-getattributefrom
perf/precompute-relation-type

Conversation

@collerek
Copy link
Copy Markdown
Collaborator

@collerek collerek commented May 5, 2026

Summary

Three micro-optimizations on the per-Model.__init__ relation-bookkeeping path. Stacked on top of #1650 (the __getattribute__ cleanup).

  1. Precompute field.relation_type in BaseField.__init__. The branch on is_multi / is_through / virtual / is_relation runs once at field-declaration time. RelationsManager._add_relation then reads field.relation_type directly instead of calling _get_relation_type(field) per relation per Model.__init__. Saves ~100 k method calls per all_with_related scenario; _get_relation_type deleted. RelationType is lazy-imported via a module-level _relation_type_cls() helper to break the circular ormar.relationsrelation_managerutilsforeign_keybase chain.
  2. Drop RelationsManager._related_fields / _related_names. __contains__ now checks the _relations dict (already keyed by field name). Removes a per-init list-comp + two STORE_ATTR ops.
  3. _add_relation reads field.relation_type directly. An assert narrows the Optional for mypy and documents the invariant (only relation/through fields reach this path).

Behavior unchanged.

Benchmark deltas (median, pytest-benchmark, --warmup, 10+ rounds)

Test Before After Δ
test_initializing_models[250] 2 957 µs 2 726 µs −7.8 %
test_get_all[1000] 17.29 ms 16.06 ms −7.1 %
test_initializing_models_with_related_models[10] 308 µs 291 µs −5.7 %
test_initializing_models[500] 5 462 µs 5 371 µs −1.7 %
Single-row get_one / first I/O-dominated, within noise

cProfile (all_with_related scenario): 5.481 s → 5.328 s = −2.8 %. _get_relation_type no longer in the trace.

Test plan

  • make pre-commit — clean (mypy strict)
  • make coverage — 628 passed, 25 skipped, 100.00 % coverage
  • cProfile on all_with_related confirms _get_relation_type no longer in top-25 by tottime
  • All targeted tests green: tests/test_relations/, tests/test_queries/test_queryproxy_on_m2m_models.py, test_aggr_functions.py, test_reverse_fk_queryset.py

Notes

  • This PR's base is perf: replace RelationProxy.__getattribute__ with explicit count/clear #1650 (perf/relationproxy-getattribute) — once that merges into check-optimisations-and-rs, the diff here will rebase to just the three files shown.
  • _resolve_relation_type was simplified during review from function-attribute caching to a module-level _RELATION_TYPE variable + global (idiomatic, no # type: ignore needed).
  • An earlier _to_remove sentinel change (None instead of always-empty set()) was reverted during review — the saving (~7 µs / scenario) didn't justify the added Optional type + assert + conditional checks.

🤖 Generated with Claude Code

Three micro-optimizations on the per-Model.__init__ relation bookkeeping
path:

1. Precompute ``relation_type`` once in ``BaseField.__init__`` (priority-
   ordered branch on ``is_multi`` / ``is_through`` / ``virtual`` / ``is_relation``).
   ``RelationsManager._add_relation`` reads ``field.relation_type``
   directly instead of calling ``_get_relation_type(field)`` per relation
   per Model.__init__. Saves ~100k method calls per all_with_related
   scenario. ``_get_relation_type`` deleted.

   ``RelationType`` is lazy-imported via a module-level
   ``_relation_type_cls()`` helper to break the circular
   ``ormar.relations`` → ``relation_manager`` → ``utils`` →
   ``foreign_key`` → ``base`` chain.

2. Drop ``RelationsManager._related_fields`` and ``_related_names``
   attributes. ``__contains__`` now checks ``self._relations`` (already
   keyed by field name). Removes a per-init list-comp and two
   ``STORE_ATTR`` ops.

3. ``RelationsManager._add_relation`` reads ``field.relation_type``
   directly; an ``assert`` narrows ``Optional[RelationType]`` for mypy
   and documents the invariant (only relation/through fields reach this
   path).

Behavior unchanged. 628 tests pass at 100 % coverage.

Benchmark deltas (median, pytest-benchmark):
- test_initializing_models[250]                          -7.8 %
- test_get_all[1000]                                     -7.1 %
- test_initializing_models_with_related_models[10]       -5.7 %
- test_initializing_models[500]                          -1.7 %
- I/O-dominated single-row queries within noise

cProfile (all_with_related): scenario 5.481 s -> 5.328 s (-2.8 %).
``_get_relation_type`` no longer in the trace.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@collerek collerek merged commit 692acc5 into perf/relationproxy-getattribute May 5, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant