qik-loops 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. data/.gitignore +8 -0
  2. data/LICENSE +21 -0
  3. data/README.rdoc +259 -0
  4. data/Rakefile +48 -0
  5. data/VERSION.yml +5 -0
  6. data/bin/loops +16 -0
  7. data/bin/loops-memory-stats +259 -0
  8. data/generators/loops/loops_generator.rb +28 -0
  9. data/generators/loops/templates/app/loops/APP_README +1 -0
  10. data/generators/loops/templates/app/loops/queue_loop.rb +8 -0
  11. data/generators/loops/templates/app/loops/simple_loop.rb +12 -0
  12. data/generators/loops/templates/config/loops.yml +34 -0
  13. data/generators/loops/templates/script/loops +20 -0
  14. data/init.rb +1 -0
  15. data/lib/loops.rb +167 -0
  16. data/lib/loops/autoload.rb +20 -0
  17. data/lib/loops/base.rb +148 -0
  18. data/lib/loops/cli.rb +35 -0
  19. data/lib/loops/cli/commands.rb +124 -0
  20. data/lib/loops/cli/options.rb +273 -0
  21. data/lib/loops/command.rb +36 -0
  22. data/lib/loops/commands/debug_command.rb +8 -0
  23. data/lib/loops/commands/list_command.rb +11 -0
  24. data/lib/loops/commands/start_command.rb +24 -0
  25. data/lib/loops/commands/stats_command.rb +5 -0
  26. data/lib/loops/commands/stop_command.rb +18 -0
  27. data/lib/loops/daemonize.rb +68 -0
  28. data/lib/loops/engine.rb +207 -0
  29. data/lib/loops/errors.rb +6 -0
  30. data/lib/loops/logger.rb +212 -0
  31. data/lib/loops/process_manager.rb +114 -0
  32. data/lib/loops/queue.rb +78 -0
  33. data/lib/loops/version.rb +31 -0
  34. data/lib/loops/worker.rb +102 -0
  35. data/lib/loops/worker_pool.rb +55 -0
  36. data/loops.gemspec +98 -0
  37. data/spec/loop_lock_spec.rb +61 -0
  38. data/spec/loops/base_spec.rb +92 -0
  39. data/spec/loops/cli_spec.rb +156 -0
  40. data/spec/loops_spec.rb +20 -0
  41. data/spec/rails/another_loop.rb +4 -0
  42. data/spec/rails/app/loops/complex_loop.rb +12 -0
  43. data/spec/rails/app/loops/simple_loop.rb +6 -0
  44. data/spec/rails/config.yml +6 -0
  45. data/spec/rails/config/boot.rb +1 -0
  46. data/spec/rails/config/environment.rb +5 -0
  47. data/spec/rails/config/loops.yml +13 -0
  48. data/spec/spec_helper.rb +110 -0
  49. metadata +122 -0
data/.gitignore ADDED
@@ -0,0 +1,8 @@
1
+ .DS_Store
2
+ doc
3
+ pkg
4
+ .yardoc
5
+ .idea
6
+ spec/rails/logs
7
+ spec/rails/tmp
8
+ *.pid
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License
2
+
3
+ Copyright (c) 2008-2009 Alexey Kovyrin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,259 @@
1
+ = Simple background loops framework
2
+
3
+ <tt>loops</tt> is a small and lightweight framework for Ruby on Rails, Merb and other ruby
4
+ frameworks created to support simple background loops in your application which are usually
5
+ used to do some background data processing on your servers (queue workers, batch tasks
6
+ processors, etc).
7
+
8
+ _Warning_: If you use some pre-2.0 version of this plugin, read a dedicated paragraph below.
9
+
10
+
11
+ == What would you use it for?
12
+
13
+ Originally loops plugin was created to make our own loops code a bit more organized. We used
14
+ to have dozens of different modules with methods that were called with script/runner and then
15
+ used with nohup and other painful backgrounding techniques. When you have such a number of
16
+ loops/workers to run in background it becomes a nightmare to manage them on a regular basis
17
+ (restarts, code upgrades, status/health checking, etc).
18
+
19
+ After a few takes on writing our scripts in a more organized way we were able to generalize most
20
+ of the code so now our loops started looking like a classes with a single mandatory public method
21
+ called *run*. Everything else (spawning many workers, managing them, logging, backgrounding,
22
+ pid-files management, etc) is handled by the plugin itself.
23
+
24
+
25
+ == But there are dozens of libraries like this! Why do we need yet another one?
26
+
27
+ The major idea behind this small project was to create a deadly simple and yet robust framework to
28
+ be able to run some tasks in background and do not think about spawning many workers, restarting
29
+ them when they die, etc. So, if you need to be able to run either one or many copies of your worker
30
+ and you do not want to think about re-spawning your scripts when they die and you do not want to
31
+ spend megabytes of RAM on separate copies of Ruby interpreter (when you run each copy of your
32
+ loop as a separate process controlled by monit/god/etc), then you should try this framework --
33
+ you're going to like it.
34
+
35
+
36
+ == How to install?
37
+
38
+ There are two options when approaching db-charmer installation:
39
+
40
+ * using the gem (recommended)
41
+ * install as a Rails plugin
42
+
43
+ To install as a gem, add this to your environment.rb:
44
+
45
+ config.gem 'loops'
46
+
47
+ And then run the command:
48
+
49
+ sudo rake gems:install
50
+
51
+ To install loops as a Rails plugin you need to do rhw following:
52
+
53
+ ./script/plugin install git://github.com/kovyrin/loops.git
54
+
55
+ This will install the whole package in your vendor/plugins directory.
56
+ For merb applications, just check out the code and place it to the vendor/plugins directory.
57
+
58
+
59
+ After you are done with the installation, you need to generate binary and configuration
60
+ files by running:
61
+
62
+ ./script/generate loops
63
+
64
+ This will create the following list of files:
65
+ * <tt>./script/loops</tt> - binary file that will be used to manage your loops
66
+ * <tt>./config/loops.yml</tt> - example configuration file
67
+ * <tt>./app/loops/simple.rb</tt> - REALLY simple loop example
68
+ * <tt>./app/loops/queue_loop.rb</tt> - simple ActiveMQ queue worker
69
+
70
+
71
+ == How to use?
72
+
73
+ Here is a simple loop scaffold for you to start from (put this file to
74
+ <tt>app/loops/hello_world_loop.rb</tt>):
75
+
76
+ class HelloWorldLoop < Loops::Base
77
+ def run
78
+ with_period_of(1) do # period is in seconds
79
+ debug("Hello, debug log!")
80
+ sleep(config['sleep_period']) # Do something "useful" and make it configurable
81
+ debug("Hello, debug log (yes, once again)!")
82
+ end
83
+ end
84
+ end
85
+
86
+ When you have your loop ready to use, add the following lines to your (maybe empty yet)
87
+ <tt>config/loops.yml</tt> file:
88
+
89
+ hello_world:
90
+ type: simple
91
+ sleep_period: 10
92
+
93
+ This is it! To manage your loop, just run one of the following commands:
94
+
95
+ * To list all configured loops:
96
+
97
+ $ ./script/loops list
98
+
99
+ * To run all enabled (actually non-disabled) loops in foreground:
100
+
101
+ $ ./script/loops start
102
+
103
+ * To run all enabled loops in background:
104
+
105
+ $ ./script/loops start -d
106
+
107
+ * To run specific loop in background:
108
+
109
+ $ ./script/loops start hello_world -d
110
+
111
+ * To see all possible options:
112
+
113
+ $ ./script/loops help
114
+
115
+
116
+ _Notice:_ If you use loops as a gem, you will get a system binary called <tt>loops</tt> which you
117
+ could use instead of your <tt>./script/loops<tt> binary:
118
+
119
+ $ loops help
120
+
121
+
122
+ == How to run more than one worker?
123
+
124
+ If you want to have more than one copy of your worker running, that is as simple as adding one
125
+ option to your loop configuration:
126
+
127
+ hello_world:
128
+ type: simple
129
+ sleep_period: 10
130
+ workers_number: 1
131
+
132
+ This <tt>workers_number</tt> option would tell loops manager to spawn more than one copy of
133
+ your loop and run them in parallel. The only thing you'd need to do is to remember about
134
+ concurrent work of your loops. For example, if you have some kind of database table with elements
135
+ you need to process, you can create a simple database-based locks system or use any
136
+ memcache-based locks.
137
+
138
+
139
+ == How to run more than one loop using the same class?
140
+
141
+ You can run the same loop class with different configuration parameters by explicitly identifying
142
+ the loop class to execute:
143
+
144
+ hello:
145
+ type: simple
146
+ loop_name: some_module/my_worker
147
+ language: English
148
+
149
+ salut:
150
+ type: simple
151
+ loop_name: some_module/my_worker
152
+ language: French
153
+
154
+ Now two independent sets of loops are using the same class <tt>SomeModule::MyWorkerLoop</tt>
155
+ customized by the language parameter.
156
+
157
+
158
+ == I want to keep my loop running on machine reboots. How to do it?
159
+
160
+ We use monit to keep loop monitors runnings. You could use something like this in your configs:
161
+
162
+ check process loop-slow_logs with pidfile /your/project/current/tmp/pids/loop-slow_logs.pid
163
+ group loops
164
+ start program "/your/project/current/script/loops start slow_logs -e loops -p tmp/pids/loop-slow_logs.pid -d"
165
+ stop program "/your/project/current/script/loops stop slow_logs -e loops -p tmp/pids/loop-slow_logs.pid"
166
+
167
+
168
+ == ActiveMQ-based workers? What's that?
169
+
170
+ In some of our worker loops we poll ActiveMQ queue and process its items to perform some
171
+ asynchronous operations. So, to make it simpler for us to create such a workers, we've created
172
+ really simple loops class extension that wraps your code with basic queue polling/acknowledging
173
+ code and as the result, you can create a loops like this:
174
+
175
+ class MyQueueLoop < Loops::Queue
176
+ def process_message(message)
177
+ debug "Received a message: #{message.body}"
178
+ debug "sleeping..."
179
+ sleep(0.5 + rand(10) / 10.0) # do something "useful" with the message :-)
180
+ debug "done..."
181
+ end
182
+ end
183
+
184
+ With configs like this:
185
+
186
+ # An example of a STOMP queue-based loop
187
+ my_queue:
188
+ type: queue
189
+ host: 127.0.0.1
190
+ port: 61613
191
+ queue_name: blah
192
+
193
+ Of course, this solution scales perfectly and to make your queue processing faster you just
194
+ need to add more workers (by adding <tt>workers_number: N</tt> option).
195
+
196
+ _Warning_: This type of loops requires you to have the <tt>stomp</tt> gem installed in your
197
+ system.
198
+
199
+
200
+ == There is this <tt>workers_engine</tt> option in the config file. What do you use it for?
201
+
202
+ There are two so called "workers engines" in loops: <tt>fork</tt> and <tt>thread</tt>.
203
+ They're used to control the way process manager would spawn new loops workers:
204
+ with the <tt>fork</tt> engine we'll load all the loops classes and then fork ruby interpreter
205
+ as many times as many workers we need. With the <tt>thread</tt> engine we'd do Thread.new
206
+ instead of forking. Thread engine could be useful if you are sure your loop won't lock
207
+ ruby interpreter (it does not do native calls, etc) or if you use some interpreter that does
208
+ not support forks (like jruby).
209
+
210
+ The default engine is <tt>fork</tt>.
211
+
212
+
213
+ == What Ruby implementations does it work for?
214
+
215
+ We've tested and used the plugin on MRI 1.8.6/1.8.7 and on JRuby 1.4.0. At this point we do
216
+ not support demonization in JRuby and never tested the code on Ruby 1.9. Obviously because
217
+ of JVM limitations you won't be able to use <tt>fork</tt> workers engine in JRuby, but
218
+ threaded workers do pretty well.
219
+
220
+ Recommended version of ruby ro run loops is Ruby Enterprise Edition. This is because we
221
+ have a support for their Copy-On-Write friendly garbage collector that makes your loops
222
+ much smaller in memory (since they share the most of the code). Even with one loop process
223
+ you'd save some memory because your loop monitor process would share most of the memory with
224
+ the loop itself. When you run on RubyEE, you could use <tt>loops stats</tt> command to get
225
+ detailed loops memory stats:
226
+
227
+ [root@analyics current]# ./script/loops stats
228
+
229
+ --------- Loops processes ----------
230
+ PID PPID VMSize Private Name
231
+ ------------------------------------
232
+ 9062 1 199.3 MB 32.4 MB loops monitor: activemq
233
+ 9234 9062 211.9 MB 37.5 MB loop worker: activemq
234
+ 9251 9062 213.3 MB 38.4 MB loop worker: activemq
235
+ 9267 9062 211.9 MB 37.1 MB loop worker: activemq
236
+ 9268 9062 211.9 MB 38.0 MB loop worker: activemq
237
+ ### Processes: 5
238
+ ### Total private dirty RSS: 183.33 MB
239
+
240
+
241
+
242
+ == Migrating from pre-2.0 releases
243
+
244
+ Before version 2.0 has been released, this code was developed as a Rails plugin only and
245
+ did not have any versions numbering system in place. So we call all those old versions
246
+ a pre-2.0 releases. If you use one of those relases (if your loops plugin does not have
247
+ the <tt>VERSION.yml</tt> file in the root directory), be careful when upgrading because
248
+ there are a few incompatible changes we have made in the loops command: <tt>-h</tt>,
249
+ <tt>-a</tt>, <tt>-s</tt>, <tt>-L</tt> and <tt>-l</tt> options were deprecated
250
+ and replaced with a friendlier word commands.
251
+ Use <tt>loops help</tt> to get help.
252
+
253
+
254
+ == Who are the authors?
255
+
256
+ This plugin has been created in Scribd.com for our internal use and then the sources were opened
257
+ for other people to use. All the code in this package has been developed by Alexey Kovyrin,
258
+ Dmytro Shteflyuk and Alexey Verkhovsky for Scribd.com and is released under the MIT license.
259
+ For more details, see LICENSE file.
data/Rakefile ADDED
@@ -0,0 +1,48 @@
1
+ require 'rake'
2
+
3
+ begin
4
+ require 'jeweler'
5
+ Jeweler::Tasks.new do |gemspec|
6
+ gemspec.name = 'qik-loops'
7
+ gemspec.summary = 'Simple background loops framework for ruby (qik.com)'
8
+ gemspec.description = 'Loops is a small and lightweight framework for Ruby on Rails, Merb and other ruby frameworks created to support simple background loops in your application which are usually used to do some background data processing on your servers (queue workers, batch tasks processors, etc).'
9
+ gemspec.email = 'gleb.pomykalov@qik.com'
10
+ gemspec.homepage = 'http://github.com/glebpom/loops'
11
+ gemspec.authors = ['Alexey Kovyrin', 'Dmytro Shteflyuk', 'Gleb Pomykalov']
12
+ gemspec.files.include ['lib/**/*']
13
+ end
14
+ Jeweler::GemcutterTasks.new
15
+ rescue LoadError
16
+ puts 'Jeweler not available. Install it with: sudo gem install jeweler'
17
+ end
18
+
19
+ begin
20
+ require 'spec/rake/spectask'
21
+
22
+ desc 'Default: run unit tests.'
23
+ task :default => :spec
24
+
25
+ desc 'Test the loops plugin.'
26
+ Spec::Rake::SpecTask.new(:spec) do |t|
27
+ t.libs << 'lib'
28
+ t.pattern = 'spec/**/*_spec.rb'
29
+ t.verbose = true
30
+ t.spec_opts = ['-cfs']
31
+ end
32
+ rescue LoadError
33
+ puts 'RSpec not available. Install it with: sudo gem install rspec'
34
+ end
35
+
36
+ begin
37
+ require 'yard'
38
+ YARD::Rake::YardocTask.new(:yard) do |t|
39
+ t.options = ['--title', 'Loops Documentation']
40
+ if ENV['PRIVATE']
41
+ t.options.concat ['--protected', '--private']
42
+ else
43
+ t.options.concat ['--protected', '--no-private']
44
+ end
45
+ end
46
+ rescue LoadError
47
+ puts 'Yard not available. Install it with: sudo gem install yard'
48
+ end
data/VERSION.yml ADDED
@@ -0,0 +1,5 @@
1
+ ---
2
+ :minor: 1
3
+ :patch: 0
4
+ :build:
5
+ :major: 2
data/bin/loops ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env ruby
2
+ $:.unshift(File.dirname(__FILE__) + '/../lib') unless $:.include?(File.dirname(__FILE__) + '/../lib')
3
+
4
+ require File.dirname(__FILE__) + '/../lib/loops'
5
+
6
+ begin
7
+ # The dup is to keep ARGV intact, so that tools like ruby-debug can respawn.
8
+ failure = Loops::CLI.execute
9
+ Kernel.exit(failure ? 1 : 0)
10
+ rescue SystemExit => e
11
+ Kernel.exit(e.status)
12
+ rescue Exception => e
13
+ STDERR.puts("Unhandled exception: #{e.message} (#{e.class})")
14
+ STDERR.puts(e.backtrace.join("\n"))
15
+ Kernel.exit(1)
16
+ end
@@ -0,0 +1,259 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ #
4
+ # This script is based on Phusion's passenger-memory-stats
5
+ # Almost all of the code is Copyright (c) 2008, 2009 Phusion
6
+ # For more details go to http://www.modrails.com/
7
+ #
8
+
9
+ # ANSI color codes
10
+ RESET = "\e[0m"
11
+ BOLD = "\e[1m"
12
+ WHITE = "\e[37m"
13
+ YELLOW = "\e[33m"
14
+ BLUE_BG = "\e[44m"
15
+
16
+ # Container for tabular data.
17
+ class Table
18
+ def initialize(column_names)
19
+ @column_names = column_names
20
+ @rows = []
21
+ end
22
+
23
+ def add_row(values)
24
+ @rows << values.to_a
25
+ end
26
+
27
+ def add_rows(list_of_rows)
28
+ list_of_rows.each do |row|
29
+ add_row(row)
30
+ end
31
+ end
32
+
33
+ def remove_column(name)
34
+ i = @column_names.index(name)
35
+ @column_names.delete_at(i)
36
+ @rows.each do |row|
37
+ row.delete_at(i)
38
+ end
39
+ end
40
+
41
+ def to_s(title = nil)
42
+ max_column_widths = [1] * @column_names.size
43
+ (@rows + [@column_names]).each do |row|
44
+ row.each_with_index do |value, i|
45
+ max_column_widths[i] = [value.to_s.size, max_column_widths[i]].max
46
+ end
47
+ end
48
+
49
+ format_string = max_column_widths.map{ |i| "%#{-i}s" }.join(" ")
50
+ header = sprintf(format_string, *@column_names).rstrip << "\n"
51
+ if title
52
+ free_space = header.size - title.size - 2
53
+ if free_space <= 0
54
+ left_bar_size = 3
55
+ right_bar_size = 3
56
+ else
57
+ left_bar_size = free_space / 2
58
+ right_bar_size = free_space - left_bar_size
59
+ end
60
+ result = "#{BLUE_BG}#{BOLD}#{YELLOW}\n"
61
+ result << "#{"-" * left_bar_size} #{title} #{"-" * right_bar_size}\n"
62
+ if !@rows.empty?
63
+ result << WHITE
64
+ result << header
65
+ end
66
+ else
67
+ result = header.dup
68
+ end
69
+ if @rows.empty?
70
+ result << RESET
71
+ else
72
+ result << ("-" * header.size) << "#{RESET}\n"
73
+ @rows.each do |row|
74
+ result << sprintf(format_string, *row).rstrip << "\n"
75
+ end
76
+ end
77
+ result
78
+ end
79
+ end
80
+
81
+ class MemoryStats
82
+ class Process
83
+ attr_accessor :pid
84
+ attr_accessor :ppid
85
+ attr_accessor :threads
86
+ attr_accessor :vm_size # in KB
87
+ attr_accessor :rss # in KB
88
+ attr_accessor :name
89
+ attr_accessor :private_dirty_rss # in KB
90
+
91
+ def vm_size_in_mb
92
+ return sprintf("%.1f MB", vm_size / 1024.0)
93
+ end
94
+
95
+ def rss_in_mb
96
+ return sprintf("%.1f MB", rss / 1024.0)
97
+ end
98
+
99
+ def private_dirty_rss_in_mb
100
+ if private_dirty_rss.is_a?(Numeric)
101
+ return sprintf("%.1f MB", private_dirty_rss / 1024.0)
102
+ else
103
+ return "?"
104
+ end
105
+ end
106
+
107
+ def to_a
108
+ return [pid, ppid, vm_size_in_mb, private_dirty_rss_in_mb, rss_in_mb, name]
109
+ end
110
+ end
111
+
112
+ def start
113
+ loops_processes = list_processes(:match =>
114
+ /^loops* (monitor|worker)/)
115
+ print_process_list("Loops processes", loops_processes, :show_ppid => true)
116
+
117
+ if RUBY_PLATFORM !~ /linux/
118
+ puts
119
+ puts "*** WARNING: The private dirty RSS can only be displayed " <<
120
+ "on Linux. You're currently using '#{RUBY_PLATFORM}'."
121
+ elsif ::Process.uid != 0 && loops_processes.any?{ |p| p.private_dirty_rss.nil? }
122
+ puts
123
+ puts "*** WARNING: Please run this tool as root. Otherwise the " <<
124
+ "private dirty RSS of processes cannot be determined."
125
+ end
126
+ end
127
+
128
+ # Returns a list of Process objects that match the given search criteria.
129
+ #
130
+ # # Search by executable path.
131
+ # list_processes(:exe => '/usr/sbin/apache2')
132
+ #
133
+ # # Search by executable name.
134
+ # list_processes(:name => 'ruby1.8')
135
+ #
136
+ # # Search by process name.
137
+ # list_processes(:match => 'Passenger FrameworkSpawner')
138
+ def list_processes(options)
139
+ if options[:exe]
140
+ name = options[:exe].sub(/.*\/(.*)/, '\1')
141
+ if RUBY_PLATFORM =~ /linux/
142
+ ps = "ps -C '#{name}'"
143
+ else
144
+ ps = "ps -A"
145
+ options[:match] = Regexp.new(Regexp.escape(name))
146
+ end
147
+ elsif options[:name]
148
+ if RUBY_PLATFORM =~ /linux/
149
+ ps = "ps -C '#{options[:name]}'"
150
+ else
151
+ ps = "ps -A"
152
+ options[:match] = Regexp.new(" #{Regexp.escape(options[:name])}")
153
+ end
154
+ elsif options[:match]
155
+ ps = "ps -A"
156
+ else
157
+ raise ArgumentError, "Invalid options."
158
+ end
159
+
160
+ processes = []
161
+ case RUBY_PLATFORM
162
+ when /solaris/
163
+ list = `#{ps} -o pid,ppid,nlwp,vsz,rss,comm`.split("\n")
164
+ threads_known = true
165
+ when /darwin/
166
+ list = `#{ps} -w -o pid,ppid,vsz,rss,command`.split("\n")
167
+ threads_known = false
168
+ else
169
+ list = `#{ps} -w -o pid,ppid,nlwp,vsz,rss,command`.split("\n")
170
+ threads_known = true
171
+ end
172
+ list.shift
173
+ list.each do |line|
174
+ line.gsub!(/^ */, '')
175
+ line.gsub!(/ *$/, '')
176
+
177
+ p = Process.new
178
+ if threads_known
179
+ p.pid, p.ppid, p.threads, p.vm_size, p.rss, p.name = line.split(/ +/, 6)
180
+ else
181
+ p.pid, p.ppid, p.vm_size, p.rss, p.name = line.split(/ +/, 5)
182
+ p.threads = "?"
183
+ end
184
+ p.name.sub!(/\Aruby: /, '')
185
+ p.name.sub!(/ \(ruby\)\Z/, '')
186
+ if p.name !~ /^ps/ && (!options[:match] || p.name.match(options[:match]))
187
+ # Convert some values to integer.
188
+ [:pid, :ppid, :vm_size, :rss].each do |attr|
189
+ p.send("#{attr}=", p.send(attr).to_i)
190
+ end
191
+ p.threads = p.threads.to_i if threads_known
192
+
193
+ if platform_provides_private_dirty_rss_information?
194
+ p.private_dirty_rss = determine_private_dirty_rss(p.pid)
195
+ end
196
+ processes << p
197
+ end
198
+ end
199
+ return processes
200
+ end
201
+
202
+ private
203
+ def platform_provides_private_dirty_rss_information?
204
+ return RUBY_PLATFORM =~ /linux/
205
+ end
206
+
207
+ # Returns the private dirty RSS for the given process, in KB.
208
+ def determine_private_dirty_rss(pid)
209
+ total = 0
210
+ File.read("/proc/#{pid}/smaps").split("\n").each do |line|
211
+ line =~ /^(Private)_Dirty: +(\d+)/
212
+ if $2
213
+ total += $2.to_i
214
+ end
215
+ end
216
+ if total == 0
217
+ return nil
218
+ else
219
+ return total
220
+ end
221
+ rescue Errno::EACCES, Errno::ENOENT
222
+ return nil
223
+ end
224
+
225
+ def print_process_list(title, processes, options = {})
226
+ table = Table.new(%w{PID PPID VMSize Private Resident Name})
227
+ table.add_rows(processes)
228
+ if options.has_key?(:show_ppid) && !options[:show_ppid]
229
+ table.remove_column('PPID')
230
+ end
231
+ if platform_provides_private_dirty_rss_information?
232
+ table.remove_column('Resident')
233
+ else
234
+ table.remove_column('Private')
235
+ end
236
+ puts table.to_s(title)
237
+
238
+ if platform_provides_private_dirty_rss_information?
239
+ total_private_dirty_rss = 0
240
+ some_private_dirty_rss_cannot_be_determined = false
241
+ processes.each do |p|
242
+ if p.private_dirty_rss.is_a?(Numeric)
243
+ total_private_dirty_rss += p.private_dirty_rss
244
+ else
245
+ some_private_dirty_rss_cannot_be_determined = true
246
+ end
247
+ end
248
+ puts "### Processes: #{processes.size}"
249
+ printf "### Total private dirty RSS: %.2f MB", total_private_dirty_rss / 1024.0
250
+ if some_private_dirty_rss_cannot_be_determined
251
+ puts " (?)"
252
+ else
253
+ puts
254
+ end
255
+ end
256
+ end
257
+ end
258
+
259
+ MemoryStats.new.start