Skip to content

Commit 6d9b6cd

Browse files
committed
avoid spawning threads for simple lookups
1 parent c3c06bc commit 6d9b6cd

File tree

1 file changed

+86
-51
lines changed
  • hls-graph/src/Development/IDE/Graph/Internal

1 file changed

+86
-51
lines changed

hls-graph/src/Development/IDE/Graph/Internal/Database.hs

Lines changed: 86 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,24 @@
11
-- We deliberately want to ensure the function we add to the rule database
22
-- has the constraints we need on it when we get it out.
33
{-# OPTIONS_GHC -Wno-redundant-constraints #-}
4-
{-# LANGUAGE LambdaCase #-}
5-
{-# LANGUAGE NamedFieldPuns #-}
6-
{-# LANGUAGE RecordWildCards #-}
7-
{-# LANGUAGE ScopedTypeVariables #-}
8-
{-# LANGUAGE TupleSections #-}
9-
{-# LANGUAGE TypeFamilies #-}
10-
{-# LANGUAGE ViewPatterns #-}
4+
{-# LANGUAGE DeriveFunctor #-}
5+
{-# LANGUAGE DerivingStrategies #-}
6+
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
7+
{-# LANGUAGE LambdaCase #-}
8+
{-# LANGUAGE NamedFieldPuns #-}
9+
{-# LANGUAGE RecordWildCards #-}
10+
{-# LANGUAGE ScopedTypeVariables #-}
11+
{-# LANGUAGE TupleSections #-}
12+
{-# LANGUAGE TypeFamilies #-}
13+
{-# LANGUAGE ViewPatterns #-}
1114

12-
module Development.IDE.Graph.Internal.Database where
15+
module Development.IDE.Graph.Internal.Database (newDatabase, incDatabase, build) where
1316

1417
import Control.Concurrent.Async
1518
import Control.Concurrent.Extra
1619
import Control.Exception
1720
import Control.Monad
21+
import Control.Monad.IO.Class (MonadIO (liftIO))
1822
import Control.Monad.Trans.Class (lift)
1923
import Control.Monad.Trans.Reader
2024
import qualified Control.Monad.Trans.State.Strict as State
@@ -76,81 +80,84 @@ build
7680
:: forall key value . (Shake.RuleResult key ~ value, Typeable key, Show key, Hashable key, Eq key, Typeable value)
7781
=> Database -> [key] -> IO ([Id], [value])
7882
build db keys = do
79-
(ids, vs) <- fmap unzip $ builder db $ map (Right . Key) keys
83+
(ids, vs) <- runAIO $ fmap unzip $ either return liftIO =<< builder db (map (Right . Key) keys)
8084
pure (ids, map (asV . resultValue) vs)
8185
where
8286
asV :: Value -> value
8387
asV (Value x) = unwrapDynamic x
8488

85-
-- | Build a list of keys in parallel
89+
-- | Build a list of keys and return their results.
90+
-- If none of the keys are dirty, we can return the results immediately.
91+
-- Otherwise, a blocking computation is returned *which must be evaluated asynchronously* to avoid deadlock.
8692
builder
87-
:: Database -> [Either Id Key] -> IO [(Id, Result)]
93+
:: Database -> [Either Id Key] -> AIO (Either [(Id, Result)] (IO [(Id, Result)]))
8894
builder db@Database{..} keys = do
89-
-- Async things that I own and am responsible for killing
90-
ownedAsync <- newIORef []
91-
flip onException (cleanupAsync ownedAsync) $ do
92-
9395
-- Things that I need to force before my results are ready
94-
toForce <- newIORef []
96+
toForce <- liftIO $ newIORef []
9597

96-
results <- withLock databaseLock $ do
97-
forM keys $ \idKey -> do
98+
results <- withLockAIO databaseLock $ do
99+
flip traverse keys $ \idKey -> do
98100
-- Resolve the id
99101
id <- case idKey of
100102
Left id -> pure id
101103
Right key -> do
102-
ids <- readIORef databaseIds
104+
ids <- liftIO $ readIORef databaseIds
103105
case Intern.lookup key ids of
104106
Just v -> pure v
105107
Nothing -> do
106108
(ids, id) <- pure $ Intern.add key ids
107-
writeIORef' databaseIds ids
109+
liftIO $ writeIORef' databaseIds ids
108110
return id
109111

110112
-- Spawn the id if needed
111-
status <- Ids.lookup databaseValues id
113+
status <- liftIO $ Ids.lookup databaseValues id
112114
val <- case fromMaybe (fromRight undefined idKey, Dirty Nothing) status of
113115
(_, Clean r) -> pure r
114116
(_, Running act _) -> do
115-
-- we promise to force everything in todo before reading the results
116-
-- so the following unsafePerformIO isn't actually unsafe
117117
let (force, val) = splitIO act
118-
modifyIORef toForce (force:)
118+
liftIO $ modifyIORef toForce (force:)
119119
pure val
120120
(key, Dirty s) -> do
121-
-- Important we don't lose any Async things we create
122-
act <- uninterruptibleMask $ \restore -> do
123-
-- the child actions should always be spawned unmasked
124-
-- or they can't be killed
125-
async <- async $ restore $ check db key id s
126-
modifyIORef ownedAsync (async:)
127-
pure $ wait async
128-
Ids.insert databaseValues id (key, Running act s)
121+
act <- join <$> unliftAIO (refresh db key id s)
122+
liftIO $ Ids.insert databaseValues id (key, Running act s)
129123
let (force, val) = splitIO act
130-
modifyIORef toForce (force:)
124+
liftIO $ modifyIORef toForce (force:)
131125
pure val
132-
133126
pure (id, val)
134127

135-
sequence_ =<< readIORef toForce
136-
pure results
128+
toForceList <- liftIO $ readIORef toForce
129+
case toForceList of
130+
[] -> return $ Left results
131+
_ -> return $ Right $ do
132+
sequence_ toForceList
133+
pure results
137134

138-
cleanupAsync :: IORef [Async a] -> IO ()
139-
cleanupAsync ref = mapConcurrently_ uninterruptibleCancel =<< readIORef ref
140-
141-
-- | Check if we need to run the database.
142-
check :: Database -> Key -> Id -> Maybe Result -> IO Result
143-
check db key id result@(Just me@Result{resultDeps=Just deps}) = do
135+
-- | Refresh a key:
136+
-- * If no dirty dependencies and we have evaluated the key previously, then we refresh it in the current thread.
137+
-- This assumes that the implementation will be a lookup
138+
-- * Otherwise, we spawn a new thread to refresh the dirty deps (if any) and the key itself
139+
refresh :: Database -> Key -> Id -> Maybe Result -> AIO (IO Result)
140+
refresh db key id result@(Just me@Result{resultDeps=Just deps}) = do
144141
res <- builder db $ map Left deps
145-
let dirty = any (\(_,dep) -> resultBuilt me < resultChanged dep) res
146-
let mode = if dirty then Shake.RunDependenciesChanged else Shake.RunDependenciesSame
147-
spawn db key id mode result
148-
check db key id result = spawn db key id Shake.RunDependenciesChanged result
142+
case res of
143+
Left res ->
144+
if isDirty res
145+
then asyncWithCleanUp $ liftIO $ compute db key id Shake.RunDependenciesChanged result
146+
else pure $ compute db key id Shake.RunDependenciesSame result
147+
Right iores -> asyncWithCleanUp $ liftIO $ do
148+
res <- iores
149+
let mode = if isDirty res then Shake.RunDependenciesChanged else Shake.RunDependenciesSame
150+
compute db key id mode result
151+
where
152+
isDirty = any (\(_,dep) -> resultBuilt me < resultChanged dep)
153+
154+
refresh db key id result =
155+
asyncWithCleanUp $ liftIO $ compute db key id Shake.RunDependenciesChanged result
149156

150157

151-
-- | Spawn a new computation to run the action.
152-
spawn :: Database -> Key -> Id -> Shake.RunMode -> Maybe Result -> IO Result
153-
spawn db@Database{..} key id mode result = do
158+
-- | Compute a key.
159+
compute :: Database -> Key -> Id -> Shake.RunMode -> Maybe Result -> IO Result
160+
compute db@Database{..} key id mode result = do
154161
let act = runRule databaseRules key (fmap resultData result) mode
155162
deps <- newIORef $ Just []
156163
(execution, Shake.RunResult{..}) <-
@@ -218,6 +225,34 @@ transitiveDirtySet database = flip State.execStateT Set.empty . traverse_ loop
218225
next <- lift $ getReverseDependencies database x
219226
traverse_ loop (maybe mempty Set.toList next)
220227

228+
-- | IO extended to track created asyncs to clean them up when the thread is killed,
229+
-- generalizing 'withAsync'
230+
newtype AIO a = AIO { unAIO :: ReaderT (IORef [Async ()]) IO a }
231+
deriving newtype (Applicative, Functor, Monad, MonadIO)
221232

222-
idFromInt :: Set.Key -> Id
223-
idFromInt = id
233+
runAIO :: AIO a -> IO a
234+
runAIO (AIO act) = do
235+
asyncs <- newIORef []
236+
runReaderT act asyncs `onException` cleanupAsync asyncs
237+
238+
asyncWithCleanUp :: AIO a -> AIO (IO a)
239+
asyncWithCleanUp act = do
240+
st <- AIO ask
241+
io <- unliftAIO act
242+
liftIO $ uninterruptibleMask $ \restore -> do
243+
a <- async $ restore io
244+
modifyIORef st (void a :)
245+
return $ wait a
246+
247+
withLockAIO :: Lock -> AIO a -> AIO a
248+
withLockAIO lock act = do
249+
io <- unliftAIO act
250+
liftIO $ withLock lock io
251+
252+
unliftAIO :: AIO a -> AIO (IO a)
253+
unliftAIO act = do
254+
st <- AIO ask
255+
return $ runReaderT (unAIO act) st
256+
257+
cleanupAsync :: IORef [Async a] -> IO ()
258+
cleanupAsync ref = mapConcurrently_ uninterruptibleCancel =<< readIORef ref

0 commit comments

Comments
 (0)