1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-07-18 20:59:39 +02:00

Temporary transactions page performance fix (#2372)
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions

* Temporary transactions page performance fix

* Fix Cursor bugs

* More bugbot bug fixes
This commit is contained in:
Zach Gollwitzer 2025-06-15 11:36:21 -04:00 committed by GitHub
parent a5f1677f60
commit 6d9bb7f0eb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -3,6 +3,8 @@ class TransactionsController < ApplicationController
before_action :store_params!, only: :index
require "digest/md5"
def new
super
@income_categories = Current.family.categories.incomes.alphabetically
@ -15,35 +17,91 @@ class TransactionsController < ApplicationController
set_focused_record(transactions_query, params[:focused_record_id], default_per_page: 50)
@pagy, @transactions = pagy(
transactions_query.includes(
{ entry: :account },
:category, :merchant, :tags,
transfer_as_outflow: { inflow_transaction: { entry: :account } },
transfer_as_inflow: { outflow_transaction: { entry: :account } }
).reverse_chronological,
limit: params[:per_page].presence || default_params[:per_page],
params: ->(params) { params.except(:focused_record_id) }
# ------------------------------------------------------------------
# Cache the expensive includes & pagination block so the DB work only
# runs when either the query params change *or* any entry has been
# updated for the current family.
# ------------------------------------------------------------------
latest_update_ts = Current.family.entries.maximum(:updated_at)&.utc&.to_i || 0
items_per_page = (params[:per_page].presence || default_params[:per_page]).to_i
items_per_page = 1 if items_per_page <= 0
current_page = (params[:page].presence || default_params[:page]).to_i
current_page = 1 if current_page <= 0
# Build a compact cache digest: sanitized filters + page info + a
# token that changes on updates *or* deletions.
entries_changed_token = [ latest_update_ts, Current.family.entries.count ].join(":")
digest_source = {
q: @q, # processed & sanitised search params
page: current_page, # requested page number
per: items_per_page, # page size
tok: entries_changed_token
}.to_json
cache_key = Current.family.build_cache_key(
"transactions_idx_#{Digest::MD5.hexdigest(digest_source)}"
)
# -------------------------------------------------------------------
# Cache totals
# -------------------------------------------------------------------
# Totals calculation is expensive (heavy SQL with grouping). We cache the
# result keyed by:
# • Family id
# • The family-level cache key that already embeds entries.maximum(:updated_at)
# • A digest of the current search params so each distinct filter set gets
# its own cache entry.
# When any entry is created/updated/deleted, the family cache key changes,
# automatically invalidating all related totals.
cache_data = Rails.cache.fetch(cache_key, expires_in: 30.minutes) do
current_page_i = current_page
params_digest = Digest::MD5.hexdigest(@q.to_json)
cache_key = Current.family.build_cache_key("transactions_totals_#{params_digest}")
# Initial query
offset = (current_page_i - 1) * items_per_page
ids = transactions_query
.reverse_chronological
.limit(items_per_page)
.offset(offset)
.pluck(:id)
@totals = Rails.cache.fetch(cache_key) do
Current.family.income_statement.totals(transactions_scope: transactions_query)
total_count = transactions_query.count
if ids.empty? && total_count.positive? && current_page_i > 1
current_page_i = (total_count.to_f / items_per_page).ceil
offset = (current_page_i - 1) * items_per_page
ids = transactions_query
.reverse_chronological
.limit(items_per_page)
.offset(offset)
.pluck(:id)
end
{ ids: ids, total_count: total_count, current_page: current_page_i }
end
ids = cache_data[:ids]
total_count = cache_data[:total_count]
current_page = cache_data[:current_page]
# Build Pagy object (this part is cheap done *after* potential
# page fallback so the pagination UI reflects the adjusted page
# number).
@pagy = Pagy.new(
count: total_count,
page: current_page,
items: items_per_page,
params: ->(p) { p.except(:focused_record_id) }
)
# Fetch the transactions in the cached order
@transactions = Current.family.transactions
.active
.where(id: ids)
.includes(
{ entry: :account },
:category, :merchant, :tags,
transfer_as_outflow: { inflow_transaction: { entry: :account } },
transfer_as_inflow: { outflow_transaction: { entry: :account } }
)
# Preserve the order defined by `ids`
@transactions = ids.map { |id| @transactions.detect { |t| t.id == id } }.compact
@totals = Current.family.income_statement.totals(transactions_scope: transactions_query)
end
def clear_filter