Skip to content

Commit d999084

Browse files
mpickeringwz1000
andauthored
Use stale information if it's available to answer requests quickly (#624)
* Use stale information for hover and completions This introduces a new function `useWithStaleFast` which returns with stale information WITHOUT checking freshness like `use` and `useWithStale`. Greatly improve debug logging All actions triggered by shakeRun now also pass an identifier which means that the debug logging shows which actions are starting/finishing We also distinguish between internal and external events. By default external events are ones triggered by runAction and the debug output is displayed to the user in command line and --lsp mode. In order to see internal logging statements, there is a new flag called --verbose which also prints out internal events such as file modification flushes. Cleaner variant using runAfter Step 1: Do not run actions with shakeRun Queue implementation, living, breathing Use a priority queue to schedule shake actions. Most user actions are answered immediately with a cache but also spawn a shake action to check the cached value we consulted was up to date. * Remove DelayedActionExtra * hlint * Fix progress * Always block instead of fail on initial computation * Can block for code lens * Update docs Co-authored-by: Zubin Duggal <[email protected]>
1 parent a873c28 commit d999084

14 files changed

+325
-194
lines changed

cabal.project

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
packages: .

exe/Arguments.hs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ data Arguments = Arguments
1414
,argsShakeProfiling :: Maybe FilePath
1515
,argsTesting :: Bool
1616
,argsThreads :: Int
17+
,argsVerbose :: Bool
1718
}
1819

1920
getArguments :: IO Arguments
@@ -33,3 +34,4 @@ arguments = Arguments
3334
<*> optional (strOption $ long "shake-profiling" <> metavar "DIR" <> help "Dump profiling reports to this directory")
3435
<*> switch (long "test" <> help "Enable additional lsp messages used by the testsuite")
3536
<*> option auto (short 'j' <> help "Number of threads (0: automatic)" <> metavar "NUM" <> value 0 <> showDefault)
37+
<*> switch (long "verbose" <> help "Include internal events in logging output")

exe/Main.hs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,9 +130,10 @@ main = do
130130
, optTesting = IdeTesting argsTesting
131131
, optThreads = argsThreads
132132
}
133+
logLevel = if argsVerbose then minBound else Info
133134
debouncer <- newAsyncDebouncer
134135
initialise caps (mainRule >> pluginRules plugins)
135-
getLspId event wProg wIndefProg (logger minBound) debouncer options vfs
136+
getLspId event wProg wIndefProg (logger logLevel) debouncer options vfs
136137
else do
137138
-- GHC produces messages with UTF8 in them, so make sure the terminal doesn't error
138139
hSetEncoding stdout utf8
@@ -161,7 +162,7 @@ main = do
161162

162163
putStrLn "\nStep 4/4: Type checking the files"
163164
setFilesOfInterest ide $ HashSet.fromList $ map toNormalizedFilePath' files
164-
results <- runAction ide $ uses TypeCheck (map toNormalizedFilePath' files)
165+
results <- runAction "User TypeCheck" ide $ uses TypeCheck (map toNormalizedFilePath' files)
165166
let (worked, failed) = partition fst $ zip (map isJust results) files
166167
when (failed /= []) $
167168
putStr $ unlines $ "Files that failed:" : map ((++) " * " . snd) failed

src/Development/IDE/Core/FileStore.hs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ module Development.IDE.Core.FileStore(
77
getFileContents,
88
getVirtualFile,
99
setBufferModified,
10+
setFileModified,
1011
setSomethingModified,
1112
fileStoreRules,
1213
VFSHandle,
@@ -31,6 +32,7 @@ import qualified Data.ByteString.Char8 as BS
3132
import Development.IDE.Types.Diagnostics
3233
import Development.IDE.Types.Location
3334
import Development.IDE.Core.OfInterest (kick)
35+
import Development.IDE.Core.RuleTypes
3436
import qualified Data.Rope.UTF16 as Rope
3537

3638
#ifdef mingw32_HOST_OS
@@ -45,6 +47,8 @@ import Foreign.Storable
4547
import qualified System.Posix.Error as Posix
4648
#endif
4749

50+
import qualified Development.IDE.Types.Logger as L
51+
4852
import Language.Haskell.LSP.Core
4953
import Language.Haskell.LSP.VFS
5054

@@ -180,6 +184,20 @@ setBufferModified state absFile contents = do
180184
set (filePathToUri' absFile) contents
181185
void $ shakeRestart state [kick]
182186

187+
-- | Note that some buffer for a specific file has been modified but not
188+
-- with what changes.
189+
setFileModified :: IdeState -> NormalizedFilePath -> IO ()
190+
setFileModified state nfp = do
191+
VFSHandle{..} <- getIdeGlobalState state
192+
when (isJust setVirtualFileContents) $
193+
fail "setSomethingModified can't be called on this type of VFSHandle"
194+
let da = mkDelayedAction "FileStoreTC" L.Info $ do
195+
ShakeExtras{progressUpdate} <- getShakeExtras
196+
liftIO $ progressUpdate KickStarted
197+
void $ use GetSpanInfo nfp
198+
liftIO $ progressUpdate KickCompleted
199+
shakeRestart state [da]
200+
183201
-- | Note that some buffer somewhere has been modified, but don't say what.
184202
-- Only valid if the virtual file system was initialised by LSP, as that
185203
-- independently tracks which files are modified.

src/Development/IDE/Core/OfInterest.hs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,13 @@ import Data.HashSet (HashSet)
2424
import qualified Data.HashSet as HashSet
2525
import qualified Data.Text as T
2626
import Data.Tuple.Extra
27-
import Data.Functor
2827
import Development.Shake
2928

3029
import Development.IDE.Types.Location
3130
import Development.IDE.Types.Logger
3231
import Development.IDE.Core.RuleTypes
3332
import Development.IDE.Core.Shake
34-
33+
import Control.Monad
3534

3635
newtype OfInterestVar = OfInterestVar (Var (HashSet NormalizedFilePath))
3736
instance IsIdeGlobal OfInterestVar
@@ -81,12 +80,13 @@ modifyFilesOfInterest state f = do
8180
OfInterestVar var <- getIdeGlobalState state
8281
files <- modifyVar var $ pure . dupe . f
8382
logDebug (ideLogger state) $ "Set files of interest to: " <> T.pack (show $ HashSet.toList files)
84-
void $ shakeRestart state [kick]
83+
let das = map (\nfp -> mkDelayedAction "OfInterest" Debug (use GetSpanInfo nfp)) (HashSet.toList files)
84+
shakeRestart state das
8585

8686
-- | Typecheck all the files of interest.
8787
-- Could be improved
88-
kick :: Action ()
89-
kick = do
88+
kick :: DelayedAction ()
89+
kick = mkDelayedAction "kick" Debug $ do
9090
files <- getFilesOfInterest
9191
ShakeExtras{progressUpdate} <- getShakeExtras
9292
liftIO $ progressUpdate KickStarted

src/Development/IDE/Core/Rules.hs

Lines changed: 74 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import Development.IDE.GHC.Compat hiding (parseModule, typecheckModule)
5050
import Development.IDE.GHC.Util
5151
import Development.IDE.GHC.WithDynFlags
5252
import Data.Either.Extra
53+
import qualified Development.IDE.Types.Logger as L
5354
import Data.Maybe
5455
import Data.Foldable
5556
import qualified Data.IntMap.Strict as IntMap
@@ -62,6 +63,7 @@ import Development.Shake hiding (Diagnostic)
6263
import Development.IDE.Core.RuleTypes
6364
import Development.IDE.Spans.Type
6465
import qualified Data.ByteString.Char8 as BS
66+
import Development.IDE.Core.PositionMapping
6567

6668
import qualified GHC.LanguageExtensions as LangExt
6769
import HscTypes
@@ -76,10 +78,12 @@ import Development.Shake.Classes hiding (get, put)
7678
import Control.Monad.Trans.Except (runExceptT)
7779
import Data.ByteString (ByteString)
7880
import Control.Concurrent.Async (concurrently)
81+
import System.Time.Extra
82+
import Control.Monad.Reader
83+
import System.Directory ( getModificationTime )
84+
import Control.Exception
7985

8086
import Control.Monad.State
81-
import System.IO.Error (isDoesNotExistError)
82-
import Control.Exception.Safe (IOException, catch)
8387
import FastString (FastString(uniq))
8488
import qualified HeaderInfo as Hdr
8589

@@ -91,14 +95,14 @@ toIdeResult = either (, Nothing) (([],) . Just)
9195

9296
-- | useE is useful to implement functions that aren’t rules but need shortcircuiting
9397
-- e.g. getDefinition.
94-
useE :: IdeRule k v => k -> NormalizedFilePath -> MaybeT Action v
95-
useE k = MaybeT . use k
98+
useE :: IdeRule k v => k -> NormalizedFilePath -> MaybeT IdeAction (v, PositionMapping)
99+
useE k = MaybeT . useWithStaleFast k
96100

97-
useNoFileE :: IdeRule k v => k -> MaybeT Action v
98-
useNoFileE k = useE k emptyFilePath
101+
useNoFileE :: IdeRule k v => IdeState -> k -> MaybeT IdeAction v
102+
useNoFileE _ide k = fst <$> useE k emptyFilePath
99103

100-
usesE :: IdeRule k v => k -> [NormalizedFilePath] -> MaybeT Action [v]
101-
usesE k = MaybeT . fmap sequence . uses k
104+
usesE :: IdeRule k v => k -> [NormalizedFilePath] -> MaybeT IdeAction [(v,PositionMapping)]
105+
usesE k = MaybeT . fmap sequence . mapM (useWithStaleFast k)
102106

103107
defineNoFile :: IdeRule k v => (k -> Action v) -> Rules ()
104108
defineNoFile f = define $ \k file -> do
@@ -120,78 +124,91 @@ getDependencies :: NormalizedFilePath -> Action (Maybe [NormalizedFilePath])
120124
getDependencies file = fmap transitiveModuleDeps <$> use GetDependencies file
121125

122126
-- | Try to get hover text for the name under point.
123-
getAtPoint :: NormalizedFilePath -> Position -> Action (Maybe (Maybe Range, [T.Text]))
127+
getAtPoint :: NormalizedFilePath -> Position -> IdeAction (Maybe (Maybe Range, [T.Text]))
124128
getAtPoint file pos = fmap join $ runMaybeT $ do
125-
opts <- lift getIdeOptions
126-
spans <- useE GetSpanInfo file
127-
return $ AtPoint.atPoint opts spans pos
129+
ide <- ask
130+
opts <- liftIO $ getIdeOptionsIO ide
131+
(spans, mapping) <- useE GetSpanInfo file
132+
!pos' <- MaybeT (return $ fromCurrentPosition mapping pos)
133+
return $ AtPoint.atPoint opts spans pos'
128134

129135
-- | Goto Definition.
130-
getDefinition :: NormalizedFilePath -> Position -> Action (Maybe Location)
131-
getDefinition file pos = fmap join $ runMaybeT $ do
132-
opts <- lift getIdeOptions
133-
spans <- useE GetSpanInfo file
134-
lift $ AtPoint.gotoDefinition (getHieFile file) opts (spansExprs spans) pos
135-
136-
getTypeDefinition :: NormalizedFilePath -> Position -> Action (Maybe [Location])
136+
getDefinition :: NormalizedFilePath -> Position -> IdeAction (Maybe Location)
137+
getDefinition file pos = runMaybeT $ do
138+
ide <- ask
139+
opts <- liftIO $ getIdeOptionsIO ide
140+
(spans,mapping) <- useE GetSpanInfo file
141+
!pos' <- MaybeT (return $ fromCurrentPosition mapping pos)
142+
AtPoint.gotoDefinition (getHieFile ide file) opts (spansExprs spans) pos'
143+
144+
getTypeDefinition :: NormalizedFilePath -> Position -> IdeAction (Maybe [Location])
137145
getTypeDefinition file pos = runMaybeT $ do
138-
opts <- lift getIdeOptions
139-
spans <- useE GetSpanInfo file
140-
lift $ AtPoint.gotoTypeDefinition (getHieFile file) opts (spansExprs spans) pos
141-
146+
ide <- ask
147+
opts <- liftIO $ getIdeOptionsIO ide
148+
(spans,mapping) <- useE GetSpanInfo file
149+
!pos' <- MaybeT (return $ fromCurrentPosition mapping pos)
150+
AtPoint.gotoTypeDefinition (getHieFile ide file) opts (spansExprs spans) pos'
142151

143152
getHieFile
144-
:: NormalizedFilePath -- ^ file we're editing
153+
:: ShakeExtras
154+
-> NormalizedFilePath -- ^ file we're editing
145155
-> Module -- ^ module dep we want info for
146-
-> Action (Maybe (HieFile, FilePath)) -- ^ hie stuff for the module
147-
getHieFile file mod = do
148-
TransitiveDependencies {transitiveNamedModuleDeps} <- use_ GetDependencies file
156+
-> MaybeT IdeAction (HieFile, FilePath) -- ^ hie stuff for the module
157+
getHieFile ide file mod = do
158+
TransitiveDependencies {transitiveNamedModuleDeps} <- fst <$> useE GetDependencies file
149159
case find (\x -> nmdModuleName x == moduleName mod) transitiveNamedModuleDeps of
150160
Just NamedModuleDep{nmdFilePath=nfp} -> do
151161
let modPath = fromNormalizedFilePath nfp
152-
(_diags, hieFile) <- getHomeHieFile nfp
153-
return $ (, modPath) <$> hieFile
154-
_ -> getPackageHieFile mod file
155-
162+
hieFile <- getHomeHieFile nfp
163+
return (hieFile, modPath)
164+
_ -> getPackageHieFile ide mod file
156165

157-
getHomeHieFile :: NormalizedFilePath -> Action ([IOException], Maybe HieFile)
166+
getHomeHieFile :: NormalizedFilePath -> MaybeT IdeAction HieFile
158167
getHomeHieFile f = do
159-
ms <- use_ GetModSummary f
160-
161-
-- .hi and .hie files are generated as a byproduct of typechecking.
162-
-- To avoid duplicating staleness checking already performed for .hi files,
163-
-- we overapproximate here by depending on the GetModIface rule.
164-
hiFile <- use GetModIface f
165-
166-
case hiFile of
167-
Nothing -> return ([], Nothing)
168-
Just _ -> liftIO $ do
169-
hf <- loadHieFile $ ml_hie_file $ ms_location ms
170-
return ([], Just hf)
171-
`catch` \e ->
172-
if isDoesNotExistError e
173-
then return ([], Nothing)
174-
else return ([e], Nothing)
175-
176-
getPackageHieFile :: Module -- ^ Package Module to load .hie file for
168+
ms <- fst <$> useE GetModSummary f
169+
let normal_hie_f = toNormalizedFilePath' hie_f
170+
hie_f = ml_hie_file $ ms_location ms
171+
172+
mbHieTimestamp <- either (\(_ :: IOException) -> Nothing) Just <$> (liftIO $ try $ getModificationTime hie_f)
173+
srcTimestamp <- MaybeT (either (\(_ :: IOException) -> Nothing) Just <$> (liftIO $ try $ getModificationTime $ fromNormalizedFilePath f))
174+
liftIO $ print (mbHieTimestamp, srcTimestamp, hie_f, normal_hie_f)
175+
let isUpToDate
176+
| Just d <- mbHieTimestamp = d > srcTimestamp
177+
| otherwise = False
178+
179+
if isUpToDate
180+
then do
181+
hf <- liftIO $ whenMaybe isUpToDate (loadHieFile hie_f)
182+
MaybeT $ return hf
183+
else do
184+
wait <- lift $ delayedAction $ mkDelayedAction "OutOfDateHie" L.Info $ do
185+
hsc <- hscEnv <$> use_ GhcSession f
186+
pm <- use_ GetParsedModule f
187+
typeCheckRuleDefinition hsc pm DoGenerateInterfaceFiles
188+
_ <- MaybeT $ liftIO $ timeout 1 wait
189+
liftIO $ loadHieFile hie_f
190+
191+
192+
getPackageHieFile :: ShakeExtras
193+
-> Module -- ^ Package Module to load .hie file for
177194
-> NormalizedFilePath -- ^ Path of home module importing the package module
178-
-> Action (Maybe (HieFile, FilePath))
179-
getPackageHieFile mod file = do
180-
pkgState <- hscEnv <$> use_ GhcSession file
181-
IdeOptions {..} <- getIdeOptions
195+
-> MaybeT IdeAction (HieFile, FilePath)
196+
getPackageHieFile ide mod file = do
197+
pkgState <- hscEnv . fst <$> useE GhcSession file
198+
IdeOptions {..} <- liftIO $ getIdeOptionsIO ide
182199
let unitId = moduleUnitId mod
183200
case lookupPackageConfig unitId pkgState of
184201
Just pkgConfig -> do
185202
-- 'optLocateHieFile' returns Nothing if the file does not exist
186203
hieFile <- liftIO $ optLocateHieFile optPkgLocationOpts pkgConfig mod
187204
path <- liftIO $ optLocateSrcFile optPkgLocationOpts pkgConfig mod
188205
case (hieFile, path) of
189-
(Just hiePath, Just modPath) ->
206+
(Just hiePath, Just modPath) -> MaybeT $
190207
-- deliberately loaded outside the Shake graph
191208
-- to avoid dependencies on non-workspace files
192209
liftIO $ Just . (, modPath) <$> loadHieFile hiePath
193-
_ -> return Nothing
194-
_ -> return Nothing
210+
_ -> MaybeT $ return Nothing
211+
_ -> MaybeT $ return Nothing
195212

196213
-- | Parse the contents of a daml file.
197214
getParsedModule :: NormalizedFilePath -> Action (Maybe ParsedModule)

src/Development/IDE/Core/Service.hs

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
-- using the "Shaker" abstraction layer for in-memory use.
1010
--
1111
module Development.IDE.Core.Service(
12-
getIdeOptions,
12+
getIdeOptions, getIdeOptionsIO,
1313
IdeState, initialise, shutdown,
1414
runAction,
1515
writeProfile,
@@ -20,24 +20,21 @@ module Development.IDE.Core.Service(
2020

2121
import Data.Maybe
2222
import Development.IDE.Types.Options (IdeOptions(..))
23-
import Control.Monad
2423
import Development.IDE.Core.Debouncer
2524
import Development.IDE.Core.FileStore (VFSHandle, fileStoreRules)
2625
import Development.IDE.Core.FileExists (fileExistsRules)
2726
import Development.IDE.Core.OfInterest
28-
import Development.IDE.Types.Logger
27+
import Development.IDE.Types.Logger as Logger
2928
import Development.Shake
3029
import qualified Language.Haskell.LSP.Messages as LSP
3130
import qualified Language.Haskell.LSP.Types as LSP
3231
import qualified Language.Haskell.LSP.Types.Capabilities as LSP
3332

3433
import Development.IDE.Core.Shake
34+
import Control.Monad
3535

3636

3737

38-
newtype GlobalIdeOptions = GlobalIdeOptions IdeOptions
39-
instance IsIdeGlobal GlobalIdeOptions
40-
4138
------------------------------------------------------------
4239
-- Exposed API
4340

@@ -84,10 +81,6 @@ shutdown = shakeShut
8481
-- This will return as soon as the result of the action is
8582
-- available. There might still be other rules running at this point,
8683
-- e.g., the ofInterestRule.
87-
runAction :: IdeState -> Action a -> IO a
88-
runAction ide action = join $ shakeEnqueue ide action
89-
90-
getIdeOptions :: Action IdeOptions
91-
getIdeOptions = do
92-
GlobalIdeOptions x <- getIdeGlobalAction
93-
return x
84+
runAction :: String -> IdeState -> Action a -> IO a
85+
runAction herald ide act =
86+
join $ shakeEnqueue ide (mkDelayedAction herald Logger.Info act)

0 commit comments

Comments
 (0)