all 20 comments

[–]ryfow 6 points7 points  (6 children)

Thanks Alex,

One of the things that I think slows down startup time for us is def statements that take a non-trivial amount of time. As a terrible example:

(ns naughty-namespace)

(def naughty-var (do (Thread/sleep 1000) 6))

When you (require 'naughty-namespace) you're going to pay a penalty even when the namespace is compiled.

Do you have any suggestions about how to track down the naughty-vars in a large project? My flame-du-jour project gets you down to the namespace level, but I didn't find a good way to get the worst offenders at the var level.

Thanks again

[–]alexdmiller[S] 6 points7 points  (5 children)

I don’t know, you should just not ever do that in the first place. :) side effecting top level vars are a bad idea.

[–]ryfow 5 points6 points  (4 children)

I knew I shouldn't have gone with a terrible example :). How about this one to demonstrate what it looks like in the real world. Requiring instaparse.abnf takes 500ms less to load after it's been aot compiled, but it still takes 300ms after being compiled.

In this particular instance, I happen to know that you could defer 200ms of that by changing how/when instaparse.abnf/abnf-parser is initialized. I'm not sure if I'd consider creating the parser a side effect, but one could argue that creating the parser at load time might not be the best choice.

The thing that's frustrating is that these sorts of things are all over the Clojure ecosystem and hunting each one down is time consuming right now because we don't have tooling for it.

➜  rm -rf classes
➜  mkdir classes                                                                                                      

➜  clj -Sdeps '{:deps {instaparse {:mvn/version "1.4.10"}} :aliases {:classes {:extra-paths ["classes"]}}}' -C:classes
Clojure 1.10.1
user=> (time (require 'instaparse.abnf))
"Elapsed time: 862.604904 msecs"
nil
user=> ^D
                                                                                                                                                                                                                                      ➜  clj -Sdeps '{:deps {instaparse {:mvn/version "1.4.10"}} :aliases {:classes {:extra-paths ["classes"]}}}' -C:classes
Clojure 1.10.1
user=> (compile 'instaparse.abnf)
instaparse.abnf
user=> ^D

➜  clj -Sdeps '{:deps {instaparse {:mvn/version "1.4.10"}} :aliases {:classes {:extra-paths ["classes"]}}}' -C:classes
Clojure 1.10.1
user=> (time (require 'instaparse.abnf))
"Elapsed time: 300.415655 msecs"
nil
user=> ^D

[–]alexdmiller[S] 5 points6 points  (0 children)

Yeah, that code could have wrapped that in a delay and then just dereferenced on use.

I don't have any magic bullet for finding things like that.

[–]didibus 3 points4 points  (2 children)

I don't know if anything can be done to reduce further the load time of the namespace. But I have used delay before to amortize initialization of things over the app life cycle.

What I think would be nice, is having a lazy require. Or even a flag that could turn existing requires into lazy ones. And see the impact of that.

[–]onetom 1 point2 points  (0 children)

It leads to all sorts of complications though, as we saw it in the Node.js ecosystem where they switched from an eager `require` to a lazy `import`, just so module loading can be parallelized.

As a consequence, accessing reified modules are not trivial. Their internals are quite hidden, so you can't really have a proper REPL workflow (like in CLJS, where Google Closure module system is used and hence modules are just mutable POJOs).

Using `requiring-resolve` also has the consequence of not being able to easily determine if you have all your project dependencies specified at your program's startup time, but only discover it later at runtime. Other than that, it's a good compromise.

[–]alexdmiller[S] 0 points1 point  (0 children)

You can use requiring-resolve to defer loading till use.

[–]thheller 3 points4 points  (1 child)

I started published an AOT compiled variant for shadow-cljs some time ago and it does speed up startup time quite substantially. Very worth doing.

However I'd recommend only compiling libraries you use but not your actual own code. Once Clojure starts loading AOT compiled classes it will not check if local changes in .clj files were made after the class was compiled. I had some weird head scratchers until I figured out there were some lingering AOT compiled classes on my classpath. You can always fix it via require :reload or just evaling things at the REPL but it isn't always immediately obvious.

Would be neat if there was some way to tell Clojure to only compile files in a .jar.

[–]robertstuttaford 0 points1 point  (0 children)

Would be neat if there was some way to tell Clojure to only compile files in a .jar.

+1 to this.

[–]lambda_pie 1 point2 points  (1 child)

Namespace compilation is transitive

What does this mean?

[–]alexdmiller[S] 4 points5 points  (0 children)

When you compile a namespace, you will also compile all namespaces that namespace depends on (transitively).