Skip to content

Commit 1d1f2db

Browse files
Enhance benchmarks & bug fixes (#823)
* parse allocations * WaitForShakeQueue * Measure user time and shake time in experiments * clean ups * Prevent a potential crash of the shake enqueue thread * Fix a bug that was preventing reenqueud actions from getting flushed * Avoid running the check-project action per file What we really want is to check the project once per cradle * Backwards compat. * Review feedback * Fix typo Co-authored-by: Neil Mitchell <[email protected]> Co-authored-by: Neil Mitchell <[email protected]>
1 parent 55e3810 commit 1d1f2db

File tree

4 files changed

+96
-59
lines changed

4 files changed

+96
-59
lines changed

bench/lib/Experiments.hs

Lines changed: 59 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ import System.Process
3636
import System.Time.Extra
3737
import Text.ParserCombinators.ReadP (readP_to_S)
3838
import System.Environment.Blank (getEnv)
39+
import Development.IDE.Plugin.Test
40+
import Data.Aeson (Value(Null))
3941

4042
-- Points to a string in the target file,
4143
-- convenient for hygienic edits
@@ -71,7 +73,7 @@ experiments =
7173
---------------------------------------------------------------------------------------
7274
bench "edit" 10 $ \doc -> do
7375
changeDoc doc [hygienicEdit]
74-
void (skipManyTill anyMessage message :: Session WorkDoneProgressEndNotification)
76+
waitForProgressDone
7577
return True,
7678
---------------------------------------------------------------------------------------
7779
bench "hover after edit" 10 $ \doc -> do
@@ -97,7 +99,7 @@ experiments =
9799
10
98100
( \doc -> do
99101
changeDoc doc [breakingEdit]
100-
void (skipManyTill anyMessage message :: Session WorkDoneProgressEndNotification)
102+
waitForProgressDone
101103
return identifierP
102104
)
103105
( \p doc -> do
@@ -239,15 +241,28 @@ runBenchmarks allBenchmarks = do
239241
in (b,) <$> runBench run b
240242

241243
-- output raw data as CSV
242-
let headers = ["name", "success", "samples", "startup", "setup", "experiment", "maxResidency"]
244+
let headers =
245+
[ "name"
246+
, "success"
247+
, "samples"
248+
, "startup"
249+
, "setup"
250+
, "userTime"
251+
, "delayedTime"
252+
, "totalTime"
253+
, "maxResidency"
254+
, "allocatedBytes"]
243255
rows =
244256
[ [ name,
245257
show success,
246258
show samples,
247259
show startup,
248260
show runSetup',
261+
show userWaits,
262+
show delayedWork,
249263
show runExperiment,
250-
showMB maxResidency
264+
show maxResidency,
265+
show allocations
251266
]
252267
| (Bench {name, samples}, BenchRun {..}) <- results,
253268
let runSetup' = if runSetup < 0.01 then 0 else runSetup
@@ -265,8 +280,11 @@ runBenchmarks allBenchmarks = do
265280
show samples,
266281
showDuration startup,
267282
showDuration runSetup',
283+
showDuration userWaits,
284+
showDuration delayedWork,
268285
showDuration runExperiment,
269-
showMB maxResidency
286+
showMB maxResidency,
287+
showMB allocations
270288
]
271289
| (Bench {name, samples}, BenchRun {..}) <- results,
272290
let runSetup' = if runSetup < 0.01 then 0 else runSetup
@@ -280,6 +298,7 @@ runBenchmarks allBenchmarks = do
280298
unwords $
281299
[ ghcide ?config,
282300
"--lsp",
301+
"--test",
283302
"--cwd",
284303
dir,
285304
"+RTS",
@@ -288,9 +307,9 @@ runBenchmarks allBenchmarks = do
288307
]
289308
++ ghcideOptions ?config
290309
++ concat
291-
[ ["--shake-profiling", path]
292-
| Just path <- [shakeProfiling ?config]
310+
[ ["--shake-profiling", path] | Just path <- [shakeProfiling ?config]
293311
]
312+
++ ["--verbose" | verbose ?config]
294313
lspTestCaps =
295314
fullCaps {_window = Just $ WindowClientCapabilities $ Just True}
296315
conf =
@@ -305,12 +324,15 @@ data BenchRun = BenchRun
305324
{ startup :: !Seconds,
306325
runSetup :: !Seconds,
307326
runExperiment :: !Seconds,
327+
userWaits :: !Seconds,
328+
delayedWork :: !Seconds,
308329
success :: !Bool,
309-
maxResidency :: !Int
330+
maxResidency :: !Int,
331+
allocations :: !Int
310332
}
311333

312334
badRun :: BenchRun
313-
badRun = BenchRun 0 0 0 False 0
335+
badRun = BenchRun 0 0 0 0 0 False 0 0
314336

315337
waitForProgressDone :: Session ()
316338
waitForProgressDone =
@@ -328,27 +350,36 @@ runBench runSess Bench {..} = handleAny (\e -> print e >> return badRun)
328350
changeDoc doc [hygienicEdit]
329351
waitForProgressDone
330352

331-
332353
liftIO $ output $ "Running " <> name <> " benchmark"
333354
(runSetup, userState) <- duration $ benchSetup doc
334-
let loop 0 = return True
335-
loop n = do
355+
let loop !userWaits !delayedWork 0 = return $ Just (userWaits, delayedWork)
356+
loop !userWaits !delayedWork n = do
336357
(t, res) <- duration $ experiment userState doc
337358
if not res
338-
then return False
359+
then return Nothing
339360
else do
340361
output (showDuration t)
341-
loop (n -1)
342-
343-
(runExperiment, success) <- duration $ loop samples
362+
-- Wait for the delayed actions to finish
363+
waitId <- sendRequest (CustomClientMethod "test") WaitForShakeQueue
364+
(td, resp) <- duration $ skipManyTill anyMessage $ responseForId waitId
365+
case resp of
366+
ResponseMessage{_result=Right Null} -> do
367+
loop (userWaits+t) (delayedWork+td) (n -1)
368+
_ ->
369+
-- Assume a ghcide build lacking the WaitForShakeQueue command
370+
loop (userWaits+t) delayedWork (n -1)
371+
372+
(runExperiment, result) <- duration $ loop 0 0 samples
373+
let success = isJust result
374+
(userWaits, delayedWork) = fromMaybe (0,0) result
344375

345376
-- sleep to give ghcide a chance to GC
346377
liftIO $ threadDelay 1100000
347378

348-
maxResidency <- liftIO $
379+
(maxResidency, allocations) <- liftIO $
349380
ifM (doesFileExist gcStats)
350-
(parseMaxResidency <$> readFile gcStats)
351-
(pure 0)
381+
(parseMaxResidencyAndAllocations <$> readFile gcStats)
382+
(pure (0,0))
352383

353384
return BenchRun {..}
354385
where
@@ -400,13 +431,15 @@ setup = do
400431

401432
--------------------------------------------------------------------------------------------
402433

403-
-- Parse the max residency in RTS -s output
404-
parseMaxResidency :: String -> Int
405-
parseMaxResidency input =
406-
case find ("maximum residency" `isInfixOf`) (reverse $ lines input) of
407-
Just l -> read $ filter isDigit $ head (words l)
408-
Nothing -> -1
409-
434+
-- Parse the max residency and allocations in RTS -s output
435+
parseMaxResidencyAndAllocations :: String -> (Int, Int)
436+
parseMaxResidencyAndAllocations input =
437+
(f "maximum residency", f "bytes allocated in the heap")
438+
where
439+
inps = reverse $ lines input
440+
f label = case find (label `isInfixOf`) inps of
441+
Just l -> read $ filter isDigit $ head $ words l
442+
Nothing -> -1
410443

411444
escapeSpaces :: String -> String
412445
escapeSpaces = map f

session-loader/Development/IDE/Session.hs

Lines changed: 24 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ loadSession dir = do
199199

200200

201201
let session :: (Maybe FilePath, NormalizedFilePath, ComponentOptions, FilePath)
202-
-> IO ([NormalizedFilePath],(IdeResult HscEnvEq,[FilePath]))
202+
-> IO (IdeResult HscEnvEq,[FilePath])
203203
session args@(hieYaml, _cfp, _opts, _libDir) = do
204204
(hscEnv, new, old_deps) <- packageSetup args
205205

@@ -245,11 +245,21 @@ loadSession dir = do
245245
invalidateShakeCache
246246
restartShakeSession [kick]
247247

248-
let resultCachedTargets = concatMap targetLocations all_targets
249-
250-
return (resultCachedTargets, second Map.keys res)
251-
252-
let consultCradle :: Maybe FilePath -> FilePath -> IO ([NormalizedFilePath], (IdeResult HscEnvEq, [FilePath]))
248+
-- Typecheck all files in the project on startup
249+
unless (null cs || not checkProject) $ do
250+
cfps' <- liftIO $ filterM (IO.doesFileExist . fromNormalizedFilePath) (concatMap targetLocations cs)
251+
void $ shakeEnqueue extras $ mkDelayedAction "InitialLoad" Debug $ void $ do
252+
mmt <- uses GetModificationTime cfps'
253+
let cs_exist = catMaybes (zipWith (<$) cfps' mmt)
254+
modIfaces <- uses GetModIface cs_exist
255+
-- update exports map
256+
extras <- getShakeExtras
257+
let !exportsMap' = createExportsMap $ mapMaybe (fmap hirModIface) modIfaces
258+
liftIO $ modifyVar_ (exportsMap extras) $ evaluate . (exportsMap' <>)
259+
260+
return (second Map.keys res)
261+
262+
let consultCradle :: Maybe FilePath -> FilePath -> IO (IdeResult HscEnvEq, [FilePath])
253263
consultCradle hieYaml cfp = do
254264
lfp <- flip makeRelative cfp <$> getCurrentDirectory
255265
logInfo logger $ T.pack ("Consulting the cradle for " <> show lfp)
@@ -276,7 +286,7 @@ loadSession dir = do
276286
InstallationNotFound{..} ->
277287
error $ "GHC installation not found in libdir: " <> libdir
278288
InstallationMismatch{..} ->
279-
return ([],(([renderPackageSetupException cfp GhcVersionMismatch{..}], Nothing),[]))
289+
return (([renderPackageSetupException cfp GhcVersionMismatch{..}], Nothing),[])
280290
InstallationChecked _compileTime _ghcLibCheck ->
281291
session (hieYaml, toNormalizedFilePath' cfp, opts, libDir)
282292
-- Failure case, either a cradle error or the none cradle
@@ -286,12 +296,12 @@ loadSession dir = do
286296
let res = (map (renderCradleError ncfp) err, Nothing)
287297
modifyVar_ fileToFlags $ \var -> do
288298
pure $ Map.insertWith HM.union hieYaml (HM.singleton ncfp (res, dep_info)) var
289-
return ([ncfp],(res,[]))
299+
return (res,[])
290300

291301
-- This caches the mapping from hie.yaml + Mod.hs -> [String]
292302
-- Returns the Ghc session and the cradle dependencies
293303
let sessionOpts :: (Maybe FilePath, FilePath)
294-
-> IO ([NormalizedFilePath], (IdeResult HscEnvEq, [FilePath]))
304+
-> IO (IdeResult HscEnvEq, [FilePath])
295305
sessionOpts (hieYaml, file) = do
296306
v <- fromMaybe HM.empty . Map.lookup hieYaml <$> readVar fileToFlags
297307
cfp <- canonicalizePath file
@@ -306,37 +316,25 @@ loadSession dir = do
306316
-- Keep the same name cache
307317
modifyVar_ hscEnvs (return . Map.adjust (\(h, _) -> (h, [])) hieYaml )
308318
consultCradle hieYaml cfp
309-
else return (HM.keys v, (opts, Map.keys old_di))
319+
else return (opts, Map.keys old_di)
310320
Nothing -> consultCradle hieYaml cfp
311321

312322
-- The main function which gets options for a file. We only want one of these running
313323
-- at a time. Therefore the IORef contains the currently running cradle, if we try
314324
-- to get some more options then we wait for the currently running action to finish
315325
-- before attempting to do so.
316-
let getOptions :: FilePath -> IO ([NormalizedFilePath],(IdeResult HscEnvEq, [FilePath]))
326+
let getOptions :: FilePath -> IO (IdeResult HscEnvEq, [FilePath])
317327
getOptions file = do
318328
hieYaml <- cradleLoc file
319329
sessionOpts (hieYaml, file) `catch` \e ->
320-
return ([],(([renderPackageSetupException file e], Nothing),[]))
330+
return (([renderPackageSetupException file e], Nothing),[])
321331

322332
returnWithVersion $ \file -> do
323-
(cs, opts) <- liftIO $ join $ mask_ $ modifyVar runningCradle $ \as -> do
333+
opts <- liftIO $ join $ mask_ $ modifyVar runningCradle $ \as -> do
324334
-- If the cradle is not finished, then wait for it to finish.
325335
void $ wait as
326336
as <- async $ getOptions file
327-
return (fmap snd as, wait as)
328-
unless (null cs) $ do
329-
cfps' <- liftIO $ filterM (IO.doesFileExist . fromNormalizedFilePath) cs
330-
-- Typecheck all files in the project on startup
331-
void $ shakeEnqueue extras $ mkDelayedAction "InitialLoad" Debug $ void $ do
332-
when checkProject $ do
333-
mmt <- uses GetModificationTime cfps'
334-
let cs_exist = catMaybes (zipWith (<$) cfps' mmt)
335-
modIfaces <- uses GetModIface cs_exist
336-
-- update xports map
337-
extras <- getShakeExtras
338-
let !exportsMap' = createExportsMap $ mapMaybe (fmap hirModIface) modIfaces
339-
liftIO $ modifyVar_ (exportsMap extras) $ evaluate . (exportsMap' <>)
337+
return (as, wait as)
340338
pure opts
341339

342340
-- | Run the specific cradle on a specific FilePath via hie-bios.

src/Development/IDE/Core/Shake.hs

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -562,15 +562,14 @@ shakeRestart IdeState{..} acts =
562562
withMVar'
563563
shakeSession
564564
(\runner -> do
565-
(stopTime,queue) <- duration (cancelShakeSession runner)
565+
(stopTime,()) <- duration (cancelShakeSession runner)
566566
res <- shakeDatabaseProfile shakeProfileDir shakeDb
567567
let profile = case res of
568568
Just fp -> ", profile saved at " <> fp
569569
_ -> ""
570570
logDebug (logger shakeExtras) $ T.pack $
571571
"Restarting build session (aborting the previous one took " ++
572572
showDuration stopTime ++ profile ++ ")"
573-
return queue
574573
)
575574
-- It is crucial to be masked here, otherwise we can get killed
576575
-- between spawning the new thread and updating shakeSession.
@@ -621,9 +620,8 @@ newSession ShakeExtras{..} shakeDb acts = do
621620
"finish: " ++ actionName d ++ " (took " ++ showDuration runTime ++ ")"
622621

623622
workRun restore = do
624-
let acts' = pumpActionThread : map getAction (reenqueued ++ acts)
625-
res <- try @SomeException
626-
(restore $ shakeRunDatabase shakeDb acts')
623+
let acts' = pumpActionThread : map run (reenqueued ++ acts)
624+
res <- try @SomeException (restore $ shakeRunDatabase shakeDb acts')
627625
let res' = case res of
628626
Left e -> "exception: " <> displayException e
629627
Right _ -> "completed"
@@ -658,8 +656,8 @@ instantiateDelayedAction (DelayedAction _ s p a) = do
658656
alreadyDone <- liftIO $ isJust <$> waitBarrierMaybe b
659657
unless alreadyDone $ do
660658
x <- actionCatch @SomeException (Right <$> a) (pure . Left)
661-
liftIO $ do
662-
signalBarrier b x
659+
-- ignore exceptions if the barrier has been filled concurrently
660+
liftIO $ void $ try @SomeException $ signalBarrier b x
663661
d' = DelayedAction (Just u) s p a'
664662
return (b, d')
665663

src/Development/IDE/Plugin/Test.hs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,14 @@ import Language.Haskell.LSP.Messages
2020
import Language.Haskell.LSP.Types
2121
import System.Time.Extra
2222
import Development.IDE.Core.RuleTypes
23+
import Control.Monad
2324

2425
data TestRequest
2526
= BlockSeconds Seconds -- ^ :: Null
2627
| GetInterfaceFilesDir FilePath -- ^ :: String
2728
| GetShakeSessionQueueCount -- ^ :: Number
29+
| WaitForShakeQueue
30+
-- ^ Block until the Shake queue is empty. Returns Null
2831
deriving Generic
2932
deriving anyclass (FromJSON, ToJSON)
3033

@@ -61,4 +64,9 @@ requestHandler _ s (GetInterfaceFilesDir fp) = do
6164
requestHandler _ s GetShakeSessionQueueCount = do
6265
n <- atomically $ countQueue $ actionQueue $ shakeExtras s
6366
return $ Right (toJSON n)
67+
requestHandler _ s WaitForShakeQueue = do
68+
atomically $ do
69+
n <- countQueue $ actionQueue $ shakeExtras s
70+
when (n>0) retry
71+
return $ Right Null
6472

0 commit comments

Comments
 (0)