Skip to content

perf(FormulaDict): sort lazily instead of on every assignment - #452

Merged
rkingsbury merged 1 commit into
mainfrom
rk/formuladict-lazy-sort
Aug 3, 2026
Merged

perf(FormulaDict): sort lazily instead of on every assignment#452
rkingsbury merged 1 commit into
mainfrom
rk/formuladict-lazy-sort

Conversation

@rkingsbury

Copy link
Copy Markdown
Member

Summary

FormulaDict (backing Solution.components) re-sorted its entire contents on every __setitem__. A burst of writes — populating a Solution, or the volume setter's self.components[s] *= factor loop — was therefore O(n² log n).

This defers the sort: a mutation just marks the dict dirty, and the sort happens lazily at the iteration barrier (__iter__, plus __repr__ since UserDict.__repr__ reads self.data directly). Callers still see the same descending-by-amount order, because keys()/values()/items()/list()/dict()/for all funnel through __iter__. Deleting a key preserves relative order, so it leaves the flag untouched.

No behavior change — iteration and repr order are identical to the previous eager sorting; only the timing of the sort moves.

Benchmark — building all 23 presets from test_from_preset

Metric Before After Change
FormulaDict re-sorts / full pass 5843 94 ~62x fewer
Isolated build, median (all presets) 38.06 s 29.67 s ~22% faster
pytest test_from_preset wall time 87.00 s 71.51 s ~18% faster

Testing

  • test_from_preset — 23/23 pass.
  • test_component_subsets — directly asserts the descending-amount ordering contract of cations/anions/neutrals; passes.
  • test_formula_dict, test_charge_balance, test_get_amount, test_components_by_element(_nested), test_arithmetic_and_copy, test_from_dict_complex, and the serialization tests — all pass.

🤖 Generated with Claude Code

FormulaDict re-sorted its entire contents on every __setitem__, so a burst
of writes -- populating a Solution, or rescaling its volume (which does
 in a loop) -- was O(n**2 log n).

Defer the sort: mark the dict dirty on mutation and sort lazily at the
iteration barrier (__iter__, and __repr__ which bypasses __iter__ in
UserDict). Callers still observe the same descending-by-amount order, since
keys()/values()/items()/list()/dict()/for all funnel through __iter__.
Deletion preserves relative order, so it leaves the dirty flag untouched.

Building all 23 presets drops from ~5843 re-sorts to ~94 (~62x fewer) and
is ~20% faster in isolation; test_from_preset wall time falls ~87s -> ~72s.
No behavior change: iteration/repr order is identical to eager sorting.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@rkingsbury rkingsbury added this to the v1.6.0 milestone Aug 3, 2026
@codecov

codecov Bot commented Aug 3, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 87.81%. Comparing base (f8d582b) to head (4f14b17).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #452      +/-   ##
==========================================
+ Coverage   87.75%   87.81%   +0.06%     
==========================================
  Files          14       14              
  Lines        1935     1945      +10     
  Branches      337      338       +1     
==========================================
+ Hits         1698     1708      +10     
  Misses        188      188              
  Partials       49       49              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@rkingsbury
rkingsbury merged commit 9cc3c26 into main Aug 3, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant