rbkit 0.0.1 → 0.1.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +5 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +17 -0
  5. data/CHANGELOG.md +5 -0
  6. data/Gemfile +6 -2
  7. data/LICENSE.txt +22 -0
  8. data/README.md +74 -11
  9. data/Rakefile +27 -3
  10. data/docs/EVENT_FORMAT.md +195 -0
  11. data/experiments/object_dump.rb +1 -1
  12. data/experiments/rbkit_command_test.rb +3 -1
  13. data/experiments/using_rbkit.rb +1 -1
  14. data/ext/extconf.rb +95 -12
  15. data/ext/rbkit_allocation_info.c +91 -0
  16. data/ext/rbkit_allocation_info.h +17 -0
  17. data/ext/rbkit_event.c +71 -0
  18. data/ext/rbkit_event.h +63 -0
  19. data/ext/rbkit_event_packer.c +251 -0
  20. data/ext/rbkit_event_packer.h +23 -0
  21. data/ext/rbkit_message_aggregator.c +9 -16
  22. data/ext/rbkit_message_aggregator.h +0 -1
  23. data/ext/rbkit_object_graph.c +6 -49
  24. data/ext/rbkit_object_graph.h +12 -3
  25. data/ext/rbkit_test_helper.c +25 -0
  26. data/ext/rbkit_test_helper.h +1 -0
  27. data/ext/rbkit_tracer.c +106 -323
  28. data/ext/rbkit_tracer.h +2 -10
  29. data/lib/rbkit.rb +57 -35
  30. data/lib/rbkit/rbkit_gc.rb +79 -0
  31. data/lib/rbkit/version.rb +1 -1
  32. data/logo.png +0 -0
  33. data/rbkit.gemspec +1 -0
  34. data/setup.rb +37 -0
  35. data/spec/gc_stat_spec.rb +31 -0
  36. data/spec/hash_event_spec.rb +29 -0
  37. data/spec/obj_created_spec.rb +52 -0
  38. data/spec/obj_destroyed_spec.rb +44 -0
  39. data/spec/object_space_dump_spec.rb +77 -0
  40. data/spec/rbkit_helpful_messages_spec.rb +61 -0
  41. data/spec/spec_helper.rb +11 -6
  42. data/spec/start_server_spec.rb +29 -0
  43. data/spec/status_spec.rb +48 -0
  44. data/spec/support/foo_bar_sample_class.rb +24 -0
  45. data/spec/support/have_message_matcher.rb +26 -0
  46. metadata +40 -4
  47. data/schema/tracing_info.md +0 -8
@@ -9,27 +9,19 @@
9
9
  #include "msgpack.h"
10
10
 
11
11
  // Structure is used to store profiling data
12
- struct gc_hooks {
12
+ typedef struct _rbkit_logger {
13
13
  VALUE hooks[3];
14
14
  VALUE enabled;
15
- void (*funcs[3])(void *data, int event_index);
16
- void *args[3];
17
15
  void *data;
18
16
  st_table *object_table;
19
17
  st_table *str_table;
20
18
  VALUE newobj_trace;
21
19
  VALUE freeobj_trace;
22
- int keep_remains;
23
20
  msgpack_sbuffer *sbuf;
24
21
  msgpack_packer *msgpacker;
25
- };
22
+ } rbkit_logger;
26
23
 
27
24
  char * tracer_string_recv(void *socket);
28
25
  int tracer_string_send(void *socket, const char *message);
29
- void pack_value_object(msgpack_packer *packer, VALUE value);
30
- void pack_string(msgpack_packer *packer, char *string);
31
- void pack_timestamp(msgpack_packer *packer);
32
- void pack_event_header(msgpack_packer *packer, const char *event_type, int map_size);
33
- void pack_pointer(msgpack_packer *packer, VALUE object_id);
34
26
 
35
27
  #endif
@@ -1,57 +1,64 @@
1
1
  require "rbkit_tracer"
2
2
  require "rbkit/timer"
3
+ require "rbkit/rbkit_gc"
3
4
  require "objspace"
4
5
 
5
6
  # Class implements user friendly interface in pure Ruby for profiler.
6
7
  module Rbkit
8
+ DEFAULT_PUB_PORT = 5555
9
+ DEFAULT_REQ_PORT = 5556
10
+
7
11
  class Profiler
8
12
  attr_accessor :pub_port, :request_port
9
13
 
10
14
  def initialize(pub_port, request_port)
15
+ [pub_port, request_port].each{|port| validate_port_range(port) }
11
16
  @pub_port = pub_port
12
17
  @request_port = request_port
13
18
  @profiler_thread = nil
14
19
  @stop_thread = false
15
20
  @server_running = false
16
21
  @gc_stats_timer = Rbkit::Timer.new(5) do
17
- data = GC.stat
18
- no_of_allocated_pages = data[:heap_length]
19
- max_objects_per_page = GC::INTERNAL_CONSTANTS[:HEAP_OBJ_LIMIT]
20
- size_of_one_obj = GC::INTERNAL_CONSTANTS[:RVALUE_SIZE]
21
- data[:total_heap_size] = no_of_allocated_pages * max_objects_per_page *
22
- size_of_one_obj
23
- data[:total_memsize] = ObjectSpace.memsize_of_all
24
- Rbkit.send_hash_as_event(data, "gc_stats")
22
+ data = RbkitGC.stat
23
+ Rbkit.send_hash_as_event(data, Rbkit::EVENT_TYPES[:gc_stats])
25
24
  end
26
25
  @message_dispatch_timer = Rbkit::Timer.new(1) do
27
26
  Rbkit.send_messages
28
27
  end
29
28
  end
30
29
 
31
- def start_server(enable_profiling: false)
32
- return if @server_running
30
+ def start_server(enable_object_trace: false, enable_gc_stats: false)
31
+ if @server_running || !Rbkit.start_stat_server(pub_port, request_port)
32
+ $stderr.puts "Rbkit server couldn't bind to socket, check if it is already" \
33
+ " running. Profiling data will not be available."
34
+ return false
35
+ end
36
+ Rbkit.start_stat_tracing if enable_object_trace
37
+ @enable_gc_stats = enable_gc_stats
38
+ @server_running = true
33
39
  @profiler_thread = Thread.new do
34
- Rbkit.start_stat_server(pub_port, request_port)
35
- Rbkit.start_stat_tracing if enable_profiling
36
40
  loop do
37
41
  break if @stop_thread
38
42
  incoming_request = Rbkit.poll_for_request
39
43
  process_incoming_request(incoming_request)
40
- @gc_stats_timer.run
44
+ @gc_stats_timer.run if @enable_gc_stats
41
45
  @message_dispatch_timer.run
42
46
  # Let us sleep this thread for a bit, so as other things can run.
43
47
  sleep(0.05)
44
48
  end
45
49
  end
46
- @server_running = true
50
+ at_exit { make_clean_exit(exiting: true) }
51
+ true
47
52
  end
48
53
 
49
54
  def process_incoming_request(incoming_request)
50
55
  case incoming_request
51
56
  when "start_memory_profile"
52
57
  Rbkit.start_stat_tracing
58
+ @enable_gc_stats = true
53
59
  when "stop_memory_profile"
54
60
  Rbkit.stop_stat_tracing
61
+ @enable_gc_stats = false
55
62
  when "trigger_gc"
56
63
  GC.start
57
64
  when "objectspace_snapshot"
@@ -60,42 +67,57 @@ module Rbkit
60
67
  end
61
68
 
62
69
  def stop_server
63
- return if !@server_running
64
70
  Rbkit.stop_stat_server
65
- @server_running = false
66
71
  end
67
72
 
68
- def make_clean_exit
73
+ def make_clean_exit(exiting: false)
74
+ return false if !@server_running
69
75
  @stop_thread = true
70
76
  stop_server
77
+ @server_running = false
78
+ true
71
79
  end
72
- end
73
80
 
74
- ########### Rbkit API ###########
81
+ private
75
82
 
76
- # Starts the server and enables memory profiling tracepoints
77
- def self.start_profiling(pub_port = nil, request_port = nil)
78
- @profiler = Rbkit::Profiler.new(pub_port, request_port)
79
- @profiler.start_server(enable_profiling: true)
80
- at_exit do
81
- self.stop_server
83
+ def validate_port_range(port)
84
+ raise ArgumentError, 'Invalid port value' unless (1024..65000).include?(port)
82
85
  end
83
86
  end
84
87
 
85
- # Just starts the server and waits for instructions.
86
- # The client needs to connect to the request_port and send
87
- # commands over the wire. The client also needs to connect
88
- # to the pub_port and subscribe to the responses from the server.
89
- def self.start_server(pub_port = nil, request_port = nil)
90
- @profiler = Rbkit::Profiler.new(pub_port, request_port)
88
+ ########### Rbkit API ###########
89
+
90
+ # Starts the Rbkit server and waits for a client to connect and issue
91
+ # commands to the request_port, until then there's zero performance overhead.
92
+ # Profiling data is sent asynchronously over pub_port.
93
+ # This method can be called early in a ruby application so that
94
+ # whenever profiling needs to be done, the client can attach itself to the
95
+ # inactive server, do the profiling and leave.
96
+ def self.start_server(pub_port: DEFAULT_PUB_PORT, request_port: DEFAULT_REQ_PORT)
97
+ @profiler ||= Rbkit::Profiler.new(pub_port, request_port)
91
98
  @profiler.start_server
92
- at_exit do
93
- self.stop_server
94
- end
99
+ end
100
+
101
+ # Starts the server with all tracepoints enabled by default. User can
102
+ # optionally disable tracepoints using the optional arguments.
103
+ # This method can be used to profile the startup process of a ruby
104
+ # application where sending commands from the client to enable
105
+ # profiling is not feasible.
106
+ def self.start_profiling(pub_port: DEFAULT_PUB_PORT, request_port: DEFAULT_REQ_PORT,
107
+ enable_object_trace: true, enable_gc_stats: true)
108
+ @profiler ||= Rbkit::Profiler.new(pub_port, request_port)
109
+ @profiler.start_server(enable_object_trace: enable_object_trace,
110
+ enable_gc_stats: enable_gc_stats)
95
111
  end
96
112
 
97
113
  # Stops profiling and brings down the rbkit server if it's running
98
114
  def self.stop_server
99
- @profiler.make_clean_exit
115
+ if !@profiler.nil? && @profiler.make_clean_exit
116
+ @profiler = nil
117
+ true
118
+ else
119
+ $stderr.puts "Cannot stop Rbkit server. Is it running?"
120
+ false
121
+ end
100
122
  end
101
123
  end
@@ -0,0 +1,79 @@
1
+ require "objspace"
2
+
3
+ module Rbkit
4
+ class RbkitGC
5
+ # Returns a standardized hash containing the data
6
+ # returned by GC.stat
7
+ # @return [Hash] Keys :
8
+ # [count] Count of major and minor GCs so far
9
+ # [minor_gc_count] Count of minor GCs
10
+ # [major_gc_count] Count of major GCs
11
+ # [heap_allocated_pages] Count of allocated pages (heap_eden_pages + heap_tomb_pages)
12
+ # [heap_eden_pages] Count of pages which has atleast one live object
13
+ # [heap_tomb_pages] Count of pages which don't have any object yet
14
+ # [heap_allocatable_pages] Count of pages that will be allocated if Ruby runs out of heap
15
+ # [heap_sorted_length] Count of total number of sorted pages (>= heap_allocated_pages + heap_allocatable_pages)
16
+ # [heap_live_slots] Count of slots in all pages having live objects
17
+ # [heap_free_slots] Count of free slots in all pages
18
+ # [heap_final_slots] Count of zombie objects
19
+ # [heap_swept_slots] Count of slots swept after last GC
20
+ # [old_objects] Count of old generation objects
21
+ # [old_objects_limit] Old generation object count after which GC is triggered
22
+ # [total_allocated_objects] Number of created objects in the lifetime of the process
23
+ # [total_freed_objects] Number of freed objects in the lifetime of the process
24
+ # [malloc_increase_bytes] Malloc'ed bytes since last GC
25
+ # [malloc_increase_bytes_limit] Minor GC is triggered when malloc_increase_bytes exceeds this value
26
+ # [oldmalloc_increase_bytes] Malloc'ed bytes for old objects since last major GC
27
+ # [oldmalloc_increase_bytes_limit] Major GC is triggered with oldmalloc_increase_bytes exceeds this value
28
+ # [total_heap_size] heap_allocated_pages * max slots per page * size of one slot
29
+ # [total_memsize] ObjectSpace.memsize_of_all
30
+ def self.stat
31
+ stats = {}
32
+ data = GC.stat
33
+
34
+ stats[:count] = data[:count]
35
+ stats[:minor_gc_count] = data[:minor_gc_count]
36
+ stats[:major_gc_count] = data[:major_gc_count]
37
+
38
+ if RUBY_VERSION >= "2.2.0"
39
+ [
40
+ :heap_allocated_pages, :heap_eden_pages, :heap_tomb_pages,
41
+ :heap_allocatable_pages, :heap_sorted_length, :heap_live_slots,
42
+ :heap_free_slots, :heap_final_slots, :heap_swept_slots,
43
+ :old_objects, :old_objects_limit, :total_allocated_objects,
44
+ :total_freed_objects, :malloc_increase_bytes,
45
+ :malloc_increase_bytes_limit, :oldmalloc_increase_bytes,
46
+ :oldmalloc_increase_bytes_limit
47
+ ].each do |key|
48
+ stats[key] = data[key]
49
+ end
50
+ elsif RUBY_VERSION >= "2.1.0"
51
+ stats[:heap_allocated_pages] = data[:heap_used]
52
+ stats[:heap_eden_pages] = data[:heap_eden_page_length]
53
+ stats[:heap_tomb_pages] = data[:heap_tomb_page_length]
54
+ stats[:heap_allocatable_pages] = data[:heap_increment]
55
+ stats[:heap_sorted_length] = data[:heap_length]
56
+ stats[:heap_live_slots] = data[:heap_live_slot]
57
+ stats[:heap_free_slots] = data[:heap_free_slot]
58
+ stats[:heap_final_slots] = data[:heap_final_slot]
59
+ stats[:heap_swept_slots] = data[:heap_swept_slot]
60
+ stats[:old_objects] = data[:old_object]
61
+ stats[:old_objects_limit] = data[:old_object_limit]
62
+ stats[:total_allocated_objects] = data[:total_allocated_object]
63
+ stats[:total_freed_objects] = data[:total_freed_object]
64
+ stats[:malloc_increase_bytes] = data[:malloc_increase]
65
+ stats[:malloc_increase_bytes_limit] = data[:malloc_limit]
66
+ stats[:oldmalloc_increase_bytes] = data[:oldmalloc_increase]
67
+ stats[:oldmalloc_increase_bytes_limit] = data[:oldmalloc_limit]
68
+ end
69
+
70
+ no_of_allocated_pages = stats[:heap_allocated_pages] rescue 0
71
+ max_objects_per_page = GC::INTERNAL_CONSTANTS[:HEAP_OBJ_LIMIT]
72
+ size_of_one_obj = GC::INTERNAL_CONSTANTS[:RVALUE_SIZE]
73
+ stats[:total_heap_size] = no_of_allocated_pages * max_objects_per_page *
74
+ size_of_one_obj
75
+ stats[:total_memsize] = ObjectSpace.memsize_of_all
76
+ stats
77
+ end
78
+ end
79
+ end
@@ -1,3 +1,3 @@
1
1
  module Rbkit
2
- VERSION = "0.0.1"
2
+ VERSION = "0.1.6"
3
3
  end
Binary file
@@ -14,6 +14,7 @@ Gem::Specification.new do |s|
14
14
  if s.respond_to? :required_rubygems_version=
15
15
  s.required_rubygems_version = Gem::Requirement.new(">= 0")
16
16
  end
17
+ s.required_ruby_version = '>= 2.1.0'
17
18
  s.authors = ["Hemant Kumar", "Emil Soman", "Kashyap"]
18
19
  s.description = %q{Something small for process management}
19
20
  s.email = %q{hemant@codemancers.com emil@codemancers.com kashyap@codemancers.com}
@@ -0,0 +1,37 @@
1
+ require 'rbconfig'
2
+ require 'fileutils'
3
+
4
+ class Setup
5
+ attr_accessor :site_dir, :site_lib_dir, :site_arch_dir
6
+
7
+ def initialize
8
+ @site_dir = RbConfig::CONFIG["sitedir"]
9
+ @site_lib_dir = RbConfig::CONFIG["sitelibdir"]
10
+ @site_arch_dir = RbConfig::CONFIG["sitearchdir"]
11
+ end
12
+
13
+ def compile
14
+ Dir.chdir 'ext' do
15
+ if File.exist?("Makefile")
16
+ system("make clean")
17
+ end
18
+
19
+ system("#{Gem.ruby} extconf.rb")
20
+ system("make")
21
+ end
22
+ end
23
+
24
+ def copy_files
25
+ FileUtils.cp_r "lib/.", site_lib_dir, verbose: true
26
+ ext_path =
27
+ File.absolute_path "ext/rbkit_tracer.#{RbConfig::MAKEFILE_CONFIG['DLEXT']}"
28
+ FileUtils.cp_r ext_path, site_arch_dir, verbose: true
29
+ end
30
+ end
31
+
32
+ if __FILE__ == $0
33
+ Setup.new.tap do |setup|
34
+ setup.compile
35
+ setup.copy_files
36
+ end
37
+ end
@@ -0,0 +1,31 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'gc_stat' do
4
+ let(:stat) { Rbkit::RbkitGC.stat }
5
+ it 'should have the correct gc stat keys' do
6
+ expect(stat.keys).to eql [
7
+ :count,
8
+ :minor_gc_count,
9
+ :major_gc_count,
10
+ :heap_allocated_pages,
11
+ :heap_eden_pages,
12
+ :heap_tomb_pages,
13
+ :heap_allocatable_pages,
14
+ :heap_sorted_length,
15
+ :heap_live_slots,
16
+ :heap_free_slots,
17
+ :heap_final_slots,
18
+ :heap_swept_slots,
19
+ :old_objects,
20
+ :old_objects_limit,
21
+ :total_allocated_objects,
22
+ :total_freed_objects,
23
+ :malloc_increase_bytes,
24
+ :malloc_increase_bytes_limit,
25
+ :oldmalloc_increase_bytes,
26
+ :oldmalloc_increase_bytes_limit,
27
+ :total_heap_size,
28
+ :total_memsize ]
29
+ end
30
+ end
31
+
@@ -0,0 +1,29 @@
1
+ require 'spec_helper'
2
+ require 'msgpack'
3
+
4
+ describe 'send_hash_as_event' do
5
+ let(:hash) { {'foo' => 'bar', 123 => "hello world"} }
6
+ let(:payload) { Rbkit::MESSAGE_FIELDS[:payload] }
7
+ let(:event_type) { Rbkit::MESSAGE_FIELDS[:event_type] }
8
+ describe 'when event_type is known' do
9
+ before do
10
+ Rbkit.start_profiling(enable_gc_stats: false, enable_object_trace: false)
11
+ Rbkit.send_hash_as_event(hash, Rbkit::EVENT_TYPES[:gc_stats])
12
+ packed_message = Rbkit.get_queued_messages
13
+ @message = MessagePack.unpack packed_message
14
+ Rbkit.stop_server
15
+ end
16
+ it 'should create a custom event with the serialized hash' do
17
+ expect(@message[payload].first[event_type]).to eql Rbkit::EVENT_TYPES[:gc_stats]
18
+ expect(@message[payload].first[payload]).to eql hash
19
+ end
20
+ end
21
+
22
+ describe 'when event_type is not known' do
23
+ it 'should raise NotImplementedError with a meaningful message' do
24
+ expect { Rbkit.send_hash_as_event(hash, 100) }
25
+ .to raise_error(NotImplementedError,
26
+ "Rbkit : Unpacking of event type '100' not implemented")
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,52 @@
1
+ require 'spec_helper'
2
+ require 'support/have_message_matcher'
3
+ require 'msgpack'
4
+
5
+ describe "obj_created event" do
6
+ let(:payload) { Rbkit::MESSAGE_FIELDS[:payload] }
7
+ let(:class_name) { Rbkit::MESSAGE_FIELDS[:class_name] }
8
+ let(:event_type) { Rbkit::MESSAGE_FIELDS[:event_type] }
9
+ let(:object_id) { Rbkit::MESSAGE_FIELDS[:object_id] }
10
+ let(:foo_info) do
11
+ @message_list[payload]
12
+ .select{|x| x[event_type] == Rbkit::EVENT_TYPES[:obj_created] &&
13
+ x[payload][class_name] =='Foo' }
14
+ end
15
+ let(:bar_info) do
16
+ @message_list[payload]
17
+ .select{|x| x[event_type] == Rbkit::EVENT_TYPES[:obj_created] &&
18
+ x[payload][class_name] =='Bar'}
19
+ end
20
+ let(:short_lived_bar_info) do
21
+ @message_list[payload]
22
+ .select{|x| x[event_type] == Rbkit::EVENT_TYPES[:obj_created] &&
23
+ x[payload][class_name] =='ShortLivedBar' }
24
+ end
25
+ before(:all) do
26
+ Rbkit.start_profiling(enable_gc_stats: false, enable_object_trace: true)
27
+ @foo_obj = Foo.new
28
+ packed_message = Rbkit.get_queued_messages
29
+ Rbkit.stop_server
30
+ @message_list = MessagePack.unpack packed_message
31
+ end
32
+ it "should be part of message list" do
33
+ expect(@message_list).to have_message(Rbkit::EVENT_TYPES[:obj_created])
34
+ end
35
+
36
+ it 'should record objects only once' do
37
+ expect(foo_info.size).to eql 1
38
+ expect(bar_info.size).to eql 1
39
+ expect(short_lived_bar_info.size).to eql 1
40
+ end
41
+
42
+ it 'should record correct object_id' do
43
+ expect(foo_info.first[payload][object_id]).to eql @foo_obj.object_id
44
+ expect(bar_info.first[payload][object_id]).to eql @foo_obj.bar.object_id
45
+ end
46
+
47
+ it 'should record correct class_name' do
48
+ expect(foo_info.first[payload][class_name]).to eql @foo_obj.class.to_s
49
+ expect(bar_info.first[payload][class_name]).to eql @foo_obj.bar.class.to_s
50
+ end
51
+ end
52
+
@@ -0,0 +1,44 @@
1
+ require 'spec_helper'
2
+ require 'support/have_message_matcher'
3
+ require 'msgpack'
4
+
5
+ describe "obj_destroyed event" do
6
+ let(:payload) { Rbkit::MESSAGE_FIELDS[:payload] }
7
+ let(:event_type) { Rbkit::MESSAGE_FIELDS[:event_type] }
8
+ let(:object_id) { Rbkit::MESSAGE_FIELDS[:object_id] }
9
+ let(:class_name) { Rbkit::MESSAGE_FIELDS[:class_name] }
10
+ let(:foo_info) do
11
+ @message_list[payload]
12
+ .select{|x| x[event_type] == Rbkit::EVENT_TYPES[:obj_destroyed] &&
13
+ x[payload][object_id] == @foo_obj.object_id }
14
+ end
15
+ let(:short_lived_bar_info) do
16
+ short_lived_bar_object_id = @message_list[payload]
17
+ .find{|x| x[event_type] == Rbkit::EVENT_TYPES[:obj_created] &&
18
+ x[payload][class_name] == 'ShortLivedBar'}[payload][object_id]
19
+ @message_list[payload]
20
+ .select{|x| x[event_type] == Rbkit::EVENT_TYPES[:obj_destroyed] &&
21
+ x[payload][object_id] == short_lived_bar_object_id}
22
+ end
23
+ before(:all) do
24
+ Rbkit.start_profiling(enable_gc_stats: false, enable_object_trace: true)
25
+ @foo_obj = Foo.new
26
+ GC.start
27
+ packed_message = Rbkit.get_queued_messages
28
+ Rbkit.stop_server
29
+ @message_list = MessagePack.unpack packed_message
30
+ end
31
+ it "should be part of message list" do
32
+ expect(@message_list).to have_message(Rbkit::EVENT_TYPES[:obj_destroyed])
33
+ end
34
+
35
+ it 'should record the deleted object' do
36
+ expect(short_lived_bar_info.size).to eql 1
37
+ end
38
+
39
+ it 'should not record the live object' do
40
+ expect(foo_info.size).to eql 0
41
+ end
42
+ end
43
+
44
+