Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix memory leaks on historyHook and workspaceHistoryHook #653

Merged
merged 4 commits into from
Nov 24, 2021

Conversation

RubenAstudillo
Copy link
Contributor

@RubenAstudillo RubenAstudillo commented Nov 22, 2021

Description

Both of these hooks leaked memory on long running sessions.
per-cost-center

To fix it I had to force the thunks stored on the hooks as the compiler wouldn't be able to see where they would be demanded as they depended on the user interaction. Now the graph looks like this.
fixed-leak

The most polemic change on this PR is the inclusion of the parallel library as a dependency. I needed for the Control.Seq module which gave me combinators to force just enough of the Data.Seq and [] data types.

Checklist

  • [🗸] I've read CONTRIBUTING.md

  • [🗸] I've considered how to best test these changes (property, unit,
    manually, ...) and concluded: It shows the memory improvements!

  • [🗸] I updated the CHANGES.md file (None)

@RubenAstudillo RubenAstudillo marked this pull request as ready for review November 22, 2021 20:33
Copy link
Member

@slotThe slotThe left a comment

Choose a reason for hiding this comment

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

This is neat, thanks!

I wonder if we could get away with just stictifying the respective data structures (since neither is exported); i.e.,

data SP a b = SP !a !b
  deriving (Read, Show)

newtype WorkspaceHistory = WorkspaceHistory
  { history :: [StrictPair ScreenId WorkspaceId]
    -- ^ Workspace Screens in reverse-chronological order.
  } deriving (Read, Show)

---

data HistoryDB = HistoryDB !(Maybe Window) -- currently focused window
                           !(Seq Window)   -- previously focused windows
               deriving (Read, Show)

This is still not ultra-strict but since we're not really interesting in forcing things more than to WHNF it should be fine I think. We may or may not still need the odd call to seq to force the list, not entirely sure about that right now

EDIT: Oh, and

* [🗸] I've considered how to best test these changes (property, unit,
  manually, ...) and concluded: XXX

:)

@RubenAstudillo
Copy link
Contributor Author

I wonder if we could get away with just stictifying the respective data structures (since neither is exported); i.e.,

data SP a b = SP !a !b
  deriving (Read, Show)

newtype WorkspaceHistory = WorkspaceHistory
  { history :: [StrictPair ScreenId WorkspaceId]
    -- ^ Workspace Screens in reverse-chronological order.
  } deriving (Read, Show)

---

data HistoryDB = HistoryDB !(Maybe Window) -- currently focused window
                           !(Seq Window)   -- previously focused windows
               deriving (Read, Show)

This is still not ultra-strict but since we're not really interesting in forcing things more than to WHNF it should be fine I think. We may or may not still need the odd call to seq to force the list, not entirely sure about that right now

We are interested on forcing things to more than WHNF. On the WorkspaceHistory patch I needed to force at least until each element of each pair on the history list. WHNF on that data structure would be the first cons of that list, not enough.

On a related note, given that we are concerned with forcing evaluation until certain "depth", using strict data types as StrictPair as above is not enough. We would also need:

  • A strict list container so we match to WHNF any StrictPair.
  • Match on the WorkspaceHistory to WHNF before the put calls. As it is a newtype, it doesn't have extra bottoms. But we still need its head matched, so it forces the strict list and then the StrictPair.

So, instead of doing those two things, we can use the combinators on Control.Seq and match before the put call to get the correct behavior.

The same is true in respect to HistoryDB. We would need a strict Seq data structure. A bang pattern as !(Seq Window) is not enough to deal with this leak as it forces only the head. Every other element remains as a thunk until a undefined point in the future.

EDIT: Oh, and

* [🗸] I've considered how to best test these changes (property, unit,
  manually, ...) and concluded: XXX

Thanks. I should have put something on those XXX. I edited that part on the original post.

updateHistory leaks unfiltered windows from previous states as it is never
forced. The consumer of such data structure is not visible to ghc, so the
demand analysis has to fallback on pure laziness.

We fix this inserting evaluation points on the `historyHook` function. We do
this for two reasons, this is the only function calling `updateHistory`.
Plus we cannot do it clearly at the `updateHistory` function as we operate
inside a continuation on withWindowSet. In respect to the `put`, everything
would be a big thunk.
The XS.modify was leaving thunk on the history that the demand analyser
could not prove to be neccesary as they depended on the future user
interaction. This was bad as the time advance there was less and less
neccesity to force such value, so the thunk would be increasing. Since the
datatypes that the `WorkspaceHistory` are really simple, we can just
evaluate and save a good chunk of memory.
@RubenAstudillo
Copy link
Contributor Author

I was missing the parallel dependency for the test build. Allow the workflows to be re run.

@liskin
Copy link
Member

liskin commented Nov 23, 2021

The most polemic change on this PR is the inclusion of the parallel library as a dependency. I needed for the Control.Seq module which gave me combinators to force just enough of the Data.Seq and [] data types.

Would https://hackage.haskell.org/package/deepseq work here as well? Both seem to be maintained by the Core Libraries Committee but deepseq is distributed with GHC so adding that dependency is easier.

@RubenAstudillo
Copy link
Contributor Author

Yeah, we are basically doing the same as in deepseq now. It is a better dependency. I will use that, re-run my tests and publish an update.

@RubenAstudillo
Copy link
Contributor Author

RubenAstudillo commented Nov 23, 2021

@liskin using deepseq exposes other tradeoffs. The NFData typeclass instance has to be derived for WorkspaceHistory. This can be done on two ways per the docs

  1. Derive Generic, Generic1 and then NFData. The Generics instances are just to for using the derive anyclass instance for NFData. But these are heavyweigh for just forcing.
  2. Use GeneralizedNewtypeDeriving for deriving NFData. This requires a NFData instance on ScreenId on the main xmonad project.

I consider both of these alternatives worse than the current status. Specially considering ScreenId is a newtype, so it doesn't introduce an extra bottom between it and the base Int it contains. So the strictness of ScreenId is equivalent to a shallow seq. I use this on the current code but it is not apparent for the NFData deriving mechanism.

I vote for keep using parallel instead of deepseq. Thoughts?

@slotThe
Copy link
Member

slotThe commented Nov 24, 2021

I vote for keep using parallel instead of deepseq. Thoughts?

While GeneralizedNewtypeDeriving is probably a no-go as we would like to keep compatibility with xmonad 0.17.0 a bit longer than a few weeks (well, I would, anyways), I don't really see a problem with deriving Generic. It's not like this will affect a lot of data structures such that actual compile times are affected.

I consider both of these alternatives worse than the current status. Specially considering ScreenId is a newtype, so it doesn't introduce an extra bottom between it and the base Int it contains. So the strictness of ScreenId is equivalent to a shallow seq. I use this on the current code but it is not apparent for the NFData deriving mechanism.

While this is true, I think the idea of what we want to achieve is expressed much more clearly by "this is an instance of NFData".

@RubenAstudillo
Copy link
Contributor Author

RubenAstudillo commented Nov 24, 2021

@slotThe You are right in that the intent is more clear with an NFData instance!
fixed-leak-deepseq
The memory figures are basically the same. I still had to do an instance by hand but the liftRnf/liftRnf2 and rwhnf from deepseq were useful. Can you give permission to re-run the GH workflows?

@RubenAstudillo
Copy link
Contributor Author

I think is merge ready.

Copy link
Member

@slotThe slotThe left a comment

Choose a reason for hiding this comment

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

LGTM now!

XMonad/Util/ExtensibleState.hs Outdated Show resolved Hide resolved
@RubenAstudillo
Copy link
Contributor Author

RubenAstudillo commented Nov 24, 2021

Using the latest version with $! the memory profile is the same

fixed-leak-deepseq

Again, let the workflows re-run and I think is ready to merge. Thanks for all you patience and time @slotThe and @liskin 💪 .

@slotThe slotThe merged commit 3d71669 into xmonad:master Nov 24, 2021
@slotThe
Copy link
Member

slotThe commented Nov 24, 2021

Thanks!

liskin added a commit to liskin/xmonad-contrib that referenced this pull request Jan 15, 2022
Simplify stuff a bit. Prevent memory leaks additionally in:
`workspaceHistoryTransaction` and `workspaceHistoryModify`.

Related: xmonad#653
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants