dundalek/closh

Explore possibility to run via Planck (or other clojure environments)

dundalek opened this issue · 4 comments

Existing JVM and Lumo runtimes have a big footprint and relatively slow startup time.

Planck would be interesting target for scripting because it is quite small (Linux binary is around 4 MB) and starts instantly.

Tried out Planck just now, but it seems Lumo is about 2x faster (when starting) and uses about the same amount of VM, while Joker (a Clojure interpreter written in Go) is roughly 4x faster and uses substantially less VM, at least on my OSX laptop on Ubuntu 16.04 desktop machines.

Is there something I'm missing that prevents me seeing the improvements you describe, above?

craig@doe:~$ xtime joker -e '(println "i am here")'
i am here
u=0.06 s=0.00 r=0.04 cpu=130% kBresavg=0 kBresmax=16856 kBundata=0 kBunstack=0 kBtext=0 Bpagsiz=4096 kBavgtot=0
  fsin=0 fsout=0 sockrcv=0 socksnt=0 pfmaj=0 pfmin=2413 vol=174 invol=5 signals=0 swaps=0
  rc=0 joker -e (println "i am here")
craig@doe:~$ xtime joker -e '(println "i am here")'
i am here
u=0.05 s=0.00 r=0.04 cpu=146% kBresavg=0 kBresmax=17056 kBundata=0 kBunstack=0 kBtext=0 Bpagsiz=4096 kBavgtot=0
  fsin=0 fsout=0 sockrcv=0 socksnt=0 pfmaj=0 pfmin=2492 vol=205 invol=6 signals=0 swaps=0
  rc=0 joker -e (println "i am here")
craig@doe:~$ xtime joker -e '(println "i am here")'
i am here
u=0.03 s=0.00 r=0.02 cpu=123% kBresavg=0 kBresmax=16792 kBundata=0 kBunstack=0 kBtext=0 Bpagsiz=4096 kBavgtot=0
  fsin=0 fsout=0 sockrcv=0 socksnt=0 pfmaj=0 pfmin=2312 vol=196 invol=11 signals=0 swaps=0
  rc=0 joker -e (println "i am here")
craig@doe:~$ xtime lumo -e '(println "i am here")'
i am here
u=0.52 s=0.04 r=0.72 cpu=78% kBresavg=0 kBresmax=109744 kBundata=0 kBunstack=0 kBtext=0 Bpagsiz=4096 kBavgtot=0
  fsin=139544 fsout=0 sockrcv=0 socksnt=0 pfmaj=384 pfmin=19837 vol=827 invol=13 signals=0 swaps=0
  rc=0 lumo -e (println "i am here")
craig@doe:~$ xtime lumo -e '(println "i am here")'
i am here
u=0.43 s=0.02 r=0.34 cpu=130% kBresavg=0 kBresmax=110188 kBundata=0 kBunstack=0 kBtext=0 Bpagsiz=4096 kBavgtot=0
  fsin=0 fsout=0 sockrcv=0 socksnt=0 pfmaj=0 pfmin=19759 vol=226 invol=6 signals=0 swaps=0
  rc=0 lumo -e (println "i am here")
craig@doe:~$ xtime lumo -e '(println "i am here")'
i am here
u=0.49 s=0.05 r=0.44 cpu=124% kBresavg=0 kBresmax=110116 kBundata=0 kBunstack=0 kBtext=0 Bpagsiz=4096 kBavgtot=0
  fsin=0 fsout=0 sockrcv=0 socksnt=0 pfmaj=0 pfmin=19848 vol=215 invol=3 signals=0 swaps=0
  rc=0 lumo -e (println "i am here")
craig@doe:~$ xtime plk -e '(println "i am here")'
i am here
u=1.08 s=0.13 r=0.94 cpu=129% kBresavg=0 kBresmax=129584 kBundata=0 kBunstack=0 kBtext=0 Bpagsiz=4096 kBavgtot=0
  fsin=22424 fsout=0 sockrcv=0 socksnt=0 pfmaj=111 pfmin=29369 vol=3450 invol=571 signals=0 swaps=0
  rc=0 plk -e (println "i am here")
craig@doe:~$ xtime plk -e '(println "i am here")'
i am here
u=1.06 s=0.11 r=0.79 cpu=147% kBresavg=0 kBresmax=129892 kBundata=0 kBunstack=0 kBtext=0 Bpagsiz=4096 kBavgtot=0
  fsin=848 fsout=0 sockrcv=0 socksnt=0 pfmaj=13 pfmin=29443 vol=3148 invol=974 signals=0 swaps=0
  rc=0 plk -e (println "i am here")
craig@doe:~$ xtime plk -e '(println "i am here")'
i am here
u=1.16 s=0.05 r=0.81 cpu=149% kBresavg=0 kBresmax=130120 kBundata=0 kBunstack=0 kBtext=0 Bpagsiz=4096 kBavgtot=0
  fsin=176 fsout=0 sockrcv=0 socksnt=0 pfmaj=3 pfmin=29632 vol=2937 invol=2025 signals=0 swaps=0
  rc=0 plk -e (println "i am here")
craig@doe:~$

Joker looks pretty cool! I hope one day a more light-weight Clojure implementation with native executable support will appear. My favorite would probably be something written Rust, but C or Go might do as well. Or if Graal gets smart enough for self-hosted Clojure.

It would be cool to try to port Closh to Joker and gain the smaller footprint benefits. I haven't dug too deep into Joker but I am afraid that its Clojure-compatibility might be too limited so porting would require significant rewrites and effort. I am also not proficient with Go myself.

With Planck we could probably get to a binary size under 10 MB which would result in potential to use Closh for scripting, for example in deploy scripts. Since Planck runs CLJS then the porting effort should be reasonable given the benefits.

Thanks for the benchmarking. Good point on the memory usage, I did not realize that Planck consumes a quite large amount of memory.

Based on some performance analysis I did a couple of months ago, Go looked surprisingly good compared to C:

https://burleyarch.com/2018/09/08/performance-of-lispzerogo-vs-lispzero-c/

I haven't looked into Rust yet, but the above exercise required me to learn (enough of) Go, so I'm not averse to exploring Rust similarly if there's evidence its performance would be enough better (say, by an Order of Magnitude (OoM)), in terms of startup time and VM consumption (the two big issues with JVM and JS runtimes, compared to, say, starting up bash), to make that effort worthwhile.

Before doing the above analysis, I tried to identify "base performance" of (mostly) Lisp-y environments, and found Clojure and ClojureScript to be wanting compared to some others (that I assume are written in C). Haven't written up that analysis yet.

My current "ask" is for a minimal-overhead Clojure shell that can be used in lieu of bash for scripting. (I'm also interested in interactive use, but not as much.)

Fortunately, around the time I was thinking maybe I'd have to write a Clojure interpreter in C, I discovered joker. Since that was written in Go, I looked into its performance and found it excellent (well within a OoM of C for my simple tests).

Then, when I was about to hand-convert my primitive Lisp interpreter from C into Go, I discovered c2go, so used that.

Not sure what similar offerings might be around for Rust, D, or whatever else is worth looking into, but for now, having joker in hand, with Go's performance suggesting plenty of headroom for improvement (in fact a recent change to joker resulted in 4x startup-time improvement according to my perf analysis), I'd need a pretty good reason (performance and/or funding) to consider writing a Go interpreter in another language.

But I'm a long ways away from understanding all the ins and outs of the Clojure ecosystem, including what features are missing from joker that closh or anything else might need, how difficult it would be to implement, etc. E.g. joker has no transducers (yet).....

So far I've been mostly focusing on the interactive mode of the shell where having full-size Clojure and the ecosystem of libraries is great for quick iteration and experimentation. But recently I've been getting feedback about script mode so this gains my interest too.

To get an idea of the scope of an implementation, here is a basic overview of closh components as it is now:

  • reader - Since most shell operators can be just read as symbols writing custom reader could be avoided so reading is done with tools.reader. It needed minor tweaks to read things like IP addresses (e.g. 8.8.8.8 which would be an invalid number) or paths (e.g. /a/path/to/file which would be an invalid symbol). Thanks to tools.reader being written in Clojure the customizations were fairly simple.
  • parser - clojure.spec is used for parsing, specs look like a grammar for parser generators. Doing conform transforms the input into AST.
  • compiler - This is just a data transformation using plain clojure functions. Generated code uses the pipeline.
  • pipeline - Functionality to run sequence of processes and pipe data.
  • platform - Abstracted functions to spawn processes and redirect IO. Abstractions for JVM and node.js provide almost identical API which the pipeline utilizes.
  • frontend - This provides the repl for interactive use. For CLJS version we use internal node.js readline module. The CLJ version runs on clojure.main/repl with rebel-readline.

The reader customization, platform and frontends have separate implementations for CLJ and CLJS versions. The code for parser, compiler and pipeline is shared (using common CLJC files).

From my limited understading of joker I am afraid that only the pipeline code can be salvaged. Basically everything else would need to get reimplemented.