Fork me on GitHub
#clojure-dev
<
2019-09-12
>
ikitommi04:09:44

Hi. Would like to revisit the client-side compilation cache for libraries (https://clojureverse.org/t/deploying-aot-compiled-libraries/2545/6). I think this is really important for the developer ux as the batteries-included web stacks have a load time of 10+sec just to produce a hello world server. If the dependencies haven’t changed (mostly dont between repl startups), the compiler would use client-side AOTed classed. Is there any work related to this ongoing? Is this something that someone from the non-core team could try to solve as I guess you Alex & Rich are busy doing all other things too?

ikitommi04:09:44

Here’s a example what happens when adding 2 useful utility libs to an utility lib:

:; just the code
(time (require '[malli.core]))
"Elapsed time: 273.110534 msecs"

;; with borkdude/sci
(time (require '[malli.core]))
"Elapsed time: 1303.306083 msecs"

;; with borkdude/sci + borkdude/edamame
(time (require '[malli.core]))
"Elapsed time: 1913.595456 msecs"

dominicm07:09:44

I think this is in Alex's list, I asked about this a while ago in this channel. In particular I was frustrated about the load time of core async.

ghadi13:09:44

I have something that cuts core.async load time by 5 when AOTed @dominicm , compatibly

❤️ 1
dominicm13:09:38

What about without AOT? My problem is that it takes 10s to require a library if it requires core async, making the library seem "heavy". It's largely aesthetic.

ghadi13:09:46

i don't care about no AOT because you have to invoke the compiler

dominicm13:09:41

For me, the caching is more interesting. I actually don't care too much about production start-up time.

ghadi13:09:58

understood, but caching will get no faster than AOT, because caching relies upon the AOT artifacts

dominicm13:09:48

For sure, admittedly I'd assumed that core async was reasonably fast when AOTd, but that's probably not the case if you're looking into it.

ghadi13:09:32

my use case is stuff like AWS Lambdas

ghadi13:09:37

where you pay for init

dominicm13:09:56

Yeah, it really matters there. Although I really like the datomic ions model and I'll probably do the thin lambda model next time.

ghadi13:09:13

I can't make sense of the timings above without researching what is being loaded. Do you know @ikitommi ?

ikitommi13:09:22

@ghadi In my example, the actual libraries don’t matter. The library source gets recompiled every time a repl starts, even if the dependencies haven’t been changed. Local cache would be sweet. Here’s load time of Schema on my macbook:

➜  ~ clj -Sdeps '{:deps {prismatic/schema {:mvn/version "1.1.12"}}}'
Clojure 1.10.0
user=> (time (require '[schema.core :as s]))
"Elapsed time: 1117.321478 msecs"
nil

ikitommi13:09:04

but in my case, both sci & edamame use a (different) inlined version of tools.reader, discussed with @borkdude about that, will make a PR where tools.reader is used as dependency. Should make the code load faster.

ikitommi13:09:33

e.g. one (AOT’ed?) version instead of 2*source codes.

ghadi13:09:15

not invoking the compiler is always faster than invoking the compiler

universe_guy 1
partywombat 1
ghadi13:09:03

in other words, if we had a class cache (AOT'ed assets), would that be enough?

ghadi13:09:39

in the case of core.async: no. It loads tools.analyzer, tools.reader, and the go compiler, when it doesn't need it

ikitommi13:09:42

so that once they are locally compiled, the locally compiled (AOT’ed) version would be used if the deps don’t change? that would be totally awesome

ghadi14:09:17

you could do AOT stashing with changing deps, as long as the cache key is sufficiently smart @ikitommi

ghadi14:09:48

this is a large part of perceived startup time, but not the totality of it

ghadi14:09:08

many projects with unnecessary dependencies (cultural issue) dependency granularity can be large

dominicm15:09:10

Is there any downside to using the aot version of a dependency when available?

dominicm15:09:37

I suppose if the user doesn't load an aot version in their code, the aot version will be loaded as priority?

mikerod15:09:09

@dominicm I’ve certainly seen issues around ns order of reloading when mixing AOT and JIT stuff together over the years

mikerod15:09:21

Plenty of CLJ issues on Jira around it. Many fixed, but still there are situations

mikerod15:09:47

I’m not sure what context you are aiming at - is this for dev-time? I’d think that could potentially get annoying due to these sort of concerns - but I guess I’m not the definitive source of wisdom here.

mikerod15:09:24

I’ve just had to track some really tricky classloader problems in the past around JIT compile reloads and AOT loaded classes

dominicm15:09:27

I'm thinking of AOT'd dependencies

dominicm15:09:41

In case that changes your answer?

mikerod15:09:55

as in, the dependencies are deployed to the repo AOT’ed or you AOT them locally?

mikerod15:09:10

Also, keep in mind that an AOT’ed lib, by default, does not know it’s own “boundaries”

mikerod15:09:36

it’ll AOT all it’s own deps at the same time. build tools, like lein have a feature that strips out the non-lib-project’s class files to avoid problems with this

mikerod15:09:57

so the issue here ends up being when you AOT 2 of your own libs A and B, and they both use C

mikerod15:09:11

both will AOT C, and if C is not the same between the two, you will have classfiles for both on classpath

mikerod15:09:20

sometimes this can be problematic

mikerod15:09:37

with JIT, you can just push your own C

ghadi15:09:43

not talking deploying anything AOT

ghadi15:09:02

still distributing source

mikerod15:09:07

good. distributing source seems to have other advantages as well (source-compatibility is perhaps stronger than “binary” across clj versions, end-user gets to choose how to compile their “whole app”, with things like direct-linking, etc)

mikerod15:09:35

but anyways, @ghadi knows far more than I do on this topic, so I shouldn’t be chiming in I think hah

dominicm15:09:30

Would a non transitive aot be possible?

mikerod15:09:12

but some build tools (`lein` is really the one I know does this), will remove the dependency classfiles after the AOT is complete - which is mentioned in the closing remarks here https://clojure.atlassian.net/browse/CLJ-322?focusedCommentId=36994&amp;page=com.atlassian.jira.plugin.system.issuetabpanels%3Acomment-tabpanel#comment-36994

dominicm15:09:21

I see, so I should filter the transitive myself if I do that.

hiredman16:09:32

stripping out the transitive aot stuff will also cause breakage

mikerod16:09:41

not if you place the source for JIT those on the classpath during the actual runtime ?

mikerod16:09:00

this is how it ends up working if you do use lein to AOT a lib project (not something I like to do still)

mikerod16:09:50

The AOT’ed classes should be able to initiate their dependency JIT again via their load forms they initialize with

mikerod16:09:59

those shouldn’t need to be coming from also AOT’ed deps

hiredman16:09:11

if you use the lib in a project, and the project's dependencies override the libs

mikerod16:09:23

there are cases where AOT is essential of course, for direct interop things in some cases

mikerod16:09:47

> and the project’s dependencies override the libs with an incompatible version I think you mean?

mikerod16:09:59

if so, sure - that’ s the world of jar (dep) hell though

hiredman16:09:16

source compatibility is not the same thing as binary compatibility

mikerod16:09:36

then I definitely don’t follow you

mikerod16:09:44

and I know that’s not the same - just not sure how it’s relevant

hiredman16:09:46

so for example, if you had a library that removed a ^long type hint on an argument

hiredman17:09:21

that isn't a breaking change source wise, but that will break aot compiled code

hiredman17:09:29

lein's approach isn't something where say rich sat down and said "oh this will work well with how aot compilation works" lein's approach is phil got frustrated with how transitive aot compilation works, and just decided to kludge deleting some stuff, and that'll be fine right?

mikerod17:09:49

Yeah, I understand that part

mikerod17:09:58

Also, I agree that isn’t strictly “source compatible”

mikerod17:09:07

but it is what I’d consider an edge case

hiredman17:09:10

it is source compatible

mikerod17:09:11

it’s interop-sensitive stuff - hints

hiredman17:09:20

it isn't binary (byte code) compatible

mikerod17:09:33

I mean, consuming via AOT’ed stuff puts more strictness on what source you can reliably JIT - in particular, around interop details - such as hints

mikerod17:09:42

either way, I won’t drag it on. I’m not a fan of AOT’ing libs - so not really taking a stance in favor of it.

seancorfield17:09:29

If any sort of AOT cache is introduced, there had better be a way to disable it -- or to not have it as default and need to opt into it. Given all we know about how problematic it can be at the file/lib level, I dread to think what sort of weird problems beginners are going to run into if CLI/`deps.edn` forces this on them 😞

👍 1
1
💯 1
seancorfield17:09:33

(I'm no fan of AOT as it currently works and avoid it at all costs)

hiredman17:09:57

oh, hah, here I am commenting on clj-322 about why boot's shift isn't a complete solution, and lein's removing of transitive class files was turned off by default at some point(I don't know the current status of that feature) with a link to the lein issue showing what it broke and why it was turned off

mikerod17:09:28

@hiredman I think lein is on by default again now

mikerod17:09:24

but I do remember reading this

hiredman17:09:26

it doesn't look like it does it by default, even though the sample.project.clj shows setting it to true, but it is hard to tell

hiredman17:09:49

https://github.com/technomancy/leiningen/blob/master/src/leiningen/compile.clj#L105-L111 it only deletes if that key is set in the project, and a search in the repo doesn't show that key being set anywhere by default (sample.project.clj does show that key set with the opposite of its default value)

mikerod18:09:00

@hiredman recently I saw people at work AOT’ing libs and i was concerned - but then noticed it seemed to automatically be doing this clean

mikerod18:09:05

(I still suggested not to do it)

mikerod18:09:14

so that’s where I came up with “I think it is automatically doing it”

mikerod18:09:34

easy enough to check though - you could be right. I just am going from an experience in one of the recent’ish lein versions. also, I guess this is a topic for lein at that point.

ghadi18:09:22

the idea is caching AOT'ed artifacts -- not distribution

ghadi18:09:52

libs people use are compiled when they're loaded, a cache would be about skipping the compilation

aw_yeah 1
ghadi18:09:05

many different ways to organize such a cache

ghadi18:09:30

many wrong ways, too 🙂

👍 1
1
🙃 1
1
andy.fingerhut18:09:08

Are there perhaps libs that are just so danged dynamic in the compiled code they generate, based upon run-time factors when they are loaded, that they would be too difficult to cache? I don't have an example in hand, but there must be some libs that change what they def/defn based upon some JVM property string or something.

andy.fingerhut18:09:38

or custom environment variables

hiredman18:09:33

yeah, a cache on the tool side, where the tool can observe the inputs and see if they change vs. aot compiling and publishing the artifacts is the most likely way to have it actually work

👍 1
hiredman18:09:07

I could imagine tools.deps keeping a caching of per classpath classes directories

ghadi18:09:47

a cache needs to be dependency-aware

ghadi18:09:05

still doesn't solve library bloat

seancorfield19:09:48

I'd be OK with the tooling compiling and caching non-local libs I depend on, but I wouldn't want it compiling and caching either :local/root or :paths dependencies.

1
1