-
-
Notifications
You must be signed in to change notification settings - Fork 389
Stabilize the build system by correctly house keeping the dirtykeys and rule values [flaky test #4185 #4093] #4190
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
684a850
5d09837
13528d7
fdbb7aa
6fc3646
e247ae1
7b7ea4d
c31a375
bfb06a3
8adf5a4
bbc5c95
48d5644
e967dde
69c9396
02f0d41
554102d
c983727
3748fc2
3107879
a65ac5c
f7a15cb
f4690c5
c6a33cb
e6105ff
610355c
63b1956
4d28344
2eb29b4
2c61a63
7423695
0c4a2f5
bea88b5
c9219f0
7a08b03
dc18b74
dc71a40
342f52f
240254e
797d9e9
7704d6a
ee1c334
e7d380b
e0a7ff7
a410dd9
cc1fa28
ebce5eb
24ec73f
d609b34
91f88b3
db969c8
2eb20d2
035a71c
f4f80f7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -106,7 +106,7 @@ | |
import Database.SQLite.Simple | ||
import Development.IDE.Core.Tracing (withTrace) | ||
import Development.IDE.Session.Diagnostics (renderCradleError) | ||
import Development.IDE.Types.Shake (WithHieDb) | ||
import Development.IDE.Types.Shake (WithHieDb, toNoFileKey) | ||
import HieDb.Create | ||
import HieDb.Types | ||
import HieDb.Utils | ||
|
@@ -474,10 +474,9 @@ | |
clientConfig <- getClientConfigAction | ||
extras@ShakeExtras{restartShakeSession, ideNc, knownTargetsVar, lspEnv | ||
} <- getShakeExtras | ||
let invalidateShakeCache :: IO () | ||
invalidateShakeCache = do | ||
let invalidateShakeCache = do | ||
void $ modifyVar' version succ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this should be moved out of the restart. |
||
join $ atomically $ recordDirtyKeys extras GhcSessionIO [emptyFilePath] | ||
return $ toNoFileKey GhcSessionIO | ||
|
||
IdeOptions{ optTesting = IdeTesting optTesting | ||
, optCheckProject = getCheckProject | ||
|
@@ -510,16 +509,16 @@ | |
TargetModule _ -> do | ||
found <- filterM (IO.doesFileExist . fromNormalizedFilePath) targetLocations | ||
return [(targetTarget, Set.fromList found)] | ||
hasUpdate <- join $ atomically $ do | ||
hasUpdate <- atomically $ do | ||
known <- readTVar knownTargetsVar | ||
let known' = flip mapHashed known $ \k -> | ||
HM.unionWith (<>) k $ HM.fromList knownTargets | ||
hasUpdate = if known /= known' then Just (unhashed known') else Nothing | ||
writeTVar knownTargetsVar known' | ||
logDirtyKeys <- recordDirtyKeys extras GetKnownTargets [emptyFilePath] | ||
return (logDirtyKeys >> pure hasUpdate) | ||
pure hasUpdate | ||
for_ hasUpdate $ \x -> | ||
logWith recorder Debug $ LogKnownFilesUpdated x | ||
return $ toNoFileKey GetKnownTargets | ||
|
||
-- Create a new HscEnv from a hieYaml root and a set of options | ||
let packageSetup :: (Maybe FilePath, NormalizedFilePath, ComponentOptions, FilePath) | ||
|
@@ -612,18 +611,14 @@ | |
, "If you are using a .cabal file, please ensure that this module is listed in either the exposed-modules or other-modules section" | ||
] | ||
|
||
void $ modifyVar' fileToFlags $ | ||
Map.insert hieYaml this_flags_map | ||
void $ modifyVar' filesMap $ | ||
flip HM.union (HM.fromList (map ((,hieYaml) . fst) $ concatMap toFlagsMap all_targets)) | ||
|
||
void $ extendKnownTargets all_targets | ||
|
||
-- Invalidate all the existing GhcSession build nodes by restarting the Shake session | ||
invalidateShakeCache | ||
|
||
void $ modifyVar' fileToFlags $ Map.insert hieYaml this_flags_map | ||
void $ modifyVar' filesMap $ flip HM.union (HM.fromList (map ((,hieYaml) . fst) $ concatMap toFlagsMap all_targets)) | ||
-- The VFS doesn't change on cradle edits, re-use the old one. | ||
restartShakeSession VFSUnmodified "new component" [] | ||
-- Invalidate all the existing GhcSession build nodes by restarting the Shake session | ||
keys2 <- invalidateShakeCache | ||
restartShakeSession VFSUnmodified "new component" [] $ do | ||
keys1 <- extendKnownTargets all_targets | ||
return [keys1, keys2] | ||
|
||
-- Typecheck all files in the project on startup | ||
checkProject <- getCheckProject | ||
|
@@ -678,7 +673,7 @@ | |
InstallationMismatch{..} -> | ||
return (([renderPackageSetupException cfp GhcVersionMismatch{..}], Nothing),[]) | ||
InstallationChecked _compileTime _ghcLibCheck -> do | ||
atomicModifyIORef' cradle_files (\xs -> (cfp:xs,())) | ||
Check warning on line 676 in ghcide/session-loader/Development/IDE/Session.hs
|
||
session (hieYaml, toNormalizedFilePath' cfp, opts, libDir) | ||
-- Failure case, either a cradle error or the none cradle | ||
Left err -> do | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -28,6 +28,7 @@ import qualified Development.IDE.Core.Shake as Shake | |
import Development.IDE.Graph | ||
import Development.IDE.Types.Location | ||
import Development.IDE.Types.Options | ||
import Development.IDE.Types.Shake (toKey) | ||
import qualified Focus | ||
import Ide.Logger (Pretty (pretty), | ||
Recorder, WithPriority, | ||
|
@@ -105,12 +106,12 @@ getFileExistsMapUntracked = do | |
FileExistsMapVar v <- getIdeGlobalAction | ||
return v | ||
|
||
-- | Modify the global store of file exists. | ||
modifyFileExists :: IdeState -> [(NormalizedFilePath, FileChangeType)] -> IO () | ||
-- | Modify the global store of file exists and return the keys that need to be marked as dirty | ||
modifyFileExists :: IdeState -> [(NormalizedFilePath, FileChangeType)] -> IO [Key] | ||
soulomoon marked this conversation as resolved.
Show resolved
Hide resolved
|
||
modifyFileExists state changes = do | ||
FileExistsMapVar var <- getIdeGlobalState state | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think |
||
-- Masked to ensure that the previous values are flushed together with the map update | ||
join $ mask_ $ atomicallyNamed "modifyFileExists" $ do | ||
mask_ $ atomicallyNamed "modifyFileExists" $ do | ||
forM_ changes $ \(f,c) -> | ||
case fromChange c of | ||
Just c' -> STM.focus (Focus.insert c') f var | ||
|
@@ -119,10 +120,10 @@ modifyFileExists state changes = do | |
-- flush previous values | ||
let (fileModifChanges, fileExistChanges) = | ||
partition ((== FileChangeType_Changed) . snd) changes | ||
mapM_ (deleteValue (shakeExtras state) GetFileExists . fst) fileExistChanges | ||
io1 <- recordDirtyKeys (shakeExtras state) GetFileExists $ map fst fileExistChanges | ||
io2 <- recordDirtyKeys (shakeExtras state) GetModificationTime $ map fst fileModifChanges | ||
return (io1 <> io2) | ||
keys0 <- concat <$> mapM (deleteValue (shakeExtras state) GetFileExists . fst) fileExistChanges | ||
soulomoon marked this conversation as resolved.
Show resolved
Hide resolved
|
||
let keys1 = map (toKey GetFileExists . fst) fileExistChanges | ||
let keys2 = map (toKey GetModificationTime . fst) fileModifChanges | ||
return (keys0 <> keys1 <> keys2) | ||
|
||
fromChange :: FileChangeType -> Maybe Bool | ||
fromChange FileChangeType_Created = Just True | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -49,6 +49,7 @@ import Development.IDE.Import.DependencyInformation | |
import Development.IDE.Types.Diagnostics | ||
import Development.IDE.Types.Location | ||
import Development.IDE.Types.Options | ||
import Development.IDE.Types.Shake (toKey) | ||
import HieDb.Create (deleteMissingRealFiles) | ||
import Ide.Logger (Pretty (pretty), | ||
Priority (Info), | ||
|
@@ -148,24 +149,24 @@ isInterface :: NormalizedFilePath -> Bool | |
isInterface f = takeExtension (fromNormalizedFilePath f) `elem` [".hi", ".hi-boot", ".hie", ".hie-boot", ".core"] | ||
|
||
-- | Reset the GetModificationTime state of interface files | ||
resetInterfaceStore :: ShakeExtras -> NormalizedFilePath -> STM () | ||
resetInterfaceStore :: ShakeExtras -> NormalizedFilePath -> STM [Key] | ||
resetInterfaceStore state f = do | ||
deleteValue state GetModificationTime f | ||
|
||
-- | Reset the GetModificationTime state of watched files | ||
-- Assumes the list does not include any FOIs | ||
resetFileStore :: IdeState -> [(NormalizedFilePath, LSP.FileChangeType)] -> IO () | ||
resetFileStore :: IdeState -> [(NormalizedFilePath, LSP.FileChangeType)] -> IO [Key] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This could be in STM, I think? Unless it's deliberately not to reduce contention. |
||
resetFileStore ideState changes = mask $ \_ -> do | ||
-- we record FOIs document versions in all the stored values | ||
-- so NEVER reset FOIs to avoid losing their versions | ||
-- FOI filtering is done by the caller (LSP Notification handler) | ||
forM_ changes $ \(nfp, c) -> do | ||
case c of | ||
LSP.FileChangeType_Changed | ||
-- already checked elsewhere | not $ HM.member nfp fois | ||
-> atomically $ | ||
deleteValue (shakeExtras ideState) GetModificationTime nfp | ||
_ -> pure () | ||
fmap concat <$> | ||
forM changes $ \(nfp, c) -> do | ||
case c of | ||
LSP.FileChangeType_Changed | ||
-- already checked elsewhere | not $ HM.member nfp fois | ||
-> atomically $ deleteValue (shakeExtras ideState) GetModificationTime nfp | ||
_ -> pure [] | ||
|
||
|
||
modificationTime :: FileVersion -> Maybe UTCTime | ||
|
@@ -215,16 +216,18 @@ setFileModified :: Recorder (WithPriority Log) | |
-> IdeState | ||
-> Bool -- ^ Was the file saved? | ||
-> NormalizedFilePath | ||
-> IO [Key] | ||
-> IO () | ||
setFileModified recorder vfs state saved nfp = do | ||
setFileModified recorder vfs state saved nfp actionBefore = do | ||
ideOptions <- getIdeOptionsIO $ shakeExtras state | ||
doCheckParents <- optCheckParents ideOptions | ||
let checkParents = case doCheckParents of | ||
AlwaysCheck -> True | ||
CheckOnSave -> saved | ||
_ -> False | ||
join $ atomically $ recordDirtyKeys (shakeExtras state) GetModificationTime [nfp] | ||
restartShakeSession (shakeExtras state) vfs (fromNormalizedFilePath nfp ++ " (modified)") [] | ||
restartShakeSession (shakeExtras state) vfs (fromNormalizedFilePath nfp ++ " (modified)") [] $ do | ||
keys<-actionBefore | ||
return (toKey GetModificationTime nfp:keys) | ||
when checkParents $ | ||
typecheckParents recorder state nfp | ||
|
||
|
@@ -244,14 +247,11 @@ typecheckParentsAction recorder nfp = do | |
-- | Note that some keys have been modified and restart the session | ||
-- Only valid if the virtual file system was initialised by LSP, as that | ||
-- independently tracks which files are modified. | ||
setSomethingModified :: VFSModified -> IdeState -> [Key] -> String -> IO () | ||
setSomethingModified vfs state keys reason = do | ||
setSomethingModified :: VFSModified -> IdeState -> String -> IO [Key] -> IO () | ||
setSomethingModified vfs state reason actionBetweenSession = do | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. again, it mostly computes dirty keys, right? |
||
-- Update database to remove any files that might have been renamed/deleted | ||
atomically $ do | ||
writeTQueue (indexQueue $ hiedbWriter $ shakeExtras state) (\withHieDb -> withHieDb deleteMissingRealFiles) | ||
modifyTVar' (dirtyKeys $ shakeExtras state) $ \x -> | ||
foldl' (flip insertKeySet) x keys | ||
void $ restartShakeSession (shakeExtras state) vfs reason [] | ||
atomically $ writeTQueue (indexQueue $ hiedbWriter $ shakeExtras state) (\withHieDb -> withHieDb deleteMissingRealFiles) | ||
void $ restartShakeSession (shakeExtras state) vfs reason [] actionBetweenSession | ||
|
||
registerFileWatches :: [String] -> LSP.LspT Config IO Bool | ||
registerFileWatches globs = do | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -57,7 +57,7 @@ | |
FileVersion(..), | ||
updatePositionMapping, | ||
updatePositionMappingHelper, | ||
deleteValue, recordDirtyKeys, | ||
deleteValue, | ||
WithProgressFunc, WithIndefiniteProgressFunc, | ||
ProgressEvent(..), | ||
DelayedAction, mkDelayedAction, | ||
|
@@ -123,7 +123,7 @@ | |
import Development.IDE.Core.ProgressReporting | ||
import Development.IDE.Core.RuleTypes | ||
import Development.IDE.Core.Tracing | ||
import Development.IDE.GHC.Compat (NameCache, | ||
Check warning on line 126 in ghcide/src/Development/IDE/Core/Shake.hs
|
||
NameCacheUpdater (..), | ||
initNameCache, | ||
knownKeyNames) | ||
|
@@ -300,6 +300,7 @@ | |
:: VFSModified | ||
-> String | ||
-> [DelayedAction ()] | ||
-> IO [Key] | ||
-> IO () | ||
#if MIN_VERSION_ghc(9,3,0) | ||
,ideNc :: NameCache | ||
|
@@ -557,26 +558,17 @@ | |
|
||
|
||
-- | Delete the value stored for a given ide build key | ||
-- and return the key that was deleted. | ||
deleteValue | ||
:: Shake.ShakeValue k | ||
=> ShakeExtras | ||
-> k | ||
-> NormalizedFilePath | ||
-> STM () | ||
deleteValue ShakeExtras{dirtyKeys, state} key file = do | ||
-> STM [Key] | ||
soulomoon marked this conversation as resolved.
Show resolved
Hide resolved
|
||
deleteValue ShakeExtras{state} key file = do | ||
STM.delete (toKey key file) state | ||
modifyTVar' dirtyKeys $ insertKeySet (toKey key file) | ||
return [toKey key file] | ||
|
||
recordDirtyKeys | ||
:: Shake.ShakeValue k | ||
=> ShakeExtras | ||
-> k | ||
-> [NormalizedFilePath] | ||
-> STM (IO ()) | ||
recordDirtyKeys ShakeExtras{dirtyKeys} key file = do | ||
modifyTVar' dirtyKeys $ \x -> foldl' (flip insertKeySet) x (toKey key <$> file) | ||
return $ withEventTrace "recordDirtyKeys" $ \addEvent -> do | ||
addEvent (fromString $ unlines $ "dirty " <> show key : map fromNormalizedFilePath file) | ||
|
||
-- | We return Nothing if the rule has not run and Just Failed if it has failed to produce a value. | ||
getValues :: | ||
|
@@ -759,12 +751,16 @@ | |
-- | Restart the current 'ShakeSession' with the given system actions. | ||
-- Any actions running in the current session will be aborted, | ||
-- but actions added via 'shakeEnqueue' will be requeued. | ||
shakeRestart :: Recorder (WithPriority Log) -> IdeState -> VFSModified -> String -> [DelayedAction ()] -> IO () | ||
shakeRestart recorder IdeState{..} vfs reason acts = | ||
shakeRestart :: Recorder (WithPriority Log) -> IdeState -> VFSModified -> String -> [DelayedAction ()] -> IO [Key] -> IO () | ||
shakeRestart recorder IdeState{..} vfs reason acts ioActionBetweenShakeSession = | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's more like "compute dirty keys", right? |
||
withMVar' | ||
shakeSession | ||
(\runner -> do | ||
(stopTime,()) <- duration $ logErrorAfter 10 $ cancelShakeSession runner | ||
keys <- ioActionBetweenShakeSession | ||
-- it is every important to update the dirty keys after we enter the critical section | ||
-- see Note [Housekeeping rule cache and dirty key outside of hls-graph] | ||
atomically $ modifyTVar' (dirtyKeys shakeExtras) $ \x -> foldl' (flip insertKeySet) x keys | ||
res <- shakeDatabaseProfile shakeDb | ||
backlog <- readTVarIO $ dirtyKeys shakeExtras | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we even need the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes, I am thinking about removing it too, but our garbage collector depend on it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So we have to keep it this way for now. Maybe until we find a better way to handle the garbage collector, but this should be in another PR. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It looks to me like the garbage collector calls There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, but it also mark the key as dirty in shakeExtra's dirtykeys at There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is odd since we might have cleared it in the rule cahce, |
||
queue <- atomicallyNamed "actionQueue - peek" $ peekInProgress $ actionQueue shakeExtras | ||
|
@@ -1198,7 +1194,7 @@ | |
Just (v@(Succeeded _ x), diags) -> do | ||
ver <- estimateFileVersionUnsafely key (Just x) file | ||
doDiagnostics (vfsVersion =<< ver) $ Vector.toList diags | ||
return $ Just $ RunResult ChangedNothing old $ A v | ||
return $ Just $ RunResult ChangedNothing old (A v) $ return () | ||
_ -> return Nothing | ||
_ -> | ||
-- assert that a "clean" rule is never a cache miss | ||
|
@@ -1222,7 +1218,6 @@ | |
Nothing -> do | ||
pure (toShakeValue ShakeStale mbBs, staleV) | ||
Just v -> pure (maybe ShakeNoCutoff ShakeResult mbBs, Succeeded ver v) | ||
liftIO $ atomicallyNamed "define - write" $ setValues state key file res (Vector.fromList diags) | ||
doDiagnostics (vfsVersion =<< ver) diags | ||
let eq = case (bs, fmap decodeShakeValue mbOld) of | ||
(ShakeResult a, Just (ShakeResult b)) -> cmp a b | ||
|
@@ -1232,9 +1227,12 @@ | |
_ -> False | ||
return $ RunResult | ||
(if eq then ChangedRecomputeSame else ChangedRecomputeDiff) | ||
(encodeShakeValue bs) $ | ||
A res | ||
liftIO $ atomicallyNamed "define - dirtyKeys" $ modifyTVar' dirtyKeys (deleteKeySet $ toKey key file) | ||
(encodeShakeValue bs) | ||
(A res) $ do | ||
-- this hook needs to be run in the same transaction as the key is marked clean | ||
-- see Note [Housekeeping rule cache and dirty key outside of hls-graph] | ||
setValues state key file res (Vector.fromList diags) | ||
modifyTVar' dirtyKeys (deleteKeySet $ toKey key file) | ||
soulomoon marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return res | ||
where | ||
-- Highly unsafe helper to compute the version of a file | ||
|
@@ -1258,6 +1256,32 @@ | |
-- * creating bogus "file does not exists" diagnostics | ||
| otherwise = useWithoutDependency (GetModificationTime_ False) fp | ||
|
||
-- Note [Housekeeping rule cache and dirty key outside of hls-graph] | ||
-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
-- Hls-graph contains its own internal running state for each key in the shakeDatabase. | ||
-- ShakeExtras contains `state` field (rule result cache) and `dirtyKeys` (keys that became | ||
-- dirty in between build sessions) that is not visible to the hls-graph | ||
-- Essentially, we need to keep the rule cache and dirty key and hls-graph's internal state | ||
-- in sync. | ||
|
||
-- 1. A dirty key collected in a session should not be removed from dirty keys in the same session. | ||
-- Since if we clean out the dirty key in the same session, | ||
-- 1.1. we will lose the chance to dirty its reverse dependencies. Since it only happens during session restart. | ||
-- 1.2. a key might be marked as dirty in ShakeExtras while it's being recomputed by hls-graph which could lead to it's premature removal from dirtyKeys. | ||
-- See issue https://github.com/haskell/haskell-language-server/issues/4093 for more details. | ||
|
||
-- 2. When a key is marked clean in the hls-graph's internal running | ||
-- state, the rule cache and dirty keys are updated in the same transaction. | ||
-- otherwise, some situations like the following can happen: | ||
-- thread 1: hls-graph session run a key | ||
-- thread 1: defineEarlyCutoff' run the action for the key | ||
-- thread 1: the action is done, rule cache and dirty key are updated | ||
-- thread 2: we restart the hls-graph session, thread 1 is killed, the | ||
-- hls-graph's internal state is not updated. | ||
-- This is problematic with early cut off because we are having a new rule cache matching the | ||
-- old hls-graph's internal state, which might case it's reverse dependency to skip the recomputation. | ||
-- See https://github.com/haskell/haskell-language-server/issues/4194 for more details. | ||
|
||
traceA :: A v -> String | ||
traceA (A Failed{}) = "Failed" | ||
traceA (A Stale{}) = "Stale" | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
does this in fact invalidate anything any more?