Skip to content

Commit 7309062

Browse files
mpickeringcocreature
authored andcommitted
Improve performance by caching conversion to NormalizedUri (#384)
* Cache conversion to NormalizedUri from NormalizedFilePath This conversion is quite expensive to repeat multiple times. Therefore we cache it when creating a NormalizedFilePath so it's only computed once. Making this change causes a benchmark which calls hover 1000 times to go down from 259s to 44s. * Use a HashMap rather than a Map for debouncer NormalizedUri is the primary key type for the debouncer which will have a cached hash in the next haskell-lsp release. * NormalizedFilePath: Make the hash strict so it can be unpacked
1 parent e392c49 commit 7309062

File tree

2 files changed

+48
-11
lines changed

2 files changed

+48
-11
lines changed

src/Development/IDE/Core/Debouncer.hs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@ import Control.Concurrent.Extra
1111
import Control.Concurrent.Async
1212
import Control.Exception
1313
import Control.Monad.Extra
14-
import Data.Map.Strict (Map)
15-
import qualified Data.Map.Strict as Map
14+
import Data.Hashable
15+
import Data.HashMap.Strict (HashMap)
16+
import qualified Data.HashMap.Strict as Map
1617
import System.Time.Extra
1718

1819
-- | A debouncer can be used to avoid triggering many events
@@ -21,7 +22,7 @@ import System.Time.Extra
2122
-- by delaying each event for a given time. If another event
2223
-- is registered for the same key within that timeframe,
2324
-- only the new event will fire.
24-
newtype Debouncer k = Debouncer (Var (Map k (Async ())))
25+
newtype Debouncer k = Debouncer (Var (HashMap k (Async ())))
2526

2627
-- | Create a new empty debouncer.
2728
newDebouncer :: IO (Debouncer k)
@@ -35,7 +36,7 @@ newDebouncer = do
3536
-- If there is a pending event for the same key, the pending event will be killed.
3637
-- Events are run unmasked so it is up to the user of `registerEvent`
3738
-- to mask if required.
38-
registerEvent :: Ord k => Debouncer k -> Seconds -> k -> IO () -> IO ()
39+
registerEvent :: (Eq k, Hashable k) => Debouncer k -> Seconds -> k -> IO () -> IO ()
3940
registerEvent (Debouncer d) delay k fire = modifyVar_ d $ \m -> mask_ $ do
4041
whenJust (Map.lookup k m) cancel
4142
a <- asyncWithUnmask $ \unmask -> unmask $ do

src/Development/IDE/Types/Location.hs

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,22 +49,55 @@ import Language.Haskell.LSP.Types as LSP (
4949
)
5050
import SrcLoc as GHC
5151
import Text.ParserCombinators.ReadP as ReadP
52+
import GHC.Generics
5253

5354

5455
-- | Newtype wrapper around FilePath that always has normalized slashes.
55-
newtype NormalizedFilePath = NormalizedFilePath FilePath
56-
deriving (Eq, Ord, Show, Hashable, NFData, Binary)
56+
-- The NormalizedUri and hash of the FilePath are cached to avoided
57+
-- repeated normalisation when we need to compute them (which is a lot).
58+
--
59+
-- This is one of the most performance critical parts of ghcide, do not
60+
-- modify it without profiling.
61+
data NormalizedFilePath = NormalizedFilePath NormalizedUriWrapper !Int !FilePath
62+
deriving (Generic, Eq, Ord)
63+
64+
instance NFData NormalizedFilePath where
65+
instance Binary NormalizedFilePath where
66+
put (NormalizedFilePath _ _ fp) = put fp
67+
get = do
68+
v <- Data.Binary.get :: Get FilePath
69+
return (toNormalizedFilePath v)
70+
71+
72+
instance Show NormalizedFilePath where
73+
show (NormalizedFilePath _ _ fp) = "NormalizedFilePath " ++ show fp
74+
75+
instance Hashable NormalizedFilePath where
76+
hash (NormalizedFilePath _ h _) = h
77+
78+
-- Just to define NFData and Binary
79+
newtype NormalizedUriWrapper =
80+
NormalizedUriWrapper { unwrapNormalizedFilePath :: NormalizedUri }
81+
deriving (Show, Generic, Eq, Ord)
82+
83+
instance NFData NormalizedUriWrapper where
84+
rnf = rwhnf
85+
86+
87+
instance Hashable NormalizedUriWrapper where
5788

5889
instance IsString NormalizedFilePath where
5990
fromString = toNormalizedFilePath
6091

6192
toNormalizedFilePath :: FilePath -> NormalizedFilePath
6293
-- We want to keep empty paths instead of normalising them to "."
63-
toNormalizedFilePath "" = NormalizedFilePath ""
64-
toNormalizedFilePath fp = NormalizedFilePath $ normalise fp
94+
toNormalizedFilePath "" = NormalizedFilePath (NormalizedUriWrapper emptyPathUri) (hash ("" :: String)) ""
95+
toNormalizedFilePath fp =
96+
let nfp = normalise fp
97+
in NormalizedFilePath (NormalizedUriWrapper $ filePathToUriInternal' nfp) (hash nfp) nfp
6598

6699
fromNormalizedFilePath :: NormalizedFilePath -> FilePath
67-
fromNormalizedFilePath (NormalizedFilePath fp) = fp
100+
fromNormalizedFilePath (NormalizedFilePath _ _ fp) = fp
68101

69102
-- | We use an empty string as a filepath when we don’t have a file.
70103
-- However, haskell-lsp doesn’t support that in uriToFilePath and given
@@ -76,10 +109,13 @@ uriToFilePath' uri
76109
| otherwise = LSP.uriToFilePath uri
77110

78111
emptyPathUri :: NormalizedUri
79-
emptyPathUri = filePathToUri' ""
112+
emptyPathUri = filePathToUriInternal' ""
80113

81114
filePathToUri' :: NormalizedFilePath -> NormalizedUri
82-
filePathToUri' (NormalizedFilePath fp) = toNormalizedUri $ Uri $ T.pack $ LSP.fileScheme <> "//" <> platformAdjustToUriPath fp
115+
filePathToUri' (NormalizedFilePath (NormalizedUriWrapper u) _ _) = u
116+
117+
filePathToUriInternal' :: FilePath -> NormalizedUri
118+
filePathToUriInternal' fp = toNormalizedUri $ Uri $ T.pack $ LSP.fileScheme <> "//" <> platformAdjustToUriPath fp
83119
where
84120
-- The definitions below are variants of the corresponding functions in Language.Haskell.LSP.Types.Uri that assume that
85121
-- the filepath has already been normalised. This is necessary since normalising the filepath has a nontrivial cost.

0 commit comments

Comments
 (0)