Skip to content

Fix infinite memory usage by allowing old data to be garbage collected#2473

Open
alerque wants to merge 4 commits intosimonmichael:masterfrom
alerque:ui-leak
Open

Fix infinite memory usage by allowing old data to be garbage collected#2473
alerque wants to merge 4 commits intosimonmichael:masterfrom
alerque:ui-leak

Conversation

@alerque
Copy link
Collaborator

@alerque alerque commented Oct 3, 2025

Fixes #1825

... at least the increasing memory usage on each press of g is fixed by making new data structures and allowing the old data to be garbage collected. I'm not sure about CPU usage as that did not go up appreciably in my repro for this issue, and I haven't tested this long enough to know if it also solves the ‘leave running for days’ variant of the problem rather than the repeated manual refresh variant, but it seems likely it will.

@alerque alerque added the A-BUG Something wrong, confusing or sub-standard in the software, docs, or user experience. label Oct 3, 2025
@alerque alerque requested a review from simonmichael October 3, 2025 22:44
@alerque
Copy link
Collaborator Author

alerque commented Oct 4, 2025

See issue comments for updates on testing, I did do a longer term run test and got more conclusive results than the initial speculation here.

@simonmichael
Copy link
Owner

I haven't had much quality hacking time just lately - but I am loving this burst of activity, will follow up asap!

@alerque
Copy link
Collaborator Author

alerque commented Oct 6, 2025

I just converted this to a draft again because the latest commits here have not been properly vetted. I'm hacking wildly trying to make out where the leak(s) are. The current iteration runs much better for me including not leaking memory from the registry screen, but I haven't reasoned through everything enough to verify functionality won't have been lost.

@alerque alerque force-pushed the ui-leak branch 2 times, most recently from 4740f83 to 82bbc97 Compare October 6, 2025 23:49
Copy link
Owner

@simonmichael simonmichael left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice strictness investigations. I have noted some questions.

e | e `elem` [AppEvent FileChange, VtyEvent (EvKey (KChar 'g') [])] -> uiReload copts d ui >>= put'
e | e `elem` [AppEvent FileChange, VtyEvent (EvKey (KChar 'g') [])] -> do
ui' <- uiReload copts d ui
put' $ regenerateScreens (ajournal ui') d ui'
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reloading in the register screen now has an extra call to regenerateScreens. Does this help ? I think it's also being called by uiReload. Needs an explanatory comment at least.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes it is extra work but without the extra call to regenerateScreens here some of the data that used to be attached to the UI before the reload but hadn't been displayed yet and hence not fully processed are not released and they end up becoming dangling thunks. I'm sure this is not the most efficient way to handle this, but the alternative is a memory leak and my Haskell foo is not up to a better fix.

-- sendVtyEvents [EvKey KEnter [], EvKey (KChar 'g') []] -- XXX Might be disrupted if other events are queued
tsReload copts d ui = uiReload copts d ui >>= put'

tsReloadIfFileChanged copts d j ui = liftIO (uiReloadIfFileChanged copts d j ui) >>= put'
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This special handling for regenerating transaction screen was added in 1.50.1 to fix #2288. It has been removed and I think not fully replaced by later commits ? Also is the comment above still accurate ?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PS a little more context, my understanding was that the right fix for #2288 would be to make regenerateScreens take parent screens' state into account (fold rather than map).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR does not regress #2288 if that's what your asking. It represents a bit more aggressive way of fixing that doesn't leave unused thunks that cannot be garbage collected.

No the comment is not accurate any more, I'll remove that too.

updatedTxn = case find (\t -> tindex t == tindex currentTxn) (jtxns j) of
Just t -> t
Nothing -> currentTxn -- fallback to current if not found
in tss { _tssTransaction = (currentIdx, updatedTxn) }
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this replace the special regeneration previously in TransactionScreen.hs ?
Jumping to a different transaction after reload or file change is not ideal.
I think the old code exited to the register screen in that situation.

Copy link
Collaborator Author

@alerque alerque Dec 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this does effectively do what the removed code was doing. I also agree jumping to a different transaction if the data changes on disk and the new data has different transaction indexing is not ideal, but this does not seem to be a regression. It does the same thing for me in 1.51.1 as well. Also both old and new versions fall pretty flat if the index they were looking at no longer exists at all. I think that would be a good case for a separate bug report though as it isn't a behavior regression and adding features while trying to chase down memory leaks doesn't sound like my idea of a good time.

RS rss -> RS $! rsUpdate (aopts ui') d j rss
TS tss -> TS $! tsUpdate (aopts ui') d j tss
ES _ -> s
in ui'
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Screen types and functions are listed explicitly in a bunch of places, so maybe it's no great loss to do that here also - but is it possible to achieve the same strictness without listing them all ?

More seriously, doesn't it now only regenerate the current screen ?

Copy link
Owner

@simonmichael simonmichael Oct 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Comments so far are based on code review, I haven't tested yet. Unfortunately it's easy to break behaviours in hledger-ui in ways that currently are hard to test, except manually.)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤷‍♂️ Yes, these are listed in like half a dozen places, I'm sure that could be refactored. What wasn't working was the previous "smart" way of updating just a bit of the screen was orphaning unresolved data structures and accumulated thunks.

Yes, as I understand it, it is now only regenerating the current screen, but before it does that it forces a full copy of the journal data in memory so the old one can be garbage collected. Other screens will generate when you navigate to them, so my general impression is this is a more sound approach. I still think there are holes here and the whole UI feels upside down to me. That might be because the architecture really is kind of backwards or it might be I still haven't really gotten my head around Haskell. The smart money is on the latter, but it's probably some of both.

@alerque alerque force-pushed the ui-leak branch 2 times, most recently from a95408c to e1b9cad Compare December 9, 2025 21:45
@alerque alerque marked this pull request as ready for review December 9, 2025 21:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-BUG Something wrong, confusing or sub-standard in the software, docs, or user experience.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

hledger-ui --watch consumes more CPU and RAM over time [$150 x 3]

2 participants