Skip to content

Commit 6524122

Browse files
Expose runtime metrics via EKG (#2267)
* Collect metrics and expose an EKG server * register gc metrics locally * getDatabaseKeys * fixups * Abstract monitoring and put EKG behind a Cabal flag * Add CI for the new flag * fix double ifdef * link to GHC docs for eventlogs * log when stopping the EKG server * Allow CPP in modules * Add doc comments * Apply suggestions from code review Co-authored-by: Michael Peyton Jones <[email protected]> * Compat. with ghc 9.2 * use an ekg-json snapshot that preserves compat. with ghc 8.x * confine CPP to the EKG module Co-authored-by: Michael Peyton Jones <[email protected]>
1 parent b6d87c5 commit 6524122

File tree

15 files changed

+256
-54
lines changed

15 files changed

+256
-54
lines changed

.github/workflows/flags.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ jobs:
6565
run: cabal v2-build hls-graph --flags="embed-files stm-stats"
6666

6767
- name: Build `ghcide` with flags
68-
run: cabal v2-build ghcide --flags="ghc-patched-unboxed-bytecode test-exe executable bench-exe"
68+
run: cabal v2-build ghcide --flags="ghc-patched-unboxed-bytecode test-exe executable bench-exe ekg"
6969

7070
# we have to clean up warnings for 9.0 and 9.2 before enable -WAll
7171
- if: matrix.ghc != '9.0.2' && matrix.ghc != '9.2.2'

cabal.project

+19
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,15 @@ constraints:
5353
ghc-lib-parser-ex -auto,
5454
stylish-haskell +ghc-lib
5555

56+
-- This is benign and won't affect our ability to release to Hackage,
57+
-- because we only depend on `ekg-json` when a non-default flag
58+
-- is turned on.
59+
source-repository-package
60+
type:git
61+
location: https://github.com/pepeiborra/ekg-json
62+
tag: 7a0af7a8fd38045fd15fb13445bdcc7085325460
63+
-- https://github.com/tibbe/ekg-json/pull/12
64+
5665
allow-newer:
5766
-- ghc-9.2
5867
----------
@@ -70,3 +79,13 @@ allow-newer:
7079
-- for shake-bench
7180
Chart:lens,
7281
Chart-diagrams:lens,
82+
83+
-- for ekg
84+
ekg-core:base,
85+
ekg-core:ghc-prim,
86+
ekg-wai:base,
87+
ekg-wai:time,
88+
89+
-- for shake-bench
90+
Chart-diagrams:diagrams-core,
91+
SVGFonts:diagrams-core

docs/contributing/contributing.md

+29
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,35 @@ If you don't want to use [nix](https://nixos.org/guides/install-nix.html), you c
225225

226226
See the [tutorial](./plugin-tutorial.md) on writing a plugin in HLS.
227227

228+
## Measuring, benchmarking and tracing
229+
230+
### Metrics
231+
232+
When ghcide is built with the `ekg` flag, HLS opens a metrics server on port 8999 exposing GC and ghcide metrics. The ghcide metrics currently exposed are:
233+
234+
- `ghcide.values_count`- count of build results in the store
235+
- `ghcide.database_count` - count of build keys in the store (these two would be the same in the absence of GC)
236+
- `ghcide.build_count` - build count. A key is GC'ed if it is dirty and older than 100 builds
237+
- `ghcide.dirty_keys_count` - non transitive count of dirty build keys
238+
- `ghcide.indexing_pending_count` - count of items in the indexing queue
239+
- `ghcide.exports_map_count` - count of identifiers in the exports map.
240+
241+
### Benchmarks
242+
243+
If you are touching performance sensitive code, take the time to run a differential
244+
benchmark between HEAD and master using the benchHist script. This assumes that
245+
"master" points to the upstream master.
246+
247+
Run the benchmarks with `cabal bench ghcide`.
248+
249+
It should take around 25 minutes and the results will be stored in the `ghcide/bench-results` folder. To interpret the results, see the comments in the `ghcide/bench/hist/Main.hs` module.
250+
251+
More details in [bench/README](../../ghcide/bench/README.md)
252+
253+
### Tracing
254+
255+
HLS records opentelemetry [eventlog traces](https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/runtime_control.html#rts-eventlog) via [opentelemetry](https://hackage.haskell.org/package/opentelemetry). To generate the traces, build with `-eventlog` and run with `+RTS -l`. To visualize the traces, install [Tracy](https://github.com/wolfpld/tracy) and use [eventlog-to-tracy](https://hackage.haskell.org/package/opentelemetry-extra) to open the generated eventlog.
256+
228257
## Adding support for a new editor
229258

230259
Adding support for new editors is fairly easy if the editor already has good support for generic LSP-based extensions.

ghcide/.hlint.yaml

-1
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,6 @@
122122
- Development.IDE.Plugin.Completions
123123
- Development.IDE.Plugin.Completions.Logic
124124
- Development.IDE.Types.Location
125-
- Main
126125

127126
- flags:
128127
- default: false

ghcide/exe/Arguments.hs

+2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ data Arguments = Arguments
1919
,argsVerbose :: Bool
2020
,argsCommand :: Command
2121
,argsConservativeChangeTracking :: Bool
22+
,argsMonitoringPort :: Int
2223
}
2324

2425
getArguments :: IdePlugins IdeState -> IO Arguments
@@ -40,6 +41,7 @@ arguments plugins = Arguments
4041
<*> switch (short 'd' <> long "verbose" <> help "Include internal events in logging output")
4142
<*> (commandP plugins <|> lspCommand <|> checkCommand)
4243
<*> switch (long "conservative-change-tracking" <> help "disable reactive change tracking (for testing/debugging)")
44+
<*> option auto (long "monitoring-port" <> metavar "PORT" <> value 8999 <> showDefault <> help "Port to use for EKG monitoring (if the binary is built with EKG)")
4345
where
4446
checkCommand = Check <$> many (argument str (metavar "FILES/DIRS..."))
4547
lspCommand = LSP <$ flag' True (long "lsp" <> help "Start talking to an LSP client")

ghcide/exe/Main.hs

+3-1
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,13 @@ import Development.IDE.Core.Rules (mainRule)
1919
import qualified Development.IDE.Core.Rules as Rules
2020
import Development.IDE.Core.Tracing (withTelemetryLogger)
2121
import qualified Development.IDE.Main as IDEMain
22+
import qualified Development.IDE.Monitoring.OpenTelemetry as OpenTelemetry
23+
import qualified Development.IDE.Monitoring.EKG as EKG
2224
import qualified Development.IDE.Plugin.HLS.GhcIde as GhcIde
2325
import Development.IDE.Types.Logger (Logger (Logger),
2426
LoggingColumn (DataColumn, PriorityColumn),
2527
Pretty (pretty),
2628
Priority (Debug, Info, Error),
27-
Recorder (Recorder),
2829
WithPriority (WithPriority, priority),
2930
cfilter, cmapWithPrio,
3031
makeDefaultStderrRecorder, layoutPretty, renderStrict, defaultLayoutOptions)
@@ -142,4 +143,5 @@ main = withTelemetryLogger $ \telemetryLogger -> do
142143
, optCheckProject = pure $ checkProject config
143144
, optRunSubset = not argsConservativeChangeTracking
144145
}
146+
, IDEMain.argsMonitoring = OpenTelemetry.monitoring <> EKG.monitoring logger argsMonitoringPort
145147
}

ghcide/ghcide.cabal

+19
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ flag ghc-patched-unboxed-bytecode
3030
default: False
3131
manual: True
3232

33+
flag ekg
34+
description: Enable EKG monitoring of the build graph and other metrics on port 8999
35+
default: False
36+
manual: True
37+
3338
library
3439
default-language: Haskell2010
3540
build-depends:
@@ -182,6 +187,7 @@ library
182187
Development.IDE.GHC.Util
183188
Development.IDE.Import.DependencyInformation
184189
Development.IDE.Import.FindImports
190+
Development.IDE.Monitoring.EKG
185191
Development.IDE.LSP.HoverDefinition
186192
Development.IDE.LSP.LanguageServer
187193
Development.IDE.LSP.Outline
@@ -198,6 +204,8 @@ library
198204
Development.IDE.Types.KnownTargets
199205
Development.IDE.Types.Location
200206
Development.IDE.Types.Logger
207+
Development.IDE.Types.Monitoring
208+
Development.IDE.Monitoring.OpenTelemetry
201209
Development.IDE.Types.Options
202210
Development.IDE.Types.Shake
203211
Development.IDE.Plugin
@@ -236,6 +244,12 @@ library
236244
exposed-modules:
237245
Development.IDE.GHC.Compat.CPP
238246

247+
if flag(ekg)
248+
build-depends:
249+
ekg-wai,
250+
ekg-core,
251+
cpp-options: -DMONITORING_EKG
252+
239253
flag test-exe
240254
description: Build the ghcide-test-preprocessor executable
241255
default: True
@@ -356,6 +370,11 @@ executable ghcide
356370

357371
if !flag(executable)
358372
buildable: False
373+
if flag(ekg)
374+
build-depends:
375+
ekg-wai,
376+
ekg-core,
377+
cpp-options: -DMONITORING_EKG
359378

360379
test-suite ghcide-tests
361380
type: exitcode-stdio-1.0

ghcide/src/Development/IDE/Core/Service.hs

+22-20
Original file line numberDiff line numberDiff line change
@@ -18,30 +18,30 @@ module Development.IDE.Core.Service(
1818
Log(..),
1919
) where
2020

21-
import Control.Applicative ((<|>))
21+
import Control.Applicative ((<|>))
2222
import Development.IDE.Core.Debouncer
23-
import Development.IDE.Core.FileExists (fileExistsRules)
24-
import Development.IDE.Core.OfInterest hiding (Log, LogShake)
23+
import Development.IDE.Core.FileExists (fileExistsRules)
24+
import Development.IDE.Core.OfInterest hiding (Log, LogShake)
2525
import Development.IDE.Graph
26-
import Development.IDE.Types.Logger as Logger (Logger,
27-
Pretty (pretty),
28-
Priority (Debug),
29-
Recorder,
30-
WithPriority,
31-
cmapWithPrio)
32-
import Development.IDE.Types.Options (IdeOptions (..))
26+
import Development.IDE.Types.Logger as Logger (Logger,
27+
Pretty (pretty),
28+
Priority (Debug),
29+
Recorder,
30+
WithPriority,
31+
cmapWithPrio)
32+
import Development.IDE.Types.Options (IdeOptions (..))
3333
import Ide.Plugin.Config
34-
import qualified Language.LSP.Server as LSP
35-
import qualified Language.LSP.Types as LSP
34+
import qualified Language.LSP.Server as LSP
35+
import qualified Language.LSP.Types as LSP
3636

3737
import Control.Monad
38-
import qualified Development.IDE.Core.FileExists as FileExists
39-
import qualified Development.IDE.Core.OfInterest as OfInterest
40-
import Development.IDE.Core.Shake hiding (Log)
41-
import qualified Development.IDE.Core.Shake as Shake
42-
import Development.IDE.Types.Shake (WithHieDb)
43-
import System.Environment (lookupEnv)
44-
38+
import qualified Development.IDE.Core.FileExists as FileExists
39+
import qualified Development.IDE.Core.OfInterest as OfInterest
40+
import Development.IDE.Core.Shake hiding (Log)
41+
import qualified Development.IDE.Core.Shake as Shake
42+
import Development.IDE.Types.Monitoring (Monitoring)
43+
import Development.IDE.Types.Shake (WithHieDb)
44+
import System.Environment (lookupEnv)
4545

4646
data Log
4747
= LogShake Shake.Log
@@ -68,8 +68,9 @@ initialise :: Recorder (WithPriority Log)
6868
-> IdeOptions
6969
-> WithHieDb
7070
-> IndexQueue
71+
-> Monitoring
7172
-> IO IdeState
72-
initialise recorder defaultConfig mainRule lspEnv logger debouncer options withHieDb hiedbChan = do
73+
initialise recorder defaultConfig mainRule lspEnv logger debouncer options withHieDb hiedbChan metrics = do
7374
shakeProfiling <- do
7475
let fromConf = optShakeProfiling options
7576
fromEnv <- lookupEnv "GHCIDE_BUILD_PROFILING"
@@ -86,6 +87,7 @@ initialise recorder defaultConfig mainRule lspEnv logger debouncer options withH
8687
withHieDb
8788
hiedbChan
8889
(optShakeOptions options)
90+
metrics
8991
$ do
9092
addIdeGlobal $ GlobalIdeOptions options
9193
ofInterestRules (cmapWithPrio LogOfInterest recorder)

ghcide/src/Development/IDE/Core/Shake.hs

+34-24
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,6 @@ import Data.Typeable
118118
import Data.Unique
119119
import Data.Vector (Vector)
120120
import qualified Data.Vector as Vector
121-
import Debug.Trace.Flags (userTracingEnabled)
122121
import Development.IDE.Core.Debouncer
123122
import Development.IDE.Core.FileUtils (getModTime)
124123
import Development.IDE.Core.PositionMapping
@@ -136,6 +135,7 @@ import Development.IDE.Graph hiding (ShakeValue)
136135
import qualified Development.IDE.Graph as Shake
137136
import Development.IDE.Graph.Database (ShakeDatabase,
138137
shakeGetBuildStep,
138+
shakeGetDatabaseKeys,
139139
shakeNewDatabase,
140140
shakeProfileDatabase,
141141
shakeRunDatabaseForKeys)
@@ -152,6 +152,8 @@ import Development.IDE.Types.Options
152152
import Development.IDE.Types.Shake
153153
import qualified Focus
154154
import GHC.Fingerprint
155+
import Language.LSP.Types.Capabilities
156+
import OpenTelemetry.Eventlog
155157
import HieDb.Types
156158
import Ide.Plugin.Config
157159
import qualified Ide.PluginUtils as HLS
@@ -160,14 +162,13 @@ import Language.LSP.Diagnostics
160162
import qualified Language.LSP.Server as LSP
161163
import Language.LSP.Types
162164
import qualified Language.LSP.Types as LSP
163-
import Language.LSP.Types.Capabilities
164165
import Language.LSP.VFS
165166
import qualified "list-t" ListT
166-
import OpenTelemetry.Eventlog
167167
import qualified StmContainers.Map as STM
168168
import System.FilePath hiding (makeRelative)
169169
import System.IO.Unsafe (unsafePerformIO)
170170
import System.Time.Extra
171+
import Development.IDE.Types.Monitoring (Monitoring(..))
171172

172173
data Log
173174
= LogCreateHieDbExportsMapStart
@@ -388,7 +389,7 @@ lastValueIO s@ShakeExtras{positionMapping,persistentKeys,state} k file = do
388389
| otherwise = do
389390
pmap <- readTVarIO persistentKeys
390391
mv <- runMaybeT $ do
391-
liftIO $ Logger.logDebug (logger s) $ T.pack $ "LOOKUP UP PERSISTENT FOR: " ++ show k
392+
liftIO $ Logger.logDebug (logger s) $ T.pack $ "LOOKUP PERSISTENT FOR: " ++ show k
392393
f <- MaybeT $ pure $ HMap.lookup (Key k) pmap
393394
(dv,del,ver) <- MaybeT $ runIdeAction "lastValueIO" s $ f file
394395
MaybeT $ pure $ (,del,ver) <$> fromDynamic dv
@@ -462,6 +463,7 @@ data IdeState = IdeState
462463
,shakeSession :: MVar ShakeSession
463464
,shakeExtras :: ShakeExtras
464465
,shakeDatabaseProfile :: ShakeDatabase -> IO (Maybe FilePath)
466+
,stopMonitoring :: IO ()
465467
}
466468

467469

@@ -557,10 +559,13 @@ shakeOpen :: Recorder (WithPriority Log)
557559
-> WithHieDb
558560
-> IndexQueue
559561
-> ShakeOptions
562+
-> Monitoring
560563
-> Rules ()
561564
-> IO IdeState
562565
shakeOpen recorder lspEnv defaultConfig logger debouncer
563-
shakeProfileDir (IdeReportProgress reportProgress) ideTesting@(IdeTesting testing) withHieDb indexQueue opts rules = mdo
566+
shakeProfileDir (IdeReportProgress reportProgress)
567+
ideTesting@(IdeTesting testing)
568+
withHieDb indexQueue opts monitoring rules = mdo
564569
let log :: Logger.Priority -> Log -> IO ()
565570
log = logWith recorder
566571

@@ -608,36 +613,40 @@ shakeOpen recorder lspEnv defaultConfig logger debouncer
608613
rules
609614
shakeSession <- newEmptyMVar
610615
shakeDatabaseProfile <- shakeDatabaseProfileIO shakeProfileDir
611-
let ideState = IdeState{..}
612616

613617
IdeOptions
614618
{ optOTMemoryProfiling = IdeOTMemoryProfiling otProfilingEnabled
615619
, optProgressStyle
620+
, optCheckParents
616621
} <- getIdeOptionsIO shakeExtras
617622

618-
void $ startTelemetry shakeDb shakeExtras
619623
startProfilingTelemetry otProfilingEnabled logger $ state shakeExtras
620624

621-
return ideState
622-
623-
startTelemetry :: ShakeDatabase -> ShakeExtras -> IO (Async ())
624-
startTelemetry db extras@ShakeExtras{..}
625-
| userTracingEnabled = do
626-
countKeys <- mkValueObserver "cached keys count"
627-
countDirty <- mkValueObserver "dirty keys count"
628-
countBuilds <- mkValueObserver "builds count"
629-
IdeOptions{optCheckParents} <- getIdeOptionsIO extras
630625
checkParents <- optCheckParents
631-
regularly 1 $ do
632-
observe countKeys . countRelevantKeys checkParents . map fst =<< (atomically . ListT.toList . STM.listT) state
633-
readTVarIO dirtyKeys >>= observe countDirty . countRelevantKeys checkParents . HSet.toList
634-
shakeGetBuildStep db >>= observe countBuilds
635626

636-
| otherwise = async (pure ())
637-
where
638-
regularly :: Seconds -> IO () -> IO (Async ())
639-
regularly delay act = async $ forever (act >> sleep delay)
627+
-- monitoring
628+
let readValuesCounter = fromIntegral . countRelevantKeys checkParents <$> getStateKeys shakeExtras
629+
readDirtyKeys = fromIntegral . countRelevantKeys checkParents . HSet.toList <$> readTVarIO(dirtyKeys shakeExtras)
630+
readIndexPending = fromIntegral . HMap.size <$> readTVarIO (indexPending $ hiedbWriter shakeExtras)
631+
readExportsMap = fromIntegral . HMap.size . getExportsMap <$> readTVarIO (exportsMap shakeExtras)
632+
readDatabaseCount = fromIntegral . countRelevantKeys checkParents . map fst <$> shakeGetDatabaseKeys shakeDb
633+
readDatabaseStep = fromIntegral <$> shakeGetBuildStep shakeDb
634+
635+
registerGauge monitoring "ghcide.values_count" readValuesCounter
636+
registerGauge monitoring "ghcide.dirty_keys_count" readDirtyKeys
637+
registerGauge monitoring "ghcide.indexing_pending_count" readIndexPending
638+
registerGauge monitoring "ghcide.exports_map_count" readExportsMap
639+
registerGauge monitoring "ghcide.database_count" readDatabaseCount
640+
registerCounter monitoring "ghcide.num_builds" readDatabaseStep
641+
642+
stopMonitoring <- start monitoring
643+
644+
let ideState = IdeState{..}
645+
return ideState
646+
640647

648+
getStateKeys :: ShakeExtras -> IO [Key]
649+
getStateKeys = (fmap.fmap) fst . atomically . ListT.toList . STM.listT . state
641650

642651
-- | Must be called in the 'Initialized' handler and only once
643652
shakeSessionInit :: Recorder (WithPriority Log) -> IdeState -> IO ()
@@ -657,6 +666,7 @@ shakeShut IdeState{..} = do
657666
for_ runner cancelShakeSession
658667
void $ shakeDatabaseProfile shakeDb
659668
progressStop $ progress shakeExtras
669+
stopMonitoring
660670

661671

662672
-- | This is a variant of withMVar where the first argument is run unmasked and if it throws

0 commit comments

Comments
 (0)