ghcide-2.10.0.0: src/Development/IDE/Core/ProgressReporting.hs
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}
module Development.IDE.Core.ProgressReporting
( ProgressEvent (..),
PerFileProgressReporting (..),
ProgressReporting,
noPerFileProgressReporting,
progressReporting,
progressReportingNoTrace,
-- utilities, reexported for use in Core.Shake
mRunLspT,
mRunLspTCallback,
-- for tests
recordProgress,
InProgressState (..),
progressStop,
progressUpdate
)
where
import Control.Concurrent.STM (STM)
import Control.Concurrent.STM.Stats (TVar, atomically,
atomicallyNamed, modifyTVar',
newTVarIO, readTVar, retry)
import Control.Concurrent.Strict (modifyVar_, newVar,
threadDelay)
import Control.Monad.Extra hiding (loop)
import Control.Monad.IO.Class
import Control.Monad.Trans.Class (lift)
import Data.Functor (($>))
import qualified Data.Text as T
import Development.IDE.GHC.Orphans ()
import Development.IDE.Types.Location
import Development.IDE.Types.Options
import qualified Focus
import Language.LSP.Protocol.Types
import Language.LSP.Server (ProgressAmount (..),
ProgressCancellable (..),
withProgress)
import qualified Language.LSP.Server as LSP
import qualified StmContainers.Map as STM
import UnliftIO (Async, async, bracket, cancel)
data ProgressEvent
= ProgressNewStarted
| ProgressCompleted
| ProgressStarted
data ProgressReporting = ProgressReporting
{ _progressUpdate :: ProgressEvent -> IO (),
_progressStop :: IO ()
-- ^ we are using IO here because creating and stopping the `ProgressReporting`
-- is different from how we use it.
}
data PerFileProgressReporting = PerFileProgressReporting
{
inProgress :: forall a. NormalizedFilePath -> IO a -> IO a,
-- ^ see Note [ProgressReporting API and InProgressState]
progressReportingInner :: ProgressReporting
}
class ProgressReporter a where
progressUpdate :: a -> ProgressEvent -> IO ()
progressStop :: a -> IO ()
instance ProgressReporter ProgressReporting where
progressUpdate = _progressUpdate
progressStop = _progressStop
instance ProgressReporter PerFileProgressReporting where
progressUpdate = _progressUpdate . progressReportingInner
progressStop = _progressStop . progressReportingInner
{- Note [ProgressReporting API and InProgressState]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The progress of tasks can be tracked in two ways:
1. `ProgressReporting`: we have an internal state that actively tracks the progress.
Changes to the progress are made directly to this state.
2. `ProgressReporting`: there is an external state that tracks the progress.
The external state is converted into an STM Int for the purpose of reporting progress.
The `inProgress` function is only useful when we are using `ProgressReporting`.
-}
noProgressReporting :: ProgressReporting
noProgressReporting = ProgressReporting
{ _progressUpdate = const $ pure (),
_progressStop = pure ()
}
noPerFileProgressReporting :: IO PerFileProgressReporting
noPerFileProgressReporting =
return $
PerFileProgressReporting
{ inProgress = const id,
progressReportingInner = noProgressReporting
}
-- | State used in 'delayedProgressReporting'
data State
= NotStarted
| Stopped
| Running (Async ())
-- | State transitions used in 'delayedProgressReporting'
data Transition = Event ProgressEvent | StopProgress
updateState :: IO () -> Transition -> State -> IO State
updateState _ _ Stopped = pure Stopped
updateState start (Event ProgressNewStarted) NotStarted = Running <$> async start
updateState start (Event ProgressNewStarted) (Running job) = cancel job >> Running <$> async start
updateState start (Event ProgressStarted) NotStarted = Running <$> async start
updateState _ (Event ProgressStarted) (Running job) = return (Running job)
updateState _ (Event ProgressCompleted) (Running job) = cancel job $> NotStarted
updateState _ (Event ProgressCompleted) st = pure st
updateState _ StopProgress (Running job) = cancel job $> Stopped
updateState _ StopProgress st = pure st
-- | Data structure to track progress across the project
-- see Note [ProgressReporting API and InProgressState]
data InProgressState
= InProgressState
{ -- | Number of files to do
todoVar :: TVar Int,
-- | Number of files done
doneVar :: TVar Int,
currentVar :: STM.Map NormalizedFilePath Int
}
newInProgress :: IO InProgressState
newInProgress = InProgressState <$> newTVarIO 0 <*> newTVarIO 0 <*> STM.newIO
recordProgress :: InProgressState -> NormalizedFilePath -> (Int -> Int) -> IO ()
recordProgress InProgressState {..} file shift = do
(prev, new) <- atomicallyNamed "recordProgress" $ STM.focus alterPrevAndNew file currentVar
atomicallyNamed "recordProgress2" $ case (prev, new) of
(Nothing, 0) -> modifyTVar' doneVar (+ 1) >> modifyTVar' todoVar (+ 1)
(Nothing, _) -> modifyTVar' todoVar (+ 1)
(Just 0, 0) -> pure ()
(Just 0, _) -> modifyTVar' doneVar pred
(Just _, 0) -> modifyTVar' doneVar (+ 1)
(Just _, _) -> pure ()
where
alterPrevAndNew = do
prev <- Focus.lookup
Focus.alter alter
new <- Focus.lookupWithDefault 0
return (prev, new)
alter x = let x' = maybe (shift 0) shift x in Just x'
-- | `progressReportingNoTrace` initiates a new progress reporting session.
-- It functions similarly to `progressReporting`, but it utilizes an external state for progress tracking.
-- Refer to Note [ProgressReporting API and InProgressState] for more details.
progressReportingNoTrace ::
STM Int ->
STM Int ->
Maybe (LSP.LanguageContextEnv c) ->
T.Text ->
ProgressReportingStyle ->
IO ProgressReporting
progressReportingNoTrace _ _ Nothing _title _optProgressStyle = return noProgressReporting
progressReportingNoTrace todo done (Just lspEnv) title optProgressStyle = do
progressState <- newVar NotStarted
let _progressUpdate event = liftIO $ updateStateVar $ Event event
_progressStop = updateStateVar StopProgress
updateStateVar = modifyVar_ progressState . updateState (progressCounter lspEnv title optProgressStyle todo done)
return ProgressReporting {..}
-- | `progressReporting` initiates a new progress reporting session.
-- It necessitates the active tracking of progress using the `inProgress` function.
-- Refer to Note [ProgressReporting API and InProgressState] for more details.
progressReporting ::
Maybe (LSP.LanguageContextEnv c) ->
T.Text ->
ProgressReportingStyle ->
IO PerFileProgressReporting
progressReporting Nothing _title _optProgressStyle = noPerFileProgressReporting
progressReporting (Just lspEnv) title optProgressStyle = do
inProgressState <- newInProgress
progressReportingInner <- progressReportingNoTrace (readTVar $ todoVar inProgressState)
(readTVar $ doneVar inProgressState) (Just lspEnv) title optProgressStyle
let
inProgress :: NormalizedFilePath -> IO a -> IO a
inProgress = updateStateForFile inProgressState
return PerFileProgressReporting {..}
where
updateStateForFile inProgress file = UnliftIO.bracket (liftIO $ f succ) (const $ liftIO $ f pred) . const
where
-- This functions are deliberately eta-expanded to avoid space leaks.
-- Do not remove the eta-expansion without profiling a session with at
-- least 1000 modifications.
f = recordProgress inProgress file
-- Kill this to complete the progress session
progressCounter ::
LSP.LanguageContextEnv c ->
T.Text ->
ProgressReportingStyle ->
STM Int ->
STM Int ->
IO ()
progressCounter lspEnv title optProgressStyle getTodo getDone =
LSP.runLspT lspEnv $ withProgress title Nothing NotCancellable $ \update -> loop update 0
where
loop _ _ | optProgressStyle == NoProgress = forever $ liftIO $ threadDelay maxBound
loop update prevPct = do
(todo, done, nextPct) <- liftIO $ atomically $ do
todo <- getTodo
done <- getDone
let nextFrac :: Double
nextFrac = if todo == 0 then 0 else fromIntegral done / fromIntegral todo
nextPct :: UInt
nextPct = floor $ 100 * nextFrac
when (nextPct == prevPct) retry
pure (todo, done, nextPct)
_ <- update (ProgressAmount (Just nextPct) (Just $ T.pack $ show done <> "/" <> show todo))
loop update nextPct
mRunLspT :: (Applicative m) => Maybe (LSP.LanguageContextEnv c) -> LSP.LspT c m () -> m ()
mRunLspT (Just lspEnv) f = LSP.runLspT lspEnv f
mRunLspT Nothing _ = pure ()
mRunLspTCallback ::
(Monad m) =>
Maybe (LSP.LanguageContextEnv c) ->
(LSP.LspT c m a -> LSP.LspT c m a) ->
m a ->
m a
mRunLspTCallback (Just lspEnv) f g = LSP.runLspT lspEnv $ f (lift g)
mRunLspTCallback Nothing _ g = g