enso/docs/infrastructure/native-image.md
Hubert Plociniczak 096fcfee82
Generate native image for engine-runner (#3638)
This PR adds a possibility to generate native-image for engine-runner.
Note that due to on-demand loading of stdlib, programs that make use of it are currently not yet supported
(that will be resolved at a later point).
The purpose of this PR is only to make sure that we can generate a bare minimum runner because due to lack TruffleBoundaries or misconfiguration in reflection config, this can get broken very easily.
To generate a native image simply execute:
```
sbt> engine-runner-native/buildNativeImage
... (wait a few minutes)
```
The executable is called `runner` and can be tested via a simple test that is in the resources. To illustrate the benefits
see the timings difference between the non-native and native one:
```
>time built-distribution/enso-engine-0.0.0-dev-linux-amd64/enso-0.0.0-dev/bin/enso --no-ir-caches --in-project test/Tests/ --run engine/runner-native/src/test/resources/Factorial.enso 6
720

real	0m4.503s
user	0m9.248s
sys	0m1.494s
> time ./runner --run engine/runner-native/src/test/resources/Factorial.enso 6
720

real	0m0.176s
user	0m0.042s
sys	0m0.038s
```

# Important Notes
Notice that due to a [bug in GraalVM](https://github.com/oracle/graal/issues/4200), which is already fixed in 22.x, and us still being on 21.x for the time being, I had to add a workaround to our sbt build to build a different fat jar for native image. To workaround it I had to exclude sqlite jar. Hence native image task is on `engine-runner-native` and not on `engine-runner`.

Will need to add the above command to CI.
2022-09-22 14:45:10 +00:00

9.5 KiB

layout title category tags order
developer-doc Native Image infrastructure
infrastructure
build
native
native-image
3

Native Image

NativeImage defines a task that is used for compiling a project into a native binary using Graal's Native Image. It compiles the project and runs the Native Image tool which builds the image. Currently, Native Image is used for building the Launcher.

Requirements

Native Image Component

The Native Image component has to be installed within the used GraalVM distribution. It can be installed by running <path-to-graal-home>/bin/gu install native-image.

Additional Linux Dependencies

To be able to link statically on Linux, we need to link against a libc implementation. The default glibc contains a bug that would cause crashes when downloading files form the internet, which is a crucial Launcher functionality. Instead, musl implementation is suggested by Graal as an alternative. The sbt task automatically downloads a bundle containing all requirements for a static build with musl. It only requires a tar command to be available to extract the bundle.

Currently, to use musl, the --libc=musl option has to be added to the build and x86_64-linux-musl-gcc must be available in the system PATH for the native-image. In the future it is possible that a different option will be used or that the bundle will not be required anymore if it became prepackaged. This task may thus need an update when moving to a newer version of Graal. More information may be found in the Native Image documentation.

To make the bundle work correctly with GraalVM 20.2, a shell script called x86_64-linux-musl-gcc which loads the bundle's configuration is created by the task and the paths starting with /build/bundle in musl-gcc.specs are replaced with absolute paths to the bundle location.

Static Builds

The task is parametrized with staticOnLinux parameter which if set to true, will statically link the built binary, to ensure portability between Linux distributions. For Windows and MacOS, the binaries should generally be portable, as described in Launcher Portability.

No Cross-Compilation

As Native Image does not support cross-compilation, the native binaries can only be built for the platform and architecture that the build is running on.

Configuration

As the Native Image builds a native binary, certain capabilities, like reflection, may be limited. The build system tries to automatically detect some reflective accesses, but it cannot detect everything. It is possible for the built binary to fail with java.lang.ClassNotFoundException or the following error:

java.lang.InstantiationException: Type `XYZ` can not be instantiated reflectively as it does not have a no-parameter constructor or the no-parameter constructor has not been added explicitly to the native image.`

To avoid such issues, additional configuration has to be added to the Native Image build so that it can include the missing constructors.

This can be done manually by creating a file reflect-config.json. The build task looks for the configuration files in every subdirectory of META-INF/native-image on the project classpath.

Creating the configuration manually may be tedious and error-prone, so GraalVM includes a tool for assisted configuration. The link describes in detail how the tool can be used. The gist is, the JVM version of the application can be run with a special agentlib in order to trace reflective accesses and save the generated configuration to the provided directory. To run the tool it is easiest to assemble the application into a JAR and run it with the following command:

java \
  -agentlib:native-image-agent=config-merge-dir=/path/to/native-image-config \
  -jar /path/to/application.jar \
  <application arguments>

For example, to update settings for the Launcher:

java -agentlib:native-image-agent=config-merge-dir=engine/launcher/src/main/resources/META-INF/native-image/org/enso/launcher -jar launcher.jar <arguments>

The command may need to be re-run with different arguments to ensure that all execution paths that use reflection are covered. The configuration files between consecutive runs will be merged (a warning may be issued for the first run if the configuration files did not exist, this is not a problem).

It is possible that different classes are reflectively accessed on different platforms. In that case it may be necessary to run the agent on multiple platforms and merge the configs. If the conflicts were conflicting (i.e. some reflectively accessed classes existed only on one platform), it may be necessary to maintain separate configs for each platform. Currently, the native-image-agent is not available on Windows, so Windows-specific reflective accesses may have to be gathered manually. For some types of accesses it may be possible to force the Windows-specific code paths to run on Linux and gather these accesses semi-automatically.

After updating the Native Image configuration, make sure to clean it by running

cd tools/native-image-config-cleanup && npm install && npm start

Launcher Configuration

In case of the launcher, to gather the relevant reflective accesses one wants to test as many execution paths as possible, especially the ones that are likely to use reflection. One of these areas is HTTP support and archive extraction.

To trace this accesses, it is good to run at least ... launcher.jar install engine which will trigger HTTP downloads and archive extraction.

Currently, archive-related accesses are platform dependent - Linux launcher only uses .tar.gz and Windows uses .zip. While the Linux launcher never unpacks ZIP files, we can manually force it to do so, to register the reflection configuration that will than be used on Windows to enable ZIP extraction.

To force the launcher to extract a ZIP on Linux, one can add the following code snippet (with the necessary imports) to org.enso.launcher.cli.Main.main:

Archive.extractArchive(Path.of("enso-engine-windows.zip"), Path.of("somewhere"), None)

With this snippet, launcher.jar should be built using the launcher / assembly task, and the tracing tool should be re-run as shown above.

Moreover, some reflective accesses may not be detected by the tool automatically, so they may need to be added manually. One of them is an access to the class [B when using Akka, so it would require manually adding it to the reflect-config.json. This strange looking access is most likely reflective access to an array of bytes. To make it easier, a package akka-native has been created that gathers workarounds required to be able to build native images using Akka, so it is enough to just add it as a dependency. It does not handle other reflective accesses that are related to Akka, because the ones that are needed are gathered automatically using the tool described above.

Project Manager Configuration

Configuring the Native Image for the Project Manager goes similarly as with the launcher. You need to build the JAR with project-manager/assembly and execute the test scenarios by starting it with:

java -agentlib:native-image-agent=config-merge-dir=lib/scala/project-manager/src/main/resources/META-INF/native-image/org/enso/projectmanager -jar project-manager.jar

To trace relevant reflection paths, the primary scenario is to start the Project Manager and connect an IDE to it. Since the Project Manager is able to install engine versions, similar steps should be taken to force it to extract a zip archive, as described in Launcher Configuration above. If necessary, other scenarios, like project renaming may be covered.

Remember to run the cleanup script as described above, as tracing the Project Manager seems to find recursive accesses of some ephemeral-like classes named Foo/0x00001234.... This classes are not accessible when building the Native Image and they lead to warnings. For now no clues have been found that ignoring these classes would impact the native build, it seems that they can be ignored safely.

Engine runner Configuration

The Native Image generation for the Engine Runner is currently in a preview state. Limitations are currently mostly due to Java interop and loading of stdlib components. To generate the Native Image for runner simply execute

sbt> engine-runner-native/buildNativeImage

and execute the binary on a sample factorial test program

> runner --run engine/runner-native/src/test/resources/Factorial.enso 6

The task that generates the Native Image, along with all the necessary configuration, reside in a separate project due to a bug in the currently used GraalVM version.