There have been previous issues about hover performance in big projects but so far no substantial progress has been made.
I tried again today on ghc/Main.hs with the wip/multi-rebased branch which is based on #457 and things seemed very bad still.
optThreads = 1 - Hover time was 2.2soptThreads = 0 - Hover time was 6.5sI tried on the command line and in the editor and could reproduce similar numbers in both, so it's not entirely to do with the FileExists logic which previously closed #101.
Here is the profile of performing a single hover request.
https://gist.github.com/mpickering/c12db7d1b9c0724024259f8411189ca6
Load the JSON into speedscope.app in order to view it.
I can't believe that I am the only person who observes this or wants this fixed! But it seems like I am so far the only person to complain about it.
Also see #101 and #485
Without profiling turned on the times are
optThreads = 1 = 1.3soptThreads = 0 = 2sCan you share a Shake profile ?
Now I have turned off (Haskell) profiling, after the first hover, it seems that the hover call is down to 0.7s which feels more acceptable. I wonder if this was fixed by #471
That is, 0.7s with optThreads = 1, optThreads = 0 is still too slow at over 1.5s.
Shake profile, it is not very useful imo: https://gist.githubusercontent.com/mpickering/25b13bf30f6558f18487501c3ec4f63b/raw/eff7933fba95d2f148a3ea5256a0a807ed3cecf3/ide-20200323-113440-00006-0.49.html
Shake profile, it is not very useful imo: https://gist.githubusercontent.com/mpickering/25b13bf30f6558f18487501c3ec4f63b/raw/eff7933fba95d2f148a3ea5256a0a807ed3cecf3/ide-20200323-113440-00006-0.49.html
It doesn't tell us the cause, but at least it rules out GetFileExists as the culprit, as the Shake profile shows the only invocations of this rule are for interface files, expected since they do not live in the workspace
Any chance of a normal .prof output? I'm used to reading those and have all the right tools to do so. While this profile view might (or might not) be better, I'd probably want to start with a simpler problem and build up to learning how it works, whereas with this code I'm totally lost.
The code appears to call getSrcSpanInfos which then calls evalGhcEnv multiple times. Why? I have no idea. I guess it's under runGhcEnv somewhere? But each call makes one call to runGhcEnv, so why multiple ones?
Another view has ioMsgMaybe taking all the time. That seems odd.
My guess is I'm not reading the profile right.
@ndmitchell Nope, not easily because the .prof file is dominated by the initial loading of the project. This profile is for a single hover request which is not easy to isolate in a .prof file.
The first pane does not appear to be very useful here, that's shows an execution trace of the hover. The "left-heavy" pane is like the flamegraph output, similar to what is in a .prof file.
ioMsgMaybe is probably not taking up a lot of time, but there are also probably no cost centres underneath that. The whole call is underneath the getSrcSpanInfos cost centre though, so that will only happen on the first request so probably best to ignore that part of the profile. I would concentrate on the latter half of the left-heavy view, where there is 1s of internal computation which will happen on every hover request.
The other question which can't really be observed in the profile is why optThreads = 1 is so much faster than any other setting I tried.
Thanks @mpickering - looking at it with that information in my brain, I see:
buildOne is a build only, and that subsequent runs don't rebuild - but if they did, that would be the root cause. If you exclude things not under buildOne or the GHC call, you're down to about 100ms, which doesn't match what you are seeing.runKey calls nubDepends. That's 161ms, which is awfully heavy. Would be great to get execution counts rather than just times. With that information I'd know how many calls there were to nubDepends and how many to insert. This should be something only called once, but entirely possible it isn't and that's a bug.Here is the JSON for the second hover. Bare in mind again, that this is with optsThread = 1, which makes the performance close to acceptable.
The fmap comes from src/General/Wait.hs, it would be good to know what function this is called with so I can insert more cost centres.
Looking in the first pane of the profile, I found a call to nubDepends and it is called a lot of times, lasting either 1-2 frames for each call.
The calls to fmap under buildRunDependenciesChanged seem to happen just after the nubDepends calls and last around 1-3 frames in general.
Trying on a smaller project (ghcide)
optThreads = 1 - 0.05 hover responseoptThreads = 0 - 0.35 hover responseSeems like I will be compiling my forks with optThreads = 1 for the foreseeable future.
Looking in threadscope, it is clear that the work is not split across the available cores. There is barely any overlap between the different HECs which is why the single threaded version is much faster.

Just to add another data point, in our codebase at StanChart, on my Xeon Gold 6234 with 16 physical cores:
We know that Shake parallelizes real builds really well, but it seems clear the same strategy doesn't work so well for ghcide where some of the rules are very fast and the overheads of parallelizing them outweigh the benefits. @ndmitchell is there a way to make some rules sequential, like GetFileExists and GetModTime?
Thanks @pepeiborra, I can also observe in theadscope all cores being utilised during initialisation so your numbers make sense to me.
@pepeiborra Are those initial hover numbers or subsequent hover numbers after the hover information has been computed and cached?
Subsequent. The initial one is around 5-6 seconds.
I experimented with removing the shake scheduler, just forking threads and leaving it up to the GHC scheduler. This didn't seem to have any consistent negative impact on ghcide or hadrian performance(in fact building ghc became very slightly faster on my machine). After this, I modified the scheduler to use forkOn instead of forkIO to run actions on a fixed capability if their previous run took less time than a threshold value(currently 50ms). This seemed to bring hover performance in line with what @mpickering and @pepeiborra are seeing with the optThreads = 1 version, while still retaining some parallelism for the boot procedure and other expensive actions.
You can find my branch here.
Here is an eventlog with the regular scheduler that shows hover taking ~0.2s: https://drive.google.com/open?id=1TkD6IAav3l2vSXECjdiNYCfYvLy-9a6y
Here is an eventlog with my branch that shows hover taking ~0.02s: https://drive.google.com/open?id=1fwnqtEj4dk7_ZTw3faeHRzGTCnq-0HqX
@wz1000, I would be very interested in seeing an eventlog of your branch without forkOn. Really forkIO should be the right choice here. Bound threads incur overheads that you really don't want (see GHC #18221).
@bgamari the first eventlog linked above is the one without forkOn
Looking at your Shake profile, I see 12,140 calls to DoesFileExist, which will be responsible for about two thirds of the threads being spawned. However, the huge number of DoesFileExist was to cope with the fact that GHC used a flat hierarchy with lots of include directories (a very odd approach to Haskell code layout). If the compiler include path is set properly, I would expect 521 such calls. Can we rebenchmark with a GHC using proper hierarchy? And confirm the include paths have been adjusted appropriately?
As for whether making some rules like modtime/does-file-exist faster, yes, we could certainly have a flag that rules could declare they should be run on the existing thread, not spawn a new thread. That's how Shake used to work a few years ago, and then I generalised it, however, I'd be keen to see benchmarks showing what is slow, and thus measuring improvements, so we can make sure we are targeting the right place.
Using Ghcide HEAD, Windows, GHC 8.6.5, I wrote a benchmark that loads up Shake and does 1000 hover requests, with threaded/N4. It uses the root module which has about 100 dependencies. I hacked Ghcide to make sure I always took the doesFileExist fast-path. Benchmark code at https://gist.github.com/ndmitchell/11467985dbf1855e62035fa97248a585. I compiled in Shake (so I could get better profiling and experiment quicker) and took a look at what happened if I made certain changes (these are for 1K requests, so divide by 100 to see the real time):
The last of these is wrong, but a very useful lower-bound. Looking at the rules that I couldn't count as cheap, I got the list I've pasted in #583. A 25% performance increase would be nice, but it's hardly going to change the world, so I'm inclined to investigate more before adding spawn avoidance. Running profiling on the version that avoids spawning threads for things that I think will be cheap showed:
There seem several ways to go:
Approach (4) is the one I've been pursuing at https://github.com/wz1000/ghcide/tree/hiedb
The idea is to aggressively reuse stale results, and rely on hiedb to answer queries. In this model, ghcide acts as a sort of indexing service for hiedb, generating .hi and .hie files which are indexed and saved in the database, available for all future queries, even across restarts. A local hot cache of .hie files/typechecked modules is maintained on top of this to answer queries for files of interest (the ones the user is currently editing). All information that is not in some sense "local" to a particular module is accessed through the database, and local information (like references of local variables and types) is available through these hot caches.
The invariant we try to maintain is that of eventual consistency and instant responsiveness, rather than 100% accuracy, which I think is more suitable for an IDE. The responses get progressively better as the index state catches up to the expected state, but in practice this often seems to be good enough.
The hover, go to (type) definition, references, document highlight and workspace symbols requests have been (re)written to fit in this model. This week I plan to finish my work on teaching hiedb to index .hi files, so that module imports, identifiers exported from modules, their types and their documentation is also stored in the database, which we can then use to serve completion requests.
I can indeed confirm that the IDE is extremely responsive with this branch, and can even answer many requests instantly on startup, even before the initial load and session setup has completed, provided that a previous run had saved an .hie file to disk.
@wz1000 the hiedb approach would also be scalable to large codebases, which is a big deal. Let's have this.
@ndmitchell did you measure allocations for any of the experiments? The unexplained time could be going into synchronization and gc pauses. The 'ShakeSession' approach achieves Shake invalidation avoidance for hovers and goto-definition, but not for completions - reading completions directly from the state seems the only way to go.
The approach sounds super amazing to me. I didn't measure allocations for the experiments, but my benchmark sends the messages sequentially, so there would have been no impact from the ShakeSession changes. Could have definitely been timings going into GC/synchronisation, but not obvious why.
The approach sounds super amazing to me. I didn't measure allocations for the experiments, but my benchmark sends the messages sequentially, so there would have been no impact from the ShakeSession changes. Could have definitely been timings going into GC/synchronisation, but not obvious why.
There would have been a very noticeable impact, as no Shake cache invalidation would have happened unless your benchmark makes any edit. So I would expect the benchmark to complete in 0.30s
There would have been a very noticeable impact, as no Shake cache invalidation would have happened unless your benchmark makes any edit.
Ah, I hadn't remembered that. Yeah, mine was before your changes, which I imagine will have a huge impact.
I measured again, after the impact of fixing hash collisions in #588. This time I took the approach of knocking things out, which are required for correctness, and seeing how much time remained. 100 hover runs was the benchmark. Some times vary by 0.5s between runs, so take it as indicative.
My suggestion is we turn the benchmarking tools into real tools #582, or wait for telemetry, then solicit more repeatable and diverse benchmarks that either back up the initial results at the top of this thread, or show that enough has changed it is no longer true.
Most helpful comment
Approach (4) is the one I've been pursuing at https://github.com/wz1000/ghcide/tree/hiedb
The idea is to aggressively reuse stale results, and rely on
hiedbto answer queries. In this model,ghcideacts as a sort of indexing service forhiedb, generating.hiand.hiefiles which are indexed and saved in the database, available for all future queries, even across restarts. A local hot cache of.hiefiles/typechecked modules is maintained on top of this to answer queries for files of interest (the ones the user is currently editing). All information that is not in some sense "local" to a particular module is accessed through the database, and local information (like references of local variables and types) is available through these hot caches.The invariant we try to maintain is that of eventual consistency and instant responsiveness, rather than 100% accuracy, which I think is more suitable for an IDE. The responses get progressively better as the index state catches up to the expected state, but in practice this often seems to be good enough.
The hover, go to (type) definition, references, document highlight and workspace symbols requests have been (re)written to fit in this model. This week I plan to finish my work on teaching
hiedbto index.hifiles, so that module imports, identifiers exported from modules, their types and their documentation is also stored in the database, which we can then use to serve completion requests.I can indeed confirm that the IDE is extremely responsive with this branch, and can even answer many requests instantly on startup, even before the initial load and session setup has completed, provided that a previous run had saved an
.hiefile to disk.