From 23d1e814dd105f8becf34263630847835f31a98d Mon Sep 17 00:00:00 2001 From: soulomoon Date: Wed, 6 May 2026 20:16:49 +0800 Subject: [PATCH 01/32] hls-graph: isolate runtime engine improvements --- hls-graph/hls-graph.cabal | 2 + hls-graph/src/Development/IDE/Graph.hs | 1 + .../src/Development/IDE/Graph/Database.hs | 116 ++++- .../Development/IDE/Graph/Internal/Action.hs | 124 +++-- .../IDE/Graph/Internal/Database.hs | 478 ++++++++++-------- .../src/Development/IDE/Graph/Internal/Key.hs | 58 ++- .../Development/IDE/Graph/Internal/Rules.hs | 2 + .../Development/IDE/Graph/Internal/Types.hs | 407 +++++++++++++-- hls-graph/test/ActionSpec.hs | 55 +- hls-graph/test/DatabaseSpec.hs | 11 +- 10 files changed, 912 insertions(+), 342 deletions(-) diff --git a/hls-graph/hls-graph.cabal b/hls-graph/hls-graph.cabal index 52bec5beac..024e44436b 100644 --- a/hls-graph/hls-graph.cabal +++ b/hls-graph/hls-graph.cabal @@ -70,6 +70,7 @@ library autogen-modules: Paths_hls_graph hs-source-dirs: src build-depends: + , mtl ^>=2.3.1 , aeson , async >=2.0 , base >=4.12 && <5 @@ -92,6 +93,7 @@ library , transformers , unliftio , unordered-containers + , prettyprinter if flag(embed-files) cpp-options: -DFILE_EMBED diff --git a/hls-graph/src/Development/IDE/Graph.hs b/hls-graph/src/Development/IDE/Graph.hs index 81ad3b3dfd..915da203aa 100644 --- a/hls-graph/src/Development/IDE/Graph.hs +++ b/hls-graph/src/Development/IDE/Graph.hs @@ -18,6 +18,7 @@ module Development.IDE.Graph( -- * Actions for inspecting the keys in the database getDirtySet, getKeysAndVisitedAge, + module Development.IDE.Graph.KeyMap, module Development.IDE.Graph.KeySet, ) where diff --git a/hls-graph/src/Development/IDE/Graph/Database.hs b/hls-graph/src/Development/IDE/Graph/Database.hs index bd8601cd16..702a6eed0a 100644 --- a/hls-graph/src/Development/IDE/Graph/Database.hs +++ b/hls-graph/src/Development/IDE/Graph/Database.hs @@ -2,17 +2,39 @@ module Development.IDE.Graph.Database( ShakeDatabase, ShakeValue, shakeNewDatabase, + shakeNewDatabaseWithRuntime, shakeRunDatabase, shakeRunDatabaseForKeys, + shakeRunDatabaseWithExceptions, + shakeRunDatabaseForKeysWithExceptions, + shakeRunDatabaseForKeysSep, shakeProfileDatabase, shakeGetBuildStep, shakeGetDatabaseKeys, shakeGetDirtySet, shakeGetCleanKeys - ,shakeGetBuildEdges) where -import Control.Concurrent.STM.Stats (readTVarIO) + ,shakeGetBuildEdges, + shakeShutDatabase, + shakeGetActionQueueLength, + shakeComputeToPreserve, + -- shakedatabaseRuntimeDep, + shakePeekAsyncsDelivers, + instantiateDelayedAction, + mkDelayedAction, + shakeDatabaseSize) where +import Control.Concurrent.Extra (Barrier, newBarrier, + signalBarrier, + waitBarrierMaybe) +import Control.Concurrent.STM.Stats (atomically, + atomicallyNamed, + readTVarIO) +import Control.Exception (SomeException, + throwIO, try) +import Control.Monad (join, unless, void) +import Control.Monad.IO.Class (liftIO) import Data.Dynamic import Data.Maybe +import Data.Unique import Development.IDE.Graph.Classes () import Development.IDE.Graph.Internal.Action import Development.IDE.Graph.Internal.Database @@ -21,20 +43,33 @@ import Development.IDE.Graph.Internal.Options import Development.IDE.Graph.Internal.Profile (writeProfile) import Development.IDE.Graph.Internal.Rules import Development.IDE.Graph.Internal.Types +import qualified Development.IDE.Graph.Internal.Types as Logger +import qualified StmContainers.Map as SMap -- Placeholder to be the 'extra' if the user doesn't set it data NonExportedType = NonExportedType +shakeShutDatabase :: KeySet -> ShakeDatabase -> IO () +shakeShutDatabase dirties (ShakeDatabase _ _ db) = shutDatabase dirties db + shakeNewDatabase :: ShakeOptions -> Rules () -> IO ShakeDatabase shakeNewDatabase opts rules = do + aq <- newQueue + shakeNewDatabaseWithRuntime (const $ pure ()) aq opts rules + +shakeNewDatabaseWithRuntime :: (String -> IO ()) -> ActionQueue -> ShakeOptions -> Rules () -> IO ShakeDatabase +shakeNewDatabaseWithRuntime l aq opts rules = do let extra = fromMaybe (toDyn NonExportedType) $ shakeExtra opts (theRules, actions) <- runRules extra rules - db <- newDatabase extra theRules + db <- newDatabase l aq extra theRules pure $ ShakeDatabase (length actions) actions db shakeRunDatabase :: ShakeDatabase -> [Action a] -> IO [a] -shakeRunDatabase = shakeRunDatabaseForKeys Nothing +shakeRunDatabase s xs = shakeRunDatabaseForKeys Nothing s xs + +shakeRunDatabaseWithExceptions :: ShakeDatabase -> [Action a] -> IO [Either SomeException a] +shakeRunDatabaseWithExceptions s xs = shakeRunDatabaseForKeysWithExceptions Nothing s xs -- | Returns the set of dirty keys annotated with their age (in # of builds) shakeGetDirtySet :: ShakeDatabase -> IO [(Key, Int)] @@ -52,19 +87,80 @@ unvoid :: Functor m => m () -> m a unvoid = fmap undefined -- | Assumes that the database is not running a build +-- The nested IO is to +-- seperate incrementing the step from running the build. +-- Also immediately enqueues upsweep actions for the newly dirty keys. +shakeRunDatabaseForKeysSep + :: Maybe (([Key],[Key]),KeySet) -- ^ Set of keys changed since last run. 'Nothing' means everything has changed + -> ShakeDatabase + -> [Action a] + -> IO (IO [Either SomeException a]) +shakeRunDatabaseForKeysSep keysChanged sdb@(ShakeDatabase _ as1 db) acts = do + preserves <- incDatabase db keysChanged + reenqueued <- atomicallyNamed "actionQueue - peek" $ peekInProgress (databaseActionQueue db) + let reenqueuedExceptPreserves = filter (\d -> uniqueID d `notMemberKeySet` preserves) reenqueued + let ignoreResultActs = as1 + return $ do + seqRunActions (newKey "root") db $ map (pumpActionThreadReRun sdb) reenqueuedExceptPreserves + drop (length ignoreResultActs) <$> runActions (newKey "root") db (map unvoid ignoreResultActs ++ acts) + +instantiateDelayedAction + :: DelayedAction a + -> IO (Barrier (Either SomeException a), DelayedActionInternal) +instantiateDelayedAction (DelayedAction u s p a) = do + b <- newBarrier + let a' = do + -- work gets reenqueued when the Shake session is restarted + -- it can happen that a work item finished just as it was reenqueued + -- in that case, skipping the work is fine + alreadyDone <- liftIO $ isJust <$> waitBarrierMaybe b + unless alreadyDone $ do + x <- actionCatch @SomeException (Right <$> a) (pure . Left) + -- ignore exceptions if the barrier has been filled concurrently + liftIO $ void $ try @SomeException $ signalBarrier b x + d' = DelayedAction u s p a' + return (b, d') + +mkDelayedAction :: String -> Logger.Priority -> Action a -> IO (DelayedAction a) +mkDelayedAction s p a = do + u <- newUnique + return $ DelayedAction (newDirectKey $ hashUnique u) s (toEnum (fromEnum p)) a + +shakeComputeToPreserve :: ShakeDatabase -> KeySet -> IO (KeySet, ([Key], [Key]), Int, [Key]) +shakeComputeToPreserve (ShakeDatabase _ _ db) ks = atomically (computeToPreserve db ks) + shakeRunDatabaseForKeys :: Maybe [Key] -- ^ Set of keys changed since last run. 'Nothing' means everything has changed -> ShakeDatabase -> [Action a] -> IO [a] -shakeRunDatabaseForKeys keysChanged (ShakeDatabase lenAs1 as1 db) as2 = do - incDatabase db keysChanged - fmap (drop lenAs1) $ runActions db $ map unvoid as1 ++ as2 +shakeRunDatabaseForKeys keysChanged sdb as2 = + shakeRunDatabaseForKeysWithExceptions keysChanged sdb as2 >>= traverse (either throwIO pure) + +shakeRunDatabaseForKeysWithExceptions + :: Maybe [Key] + -- ^ Set of keys changed since last run. 'Nothing' means everything has changed + -> ShakeDatabase + -> [Action a] + -> IO [Either SomeException a] +shakeRunDatabaseForKeysWithExceptions Nothing sdb as2 = join $ shakeRunDatabaseForKeysSep Nothing sdb as2 +shakeRunDatabaseForKeysWithExceptions (Just x) sdb as2 = + let y = fromListKeySet x in join $ shakeRunDatabaseForKeysSep (Just (([], toListKeySet y), y)) sdb as2 + + +shakePeekAsyncsDelivers :: ShakeDatabase -> IO [DeliverStatus] +shakePeekAsyncsDelivers (ShakeDatabase _ _ db) = peekAsyncsDelivers db + +shakeDatabaseSize :: ShakeDatabase -> IO Int +shakeDatabaseSize (ShakeDatabase _ _ db) = databaseSize db + +databaseSize :: Database -> IO Int +databaseSize db = atomically $ SMap.size $ databaseValues db -- | Given a 'ShakeDatabase', write an HTML profile to the given file about the latest run. shakeProfileDatabase :: ShakeDatabase -> FilePath -> IO () -shakeProfileDatabase (ShakeDatabase _ _ s) file = writeProfile file s +shakeProfileDatabase (ShakeDatabase _ _ db) file = writeProfile file db -- | Returns the clean keys in the database shakeGetCleanKeys :: ShakeDatabase -> IO [(Key, Result )] @@ -83,3 +179,7 @@ shakeGetBuildEdges (ShakeDatabase _ _ db) = do -- annotated with how long ago (in # builds) they were visited shakeGetDatabaseKeys :: ShakeDatabase -> IO [(Key, Int)] shakeGetDatabaseKeys (ShakeDatabase _ _ db) = getKeysAndVisitAge db + +shakeGetActionQueueLength :: ShakeDatabase -> IO Int +shakeGetActionQueueLength (ShakeDatabase _ _ db) = do + fromIntegral <$> atomically (countQueue (databaseActionQueue db)) diff --git a/hls-graph/src/Development/IDE/Graph/Internal/Action.hs b/hls-graph/src/Development/IDE/Graph/Internal/Action.hs index 6d47d9b511..4db7899b80 100644 --- a/hls-graph/src/Development/IDE/Graph/Internal/Action.hs +++ b/hls-graph/src/Development/IDE/Graph/Internal/Action.hs @@ -14,14 +14,22 @@ module Development.IDE.Graph.Internal.Action , runActions , Development.IDE.Graph.Internal.Action.getDirtySet , getKeysAndVisitedAge +, isAsyncException +, pumpActionThread +, pumpActionThreadReRun +, sequenceRun +, seqRunActions ) where import Control.Concurrent.Async +import Control.Concurrent.STM.Stats (atomicallyNamed) import Control.DeepSeq (force) import Control.Exception +import Control.Monad (void) import Control.Monad.IO.Class +import Control.Monad.RWS (MonadReader (ask), + asks) import Control.Monad.Trans.Class -import Control.Monad.Trans.Reader import Data.Foldable (toList) import Data.Functor.Identity import Data.IORef @@ -31,66 +39,91 @@ import Development.IDE.Graph.Internal.Key import Development.IDE.Graph.Internal.Rules (RuleResult) import Development.IDE.Graph.Internal.Types import System.Exit +import UnliftIO (atomically) type ShakeValue a = (Show a, Typeable a, Eq a, Hashable a, NFData a) -- | Always rerun this rule when dirty, regardless of the dependencies. alwaysRerun :: Action () alwaysRerun = do - ref <- Action $ asks actionDeps + ref <- asks actionDeps liftIO $ modifyIORef' ref (AlwaysRerunDeps mempty <>) -parallel :: [Action a] -> Action [a] -parallel [] = pure [] -parallel [x] = fmap (:[]) x +-- parallel :: [Action a] -> Action [Either SomeException a] +-- parallel [] = return [] +-- parallel xs = do +-- a <- ask +-- deps <- liftIO $ readIORef $ actionDeps a +-- case deps of +-- UnknownDeps -> +-- -- if we are already in the rerun mode, nothing we do is going to impact our state +-- -- runActionInDb "parallel" xs +-- runActionInDb "parallel" xs +-- deps -> error $ "parallel not supported when we have precise dependencies: " ++ show deps + +parallel :: [Action a] -> Action [Either SomeException a] +parallel [] = return [] parallel xs = do - a <- Action ask + a <- ask deps <- liftIO $ readIORef $ actionDeps a case deps of - UnknownDeps -> + UnknownDeps -> do -- if we are already in the rerun mode, nothing we do is going to impact our state - liftIO $ mapConcurrently (ignoreState a) xs - deps -> do - (newDeps, res) <- liftIO $ unzip <$> mapConcurrently (usingState a) xs - liftIO $ writeIORef (actionDeps a) $ mconcat $ deps : newDeps - pure res - where - usingState a x = do - ref <- newIORef mempty - res <- runReaderT (fromAction x) a{actionDeps=ref} - deps <- readIORef ref - pure (deps, res) + -- runActionInDb "parallel" xs + liftIO $ mapConcurrently (fmap Right . ignoreState a) xs + deps -> error $ "parallel not supported when we have precise dependencies: " ++ show deps + +pumpActionThreadReRun :: ShakeDatabase -> DelayedAction () -> Action () +pumpActionThreadReRun (ShakeDatabase _ _ db) d = do + a <- ask + s <- atomically $ getDataBaseStepInt db + liftIO $ runInThreadStmInNewThreads db + (DeliverStatus s (actionName d) (uniqueID d)) + (ignoreState a $ runOne d) (const $ return ()) + where + runOne d = setActionKey (uniqueID d) $ do + _ <- getAction d + liftIO $ atomically $ doneQueue d (databaseActionQueue db) + +pumpActionThread :: ShakeDatabase -> (String -> IO ()) -> Action b +pumpActionThread sdb@(ShakeDatabase _ _ db) logMsg = do + do + d <- liftIO $ atomicallyNamed "action queue - pop" $ popQueue (databaseActionQueue db) + pumpActionThreadReRun sdb d + pumpActionThread sdb logMsg ignoreState :: SAction -> Action b -> IO b ignoreState a x = do ref <- newIORef mempty - runReaderT (fromAction x) a{actionDeps=ref} + runActionMonad x a{actionDeps=ref} actionFork :: Action a -> (Async a -> Action b) -> Action b actionFork act k = do - a <- Action ask + a <- ask deps <- liftIO $ readIORef $ actionDeps a let db = actionDatabase a case deps of UnknownDeps -> do - -- if we are already in the rerun mode, nothing we do is going to impact our state - [res] <- liftIO $ withAsync (ignoreState a act) $ \as -> runActions db [k as] - return res + [res] <- liftIO $ withAsync (ignoreState a act) $ \as -> + runActions (actionKey a) db [k as] + liftIO $ either throwIO pure res _ -> - error "please help me" + error "actionFork is only supported when dependencies are unknown" isAsyncException :: SomeException -> Bool isAsyncException e + | Just (_ :: SomeAsyncException) <- fromException e = True | Just (_ :: AsyncCancelled) <- fromException e = True | Just (_ :: AsyncException) <- fromException e = True + | Just (_ :: AsyncParentKill) <- fromException e = True | Just (_ :: ExitCode) <- fromException e = True | otherwise = False actionCatch :: Exception e => Action a -> (e -> Action a) -> Action a actionCatch a b = do - v <- Action ask - Action $ lift $ catchJust f (runReaderT (fromAction a) v) (\x -> runReaderT (fromAction (b x)) v) + v <- ask + liftIO $ catchJust f (runActionMonad a v) (\x -> runActionMonad (b x) v) where -- Catch only catches exceptions that were caused by this code, not those that -- are a result of program termination @@ -99,23 +132,24 @@ actionCatch a b = do actionBracket :: IO a -> (a -> IO b) -> (a -> Action c) -> Action c actionBracket a b c = do - v <- Action ask - Action $ lift $ bracket a b (\x -> runReaderT (fromAction (c x)) v) + v <- ask + liftIO $ bracket a b (\x -> runActionMonad (c x) v) actionFinally :: Action a -> IO b -> Action a actionFinally a b = do v <- Action ask - Action $ lift $ finally (runReaderT (fromAction a) v) b + Action $ lift $ finally (runActionMonad a v) b apply1 :: (RuleResult key ~ value, ShakeValue key, Typeable value) => key -> Action value apply1 k = runIdentity <$> apply (Identity k) apply :: (Traversable f, RuleResult key ~ value, ShakeValue key, Typeable value) => f key -> Action (f value) apply ks = do - db <- Action $ asks actionDatabase - stack <- Action $ asks actionStack - (is, vs) <- liftIO $ build db stack ks - ref <- Action $ asks actionDeps + db <- asks actionDatabase + stack <- asks actionStack + pk <- getActionKey + (is, vs) <- liftIO $ build pk db stack ks + ref <- asks actionDeps let !ks = force $ fromListKeySet $ toList is liftIO $ modifyIORef' ref (ResultDeps [ks] <>) pure vs @@ -123,15 +157,27 @@ apply ks = do -- | Evaluate a list of keys without recording any dependencies. applyWithoutDependency :: (Traversable f, RuleResult key ~ value, ShakeValue key, Typeable value) => f key -> Action (f value) applyWithoutDependency ks = do - db <- Action $ asks actionDatabase - stack <- Action $ asks actionStack - (_, vs) <- liftIO $ build db stack ks + db <- asks actionDatabase + stack <- asks actionStack + pk <- getActionKey + (_, vs) <- liftIO $ build pk db stack ks pure vs -runActions :: Database -> [Action a] -> IO [a] -runActions db xs = do +runActions :: Key -> Database -> [Action a] -> IO [Either SomeException a] +runActions pk db xs = do + deps <- newIORef mempty + runActionMonad (parallel xs) $ SAction pk db deps emptyStack + +seqRunActions :: Key -> Database -> [Action a] -> IO () +seqRunActions pk db xs = do deps <- newIORef mempty - runReaderT (fromAction $ parallel xs) $ SAction db deps emptyStack + runActionMonad (sequenceRun xs) $ SAction pk db deps emptyStack + +sequenceRun :: [Action a] -> Action () +sequenceRun [] = return () +sequenceRun (x:xs) = do + void x + sequenceRun xs -- | Returns the set of dirty keys annotated with their age (in # of builds) getDirtySet :: Action [(Key, Int)] diff --git a/hls-graph/src/Development/IDE/Graph/Internal/Database.hs b/hls-graph/src/Development/IDE/Graph/Internal/Database.hs index 359e5ceb6a..13dd914dd6 100644 --- a/hls-graph/src/Development/IDE/Graph/Internal/Database.hs +++ b/hls-graph/src/Development/IDE/Graph/Internal/Database.hs @@ -5,42 +5,43 @@ {-# LANGUAGE CPP #-} {-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE LambdaCase #-} +{-# LANGUAGE RankNTypes #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE TypeFamilies #-} -module Development.IDE.Graph.Internal.Database (compute, newDatabase, incDatabase, build, getDirtySet, getKeysAndVisitAge) where +module Development.IDE.Graph.Internal.Database (compute, newDatabase, incDatabase, build, getDirtySet, getKeysAndVisitAge, AsyncParentKill(..), computeToPreserve, getRunTimeRDeps, spawnAsyncWithDbRegistration) where import Prelude hiding (unzip) -import Control.Concurrent.Async -import Control.Concurrent.Extra -import Control.Concurrent.STM.Stats (STM, atomically, - atomicallyNamed, +import Control.Concurrent.STM.Stats (STM, atomicallyNamed, modifyTVar', newTVarIO, - readTVarIO) + readTVar, readTVarIO, + retry) import Control.Exception import Control.Monad import Control.Monad.IO.Class (MonadIO (liftIO)) -import Control.Monad.Trans.Class (lift) import Control.Monad.Trans.Reader -import qualified Control.Monad.Trans.State.Strict as State import Data.Dynamic -import Data.Either -import Data.Foldable (for_, traverse_) +import Data.Foldable (foldrM) import Data.IORef.Extra import Data.Maybe import Data.Traversable (for) import Data.Tuple.Extra -import Debug.Trace (traceM) import Development.IDE.Graph.Classes import Development.IDE.Graph.Internal.Key import Development.IDE.Graph.Internal.Rules import Development.IDE.Graph.Internal.Types +import Development.IDE.Graph.Internal.Types () import qualified Focus import qualified ListT import qualified StmContainers.Map as SMap -import System.IO.Unsafe -import System.Time.Extra (duration, sleep) +import System.Time.Extra (duration) +import UnliftIO (MVar, atomically, + newEmptyMVar, putMVar, + takeMVar) + +import qualified Data.List as List +import qualified UnliftIO.Exception as UE #if MIN_VERSION_base(4,19,0) import Data.Functor (unzip) @@ -49,95 +50,143 @@ import Data.List.NonEmpty (unzip) #endif -newDatabase :: Dynamic -> TheRules -> IO Database -newDatabase databaseExtra databaseRules = do +newDatabase :: (String -> IO ()) -> ActionQueue -> Dynamic -> TheRules -> IO Database +newDatabase dataBaseLogger databaseActionQueue databaseExtra databaseRules = do databaseStep <- newTVarIO $ Step 0 + databaseThreads <- newTVarIO [] + databaseValuesLock <- newTVarIO True databaseValues <- atomically SMap.new + databaseRRuntimeDep <- atomically SMap.new + databaseRuntimeDepRoot <- atomically SMap.new + databaseRRuntimeDepRoot <- atomically SMap.new + databaseTransitiveRRuntimeDepCache <- atomically SMap.new pure Database{..} -- | Increment the step and mark dirty. -- Assumes that the database is not running a build -incDatabase :: Database -> Maybe [Key] -> IO () -- only some keys are dirty -incDatabase db (Just kk) = do - atomicallyNamed "incDatabase" $ modifyTVar' (databaseStep db) $ \(Step i) -> Step $ i + 1 - transitiveDirtyKeys <- transitiveDirtySet db kk - for_ (toListKeySet transitiveDirtyKeys) $ \k -> - -- Updating all the keys atomically is not necessary - -- since we assume that no build is mutating the db. - -- Therefore run one transaction per key to minimise contention. - atomicallyNamed "incDatabase" $ SMap.focus updateDirty k (databaseValues db) +incDatabase :: Database -> Maybe (([Key], [Key]), KeySet) -> IO KeySet +incDatabase db (Just ((_oldKeys, newKeys), preserves)) = do + atomicallyNamed "incDatabase" $ modifyTVar' (databaseStep db) $ \(Step i) -> Step $ i + 1 + forM_ newKeys $ \newKey -> atomically $ SMap.focus updateDirty newKey (databaseValues db) + -- only upsweep the keys that are not preserved + -- atomically $ writeUpsweepQueue (filter (`notMemberKeySet` preserves) oldkeys ++ newKeys) db + return $ preserves -- all keys are dirty incDatabase db Nothing = do atomically $ modifyTVar' (databaseStep db) $ \(Step i) -> Step $ i + 1 let list = SMap.listT (databaseValues db) + -- all running keys are also dirty atomicallyNamed "incDatabase - all " $ flip ListT.traverse_ list $ \(k,_) -> SMap.focus updateDirty k (databaseValues db) + return $ mempty + +computeToPreserve :: Database -> KeySet -> STM (KeySet, ([Key], [Key]), Int, [Key]) +computeToPreserve db dirtySet = do + (oldKeys, newKeys, affected) <- transitiveDirtyListBottomUpDiff db (toListKeySet dirtySet) [] + pure (affected, (oldKeys, newKeys), length newKeys, []) updateDirty :: Monad m => Focus.Focus KeyDetails m () updateDirty = Focus.adjust $ \(KeyDetails status rdeps) -> let status' - | Running _ _ _ x <- status = Dirty x + | Running _ x <- status = Dirty x | Clean x <- status = Dirty (Just x) | otherwise = status in KeyDetails status' rdeps + + +-- updateClean :: Monad m => Focus.Focus KeyDetails m () +-- updateClean = Focus.adjust $ \(KeyDetails _ rdeps) -> -- | Unwrap and build a list of keys in parallel -build - :: forall f key value . (Traversable f, RuleResult key ~ value, Typeable key, Show key, Hashable key, Eq key, Typeable value) - => Database -> Stack -> f key -> IO (f Key, f value) --- build _ st k | traceShow ("build", st, k) False = undefined -build db stack keys = do - built <- runAIO $ do - built <- builder db stack (fmap newKey keys) - case built of - Left clean -> return clean - Right dirty -> liftIO dirty - let (ids, vs) = unzip built - pure (ids, fmap (asV . resultValue) vs) - where - asV :: Value -> value - asV (Value x) = unwrapDynamic x +build :: + forall f key value. + (Traversable f, RuleResult key ~ value, Typeable key, Show key, Hashable key, Eq key, Typeable value) => + Key -> Database -> Stack -> f key -> IO (f Key, f value) +build pk db stack keys = do + built <- builder pk db stack (fmap newKey keys) + let (ids, vs) = unzip built + pure (ids, fmap (asV . resultValue) vs) + where + asV :: Value -> value + asV (Value x) = unwrapDynamic x + -- | Build a list of keys and return their results. -- If none of the keys are dirty, we can return the results immediately. -- Otherwise, a blocking computation is returned *which must be evaluated asynchronously* to avoid deadlock. -builder - :: Traversable f => Database -> Stack -> f Key -> AIO (Either (f (Key, Result)) (IO (f (Key, Result)))) --- builder _ st kk | traceShow ("builder", st,kk) False = undefined -builder db@Database{..} stack keys = withRunInIO $ \(RunInIO run) -> do - -- Things that I need to force before my results are ready - toForce <- liftIO $ newTVarIO [] - current <- liftIO $ readTVarIO databaseStep - results <- liftIO $ for keys $ \id -> - -- Updating the status of all the dependencies atomically is not necessary. - -- Therefore, run one transaction per dep. to avoid contention - atomicallyNamed "builder" $ do - -- Spawn the id if needed - status <- SMap.lookup id databaseValues - val <- case viewDirty current $ maybe (Dirty Nothing) keyStatus status of - Clean r -> pure r - Running _ force val _ - | memberStack id stack -> throw $ StackException stack - | otherwise -> do - modifyTVar' toForce (Wait force :) - pure val - Dirty s -> do - let act = run (refresh db stack id s) - (force, val) = splitIO (join act) - SMap.focus (updateStatus $ Running current force val s) id databaseValues - modifyTVar' toForce (Spawn force:) - pure val - - pure (id, val) - - toForceList <- liftIO $ readTVarIO toForce - let waitAll = run $ waitConcurrently_ toForceList - case toForceList of - [] -> return $ Left results - _ -> return $ Right $ do - waitAll - pure results +builder :: (Traversable f) => Key -> Database -> Stack -> f Key -> IO (f (Key, Result)) +builder pk db stack keys = do + waits <- for keys (\k -> builderOne pk db stack k) + for waits (interpreBuildContinue db pk) + +-- the first run should not block +data BuildContinue + = BCContinue !(Maybe (MVar (Either SomeException (Key, Result)))) + | BCStop Key Result + +-- interpreBuildContinue :: BuildContinue -> IO (Key, Result) +interpreBuildContinue :: Database -> Key -> (Key, BuildContinue) -> IO (Key, Result) +interpreBuildContinue _db _pk (_kid, BCStop k v) = return (k, v) +interpreBuildContinue db _pk (kid, BCContinue Nothing) = builderOneFinal db emptyStack kid +interpreBuildContinue _db _pk (_kid, BCContinue (Just barrier)) = + takeMVar barrier >>= either throwIO pure + + +builderOne :: Key -> Database -> Stack -> Key -> IO (Key, BuildContinue) +builderOne parentKey db stack kid = do + r <- builderOne' parentKey db stack kid + return (kid, r) + +builderOneFinal :: Database -> Stack -> Key -> IO (Key, Result) +builderOneFinal Database {..} stack key = do + -- join is used to register the async + atomicallyNamed "builder" $ do + status <- SMap.lookup key databaseValues + case (viewToRun $ keyStatus <$> status) of + (Dirty _prev) -> retry + (Clean r) -> return (key, r) + (Running _step _s) + | memberStack key stack -> throw $ StackException stack + | otherwise -> retry + +builderOne' :: Key -> Database -> Stack -> Key -> IO BuildContinue +builderOne' parentKey db@Database {..} stack key = UE.uninterruptibleMask $ \restore -> do + atomicallyNamed "builder" $ insertdatabaseRuntimeDep key parentKey db + barrier <- newEmptyMVar + -- join is used to register the async + join $ restore $ mask_ $ atomicallyNamed "builder" $ do + dbNotLocked db + status <- SMap.lookup key databaseValues + current <- readTVar databaseStep + + case (viewToRun $ keyStatus <$> status) of + (Dirty prev) -> do + SMap.focus (updateStatus $ Running current prev) key databaseValues + let register = spawnRefresh db stack key barrier prev (return ()) refresh + -- why it is important to use rollback here + + {- Note [Rollback is required if killed before registration] + It is important to use rollback here because a key might be killed before it is registered, even though it is not one of the dirty keys. + In this case, it would skip being marked as dirty. Therefore, we have to roll back here if it is killed, to ensure consistency. + -} + (\_ -> atomicallyNamed "builderOne rollback" $ SMap.focus updateDirty key databaseValues) + restore + return $ register >> return (BCContinue (Just barrier)) + (Clean r) -> pure . pure $ BCStop key r + (Running _step _s) + | memberStack key stack -> throw $ StackException stack + | otherwise -> pure . pure $ BCContinue Nothing + +-- Original spawnRefresh implementation moved below to use the abstraction +-- handleResult :: (Show a1, MonadIO m) => a1 -> MVar (Either a2 (a1, b)) -> Either a2 b -> m () +handleResult :: MonadIO m => Key -> MVar (Either SomeException (Key, b)) -> Either SomeException b -> m () +handleResult k barrier eResult = do + case eResult of + Right r -> putMVar barrier (Right (k, r)) + -- accumulate the async kill info for debugging + Left e | Just (AsyncParentKill tid s ks) <- fromException e -> putMVar barrier (Left (toException $ AsyncParentKill tid s (k:ks))) + Left e -> putMVar barrier (Left e) -- | isDirty @@ -145,6 +194,7 @@ builder db@Database{..} stack keys = withRunInIO $ \(RunInIO run) -> do isDirty :: Foldable t => Result -> t (a, Result) -> Bool isDirty me = any (\(_,dep) -> resultBuilt me < resultChanged dep) + -- | Refresh dependencies for a key and compute the key: -- The refresh the deps linearly(last computed order of the deps for the key). -- If any of the deps is dirty in the process, we jump to the actual computation of the key @@ -152,44 +202,35 @@ isDirty me = any (\(_,dep) -> resultBuilt me < resultChanged dep) -- * If no dirty dependencies and we have evaluated the key previously, then we refresh it in the current thread. -- This assumes that the implementation will be a lookup -- * Otherwise, we spawn a new thread to refresh the dirty deps (if any) and the key itself -refreshDeps :: KeySet -> Database -> Stack -> Key -> Result -> [KeySet] -> AIO Result +refreshDeps :: KeySet -> Database -> Stack -> Key -> Result -> [KeySet] -> IO Result refreshDeps visited db stack key result = \case -- no more deps to refresh - [] -> liftIO $ compute db stack key RunDependenciesSame (Just result) + [] -> compute db stack key RunDependenciesSame (Just result) (dep:deps) -> do let newVisited = dep <> visited - res <- builder db stack (toListKeySet (dep `differenceKeySet` visited)) - case res of - Left res -> if isDirty result res + res <- builder key db stack (toListKeySet (dep `differenceKeySet` visited)) + if isDirty result res -- restart the computation if any of the deps are dirty - then liftIO $ compute db stack key RunDependenciesChanged (Just result) + then compute db stack key RunDependenciesChanged (Just result) -- else kick the rest of the deps else refreshDeps newVisited db stack key result deps - Right iores -> do - res <- liftIO iores - if isDirty result res - then liftIO $ compute db stack key RunDependenciesChanged (Just result) - else refreshDeps newVisited db stack key result deps - --- | Refresh a key: -refresh :: Database -> Stack -> Key -> Maybe Result -> AIO (IO Result) --- refresh _ st k _ | traceShow ("refresh", st, k) False = undefined + + +refresh :: Database -> Stack -> Key -> Maybe Result -> IO Result refresh db stack key result = case (addStack key stack, result) of (Left e, _) -> throw e - (Right stack, Just me@Result{resultDeps = ResultDeps deps}) -> asyncWithCleanUp $ refreshDeps mempty db stack key me (reverse deps) - (Right stack, _) -> - asyncWithCleanUp $ liftIO $ compute db stack key RunDependenciesChanged result - + (Right stack, Just me@Result{resultDeps = ResultDeps deps}) -> refreshDeps mempty db stack key me (reverse deps) + (Right stack, _) -> compute db stack key RunDependenciesChanged result -- | Compute a key. compute :: Database -> Stack -> Key -> RunMode -> Maybe Result -> IO Result --- compute _ st k _ _ | traceShow ("compute", st, k) False = undefined compute db@Database{..} stack key mode result = do let act = runRule databaseRules key (fmap resultData result) mode - deps <- newIORef UnknownDeps + deps <- liftIO $ newIORef UnknownDeps + curStep <- liftIO $ readTVarIO databaseStep + -- dataBaseLogger $ "Computing key: " ++ show key ++ " at step " ++ show curStep (execution, RunResult{..}) <- - duration $ runReaderT (fromAction act) $ SAction db deps stack - curStep <- readTVarIO databaseStep - deps <- readIORef deps + liftIO $ duration $ runReaderT (fromAction act) $ SAction key db deps stack + deps <- liftIO $ readIORef deps let lastChanged = maybe curStep resultChanged result let lastBuild = maybe curStep resultBuilt result -- changed time is always older than or equal to build time @@ -203,22 +244,23 @@ compute db@Database{..} stack key mode result = do let -- only update the deps when the rule ran with changes actualDeps = if runChanged /= ChangedNothing then deps else previousDeps previousDeps= maybe UnknownDeps resultDeps result - let res = Result runValue built changed curStep actualDeps execution runStore - case getResultDepsDefault mempty actualDeps of - deps | not (nullKeySet deps) - && runChanged /= ChangedNothing - -> do - -- IMPORTANT: record the reverse deps **before** marking the key Clean. - -- If an async exception strikes before the deps have been recorded, - -- we won't be able to accurately propagate dirtiness for this key - -- on the next build. - void $ + let res = Result { resultValue = runValue, resultBuilt = built, resultChanged = changed, resultVisited = curStep, resultDeps = actualDeps, resultExecution = execution, resultData = runStore } + liftIO $ atomicallyNamed "compute and run hook" $ do + dbNotLocked db + case getResultDepsDefault mempty actualDeps of + deps | not (nullKeySet deps) + && runChanged /= ChangedNothing + -> do + -- IMPORTANT: record the reverse deps **before** marking the key Clean. + -- If an async exception strikes before the deps have been recorded, + -- we won't be able to accurately propagate dirtiness for this key + -- on the next build. updateReverseDeps key db (getResultDepsDefault mempty previousDeps) deps - _ -> pure () - atomicallyNamed "compute and run hook" $ do + _ -> pure () runHook + -- it might be overridden by error if another kills this thread SMap.focus (updateStatus $ Clean res) key databaseValues pure res @@ -237,8 +279,8 @@ getDirtySet db = do calcAgeStatus _ = Nothing return $ mapMaybe (secondM calcAgeStatus) dbContents --- | Returns an approximation of the database keys, --- annotated with how long ago (in # builds) they were visited +-- | Returns an approximation of the database keys, annotated with how long ago +-- they were visited in build steps. getKeysAndVisitAge :: Database -> IO [(Key, Int)] getKeysAndVisitAge db = do values <- getDatabaseValues db @@ -247,18 +289,6 @@ getKeysAndVisitAge db = do getAge Result{resultVisited = Step s} = curr - s return keysWithVisitAge -------------------------------------------------------------------------------- --- Lazy IO trick - -data Box a = Box {fromBox :: a} - --- | Split an IO computation into an unsafe lazy value and a forcing computation -splitIO :: IO a -> (IO (), a) -splitIO act = do - let act2 = Box <$> act - let res = unsafePerformIO act2 - (void $ evaluate res, fromBox res) - --------------------------------------------------------------------------------- -- Reverse dependencies -- | Update the reverse dependencies of an Id @@ -267,7 +297,7 @@ updateReverseDeps -> Database -> KeySet -- ^ Previous direct dependencies of Id -> KeySet -- ^ Current direct dependencies of Id - -> IO () + -> STM () -- mask to ensure that all the reverse dependencies are updated updateReverseDeps myId db prev new = do forM_ (toListKeySet $ prev `differenceKeySet` new) $ \d -> @@ -280,100 +310,100 @@ updateReverseDeps myId db prev new = do -- updating all the reverse deps atomically is not needed. -- Therefore, run individual transactions for each update -- in order to avoid contention - doOne f id = atomicallyNamed "updateReverseDeps" $ - SMap.focus (alterRDeps f) id (databaseValues db) - -getReverseDependencies :: Database -> Key -> STM (Maybe KeySet) -getReverseDependencies db = (fmap.fmap) keyReverseDeps . flip SMap.lookup (databaseValues db) - -transitiveDirtySet :: Foldable t => Database -> t Key -> IO KeySet -transitiveDirtySet database = flip State.execStateT mempty . traverse_ loop - where - loop x = do - seen <- State.get - if x `memberKeySet` seen then pure () else do - State.put (insertKeySet x seen) - next <- lift $ atomically $ getReverseDependencies database x - traverse_ loop (maybe mempty toListKeySet next) - --------------------------------------------------------------------------------- --- Asynchronous computations with cancellation - --- | A simple monad to implement cancellation on top of 'Async', --- generalizing 'withAsync' to monadic scopes. -newtype AIO a = AIO { unAIO :: ReaderT (IORef [Async ()]) IO a } - deriving newtype (Applicative, Functor, Monad, MonadIO) - --- | Run the monadic computation, cancelling all the spawned asyncs if an exception arises -runAIO :: AIO a -> IO a -runAIO (AIO act) = do - asyncs <- newIORef [] - runReaderT act asyncs `onException` cleanupAsync asyncs - --- | Like 'async' but with built-in cancellation. --- Returns an IO action to wait on the result. -asyncWithCleanUp :: AIO a -> AIO (IO a) -asyncWithCleanUp act = do - st <- AIO ask - io <- unliftAIO act - -- mask to make sure we keep track of the spawned async - liftIO $ uninterruptibleMask $ \restore -> do - a <- async $ restore io - atomicModifyIORef'_ st (void a :) - return $ wait a - -unliftAIO :: AIO a -> AIO (IO a) -unliftAIO act = do - st <- AIO ask - return $ runReaderT (unAIO act) st - -newtype RunInIO = RunInIO (forall a. AIO a -> IO a) - -withRunInIO :: (RunInIO -> AIO b) -> AIO b -withRunInIO k = do - st <- AIO ask - k $ RunInIO (\aio -> runReaderT (unAIO aio) st) - -cleanupAsync :: IORef [Async a] -> IO () --- mask to make sure we interrupt all the asyncs -cleanupAsync ref = uninterruptibleMask $ \unmask -> do - asyncs <- atomicModifyIORef' ref ([],) - -- interrupt all the asyncs without waiting - mapM_ (\a -> throwTo (asyncThreadId a) AsyncCancelled) asyncs - -- Wait until all the asyncs are done - -- But if it takes more than 10 seconds, log to stderr - unless (null asyncs) $ do - let warnIfTakingTooLong = unmask $ forever $ do - sleep 10 - traceM "cleanupAsync: waiting for asyncs to finish" - withAsync warnIfTakingTooLong $ \_ -> - mapM_ waitCatch asyncs - -data Wait - = Wait {justWait :: !(IO ())} - | Spawn {justWait :: !(IO ())} - -fmapWait :: (IO () -> IO ()) -> Wait -> Wait -fmapWait f (Wait io) = Wait (f io) -fmapWait f (Spawn io) = Spawn (f io) - -waitOrSpawn :: Wait -> IO (Either (IO ()) (Async ())) -waitOrSpawn (Wait io) = pure $ Left io -waitOrSpawn (Spawn io) = Right <$> async io - -waitConcurrently_ :: [Wait] -> AIO () -waitConcurrently_ [] = pure () -waitConcurrently_ [one] = liftIO $ justWait one -waitConcurrently_ many = do - ref <- AIO ask - -- spawn the async computations. - -- mask to make sure we keep track of all the asyncs. - (asyncs, syncs) <- liftIO $ uninterruptibleMask $ \unmask -> do - waits <- liftIO $ traverse (waitOrSpawn . fmapWait unmask) many - let (syncs, asyncs) = partitionEithers waits - liftIO $ atomicModifyIORef'_ ref (asyncs ++) - return (asyncs, syncs) - -- work on the sync computations - liftIO $ sequence_ syncs - -- wait for the async computations before returning - liftIO $ traverse_ wait asyncs + doOne f id = SMap.focus (alterRDeps f) id (databaseValues db) + +-- compute the transitive reverse dependencies of a set of keys + +-- non-root +-- inline +{-# INLINE getRunTimeRDeps #-} +getRunTimeRDeps :: Database -> Key -> STM (Maybe KeySet) +getRunTimeRDeps db k = SMap.lookup k (databaseRRuntimeDep db) + +{-# INLINE getDeps #-} +getDeps :: SMap.Map Key KeySet -> Key -> STM (Maybe KeySet) +getDeps m k = SMap.lookup k m + +-- Edges in the reverse-dependency graph go from a child to its parents. +-- We perform a DFS and, after exploring all outgoing edges, cons the node onto +-- the accumulator. This yields children-before-parents order directly. + +-- the lefts are keys that are no longer affected, we can try to mark them clean +-- the rights are new affected keys, we need to mark them dirty +transitiveDirtyListBottomUpDiff :: Database -> [Key] -> [Key] -> STM ([Key], [Key], KeySet) +transitiveDirtyListBottomUpDiff database seeds allOldKeys = do + (newKeys, seen) <- cacheTransitiveDirtyListBottomUpDFSWithRootKey database $ fromListKeySet seeds + let oldKeys = filter (`notMemberKeySet` seen) allOldKeys + return (oldKeys, newKeys, seen) + +cacheTransitiveDirtyListBottomUpDFSWithRootKey :: Database -> KeySet -> STM ([Key], KeySet) +cacheTransitiveDirtyListBottomUpDFSWithRootKey db@Database{..} seeds = do + (newKeys, seen) <- cacheTransitiveDirtyListBottomUpDFS db seeds + -- we should put pump root keys back to seen + -- for each new key, get its root keys and put them back to seen + -- newKeys is for upsweep, databaseRRuntimeDepRoot only add new root keys which is not needed for upsweep + -- but seen is for thread filtering, we need to make sure all root keys are in seen + (_newKeys, newSeen) <- transitiveDirtyListBottomUpDFS databaseRRuntimeDepRoot seen + let rootKey = newKey "root" + return $ (List.delete rootKey newKeys, deleteKeySet rootKey newSeen) + + + +cacheTransitiveDirtyListBottomUpDFS :: Database -> KeySet -> STM ([Key], KeySet) +cacheTransitiveDirtyListBottomUpDFS Database{..} seeds = do + SMap.lookup seeds databaseTransitiveRRuntimeDepCache >>= \case + Just v -> return v + Nothing -> do + r <- transitiveDirtyListBottomUpDFS databaseRRuntimeDep seeds + SMap.insert r seeds databaseTransitiveRRuntimeDepCache + return r + +transitiveDirtyListBottomUpDFS :: SMap.Map Key KeySet -> KeySet -> STM ([Key], KeySet) +transitiveDirtyListBottomUpDFS database seeds = do + let go1 :: Key -> ([Key], KeySet) -> STM ([Key], KeySet) + go1 x acc@(dirties, seen) = do + if x `memberKeySet` seen + then pure acc + else do + let newAcc = (dirties, insertKeySet x seen) + mnext <- getDeps database x + (newDirties, newSeen) <- foldrM go1 newAcc (maybe mempty toListKeySet mnext) + return (x:newDirties, newSeen) + -- if it is root key, we do not add it to the dirty list + -- since root key is not up for upsweep + -- but it would be in the seen list, so we would kill dirty root key async + -- traverse all seeds + foldrM go1 ([], mempty) (toListKeySet seeds) + +-- | Original spawnRefresh using the general pattern +-- inline +{-# INLINE spawnRefresh #-} +spawnRefresh :: + Database -> + t -> + Key -> + MVar (Either SomeException (Key, Result)) -> + Maybe Result -> + STM () -> + (Database -> t -> Key -> Maybe Result -> IO Result) -> + (SomeException -> IO ()) -> + (forall a. IO a -> IO a) -> + IO () +spawnRefresh db@Database {..} stack key barrier prevResult registerHook refresher rollBack restore = do + Step currentStep <- readTVarIO databaseStep + spawnAsyncWithDbRegistration + db + (DeliverStatus currentStep ("async computation; " ++ show key) key) + registerHook + (refresher db stack key prevResult) + (\r -> do + case r of + Left e -> rollBack e + Right _ -> return () + handleResult key barrier r + ) restore + +-- Attempt to clear a Dirty parent that ended up with unchanged children during this event. +-- If the parent is Dirty, and every direct child is either Clean/Exception/Running for a step < eventStep, +-- and no child changed at/after eventStep, mark parent Clean (preserving its last Clean result), +-- and recursively attempt the same for its own parents. diff --git a/hls-graph/src/Development/IDE/Graph/Internal/Key.hs b/hls-graph/src/Development/IDE/Graph/Internal/Key.hs index 85cebeb110..a05aed2b40 100644 --- a/hls-graph/src/Development/IDE/Graph/Internal/Key.hs +++ b/hls-graph/src/Development/IDE/Graph/Internal/Key.hs @@ -6,6 +6,7 @@ module Development.IDE.Graph.Internal.Key ( Key -- Opaque - don't expose constructor, use newKey to create , KeyValue (..) , pattern Key + , pattern DirectKey , newKey , renderKey -- * KeyMap @@ -31,6 +32,9 @@ module Development.IDE.Graph.Internal.Key , fromListKeySet , deleteKeySet , differenceKeySet + , unionKeySet + , notMemberKeySet + , newDirectKey ) where --import Control.Monad.IO.Class () @@ -47,31 +51,51 @@ import Data.Text (Text) import qualified Data.Text as T import Data.Typeable import Development.IDE.Graph.Classes +import Prettyprinter import System.IO.Unsafe newtype Key = UnsafeMkKey Int + pattern Key :: () => (Typeable a, Hashable a, Show a) => a -> Key -pattern Key a <- (lookupKeyValue -> KeyValue a _) +pattern Key a <- (lookupKeyValue -> (KeyValue a _)) +pattern DirectKey :: Int -> Key +pattern DirectKey a <- (lookupKeyValue -> (DirectKeyValue a)) {-# COMPLETE Key #-} +{-# COMPLETE Key, DirectKey #-} + +instance Pretty Key where + pretty = pretty . renderKey -data KeyValue = forall a . (Typeable a, Hashable a, Show a) => KeyValue a Text +data KeyValue = forall a . (Typeable a, Hashable a, Show a) => + KeyValue a Text | + DirectKeyValue Int instance Eq KeyValue where - KeyValue a _ == KeyValue b _ = Just a == cast b + KeyValue a _ == KeyValue b _ = Just a == cast b + DirectKeyValue a == DirectKeyValue b = a == b + _ == _ = False instance Hashable KeyValue where - hashWithSalt i (KeyValue x _) = hashWithSalt i (typeOf x, x) + + hashWithSalt i (KeyValue x _) = hashWithSalt i (typeOf x, x) + hashWithSalt i (DirectKeyValue x) = hashWithSalt i (typeOf x, x) instance Show KeyValue where - show (KeyValue _ t) = T.unpack t + show (KeyValue _ t) = T.unpack t + show (DirectKeyValue i) = "DirectKeyValue " ++ show i data GlobalKeyValueMap = GlobalKeyValueMap !(Map.HashMap KeyValue Key) !(IntMap KeyValue) {-# UNPACK #-} !Int keyMap :: IORef GlobalKeyValueMap keyMap = unsafePerformIO $ newIORef (GlobalKeyValueMap Map.empty IM.empty 0) - {-# NOINLINE keyMap #-} +-- | Create a new key that is guaranteed not to collide with any other key. +-- This is useful for keys that are not based on user data, e.g., for +-- tracking temporary actions. +newDirectKey :: Int -> Key +newDirectKey i = UnsafeMkKey (- abs i) + newKey :: (Typeable a, Hashable a, Show a) => a -> Key newKey k = unsafePerformIO $ do let !newKey = KeyValue k (T.pack (show k)) @@ -94,7 +118,9 @@ lookupKeyValue (UnsafeMkKey x) = unsafePerformIO $ do -- i.e. when it is forced for the lookup in the IntMap. k <- evaluate x GlobalKeyValueMap _ im _ <- readIORef keyMap - pure $! im IM.! k + case im IM.!? k of + Just v -> pure $! v + Nothing -> pure $! DirectKeyValue k {-# NOINLINE lookupKeyValue #-} @@ -103,13 +129,18 @@ instance Eq Key where instance Hashable Key where hashWithSalt i (UnsafeMkKey x) = hashWithSalt i x instance Show Key where - show (Key x) = show x + show (Key x) = show x + show (DirectKey x) = "DirectKey " ++ show x renderKey :: Key -> Text -renderKey (lookupKeyValue -> KeyValue _ t) = t +renderKey (lookupKeyValue -> (KeyValue _ t)) = t +renderKey (lookupKeyValue -> (DirectKeyValue i)) = T.pack ("DirectKeyValue " ++ show i) newtype KeySet = KeySet IntSet - deriving newtype (Eq, Ord, Semigroup, Monoid, NFData) + deriving newtype (Eq, Ord, Semigroup, Monoid, NFData, Hashable) + +instance Pretty KeySet where + pretty (KeySet is) = pretty (coerce (IS.toList is) :: [Key]) instance Show KeySet where showsPrec p (KeySet is)= showParen (p > 10) $ @@ -122,6 +153,9 @@ insertKeySet = coerce IS.insert memberKeySet :: Key -> KeySet -> Bool memberKeySet = coerce IS.member +notMemberKeySet :: Key -> KeySet -> Bool +notMemberKeySet = coerce IS.notMember + toListKeySet :: KeySet -> [Key] toListKeySet = coerce IS.toList @@ -131,6 +165,10 @@ nullKeySet = coerce IS.null differenceKeySet :: KeySet -> KeySet -> KeySet differenceKeySet = coerce IS.difference + +unionKeySet :: KeySet -> KeySet -> KeySet +unionKeySet = coerce IS.union + deleteKeySet :: Key -> KeySet -> KeySet deleteKeySet = coerce IS.delete diff --git a/hls-graph/src/Development/IDE/Graph/Internal/Rules.hs b/hls-graph/src/Development/IDE/Graph/Internal/Rules.hs index 9a5f36ca35..c8d951810d 100644 --- a/hls-graph/src/Development/IDE/Graph/Internal/Rules.hs +++ b/hls-graph/src/Development/IDE/Graph/Internal/Rules.hs @@ -42,12 +42,14 @@ addRule f = do v <- f (fromJust $ cast a :: key) b c v <- liftIO $ evaluate v pure $ Value . toDyn <$> v + f2 (DirectKey a) _ _ = error $ "DirectKey " ++ show a ++ " has no associated rule" runRule :: TheRules -> Key -> Maybe BS.ByteString -> RunMode -> Action (RunResult Value) runRule rules key@(Key t) bs mode = case Map.lookup (typeOf t) rules of Nothing -> liftIO $ errorIO $ "Could not find key: " ++ show key Just x -> unwrapDynamic x key bs mode +runRule _ (DirectKey a) _ _ = error $ "DirectKey " ++ show a ++ " has no associated rule" runRules :: Dynamic -> Rules () -> IO (TheRules, [Action ()]) runRules rulesExtra (Rules rules) = do diff --git a/hls-graph/src/Development/IDE/Graph/Internal/Types.hs b/hls-graph/src/Development/IDE/Graph/Internal/Types.hs index 34bed42391..e7b8ede75f 100644 --- a/hls-graph/src/Development/IDE/Graph/Internal/Types.hs +++ b/hls-graph/src/Development/IDE/Graph/Internal/Types.hs @@ -1,34 +1,62 @@ {-# LANGUAGE CPP #-} {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE LambdaCase #-} {-# LANGUAGE RecordWildCards #-} module Development.IDE.Graph.Internal.Types where -import Control.Concurrent.STM (STM) -import Control.Monad ((>=>)) +import Control.Concurrent.STM (STM, TQueue, TVar, check, + flushTQueue, isEmptyTQueue, + modifyTVar', newTQueue, + newTVar, readTQueue, + readTVar, unGetTQueue, + writeTQueue) +import Control.Exception (throw) +import Control.Monad (forM, forM_, forever, + unless, when) import Control.Monad.Catch import Control.Monad.IO.Class -import Control.Monad.Trans.Reader +import Control.Monad.RWS (MonadReader (local), asks) +import Control.Monad.Trans.Reader (ReaderT (..)) import Data.Aeson (FromJSON, ToJSON) import Data.Bifunctor (second) import qualified Data.ByteString as BS import Data.Dynamic import Data.Foldable (fold) import qualified Data.HashMap.Strict as Map +import Data.HashSet (HashSet) +import qualified Data.HashSet as Set import Data.IORef -import Data.List (intercalate) -import Data.Maybe +import Data.List (intercalate, partition) +import Data.Maybe (fromMaybe, isJust, + isNothing) import Data.Typeable +import Debug.Trace (traceEventIO) import Development.IDE.Graph.Classes import Development.IDE.Graph.Internal.Key -import GHC.Conc (TVar, atomically) +import qualified Focus +import GHC.Conc () import GHC.Generics (Generic) import qualified ListT +import Numeric.Natural +import Prettyprinter import qualified StmContainers.Map as SMap import StmContainers.Map (Map) -import System.Time.Extra (Seconds) -import UnliftIO (MonadUnliftIO) +import System.Time.Extra (Seconds, sleep) +import UnliftIO (Async (asyncThreadId), + MonadUnliftIO, + asyncExceptionFromException, + asyncExceptionToException, + asyncWithUnmask, + atomically, cancelWith, + newEmptyTMVarIO, poll, + putTMVar, readTMVar, + readTVarIO, throwTo, + waitCatch, withAsync) +import UnliftIO.Concurrent (ThreadId, myThreadId) +import qualified UnliftIO.Exception as UE + #if !MIN_VERSION_base(4,18,0) import Control.Applicative (liftA2) @@ -68,35 +96,168 @@ data SRules = SRules { -- 'Development.IDE.Graph.Internal.Action.actionCatch'. In particular, it is -- permissible to use the 'MonadFail' instance, which will lead to an 'IOException'. newtype Action a = Action {fromAction :: ReaderT SAction IO a} - deriving newtype (Monad, Applicative, Functor, MonadIO, MonadFail, MonadThrow, MonadCatch, MonadMask, MonadUnliftIO) + deriving newtype (Monad, Applicative, Functor, MonadIO, MonadFail, MonadThrow, MonadCatch, MonadMask, MonadUnliftIO, MonadReader SAction) + +runActionMonad :: Action a -> SAction -> IO a +runActionMonad (Action r) s = runReaderT r s data SAction = SAction { + actionKey :: !Key, actionDatabase :: !Database, actionDeps :: !(IORef ResultDeps), actionStack :: !Stack } getDatabase :: Action Database -getDatabase = Action $ asks actionDatabase +getDatabase = asks actionDatabase + +getActionKey :: Action Key +getActionKey = asks actionKey + +setActionKey :: Key -> Action a -> Action a +setActionKey k act = local (\s' -> s'{actionKey = k}) act --- | waitForDatabaseRunningKeysAction waits for all keys in the database to finish running. -waitForDatabaseRunningKeysAction :: Action () -waitForDatabaseRunningKeysAction = getDatabase >>= liftIO . waitForDatabaseRunningKeys --------------------------------------------------------------------- -- DATABASE +-- | A simple priority used for annotating delayed actions. +-- Ordering is important: Debug < Info < Warning < Error +data Priority + = Debug + | Info + | Warning + | Error + deriving (Eq, Show, Read, Ord, Enum, Bounded) + +type DelayedActionInternal = DelayedAction () +-- | A delayed action that carries an Action payload. +data DelayedAction a = DelayedAction + { uniqueID :: Key + , actionName :: String -- ^ Name we use for debugging + , actionPriority :: Priority -- ^ Priority with which to log the action + , getAction :: Action a -- ^ The payload + } + deriving (Functor) + +actionNameKey :: DelayedAction a -> String +actionNameKey d = actionName d ++ " (" ++ show (uniqueID d) ++ ")" +instance Eq (DelayedAction a) where + a == b = uniqueID a == uniqueID b + +instance Hashable (DelayedAction a) where + hashWithSalt s = hashWithSalt s . uniqueID + +instance Show (DelayedAction a) where + show d = "DelayedAction: " ++ actionName d + +------------------------------------------------------------------------------- + +-- | A queue of delayed actions for the graph 'Action' monad. +data ActionQueue = ActionQueue + { newActions :: TQueue (DelayedAction ()) + , inProgress :: TVar (HashSet (DelayedAction ())) + } + +newQueue :: IO ActionQueue +newQueue = atomically $ do + newActions <- newTQueue + inProgress <- newTVar mempty + return ActionQueue {..} + +pushQueue :: DelayedAction () -> ActionQueue -> STM () +pushQueue act ActionQueue {..} = writeTQueue newActions act + +-- | Append to the front of the queue +unGetQueue :: DelayedAction () -> ActionQueue -> STM () +unGetQueue act ActionQueue {..} = unGetTQueue newActions act + +-- | You must call 'doneQueue' to signal completion +popQueue :: ActionQueue -> STM (DelayedAction ()) +popQueue ActionQueue {..} = do + x <- readTQueue newActions + modifyTVar' inProgress (Set.insert x) + return x + +popAllQueue :: ActionQueue -> STM [DelayedAction ()] +popAllQueue ActionQueue {..} = do + xs <- flushTQueue newActions + modifyTVar' inProgress (\s -> s `Set.union` Set.fromList xs) + return xs + +insertRunnning :: DelayedAction () -> ActionQueue -> STM () +insertRunnning act ActionQueue {..} = modifyTVar' inProgress (Set.insert act) + +-- | Completely remove an action from the queue +abortQueue :: DelayedAction () -> ActionQueue -> STM () +abortQueue x ActionQueue {..} = do + qq <- flushTQueue newActions + mapM_ (writeTQueue newActions) (filter (/= x) qq) + modifyTVar' inProgress (Set.delete x) + +-- | Mark an action as complete when called after 'popQueue'. +-- Has no effect otherwise +doneQueue :: DelayedAction () -> ActionQueue -> STM () +doneQueue x ActionQueue {..} = do + modifyTVar' inProgress (Set.delete x) + +countQueue :: ActionQueue -> STM Natural +countQueue ActionQueue{..} = do + backlog <- flushTQueue newActions + mapM_ (writeTQueue newActions) backlog + m <- Set.size <$> readTVar inProgress + return $ fromIntegral $ length backlog + m + +peekInProgress :: ActionQueue -> STM [DelayedAction ()] +peekInProgress ActionQueue {..} = Set.toList <$> readTVar inProgress + +isActionQueueEmpty :: ActionQueue -> STM Bool +isActionQueueEmpty ActionQueue {..} = do + emptyQueue <- isEmptyTQueue newActions + inProg <- Set.null <$> readTVar inProgress + return (emptyQueue && inProg) + data ShakeDatabase = ShakeDatabase !Int [Action ()] Database newtype Step = Step Int - deriving newtype (Eq,Ord,Hashable,Show) + deriving newtype (Eq,Ord,Hashable,Show,Num,Enum,Real,Integral) ---------------------------------------------------------------------- --- Keys +data DeliverStatus = DeliverStatus + { deliverStep :: Int + , deliverName :: String + , deliverKey :: Key + } deriving (Show) +instance Pretty DeliverStatus where + pretty (DeliverStatus step name key) = + pretty ("Step:" :: String) <+> pretty step <> comma + <+> pretty ("name:" :: String) <+> pretty name <> comma + <+> pretty ("key:" :: String) <+> pretty (show key) +getShakeStep :: MonadIO m => ShakeDatabase -> m Step +getShakeStep (ShakeDatabase _ _ db) = do + s <- readTVarIO $ databaseStep db + return s +lockShakeDatabaseValues :: MonadIO m => ShakeDatabase -> m () +lockShakeDatabaseValues (ShakeDatabase _ _ db) = do + liftIO $ atomically $ modifyTVar' (databaseValuesLock db) (const False) +unlockShakeDatabaseValues :: MonadIO m => ShakeDatabase -> m () +unlockShakeDatabaseValues (ShakeDatabase _ _ db) = do + liftIO $ atomically $ modifyTVar' (databaseValuesLock db) (const True) + +withShakeDatabaseValuesLock :: ShakeDatabase -> IO c -> IO c +withShakeDatabaseValuesLock sdb act = do + UE.bracket_ (lockShakeDatabaseValues sdb) (unlockShakeDatabaseValues sdb) act + +dbNotLocked :: Database -> STM () +dbNotLocked db = do + check =<< readTVar (databaseValuesLock db) + + +--------------------------------------------------------------------- +-- Keys newtype Value = Value Dynamic data KeyDetails = KeyDetails { @@ -109,14 +270,176 @@ onKeyReverseDeps f it@KeyDetails{..} = it{keyReverseDeps = f keyReverseDeps} data Database = Database { - databaseExtra :: Dynamic, - databaseRules :: TheRules, - databaseStep :: !(TVar Step), - databaseValues :: !(Map Key KeyDetails) + databaseExtra :: Dynamic, + + databaseThreads :: TVar [(DeliverStatus, Async ())], + + databaseRuntimeDepRoot :: SMap.Map Key KeySet, + + databaseRRuntimeDepRoot :: SMap.Map Key KeySet, + databaseRRuntimeDep :: SMap.Map Key KeySet, + -- it is used to compute the transitive reverse deps, so + -- if not in any of the transitive reverse deps of a dirty node, it is clean + -- we can skip clean the threads. + -- this is update right before we query the database for the key result. + databaseTransitiveRRuntimeDepCache :: SMap.Map KeySet ([Key], KeySet), + -- ^ this is a cache for transitive reverse deps if we have computed it before + -- and the databaseRRuntimeDep did not change since last time + -- it is very useful for large projects where many files depend on a few common files + -- e.g. we do not want to recompute the transitive reverse deps every time we enter a letter + -- to a file. + + + dataBaseLogger :: String -> IO (), + + -- The action queue and + databaseActionQueue :: ActionQueue, + + + databaseRules :: TheRules, + databaseStep :: !(TVar Step), + + databaseValuesLock :: !(TVar Bool), + -- when we restart a build, we set this to False to block any other + -- threads from reading databaseValues + databaseValues :: !(Map Key KeyDetails) + } -waitForDatabaseRunningKeys :: Database -> IO () -waitForDatabaseRunningKeys = getDatabaseValues >=> mapM_ (waitRunning . snd) + +--------------------------------------------------------------------- +-- | Remove finished asyncs from 'databaseThreads' (non-blocking). +-- Uses 'poll' to check completion without waiting. +pruneFinished :: Database -> IO () +pruneFinished db@Database{..} = do + threads <- readTVarIO databaseThreads + statuses <- forM threads $ \(d,a) -> do + p <- poll a + return (d,a,p) + let still = [ (d,a) | (d,a,p) <- statuses, isNothing p ] + -- deleteDatabaseRuntimeDep of finished async keys + forM_ statuses $ \(d,_,p) -> when (isJust p) $ do + let k = deliverKey d + when (k /= newKey "root") $ atomically $ deleteDatabaseRuntimeDep k db + atomically $ modifyTVar' databaseThreads (const still) + +deleteDatabaseRuntimeDep :: Key -> Database -> STM () +deleteDatabaseRuntimeDep k db = do + result <- SMap.lookup k (databaseRuntimeDepRoot db) + case result of + Nothing -> return () + Just deps -> do + SMap.delete k (databaseRuntimeDepRoot db) + -- also remove k from all its reverse deps + forM_ (toListKeySet deps) $ \d -> do + SMap.focus (Focus.alter (fmap (deleteKeySet k))) d (databaseRRuntimeDepRoot db) + + +-- record runtime reverse deps for each key, +-- if it is root key, also reverse deps so when the root key is done, we can clean up the reverse deps. +insertdatabaseRuntimeDep :: Key -> Key -> Database -> STM () +insertdatabaseRuntimeDep k pk db = do + if isRootKey pk || isRootKey k + then do + SMap.focus (Focus.alter (Just . maybe (singletonKeySet k) (insertKeySet k))) pk (databaseRuntimeDepRoot db) + SMap.focus (Focus.alter (Just . maybe (singletonKeySet pk) (insertKeySet pk))) k (databaseRRuntimeDepRoot db) + else do + -- databaseRRuntimeDep only incremental, so no need to keep a reverse one + -- Also I want to know if the database changed + -- if changed we need to reset databaseTransitiveRRuntimeDepCache + SMap.lookup k (databaseRRuntimeDep db) >>= \case + Nothing -> do + SMap.insert (singletonKeySet pk) k (databaseRRuntimeDep db) + SMap.reset (databaseTransitiveRRuntimeDepCache db) + Just s -> when (pk `notMemberKeySet` s) $ do + SMap.insert (insertKeySet pk s) k (databaseRRuntimeDep db) + SMap.reset (databaseTransitiveRRuntimeDepCache db) + +-- inline +{-# INLINE isRootKey #-} +isRootKey :: Key -> Bool +isRootKey (DirectKey _a) = True +isRootKey _ = False + +--------------------------------------------------------------------- + +-- | Abstract pattern for spawning async computations with database registration. +-- This pattern is used by spawnRefresh and can be used by other functions that need: +-- 1. Protected async creation with uninterruptibleMask +-- 2. Database thread tracking and state updates +-- 3. Controlled start coordination via barriers +-- 4. Exception safety with rollback on registration failure +-- @ inline +{-# INLINE spawnAsyncWithDbRegistration #-} +spawnAsyncWithDbRegistration :: Database -> DeliverStatus -> STM () -> IO a1 -> (Either SomeException a1 -> IO ()) -> (forall a. IO a -> IO a) -> IO () +spawnAsyncWithDbRegistration db@Database{..} deliver registerHook asyncBody handler restore = do + startBarrier <- newEmptyTMVarIO + -- 1. we need to make sure the thread is registered before we actually start + -- 2. we should not start in between the restart + -- 3. if it is killed before we start, we need to cancel the async + let register a = do + dbNotLocked db + registerHook + modifyTVar' databaseThreads ((deliver, a):) + -- make sure we only start after the restart + putTMVar startBarrier () + a <- asyncWithUnmask $ \restore -> (handler =<< ((restore $ atomically (readTMVar startBarrier) >> (Right <$> asyncBody)) `catch` \e@(SomeException _) -> return (Left e))) + (restore $ atomically $ register a) + `catch` \e@(SomeException _) -> do + cancelWith a e + throw e + +-- inline +{-# INLINE runInThreadStmInNewThreads #-} +runInThreadStmInNewThreads :: Database -> DeliverStatus -> IO a -> (Either SomeException a -> IO ()) -> IO () +runInThreadStmInNewThreads db deliver act handler = uninterruptibleMask $ \restore -> + spawnAsyncWithDbRegistration db deliver (return ()) act handler restore + +getDataBaseStepInt :: Database -> STM Int +getDataBaseStepInt db = do + Step s <- readTVar $ databaseStep db + return s + +data AsyncParentKill = AsyncParentKill ThreadId Step [Key] + deriving (Show, Eq) + +instance Exception AsyncParentKill where + toException = asyncExceptionToException + fromException = asyncExceptionFromException + +shutDatabase ::KeySet -> Database -> IO () +shutDatabase dirties db@Database{..} = uninterruptibleMask $ \unmask -> do + -- wait for all threads to finish + asyncs <- readTVarIO databaseThreads + step <- readTVarIO databaseStep + tid <- myThreadId + let rootKey = newKey "root" + let (toCancel, remains) = partition (\(k, _) -> deliverKey k `memberKeySet` dirties || deliverKey k == rootKey) asyncs + atomically $ modifyTVar' databaseThreads (const remains) + mapM_ (\(k, a) -> throwTo (asyncThreadId a) $ AsyncParentKill tid step [deliverKey k, newKey "shutDatabase"]) toCancel + -- Wait until all the asyncs are done + -- But if it takes more than 10 seconds, log to stderr + unless (null asyncs) $ do + let warnIfTakingTooLong = unmask $ forever $ do + sleep 5 + as <- readTVarIO databaseThreads + -- poll each async: Nothing => still running + statuses <- forM as $ \(d,a) -> do + p <- poll a + return (d, a, p) + let still = [ (deliverName d, show (asyncThreadId a)) | (d,a,p) <- statuses, isNothing p ] + traceEventIO $ "cleanupAsync: waiting for asyncs to finish; total=" ++ show (length as) ++ ", stillRunning=" ++ show (length still) + traceEventIO $ "cleanupAsync: still running (deliverName, threadId) = " ++ show still + withAsync warnIfTakingTooLong $ \_ -> mapM_ (waitCatch . snd) toCancel + forM_ toCancel $ \(d,_p) -> do + let k = deliverKey d + when (k /= newKey "root") $ atomically $ deleteDatabaseRuntimeDep k db + pruneFinished db + +peekAsyncsDelivers :: MonadIO m => Database -> m [DeliverStatus] +peekAsyncsDelivers db = do + asyncs <- readTVarIO (databaseThreads db) + return $ fst <$> asyncs getDatabaseValues :: Database -> IO [(Key, Status)] getDatabaseValues = atomically @@ -127,26 +450,38 @@ getDatabaseValues = atomically data Status = Clean !Result + -- dirty should say why it is dirty, + -- it should and only should be clean, + -- once all the event has been processed, + -- once event is represeted by a step | Dirty (Maybe Result) | Running { - runningStep :: !Step, - runningWait :: !(IO ()), - runningResult :: Result, -- LAZY - runningPrev :: !(Maybe Result) + runningStep :: !Step, + -- runningResult :: Result, -- LAZY + runningPrev :: !(Maybe Result) + -- runningWait :: !(MVar (Either SomeException (Key, Result))) } +instance Show Status where + show (Clean _) = "Clean" + show (Dirty _) = "Dirty" + show (Running s _ ) = "Running step " ++ show s viewDirty :: Step -> Status -> Status -viewDirty currentStep (Running s _ _ re) | currentStep /= s = Dirty re +-- viewDirty currentStep (Running s re _ _) | currentStep /= s = Dirty re viewDirty _ other = other + +viewToRun :: Maybe Status -> Status +-- viewToRun _currentStep (Dirty _) = Nothing +-- viewToRun currentStep (Running s _re _ _) | currentStep /= s = Nothing +viewToRun Nothing = (Dirty Nothing) +viewToRun (Just other) = other + getResult :: Status -> Maybe Result -getResult (Clean re) = Just re -getResult (Dirty m_re) = m_re -getResult (Running _ _ _ m_re) = m_re -- watch out: this returns the previous result +getResult (Clean re) = Just re +getResult (Dirty m_re) = m_re +getResult (Running _ m_re ) = m_re -- watch out: this returns the previous result -waitRunning :: Status -> IO () -waitRunning Running{..} = runningWait -waitRunning _ = return () data Result = Result { resultValue :: !Value, @@ -194,6 +529,12 @@ data RunMode | RunDependenciesChanged -- ^ At least one of my dependencies from last time have changed, or I have no recorded dependencies. deriving (Eq,Show) +instance Monoid RunMode where + mempty = RunDependenciesSame +instance Semigroup RunMode where + RunDependenciesSame <> b = b + RunDependenciesChanged <> _ = RunDependenciesChanged + instance NFData RunMode where rnf x = x `seq` () -- | How the output of a rule has changed. diff --git a/hls-graph/test/ActionSpec.hs b/hls-graph/test/ActionSpec.hs index 97ab5555ac..079527aa70 100644 --- a/hls-graph/test/ActionSpec.hs +++ b/hls-graph/test/ActionSpec.hs @@ -7,7 +7,10 @@ import Control.Concurrent (MVar, readMVar) import qualified Control.Concurrent as C import Control.Concurrent.STM import Control.Monad.IO.Class (MonadIO (..)) -import Development.IDE.Graph (shakeOptions) +import Data.Typeable (Typeable) +import Development.IDE.Graph (RuleResult, + shakeOptions) +import Development.IDE.Graph.Classes (Hashable) import Development.IDE.Graph.Database (shakeNewDatabase, shakeRunDatabase, shakeRunDatabaseForKeys) @@ -21,9 +24,17 @@ import Test.Hspec +buildWithRoot :: forall f key value . (Traversable f, RuleResult key ~ value, Typeable key, Show key, Hashable key, Typeable value) => Database -> Stack -> f key -> IO (f Key, f value) +buildWithRoot = build (newKey ("root" :: [Char])) + +itInThread :: String -> IO () -> SpecWith () +itInThread = it + +shakeRunDatabaseFromRight :: ShakeDatabase -> [Action a] -> IO [a] +shakeRunDatabaseFromRight = shakeRunDatabase spec :: Spec spec = do - describe "apply1" $ it "Test build update, Buggy dirty mechanism in hls-graph #4237" $ do + describe "apply1" $ itInThread "Test build update, Buggy dirty mechanism in hls-graph #4237" $ do let ruleStep1 :: MVar Int -> Rules () ruleStep1 m = addRule $ \CountRule _old mode -> do -- depends on ruleSubBranch, it always changed if dirty @@ -43,7 +54,7 @@ spec = do ruleSubBranch count ruleStep1 count1 -- bootstrapping the database - _ <- shakeRunDatabase db $ pure $ apply1 CountRule -- count = 1 + _ <- shakeRunDatabaseFromRight db $ pure $ apply1 CountRule -- count = 1 let child = newKey SubBranchRule let parent = newKey CountRule -- instruct to RunDependenciesChanged then CountRule should be recomputed @@ -58,43 +69,43 @@ spec = do _res3 <- shakeRunDatabaseForKeys (Just [parent]) db [apply1 CountRule] -- count = 2 c1 <- readMVar count1 c1 `shouldBe` 2 - describe "apply1" $ do - it "computes a rule with no dependencies" $ do + describe "apply1" $ do + itInThread "computes a rule with no dependencies" $ do db <- shakeNewDatabase shakeOptions ruleUnit - res <- shakeRunDatabase db $ + res <- shakeRunDatabaseFromRight db $ pure $ apply1 (Rule @()) res `shouldBe` [()] - it "computes a rule with one dependency" $ do + itInThread "computes a rule with one dependency" $ do db <- shakeNewDatabase shakeOptions $ do ruleUnit ruleBool - res <- shakeRunDatabase db $ pure $ apply1 Rule + res <- shakeRunDatabaseFromRight db $ pure $ apply1 Rule res `shouldBe` [True] - it "tracks direct dependencies" $ do + itInThread "tracks direct dependencies" $ do db@(ShakeDatabase _ _ theDb) <- shakeNewDatabase shakeOptions $ do ruleUnit ruleBool let theKey = Rule @Bool - res <- shakeRunDatabase db $ + res <- shakeRunDatabaseFromRight db $ pure $ apply1 theKey res `shouldBe` [True] Just (Clean res) <- lookup (newKey theKey) <$> getDatabaseValues theDb resultDeps res `shouldBe` ResultDeps [singletonKeySet $ newKey (Rule @())] - it "tracks reverse dependencies" $ do + itInThread "tracks reverse dependencies" $ do db@(ShakeDatabase _ _ Database {..}) <- shakeNewDatabase shakeOptions $ do ruleUnit ruleBool let theKey = Rule @Bool - res <- shakeRunDatabase db $ + res <- shakeRunDatabaseFromRight db $ pure $ apply1 theKey res `shouldBe` [True] Just KeyDetails {..} <- atomically $ STM.lookup (newKey (Rule @())) databaseValues keyReverseDeps `shouldBe` singletonKeySet (newKey theKey) - it "rethrows exceptions" $ do + itInThread "rethrows exceptions" $ do db <- shakeNewDatabase shakeOptions $ addRule $ \(Rule :: Rule ()) _old _mode -> error "boom" - let res = shakeRunDatabase db $ pure $ apply1 (Rule @()) + let res = shakeRunDatabaseFromRight db $ pure $ apply1 (Rule @()) res `shouldThrow` anyErrorCall - it "computes a rule with branching dependencies does not invoke phantom dependencies #3423" $ do + itInThread "computes a rule with branching dependencies does not invoke phantom dependencies #3423" $ do cond <- C.newMVar True count <- C.newMVar 0 (ShakeDatabase _ _ theDb) <- shakeNewDatabase shakeOptions $ do @@ -105,18 +116,18 @@ spec = do -- build the one with the condition True -- This should call the SubBranchRule once -- cond rule would return different results each time - res0 <- build theDb emptyStack [BranchedRule] + res0 <- buildWithRoot theDb emptyStack [BranchedRule] snd res0 `shouldBe` [1 :: Int] - incDatabase theDb Nothing + _ <- incDatabase theDb Nothing -- build the one with the condition False -- This should not call the SubBranchRule - res1 <- build theDb emptyStack [BranchedRule] + res1 <- buildWithRoot theDb emptyStack [BranchedRule] snd res1 `shouldBe` [2 :: Int] - -- SubBranchRule should be recomputed once before this (when the condition was True) - countRes <- build theDb emptyStack [SubBranchRule] + -- SubBranchRule should be recomputed once before this (when the condition was True) + countRes <- buildWithRoot theDb emptyStack [SubBranchRule] snd countRes `shouldBe` [1 :: Int] - describe "applyWithoutDependency" $ it "does not track dependencies" $ do + describe "applyWithoutDependency" $ itInThread "does not track dependencies" $ do db@(ShakeDatabase _ _ theDb) <- shakeNewDatabase shakeOptions $ do ruleUnit addRule $ \Rule _old _mode -> do @@ -124,7 +135,7 @@ spec = do return $ RunResult ChangedRecomputeDiff "" True $ return () let theKey = Rule @Bool - res <- shakeRunDatabase db $ + res <- shakeRunDatabaseFromRight db $ pure $ applyWithoutDependency [theKey] res `shouldBe` [[True]] Just (Clean res) <- lookup (newKey theKey) <$> getDatabaseValues theDb diff --git a/hls-graph/test/DatabaseSpec.hs b/hls-graph/test/DatabaseSpec.hs index 9061bfa89d..643356c429 100644 --- a/hls-graph/test/DatabaseSpec.hs +++ b/hls-graph/test/DatabaseSpec.hs @@ -2,6 +2,7 @@ module DatabaseSpec where +import ActionSpec (itInThread) import Development.IDE.Graph (newKey, shakeOptions) import Development.IDE.Graph.Database (shakeNewDatabase, shakeRunDatabase) @@ -13,11 +14,10 @@ import Example import System.Time.Extra (timeout) import Test.Hspec - spec :: Spec spec = do describe "Evaluation" $ do - it "detects cycles" $ do + itInThread "detects cycles" $ do db <- shakeNewDatabase shakeOptions $ do ruleBool addRule $ \Rule _old _mode -> do @@ -27,17 +27,16 @@ spec = do timeout 1 res `shouldThrow` \StackException{} -> True describe "compute" $ do - it "build step and changed step updated correctly" $ do + itInThread "build step and changed step updated correctly" $ do (ShakeDatabase _ _ theDb) <- shakeNewDatabase shakeOptions $ do ruleStep - let k = newKey $ Rule @() -- ChangedRecomputeSame r1@Result{resultChanged=rc1, resultBuilt=rb1} <- compute theDb emptyStack k RunDependenciesChanged Nothing - incDatabase theDb Nothing + _ <- incDatabase theDb Nothing -- ChangedRecomputeSame r2@Result{resultChanged=rc2, resultBuilt=rb2} <- compute theDb emptyStack k RunDependenciesChanged (Just r1) - incDatabase theDb Nothing + _ <- incDatabase theDb Nothing -- changed Nothing Result{resultChanged=rc3, resultBuilt=rb3} <- compute theDb emptyStack k RunDependenciesSame (Just r2) rc1 `shouldBe` Step 0 From b894af1a8cf112bf33ad3b3b962e5bef7e8ae0f2 Mon Sep 17 00:00:00 2001 From: soulomoon Date: Wed, 6 May 2026 20:20:00 +0800 Subject: [PATCH 02/32] Isolate hls-graph runtime engine improvements --- hlint.eventlog | Bin 0 -> 236936 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 hlint.eventlog diff --git a/hlint.eventlog b/hlint.eventlog new file mode 100644 index 0000000000000000000000000000000000000000..ff7d6c323cd1510e4e30297ea48ce0905ce5226c GIT binary patch literal 236936 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

a5rA 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 Date: Wed, 6 May 2026 21:01:50 +0800 Subject: [PATCH 03/32] ghcide: integrate hls-graph runtime engine --- .../session-loader/Development/IDE/Session.hs | 3 +- ghcide/src/Development/IDE/Core/FileStore.hs | 5 +- .../src/Development/IDE/Core/PluginUtils.hs | 10 +- ghcide/src/Development/IDE/Core/Service.hs | 5 +- ghcide/src/Development/IDE/Core/Shake.hs | 155 +++++++++++------- ghcide/src/Development/IDE/Types/Action.hs | 111 +++---------- 6 files changed, 138 insertions(+), 151 deletions(-) diff --git a/ghcide/session-loader/Development/IDE/Session.hs b/ghcide/session-loader/Development/IDE/Session.hs index 7e1a062a7a..1b10a68631 100644 --- a/ghcide/session-loader/Development/IDE/Session.hs +++ b/ghcide/session-loader/Development/IDE/Session.hs @@ -905,7 +905,7 @@ session recorder sessionShake sessionState knownTargetsVar(hieYaml, cfp, opts, l -- Typecheck all files in the project on startup unless (null new_components_info || not checkProject) $ do cfps' <- liftIO $ filterM (IO.doesFileExist . fromNormalizedFilePath) (concatMap targetLocations all_targets) - void $ enqueueActions sessionShake $ mkDelayedAction "InitialLoad" Debug $ void $ do + initialLoad <- mkDelayedAction "InitialLoad" Debug $ void $ do mmt <- uses GetModificationTime cfps' let cs_exist = catMaybes (zipWith (<$) cfps' mmt) modIfaces <- uses GetModIface cs_exist @@ -913,6 +913,7 @@ session recorder sessionShake sessionState knownTargetsVar(hieYaml, cfp, opts, l shakeExtras <- getShakeExtras let !exportsMap' = createExportsMap $ mapMaybe (fmap hirModIface) modIfaces liftIO $ atomically $ modifyTVar' (exportsMap shakeExtras) (exportsMap' <>) + void $ enqueueActions sessionShake initialLoad return [keys1, keys2] -- | Create a new HscEnv from a hieYaml root and a set of options diff --git a/ghcide/src/Development/IDE/Core/FileStore.hs b/ghcide/src/Development/IDE/Core/FileStore.hs index 7d253131d6..8fb87d48f5 100644 --- a/ghcide/src/Development/IDE/Core/FileStore.hs +++ b/ghcide/src/Development/IDE/Core/FileStore.hs @@ -286,8 +286,9 @@ setFileModified recorder vfs state saved nfp actionBefore = do typecheckParents recorder state nfp typecheckParents :: Recorder (WithPriority Log) -> IdeState -> NormalizedFilePath -> IO () -typecheckParents recorder state nfp = void $ shakeEnqueue (shakeExtras state) parents - where parents = mkDelayedAction "ParentTC" L.Debug (typecheckParentsAction recorder nfp) +typecheckParents recorder state nfp = do + parents <- mkDelayedAction "ParentTC" L.Debug (typecheckParentsAction recorder nfp) + void $ shakeEnqueue (shakeExtras state) parents typecheckParentsAction :: Recorder (WithPriority Log) -> NormalizedFilePath -> Action () typecheckParentsAction recorder nfp = do diff --git a/ghcide/src/Development/IDE/Core/PluginUtils.hs b/ghcide/src/Development/IDE/Core/PluginUtils.hs index 2b5caf8ff0..398bfbf3bd 100644 --- a/ghcide/src/Development/IDE/Core/PluginUtils.hs +++ b/ghcide/src/Development/IDE/Core/PluginUtils.hs @@ -71,14 +71,16 @@ import qualified StmContainers.Map as STM -- |ExceptT version of `runAction`, takes a ExceptT Action runActionE :: MonadIO m => String -> IdeState -> ExceptT e Action a -> ExceptT e m a runActionE herald ide act = - mapExceptT liftIO . ExceptT $ - join $ shakeEnqueue (shakeExtras ide) (mkDelayedAction herald Logger.Debug $ runExceptT act) + mapExceptT liftIO . ExceptT $ do + delayed <- mkDelayedAction herald Logger.Debug $ runExceptT act + join $ shakeEnqueue (shakeExtras ide) delayed -- |MaybeT version of `runAction`, takes a MaybeT Action runActionMT :: MonadIO m => String -> IdeState -> MaybeT Action a -> MaybeT m a runActionMT herald ide act = - mapMaybeT liftIO . MaybeT $ - join $ shakeEnqueue (shakeExtras ide) (mkDelayedAction herald Logger.Debug $ runMaybeT act) + mapMaybeT liftIO . MaybeT $ do + delayed <- mkDelayedAction herald Logger.Debug $ runMaybeT act + join $ shakeEnqueue (shakeExtras ide) delayed -- |ExceptT version of `use` that throws a PluginRuleFailed upon failure useE :: IdeRule k v => k -> NormalizedFilePath -> ExceptT PluginError Action v diff --git a/ghcide/src/Development/IDE/Core/Service.hs b/ghcide/src/Development/IDE/Core/Service.hs index 3d98833ab2..aefb8ffdde 100644 --- a/ghcide/src/Development/IDE/Core/Service.hs +++ b/ghcide/src/Development/IDE/Core/Service.hs @@ -108,5 +108,6 @@ shutdown = shakeShut -- available. There might still be other rules running at this point, -- e.g., the ofInterestRule. runAction :: String -> IdeState -> Action a -> IO a -runAction herald ide act = - join $ shakeEnqueue (shakeExtras ide) (mkDelayedAction herald Logger.Debug act) +runAction herald ide act = do + delayed <- mkDelayedAction herald Logger.Debug act + join $ shakeEnqueue (shakeExtras ide) delayed diff --git a/ghcide/src/Development/IDE/Core/Shake.hs b/ghcide/src/Development/IDE/Core/Shake.hs index 92bac9321c..6efa162c61 100644 --- a/ghcide/src/Development/IDE/Core/Shake.hs +++ b/ghcide/src/Development/IDE/Core/Shake.hs @@ -108,8 +108,7 @@ import Data.Hashable import qualified Data.HashMap.Strict as HMap import Data.HashSet (HashSet) import qualified Data.HashSet as HSet -import Data.List.Extra (foldl', partition, - takeEnd) +import Data.List.Extra (partition, takeEnd) import qualified Data.Map.Strict as Map import Data.Maybe import qualified Data.SortedList as SL @@ -119,7 +118,6 @@ import Data.Time import Data.Traversable import Data.Tuple.Extra import Data.Typeable -import Data.Unique import Data.Vector (Vector) import qualified Data.Vector as Vector import Development.IDE.Core.Debouncer @@ -148,11 +146,19 @@ import Development.IDE.Graph hiding (ShakeValue, action) import qualified Development.IDE.Graph as Shake import Development.IDE.Graph.Database (ShakeDatabase, + instantiateDelayedAction, + shakeComputeToPreserve, + shakeGetActionQueueLength, shakeGetBuildStep, shakeGetDatabaseKeys, - shakeNewDatabase, + shakeNewDatabaseWithRuntime, + shakePeekAsyncsDelivers, shakeProfileDatabase, - shakeRunDatabaseForKeys) + shakeRunDatabaseForKeysSep, + shakeShutDatabase) +import qualified Development.IDE.Graph.Database as GraphDatabase +import Development.IDE.Graph.Internal.Action (pumpActionThread) +import qualified Development.IDE.Graph.Internal.Types as GraphRuntime import Development.IDE.Graph.Rule import Development.IDE.Types.Action import Development.IDE.Types.Diagnostics @@ -190,7 +196,7 @@ import UnliftIO (MonadUnliftIO (withRunI data Log = LogCreateHieDbExportsMapStart | LogCreateHieDbExportsMapFinish !Int - | LogBuildSessionRestart !String ![DelayedActionInternal] !KeySet !Seconds !(Maybe FilePath) + | LogBuildSessionRestart !String ![DelayedActionInternal] !KeySet !Seconds !(Maybe FilePath) !RuntimeRestartStats | LogBuildSessionRestartTakingTooLong !Seconds | LogDelayedAction !(DelayedAction ()) !Seconds | LogBuildSessionFinish !(Maybe SomeException) @@ -205,17 +211,32 @@ data Log | LogSetFilesOfInterest ![(NormalizedFilePath, FileOfInterestStatus)] deriving Show +data RuntimeRestartStats = RuntimeRestartStats + { runtimeDirtyCount :: !Int + , runtimeAffectedCount :: !Int + , runtimePreservedCount :: !Int + , runtimeActionQueueCount :: !Int + , runtimeSurvivingActions :: ![String] + } + deriving Show + instance Pretty Log where pretty = \case LogCreateHieDbExportsMapStart -> "Initializing exports map from hiedb" LogCreateHieDbExportsMapFinish exportsMapSize -> "Done initializing exports map from hiedb. Size:" <+> pretty exportsMapSize - LogBuildSessionRestart reason actionQueue keyBackLog abortDuration shakeProfilePath -> + LogBuildSessionRestart reason actionQueue keyBackLog abortDuration shakeProfilePath RuntimeRestartStats{..} -> vcat [ "Restarting build session due to" <+> pretty reason , "Action Queue:" <+> pretty (map actionName actionQueue) , "Keys:" <+> pretty (map show $ toListKeySet keyBackLog) + , "Runtime:" + <+> "dirty=" <> pretty runtimeDirtyCount + <> ", affected=" <> pretty runtimeAffectedCount + <> ", preserved=" <> pretty runtimePreservedCount + <> ", queued=" <> pretty runtimeActionQueueCount + <> ", surviving=" <> pretty runtimeSurvivingActions , "Aborting previous build session took" <+> pretty (showDuration abortDuration) <+> pretty shakeProfilePath ] LogBuildSessionRestartTakingTooLong seconds -> "Build restart is taking too long (" <> pretty seconds <> " seconds)" @@ -723,7 +744,9 @@ shakeOpen recorder lspEnv defaultConfig idePlugins debouncer vfsVar <- newTVarIO =<< vfsSnapshot lspEnv pure ShakeExtras{shakeRecorder = recorder, ..} shakeDb <- - shakeNewDatabase + shakeNewDatabaseWithRuntime + (const $ pure ()) + (actionQueue shakeExtras) opts { shakeExtra = newShakeExtra shakeExtras } rules shakeSession <- newEmptyMVar @@ -766,7 +789,7 @@ shakeSessionInit recorder IdeState{..} = do -- Take a snapshot of the VFS - it should be empty as we've received no notifications -- till now, but it can't hurt to be in sync with the `lsp` library. vfs <- vfsSnapshot (lspEnv shakeExtras) - initSession <- newSession recorder shakeExtras (VFSModified vfs) shakeDb [] "shakeSessionInit" + initSession <- newSession recorder shakeExtras (VFSModified vfs) shakeDb [] "shakeSessionInit" Nothing putMVar shakeSession initSession logWith recorder Debug LogSessionInitialised @@ -776,6 +799,8 @@ shakeShut IdeState{..} = do -- Shake gets unhappy if you try to close when there is a running -- request so we first abort that. for_ runner cancelShakeSession + running <- shakePeekAsyncsDelivers shakeDb + shakeShutDatabase (fromListKeySet $ map GraphRuntime.deliverKey running) shakeDb void $ shakeDatabaseProfile shakeDb progressStop $ progress shakeExtras progressStop $ indexProgressReporting $ hiedbWriter shakeExtras @@ -793,8 +818,12 @@ withMVar' var unmasked masked = uninterruptibleMask $ \restore -> do pure c -mkDelayedAction :: String -> Logger.Priority -> Action a -> DelayedAction a -mkDelayedAction = DelayedAction Nothing +toGraphPriority :: Logger.Priority -> GraphRuntime.Priority +toGraphPriority = toEnum . fromEnum + +mkDelayedAction :: String -> Logger.Priority -> Action a -> IO (DelayedAction a) +mkDelayedAction name priority = + GraphDatabase.mkDelayedAction name (toGraphPriority priority) -- | These actions are run asynchronously after the current action is -- finished running. For example, to trigger a key build after a rule @@ -810,12 +839,15 @@ delayedAction a = do -- but actions added via 'shakeEnqueue' will be requeued. shakeRestart :: Recorder (WithPriority Log) -> IdeState -> VFSModified -> String -> [DelayedAction ()] -> IO [Key] -> IO () shakeRestart recorder IdeState{..} vfs reason acts ioActionBetweenShakeSession = - void $ awaitRunInThread (restartQueue shakeExtras) $ do + void $ awaitRunInThread (restartQueue shakeExtras) $ GraphRuntime.withShakeDatabaseValuesLock shakeDb $ do withMVar' shakeSession (\runner -> do - (stopTime,()) <- duration $ logErrorAfter 10 $ cancelShakeSession runner + logErrorAfter 10 $ cancelShakeSession runner keys <- ioActionBetweenShakeSession + IdeOptions{optRunSubset} <- getIdeOptionsIO shakeExtras + (stopTime, (runtimeKeysChanged, runtimeStats)) <- + duration $ prepareRuntimeRestart optRunSubset keys -- 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 @@ -824,19 +856,56 @@ shakeRestart recorder IdeState{..} vfs reason acts ioActionBetweenShakeSession = queue <- atomicallyNamed "actionQueue - peek" $ peekInProgress $ actionQueue shakeExtras -- this log is required by tests - logWith recorder Debug $ LogBuildSessionRestart reason queue backlog stopTime res + logWith recorder Debug $ LogBuildSessionRestart reason queue backlog stopTime res runtimeStats + return runtimeKeysChanged ) -- It is crucial to be masked here, otherwise we can get killed -- between spawning the new thread and updating shakeSession. -- See https://github.com/haskell/ghcide/issues/79 - (\() -> do - (,()) <$> newSession recorder shakeExtras vfs shakeDb acts reason) + (\runtimeKeysChanged -> do + (,()) <$> newSession recorder shakeExtras vfs shakeDb acts reason runtimeKeysChanged) where logErrorAfter :: Seconds -> IO () -> IO () logErrorAfter seconds action = flip withAsync (const action) $ do sleep seconds logWith recorder Error (LogBuildSessionRestartTakingTooLong seconds) + prepareRuntimeRestart :: Bool -> [Key] -> IO (RuntimeKeysChanged, RuntimeRestartStats) + prepareRuntimeRestart optRunSubset keys + | optRunSubset = do + (affected, changedKeys, _lookupCount, _) <- + shakeComputeToPreserve shakeDb $ fromListKeySet keys + logErrorAfter 10 $ shakeShutDatabase affected shakeDb + surviving <- shakePeekAsyncsDelivers shakeDb + queueCount <- shakeGetActionQueueLength shakeDb + let preserved = fromListKeySet $ map GraphRuntime.deliverKey surviving + pure + ( Just (changedKeys, preserved) + , RuntimeRestartStats + { runtimeDirtyCount = length keys + , runtimeAffectedCount = lengthKeySet affected + , runtimePreservedCount = lengthKeySet preserved + , runtimeActionQueueCount = queueCount + , runtimeSurvivingActions = map GraphRuntime.deliverName surviving + } + ) + | otherwise = do + running <- shakePeekAsyncsDelivers shakeDb + let affected = fromListKeySet $ map GraphRuntime.deliverKey running + logErrorAfter 10 $ shakeShutDatabase affected shakeDb + surviving <- shakePeekAsyncsDelivers shakeDb + queueCount <- shakeGetActionQueueLength shakeDb + pure + ( Nothing + , RuntimeRestartStats + { runtimeDirtyCount = length keys + , runtimeAffectedCount = lengthKeySet affected + , runtimePreservedCount = length surviving + , runtimeActionQueueCount = queueCount + , runtimeSurvivingActions = map GraphRuntime.deliverName surviving + } + ) + -- | Enqueue an action in the existing 'ShakeSession'. -- Returns a computation to block until the action is run, propagating exceptions. -- Assumes a 'ShakeSession' is available. @@ -861,6 +930,8 @@ shakeEnqueue ShakeExtras{actionQueue, shakeRecorder} act = do data VFSModified = VFSUnmodified | VFSModified !VFS +type RuntimeKeysChanged = Maybe (([Key], [Key]), KeySet) + -- | Set up a new 'ShakeSession' with a set of initial actions -- Will crash if there is an existing 'ShakeSession' running. newSession @@ -870,44 +941,30 @@ newSession -> ShakeDatabase -> [DelayedActionInternal] -> String + -> RuntimeKeysChanged -> IO ShakeSession -newSession recorder extras@ShakeExtras{..} vfsMod shakeDb acts reason = do +newSession recorder ShakeExtras{..} vfsMod shakeDb acts reason runtimeKeysChanged = do -- Take a new VFS snapshot case vfsMod of VFSUnmodified -> pure () VFSModified vfs -> atomically $ writeTVar vfsVar vfs - IdeOptions{optRunSubset} <- getIdeOptionsIO extras + unless (null acts) $ + atomicallyNamed "actionQueue - push initial" $ + mapM_ (`pushQueue` actionQueue) acts reenqueued <- atomicallyNamed "actionQueue - peek" $ peekInProgress actionQueue - allPendingKeys <- - if optRunSubset - then Just <$> readTVarIO dirtyKeys - else return Nothing + startDatabase <- shakeRunDatabaseForKeysSep runtimeKeysChanged shakeDb [pumpActionThread shakeDb (const $ pure ())] let - -- A daemon-like action used to inject additional work - -- Runs actions from the work queue sequentially - pumpActionThread otSpan = do - d <- liftIO $ atomicallyNamed "action queue - pop" $ popQueue actionQueue - actionFork (run otSpan d) $ \_ -> pumpActionThread otSpan - - -- TODO figure out how to thread the otSpan into defineEarlyCutoff - run _otSpan d = do - start <- liftIO offsetTime - getAction d - liftIO $ atomicallyNamed "actionQueue - done" $ doneQueue d actionQueue - runTime <- liftIO start - logWith recorder (actionPriority d) $ LogDelayedAction d runTime - -- The inferred type signature doesn't work in ghc >= 9.0.1 workRun :: (forall b. IO b -> IO b) -> IO (IO ()) workRun restore = withSpan "Shake session" $ \otSpan -> do setTag otSpan "reason" (fromString reason) setTag otSpan "queue" (fromString $ unlines $ map actionName reenqueued) - whenJust allPendingKeys $ \kk -> setTag otSpan "keys" (BS8.pack $ unlines $ map show $ toListKeySet kk) - let keysActs = pumpActionThread otSpan : map (run otSpan) (reenqueued ++ acts) + whenJust runtimeKeysChanged $ \((_, newKeys), _) -> + setTag otSpan "keys" (BS8.pack $ unlines $ map show newKeys) res <- try @SomeException $ - restore $ shakeRunDatabaseForKeys (toListKeySet <$> allPendingKeys) shakeDb keysActs + restore startDatabase return $ do let exception = case res of @@ -931,24 +988,6 @@ newSession recorder extras@ShakeExtras{..} vfsMod shakeDb acts reason = do pure (ShakeSession{..}) -instantiateDelayedAction - :: DelayedAction a - -> IO (Barrier (Either SomeException a), DelayedActionInternal) -instantiateDelayedAction (DelayedAction _ s p a) = do - u <- newUnique - b <- newBarrier - let a' = do - -- work gets reenqueued when the Shake session is restarted - -- it can happen that a work item finished just as it was reenqueued - -- in that case, skipping the work is fine - alreadyDone <- liftIO $ isJust <$> waitBarrierMaybe b - unless alreadyDone $ do - x <- actionCatch @SomeException (Right <$> a) (pure . Left) - -- ignore exceptions if the barrier has been filled concurrently - liftIO $ void $ try @SomeException $ signalBarrier b x - d' = DelayedAction (Just u) s p a' - return (b, d') - getDiagnostics :: IdeState -> STM [FileDiagnostic] getDiagnostics IdeState{shakeExtras = ShakeExtras{diagnostics}} = do getAllDiagnostics diagnostics @@ -1106,7 +1145,7 @@ useWithStaleFast' key file = do -- Async trigger the key to be built anyway because we want to -- keep updating the value in the key. - waitValue <- delayedAction $ mkDelayedAction ("C:" ++ show key ++ ":" ++ fromNormalizedFilePath file) Debug $ use key file + waitValue <- delayedAction =<< liftIO (mkDelayedAction ("C:" ++ show key ++ ":" ++ fromNormalizedFilePath file) Debug $ use key file) s@ShakeExtras{state} <- askShake r <- liftIO $ atomicallyNamed "useStateFast" $ getValues state key file diff --git a/ghcide/src/Development/IDE/Types/Action.hs b/ghcide/src/Development/IDE/Types/Action.hs index 0aedd1d0da..906a0be9e7 100644 --- a/ghcide/src/Development/IDE/Types/Action.hs +++ b/ghcide/src/Development/IDE/Types/Action.hs @@ -1,88 +1,31 @@ module Development.IDE.Types.Action - ( DelayedAction (..), - DelayedActionInternal, - ActionQueue, - newQueue, - pushQueue, - popQueue, - doneQueue, - peekInProgress, - abortQueue,countQueue) + ( Action + , DelayedAction (..) + , DelayedActionInternal + , ActionQueue + , newQueue + , pushQueue + , popQueue + , doneQueue + , peekInProgress + , abortQueue + , countQueue + , isActionQueueEmpty + , unGetQueue + , countInProgress + ) where import Control.Concurrent.STM -import Data.Hashable (Hashable (..)) -import Data.HashSet (HashSet) -import qualified Data.HashSet as Set -import Data.Unique (Unique) -import Development.IDE.Graph (Action) -import Ide.Logger -import Numeric.Natural - -data DelayedAction a = DelayedAction - { uniqueID :: Maybe Unique, - -- | Name we use for debugging - actionName :: String, - -- | Priority with which to log the action - actionPriority :: Priority, - -- | The payload - getAction :: Action a - } - deriving (Functor) - -type DelayedActionInternal = DelayedAction () - -instance Eq (DelayedAction a) where - a == b = uniqueID a == uniqueID b - -instance Hashable (DelayedAction a) where - hashWithSalt s = hashWithSalt s . uniqueID - -instance Show (DelayedAction a) where - show d = "DelayedAction: " ++ actionName d - ------------------------------------------------------------------------------- - -data ActionQueue = ActionQueue - { newActions :: TQueue DelayedActionInternal, - inProgress :: TVar (HashSet DelayedActionInternal) - } - -newQueue :: IO ActionQueue -newQueue = atomically $ do - newActions <- newTQueue - inProgress <- newTVar mempty - return ActionQueue {..} - -pushQueue :: DelayedActionInternal -> ActionQueue -> STM () -pushQueue act ActionQueue {..} = writeTQueue newActions act - --- | You must call 'doneQueue' to signal completion -popQueue :: ActionQueue -> STM DelayedActionInternal -popQueue ActionQueue {..} = do - x <- readTQueue newActions - modifyTVar inProgress (Set.insert x) - return x - --- | Completely remove an action from the queue -abortQueue :: DelayedActionInternal -> ActionQueue -> STM () -abortQueue x ActionQueue {..} = do - qq <- flushTQueue newActions - mapM_ (writeTQueue newActions) (filter (/= x) qq) - modifyTVar' inProgress (Set.delete x) - --- | Mark an action as complete when called after 'popQueue'. --- Has no effect otherwise -doneQueue :: DelayedActionInternal -> ActionQueue -> STM () -doneQueue x ActionQueue {..} = do - modifyTVar' inProgress (Set.delete x) - -countQueue :: ActionQueue -> STM Natural -countQueue ActionQueue{..} = do - backlog <- flushTQueue newActions - mapM_ (writeTQueue newActions) backlog - m <- Set.size <$> readTVar inProgress - return $ fromIntegral $ length backlog + m - -peekInProgress :: ActionQueue -> STM [DelayedActionInternal] -peekInProgress ActionQueue {..} = Set.toList <$> readTVar inProgress +import Development.IDE.Graph.Internal.Types (Action, ActionQueue, + DelayedAction (..), + DelayedActionInternal, + abortQueue, countQueue, + doneQueue, + isActionQueueEmpty, + newQueue, peekInProgress, + popQueue, pushQueue, + unGetQueue) + +countInProgress :: ActionQueue -> STM Int +countInProgress queue = length <$> peekInProgress queue From f72ca375c8dece2253bf652cffee5409579f9ecd Mon Sep 17 00:00:00 2001 From: soulomoon Date: Wed, 6 May 2026 21:02:27 +0800 Subject: [PATCH 04/32] chore: remove accidental eventlog --- hlint.eventlog | Bin 236936 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 hlint.eventlog diff --git a/hlint.eventlog b/hlint.eventlog deleted file mode 100644 index ff7d6c323cd1510e4e30297ea48ce0905ce5226c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 236936 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

a5rA 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 Date: Thu, 7 May 2026 00:47:28 +0800 Subject: [PATCH 05/32] Confirm reactor shutdown before cleanup --- ghcide/src/Development/IDE/LSP/LanguageServer.hs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/ghcide/src/Development/IDE/LSP/LanguageServer.hs b/ghcide/src/Development/IDE/LSP/LanguageServer.hs index 7ccc4ac369..7fd7d0ae35 100644 --- a/ghcide/src/Development/IDE/LSP/LanguageServer.hs +++ b/ghcide/src/Development/IDE/LSP/LanguageServer.hs @@ -300,9 +300,7 @@ handleInit lifecycleCtx env (TRequestMessage _ _ m params) = otTracedHandler "In lifetimeConfirm "due to exception in reactor thread" logWith recorder Error $ LogReactorThreadException e ctxForceShutdown lifecycleCtx - _ -> do - lifetimeConfirm "due to shutdown message" - return () + _ -> return () exceptionInHandler e = do logWith recorder Error $ LogReactorMessageActionException e @@ -344,7 +342,10 @@ handleInit lifecycleCtx env (TRequestMessage _ _ m params) = otTracedHandler "In case msg of ReactorNotification act -> handle exceptionInHandler act ReactorRequest _id act k -> void $ async $ checkCancelled _id act k - logWith recorder Info LogReactorThreadStopped + -- Confirm as soon as the reactor loop observes the stop signal. Worker + -- and Shake cleanup continue while the surrounding ContT unwinds. + lifetimeConfirm "due to shutdown message" + logWith recorder Info LogReactorThreadStopped ide <- readMVar ideMVar registerIdeConfiguration (shakeExtras ide) initConfig From 71e697edadace52f421f66493b8b5bc87d8b641b Mon Sep 17 00:00:00 2001 From: soulomoon Date: Thu, 7 May 2026 00:58:55 +0800 Subject: [PATCH 06/32] Run LSP shutdown after worker teardown --- .../src/Development/IDE/LSP/LanguageServer.hs | 23 ++++++------------- ghcide/src/Development/IDE/Main.hs | 4 ++-- 2 files changed, 9 insertions(+), 18 deletions(-) diff --git a/ghcide/src/Development/IDE/LSP/LanguageServer.hs b/ghcide/src/Development/IDE/LSP/LanguageServer.hs index 7fd7d0ae35..fd3d212e59 100644 --- a/ghcide/src/Development/IDE/LSP/LanguageServer.hs +++ b/ghcide/src/Development/IDE/LSP/LanguageServer.hs @@ -295,12 +295,14 @@ handleInit lifecycleCtx env (TRequestMessage _ _ m params) = otTracedHandler "In let loggedTeardown me = do -- shutdown shake + tryReadMVar ideMVar >>= traverse_ shutdown case me of Left e -> do lifetimeConfirm "due to exception in reactor thread" logWith recorder Error $ LogReactorThreadException e ctxForceShutdown lifecycleCtx - _ -> return () + _ -> + lifetimeConfirm "due to shutdown message" exceptionInHandler e = do logWith recorder Error $ LogReactorMessageActionException e @@ -326,11 +328,7 @@ handleInit lifecycleCtx env (TRequestMessage _ _ m params) = otTracedHandler "In exceptionInHandler e k $ TResponseError (InR ErrorCodes_InternalError) (T.pack $ show e) Nothing _ <- flip forkFinally loggedTeardown $ do - -- Need to be careful about when the shutdown occurs, it needs to be shut - -- down after the session loader and restarting threads, and before the - -- hiedb connections are closed. - let shutdownSession = tryReadMVar ideMVar >>= traverse_ shutdown - runWithWorkerThreads (cmapWithPrio LogSession recorder) dbLoc shutdownSession $ \withHieDb' threadQueue' -> do + runWithWorkerThreads (cmapWithPrio LogSession recorder) dbLoc $ \withHieDb' threadQueue' -> do ide <- ctxGetIdeState lifecycleCtx env root withHieDb' threadQueue' putMVar ideMVar ide -- Keep this after putMVar ideMVar ide; otherwise shutdown during @@ -342,10 +340,7 @@ handleInit lifecycleCtx env (TRequestMessage _ _ m params) = otTracedHandler "In case msg of ReactorNotification act -> handle exceptionInHandler act ReactorRequest _id act k -> void $ async $ checkCancelled _id act k - -- Confirm as soon as the reactor loop observes the stop signal. Worker - -- and Shake cleanup continue while the surrounding ContT unwinds. - lifetimeConfirm "due to shutdown message" - logWith recorder Info LogReactorThreadStopped + logWith recorder Info LogReactorThreadStopped ide <- readMVar ideMVar registerIdeConfiguration (shakeExtras ide) initConfig @@ -355,13 +350,9 @@ handleInit lifecycleCtx env (TRequestMessage _ _ m params) = otTracedHandler "In -- | runWithWorkerThreads -- create several threads to run the session, db and session loader -- see Note [Serializing runs in separate thread] -runWithWorkerThreads :: Recorder (WithPriority Session.Log) -> FilePath -> IO () -> (WithHieDb -> ThreadQueue -> IO ()) -> IO () -runWithWorkerThreads recorder dbLoc shutdownSession f = evalContT $ do +runWithWorkerThreads :: Recorder (WithPriority Session.Log) -> FilePath -> (WithHieDb -> ThreadQueue -> IO ()) -> IO () +runWithWorkerThreads recorder dbLoc f = evalContT $ do (WithHieDbShield hiedb, threadQueue) <- runWithDb recorder dbLoc - -- The shake session needs to be shut down prior to the hiedb connections - -- being cleaned up, otherwise shake could be referencing dead connections. - -- This is passed in via the callsites. - ContT $ \action -> action () `finally` shutdownSession sessionRestartTQueue <- withWorkerQueueSimple (cmapWithPrio Session.LogSessionWorkerThread recorder) "RestartTQueue" sessionLoaderTQueue <- withWorkerQueueSimple (cmapWithPrio Session.LogSessionWorkerThread recorder) "SessionLoaderTQueue" liftIO $ f hiedb (ThreadQueue threadQueue sessionRestartTQueue sessionLoaderTQueue) diff --git a/ghcide/src/Development/IDE/Main.hs b/ghcide/src/Development/IDE/Main.hs index feb0050a79..58cffe27e7 100644 --- a/ghcide/src/Development/IDE/Main.hs +++ b/ghcide/src/Development/IDE/Main.hs @@ -378,7 +378,7 @@ defaultMain recorder Arguments{..} = withHeapStats (cmapWithPrio LogHeapStats re Check argFiles -> do let dir = argsProjectRoot dbLoc <- getHieDbLoc dir - runWithWorkerThreads (cmapWithPrio LogSession recorder) dbLoc mempty $ \hiedb threadQueue -> do + runWithWorkerThreads (cmapWithPrio LogSession recorder) dbLoc $ \hiedb threadQueue -> do -- GHC produces messages with UTF8 in them, so make sure the terminal doesn't error hSetEncoding stdout utf8 hSetEncoding stderr utf8 @@ -436,7 +436,7 @@ defaultMain recorder Arguments{..} = withHeapStats (cmapWithPrio LogHeapStats re Custom (IdeCommand c) -> do let root = argsProjectRoot dbLoc <- getHieDbLoc root - runWithWorkerThreads (cmapWithPrio LogSession recorder) dbLoc mempty $ \hiedb threadQueue -> do + runWithWorkerThreads (cmapWithPrio LogSession recorder) dbLoc $ \hiedb threadQueue -> do sessionLoader <- loadSessionWithOptions (cmapWithPrio LogSession recorder) argsSessionLoadingOptions "." (tLoaderQueue threadQueue) let def_options = argsIdeOptions argsDefaultHlsConfig sessionLoader ideOptions = def_options From 49f930476054be937fda6d520f91e4f8d520ee14 Mon Sep 17 00:00:00 2001 From: soulomoon Date: Thu, 7 May 2026 01:44:52 +0800 Subject: [PATCH 07/32] Fix shutdown warning helper masking --- .../src/Development/IDE/LSP/LanguageServer.hs | 18 +++++++++++++----- ghcide/src/Development/IDE/Main.hs | 4 ++-- .../Development/IDE/Graph/Internal/Types.hs | 9 +++++---- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/ghcide/src/Development/IDE/LSP/LanguageServer.hs b/ghcide/src/Development/IDE/LSP/LanguageServer.hs index fd3d212e59..7ccc4ac369 100644 --- a/ghcide/src/Development/IDE/LSP/LanguageServer.hs +++ b/ghcide/src/Development/IDE/LSP/LanguageServer.hs @@ -295,14 +295,14 @@ handleInit lifecycleCtx env (TRequestMessage _ _ m params) = otTracedHandler "In let loggedTeardown me = do -- shutdown shake - tryReadMVar ideMVar >>= traverse_ shutdown case me of Left e -> do lifetimeConfirm "due to exception in reactor thread" logWith recorder Error $ LogReactorThreadException e ctxForceShutdown lifecycleCtx - _ -> + _ -> do lifetimeConfirm "due to shutdown message" + return () exceptionInHandler e = do logWith recorder Error $ LogReactorMessageActionException e @@ -328,7 +328,11 @@ handleInit lifecycleCtx env (TRequestMessage _ _ m params) = otTracedHandler "In exceptionInHandler e k $ TResponseError (InR ErrorCodes_InternalError) (T.pack $ show e) Nothing _ <- flip forkFinally loggedTeardown $ do - runWithWorkerThreads (cmapWithPrio LogSession recorder) dbLoc $ \withHieDb' threadQueue' -> do + -- Need to be careful about when the shutdown occurs, it needs to be shut + -- down after the session loader and restarting threads, and before the + -- hiedb connections are closed. + let shutdownSession = tryReadMVar ideMVar >>= traverse_ shutdown + runWithWorkerThreads (cmapWithPrio LogSession recorder) dbLoc shutdownSession $ \withHieDb' threadQueue' -> do ide <- ctxGetIdeState lifecycleCtx env root withHieDb' threadQueue' putMVar ideMVar ide -- Keep this after putMVar ideMVar ide; otherwise shutdown during @@ -350,9 +354,13 @@ handleInit lifecycleCtx env (TRequestMessage _ _ m params) = otTracedHandler "In -- | runWithWorkerThreads -- create several threads to run the session, db and session loader -- see Note [Serializing runs in separate thread] -runWithWorkerThreads :: Recorder (WithPriority Session.Log) -> FilePath -> (WithHieDb -> ThreadQueue -> IO ()) -> IO () -runWithWorkerThreads recorder dbLoc f = evalContT $ do +runWithWorkerThreads :: Recorder (WithPriority Session.Log) -> FilePath -> IO () -> (WithHieDb -> ThreadQueue -> IO ()) -> IO () +runWithWorkerThreads recorder dbLoc shutdownSession f = evalContT $ do (WithHieDbShield hiedb, threadQueue) <- runWithDb recorder dbLoc + -- The shake session needs to be shut down prior to the hiedb connections + -- being cleaned up, otherwise shake could be referencing dead connections. + -- This is passed in via the callsites. + ContT $ \action -> action () `finally` shutdownSession sessionRestartTQueue <- withWorkerQueueSimple (cmapWithPrio Session.LogSessionWorkerThread recorder) "RestartTQueue" sessionLoaderTQueue <- withWorkerQueueSimple (cmapWithPrio Session.LogSessionWorkerThread recorder) "SessionLoaderTQueue" liftIO $ f hiedb (ThreadQueue threadQueue sessionRestartTQueue sessionLoaderTQueue) diff --git a/ghcide/src/Development/IDE/Main.hs b/ghcide/src/Development/IDE/Main.hs index 58cffe27e7..feb0050a79 100644 --- a/ghcide/src/Development/IDE/Main.hs +++ b/ghcide/src/Development/IDE/Main.hs @@ -378,7 +378,7 @@ defaultMain recorder Arguments{..} = withHeapStats (cmapWithPrio LogHeapStats re Check argFiles -> do let dir = argsProjectRoot dbLoc <- getHieDbLoc dir - runWithWorkerThreads (cmapWithPrio LogSession recorder) dbLoc $ \hiedb threadQueue -> do + runWithWorkerThreads (cmapWithPrio LogSession recorder) dbLoc mempty $ \hiedb threadQueue -> do -- GHC produces messages with UTF8 in them, so make sure the terminal doesn't error hSetEncoding stdout utf8 hSetEncoding stderr utf8 @@ -436,7 +436,7 @@ defaultMain recorder Arguments{..} = withHeapStats (cmapWithPrio LogHeapStats re Custom (IdeCommand c) -> do let root = argsProjectRoot dbLoc <- getHieDbLoc root - runWithWorkerThreads (cmapWithPrio LogSession recorder) dbLoc $ \hiedb threadQueue -> do + runWithWorkerThreads (cmapWithPrio LogSession recorder) dbLoc mempty $ \hiedb threadQueue -> do sessionLoader <- loadSessionWithOptions (cmapWithPrio LogSession recorder) argsSessionLoadingOptions "." (tLoaderQueue threadQueue) let def_options = argsIdeOptions argsDefaultHlsConfig sessionLoader ideOptions = def_options diff --git a/hls-graph/src/Development/IDE/Graph/Internal/Types.hs b/hls-graph/src/Development/IDE/Graph/Internal/Types.hs index e7b8ede75f..156d3a44b9 100644 --- a/hls-graph/src/Development/IDE/Graph/Internal/Types.hs +++ b/hls-graph/src/Development/IDE/Graph/Internal/Types.hs @@ -53,7 +53,8 @@ import UnliftIO (Async (asyncThreadId), newEmptyTMVarIO, poll, putTMVar, readTMVar, readTVarIO, throwTo, - waitCatch, withAsync) + waitCatch, + withAsyncWithUnmask) import UnliftIO.Concurrent (ThreadId, myThreadId) import qualified UnliftIO.Exception as UE @@ -408,7 +409,7 @@ instance Exception AsyncParentKill where fromException = asyncExceptionFromException shutDatabase ::KeySet -> Database -> IO () -shutDatabase dirties db@Database{..} = uninterruptibleMask $ \unmask -> do +shutDatabase dirties db@Database{..} = uninterruptibleMask $ \_unmask -> do -- wait for all threads to finish asyncs <- readTVarIO databaseThreads step <- readTVarIO databaseStep @@ -420,7 +421,7 @@ shutDatabase dirties db@Database{..} = uninterruptibleMask $ \unmask -> do -- Wait until all the asyncs are done -- But if it takes more than 10 seconds, log to stderr unless (null asyncs) $ do - let warnIfTakingTooLong = unmask $ forever $ do + let warnIfTakingTooLong = forever $ do sleep 5 as <- readTVarIO databaseThreads -- poll each async: Nothing => still running @@ -430,7 +431,7 @@ shutDatabase dirties db@Database{..} = uninterruptibleMask $ \unmask -> do let still = [ (deliverName d, show (asyncThreadId a)) | (d,a,p) <- statuses, isNothing p ] traceEventIO $ "cleanupAsync: waiting for asyncs to finish; total=" ++ show (length as) ++ ", stillRunning=" ++ show (length still) traceEventIO $ "cleanupAsync: still running (deliverName, threadId) = " ++ show still - withAsync warnIfTakingTooLong $ \_ -> mapM_ (waitCatch . snd) toCancel + withAsyncWithUnmask (\restore -> restore warnIfTakingTooLong) $ \_ -> mapM_ (waitCatch . snd) toCancel forM_ toCancel $ \(d,_p) -> do let k = deliverKey d when (k /= newKey "root") $ atomically $ deleteDatabaseRuntimeDep k db From 8f80b9eef0f87b9525eceaf17d353f54cabb743b Mon Sep 17 00:00:00 2001 From: soulomoon Date: Thu, 7 May 2026 01:45:44 +0800 Subject: [PATCH 08/32] chore: update GHC version in benchmark matrix --- .github/workflows/bench.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index 0d4f62fc23..49d04e3667 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -120,7 +120,7 @@ jobs: strategy: fail-fast: false matrix: - ghc: ['9.8', '9.10'] + ghc: ['9.10'] os: [ubuntu-latest] cabal: ['3.14'] example: ['cabal', 'lsp-types'] From eecfa90ec0c36ebd066fb6cf7624bd69a179ee4b Mon Sep 17 00:00:00 2001 From: soulomoon Date: Thu, 7 May 2026 01:50:38 +0800 Subject: [PATCH 09/32] chore: remove GHC 9.8 from benchmark matrix --- .github/workflows/bench.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index 49d04e3667..ebcca4de17 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -51,7 +51,6 @@ jobs: # see discussion https://github.com/haskell/haskell-language-server/pull/4118 # also possible to add more GHCs if we performs better in the future. ghc: - - '9.8' - '9.10' os: - ubuntu-latest From ed2a709348c3f3350b6f40dfe5c160b83e07ccaa Mon Sep 17 00:00:00 2001 From: soulomoon Date: Sat, 25 Oct 2025 16:24:49 +0800 Subject: [PATCH 10/32] fix bench 9.12 --- cabal.project | 2 +- shake-bench/shake-bench.cabal | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cabal.project b/cabal.project index c72283b196..a3f8a0b9c3 100644 --- a/cabal.project +++ b/cabal.project @@ -51,7 +51,7 @@ constraints: allow-newer: cabal-install-parsers:Cabal-syntax, -if impl(ghc >= 9.11) +if impl(ghc >= 9.13) benchmarks: False allow-newer: cabal-install-parsers:base, diff --git a/shake-bench/shake-bench.cabal b/shake-bench/shake-bench.cabal index c381089aba..0b5a5e20f7 100644 --- a/shake-bench/shake-bench.cabal +++ b/shake-bench/shake-bench.cabal @@ -16,7 +16,7 @@ source-repository head location: https://github.com/haskell/haskell-language-server.git library - if impl(ghc > 9.11) + if impl(ghc > 9.13) buildable: False exposed-modules: Development.Benchmark.Rules hs-source-dirs: src From a11fbe429aca2441e7ffdd5eed7c78fce9658054 Mon Sep 17 00:00:00 2001 From: soulomoon Date: Tue, 28 Oct 2025 19:23:41 +0800 Subject: [PATCH 11/32] update bench config --- bench/config.yaml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/bench/config.yaml b/bench/config.yaml index 1144efa0ec..84e7cd65d2 100644 --- a/bench/config.yaml +++ b/bench/config.yaml @@ -27,12 +27,12 @@ examples: - src/Distribution/Types/ComponentLocalBuildInfo.hs extra-args: [] # extra HLS command line args # Small-sized project with TH - - name: lsp-types - package: lsp-types - version: 2.1.1.0 - modules: - - src/Language/LSP/Protocol/Types/SemanticTokens.hs - - generated/Language/LSP/Protocol/Internal/Types/NotebookDocumentChangeEvent.hs + # - name: lsp-types + # package: lsp-types + # version: 2.1.1.0 + # modules: + # - src/Language/LSP/Protocol/Types/SemanticTokens.hs + # - generated/Language/LSP/Protocol/Internal/Types/NotebookDocumentChangeEvent.hs - name: MultiLayerModules path: bench/MultiLayerModules.sh @@ -126,6 +126,7 @@ versions: # - 1.8.0.0 - upstream: origin/master # - HEAD~1 +- d323f40cf7a7b88fa6704f8a24e74836965652f7 - HEAD # A list of plugin configurations to analyze From 46d5609628cdd2fdec4792a5396f7dd81dbdb71b Mon Sep 17 00:00:00 2001 From: soulomoon Date: Wed, 29 Oct 2025 19:30:51 +0800 Subject: [PATCH 12/32] update bench config --- bench/config.yaml | 96 +++++++++++++++++++++++------------------------ 1 file changed, 48 insertions(+), 48 deletions(-) diff --git a/bench/config.yaml b/bench/config.yaml index 84e7cd65d2..da0d8c705e 100644 --- a/bench/config.yaml +++ b/bench/config.yaml @@ -27,55 +27,55 @@ examples: - src/Distribution/Types/ComponentLocalBuildInfo.hs extra-args: [] # extra HLS command line args # Small-sized project with TH - # - name: lsp-types - # package: lsp-types - # version: 2.1.1.0 - # modules: - # - src/Language/LSP/Protocol/Types/SemanticTokens.hs - # - generated/Language/LSP/Protocol/Internal/Types/NotebookDocumentChangeEvent.hs - - - name: MultiLayerModules - path: bench/MultiLayerModules.sh - script: True - script-args: ["--th"] - modules: - - MultiLayerModules.hs - - DummyLevel0M01.hs - - DummyLevel1M01.hs - - name: MultiLayerModulesNoTH - path: bench/MultiLayerModules.sh - script: True - script-args: [] - modules: - - MultiLayerModules.hs - - DummyLevel0M01.hs - - DummyLevel1M01.hs - - - name: DummyLevel0M01 - path: bench/MultiLayerModules.sh - script: True - script-args: ["--th"] + - name: lsp-types + package: lsp-types + version: 2.3.0.1 modules: - - DummyLevel0M01.hs - - name: DummyLevel0M01NoTH - path: bench/MultiLayerModules.sh - script: True - script-args: [] - modules: - - DummyLevel0M01.hs + - src/Language/LSP/Protocol/Types/SemanticTokens.hs + - generated/Language/LSP/Protocol/Internal/Types/NotebookDocumentChangeEvent.hs - - name: DummyLevel1M01 - path: bench/MultiLayerModules.sh - script: True - script-args: ["--th"] - modules: - - DummyLevel1M01.hs - - name: DummyLevel1M01NoTH - path: bench/MultiLayerModules.sh - script: True - script-args: [] - modules: - - DummyLevel1M01.hs + # - name: MultiLayerModules + # path: bench/MultiLayerModules.sh + # script: True + # script-args: ["--th"] + # modules: + # - MultiLayerModules.hs + # - DummyLevel0M01.hs + # - DummyLevel1M01.hs + # - name: MultiLayerModulesNoTH + # path: bench/MultiLayerModules.sh + # script: True + # script-args: [] + # modules: + # - MultiLayerModules.hs + # - DummyLevel0M01.hs + # - DummyLevel1M01.hs + + # - name: DummyLevel0M01 + # path: bench/MultiLayerModules.sh + # script: True + # script-args: ["--th"] + # modules: + # - DummyLevel0M01.hs + # - name: DummyLevel0M01NoTH + # path: bench/MultiLayerModules.sh + # script: True + # script-args: [] + # modules: + # - DummyLevel0M01.hs + + # - name: DummyLevel1M01 + # path: bench/MultiLayerModules.sh + # script: True + # script-args: ["--th"] + # modules: + # - DummyLevel1M01.hs + # - name: DummyLevel1M01NoTH + # path: bench/MultiLayerModules.sh + # script: True + # script-args: [] + # modules: + # - DummyLevel1M01.hs # Small but heavily multi-component example # Disabled as it is far to slow. hie-bios >0.7.2 should help @@ -126,7 +126,7 @@ versions: # - 1.8.0.0 - upstream: origin/master # - HEAD~1 -- d323f40cf7a7b88fa6704f8a24e74836965652f7 +# - d323f40cf7a7b88fa6704f8a24e74836965652f7 - HEAD # A list of plugin configurations to analyze From 0f4ea7bb4dd1a2b9ffc02e66b9ee2c782d91fd31 Mon Sep 17 00:00:00 2001 From: soulomoon Date: Wed, 29 Oct 2025 20:29:15 +0800 Subject: [PATCH 13/32] update bench config --- bench/config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bench/config.yaml b/bench/config.yaml index da0d8c705e..d72face8b5 100644 --- a/bench/config.yaml +++ b/bench/config.yaml @@ -125,9 +125,9 @@ versions: # - 1.8.0.0 - upstream: origin/master +- improve-hls-runtime-keep-async-only-databse-keys-upsweep-tmp # - HEAD~1 -# - d323f40cf7a7b88fa6704f8a24e74836965652f7 -- HEAD +- improve-hls-runtime-keep-async-only-databse-keys-downsweep-skip-non-dirties # A list of plugin configurations to analyze # WARNING: Currently bench versions later than e4234a3a5e347db249fccefb8e3fb36f89e8eafb From 5482d79f9c8c81e099098808f08b7a1e4f9700bb Mon Sep 17 00:00:00 2001 From: soulomoon Date: Tue, 18 Nov 2025 18:41:42 +0800 Subject: [PATCH 14/32] update bench CI to 9.12 --- .github/workflows/bench.yml | 3 ++- bench/config.yaml | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index 0d4f62fc23..8ab79f74a3 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -120,7 +120,8 @@ jobs: strategy: fail-fast: false matrix: - ghc: ['9.8', '9.10'] + # ghc: ['9.8', '9.10'] + ghc: ['9.12', '9.10'] os: [ubuntu-latest] cabal: ['3.14'] example: ['cabal', 'lsp-types'] diff --git a/bench/config.yaml b/bench/config.yaml index d72face8b5..efba543fe4 100644 --- a/bench/config.yaml +++ b/bench/config.yaml @@ -125,9 +125,10 @@ versions: # - 1.8.0.0 - upstream: origin/master -- improve-hls-runtime-keep-async-only-databse-keys-upsweep-tmp +# - improve-hls-runtime-keep-async-only-databse-keys-upsweep-tmp # - HEAD~1 -- improve-hls-runtime-keep-async-only-databse-keys-downsweep-skip-non-dirties +# - improve-hls-runtime-keep-async-only-databse-keys-downsweep-skip-non-dirties +- HEAD # A list of plugin configurations to analyze # WARNING: Currently bench versions later than e4234a3a5e347db249fccefb8e3fb36f89e8eafb From 28a6ddd00dbbbefab3de009bf58b846b8441722f Mon Sep 17 00:00:00 2001 From: soulomoon Date: Tue, 18 Nov 2025 22:30:19 +0800 Subject: [PATCH 15/32] refactor: update GHC version handling in benchmark workflow --- .github/workflows/bench.yml | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index 8ab79f74a3..45da04da9b 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -19,7 +19,10 @@ jobs: runs-on: ubuntu-latest outputs: should_skip: ${{ steps.skip_check.outputs.should_skip }} + bench_ghc_versions: ${{ steps.set_ghc_versions.outputs.versions }} steps: + - id: set_ghc_versions + run: echo 'versions=["9.12", "9.10"]' >> $GITHUB_OUTPUT - id: skip_check uses: fkirc/skip-duplicate-actions@v5.3.1 with: @@ -45,14 +48,7 @@ jobs: strategy: fail-fast: false matrix: - # benching the two latest GHCs we support now - # since benchmark are expansive. - # choosing the two latest are easier to maintain and more forward looking - # see discussion https://github.com/haskell/haskell-language-server/pull/4118 - # also possible to add more GHCs if we performs better in the future. - ghc: - - '9.8' - - '9.10' + ghc: ${{ fromJSON(needs.pre_job.outputs.bench_ghc_versions) }} os: - ubuntu-latest @@ -120,8 +116,7 @@ jobs: strategy: fail-fast: false matrix: - # ghc: ['9.8', '9.10'] - ghc: ['9.12', '9.10'] + ghc: ${{ fromJSON(needs.pre_job.outputs.bench_ghc_versions) }} os: [ubuntu-latest] cabal: ['3.14'] example: ['cabal', 'lsp-types'] From 5a5a837e7699edf52f5082dc5b2cbf5da99172d1 Mon Sep 17 00:00:00 2001 From: soulomoon Date: Thu, 7 May 2026 03:19:04 +0800 Subject: [PATCH 16/32] Enable benchmark CI for GHC 9.14 --- .github/workflows/bench.yml | 8 +++++++- cabal.project | 30 +++++++++++++++++++++++++++++- shake-bench/shake-bench.cabal | 2 +- 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index 45da04da9b..ede4e3229e 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -22,7 +22,13 @@ jobs: bench_ghc_versions: ${{ steps.set_ghc_versions.outputs.versions }} steps: - id: set_ghc_versions - run: echo 'versions=["9.12", "9.10"]' >> $GITHUB_OUTPUT + run: | + # benching the two latest GHCs we support now + # since benchmark are expansive. + # choosing the two latest are easier to maintain and more forward looking + # see discussion https://github.com/haskell/haskell-language-server/pull/4118 + # also possible to add more GHCs if we performs better in the future. + echo 'versions=["9.12", "9.14"]' >> $GITHUB_OUTPUT - id: skip_check uses: fkirc/skip-duplicate-actions@v5.3.1 with: diff --git a/cabal.project b/cabal.project index a3f8a0b9c3..8cf5cf58c7 100644 --- a/cabal.project +++ b/cabal.project @@ -51,35 +51,63 @@ constraints: allow-newer: cabal-install-parsers:Cabal-syntax, -if impl(ghc >= 9.13) +if impl(ghc >= 9.15) benchmarks: False + +if impl(ghc >= 9.13) allow-newer: cabal-install-parsers:base, cabal-install-parsers:time, if impl(ghc >= 9.14) allow-newer: + SVGFonts:containers, + active:base, aeson:containers, aeson:template-haskell, aeson:time, binary-instances:base, binary-instances:tagged, binary-orphans:base, + blaze-svg:base, boring:base, cabal-install-parsers:containers, constraints-extras:template-haskell, dependent-map:containers, + diagrams-contrib:base, + diagrams-contrib:containers, + diagrams-core:base, + diagrams-core:containers, + diagrams-lib:base, + diagrams-lib:containers, + diagrams-postscript:base, + diagrams-svg:base, + diagrams-svg:containers, + dual-tree:base, + eventlog2html:containers, + eventlog2html:optparse-applicative, + force-layout:base, + ghc-events:base, ghc-trace-events:base, hie-compat:base, indexed-traversable:base, indexed-traversable:containers, indexed-traversable-instances:base, lukko:base, + microstache:base, + microstache:containers, + monoid-extras:base, + monad-control:transformers-compat, + newtype-generics:base, quickcheck-instances:base, quickcheck-instances:containers, + random-fu:random, semialign:base, semialign:containers, + statestack:base, string-interpolate:template-haskell, + svg-builder:base, + tasty-hspec:QuickCheck, tasty-hspec:base, text-iso8601:time, these:base, diff --git a/shake-bench/shake-bench.cabal b/shake-bench/shake-bench.cabal index 0b5a5e20f7..ecfc2ba736 100644 --- a/shake-bench/shake-bench.cabal +++ b/shake-bench/shake-bench.cabal @@ -16,7 +16,7 @@ source-repository head location: https://github.com/haskell/haskell-language-server.git library - if impl(ghc > 9.13) + if impl(ghc >= 9.15) buildable: False exposed-modules: Development.Benchmark.Rules hs-source-dirs: src From 8cb6663a61cfae6d4959277788a7cbf99fc42ee6 Mon Sep 17 00:00:00 2001 From: soulomoon Date: Thu, 7 May 2026 03:48:18 +0800 Subject: [PATCH 17/32] Drop benchmark SVG generation --- bench/Main.hs | 12 +- bench/README.md | 34 +- cabal.project | 21 -- haskell-language-server.cabal | 3 +- shake-bench/shake-bench.cabal | 6 - .../src/Development/Benchmark/Rules.hs | 312 ++---------------- 6 files changed, 46 insertions(+), 342 deletions(-) diff --git a/bench/Main.hs b/bench/Main.hs index eec4380eb4..d1ac603b18 100644 --- a/bench/Main.hs +++ b/bench/Main.hs @@ -20,14 +20,12 @@ | └── │   ├── .gcStats.log - RTS -s output │   ├── .csv - stats for the experiment - │   ├── .svg - Graph of bytes over elapsed time - │   ├── .diff.svg - idem, including the previous version │   ├── .log - ghcide-bench output │   └── results.csv - results of all the experiments for the example ├── results.csv - aggregated results of all the experiments and versions - └── .svg - graph of bytes over elapsed time, for all the included versions + └── resultDiff.csv - diff of aggregated results compared with previous version - For diff graphs, the "previous version" is the preceding entry in the list of versions + For diff results, the "previous version" is the preceding entry in the list of versions in the config file. A possible improvement is to obtain this info via `git rev-list`. To execute the script: @@ -36,8 +34,8 @@ To build a specific analysis, enumerate the desired file artifacts - > stack bench --ba "bench-results/HEAD/results.csv bench-results/HEAD/edit.diff.svg" - > cabal bench --benchmark-options "bench-results/HEAD/results.csv bench-results/HEAD/edit.diff.svg" + > stack bench --ba "bench-results/HEAD/results.csv" + > cabal bench --benchmark-options "bench-results/HEAD/results.csv" -} {-# LANGUAGE DeriveAnyClass #-} @@ -165,8 +163,6 @@ createBuildSystem config = do benchRules build (MkBenchRules (askOracle $ GetSamples ()) benchHls warmupHls "haskell-language-server" (parallelism configStatic)) addGetParentOracle csvRules build - svgRules build - heapProfileRules build phonyRules "" binaryName NoProfiling build (examples configStatic) whenJust (profileInterval configStatic) $ \i -> do diff --git a/bench/README.md b/bench/README.md index 1dc1e6a3d4..265883e61a 100644 --- a/bench/README.md +++ b/bench/README.md @@ -41,30 +41,30 @@ The Shake script supports a number of phony targets that allow running a subset ``` Targets: - bench-results/binaries/*/commitid - - bench-results/binaries/HEAD/ghcide + - bench-results/binaries/HEAD/haskell-language-server - bench-results/binaries/HEAD/ghc.path - - bench-results/binaries/*/ghcide + - bench-results/binaries/*/haskell-language-server - bench-results/binaries/*/ghc.path - bench-results/binaries/*/*.warmup - - bench-results/*/*/*/*.csv - - bench-results/*/*/*/*.gcStats.log - - bench-results/*/*/*/*.output.log - - bench-results/*/*/*/*.eventlog - - bench-results/*/*/*/*.hp + - bench-results/*/*/*/*/*.csv + - bench-results/*/*/*/*/*.gcStats.log + - bench-results/*/*/*/*/*.output.log + - bench-results/*/*/*/*/*.eventlog + - bench-results/*/*/*/*/*.hp + - bench-results/*/*/*/*/results.csv + - bench-results/*/*/*/*/resultDiff.csv - bench-results/*/*/*/results.csv - - bench-results/*/*/results.csv - - bench-results/*/results.csv - bench-results/*/*/*/resultDiff.csv + - bench-results/*/*/results.csv - bench-results/*/*/resultDiff.csv + - bench-results/*/results.csv - bench-results/*/resultDiff.csv - - bench-results/*/*/*/*.svg - - bench-results/*/*/*/*.diff.svg - - bench-results/*/*/*.svg - - bench-results/*/*/*/*.heap.svg - - Cabal-3.0.0.0 - - lsp-types-1.0.0.1 + - cabal + - lsp-types - all - - profiled-Cabal-3.0.0.0 - - profiled-lsp-types-1.0.0.1 + - all-binaries + - profiled-cabal + - profiled-lsp-types - profiled-all + - profiled-all-binaries ``` diff --git a/cabal.project b/cabal.project index 8cf5cf58c7..2135835999 100644 --- a/cabal.project +++ b/cabal.project @@ -61,33 +61,16 @@ if impl(ghc >= 9.13) if impl(ghc >= 9.14) allow-newer: - SVGFonts:containers, - active:base, aeson:containers, aeson:template-haskell, aeson:time, binary-instances:base, binary-instances:tagged, binary-orphans:base, - blaze-svg:base, boring:base, cabal-install-parsers:containers, constraints-extras:template-haskell, dependent-map:containers, - diagrams-contrib:base, - diagrams-contrib:containers, - diagrams-core:base, - diagrams-core:containers, - diagrams-lib:base, - diagrams-lib:containers, - diagrams-postscript:base, - diagrams-svg:base, - diagrams-svg:containers, - dual-tree:base, - eventlog2html:containers, - eventlog2html:optparse-applicative, - force-layout:base, - ghc-events:base, ghc-trace-events:base, hie-compat:base, indexed-traversable:base, @@ -96,17 +79,13 @@ if impl(ghc >= 9.14) lukko:base, microstache:base, microstache:containers, - monoid-extras:base, monad-control:transformers-compat, - newtype-generics:base, quickcheck-instances:base, quickcheck-instances:containers, random-fu:random, semialign:base, semialign:containers, - statestack:base, string-interpolate:template-haskell, - svg-builder:base, tasty-hspec:QuickCheck, tasty-hspec:base, text-iso8601:time, diff --git a/haskell-language-server.cabal b/haskell-language-server.cabal index 9e0d131dab..614f91f66b 100644 --- a/haskell-language-server.cabal +++ b/haskell-language-server.cabal @@ -1979,8 +1979,7 @@ benchmark benchmark main-is: Main.hs hs-source-dirs: bench build-tool-depends: - haskell-language-server:ghcide-bench, - eventlog2html:eventlog2html, + haskell-language-server:ghcide-bench default-extensions: LambdaCase RecordWildCards diff --git a/shake-bench/shake-bench.cabal b/shake-bench/shake-bench.cabal index ecfc2ba736..934e30a02d 100644 --- a/shake-bench/shake-bench.cabal +++ b/shake-bench/shake-bench.cabal @@ -24,12 +24,6 @@ library aeson, base == 4.*, bytestring, - Chart, - Chart-diagrams, - diagrams-contrib, - diagrams-core, - diagrams-lib, - diagrams-svg, directory, extra >= 1.7.2, filepath, diff --git a/shake-bench/src/Development/Benchmark/Rules.hs b/shake-bench/src/Development/Benchmark/Rules.hs index 8ba2b3f0df..f63d8c36bc 100644 --- a/shake-bench/src/Development/Benchmark/Rules.hs +++ b/shake-bench/src/Development/Benchmark/Rules.hs @@ -27,20 +27,17 @@ │  └── commitid - Git commit id for this reference ├─ │ ├── results.csv - aggregated results for all the versions and configurations - │ ├── .svg - graph of bytes over elapsed time, for all the versions and configurations | └── │ └── │   ├── .gcStats.log - RTS -s output │   ├── .csv - stats for the experiment - │   ├── .svg - Graph of bytes over elapsed time - │   ├── .diff.svg - idem, including the previous version - │   ├── .heap.svg - Heap profile + │   ├── .hp - raw heap profile data │   ├── .log - bench stdout │   └── results.csv - results of all the experiments for the example ├── results.csv - aggregated results of all the examples, experiments, versions and configurations - └── .svg - graph of bytes over elapsed time, for all the examples, experiments, versions and configurations + └── resultDiff.csv - diff of aggregated results compared with previous version - For diff graphs, the "previous version" is the preceding entry in the list of versions + For diff results, the "previous version" is the preceding entry in the list of versions in the config file. A possible improvement is to obtain this info via `git rev-list`. -} {-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} @@ -50,8 +47,6 @@ module Development.Benchmark.Rules benchRules, MkBenchRules(..), BenchProject(..), ProfilingMode(..), addGetParentOracle, csvRules, - svgRules, - heapProfileRules, phonyRules, allTargetsForExample, GetExample(..), GetExamples(..), @@ -68,47 +63,32 @@ module Development.Benchmark.Rules ) where import Control.Applicative -import Control.Lens (preview, view, (^.)) +import Control.Lens (preview, (^.)) import Control.Monad -import qualified Control.Monad.State as S -import Data.Aeson (FromJSON (..), - ToJSON (..), - Value (..), object, - (.!=), (.:?), (.=)) -import Data.Aeson.Lens (AsJSON (_JSON), - _Object, _String) -import Data.ByteString.Lazy (ByteString) -import Data.Char (isAlpha, isDigit) -import Data.List (find, intercalate, - isInfixOf, - isSuffixOf, - stripPrefix, - transpose) -import Data.List.Extra (lower, splitOn) -import Data.Maybe (fromMaybe) -import Data.String (fromString) -import Data.Text (Text) -import qualified Data.Text as T +import Data.Aeson (FromJSON (..), ToJSON (..), + Value (..), object, (.!=), (.:?), + (.=)) +import Data.Aeson.Lens (AsJSON (_JSON), _Object, _String) +import Data.ByteString.Lazy (ByteString) +import Data.Char (isDigit) +import Data.List (find, intercalate, isInfixOf, + isSuffixOf, stripPrefix, transpose) +import Data.List.Extra (lower, splitOn) +import Data.Maybe (fromMaybe) +import Data.String (fromString) +import Data.Text (Text) +import qualified Data.Text as T import Development.Shake -import Development.Shake.Classes (Binary, Hashable, - NFData, Typeable) -import GHC.Exts (IsList (toList), - fromList) -import GHC.Generics (Generic) -import GHC.Stack (HasCallStack) -import qualified Graphics.Rendering.Chart.Backend.Diagrams as E -import qualified Graphics.Rendering.Chart.Easy as E +import Development.Shake.Classes (Binary, Hashable, NFData, Typeable) +import GHC.Exts (IsList (toList), fromList) +import GHC.Generics (Generic) import Numeric.Natural -import System.Directory (createDirectoryIfMissing, - findExecutable, - renameFile) +import System.Directory (createDirectoryIfMissing, + findExecutable) import System.FilePath -import System.Time.Extra (Seconds) -import qualified Text.ParserCombinators.ReadP as P +import System.Time.Extra (Seconds) import Text.Printf -import Text.Read (Read (..), get, - readMaybe, - readP_to_Prec) +import Text.Read (readMaybe) newtype GetExperiments = GetExperiments () deriving newtype (Binary, Eq, Hashable, NFData, Show) newtype GetVersions = GetVersions () deriving newtype (Binary, Eq, Hashable, NFData, Show) @@ -142,28 +122,11 @@ class (Binary e, Eq e, Hashable e, NFData e, Show e, Typeable e) => IsExample e allTargetsForExample :: IsExample e => ProfilingMode -> FilePath -> e -> Action [FilePath] allTargetsForExample prof baseFolder ex = do - experiments <- askOracle $ GetExperiments () - versions <- askOracle $ GetVersions () - configurations <- askOracle $ GetConfigurations () let buildFolder = baseFolder profilingPath prof return $ [ buildFolder getExampleName ex "results.csv" , buildFolder getExampleName ex "resultDiff.csv"] - ++ [ buildFolder getExampleName ex escaped (escapeExperiment e) <.> "svg" - | e <- experiments - ] - ++ [ buildFolder - getExampleName ex - T.unpack (humanName ver) - confName - escaped (escapeExperiment e) <.> - mode - | e <- experiments, - ver <- versions, - Configuration{confName} <- configurations, - mode <- ["svg", "diff.svg"] ++ ["heap.svg" | prof /= NoProfiling] - ] allBinaries :: FilePath -> String -> Action [FilePath] allBinaries buildFolder executableName = do @@ -472,77 +435,6 @@ parseLine = map f . splitOn "," Just time -> Time time Nothing -> ItemString x --------------------------------------------------------------------------------- - --- | Rules to produce charts for the GC stats -svgRules :: FilePattern -> Rules () -svgRules build = do - -- chart GC stats for an experiment on a given revision - priority 1 $ - build -/- "*/*/*/*/*.svg" %> \out -> do - let [_, _, _example, ver, conf, _exp] = splitDirectories out - runLog <- loadRunLog (Escaped $ replaceExtension out "csv") ver conf - let diagram = Diagram Live [runLog] title - title = ver <> " live bytes over time" - plotDiagram True diagram out - - -- chart of GC stats for an experiment on this and the previous revision - priority 2 $ - build -/- "*/*/*/*/*.diff.svg" %> \out -> do - let [b, flav, example, ver, conf, exp_] = splitDirectories out - exp = Escaped $ dropExtension2 exp_ - prev <- fmap T.unpack $ askOracle $ GetParent $ T.pack ver - - runLog <- loadRunLog (Escaped $ replaceExtension (dropExtension out) "csv") ver conf - runLogPrev <- loadRunLog (Escaped $ joinPath [b,flav, example, prev, conf, replaceExtension (dropExtension exp_) "csv"]) prev conf - - let diagram = Diagram Live [runLog, runLogPrev] title - title = show (unescapeExperiment exp) <> " - live bytes over time compared" - plotDiagram True diagram out - - -- aggregated chart of GC stats for all the configurations - build -/- "*/*/*/*.svg" %> \out -> do - let exp = Escaped $ dropExtension $ takeFileName out - [b, flav, example, ver] = splitDirectories out - versions <- askOracle $ GetVersions () - configurations <- askOracle $ GetConfigurations () - - runLogs <- forM configurations $ \Configuration{confName} -> do - loadRunLog (Escaped $ takeDirectory out confName replaceExtension (takeFileName out) "csv") ver confName - - let diagram = Diagram Live runLogs title - title = show (unescapeExperiment exp) <> " - live bytes over time" - plotDiagram False diagram out - - -- aggregated chart of GC stats for all the revisions - build -/- "*/*/*.svg" %> \out -> do - let exp = Escaped $ dropExtension $ takeFileName out - versions <- askOracle $ GetVersions () - configurations <- askOracle $ GetConfigurations () - - runLogs <- forM (filter include versions) $ \v -> - forM configurations $ \Configuration{confName} -> do - let v' = T.unpack (humanName v) - loadRunLog (Escaped $ takeDirectory out v' confName replaceExtension (takeFileName out) "csv") v' confName - - let diagram = Diagram Live (concat runLogs) title - title = show (unescapeExperiment exp) <> " - live bytes over time" - plotDiagram False diagram out - -heapProfileRules :: FilePattern -> Rules () -heapProfileRules build = do - priority 3 $ - build -/- "*/*/*/*/*.heap.svg" %> \out -> do - let hpFile = dropExtension2 out <.> "hp" - need [hpFile] - cmd_ ("eventlog2html" :: String) ["--heap-profile", hpFile] - liftIO $ renameFile (dropExtension hpFile <.> "svg") out - -dropExtension2 :: FilePath -> FilePath -dropExtension2 = dropExtension . dropExtension --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- - -- | Default build system that handles Cabal and Stack data BuildSystem = Cabal | Stack deriving (Eq, Read, Show, Generic) @@ -611,119 +503,6 @@ findPrev name (x : y : xx) | otherwise = findPrev name (y : xx) findPrev name _ = name --------------------------------------------------------------------------------- - --- | A line in the output of -S -data Frame = Frame - { allocated, copied, live :: !Int, - user, elapsed, totUser, totElapsed :: !Double, - generation :: !Int - } - deriving (Show) - -instance Read Frame where - readPrec = do - spaces - allocated <- readPrec @Int <* spaces - copied <- readPrec @Int <* spaces - live <- readPrec @Int <* spaces - user <- readPrec @Double <* spaces - elapsed <- readPrec @Double <* spaces - totUser <- readPrec @Double <* spaces - totElapsed <- readPrec @Double <* spaces - _ <- readPrec @Int <* spaces - _ <- readPrec @Int <* spaces - "(Gen: " <- replicateM 7 get - generation <- readPrec @Int - ')' <- get - return Frame {..} - where - spaces = readP_to_Prec $ const P.skipSpaces - --- | A file path containing the output of -S for a given run -data RunLog = RunLog - { runVersion :: !String, - runConfiguration :: !String, - runFrames :: ![Frame], - runSuccess :: !Bool, - runFirstReponse :: !(Maybe Seconds) - } - -loadRunLog :: HasCallStack => Escaped FilePath -> String -> String -> Action RunLog -loadRunLog (Escaped csv_fp) ver conf = do - let log_fp = replaceExtension csv_fp "gcStats.log" - log <- readFileLines log_fp - csv <- readFileLines csv_fp - let frames = - [ f - | l <- log, - Just f <- [readMaybe l], - -- filter out gen 0 events as there are too many - generation f == 1 - ] - -- TODO this assumes a certain structure in the CSV file - (success, firstResponse) = case map (map T.strip . T.split (== ',') . T.pack) csv of - [header, row] - | let table = zip header row - timeForFirstResponse :: Maybe Seconds - timeForFirstResponse = readMaybe . T.unpack =<< lookup "firstBuildTime" table - , Just s <- lookup "success" table - , Just s <- readMaybe (T.unpack s) - -> (s,timeForFirstResponse) - _ -> error $ "Cannot parse: " <> csv_fp - return $ RunLog ver conf frames success firstResponse - --------------------------------------------------------------------------------- - -data TraceMetric = Allocated | Copied | Live | User | Elapsed - deriving (Generic, Enum, Bounded, Read) - -instance Show TraceMetric where - show Allocated = "Allocated bytes" - show Copied = "Copied bytes" - show Live = "Live bytes" - show User = "User time" - show Elapsed = "Elapsed time" - -frameMetric :: TraceMetric -> Frame -> Double -frameMetric Allocated = fromIntegral . allocated -frameMetric Copied = fromIntegral . copied -frameMetric Live = fromIntegral . live -frameMetric Elapsed = elapsed -frameMetric User = user - -data Diagram = Diagram - { traceMetric :: TraceMetric, - runLogs :: [RunLog], - title :: String - } - deriving (Generic) - -plotDiagram :: Bool -> Diagram -> FilePath -> Action () -plotDiagram includeFailed t@Diagram {traceMetric, runLogs} out = do - let extract = frameMetric traceMetric - liftIO $ E.toFile E.def out $ do - E.layout_title E..= title t - E.setColors myColors - forM_ runLogs $ \rl -> - when (includeFailed || runSuccess rl) $ do - -- Get the color we are going to use - ~(c:_) <- E.liftCState $ S.gets (E.view E.colors) - E.plot $ do - lplot <- E.line - (runVersion rl ++ " " ++ runConfiguration rl ++ if runSuccess rl then "" else " (FAILED)") - [ [ (totElapsed f, extract f) - | f <- runFrames rl - ] - ] - return (lplot E.& E.plot_lines_style . E.line_width E.*~ 2) - case runFirstReponse rl of - Just t -> E.plot $ pure $ - E.vlinePlot ("First build: " ++ runVersion rl) (E.defaultPlotLineStyle E.& E.line_color E..~ c) t - _ -> pure () - --------------------------------------------------------------------------------- - newtype Escaped a = Escaped {escaped :: a} newtype Unescaped a = Unescaped {unescaped :: a} @@ -749,49 +528,6 @@ a -/- b = a <> "/" <> b interleave :: [[a]] -> [a] interleave = concat . transpose --------------------------------------------------------------------------------- - -myColors :: [E.AlphaColour Double] -myColors = map E.opaque - [ E.blue - , E.green - , E.red - , E.orange - , E.yellow - , E.violet - , E.black - , E.gold - , E.brown - , E.hotpink - , E.aliceblue - , E.aqua - , E.beige - , E.bisque - , E.blueviolet - , E.burlywood - , E.cadetblue - , E.chartreuse - , E.coral - , E.crimson - , E.darkblue - , E.darkgray - , E.darkgreen - , E.darkkhaki - , E.darkmagenta - , E.deeppink - , E.dodgerblue - , E.firebrick - , E.forestgreen - , E.fuchsia - , E.greenyellow - , E.lightsalmon - , E.seagreen - , E.olive - , E.sandybrown - , E.sienna - , E.peru - ] - dummyHp :: String dummyHp = "JOB \"ghcide\" \ From cd6ee40c436dc15ff6e906187e65c04bae041baa Mon Sep 17 00:00:00 2001 From: soulomoon Date: Thu, 7 May 2026 03:54:36 +0800 Subject: [PATCH 18/32] refactor: remove unused dependencies for GHC 9.14 --- cabal.project | 5 ----- 1 file changed, 5 deletions(-) diff --git a/cabal.project b/cabal.project index 2135835999..6ed4cf58d7 100644 --- a/cabal.project +++ b/cabal.project @@ -77,16 +77,11 @@ if impl(ghc >= 9.14) indexed-traversable:containers, indexed-traversable-instances:base, lukko:base, - microstache:base, - microstache:containers, - monad-control:transformers-compat, quickcheck-instances:base, quickcheck-instances:containers, - random-fu:random, semialign:base, semialign:containers, string-interpolate:template-haskell, - tasty-hspec:QuickCheck, tasty-hspec:base, text-iso8601:time, these:base, From 1de9bab7a6b5a92f252c3c0b5132b6e1c14fa2ed Mon Sep 17 00:00:00 2001 From: soulomoon Date: Thu, 7 May 2026 06:21:42 +0800 Subject: [PATCH 19/32] Use Cabal 3.16 for benchmark CI --- .github/workflows/bench.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index ede4e3229e..e67b659d0a 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -124,7 +124,7 @@ jobs: matrix: ghc: ${{ fromJSON(needs.pre_job.outputs.bench_ghc_versions) }} os: [ubuntu-latest] - cabal: ['3.14'] + cabal: ['3.16.1.0'] example: ['cabal', 'lsp-types'] steps: @@ -163,7 +163,7 @@ jobs: column -s, -t < bench-results/unprofiled/${{ matrix.example }}/resultDiff.csv | tee bench-results/unprofiled/${{ matrix.example }}/resultDiff.txt - name: tar benchmarking artifacts - run: find bench-results -name "*.csv" -or -name "*.svg" -or -name "*.html" | xargs tar -czf benchmark-artifacts.tar.gz + run: find bench-results -name "*.csv" | xargs tar -czf benchmark-artifacts.tar.gz - name: Archive benchmarking artifacts uses: actions/upload-artifact@v6 From abb8a0e20354c9e488bcdee5e8a553e83fd249cd Mon Sep 17 00:00:00 2001 From: soulomoon Date: Thu, 7 May 2026 06:25:33 +0800 Subject: [PATCH 20/32] fix: update Cabal version to 3.16 in benchmark workflow --- .github/workflows/bench.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index e67b659d0a..7e9a5ba679 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -124,7 +124,7 @@ jobs: matrix: ghc: ${{ fromJSON(needs.pre_job.outputs.bench_ghc_versions) }} os: [ubuntu-latest] - cabal: ['3.16.1.0'] + cabal: ['3.16'] example: ['cabal', 'lsp-types'] steps: From e9530c28ed02f974d68a1cf9fae479d0b3e3b71b Mon Sep 17 00:00:00 2001 From: soulomoon Date: Thu, 7 May 2026 07:03:16 +0800 Subject: [PATCH 21/32] Update benchmark examples for GHC 9.14 --- bench/config.yaml | 4 ++-- ghcide-bench/src/Experiments.hs | 32 +++++++++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/bench/config.yaml b/bench/config.yaml index efba543fe4..5610c31fe7 100644 --- a/bench/config.yaml +++ b/bench/config.yaml @@ -21,7 +21,7 @@ examples: # Medium-sized project without TH - name: cabal package: Cabal - version: 3.10.2.1 + version: 3.16.1.0 modules: - src/Distribution/Simple.hs - src/Distribution/Types/ComponentLocalBuildInfo.hs @@ -29,7 +29,7 @@ examples: # Small-sized project with TH - name: lsp-types package: lsp-types - version: 2.3.0.1 + version: 2.4.0.0 modules: - src/Language/LSP/Protocol/Types/SemanticTokens.hs - generated/Language/LSP/Protocol/Internal/Types/NotebookDocumentChangeEvent.hs diff --git a/ghcide-bench/src/Experiments.hs b/ghcide-bench/src/Experiments.hs index c53ffd0a7c..f33a705233 100644 --- a/ghcide-bench/src/Experiments.hs +++ b/ghcide-bench/src/Experiments.hs @@ -718,7 +718,7 @@ setup = do -- Need this in case there is a parent cabal.project somewhere writeFile (path "cabal.project") - "packages: ." + (cabalProjectForPackage ExamplePackage{..}) writeFile (path "cabal.project.local") "" @@ -745,6 +745,11 @@ setup = do writeFile hieYamlPath simpleStackCradleContent return path + checkExampleModulesExist benchDir (example ?config) + case (buildTool ?config, exampleDetails (example ?config)) of + (Cabal, ExampleHackage{}) -> buildCabalExample benchDir + _ -> return () + whenJust (shakeProfiling ?config) $ createDirectoryIfMissing True let cleanUp = case exampleDetails (example ?config) of @@ -756,6 +761,31 @@ setup = do return SetupResult{..} +checkExampleModulesExist :: FilePath -> Example -> IO () +checkExampleModulesExist benchDir Example{..} = + forM_ exampleModules $ \target -> do + let fullPath = benchDir target + exists <- doesFileExist fullPath + unless exists $ + fail $ "Benchmark example " <> show exampleName + <> " is missing target file " <> show target + <> " at " <> fullPath + +buildCabalExample :: HasConfig => FilePath -> IO () +buildCabalExample path = do + output $ "cabal build all -j in " <> path + cmd_ (Cwd path) ("cabal" :: String) (["build", "all", "-j"] :: [String]) + +cabalProjectForPackage :: ExamplePackage -> String +cabalProjectForPackage ExamplePackage{packageName = "lsp-types"} = + unlines + [ "packages: ." + , "if impl(ghc >= 9.14)" + , " allow-newer: boring:base" + ] +cabalProjectForPackage _ = + "packages: ." + setupDocumentContents :: Config -> Session [DocumentPositions] setupDocumentContents config = forM (exampleModules $ example config) $ \m -> do From 9786cd781df6ebd30f2c91e6da13e12e0918edaf Mon Sep 17 00:00:00 2001 From: soulomoon Date: Thu, 7 May 2026 17:24:14 +0800 Subject: [PATCH 22/32] Fix benchmark target project setup --- ghcide-bench/src/Experiments.hs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/ghcide-bench/src/Experiments.hs b/ghcide-bench/src/Experiments.hs index f33a705233..968add9252 100644 --- a/ghcide-bench/src/Experiments.hs +++ b/ghcide-bench/src/Experiments.hs @@ -708,12 +708,11 @@ setup = do package = packageName <> "-" <> showVersion packageVersion hieYamlPath = path "hie.yaml" alreadySetup <- doesDirectoryExist path - unless alreadySetup $ - case buildTool ?config of + case buildTool ?config of Cabal -> do let cabalVerbosity = "-v" ++ show (fromEnum (verbose ?config)) - callCommandLogging $ "cabal get " <> cabalVerbosity <> " " <> package <> " -d " <> examplesPath - let hieYamlPath = path "hie.yaml" + unless alreadySetup $ + callCommandLogging $ "cabal get " <> cabalVerbosity <> " " <> package <> " -d " <> examplesPath writeFile hieYamlPath simpleCabalCradleContent -- Need this in case there is a parent cabal.project somewhere writeFile @@ -722,7 +721,7 @@ setup = do writeFile (path "cabal.project.local") "" - Stack -> do + Stack -> unless alreadySetup $ do let stackVerbosity = case verbosity ?config of Quiet -> "--silent" Normal -> "" @@ -780,8 +779,7 @@ cabalProjectForPackage :: ExamplePackage -> String cabalProjectForPackage ExamplePackage{packageName = "lsp-types"} = unlines [ "packages: ." - , "if impl(ghc >= 9.14)" - , " allow-newer: boring:base" + , "allow-newer: boring:base" ] cabalProjectForPackage _ = "packages: ." From 399bba3fb9ca775d9e35bfcb32e0551cc52c8c74 Mon Sep 17 00:00:00 2001 From: soulomoon Date: Thu, 7 May 2026 19:53:40 +0800 Subject: [PATCH 23/32] fix: restore benchmark configurations for MultiLayerModules and DummyLevel0/1M01 --- bench/config.yaml | 84 +++++++++++++++++++++++------------------------ 1 file changed, 41 insertions(+), 43 deletions(-) diff --git a/bench/config.yaml b/bench/config.yaml index 5610c31fe7..8ea3e45086 100644 --- a/bench/config.yaml +++ b/bench/config.yaml @@ -34,48 +34,48 @@ examples: - src/Language/LSP/Protocol/Types/SemanticTokens.hs - generated/Language/LSP/Protocol/Internal/Types/NotebookDocumentChangeEvent.hs - # - name: MultiLayerModules - # path: bench/MultiLayerModules.sh - # script: True - # script-args: ["--th"] - # modules: - # - MultiLayerModules.hs - # - DummyLevel0M01.hs - # - DummyLevel1M01.hs - # - name: MultiLayerModulesNoTH - # path: bench/MultiLayerModules.sh - # script: True - # script-args: [] - # modules: - # - MultiLayerModules.hs - # - DummyLevel0M01.hs - # - DummyLevel1M01.hs - - # - name: DummyLevel0M01 - # path: bench/MultiLayerModules.sh - # script: True - # script-args: ["--th"] - # modules: - # - DummyLevel0M01.hs - # - name: DummyLevel0M01NoTH - # path: bench/MultiLayerModules.sh - # script: True - # script-args: [] - # modules: - # - DummyLevel0M01.hs + - name: MultiLayerModules + path: bench/MultiLayerModules.sh + script: True + script-args: ["--th"] + modules: + - MultiLayerModules.hs + - DummyLevel0M01.hs + - DummyLevel1M01.hs + - name: MultiLayerModulesNoTH + path: bench/MultiLayerModules.sh + script: True + script-args: [] + modules: + - MultiLayerModules.hs + - DummyLevel0M01.hs + - DummyLevel1M01.hs + + - name: DummyLevel0M01 + path: bench/MultiLayerModules.sh + script: True + script-args: ["--th"] + modules: + - DummyLevel0M01.hs + - name: DummyLevel0M01NoTH + path: bench/MultiLayerModules.sh + script: True + script-args: [] + modules: + - DummyLevel0M01.hs - # - name: DummyLevel1M01 - # path: bench/MultiLayerModules.sh - # script: True - # script-args: ["--th"] - # modules: - # - DummyLevel1M01.hs - # - name: DummyLevel1M01NoTH - # path: bench/MultiLayerModules.sh - # script: True - # script-args: [] - # modules: - # - DummyLevel1M01.hs + - name: DummyLevel1M01 + path: bench/MultiLayerModules.sh + script: True + script-args: ["--th"] + modules: + - DummyLevel1M01.hs + - name: DummyLevel1M01NoTH + path: bench/MultiLayerModules.sh + script: True + script-args: [] + modules: + - DummyLevel1M01.hs # Small but heavily multi-component example # Disabled as it is far to slow. hie-bios >0.7.2 should help @@ -125,9 +125,7 @@ versions: # - 1.8.0.0 - upstream: origin/master -# - improve-hls-runtime-keep-async-only-databse-keys-upsweep-tmp # - HEAD~1 -# - improve-hls-runtime-keep-async-only-databse-keys-downsweep-skip-non-dirties - HEAD # A list of plugin configurations to analyze From 2f8a559f0c4ab5362efda15dc044731f8a7df8b0 Mon Sep 17 00:00:00 2001 From: soulomoon Date: Thu, 7 May 2026 21:41:31 +0800 Subject: [PATCH 24/32] fix: hide uninformative benchmark diffs --- .../src/Development/Benchmark/Rules.hs | 39 ++++++++++++++----- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/shake-bench/src/Development/Benchmark/Rules.hs b/shake-bench/src/Development/Benchmark/Rules.hs index f63d8c36bc..b471f5eca7 100644 --- a/shake-bench/src/Development/Benchmark/Rules.hs +++ b/shake-bench/src/Development/Benchmark/Rules.hs @@ -379,13 +379,17 @@ csvRules build = do results = map tail allResults writeFileChanged out $ unlines $ header : concat results priority 2 $ build -/- "*/*/*/*/resultDiff.csv" %> \out -> do - let out2@[b, flav, example, ver, conf, exp_] = splitDirectories out + let [b, flav, example, ver, conf, _exp] = splitDirectories out prev <- fmap T.unpack $ askOracle $ GetParent $ T.pack ver allResultsCur <- readFileLines $ joinPath [b ,flav, example, ver, conf] "results.csv" - allResultsPrev <- readFileLines $ joinPath [b ,flav, example, prev, conf] "results.csv" - let resultsPrev = tail allResultsPrev - let resultsCur = tail allResultsCur - let resultDiff = zipWith convertToDiffResults resultsCur resultsPrev + resultDiff <- + if prev == ver + then pure [] + else do + allResultsPrev <- readFileLines $ joinPath [b ,flav, example, prev, conf] "results.csv" + let resultsPrev = tail allResultsPrev + let resultsCur = tail allResultsCur + pure $ zipWith convertToDiffResults resultsCur resultsPrev writeFileChanged out $ unlines $ head allResultsCur : resultDiff -- aggregate all configurations for an experiment priority 3 $ build -/- "*/*/*/results.csv" %> genConfig "results.csv" @@ -413,15 +417,32 @@ convertToDiffResults line baseLine = intercalate "," diffResults showItemDiffResult :: (Item, Maybe Double) -> String showItemDiffResult (ItemString x, _) = x showItemDiffResult (_, Nothing) = "NA" -showItemDiffResult (Mem x, Just y) = printf "%.2f" (y * 100 - 100) <> "%" -showItemDiffResult (Time x, Just y) = printf "%.2f" (y * 100 - 100) <> "%" +showItemDiffResult (Mem _, Just y) = showPercentageDiff y +showItemDiffResult (Time _, Just y) = showPercentageDiff y + +showPercentageDiff :: Double -> String +showPercentageDiff ratio + | not (isFinite percent) = "NA" + | abs percent < 0.005 = "" + | otherwise = printf "%.2f" percent <> "%" + where + percent = ratio * 100 - 100 + +isFinite :: Double -> Bool +isFinite x = not (isNaN x || isInfinite x) diffItem :: Item -> Item -> (Item, Maybe Double) -diffItem (Mem x) (Mem y) = (Mem x, Just $ fromIntegral x / fromIntegral y) -diffItem (Time x) (Time y) = (Time x, if y == 0 then Nothing else Just $ x / y) +diffItem (Mem x) (Mem y) = (Mem x, ratioMaybe (fromIntegral x) (fromIntegral y)) +diffItem (Time x) (Time y) = (Time x, ratioMaybe x y) diffItem (ItemString x) (ItemString y) = (ItemString x, Nothing) diffItem _ _ = (ItemString "no match", Nothing) +ratioMaybe :: Double -> Double -> Maybe Double +ratioMaybe x y + | y == 0 = Nothing + | not (isFinite x && isFinite y) = Nothing + | otherwise = Just $ x / y + data Item = Mem Int | Time Double | ItemString String deriving (Show) From b7e950add148de684123e5d31544f881b060f77a Mon Sep 17 00:00:00 2001 From: soulomoon Date: Thu, 7 May 2026 23:22:16 +0800 Subject: [PATCH 25/32] fix: remove threshold for insignificant percentage differences in showPercentageDiff --- shake-bench/src/Development/Benchmark/Rules.hs | 1 - 1 file changed, 1 deletion(-) diff --git a/shake-bench/src/Development/Benchmark/Rules.hs b/shake-bench/src/Development/Benchmark/Rules.hs index b471f5eca7..2ddcb97cc5 100644 --- a/shake-bench/src/Development/Benchmark/Rules.hs +++ b/shake-bench/src/Development/Benchmark/Rules.hs @@ -423,7 +423,6 @@ showItemDiffResult (Time _, Just y) = showPercentageDiff y showPercentageDiff :: Double -> String showPercentageDiff ratio | not (isFinite percent) = "NA" - | abs percent < 0.005 = "" | otherwise = printf "%.2f" percent <> "%" where percent = ratio * 100 - 100 From 2a71e12a6850655040cb8c0dda744de6f84d5e98 Mon Sep 17 00:00:00 2001 From: soulomoon Date: Fri, 8 May 2026 00:57:53 +0800 Subject: [PATCH 26/32] refactor: simplify spawnAsyncWithDbRegistration by removing registerHook parameter --- hls-graph/src/Development/IDE/Graph/Internal/Database.hs | 6 ++---- hls-graph/src/Development/IDE/Graph/Internal/Key.hs | 1 - hls-graph/src/Development/IDE/Graph/Internal/Types.hs | 7 +++---- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/hls-graph/src/Development/IDE/Graph/Internal/Database.hs b/hls-graph/src/Development/IDE/Graph/Internal/Database.hs index 13dd914dd6..59a385bd36 100644 --- a/hls-graph/src/Development/IDE/Graph/Internal/Database.hs +++ b/hls-graph/src/Development/IDE/Graph/Internal/Database.hs @@ -163,7 +163,7 @@ builderOne' parentKey db@Database {..} stack key = UE.uninterruptibleMask $ \res case (viewToRun $ keyStatus <$> status) of (Dirty prev) -> do SMap.focus (updateStatus $ Running current prev) key databaseValues - let register = spawnRefresh db stack key barrier prev (return ()) refresh + let register = spawnRefresh db stack key barrier prev refresh -- why it is important to use rollback here {- Note [Rollback is required if killed before registration] @@ -384,17 +384,15 @@ spawnRefresh :: Key -> MVar (Either SomeException (Key, Result)) -> Maybe Result -> - STM () -> (Database -> t -> Key -> Maybe Result -> IO Result) -> (SomeException -> IO ()) -> (forall a. IO a -> IO a) -> IO () -spawnRefresh db@Database {..} stack key barrier prevResult registerHook refresher rollBack restore = do +spawnRefresh db@Database {..} stack key barrier prevResult refresher rollBack restore = do Step currentStep <- readTVarIO databaseStep spawnAsyncWithDbRegistration db (DeliverStatus currentStep ("async computation; " ++ show key) key) - registerHook (refresher db stack key prevResult) (\r -> do case r of diff --git a/hls-graph/src/Development/IDE/Graph/Internal/Key.hs b/hls-graph/src/Development/IDE/Graph/Internal/Key.hs index a05aed2b40..522349a76e 100644 --- a/hls-graph/src/Development/IDE/Graph/Internal/Key.hs +++ b/hls-graph/src/Development/IDE/Graph/Internal/Key.hs @@ -62,7 +62,6 @@ pattern Key :: () => (Typeable a, Hashable a, Show a) => a -> Key pattern Key a <- (lookupKeyValue -> (KeyValue a _)) pattern DirectKey :: Int -> Key pattern DirectKey a <- (lookupKeyValue -> (DirectKeyValue a)) -{-# COMPLETE Key #-} {-# COMPLETE Key, DirectKey #-} instance Pretty Key where diff --git a/hls-graph/src/Development/IDE/Graph/Internal/Types.hs b/hls-graph/src/Development/IDE/Graph/Internal/Types.hs index 156d3a44b9..1890b38a3d 100644 --- a/hls-graph/src/Development/IDE/Graph/Internal/Types.hs +++ b/hls-graph/src/Development/IDE/Graph/Internal/Types.hs @@ -372,15 +372,14 @@ isRootKey _ = False -- 4. Exception safety with rollback on registration failure -- @ inline {-# INLINE spawnAsyncWithDbRegistration #-} -spawnAsyncWithDbRegistration :: Database -> DeliverStatus -> STM () -> IO a1 -> (Either SomeException a1 -> IO ()) -> (forall a. IO a -> IO a) -> IO () -spawnAsyncWithDbRegistration db@Database{..} deliver registerHook asyncBody handler restore = do +spawnAsyncWithDbRegistration :: Database -> DeliverStatus -> IO a1 -> (Either SomeException a1 -> IO ()) -> (forall a. IO a -> IO a) -> IO () +spawnAsyncWithDbRegistration db@Database{..} deliver asyncBody handler restore = do startBarrier <- newEmptyTMVarIO -- 1. we need to make sure the thread is registered before we actually start -- 2. we should not start in between the restart -- 3. if it is killed before we start, we need to cancel the async let register a = do dbNotLocked db - registerHook modifyTVar' databaseThreads ((deliver, a):) -- make sure we only start after the restart putTMVar startBarrier () @@ -394,7 +393,7 @@ spawnAsyncWithDbRegistration db@Database{..} deliver registerHook asyncBody hand {-# INLINE runInThreadStmInNewThreads #-} runInThreadStmInNewThreads :: Database -> DeliverStatus -> IO a -> (Either SomeException a -> IO ()) -> IO () runInThreadStmInNewThreads db deliver act handler = uninterruptibleMask $ \restore -> - spawnAsyncWithDbRegistration db deliver (return ()) act handler restore + spawnAsyncWithDbRegistration db deliver act handler restore getDataBaseStepInt :: Database -> STM Int getDataBaseStepInt db = do From 1bcc1bd12bef0e319a265ae25440bfc426b9e92d Mon Sep 17 00:00:00 2001 From: soulomoon Date: Fri, 8 May 2026 01:12:46 +0800 Subject: [PATCH 27/32] fix: handle unmatched Key cases in fromKey and fromKeyType functions --- ghcide/src/Development/IDE/Types/Shake.hs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ghcide/src/Development/IDE/Types/Shake.hs b/ghcide/src/Development/IDE/Types/Shake.hs index cc8f84e3b6..a14d2b575b 100644 --- a/ghcide/src/Development/IDE/Types/Shake.hs +++ b/ghcide/src/Development/IDE/Types/Shake.hs @@ -82,6 +82,7 @@ fromKey :: Typeable k => Key -> Maybe (k, NormalizedFilePath) fromKey (Key k) | Just (Q (k', f)) <- cast k = Just (k', f) | otherwise = Nothing +fromKey _ = Nothing -- | fromKeyType (Q (k,f)) = (typeOf k, f) fromKeyType :: Key -> Maybe (SomeTypeRep, NormalizedFilePath) @@ -91,6 +92,7 @@ fromKeyType (Key k) , Q (_, f) <- k = Just (SomeTypeRep a, f) | otherwise = Nothing +fromKeyType _ = Nothing toNoFileKey :: (Show k, Typeable k, Eq k, Hashable k) => k -> Key toNoFileKey k = newKey $ Q (k, emptyFilePath) From 6095bcda0a275ff0074a92ff65c2aca1ee02a8e4 Mon Sep 17 00:00:00 2001 From: soulomoon Date: Fri, 8 May 2026 01:16:28 +0800 Subject: [PATCH 28/32] fix: correct newDirectKey implementation to avoid key collisions --- hls-graph/src/Development/IDE/Graph/Internal/Key.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hls-graph/src/Development/IDE/Graph/Internal/Key.hs b/hls-graph/src/Development/IDE/Graph/Internal/Key.hs index 522349a76e..9447c71a8a 100644 --- a/hls-graph/src/Development/IDE/Graph/Internal/Key.hs +++ b/hls-graph/src/Development/IDE/Graph/Internal/Key.hs @@ -93,7 +93,7 @@ keyMap = unsafePerformIO $ newIORef (GlobalKeyValueMap Map.empty IM.empty 0) -- This is useful for keys that are not based on user data, e.g., for -- tracking temporary actions. newDirectKey :: Int -> Key -newDirectKey i = UnsafeMkKey (- abs i) +newDirectKey i = UnsafeMkKey $ negate (abs i + 1) newKey :: (Typeable a, Hashable a, Show a) => a -> Key newKey k = unsafePerformIO $ do From 027d58883c24ba04d41495b3db44272ade112009 Mon Sep 17 00:00:00 2001 From: soulomoon Date: Fri, 8 May 2026 05:58:03 +0800 Subject: [PATCH 29/32] Clarify hls-graph runtime restart keys --- ghcide/src/Development/IDE/Core/Shake.hs | 18 ++-- .../src/Development/IDE/Graph/Database.hs | 12 ++- .../IDE/Graph/Internal/Database.hs | 100 ++++++++++-------- .../Development/IDE/Graph/Internal/Types.hs | 9 +- hls-graph/test/ActionSpec.hs | 56 +++++++++- 5 files changed, 135 insertions(+), 60 deletions(-) diff --git a/ghcide/src/Development/IDE/Core/Shake.hs b/ghcide/src/Development/IDE/Core/Shake.hs index 6efa162c61..5537a1b8b4 100644 --- a/ghcide/src/Development/IDE/Core/Shake.hs +++ b/ghcide/src/Development/IDE/Core/Shake.hs @@ -145,7 +145,8 @@ import Development.IDE.GHC.Orphans () import Development.IDE.Graph hiding (ShakeValue, action) import qualified Development.IDE.Graph as Shake -import Development.IDE.Graph.Database (ShakeDatabase, +import Development.IDE.Graph.Database (RuntimeRestartKeys (..), + ShakeDatabase, instantiateDelayedAction, shakeComputeToPreserve, shakeGetActionQueueLength, @@ -873,17 +874,16 @@ shakeRestart recorder IdeState{..} vfs reason acts ioActionBetweenShakeSession = prepareRuntimeRestart :: Bool -> [Key] -> IO (RuntimeKeysChanged, RuntimeRestartStats) prepareRuntimeRestart optRunSubset keys | optRunSubset = do - (affected, changedKeys, _lookupCount, _) <- - shakeComputeToPreserve shakeDb $ fromListKeySet keys - logErrorAfter 10 $ shakeShutDatabase affected shakeDb + runtimeRestartKeys <- shakeComputeToPreserve shakeDb $ fromListKeySet keys + logErrorAfter 10 $ shakeShutDatabase (restartKillKeys runtimeRestartKeys) shakeDb surviving <- shakePeekAsyncsDelivers shakeDb queueCount <- shakeGetActionQueueLength shakeDb let preserved = fromListKeySet $ map GraphRuntime.deliverKey surviving pure - ( Just (changedKeys, preserved) + ( Just (runtimeRestartKeys, preserved) , RuntimeRestartStats { runtimeDirtyCount = length keys - , runtimeAffectedCount = lengthKeySet affected + , runtimeAffectedCount = lengthKeySet (restartKillKeys runtimeRestartKeys) , runtimePreservedCount = lengthKeySet preserved , runtimeActionQueueCount = queueCount , runtimeSurvivingActions = map GraphRuntime.deliverName surviving @@ -930,7 +930,7 @@ shakeEnqueue ShakeExtras{actionQueue, shakeRecorder} act = do data VFSModified = VFSUnmodified | VFSModified !VFS -type RuntimeKeysChanged = Maybe (([Key], [Key]), KeySet) +type RuntimeKeysChanged = Maybe (RuntimeRestartKeys, KeySet) -- | Set up a new 'ShakeSession' with a set of initial actions -- Will crash if there is an existing 'ShakeSession' running. @@ -961,8 +961,8 @@ newSession recorder ShakeExtras{..} vfsMod shakeDb acts reason runtimeKeysChange workRun restore = withSpan "Shake session" $ \otSpan -> do setTag otSpan "reason" (fromString reason) setTag otSpan "queue" (fromString $ unlines $ map actionName reenqueued) - whenJust runtimeKeysChanged $ \((_, newKeys), _) -> - setTag otSpan "keys" (BS8.pack $ unlines $ map show newKeys) + whenJust runtimeKeysChanged $ \(runtimeRestartKeys, _) -> + setTag otSpan "keys" (BS8.pack $ unlines $ map show $ restartDirtyKeys runtimeRestartKeys) res <- try @SomeException $ restore startDatabase return $ do diff --git a/hls-graph/src/Development/IDE/Graph/Database.hs b/hls-graph/src/Development/IDE/Graph/Database.hs index 702a6eed0a..9dbe51814d 100644 --- a/hls-graph/src/Development/IDE/Graph/Database.hs +++ b/hls-graph/src/Development/IDE/Graph/Database.hs @@ -16,6 +16,7 @@ module Development.IDE.Graph.Database( ,shakeGetBuildEdges, shakeShutDatabase, shakeGetActionQueueLength, + RuntimeRestartKeys(..), shakeComputeToPreserve, -- shakedatabaseRuntimeDep, shakePeekAsyncsDelivers, @@ -91,7 +92,7 @@ unvoid = fmap undefined -- seperate incrementing the step from running the build. -- Also immediately enqueues upsweep actions for the newly dirty keys. shakeRunDatabaseForKeysSep - :: Maybe (([Key],[Key]),KeySet) -- ^ Set of keys changed since last run. 'Nothing' means everything has changed + :: Maybe (RuntimeRestartKeys, KeySet) -- ^ Set of keys changed since last run. 'Nothing' means everything has changed -> ShakeDatabase -> [Action a] -> IO (IO [Either SomeException a]) @@ -126,7 +127,7 @@ mkDelayedAction s p a = do u <- newUnique return $ DelayedAction (newDirectKey $ hashUnique u) s (toEnum (fromEnum p)) a -shakeComputeToPreserve :: ShakeDatabase -> KeySet -> IO (KeySet, ([Key], [Key]), Int, [Key]) +shakeComputeToPreserve :: ShakeDatabase -> KeySet -> IO RuntimeRestartKeys shakeComputeToPreserve (ShakeDatabase _ _ db) ks = atomically (computeToPreserve db ks) shakeRunDatabaseForKeys @@ -146,7 +147,12 @@ shakeRunDatabaseForKeysWithExceptions -> IO [Either SomeException a] shakeRunDatabaseForKeysWithExceptions Nothing sdb as2 = join $ shakeRunDatabaseForKeysSep Nothing sdb as2 shakeRunDatabaseForKeysWithExceptions (Just x) sdb as2 = - let y = fromListKeySet x in join $ shakeRunDatabaseForKeysSep (Just (([], toListKeySet y), y)) sdb as2 + let y = fromListKeySet x + restartKeys = RuntimeRestartKeys + { restartKillKeys = y + , restartDirtyKeys = toListKeySet y + } + in join $ shakeRunDatabaseForKeysSep (Just (restartKeys, y)) sdb as2 shakePeekAsyncsDelivers :: ShakeDatabase -> IO [DeliverStatus] diff --git a/hls-graph/src/Development/IDE/Graph/Internal/Database.hs b/hls-graph/src/Development/IDE/Graph/Internal/Database.hs index 59a385bd36..5318beda6e 100644 --- a/hls-graph/src/Development/IDE/Graph/Internal/Database.hs +++ b/hls-graph/src/Development/IDE/Graph/Internal/Database.hs @@ -9,7 +9,7 @@ {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE TypeFamilies #-} -module Development.IDE.Graph.Internal.Database (compute, newDatabase, incDatabase, build, getDirtySet, getKeysAndVisitAge, AsyncParentKill(..), computeToPreserve, getRunTimeRDeps, spawnAsyncWithDbRegistration) where +module Development.IDE.Graph.Internal.Database (compute, newDatabase, incDatabase, build, getDirtySet, getKeysAndVisitAge, AsyncParentKill(..), RuntimeRestartKeys(..), computeToPreserve, getRunTimeRDeps, spawnAsyncWithDbRegistration) where import Prelude hiding (unzip) @@ -38,9 +38,8 @@ import qualified StmContainers.Map as SMap import System.Time.Extra (duration) import UnliftIO (MVar, atomically, newEmptyMVar, putMVar, - takeMVar) + readMVar) -import qualified Data.List as List import qualified UnliftIO.Exception as UE #if MIN_VERSION_base(4,19,0) @@ -65,12 +64,11 @@ newDatabase dataBaseLogger databaseActionQueue databaseExtra databaseRules = do -- | Increment the step and mark dirty. -- Assumes that the database is not running a build -- only some keys are dirty -incDatabase :: Database -> Maybe (([Key], [Key]), KeySet) -> IO KeySet -incDatabase db (Just ((_oldKeys, newKeys), preserves)) = do +incDatabase :: Database -> Maybe (RuntimeRestartKeys, KeySet) -> IO KeySet +incDatabase db (Just (RuntimeRestartKeys{..}, preserves)) = do atomicallyNamed "incDatabase" $ modifyTVar' (databaseStep db) $ \(Step i) -> Step $ i + 1 - forM_ newKeys $ \newKey -> atomically $ SMap.focus updateDirty newKey (databaseValues db) - -- only upsweep the keys that are not preserved - -- atomically $ writeUpsweepQueue (filter (`notMemberKeySet` preserves) oldkeys ++ newKeys) db + forM_ restartDirtyKeys $ \newKey -> atomically $ SMap.focus updateDirty newKey (databaseValues db) + -- Only re-enqueue actions that were not preserved across the restart. return $ preserves -- all keys are dirty @@ -82,10 +80,29 @@ incDatabase db Nothing = do SMap.focus updateDirty k (databaseValues db) return $ mempty -computeToPreserve :: Database -> KeySet -> STM (KeySet, ([Key], [Key]), Int, [Key]) -computeToPreserve db dirtySet = do - (oldKeys, newKeys, affected) <- transitiveDirtyListBottomUpDiff db (toListKeySet dirtySet) [] - pure (affected, (oldKeys, newKeys), length newKeys, []) +data RuntimeRestartKeys = RuntimeRestartKeys + { restartKillKeys :: !KeySet + -- ^ Keys used to select running runtime actions to stop before the next + -- session starts. This may include rule keys and delayed-action 'DirectKey's. + , restartDirtyKeys :: ![Key] + -- ^ Rule database keys to mark dirty before the next run. In the ghcide + -- restart path this is rule-key-only by construction; the raw hls-graph API + -- does not enforce that invariant by type. + } deriving Show + +-- Note [RuntimeRestartKeys] +-- The restart plan intentionally keeps runtime cancellation separate from rule +-- dirtiness. 'restartKillKeys' is consumed by shutdown and may include direct +-- delayed-action keys. 'restartDirtyKeys' is consumed by the rule database and +-- is expected to contain only rule keys that can be marked dirty. +-- For the ghcide restart path, the initial dirty seeds come from rule keys +-- ('toKey'/'toNoFileKey'), so 'restartDirtyKeys' can use the +-- 'databaseRRuntimeDep' closure directly. Direct/root runtime edges are stored +-- separately in 'databaseRRuntimeDepRoot' by 'insertdatabaseRuntimeDep' and are +-- expanded only for 'restartKillKeys'. The raw hls-graph API does not enforce +-- this seed invariant by type. +computeToPreserve :: Database -> KeySet -> STM RuntimeRestartKeys +computeToPreserve = transitiveDirtyKeysBottomUp updateDirty :: Monad m => Focus.Focus KeyDetails m () updateDirty = Focus.adjust $ \(KeyDetails status rdeps) -> @@ -130,7 +147,7 @@ interpreBuildContinue :: Database -> Key -> (Key, BuildContinue) -> IO (Key, Res interpreBuildContinue _db _pk (_kid, BCStop k v) = return (k, v) interpreBuildContinue db _pk (kid, BCContinue Nothing) = builderOneFinal db emptyStack kid interpreBuildContinue _db _pk (_kid, BCContinue (Just barrier)) = - takeMVar barrier >>= either throwIO pure + readMVar barrier >>= either throwIO pure builderOne :: Key -> Database -> Stack -> Key -> IO (Key, BuildContinue) @@ -324,32 +341,22 @@ getRunTimeRDeps db k = SMap.lookup k (databaseRRuntimeDep db) getDeps :: SMap.Map Key KeySet -> Key -> STM (Maybe KeySet) getDeps m k = SMap.lookup k m --- Edges in the reverse-dependency graph go from a child to its parents. --- We perform a DFS and, after exploring all outgoing edges, cons the node onto --- the accumulator. This yields children-before-parents order directly. - --- the lefts are keys that are no longer affected, we can try to mark them clean --- the rights are new affected keys, we need to mark them dirty -transitiveDirtyListBottomUpDiff :: Database -> [Key] -> [Key] -> STM ([Key], [Key], KeySet) -transitiveDirtyListBottomUpDiff database seeds allOldKeys = do - (newKeys, seen) <- cacheTransitiveDirtyListBottomUpDFSWithRootKey database $ fromListKeySet seeds - let oldKeys = filter (`notMemberKeySet` seen) allOldKeys - return (oldKeys, newKeys, seen) - -cacheTransitiveDirtyListBottomUpDFSWithRootKey :: Database -> KeySet -> STM ([Key], KeySet) -cacheTransitiveDirtyListBottomUpDFSWithRootKey db@Database{..} seeds = do - (newKeys, seen) <- cacheTransitiveDirtyListBottomUpDFS db seeds - -- we should put pump root keys back to seen - -- for each new key, get its root keys and put them back to seen - -- newKeys is for upsweep, databaseRRuntimeDepRoot only add new root keys which is not needed for upsweep - -- but seen is for thread filtering, we need to make sure all root keys are in seen - (_newKeys, newSeen) <- transitiveDirtyListBottomUpDFS databaseRRuntimeDepRoot seen +transitiveDirtyKeysBottomUp :: Database -> KeySet -> STM RuntimeRestartKeys +transitiveDirtyKeysBottomUp db@Database{..} seeds = do + TransitiveDirtyKeys dirtyKeys seen <- cacheTransitiveDirtyListBottomUpDFS db seeds + -- restartDirtyKeys should contain only rule keys. restartKillKeys also needs + -- the root/direct delayed-action keys, so expand through the root dependency + -- map only for the kill set. + TransitiveDirtyKeys _newKeys newSeen <- transitiveDirtyListBottomUpDFS databaseRRuntimeDepRoot seen let rootKey = newKey "root" - return $ (List.delete rootKey newKeys, deleteKeySet rootKey newSeen) + pure RuntimeRestartKeys + { restartDirtyKeys = dirtyKeys + , restartKillKeys = deleteKeySet rootKey newSeen + } -cacheTransitiveDirtyListBottomUpDFS :: Database -> KeySet -> STM ([Key], KeySet) +cacheTransitiveDirtyListBottomUpDFS :: Database -> KeySet -> STM TransitiveDirtyKeys cacheTransitiveDirtyListBottomUpDFS Database{..} seeds = do SMap.lookup seeds databaseTransitiveRRuntimeDepCache >>= \case Just v -> return v @@ -358,22 +365,25 @@ cacheTransitiveDirtyListBottomUpDFS Database{..} seeds = do SMap.insert r seeds databaseTransitiveRRuntimeDepCache return r -transitiveDirtyListBottomUpDFS :: SMap.Map Key KeySet -> KeySet -> STM ([Key], KeySet) +-- Edges in the reverse-dependency graph go from a child to its parents. +-- We perform a DFS and, after exploring all outgoing edges, cons the node onto +-- the accumulator. This yields children-before-parents order directly. +transitiveDirtyListBottomUpDFS :: SMap.Map Key KeySet -> KeySet -> STM TransitiveDirtyKeys transitiveDirtyListBottomUpDFS database seeds = do - let go1 :: Key -> ([Key], KeySet) -> STM ([Key], KeySet) - go1 x acc@(dirties, seen) = do + let go1 :: Key -> TransitiveDirtyKeys -> STM TransitiveDirtyKeys + go1 x acc@TransitiveDirtyKeys{transitiveDirtySet = seen} = do if x `memberKeySet` seen then pure acc else do - let newAcc = (dirties, insertKeySet x seen) + let newAcc = acc{transitiveDirtySet = insertKeySet x seen} mnext <- getDeps database x - (newDirties, newSeen) <- foldrM go1 newAcc (maybe mempty toListKeySet mnext) - return (x:newDirties, newSeen) - -- if it is root key, we do not add it to the dirty list - -- since root key is not up for upsweep - -- but it would be in the seen list, so we would kill dirty root key async + childClosure <- foldrM go1 newAcc (maybe mempty toListKeySet mnext) + return childClosure{transitiveDirtyList = x : transitiveDirtyList childClosure} + -- Root keys are filtered out by 'transitiveDirtyKeysBottomUp' + -- for the dirty list, but kept in the set long enough to find + -- runtime roots that need shutdown. -- traverse all seeds - foldrM go1 ([], mempty) (toListKeySet seeds) + foldrM go1 (TransitiveDirtyKeys [] mempty) (toListKeySet seeds) -- | Original spawnRefresh using the general pattern -- inline diff --git a/hls-graph/src/Development/IDE/Graph/Internal/Types.hs b/hls-graph/src/Development/IDE/Graph/Internal/Types.hs index 1890b38a3d..43236ac1c8 100644 --- a/hls-graph/src/Development/IDE/Graph/Internal/Types.hs +++ b/hls-graph/src/Development/IDE/Graph/Internal/Types.hs @@ -283,7 +283,7 @@ data Database = Database { -- if not in any of the transitive reverse deps of a dirty node, it is clean -- we can skip clean the threads. -- this is update right before we query the database for the key result. - databaseTransitiveRRuntimeDepCache :: SMap.Map KeySet ([Key], KeySet), + databaseTransitiveRRuntimeDepCache :: SMap.Map KeySet TransitiveDirtyKeys, -- ^ this is a cache for transitive reverse deps if we have computed it before -- and the databaseRRuntimeDep did not change since last time -- it is very useful for large projects where many files depend on a few common files @@ -307,6 +307,13 @@ data Database = Database { } +data TransitiveDirtyKeys = TransitiveDirtyKeys + { transitiveDirtyList :: ![Key] + -- ^ Dirty keys in children-before-parents order. + , transitiveDirtySet :: !KeySet + -- ^ Same transitive closure as a set, used for membership/filtering. + } deriving Show + --------------------------------------------------------------------- -- | Remove finished asyncs from 'databaseThreads' (non-blocking). diff --git a/hls-graph/test/ActionSpec.hs b/hls-graph/test/ActionSpec.hs index 079527aa70..31f1f873a8 100644 --- a/hls-graph/test/ActionSpec.hs +++ b/hls-graph/test/ActionSpec.hs @@ -6,20 +6,30 @@ module ActionSpec where import Control.Concurrent (MVar, readMVar) import qualified Control.Concurrent as C import Control.Concurrent.STM +import Control.Exception (SomeException) +import Control.Monad (void) import Control.Monad.IO.Class (MonadIO (..)) import Data.Typeable (Typeable) import Development.IDE.Graph (RuleResult, shakeOptions) import Development.IDE.Graph.Classes (Hashable) -import Development.IDE.Graph.Database (shakeNewDatabase, +import Development.IDE.Graph.Database (RuntimeRestartKeys (..), + mkDelayedAction, + shakeComputeToPreserve, + shakeNewDatabase, shakeRunDatabase, - shakeRunDatabaseForKeys) + shakeRunDatabaseForKeys, + shakeShutDatabase) +import Development.IDE.Graph.Internal.Action (actionCatch, + actionFinally, + pumpActionThreadReRun) import Development.IDE.Graph.Internal.Database (build, incDatabase) import Development.IDE.Graph.Internal.Key import Development.IDE.Graph.Internal.Types import Development.IDE.Graph.Rule import Example import qualified StmContainers.Map as STM +import System.Timeout (timeout) import Test.Hspec @@ -32,6 +42,13 @@ itInThread = it shakeRunDatabaseFromRight :: ShakeDatabase -> [Action a] -> IO [a] shakeRunDatabaseFromRight = shakeRunDatabase + +waitForRuntimeRootDep :: Database -> Key -> Key -> IO () +waitForRuntimeRootDep Database{..} child parent = + atomically $ do + deps <- STM.lookup child databaseRRuntimeDepRoot + check $ maybe False (memberKeySet parent) deps + spec :: Spec spec = do describe "apply1" $ itInThread "Test build update, Buggy dirty mechanism in hls-graph #4237" $ do @@ -105,6 +122,41 @@ spec = do db <- shakeNewDatabase shakeOptions $ addRule $ \(Rule :: Rule ()) _old _mode -> error "boom" let res = shakeRunDatabaseFromRight db $ pure $ apply1 (Rule @()) res `shouldThrow` anyErrorCall + itInThread "restart kills a delayed action parked behind a caught producer failure" $ do + producerStarted <- C.newEmptyMVar + releaseProducer <- C.newEmptyMVar + producerCaught <- C.newEmptyMVar + waiterFinalized <- C.newEmptyMVar + sdb@(ShakeDatabase _ _ theDb) <- shakeNewDatabase shakeOptions $ + addRule $ \(Rule :: Rule Int) _old _mode -> do + liftIO $ void $ C.tryPutMVar producerStarted () + liftIO $ readMVar releaseProducer + error "boom" + producer <- mkDelayedAction "producer" Debug $ + actionCatch @SomeException + (void $ apply1 (Rule @Int)) + (\_ -> liftIO $ void $ C.tryPutMVar producerCaught ()) + waiter <- mkDelayedAction "waiter" Debug $ + actionFinally + (do + liftIO $ readMVar producerStarted + void $ apply1 (Rule @Int)) + (void $ C.tryPutMVar waiterFinalized ()) + + _ <- shakeRunDatabaseForKeys Nothing sdb + [ pumpActionThreadReRun sdb producer + , pumpActionThreadReRun sdb waiter + ] + let dirtyKey = newKey (Rule @Int) + waitForRuntimeRootDep theDb dirtyKey (uniqueID waiter) + C.putMVar releaseProducer () + readMVar producerCaught + C.tryReadMVar waiterFinalized >>= (`shouldBe` Nothing) + + runtimeRestartKeys <- shakeComputeToPreserve sdb (singletonKeySet dirtyKey) + uniqueID waiter `memberKeySet` restartKillKeys runtimeRestartKeys `shouldBe` True + shakeShutDatabase (restartKillKeys runtimeRestartKeys) sdb + timeout 1000000 (readMVar waiterFinalized) >>= (`shouldBe` Just ()) itInThread "computes a rule with branching dependencies does not invoke phantom dependencies #3423" $ do cond <- C.newMVar True count <- C.newMVar 0 From cf73b9996af4b3bf77c82a7e2b1377873191ebc4 Mon Sep 17 00:00:00 2001 From: soulomoon Date: Sat, 27 Sep 2025 22:44:58 +0800 Subject: [PATCH 30/32] Add no-file rule for global kick action and refactor kick usage (cherry picked from commit e4ead4ff6dfc50b810d0c66c4c37e89eba418c1a) --- ghcide/src/Development/IDE/Core/OfInterest.hs | 15 +++++++++++---- ghcide/src/Development/IDE/Core/RuleTypes.hs | 8 ++++++++ ghcide/src/Development/IDE/Main.hs | 4 ++-- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/ghcide/src/Development/IDE/Core/OfInterest.hs b/ghcide/src/Development/IDE/Core/OfInterest.hs index 19e0f40e24..9b213796ac 100644 --- a/ghcide/src/Development/IDE/Core/OfInterest.hs +++ b/ghcide/src/Development/IDE/Core/OfInterest.hs @@ -15,7 +15,7 @@ module Development.IDE.Core.OfInterest( kick, FileOfInterestStatus(..), OfInterestVar(..), scheduleGarbageCollection, - Log(..) + Log(..), doKick ) where import Control.Concurrent.Strict @@ -39,7 +39,7 @@ import Development.IDE.Plugin.Completions.Types import Development.IDE.Types.Exports import Development.IDE.Types.Location import Development.IDE.Types.Options (IdeTesting (..)) -import Development.IDE.Types.Shake (toKey) +import Development.IDE.Types.Shake (toKey, toNoFileKey) import GHC.TypeLits (KnownSymbol) import Ide.Logger (Pretty (pretty), Priority (..), @@ -66,6 +66,10 @@ ofInterestRules :: Recorder (WithPriority Log) -> Rules () ofInterestRules recorder = do addIdeGlobal . OfInterestVar =<< liftIO (newVar HashMap.empty) addIdeGlobal . GarbageCollectVar =<< liftIO (newVar False) + -- A no-file rule to perform the global kick action + defineEarlyCutOffNoFile (cmapWithPrio LogShake recorder) $ \Kick -> do + kick + pure ("", ()) defineEarlyCutoff (cmapWithPrio LogShake recorder) $ RuleNoDiagnostics $ \IsFileOfInterest f -> do alwaysRerun filesOfInterest <- getFilesOfInterestUntracked @@ -113,7 +117,7 @@ addFileOfInterest state f v = do then do logWith (ideLogger state) Debug $ LogSetFilesOfInterest (HashMap.toList files) - return [toKey IsFileOfInterest f] + return [toKey IsFileOfInterest f, toNoFileKey Kick] else return [] deleteFileOfInterest :: IdeState -> NormalizedFilePath -> IO [Key] @@ -122,12 +126,15 @@ deleteFileOfInterest state f = do files <- modifyVar' var $ HashMap.delete f logWith (ideLogger state) Debug $ LogSetFilesOfInterest (HashMap.toList files) - return [toKey IsFileOfInterest f] + return [toKey IsFileOfInterest f, toNoFileKey Kick] scheduleGarbageCollection :: IdeState -> IO () scheduleGarbageCollection state = do GarbageCollectVar var <- getIdeGlobalState state writeVar var True +doKick :: Action () +doKick = useNoFile_ Kick + -- | Typecheck all the files of interest. -- Could be improved kick :: Action () diff --git a/ghcide/src/Development/IDE/Core/RuleTypes.hs b/ghcide/src/Development/IDE/Core/RuleTypes.hs index e10c26e953..7394cce308 100644 --- a/ghcide/src/Development/IDE/Core/RuleTypes.hs +++ b/ghcide/src/Development/IDE/Core/RuleTypes.hs @@ -518,6 +518,14 @@ data IsFileOfInterest = IsFileOfInterest instance Hashable IsFileOfInterest instance NFData IsFileOfInterest +-- | A no-file rule that triggers the IDE "kick" action +data Kick = Kick + deriving (Eq, Show, Generic) +instance Hashable Kick +instance NFData Kick + +type instance RuleResult Kick = () + data GetModSummaryWithoutTimestamps = GetModSummaryWithoutTimestamps deriving (Eq, Show, Generic) instance Hashable GetModSummaryWithoutTimestamps diff --git a/ghcide/src/Development/IDE/Main.hs b/ghcide/src/Development/IDE/Main.hs index feb0050a79..b54f37b32b 100644 --- a/ghcide/src/Development/IDE/Main.hs +++ b/ghcide/src/Development/IDE/Main.hs @@ -40,7 +40,7 @@ import Development.IDE.Core.IdeConfiguration (IdeConfiguration (..) modifyClientSettings, registerIdeConfiguration) import Development.IDE.Core.OfInterest (FileOfInterestStatus (OnDisk), - kick, + doKick, setFilesOfInterest) import Development.IDE.Core.Rules (mainRule) import qualified Development.IDE.Core.Rules as Rules @@ -304,7 +304,7 @@ defaultMain recorder Arguments{..} = withHeapStats (cmapWithPrio LogHeapStats re argsParseConfig = getConfigFromNotification argsHlsPlugins rules = do argsRules - unless argsDisableKick $ action kick + unless argsDisableKick $ action $ doKick pluginRules plugins -- install the main and ghcide-plugin rules -- install the kick action, which triggers a typecheck on every From 089a7815247b55ce739a9c414d1b0c068b9e6b7a Mon Sep 17 00:00:00 2001 From: soulomoon Date: Wed, 1 Oct 2025 21:19:17 +0800 Subject: [PATCH 31/32] fix: adjust doKick behavior to always kick during testing (cherry picked from commit c71527dfee29ac17caebab9292ec4ca988803ca5) --- ghcide/src/Development/IDE/Core/OfInterest.hs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/ghcide/src/Development/IDE/Core/OfInterest.hs b/ghcide/src/Development/IDE/Core/OfInterest.hs index 9b213796ac..79addaa39a 100644 --- a/ghcide/src/Development/IDE/Core/OfInterest.hs +++ b/ghcide/src/Development/IDE/Core/OfInterest.hs @@ -133,7 +133,13 @@ scheduleGarbageCollection state = do writeVar var True doKick :: Action () -doKick = useNoFile_ Kick +doKick = do + ShakeExtras{ideTesting = IdeTesting testing} <- getShakeExtras + -- only kick always if testing, otherwise we rely on the kick rule + if testing + then kick + else void $ useNoFile Kick + -- | Typecheck all the files of interest. -- Could be improved From f8a925036e34f72544d2c82339a7f1e61a7c0322 Mon Sep 17 00:00:00 2001 From: soulomoon Date: Tue, 12 May 2026 21:12:11 +0800 Subject: [PATCH 32/32] Add typing burst benchmark coverage --- bench/config.yaml | 6 +++ ghcide-bench/README.md | 2 + ghcide-bench/src/Experiments.hs | 87 ++++++++++++++++++++++++++++++--- ghcide-bench/test/Main.hs | 2 + 4 files changed, 91 insertions(+), 6 deletions(-) diff --git a/bench/config.yaml b/bench/config.yaml index 8ea3e45086..b1218976ce 100644 --- a/bench/config.yaml +++ b/bench/config.yaml @@ -95,16 +95,22 @@ experiments: - "edit" - "hover" - "semanticTokens" + - "semanticTokens after typing burst" - "hover after edit" + - "hover after typing burst" # - "hover after cradle edit" - "getDefinition" - "getDefinition after edit" + - "getDefinition after typing burst" - "completions" - "completions after edit" + - "completions after typing burst" - "code actions" - "code actions after edit" + - "code actions after typing burst" - "code actions after cradle edit" - "documentSymbols after edit" + - "documentSymbols after typing burst" - "hole fit suggestions" - "eval execute single-line code lens" - "eval execute multi-line code lens" diff --git a/ghcide-bench/README.md b/ghcide-bench/README.md index f815635157..baea428d93 100644 --- a/ghcide-bench/README.md +++ b/ghcide-bench/README.md @@ -47,6 +47,8 @@ Currently the following experiments are defined: - *code actions*: makes an edit that breaks typechecking and asks for code actions - *hole fit suggestions*: measures the performance of hole fits - *X after edit*: combines the *edit* and X experiments +- *X after typing burst*: makes five hygienic edits with a 250 ms delay between + edits, then waits for the X response - *X after cradle edit*: combines the X experiments with an edit to the `hie.yaml` file One can define additional experiments easily, for e.g. formatting, code lenses, renames, etc. diff --git a/ghcide-bench/src/Experiments.hs b/ghcide-bench/src/Experiments.hs index 968add9252..29b74716f9 100644 --- a/ghcide-bench/src/Experiments.hs +++ b/ghcide-bench/src/Experiments.hs @@ -83,6 +83,12 @@ headerEdit = , _text = "-- header comment \n" } +typingBurstEditCount :: Int +typingBurstEditCount = 5 + +typingBurstDelay :: Seconds +typingBurstDelay = 0.25 + data DocumentPositions = DocumentPositions { -- | A position that can be used to generate non null goto-def and completion responses identifierP :: Maybe Position, @@ -100,6 +106,14 @@ allWithIdentifierPos f docs = case applicableDocs of where applicableDocs = filter (isJust . identifierP) docs +applyTypingBurst :: [DocumentPositions] -> Session () +applyTypingBurst docs = + forM_ [1..typingBurstEditCount] $ \n -> do + forM_ docs $ \DocumentPositions{..} -> + changeDoc doc [charEdit stringLiteralP] + when (n < typingBurstEditCount) $ + liftIO $ sleep typingBurstDelay + experiments :: HasConfig => [Bench] experiments = [ @@ -115,6 +129,15 @@ experiments = Nothing -> return False return $ and r, --------------------------------------------------------------------------------------- + bench "semanticTokens after typing burst" $ \docs -> do + applyTypingBurst docs + r <- forM docs $ \DocumentPositions{..} -> do + tks <- getSemanticTokens doc + case tks ^? LSP._L of + Just _ -> return True + Nothing -> return False + return $ and r, + --------------------------------------------------------------------------------------- bench "hover" $ allWithIdentifierPos $ \DocumentPositions{..} -> isJust <$> getHover doc (fromJust identifierP), --------------------------------------------------------------------------------------- @@ -124,6 +147,11 @@ experiments = flip allWithIdentifierPos docs $ \DocumentPositions{..} -> isJust <$> getHover doc (fromJust identifierP), --------------------------------------------------------------------------------------- + bench "hover after typing burst" $ \docs -> do + applyTypingBurst docs + flip allWithIdentifierPos docs $ \DocumentPositions{..} -> + isJust <$> getHover doc (fromJust identifierP), + --------------------------------------------------------------------------------------- bench "hover after cradle edit" (\docs -> do @@ -158,10 +186,15 @@ experiments = hasDefinitions <$> getDefinitions doc (fromJust identifierP), --------------------------------------------------------------------------------------- bench "getDefinition after edit" $ \docs -> do - forM_ docs $ \DocumentPositions{..} -> - changeDoc doc [charEdit stringLiteralP] - flip allWithIdentifierPos docs $ \DocumentPositions{..} -> - hasDefinitions <$> getDefinitions doc (fromJust identifierP), + forM_ docs $ \DocumentPositions{..} -> + changeDoc doc [charEdit stringLiteralP] + flip allWithIdentifierPos docs $ \DocumentPositions{..} -> + hasDefinitions <$> getDefinitions doc (fromJust identifierP), + --------------------------------------------------------------------------------------- + bench "getDefinition after typing burst" $ \docs -> do + applyTypingBurst docs + flip allWithIdentifierPos docs $ \DocumentPositions{..} -> + hasDefinitions <$> getDefinitions doc (fromJust identifierP), --------------------------------------------------------------------------------------- bench "documentSymbols" $ allM $ \DocumentPositions{..} -> do fmap (either (not . null) (not . null)) . getDocumentSymbols $ doc, @@ -172,6 +205,11 @@ experiments = flip allM docs $ \DocumentPositions{..} -> either (not . null) (not . null) <$> getDocumentSymbols doc, --------------------------------------------------------------------------------------- + bench "documentSymbols after typing burst" $ \docs -> do + applyTypingBurst docs + flip allM docs $ \DocumentPositions{..} -> + either (not . null) (not . null) <$> getDocumentSymbols doc, + --------------------------------------------------------------------------------------- bench "completions" $ \docs -> do flip allWithIdentifierPos docs $ \DocumentPositions{..} -> not . null <$> getCompletions doc (fromJust identifierP), @@ -182,6 +220,11 @@ experiments = flip allWithIdentifierPos docs $ \DocumentPositions{..} -> not . null <$> getCompletions doc (fromJust identifierP), --------------------------------------------------------------------------------------- + bench "completions after typing burst" $ \docs -> do + applyTypingBurst docs + flip allWithIdentifierPos docs $ \DocumentPositions{..} -> + not . null <$> getCompletions doc (fromJust identifierP), + --------------------------------------------------------------------------------------- bench "code actions" ( \docs -> do @@ -206,6 +249,17 @@ experiments = getCodeActions doc (Range p p)) ), --------------------------------------------------------------------------------------- + bench + "code actions after typing burst" + ( \docs -> do + unless (any (isJust . identifierP) docs) $ + error "None of the example modules is suitable for this experiment" + applyTypingBurst docs + not . null . catMaybes <$> forM docs (\DocumentPositions{..} -> do + forM identifierP $ \p -> + getCodeActions doc (Range p p)) + ), + --------------------------------------------------------------------------------------- bench "code actions after cradle edit" ( \docs -> do @@ -342,7 +396,28 @@ examplesPath :: FilePath examplesPath = "bench/example" defConfig :: Config -Success defConfig = execParserPure defaultPrefs (info configP fullDesc) [] +defConfig = Config + { verbosity = Normal + , shakeProfiling = Nothing + , otMemoryProfiling = Nothing + , outputCSV = "results.csv" + , buildTool = Cabal + , ghcideOptions = [] + , matches = [] + , repetitions = Nothing + , ghcide = "ghcide" + , timeoutLsp = 60 + , example = Example + { exampleName = "Cabal" + , exampleDetails = ExampleHackage ExamplePackage + { packageName = "Cabal" + , packageVersion = makeVersion [3,16,1,0] + } + , exampleModules = ["src/Distribution/Simple.hs"] + , exampleExtraArgs = [] + } + , lspConfig = False + } quiet, verbose :: Config -> Bool verbose = (== All) . verbosity @@ -383,7 +458,7 @@ configP = packageP = ExamplePackage <$> strOption (long "example-package-name" <> value "Cabal") - <*> option versionP (long "example-package-version" <> value (makeVersion [3,6,0,0])) + <*> option versionP (long "example-package-version" <> value (makeVersion [3,16,1,0])) pathOrScriptP = ExamplePath <$> strOption (long "example-path") <|> ExampleScript <$> strOption (long "example-script") <*> many (strOption (long "example-script-args" <> help "arguments for the example generation script")) diff --git a/ghcide-bench/test/Main.hs b/ghcide-bench/test/Main.hs index a58016ab2b..281e66fd34 100644 --- a/ghcide-bench/test/Main.hs +++ b/ghcide-bench/test/Main.hs @@ -36,6 +36,8 @@ benchmarkTests = | e <- Bench.experiments , Bench.name e /= "edit" -- the edit experiment does not ever fail , Bench.name e /= "hole fit suggestions" -- is too slow! + , not ("semanticTokens" `isInfixOf` Bench.name e) -- ghcide does not load the semantic-tokens plugin + , not ("code actions" `isInfixOf` Bench.name e) -- ghcide does not load the code-action plugin -- the cradle experiments are way too slow , not ("cradle" `isInfixOf` Bench.name e) ]