This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
module Concurrent
|
||||
# @!visibility private
|
||||
module Utility
|
||||
|
||||
# @!visibility private
|
||||
module EngineDetector
|
||||
def on_cruby?
|
||||
RUBY_ENGINE == 'ruby'
|
||||
end
|
||||
|
||||
def on_jruby?
|
||||
RUBY_ENGINE == 'jruby'
|
||||
end
|
||||
|
||||
def on_truffleruby?
|
||||
RUBY_ENGINE == 'truffleruby'
|
||||
end
|
||||
|
||||
def on_windows?
|
||||
!(RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/).nil?
|
||||
end
|
||||
|
||||
def on_osx?
|
||||
!(RbConfig::CONFIG['host_os'] =~ /darwin|mac os/).nil?
|
||||
end
|
||||
|
||||
def on_linux?
|
||||
!(RbConfig::CONFIG['host_os'] =~ /linux/).nil?
|
||||
end
|
||||
|
||||
def ruby_version(version = RUBY_VERSION, comparison, major, minor, patch)
|
||||
result = (version.split('.').map(&:to_i) <=> [major, minor, patch])
|
||||
comparisons = { :== => [0],
|
||||
:>= => [1, 0],
|
||||
:<= => [-1, 0],
|
||||
:> => [1],
|
||||
:< => [-1] }
|
||||
comparisons.fetch(comparison).include? result
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# @!visibility private
|
||||
extend Utility::EngineDetector
|
||||
end
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
module Concurrent
|
||||
|
||||
# @!macro monotonic_get_time
|
||||
#
|
||||
# Returns the current time as tracked by the application monotonic clock.
|
||||
#
|
||||
# @param [Symbol] unit the time unit to be returned, can be either
|
||||
# :float_second, :float_millisecond, :float_microsecond, :second,
|
||||
# :millisecond, :microsecond, or :nanosecond default to :float_second.
|
||||
#
|
||||
# @return [Float] The current monotonic time since some unspecified
|
||||
# starting point
|
||||
#
|
||||
# @!macro monotonic_clock_warning
|
||||
def monotonic_time(unit = :float_second)
|
||||
Process.clock_gettime(Process::CLOCK_MONOTONIC, unit)
|
||||
end
|
||||
module_function :monotonic_time
|
||||
end
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
require 'concurrent/utility/engine'
|
||||
# Synchronization::AbstractObject must be defined before loading the extension
|
||||
require 'concurrent/synchronization/abstract_object'
|
||||
|
||||
module Concurrent
|
||||
# @!visibility private
|
||||
module Utility
|
||||
# @!visibility private
|
||||
module NativeExtensionLoader
|
||||
|
||||
def allow_c_extensions?
|
||||
Concurrent.on_cruby?
|
||||
end
|
||||
|
||||
def c_extensions_loaded?
|
||||
defined?(@c_extensions_loaded) && @c_extensions_loaded
|
||||
end
|
||||
|
||||
def load_native_extensions
|
||||
if Concurrent.on_cruby? && !c_extensions_loaded?
|
||||
['concurrent/concurrent_ruby_ext',
|
||||
"concurrent/#{RUBY_VERSION[0..2]}/concurrent_ruby_ext"
|
||||
].each { |p| try_load_c_extension p }
|
||||
end
|
||||
|
||||
if Concurrent.on_jruby? && !java_extensions_loaded?
|
||||
begin
|
||||
require 'concurrent/concurrent_ruby.jar'
|
||||
set_java_extensions_loaded
|
||||
rescue LoadError => e
|
||||
raise e, "Java extensions are required for JRuby.\n" + e.message, e.backtrace
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_error_path(error)
|
||||
if error.respond_to? :path
|
||||
error.path
|
||||
else
|
||||
error.message.split(' -- ').last
|
||||
end
|
||||
end
|
||||
|
||||
def set_c_extensions_loaded
|
||||
@c_extensions_loaded = true
|
||||
end
|
||||
|
||||
def java_extensions_loaded?
|
||||
defined?(@java_extensions_loaded) && @java_extensions_loaded
|
||||
end
|
||||
|
||||
def set_java_extensions_loaded
|
||||
@java_extensions_loaded = true
|
||||
end
|
||||
|
||||
def try_load_c_extension(path)
|
||||
require path
|
||||
set_c_extensions_loaded
|
||||
rescue LoadError => e
|
||||
if load_error_path(e) == path
|
||||
# move on with pure-Ruby implementations
|
||||
# TODO (pitr-ch 12-Jul-2018): warning on verbose?
|
||||
else
|
||||
raise e
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
# @!visibility private
|
||||
extend Utility::NativeExtensionLoader
|
||||
end
|
||||
|
||||
Concurrent.load_native_extensions
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
module Concurrent
|
||||
# @!visibility private
|
||||
module Utility
|
||||
# @private
|
||||
module NativeInteger
|
||||
# http://stackoverflow.com/questions/535721/ruby-max-integer
|
||||
MIN_VALUE = -(2**(0.size * 8 - 2))
|
||||
MAX_VALUE = (2**(0.size * 8 - 2) - 1)
|
||||
|
||||
def ensure_upper_bound(value)
|
||||
if value > MAX_VALUE
|
||||
raise RangeError.new("#{value} is greater than the maximum value of #{MAX_VALUE}")
|
||||
end
|
||||
value
|
||||
end
|
||||
|
||||
def ensure_lower_bound(value)
|
||||
if value < MIN_VALUE
|
||||
raise RangeError.new("#{value} is less than the maximum value of #{MIN_VALUE}")
|
||||
end
|
||||
value
|
||||
end
|
||||
|
||||
def ensure_integer(value)
|
||||
unless value.is_a?(Integer)
|
||||
raise ArgumentError.new("#{value} is not an Integer")
|
||||
end
|
||||
value
|
||||
end
|
||||
|
||||
def ensure_integer_and_bounds(value)
|
||||
ensure_integer value
|
||||
ensure_upper_bound value
|
||||
ensure_lower_bound value
|
||||
end
|
||||
|
||||
def ensure_positive(value)
|
||||
if value < 0
|
||||
raise ArgumentError.new("#{value} cannot be negative")
|
||||
end
|
||||
value
|
||||
end
|
||||
|
||||
def ensure_positive_and_no_zero(value)
|
||||
if value < 1
|
||||
raise ArgumentError.new("#{value} cannot be negative or zero")
|
||||
end
|
||||
value
|
||||
end
|
||||
|
||||
extend self
|
||||
end
|
||||
end
|
||||
end
|
||||
+220
@@ -0,0 +1,220 @@
|
||||
require 'etc'
|
||||
require 'rbconfig'
|
||||
require 'concurrent/delay'
|
||||
|
||||
module Concurrent
|
||||
# @!visibility private
|
||||
module Utility
|
||||
|
||||
# @!visibility private
|
||||
class ProcessorCounter
|
||||
def initialize
|
||||
@processor_count = Delay.new { compute_processor_count }
|
||||
@physical_processor_count = Delay.new { compute_physical_processor_count }
|
||||
@cpu_quota = Delay.new { compute_cpu_quota }
|
||||
@cpu_shares = Delay.new { compute_cpu_shares }
|
||||
end
|
||||
|
||||
def processor_count
|
||||
@processor_count.value
|
||||
end
|
||||
|
||||
def physical_processor_count
|
||||
@physical_processor_count.value
|
||||
end
|
||||
|
||||
def available_processor_count
|
||||
cpu_count = processor_count.to_f
|
||||
quota = cpu_quota
|
||||
|
||||
return cpu_count if quota.nil?
|
||||
|
||||
# cgroup cpus quotas have no limits, so they can be set to higher than the
|
||||
# real count of cores.
|
||||
if quota > cpu_count
|
||||
cpu_count
|
||||
else
|
||||
quota
|
||||
end
|
||||
end
|
||||
|
||||
def cpu_quota
|
||||
@cpu_quota.value
|
||||
end
|
||||
|
||||
def cpu_shares
|
||||
@cpu_shares.value
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def compute_processor_count
|
||||
if Concurrent.on_jruby?
|
||||
java.lang.Runtime.getRuntime.availableProcessors
|
||||
else
|
||||
Etc.nprocessors
|
||||
end
|
||||
end
|
||||
|
||||
def compute_physical_processor_count
|
||||
ppc = case RbConfig::CONFIG["target_os"]
|
||||
when /darwin\d\d/
|
||||
IO.popen("/usr/sbin/sysctl -n hw.physicalcpu", &:read).to_i
|
||||
when /linux/
|
||||
cores = {} # unique physical ID / core ID combinations
|
||||
phy = 0
|
||||
IO.read("/proc/cpuinfo").scan(/^physical id.*|^core id.*/) do |ln|
|
||||
if ln.start_with?("physical")
|
||||
phy = ln[/\d+/]
|
||||
elsif ln.start_with?("core")
|
||||
cid = phy + ":" + ln[/\d+/]
|
||||
cores[cid] = true if not cores[cid]
|
||||
end
|
||||
end
|
||||
cores.count
|
||||
when /mswin|mingw/
|
||||
# Get-CimInstance introduced in PowerShell 3 or earlier: https://learn.microsoft.com/en-us/previous-versions/powershell/module/cimcmdlets/get-ciminstance?view=powershell-3.0
|
||||
result = run('powershell -command "Get-CimInstance -ClassName Win32_Processor -Property NumberOfCores | Select-Object -Property NumberOfCores"')
|
||||
if !result || $?.exitstatus != 0
|
||||
# fallback to deprecated wmic for older systems
|
||||
result = run("wmic cpu get NumberOfCores")
|
||||
end
|
||||
if !result || $?.exitstatus != 0
|
||||
# Bail out if both commands returned something unexpected
|
||||
processor_count
|
||||
else
|
||||
# powershell: "\nNumberOfCores\n-------------\n 4\n\n\n"
|
||||
# wmic: "NumberOfCores \n\n4 \n\n\n\n"
|
||||
result.scan(/\d+/).map(&:to_i).reduce(:+)
|
||||
end
|
||||
else
|
||||
processor_count
|
||||
end
|
||||
# fall back to logical count if physical info is invalid
|
||||
ppc > 0 ? ppc : processor_count
|
||||
rescue
|
||||
return 1
|
||||
end
|
||||
|
||||
def run(command)
|
||||
IO.popen(command, &:read)
|
||||
rescue Errno::ENOENT
|
||||
end
|
||||
|
||||
def compute_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
|
||||
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 compute_cpu_shares
|
||||
if RbConfig::CONFIG["target_os"].include?("linux")
|
||||
if File.exist?("/sys/fs/cgroup/cpu.weight")
|
||||
# cgroups v2: https://docs.kernel.org/admin-guide/cgroup-v2.html#cpu-interface-files
|
||||
# Ref: https://github.com/kubernetes/enhancements/tree/master/keps/sig-node/2254-cgroup-v2#phase-1-convert-from-cgroups-v1-settings-to-v2
|
||||
weight = File.read("/sys/fs/cgroup/cpu.weight").to_f
|
||||
((((weight - 1) * 262142) / 9999) + 2) / 1024
|
||||
elsif File.exist?("/sys/fs/cgroup/cpu/cpu.shares")
|
||||
# cgroups v1: https://kernel.googlesource.com/pub/scm/linux/kernel/git/glommer/memcg/+/cpu_stat/Documentation/cgroups/cpu.txt
|
||||
File.read("/sys/fs/cgroup/cpu/cpu.shares").to_f / 1024
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# create the default ProcessorCounter on load
|
||||
@processor_counter = Utility::ProcessorCounter.new
|
||||
singleton_class.send :attr_reader, :processor_counter
|
||||
|
||||
# Number of processors seen by the OS and used for process scheduling. For
|
||||
# performance reasons the calculated value will be memoized on the first
|
||||
# call.
|
||||
#
|
||||
# When running under JRuby the Java runtime call
|
||||
# `java.lang.Runtime.getRuntime.availableProcessors` will be used. According
|
||||
# to the Java documentation this "value may change during a particular
|
||||
# invocation of the virtual machine... [applications] should therefore
|
||||
# occasionally poll this property." We still memoize this value once under
|
||||
# JRuby.
|
||||
#
|
||||
# Otherwise Ruby's Etc.nprocessors will be used.
|
||||
#
|
||||
# @return [Integer] number of processors seen by the OS or Java runtime
|
||||
#
|
||||
# @see http://docs.oracle.com/javase/6/docs/api/java/lang/Runtime.html#availableProcessors()
|
||||
def self.processor_count
|
||||
processor_counter.processor_count
|
||||
end
|
||||
|
||||
# Number of physical processor cores on the current system. For performance
|
||||
# reasons the calculated value will be memoized on the first call.
|
||||
#
|
||||
# On Windows the Win32 API will be queried for the `NumberOfCores from
|
||||
# Win32_Processor`. This will return the total number "of cores for the
|
||||
# current instance of the processor." On Unix-like operating systems either
|
||||
# the `hwprefs` or `sysctl` utility will be called in a subshell and the
|
||||
# returned value will be used. In the rare case where none of these methods
|
||||
# work or an exception is raised the function will simply return 1.
|
||||
#
|
||||
# @return [Integer] number physical processor cores on the current system
|
||||
#
|
||||
# @see https://github.com/grosser/parallel/blob/4fc8b89d08c7091fe0419ca8fba1ec3ce5a8d185/lib/parallel.rb
|
||||
#
|
||||
# @see http://msdn.microsoft.com/en-us/library/aa394373(v=vs.85).aspx
|
||||
# @see http://www.unix.com/man-page/osx/1/HWPREFS/
|
||||
# @see http://linux.die.net/man/8/sysctl
|
||||
def self.physical_processor_count
|
||||
processor_counter.physical_processor_count
|
||||
end
|
||||
|
||||
# Number of processors cores available for process scheduling.
|
||||
# This method takes in account the CPU quota if the process is inside a cgroup with a
|
||||
# dedicated CPU quota (typically Docker).
|
||||
# Otherwise it returns the same value as #processor_count but as a Float.
|
||||
#
|
||||
# For performance reasons the calculated value will be memoized on the first
|
||||
# call.
|
||||
#
|
||||
# @return [Float] number of available processors
|
||||
def self.available_processor_count
|
||||
processor_counter.available_processor_count
|
||||
end
|
||||
|
||||
# The maximum number of processors cores available for process scheduling.
|
||||
# Returns `nil` if there is no enforced limit, or a `Float` if the
|
||||
# process is inside a cgroup with a dedicated CPU quota (typically Docker).
|
||||
#
|
||||
# Note that nothing prevents setting a CPU quota higher than the actual number of
|
||||
# cores on the system.
|
||||
#
|
||||
# For performance reasons the calculated value will be memoized on the first
|
||||
# call.
|
||||
#
|
||||
# @return [nil, Float] Maximum number of available processors as set by a cgroup CPU quota, or nil if none set
|
||||
def self.cpu_quota
|
||||
processor_counter.cpu_quota
|
||||
end
|
||||
|
||||
# The CPU shares requested by the process. For performance reasons the calculated
|
||||
# value will be memoized on the first call.
|
||||
#
|
||||
# @return [Float, nil] CPU shares requested by the process, or nil if not set
|
||||
def self.cpu_shares
|
||||
processor_counter.cpu_shares
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user