heap_periscope_agent 0.1.0 → 0.3.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 26c1519c5f34b72f63b341f5b3bd5bcdefaab803da97b88fd59e7971fcf9de7a
4
- data.tar.gz: d5f3f63e496b04726790d16095298133ce410f991b2eb1dd8675245f7083aad2
3
+ metadata.gz: 6adb67f8381e1ed6c7fb6dbcd0fd8e7465027e7c1df3da699c7ffd88a8b89933
4
+ data.tar.gz: defa2d073ad2420c5f21756ccd0e092e820271ab9b74594138841310cee50477
5
5
  SHA512:
6
- metadata.gz: ce334cdc9dbe12bb33b536b0a0331c9d2574637dee691997767adeff498f4347ef7495b41ed08c7c51057409d032b800a84606b7d0e0535a836b6ca51030db0b
7
- data.tar.gz: a09edf18ed45aae75e8978440e6ec36e486280b9ad376ef802901cab933d67ddf83f188a8b429ffaaf3793a3fe4f7ea412e3aebb7f59e84ab79a60571e9393a0
6
+ metadata.gz: d19b5b9d2b8699a18d3615659cb91dc133c41cecf1ff423f571085a66d928f4e809d872021b49b1c30bcca98eed65213a495265c9272c548abc27b0ef230cec1
7
+ data.tar.gz: e9b9fe743a621a3233cbda8011a02094b89fa92c4b4b52ef719059384c7324c0cb1305070457f434cdee552362a86ec78554188c985061e2895359f3206c3eef
@@ -0,0 +1,15 @@
1
+ require 'rails/generators/base'
2
+
3
+ module HeapPeriscopeAgent
4
+ module Generators
5
+ class ConfigGenerator < Rails::Generators::Base
6
+ source_root File.expand_path("templates", __dir__)
7
+
8
+ def copy_initializer_file
9
+ copy_file "heap_periscope_agent.rb.tt", "config/initializers/heap_periscope_agent.rb"
10
+ puts "\nHeapPeriscopeAgent initializer created at config/initializers/heap_periscope_agent.rb"
11
+ puts "Please review and customize the configuration as needed for your environment."
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,16 @@
1
+ require 'rails/generators/base'
2
+
3
+ module HeapPeriscopeAgent
4
+ module Generators
5
+ class ControllerInstrumentationGenerator < Rails::Generators::Base
6
+ source_root File.expand_path("templates", __dir__)
7
+
8
+ def copy_controller_initializer
9
+ template "heap_periscope_agent_controller.rb.tt", "config/initializers/heap_periscope_agent_controller.rb"
10
+ puts "\nHeapPeriscopeAgent controller instrumentation created at config/initializers/heap_periscope_agent_controller.rb"
11
+ puts "This will track living objects after each controller action."
12
+ puts "Remember to enable `config.enable_controller_instrumentation = true` in your main HeapPeriscopeAgent initializer."
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,30 @@
1
+ require 'rails/generators/base'
2
+ require 'active_support/core_ext/string/inflections'
3
+
4
+ module HeapPeriscopeAgent
5
+ module Generators
6
+ class JobTrackerGenerator < Rails::Generators::Base
7
+ argument :job_class_name, type: :string, desc: "The name of the Sidekiq job class to track (e.g. MyJob or MyModule::MyJob)"
8
+
9
+ def add_tracker_to_job
10
+ job_file_path = find_job_file
11
+ if job_file_path && File.exist?(job_file_path)
12
+ # Ensure the tracker module is required.
13
+ insert_into_file job_file_path, "require 'heap_periscope_agent/sidekiq_job_tracker'\n\n", before: /class|module/
14
+
15
+ # Prepend the tracker module to the class.
16
+ inject_into_class job_file_path, job_class_name, " prepend HeapPeriscopeAgent::SidekiqJobTracker\n"
17
+ say "Added HeapPeriscopeAgent::SidekiqJobTracker to #{job_class_name} in #{job_file_path}", :green
18
+ else
19
+ say "Could not find job file for '#{job_class_name}'. Expected at '#{job_file_path}'.", :red
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def find_job_file
26
+ File.join("app", "jobs", "#{job_class_name.underscore}.rb")
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,16 @@
1
+ require 'rails/generators/base'
2
+
3
+ module HeapPeriscopeAgent
4
+ module Generators
5
+ class ModelInstrumentationGenerator < Rails::Generators::Base
6
+ source_root File.expand_path("templates", __dir__)
7
+
8
+ def copy_model_initializer
9
+ template "heap_periscope_agent_model.rb.tt", "config/initializers/heap_periscope_agent_model.rb"
10
+ puts "\nHeapPeriscopeAgent model instrumentation created at config/initializers/heap_periscope_agent_model.rb"
11
+ puts "This will track living objects after each model create, update, and destroy."
12
+ puts "Remember to enable `config.enable_model_instrumentation = true` in your main HeapPeriscopeAgent initializer."
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,15 @@
1
+ require 'rails/generators/base'
2
+
3
+ module HeapPeriscopeAgent
4
+ module Generators
5
+ class RakeInstrumentationGenerator < Rails::Generators::Base
6
+ source_root File.expand_path("templates", __dir__)
7
+
8
+ def copy_rake_initializer
9
+ template "heap_periscope_agent_rake.rb.tt", "config/initializers/heap_periscope_agent_rake.rb"
10
+ puts "\nHeapPeriscopeAgent Rake instrumentation created at config/initializers/heap_periscope_agent_rake.rb"
11
+ puts "This will track all Rake tasks invoked from the command line."
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,43 @@
1
+ require 'rails/generators/base'
2
+
3
+ module HeapPeriscopeAgent
4
+ module Generators
5
+ class RakeTaskTrackerGenerator < Rails::Generators::Base
6
+ argument :task_name, type: :string, desc: "The name of the Rake task to track (e.g. my_namespace:my_task)"
7
+
8
+ def add_rake_tracker
9
+ rakefile_path = "lib/tasks/zzz_heap_periscope_agent_trackers.rake"
10
+
11
+ # Create the file with a header if it doesn't exist
12
+ unless File.exist?(rakefile_path)
13
+ create_file rakefile_path, "# This file is for auto-generated Rake task trackers for HeapPeriscopeAgent.\n# It is named with a 'zzz_' prefix to ensure it loads after other tasks are defined.\n\n"
14
+ end
15
+
16
+ tracker_code = <<~RUBY
17
+ # --- Tracker for #{task_name} task ---
18
+ if Rake::Task.task_defined?('#{task_name}')
19
+ task_to_track = Rake::Task['#{task_name}']
20
+ original_actions = task_to_track.actions.dup
21
+ task_to_track.clear_actions
22
+
23
+ task_to_track.enhance do |t, args|
24
+ begin
25
+ require 'heap_periscope_agent' # Ensure agent is loaded and configured
26
+ HeapPeriscopeAgent.log("Starting agent for Rake task: #{t.name}")
27
+ HeapPeriscopeAgent.start
28
+ original_actions.each { |action| action.call(t, args) } # Execute original task
29
+ ensure
30
+ HeapPeriscopeAgent.log("Stopping agent for Rake task: #{t.name}")
31
+ HeapPeriscopeAgent.stop
32
+ end
33
+ end
34
+ end
35
+ RUBY
36
+
37
+ append_to_file(rakefile_path, "\n#{tracker_code}\n")
38
+ say "Added tracker for Rake task '#{task_name}' in #{rakefile_path}", :green
39
+ say "NOTE: This method relies on this file being loaded *after* the task is defined. The 'zzz_' prefix helps, but is not a guarantee.", :yellow
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,15 @@
1
+ require 'rails/generators/base'
2
+
3
+ module HeapPeriscopeAgent
4
+ module Generators
5
+ class SidekiqMiddlewareGenerator < Rails::Generators::Base
6
+ source_root File.expand_path("templates", __dir__)
7
+
8
+ def copy_sidekiq_initializer
9
+ template "heap_periscope_agent_sidekiq.rb.tt", "config/initializers/heap_periscope_agent_sidekiq.rb"
10
+ puts "\nHeapPeriscopeAgent Sidekiq middleware created at config/initializers/heap_periscope_agent_sidekiq.rb"
11
+ puts "This will track all Sidekiq jobs. Please restart your Sidekiq server for changes to take effect."
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,36 @@
1
+ HeapPeriscopeAgent.configure do |config|
2
+ # Interval in seconds for collecting and reporting metrics.
3
+ # This determines how frequently a full snapshot of heap and GC stats is sent.
4
+ # Default: 10
5
+ config.interval = 10
6
+
7
+ # Host of the server where metrics will be sent via UDP.
8
+ # Ensure this is reachable from your application environment.
9
+ # Default: '127.0.0.1'
10
+ config.host = '127.0.0.1'
11
+
12
+ # Port of the metrics server.
13
+ # This must match the port your UDP server is listening on.
14
+ # Default: 9000
15
+ config.port = 9000
16
+
17
+ # Enable verbose logging from the agent to STDOUT/STDERR.
18
+ # Useful for debugging the agent's behavior.
19
+ # Default: true
20
+ config.verbose = true
21
+
22
+ # Enable collection of detailed object allocation information (counts by class).
23
+ # This can have a higher performance overhead, especially during ObjectSpace.each_object calls.
24
+ # Use judiciously in production.
25
+ # Default: false
26
+ config.enable_detailed_objects = false
27
+
28
+ # If detailed_objects is enabled, this limits the number of
29
+ # distinct object types (by class name) to report on, sorted by count.
30
+ # Default: 20
31
+ config.detailed_objects_limit = 20
32
+
33
+ config.enable_model_instrumentation = true
34
+ config.enable_controller_instrumentation = true
35
+ config.enable_rake_instrumentation = true
36
+ end
@@ -0,0 +1,40 @@
1
+ # config/initializers/heap_periscope_agent_controller.rb
2
+ # This file is automatically generated by the heap_periscope_agent gem.
3
+ # It instruments Rails controller actions to report living objects.
4
+
5
+ if defined?(Rails) && Rails.version.to_i >= 3
6
+ ActiveSupport.on_load(:action_controller_base) do
7
+ # Include this module to enable instrumentation for all controller actions.
8
+ # Ensure `config.enable_controller_instrumentation = true` is set in your
9
+ # main heap_periscope_agent initializer.
10
+ module HeapPeriscopeAgentControllerInstrumentation
11
+ extend ActiveSupport::Concern
12
+
13
+ included do
14
+ around_action :heap_periscope_agent_track_action
15
+ end
16
+
17
+ private
18
+
19
+ def heap_periscope_agent_track_action
20
+ if HeapPeriscopeAgent.configuration.enable_controller_instrumentation
21
+ yield # Execute the controller action
22
+
23
+ # After the action, collect detailed living objects
24
+ detailed_objects = HeapPeriscopeAgent::Collector.collect_detailed_living_objects_for_span(
25
+ HeapPeriscopeAgent.configuration.detailed_objects_limit
26
+ )
27
+
28
+ span_name = "#{self.class.name}##{action_name}"
29
+ HeapPeriscopeAgent::Reporter.add_span_report('controller', span_name, detailed_objects)
30
+ else
31
+ yield # Execute the controller action without tracking
32
+ end
33
+ end
34
+ end
35
+
36
+ # Include the instrumentation module into ActionController::Base
37
+ # This makes it apply to all controllers inheriting from ApplicationController
38
+ ActionController::Base.send :include, HeapPeriscopeAgentControllerInstrumentation
39
+ end
40
+ end
@@ -0,0 +1,56 @@
1
+ # config/initializers/heap_periscope_agent_model.rb
2
+ # This file is automatically generated by the heap_periscope_agent gem.
3
+ # It instruments Rails model actions to report living objects.
4
+
5
+ if defined?(Rails) && defined?(ActiveRecord) && Rails.version.to_i >= 3
6
+ ActiveSupport.on_load(:active_record) do
7
+ # Include this module to enable instrumentation for all model actions.
8
+ # Ensure `config.enable_model_instrumentation = true` is set in your
9
+ # main heap_periscope_agent initializer.
10
+ module HeapPeriscopeAgentModelInstrumentation
11
+ extend ActiveSupport::Concern
12
+
13
+ included do
14
+ around_save :heap_periscope_agent_track_model_save
15
+ around_destroy :heap_periscope_agent_track_model_destroy
16
+ end
17
+
18
+ private
19
+
20
+ def heap_periscope_agent_track_model_save
21
+ # This check is duplicated from track_model_destroy but is cheap.
22
+ # It avoids capturing the `is_new_record` flag unnecessarily.
23
+ return yield unless HeapPeriscopeAgent.configuration.enable_model_instrumentation
24
+
25
+ is_new_record = new_record?
26
+ yield # Execute the save operation
27
+
28
+ # After the action, collect detailed living objects
29
+ detailed_objects = HeapPeriscopeAgent::Collector.collect_detailed_living_objects_for_span(
30
+ HeapPeriscopeAgent.configuration.detailed_objects_limit
31
+ )
32
+
33
+ action_name = is_new_record ? 'create' : 'update'
34
+ span_name = "#{self.class.name}##{action_name}"
35
+ HeapPeriscopeAgent::Reporter.add_span_report('model', span_name, detailed_objects)
36
+ end
37
+
38
+ def heap_periscope_agent_track_model_destroy
39
+ return yield unless HeapPeriscopeAgent.configuration.enable_model_instrumentation
40
+
41
+ yield # Execute the destroy operation
42
+
43
+ detailed_objects = HeapPeriscopeAgent::Collector.collect_detailed_living_objects_for_span(
44
+ HeapPeriscopeAgent.configuration.detailed_objects_limit
45
+ )
46
+
47
+ span_name = "#{self.class.name}#destroy"
48
+ HeapPeriscopeAgent::Reporter.add_span_report('model', span_name, detailed_objects)
49
+ end
50
+ end
51
+
52
+ # Include the instrumentation module into ActiveRecord::Base
53
+ # This makes it apply to all models inheriting from it.
54
+ ActiveRecord::Base.send :include, HeapPeriscopeAgentModelInstrumentation
55
+ end
56
+ end
@@ -0,0 +1,28 @@
1
+ # This file was generated by `rails generate heap_periscope_agent:rake_instrumentation`
2
+ # It instruments Rake to track memory usage for the entire duration of a Rake command.
3
+
4
+ # We only want to apply this patch when running Rake tasks, not in a Rails server or console.
5
+ is_rake_execution = (defined?(Rake) && Rake.application.top_level_tasks.any?)
6
+ is_server_or_console = (defined?(Rails::Server) || defined?(Rails::Console))
7
+
8
+ if is_rake_execution && !is_server_or_console
9
+ HeapPeriscopeAgent.log("Setting up Rake instrumentation for HeapPeriscopeAgent.")
10
+
11
+ module HeapPeriscopeAgent
12
+ module RakeApplicationInstrumentation
13
+ def top_level
14
+ begin
15
+ HeapPeriscopeAgent.log("Rake execution started. Starting agent...")
16
+ HeapPeriscopeAgent.start
17
+ super
18
+ ensure
19
+ HeapPeriscopeAgent.log("Rake execution finished. Stopping agent.")
20
+ HeapPeriscopeAgent.stop
21
+ end
22
+ end
23
+ end
24
+ end
25
+
26
+ Rake::Application.prepend(HeapPeriscopeAgent::RakeApplicationInstrumentation)
27
+ HeapPeriscopeAgent.log("Rake::Application is now instrumented by HeapPeriscopeAgent.")
28
+ end
@@ -0,0 +1,24 @@
1
+ # This file was generated by `rails generate heap_periscope_agent:sidekiq_middleware`
2
+ # It sets up Sidekiq server middleware to automatically track memory usage for all jobs.
3
+
4
+ if defined?(Sidekiq) && Sidekiq.server?
5
+ HeapPeriscopeAgent.log("Setting up Sidekiq server middleware for HeapPeriscopeAgent.")
6
+
7
+ class HeapPeriscopeAgent::SidekiqMiddleware
8
+ def call(worker, job, queue)
9
+ # The agent uses a reference counter, so it's safe to call start/stop for every job.
10
+ # It will only run one reporting thread per process.
11
+ HeapPeriscopeAgent.start
12
+ yield
13
+ ensure
14
+ HeapPeriscopeAgent.stop
15
+ end
16
+ end
17
+
18
+ Sidekiq.configure_server do |config|
19
+ config.server_middleware do |chain|
20
+ HeapPeriscopeAgent.log("Adding HeapPeriscopeAgent::SidekiqMiddleware to Sidekiq.")
21
+ chain.add HeapPeriscopeAgent::SidekiqMiddleware
22
+ end
23
+ end
24
+ end
@@ -1,12 +1,38 @@
1
- require 'time'
2
- require 'json'
3
- require 'socket'
4
1
  require 'objspace'
5
- require 'concurrent/atomic/atomic_boolean'
6
- require 'thread'
7
2
 
8
3
  module HeapPeriscopeAgent
9
4
  class Collector
5
+ PLATFORM_CLASS_IDENTIFIERS = [
6
+ # Ruby Core Classes
7
+ "String",
8
+ "Array",
9
+ "Hash",
10
+ "Integer",
11
+ "Float",
12
+ "Symbol",
13
+ "Regexp",
14
+ "Time",
15
+ "Proc",
16
+ "Thread",
17
+ "Range",
18
+ "Set",
19
+ "Enumerator",
20
+ "TrueClass",
21
+ "FalseClass",
22
+ "NilClass",
23
+ # Rails Framework Namespaces
24
+ "ActiveRecord::",
25
+ "ActiveSupport::",
26
+ "ActionView::",
27
+ "ActionController::",
28
+ "ActionDispatch::",
29
+ "ActionMailer::",
30
+ "ActiveJob::",
31
+ "ActiveModel::",
32
+ "Rails::",
33
+ "Sprockets::" # Often included with Rails asset pipeline
34
+ ].freeze
35
+
10
36
  def self.collect_snapshot(detailed_mode = false)
11
37
  data = {
12
38
  gc_stats: GC.stat,
@@ -14,21 +40,95 @@ module HeapPeriscopeAgent
14
40
  }
15
41
 
16
42
  if detailed_mode
17
- data[:living_objects_by_class] = collect_detailed_living_objects(HeadPeriscopeAgent.configuration.detailed_objects_limit)
43
+ data[:living_objects_by_class] = collect_detailed_living_objects(HeapPeriscopeAgent.configuration.detailed_objects_limit)
18
44
  end
19
45
 
20
46
  data
21
47
  end
22
48
 
49
+ def self.is_platform_class?(class_name_str)
50
+ return false if class_name_str.nil? || class_name_str.empty?
51
+ PLATFORM_CLASS_IDENTIFIERS.any? do |identifier|
52
+ if identifier.end_with?("::")
53
+ class_name_str.start_with?(identifier)
54
+ else
55
+ class_name_str == identifier
56
+ end
57
+ end
58
+ end
59
+
60
+ def self.human_readable_bytes(bytes)
61
+ units = ['B', 'KB', 'MB', 'GB', 'TB']
62
+ return '0 B' if bytes == 0
63
+ i = (Math.log(bytes) / Math.log(1024)).floor
64
+ i = [i, units.length - 1].min
65
+ "#{'%.2f' % (bytes.to_f / (1024 ** i))} #{units[i]}"
66
+ end
67
+
68
+ def self.collect_detailed_living_objects_for_span(limit)
69
+ raw_data = _collect_raw_object_data
70
+
71
+ # Convert raw_data to the desired array format for spans
72
+ # [{ name: "String", type: "native", count: 10, size: "100KB" }, ...]
73
+ sorted_data = raw_data.sort_by { |_, data| -data[:count] }.first(limit)
74
+
75
+ sorted_data.map do |class_name, data|
76
+ {
77
+ name: class_name,
78
+ type: is_platform_class?(class_name) ? 'native' : 'application',
79
+ count: data[:count],
80
+ size: human_readable_bytes(data[:total_size])
81
+ }
82
+ end
83
+ end
84
+
23
85
  private
24
86
 
25
- def self.collect_detailed_living_objects(limit)
26
- counts = Hash.new(0)
87
+ def self._collect_raw_object_data
88
+ raw_data = Hash.new do |h, k|
89
+ h[k] = { count: 0, total_size: 0 }
90
+ end
91
+
27
92
  ObjectSpace.each_object do |obj|
28
- class_name = obj.class.name rescue 'Anonymous/Singleton Class'
29
- counts[class_name] += 1
93
+ begin
94
+ # Get class name
95
+ obj_class = obj.class
96
+ class_name = obj_class.name
97
+ class_name = obj_class.inspect if class_name.nil? || class_name.empty?
98
+
99
+ raw_data[class_name][:count] += 1
100
+
101
+ # Attempt to get object size.
102
+ begin
103
+ size = ObjectSpace.memsize_of(obj)
104
+ raw_data[class_name][:total_size] += size
105
+ rescue TypeError, NoMethodError
106
+ # Ignore objects that don't have a measurable size (e.g., immediates, BasicObject)
107
+ rescue => e
108
+ HeapPeriscopeAgent.log("Error getting memsize_of for #{class_name}: #{e.message}", level: :warn)
109
+ end
110
+ rescue NoMethodError # Handles BasicObject where .class is not defined
111
+ class_name = obj.inspect rescue 'Uninspectable BasicObject'
112
+ raw_data[class_name][:count] += 1
113
+ rescue => e
114
+ error_key = 'Error Collecting Object Data'
115
+ raw_data[error_key][:count] += 1
116
+ HeapPeriscopeAgent.log("Error during object iteration: #{e.message}", level: :warn)
117
+ end
30
118
  end
31
- counts.sort_by { |_, count| -count }.first(limit).to_h
119
+ raw_data
120
+ end
121
+
122
+ # Collects detailed living objects for the main snapshot report.
123
+ # This method is kept for the `living_objects_by_class` part of the payload.
124
+ def self.collect_detailed_living_objects(limit)
125
+ raw_data = _collect_raw_object_data
126
+
127
+ formatted_data = raw_data.map do |class_name, data|
128
+ [class_name, { count: data[:count], is_platform_class: is_platform_class?(class_name) }]
129
+ end.to_h
130
+
131
+ formatted_data.sort_by { |_, data| -data[:count] }.first(limit).to_h
32
132
  end
33
133
  end
34
134
  end
@@ -1,6 +1,7 @@
1
1
  module HeapPeriscopeAgent
2
2
  class Configuration
3
- attr_accessor :interval, :host, :port, :verbose, :enable_detailed_objects, :detailed_objects_limit
3
+ attr_accessor :interval, :host, :port, :verbose, :enable_detailed_objects,
4
+ :detailed_objects_limit, :service_name, :enable_controller_instrumentation, :enable_model_instrumentation
4
5
 
5
6
  def initialize
6
7
  @interval = 10 # seconds
@@ -9,6 +10,9 @@ module HeapPeriscopeAgent
9
10
  @verbose = true
10
11
  @enable_detailed_objects = false
11
12
  @detailed_objects_limit = 20
13
+ @service_name = nil
14
+ @enable_controller_instrumentation = false
15
+ @enable_model_instrumentation = false
12
16
  end
13
17
  end
14
18
  end
@@ -0,0 +1,10 @@
1
+ require 'heap_periscope_agent'
2
+
3
+ module HeapPeriscopeAgent
4
+ class Railtie < Rails::Railtie
5
+ initializer "heap_periscope_agent.start_agent", after: :load_config_initializers do |app|
6
+ HeapPeriscopeAgent.log("Rails Initializer: Starting HeapPeriscopeAgent...")
7
+ HeapPeriscopeAgent.start
8
+ end
9
+ end
10
+ end
@@ -2,64 +2,80 @@ require 'time'
2
2
  require 'json'
3
3
  require 'socket'
4
4
  require 'objspace'
5
- require 'concurrent/atomic/atomic_boolean'
5
+ require 'concurrent/atomic/atomic_fixnum'
6
6
  require 'thread'
7
7
 
8
8
  module HeapPeriscopeAgent
9
9
  class Reporter
10
- def self.start
11
- @config = HeapPeriscopeAgent.configuration
12
- @running = Concurrent::AtomicBoolean.new(false)
10
+ @lock = Mutex.new
11
+ @active_count = Concurrent::AtomicFixnum.new(0)
12
+ @thread = nil
13
+ @last_gc_total_time = 0
14
+ @span_reports = {}
13
15
 
14
- return if @running.true?
15
- @running.make_true
16
+ def self.start
17
+ @config ||= HeapPeriscopeAgent.configuration
16
18
 
17
- log("Reporter starting with interval: #{@config.interval}s.")
19
+ if @active_count.increment == 1
20
+ @lock.synchronize do
21
+ return if @thread&.alive?
18
22
 
19
- # Enable the standard GC Profiler
20
- GC::Profiler.enable
21
- # Store the initial total time to calculate deltas later
22
- @last_gc_total_time = GC::Profiler.total_time
23
+ HeapPeriscopeAgent.log("Reporter starting with interval: #{@config.interval}s.")
23
24
 
24
- @thread = Thread.new do
25
- @socket = UDPSocket.new
26
- last_snapshot_time = Time.now
25
+ GC::Profiler.enable
26
+ @last_gc_total_time = GC::Profiler.total_time
27
27
 
28
- while @running.true?
29
- # Periodically send a full snapshot
30
- if Time.now - last_snapshot_time >= @config.interval
31
- send_snapshot_report
28
+ @thread = Thread.new do
29
+ @socket = UDPSocket.new
32
30
  last_snapshot_time = Time.now
33
- end
34
31
 
35
- # Check for new GC activity since the last check
36
- send_gc_profiler_report
32
+ while @active_count.value > 0
33
+ if Time.now - last_snapshot_time >= @config.interval
34
+ send_snapshot_report
35
+ last_snapshot_time = Time.now
36
+ end
37
37
 
38
- sleep(1) # Main loop delay
39
- end
38
+ send_gc_profiler_report
40
39
 
41
- log("Reporter loop finished.")
42
- GC::Profiler.disable # Clean up the profiler
43
- @socket.close
44
- end
40
+ sleep(1)
41
+ end
42
+
43
+ HeapPeriscopeAgent.log("Reporter loop finished.")
44
+ GC::Profiler.disable
45
+ @socket.close
46
+ end
45
47
 
46
- @thread.report_on_exception = true
47
- log("Reporter thread initiated.")
48
+ @thread.report_on_exception = true
49
+ HeapPeriscopeAgent.log("Reporter thread initiated.")
50
+ end
51
+ else
52
+ HeapPeriscopeAgent.log("Reporter already running. Active count: #{@active_count.value}")
53
+ end
48
54
  end
49
55
 
50
56
  def self.stop
51
- return unless @running&.true?
57
+ return if @active_count.value.zero?
58
+
59
+ if @active_count.decrement == 0
60
+ @lock.synchronize do
61
+ return unless @active_count.value.zero?
62
+
63
+ thread_to_join = @thread
64
+ @thread = nil
65
+
66
+ HeapPeriscopeAgent.log("Stopping reporter...")
52
67
 
53
- log("Stopping reporter...")
54
- @running.make_false
55
- if @thread&.join(5)
56
- log("Reporter thread stopped gracefully.")
68
+ if thread_to_join&.join(5)
69
+ HeapPeriscopeAgent.log("Reporter thread stopped gracefully.")
70
+ else
71
+ HeapPeriscopeAgent.log("Reporter thread did not stop in time, killing.", level: :warn)
72
+ thread_to_join&.kill
73
+ end
74
+ HeapPeriscopeAgent.log("Reporter stop process complete.")
75
+ end
57
76
  else
58
- log("Reporter thread did not stop in time, killing.", level: :warn)
59
- @thread&.kill
77
+ HeapPeriscopeAgent.log("Jobs still active. Active count: #{@active_count.value}")
60
78
  end
61
- @thread = nil
62
- log("Reporter stop process complete.")
63
79
  end
64
80
 
65
81
  def self.report_once!
@@ -69,31 +85,51 @@ module HeapPeriscopeAgent
69
85
  @socket.close
70
86
  end
71
87
 
88
+ def self.add_span_report(span_type, span_name, objects_data)
89
+ @span_reports[span_type] ||= []
90
+ @span_reports[span_type] << { name: span_name, live_objects: objects_data }
91
+ end
92
+
72
93
  private
73
94
 
95
+ def self.service_name
96
+ if @config && @config.service_name
97
+ return @config.service_name
98
+ end
99
+
100
+ if defined?(::Sidekiq) && ::Sidekiq.server?
101
+ 'Sidekiq'
102
+ elsif File.basename($0) == 'rake'
103
+ 'Rake'
104
+ elsif defined?(::Rails)
105
+ 'Rails'
106
+ else
107
+ File.basename($0)
108
+ end
109
+ end
110
+
74
111
  def self.send_snapshot_report
75
- log("Collecting periodic snapshot...")
76
- stats = HeapPeriscopeAgent.Collector.collect_snapshot(@config.enable_detailed_objects)
77
- send_payload(stats, "snapshot")
112
+ HeapPeriscopeAgent.log("Collecting periodic snapshot...")
113
+ stats = HeapPeriscopeAgent::Collector.collect_snapshot(@config.enable_detailed_objects)
114
+
115
+ payload_data = stats.merge(living_objects_by_spans: @span_reports)
116
+ send_payload(payload_data, "snapshot")
117
+
118
+ @span_reports = {}
78
119
  end
79
120
 
80
121
  def self.send_gc_profiler_report
81
122
  current_gc_total_time = GC::Profiler.total_time
82
- # Calculate time spent in GC since our last check
83
123
  gc_time_delta = current_gc_total_time - @last_gc_total_time
84
124
 
85
- # If there was GC activity, report it
86
125
  if gc_time_delta > 0
87
- log("Detected GC activity. Sending GC profiler report.")
126
+ HeapPeriscopeAgent.log("Detected GC activity. Sending GC profiler report.")
88
127
  payload = {
89
- # Convert from seconds to milliseconds
90
128
  gc_duration_since_last_check_ms: (gc_time_delta * 1000).round(2),
91
- gc_invocation_count: GC.count, # Total number of GCs so far
92
129
  latest_gc_info: GC.latest_gc_info,
93
130
  }
94
131
  send_payload(payload, "gc_profiler_report")
95
132
 
96
- # Update the last known time
97
133
  @last_gc_total_time = current_gc_total_time
98
134
  end
99
135
  end
@@ -102,21 +138,15 @@ module HeapPeriscopeAgent
102
138
  payload = {
103
139
  type: type,
104
140
  process_id: Process.pid,
141
+ service_name: self.service_name,
105
142
  reported_at: Time.now.utc.iso8601,
106
143
  payload: data
107
144
  }.to_json
108
145
 
109
146
  @socket.send(payload, 0, @config.host, @config.port)
110
- log("Sent #{type} payload.")
147
+ HeapPeriscopeAgent.log("Sent #{type} payload.")
111
148
  rescue => e
112
- log("Failed to send payload: #{e.message}", level: :error)
113
- end
114
-
115
- def self.log(message, level: :info)
116
- return unless @config&.verbose
117
- timestamp = Time.now.strftime('%Y-%m-%d %H:%M:%S')
118
- output = "[HeapPeriscopeAgent][#{level.to_s.upcase}] #{timestamp}: #{message}"
119
- level == :error ? warn(output) : puts(output)
149
+ HeapPeriscopeAgent.log("Failed to send payload: #{e.message}", level: :error)
120
150
  end
121
151
  end
122
152
  end
@@ -0,0 +1,12 @@
1
+ module HeapPeriscopeAgent
2
+ module SidekiqJobTracker
3
+ def perform(*args)
4
+ HeapPeriscopeAgent.log("Starting agent for #{self.class.name}...")
5
+ HeapPeriscopeAgent.start
6
+ super
7
+ ensure
8
+ HeapPeriscopeAgent.log("Stopping agent for #{self.class.name}.")
9
+ HeapPeriscopeAgent.stop
10
+ end
11
+ end
12
+ end
@@ -1,3 +1,3 @@
1
1
  module HeapPeriscopeAgent
2
- VERSION = "0.1.0"
2
+ VERSION = "0.3.0"
3
3
  end
@@ -1,14 +1,54 @@
1
- require_relative './heap_periscope_agent/version.rb'
2
- require_relative './heap_periscope_agent/configuration.rb'
3
- require_relative './heap_periscope_agent/collector.rb'
4
- require_relative './heap_periscope_agent/reporter.rb'
1
+ require_relative "heap_periscope_agent/version"
2
+ require_relative "heap_periscope_agent/configuration"
3
+ require_relative "heap_periscope_agent/collector"
4
+ require_relative "heap_periscope_agent/reporter"
5
+ require_relative "heap_periscope_agent/sidekiq_job_tracker"
5
6
 
6
7
  module HeapPeriscopeAgent
7
- def self.configuration
8
- @configuration ||= HeapPeriscopeAgent::Configuration.new
8
+ class << self
9
+ attr_writer :configuration
10
+
11
+ def configuration
12
+ @configuration ||= Configuration.new
13
+ end
14
+
15
+ def configure
16
+ yield(configuration)
17
+ end
18
+
19
+ def start
20
+ log("HeapPeriscopeAgent starting...")
21
+ Reporter.start
22
+ log("HeapPeriscopeAgent started successfully.")
23
+ rescue => e
24
+ log("Failed to start HeapPeriscopeAgent: #{e.message} #{e.backtrace.join("\n")}", level: :error)
25
+ end
26
+
27
+ def stop
28
+ log("HeapPeriscopeAgent stopping...")
29
+ Reporter.stop
30
+ log("HeapPeriscopeAgent stopped.")
31
+ rescue => e
32
+ log("Failed to stop HeapPeriscopeAgent: #{e.message}", level: :error)
33
+ end
34
+
35
+ def report_once!
36
+ Reporter.report_once!
37
+ end
38
+
39
+ def log(message, level: :info)
40
+ return unless configuration&.verbose
41
+ timestamp = Time.now.strftime('%Y-%m-%d %H:%M:%S')
42
+ output = "[HeapPeriscopeAgent][#{level.to_s.upcase}] #{timestamp}: #{message}"
43
+ if level == :error || level == :warn
44
+ Kernel.warn(output)
45
+ else
46
+ Kernel.puts(output)
47
+ end
48
+ end
9
49
  end
10
50
 
11
- def self.configure
12
- yield(configuration)
51
+ if defined?(Rails::Railtie)
52
+ require_relative "heap_periscope_agent/railtie"
13
53
  end
14
- end
54
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: heap_periscope_agent
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jonathan Natanael Siahaan
@@ -13,16 +13,36 @@ description: |
13
13
  Heap Periscope Agent offers deep insights into your Ruby application's memory behavior.
14
14
  It collects and reports real-time Garbage Collection (GC) statistics and object
15
15
  allocation patterns, empowering developers to identify memory leaks, optimize usage,
16
- and enhance performance. Highly configurable and designed for minimal overhead.
16
+ and enhance performance. This gem is the backend agent for memory monitoring. To
17
+ visualize the collected data, you must also install the companion gem,
18
+ heap_periscope_ui
19
+
20
+ The agent's visualizer is available here:
21
+ - Gem: https://rubygems.org/gems/heap_periscope_ui
22
+ - Repository: https://github.com/codepawpaw/heap_periscope_ui
17
23
  email: js.jonathan.n@gmail.com
18
24
  executables: []
19
25
  extensions: []
20
26
  extra_rdoc_files: []
21
27
  files:
28
+ - lib/generators/heap_periscope_agent/config_generator.rb
29
+ - lib/generators/heap_periscope_agent/controller_instrumentation_generator.rb
30
+ - lib/generators/heap_periscope_agent/job_tracker_generator.rb
31
+ - lib/generators/heap_periscope_agent/model_instrumentation_generator.rb
32
+ - lib/generators/heap_periscope_agent/rake_instrumentation_generator.rb
33
+ - lib/generators/heap_periscope_agent/rake_task_tracker_generator.rb
34
+ - lib/generators/heap_periscope_agent/sidekiq_middleware_generator.rb
35
+ - lib/generators/heap_periscope_agent/templates/heap_periscope_agent.rb.tt
36
+ - lib/generators/heap_periscope_agent/templates/heap_periscope_agent_controller.rb.tt
37
+ - lib/generators/heap_periscope_agent/templates/heap_periscope_agent_model.rb.tt
38
+ - lib/generators/heap_periscope_agent/templates/heap_periscope_agent_rake.rb.tt
39
+ - lib/generators/heap_periscope_agent/templates/heap_periscope_agent_sidekiq.rb.tt
22
40
  - lib/heap_periscope_agent.rb
23
41
  - lib/heap_periscope_agent/collector.rb
24
42
  - lib/heap_periscope_agent/configuration.rb
43
+ - lib/heap_periscope_agent/railtie.rb
25
44
  - lib/heap_periscope_agent/reporter.rb
45
+ - lib/heap_periscope_agent/sidekiq_job_tracker.rb
26
46
  - lib/heap_periscope_agent/version.rb
27
47
  homepage: https://github.com/codepawpaw/heap_periscope_agent
28
48
  licenses: