3

I've written a Java application that I want to package for the main OSes so that I can provide it as a self-contained installable image. To do this, I use jpackage, with help from the best-named plugin I've come across, The Badass Runtime Plugin (https://badass-runtime-plugin.beryx.org)

My problem is that although the packaging works the performance of the packaged application is dreadful when compared to the compiled Java.

When I build my application (I use gradle for this) on MacOS, the build output includes an install directory containing the compiled classes and a launch script, and an installer directory containing a dmg with the app file.

Running the application from the compiled classes via the launch script works great. Running the application from the app (after installing from the dmg) is very much slower - frequently 10x and worse.

The launch script for the compiled classes points at my local JDK instance (openjdk 16), whereas the app file contains the jdk modules needed for the app to run. This is by design of course, but the only difference I can think of between the two.

Thanks for reading this far - I realise this is a fairly niche problem. If anyone has any experience or thoughts that might help I'd love to hear it.

Thanks, Andy

Edit 10th Sept - thanks for all the interest and comments so far. A couple more notes about my app that might be relevant:

It's a heavy user of RocksDB for off-heap, file-backed data storage. Both the compiled Java and the packaged app use the same sst files, so the data is identical. The app and data are running/present on the same machine, a MacBook Pro with SSD drive and plenty of capacity.

It launches Javalin instances to fire up https connections for a browser based UI and accepting data requests.

AndyW
  • 101
  • 7
  • Which JRE does the packaged version include? – tgdavies Sep 09 '21 at 22:45
  • It's built from the same openjdk-16.0.1.jdk as the local compile, but with a subset of modules ('java.se', 'jdk.crypto.ec', 'jdk.compiler', 'java.compiler', 'jdk.zipfs') – AndyW Sep 09 '21 at 23:19
  • sounds like one includes the run-time and the other doesn't. That would account for difference in launch-time. (It has to create the Java run-time...) Maybe I'm misunderstanding though. Is it just the time to start the app, or throughout? – pcalkins Sep 09 '21 at 23:23
  • @pcalkins The jpackage tool embeds a JRE – Slaw Sep 09 '21 at 23:26
  • Yes that's right - the jpackage tool embeds the cut-down (by module) version of the JRE. No issue with the launch time, it's the run time event processing that's the problem. Everything works as it does outside of the app, just much slower. – AndyW Sep 09 '21 at 23:38
  • Are you using exactly the same JVM parameters? – tgdavies Sep 09 '21 at 23:51
  • Yes, same parameters. Confirmed they're the same in VisualVM as well at runtime. – AndyW Sep 09 '21 at 23:56
  • What kind of operations do you suspect are slowed? Can you post some example code for us to try? – Basil Bourque Sep 10 '21 at 03:32
  • It's a fairly large app and I haven't been able to identify any specific code that sticks out as running slowly, so no useful example to give. It does use RocksDB heavily though, so I've suspected that as an issue. Also Javalin, but I'm less inclined to think that's the problem - but I don't know, it might be. I've updated my question with a little more info around these. – AndyW Sep 10 '21 at 08:07
  • VisualVM should help you determine where the bottlenecks are – Slaw Sep 10 '21 at 20:24
  • Yes, I've spent quite a lot of time looking at this with VisualVM. The problem isn't bottlenecks in the code, it's that literally everything runs much slower when packaged as a MacOS app. For example, one of my more demanding calls takes about 6s - with VisualVM profiling the CPU against the compiled classes it takes 31s, but against the app it's 684s. Drilling in shows the same methods called, same invocations, but major difference in time. Even the simplest call (RocksDB.releaseSnapshot) goes from 0.001ms to 0.007ms; the largest goes from 8.5s to 95.7s. Same across the board. – AndyW Sep 11 '21 at 14:22
  • Are the configurations (e.g. heap space, GC, etc.) the same after packaging? – Slaw Sep 12 '21 at 17:11
  • Yes, VisualVM shows both with: -XX:+UseZGC -Xms256m -Xmx2048m -Dfile.encoding=UTF-8 The packaged app also has 2 -Djpackage.app args for metadata. The system.properties are the same except for the class path and java home differences you'd expect, plus the user.dir for the packaged app is /, but for the unpackaged classes it points to the directory that I run the launch script from. – AndyW Sep 12 '21 at 21:00
  • Then I'm out of ideas. I suppose something could possibly be interfering with the packaged version (e.g. security systems), but that's just a wild guess that I have no basis for. – Slaw Sep 14 '21 at 01:12
  • Np, it's quite a head scratcher. Thanks for your time and efforts looking into this. – AndyW Sep 14 '21 at 08:10
  • Is the difference in performance only there for the RocksJava calls that call RocksDB native code, or do you see it for every Java method call? I want to try and rule in/out if this is RocksDB specific or just Java specific for your packaged application. – adamretter Sep 14 '21 at 18:06
  • It seems to be affecting every method call, not just those to RocksDb. – AndyW Sep 14 '21 at 21:19
  • This is very strange, can you compare the packaged and non-packaged apps with a profiler to find out where the underlying difference is? – adamretter Sep 14 '21 at 21:24
  • Yes, very strange indeed. I've been using VisualVM profiler to compare the 2, as per my comment a bit further up this thread. – AndyW Sep 15 '21 at 08:15
  • You might want something that can profile from a lower-level, i.e. to see if one application is making syscalls/messages that the other is not (perhaps due to its packaging) – adamretter Sep 15 '21 at 15:54

1 Answers1

0

I did have the same issue somehow the binary made by jpackage is waaay too slow, did not find any solutions in tweaking the creation of the binary, so I ended up replacing it with a sh that starts the jar and this made the app work normal.

So after creating the app-image in the /Contents/app replaced the binary with

#!/bin/bash
$DIR/../runtime/Contents/Home/bin/java -Xdock:icon=$DIR/../Resources/icon.icns -Djava.library.path=$DIR:$DIR/../MacOS -Djava.launcher.path=$DIR/../MacOS -Dapp.preferences.id=org/springframework/boot/loader -Dstorage.diskCache.bufferSize=200 -Xmx4g -cp $DIR/../app/server.jar org.springframework.boot.loader.JarLauncher