Join GitHub today
GitHub is home to over 40 million developers working together to host and review code, manage projects, and build software together.
Sign upDo some profiling to find out why it takes so long to start a REPL #1717
Comments
This comment has been minimized.
This comment has been minimized.
Hi @Malabarba Maybe we can use a benchmark here. I found this http://doc.endlessparentheses.com/Fun/benchmark. If you think this is one possible way, I will try look at it and see what I can discover. |
This comment has been minimized.
This comment has been minimized.
Related ticket - clojure-emacs/cider-nrepl#218 I'm reasonably sure that the inlined deps make it so slow to start a CIDER REPL, but I can't find our discussion on the subject. Before introducing mranderson the REPL started 3-4 times if I remember correctly. |
This comment has been minimized.
This comment has been minimized.
Unfortunately that's not going to help. We need to profile the Clojure side of things (the cider-nrepl middleware). So this is strictly a Java/Clojure job. |
This comment has been minimized.
This comment has been minimized.
Truth. Well, anyway I will try to look that way you commented and try to find something |
This comment has been minimized.
This comment has been minimized.
In theory, there's no reason why injecting deps would make things slower. After all, it doesn't change the number of namespaces that need to be loaded on a blank project. BTW, do we AOT compile cider-nrepl? |
This comment has been minimized.
This comment has been minimized.
AOT compilation is used very rarely. It has some weird edge-cases and there's no real benefit (other than obfuscating proprietary source code). When the JVM starts a clojure app it spends a trivial amount of time compiling stuff. |
This comment has been minimized.
This comment has been minimized.
Guess someone can see what's the boot time now with a clean cider-nrepl and one built with mranderson, so we would have a starting point. We never measured even this - I just remember people started complaining about the slow boot immediately after we started using mranderson. |
This comment has been minimized.
This comment has been minimized.
I just measured 19 sec on both cases. So looks like inlining is not the cause. |
This comment has been minimized.
This comment has been minimized.
AOT compilation reduces that by about 4 seconds. A bit better, but still noticeably slow. |
This comment has been minimized.
This comment has been minimized.
If you just do I'm guessing the difference between now and then is that you're now working on projects with more dependencies ;) |
This comment has been minimized.
This comment has been minimized.
Well, I'm testing this on a blank project. Besides, project dependencies usually don't add startup time since they're not loaded at startup. However, I just realised that about 6 sec of my benchmark were coming from refactor-nrepl auto-injection. Here's a rough breakdown with only cider-nrepl:
Looks like we gained a good 4 seconds on version 0.7. Is that perhaps due to a specific dependency? |
This comment has been minimized.
This comment has been minimized.
Guess we should check what we added then. On Monday, 25 April 2016, Artur Malabarba notifications@github.com wrote:
Best Regards, |
This comment has been minimized.
This comment has been minimized.
What's a good way to look into this? |
This comment has been minimized.
This comment has been minimized.
In 0.7 we've added:
|
This comment has been minimized.
This comment has been minimized.
For me running just Running Removing |
This comment has been minimized.
This comment has been minimized.
Also, tried to do some profiling.
Yeah, of course, majority of those method calls/loaded classes are just Clojure runtime classes/methods. But 5 GCs... Can anyone point to the best way to start working on that issue? |
This comment has been minimized.
This comment has been minimized.
@kovrik Could you post the full reports you for from your profiling? Maybe we can spot some unnecessary loops or something. |
This comment has been minimized.
This comment has been minimized.
@Malabarba |
This comment has been minimized.
This comment has been minimized.
Thanks! If possible, also post the raw original in a gist somewhere. It could be useful too. |
This comment has been minimized.
This comment has been minimized.
Profiling snapshot size is 851 MB. Need some time to review it :) |
This comment has been minimized.
This comment has been minimized.
Oh dear |
This comment has been minimized.
This comment has been minimized.
On Mac it takes ~16 seconds. (println "START:" (format "%.0f" (/ (. System (nanoTime)) 1000000000.0))) into And :repl-options {:init (println "END:" (format "%.0f" (/ (. System (nanoTime)) 1000000000.0)))} into I'm not quite familiar with how cider works, reading sources at the moment, but any help would be appreciated :) Did a quick look at profiling results:
So, my initial guess would be that it takes so long to just load all dependencies required, do all imports etc. Trying to confirm that. Is there a way to reduce number of dependencies or not load them all at once, but load them on-demand? |
This comment has been minimized.
This comment has been minimized.
Also, I see the following message: The only place I see that message being printed is It starts 2 JVMs, isn't it? But why I see message 3 times. not 2? |
This comment has been minimized.
This comment has been minimized.
Timings:
I would say that we are looking at optimizing last 3 commands (each add ~10 secs). |
This comment has been minimized.
This comment has been minimized.
It starts two? |
This comment has been minimized.
This comment has been minimized.
~ λ jps
72112 Jps
~ λ lein repl
nREPL server started on port 51232 on host 127.0.0.1 - nrepl://127.0.0.1:51232
REPL-y 0.3.7, nREPL 0.2.12
Clojure 1.8.0
Java HotSpot(TM) 64-Bit Server VM 1.8.0-b132
~ λ jps
72032 main
71986 main
72119 Jps (http://stackoverflow.com/questions/27339206/why-does-leiningen-keep-its-own-jvm-running) |
This comment has been minimized.
This comment has been minimized.
Ah! Not to derail the thread, but what happens in the two different JVMs? |
This comment has been minimized.
This comment has been minimized.
Would removing the require statements in the |
This comment has been minimized.
This comment has been minimized.
Hmm, format is a bit of functionality that's not important at all. Probably we should spin it off to some separate middleware or something like this if it causes such a performance hit. |
This comment has been minimized.
This comment has been minimized.
I would suggest someone else to experiment with it too. Profiler could be lying. On my machine, dropping |
This comment has been minimized.
This comment has been minimized.
@expez it is a nice experiment to try and probably we need to dig in core for the confirmation but probably ns or not, if the require is called it will load the vars in it. I am not familiar with this in particular, but I was wondering whether there is a way to say: please only load the vars that I use. Selectively loading only the var in a particular execution path maybe can improve things, but probably it has already been done and I am just rambling |
This comment has been minimized.
This comment has been minimized.
@alexander-yakushev good job! This is exactly the kind of thing we need to figure out, I will try to confirm your findings. |
This comment has been minimized.
This comment has been minimized.
Now that @benedekfazekas uploaded a new refactor-nrepl snapshot, I thought I'd post a comparison of startup times before and after some of this
This was run on a crouton chroot of my Samsung Chromebook Pro on a fresh Leiningen project. |
This comment has been minimized.
This comment has been minimized.
Lazy loading is a good solution, at EuroClojure we were discussing about it, it changes semantics but if we could load only the var that are effectively used (finer granularity), then startup time would increase for sure. There are many things that I don't know in core for advocating this but I was thinking of throwing it there and it is nice to see that |
This comment has been minimized.
This comment has been minimized.
I guess there are more opportunities for lazy loading as the bulk of the middlewares are not utilized during the boot process. Hopefully the load times could be brought down even more. |
This comment has been minimized.
This comment has been minimized.
Here's the Current FlameGraph. I generate these with flame-du-jour Lazy loading |
This comment has been minimized.
This comment has been minimized.
Another interesting thing about that flamegraph is that refactor-nrepl and cider-nrepl both rewrite and load |
This comment has been minimized.
This comment has been minimized.
yeah that is a fair point
…On 23 Jul 2017 9:53 p.m., "Ryan Fowler" ***@***.***> wrote:
Another interesting thing about that flamegraph is that refactor-nrepl and
cider-nrepl both rewrite and load clojure.tools.namespace.parse so that
code is getting loaded twice under two different namespace names. I haven't
done an exhaustive search, so there may be other duplicated loading.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#1717 (comment)>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/AA9PnlBud4PRCRlXXnNltffqwlQulLDQks5sQ6SggaJpZM4IPFVz>
.
|
This comment has been minimized.
This comment has been minimized.
As an aside, JetBrains take startup time really seriously. Extensions to IntelliJ are loaded from an IoC container, and the classes aren't even loaded until the functionality is actually required. Could you do something similar, perhaps associate each editor command that uses some nREPL functionality with the namespace of the server-side bit it requires, and ensure that that is loaded when the command is actually executed? I know that nREPL doesn't like dynamic fiddling with middleware, but something like that might be possible. |
This comment has been minimized.
This comment has been minimized.
Still, I don't quite understand why it takes 32 seconds (just checked it again on my machine) to start it. Lazy loading is a very nice thing, should make startup much faster (but still, why it takes so long? just because of the number of classes? or there is something else? some computations on startup?). For example, Wildfly container (which is huge) loads ~331 modules on startup (which takes ~3.5 seconds - quite impressive, I would say), and lazy-loads ~400 other modules. Can't find nice info on Wildfly 10, but in general it should be pretty much the same as for JBoss AS 7: So the general idea is:
Can we do something similar here? |
This comment has been minimized.
This comment has been minimized.
Also, I've just pushed some changes to https://github.com/kovrik/cider-diet repo (updated dependencies to the latest versions). |
This comment has been minimized.
This comment has been minimized.
It's most the number of classed that get loaded right away. The ns rewriting that we're doing for the deps bloats the number of classes a lot, but it's the only way we found to avoid dependency conflicts.
Apart from off-loading some requires everything else requires changes in the nREPL server itself. Current it doesn't support lazy middleware loading. This shouldn't be hard to do I think, but until the project is taken out of contrib I doubt anyone would won't to work on such a feature. The only relatively easy thing we can do in the meantime is to try to move most code from the middlewares to some utility libs, so they can be easily loaded when the middleware is used for the first time. |
This comment has been minimized.
This comment has been minimized.
Looking at the repo's README it's not clear to me what exactly have you done to speed this up and what are the tradeoffs. |
This comment has been minimized.
This comment has been minimized.
I no longer use Clojure, so I might be wrong or miss something.
As far as I remember, nREPL itself was not a bottleneck. Starting vanilla Clojure REPL is blazing fast (~1 second). In my particular case the bottleneck is Leiningen. I don't know what Leiningen does on startup (on Benchmarks:
|
This comment has been minimized.
This comment has been minimized.
we could give this a try I suppose and provide a var for other cider "plugins" to add their own middleware similar to what we do for the CLI params |
This comment has been minimized.
This comment has been minimized.
I don't have great plans for taking the next step, but I do have some familiarity with the problem that hasn't been discussed yet. Maybe this will trigger an idea for someone. There are essentially two types of middleware that I've noticed. These are names I'm making up.
op handlers are clean because you can just deref a delay when you receive the op you're interested in. That's basically what I did with refactor-nrepl. I just delayed loading of middleware that takes time to load. I think it'd be fairly easy to make a consistent delayed pattern for op handlers. cider-nrepl seems to have more handler wrappers (for example debug and pprint). For those, it seemed cleanest to delay the I monkeyed around with a delayed-refer macro, but it stopped being satisfying when I realized that every pprint library in the world names its pprint function |
This comment has been minimized.
This comment has been minimized.
I do this for a lot of my Java/Kotlin->Clojure interop, i.e. calling into Clojure from classes defined in Java/Kotlin. The implementation is pretty trivial:
...where |
This comment has been minimized.
This comment has been minimized.
Actually, looking at that code, I think I just saw a bug - it should be doing the TCCL dance to set the right classloader before calling I also suspect that this issue no longer warrants the "low hanging fruit" tag :-) |
This comment has been minimized.
This comment has been minimized.
Yeah, no kidding. This is probably the highest (and juiciest!) fruit in the tree! |
This comment has been minimized.
This comment has been minimized.
Haha |
This comment has been minimized.
This comment has been minimized.
@kovrik's note triggered my memory on LEIN_FAST_TRAMPOLINE. Is there a reason we're not using it? Seems to drop another 5ish seconds off my repl/cider startup time. This is with the same profiles.clj as above.
|
Currently, when starting a REPL in a project with the cider-nrepl middleware/plugin, it takes on the order of 30 seconds to go from 0 to REPL.
Even though that's not a huge deal breaker, it does get very annoying under some workflows.
What's worse, I've found that it sort of pushes me into some specific habits, like never restarting a REPL, or preferring to use the command-line to run tests if I don't have an open REPL.
While such habits aren't obviously bad, they aren't obviously good either, and I don't think it's good that CIDER incentivates them.
It would be great if we had some profiling data on what takes so long. If it turns out to be unavoidable, then so be it. But maybe it's one specific dependency we could ditch. Or maybe it's a namespace that's doing something silly.