Skip to content

Commit d0e3e0f

Browse files
authored
HLS benchmarks (#3117)
* extract ghcide:experiments-types * extract haskell-language-server:plugins and let go of examples The main goal here is to move the Plugins module into an internal library so that it can be reused from the benchmark suite. In order to make that easier, and since they hardly serve a purpose in a repository with 25 plugins, I delete the Example and Example2 plugin descriptors and their dependencies. * HLS benchmark suite Port the ghcide benchmark suite to HLS and benchmark plugin "configurations" independently. This includes the following changes to the ghcide benchmark suite and HLS: - Support for "configurations" which are defined as sets of plugin ids. The benchmark will be run with only these plugins enabled and all others disabled - Support for configurable concurrency. This relies on RTS -ol and -po flags to place the RTS traces in the target location rather than in the cwd This change requires two commits, the next one places ghcide/bench/hist/Main.hs into its final location to help 'git' recognize the change as a file move * ghcide/bench/hist/Main.hs -> bench/Main.hs * CI - fix artifact names for uniqueness * disable shorten HLS step * Do not store eventlogs to avoid out of disk space * render durations up to milliseconds * shorten titles Goal is to display the formatted CSV (via column) one row per line * exclude formatting plugin configurations * Extract ghcide-bench to a standalone package * ghcide-bench: fix stderr capturing * Fix mem stats We parse maxResidency and allocatedBytes from the RTS -S output, but runSessionWithHandles kills the server without waiting for it to exit and these stats don't get logged. The solution is to use runSessionWithHandles', but unfortunately it is internal and not exposed. I have raised a PR to expose it and in the meantime we need a source repo package. * feedbacks * delete Example plugins
1 parent 55d9024 commit d0e3e0f

File tree

32 files changed

+1296
-1220
lines changed

32 files changed

+1296
-1220
lines changed

Diff for: .github/workflows/bench.yml

+14-12
Original file line numberDiff line numberDiff line change
@@ -61,16 +61,17 @@ jobs:
6161
with:
6262
ghc: ${{ matrix.ghc }}
6363
os: ${{ runner.os }}
64+
shorten-hls: "false"
6465

6566
# max-backjumps is increased as a temporary solution
6667
# for dependency resolution failure
6768
- run: cabal configure --enable-benchmarks --max-backjumps 12000
6869

6970
- name: Build
70-
run: cabal build ghcide:benchHist
71+
run: cabal build haskell-language-server:benchmark
7172

7273
- name: Bench init
73-
run: cabal bench ghcide:benchHist -j --benchmark-options="all-binaries"
74+
run: cabal bench -j --benchmark-options="all-binaries"
7475

7576
# tar is required to preserve file permissions
7677
# compression speeds up upload/download nicely
@@ -85,14 +86,14 @@ jobs:
8586
- name: Upload workspace
8687
uses: actions/upload-artifact@v3
8788
with:
88-
name: workspace
89+
name: workspace-${{ matrix.ghc }}-${{ matrix.os }}
8990
retention-days: 1
9091
path: workspace.tar.gz
9192

9293
- name: Upload .cabal
9394
uses: actions/upload-artifact@v3
9495
with:
95-
name: cabal-home
96+
name: cabal-home-${{ matrix.ghc }}-${{ matrix.os }}
9697
retention-days: 1
9798
path: ~/.cabal/cabal.tar.gz
9899

@@ -118,13 +119,13 @@ jobs:
118119
- name: Download cabal home
119120
uses: actions/download-artifact@v3
120121
with:
121-
name: cabal-home
122+
name: cabal-home-${{ matrix.ghc }}-${{ matrix.os }}
122123
path: .
123124

124125
- name: Download workspace
125126
uses: actions/download-artifact@v3
126127
with:
127-
name: workspace
128+
name: workspace-${{ matrix.ghc }}-${{ matrix.os }}
128129
path: .
129130

130131
- name: untar
@@ -134,28 +135,29 @@ jobs:
134135
tar xzf cabal.tar.gz --directory ~/.cabal
135136
136137
- name: Bench
137-
run: cabal bench ghcide:benchHist -j --benchmark-options="${{ matrix.example }}"
138+
run: cabal bench -j --benchmark-options="${{ matrix.example }}"
138139

139140
- name: Display results
140141
run: |
141-
column -s, -t < ghcide/bench-results/unprofiled/${{ matrix.example }}/results.csv | tee ghcide/bench-results/unprofiled/${{ matrix.example }}/results.txt
142+
column -s, -t < bench-results/unprofiled/${{ matrix.example }}/results.csv | tee bench-results/unprofiled/${{ matrix.example }}/results.txt
142143
143144
- name: tar benchmarking artifacts
144-
run: find ghcide/bench-results -name "*.csv" -or -name "*.svg" -or -name "*.html" | xargs tar -czf benchmark-artifacts.tar.gz
145+
run: find bench-results -name "*.csv" -or -name "*.svg" -or -name "*.html" | xargs tar -czf benchmark-artifacts.tar.gz
145146

146147
- name: Archive benchmarking artifacts
147148
uses: actions/upload-artifact@v3
148149
with:
149-
name: bench-results-${{ runner.os }}-${{ matrix.ghc }}
150+
name: bench-results-${{ matrix.example }}-${{ runner.os }}-${{ matrix.ghc }}
150151
path: benchmark-artifacts.tar.gz
151152

152153
- name: tar benchmarking logs
153-
run: find ghcide/bench-results -name "*.log" -or -name "*.eventlog" -or -name "*.hp" | xargs tar -czf benchmark-logs.tar.gz
154+
# We dont' store the eventlogs because the CI workers risk running out of disk space
155+
run: find bench-results -name "*.log" -or -name "*.hp" | xargs tar -czf benchmark-logs.tar.gz
154156

155157
- name: Archive benchmark logs
156158
uses: actions/upload-artifact@v3
157159
with:
158-
name: bench-logs-${{ runner.os }}-${{ matrix.ghc }}
160+
name: bench-logs-${{ matrix.example }}-${{ runner.os }}-${{ matrix.ghc }}
159161
path: benchmark-logs.tar.gz
160162

161163
bench_post_job:

Diff for: .gitignore

+2-1
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,10 @@ test/testdata/**/hie.yaml
3434
/.direnv/
3535
/.envrc
3636

37-
# ghcide-bench
37+
# bench
3838
*.identifierPosition
3939
/bench/example
40+
/bench-results
4041

4142
# nix
4243
result

Diff for: bench/Main.hs

+282
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
2+
{- Bench history
3+
4+
A Shake script to analyze the performance of HLS over the git history of the project
5+
6+
Driven by a config file `bench/config.yaml` containing the list of Git references to analyze.
7+
8+
Builds each one of them and executes a set of experiments using the ghcide-bench suite.
9+
10+
The results of the benchmarks and the analysis are recorded in the file
11+
system with the following structure:
12+
13+
bench-results
14+
├── <git-reference>
15+
│  ├── ghc.path - path to ghc used to build the binary
16+
│  └── haskell-language-server - binary for this version
17+
├─ <example>
18+
│ ├── results.csv - aggregated results for all the versions
19+
│ └── <git-reference>
20+
| └── <HLS plugin>
21+
│   ├── <experiment>.gcStats.log - RTS -s output
22+
│   ├── <experiment>.csv - stats for the experiment
23+
│   ├── <experiment>.svg - Graph of bytes over elapsed time
24+
│   ├── <experiment>.diff.svg - idem, including the previous version
25+
│   ├── <experiment>.log - ghcide-bench output
26+
│   └── results.csv - results of all the experiments for the example
27+
├── results.csv - aggregated results of all the experiments and versions
28+
└── <experiment>.svg - graph of bytes over elapsed time, for all the included versions
29+
30+
For diff graphs, the "previous version" is the preceding entry in the list of versions
31+
in the config file. A possible improvement is to obtain this info via `git rev-list`.
32+
33+
To execute the script:
34+
35+
> cabal/stack bench
36+
37+
To build a specific analysis, enumerate the desired file artifacts
38+
39+
> stack bench --ba "bench-results/HEAD/results.csv bench-results/HEAD/edit.diff.svg"
40+
> cabal bench --benchmark-options "bench-results/HEAD/results.csv bench-results/HEAD/edit.diff.svg"
41+
42+
-}
43+
{-# LANGUAGE DeriveAnyClass #-}
44+
{-# LANGUAGE DerivingStrategies #-}
45+
{-# LANGUAGE TypeFamilies #-}
46+
{-# OPTIONS -Wno-orphans #-}
47+
{-# LANGUAGE PackageImports #-}
48+
49+
import Control.Lens (preview, (^.))
50+
import Control.Monad.Extra
51+
import Data.Aeson (Value (..), encode)
52+
import Data.Aeson.Lens
53+
import Data.Default
54+
import Data.Foldable (find)
55+
import qualified Data.Map.Strict as Map
56+
import Data.Maybe
57+
import Data.Text (pack, unpack)
58+
import Data.Yaml (FromJSON (..), ToJSON (toJSON),
59+
decodeFileThrow)
60+
import Development.Benchmark.Rules hiding (parallelism)
61+
import Development.Shake (Action,
62+
Change (ChangeModtimeAndDigestInput),
63+
CmdOption (Cwd, StdinBS),
64+
RuleResult, Rules,
65+
ShakeOptions (shakeChange, shakeThreads),
66+
actionBracket, addOracle,
67+
askOracle, command, command_,
68+
getDirectoryFiles, liftIO, need,
69+
newCache, shakeArgsWith,
70+
shakeOptions, versioned, want)
71+
import Development.Shake.Classes
72+
import Experiments.Types (Example (exampleName),
73+
exampleToOptions)
74+
import GHC.Exts (toList)
75+
import GHC.Generics (Generic)
76+
import HlsPlugins (idePlugins)
77+
import qualified Ide.Plugin.Config as Plugin
78+
import Ide.Types
79+
import Numeric.Natural (Natural)
80+
import System.Console.GetOpt
81+
import System.Directory
82+
import System.FilePath
83+
import System.IO.Error (tryIOError)
84+
85+
configPath :: FilePath
86+
configPath = "bench/config.yaml"
87+
88+
configOpt :: OptDescr (Either String FilePath)
89+
configOpt = Option [] ["config"] (ReqArg Right configPath) "config file"
90+
91+
binaryName :: String
92+
binaryName = "haskell-language-server"
93+
94+
-- | Read the config without dependency
95+
readConfigIO :: FilePath -> IO (Config BuildSystem)
96+
readConfigIO = decodeFileThrow
97+
98+
instance IsExample Example where getExampleName = exampleName
99+
type instance RuleResult GetExample = Maybe Example
100+
type instance RuleResult GetExamples = [Example]
101+
102+
shakeOpts :: ShakeOptions
103+
shakeOpts =
104+
shakeOptions{shakeChange = ChangeModtimeAndDigestInput, shakeThreads = 0}
105+
106+
main :: IO ()
107+
main = shakeArgsWith shakeOpts [configOpt] $ \configs wants -> pure $ Just $ do
108+
let config = fromMaybe configPath $ listToMaybe configs
109+
_configStatic <- createBuildSystem config
110+
case wants of
111+
[] -> want ["all"]
112+
_ -> want wants
113+
114+
hlsBuildRules :: MkBuildRules BuildSystem
115+
hlsBuildRules = MkBuildRules findGhcForBuildSystem binaryName projectDepends buildHls
116+
where
117+
recordDepends path =
118+
need . map (path </>) =<< getDirectoryFiles path ["//*.hs"]
119+
projectDepends = do
120+
recordDepends "src"
121+
recordDepends "exe"
122+
recordDepends "plugins"
123+
recordDepends "ghcide/session-loader"
124+
recordDepends "ghcide/src"
125+
recordDepends "hls-graph/src"
126+
recordDepends "hls-plugin-api/src"
127+
need =<< getDirectoryFiles "." ["*.cabal"]
128+
129+
--------------------------------------------------------------------------------
130+
data Config buildSystem = Config
131+
{ experiments :: [Unescaped String],
132+
configurations :: [ConfigurationDescriptor],
133+
examples :: [Example],
134+
samples :: Natural,
135+
versions :: [GitCommit],
136+
-- | Output folder ('foo' works, 'foo/bar' does not)
137+
outputFolder :: String,
138+
buildTool :: buildSystem,
139+
profileInterval :: Maybe Double,
140+
parallelism :: Natural
141+
}
142+
deriving (Generic, Show)
143+
deriving anyclass (FromJSON)
144+
145+
createBuildSystem :: FilePath -> Rules (Config BuildSystem)
146+
createBuildSystem config = do
147+
readConfig <- newCache $ \fp -> need [fp] >> liftIO (readConfigIO fp)
148+
149+
_ <- addOracle $ \GetExperiments {} -> experiments <$> readConfig config
150+
_ <- addOracle $ \GetVersions {} -> versions <$> readConfig config
151+
_ <- versioned 1 $ addOracle $ \GetExamples{} -> examples <$> readConfig config
152+
_ <- versioned 1 $ addOracle $ \(GetExample name) -> find (\e -> getExampleName e == name) . examples <$> readConfig config
153+
_ <- addOracle $ \GetBuildSystem {} -> buildTool <$> readConfig config
154+
_ <- addOracle $ \GetSamples{} -> samples <$> readConfig config
155+
_ <- addOracle $ \GetConfigurations{} -> do
156+
Config{configurations} <- readConfig config
157+
return [ Configuration confName (encode $ disableAllPluginsBut (`elem` confPlugins))
158+
| ConfigurationDescriptor{..} <- configurations
159+
]
160+
161+
configStatic <- liftIO $ readConfigIO config
162+
let build = outputFolder configStatic
163+
164+
buildRules build hlsBuildRules
165+
benchRules build (MkBenchRules (askOracle $ GetSamples ()) benchHls warmupHls "haskell-language-server" (parallelism configStatic))
166+
csvRules build
167+
svgRules build
168+
heapProfileRules build
169+
phonyRules "" binaryName NoProfiling build (examples configStatic)
170+
171+
whenJust (profileInterval configStatic) $ \i -> do
172+
phonyRules "profiled-" binaryName (CheapHeapProfiling i) build (examples configStatic)
173+
174+
return configStatic
175+
176+
disableAllPluginsBut :: (PluginId -> Bool) -> Plugin.Config
177+
disableAllPluginsBut pred = def {Plugin.plugins = pluginsMap} where
178+
pluginsMap = Map.fromList
179+
[ (p, def { Plugin.plcGlobalOn = globalOn})
180+
| PluginDescriptor{pluginId = plugin@(PluginId p)} <- plugins
181+
, let globalOn =
182+
-- ghcide-core is required, nothing works without it
183+
plugin == PluginId (pack "ghcide-core")
184+
-- document symbols is required by the benchmark suite
185+
|| plugin == PluginId (pack "ghcide-hover-and-symbols")
186+
|| pred plugin
187+
]
188+
IdePlugins plugins = idePlugins mempty
189+
190+
newtype GetSamples = GetSamples () deriving newtype (Binary, Eq, Hashable, NFData, Show)
191+
type instance RuleResult GetSamples = Natural
192+
193+
--------------------------------------------------------------------------------
194+
195+
buildHls :: BuildSystem -> ProjectRoot -> OutputFolder -> Action ()
196+
buildHls Cabal root out = actionBracket
197+
(do
198+
projectLocalExists <- liftIO $ doesFileExist projectLocal
199+
when projectLocalExists $ liftIO $ do
200+
void $ tryIOError $ removeFile (projectLocal <.> "restore-after-benchmark")
201+
renameFile projectLocal (projectLocal <.> "restore-after-benchmark")
202+
liftIO $ writeFile projectLocal $ unlines
203+
["package haskell-language-server"
204+
," ghc-options: -eventlog -rtsopts"
205+
,"package ghcide"
206+
," flags: +ekg"
207+
]
208+
return projectLocalExists)
209+
(\projectLocalExists -> do
210+
removeFile projectLocal
211+
when projectLocalExists $
212+
renameFile (projectLocal <.> "restore-after-benchmark") projectLocal
213+
) $ \_ -> command_ [Cwd root] "cabal"
214+
["install"
215+
,"haskell-language-server:exe:haskell-language-server"
216+
,"--installdir=" ++ out
217+
,"--install-method=copy"
218+
,"--overwrite-policy=always"
219+
]
220+
where
221+
projectLocal = root </> "cabal.project.local"
222+
223+
buildHls Stack root out =
224+
command_ [Cwd root] "stack"
225+
["--local-bin-path=" <> out
226+
,"build"
227+
,"haskell-language-server:haskell-language-server"
228+
,"--copy-bins"
229+
,"--ghc-options=-rtsopts"
230+
,"--ghc-options=-eventlog"
231+
]
232+
233+
benchHls
234+
:: Natural -> BuildSystem -> [CmdOption] -> BenchProject Example -> Action ()
235+
benchHls samples buildSystem args BenchProject{..} = do
236+
command_ ([StdinBS configuration] ++ args) "ghcide-bench" $
237+
[ "--timeout=300",
238+
"--no-clean",
239+
"-v",
240+
"--samples=" <> show samples,
241+
"--csv=" <> outcsv,
242+
"--ghcide=" <> exePath,
243+
"--select",
244+
unescaped (unescapeExperiment experiment),
245+
"--lsp-config"
246+
] ++
247+
exampleToOptions example exeExtraArgs ++
248+
[ "--stack" | Stack == buildSystem
249+
]
250+
251+
warmupHls :: BuildSystem -> FilePath -> [CmdOption] -> Example -> Action ()
252+
warmupHls buildSystem exePath args example = do
253+
command args "ghcide-bench" $
254+
[ "--no-clean",
255+
"-v",
256+
"--samples=1",
257+
"--ghcide=" <> exePath,
258+
"--select=hover"
259+
] ++
260+
exampleToOptions example [] ++
261+
[ "--stack" | Stack == buildSystem
262+
]
263+
264+
--------------------------------------------------------------------------------
265+
data ConfigurationDescriptor = ConfigurationDescriptor
266+
{ confName :: String
267+
, confPlugins :: [PluginId]
268+
}
269+
deriving Show
270+
271+
instance FromJSON ConfigurationDescriptor where
272+
parseJSON (String s) = pure $ ConfigurationDescriptor (unpack s) [PluginId s]
273+
parseJSON o@Object{} = do
274+
let keymap = o ^. _Object
275+
matchKey = preview _String . toJSON
276+
case toList keymap of
277+
-- excuse the aeson 2.0 compatibility hack
278+
[(matchKey -> Just name, Array values)] -> do
279+
pluginIds <- traverse parseJSON values
280+
pure $ ConfigurationDescriptor (unpack name) (map PluginId $ toList pluginIds)
281+
other -> fail $ "Expected object with name and array of plugin ids: " <> show other
282+
parseJSON _ = fail "Expected plugin id or object with name and array of plugin ids"

0 commit comments

Comments
 (0)