Skip to content

Commit 0ff88c6

Browse files
ShakeSession and shakeEnqueue (#554)
* ShakeSession and shakeRunGently Currently we start a new Shake session for every interaction with the Shake database, including type checking, hovers, code actions, completions, etc. Since only one Shake session can ever exist, we abort the active session if any in order to execute the new command in a responsive manner. This is suboptimal in many, many ways: - A hover in module M aborts the typechecking of module M, only to start over! - Read-only commands (hover, code action, completion) need to typecheck all the modules! (or rather, ask Shake to check that the typechecks are current) - There is no way to run non-interfering commands concurrently This is an experiment inspired by the 'ShakeQueue' of @mpickering, and the follow-up discussion in mpickering/ghcide#7 We introduce the concept of the 'ShakeSession' as part of the IDE state. The 'ShakeSession' is initialized by a call to 'shakeRun', and survives until the next call to 'shakeRun'. It is important that the session is restarted as soon as the filesystem changes, to ensure that the database is current. The 'ShakeSession' enables a new command 'shakeRunGently', which appends work to the existing 'ShakeSession'. This command can be called in parallel without any restriction. * Simplify by assuming there is always a ShakeSession * Improved naming and docs * Define runActionSync on top of shakeEnqueue shakeRun is not correct as it never returns anymore * Drive progress reporting from newSession The previous approach reused the shakeProgress thread, which doesn't work anymore as ShakeSession keeps the ShakeDatabase open until the next edit * Deterministic progress messages in tests Dropping the 0.1s sleep to ensure that progress messages during tests are deterministic * Make kick explicit This is required for progress reporting to work, see notes in shakeRun As to whether this is the right thing to do: 1. Less magic, more explicit 2. There's only 2 places where kick is actually used * apply Neil's feedback * avoid a deadlock when the enqueued action throws * Simplify runAction + comments * use a Barrier for clarity A Barrier is a smaller abstraction than an MVar, and the next version of the extra package will come with a suitably small implementation: ndmitchell/extra@98c2a83 * Log timings for code actions, hovers and completions * Rename shakeRun to shakeRestart The action returned by shakeRun now blocks until another call to shakeRun is made, which is a change in behaviour,. but all the current uses of shakeRun ignore this action. Since the new behaviour is not useful, this change simplifies and updates the docs and name accordingly * delete runActionSync as it's just runAction * restart shake session on new component created * requeue pending actions on session restart * hlint * Bumped the delay from 5 to 6 * Add a test for the non-lsp command line * Update exe/Main.hs Co-authored-by: Moritz Kiefer <[email protected]>
1 parent 5a754e1 commit 0ff88c6

File tree

10 files changed

+240
-116
lines changed

10 files changed

+240
-116
lines changed

exe/Main.hs

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ import System.Time.Extra
5858
import HIE.Bios.Environment (addCmdOpts, makeDynFlagsAbsolute)
5959
import Paths_ghcide
6060
import Development.GitRev
61-
import Development.Shake (Action, action)
61+
import Development.Shake (Action)
6262
import qualified Data.HashSet as HashSet
6363
import qualified Data.HashMap.Strict as HM
6464
import qualified Data.Map.Strict as Map
@@ -124,11 +124,11 @@ main = do
124124
let options = (defaultIdeOptions $ loadSessionShake dir)
125125
{ optReportProgress = clientSupportsProgress caps
126126
, optShakeProfiling = argsShakeProfiling
127-
, optTesting = argsTesting
127+
, optTesting = IdeTesting argsTesting
128128
, optThreads = argsThreads
129129
}
130130
debouncer <- newAsyncDebouncer
131-
initialise caps (mainRule >> pluginRules plugins >> action kick)
131+
initialise caps (mainRule >> pluginRules plugins)
132132
getLspId event (logger minBound) debouncer options vfs
133133
else do
134134
-- GHC produces messages with UTF8 in them, so make sure the terminal doesn't error
@@ -156,14 +156,14 @@ main = do
156156

157157
putStrLn "\nStep 4/4: Type checking the files"
158158
setFilesOfInterest ide $ HashSet.fromList $ map toNormalizedFilePath' files
159-
results <- runActionSync ide $ uses TypeCheck (map toNormalizedFilePath' files)
159+
results <- runAction ide $ uses TypeCheck (map toNormalizedFilePath' files)
160160
let (worked, failed) = partition fst $ zip (map isJust results) files
161161
when (failed /= []) $
162162
putStr $ unlines $ "Files that failed:" : map ((++) " * " . snd) failed
163163

164164
let files xs = let n = length xs in if n == 1 then "1 file" else show n ++ " files"
165165
putStrLn $ "\nCompleted (" ++ files worked ++ " worked, " ++ files failed ++ " failed)"
166-
return ()
166+
unless (null failed) (exitWith $ ExitFailure (length failed))
167167

168168
expandFiles :: [FilePath] -> IO [FilePath]
169169
expandFiles = concatMapM $ \x -> do
@@ -177,12 +177,6 @@ expandFiles = concatMapM $ \x -> do
177177
fail $ "Couldn't find any .hs/.lhs files inside directory: " ++ x
178178
return files
179179

180-
181-
kick :: Action ()
182-
kick = do
183-
files <- getFilesOfInterest
184-
void $ uses TypeCheck $ HashSet.toList files
185-
186180
-- | Print an LSP event.
187181
showEvent :: Lock -> FromServerMessage -> IO ()
188182
showEvent _ (EventFileDiagnostics _ []) = return ()
@@ -230,15 +224,15 @@ setNameCache nc hsc = hsc { hsc_NC = nc }
230224
loadSessionShake :: FilePath -> Action (FilePath -> Action (IdeResult HscEnvEq))
231225
loadSessionShake fp = do
232226
se <- getShakeExtras
233-
IdeOptions{optTesting} <- getIdeOptions
234-
res <- liftIO $ loadSession optTesting se fp
227+
IdeOptions{optTesting = IdeTesting ideTesting} <- getIdeOptions
228+
res <- liftIO $ loadSession ideTesting se fp
235229
return (fmap liftIO res)
236230

237231
-- | This is the key function which implements multi-component support. All
238232
-- components mapping to the same hie.yaml file are mapped to the same
239233
-- HscEnv which is updated as new components are discovered.
240234
loadSession :: Bool -> ShakeExtras -> FilePath -> IO (FilePath -> IO (IdeResult HscEnvEq))
241-
loadSession optTesting ShakeExtras{logger, eventer} dir = do
235+
loadSession optTesting ShakeExtras{logger, eventer, restartShakeSession} dir = do
242236
-- Mapping from hie.yaml file to HscEnv, one per hie.yaml file
243237
hscEnvs <- newVar Map.empty :: IO (Var HieMap)
244238
-- Mapping from a Filepath to HscEnv
@@ -342,6 +336,9 @@ loadSession optTesting ShakeExtras{logger, eventer} dir = do
342336
modifyVar_ fileToFlags $ \var -> do
343337
pure $ Map.insert hieYaml (HM.fromList (cs ++ cached_targets)) var
344338

339+
-- Invalidate all the existing GhcSession build nodes by restarting the Shake session
340+
restartShakeSession [kick]
341+
345342
return (fst res)
346343

347344
let consultCradle :: Maybe FilePath -> FilePath -> IO (IdeResult HscEnvEq)

src/Development/IDE/Core/FileStore.hs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import System.IO.Error
3030
import qualified Data.ByteString.Char8 as BS
3131
import Development.IDE.Types.Diagnostics
3232
import Development.IDE.Types.Location
33+
import Development.IDE.Core.OfInterest (kick)
3334
import qualified Data.Rope.UTF16 as Rope
3435

3536
#ifdef mingw32_HOST_OS
@@ -174,7 +175,7 @@ setBufferModified state absFile contents = do
174175
VFSHandle{..} <- getIdeGlobalState state
175176
whenJust setVirtualFileContents $ \set ->
176177
set (filePathToUri' absFile) contents
177-
void $ shakeRun state []
178+
void $ shakeRestart state [kick]
178179

179180
-- | Note that some buffer somewhere has been modified, but don't say what.
180181
-- Only valid if the virtual file system was initialised by LSP, as that
@@ -184,4 +185,4 @@ setSomethingModified state = do
184185
VFSHandle{..} <- getIdeGlobalState state
185186
when (isJust setVirtualFileContents) $
186187
fail "setSomethingModified can't be called on this type of VFSHandle"
187-
void $ shakeRun state []
188+
void $ shakeRestart state [kick]

src/Development/IDE/Core/OfInterest.hs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
module Development.IDE.Core.OfInterest(
1010
ofInterestRules,
1111
getFilesOfInterest, setFilesOfInterest, modifyFilesOfInterest,
12+
kick
1213
) where
1314

1415
import Control.Concurrent.Extra
@@ -28,6 +29,7 @@ import Development.Shake
2829

2930
import Development.IDE.Types.Location
3031
import Development.IDE.Types.Logger
32+
import Development.IDE.Core.RuleTypes
3133
import Development.IDE.Core.Shake
3234

3335

@@ -79,4 +81,11 @@ modifyFilesOfInterest state f = do
7981
OfInterestVar var <- getIdeGlobalState state
8082
files <- modifyVar var $ pure . dupe . f
8183
logDebug (ideLogger state) $ "Set files of interest to: " <> T.pack (show $ HashSet.toList files)
82-
void $ shakeRun state []
84+
void $ shakeRestart state [kick]
85+
86+
-- | Typecheck all the files of interest.
87+
-- Could be improved
88+
kick :: Action ()
89+
kick = do
90+
files <- getFilesOfInterest
91+
void $ uses TypeCheck $ HashSet.toList files

src/Development/IDE/Core/Service.hs

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,12 @@ module Development.IDE.Core.Service(
1111
getIdeOptions,
1212
IdeState, initialise, shutdown,
1313
runAction,
14-
runActionSync,
1514
writeProfile,
1615
getDiagnostics, unsafeClearDiagnostics,
1716
ideLogger,
1817
updatePositionMapping,
1918
) where
2019

21-
import Control.Concurrent.Extra
22-
import Control.Concurrent.Async
2320
import Data.Maybe
2421
import Development.IDE.Types.Options (IdeOptions(..))
2522
import Control.Monad
@@ -29,7 +26,6 @@ import Development.IDE.Core.FileExists (fileExistsRules)
2926
import Development.IDE.Core.OfInterest
3027
import Development.IDE.Types.Logger
3128
import Development.Shake
32-
import Data.Either.Extra
3329
import qualified Language.Haskell.LSP.Messages as LSP
3430
import qualified Language.Haskell.LSP.Types as LSP
3531
import qualified Language.Haskell.LSP.Types.Capabilities as LSP
@@ -62,6 +58,7 @@ initialise caps mainRule getLspId toDiags logger debouncer options vfs =
6258
debouncer
6359
(optShakeProfiling options)
6460
(optReportProgress options)
61+
(optTesting options)
6562
shakeOptions
6663
{ shakeThreads = optThreads options
6764
, shakeFiles = fromMaybe "/dev/null" (optShakeFiles options)
@@ -83,23 +80,7 @@ shutdown = shakeShut
8380
-- available. There might still be other rules running at this point,
8481
-- e.g., the ofInterestRule.
8582
runAction :: IdeState -> Action a -> IO a
86-
runAction ide action = do
87-
bar <- newBarrier
88-
res <- shakeRun ide [do v <- action; liftIO $ signalBarrier bar v; return v]
89-
-- shakeRun might throw an exception (either through action or a default rule),
90-
-- in which case action may not complete successfully, and signalBarrier might not be called.
91-
-- Therefore we wait for either res (which propagates the exception) or the barrier.
92-
-- Importantly, if the barrier does finish, cancelling res only kills waiting for the result,
93-
-- it doesn't kill the actual work
94-
fmap fromEither $ race (head <$> res) $ waitBarrier bar
95-
96-
97-
-- | `runActionSync` is similar to `runAction` but it will
98-
-- wait for all rules (so in particular the `ofInterestRule`) to
99-
-- finish running. This is mainly useful in tests, where you want
100-
-- to wait for all rules to fire so you can check diagnostics.
101-
runActionSync :: IdeState -> Action a -> IO a
102-
runActionSync s act = fmap head $ join $ shakeRun s [act]
83+
runAction ide action = join $ shakeEnqueue ide action
10384

10485
getIdeOptions :: Action IdeOptions
10586
getIdeOptions = do

0 commit comments

Comments
 (0)