rbkit 0.0.1 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+