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 +4 -4
- data/lib/generators/heap_periscope_agent/config_generator.rb +15 -0
- data/lib/generators/heap_periscope_agent/controller_instrumentation_generator.rb +16 -0
- data/lib/generators/heap_periscope_agent/job_tracker_generator.rb +30 -0
- data/lib/generators/heap_periscope_agent/model_instrumentation_generator.rb +16 -0
- data/lib/generators/heap_periscope_agent/rake_instrumentation_generator.rb +15 -0
- data/lib/generators/heap_periscope_agent/rake_task_tracker_generator.rb +43 -0
- data/lib/generators/heap_periscope_agent/sidekiq_middleware_generator.rb +15 -0
- data/lib/generators/heap_periscope_agent/templates/heap_periscope_agent.rb.tt +36 -0
- data/lib/generators/heap_periscope_agent/templates/heap_periscope_agent_controller.rb.tt +40 -0
- data/lib/generators/heap_periscope_agent/templates/heap_periscope_agent_model.rb.tt +56 -0
- data/lib/generators/heap_periscope_agent/templates/heap_periscope_agent_rake.rb.tt +28 -0
- data/lib/generators/heap_periscope_agent/templates/heap_periscope_agent_sidekiq.rb.tt +24 -0
- data/lib/heap_periscope_agent/collector.rb +111 -11
- data/lib/heap_periscope_agent/configuration.rb +5 -1
- data/lib/heap_periscope_agent/railtie.rb +10 -0
- data/lib/heap_periscope_agent/reporter.rb +86 -56
- data/lib/heap_periscope_agent/sidekiq_job_tracker.rb +12 -0
- data/lib/heap_periscope_agent/version.rb +1 -1
- data/lib/heap_periscope_agent.rb +49 -9
- metadata +22 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6adb67f8381e1ed6c7fb6dbcd0fd8e7465027e7c1df3da699c7ffd88a8b89933
|
4
|
+
data.tar.gz: defa2d073ad2420c5f21756ccd0e092e820271ab9b74594138841310cee50477
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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(
|
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.
|
26
|
-
|
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
|
-
|
29
|
-
|
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
|
-
|
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,
|
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/
|
5
|
+
require 'concurrent/atomic/atomic_fixnum'
|
6
6
|
require 'thread'
|
7
7
|
|
8
8
|
module HeapPeriscopeAgent
|
9
9
|
class Reporter
|
10
|
-
|
11
|
-
|
12
|
-
|
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
|
-
|
15
|
-
@
|
16
|
+
def self.start
|
17
|
+
@config ||= HeapPeriscopeAgent.configuration
|
16
18
|
|
17
|
-
|
19
|
+
if @active_count.increment == 1
|
20
|
+
@lock.synchronize do
|
21
|
+
return if @thread&.alive?
|
18
22
|
|
19
|
-
|
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
|
-
|
25
|
-
|
26
|
-
last_snapshot_time = Time.now
|
25
|
+
GC::Profiler.enable
|
26
|
+
@last_gc_total_time = GC::Profiler.total_time
|
27
27
|
|
28
|
-
|
29
|
-
|
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
|
-
|
36
|
-
|
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
|
-
|
39
|
-
end
|
38
|
+
send_gc_profiler_report
|
40
39
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
40
|
+
sleep(1)
|
41
|
+
end
|
42
|
+
|
43
|
+
HeapPeriscopeAgent.log("Reporter loop finished.")
|
44
|
+
GC::Profiler.disable
|
45
|
+
@socket.close
|
46
|
+
end
|
45
47
|
|
46
|
-
|
47
|
-
|
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
|
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
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
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("
|
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
|
77
|
-
|
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
|
data/lib/heap_periscope_agent.rb
CHANGED
@@ -1,14 +1,54 @@
|
|
1
|
-
require_relative
|
2
|
-
require_relative
|
3
|
-
require_relative
|
4
|
-
require_relative
|
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
|
-
|
8
|
-
|
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
|
-
|
12
|
-
|
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.
|
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.
|
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:
|