Skip to content

Commit 7080db9

Browse files
authored
Finer dependencies for GhcSessionFun (#643)
* Cache the results of loadSession until the components change * Track the cradle dependencies * hlint * Add cradle to watched files test * Add comment on sessionVersion field
1 parent ba4bdb2 commit 7080db9

File tree

4 files changed

+88
-58
lines changed

4 files changed

+88
-58
lines changed

Diff for: exe/Main.hs

+42-36
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import Control.Concurrent.Extra
1818
import Control.Exception.Safe
1919
import Control.Monad.Extra
2020
import Control.Monad.IO.Class
21+
import Data.Bifunctor (Bifunctor(second))
2122
import Data.Default
2223
import Data.Either
2324
import Data.Foldable (for_)
@@ -122,7 +123,8 @@ main = do
122123
runLanguageServer options (pluginHandler plugins) onInitialConfiguration onConfigurationChange $ \getLspId event vfs caps wProg wIndefProg -> do
123124
t <- t
124125
hPutStrLn stderr $ "Started LSP server in " ++ showDuration t
125-
let options = (defaultIdeOptions $ loadSessionShake dir)
126+
sessionLoader <- loadSession dir
127+
let options = (defaultIdeOptions sessionLoader)
126128
{ optReportProgress = clientSupportsProgress caps
127129
, optShakeProfiling = argsShakeProfiling
128130
, optTesting = IdeTesting argsTesting
@@ -154,7 +156,8 @@ main = do
154156
vfs <- makeVFSHandle
155157
debouncer <- newAsyncDebouncer
156158
let dummyWithProg _ _ f = f (const (pure ()))
157-
ide <- initialise def mainRule (pure $ IdInt 0) (showEvent lock) dummyWithProg (const (const id)) (logger minBound) debouncer (defaultIdeOptions $ loadSessionShake dir) vfs
159+
sessionLoader <- loadSession dir
160+
ide <- initialise def mainRule (pure $ IdInt 0) (showEvent lock) dummyWithProg (const (const id)) (logger minBound) debouncer (defaultIdeOptions sessionLoader) vfs
158161

159162
putStrLn "\nStep 4/4: Type checking the files"
160163
setFilesOfInterest ide $ HashSet.fromList $ map toNormalizedFilePath' files
@@ -223,40 +226,43 @@ targetToFile _ (TargetFile f _) = do
223226
setNameCache :: IORef NameCache -> HscEnv -> HscEnv
224227
setNameCache nc hsc = hsc { hsc_NC = nc }
225228

226-
loadSessionShake :: FilePath -> Action (FilePath -> Action (IdeResult HscEnvEq))
227-
loadSessionShake fp = do
228-
se <- getShakeExtras
229-
IdeOptions{optTesting = IdeTesting ideTesting} <- getIdeOptions
230-
res <- liftIO $ loadSession ideTesting se fp
231-
return (fmap liftIO res)
232-
233229
-- | This is the key function which implements multi-component support. All
234230
-- components mapping to the same hie.yaml file are mapped to the same
235231
-- HscEnv which is updated as new components are discovered.
236-
loadSession :: Bool -> ShakeExtras -> FilePath -> IO (FilePath -> IO (IdeResult HscEnvEq))
237-
loadSession optTesting ShakeExtras{logger, eventer, restartShakeSession, withIndefiniteProgress} dir = do
232+
loadSession :: FilePath -> IO (Action IdeGhcSession)
233+
loadSession dir = do
238234
-- Mapping from hie.yaml file to HscEnv, one per hie.yaml file
239235
hscEnvs <- newVar Map.empty :: IO (Var HieMap)
240236
-- Mapping from a Filepath to HscEnv
241237
fileToFlags <- newVar Map.empty :: IO (Var FlagsMap)
238+
-- Version of the mappings above
239+
version <- newVar 0
240+
let returnWithVersion fun = IdeGhcSession fun <$> liftIO (readVar version)
241+
let invalidateShakeCache = do
242+
modifyVar_ version (return . succ)
243+
-- This caches the mapping from Mod.hs -> hie.yaml
244+
cradleLoc <- liftIO $ memoIO $ \v -> do
245+
res <- findCradle v
246+
-- Sometimes we get C:, sometimes we get c:, and sometimes we get a relative path
247+
-- try and normalise that
248+
-- e.g. see https://github.com/digital-asset/ghcide/issues/126
249+
res' <- traverse IO.makeAbsolute res
250+
return $ normalise <$> res'
242251

243252
libdir <- getLibdir
244253
installationCheck <- ghcVersionChecker libdir
245254

255+
dummyAs <- async $ return (error "Uninitialised")
256+
runningCradle <- newVar dummyAs :: IO (Var (Async (IdeResult HscEnvEq,[FilePath])))
257+
246258
case installationCheck of
247259
InstallationNotFound{..} ->
248260
error $ "GHC installation not found in libdir: " <> libdir
249261
InstallationMismatch{..} ->
250-
return $ \fp -> return ([renderPackageSetupException compileTime fp GhcVersionMismatch{..}], Nothing)
251-
InstallationChecked compileTime ghcLibCheck -> do
252-
-- This caches the mapping from Mod.hs -> hie.yaml
253-
cradleLoc <- memoIO $ \v -> do
254-
res <- findCradle v
255-
-- Sometimes we get C:, sometimes we get c:, and sometimes we get a relative path
256-
-- try and normalise that
257-
-- e.g. see https://github.com/digital-asset/ghcide/issues/126
258-
res' <- traverse IO.makeAbsolute res
259-
return $ normalise <$> res'
262+
return $ returnWithVersion $ \fp -> return (([renderPackageSetupException compileTime fp GhcVersionMismatch{..}], Nothing),[])
263+
InstallationChecked compileTime ghcLibCheck -> return $ do
264+
ShakeExtras{logger, eventer, restartShakeSession, withIndefiniteProgress} <- getShakeExtras
265+
IdeOptions{optTesting = IdeTesting optTesting} <- getIdeOptions
260266

261267
-- Create a new HscEnv from a hieYaml root and a set of options
262268
-- If the hieYaml file already has an HscEnv, the new component is
@@ -269,7 +275,8 @@ loadSession optTesting ShakeExtras{logger, eventer, restartShakeSession, withInd
269275
hscEnv <- emptyHscEnv
270276
(df, targets) <- evalGhcEnv hscEnv $
271277
setOptions opts (hsc_dflags hscEnv)
272-
dep_info <- getDependencyInfo (componentDependencies opts ++ maybeToList hieYaml)
278+
let deps = componentDependencies opts ++ maybeToList hieYaml
279+
dep_info <- getDependencyInfo deps
273280
-- Now lookup to see whether we are combining with an existing HscEnv
274281
-- or making a new one. The lookup returns the HscEnv and a list of
275282
-- information about other components loaded into the HscEnv
@@ -329,7 +336,8 @@ loadSession optTesting ShakeExtras{logger, eventer, restartShakeSession, withInd
329336
-- existing packages
330337
pure (Map.insert hieYaml (newHscEnv, new_deps) m, (newHscEnv, head new_deps', tail new_deps'))
331338

332-
let session :: (Maybe FilePath, NormalizedFilePath, ComponentOptions) -> IO (IdeResult HscEnvEq)
339+
340+
let session :: (Maybe FilePath, NormalizedFilePath, ComponentOptions) -> IO (IdeResult HscEnvEq,[FilePath])
333341
session (hieYaml, cfp, opts) = do
334342
(hscEnv, new, old_deps) <- packageSetup (hieYaml, cfp, opts)
335343
-- Make a map from unit-id to DynFlags, this is used when trying to
@@ -350,11 +358,12 @@ loadSession optTesting ShakeExtras{logger, eventer, restartShakeSession, withInd
350358
pure $ Map.insert hieYaml (HM.fromList (cs ++ cached_targets)) var
351359

352360
-- Invalidate all the existing GhcSession build nodes by restarting the Shake session
361+
invalidateShakeCache
353362
restartShakeSession [kick]
354363

355-
return (fst res)
364+
return (second Map.keys res)
356365

357-
let consultCradle :: Maybe FilePath -> FilePath -> IO (IdeResult HscEnvEq)
366+
let consultCradle :: Maybe FilePath -> FilePath -> IO (IdeResult HscEnvEq, [FilePath])
358367
consultCradle hieYaml cfp = do
359368
when optTesting $ eventer $ notifyCradleLoaded cfp
360369
logInfo logger $ T.pack ("Consulting the cradle for " <> show cfp)
@@ -379,10 +388,11 @@ loadSession optTesting ShakeExtras{logger, eventer, restartShakeSession, withInd
379388
let res = (map (renderCradleError ncfp) err, Nothing)
380389
modifyVar_ fileToFlags $ \var -> do
381390
pure $ Map.insertWith HM.union hieYaml (HM.singleton ncfp (res, dep_info)) var
382-
return res
391+
return (res,[])
383392

384393
-- This caches the mapping from hie.yaml + Mod.hs -> [String]
385-
let sessionOpts :: (Maybe FilePath, FilePath) -> IO (IdeResult HscEnvEq)
394+
-- Returns the Ghc session and the cradle dependencies
395+
let sessionOpts :: (Maybe FilePath, FilePath) -> IO (IdeResult HscEnvEq, [FilePath])
386396
sessionOpts (hieYaml, file) = do
387397
v <- fromMaybe HM.empty . Map.lookup hieYaml <$> readVar fileToFlags
388398
cfp <- canonicalizePath file
@@ -397,30 +407,26 @@ loadSession optTesting ShakeExtras{logger, eventer, restartShakeSession, withInd
397407
-- Keep the same name cache
398408
modifyVar_ hscEnvs (return . Map.adjust (\(h, _) -> (h, [])) hieYaml )
399409
consultCradle hieYaml cfp
400-
else return opts
410+
else return (opts, Map.keys old_di)
401411
Nothing -> consultCradle hieYaml cfp
402412

403-
dummyAs <- async $ return (error "Uninitialised")
404-
runningCradle <- newVar dummyAs :: IO (Var (Async (IdeResult HscEnvEq)))
405413
-- The main function which gets options for a file. We only want one of these running
406414
-- at a time. Therefore the IORef contains the currently running cradle, if we try
407415
-- to get some more options then we wait for the currently running action to finish
408416
-- before attempting to do so.
409-
let getOptions :: FilePath -> IO (IdeResult HscEnvEq)
417+
let getOptions :: FilePath -> IO (IdeResult HscEnvEq, [FilePath])
410418
getOptions file = do
411419
hieYaml <- cradleLoc file
412420
sessionOpts (hieYaml, file) `catch` \e ->
413-
return ([renderPackageSetupException compileTime file e], Nothing)
421+
return (([renderPackageSetupException compileTime file e], Nothing),[])
414422

415-
return $ \file -> do
416-
join $ mask_ $ modifyVar runningCradle $ \as -> do
423+
returnWithVersion $ \file -> do
424+
liftIO $ join $ mask_ $ modifyVar runningCradle $ \as -> do
417425
-- If the cradle is not finished, then wait for it to finish.
418426
void $ wait as
419427
as <- async $ getOptions file
420428
return (as, wait as)
421429

422-
423-
424430
-- | Create a mapping from FilePaths to HscEnvEqs
425431
newComponentCache
426432
:: Logger

Diff for: src/Development/IDE/Core/Rules.hs

+28-18
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,14 @@
1212
--
1313
module Development.IDE.Core.Rules(
1414
IdeState, GetDependencies(..), GetParsedModule(..), TransitiveDependencies(..),
15-
Priority(..), GhcSessionIO(..), GhcSessionFun(..),
15+
Priority(..), GhcSessionIO(..),
1616
priorityTypeCheck,
1717
priorityGenerateCore,
1818
priorityFilesOfInterest,
1919
runAction, useE, useNoFileE, usesE,
20-
toIdeResult, defineNoFile,
20+
toIdeResult,
21+
defineNoFile,
22+
defineEarlyCutOffNoFile,
2123
mainRule,
2224
getAtPoint,
2325
getDefinition,
@@ -103,6 +105,11 @@ defineNoFile f = define $ \k file -> do
103105
if file == emptyFilePath then do res <- f k; return ([], Just res) else
104106
fail $ "Rule " ++ show k ++ " should always be called with the empty string for a file"
105107

108+
defineEarlyCutOffNoFile :: IdeRule k v => (k -> Action (ByteString, v)) -> Rules ()
109+
defineEarlyCutOffNoFile f = defineEarlyCutoff $ \k file -> do
110+
if file == emptyFilePath then do (hash, res) <- f k; return (Just hash, ([], Just res)) else
111+
fail $ "Rule " ++ show k ++ " should always be called with the empty string for a file"
112+
106113

107114
------------------------------------------------------------
108115
-- Exposed API
@@ -535,33 +542,36 @@ generateByteCodeRule =
535542
-- A local rule type to get caching. We want to use newCache, but it has
536543
-- thread killed exception issues, so we lift it to a full rule.
537544
-- https://github.com/digital-asset/daml/pull/2808#issuecomment-529639547
538-
type instance RuleResult GhcSessionIO = GhcSessionFun
545+
type instance RuleResult GhcSessionIO = IdeGhcSession
539546

540547
data GhcSessionIO = GhcSessionIO deriving (Eq, Show, Typeable, Generic)
541548
instance Hashable GhcSessionIO
542549
instance NFData GhcSessionIO
543550
instance Binary GhcSessionIO
544551

545-
newtype GhcSessionFun = GhcSessionFun (FilePath -> Action (IdeResult HscEnvEq))
546-
instance Show GhcSessionFun where show _ = "GhcSessionFun"
547-
instance NFData GhcSessionFun where rnf !_ = ()
548-
549-
550552
loadGhcSession :: Rules ()
551553
loadGhcSession = do
552-
defineNoFile $ \GhcSessionIO -> do
554+
-- This function should always be rerun because it tracks changes
555+
-- to the version of the collection of HscEnv's.
556+
defineEarlyCutOffNoFile $ \GhcSessionIO -> do
557+
alwaysRerun
553558
opts <- getIdeOptions
554-
GhcSessionFun <$> optGhcSession opts
555-
-- This function should always be rerun because it consults a cache to
556-
-- see what HscEnv needs to be used for the file, which can change.
557-
-- However, it should also cut-off early if it's the same HscEnv as
558-
-- last time
559+
res <- optGhcSession opts
560+
561+
let fingerprint = hash (sessionVersion res)
562+
return (BS.pack (show fingerprint), res)
563+
559564
defineEarlyCutoff $ \GhcSession file -> do
560-
GhcSessionFun fun <- useNoFile_ GhcSessionIO
561-
alwaysRerun
562-
val <- fun $ fromNormalizedFilePath file
565+
IdeGhcSession{loadSessionFun} <- useNoFile_ GhcSessionIO
566+
(val,deps) <- liftIO $ loadSessionFun $ fromNormalizedFilePath file
567+
568+
-- add the deps to the Shake graph
569+
let addDependency fp = do
570+
let nfp = toNormalizedFilePath' fp
571+
itExists <- getFileExists nfp
572+
when itExists $ void $ use_ GetModificationTime nfp
573+
mapM_ addDependency deps
563574

564-
-- TODO: What was this doing before?
565575
opts <- getIdeOptions
566576
let cutoffHash =
567577
case optShakeFiles opts of

Diff for: src/Development/IDE/Types/Options.hs

+14-2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ module Development.IDE.Types.Options
1414
, IdePkgLocationOptions(..)
1515
, defaultIdeOptions
1616
, IdeResult
17+
, IdeGhcSession(..)
1718
) where
1819

1920
import Development.Shake
@@ -23,12 +24,23 @@ import GhcPlugins as GHC hiding (fst3, (<>))
2324
import qualified Language.Haskell.LSP.Types.Capabilities as LSP
2425
import qualified Data.Text as T
2526
import Development.IDE.Types.Diagnostics
27+
import Control.DeepSeq (NFData(..))
28+
29+
data IdeGhcSession = IdeGhcSession
30+
{ loadSessionFun :: FilePath -> IO (IdeResult HscEnvEq, [FilePath])
31+
-- ^ Returns the Ghc session and the cradle dependencies
32+
, sessionVersion :: !Int
33+
-- ^ Used as Shake key, versions must be unique and not reused
34+
}
35+
36+
instance Show IdeGhcSession where show _ = "IdeGhcSession"
37+
instance NFData IdeGhcSession where rnf !_ = ()
2638

2739
data IdeOptions = IdeOptions
2840
{ optPreprocessor :: GHC.ParsedSource -> IdePreprocessedSource
2941
-- ^ Preprocessor to run over all parsed source trees, generating a list of warnings
3042
-- and a list of errors, along with a new parse tree.
31-
, optGhcSession :: Action (FilePath -> Action (IdeResult HscEnvEq))
43+
, optGhcSession :: Action IdeGhcSession
3244
-- ^ Setup a GHC session for a given file, e.g. @Foo.hs@.
3345
-- For the same 'ComponentOptions' from hie-bios, the resulting function will be applied once per file.
3446
-- It is desirable that many files get the same 'HscEnvEq', so that more IDE features work.
@@ -80,7 +92,7 @@ clientSupportsProgress :: LSP.ClientCapabilities -> IdeReportProgress
8092
clientSupportsProgress caps = IdeReportProgress $ Just True ==
8193
(LSP._workDoneProgress =<< LSP._window (caps :: LSP.ClientCapabilities))
8294

83-
defaultIdeOptions :: Action (FilePath -> Action (IdeResult HscEnvEq)) -> IdeOptions
95+
defaultIdeOptions :: Action IdeGhcSession -> IdeOptions
8496
defaultIdeOptions session = IdeOptions
8597
{optPreprocessor = IdePreprocessedSource [] []
8698
,optGhcSession = session

Diff for: test/exe/Main.hs

+4-2
Original file line numberDiff line numberDiff line change
@@ -494,21 +494,23 @@ watchedFilesTests = testGroup "watched files"
494494
watchedFileRegs <- getWatchedFilesSubscriptionsUntil @PublishDiagnosticsNotification
495495

496496
-- Expect 4 subscriptions (A does not get any because it's VFS):
497+
-- - /path-to-workspace/hie.yaml
497498
-- - /path-to-workspace/WatchedFilesMissingModule.hs
498499
-- - /path-to-workspace/WatchedFilesMissingModule.lhs
499500
-- - /path-to-workspace/src/WatchedFilesMissingModule.hs
500501
-- - /path-to-workspace/src/WatchedFilesMissingModule.lhs
501-
liftIO $ length watchedFileRegs @?= 4
502+
liftIO $ length watchedFileRegs @?= 5
502503

503504
, testSession' "non workspace file" $ \sessionDir -> do
504505
liftIO $ writeFile (sessionDir </> "hie.yaml") "cradle: {direct: {arguments: [\"-i/tmp\"]}}"
505506
_doc <- createDoc "A.hs" "haskell" "{-# LANGUAGE NoImplicitPrelude#-}\nmodule A where\nimport WatchedFilesMissingModule"
506507
watchedFileRegs <- getWatchedFilesSubscriptionsUntil @PublishDiagnosticsNotification
507508

508509
-- Expect 2 subscriptions (/tmp does not get any as it is out of the workspace):
510+
-- - /path-to-workspace/hie.yaml
509511
-- - /path-to-workspace/WatchedFilesMissingModule.hs
510512
-- - /path-to-workspace/WatchedFilesMissingModule.lhs
511-
liftIO $ length watchedFileRegs @?= 2
513+
liftIO $ length watchedFileRegs @?= 3
512514

513515
-- TODO add a test for didChangeWorkspaceFolder
514516
]

0 commit comments

Comments
 (0)