File: processor_counter.rb

package info (click to toggle)
ruby-concurrent 1.3.5-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 4,136 kB
  • sloc: ruby: 30,875; java: 6,128; ansic: 265; makefile: 26; sh: 19
file content (220 lines) | stat: -rw-r--r-- 8,715 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
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