Reduce startup time #135
Reduce startup time #135
Comments
Mostly fixed by #138 |
Here's the current breakdown of time spent, from a cold start (i.e. running the executable), totaling about ~3600ms:
Here's the cost of a hot start: the cost of initializing everything a second time in the same process, totaling ~500ms:
Not terrible now (used to be 10s!) but still could probably be sped up |
Looks like about 1600ms is spend initializing scalac the first time. We should be able to work around the usage of scalac in the case where everything is cached. That will speed it up considerably Another 1000s is spend initializing Scalaparse for the first time. I'm not sure how much of that time is spent classloading and how much is spent in the static initializers, but it seems plausible we could speed it up |
@laci37 do you want to take a crack at this? I have an idea that should save us the 1600ms to initialize scalac:
This would involve some refactoring to move the caching bits into the correct place, but I'm pretty sure it'll work. That'll bring our startup time into the 1-2s range which is not fast, but not bad =) |
We could also trivially cache the output of the |
I think one should allow for Ammonite-REPL to be a non-JVM program connecting to a daemon JVM running Ammonite sessions, though not all will want to use that. That can be done using NailGun (github.com/martylamb/nailgun). The daemon JVM should try to keep one prespawned session. This would allow literally instant startup, since nailgun's client is written in C. I've used this in the past for Blaisorblade/git-filter-branch-msgs, so that I could use invoke a JVM program inside a bash loop.
|
Yep! @laci37 actually wanted to do that and it was part of the original GSOC proposal. We've since been distracted with other slightly-more-high-pri things, but we could always revisit. If anyone else wants to take a shot at daemonizing Ammonite's JVM at this feel free to take a crack at it and prove out what it'd take. |
A persistent daemon comes with all kinds of problems when it runs for a long time. The most obvious is that when we import the same library in different versions, we can fill up the class cache. So I would advise against it. (At most, use a daemon only for coordination, like how "universal" environment variables are done in fish shell) My suggestion: Use the approach (or even the code) from Drip: Spin up VMs in a pool and use these already initialized VMs from the pool. Fill up the pool in the background as needed. In a simple test, I get "startup" times at about 0.126 seconds for Ammonite (with jdk1.8.0_25). Bash needs 0.063 seconds and Dash 0.032 seconds on the same machine. This looks good enough for me. |
Interesting point about the daemon. And Drip is already designed to be incorporated. Thanks for the pointer!
I thought you meant from bash, failed to reproduce, and then I realized you might mean from Drip. I'll try the latter, seems easy enough. |
@Blaisorblade I got the comparison times with |
@Blaisorblade With the newest version of Ammonite I have no problems with file handles any more. But the effect of Drip is not as big as before. $ time java -cp amm ammonite.repl.Repl -c exit
4.66 real 12.09 user 0.51 sys
$ time drip -cp amm ammonite.repl.Repl -c exit
5.11 real 13.11 user 0.55 sys
$ time drip -cp amm ammonite.repl.Repl -c exit
4.81 real 0.05 user 0.07 sys So I get startup time from 16 seconds down to under 5 seconds. Nice, but not as great as in the previous measurements. Probably with playing around with |
I don't have time to contribute, but I'd like to point out how to interpret those data. This snippet from Drip's docs sounds relevant:
This suggests that the new version of Ammonite moved some initialization code to later, so that the work is not done early enough (can't confirm without knowing the versions, but also doesn't necessarily matter). |
Just for fun, I experimented with nailgun integration and got some positive results for running Scala scripts, see diff: olafurpg@381680e To run it:
Scala scripts work like a charm. Rough numbers from compiling a new file on a warm sever:
With a file watcher like
The repl doesn't work. Pressing up/down/ctrl-c is broken for some I am sure @geggo98 raised some legitimate concerns about having a persistent daemon running. Regularly restarting the server might be a solution for that. |
If anybody is interested in fast shell startup, the basic problem is that nailgun offers only a socket forwarding to a terminal, and TTY detection only sees a socket and not a terminal. In fact, nailgun's redirection is done at the Java level and might not even affect the native stdin/stdout/stderr file descriptors (https://github.com/martylamb/nailgun/blob/1ad9ad9d2d17c895144a9ee0e7acb1d3d90fb66f/nailgun-server/src/main/java/com/martiansoftware/nailgun/NGSession.java#L257-L265). To see this forwarding problem, observe the perfect stream forwarder (
One hacky but relatively easy-ish fix would be that Ammonite's calls to A cleaner fix is harder: nailgun (or a replacement) could offer a virtual terminal rather than a pipe to the process, and forward terminal commands. That would be cleaner but much harder (if it is feasible at all), and UNIX terminal APIs are a mess and non-portable. No clue on Windows. I suspect this would only work with some existing terminal emulator... You'd probably want some Finally, here's a testcase showing that expect does better forwarding than cat. I'm not sure if those "pure Java" implementations achieve the same.
|
I am not convinced it's worth the effort to get the repl running via nailgun. In my experience, repl sessions usually stay open several minutes if not hours. Shaving 2-3 seconds off the startup time won't have a big impact on my productivity. I hacked together a script #!/bin/bash
set -e
command -v ng &> /dev/null
command -v amm_ng &> /dev/null
command -v entr &> /dev/null
startNailgun() {
echo "Starting nailgun..."
amm_ng &
sleep 1
ng ng-alias amm ammonite.Main
}
usage() {
cat << EOF
ss [file] # run scala script
ss loop [file] # continuously run script on file save
ss restart # restart nailgun server
ss help # print this help message
EOF
}
if [ $1 = "help" ]; then
usage
elif [ "$1" = "restart" ]; then
ng ng-stop || true
sleep 1
startNailgun
else
ng ng-stats &> /dev/null || startNailgun
if [ "$1" = "loop" ]; then
shift 1
ls "$@" | entr time ng amm "$@"
else
ng amm "$@"
fi
fi I don't know if this should be bundled with ammonite, but it's already pretty useful for myself. |
My current inclination is to bundle the file-watching ability as part of the That should give us the benefits of a warm JVM without the complexity of dealing with nailgun and all that, as long as we're happy with just the |
A I agree that the long-running daemon setup is error-prone and it would be best to get away without it, if possible. |
I've needed Scala scripts with fast startup—once (because most of my scripting is in bash). I also think @olafurpg's code is in fact simple enough for inclusion for those interested, especially to use Ammonite to replace bash scripting. But I understand the maintenance burden might matter. But in my case since the nailgun integration was easy enough to do it on my own, I'm currently fine with leaving this use case open. |
Starting a new REPL takes 5 to 10 seconds. This should be decreased, as this becomes annoying if you are using tmux and launch a new shell for each program you are working with.
The text was updated successfully, but these errors were encountered: