| CARVIEW |
Navigation Menu
-
Notifications
You must be signed in to change notification settings - Fork 94
Fix 3 space leaks and refactoring of PositionMapping #557
Conversation
Eta-expanding the function means GHC no longer allocates a function closure every time `withProgress` is called (which is a lot). See: https://www.joachim-breitner.de/blog/763-Faster_Winter_5__Eta-Expanding_ReaderT
Ensure that PositionMappings are shared between versions There was a quadratic space leak as the tails of the position maps were not shared with each other. Now the space usage is linear which is produces more acceptable levels of residency after 3000 modifications.
pepeiborra
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice work!
| (from1 >=> from2) | ||
|
|
||
| idDelta :: PositionDelta | ||
| idDelta = PositionDelta Just Just |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not define Monoid and Semigroup instances for PositionDelta?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It didn't occur to me in particular. I'm a GHC developer so I prefer having explicitly named functions!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lies. If you were a real GHC developer you'd have used ; { and your commit would have been rendered in comic sans.
src/Development/IDE/Core/Shake.hs
Outdated
| withProgress :: (Eq a, Hashable a) => Var (HMap.HashMap a Int) -> a -> Action b -> Action b | ||
| withProgress var file = actionBracket (f succ) (const $ f pred) . const | ||
| where f shift = modifyVar_ var $ return . HMap.alter (Just . shift . fromMaybe 0) file | ||
| where f shift = modifyVar_ var $ \x -> return (HMap.alter (Just . shift . fromMaybe 0) file x) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The linked blog post by @nomeata explains why this can lead to performance gains, but it isn't clear that it fixes a space leak
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I profiled a session where I repeatedly modified the file and could observe this function closure growing in the profile. I eta-expanded and the band was no longer present in the profile. So I agree it's not clear why this fixes the leak I don't think it should block merging.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we make it evaluate (HMap.alter - I suspect the space leak is removed because GHC manages to optimise a little bit more in some place. The evaluate would mean the operation guarantees to run in the IO monad and not stack up.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
| ,positionMapping :: Var (HMap.HashMap NormalizedUri (Map TextDocumentVersion PositionMapping)) | ||
| ,positionMapping :: Var (HMap.HashMap NormalizedUri (Map TextDocumentVersion (PositionMapping, PositionMapping))) | ||
| -- ^ Map from a text document version to a PositionMapping that describes how to map | ||
| -- positions in a version of that document to positions in the latest version |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unrelated to this change, but is positionMapping ever flushed? It's a space leak that grows with every edit, isn't it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes it grows every edit, I have some thoughts about doing some garbage collection but now the growth is only linear it is not as serious a problem. We don't end up with 1gb of residency just from the position map.
For example, we could remove the whole position map from removing a file from FOI as well as flush out part of the Values map.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that we do have some code for this in DAML https://github.com/digital-asset/daml/blob/master/compiler/damlc/daml-ide-core/src/Development/IDE/Core/Rules/Daml.hs#L946
| withProgress :: (Eq a, Hashable a) => Var (HMap.HashMap a Int) -> a -> Action b -> Action b | ||
| withProgress var file = actionBracket (f succ) (const $ f pred) . const | ||
| where f shift = modifyVar_ var $ \x -> return (HMap.alter (Just . shift . fromMaybe 0) file x) | ||
| where f shift = modifyVar_ var $ \x -> return (HMap.alter (\x -> Just (shift (fromMaybe 0 x))) file x) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we make this Just $! (shift ...). I suspect this does fix the space leak, but by luck rather than guarantee. A $! would be a guarantee.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
ndmitchell
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome work!
| withProgress var file = actionBracket (f succ) (const $ f pred) . const | ||
| -- This functions are deliberately eta-expanded to avoid space leaks. | ||
| -- Do not remove the eta-expansion without profiling a session with at | ||
| -- least 1000 modifications. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Once we are using $! and evaluate we can hang these comments off more solid guarantees.
|
Could we get a ticket for the "we reduce the space leak from quadratic to linear", since presumably eventually that linear will need fixing too. |
cocreature
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great! I agree with @ndmitchell that some of the changes would benefit from a bit of evaluate or $!.
| ,positionMapping :: Var (HMap.HashMap NormalizedUri (Map TextDocumentVersion PositionMapping)) | ||
| ,positionMapping :: Var (HMap.HashMap NormalizedUri (Map TextDocumentVersion (PositionMapping, PositionMapping))) | ||
| -- ^ Map from a text document version to a PositionMapping that describes how to map | ||
| -- positions in a version of that document to positions in the latest version |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that we do have some code for this in DAML https://github.com/digital-asset/daml/blob/master/compiler/damlc/daml-ide-core/src/Development/IDE/Core/Rules/Daml.hs#L946
src/Development/IDE/Core/Shake.hs
Outdated
| withProgress :: (Eq a, Hashable a) => Var (HMap.HashMap a Int) -> a -> Action b -> Action b | ||
| withProgress var file = actionBracket (f succ) (const $ f pred) . const | ||
| where f shift = modifyVar_ var $ return . HMap.alter (Just . shift . fromMaybe 0) file | ||
| where f shift = modifyVar_ var $ \x -> return (HMap.alter (Just . shift . fromMaybe 0) file x) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
| withProgress :: (Eq a, Hashable a) => Var (HMap.HashMap a Int) -> a -> Action b -> Action b | ||
| withProgress var file = actionBracket (f succ) (const $ f pred) . const | ||
| where f shift = modifyVar_ var $ \x -> return (HMap.alter (Just . shift . fromMaybe 0) file x) | ||
| where f shift = modifyVar_ var $ \x -> return (HMap.alter (\x -> Just (shift (fromMaybe 0 x))) file x) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
|
It may take me a while to get around to making these changes as I will have to profile ghcide again in order to verify the suggestions work correctly. |
|
We are somewhat blocked here because I don't want to spend any more time profiling, when I know these changes fix the leaks, but you don't want to merge because there is a suggestion other ways to fix them would be preferable. My preference is to merge the patch as-is and make tickets for the other suggested changes which can be picked up by other people. It is unfortunate to just leave these leaks unfixed whilst we are in this impasse. The script I used to profile is in my repo - https://github.com/mpickering/ghcide/blob/hls/test-leak |
|
Sounds sensible, still need to fix the CI errors though before we can merge it. |
|
The fixes as written are inherently fragile, relying on subtle compiler invariants, so I suspect they don't fix it on all versions and won't fix it if we breathe on the code funny. However, the points you've identified are clearly space leaks, and based on my history of hunting space leaks I can understand exactly why they are space leaks, and what the robust fix is - which I put in the comment. My recommendation would be to switch to the robust versions, which I'd give a 90%+ probability of fixing now and continuing to fix in the future. If you're not happy with doing that in this patch I suggest I follow up with the changes I identified, but non-space-leak tested, and we merge that. I agree that retesting would be ideal, but without a CI infrastructure that spots these leaks robustly, we're somewhat flying blind and hopeful anyway, so I think anything we do is not ideal. |
A Delta is a change between two versions A Mapping is a change from the current version to a specific older version. Fix hlint Fix hlint
b09b620 to
36a5940
Compare
|
Happy to merge the more robust fixes separately. |
Follow up from haskell#557. We definitely want the progress state to be fully evaluated, so demand that with evaluating functions like evaluate and $!, rather than relying on the compiler to get it right. My guess is the `$!` is unnecessary now we have `evaluate`, but it's also not harmful, so belt and braces approach.
Follow up from #557. We definitely want the progress state to be fully evaluated, so demand that with evaluating functions like evaluate and $!, rather than relying on the compiler to get it right. My guess is the `$!` is unnecessary now we have `evaluate`, but it's also not harmful, so belt and braces approach.
) * Rats: Fix space leak in withProgress Eta-expanding the function means GHC no longer allocates a function closure every time `withProgress` is called (which is a lot). See: https://www.joachim-breitner.de/blog/763-Faster_Winter_5__Eta-Expanding_ReaderT * Rats: Share computation of position mapping Ensure that PositionMappings are shared between versions There was a quadratic space leak as the tails of the position maps were not shared with each other. Now the space usage is linear which is produces more acceptable levels of residency after 3000 modifications. * Rats: Eta-expand modification function See: https://www.joachim-breitner.de/blog/763-Faster_Winter_5__Eta-Expanding_ReaderT * Add a comment warning about eta-reducing * Distinguish between a Delta and a Mapping in PositionMapping A Delta is a change between two versions A Mapping is a change from the current version to a specific older version. Fix hlint Fix hlint
Follow up from haskell/ghcide#557. We definitely want the progress state to be fully evaluated, so demand that with evaluating functions like evaluate and $!, rather than relying on the compiler to get it right. My guess is the `$!` is unnecessary now we have `evaluate`, but it's also not harmful, so belt and braces approach.
) * Rats: Fix space leak in withProgress Eta-expanding the function means GHC no longer allocates a function closure every time `withProgress` is called (which is a lot). See: https://www.joachim-breitner.de/blog/763-Faster_Winter_5__Eta-Expanding_ReaderT * Rats: Share computation of position mapping Ensure that PositionMappings are shared between versions There was a quadratic space leak as the tails of the position maps were not shared with each other. Now the space usage is linear which is produces more acceptable levels of residency after 3000 modifications. * Rats: Eta-expand modification function See: https://www.joachim-breitner.de/blog/763-Faster_Winter_5__Eta-Expanding_ReaderT * Add a comment warning about eta-reducing * Distinguish between a Delta and a Mapping in PositionMapping A Delta is a change between two versions A Mapping is a change from the current version to a specific older version. Fix hlint Fix hlint
Follow up from haskell/ghcide#557. We definitely want the progress state to be fully evaluated, so demand that with evaluating functions like evaluate and $!, rather than relying on the compiler to get it right. My guess is the `$!` is unnecessary now we have `evaluate`, but it's also not harmful, so belt and braces approach.
) * Rats: Fix space leak in withProgress Eta-expanding the function means GHC no longer allocates a function closure every time `withProgress` is called (which is a lot). See: https://www.joachim-breitner.de/blog/763-Faster_Winter_5__Eta-Expanding_ReaderT * Rats: Share computation of position mapping Ensure that PositionMappings are shared between versions There was a quadratic space leak as the tails of the position maps were not shared with each other. Now the space usage is linear which is produces more acceptable levels of residency after 3000 modifications. * Rats: Eta-expand modification function See: https://www.joachim-breitner.de/blog/763-Faster_Winter_5__Eta-Expanding_ReaderT * Add a comment warning about eta-reducing * Distinguish between a Delta and a Mapping in PositionMapping A Delta is a change between two versions A Mapping is a change from the current version to a specific older version. Fix hlint Fix hlint
Follow up from haskell/ghcide#557. We definitely want the progress state to be fully evaluated, so demand that with evaluating functions like evaluate and $!, rather than relying on the compiler to get it right. My guess is the `$!` is unnecessary now we have `evaluate`, but it's also not harmful, so belt and braces approach.
Please review this commit by commit.
The first three commits are just about fixing space leaks.
The fourth commit is a comment warning about not unfixing the leak.
The fifth commit is a refactoring which makes the correctness of the first commit more obvious but seemed distracting to include in the same commit.