postmodern / chruby

Changes the current Ruby
MIT License
2.85k stars 190 forks source link

Is there a deterministic way of checking whether bundle exec is necessary? #486

Open mculp opened 1 year ago

mculp commented 1 year ago

Sort of related to https://github.com/postmodern/chruby/pull/433, I've recently found out that bundle exec adds a significant amount of time to commands as opposed to the naked command when the binstub doesn't exist. This is especially important when running linters as a pre-commit hook.

I posted on lefthook's discussion board about this, but our team mostly uses chruby in local environments because it's in our onboarding guide, and y'all seem to more likely to know the answer to this: is there a deterministic or even near-deterministic way of checking whether bundle exec is necessary?

I came up with this pretty naive exec-wrapper script:

#!/usr/bin/env sh

if test -n "$RUBY_VERSION"
then
  if test -x "$0"
  then
    exec "$@"
  elif test -x chruby-exec
  then
    chruby-exec "$RUBY_VERSION" -- "$@"
  fi
else
  bundle exec "$@"
fi
Command Mean [ms] Min [ms] Max [ms] Relative
rubocop --server ... 190.9 ± 5.9 183.4 199.9 1.00
bin/exec-wrapper rubocop --server ... 200.2 ± 8.4 184.1 212.1 1.05 ± 0.05
chruby-exec 3.1.2 -- rubocop --server ... 210.5 ± 7.8 195.2 222.7 1.10 ± 0.05
bundle exec rubocop ... 709.4 ± 5.2 702.7 716.7 3.72 ± 0.12
Benchmark 1: bin/exec-wrapper rubocop --server ...
  Time (mean ± σ):     197.1 ms ±   4.9 ms    [User: 93.1 ms, System: 30.3 ms]
  Range (min … max):   186.4 ms … 202.3 ms    14 runs

Benchmark 2: rubocop --server ...
  Time (mean ± σ):     189.4 ms ±   7.3 ms    [User: 92.3 ms, System: 28.8 ms]
  Range (min … max):   173.7 ms … 200.5 ms    15 runs

Benchmark 3: bundle exec rubocop --server ...
  Time (mean ± σ):     706.4 ms ±   5.3 ms    [User: 567.1 ms, System: 64.8 ms]
  Range (min … max):   699.2 ms … 715.6 ms    10 runs

Benchmark 4: chruby-exec 3.1.2 -- rubocop ...
  Time (mean ± σ):     212.0 ms ±   5.2 ms    [User: 98.5 ms, System: 34.4 ms]
  Range (min … max):   203.9 ms … 219.4 ms    14 runs

Summary
  'rubocop --server ...
    1.04 ± 0.05 times faster than 'bin/exec-wrapper rubocop --server ...'
    1.12 ± 0.05 times faster than 'chruby-exec 3.1.2 -- rubocop --server ...'
    3.73 ± 0.15 times faster than 'bundle exec rubocop --server ...'

I would like to commit this pretty significant quality of life change, but I don't want to break anyone's machine by missing something in this script.

eregon commented 1 year ago

All except bundle exec rubocop will pick the latest version of rubocop installed and not the one specified in the Gemfile for the project, which can lead to failures, etc.

That big an overhead for bundle exec seems too much though, could you open an issue at https://github.com/rubygems/rubygems/issues ?

mculp commented 1 year ago

I see, that's exactly the answer I was looking for. What if we were using postmodern's gem_home gem or some other "gemset" tool? Is that still a thing? gem_home itself looks pretty out of date.

Sure, I'll file an issue there. I first noticed this when someone else mentioned it happening with them as well in an attempt to improve rubocop boot time.

eregon commented 1 year ago

IMHO gemset tools are pretty much useless nowadays. Having a dedicated gem_home for an app to avoid bundle exec would only work if you wipe out the entire gem home every time you update a gem in Gemfile/Gemfile.lock and then reinstall everything, and only if there are no extra gems installed in the default gem home. Basically, very messy and won't be practical I think. It would also not work if the a default/bundled gems of that Ruby is more recent than what you use (would pick that version instead).

postmodern commented 1 year ago

It might be possible to check if vendor/bundle exists and add the $ruby/$ruby_api_version/bin directory to PATH. However, like you pointed out, you might not want all executables to automatically use the bundled version. Also, installing gems into vendor/bundle per-project might consume extra disk space.

I've been using this bash alias to selectively make certain common executables "bundle aware".

It would be nice if rubygems handled this automatically in it's own binstubs which are generated when you install a gem with executables.

mculp commented 1 year ago

yeah, I'm trying to avoid bundle exec for linting because it's so much faster without bundle exec (and runs fine w/ chruby)

I went from greater than 2s running rubocop on 1 staged file to 190ms (with some other tweaks), but bundle exec is a huge chunk of that -- at least 500ms and sometimes up to 1000ms on my machine.

Maybe instead of trying to genericize for all commands, I'll write a specific wrapper for Rubocop. If the Rubocop server is running, then the files have been loaded already and there shouldn't be a need for bundle exec