# --------------------------------------------------------------------------------------------------------------------- # GxG Engine interface: rational memory management, and quota supports module GxG # module Engine # ok... why? # example: ::Process::getrlimit(:AS) => [<1Exabyte>,<1Exabyte>] # Now that is the Amsterdam-style pull on a bong with stirrups - so far as dealing with real memory limits goes. # Rather - I want my apps to be *able* to deal with reality as the system resources determine # Eventually provides a *corrected* drop-in replacement for Process module in standard MRI 1.9.2 # See : http://www.ruby-doc.org/core-1.9.3/Process.html # @@details = {} # running effectively in 32 or 64 bits? See: http://www.ruby-forum.com/topic/164583 @@details[:runtime_bits] = ([ -1 ].pack('l!').length * 8) # default to 32 bit size: if @@details[:runtime_bits] == 64 @@details[:runtime_rvalue_size] = 40 else @@details[:runtime_rvalue_size] = 20 end # @@details[:files] = {:maximum_file_descriptors => 1024} # FORNOW : Review : server claims 50% of all system threads, saw a default of 18931 imposed by system quotas. # @@details[:events] = {:maximum_event_descriptors => (GxG::SYSTEM["kernel.threads_max"]["kernel.threads_max"].to_f * 0.5).to_i , :event_descriptors_threshold => 0.05, :event_descriptors_envelope => (20..100), :event_minimum_threshold => 20} @@details[:events] = {:maximum_event_descriptors => 18931, :event_descriptors_threshold => 0.05, :event_descriptors_envelope => (20..100), :event_minimum_threshold => 20} # Fix for Windows: per-process theads max is 2000. if GxG::SYSTEM.platform[:platform] == :windows @@details[:files][:maximum_file_descriptors] = 512 @@details[:events][:maximum_event_descriptors] = 2000 end # @@details[:runtime] = RUBY_ENGINE.to_sym @@details[:runtime_version] = GxG::Version.new({:interpret => RUBY_VERSION, :patch_level => RUBY_PATCHLEVEL, :revision => RUBY_REVISION}) # determine the byte size of a Ruby RVALUE (a.k.a :slot_size) # See: http://www.engineyard.com/blog/2010/mri-memory-allocation-a-primer-for-developers/ # @@details[:memory_load] = {:high => false, :medium => false, :low => true, :percent => 0.0} @@details[:engine_load] = {:high => false, :medium => false, :low => true, :percent => 0.0} # @@details[:key_map] = [] @@details[:key_map] << {:key => "files.max_descriptors", :path => "/:files/:maximum_file_descriptors"} @@details[:key_map] << {:key => "events.max_descriptors", :path => "/:events/:maximum_event_descriptors"} # @@thread_safety = Mutex.new @@event_descriptors = 0 # def self.detail_mapping() @@details[:key_map] end # def self.[](the_path="") # Public interface for accessing system keyed values. # access cached or system-read values on /proc (sysctl-style) keys. (frankly missing from BSD, etc) result = nil reference = ::GxG::Engine::detail_mapping() # reference.to_enum.each do |mapping| if mapping[:key] == the_path @@thread_safety.synchronize { result = @@details.get_at_path(mapping[:path]).clone } break end end # unless result # # attempt to fetch the data from the system somehow. # result = get_system_data(the_path) # unless result.keys.size > 0 # result = nil # end # end # result end # def self.memory_load_high?() @@thread_safety.synchronize { @@details[:memory_load][:high] } end # def self.memory_load_medium?() @@thread_safety.synchronize { @@details[:memory_load][:medium] } end # def self.memory_load_low?() @@thread_safety.synchronize { @@details[:memory_load][:low] } end # def self.memory_load() @@thread_safety.synchronize { @@details[:memory_load][:percent] } end # def self.engine_load_high?() @@thread_safety.synchronize { @@details[:engine_load][:high] } end # def self.engine_load_medium?() @@thread_safety.synchronize { @@details[:engine_load][:medium] } end # def self.engine_load_low?() @@thread_safety.synchronize { @@details[:engine_load][:low] } end # def self.engine_load() @@thread_safety.synchronize { @@details[:engine_load][:percent] } end # def self.determine_memory_load() current_load = ((GxG::Engine::memory_used()[:total].to_f / GxG::SYSTEM.memory_limits()[:process].first.to_f) * 100.0) if (0.0..40.0).include?(current_load) # low @@thread_safety.synchronize { @@details[:memory_load][:high] = false @@details[:memory_load][:medium] = false @@details[:memory_load][:low] = true @@details[:memory_load][:percent] = current_load } else if (40.0..70.0).include?(current_load) # medium @@thread_safety.synchronize { @@details[:memory_load][:high] = false @@details[:memory_load][:medium] = true @@details[:memory_load][:low] = false @@details[:memory_load][:percent] = current_load } else if current_load > 70.0 # high @@thread_safety.synchronize { @@details[:memory_load][:high] = true @@details[:memory_load][:medium] = false @@details[:memory_load][:low] = false @@details[:memory_load][:percent] = current_load } end end end end # def self.determine_engine_load() max_event_descriptors = ::GxG::Engine::maximum_event_descriptors() available_events = ::GxG::Engine::available_event_descriptors() current_load = (((max_event_descriptors - available_events).to_f / max_event_descriptors.to_f) * 100.0) if (0.0..40.0).include?(current_load) # low @@thread_safety.synchronize { @@details[:engine_load][:high] = false @@details[:engine_load][:medium] = false @@details[:engine_load][:low] = true @@details[:memory_load][:percent] = current_load } else if (40.0..70.0).include?(current_load) # medium @@thread_safety.synchronize { @@details[:engine_load][:high] = false @@details[:engine_load][:medium] = true @@details[:engine_load][:low] = false @@details[:engine_load][:percent] = current_load } else if current_load > 70.0 # high @@thread_safety.synchronize { @@details[:engine_load][:high] = true @@details[:engine_load][:medium] = false @@details[:engine_load][:low] = false @@details[:engine_load][:percent] = current_load } end end end end # def self.engine_gear() # inverse of load result = :low @@thread_safety.synchronize { if @@details[:engine_load][:medium] result = :medium end if @@details[:engine_load][:low] result = :high end } result end # def self.memory_gear() # inverse of load result = :low @@thread_safety.synchronize { if @@details[:memory_load][:medium] result = :medium end if @@details[:memory_load][:low] result = :high end } result end # def self.gear() "#{::GxG::Engine::engine_gear().to_s}_#{::GxG::Engine::memory_gear().to_s}".to_sym end # def self.determine_loads() mem_gear = ::GxG::Engine::memory_gear() eng_gear = ::GxG::Engine::engine_gear() { :engine => {:load => ::GxG::Engine::determine_engine_load(), :gear => eng_gear}, :memory => {:load => ::GxG::Engine::determine_memory_load(), :gear => mem_gear}, :gear => "#{eng_gear.to_s}_#{mem_gear.to_s}".to_sym } end # include ::Process # overrides # # [:exec, :fork, :spawn, :exit!, :exit, :abort, :kill, :wait, :wait2, :waitpid, :waitpid2, :waitall, :detach, :pid, :ppid, # . def self.exec(*args) ::Process::exec(*args) end # def self.fork(*args) ::Process::fork(*args) end # def self.spawn(*args) ::Process::spawn(*args) end # def self.exit!(*args) ::Process::exit!(*args) end # def self.exit(*args) ::Process::exit(*args) end # def self.abort(*args) ::Process::abort(*args) end # def self.kill(*args) ::Process::kill(*args) end # def self.wait(*args) ::Process::wait(*args) end # def self.wait2(*args) ::Process::wait2(*args) end # def self.waitpid(*args) ::Process::waitpid(*args) end # def self.waitpid2(*args) ::Process::waitpid2(*args) end # def self.waitall(*args) ::Process::waitall(*args) end # def self.detach(*args) ::Process::detach(*args) end # def self.pid(*args) ::Process::pid(*args) end # def self.ppid(*args) ::Process::ppid(*args) end # # :getpgrp, :setpgrp, :getpgid, :setpgid, :setsid, :getpriority, :setpriority, :getrlimit, :setrlimit, :uid, :gid, :euid, :egid, # def self.getpgrp(*args) ::Process::getpgrp(*args) end # def self.setpgrp(*args) ::Process::setpgrp(*args) end # def self.getpgid(*args) ::Process::getpgid(*args) end # def self.setpgid(*args) ::Process::setpgid(*args) end # def self.setsid(*args) ::Process::setsid(*args) end # def self.getpriority(*args) ::Process::getpriority(*args) end # def self.setpriority(*args) ::Process::setpriority(*args) end # def self.getrlimit(key) result = nil # FIX: GxG::Engine::getrlimit : Awaiting rbx bug #2112 fix : https://github.com/rubinius/rubinius/issues/2112 case ::GxG::Engine::profile()[:engine] when :ruby raw_data = ::Process::getrlimit(key) case key when :AS if GxG::SYSTEM memory_limits = GxG::SYSTEM.memory_limits() result = [raw_data[0],(memory_limits[:process].max)] else result = raw_data end else result = raw_data end when :jruby memory_limits = GxG::SYSTEM.memory_limits() result = [(memory_limits[:process].first),(memory_limits[:process].last)] when :rbx memory_limits = GxG::SYSTEM.memory_limits() result = [(memory_limits[:process].first),(memory_limits[:process].last)] end result end # def self.setrlimit(*args) ::Process::setrlimit(*args) end # def self.uid(*args) ::Process::uid(*args) end # def self.gid(*args) ::Process::gid(*args) end # def self.euid(*args) ::Process::euid(*args) end # def self.egid(*args) ::Process::egid(*args) end # # :initgroups, :groups, :maxgroups, :daemon, :times] # def self.initgroups(*args) ::Process::initgroups(*args) end # def self.groups(*args) ::Process::groups(*args) end # def self.maxgroups(*args) ::Process::maxgroups(*args) end # def self.daemon(*args) ::Process::daemon(*args) end # def self.times(*args) ::Process::times(*args) end # # extensions # def self.profile() @@thread_safety.synchronize { {:engine => @@details[:runtime], :version => @@details[:runtime_version], :bits => @@details[:runtime_bits], :slot_size => @@details[:runtime_rvalue_size]}.clone } end # def self.instance_process(the_class, &block) if block.respond_to?(:call) ::ObjectSpace.each_object(Class) do |the_object| if the_object.alive?() if the_object.is_a?(the_class) block.call(the_object) end end end end end # def self.object_memory_used() # slots = 0 other = 0 ::ObjectSpace.each_object(Class) do |the_object| slots += 1 if the_object.alive?() if the_object.is_a?(::String) other += the_object.bytesize() end end end # ((::GxG::Engine::profile()[:slot_size].to_i * slots) + other) end # def self.memory_used() # ok... why in the fly'n F isn't this provided by core ruby?!? # Attribution: http://stackoverflow.com/questions/7220896/get-current-ruby-process-memory-usage result = {:actual => 0, :virtual => 0, :total => 0} # if ::GxG::SYSTEM case ::GxG::SYSTEM.platform[:platform] when :windows # FORNOW: user object space enumeration result[:actual] = ::GxG::Engine::object_memory_used() when :bsd pid_string = "#{::Process::pid}" env = self.environment() case env[:environment] when :openbsd # See: http://modman.unixdev.net/?sektion=1&page=procmap&manpath=OpenBSD-3.6 # use `procmap -p #{pid_string}` # LATER: GxG::Engine::memory_used (OpenBSD) : determine how to cull procmap text data into :actual and :virtual memory usage. # FORNOW: user object space enumeration result[:actual] = ::GxG::Engine::object_memory_used() # when :freebsd, :dragonfly, :pcbsd # TODO: GxG::Engine::memory_used: Test with FreeBSD, DragonFlyBSD, and PCBSD. raw_data = `top -d 1` raw_data.to_enum(:each_line).each do |the_line| if the_line.split(" ")[0] == pid_string result[:actual] = (the_line.split(" ")[6] << "B").numeric_values([:byte])[:byte].to_i # TODO: GxG::Engine.memory_used() (netbsd) : Find a reliable way to get result[:virtual] mem used. break end end when :netbsd # TODO: GxG::Engine::memory_used: Test with NetBSD. raw_data = `top -d 1` raw_data.to_enum(:each_line).each do |the_line| if the_line.split(" ")[0] == pid_string result[:actual] = (the_line.split(" ")[4] << "B").numeric_values([:byte])[:byte].to_i # TODO: GxG::Engine.memory_used() (netbsd) : Find a reliable way to get result[:virtual] mem used. break end end when :darwin, :macos # TODO: GxG::Engine::memory_used: Test with Darwin/MacOSX. raw_data = `top -l 1` raw_data.to_enum(:each_line).each do |the_line| if the_line.split(" ")[0] == pid_string result[:actual] = (the_line.split(" ")[9] << "B").numeric_values([:byte])[:byte].to_i # TODO: GxG::Engine.memory_used() (darwin) : Find a reliable way to get result[:virtual] mem used. break end end end when :linux, :solaris # PS is universal, but unfortunately universally incorrect, See: http://stackoverflow.com/questions/131303/linux-how-to-measure-actual-memory-usage-of-an-application-or-process # TODO: GxG::Engine::memory_used: create a non-toolbox version for bootstrap memory profiling. # TODO: GxG::Engine::memory_used: Test with Solaris. raw_data = `pmap -d #{::Process::pid} | tail -n 1`.split(" ") result[:actual] = (raw_data[3].to_s << "B").numeric_values()[:byte].to_i result[:virtual] = (raw_data[1].to_s << "B").numeric_values()[:byte].to_i # result[:shared] = (raw_data[5] << "B").numeric_values()[:byte].to_i end end result[:total] = (result[:actual] + result[:virtual]) result end # def self.memory_available() # Memory available for this process result = {:actual => 0, :virtual => 0, :total => 0} if ::GxG::SYSTEM used_memory = ::GxG::Engine::memory_used() free_memory = ::GxG::SYSTEM.memory() limits = ::GxG::Engine::getrlimit(:AS) latitude_total = (limits[0] - used_memory[:actual]) if latitude_total < 0 latitude_total = 0 end # latitude_virtual = (limits[0] - used_memory[:actual]) # latitude_actual = (limits[0] - used_memory[:virtual]) # indicates available ram vs. paged swap first if free_memory[:actual][:free].to_i > limits[0] result[:actual] = latitude_total result[:virtual] = 0 else result[:actual] = free_memory[:actual][:free] # vm calc is crack-monkey, but someplace close to reality. if free_memory[:virtual][:free] > ((limits[0] - result[:actual]) - used_memory[:total]) result[:virtual] = ((limits[0] - result[:actual]) - used_memory[:total]) unless result[:virtual] > 0 result[:virtual] = 0 end else if ((free_memory[:virtual][:free] - result[:actual]) - used_memory[:total]) > 0 result[:virtual] = ((free_memory[:virtual][:free] - result[:actual]) - used_memory[:total]) else result[:virtual] = 0 end end end # end result[:total] = (result[:actual] + result[:virtual]) result end # def self.engine_data_keys(other_platform=nil) # result = [] data = [] data << {:key => "files.max_descriptors", :platforms => [:linux], :operation => {:getconf => "OPEN_MAX", :format => :to_int}} # TODO: Research maximum socket limit under zmq: # See: http://news.ycombinator.com/item?id=1740823 # Also : http://stackoverflow.com/questions/651665/how-many-socket-connections-possible # inventory the various platform variables that affect what threshold of max_socket_count a given system would end up with. # TODO: find a way to ask the OS and ZMQ for: how many descriptors available to this process, and how many in use under zmq 2.x & 3.x respectively? # ALSO: factor in passed preferences for these settings. # update available_file_descriptors when all that becomes clear and is a bit settled. # current_memory = ::GxG::Engine::memory_available()[:actual] # TODO: GxG::Engine::engine_data_keys : determine precise socket size (32bit/64bit arch) for event zmq sockets used (exact type used). # GxG::Engine::profile()[:bits] # zmq_socket_size = 16384 # # event_allocation = 0.5 # # TODO: GxG::Engine::engine_data_keys : tie event allocation percentage into defaults / config.file loader. data << {:key => "events.max_descriptors", :platforms => [:linux], :operation => {:event_allocation => 0.5, :format => :noop}} # See: http://www.metabrew.com/article/a-million-user-comet-application-with-mochiweb-part-3 # Sysctl tuning: http://www.metabrew.com/article/a-million-user-comet-application-with-mochiweb-part-1 # LATER: develop a series of formulas for calculating approx. how many clients one can support under one 'cluster' controller (session manager). data.to_enum.each do |entry| if entry[:platforms].include?((other_platform || ::GxG::SYSTEM.platform()[:platform])) result << entry end end # result end # def self.get_engine_data(keys=[]) # Support for .refresh_data_for(a_path) (private_method) # some sort of sysctl + config + etc for *all* platforms: # "sysctl -n net.ipv4.tcp_sack" becomes: "/net/ipv4/tcp/selective_ack" (external use path) # @details[:memory_limits][:buffers][:socket][:ipc] becomes colllection_point = @details.at_path(":memory_limits/:buffers/:socket/:ipc");collection_point[(key)]=value # procs for gathering data, and formatting for use. # by_platform key between sysctl/config-key and @details hash path. # {:synonyms => ["external-path"], :sysctl => "key-string', :format => :proc-name, :location => "hash-path", :location_key => (:sym or int)} # unless keys.is_a?(::Array) keys = [(keys)] end result = {} # format_noop = Proc.new do |the_data| the_data end format_to_int = Proc.new do |the_string| (the_string.numeric_values()[:integer] || 0) end format_to_float = Proc.new do |the_string| (the_string.numeric_values()[:float] || 0.0) end format_zerofalse = Proc.new do |the_string| ( format_to_int.call(the_string) > 0 ) end format_valid_with_initial = Proc.new do |the_string| raw_data = the_string.split(" ") {:initial => format_to_int.call(raw_data[1]), :valid => ((format_to_int.call(raw_data[0]))..(format_to_int.call(raw_data[2])))} end sysctl_read = Proc.new do |a_key,the_formatter| ( the_formatter.call(`sysctl -n #{a_key.to_s}`.split("\n")[0]) ) end getconf_read = Proc.new do |a_key, the_formatter| ( the_formatter.call(`getconf #{a_key.to_s}`.split("\n")[0]) ) end event_allocation = Proc.new do |the_allocation, the_formatter| # more precise zmq socket byte size goes here. zmq_socket_size = 16384 # (((::GxG::Engine::memory_available()[:actual].to_f * the_allocation.to_f).to_i / zmq_socket_size) - 1) end # reference = ::GxG::Engine::engine_data_keys() # fetch_data = Proc.new do |the_key| data = nil reference.to_enum.each do |entry| if entry[:key] == the_key case entry[:operation][:format] when :noop the_formatter = format_noop when :zerofalse the_formatter = format_zerofalse when :to_int the_formatter = format_to_int when :to_float the_formatter = format_to_float when :valid_with_initial the_formatter = format_valid_with_initial else the_formatter = format_noop end if entry[:operation][:sysctl] data = sysctl_read.call(entry[:operation][:sysctl],the_formatter) else if entry[:operation][:getconf] data = getconf_read.call(entry[:operation][:getconf],the_formatter) else if entry[:operation][:event_allocation] data = event_allocation.call(entry[:operation][:event_allocation],the_formatter) else # end # end end if data break end end end data end # keys.to_enum.each do |specifier| if specifier.is_a?(::String) # if element is String: serves as result Hash key to hold result value. # single result with this result key result[(specifier)] = fetch_data.call(specifier) else if specifier.is_a?(::Hash) # if element is Hash: {ukey => hash_path}, {:key => , :path => } # if hash_path supplied, read the data; set the @details hash element, and return the formatted data if specifier[:key] # {:key => , :path => } result[(specifier[:key])] = fetch_data.call(specifier[:key]) if specifier[:path] @@thread_safety.synchronize { @@details.set_at_path(specifier[:path],result[(specifier[:key])]) } end else # {ukey => hash_path} subkeys = specifier.keys subkeys.to_enum.each do |a_key| result[(a_key)] = fetch_data.call(a_key) @@thread_safety.synchronize { @@details.set_at_path(specifier[(a_key)],result[(a_key)]) } end end end end end # result end # def self.set_engine_data(keys=[]) # LATER: GxG::Engine::set_engine_data : when you figure out system(sudo) authentication - use this for ops that require elevated privs to set system parameters. # Define a set of universal_keys that affect what other keys; upon setting any one, unique-list a set of keys to read back in w/ hash paths. result = nil if nil ::GxG::Engine::get_engine_data(nil,nil) # compare hash value to the_value; return true if successful end result end # ### Descriptors in general def self.descriptor_limits() {:events => @@details[:events][:maximum_event_descriptors], :files => @@details[:files][:maximum_file_descriptors]}.clone end # def self.descriptor_heap_used() result = {:total => 0} slot_size = ::GxG::Engine::profile[:slot_size] file_total = 0 GxG::Engine::file_descriptors().to_enum.each do |item| file_total += item.heap_used() end @@thread_safety.synchronize { result[:files] = file_total unless ::GxG::Engine::descriptor_limits()[:events] == :files # see available_file_descriptors : beware counting event descriptors twice (w/ file descriptors) # TODO: GxG::Engine::descriptor_heap_used : sync w/ more precise event socket size determination result[:events] = (@@event_descriptors * (slot_size + 4)) end } result[:total] = (result[:files].to_i + result[:events].to_i) # result end # ### File Descriptors def self.maximum_file_descriptors() ::GxG::Engine::descriptor_limits()[:files] end # def self.file_descriptors() # Return an Array of currently opened file descriptors. # About File Descriptors: http://stackoverflow.com/questions/848717/handling-more-than-1024-file-descriptors-in-c-on-linux # result = [] exclude_list = [] [::IO, ::IO::descendants()].flatten!.to_enum.each do |the_class| unless exclude_list.include?(the_class) ::ObjectSpace.each_object(the_class) do |the_io| unless (the_io.closed? || result.include?(the_io)) result << the_io end end end end result end # def self.available_file_descriptors() (::GxG::Engine::maximum_file_descriptors() - ::GxG::Engine::file_descriptors().size) end # # ### Event Descriptors def self.maximum_event_descriptors() ::GxG::Engine::descriptor_limits()[:events] end # def self.event_descriptors_threshold() # Percentage of allocation that must be available for events to be released. @@thread_safety.synchronize { @@details[:events][:event_descriptors_threshold].clone } end # def self.event_minimum_threshold() # Percentage of allocation that must be available for events to be released. @@thread_safety.synchronize { @@details[:events][:event_minimum_threshold].clone } end # def self.events_in_use() @@thread_safety.synchronize { @@event_descriptors } end # def self.available_event_descriptors() result = 0 current = 0 @@thread_safety.synchronize { current = (::GxG::Engine::maximum_event_descriptors() - @@event_descriptors) } if current > 0 result = current end result end # def self.reserve_event_descriptor() result = false if ::GxG::Engine::available_event_descriptors() > 0 @@thread_safety.synchronize { @@event_descriptors += 1 } result = true else raise Errno::ENOMEM, "maximum number of event descriptors in use (#{GxG::Engine::maximum_event_descriptors().to_s})" end result end # def self.release_event_descriptor() result = false @@thread_safety.synchronize { if @@event_descriptors > 0 @@event_descriptors -= 1 end } result = true result end # def self.determine_event_allocations() # total = ::GxG::Engine::maximum_event_descriptors() dispatchers = [] [::GxG::Events::EventDispatcher, ::GxG::Events::EventDispatcher::descendants()].flatten!.each do |the_class| ::ObjectSpace.each_object(the_class) do |the_dispatcher| if the_dispatcher.alive?() if the_dispatcher.running?() dispatchers << the_dispatcher end end end end # Note : a ghost-dispatcher representing all other system funcitons is added to the dispatcher count. maximum_events = (total.to_f / (dispatchers.size + 1).to_f).to_i threshold = (maximum_events.to_f * ::GxG::Engine::event_descriptors_threshold().to_f).to_i unless threshold > 0 threshold = 1 end @@thread_safety.synchronize { @@details[:events][:event_minimum_threshold] = threshold @@details[:events][:event_descriptors_envelope] = ((threshold)..(maximum_events)) } true end # def self.event_allocation_envelope() result = (20..100) @@thread_safety.synchronize { result = @@details[:events][:event_descriptors_envelope] } result end # end # FIX / Review : this appears to set file and event max descriptors to NIL on Winderz. Do I need this? # ::GxG::Engine::get_engine_data(::GxG::Engine::detail_mapping()) # end #