Tricks to make Clojure(startup time) faster?


#41

That is a bit misleading. Yes Clojure starts in less than a second but then you add core.async which adds another 500ms and so on. It is very easy to get past the 1 second mark with very few actual deps. lein or boot do add overhead yes but not significantly more than other libs.

One major thing that can contribute to startup time is having a user.clj in your classpath which actually loads many deps. This file will be loaded whenever Clojure starts so even if you run something via clj that doesn’t use the user ns the deps will be loaded regardless making it appear slower than it needs to be. Many projects have a user.clj with a generated set of REPL helper functions (eg. start-figwheel). This means whatever you are running will always load everything CLJS+figwheel and so on making things very slow.

Make sure you have no user.clj on the classpath if you intend to do lots of scripting.


#42

You make valid points but I probably wouldn’t characterize what I said as misleading. I write scripts all the time and I don’t use user.clj or deps like core.async for most of these simple scripts I’m writing.


#43

I didn’t say that it isn’t possible to write fast scripts. I do agree with you, it is. It is however very easy to get into slow territory. Case in point:

Clojure 1.9.0
user=> (time (require 'clj-http.client))
"Elapsed time: 1278.772 msecs"

I also agree with people complaining about startup times for scripting. Just making the blanket statement that Clojure is slow however is just as wrong.

Things would be much easier if it was easier to see WHY something was actually starting slow. Then you can start having a reasonable discussion and see if things could be improved.


#44

From the beginning, I’d been able to reload some functions and even add dependencies from the REPL with e.g. (load-file "build.boot"). This bit worked fine!

It’s been demotivating to compose every other bit of build tooling from scratch without ever moving on to actually making things.

boot watch ... reload cljs from the command line takes 30 seconds to start. If I want to change anything about the task, that’s a restart.

Instead, I spent a while getting watch + cljs to work in the REPL. After lots of fiddling with futures, I found out that it still takes 20 seconds to restart (boot (watch) ... (cljs)) - boot deliberately doesn’t cache things between tasks. Brilliant.

Other tooling - Reloading both frontend & backend code on save. Trying out component and similar. Connecting to an editor - twice, or entering commands to toggle CLJS. Setting up cider autocomplete - fiddly with CLJ+CLJS, extra 15-20 sec startup. Rewriting build.boot/user.clj to use lazy require/resolve to get to a REPL faster. Badly missing static typing with every single line and mistake.

(And again with shadow-cljs, and again with lein, and it looks like clj is even more minimal)


#45

When I hear people complaining about startup time, it makes me curious about their workflow…

I can comment on use cases for fast startup in production, since it’s a hard requirement for me and Clojure does not fit the bill at all, which is a damn shame.

First, there is serverless. Serverless backends treat containers as ephemeral and scale out the backend in response to demand. Both of these properties mean startup time is really important, since startup time directly translates to system latency. Clojure is completely unusable in a serverless environment if you care at all about latency.

Second, and similarly, containerized environments tend to treat containers more ephemerally. It’s a game changer when there is a negligible cost of starting and stopping containers dynamically, as you get with e.g. the Go and NodeJS runtimes.

Clojure is absolutely not an option for me when working under these requirements, which happens increasingly often. Despite the language itself being one of the best in the language landscape, the runtime has forced me to move on from Clojure to languages that meet the performance requirements of more modern execution environments.


#46

I have had some success using ClojureScript in a serverless environment. It’s straightforward enough to target Node rather than the browser. Perhaps you can have your cake and eat it too?


#47

I generally write AWS lambda functions that need lower latency using Clojurescript for this reason.


#48

I wasn’t calling Clojure slow, I was curious what would it take to make it start fast? We know that ClojureScript can start pretty fast and at the same time keep Clojure semantics. So what is that thing that Clojure adds to that that makes it so slow at loading namespaces?

It’s not a very practical question. I undestand that the answer would probably be “complexity, compatibility and historical reasons”. That’s why I formulated my question as a thought experiment: imagine you can build another language that has all Clojure benefits but is not bound by current implementation. How fast could we make it? And what would be the differences from current Clojure in that case (what will we have to give up)? Is there a fundamental reason on why we can’t load 10+k clases in 100ms? In 1 second? What’s the baseline here?

I believe speed is pretty important and underappreciated (http://tonsky.me/blog/speed/). I remember Jonathan Blow demoed his language compiler compiling entire 150K lines of code from scratch without any caching at all (!) under a second. It enabled him to fully recompile full 3d game on the fly on every file change! So the baseline of what’s possible is pretty far out there.


#49

If we’re lucky, native image with Graal VM will give us fast Clojure start times :crossed_fingers:


#50

It does not keep Clojure semantics. There are no vars or even namespaces at runtime. There is no reader reading any files, compiling any code or any of that. CLJS is directly executing code without any of the “meta” stuff. Clojure is also much more dynamic in that you can call require dynamically at any time and many many other tricks you can’t do in CLJS.

Also don’t forget that JavaScript engines like v8 were specifically designed for fast startup and not long running server-type applications that Clojure/JVM was built for.


#51

Seems like in original Twitter thread it was mentioned that core namespace loads in 1.5s, and the rest of the libs take 18.5s, so my question is - is that a death by a thousand cuts or there is one bottleneck that is responsible for the most of the time.

In case it is a death by a thousand cuts, what’s the usual solution for such problem?
In case it is one bottleneck - it is pretty obvious - need to fix the bottleneck.


#52

From all the analysis I’ve read, yes, its a death by a thousand cuts. Thus everything would need to be made faster to add up to noticeable gains.

The only trick around it are techniques to cache the bootstrapping to an image that can be loaded in one go, such as app-cds on OpenJDK EE, or native-image on graal, or other similar offerings from 3rd party JVMs.

That said, the most common speedup is from not having lein start two Clojure process. That’s done with using fast trampoline. That’ll save you the time it took to start the lein Clojure process.

Another trick is to avoid nRepl. That repl takes a while to bootstrap. Unrepl or clojure.main starts much faster.


#53

But how much of it is JVM vs V8, and how much of it is the difference in semantics? If it was all mostly due to the semantics, I could picture an AOT compilation mode for Clojure which would mimic the semantics of ClojureScript, thus yes, preventing some uncommon form of runtime reification and dynamism, but that would allow fast startup times.

I would take a Clojure AOT mode that didn’t support those features, but gave me bare metal Java startup times. Then I could use it for AWS Lambdas, and scripts, and CLI tools, etc. I think it would be a good improvement.

Now, I have no idea of the effort required to add such a mode, but if someone else does it :stuck_out_tongue: I’m not going to complain.


#54

I don’t like speculating about what might be achievable. No idea how much difference the VMs make, probably very little. You can already get quite a nice boost from AOT + static linking but those are terrible for the REPL where you want things to be as dynamic as possible.

Startup has never been an issue for me since I start things once every few hours at most, usually every few days. Eliminating unnecessary startups was probably the first thing I learned when starting Clojure which basically automatically happens when you start using the REPL for everything.

Startup could probably be faster yes and it is definitely prohibitive for certain use-cases but IMHO CLJS fills those nicely. GraalVM looks promising too.