Avatar for the ormar-orm user
ormar-orm
ormar
BlogDocsChangelog

Performance History

Latest Results

perf: fast-path expand_relationship for already-typed Model values (#1656) expand_relationship had a per-call try/except framework around its constructor cache and dispatched on value.__class__.__name__ via a string-keyed dict — even though the dominant case (row materialization, user kwargs that already hold constructed Models) is just "register if asked, return value". Two changes: - Identity-based fast path before the dispatch table: if value is an instance of self.to (cheapest possible identity check via __class__ is), inline the register-if-asked + return. Skips the dict lookup and the _register_existing_model indirection entirely for the most common path. - Replace the lazy try/except cache with a @cached_property. By the time expand_relationship runs, _verify_model_can_be_initialized has already gated on requires_ref_update, so self.to is resolved and the bound methods captured in the dict are stable. _register_existing_model is dead post-fast-path (it was the dispatch target for the same-class case the fast path now covers); deleted, and its dispatch entry removed. Benchmarks vs the post-#1655 optimization tip: - init_with_related[40]: 858 µs -> 810 µs (-5.6%) - init_with_related[10]: 235 µs -> 222 µs (-5.5%) - init[1000]: 7010 µs -> 6517 µs (-7.0%) - get_all_with_related[40]: 4351 µs -> 4124 µs (-5.2%) - iterate[1000]: 17152 µs -> 16188 µs (-5.6%)
check-optimisations-and-rs
45 minutes ago
perf: lazy relation machinery in Model.__init__ (#1652) * perf: defer QuerysetProxy construction in RelationProxy Every model materialized from a row eagerly built a QuerysetProxy per reverse/m2m relation, even though the queryset machinery is rarely touched on read paths. Make `RelationProxy.queryset_proxy` a lazy property so the allocation only happens when something actually queries through the relation. Benchmarks (sqlite, on-disk): - init[1000]: 9.74 ms -> 8.42 ms (-13.6%) - get_all[1000]: 15.64 ms -> 14.20 ms (-9.2%) - iterate[1000]: 22.83 ms -> 21.33 ms (-6.6%) * perf: defer RelationProxy construction in Relation Reverse / many-to-many relations allocated their RelationProxy in Relation.__init__ for every model, even when the relation was never read. Construct on first add/get instead, and read related_models directly from the hash-cache update path so a PK change doesn't force materialization of every reverse proxy on the model. Combined with the previous QuerysetProxy change, vs baseline: - init[1000]: 9.74 ms -> 7.06 ms (-27.5%) - get_all[1000]: 15.64 ms -> 12.06 ms (-22.9%) - iterate[1000]: 22.83 ms -> 18.66 ms (-18.3%) * perf: defer Relation construction in RelationsManager RelationsManager.__init__ used to build a Relation for every declared FK on every Model.__init__. Most of those Relation instances are never read — they exist only to be checked for membership or never touched at all on row-materialization paths. Build them on demand in _get(name) using a precomputed name->field lookup instead. Combined with the previous lazy-RelationProxy and lazy-QuerysetProxy changes, vs baseline: - init[1000]: 9.74 ms -> 6.67 ms (-31.6%) - get_all[1000]: 15.64 ms -> 11.58 ms (-26.0%) - iterate[1000]: 22.83 ms -> 18.83 ms (-17.5%) - init[250]: 2.65 ms -> 1.66 ms (-37.4%)
check-optimisations-and-rs
7 hours ago
perf: replace RelationProxy.__getattribute__ with explicit count/clear (#1650) Profile-driven removal of the per-attribute-access Python override on RelationProxy. The previous __getattribute__ existed solely to redirect two list-method names ("count", "clear") to the QuerysetProxy versions; every other attribute access paid the cost of a Python-level __getattribute__ call only to fall through to super. cProfile (all_with_related, 40 000 row hydrations): __getattribute__ was 0.354 s tottime / 6.2 % of scenario time, with 420 000 calls. After this change it is no longer in the top 25 by tottime — attribute access goes through the C-level lookup path. Replacement: define count() and clear() as async methods directly on RelationProxy. They shadow list.count / list.clear by virtue of MRO and delegate to queryset_proxy after self._initialize_queryset(). The observable async semantics are unchanged: callers always did ``await proxy.count(...)`` / ``await proxy.clear(...)``; the previous override returned a bound async method, the new methods are themselves async — both produce the same coroutine. Behavior unchanged: same signatures (distinct=True / keep_reversed=True defaults), same delegation path, same QuerysetProxy initialization trigger. 628 tests pass at 100 % coverage. Benchmark deltas (median, pytest-benchmark, --warmup=on, 10+ rounds): - test_get_all_with_related_models[10] -17.2 % - test_get_all_with_related_models[20] -15.6 % - test_get_all_with_related_models[40] -7.7 % - test_get_all[250] -13.1 % - test_get_all[500] -9.9 % - test_get_all[1000] -6.4 % - test_iterate[*] within noise - single-row get_one / first I/O-dominated, ±15 % noise Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
check-optimisations-and-rs
1 day ago
perf: cache & specialize _process_kwargs hot path (#1649) Profile-driven refactor of NewBaseModel._process_kwargs, which fires on every Model.__init__ (user construction and row hydration). Removes redundant per-init work and skips no-op conversion calls for fields that are neither JSON nor bytes. Changes: - Cache _pydantic_field_names, _extra_is_ignore, _allowed_kwarg_names on the class (lazy-populated on first init); installed via metaclass add_cached_properties alongside the existing _json_fields/_bytes_fields caches. - Replace nested _convert_to_bytes(_convert_json(...)) wrapping with an explicit dispatch loop. Common path (regular ormar field, no JSON, no bytes) avoids the function-call overhead entirely. - Inline _remove_extra_parameters_if_they_should_be_ignored behind the cached _extra_is_ignore bool; remove the method (no external callers). - Remove now-unused _convert_to_bytes / _convert_json methods and the orphaned _convert_json entry in quick_access_views. Behavior unchanged: same ModelError messaging on unknown fields, same JSON encoding, same base64 bytes handling. 628 tests pass at 100 % coverage. Benchmark deltas (median, pytest-benchmark): - test_initializing_models[250] -34.9% - test_initializing_models[500] -8.8% - test_iterate[500] / test_iterate[1000] -7.9% / -7.4% - test_get_all_with_related_models[40] -3.2% - I/O-bound get_one / first / get_or_none within noise cProfile (all_with_related): _process_kwargs tottime 0.365s -> 0.238s (-35%). The line-397 dict-comp is no longer a separate hotspot. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
check-optimisations-and-rs
1 day ago

Latest Branches

CodSpeed Performance Gauge
-30%
optimizations to improve performance, add ormar-utils#1571
56 minutes ago
078ccdf
check-optimisations-and-rs
CodSpeed Performance Gauge
-28%
Added schema field to OrmarConfig#1493
2 days ago
c8e11e8
hk3dva:feature_add_schema_to_ormar_config
CodSpeed Performance Gauge
-3%
2 days ago
870aa5f
SepehrBazyar:proxy_inheritance
© 2026 CodSpeed Technology
Home Terms Privacy Docs