Skip to content

Commit dcd9327

Browse files
authored
Merge pull request #501 from Shopify/improve-parallelism
Improve CLI parallelism
2 parents 0dc269d + abe4ec3 commit dcd9327

File tree

4 files changed

+82
-2
lines changed

4 files changed

+82
-2
lines changed

.github/workflows/ci.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ jobs:
2424
with:
2525
ruby-version: ${{ matrix.ruby }}
2626
bundler-cache: true
27+
cache-version: 2
2728
- run: bundle exec rake
2829

2930
rubocop:
@@ -36,6 +37,7 @@ jobs:
3637
with:
3738
ruby-version: '3.3'
3839
bundler-cache: true
40+
cache-version: 2
3941
- run: bundle exec rubocop
4042

4143
rubies:
@@ -51,6 +53,7 @@ jobs:
5153
with:
5254
ruby-version: ${{ matrix.ruby }}
5355
bundler-cache: true
56+
cache-version: 2
5457
- run: bundle exec rake
5558

5659
psych4:
@@ -68,6 +71,7 @@ jobs:
6871
with:
6972
ruby-version: ${{ matrix.ruby }}
7073
bundler-cache: true
74+
cache-version: 2
7175
- run: bundle exec rake
7276

7377
minimal:
@@ -83,4 +87,5 @@ jobs:
8387
with:
8488
ruby-version: ${{ matrix.ruby }}
8589
bundler-cache: true
90+
cache-version: 2
8691
- run: bin/test-minimal-support

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Unreleased
22

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

59
* Allow using bootsnap without bundler. See #488.

lib/bootsnap/cli.rb

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
require "bootsnap/cli/worker_pool"
55
require "optparse"
66
require "fileutils"
7-
require "etc"
87

98
module Bootsnap
109
class CLI
@@ -29,7 +28,7 @@ def initialize(argv)
2928
self.compile_gemfile = false
3029
self.exclude = nil
3130
self.verbose = false
32-
self.jobs = Etc.nprocessors
31+
self.jobs = nil
3332
self.iseq = true
3433
self.yaml = true
3534
self.json = true

lib/bootsnap/cli/worker_pool.rb

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,88 @@
11
# frozen_string_literal: true
22

3+
require "etc"
4+
require "rbconfig"
5+
require "io/wait" unless IO.method_defined?(:wait_readable)
6+
37
module Bootsnap
48
class CLI
59
class WorkerPool
610
class << self
711
def create(size:, jobs:)
12+
size ||= default_size
813
if size > 0 && Process.respond_to?(:fork)
914
new(size: size, jobs: jobs)
1015
else
1116
Inline.new(jobs: jobs)
1217
end
1318
end
19+
20+
def default_size
21+
nprocessors = Etc.nprocessors
22+
size = [nprocessors, cpu_quota || nprocessors].min
23+
case size
24+
when 0, 1
25+
0
26+
else
27+
if fork_defunct?
28+
$stderr.puts "warning: faulty fork(2) detected, probably in cross platform docker builds. " \
29+
"Disabling parallel compilation."
30+
0
31+
else
32+
size
33+
end
34+
end
35+
end
36+
37+
def cpu_quota
38+
if RbConfig::CONFIG["target_os"].include?("linux")
39+
if File.exist?("/sys/fs/cgroup/cpu.max")
40+
# cgroups v2: https://docs.kernel.org/admin-guide/cgroup-v2.html#cpu-interface-files
41+
cpu_max = File.read("/sys/fs/cgroup/cpu.max")
42+
return nil if cpu_max.start_with?("max ") # no limit
43+
44+
max, period = cpu_max.split.map(&:to_f)
45+
max / period
46+
elsif File.exist?("/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_quota_us")
47+
# cgroups v1: https://kernel.googlesource.com/pub/scm/linux/kernel/git/glommer/memcg/+/cpu_stat/Documentation/cgroups/cpu.txt
48+
max = File.read("/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_quota_us").to_i
49+
# If the cpu.cfs_quota_us is -1, cgroup does not adhere to any CPU time restrictions
50+
# https://docs.kernel.org/scheduler/sched-bwc.html#management
51+
return nil if max <= 0
52+
53+
period = File.read("/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_period_us").to_f
54+
max / period
55+
end
56+
end
57+
end
58+
59+
def fork_defunct?
60+
return true unless ::Process.respond_to?(:fork)
61+
62+
# Ref: https://github.com/Shopify/bootsnap/issues/495
63+
# The second forked process will hang on some QEMU environments
64+
r, w = IO.pipe
65+
pids = 2.times.map do
66+
::Process.fork do
67+
exit!(true)
68+
end
69+
end
70+
w.close
71+
r.wait_readable(1) # Wait at most 1s
72+
73+
defunct = false
74+
75+
pids.each do |pid|
76+
_pid, status = ::Process.wait2(pid, ::Process::WNOHANG)
77+
if status.nil? # Didn't exit in 1s
78+
defunct = true
79+
Process.kill(:KILL, pid)
80+
::Process.wait2(pid)
81+
end
82+
end
83+
84+
defunct
85+
end
1486
end
1587

1688
class Inline

0 commit comments

Comments
 (0)