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

Add GCC to runtime dependencies #127

Merged
merged 1 commit into from
Nov 29, 2018
Merged

Add GCC to runtime dependencies #127

merged 1 commit into from
Nov 29, 2018

Conversation

schneems
Copy link
Contributor

Ruby 2.6.0 ships with a JIT (just in time compiler) that utilizes GCC (or any other cc such as clang) at runtime. This version of Ruby will be released on December 25th, 2018 and Heroku needs to provide support for this feature out of the box.

Background

The design alternatives can be found in this document

https://docs.google.com/document/d/1q5yEpRddUU2JPynESmffI_BpLDW4XaBgO7iyJE-mwio/edit?usp=sharing

This PR

This PR adds the gcc package to Heroku-16 and Heroku-18 runtime images.

Size cost

Adding the gcc package increases the runtime stack image by about 84mb:

Before adding gcc

-----> Size breakdown...
# ...
       ubuntu:18.04                      85.8MB
       heroku/heroku:18                  428MB
       heroku/heroku:18-build            805MB

After adding gcc

-----> Size breakdown...
# ...
       ubuntu:18.04                      85.8MB
       heroku/heroku:18                  512MB
       heroku/heroku:18-build            805MB

It looks like this works with only gcc, however if we end up needing build-essential then the cost would increase by 64MB to 576MB.

Testing JIT on the stack images testing

On a currently running Heroku-18 dyno with Ruby 2.6:

$ hs run bash -a infinite-everglades-58502
~ $ ruby -v
ruby 2.6.0preview3 (2018-11-06 trunk 65578) [x86_64-linux]
~ $ ruby -e "def a; end; def b; end; a; a; a; a; a; a; a;  puts 'done'" --jit --jit-verbose=1
MJIT: Error in execv: /usr/bin/gcc
MJIT warning: Making precompiled header failed on compilation. Stopping MJIT worker...
done

Note: The warnings in the output indicate gcc could not be found

On a local docker image with gcc available:

$ bin/build.sh heroku-18 heroku/heroku:18 heroku/heroku:18-build
# ...
$ docker run -i -t heroku/heroku:18 /bin/bash
root@4437f834d502:/# mkdir -p vendor/ruby-2.6.0
root@4437f834d502:/# cd vendor/ruby-2.6.0
root@4437f834d502:/vendor/ruby-2.6.0# curl https://heroku-buildpack-ruby.s3.amazonaws.com/heroku-18/ruby-2.6.0.tgz -s -o - | tar zxf -
root@4437f834d502:/vendor/ruby-2.6.0# bin/ruby -e "def a; end; def b; end; a; a; a; a; a; a; a;  puts 'done'" --jit --jit-verbose=1
done
Successful MJIT finish

Note: The output indicates that MJIT was successful

For more output you can also apply --jit-wait which will force mjit to run synchronously (instead of in a background thread).

cc/ @hone @hunterloftis

Ruby 2.6.0 ships with a JIT (just in time compiler) that utilizes GCC (or any other `cc` such as clang) at runtime. This version of Ruby will be released on December 25th, 2018 and Heroku needs to provide support for this feature out of the box.

## Background

The design alternatives can be found in this document

https://docs.google.com/document/d/1q5yEpRddUU2JPynESmffI_BpLDW4XaBgO7iyJE-mwio/edit?usp=sharing

## This PR

This PR adds the `gcc` package to Heroku-16 and Heroku-18 runtime images.

## Size cost

Adding the `gcc` package increases the runtime stack image by about 84mb:

Before adding `gcc`

```
-----> Size breakdown...
# ...
       ubuntu:18.04                      85.8MB
       heroku/heroku:18                  428MB
       heroku/heroku:18-build            805MB
```

After adding `gcc`

```
-----> Size breakdown...
# ...
       ubuntu:18.04                      85.8MB
       heroku/heroku:18                  512MB
       heroku/heroku:18-build            805MB
```

It looks like this works with only `gcc`, however if we end up needing `build-essential` then the cost would increase by 64MB to 576MB.

## Testing JIT on the stack images testing

On a currently running Heroku-18 dyno with Ruby 2.6:

```
$ hs run bash -a infinite-everglades-58502
~ $ ruby -v
ruby 2.6.0preview3 (2018-11-06 trunk 65578) [x86_64-linux]
~ $ ruby -e "def a; end; def b; end; a; a; a; a; a; a; a;  puts 'done'" --jit --jit-verbose=1
MJIT: Error in execv: /usr/bin/gcc
MJIT warning: Making precompiled header failed on compilation. Stopping MJIT worker...
done
```

> Note: The warnings in the output indicate gcc could not be found

On a local docker image with `gcc` available:

```
$ bin/build.sh heroku-18 heroku/heroku:18 heroku/heroku:18-build
# ...
$ docker run -i -t heroku/heroku:18 /bin/bash
root@4437f834d502:/# mkdir -p vendor/ruby-2.6.0
root@4437f834d502:/# cd vendor/ruby-2.6.0
root@4437f834d502:/vendor/ruby-2.6.0# curl https://heroku-buildpack-ruby.s3.amazonaws.com/heroku-18/ruby-2.6.0.tgz -s -o - | tar zxf -
root@4437f834d502:/vendor/ruby-2.6.0# bin/ruby -e "def a; end; def b; end; a; a; a; a; a; a; a;  puts 'done'" --jit --jit-verbose=1
done
Successful MJIT finish
```
 
> Note: The output indicates that MJIT was successful

For more output you can also apply `--jit-wait` which will force mjit to run synchronously (instead of in a background thread).

cc/ @hone @hunterloftis
@schneems
Copy link
Contributor Author

Also looks like you can get a return status code by trying to interact with MJIT internals if it's not active. On current Heroku 18 dyno:

$ hs run bash -a infinite-everglades-58502
~ $ ruby --jit -e "RubyVM::MJIT.pause(wait: false)" ; echo $?
Traceback (most recent call last):
	1: from -e:1:in `<main>'
-e:1:in `pause': MJIT is not enabled (RuntimeError)
1

@dmathieu dmathieu requested a review from a team November 19, 2018 07:45
@dmathieu
Copy link

For the record, @schneems and I have been discussing possibly using musl instead of GCC to reduce impact.
Richard has been poking the MJIT maintainers about that idea.

If you want to use musl for MJIT, of course ruby binary should be linked against musl as well. It may have difference in that layer (can musl's malloc be configured with MALLOC_ARENA_MAX? is it fast and memory efficient? etc). I would use Optcarrot to measure pure-VM performance and to compare it with JIT.


musl is not currently tested and the maintainer is worried about differences in performance and configuration
also Ruby would need to be recompiled with the same cc that mjit uses, so we would need to re-tool to build 2.6.0+ with musl.

Our thinking now is therefore that the slug size reduction isn't worth the fairly large effort it would be to switch to musl instead of gcc.

@dmathieu dmathieu merged commit fcd8ba3 into master Nov 29, 2018
@dmathieu dmathieu deleted the schneems/gcc-2 branch November 29, 2018 09:18
@schneems
Copy link
Contributor Author

Thanks 🙏 ! When will this be released so I can test and verify everything works in prod?

@dmathieu
Copy link

Next week.

@dmathieu
Copy link

dmathieu commented Dec 3, 2018

I shipped it this morning. https://devcenter.heroku.com/changelog-items/1531

@schneems
Copy link
Contributor Author

schneems commented Dec 3, 2018

Confirm that it works! Thanks ❤️

~ $ ruby --jit -e "RubyVM::MJIT.pause(wait: false)" ; echo $?
0

edmorley added a commit that referenced this pull request Mar 21, 2024
GCC was added to our run images back in #127 in order to support
Ruby 2.6's then new MJIT feature:
https://www.ruby-lang.org/en/news/2018/12/25/ruby-2-6-0-released/

However, since then:
- The Ruby MJIT feature hasn't really resulted in significant
  performance benefits for real world use-cases like a Rails app.
- Ruby's MJIT has since been superseded by YJIT, which is faster and
  doesn't need GCC at runtime:
  https://shopify.engineering/yjit-just-in-time-compiler-cruby
  https://shopify.engineering/ruby-yjit-is-production-ready
- The image size impact of including build tools in our run images has
  increased considerably (#127 quoted it as 84 MB, but measuring now
  it's 203 MB).
- In a CNB world, image size is much more of a concern than in the S3
  `.img` + slug model, so we need to be more selective over what
  packages we include.

As such, this removes `gcc`, `make` and `libc6-dev` from the run image
for a 203 MB saving (they are still present in the build image, hence
zero changes to `installed-packages-*.txt` for that image).

Richard (Ruby owner) has confirmed he's fine with this change.

Note: I'm intentionally not adding `binutils` back (which was a
transitive dependency), since its 15 MB cost is not worth it for the
~once a year platform operator debugging use-case.

Before:

```
-----> Size breakdown...
       heroku/heroku:24         661MB
       heroku/heroku:24-build   1.13GB
```

After:

```
-----> Size breakdown...
       heroku/heroku:24         458MB
       heroku/heroku:24-build   1.13GB
```

Towards #266.
GUS-W-15159536.
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.

3 participants