Build performance
Most of the time of a small Kotlin build is not compiling: it’s starting a JVM and warming up the compiler’s JIT. Kargo avoids that cost, recompiles only the files affected by a change, builds independent work at the same time, and skips work whose inputs haven’t changed.
$ touch src/Main.kt && kargo build Compiling app v0.1.0 (jvm) Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.41sThe compiler daemon
Section titled “The compiler daemon”The first compilation starts a compiler daemon: a background JVM that runs the Kotlin compiler (and javac) in-process. Later builds send their compiler arguments to it over a local socket, so they get an already-running, already-optimized compiler. A rebuild of a small project drops from several seconds to well under one.
- One daemon runs per JDK, Kotlin version and
KARGO_COMPILER_JVM_OPTS; changing any of them starts a new one. - If you press Ctrl-C during a build, the daemon finishes that compilation without replying. A build you start right after waits for it, because the daemon runs one compilation at a time per output directory, so the two never write into the same classes directory.
- It exits after 3 hours without work, and when its state directory under
~/.kargo/daemonis removed. On exit it deletes itsinfofile. If one is left behind (after a crash or a reboot) and another program now uses its port, Kargo notices that the program does not answer like the daemon within 10 seconds and starts a new daemon instead of waiting. - It serves several builds at once, so parallel targets and concurrent
kargoinvocations share it. - Compiler output looks exactly like a standalone compiler run, with paths relative to the directory you ran
kargoin.
The first build after a reboot or a Kotlin upgrade pays the JVM startup once. That build also compiles the small daemon program with your JDK’s javac (once per JDK), and Kargo records the classes the JVM loads at startup so later daemon starts are faster.
See and stop daemons
Section titled “See and stop daemons”$ kargo daemon statuspid 48211 /home/me/.kargo/daemon/ada07be3e284d798$ kargo daemon stop Stopped 1 compiler daemonStop the daemons after changing your JDK installation in place, or to free their memory right away.
Memory
Section titled “Memory”The daemon uses the JVM’s default heap limit (a quarter of physical memory). Raise or lower it with KARGO_COMPILER_JVM_OPTS, which starts a daemon with the new options on the next build:
export KARGO_COMPILER_JVM_OPTS="-Xmx4g"Turning it off
Section titled “Turning it off”Set KARGO_DAEMON=false to run the compiler as a separate process on every build, as Kargo did before the daemon existed:
KARGO_DAEMON=false kargo buildKargo also falls back to a separate process on its own when the daemon can’t be used, for example when the JDK has no javac to build it. Run with -v to see why.
Incremental compilation
Section titled “Incremental compilation”On the JVM, Kargo recompiles only the files you changed and the files that depend on what changed, not the whole target. It uses the Kotlin Build Tools API, the same incremental compiler Gradle uses, inside the compiler daemon.
$ echo "// tweak" >> src/gen/G5.kt && kargo build Compiling bigproj v0.1.0 (jvm) Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.39sIn a project with 300 source files, changing one file rebuilds in about 0.4 seconds instead of about 5 seconds (with the daemon) or more than 20 seconds (without it).
-
What triggers a recompile. Kargo tells the compiler which sources changed since the last successful build. The compiler also recompiles the files that use a declaration whose signature changed, and the users of a
const valwhose value changed. -
Dependencies. Each classpath jar is summarized in a snapshot (cached under
~/.kargo/daemon/ic-snapshots), so adding, removing or upgrading a dependency recompiles only the code that uses what changed. The tests’ snapshot of the main classes works the same way, so a change insrc/recompiles only the tests that depend on it. -
Clean rebuilds. Changing compiler options, compiler plugins,
[frameworks]or the Kotlin version recompiles the target from scratch once. A clean build takes somewhat longer than without incremental compilation, because the compiler also records what each file depends on. -
Deleted files. Their classes are removed from
target/on the next successful build. -
Java sources. Kotlin compiles incrementally;
javacstill compiles all Java sources of a target whenever it rebuilds. -
Where the state lives. The caches are in
target/<profile>/jvm/.fingerprint/<name>.ic/, sokargo cleanremoves them. -
Path dependencies. A path dependency’s jar counts by its ABI: its public declarations, their signatures and constants, and the bodies of
inlinefunctions. A change to a private function, or to the body of a function that isn’tinline, rebuilds the dependency but none of its JVM consumers, which run the new code from its new jar. See Incremental builds across members.
Incremental compilation needs the compiler daemon, and a Kotlin version that publishes kotlin-build-tools-impl (2.4 and newer). Multiplatform projects that share code between src/ and src-jvm/ with expect/actual, and the Wasm, JS and Native targets, compile each target from scratch (in the daemon for Wasm and JS).
Turning it off
Section titled “Turning it off”Set KARGO_INCREMENTAL=false (like Cargo’s CARGO_INCREMENTAL=0) to recompile whole targets while keeping the daemon. Kargo discards the incremental caches when it does:
KARGO_INCREMENTAL=false kargo buildKargo also falls back to a full rebuild by itself when the incremental compiler fails for a reason other than a compile error, for example a corrupt cache; the details go to the daemon’s log.
Parallel work
Section titled “Parallel work”Kargo runs independent work at the same time:
- Targets.
kargo build,kargo checkandkargo fetchbuild the JVM, Wasm, JS, Native and Android targets of a multiplatform project concurrently. - Workspace members.
kargo buildandkargo checkbuild independent workspace members at the same time, each after the members it depends on;-j <n>limits how many (the default is the number of CPUs).kargo testtests one member at a time unless you pass-j. - Native platforms. Each entry in
[target.native] platformscompiles in its own Kotlin/Native process at the same time. - Tests.
kargo testcompiles every target’s tests concurrently, then runs them one target at a time so their output doesn’t interleave. - Downloads. The compiler, the JDK and your dependencies download at the same time. Dependency resolution fetches POMs and Gradle module metadata of a whole level of the graph at once, and artifacts listed in
kargo.lockdownload in parallel when the cache is empty (a fresh CI machine, for example). Requests that hit a rate limit (HTTP 429) or a temporary server error are retried with backoff.
Skipped work
Section titled “Skipped work”- Up-to-date checks. Each compilation fingerprints its sources, classpath, compiler plugins and arguments. If nothing changed,
kargo builddoesn’t start the compiler, and a no-op build takes milliseconds. - Cached toolchain lookups. The JDK’s version is read from its
releasefile instead of startingjava, and the Kotlin compiler’s classpath is remembered per version, so a no-op build needs no network access. - Fast packaging.
kargo packagecopies the classes of dependency jars into the fat jar without decompressing and recompressing them. - Kotlin/Native standard library. Debug builds of a native target link a precompiled copy of the standard library instead of lowering it again, which roughly halves a rebuild. The compiler builds it on the first debug build (one to two minutes, once per Kotlin version and platform);
--releasebuilds don’t use it. See Build speed for the details andKARGO_NATIVE_CACHE=false. - Class-data sharing. On JDK 19 and newer, the JUnit launcher used by
kargo testand a standalone compiler (with the daemon off) reuse an archive of the classes they loaded on their first run, which cuts their startup time. The archives live in~/.kargo/daemon/cds.