mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-07-18 20:59:39 +02:00
perf(income statement): cache income statement queries (#2371)
* Leftover cleanup from prior PR * Benchmark convenience task * Change default warm benchmark time * Cache income statement queries * Fix private method access
This commit is contained in:
parent
84b2426e54
commit
a5f1677f60
6 changed files with 103 additions and 77 deletions
|
@ -13,6 +13,13 @@ class Account::Syncer
|
|||
|
||||
def perform_post_sync
|
||||
account.family.auto_match_transfers!
|
||||
|
||||
# Warm IncomeStatement caches so subsequent requests are fast
|
||||
# TODO: this is a temporary solution to speed up pages. Long term we'll throw a materialized view / pre-computed table
|
||||
# in for family stats.
|
||||
income_statement = IncomeStatement.new(account.family)
|
||||
Rails.logger.info("Warming IncomeStatement caches")
|
||||
income_statement.warm_caches!
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -29,9 +29,59 @@ class Demo::Generator
|
|||
# Expose the seed so callers can reproduce a run if necessary.
|
||||
attr_reader :seed
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Performance helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
# Generate empty family - no financial data
|
||||
def generate_empty_data!(skip_clear: false)
|
||||
with_timing(__method__) do
|
||||
unless skip_clear
|
||||
puts "🧹 Clearing existing data..."
|
||||
clear_all_data!
|
||||
end
|
||||
|
||||
puts "👥 Creating empty family..."
|
||||
create_family_and_users!("Demo Family", "user@maybe.local", onboarded: true, subscribed: true)
|
||||
|
||||
puts "✅ Empty demo data loaded successfully!"
|
||||
end
|
||||
end
|
||||
|
||||
# Generate new user family - no financial data, needs onboarding
|
||||
def generate_new_user_data!(skip_clear: false)
|
||||
with_timing(__method__) do
|
||||
unless skip_clear
|
||||
puts "🧹 Clearing existing data..."
|
||||
clear_all_data!
|
||||
end
|
||||
|
||||
puts "👥 Creating new user family..."
|
||||
create_family_and_users!("Demo Family", "user@maybe.local", onboarded: false, subscribed: false)
|
||||
|
||||
puts "✅ New user demo data loaded successfully!"
|
||||
end
|
||||
end
|
||||
|
||||
# Generate comprehensive realistic demo data with multi-currency
|
||||
def generate_default_data!(skip_clear: false, email: "user@maybe.local")
|
||||
if skip_clear
|
||||
puts "⏭️ Skipping data clearing (appending new family)..."
|
||||
else
|
||||
puts "🧹 Clearing existing data..."
|
||||
clear_all_data!
|
||||
end
|
||||
|
||||
with_timing(__method__, max_seconds: 1000) do
|
||||
puts "👥 Creating demo family..."
|
||||
family = create_family_and_users!("Demo Family", email, onboarded: true, subscribed: true)
|
||||
|
||||
puts "📊 Creating realistic financial data..."
|
||||
create_realistic_categories!(family)
|
||||
create_realistic_accounts!(family)
|
||||
create_realistic_transactions!(family)
|
||||
# Auto-fill current-month budget based on recent spending averages
|
||||
generate_budget_auto_fill!(family)
|
||||
|
||||
puts "✅ Realistic demo data loaded successfully!"
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
|
@ -58,66 +108,7 @@ class Demo::Generator
|
|||
@rng.rand(*args)
|
||||
end
|
||||
|
||||
# Generate empty family - no financial data
|
||||
def generate_empty_data!(skip_clear: false)
|
||||
with_timing(__method__) do
|
||||
unless skip_clear
|
||||
puts "🧹 Clearing existing data..."
|
||||
clear_all_data!
|
||||
end
|
||||
|
||||
puts "👥 Creating empty family..."
|
||||
create_family_and_users!("Demo Family", "user@maybe.local", onboarded: true, subscribed: true)
|
||||
|
||||
puts "✅ Empty demo data loaded successfully!"
|
||||
end
|
||||
end
|
||||
|
||||
# Generate new user family - no financial data, needs onboarding
|
||||
def generate_new_user_data!(skip_clear: false)
|
||||
with_timing(__method__) do
|
||||
unless skip_clear
|
||||
puts "🧹 Clearing existing data..."
|
||||
clear_all_data!
|
||||
end
|
||||
|
||||
puts "👥 Creating new user family..."
|
||||
create_family_and_users!("Demo Family", "user@maybe.local", onboarded: false, subscribed: false)
|
||||
|
||||
puts "✅ New user demo data loaded successfully!"
|
||||
end
|
||||
end
|
||||
|
||||
# Generate comprehensive realistic demo data with multi-currency
|
||||
def generate_default_data!(skip_clear: false, email: "user@maybe.local")
|
||||
if skip_clear
|
||||
puts "⏭️ Skipping data clearing (appending new family)..."
|
||||
else
|
||||
puts "🧹 Clearing existing data..."
|
||||
clear_all_data!
|
||||
end
|
||||
|
||||
with_timing(__method__, max_seconds: 1000) do
|
||||
puts "👥 Creating demo family..."
|
||||
family = create_family_and_users!("Demo Family", email, onboarded: true, subscribed: true)
|
||||
|
||||
puts "📊 Creating realistic financial data..."
|
||||
create_realistic_categories!(family)
|
||||
create_realistic_accounts!(family)
|
||||
create_realistic_transactions!(family)
|
||||
# Auto-fill current-month budget based on recent spending averages
|
||||
generate_budget_auto_fill!(family)
|
||||
|
||||
puts "✅ Realistic demo data loaded successfully!"
|
||||
end
|
||||
end
|
||||
|
||||
# Multi-currency support (keeping existing functionality)
|
||||
def generate_multi_currency_data!(family_names)
|
||||
with_timing(__method__) do
|
||||
generate_for_scenario(:multi_currency, family_names)
|
||||
end
|
||||
end
|
||||
|
||||
def clear_all_data!
|
||||
family_count = Family.count
|
||||
|
@ -1226,8 +1217,3 @@ class Demo::Generator
|
|||
puts " ✅ Set property and vehicle valuations"
|
||||
end
|
||||
end
|
||||
|
||||
# Expose public API after full class definition
|
||||
Demo::Generator.public_instance_methods.include?(:generate_default_data!) or Demo::Generator.class_eval do
|
||||
public :generate_empty_data!, :generate_new_user_data!, :generate_default_data!, :generate_multi_currency_data!
|
||||
end
|
||||
|
|
|
@ -53,6 +53,13 @@ class IncomeStatement
|
|||
family_stats(interval: interval).find { |stat| stat.classification == "income" }&.median || 0
|
||||
end
|
||||
|
||||
def warm_caches!(interval: "month")
|
||||
totals
|
||||
family_stats(interval: interval)
|
||||
category_stats(interval: interval)
|
||||
nil
|
||||
end
|
||||
|
||||
private
|
||||
ScopeTotals = Data.define(:transactions_count, :income_money, :expense_money, :missing_exchange_rates?)
|
||||
PeriodTotal = Data.define(:classification, :total, :currency, :missing_exchange_rates?, :category_totals)
|
||||
|
@ -102,21 +109,40 @@ class IncomeStatement
|
|||
|
||||
def family_stats(interval: "month")
|
||||
@family_stats ||= {}
|
||||
@family_stats[interval] ||= FamilyStats.new(family, interval:).call
|
||||
@family_stats[interval] ||= Rails.cache.fetch([
|
||||
"income_statement", "family_stats", family.id, interval, entries_cache_version
|
||||
]) { FamilyStats.new(family, interval:).call }
|
||||
end
|
||||
|
||||
def category_stats(interval: "month")
|
||||
@category_stats ||= {}
|
||||
@category_stats[interval] ||= CategoryStats.new(family, interval:).call
|
||||
@category_stats[interval] ||= Rails.cache.fetch([
|
||||
"income_statement", "category_stats", family.id, interval, entries_cache_version
|
||||
]) { CategoryStats.new(family, interval:).call }
|
||||
end
|
||||
|
||||
def totals_query(transactions_scope:)
|
||||
@totals_query_cache ||= {}
|
||||
cache_key = Digest::MD5.hexdigest(transactions_scope.to_sql)
|
||||
@totals_query_cache[cache_key] ||= Totals.new(family, transactions_scope: transactions_scope).call
|
||||
sql_hash = Digest::MD5.hexdigest(transactions_scope.to_sql)
|
||||
|
||||
Rails.cache.fetch([
|
||||
"income_statement", "totals_query", family.id, sql_hash, entries_cache_version
|
||||
]) { Totals.new(family, transactions_scope: transactions_scope).call }
|
||||
end
|
||||
|
||||
def monetizable_currency
|
||||
family.currency
|
||||
end
|
||||
|
||||
# Returns a monotonically increasing integer based on the most recent
|
||||
# update to any Entry that belongs to the family. Incorporated into cache
|
||||
# keys so they expire automatically on data changes.
|
||||
def entries_cache_version
|
||||
@entries_cache_version ||= begin
|
||||
ts = Entry.joins(:account)
|
||||
.where(accounts: { family_id: family.id })
|
||||
.maximum(:updated_at)
|
||||
|
||||
ts.present? ? ts.to_i : 0
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -91,7 +91,7 @@
|
|||
# Calculate weight as percentage of classification total
|
||||
classification_total = classification_group.total_money.amount
|
||||
account_weight = classification_total.zero? ? 0 : account.converted_balance / classification_total * 100
|
||||
%>
|
||||
%>
|
||||
<%= render "pages/dashboard/group_weight", weight: account_weight, color: account_group.color %>
|
||||
</div>
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue