Skip to content

Improve CLI parallelism #501

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

Merged
merged 3 commits into from
May 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ jobs:
with:
ruby-version: ${{ matrix.ruby }}
bundler-cache: true
cache-version: 2
- run: bundle exec rake

rubocop:
Expand All @@ -36,6 +37,7 @@ jobs:
with:
ruby-version: '3.3'
bundler-cache: true
cache-version: 2
- run: bundle exec rubocop

rubies:
Expand All @@ -51,6 +53,7 @@ jobs:
with:
ruby-version: ${{ matrix.ruby }}
bundler-cache: true
cache-version: 2
- run: bundle exec rake

psych4:
Expand All @@ -68,6 +71,7 @@ jobs:
with:
ruby-version: ${{ matrix.ruby }}
bundler-cache: true
cache-version: 2
- run: bundle exec rake

minimal:
Expand All @@ -83,4 +87,5 @@ jobs:
with:
ruby-version: ${{ matrix.ruby }}
bundler-cache: true
cache-version: 2
- run: bin/test-minimal-support
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Unreleased

* Attempt to detect a QEMU bug that can cause `bootsnap precompile` to hang forever when building ARM64 docker images
from x86_64 machines. See #495.
* Improve CLI to detect cgroup CPU limits and avoid spawning too many worker processes.

# 1.18.4

* Allow using bootsnap without bundler. See #488.
Expand Down
3 changes: 1 addition & 2 deletions lib/bootsnap/cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
require "bootsnap/cli/worker_pool"
require "optparse"
require "fileutils"
require "etc"

module Bootsnap
class CLI
Expand All @@ -29,7 +28,7 @@ def initialize(argv)
self.compile_gemfile = false
self.exclude = nil
self.verbose = false
self.jobs = Etc.nprocessors
self.jobs = nil
self.iseq = true
self.yaml = true
self.json = true
Expand Down
72 changes: 72 additions & 0 deletions lib/bootsnap/cli/worker_pool.rb
Original file line number Diff line number Diff line change
@@ -1,16 +1,88 @@
# frozen_string_literal: true

require "etc"
require "rbconfig"
require "io/wait" unless IO.method_defined?(:wait_readable)

module Bootsnap
class CLI
class WorkerPool
class << self
def create(size:, jobs:)
size ||= default_size
if size > 0 && Process.respond_to?(:fork)
new(size: size, jobs: jobs)
else
Inline.new(jobs: jobs)
end
end

def default_size
nprocessors = Etc.nprocessors
size = [nprocessors, cpu_quota || nprocessors].min
case size
when 0, 1
0
else
if fork_defunct?
$stderr.puts "warning: faulty fork(2) detected, probably in cross platform docker builds. " \
"Disabling parallel compilation."
0
else
size
end
end
end

def cpu_quota
if RbConfig::CONFIG["target_os"].include?("linux")
if File.exist?("/sys/fs/cgroup/cpu.max")
# cgroups v2: https://docs.kernel.org/admin-guide/cgroup-v2.html#cpu-interface-files
cpu_max = File.read("/sys/fs/cgroup/cpu.max")
return nil if cpu_max.start_with?("max ") # no limit

max, period = cpu_max.split.map(&:to_f)
max / period
Copy link

Choose a reason for hiding this comment

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

These lines (and lines 53/54) are returning a float, causing issues later on when trying to spawn the worker pool. This is the error from our CI:

/home/circleci/tmp/vendor/bundle/ruby/3.1.0/gems/bootsnap-1.18.5/lib/bootsnap/cli/worker_pool.rb:164:in `spawn': undefined method `times' for 4.0:Float (NoMethodError)

        @workers = @size.times.map { Worker.new(@jobs) }
                        ^^^^^^

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Woops.

elsif File.exist?("/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_quota_us")
# cgroups v1: https://kernel.googlesource.com/pub/scm/linux/kernel/git/glommer/memcg/+/cpu_stat/Documentation/cgroups/cpu.txt
max = File.read("/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_quota_us").to_i
# If the cpu.cfs_quota_us is -1, cgroup does not adhere to any CPU time restrictions
# https://docs.kernel.org/scheduler/sched-bwc.html#management
return nil if max <= 0

period = File.read("/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_period_us").to_f
max / period
end
end
end

def fork_defunct?
return true unless ::Process.respond_to?(:fork)

# Ref: https://github.com/Shopify/bootsnap/issues/495
# The second forked process will hang on some QEMU environments
r, w = IO.pipe
pids = 2.times.map do
::Process.fork do
exit!(true)
end
end
w.close
r.wait_readable(1) # Wait at most 1s

defunct = false

pids.each do |pid|
_pid, status = ::Process.wait2(pid, ::Process::WNOHANG)
if status.nil? # Didn't exit in 1s
defunct = true
Process.kill(:KILL, pid)
::Process.wait2(pid)
end
end

defunct
end
end

class Inline
Expand Down
Loading