Skip to content

Commit 224e74f

Browse files
committed
Move worker restart ref into WorkerThread
1 parent aa88d13 commit 224e74f

7 files changed

Lines changed: 183 additions & 146 deletions

File tree

ghcide-test/exe/ShakeRestartTests.hs

Lines changed: 2 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@
44
module ShakeRestartTests (tests) where
55

66
import Control.Concurrent.STM
7-
import Data.IORef
8-
import Data.IORef.Extra (atomicModifyIORef'_)
97
import Development.IDE.Core.Shake
108
import Development.IDE.Graph (newKey)
119
import Language.LSP.VFS
@@ -20,37 +18,18 @@ tests = testGroup "shake restart merging"
2018
newestVFSModified vfs1 VFSUnmodified @?= vfs1
2119
newestVFSModified VFSUnmodified vfs1 @?= vfs1
2220

23-
, testCase "mergePendingRestart Nothing" $ do
24-
let p = PendingRestart VFSUnmodified [] ["reason"] [] []
25-
if mergePendingRestart p Nothing == p
26-
then pure ()
27-
else assertFailure "merging with nothing should get new"
28-
29-
, testCase "mergePendingRestart Just" $ do
21+
, testCase "<>" $ do
3022
done1 <- newEmptyTMVarIO
3123
done2 <- newEmptyTMVarIO
3224
let key1 = newKey ("1" :: String)
3325
key2 = newKey ("2" :: String)
3426
p1 = PendingRestart VFSUnmodified [pure [key1]] ["r1"] [] [done1]
3527
p2 = PendingRestart VFSUnmodified [pure [key2]] ["r2"] [] [done2]
36-
merged = mergePendingRestart p1 (Just p2)
28+
merged = p1 <> p2
3729

3830
pendingRestartReasons merged @?= ["r1", "r2"]
3931
keys <- sequence $ reverse $ pendingRestartActionBetweenSessions merged
4032
concat keys @?= [key2, key1]
41-
42-
, testCase "RestartSlot coalescing" $ do
43-
slot <- newRestartSlot
44-
let p1 = PendingRestart VFSUnmodified [] ["r1"] [] []
45-
p2 = PendingRestart VFSUnmodified [] ["r2"] [] []
46-
47-
atomicModifyIORef'_ (queuedRestart slot) $ Just . mergePendingRestart p1
48-
atomicModifyIORef'_ (queuedRestart slot) $ Just . mergePendingRestart p2
49-
50-
res <- atomicModifyIORef' (queuedRestart slot) (Nothing,)
51-
case res of
52-
Nothing -> assertFailure "Should have a pending restart"
53-
Just p -> pendingRestartReasons p @?= ["r2", "r1"]
5433
]
5534

5635
instance Eq VFSModified where

ghcide/session-loader/Development/IDE/Session.hs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ import Data.HashMap.Strict (HashMap)
9090
import Data.HashSet (HashSet)
9191
import qualified Data.HashSet as Set
9292
import Database.SQLite.Simple
93+
import qualified Development.IDE.Core.Shake as Shake
9394
import Development.IDE.Core.Tracing (withTrace)
9495
import Development.IDE.Core.WorkerThread
9596
import Development.IDE.Session.Dependency
@@ -136,6 +137,7 @@ data Log
136137
| LogLookupSessionCache !FilePath
137138
| LogTime !String
138139
| LogSessionGhc Ghc.Log
140+
| LogShake Shake.Log
139141
deriving instance Show Log
140142

141143
instance Pretty Log where
@@ -209,6 +211,7 @@ instance Pretty Log where
209211
LogSessionGhc msg -> pretty msg
210212
LogSessionLoadingChanged ->
211213
"Session Loading config changed, reloading the full session."
214+
LogShake msg -> pretty msg
212215

213216
-- | Bump this version number when making changes to the format of the data stored in hiedb
214217
hiedbDataVersion :: String
@@ -633,7 +636,7 @@ newSessionState = do
633636
-- components mapping to the same hie.yaml file are mapped to the same
634637
-- HscEnv which is updated as new components are discovered.
635638

636-
loadSessionWithOptions :: Recorder (WithPriority Log) -> SessionLoadingOptions -> FilePath -> TaskQueue (IO ()) -> IO (Action IdeGhcSession)
639+
loadSessionWithOptions :: Recorder (WithPriority Log) -> SessionLoadingOptions -> FilePath -> WorkerTasks STM (IO ()) -> IO (Action IdeGhcSession)
637640
loadSessionWithOptions recorder SessionLoadingOptions{..} rootDir que = do
638641
let toAbsolutePath = toAbsolute rootDir -- see Note [Root Directory]
639642

@@ -663,7 +666,7 @@ loadSessionWithOptions recorder SessionLoadingOptions{..} rootDir que = do
663666
-- see Note [Serializing runs in separate thread]
664667
-- Start the 'getOptionsLoop' if the queue is empty
665668
liftIO $ atomically $
666-
Extra.whenM (isEmptyTaskQueue que) $ do
669+
Extra.whenM (nullWorkerTasks que) $ do
667670
let newSessionLoadingOptions = SessionLoadingOptions
668671
{ findCradle = cradleLoc
669672
, ..
@@ -683,7 +686,7 @@ loadSessionWithOptions recorder SessionLoadingOptions{..} rootDir que = do
683686
, sessionLoadingOptions = newSessionLoadingOptions
684687
}
685688

686-
writeTaskQueue que (runReaderT (getOptionsLoop recorder sessionShake sessionState knownTargetsVar) sessionEnv)
689+
addWorkerTask que (runReaderT (getOptionsLoop recorder sessionShake sessionState knownTargetsVar) sessionEnv)
687690

688691
-- Each one of deps will be registered as a FileSystemWatcher in the GhcSession action
689692
-- so that we can get a workspace/didChangeWatchedFiles notification when a dep changes.

ghcide/src/Development/IDE/Core/Compile.hs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ import Development.IDE.Core.Preprocessor
7575
import Development.IDE.Core.ProgressReporting (progressUpdate)
7676
import Development.IDE.Core.RuleTypes
7777
import Development.IDE.Core.Shake
78-
import Development.IDE.Core.WorkerThread (writeTaskQueue)
78+
import Development.IDE.Core.WorkerThread (WorkerTasks (..))
7979
import Development.IDE.Core.Tracing (withTrace)
8080
import qualified Development.IDE.GHC.Compat as Compat
8181
import qualified Development.IDE.GHC.Compat as GHC
@@ -942,7 +942,7 @@ indexHieFile se mod_summary srcPath !hash hf = do
942942
-- hiedb doesn't use the Haskell src, so we clear it to avoid unnecessarily keeping it around
943943
let !hf' = hf{hie_hs_src = mempty}
944944
modifyTVar' indexPending $ HashMap.insert srcPath hash
945-
writeTaskQueue indexQueue $ \withHieDb -> do
945+
addWorkerTask indexQueue $ \withHieDb -> do
946946
-- We are now in the worker thread
947947
-- Check if a newer index of this file has been scheduled, and if so skip this one
948948
newerScheduled <- atomically $ do

ghcide/src/Development/IDE/Core/FileStore.hs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,8 @@ typecheckParentsAction recorder nfp = do
304304
setSomethingModified :: VFSModified -> IdeState -> T.Text -> IO [Key] -> IO ()
305305
setSomethingModified vfs state reason actionBetweenSession = do
306306
-- Update database to remove any files that might have been renamed/deleted
307-
atomically $ writeTaskQueue (indexQueue $ hiedbWriter $ shakeExtras state) (\withHieDb -> withHieDb deleteMissingRealFiles)
307+
let indexQueue' = indexQueue $ hiedbWriter $ shakeExtras state
308+
atomically $ addWorkerTask indexQueue' (\withHieDb -> withHieDb deleteMissingRealFiles)
308309
void $ restartShakeSession (shakeExtras state) vfs reason [] actionBetweenSession
309310

310311
registerFileWatches :: [String] -> LSP.LspT Config IO Bool

ghcide/src/Development/IDE/Core/Shake.hs

Lines changed: 69 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,10 @@ module Development.IDE.Core.Shake(
2626
IdeState, shakeSessionInit, shakeExtras, shakeDb, rootDir,
2727
ShakeExtras(..), getShakeExtras, getShakeExtrasRules,
2828
KnownTargets(..), Target(..), toKnownFiles, unionKnownTargets, mkKnownTargets,
29-
IdeRule, IdeResult, RestartQueue,
29+
IdeRule, IdeResult,
3030
GetModificationTime(GetModificationTime, GetModificationTime_, missingFileDiagnostics),
3131
shakeOpen, shakeShut,
32-
withRestartWorker, newRestartSlot,
32+
newRestartSlot,
3333
shakeEnqueue,
3434
newSession,
3535
use, useNoFile, uses, useWithStaleFast, useWithStaleFast', delayedAction,
@@ -76,14 +76,14 @@ module Development.IDE.Core.Shake(
7676
RestartSlot(..),
7777
addPersistentRule,
7878
newestVFSModified,
79-
mergePendingRestart,
8079
garbageCollectDirtyKeys,
8180
garbageCollectDirtyKeysOlderThan,
8281
Log(..),
8382
VFSModified(..), getClientConfigAction,
8483
ThreadQueue(..),
8584
runWithSignal,
86-
askShake
85+
askShake,
86+
processPendingRestart
8787
) where
8888

8989
import Control.Concurrent.Async
@@ -149,7 +149,6 @@ import Development.IDE.GHC.Compat (NameCache,
149149
initNameCache,
150150
knownKeyNames)
151151
#endif
152-
import Data.IORef.Extra (atomicModifyIORef'_)
153152
import qualified Data.Text.Encoding as T
154153
import Development.IDE.GHC.Orphans ()
155154
import Development.IDE.Graph hiding (ShakeValue,
@@ -192,10 +191,7 @@ import qualified StmContainers.Map as STM
192191
import System.FilePath hiding (makeRelative)
193192
import System.IO.Unsafe (unsafePerformIO)
194193
import System.Time.Extra
195-
import UnliftIO (IORef,
196-
MonadUnliftIO (withRunInIO),
197-
atomicModifyIORef',
198-
newIORef)
194+
import UnliftIO (MonadUnliftIO (..))
199195

200196

201197
data Log
@@ -277,16 +273,15 @@ data HieDbWriter
277273
-- | Actions to queue up on the index worker thread
278274
-- The inner `(HieDb -> IO ()) -> IO ()` wraps `HieDb -> IO ()`
279275
-- with (currently) retry functionality
280-
type IndexQueue = TaskQueue (((HieDb -> IO ()) -> IO ()) -> IO ())
281-
type RestartQueue = TaskQueue (IO ())
282-
type LoaderQueue = TaskQueue (IO ())
283-
284-
285-
data ThreadQueue = ThreadQueue {
286-
tIndexQueue :: IndexQueue
287-
, tRestartSlot :: RestartSlot
288-
, tLoaderQueue :: LoaderQueue
289-
}
276+
type IndexQueue = WorkerTasks STM (((HieDb -> IO ()) -> IO ()) -> IO ())
277+
type RestartRef = WorkerTasks STM PendingRestart
278+
type LoaderQueue = WorkerTasks STM (IO ())
279+
280+
data ThreadQueue = ThreadQueue
281+
{ tIndexQueue :: IndexQueue
282+
, tRestartSlot :: RestartSlot
283+
, tLoaderQueue :: LoaderQueue
284+
}
290285

291286
-- Note [Semantic Tokens Cache Location]
292287
-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -826,23 +821,21 @@ data PendingRestart = PendingRestart
826821
, pendingRestartDoneSignals :: ![TMVar ()]
827822
}
828823

829-
newestVFSModified :: VFSModified -> VFSModified -> VFSModified
830-
newestVFSModified VFSUnmodified old = old
831-
newestVFSModified new@(VFSModified _) _ = new
832-
833-
mergePendingRestart :: PendingRestart -> Maybe PendingRestart -> PendingRestart
834-
mergePendingRestart new Nothing = new
835-
mergePendingRestart new (Just old) = PendingRestart
824+
instance Semigroup PendingRestart where
825+
new <> old = PendingRestart
836826
{ pendingRestartVFS = newestVFSModified (pendingRestartVFS new) (pendingRestartVFS old)
837827
, pendingRestartReasons = pendingRestartReasons new ++ pendingRestartReasons old
838828
, pendingRestartActions = pendingRestartActions new ++ pendingRestartActions old
839829
, pendingRestartActionBetweenSessions = pendingRestartActionBetweenSessions new ++ pendingRestartActionBetweenSessions old
840830
, pendingRestartDoneSignals = pendingRestartDoneSignals new ++ pendingRestartDoneSignals old
841831
}
842832

833+
newestVFSModified :: VFSModified -> VFSModified -> VFSModified
834+
newestVFSModified VFSUnmodified old = old
835+
newestVFSModified new@(VFSModified _) _ = new
836+
843837
data RestartSlot = RestartSlot
844-
{ queuedRestart :: IORef (Maybe PendingRestart)
845-
, restartSignal :: MVar ()
838+
{ restartRef :: WorkerTasks STM PendingRestart
846839
, lastRestartBarrier :: TVar (TMVar ())
847840
-- ^ A barrier that is filled when the most recent shake restart completes.
848841
--
@@ -851,77 +844,62 @@ data RestartSlot = RestartSlot
851844
-- restart can then wait on this.
852845
}
853846

854-
newRestartSlot :: IO RestartSlot
855-
newRestartSlot = do
847+
newRestartSlot :: RestartRef -> IO RestartSlot
848+
newRestartSlot queuedRestart = do
856849
initialBarrier <- newTMVarIO () -- starts filled (no pending restart)
857-
RestartSlot <$> newIORef Nothing <*> newEmptyMVar <*> newTVarIO initialBarrier
850+
RestartSlot <$> pure queuedRestart <*> newTVarIO initialBarrier
858851

859852
-- | Restart the current 'ShakeSession' with the given system actions.
860853
--
861854
-- Any actions running in the current session will be aborted, but actions added
862855
-- via 'shakeEnqueue' will be requeued.
863856
shakeRestart :: IdeState -> VFSModified -> T.Text -> [DelayedAction ()] -> IO [Key] -> IO ()
864857
shakeRestart IdeState{..} vfs reason acts ioActionBetweenShakeSession = do
865-
restartDone <- newEmptyTMVarIO
866-
let slot = restartSlot shakeExtras
867-
-- Publish this restart's barrier, that dependents LSP requests can wait on.
868-
atomically $ writeTVar (lastRestartBarrier slot) restartDone
869-
atomicModifyIORef'_ (queuedRestart slot) $ Just . mergePendingRestart PendingRestart
870-
{ pendingRestartVFS = vfs
871-
, pendingRestartActionBetweenSessions = [ioActionBetweenShakeSession]
872-
, pendingRestartReasons = [reason]
873-
, pendingRestartActions = acts
874-
, pendingRestartDoneSignals = [restartDone]
875-
}
876-
void $ tryPutMVar (restartSignal slot) ()
877-
-- Block until the restart (including ioActionBetweenShakeSession) completes.
878-
-- This preserves the invariant from the original synchronous shakeRestart:
879-
-- callers (e.g. the session loader) must not proceed until their
880-
-- between-session actions have run, otherwise downstream rules can observe
881-
-- stale results (see Note at Session.hs restartSession call site).
882-
atomically $ readTMVar restartDone
883-
884-
-- | Run a worker that asynchronously processes shake restart requests. Will
885-
-- only ever queue upto 1 additional restart, accumulating data while processing
886-
-- any restart.
887-
withRestartWorker :: MVar IdeState -> IO r -> IO r
888-
withRestartWorker ideMVar action = do
889-
let restartWorkerAction = do
890-
ide@IdeState{..} <- readMVar ideMVar
891-
forever (processPendingRestart (shakeRecorder shakeExtras) ide)
892-
`catch` \(e :: SomeException) ->
893-
case fromException e of
894-
Just AsyncCancelled -> throwIO e
895-
_ -> logWith (shakeRecorder shakeExtras) Error (LogRestartWorkerException e)
896-
897-
withAsync restartWorkerAction $ \_ -> action
898-
899-
processPendingRestart :: Recorder (WithPriority Log) -> IdeState -> IO ()
900-
processPendingRestart recorder IdeState{..} = do
901-
takeMVar (restartSignal (restartSlot shakeExtras))
902-
pendingRestart <- atomicModifyIORef' (queuedRestart (restartSlot shakeExtras)) (Nothing,)
903-
void $ forM pendingRestart $ \PendingRestart {..} -> do
904-
flip finally (atomically $ traverse (flip tryPutTMVar ()) (reverse pendingRestartDoneSignals)) $ do
905-
let sessionAction runner = do
906-
(stopTime,()) <- duration $ logErrorAfter 10 $ cancelShakeSession runner
907-
keys <- fmap concat (sequence (reverse pendingRestartActionBetweenSessions))
908-
-- it is every important to update the dirty keys after we enter the critical section
909-
-- see Note [Housekeeping rule cache and dirty key outside of hls-graph]
910-
atomically $ modifyTVar' (dirtyKeys shakeExtras) $ \x -> foldl' (flip insertKeySet) x keys
911-
res <- shakeDatabaseProfile shakeDb
912-
backlog <- readTVarIO $ dirtyKeys shakeExtras
913-
queue <- atomicallyNamed "actionQueue - peek" $ peekInProgress $ actionQueue shakeExtras
914-
915-
-- this log is required by tests
916-
logWith recorder Debug $ LogBuildSessionRestart (reverse pendingRestartReasons) queue backlog stopTime res
917-
918-
withMVar' shakeSession sessionAction $ \() ->
919-
-- It is crucial to be masked here, otherwise we can get killed
920-
-- between spawning the new thread and updating shakeSession.
921-
-- See https://github.com/haskell/ghcide/issues/79
922-
(,()) <$> newSession recorder shakeExtras pendingRestartVFS shakeDb
923-
(reverse pendingRestartActions)
924-
(reverse pendingRestartReasons)
858+
restartDone <- newEmptyTMVarIO
859+
let RestartSlot {..} = restartSlot shakeExtras
860+
-- Publish this restart's barrier, that dependents LSP requests can wait on.
861+
atomically $ do
862+
writeTVar lastRestartBarrier restartDone
863+
addWorkerTask restartRef $ PendingRestart
864+
{ pendingRestartVFS = vfs
865+
, pendingRestartActionBetweenSessions = [ioActionBetweenShakeSession]
866+
, pendingRestartReasons = [reason]
867+
, pendingRestartActions = acts
868+
, pendingRestartDoneSignals = [restartDone]
869+
}
870+
871+
processPendingRestart :: Recorder (WithPriority Log) -> MVar IdeState -> PendingRestart -> IO ()
872+
processPendingRestart recorder ideMVar pendingRestart = do
873+
processPendingRestart' recorder ideMVar pendingRestart
874+
`catch` \(e :: SomeException) ->
875+
case fromException e of
876+
Just AsyncCancelled -> throwIO e
877+
_ -> logWith recorder Error (LogRestartWorkerException e)
878+
879+
processPendingRestart' :: Recorder (WithPriority Log) -> MVar IdeState -> PendingRestart -> IO ()
880+
processPendingRestart' recorder ideMVar PendingRestart{..} = do
881+
IdeState{..} <- readMVar ideMVar
882+
flip finally (atomically $ traverse (flip tryPutTMVar ()) (reverse pendingRestartDoneSignals)) $ do
883+
let sessionAction runner = do
884+
(stopTime,()) <- duration $ logErrorAfter 10 $ cancelShakeSession runner
885+
keys <- fmap concat (sequence (reverse pendingRestartActionBetweenSessions))
886+
-- it is every important to update the dirty keys after we enter the critical section
887+
-- see Note [Housekeeping rule cache and dirty key outside of hls-graph]
888+
atomically $ modifyTVar' (dirtyKeys shakeExtras) $ \x -> foldl' (flip insertKeySet) x keys
889+
res <- shakeDatabaseProfile shakeDb
890+
backlog <- readTVarIO $ dirtyKeys shakeExtras
891+
queue <- atomicallyNamed "actionQueue - peek" $ peekInProgress $ actionQueue shakeExtras
892+
893+
-- this log is required by tests
894+
logWith recorder Debug $ LogBuildSessionRestart (reverse pendingRestartReasons) queue backlog stopTime res
895+
896+
withMVar' shakeSession sessionAction $ \() ->
897+
-- It is crucial to be masked here, otherwise we can get killed
898+
-- between spawning the new thread and updating shakeSession.
899+
-- See https://github.com/haskell/ghcide/issues/79
900+
(,()) <$> newSession recorder shakeExtras pendingRestartVFS shakeDb
901+
(reverse pendingRestartActions)
902+
(reverse pendingRestartReasons)
925903
where
926904
logErrorAfter :: Seconds -> IO () -> IO ()
927905
logErrorAfter seconds action = flip withAsync (const action) $ do

0 commit comments

Comments
 (0)