Ever heard of a JarIndex? I had been doing JVM development for 10+ years and I hadn’t. Read on to discover what it is and how it can speedup your compilation and boot time. 🤓
After having worked on Shrinkwrap and publishing our results in Mapping Out the HPC Dependency Chaos, you start to see the Linux environment as a bit of an oddball.
Everything in Linux is structured around O(n) or O(n^2) search and lookup.
This feels now unsurprising given that everything in Linux searches across colon separate lists (i.e. LD_LIBRARY_PATH, RUN_PATH). This idiom however is even more pervasive and has bled into all of our language.
The JVM for instance, must search for classes amongst a set of directories, files or JARs set on the CLASS_PATH.
Everytime the JVM needs to load a class file, it must perform a linear search along all entries in the CLASS_PATH. Thanksfully, if the entries are directories or JARs, no subsequent search must be performed since the package name of a class dictates the directory structure that must exist.
io.fzakaria.Example
-> io/fzakaria/Example.class
Nevertheless, the CLASS_PATH size can be large. At $DAY_JOB$, almost all of our services launch with +300 entries (JARs) on the ClassPath.
Large enterprise codebases may feature over a thousand ClassPath entries. 😮
A large ClassPath means that the JavaVirtualMachine (JVM) needs to search entry for the desired class.
This not only affects startup time for your application, on every startup, repeatedly, but also compilation as well via javac
.
The authors of the JVM already knew about this problem, especially when the idea of Java Applets were dominant. Each JAR on the ClassPath would have been fetched via HTTP and would cause unbearable slowdown for startup.
The JDK has support for a JarIndex.
A JarIndex, is a JAR which has a special file INDEX.LIST
that effectively contains an index of all JARs on the ClassPath and the packages found within.
JarIndex-Version: 1.0
libMain.jar
Main.class
lib/libA.jar
A.class
lib/libB.jar
B.class
Whenever a class must be searched rather than searching through the CLASS_PATH, the index file is used leading to constant-time lookup for classes.
This seemingly powerful primitive confusingly has been deprecated and ultimately removed in JDK22 (JDK-8302819) 🤔 – citing challenges when having to support a broad ranges of topics such as Multi-Version JARs.
Unsuprisingly, I think this feature would be an easy fit into Bazel, Spack or Nix – as there are a lot more constraints on the type of JARs that need be supported.
I put together a small gist on what this support might look like.
def _jar_index_impl(ctx):
java_info = ctx.attr.src[JavaInfo]
java_runtime = ctx.attr._java_runtime[java_common.JavaRuntimeInfo]
java_home = java_runtime.java_home
jar_bin = "%s/bin/jar" % java_home
runtime_jars = " "
for jar in java_info.transitive_runtime_jars.to_list():
runtime_jars += jar.path + " "
cmds = [
"%s -i %s %s" % (jar_bin, java_info.java_outputs[0].class_jar.path, runtime_jars),
"cp %s %s" % (java_info.java_outputs[0].class_jar.path, ctx.outputs.index.path),
]
ctx.actions.run_shell(
inputs = [ java_info.java_outputs[0].class_jar] + java_info.transitive_runtime_jars.to_list(),
outputs = [ctx.outputs.index],
tools = java_runtime.files,
command = ";\n".join(cmds),
)
return [
DefaultInfo(files = depset([ctx.outputs.index])),
]
jar_index = rule(
implementation = _jar_index_impl,
attrs = {
"src": attr.label(
mandatory = True,
providers = [JavaInfo],
),
"_java_runtime": attr.label(
default = "@bazel_tools//tools/jdk:current_java_runtime",
providers = [java_common.JavaRuntimeInfo],
),
},
outputs = {"index": "%{name}_index.jar"},
)
Further improvements can be made, to give this index-like support to the Java compiler itself and not only for java_binary
targets.
We’ve gone out of our way on these systems to define our inputs, enforce contraints and model our dependencies. Not taking advantage of this stability and regressing to the default search often found in our tooling is leaving easy performance improvements on the floor.