Skip to content

Commit 088db2a

Browse files
committed
Collect metrics and expose an EKG server
1 parent b6d87c5 commit 088db2a

File tree

6 files changed

+117
-11
lines changed

6 files changed

+117
-11
lines changed

cabal.project

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@ constraints:
5353
ghc-lib-parser-ex -auto,
5454
stylish-haskell +ghc-lib
5555

56+
source-repository-package
57+
type:git
58+
location: https://github.com/vshabanov/ekg-json
59+
tag: 00ebe7211c981686e65730b7144fbf5350462608
60+
-- https://github.com/tibbe/ekg-json/pull/12
61+
5662
allow-newer:
5763
-- ghc-9.2
5864
----------
@@ -70,3 +76,11 @@ allow-newer:
7076
-- for shake-bench
7177
Chart:lens,
7278
Chart-diagrams:lens,
79+
80+
-- for ekg
81+
ekg-core:base,
82+
ekg-wai:base,
83+
84+
-- for shake-bench
85+
Chart-diagrams:diagrams-core,
86+
SVGFonts:diagrams-core

docs/contributing/contributing.md

Lines changed: 29 additions & 0 deletions
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+
HLS opens a metrics server on port 8000 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 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/ghcide.cabal

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ library
4848
dependent-map,
4949
dependent-sum,
5050
dlist,
51+
ekg-wai,
52+
ekg-core,
5153
exceptions,
5254
extra >= 1.7.4,
5355
enummapset,

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,11 @@ import Control.Monad
3838
import qualified Development.IDE.Core.FileExists as FileExists
3939
import qualified Development.IDE.Core.OfInterest as OfInterest
4040
import Development.IDE.Core.Shake hiding (Log)
41+
import Development.IDE.Core.Shake
4142
import qualified Development.IDE.Core.Shake as Shake
4243
import Development.IDE.Types.Shake (WithHieDb)
4344
import System.Environment (lookupEnv)
44-
45+
import System.Metrics
4546

4647
data Log
4748
= LogShake Shake.Log
@@ -68,8 +69,9 @@ initialise :: Recorder (WithPriority Log)
6869
-> IdeOptions
6970
-> WithHieDb
7071
-> IndexQueue
72+
-> Maybe Store
7173
-> IO IdeState
72-
initialise recorder defaultConfig mainRule lspEnv logger debouncer options withHieDb hiedbChan = do
74+
initialise recorder defaultConfig mainRule lspEnv logger debouncer options withHieDb hiedbChan metrics = do
7375
shakeProfiling <- do
7476
let fromConf = optShakeProfiling options
7577
fromEnv <- lookupEnv "GHCIDE_BUILD_PROFILING"
@@ -86,6 +88,7 @@ initialise recorder defaultConfig mainRule lspEnv logger debouncer options withH
8688
withHieDb
8789
hiedbChan
8890
(optShakeOptions options)
91+
metrics
8992
$ do
9093
addIdeGlobal $ GlobalIdeOptions options
9194
ofInterestRules (cmapWithPrio LogOfInterest recorder)

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

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ import Development.IDE.Graph hiding (ShakeValue)
136136
import qualified Development.IDE.Graph as Shake
137137
import Development.IDE.Graph.Database (ShakeDatabase,
138138
shakeGetBuildStep,
139+
shakeGetDatabaseKeys,
139140
shakeNewDatabase,
140141
shakeProfileDatabase,
141142
shakeRunDatabaseForKeys)
@@ -152,6 +153,23 @@ import Development.IDE.Types.Options
152153
import Development.IDE.Types.Shake
153154
import qualified Focus
154155
import GHC.Fingerprint
156+
import Language.LSP.Types.Capabilities
157+
import OpenTelemetry.Eventlog
158+
159+
import Control.Exception.Extra hiding (bracket_)
160+
import Data.Aeson (toJSON)
161+
import qualified Data.ByteString.Char8 as BS8
162+
import Data.Coerce (coerce)
163+
import Data.Default
164+
import Data.Foldable (for_, toList)
165+
import Data.HashSet (HashSet)
166+
import qualified Data.HashSet as HSet
167+
import Data.IORef.Extra (atomicModifyIORef'_,
168+
atomicModifyIORef_)
169+
import Data.String (fromString)
170+
import Data.Text (pack)
171+
import Debug.Trace.Flags (userTracingEnabled)
172+
import qualified Development.IDE.Types.Exports as ExportsMap
155173
import HieDb.Types
156174
import Ide.Plugin.Config
157175
import qualified Ide.PluginUtils as HLS
@@ -167,6 +185,7 @@ import OpenTelemetry.Eventlog
167185
import qualified StmContainers.Map as STM
168186
import System.FilePath hiding (makeRelative)
169187
import System.IO.Unsafe (unsafePerformIO)
188+
import System.Metrics (Store, registerCounter, registerGauge)
170189
import System.Time.Extra
171190

172191
data Log
@@ -388,7 +407,7 @@ lastValueIO s@ShakeExtras{positionMapping,persistentKeys,state} k file = do
388407
| otherwise = do
389408
pmap <- readTVarIO persistentKeys
390409
mv <- runMaybeT $ do
391-
liftIO $ Logger.logDebug (logger s) $ T.pack $ "LOOKUP UP PERSISTENT FOR: " ++ show k
410+
liftIO $ Logger.logDebug (logger s) $ T.pack $ "LOOKUP PERSISTENT FOR: " ++ show k
392411
f <- MaybeT $ pure $ HMap.lookup (Key k) pmap
393412
(dv,del,ver) <- MaybeT $ runIdeAction "lastValueIO" s $ f file
394413
MaybeT $ pure $ (,del,ver) <$> fromDynamic dv
@@ -557,10 +576,11 @@ shakeOpen :: Recorder (WithPriority Log)
557576
-> WithHieDb
558577
-> IndexQueue
559578
-> ShakeOptions
579+
-> Maybe Store
560580
-> Rules ()
561581
-> IO IdeState
562582
shakeOpen recorder lspEnv defaultConfig logger debouncer
563-
shakeProfileDir (IdeReportProgress reportProgress) ideTesting@(IdeTesting testing) withHieDb indexQueue opts rules = mdo
583+
shakeProfileDir (IdeReportProgress reportProgress) ideTesting@(IdeTesting testing) withHieDb indexQueue opts metrics rules = mdo
564584
let log :: Logger.Priority -> Log -> IO ()
565585
log = logWith recorder
566586

@@ -613,11 +633,28 @@ shakeOpen recorder lspEnv defaultConfig logger debouncer
613633
IdeOptions
614634
{ optOTMemoryProfiling = IdeOTMemoryProfiling otProfilingEnabled
615635
, optProgressStyle
636+
, optCheckParents
616637
} <- getIdeOptionsIO shakeExtras
617638

618639
void $ startTelemetry shakeDb shakeExtras
619640
startProfilingTelemetry otProfilingEnabled logger $ state shakeExtras
620641

642+
checkParents <- optCheckParents
643+
for_ metrics $ \store -> do
644+
let readValuesCounter = fromIntegral . countRelevantKeys checkParents . HMap.keys <$> readVar (state shakeExtras)
645+
readDirtyKeys = fromIntegral . countRelevantKeys checkParents . HSet.toList <$> readIORef (dirtyKeys shakeExtras)
646+
readIndexPending = fromIntegral . HMap.size <$> readTVarIO (indexPending $ hiedbWriter shakeExtras)
647+
readExportsMap = fromIntegral . HMap.size . getExportsMap <$> readVar (exportsMap shakeExtras)
648+
readDatabaseCount = fromIntegral . countRelevantKeys checkParents . map fst <$> shakeGetDatabaseKeys shakeDb
649+
readDatabaseStep = fromIntegral <$> shakeGetBuildStep shakeDb
650+
651+
registerGauge "ghcide.values_count" readValuesCounter store
652+
registerGauge "ghcide.dirty_keys_count" readDirtyKeys store
653+
registerGauge "ghcide.indexing_pending_count" readIndexPending store
654+
registerGauge "ghcide.exports_map_count" readExportsMap store
655+
registerGauge "ghcide.database_count" readDatabaseCount store
656+
registerCounter "ghcide.num_builds" readDatabaseStep store
657+
621658
return ideState
622659

623660
startTelemetry :: ShakeDatabase -> ShakeExtras -> IO (Async ())

ghcide/src/Development/IDE/Main.hs

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import Data.Maybe (catMaybes, isJust)
2929
import qualified Data.Text as T
3030
import Data.Text.Lazy.Encoding (decodeUtf8)
3131
import qualified Data.Text.Lazy.IO as LT
32+
import Data.Traversable (for)
3233
import Data.Typeable (typeOf)
3334
import Development.IDE (Action, GhcVersion (..),
3435
Priority (Debug, Error), Rules,
@@ -126,6 +127,9 @@ import System.IO (BufferMode (LineBufferin
126127
hSetEncoding, stderr,
127128
stdin, stdout, utf8)
128129
import System.Random (newStdGen)
130+
import qualified System.Metrics as Monitoring
131+
import System.Remote.Monitoring.Wai
132+
import qualified System.Remote.Monitoring.Wai as Monitoring
129133
import System.Time.Extra (Seconds, offsetTime,
130134
showDuration)
131135
import Text.Printf (printf)
@@ -231,6 +235,7 @@ data Arguments = Arguments
231235
, argsHandleIn :: IO Handle
232236
, argsHandleOut :: IO Handle
233237
, argsThreads :: Maybe Natural
238+
, argsMonitoringPort :: Maybe Natural
234239
}
235240

236241

@@ -268,6 +273,7 @@ defaultArguments recorder logger = Arguments
268273
-- the language server tests without the redirection.
269274
putStr " " >> hFlush stdout
270275
return newStdout
276+
, argsMonitoringPort = Just 8000
271277
}
272278

273279

@@ -355,6 +361,23 @@ defaultMain recorder Arguments{..} = withHeapStats (cmapWithPrio LogHeapStats re
355361
-- FIXME: Remove this after GHC 9 gets fully supported
356362
when (ghcVersion == GHC90) $
357363
log Warning LogOnlyPartialGhc9Support
364+
server <- fmap join $ for argsMonitoringPort $ \p -> do
365+
store <- Monitoring.newStore
366+
let startServer = Monitoring.forkServerWith store "localhost" (fromIntegral p)
367+
-- this can fail if the port is busy, throwing an async exception back to us
368+
-- to handle that, wrap the server thread in an async
369+
mb_server <- async startServer >>= waitCatch
370+
case mb_server of
371+
Right s -> do
372+
logInfo logger $ T.pack $
373+
"Started monitoring server on port " <> show p
374+
return $ Just s
375+
Left e -> do
376+
logInfo logger $ T.pack $
377+
"Unable to bind monitoring server on port "
378+
<> show p <> ":" <> show e
379+
return Nothing
380+
358381
initialise
359382
(cmapWithPrio LogService recorder)
360383
argsDefaultHlsConfig
@@ -365,6 +388,9 @@ defaultMain recorder Arguments{..} = withHeapStats (cmapWithPrio LogHeapStats re
365388
options
366389
withHieDb
367390
hieChan
391+
(Monitoring.serverMetricStore <$> server)
392+
`onException`
393+
traverse_ (killThread . serverThreadId) server
368394
dumpSTMStats
369395
Check argFiles -> do
370396
dir <- maybe IO.getCurrentDirectory return argsProjectRoot
@@ -397,7 +423,7 @@ defaultMain recorder Arguments{..} = withHeapStats (cmapWithPrio LogHeapStats re
397423
, optCheckProject = pure False
398424
, optModifyDynFlags = optModifyDynFlags def_options <> pluginModifyDynflags plugins
399425
}
400-
ide <- initialise (cmapWithPrio LogService recorder) argsDefaultHlsConfig rules Nothing logger debouncer options hiedb hieChan
426+
ide <- initialise (cmapWithPrio LogService recorder) argsDefaultHlsConfig rules Nothing logger debouncer options hiedb hieChan Nothing
401427
shakeSessionInit (cmapWithPrio LogShake recorder) ide
402428
registerIdeConfiguration (shakeExtras ide) $ IdeConfiguration mempty (hashed Nothing)
403429

@@ -450,13 +476,8 @@ defaultMain recorder Arguments{..} = withHeapStats (cmapWithPrio LogHeapStats re
450476
, optCheckProject = pure False
451477
, optModifyDynFlags = optModifyDynFlags def_options <> pluginModifyDynflags plugins
452478
}
453-
ide <- initialise (cmapWithPrio LogService recorder) argsDefaultHlsConfig rules Nothing logger debouncer options hiedb hieChan
479+
ide <- initialise (cmapWithPrio LogService recorder) argsDefaultHlsConfig rules Nothing logger debouncer options hiedb hieChan Nothing
454480
shakeSessionInit (cmapWithPrio LogShake recorder) ide
455-
registerIdeConfiguration (shakeExtras ide) $ IdeConfiguration mempty (hashed Nothing)
456-
c ide
457-
458-
{-# ANN defaultMain ("HLint: ignore Use nubOrd" :: String) #-}
459-
460481

461482
expandFiles :: [FilePath] -> IO [FilePath]
462483
expandFiles = concatMapM $ \x -> do

0 commit comments

Comments
 (0)