Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable builds under GraalVM #1266

Merged
merged 3 commits into from
Sep 18, 2018
Merged

Enable builds under GraalVM #1266

merged 3 commits into from
Sep 18, 2018

Conversation

ssaavedra
Copy link
Contributor

@ssaavedra ssaavedra commented Aug 30, 2018

This change enables Scalafmt to be built as a native-image with GraalVM from the scalafmt-cli jar.

This achieves one of the goals of #1172, namely, avoiding the JVM warm-up time.

For the scalafmt repo, I got a speed-up of 2.5x in my computer. Also, since this method creates a binary just like the current recommended way using coursier and nailgun-ng in the console, it allows for much easier integration with editors such as emacs or vim and other script-based tooling (such as pre-commit hooks).

For building the test native-image I used GraalVM CE 1..00-RC5 under Fedora 28, and I just used native-image -jar scalafmt.jar.

This is the test I performed:

[ssaavedra@proton] scalafmt % $(which time) ~/tmp/scalafmt.jar
Reformatting...
  100.0% [##########] 142 source files formatted
47.24user 0.88system 0:07.99elapsed 602%CPU (0avgtext+0avgdata 1054292maxresident)k
0inputs+960outputs (0major+265046minor)pagefaults 0swaps
[ssaavedra@proton] scalafmt % git checkout -- .
[ssaavedra@proton] scalafmt % $(which time) ~/tmp/scalafmt
Reformatting...
  100.0% [##########] 142 source files formatted
8.35user 0.27system 0:03.20elapsed 269%CPU (0avgtext+0avgdata 408940maxresident)k
0inputs+1512outputs (0major+116463minor)pagefaults 0swaps

It is important to instantiate CliOptions() in the main() method instead of using CliOptions.default because the Graal compiler runs AbsoluteFile.userDir at compile-time in order to put the actual value at compilation-time as a constant in the CliOptions.default object. Therefore, in order to have relative paths working again, the CliOptions must be ensured that is instantiated at runtime, and that such constants are not precompiled.

The PriorityQueue reimplementation was needed because currently the Scala version of PriorityQueue won't compile into GraalVM due to some calls not being straightforward enough for the static analyzer and some code gets marked as unreachable and is not included in the binary image.

@ssaavedra
Copy link
Contributor Author

I understand the failing tests are the same that were already failing for the master branch.
Also, this is a proof of concept that I came up with, but I don't see any downsides for incorporating it to the project.

@olafurpg
Copy link
Member

Thank you for this contribution @ssaavedra ! The build failures are legitimate, see the logs https://travis-ci.org/scalameta/scalafmt/jobs/422630060#L7198

[info]   =======
[info]   => Diff
[info]   =======
[info]   -scrut
[info]   -  .branchSwitch(
[info]   -      scrut.value,
[info]   -      casevals = normalcases.map { case (v, _) => v },
[info]   -      casefs =
[info]   -        normalcases.map {
[info]   -          case (_, body) => genExpr(body, _: Focus)
[info]   -        })
[info]   +scrut.branchSwitch(scrut.value,
[info]   +                   casevals = normalcases.map { case (v, _) => v },
[info]   +                   casefs = normalcases.map {
[info]   +                     case (_, body) => genExpr(body, _: Focus)
[info]   +                   }) (FormatTests.scala:81)

Tests on master are green https://travis-ci.org/scalameta/scalafmt/builds/420031450 It's only the release step that is failing.

I am open to merge this if it enables people to build native images locally. What ordering is used for the new priority queue? iirc, State extends Ordered which may not be picked up by the Java PriorityQueue.

@ssaavedra
Copy link
Contributor Author

@olafurpg you are definitely right. Something was to be expected to change, wasn't it? :)

Java will probably not honor the Ordered state, I'll take a look at that.

@ssaavedra
Copy link
Contributor Author

I have just pushed the change for Ordered.

Oddly, I think I'm reversing the arguments order. However, this is the order that makes the tests pass (the reverse order produces test errors), so I'm not sure if this is only on my head :)

Copy link
Member

@olafurpg olafurpg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @ssaavedra ! Very cool, change itself looks good to me. I'm happy to merge this if it enables scalafmt with native-image. Before merging I'd like to test it locally myself. I'm currently traveling and busy with deadlines at work so it may not be right away.

Would be great if other people can try out this branch and report how it works :)

This makes it easier to understand that the Ordering is reversed
in java.util.PriorityQueue v. scala.collection.mutable.PriorityQueue
and makes it less look like a typo.
@olafurpg olafurpg mentioned this pull request Aug 30, 2018
5 tasks
@ssaavedra
Copy link
Contributor Author

Well, thank you for creating and maintaining it in the first place.

I'm ok with further testing. Actually, thanks to Travis and existing tests I have not done the wrong thing already 🙂

I'll tag a v1.6.0-RC4-graalvm1 on my fork in the meantime so that I can push a binary there for the moment. I haven't tried using Travis to build the native-image yet, which could be interesting, but I don't have the time either right now.

Please come back with any issues.

@pjrt
Copy link
Collaborator

pjrt commented Aug 30, 2018

Ok, so tried it out, here's what I've found so far (mostly copying from gitter):

Some off-the-cuff performance comparisons: This isn't a good test (since the versions aren't the same) but GraalVM managed to format my repo in 13s, vs NailGun at 20s vs plain CLI at 34s.

The GraalVM one, however, is master/head (while the other two are 1.5.1) so not an apples to oranges
but still, pretty awesome.

Now, odd thing is that there's now a java process consuming 20% of my RAM. Whatever it is it isn't needed by anything since killing it didn't cause anything to stop working. Running the native scalafmt again did not bring it back. However, running native-image (aka, building the native binary again) causes the process to come back. So it seems the native-image cli needs to start a JVM, but it fails to stop it afterwards?

This is all in a T450s ThinkPad running Arch Linux.

Besides that, I haven't seen any (obvious) issues so far.

@ShaneDelmore
Copy link
Contributor

@ssaavedra Push that binary and I'll take it for a spin as well.

@olafurpg
Copy link
Member

@ShaneDelmore it would be good if you can try to build the binary as well, people will likely have to build their own binaries to begin with as it's unlikely we'll manage to configure the CI to build them for us.

@ShaneDelmore
Copy link
Contributor

It would be good for me to build it, but I'm too busy for that today, but if the artifact is easily retrievable I am not too busy to kick off a background format task.

@ShaneDelmore
Copy link
Contributor

Just spent a couple of minutes on this. With instructions I can try again but quick test not knowing what I am doing was:

Looked at build instructions of native-image -jar scalafmt.jar
Download graal
checkout scalafmt branch
sbt cli/packageBin (is that right?)
I see a file scalafmt/scalafmt-cli/target/scala-2.12/scalafmt-cli_2.12-1.6.0-RC4-graalvm1.jar, I'll try that
native-image -jar scalafmt/scalafmt-cli/target/scala-2.12/scalafmt-cli_2.12-1.6.0-RC4-graalvm1.jar

Build on Server(pid: 51047, port: 63446)
   classlist:     539.78 ms
fatal error: java.lang.NoClassDefFoundError: scala/Product
	at java.lang.ClassLoader.defineClass1(Native Method)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
	at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
	at java.net.URLClassLoader.defineClass(URLClassLoader.java:467)
	at java.net.URLClassLoader.access$100(URLClassLoader.java:73)
	at java.net.URLClassLoader$1.run(URLClassLoader.java:368)
	at java.net.URLClassLoader$1.run(URLClassLoader.java:362)
	at java.security.AccessController.doPrivileged(Native Method)
	at java.net.URLClassLoader.findClass(URLClassLoader.java:361)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
	at java.lang.Class.getDeclaredMethods0(Native Method)
	at java.lang.Class.privateGetDeclaredMethods(Class.java:2701)
	at java.lang.Class.getDeclaredMethod(Class.java:2128)
	at com.oracle.svm.hosted.NativeImageGeneratorRunner.buildImage(NativeImageGeneratorRunner.java:236)
	at com.oracle.svm.hosted.NativeImageGeneratorRunner.build(NativeImageGeneratorRunner.java:372)
	at com.oracle.svm.hosted.server.NativeImageBuildServer.executeCompilation(NativeImageBuildServer.java:390)
	at com.oracle.svm.hosted.server.NativeImageBuildServer.lambda$processCommand$8(NativeImageBuildServer.java:327)
	at com.oracle.svm.hosted.server.NativeImageBuildServer.withJVMContext(NativeImageBuildServer.java:408)
	at com.oracle.svm.hosted.server.NativeImageBuildServer.processCommand(NativeImageBuildServer.java:324)
	at com.oracle.svm.hosted.server.NativeImageBuildServer.processRequest(NativeImageBuildServer.java:268)
	at com.oracle.svm.hosted.server.NativeImageBuildServer.lambda$serve$7(NativeImageBuildServer.java:228)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)
Caused by: java.lang.ClassNotFoundException: scala.Product
	at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
	... 25 more
Error: Processing image build request failed

No luck, but upload a binary or give me a few pointers on how you are building (am I using the wrong scalafmt.jar?) and I will try again.

@pjrt
Copy link
Collaborator

pjrt commented Aug 30, 2018

@ShaneDelmore you want sbt cli/assembly

@ssaavedra
Copy link
Contributor Author

ssaavedra commented Aug 30, 2018

@pjrt that happens because, by default, native-binary compiles in a "compiler server". That is compile-time behavior, as you were realizing, and you can avoid that by adding the flag --no-server to the native-binary compilation line.

And exactly, @ShaneDelmore you want sbt cli/assembly so that the jar has all the dependencies needed.

Besides, good news, a static binary for Linux x86_64 is now available at https://github.com/ssaavedra/scalafmt/releases/tag/v1.6.0-RC4-graalvm1

I don't have other kinds of machines to test this with. Also, setting this up for Travis to pick up should not be that much difficult (at least in Linux), it mainly amounts to downloading the GraalVM from GitHub and using native-image from that folder. But for now, it's easier that everyone builds their own for the moment, so that we don't change the scope of this PR.

Also, thanks everyone for the engagement!

@ShaneDelmore
Copy link
Contributor

Looks great. It's slower once JIT kicks in, and slower on file ops, and garbage collection, but still looks like a big win.

Running it on 3800 scala files I see:
jvm
real 0m56.550s
user 3m50.643s
sys 0m17.476s

native-image
real 1m37.365s
user 3m46.833s
sys 0m4.483s

Additionally I see that jvm finishes enumerating files and starts formatting them on my laptop after 12 seconds, native takes 38 seconds to enumerate files before it starts formatting them. When watching it run you can visibly see the files counting by slower, with frequent pauses of <1s. None of this sounds great, except I think most users are formatting a few files at a time, not thousands, and in that case I see very impressive numbers.

Running on 22 scala files:
jvm
real 0m2.674s
user 0m8.954s
sys 0m0.579s

native-image
real 0m0.292s
user 0m0.506s
sys 0m0.180s

Very nice, that's the kind of thing I won't feel bad about calling in a git hook 👍

@ShaneDelmore
Copy link
Contributor

Agreed on making building in CI a separate PR, and also that it's not that much work. There is a nice demo from scala days that has scripts that could be re-used for this at https://github.com/graalvm/graalvm-demos/tree/master/scala-days-2018/scalac-native

@ssaavedra
Copy link
Contributor Author

@ShaneDelmore do you have any insight as of why enumerating could be slower on the native-image? Also, when running the JVM test, were you also using GraalVM's java? Because that could still be an unfair comparison against vanilla OpenJDK :)

Also, were you using --git 1 or project.git = true on the config file? Because the default recursive strategy is usually dramatically slower on both platforms than using git ls-files.

@ShaneDelmore
Copy link
Contributor

I was using vanilla jdk, no -git, literally just "scalafmt directoryName". No ideas why file enumeration would be slower.

@ShaneDelmore
Copy link
Contributor

Using --git 1 does make quite a difference on the large sample it starts formatting almost immediately native or vanilla, but native is still slower eyeballing it largely because it pauses so frequently. Are there default memory usage flags that happen to differ considerably between the two?

After adding --git 1 vanilla takes 41s and native takes 1m11s. If all of those pauses were removed I think it would be pretty close.

@pjrt
Copy link
Collaborator

pjrt commented Aug 31, 2018

Maybe JIT is better than native when given enough time?

@ssaavedra
Copy link
Contributor Author

It might be related to how GC is performed under SubstrateVM. Flags such as Xmx and Xmn can control the amount of memory available. I think you can try to have a larger young generation to avoid so many garbage collections. I am not sure if this must be specified at compile time or at runtime. Still, for the most usual use-case (IMHO) which is formatting changed files (scalafmt --diff) warmup will beat the JIT in most cases.

@olafurpg
Copy link
Member

~500ms to run Scalafmt on 22 files looks amazing 😍

cc/ @vjovanov any insight on why Substrate underperforms the JVM by ~50% as reported in #1266 (comment) for larger jobs? In particular:

When watching it run you can visibly see the files counting by slower, with frequent pauses of <1s.

@vjovanov
Copy link

Part of the slowdown (~2x) comes from using the Graal CE native-image. To get the best performance out of native-image you will have to run it with profile-guided optimizations (--pgo) from Graal EE.

The other part comes from the GC in {{native-image}}: our GC was not designed for throughput. In most of the cases, this does not make much of a difference. We could probably tweak it for this use-case and get decent numbers.

In my benchmarks, native-image --pgo without any GC pauses is 1.5x faster than HotSpot. I did not measure with GC.

@ssaavedra
Copy link
Contributor Author

Also, in my case, adding -Xmn1g (or higher) avoided garbage collections whatsoever in my testing repo. Not sure if I'd have to worry for higher values having a greater impact when performing the GC if needs to be triggered.

Test for garbage collections happening with -XX:+PrintGC.

I couldn't find a way yet to make -Xmn1g the default value when building it with native-image.

@ShaneDelmore
Copy link
Contributor

I'm not on my laptop right now but are you saying that I can just run

Scalafmt -Xmn1g directoryName

And it will know one param is for native-image and the other is for scalafmt? Easy enough for me to test when I get to my laptop if so.

@ssaavedra
Copy link
Contributor Author

ssaavedra commented Aug 31, 2018

tl;dr: yes.

It seems that the native-image compiler bakes-in some argument handling. I'm not sure if this is even exposed to the inner Java program, but yes, it will detect the -Xmn1g param, and it doesn't seem the Java program will catch the argument. You can use -XX: parameters too when running the ELF and customize debug information at runtime.

@ShaneDelmore
Copy link
Contributor

Handy. Played around with settings a bit and got it to run about the same speed as vanilla jdk with a couple GB young gen.

@ShaneDelmore
Copy link
Contributor

More fun feedback, ee with pgo does indeed beat vanilla jdk on 4000 file test, best run on 4000 files with tweaking different options is 26s, vs 38s for vanilla. Not sure what Oracle's policy is on open source and if ee version would even be allowed to use for an open source project like this, but if so it would be a good default. I can't find anything it doesn't beat the normal version at.

@tanishiking
Copy link
Member

tanishiking commented Sep 1, 2018

Hi, I also built native image and successfully ran it.
It's a great work:tada: Now we achieved instant startup for scalafmt without nailgun.
As @ssaavedra commented, I believe that this native image will lead to great improvement in the user experiences of the integrating scalafmt with editors such as emacs or vim and other script-based tooling(actually it does for me).


In order to confirm that the native image of scalafmt-cli achieved the instant startup and it has acceptable runtime performance, I evaluated the perfomance of three kinds of scalafmt-cli which are all built from current ssaavedra:master,

  • scalafmt-jvm: built normally, run on jvm.
  • ng-nailgun scalafmt: built normally, run on jvm, startup with nailgun
  • scalafmt-native: built with
    • $ sbt cli/assembly
    • $ native-image -jar scalafmt-cli/target/scala-2.12/scalafmt.jar

and ran them on scalameta/scalafmt current master and timed.
(note: I timed scalafmt-ng’s elapsed time for its second run)

Environment

$ uname -a
Linux tanishiking-ThinkPad-X1-Carbon 4.15.0-33-generic #36-Ubuntu SMP Wed Aug 15 16:00:05 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux
$ native-image --version
GraalVM Version 1.0.0-rc5    

Run on scalameta/scalafmt current master

$ /usr/bin/time ./scalafmt-jvm
Reformatting...
  100.0% [##########] 142 source files formatted
37.98user 0.69system 0:06.11elapsed 632%CPU (0avgtext+0avgdata 1001288maxresident)k
0inputs+41984outputs (0major+250221minor)pagefaults 0swaps

$ /usr/bin/time ./scalafmt-native
Reformatting...
  100.0% [##########] 142 source files formatted
6.57user 0.17system 0:02.67elapsed 251%CPU (0avgtext+0avgdata 388632maxresident)k
0inputs+952outputs (0major+99109minor)pagefaults 0swaps

$ /usr/bin/time ng-nailgun scalafmt
Reformatting...
  100.0% [##########] 142 source files formatted
0.00user 0.00system 0:01.36elapsed 0%CPU (0avgtext+0avgdata 1796maxresident)k
0inputs+0outputs (0major+73minor)pagefaults 0swaps     

Run on single scala source code (FormatWriter.scala)

$ /usr/bin/time ./scalafmt-jvm scalafmt-core/shared/src/main/scala/org/scalafmt/internal/FormatWriter.scala
12.38user 0.33system 0:02.85elapsed 445%CPU (0avgtext+0avgdata 363232maxresident)k
0inputs+41064outputs (0major+89218minor)pagefaults 0swaps    

$ /usr/bin/time ./scalafmt-native scalafmt-core/shared/src/main/scala/org/scalafmt/internal/FormatWriter.scala
0.13user 0.04system 0:00.17elapsed 100%CPU (0avgtext+0avgdata 181420maxresident)k       
0inputs+32outputs (0major+41286minor)pagefaults 0swaps 

$ /usr/bin/time ng-nailgun scalafmt scalafmt-core/shared/src/main/scala/org/scalafmt/internal/FormatWriter.scala  
0.00user 0.00system 0:00.16elapsed 1%CPU (0avgtext+0avgdata 1716maxresident)k    
0inputs+0outputs (0major+70minor)pagefaults 0swaps  

Indeed, running on JVM and starting up with nailgun has much better performance comparing to the native image, the native-image achieved great improvement comparing to scalafmt-jvm 🎉
In addition, it had remarkable runtime performance in formatting a single file since the bottleneck of running scalafmt-jvm on a single file is JVM startup time.

@ssaavedra
Copy link
Contributor Author

How is this proposal going? There seem to be no caveats reported about this change. Should I rebase the proposal onto a single commit?

Copy link
Member

@olafurpg olafurpg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the slow responses @ssaavedra I haven't had time yet to try this out myself but the responses from other users in this PR indicate it works well! The diff looks reasonable so let's just give it a go. LGTM 👍

@olafurpg olafurpg merged commit ee714bd into scalameta:master Sep 18, 2018
@olafurpg
Copy link
Member

If you figure out a good way to install git pre-commit hooks then I think a lot of people would be interested in reading such instructions on the Scalafmt website 😉

@ssaavedra
Copy link
Contributor Author

ssaavedra commented Sep 19, 2018

I have a _git_hooks folder under version control, and a "setup-repo.sh" that I call manually, because I don't know how to better automate this on git. Then, my setup-repo.sh reads something like this gist.

Then I use git scalafmt (because that way I already pass -Xmn1g and --git 1 as parameters) to reformat the code, and it gets checked automatically on git commit :-)

@dwijnand
Copy link
Contributor

It is important to instantiate CliOptions() in the main() method instead of using CliOptions.default because the Graal compiler runs AbsoluteFile.userDir at compile-time in order to put the actual value at compilation-time as a constant in the CliOptions.default object. Therefore, in order to have relative paths working again, the CliOptions must be ensured that is instantiated at runtime, and that such constants are not precompiled.

Is this specific to "userDir" or is it generic for val? If it's generic it seems like quite the pitfall.

The PriorityQueue reimplementation was needed

Any reason to not use the same implementation (the new Scala one) for Scala.js too?

Finally, just out of interest, how is it maintained that scalafmt builds on GraalVM?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants