Skip to content

Rewrite hls-graph to not use the Shake code #1759

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions hls-graph/hls-graph.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,20 @@ library
Development.IDE.Graph.Internal.Action
Development.IDE.Graph.Internal.Options
Development.IDE.Graph.Internal.Rules
Development.IDE.Graph.Internal.Database
Development.IDE.Graph.Internal.Ids
Development.IDE.Graph.Internal.Intern
Development.IDE.Graph.Internal.Types

hs-source-dirs: src
build-depends:
, async
, base >=4.12 && <5
, bytestring
, extra
, primitive
, shake >= 0.19.4
, transformers
, unordered-containers

ghc-options:
Expand Down
9 changes: 5 additions & 4 deletions hls-graph/src/Development/IDE/Graph.hs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ module Development.IDE.Graph(
reschedule,
) where

import Development.IDE.Graph.Internal.Action
import Development.IDE.Graph.Internal.Options
import Development.IDE.Graph.Internal.Rules
import qualified Development.Shake as Shake
import qualified Development.Shake as Shake
import Development.IDE.Graph.Internal.Action
import Development.IDE.Graph.Internal.Options
import Development.IDE.Graph.Internal.Rules
import Development.IDE.Graph.Internal.Types
51 changes: 41 additions & 10 deletions hls-graph/src/Development/IDE/Graph/Database.hs
Original file line number Diff line number Diff line change
@@ -1,18 +1,49 @@

module Development.IDE.Graph.Database(
Shake.ShakeDatabase,
ShakeDatabase,
shakeOpenDatabase,
shakeRunDatabase,
Shake.shakeProfileDatabase,
shakeProfileDatabase,
) where

import Development.IDE.Graph.Internal.Action
import Development.IDE.Graph.Internal.Options
import Development.IDE.Graph.Internal.Rules
import qualified Development.Shake.Database as Shake
import Development.IDE.Graph.Internal.Action
import Development.IDE.Graph.Internal.Options
import Development.IDE.Graph.Internal.Rules
import Development.IDE.Graph.Internal.Types
import Data.Maybe
import Data.Dynamic
import Development.IDE.Graph.Internal.Database
import GHC.Conc
import Control.Concurrent.Extra

shakeOpenDatabase :: ShakeOptions -> Rules () -> IO (IO Shake.ShakeDatabase, IO ())
shakeOpenDatabase a b = Shake.shakeOpenDatabase (fromShakeOptions a) (fromRules b)

shakeRunDatabase :: Shake.ShakeDatabase -> [Action a] -> IO ([a], [IO ()])
shakeRunDatabase a b = Shake.shakeRunDatabase a (map fromAction b)
data ShakeDatabase = ShakeDatabase !Int !Int [Action ()] Database

-- Placeholder to be the 'extra' if the user doesn't set it
data NonExportedType = NonExportedType

shakeOpenDatabase :: ShakeOptions -> Rules () -> IO (IO ShakeDatabase, IO ())
shakeOpenDatabase opts rules = pure (shakeNewDatabase opts rules, pure ())

shakeNewDatabase :: ShakeOptions -> Rules () -> IO ShakeDatabase
shakeNewDatabase opts rules = do
let extra = fromMaybe (toDyn NonExportedType) $ shakeExtra opts
(theRules, actions) <- runRules extra rules
db <- newDatabase extra theRules
let threads = shakeThreads opts
threads <- if threads /= 0 then pure threads else getNumProcessors
pure $ ShakeDatabase threads (length actions) actions db

shakeRunDatabase :: ShakeDatabase -> [Action a] -> IO ([a], [IO ()])
shakeRunDatabase (ShakeDatabase threads lenAs1 as1 db) as2 = withNumCapabilities threads $ do
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
shakeRunDatabase (ShakeDatabase threads lenAs1 as1 db) as2 = withNumCapabilities threads $ do
-- TODO move capability bits out of hls-graph
shakeRunDatabase (ShakeDatabase threads lenAs1 as1 db) as2 = withNumCapabilities threads $ do

incDatabase db
as <- fmap (drop lenAs1) $ runActions db $ map unvoid as1 ++ as2
return (as, [])

-- Only valid if we never pull on the results, which we don't
unvoid :: Functor m => m () -> m a
unvoid = fmap undefined

-- Noop
shakeProfileDatabase :: ShakeDatabase -> FilePath -> IO ()
shakeProfileDatabase _ file = writeFile file ""
96 changes: 79 additions & 17 deletions hls-graph/src/Development/IDE/Graph/Internal/Action.hs
Original file line number Diff line number Diff line change
@@ -1,38 +1,100 @@
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE ScopedTypeVariables #-}

module Development.IDE.Graph.Internal.Action where

import Control.Exception
import Control.Monad.Fail
import Control.Monad.IO.Class
import qualified Development.Shake as Shake
import Development.Shake.Classes
import qualified Development.Shake.Rule as Shake
import qualified Development.Shake as Shake
import Development.Shake.Classes
import Control.Exception
import Control.Concurrent.Async
import System.Exit
import Control.Monad.IO.Class
import Control.Monad.Trans.Reader
import Data.IORef
import Development.IDE.Graph.Internal.Database
import Development.IDE.Graph.Internal.Types
import Control.Monad.Extra
import Control.Monad.Trans.Class
import Control.Concurrent.Async

newtype Action a = Action {fromAction :: Shake.Action a}
deriving (Monad, Applicative, Functor, MonadIO, MonadFail)

alwaysRerun :: Action ()
alwaysRerun = Action Shake.alwaysRerun
alwaysRerun = do
ref <- Action $ asks actionDeps
liftIO $ writeIORef ref Nothing

-- No-op for now
reschedule :: Double -> Action ()
reschedule = Action . Shake.reschedule
reschedule _ = pure ()

parallel :: [Action a] -> Action [a]
parallel = Action . Shake.parallel . map fromAction
parallel [] = pure []
parallel [x] = fmap (:[]) x
parallel xs = do
a <- Action ask
deps <- liftIO $ readIORef $ actionDeps a
case deps of
Nothing ->
-- if we are already in the rerun mode, nothing we do is going to impact our state
liftIO $ mapConcurrently (ignoreState a) xs
Just deps -> do
(newDeps, res) <- liftIO $ unzip <$> mapConcurrently (usingState a) xs
liftIO $ writeIORef (actionDeps a) $ (deps ++) <$> concatMapM id newDeps
pure res
where
ignoreState a x = do
ref <- newIORef Nothing
runReaderT (fromAction x) a{actionDeps=ref}

usingState a x = do
ref <- newIORef $ Just []
res <- runReaderT (fromAction x) a{actionDeps=ref}
deps <- readIORef ref
pure (deps, res)

isAsyncException :: SomeException -> Bool
isAsyncException e
| Just (_ :: AsyncCancelled) <- fromException e = True
| Just (_ :: AsyncException) <- fromException e = True
| Just (_ :: ExitCode) <- fromException e = True
| otherwise = False


actionCatch :: Exception e => Action a -> (e -> Action a) -> Action a
actionCatch a b = Action $ Shake.actionCatch (fromAction a) (fromAction . b)
actionCatch a b = do
v <- Action ask
Action $ lift $ catchJust f (runReaderT (fromAction a) v) (\x -> runReaderT (fromAction (b x)) v)
where
-- Catch only catches exceptions that were caused by this code, not those that
-- are a result of program termination
f e | isAsyncException e = Nothing
| otherwise = fromException e

actionBracket :: IO a -> (a -> IO b) -> (a -> Action c) -> Action c
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
actionBracket :: IO a -> (a -> IO b) -> (a -> Action c) -> Action c
-- TODO Generalize type signatures and implement exception classes
actionBracket :: IO a -> (a -> IO b) -> (a -> Action c) -> Action c

actionBracket a b c = Action $ Shake.actionBracket a b (fromAction . c)
actionBracket a b c = do
v <- Action ask
Action $ lift $ bracket a b (\x -> runReaderT (fromAction (c x)) v)

actionFinally :: Action a -> IO b -> Action a
actionFinally a b = Action $ Shake.actionFinally (fromAction a) b
actionFinally a b = do
v <- Action ask
Action $ lift $ finally (runReaderT (fromAction a) v) b

apply1 :: (Shake.RuleResult key ~ value, Shake.ShakeValue key, Typeable value) => key -> Action value
apply1 = Action . Shake.apply1
apply1 k = head <$> apply [k]

apply :: (Shake.RuleResult key ~ value, Shake.ShakeValue key, Typeable value) => [key] -> Action [value]
apply = Action . Shake.apply
apply ks = do
db <- Action $ asks actionDatabase
(is, vs) <- liftIO $ build db ks
ref <- Action $ asks actionDeps
deps <- liftIO $ readIORef ref
whenJust deps $ \deps ->
liftIO $ writeIORef ref $ Just $ is ++ deps
pure vs

runActions :: Database -> [Action a] -> IO [a]
runActions db xs = do
deps <- newIORef Nothing
runReaderT (fromAction $ parallel xs) $ SAction db deps
144 changes: 144 additions & 0 deletions hls-graph/src/Development/IDE/Graph/Internal/Database.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
-- We deliberately want to ensure the function we add to the rule database
-- has the constraints we need on it when we get it out.
{-# OPTIONS_GHC -Wno-redundant-constraints #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE NamedFieldPuns #-}

module Development.IDE.Graph.Internal.Database where

import Development.IDE.Graph.Internal.Intern
import Development.IDE.Graph.Internal.Types
import Data.Dynamic
import qualified Development.IDE.Graph.Internal.Intern as Intern
import qualified Development.IDE.Graph.Internal.Ids as Ids
import Control.Concurrent.Extra
import Data.IORef.Extra
import Control.Monad
import Development.Shake.Classes
import qualified Development.Shake as Shake
import Data.Maybe
import Control.Concurrent.Async
import System.IO.Unsafe
import Development.IDE.Graph.Internal.Rules
import qualified Development.Shake.Rule as Shake
import Control.Exception
import Control.Monad.Trans.Reader
import Data.Tuple.Extra
import Data.Either

newDatabase :: Dynamic -> TheRules -> IO Database
newDatabase databaseExtra databaseRules = do
databaseStep <- newIORef $ Step 0
databaseLock <- newLock
databaseIds <- newIORef Intern.empty
databaseValues <- Ids.empty
pure Database{..}

incDatabase :: Database -> IO ()
incDatabase db = do
modifyIORef' (databaseStep db) $ \(Step i) -> Step $ i + 1
Ids.forMutate (databaseValues db) $ second $ \case
Clean x -> Dirty (Just x)
Dirty x -> Dirty x
Running _ x -> Dirty x


build
:: forall key value . (Shake.RuleResult key ~ value, Typeable key, Show key, Hashable key, Eq key, Typeable value)
=> Database -> [key] -> IO ([Id], [value])
build db keys = do
(ids, vs) <- fmap unzip $ builder db $ map (Right . Key) keys
pure (ids, map (asV . resultValue) vs)
where
asV :: Value -> value
asV (Value x) = unwrapDynamic x

builder
:: Database -> [Either Id Key] -> IO [(Id, Result)]
builder db@Database{..} keys = do
-- Async things that I own and am responsible for killing
ownedAsync <- newIORef []
flip onException (cleanupAsync ownedAsync) $ do

-- Things that I need to force before my results are ready
toForce <- newIORef []

results <- withLock databaseLock $ do
forM keys $ \idKey -> do
id <- case idKey of
Left id -> pure id
Right key -> do
ids <- readIORef databaseIds
case Intern.lookup key ids of
Just v -> pure v
Nothing -> do
(ids, id) <- pure $ Intern.add key ids
writeIORef' databaseIds ids
return id

status <- Ids.lookup databaseValues id
val <- case fromMaybe (fromRight undefined idKey, Dirty Nothing) status of
(_, Clean r) -> pure r
(_, Running act _) -> do
-- we promise to force everything in todo before reading the results
-- so the following unsafePerformIO isn't actually unsafe
let (force, val) = splitIO act
modifyIORef toForce (force:)
pure val
(key, Dirty s) -> do
-- Important we don't lose any Async things we create
act <- uninterruptibleMask $ \restore -> do
-- the child actions should always be spawned unmasked
-- or they can't be killed
async <- async $ restore $ check db key id s
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On IRC, we were discussing if it was possible to make all the book-keeping contention free, by eliminating the locks and running in STM (and using a concurrent map like stm-containers?).

This bit seems to be the problematic part, the rest of the computation seem like it can easily be moved into STM. But it might just be possible to make something work using judicious use of unsafePerformIO, unsafeIOToSTM and the proper care. Do you think this would be possible?

modifyIORef ownedAsync (async:)
pure $ wait async
Ids.insert databaseValues id (key, Running act s)
let (force, val) = splitIO act
modifyIORef toForce (force:)
pure val

pure (id, val)

sequence_ =<< readIORef toForce
pure results

cleanupAsync :: IORef [Async a] -> IO ()
cleanupAsync ref = mapConcurrently_ uninterruptibleCancel =<< readIORef ref


-- Check if we need to run the database.
check :: Database -> Key -> Id -> Maybe Result -> IO Result
check db key id result@(Just me@Result{resultDeps=Just deps}) = do
res <- builder db $ map Left deps
let dirty = all (\(_,dep) -> resultBuilt me < resultChanged dep) res
let mode = if dirty then Shake.RunDependenciesChanged else Shake.RunDependenciesSame
spawn db key id mode result
check db key id result = spawn db key id Shake.RunDependenciesChanged result


-- Spawn a new computation to run the action.
spawn :: Database -> Key -> Id -> Shake.RunMode -> Maybe Result -> IO Result
spawn db@Database{..} key id mode result = do
let act = runRule databaseRules key (fmap resultData result) mode
deps <- newIORef $ Just []
Shake.RunResult{..} <- runReaderT (fromAction act) $ SAction db deps
built <- readIORef databaseStep
deps <- readIORef deps
let changed = if runChanged == Shake.ChangedRecomputeDiff then built else maybe built resultChanged result
let res = Result runValue built changed deps runStore
withLock databaseLock $
Ids.insert databaseValues id (key, Clean res)
pure res

data Box a = Box {fromBox :: a}

splitIO :: IO a -> (IO (), a)
splitIO act = do
let act2 = Box <$> act
let res = unsafePerformIO act2
(void $ evaluate res, fromBox res)
Comment on lines +142 to +144
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not familiar with this trick. What is the Box for?

Loading