clojure-dev

Issues: https://clojure.atlassian.net/browse/CLJ | Guide: https://insideclojure.org/2015/05/01/contributing-clojure/
ikitommi 2019-09-12T04:51:44.103600Z

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?

ikitommi 2019-09-12T04:53:44.105300Z

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"

dominicm 2019-09-12T07:08:44.106800Z

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.

ghadi 2019-09-12T13:02:44.108100Z

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

1❤️
ghadi 2019-09-12T13:06:13.109200Z

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

dominicm 2019-09-12T13:12:38.109400Z

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.

ghadi 2019-09-12T13:14:46.109600Z

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

dominicm 2019-09-12T13:25:41.110700Z

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

ghadi 2019-09-12T13:27:58.111Z

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

dominicm 2019-09-12T13:30:48.111200Z

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.

ghadi 2019-09-12T13:31:32.111400Z

my use case is stuff like AWS Lambdas

ghadi 2019-09-12T13:31:37.111600Z

where you pay for init

dominicm 2019-09-12T13:32:56.111900Z

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

ikitommi 2019-09-12T13:40:22.116900Z

@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

ikitommi 2019-09-12T13:42:04.118700Z

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.

ikitommi 2019-09-12T13:42:33.119300Z

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

ghadi 2019-09-12T13:43:15.119800Z

not invoking the compiler is always faster than invoking the compiler

11
ghadi 2019-09-12T13:44:03.120600Z

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

ghadi 2019-09-12T13:44:39.121800Z

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

ikitommi 2019-09-12T13:44:42.122Z

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

ghadi 2019-09-12T14:19:17.123900Z

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

ghadi 2019-09-12T14:19:48.124800Z

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

ghadi 2019-09-12T14:21:08.126Z

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

dominicm 2019-09-12T15:07:10.126600Z

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

dominicm 2019-09-12T15:07:37.127400Z

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

2019-09-12T15:12:09.128Z

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

2019-09-12T15:12:21.128500Z

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

2019-09-12T15:12:47.129200Z

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.

2019-09-12T15:13:24.130200Z

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

dominicm 2019-09-12T15:13:27.130300Z

I'm thinking of AOT'd dependencies

dominicm 2019-09-12T15:13:41.130700Z

In case that changes your answer?

2019-09-12T15:13:55.131100Z

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

2019-09-12T15:14:10.131500Z

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

2019-09-12T15:14:36.132100Z

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

2019-09-12T15:14:57.132600Z

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

2019-09-12T15:15:11.133Z

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

2019-09-12T15:15:20.133200Z

sometimes this can be problematic

2019-09-12T15:15:37.133700Z

with JIT, you can just push your own C

ghadi 2019-09-12T15:15:43.133900Z

not talking deploying anything AOT

ghadi 2019-09-12T15:16:02.134300Z

still distributing source

2019-09-12T15:17:07.135400Z

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)

2019-09-12T15:17:35.135900Z

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

dominicm 2019-09-12T15:24:30.136300Z

Would a non transitive aot be possible?

2019-09-12T15:26:49.136500Z

discussed in this old issue https://clojure.atlassian.net/browse/CLJ-322

2019-09-12T15:27:12.137Z

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&page=com.atlassian.jira.plugin.system.issuetabpanels%3Acomment-tabpanel#comment-36994

dominicm 2019-09-12T15:39:21.137700Z

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

2019-09-12T16:01:32.138600Z

stripping out the transitive aot stuff will also cause breakage

2019-09-12T16:56:21.138800Z

@hiredman why?

2019-09-12T16:56:41.139200Z

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

2019-09-12T16:57:00.139600Z

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

2019-09-12T16:57:50.140400Z

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

2019-09-12T16:57:59.140800Z

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

2019-09-12T16:58:11.141200Z

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

2019-09-12T16:58:23.141500Z

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

2019-09-12T16:58:47.141800Z

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

2019-09-12T16:58:59.142200Z

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

2019-09-12T16:59:16.142600Z

source compatibility is not the same thing as binary compatibility

2019-09-12T16:59:36.143Z

then I definitely don’t follow you

2019-09-12T16:59:44.143400Z

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

2019-09-12T16:59:46.143600Z

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

2019-09-12T17:00:21.144100Z

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

2019-09-12T17:06:29.146600Z

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?

2019-09-12T17:07:49.146800Z

Yeah, I understand that part

2019-09-12T17:07:58.147100Z

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

2019-09-12T17:08:07.147500Z

but it is what I’d consider an edge case

2019-09-12T17:08:10.147700Z

it is source compatible

2019-09-12T17:08:11.147900Z

it’s interop-sensitive stuff - hints

2019-09-12T17:08:20.148200Z

it isn't binary (byte code) compatible

2019-09-12T17:08:33.148600Z

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

2019-09-12T17:12:42.149400Z

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.

seancorfield 2019-09-12T17:14:29.151100Z

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➕
seancorfield 2019-09-12T17:15:33.152Z

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

2019-09-12T17:20:57.153600Z

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

2019-09-12T17:33:28.154100Z

@hiredman I think lein is on by default again now

2019-09-12T17:34:24.154400Z

but I do remember reading this

2019-09-12T17:38:26.155100Z

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

2019-09-12T17:40:49.156200Z

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)

2019-09-12T18:04:00.157400Z

@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

2019-09-12T18:04:05.157600Z

(I still suggested not to do it)

2019-09-12T18:04:14.157900Z

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

2019-09-12T18:04:34.158500Z

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.

ghadi 2019-09-12T18:05:22.159800Z

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

ghadi 2019-09-12T18:05:52.160500Z

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

1
ghadi 2019-09-12T18:06:05.160900Z

many different ways to organize such a cache

ghadi 2019-09-12T18:06:30.161200Z

many wrong ways, too 🙂

1👍1➕11🙃
2019-09-12T18:14:08.162400Z

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.

2019-09-12T18:14:38.162700Z

or custom environment variables

2019-09-12T18:18:33.163800Z

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👍
2019-09-12T18:21:07.164800Z

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

ghadi 2019-09-12T18:23:38.165100Z

yup

ghadi 2019-09-12T18:23:47.165400Z

a cache needs to be dependency-aware

ghadi 2019-09-12T18:24:05.165700Z

still doesn't solve library bloat

seancorfield 2019-09-12T19:19:48.167Z

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.

11➕