tools-deps

Discuss tools.deps.alpha, tools.build, and the clj/clojure command-line scripts! See also #depstar #clj-new
ghadi 2020-07-31T00:23:35.454300Z

(isn't container support the default now in jdk11?)

seancorfield 2020-07-31T00:31:21.458Z

@pmonks "I don’t know why that’s happening (this is my first time using depstar, so I may have messed something up there)" -- no, that's just how AOT works. In order to compile your main namespace, it has to load (compile) all the namespaces it depends on so you get pretty much your whole program compiled to .class files. That's just normal. What (:gen-class) does is flag that particular namespace to be generated as a class with methods -- functions beginning with - (by default) will become named methods on that class, instead of (what Clojure usually does which is) to compile each function to its own class with an .invoke() method. So your -main function becomes a public static method of the class (which is what Java expects).

2020-07-31T00:33:08.460Z

I didn’t realise AOT-compiled-Clojure couldn’t “thunk” into on-demand-compiled-Clojure.

seancorfield 2020-07-31T00:33:22.460300Z

Leiningen tries to do some fancy post-AOT clean up I think, to delete a lot of .class files that aren't directly part of your project so you don't get "everything" compiled? Can't remember whether that is the default or an option.

2020-07-31T00:33:32.460800Z

(I almost never use AOT, so have minimal experience with it)

2020-07-31T00:33:54.461500Z

Yeah and all of my previous experience has been with leiningen.

seancorfield 2020-07-31T00:34:04.461800Z

If you don't want other nses compiled, don't :require them in your main ns -- instead use runtime require/`resolve` (or the nice new requiring-resolve in 1.10).

2020-07-31T00:34:28.462Z

Thanks - looking now…

2020-07-31T00:34:58.462900Z

I don’t particularly care either way, tbh. Have just heard folks here recommend against AOTing much, if any, of a codebase (if possible).

seancorfield 2020-07-31T00:35:45.463700Z

One good test to run here would be to build your uberjar without -C and -m on depstar -- which would not AOT anything -- and then run it with java -cp path/to/the.jar clojure.main -m mybot.main

seancorfield 2020-07-31T00:36:40.464700Z

That will do compile-on-demand and if it behaves the same as the original clj memory usage, then compile-on-demand might be responsible for the larger memory usage.

2020-07-31T00:38:05.466Z

Ok now that my 5pm US-PT (actually midnight UTC) batch job is done, I’ll give that a try. 😉

1
2020-07-31T00:42:22.466800Z

So locally, removing AOT seems to have resumed the “higher memory usage” scenario.

2020-07-31T00:42:35.467300Z

Next up, heroku env.

seancorfield 2020-07-31T00:45:11.468600Z

So it sounds like there's some overhead from loading source and compiling on demand that doesn't go away? I'm a bit surprised it doesn't all get GC'd eventually.

2020-07-31T00:47:07.468800Z

Yeah me too. Though I only have up to 24 hours of continuous operation available to look at, since Heroku forcibly restarts the dyno (VM) at least once per day. (as mentioned above, that includes at least 24 calls to (System/gc)).

2020-07-31T00:49:34.469600Z

Deployed. Now we wait for a bit for it to show up on the Heroku dashboard.

seancorfield 2020-07-31T00:51:47.471800Z

I can't say I noticed a difference at work when we switched from compile-on-demand to AOT, but then our smallest process runs with 1G heap so a few 10s of MBs wouldn't really be noticeable...

2020-07-31T00:53:24.474700Z

Makes sense. I can definitely see the CPU cost of on-demand compilation at startup showing up. 😉 Memory graph still a bit inconclusive…

2020-07-31T00:54:06.476700Z

Calling System/gc is silly. Predicting what effect (if any)calling that method has is hard, so calling it, and then trying to reason from the basis of calling it is not helpful

2020-07-31T00:54:42.478300Z

Sure, but it can’t hurt in a non-interactive app.

2020-07-31T00:54:45.478500Z

You should look at the gc logs, and look at the sizes of the generationd

2020-07-31T00:55:04.479300Z

And even a multi-second pause in this bot is preferable to exceeding memory (which puts me in Heroku “R14” jail…).

ghadi 2020-07-31T00:55:29.480500Z

how will you be in jail if the memory is bounded?

2020-07-31T00:55:50.481500Z

Calling System/gc will never stop you from running out of memory

✔️ 1
2020-07-31T00:56:09.482400Z

That is how automatic garbage collection works

2020-07-31T00:56:26.483Z

By exceeding the 512MB quota for everything inside the VM (JVM heap + JVM metaspace + memory required by any other processes + OS memory requirements (caches, etc.) + …).

2020-07-31T00:56:53.484Z

It can only monkey with the heuristics in the gc

2020-07-31T00:57:06.484500Z

Regardless, I tested it first (obvs) and it improved memory utilisation over longer periods of time.

2020-07-31T00:57:11.484800Z

You should set a lower max heap for the jvm then

2020-07-31T00:57:17.485100Z

I don’t set the heap - the container does.

2020-07-31T00:57:22.485500Z

Remember I’m running with -XX:+UseContainerSupport.

2020-07-31T00:57:33.486Z

Then have the container do a better job

2020-07-31T00:57:40.486400Z

Not to mention that controlling the heap doesn’t help with metaspace anyway.

2020-07-31T00:57:41.486700Z

If you are exceeding the limits

2020-07-31T00:57:46.486800Z

(which is where on-demand compiled classes etc. go)

2020-07-31T00:58:04.487100Z

Know anyone who works at Heroku to improve their VM impl.? Cause I don’t. 😉

2020-07-31T00:58:17.487600Z

It’s not a container.

2020-07-31T00:58:21.488Z

It’s a “dyno”.

2020-07-31T00:58:32.488500Z

I don’t provide anything but the stuff that gets fed to the JVM.

2020-07-31T00:59:03.489100Z

(and while Heroku also supports Docker, I have negative interest in switching to that model of app construction)

2020-07-31T00:59:55.489800Z

So it’s still a bit early to be sure, but it looks like it was AOT that was saving much of the memory:

2020-07-31T01:00:12.490800Z

(that second “hump” is when I deployed with AOT disabled)

2020-07-31T01:01:10.493Z

You should check the different generations, compilation may result in extra old generation garbage, that is never collected because it never needs to be

2020-07-31T01:03:24.494600Z

From what I’m seeing most of the savings are in the metaspace, not the heap.

2020-07-31T01:03:36.494900Z

(recalling that old generation is on-heap)

2020-07-31T01:05:08.496Z

Which makes sense if on-demand compiled Clojure code is somehow more off-heap-expensive (e.g. metaspace, code-cache) than AOT compiled Clojure code.

seancorfield 2020-07-31T01:05:18.496500Z

In your original numbers, you only showed 11MB difference in metaspace, but 44MB in heap.

2020-07-31T01:05:27.496800Z

Yeah that’s local.

ghadi 2020-07-31T01:05:33.497Z

if you can AOT, AOT

seancorfield 2020-07-31T01:06:06.498100Z

Aye, AOT as the last step of an application, prior to production deployment seems like a reasonable step to me.

2020-07-31T01:06:11.498300Z

@ghadi I probably will, given the memory restrictions in this environment. That doesn’t help me understand why this is happening though.

2020-07-31T01:06:36.499100Z

To date I’ve never used AOT, and if there is a substantial (~40% memory saving, for this one bot) saving to be had, that could be a discriminating factor in choosing AOT vs on-demand compilation for other apps in future.

seancorfield 2020-07-31T01:06:48.499400Z

You'll need VisualVM or something similar to see what's actually going on inside the heap/metaspace...

2020-07-31T01:07:30Z

@seancorfield no easy way to hook that up to Heroku, though it runs an agent that does break down on-heap vs off-heap memory usage for their dashboard.

seancorfield 2020-07-31T01:07:58.000700Z

Right, I meant locally, with a low heap size to vaguely mirror what's happening on Heroku.

2020-07-31T01:09:47.002Z

Yeah - I’m doing that, but the absolute numbers are pretty different than what I see on Heroku. Possibly because it’s hard to limit the JVM’s off-heap memory usage (-Xmx only affects the heap), so it’s happily slurping up the (copious, compared to Heroku) memory on my laptop.

2020-07-31T01:11:11.003800Z

BTW, here’s a more complete view of heap vs off-heap usage reported by heroku. The two deployments (the first to AOT uberjar, then back to non-AOT uberjar) are pretty obvious:

2020-07-31T01:11:46.004300Z

Stand by - I accidentally chopped off “total memory usage”.

ghadi 2020-07-31T01:11:55.004500Z

we're in the wrong channel, btw

2020-07-31T01:12:17.005200Z

We are now, yes. Originally this looked like tools.deps issue.

ghadi 2020-07-31T01:12:48.005800Z

I concur with @hiredman’s suggestion to get better details about heap regions.

ghadi 2020-07-31T01:13:24.006700Z

there are several interesting metrics depending on the GC used (G1 by default on 11)

2020-07-31T01:13:28.006800Z

Here’s a better picture:

2020-07-31T01:15:07.007800Z

Even at this granularity, it’s pretty clear that the delta in the non-heap memory usage is greater than that on-heap.

2020-07-31T01:15:18.008100Z

i.e. looking at the heap regions likely won’t help

2020-07-31T01:16:37.008900Z

(and yes the default Heroku dashboard sucks - it’s deliberately setup to encourage one to pay for a better one)

2020-07-31T01:18:35.009900Z

Sadly I have to step away from this now, but it looks like AOT is causing the difference, even if it’s unclear why. Thanks for indulging me on this!

ghadi 2020-07-31T01:31:05.010600Z

not a mystery to me. compiling is more work than not compiling

2020-07-31T02:06:07.011100Z

More work == CPU load, sure. But that work, once complete, shouldn’t leave long-lived garbage hanging around.

2020-07-31T02:08:21.012100Z

Well, unless the JVM does tricks for AOT-compiled classes that it can’t use for dynamically generated (non-disk-resident) classes (e.g. mmap’ing pre-compiled .class files rather than keeping them resident in memory at all times, unloading disk-resident .class files once they’ve been JITted, etc.).

2020-07-31T02:28:53.012800Z

Or if the Clojure compiler has caches that don’t get primed if it’s not called upon to dynamically compile some Clojure code.

alexmiller 2020-07-31T20:57:50.013600Z

A new clj release candidate (what we were formerly calling "dev" releases) is now available - 1.10.1.619 (see https://github.com/clojure/homebrew-tools#version-archive-tool-releases for installation info) • Fixes -Spom regression in overwriting groupId in existing pom.xml files • Improvements in error handling for -X • New: -F execution specifier to invoke an arbitrary function that takes a map at the command line: clj -Fclojure.core/pr :a 1 :b 2 => {:a 1 :b 2}

borkdude 2020-07-31T21:03:57.014700Z

Awesome, I already wondered why one had to specify a function in deps.edn instead of just being able to call any function

seancorfield 2020-07-31T21:05:21.015Z

(! 1038)-> clojure -Sdeps '{:aliases {:foo {:fn clojure.core/prn :args "test"}}}' -X:foo
Invalid :args for exec, must be map or alias keyword: "test"
(! 1039)-> clojure -Sdeps '{:aliases {:foo {:fn clojure.core/prn :args :bar} :bar "test"}}' -X:foo
"test"
🙂

seancorfield 2020-07-31T21:08:58.015500Z

Thanks for the quick fix to pom.xml groupId!

borkdude 2020-07-31T21:13:15.016100Z

Why not just pass the entire map as one cmd line arg?

$ clojure  -Fclojure.core/pr '{:a 1 :b 2}'
Key is missing value: {:a 1 :b 2}
or support both?

borkdude 2020-07-31T21:14:12.016800Z

not a big deal, but if the map is coming from a file, this could get a bit weird

alexmiller 2020-07-31T21:14:51.017300Z

because quoting sucks and it's more of a match to the -X

borkdude 2020-07-31T21:15:24.018Z

quoting sucks, but I bet you will soon be quoting the individual args as well, there is no escape 😉

😂 1
alexmiller 2020-07-31T21:15:24.018100Z

but I implemented both at different points :)

alexmiller 2020-07-31T21:15:31.018300Z

no pun intended :)

vlaaad 2020-07-31T21:45:41.019700Z

powershell's -X:alias still broken 😞

vlaaad 2020-07-31T21:46:19.020400Z

have you missed my suggested fix that handles splits to -X: and alias ?

if ($arg -eq "-X:") {
        $sym, $params = $params
        $ExecAlias += "$arg$sym", $params
      } else {
        $ExecAlias += $arg, $params
      }

alexmiller 2020-07-31T21:57:46.020700Z

yeah, I haven't had a chance to look at that yet

alexmiller 2020-07-31T21:57:51.020900Z

it's in the queue!

🙏 1
seancorfield 2020-07-31T22:38:35.021800Z

@alexmiller I'm a bit surprised this first version fails (in 1.10.1.619):

seanc@DESKTOP-QU2UJ1N:~/clojure$ clojure -Sdeps "{:aliases {:foo {:fn clojure.core/prn}}}" -X:foo :a 1
Invalid :args for exec, must be map or alias keyword: nil
seanc@DESKTOP-QU2UJ1N:~/clojure$ clojure -Sdeps "{:aliases {:foo {:fn clojure.core/prn :args {}}}}" -X:foo :a 1
{:a 1}

seancorfield 2020-07-31T22:42:02.022800Z

It's not a big deal but it worked before and I wasn't sure if it was a deliberate change or just an artifact of the implementation.

alexmiller 2020-07-31T22:49:57.023200Z

Yeah, that should probably work