Skip to content

Commit 6c69e9d

Browse files
authored
lock-less Values state (#2429)
1 parent 2e59c60 commit 6c69e9d

File tree

7 files changed

+142
-129
lines changed

7 files changed

+142
-129
lines changed

ghcide/ghcide.cabal

+3
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ library
5454
fuzzy,
5555
filepath,
5656
fingertree,
57+
focus,
5758
ghc-exactprint,
5859
ghc-trace-events,
5960
Glob,
@@ -62,6 +63,7 @@ library
6263
hie-compat ^>= 0.2.0.0,
6364
hls-plugin-api ^>= 1.2.0.2,
6465
lens,
66+
list-t,
6567
hiedb == 0.4.1.*,
6668
lsp-types >= 1.3.0.1 && < 1.4,
6769
lsp == 1.2.*,
@@ -81,6 +83,7 @@ library
8183
sorted-list,
8284
sqlite-simple,
8385
stm,
86+
stm-containers,
8487
syb,
8588
text,
8689
time,

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

+6-5
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ module Development.IDE.Core.FileStore(
2424
registerFileWatches
2525
) where
2626

27-
import Control.Concurrent.STM (atomically)
27+
import Control.Concurrent.STM (atomically,
28+
modifyTVar')
2829
import Control.Concurrent.STM.TQueue (writeTQueue)
2930
import Control.Concurrent.Strict
3031
import Control.Exception
@@ -63,7 +64,6 @@ import qualified Development.IDE.Types.Logger as L
6364
import qualified Data.Binary as B
6465
import qualified Data.ByteString.Lazy as LBS
6566
import qualified Data.HashSet as HSet
66-
import Data.IORef.Extra (atomicModifyIORef_)
6767
import Data.List (foldl')
6868
import qualified Data.Text as Text
6969
import Development.IDE.Core.IdeConfiguration (isWorkspaceFile)
@@ -292,9 +292,10 @@ setSomethingModified state keys reason = do
292292
when (isJust setVirtualFileContents) $
293293
fail "setSomethingModified can't be called on this type of VFSHandle"
294294
-- Update database to remove any files that might have been renamed/deleted
295-
atomically $ writeTQueue (indexQueue $ hiedbWriter $ shakeExtras state) deleteMissingRealFiles
296-
atomicModifyIORef_ (dirtyKeys $ shakeExtras state) $ \x ->
297-
foldl' (flip HSet.insert) x keys
295+
atomically $ do
296+
writeTQueue (indexQueue $ hiedbWriter $ shakeExtras state) deleteMissingRealFiles
297+
modifyTVar' (dirtyKeys $ shakeExtras state) $ \x ->
298+
foldl' (flip HSet.insert) x keys
298299
void $ restartShakeSession (shakeExtras state) reason []
299300

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

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

+52-50
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
{-# LANGUAGE DerivingStrategies #-}
66
{-# LANGUAGE DuplicateRecordFields #-}
77
{-# LANGUAGE ExistentialQuantification #-}
8+
{-# LANGUAGE PackageImports #-}
89
{-# LANGUAGE PolyKinds #-}
910
{-# LANGUAGE RankNTypes #-}
1011
{-# LANGUAGE RecursiveDo #-}
@@ -155,16 +156,17 @@ import Data.Default
155156
import Data.Foldable (toList)
156157
import Data.HashSet (HashSet)
157158
import qualified Data.HashSet as HSet
158-
import Data.IORef.Extra (atomicModifyIORef'_,
159-
atomicModifyIORef_)
160159
import Data.String (fromString)
161160
import Data.Text (pack)
162161
import Debug.Trace.Flags (userTracingEnabled)
163162
import qualified Development.IDE.Types.Exports as ExportsMap
163+
import qualified Focus
164164
import HieDb.Types
165165
import Ide.Plugin.Config
166166
import qualified Ide.PluginUtils as HLS
167167
import Ide.Types (PluginId)
168+
import qualified "list-t" ListT
169+
import qualified StmContainers.Map as STM
168170

169171
-- | We need to serialize writes to the database, so we send any function that
170172
-- needs to write to the database over the channel, where it will be picked up by
@@ -188,7 +190,7 @@ data ShakeExtras = ShakeExtras
188190
,debouncer :: Debouncer NormalizedUri
189191
,logger :: Logger
190192
,globals :: Var (HMap.HashMap TypeRep Dynamic)
191-
,state :: Var Values
193+
,state :: Values
192194
,diagnostics :: Var DiagnosticStore
193195
,hiddenDiagnostics :: Var DiagnosticStore
194196
,publishedDiagnostics :: Var (HMap.HashMap NormalizedUri [Diagnostic])
@@ -222,7 +224,7 @@ data ShakeExtras = ShakeExtras
222224
, vfs :: VFSHandle
223225
, defaultConfig :: Config
224226
-- ^ Default HLS config, only relevant if the client does not provide any Config
225-
, dirtyKeys :: IORef (HashSet Key)
227+
, dirtyKeys :: TVar (HashSet Key)
226228
-- ^ Set of dirty rule keys since the last Shake run
227229
}
228230

@@ -326,7 +328,6 @@ getIdeOptionsIO ide = do
326328
-- for the version of that value.
327329
lastValueIO :: IdeRule k v => ShakeExtras -> k -> NormalizedFilePath -> IO (Maybe (v, PositionMapping))
328330
lastValueIO s@ShakeExtras{positionMapping,persistentKeys,state} k file = do
329-
hm <- readVar state
330331
allMappings <- readVar positionMapping
331332

332333
let readPersistent
@@ -341,10 +342,10 @@ lastValueIO s@ShakeExtras{positionMapping,persistentKeys,state} k file = do
341342
MaybeT $ pure $ (,del,ver) <$> fromDynamic dv
342343
case mv of
343344
Nothing -> do
344-
void $ modifyVar' state $ HMap.alter (alterValue $ Failed True) (toKey k file)
345+
void $ atomically $ STM.focus (Focus.alter (alterValue $ Failed True)) (toKey k file) state
345346
return Nothing
346347
Just (v,del,ver) -> do
347-
void $ modifyVar' state $ HMap.alter (alterValue $ Stale (Just del) ver (toDyn v)) (toKey k file)
348+
void $ atomically $ STM.focus (Focus.alter (alterValue $ Stale (Just del) ver (toDyn v))) (toKey k file) state
348349
return $ Just (v,addDelta del $ mappingForVersion allMappings file ver)
349350

350351
-- We got a new stale value from the persistent rule, insert it in the map without affecting diagnostics
@@ -355,7 +356,7 @@ lastValueIO s@ShakeExtras{positionMapping,persistentKeys,state} k file = do
355356
-- Something already succeeded before, leave it alone
356357
_ -> old
357358

358-
case HMap.lookup (toKey k file) hm of
359+
atomically (STM.lookup (toKey k file) state) >>= \case
359360
Nothing -> readPersistent
360361
Just (ValueWithDiagnostics v _) -> case v of
361362
Succeeded ver (fromDynamic -> Just v) -> pure (Just (v, mappingForVersion allMappings file ver))
@@ -420,14 +421,14 @@ shakeDatabaseProfileIO mbProfileDir = do
420421
return (dir </> file)
421422

422423
setValues :: IdeRule k v
423-
=> Var Values
424+
=> Values
424425
-> k
425426
-> NormalizedFilePath
426427
-> Value v
427428
-> Vector FileDiagnostic
428-
-> IO ()
429+
-> STM ()
429430
setValues state key file val diags =
430-
void $ modifyVar' state $ HMap.insert (toKey key file) (ValueWithDiagnostics (fmap toDyn val) diags)
431+
STM.insert (ValueWithDiagnostics (fmap toDyn val) diags) (toKey key file) state
431432

432433

433434
-- | Delete the value stored for a given ide build key
@@ -437,9 +438,9 @@ deleteValue
437438
-> k
438439
-> NormalizedFilePath
439440
-> IO ()
440-
deleteValue ShakeExtras{dirtyKeys, state} key file = do
441-
void $ modifyVar' state $ HMap.delete (toKey key file)
442-
atomicModifyIORef_ dirtyKeys $ HSet.insert (toKey key file)
441+
deleteValue ShakeExtras{dirtyKeys, state} key file = atomically $ do
442+
STM.delete (toKey key file) state
443+
modifyTVar' dirtyKeys $ HSet.insert (toKey key file)
443444

444445
recordDirtyKeys
445446
:: Shake.ShakeValue k
@@ -448,28 +449,28 @@ recordDirtyKeys
448449
-> [NormalizedFilePath]
449450
-> IO ()
450451
recordDirtyKeys ShakeExtras{dirtyKeys} key file = withEventTrace "recordDirtyKeys" $ \addEvent -> do
451-
atomicModifyIORef_ dirtyKeys $ \x -> foldl' (flip HSet.insert) x (toKey key <$> file)
452+
atomically $ modifyTVar' dirtyKeys $ \x -> foldl' (flip HSet.insert) x (toKey key <$> file)
452453
addEvent (fromString $ "dirty " <> show key) (fromString $ unlines $ map fromNormalizedFilePath file)
453454

454455

455456
-- | We return Nothing if the rule has not run and Just Failed if it has failed to produce a value.
456457
getValues ::
457458
forall k v.
458459
IdeRule k v =>
459-
Var Values ->
460+
Values ->
460461
k ->
461462
NormalizedFilePath ->
462-
IO (Maybe (Value v, Vector FileDiagnostic))
463+
STM (Maybe (Value v, Vector FileDiagnostic))
463464
getValues state key file = do
464-
vs <- readVar state
465-
case HMap.lookup (toKey key file) vs of
465+
STM.lookup (toKey key file) state >>= \case
466466
Nothing -> pure Nothing
467467
Just (ValueWithDiagnostics v diagsV) -> do
468-
let r = fmap (fromJust . fromDynamic @v) v
468+
let !r = seqValue $ fmap (fromJust . fromDynamic @v) v
469+
!res = (r,diagsV)
469470
-- Force to make sure we do not retain a reference to the HashMap
470471
-- and we blow up immediately if the fromJust should fail
471472
-- (which would be an internal error).
472-
evaluate (r `seqValue` Just (r, diagsV))
473+
return $ Just res
473474

474475
-- | Get all the files in the project
475476
knownTargets :: Action (Hashed KnownTargets)
@@ -480,11 +481,11 @@ knownTargets = do
480481
-- | Seq the result stored in the Shake value. This only
481482
-- evaluates the value to WHNF not NF. We take care of the latter
482483
-- elsewhere and doing it twice is expensive.
483-
seqValue :: Value v -> b -> b
484-
seqValue v b = case v of
485-
Succeeded ver v -> rnf ver `seq` v `seq` b
486-
Stale d ver v -> rnf d `seq` rnf ver `seq` v `seq` b
487-
Failed _ -> b
484+
seqValue :: Value v -> Value v
485+
seqValue val = case val of
486+
Succeeded ver v -> rnf ver `seq` v `seq` val
487+
Stale d ver v -> rnf d `seq` rnf ver `seq` v `seq` val
488+
Failed _ -> val
488489

489490
-- | Open a 'IdeState', should be shut using 'shakeShut'.
490491
shakeOpen :: Maybe (LSP.LanguageContextEnv Config)
@@ -507,7 +508,7 @@ shakeOpen lspEnv defaultConfig logger debouncer
507508
ideNc <- newIORef (initNameCache us knownKeyNames)
508509
shakeExtras <- do
509510
globals <- newVar HMap.empty
510-
state <- newVar HMap.empty
511+
state <- STM.newIO
511512
diagnostics <- newVar mempty
512513
hiddenDiagnostics <- newVar mempty
513514
publishedDiagnostics <- newVar mempty
@@ -536,7 +537,7 @@ shakeOpen lspEnv defaultConfig logger debouncer
536537

537538
let clientCapabilities = maybe def LSP.resClientCapabilities lspEnv
538539

539-
dirtyKeys <- newIORef mempty
540+
dirtyKeys <- newTVarIO mempty
540541
pure ShakeExtras{..}
541542
(shakeDbM, shakeClose) <-
542543
shakeOpenDatabase
@@ -566,8 +567,8 @@ startTelemetry db extras@ShakeExtras{..}
566567
IdeOptions{optCheckParents} <- getIdeOptionsIO extras
567568
checkParents <- optCheckParents
568569
regularly 1 $ do
569-
readVar state >>= observe countKeys . countRelevantKeys checkParents . HMap.keys
570-
readIORef dirtyKeys >>= observe countDirty . countRelevantKeys checkParents . HSet.toList
570+
observe countKeys . countRelevantKeys checkParents . map fst =<< (atomically . ListT.toList . STM.listT) state
571+
readTVarIO dirtyKeys >>= observe countDirty . countRelevantKeys checkParents . HSet.toList
571572
shakeGetBuildStep db >>= observe countBuilds
572573

573574
| otherwise = async (pure ())
@@ -624,7 +625,7 @@ shakeRestart IdeState{..} reason acts =
624625
(\runner -> do
625626
(stopTime,()) <- duration (cancelShakeSession runner)
626627
res <- shakeDatabaseProfile shakeDb
627-
backlog <- readIORef $ dirtyKeys shakeExtras
628+
backlog <- readTVarIO (dirtyKeys shakeExtras)
628629
queue <- atomically $ peekInProgress $ actionQueue shakeExtras
629630
let profile = case res of
630631
Just fp -> ", profile saved at " <> fp
@@ -685,7 +686,7 @@ newSession extras@ShakeExtras{..} shakeDb acts reason = do
685686
reenqueued <- atomically $ peekInProgress actionQueue
686687
allPendingKeys <-
687688
if optRunSubset
688-
then Just <$> readIORef dirtyKeys
689+
then Just <$> readTVarIO dirtyKeys
689690
else return Nothing
690691
let
691692
-- A daemon-like action used to inject additional work
@@ -785,29 +786,30 @@ garbageCollectDirtyKeysOlderThan maxAge checkParents = otTracedGarbageCollection
785786
garbageCollectKeys :: String -> Int -> CheckParents -> [(Key, Int)] -> Action [Key]
786787
garbageCollectKeys label maxAge checkParents agedKeys = do
787788
start <- liftIO offsetTime
788-
extras <- getShakeExtras
789-
(n::Int, garbage) <- liftIO $ modifyVar (state extras) $ \vmap ->
790-
evaluate $ foldl' removeDirtyKey (vmap, (0,[])) agedKeys
791-
liftIO $ atomicModifyIORef_ (dirtyKeys extras) $ \x ->
792-
foldl' (flip HSet.insert) x garbage
789+
ShakeExtras{state, dirtyKeys, lspEnv, logger, ideTesting} <- getShakeExtras
790+
(n::Int, garbage) <- liftIO $
791+
foldM (removeDirtyKey dirtyKeys state) (0,[]) agedKeys
793792
t <- liftIO start
794793
when (n>0) $ liftIO $ do
795-
logDebug (logger extras) $ T.pack $
794+
logDebug logger $ T.pack $
796795
label <> " of " <> show n <> " keys (took " <> showDuration t <> ")"
797-
when (coerce $ ideTesting extras) $ liftIO $ mRunLspT (lspEnv extras) $
796+
when (coerce ideTesting) $ liftIO $ mRunLspT lspEnv $
798797
LSP.sendNotification (SCustomMethod "ghcide/GC")
799798
(toJSON $ mapMaybe (fmap showKey . fromKeyType) garbage)
800799
return garbage
801800

802801
where
803802
showKey = show . Q
804-
removeDirtyKey st@(vmap,(!counter, keys)) (k, age)
803+
removeDirtyKey dk values st@(!counter, keys) (k, age)
805804
| age > maxAge
806805
, Just (kt,_) <- fromKeyType k
807806
, not(kt `HSet.member` preservedKeys checkParents)
808-
, (True, vmap') <- HMap.alterF (\prev -> (isJust prev, Nothing)) k vmap
809-
= (vmap', (counter+1, k:keys))
810-
| otherwise = st
807+
= atomically $ do
808+
gotIt <- STM.focus (Focus.member <* Focus.delete) k values
809+
when gotIt $
810+
modifyTVar' dk (HSet.insert k)
811+
return $ if gotIt then (counter+1, k:keys) else st
812+
| otherwise = pure st
811813

812814
countRelevantKeys :: CheckParents -> [Key] -> Int
813815
countRelevantKeys checkParents =
@@ -906,7 +908,7 @@ useWithStaleFast' key file = do
906908
wait <- delayedAction $ mkDelayedAction ("C:" ++ show key ++ ":" ++ fromNormalizedFilePath file) Debug $ use key file
907909

908910
s@ShakeExtras{state} <- askShake
909-
r <- liftIO $ getValues state key file
911+
r <- liftIO $ atomically $ getValues state key file
910912
liftIO $ case r of
911913
-- block for the result if we haven't computed before
912914
Nothing -> do
@@ -1015,7 +1017,7 @@ defineEarlyCutoff' doDiagnostics cmp key file old mode action = do
10151017
(if optSkipProgress options key then id else inProgress progress file) $ do
10161018
val <- case old of
10171019
Just old | mode == RunDependenciesSame -> do
1018-
v <- liftIO $ getValues state key file
1020+
v <- liftIO $ atomically $ getValues state key file
10191021
case v of
10201022
-- No changes in the dependencies and we have
10211023
-- an existing successful result.
@@ -1034,10 +1036,10 @@ defineEarlyCutoff' doDiagnostics cmp key file old mode action = do
10341036
(do v <- action; liftIO $ evaluate $ force v) $
10351037
\(e :: SomeException) -> do
10361038
pure (Nothing, ([ideErrorText file $ T.pack $ show e | not $ isBadDependency e],Nothing))
1037-
modTime <- liftIO $ (currentValue . fst =<<) <$> getValues state GetModificationTime file
1039+
modTime <- liftIO $ (currentValue . fst =<<) <$> atomically (getValues state GetModificationTime file)
10381040
(bs, res) <- case res of
10391041
Nothing -> do
1040-
staleV <- liftIO $ getValues state key file
1042+
staleV <- liftIO $ atomically $ getValues state key file
10411043
pure $ case staleV of
10421044
Nothing -> (toShakeValue ShakeResult bs, Failed False)
10431045
Just v -> case v of
@@ -1048,7 +1050,7 @@ defineEarlyCutoff' doDiagnostics cmp key file old mode action = do
10481050
(Failed b, _) ->
10491051
(toShakeValue ShakeResult bs, Failed b)
10501052
Just v -> pure (maybe ShakeNoCutoff ShakeResult bs, Succeeded (vfsVersion =<< modTime) v)
1051-
liftIO $ setValues state key file res (Vector.fromList diags)
1053+
liftIO $ atomically $ setValues state key file res (Vector.fromList diags)
10521054
doDiagnostics diags
10531055
let eq = case (bs, fmap decodeShakeValue old) of
10541056
(ShakeResult a, Just (ShakeResult b)) -> cmp a b
@@ -1060,7 +1062,7 @@ defineEarlyCutoff' doDiagnostics cmp key file old mode action = do
10601062
(if eq then ChangedRecomputeSame else ChangedRecomputeDiff)
10611063
(encodeShakeValue bs) $
10621064
A res
1063-
liftIO $ atomicModifyIORef'_ dirtyKeys (HSet.delete $ toKey key file)
1065+
liftIO $ atomically $ modifyTVar' dirtyKeys (HSet.delete $ toKey key file)
10641066
return res
10651067

10661068
traceA :: A v -> String
@@ -1148,7 +1150,7 @@ updateFileDiagnostics :: MonadIO m
11481150
-> [(ShowDiagnostic,Diagnostic)] -- ^ current results
11491151
-> m ()
11501152
updateFileDiagnostics fp k ShakeExtras{logger, diagnostics, hiddenDiagnostics, publishedDiagnostics, state, debouncer, lspEnv} current = liftIO $ do
1151-
modTime <- (currentValue . fst =<<) <$> getValues state GetModificationTime fp
1153+
modTime <- (currentValue . fst =<<) <$> atomically (getValues state GetModificationTime fp)
11521154
let (currentShown, currentHidden) = partition ((== ShowDiag) . fst) current
11531155
uri = filePathToUri' fp
11541156
ver = vfsVersion =<< modTime

0 commit comments

Comments
 (0)