on master help sign in
Infrastructure for on-disk incremental builds (#189)

This is intended for the DAML CLI compiler which is also built upon `ghcide`. For now, we have no intention of using this in the DAML IDE or in ghcide but that might change in the future.

commit7a215d22ef22c447a050fbcc63900e9b5405e901 authorMoritz Kiefer <moritz.kiefer@purelyfunctional.org> dateThu 7 Nov 2019, 17:13 (7 years ago) parent981cd8b
9 files changed+159−24
src/Development/IDE/Core/Compile.hs+1 src/Development/IDE/Core/FileStore.hs+25−1 src/Development/IDE/Core/OfInterest.hs+2 src/Development/IDE/Core/RuleTypes.hs+11 src/Development/IDE/Core/Rules.hs+15−6 src/Development/IDE/Core/Service.hs+5−3 src/Development/IDE/Core/Shake.hs+94−13 src/Development/IDE/Types/Location.hs+2−1 src/Development/IDE/Types/Options.hs+4
src/Development/IDE/Core/Compile.hs+1
@@ -14,6 +14,7 @@ module Development.IDE.Core.Compile
1414 , typecheckModule
1515 , computePackageDeps
1616 , addRelativeImport
17+ , mkTcModuleResult
1718 ) where
1819
1920import Development.IDE.Core.RuleTypes
src/Development/IDE/Core/FileStore.hs+25−1
@@ -11,8 +11,12 @@ module Development.IDE.Core.FileStore(
1111 VFSHandle,
1212 makeVFSHandle,
1313 makeLSPVFSHandle,
14+ getSourceFingerprint
1415 ) where
1516
17+import Foreign.Ptr
18+import Foreign.ForeignPtr
19+import Fingerprint
1620import StringBuffer
1721import Development.IDE.GHC.Orphans()
1822import Development.IDE.GHC.Util
@@ -41,7 +45,6 @@ import Data.Time
4145import Foreign.C.String
4246import Foreign.C.Types
4347import Foreign.Marshal (alloca)
44−import Foreign.Ptr
4548import Foreign.Storable
4649import qualified System.Posix.Error as Posix
4750#endif
@@ -89,17 +92,34 @@ type instance RuleResult GetFileContents = (FileVersion, Maybe StringBuffer)
8992-- | Does the file exist.
9093type instance RuleResult GetFileExists = Bool
9194
95+type instance RuleResult FingerprintSource = Fingerprint
9296
9397data GetFileExists = GetFileExists
9498 deriving (Eq, Show, Generic)
9599instance Hashable GetFileExists
96100instance NFData GetFileExists
101+instance Binary GetFileExists
97102
98103data GetFileContents = GetFileContents
99104 deriving (Eq, Show, Generic)
100105instance Hashable GetFileContents
101106instance NFData GetFileContents
107+instance Binary GetFileContents
102108
109+data FingerprintSource = FingerprintSource
110+ deriving (Eq, Show, Generic)
111+instance Hashable FingerprintSource
112+instance NFData FingerprintSource
113+instance Binary FingerprintSource
114+
115+fingerprintSourceRule :: Rules ()
116+fingerprintSourceRule =
117+ define $ \FingerprintSource file -> do
118+ (_, mbContent) <- getFileContents file
119+ content <- liftIO $ maybe (hGetStringBuffer $ fromNormalizedFilePath file) pure mbContent
120+ fingerprint <- liftIO $ fpStringBuffer content
121+ pure ([], Just fingerprint)
122+ where fpStringBuffer (StringBuffer buf len cur) = withForeignPtr buf $ \ptr -> fingerprintData (ptr `plusPtr` cur) len
103123
104124getFileExistsRule :: VFSHandle -> Rules ()
105125getFileExistsRule vfs =
@@ -152,6 +172,9 @@ getModificationTimeRule vfs =
152172foreign import ccall "getmodtime" c_getModTime :: CString -> Ptr CTime -> Ptr CLong -> IO Int
153173#endif
154174
175+getSourceFingerprint :: NormalizedFilePath -> Action Fingerprint
176+getSourceFingerprint = use_ FingerprintSource
177+
155178getFileContentsRule :: VFSHandle -> Rules ()
156179getFileContentsRule vfs =
157180 define $ \GetFileContents file -> do
@@ -188,6 +211,7 @@ fileStoreRules vfs = do
188211 getModificationTimeRule vfs
189212 getFileContentsRule vfs
190213 getFileExistsRule vfs
214+ fingerprintSourceRule
191215
192216
193217-- | Notify the compiler service that a particular file has been modified.
src/Development/IDE/Core/OfInterest.hs+2
@@ -14,6 +14,7 @@ module Development.IDE.Core.OfInterest(
1414 ) where
1515
1616import Control.Concurrent.Extra
17+import Data.Binary
1718import Data.Hashable
1819import Control.DeepSeq
1920import GHC.Generics
@@ -44,6 +45,7 @@ data GetFilesOfInterest = GetFilesOfInterest
4445 deriving (Eq, Show, Typeable, Generic)
4546instance Hashable GetFilesOfInterest
4647instance NFData GetFilesOfInterest
48+instance Binary GetFilesOfInterest
4749
4850
4951ofInterestRules :: Rules ()
src/Development/IDE/Core/RuleTypes.hs+11
@@ -12,6 +12,7 @@ module Development.IDE.Core.RuleTypes(
1212 ) where
1313
1414import Control.DeepSeq
15+import Data.Binary
1516import Development.IDE.Import.DependencyInformation
1617import Development.IDE.GHC.Util
1718import Development.IDE.Types.Location
@@ -86,46 +87,55 @@ data GetParsedModule = GetParsedModule
8687 deriving (Eq, Show, Typeable, Generic)
8788instance Hashable GetParsedModule
8889instance NFData GetParsedModule
90+instance Binary GetParsedModule
8991
9092data GetLocatedImports = GetLocatedImports
9193 deriving (Eq, Show, Typeable, Generic)
9294instance Hashable GetLocatedImports
9395instance NFData GetLocatedImports
96+instance Binary GetLocatedImports
9497
9598data GetDependencyInformation = GetDependencyInformation
9699 deriving (Eq, Show, Typeable, Generic)
97100instance Hashable GetDependencyInformation
98101instance NFData GetDependencyInformation
102+instance Binary GetDependencyInformation
99103
100104data ReportImportCycles = ReportImportCycles
101105 deriving (Eq, Show, Typeable, Generic)
102106instance Hashable ReportImportCycles
103107instance NFData ReportImportCycles
108+instance Binary ReportImportCycles
104109
105110data GetDependencies = GetDependencies
106111 deriving (Eq, Show, Typeable, Generic)
107112instance Hashable GetDependencies
108113instance NFData GetDependencies
114+instance Binary GetDependencies
109115
110116data TypeCheck = TypeCheck
111117 deriving (Eq, Show, Typeable, Generic)
112118instance Hashable TypeCheck
113119instance NFData TypeCheck
120+instance Binary TypeCheck
114121
115122data GetSpanInfo = GetSpanInfo
116123 deriving (Eq, Show, Typeable, Generic)
117124instance Hashable GetSpanInfo
118125instance NFData GetSpanInfo
126+instance Binary GetSpanInfo
119127
120128data GenerateCore = GenerateCore
121129 deriving (Eq, Show, Typeable, Generic)
122130instance Hashable GenerateCore
123131instance NFData GenerateCore
132+instance Binary GenerateCore
124133
125134data GhcSession = GhcSession
126135 deriving (Eq, Show, Typeable, Generic)
127136instance Hashable GhcSession
128137instance NFData GhcSession
138+instance Binary GhcSession
129139
130140-- Note that we embed the filepath here instead of using the filepath associated with Shake keys.
131141-- Otherwise we will garbage collect the result since files in package dependencies will not be declared reachable.
@@ -133,3 +143,4 @@ data GetHieFile = GetHieFile FilePath
133143 deriving (Eq, Show, Typeable, Generic)
134144instance Hashable GetHieFile
135145instance NFData GetHieFile
146+instance Binary GetHieFile
src/Development/IDE/Core/Rules.hs+15−6
@@ -26,6 +26,9 @@ module Development.IDE.Core.Rules(
2626 generateCore,
2727 ) where
2828
29+import Fingerprint
30+
31+import Data.Binary
2932import Control.Monad
3033import Control.Monad.Trans.Class
3134import Control.Monad.Trans.Maybe
@@ -141,11 +144,13 @@ priorityFilesOfInterest = Priority (-2)
141144
142145getParsedModuleRule :: Rules ()
143146getParsedModuleRule =
144− define $ \GetParsedModule file -> do
147+ defineEarlyCutoff $ \GetParsedModule file -> do
145148 (_, contents) <- getFileContents file
146149 packageState <- hscEnv <$> use_ GhcSession file
147150 opt <- getIdeOptions
148− liftIO $ parseModule opt packageState (fromNormalizedFilePath file) contents
151+ r <- liftIO $ parseModule opt packageState (fromNormalizedFilePath file) contents
152+ mbFingerprint <- traverse (const $ getSourceFingerprint file) (optShakeFiles opt)
153+ pure (fingerprintToBS <$> mbFingerprint, r)
149154
150155getLocatedImportsRule :: Rules ()
151156getLocatedImportsRule =
@@ -252,11 +257,13 @@ reportImportCyclesRule =
252257-- NOTE: result does not include the argument file.
253258getDependenciesRule :: Rules ()
254259getDependenciesRule =
255− define $ \GetDependencies file -> do
260+ defineEarlyCutoff $ \GetDependencies file -> do
256261 depInfo@DependencyInformation{..} <- use_ GetDependencyInformation file
257262 let allFiles = reachableModules depInfo
258263 _ <- uses_ ReportImportCycles allFiles
259− return ([], transitiveDeps depInfo file)
264+ opts <- getIdeOptions
265+ let mbFingerprints = map (fingerprintString . fromNormalizedFilePath) allFiles <$ optShakeFiles opts
266+ return (fingerprintToBS . fingerprintFingerprints <$> mbFingerprints, ([], transitiveDeps depInfo file))
260267
261268-- Source SpanInfo is used by AtPoint and Goto Definition.
262269getSpanInfoRule :: Rules ()
@@ -301,6 +308,7 @@ type instance RuleResult GhcSessionIO = GhcSessionFun
301308data GhcSessionIO = GhcSessionIO deriving (Eq, Show, Typeable, Generic)
302309instance Hashable GhcSessionIO
303310instance NFData GhcSessionIO
311+instance Binary GhcSessionIO
304312
305313newtype GhcSessionFun = GhcSessionFun (FilePath -> Action HscEnvEq)
306314instance Show GhcSessionFun where show _ = "GhcSessionFun"
@@ -312,10 +320,11 @@ loadGhcSession = do
312320 defineNoFile $ \GhcSessionIO -> do
313321 opts <- getIdeOptions
314322 liftIO $ GhcSessionFun <$> optGhcSession opts
315− define $ \GhcSession file -> do
323+ defineEarlyCutoff $ \GhcSession file -> do
316324 GhcSessionFun fun <- useNoFile_ GhcSessionIO
317325 val <- fun $ fromNormalizedFilePath file
318− return ([], Just val)
326+ opts <- getIdeOptions
327+ return ("" <$ optShakeFiles opts, ([], Just val))
319328
320329
321330getHieFileRule :: Rules ()
src/Development/IDE/Core/Service.hs+5−3
@@ -20,6 +20,7 @@ module Development.IDE.Core.Service(
2020
2121import Control.Concurrent.Extra
2222import Control.Concurrent.Async
23+import Data.Maybe
2324import Development.IDE.Types.Options (IdeOptions(..))
2425import Control.Monad
2526import Development.IDE.Core.FileStore
@@ -55,9 +56,10 @@ initialise mainRule getLspId toDiags logger options vfs =
5556 logger
5657 (optShakeProfiling options)
5758 (optReportProgress options)
58− (shakeOptions { shakeThreads = optThreads options
59− , shakeFiles = "/dev/null"
60− }) $ do
59+ shakeOptions
60+ { shakeThreads = optThreads options
61+ , shakeFiles = fromMaybe "/dev/null" (optShakeFiles options)
62+ } $ do
6163 addIdeGlobal $ GlobalIdeOptions options
6264 fileStoreRules vfs
6365 ofInterestRules
src/Development/IDE/Core/Shake.hs+94−13
@@ -26,7 +26,7 @@ module Development.IDE.Core.Shake(
2626 shakeProfile,
2727 use, useWithStale, useNoFile, uses, usesWithStale,
2828 use_, useNoFile_, uses_,
29− define, defineEarlyCutoff,
29+ define, defineEarlyCutoff, defineOnDisk, needOnDisk, needOnDisks, fingerprintToBS,
3030 getDiagnostics, unsafeClearDiagnostics,
3131 IsIdeGlobal, addIdeGlobal, getIdeGlobalState, getIdeGlobalAction,
3232 garbageCollect,
@@ -36,10 +36,11 @@ module Development.IDE.Core.Shake(
3636 actionLogger,
3737 FileVersion(..),
3838 Priority(..),
39− updatePositionMapping
39+ updatePositionMapping,
40+ OnDiskRule(..)
4041 ) where
4142
42−import Development.Shake hiding (ShakeValue)
43+import Development.Shake hiding (ShakeValue, doesFileExist)
4344import Development.Shake.Database
4445import Development.Shake.Classes
4546import Development.Shake.Rule
@@ -47,6 +48,7 @@ import qualified Data.HashMap.Strict as HMap
4748import qualified Data.Map.Strict as Map
4849import qualified Data.Map.Merge.Strict as Map
4950import qualified Data.ByteString.Char8 as BS
51+import qualified Data.ByteString.Internal as BS
5052import Data.Dynamic
5153import Data.Maybe
5254import Data.Map.Strict (Map)
@@ -58,6 +60,9 @@ import Data.Unique
5860import Development.IDE.Core.Debouncer
5961import Development.IDE.Core.PositionMapping
6062import Development.IDE.Types.Logger hiding (Priority)
63+import Foreign.Ptr
64+import Foreign.Storable
65+import GHC.Fingerprint
6166import Language.Haskell.LSP.Diagnostics
6267import qualified Data.SortedList as SL
6368import Development.IDE.Types.Diagnostics
@@ -202,11 +207,7 @@ mappingForVersion allMappings file ver =
202207
203208type IdeRule k v =
204209 ( Shake.RuleResult k ~ v
205− , Show k
206− , Typeable k
207− , NFData k
208− , Hashable k
209− , Eq k
210+ , Shake.ShakeValue k
210211 , Show v
211212 , Typeable v
212213 , NFData v
@@ -458,12 +459,9 @@ isBadDependency x
458459 | otherwise = False
459460
460461newtype Q k = Q (k, NormalizedFilePath)
461− deriving (Eq,Hashable,NFData)
462+ deriving (Eq,Hashable,NFData, Generic)
462463
463−-- Using Database we don't need Binary instances for keys
464−instance Binary (Q k) where
465− put _ = return ()
466− get = fail "Binary.get not defined for type Development.IDE.Core.Shake.Q"
464+instance Binary k => Binary (Q k)
467465
468466instance Show k => Show (Q k) where
469467 show (Q (k, file)) = show k ++ "; " ++ fromNormalizedFilePath file
@@ -539,6 +537,88 @@ defineEarlyCutoff op = addBuiltinRule noLint noIdentity $ \(Q (key, file)) (old
539537 (encodeShakeValue bs) $
540538 A res bs
541539
540+
541+-- | Rule type, input file
542+data QDisk k = QDisk k NormalizedFilePath
543+ deriving (Eq, Generic)
544+
545+instance Hashable k => Hashable (QDisk k)
546+
547+instance NFData k => NFData (QDisk k)
548+
549+instance Binary k => Binary (QDisk k)
550+
551+instance Show k => Show (QDisk k) where
552+ show (QDisk k file) =
553+ show k ++ "; " ++ fromNormalizedFilePath file
554+
555+type instance RuleResult (QDisk k) = Bool
556+
557+data OnDiskRule = OnDiskRule
558+ { getHash :: Action BS.ByteString
559+ -- This is used to figure out if the state on disk corresponds to the state in the Shake
560+ -- database and we can therefore avoid rerunning. Often this can just be the file hash but
561+ -- in some cases we can be more aggressive, e.g., for GHC interface files this can be the ABI hash which
562+ -- is more stable than the hash of the interface file.
563+ -- An empty bytestring indicates that the state on disk is invalid, e.g., files are missing.
564+ -- We do not use a Maybe since we have to deal with encoding things into a ByteString anyway in the Shake DB.
565+ , runRule :: Action (IdeResult BS.ByteString)
566+ -- The actual rule code which produces the new hash (or Nothing if the rule failed) and the diagnostics.
567+ }
568+
569+-- This is used by the DAML compiler for incremental builds. Right now this is not used by
570+-- ghcide itself but that might change in the future.
571+-- The reason why this code lives in ghcide and in particular in this module is that it depends quite heavily on
572+-- the internals of this module that we do not want to expose.
573+defineOnDisk
574+ :: (Shake.ShakeValue k, RuleResult k ~ ())
575+ => (k -> NormalizedFilePath -> OnDiskRule)
576+ -> Rules ()
577+defineOnDisk act = addBuiltinRule noLint noIdentity $
578+ \(QDisk key file) (mbOld :: Maybe BS.ByteString) mode -> do
579+ extras <- getShakeExtras
580+ let OnDiskRule{..} = act key file
581+ let validateHash h
582+ | BS.null h = Nothing
583+ | otherwise = Just h
584+ let runAct = actionCatch runRule $
585+ \(e :: SomeException) -> pure ([ideErrorText file $ T.pack $ displayException e | not $ isBadDependency e], Nothing)
586+ case mbOld of
587+ Nothing -> do
588+ (diags, mbHash) <- runAct
589+ updateFileDiagnostics file (Key key) extras $ map snd diags
590+ pure $ RunResult ChangedRecomputeDiff (fromMaybe "" mbHash) (isJust mbHash)
591+ Just old -> do
592+ current <- validateHash <$> (actionCatch getHash $ \(_ :: SomeException) -> pure "")
593+ if mode == RunDependenciesSame && Just old == current && not (BS.null old)
594+ then
595+ -- None of our dependencies changed, we’ve had a successful run before and
596+ -- the state on disk matches the state in the Shake database.
597+ pure $ RunResult ChangedNothing (fromMaybe "" current) (isJust current)
598+ else do
599+ (diags, mbHash) <- runAct
600+ updateFileDiagnostics file (Key key) extras $ map snd diags
601+ let change
602+ | mbHash == Just old = ChangedRecomputeSame
603+ | otherwise = ChangedRecomputeDiff
604+ pure $ RunResult change (fromMaybe "" mbHash) (isJust mbHash)
605+
606+fingerprintToBS :: Fingerprint -> BS.ByteString
607+fingerprintToBS (Fingerprint a b) = BS.unsafeCreate 8 $ \ptr -> do
608+ ptr <- pure $ castPtr ptr
609+ pokeElemOff ptr 0 a
610+ pokeElemOff ptr 1 b
611+
612+needOnDisk :: (Shake.ShakeValue k, RuleResult k ~ ()) => k -> NormalizedFilePath -> Action ()
613+needOnDisk k file = do
614+ successfull <- apply1 (QDisk k file)
615+ liftIO $ unless successfull $ throwIO BadDependency
616+
617+needOnDisks :: (Shake.ShakeValue k, RuleResult k ~ ()) => k -> [NormalizedFilePath] -> Action ()
618+needOnDisks k files = do
619+ successfulls <- apply $ map (QDisk k) files
620+ liftIO $ unless (and successfulls) $ throwIO BadDependency
621+
542622toShakeValue :: (BS.ByteString -> ShakeValue) -> Maybe BS.ByteString -> ShakeValue
543623toShakeValue = maybe ShakeNoCutoff
544624
@@ -626,6 +706,7 @@ data GetModificationTime = GetModificationTime
626706 deriving (Eq, Show, Generic)
627707instance Hashable GetModificationTime
628708instance NFData GetModificationTime
709+instance Binary GetModificationTime
629710
630711-- | Get the modification time of a file.
631712type instance RuleResult GetModificationTime = FileVersion
src/Development/IDE/Types/Location.hs+2−1
@@ -25,6 +25,7 @@ module Development.IDE.Types.Location
2525
2626import Language.Haskell.LSP.Types (Location(..), Range(..), Position(..))
2727import Control.DeepSeq
28+import Data.Binary
2829import Data.Maybe as Maybe
2930import Data.Hashable
3031import Data.String
@@ -42,7 +43,7 @@ import Language.Haskell.LSP.Types as LSP (
4243
4344-- | Newtype wrapper around FilePath that always has normalized slashes.
4445newtype NormalizedFilePath = NormalizedFilePath FilePath
45− deriving (Eq, Ord, Show, Hashable, NFData)
46+ deriving (Eq, Ord, Show, Hashable, NFData, Binary)
4647
4748instance IsString NormalizedFilePath where
4849 fromString = toNormalizedFilePath
src/Development/IDE/Types/Options.hs+4
@@ -38,6 +38,9 @@ data IdeOptions = IdeOptions
3838
3939 , optThreads :: Int
4040 -- ^ Number of threads to use. Use 0 for number of threads on the machine.
41+ , optShakeFiles :: Maybe FilePath
42+ -- ^ Directory where the shake database should be stored. For ghcide this is always set to `Nothing` for now
43+ -- meaning we keep everything in memory but the daml CLI compiler uses this for incremental builds.
4144 , optShakeProfiling :: Maybe FilePath
4245 -- ^ Set to 'Just' to create a directory of profiling reports.
4346 , optReportProgress :: IdeReportProgress
@@ -77,6 +80,7 @@ defaultIdeOptions session = IdeOptions
7780 ,optExtensions = ["hs", "lhs"]
7881 ,optPkgLocationOpts = defaultIdePkgLocationOptions
7982 ,optThreads = 0
83+ ,optShakeFiles = Nothing
8084 ,optShakeProfiling = Nothing
8185 ,optReportProgress = IdeReportProgress False
8286 ,optLanguageSyntax = "haskell"