top | item 22710604

Clojure on the Desktop

300 points| vlaaad | 6 years ago |vlaaad.github.io | reply

134 comments

order
[+] wildermuthn|6 years ago|reply
I once led a mid-sized team in building a desktop application with Clojure. It might sound nuts, but we used both the JVM and Electron, using both Clojure and ClojureScript. We solved all the performance issues we’d experienced with Electron by restricting it to serve as a desktop GUI API. Literally nothing was in Electron except a small package for handling these API requests: set a menu, open a window, load a page, etc. Even inside an open window, the ClojureScript code was loaded from and communicated with the JVM, rather than being bundled into Electron. You want your desktop app to both look great and perform great. Restricting Electron to its basics while moving everything else into another system is definitely one option to consider.
[+] kickopotomus|6 years ago|reply
I am actually currently working on something similar but instead we opted to use JxBrowser[1]. It's basically what Electron used to be but has Chromium embedded into the JVM instead of Node.

[1]: https://www.teamdev.com/jxbrowser

[+] sdegutis|6 years ago|reply
I honestly cannot imagine how that would be more performant than just using Electron by itself the typical way, unless this was a very CPU intensive application like a video editor.
[+] karatestomp|6 years ago|reply
Last I checked, due to how Electron’s structured, you basically have to decouple “logic” from the UI anyway, which makes it relatively easy/natural to make the “logic” side just a thin translation layer talking to some other language.
[+] truth_seeker|6 years ago|reply
Why did you guys use JVM at all ?

If it's for multi-core computation purpose it can be with Worker Threads in JS or CLJS.

[+] vijaybritto|6 years ago|reply
Was webassembly considered before going with this approach? It's completely okay to go in a way the team is most comfortable with. But if it was pure perf reasons then webassembly would solve it right?
[+] BubRoss|6 years ago|reply
What did this do and why was using multiple languages, an entire web browser and the jvm a good fit?
[+] dimitar|6 years ago|reply
Most of the comments are about the startup RAM use and distributable size, which is a criticism shared with Electron. The demo app is not attempting to address these particular weaknesses.

The point of the article is there are some applications where processing speed is important where Clojure's and JVMs concurrency and long running and large dataset performance is very important. And the ecosystem appears to be ready to make it possible to create apps where these strengths vis-a-vis Electron are combined with the other strengths of Clojure compared to low level languages (expressive syntax, interactive development).

[+] neurobashing|6 years ago|reply
not just that: we've come full circle. At one point, there were a bunch of Java/JVM desktop apps. They were widely disliked/mocked because they were heavyweight, had slow startup time, didn't use platform-standard widgets, etc. The web became an alternative to cross-platform apps.

Now we're seeing a proliferation of Electron apps - web apps - that are widely disliked/mocked because they are heavyweight, don't use platform-standard widgets, etc. And at least in this piece, an alternative is the JVM.

[+] slgeorge|6 years ago|reply
Yeah it's really interesting because it's a fully worked example of packaging a desktop application using a hosted approach. Personally, I'm more comfortable with that stack.

Also worth looking at is Vlaaad's Cljfx (https://github.com/cljfx/cljfx). And I just want to say the number of examples that Vlaaad has created for the project is amazing and really helpful - it's a great example of helping others on-board to use your project - thanks Vlaad.

[+] daxfohl|6 years ago|reply
Yeah I'd say the point of the article is to highlight the Clojure wrapper for JavaFX, providing an option to develop JavaFX desktop apps in an Om/Reagent fashion and playing to Clojure's strengths.

All the criticism here seems more like criticism of JavaFX itself.

[+] grandinj|6 years ago|reply
I routinely run my fairly and complex java UIs clamped to 256M of memory, and startup time is measured in a handful of seconds. Distribution size is not great I'll admit, unless you go to considerable effort to slim the JRE down.
[+] Scarbutt|6 years ago|reply
IME, regular JS is much faster than regular Clojure. You can make Clojure run faster than JS by writing Java with parenthesis, but then you defeat all the purpose of using Clojure in the first place.
[+] rvz|6 years ago|reply
Hmmm... Lets see the HN Reader example then...

95MB for the macOS DMG.

177MB for the actual example app.

For a typical hello world application like the HN example, it appears that it would be no better than running an electron-made version given that they both include their own versions of their respective runtimes. In this case, this includes the entire Java runtime which Clojure needs to run.

I think I would be better off with using either C++ with Qt5 or even FreePascal with the Lazarus IDE for this instead.

[+] mister_hn|6 years ago|reply
As suggested in the article, the usage of jpackage comes with a lot of drawbacks:

- still a JRE is shipped (you MUST tailor it to the bare minimum with the flags) -> can be + ~30-40MB

- OpenJFX/JavaFX relies on Qt (especially for the WebView) -> can grow up to + ~70MB

- all the legal documents and docs are shipped together (you have to remove them with a post-process script) -> + ~5-10MB

Using JVM for desktop apps is expensive in terms of space. Time can be reduced consistently thanks to the graal native compiler, but still a C++ project outperforms Java GUI applications by 500%

[+] ken|6 years ago|reply
Those other systems don't have Reagent. Reagent is simply the best way to write user interfaces I've seen in 25 years, bar none. The only comparable jump in productivity that I can recall is moving from assembly language to a compiler.

Spending time with a profiler, we can probably improve the size and speed of a Clojure application (I've never tried using it for desktop software). No amount of time playing with Qt/Lazarus is going to give you Reagent, though.

[+] positr0n|6 years ago|reply
~5 seconds from double clicking the app to the window appears (and starts to load HN) on my 2019 MBP.

I think this is a really cool library. I use clojure daily and love it. But this app isn't a very compelling demo over electron atm.

[+] mark_l_watson|6 years ago|reply
Good point.

Lately I have been learning Swift and SwiftUI and application sizes for macOS and iOS tend to be very small.

Except for not being a lazy evaluation language, Swift development feels a little like Haskell, which to me is a good thing. I learned Swift back when TensorFlow for Swift was released and having one language that is adequate for many types of applications is useful. That said, when I want to be happy programming I try to use Common Lisp or Racket. I am approaching my 40 year anniversary for using Common Lisp and there is definitely something really great about deep familiarity with a language and it’s dev tools.

EDIT: also +1 for FreePascal. Someone hired me to use FreePascal on a project several years ago. Good language, small learning curve, and small and efficient applications. On small application sizes: I recently discovered that if you build SBCL Common Lisp from source, there is a compression option for building standalone applications that you can enable that gets application size down to about 15 MB, with application startup around 75 milliseconds.

[+] vlaaad|6 years ago|reply
This example might not be the best with regards to application size because it's main purpose is to show that packaging is now easy. As you can see in the app itself or on the screenshot, it also includes browser to render linked pages — but that can be excluded if the size is a concern.
[+] StreamBright|6 years ago|reply
>> I think I would be better off with using either C++ with Qt5 or even FreePascal with the Lazarus IDE for this instead.

Or many other things. Not sure why are we so afraid to develop native applications with minimal overhead. V and Zig is going this direction.

[+] piokoch|6 years ago|reply
I've installed the application, it needs 840 MB of the memory. Firefox with more than 100 tabs opend consumes 1.2 GB, IntelliJ with a huge java project consumes 1.6 GB. I understand that this is proof of concept application, but what eats so much RAM? Is that the data that was read or the framework itself?
[+] imtringued|6 years ago|reply
It doesn't actually need 840MB of RAM. This is an inherent limitation of the way the JVM works. It will reserve a configured amount of memory upfront and grow the heap if memory is full until it reaches the configured maximum heap size. One especially annoying thing is that the JVM will not garbage collect until the heap is actually full. This means if you allocate a little bit here and there (like every single Java program in existence) over time you are going to reach the maximum configured heap. Once the JVM has allocated heap space it will never let go of it. Therefore you might as well assume that the minimum amount of memory a JVM process consumes is the maximum that it has been configured for. In this case the maximum was probably set to 840MB. The application itself probably only uses something like 50MB.

Remember those stupid arguments that a competent programmer can write well performing code [0] in any language? Here is the thing... You actually can't. Especially not with the JVM. Let's say you use a super efficient framework that is purely optimized for startup performance and low memory like Micronaut or Quarkus. When you look at your metrics you see that the Java program is consuming 10MB of heap space and pat yourself smugly on the shoulder thinking how well written these frameworks are. Tough luck. Even if you configure the heap size to something absurdly low like 32MB your JVM process will still use 120MB. Why? Because the JVM has a minimum footprint. It has lots of parked GC threads in the background that need 2MB stacks. It has to keep the class files in memory, keep profiling data, the generated code and lots of other things. If I need something that needs to consume as little memory as possible I just stick with C++ or Lua. With those two languages staying under 1 - 2 MB of RAM is trivial.

Also... it is possible your coworkers do not even set a maximum heap and just let it stay on the default values. That's how a tiny little CRUD app as a dashboard ended up allocating 10GB of RAM to itself on a server with 128GB RAM.

[0] well performing = low memory usage in this case

[+] vlaaad|6 years ago|reply
It does not need 840 MB of memory — it uses it because the purpose of this example is to show that packaging is now easy, and non-essential parts were left out to keep it simple. I just tried limiting it to 50 MB and it runs just fine.
[+] thu2111|6 years ago|reply
It doesn't really need 840mb. If you force a GC after waiting for it to load the stories it needs "only" 350mb. Try "jcmd main GC.run" (if you have any JDK 11+ installed).

The problem is Java sees lots of memory lying unused and figures ... why should I work harder to free it up? It'll waste CPU time and energy and I might need the memory later.

In theory it actually will do background GCs and free memory back to the OS anyway so I'm not sure why it's not happening here; possibly that feature needs to be explicitly activated or maybe I just didn't wait long enough to see it happen.

As for why it needs 350mb of RAM, let's take a quick look:

    $ jcmd main GC.heap_info                                                                                                                  
    Tue Mar 31 10:53:42 2020
    61074:
     garbage-first heap   total 102400K, used 28139K [0x0000000700000000, 0x0000000800000000)
      region size 1024K, 0 young (0K), 0 survivors (0K)
     Metaspace       used 58206K, capacity 65566K, committed 65868K, reserved 1101824K
      class space    used 10342K, capacity 12810K, committed 12928K, reserved 1048576K
So as a first approximation, even after a GC and subsequent heap shrink, it's holding onto about 3x what it really needs. Total heap memory is only 28mb. That's a bit odd. Perhaps G1 is still not aggressive enough in releasing memory back to the OS. For desktop apps it'd really be better to aggressively let go of heap as fast as possible (NB: native apps don't always do this either).

The "metaspace" is holding a lot of stuff, mostly VM internal data structures. But I thought metaspace was primarily class metadata, but here there's only 10mb of that, so what's filling it up?

There's "jcmd main VM.metaspace" which tells us about 8mb of it is wasted due to fragmentation (I think). It doesn't tell us much about what's going on though.

These numbers look extremely high for a normal Java app. If I recall correctly Clojure [ab]uses the JVM in odd ways, for instance, by creating a class for every single function in the program instead of mapping them to methods as you would expect. This can easily cause a lot of overhead in class metadata compared to a normal program.

However the bulk of the memory usage you see is really just an "optimisation" - it's there, so why not be lazy and leave garbage lying around until you need the space.

[+] fctorial|6 years ago|reply
> Firefox with more than 100 tabs opend consumes 1.2 GB,

Really? On my machine it eats 2gb for 10 tabs.

[+] vikeri|6 years ago|reply
Very interesting to use a redux (re-frame) like programming model for building native desktop UI's. I have done some basic stuff with it and it sure seems powerful.

For anyone interested there will be a virtual meetup 16:th of April 6PM CEST: https://www.meetup.com/sthlm-clj/events/269204900

[+] lenkite|6 years ago|reply
Please note that the Gluon Client Plugin can be used to create native JavaFX desktop apps which take far less memory.

https://docs.gluonhq.com/client

When Clojure resolves all outstanding Graal issues, one could use Clojure for native apps too, but there are several bug tickets open on Clojure wrt Graal.

[+] geokon|6 years ago|reply
I wonder if anyone has tried cljfx->Android with Gluon's stuff
[+] jiofih|6 years ago|reply
> browser-based technology struggles to utilize multiple cores in the same VM instance

> javascript VMs choke on processing large amounts of data, which leaves them a better fit for advanced interactive forms than compilers or data processing pipelines

2005 called and wants it’s quotes back. It is trivial to spawn worker processes today and VM performance is only 2-5x behind compiled ones, often closer. Not to mention WASM.

The elephant in the room is not JavaScript or it’s VM but the browser engine.

EDIT: seems like JavaFX has its own browser-like “scene graph” and embeds WebKit, so this will suffer from the same bloat in memory use and app sizes?

[+] StreamBright|6 years ago|reply
The real question is why though. Why should we use the HTTP/HTML/JS/CSS universe to deliver interactive applications to users. I only see downsides here. Not to mention that we pull these things into native applications like it was great to begin with.

WASM might be able to address some of the shortcomings of this stack, not sure.

[+] brabel|6 years ago|reply
> seems like JavaFX has its own browser-like “scene graph” and embeds WebKit...

Since Java 9 modules were introduced, the webview (and webkit) are only included in the app if the `javafx.web` module is used. Otherwise, it will not be included in your distribution if you pack it with jpackage.

[+] vijaybritto|6 years ago|reply
This looks nice and promising for multi core apps. But, 85Mb for a demo app? It sounds heavy and might end up like intelliJ IDEs I think. How about compiling to binaries with Graal native? Can it be done?
[+] kitd|6 years ago|reply
Most interesting takeaway for me was jpackage working with GH actions. V useful. Who needs a local build system now? ;)
[+] Klonoar|6 years ago|reply
This is going to sound subjective, but: you can't claim that something is a good desktop-app building experience in 2020 and lead with something that looks like... that.

Browsers have simply set the bar too high.

[+] pengwing|6 years ago|reply
Why do you package a javascript VM at all? You could have a local server written in clojure listening on localhost:<someport>. The entire js-driven UI could just be in a browser tab.
[+] sdegutis|6 years ago|reply
> outperforms plain React a lot of the time due to optimizations that re-render components only when their inputs change

Is this not exactly how React also works?

[+] nico_h|6 years ago|reply
Anyone knows how Jpackages compares to good/bad old JNLP or is that complementary?
[+] dezzeus|6 years ago|reply
I've just started designing something very similar in my free time, but for Go (golang): interface around native GUI + WebKit webview + React + bindings.

I'm glad that we are slipping away from Electron...

[+] dgellow|6 years ago|reply
Have you checked React Native for Windows and macOS?

It goes even further as it get rid of the webview

https://microsoft.github.io/react-native-windows/

Native widgets, bridge between JS thread and the native world. That gives you good performances and the flexibility of JS/Typescrit and React to glue things together.

(I haven’t tried yet, but that looks quite nice on paper)

[+] capableweb|6 years ago|reply
Hm, as far as I know, the problem most people have with Electron is not around the language, React or bindings but the fact that there is a embedded browser in there that adds a lot to the size and memory consumption (and security issues).

I'm curious to hear why you're glad we're going away from Electron into something that seems to be about the same thing, albeit a different language?

[+] truth_seeker|6 years ago|reply

[deleted]

[+] derefr|6 years ago|reply
The “problem of concurrency” isn’t about solving embarrassingly-parallel problems by spawning async threads and letting them run. Every runtime can indeed do that, but that isn’t worth mentioning when talking about “concurrency”, because you can also do that by just spawning multiple of your runtime itself. (I.e. if you’re not doing any IPC of note, then there’s no difference between multithreading and multiprocessing. In the web case: you could just use an iframe.)

The “problem of concurrency” is really the problem of having cheap IPC between your threads (i.e. the ability to pass around large amounts of data using small messages/handles), so that you can shape your threads into architectures like a staged data-flow pipeline, or a set of schedulers for an actor system.

JS does not have cheap concurrency. (What would cheap concurrency in JS look like? Well, the ability to make an ArrayBuffer immutable, and then pass the immutable ArrayBuffer to another Worker Thread “by reference”, would be a good start. Add “mutably-immutable by copy-on-write” ArrayBuffers on top, and you could actually afford to pass ‘ownership’ of an ArrayBuffer back and forth, writing to it across multiple threads. Then you’d have something that you could maybe run a multithreaded WASM program on top of and have it run to completion before the heat-death of the universe.)

[+] francasso|6 years ago|reply
Your comments comparing worker threads to what can be achieved on the jvm (or natively) with multithreading is just a sad display of ignorance. Even more sad is the tone you used.
[+] keymone|6 years ago|reply
GruntJS/GulpJS/Webpack - none of these address the issue of transitioning to new behavior without having to fiddle with current state / fix existing object / reset and recreate some things from scratch. Clojure addresses that by forcing immutable functional mindset.

Sure, there’s a combination of tools / plugins / unorthodox (to vanilla JS) development practices that gives you the same thing, but in Clojure you just get it out of the box and nothing feels weird.