* Performance improvements in balance sync cache
Balance::SyncCache#converted_holdings called account.holdings.map { |h| h.dup }
which duplicated every holding record into a new ActiveRecord object, converted
its currency, and stored the full object in a holdings_by_date array hash.
For an investment account with years of history this allocates 100,000+
AR objects on every sync - one per holding row - creating proportional GC
pressure that scaled with account age.
The only consumer of get_holdings(date) was BaseCalculator#holdings_value_for_date,
which immediately discarded the objects after calling .sum(&:amount). The
individual holding objects were never accessed for any other attribute.
Replace the dup-and-group approach with a single aggregation pass that stores
only the per-date sum:
holdings_value_by_date: account.holdings.each_with_object(Hash.new(0)) do |h, totals|
converted = Money.new(h.amount, h.currency).exchange_to(account.currency, date: h.date).amount
totals[h.date] += converted
end
Interface change: get_holdings(date) -> get_holdings_value(date) returns a
Numeric directly rather than an Array. BaseCalculator#holdings_value_for_date
is updated accordingly, and its own per-date memoization layer is removed
since holdings_value_by_date is already fully memoized at the SyncCache level.
* fall back to 1:1 rate in SyncCache when holding exchange rate is missing; update tests to use investment class
* Perf: Index Balance::SyncCache lookups by date to eliminate O(N×D) scans
Each call to get_holdings(date) and get_entries(date) previously did a
linear scan over the full converted_holdings / converted_entries arrays.
The balance calculators call these once per day across the full account
history, making the overall complexity O(N×D) where N is the total number
of holding/entry rows and D is the number of days in the account history.
For a typical investment account (20 securities, 2 years of history):
- Holdings: 20 × 730 = 14,600 rows
- Balance loop: 730 date iterations
- Comparisons: 14,600 × 730 ≈ 10.7 million per materialise run
This change builds a hash index (grouped by date) once on first access and
reuses it for all subsequent lookups, reducing per-call complexity to O(1).
Total complexity becomes O(N) — load once, look up cheaply.
Observed wall-clock improvement on a real account: ~36 s → ~5 s for a full
Balance::Materializer run. The nightly sync benefits equally.
No behavioural change: get_holdings, get_entries, and get_valuation return
identical data — they are now just fetched via a hash key rather than a
repeated array scan.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix: Return defensive copy from get_holdings to prevent cache mutation
get_holdings was returning a direct reference to the internal cached
array from holdings_by_date. A caller appending to the result (e.g.
via <<) would silently corrupt the cache for all subsequent date
lookups in the same materialise run.
Use &.dup to return a shallow copy of the group array. Callers only
read from the result (sum, map, etc.) so this has no behavioural
impact and negligible performance cost.
get_entries is already safe — Array#select always returns a new array.
get_valuation returns a single object, not an array, so no issue there.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Remove unnecessary dup in get_holdings for consistency
No caller mutates the returned array (only .sum is called), so the
defensive copy is unnecessary overhead. This aligns get_holdings with
get_entries and get_valuation which also return cached references directly.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* Initial split transaction support
* Add support to unsplit and edit split
* Update show.html.erb
* FIX address reviews
* Improve UX
* Update show.html.erb
* Reviews
* Update edit.html.erb
* Add parent category to dialog
* Update en.yml
* Add UI indication to totals
* FIX ui update
* Add category select like rest of app
---------
Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>