Make ktfmt up to 100x faster via native-image#584
Make ktfmt up to 100x faster via native-image#584sgammon wants to merge 4 commits intofacebook:mainfrom
ktfmt up to 100x faster via native-image#584Conversation
Introduces a `ktfmt` binary, built using GraalVM's `native-image` tool. The binary behaves the same way as `ktfmt`'s existing JVM-based CLI, but is compiled ahead-of-time into a native executable. Signed-off-by: Sam Gammon <sam@elide.dev>
Signed-off-by: Sam Gammon <sam@elide.dev>
|
Hi @sgammon! Thank you for your pull request and welcome to our community. Action RequiredIn order to merge any pull request (code, docs, etc.), we require contributors to sign our Contributor License Agreement, and we don't seem to have one on file for you. ProcessIn order for us to review and merge your suggested changes, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA. Once the CLA is signed, our tooling will perform checks and validations. Afterwards, the pull request will be tagged with If you have received this in error or have any questions, please contact us at cla@meta.com. Thanks! |
...rc/main/native-image/java/com/facebook/ktfmt/nativeImage/KotlinCoreEnvironmentCompanion.java
Show resolved
Hide resolved
| ### PGO Profiles | ||
|
|
||
| Run `pgo_train.sh` to generate `default.iprof` in this folder. Then, it will be used when `-Pktfmt.native.pgo=true` is passed to `./gradlew :ktfmt:nativeCompile`. |
There was a problem hiding this comment.
Instructions to build and use PGO (Profile Guided Optimization) profiles are included, as well as profiles themselves. PGO significantly improves startup and peak performance
| { | ||
| "type": "org.jetbrains.kotlin.cli.common.CompilerSystemProperties" | ||
| }, | ||
| { | ||
| "type": { | ||
| "proxy": [ | ||
| "org.jetbrains.kotlin.com.intellij.openapi.vfs.VirtualFileListener" | ||
| ] | ||
| } | ||
| }, |
There was a problem hiding this comment.
This metadata is assembled from runs using the Native Image reflection tracing agent; it should be held in source control because every code path should be tested to produce it, and it should evolve over time as the code underneath does, too
| // Pass `-Pktfmt.native.release=true` to enable release mode for Native Image. | ||
| val nativeRelease = findProperty("ktfmt.native.release") == "true" | ||
|
|
||
| // Pass `-Pktfmt.native.target=xx` to pass `-march=xx` to Native Image. | ||
| val nativeTarget = | ||
| findProperty("ktfmt.native.target") | ||
| ?: when (val hostArch = System.getProperty("os.arch")) { | ||
| "amd64", | ||
| "x86_64" -> DefaultArchitectureTarget.amd64 | ||
| "aarch64", | ||
| "arm64" -> DefaultArchitectureTarget.arm64 | ||
| else -> error("Unrecognized host architecture: '$hostArch'") | ||
| } | ||
|
|
||
| // Pass `-Pktfmt.native.gc=xx` to select a garbage collector; options include `serial`, `G1`, and | ||
| // `epsilon`. | ||
| val nativeGc = findProperty("ktfmt.native.gc") ?: "G1" | ||
|
|
||
| // Pass `-Pktfmt.native.gc=xx` to select a garbage collector; options include `serial`, `G1`, and | ||
| // `epsilon`. | ||
| val nativeDebug = findProperty("ktfmt.native.debug") == "true" | ||
|
|
||
| // Pass `-Pktfmt.native.lto=true` to enable LTO for the Native Image binary. | ||
| val enableLto = findProperty("ktfmt.native.lto") == "true" | ||
|
|
||
| // Pass `-Pktfmt.native.muslHome=xx` or set MUSL_HOME to point to the Musl sysroot when building for | ||
| // Musl Libc. | ||
| val muslSysroot = (findProperty("ktfmt.native.muslHome") ?: System.getenv("MUSL_HOME"))?.toString() | ||
|
|
||
| // Pass `-Pktfmt.native.musl=true` to build a fully-static binary against Musl Libc. | ||
| val preferMusl = | ||
| (findProperty("ktfmt.native.musl") == "true").also { preferMusl -> | ||
| require(!preferMusl || muslSysroot != null) { | ||
| "When `ktfmt.native.musl` is true, -Pktfmt.native.muslHome or MUSL_HOME must be set to the Musl sysroot. " + | ||
| "See https://www.graalvm.org/latest/reference-manual/native-image/guides/build-static-executables/" | ||
| } | ||
| } | ||
|
|
||
| // Pass `-Pktfmt.native.smol=true` to build a small, instead of a fast, binary. | ||
| val preferSmol = (findProperty("ktfmt.native.smol") == "true") | ||
|
|
||
| // Pass `-Pktfmt.native.opt=s` to pass e.g. `-Os` to Native Image. | ||
| val nativeOpt = | ||
| when (val opt = findProperty("ktfmt.native.opt")) { | ||
| null -> | ||
| when { | ||
| preferSmol -> "s" | ||
| nativeRelease -> "3" | ||
| else -> "b" // prefer build speed | ||
| } | ||
| else -> opt | ||
| } | ||
|
|
||
| // List of PGO profiles, which are held in `src/main/native-image/profiles`. | ||
| val pgoProfiles = | ||
| listOf("default.iprof") | ||
| .map { profileName -> | ||
| layout.projectDirectory.file( | ||
| Paths.get("src", "main", "native-image", "profiles", profileName).toString() | ||
| ) | ||
| } | ||
| .let { allProfiles -> listOf("--pgo=${allProfiles.joinToString(",")}") } | ||
|
|
||
| // Pass `-Pktfmt.native.pgo=true` to build with PGO; pass `train` to enable instrumentation. | ||
| val pgoArgs = | ||
| when (val pgo = findProperty("ktfmt.native.pgo")) { | ||
| null -> if (nativeRelease) pgoProfiles else emptyList() | ||
| "true" -> pgoProfiles | ||
| "false" -> emptyList() | ||
| "train" -> listOf("--pgo-instrument") | ||
| else -> error("Unrecognized `ktfmt.native.pgo` argument: '$pgo'") | ||
| } |
There was a problem hiding this comment.
Settings which govern the Native Image build; many can be set with ./gradlew -P...
| # Native Image profiles are large | ||
| core/src/main/native-image/profiles/*.iprof |
There was a problem hiding this comment.
currently these are ignored, but they can be shipped up either in source control, or in git lfs. in an ideal case, PGO profiles would be materialized in CI as part of the release process, so they can never drift from underlying code. generally speaking, i generate PGO profiles for ktfmt by building with instrumentation and then formatting ktfmt itself.
| # clean any current native build | ||
| ./gradlew :ktfmt:clean; | ||
|
|
||
| # build natively with PGO training on | ||
| ./gradlew :ktfmt:nativeCompile \ | ||
| -Pktfmt.native.pgo=train \ | ||
| -Pktfmt.native.target=native \ | ||
| -Pktfmt.native.lto=true; | ||
|
|
||
| echo "PGO training starting" | ||
|
|
||
| ./core/build/native/nativeCompile/ktfmt -n $PWD | ||
|
|
||
| echo "PGO training completed." | ||
|
|
||
| cp -fv ./default.iprof ./core/src/main/native-image/profiles/default.iprof; | ||
|
|
||
| echo "Rebuilding..." | ||
|
|
||
| # clean for PGO-trained build | ||
| ./gradlew :ktfmt:clean; | ||
|
|
||
| # rebuild | ||
| ./gradlew :ktfmt:nativeCompile \ | ||
| -Pktfmt.native.release=true \ | ||
| -Pktfmt.native.lto=true \ | ||
| -Pktfmt.native.pgo=true; | ||
|
|
||
| echo "PGO-trained build complete." |
There was a problem hiding this comment.
full steps for pgo training, run bash pgo_train.sh to use it; this can be moved into a folder somewhere, but, because of the need for multiple gradle runs with distinct settings in each, it probably can't easily be ported into Gradle kts itself
ktfmt native imagektfmt up to 185x faster via native-image
ktfmt up to 185x faster via native-imagektfmt up to 100x faster via native-image
This comment was marked as outdated.
This comment was marked as outdated.
|
I don't work at facebook and am not a maintainer on this project. I just filed that FR 🙃 |
This comment was marked as outdated.
This comment was marked as outdated.
|
yeah I meant to react to the PR description, accidentally did the wrong one 🙈 |
|
Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Meta Open Source project. Thanks! |
hick209
left a comment
There was a problem hiding this comment.
Initial batch. There shall be more tomorrow 😃
Here are my initial comments. No need to act on anything just yet if you do not desire.
| - name: Set up GraalVM | ||
| uses: graalvm/setup-graalvm@v1 | ||
| with: | ||
| java-version: '25.0.1' | ||
| distribution: 'graalvm' | ||
| github-token: ${{ secrets.GITHUB_TOKEN }} | ||
| set-java-home: 'false' | ||
| native-image-job-reports: 'true' |
There was a problem hiding this comment.
Is this the CI part you said we need to work together to flush out or something else?
There was a problem hiding this comment.
yes, but really what i meant was, we will need CI systems for Windows and macOS, not just linux. i can easily set it up with a build matrix.
...rc/main/native-image/java/com/facebook/ktfmt/nativeImage/KotlinCoreEnvironmentCompanion.java
Show resolved
Hide resolved
| @@ -14,6 +14,7 @@ | |||
| * limitations under the License. | |||
There was a problem hiding this comment.
Overall I see a ton of changes here. Would it be possible to put them (at least a good chunk) into their own file so they do not "pollute as much" the build files? At the very least try to section them off somehow.
WDYT?
There was a problem hiding this comment.
yes, i am happy to split it up a bit
hick209
left a comment
There was a problem hiding this comment.
It's honestly a lot of things I don't really understand here.
From my limited understanding this seems like a good improvement and I'm tempted to just merge it in and give it a go.
Right now I see there some CI jobs (here from GH) failed, could you take a look to see if any of that makes sense before I pull this in for internal review?
Native ktfmt
Introduces support for a native binary version of
ktfmt, via GraalVMnative-image; no code changes have taken place, just build changes, in order to make kotlinc's infrastructure (which ktfmt depends on) usable under SVM, Substrate Virtual Machine.After some optimization and cleanup, I have some initial numbers which show promising gains:
The native version of
ktfmtis identical in every way to the regular JVM version, except it is built AOT, and, since it is already machine code, it does not need a JVM installed at runtime.We have been using this version of ktfmt internally for some time now -- a few weeks -- and it is stable for us even on codebases with hundreds or thousands of files. Once you hit many thousands of files, the performance gap between JVM and SVM begins to close (this is tunable). That makes sense, considering JVM's warmed-up JIT state is still known to beat SVM at peak performance.
However, for smaller executions of ktfmt -- particularly ones which only format changed code -- the difference can be quite persuasive toward native, because of essentially instant startup time.
Note: To actually ship this on ktfmt, we are going to need build infrastructure for each target OS/architecture pair. GraalVM's support matrix currently includes:
I am able to build
ktfmtfor each of the above targets, and have CI (personally) to help, but will need to transition that stuff to GHA and Meta's control. In conversations with Nivaldo (@hick209), he indicated CI should be no problem.Fixes and closes #441 cc / @ZacSweers
Trying it out
Here are pre-built binaries for Linux amd64 and macOS arm64, if you'd like to try it out. Each package comes with
ktfmtandktfmt-build-report.html, which is a standalone file you can open in your browser. It shows the full contents of the native image and various stats about its contents.Download for macOS arm64
Download for Linux amd64
The final compressed packages end up at about
13MBfor macOS and16MBfor Linux.