heap_periscope_ui 0.1.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.
Files changed (34) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +28 -0
  4. data/Rakefile +8 -0
  5. data/app/assets/config/heap_periscope_ui_manifest.js +3 -0
  6. data/app/assets/stylesheets/heap_periscope_ui/application.css +15 -0
  7. data/app/assets/stylesheets/heap_periscope_ui/visualizer.css +11 -0
  8. data/app/channels/heap_periscope_ui/application_cable/channel.rb +7 -0
  9. data/app/channels/heap_periscope_ui/application_cable/connection.rb +12 -0
  10. data/app/channels/heap_periscope_ui/runtime_stats_channel.rb +14 -0
  11. data/app/controllers/heap_periscope_ui/application_controller.rb +4 -0
  12. data/app/controllers/heap_periscope_ui/dashboard_controller.rb +11 -0
  13. data/app/helpers/heap_periscope_ui/application_helper.rb +4 -0
  14. data/app/jobs/heap_periscope_ui/application_job.rb +4 -0
  15. data/app/mailers/heap_periscope_ui/application_mailer.rb +6 -0
  16. data/app/models/heap_periscope_ui/application_record.rb +5 -0
  17. data/app/models/heap_periscope_ui/object_count.rb +13 -0
  18. data/app/models/heap_periscope_ui/profiler_report.rb +15 -0
  19. data/app/views/heap_periscope_ui/dashboard/index.html +1132 -0
  20. data/app/views/heap_periscope_ui/dashboard/index.html.bak +862 -0
  21. data/app/views/layouts/heap_periscope_ui/application.html.erb +14 -0
  22. data/config/initializers/heap_periscope_ui_start_udp_listener.rb +15 -0
  23. data/config/routes.rb +8 -0
  24. data/db/migrate/20250617144101_create_heap_periscope_ui_profiler_reports.rb +17 -0
  25. data/db/migrate/20250617144853_create_heap_periscope_ui_object_counts.rb +14 -0
  26. data/lib/generators/heap_periscope_ui/install/install_generator.rb +26 -0
  27. data/lib/generators/heap_periscope_ui/install/templates/README_SETUP +17 -0
  28. data/lib/generators/heap_periscope_ui/install/templates/initializer.rb +4 -0
  29. data/lib/heap_periscope_ui/engine.rb +13 -0
  30. data/lib/heap_periscope_ui/udp_listener.rb +131 -0
  31. data/lib/heap_periscope_ui/version.rb +3 -0
  32. data/lib/heap_periscope_ui.rb +16 -0
  33. data/lib/tasks/heap_periscope_ui_tasks.rake +4 -0
  34. metadata +100 -0
@@ -0,0 +1,14 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Heap Periscope UI</title>
5
+ <%= csrf_meta_tags %>
6
+ <%= csp_meta_tag %>
7
+
8
+ <%# Add stylesheet_link_tag if you have engine-specific CSS %>
9
+ <%#= stylesheet_link_tag "heap_periscope_ui/application" %>
10
+ </head>
11
+ <body>
12
+ <%= yield %>
13
+ </body>
14
+ </html>
@@ -0,0 +1,15 @@
1
+ require_relative "../../lib/heap_periscope_ui/udp_listener"
2
+
3
+ Thread.new do
4
+ puts "[HeapPeriscopeUi] UDP listener thread starting..."
5
+ host = ENV['UDP_HOST'] || '127.0.0.1'
6
+ port = ENV['UDP_PORT'] ? ENV['UDP_PORT'].to_i : 9000
7
+
8
+ begin
9
+ server = HeapPeriscopeUi::UdpListener.new(host: host, port: port)
10
+ server.run
11
+ rescue => e
12
+ puts "[HeapPeriscopeUi] ERROR in UDP listener thread: #{e.message}" # Changed to puts
13
+ puts e.backtrace.join("\n") # Changed to puts
14
+ end
15
+ end
data/config/routes.rb ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ HeapPeriscopeUi::Engine.routes.draw do
4
+ resources :reports, only: [:index, :show]
5
+ get 'dashboard', to: 'dashboard#index', as: :dashboard
6
+
7
+ root to: "dashboard#index"
8
+ end
@@ -0,0 +1,17 @@
1
+ class CreateHeapPeriscopeUiProfilerReports < ActiveRecord::Migration[7.0] # Or your target Rails version
2
+ def change
3
+ create_table :heap_periscope_ui_profiler_reports do |t|
4
+ t.integer :process_id, null: false
5
+ t.string :report_type, null: false
6
+ t.datetime :reported_at, null: false
7
+ t.float :gc_duration_ms
8
+ t.integer :gc_invocation_count
9
+ t.text :raw_snapshot_stats
10
+ t.timestamps null: false # This adds created_at and updated_at, both null: false
11
+
12
+ t.index :process_id
13
+ t.index :report_type
14
+ t.index :reported_at
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,14 @@
1
+ class CreateHeapPeriscopeUiObjectCounts < ActiveRecord::Migration[7.0] # Or your target Rails version
2
+ def change
3
+ create_table :heap_periscope_ui_object_counts do |t|
4
+ t.references :profiler_report, null: false, foreign_key: { to_table: :heap_periscope_ui_profiler_reports }, index: true
5
+ t.string :class_name, null: false
6
+ t.integer :count, null: false
7
+ t.boolean :is_platform_class, null: false
8
+ t.timestamps null: false # This adds created_at and updated_at, both null: false
9
+
10
+ t.index :class_name
11
+ # The profiler_report_id index is already created by t.references
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,26 @@
1
+ module HeapPeriscopeUi
2
+ module Generators
3
+ class InstallGenerator < Rails::Generators::Base
4
+ source_root File.expand_path("templates", __dir__)
5
+
6
+ def add_routes
7
+ route_info = "mount HeapPeriscopeUi::Engine => '/heap_periscope', as: 'heap_periscope_ui'"
8
+ action_cable_route_info = "mount ActionCable.server => '/cable'"
9
+ route_file_path = File.join(destination_root, 'config', 'routes.rb')
10
+
11
+ if File.exist?(route_file_path)
12
+ insert_into_file route_file_path, "\n #{route_info}\n", after: "Rails.application.routes.draw do"
13
+ insert_into_file route_file_path, "\n #{action_cable_route_info}\n", after: "Rails.application.routes.draw do"
14
+ else
15
+ say "Please add the following to your config/routes.rb file:"
16
+ say " #{route_info}"
17
+ end
18
+ end
19
+
20
+ def show_readme
21
+ readme "README_SETUP" if behavior == :invoke
22
+ end
23
+ end
24
+ end
25
+ end
26
+
@@ -0,0 +1,17 @@
1
+ HeapPeriscopeUi has been installed. Please follow these steps:
2
+
3
+ 1. Run database migrations:
4
+ bundle exec rails db:migrate
5
+
6
+ 2. The engine has been mounted at `/heap_periscope` in your `config/routes.rb`.
7
+ You can change this path if needed.
8
+
9
+ 3. Configure your `heap_periscope_agent` gem to send data to the
10
+ ingestion endpoint provided by this UI engine.
11
+ The default ingestion endpoint will be at: `/heap_periscope/api/v1/ingest`
12
+ (Adjust the path if you changed the mount point in your routes).
13
+
14
+ Refer to the `heap_periscope_agent` documentation for how to configure
15
+ its data reporting endpoint.
16
+
17
+ See the HeapPeriscopeUi gem's main README for more details on usage and customization.
@@ -0,0 +1,4 @@
1
+ # HeapPeriscopeUi.setup do |config|
2
+ # # Example configuration:
3
+ # # config.default_items_per_page = 50
4
+ # end
@@ -0,0 +1,13 @@
1
+ module HeapPeriscopeUi
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace HeapPeriscopeUi
4
+
5
+ initializer :append_migrations do |app|
6
+ unless app.root.to_s.match root.to_s
7
+ config.paths["db/migrate"].expanded.each do |expanded_path|
8
+ app.config.paths["db/migrate"] << expanded_path
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,131 @@
1
+ require 'socket'
2
+ require 'json'
3
+ require 'thread'
4
+
5
+ module HeapPeriscopeUi
6
+ class UdpListener
7
+ BATCH_WRITE_INTERVAL = 5 # seconds
8
+ BATCH_MAX_SIZE = 1000 # records
9
+
10
+ def initialize(host: HeapPeriscopeUi.udp_host, port: HeapPeriscopeUi.udp_port)
11
+ @host = host
12
+ @port = port
13
+ @socket = UDPSocket.new
14
+ @gc_report_batch = Queue.new
15
+ @running = false
16
+ end
17
+
18
+ def run
19
+ @running = true
20
+ puts "[HeapPeriscopeUi::UdpListener] Attempting to bind UDP server to #{@host}:#{@port}..." # Changed to puts
21
+
22
+ @socket.bind(@host, @port)
23
+ puts "[HeapPeriscopeUi::UdpListener] Successfully listening on UDP #{@host}:#{@port}" # Changed to puts
24
+
25
+ writer_thread = start_writer_thread
26
+
27
+ while @running
28
+ begin
29
+ data, addr = @socket.recvfrom(65536)
30
+ Thread.new { process_packet(data, addr) }
31
+ rescue IO::WaitReadable, Errno::EINTR
32
+ # These errors are expected during non-blocking I/O or when the socket is interrupted.
33
+ # Retry if the server is still supposed to be running.
34
+ retry if @running
35
+ rescue => e
36
+ # Using puts for consistency if Rails.logger is problematic in this thread
37
+ puts "[HeapPeriscopeUi::UdpListener] ERROR in receive loop: #{e.message}\n#{e.backtrace.join("\n")}"
38
+ end
39
+ end
40
+
41
+ rescue Interrupt
42
+ puts "[UDPServer] Shutting down..."
43
+ ensure
44
+ @running = false
45
+ writer_thread&.join(BATCH_WRITE_INTERVAL + 2)
46
+ write_gc_batch
47
+ @socket.close if @socket
48
+ puts "[UDPServer] UDP server stopped."
49
+ end
50
+
51
+ private
52
+
53
+ def process_packet(data, addr)
54
+ sender_ip = addr[3]
55
+ puts "[UDPServer] Received data from #{sender_ip}"
56
+ payload = JSON.parse(data)
57
+
58
+ # Broadcast to the stream name defined in the channel
59
+ ActionCable.server.broadcast("heap_periscope_runtime_stats", payload)
60
+
61
+ case payload['type']
62
+ when 'gc_profiler_report'
63
+ translate_gc_report(payload)
64
+ when 'snapshot'
65
+ translate_snapshot_report(payload)
66
+ else
67
+ puts "[UDPServer] Received unknown payload type: #{payload['type']}"
68
+ end
69
+
70
+ rescue JSON::ParserError => e
71
+ puts "[UDPServer] Error parsing JSON: #{e.message}"
72
+ rescue => e
73
+ puts "[UDPServer] An unexpected error occurred: #{e.message}\n#{e.backtrace.join("\n")}"
74
+ end
75
+
76
+ def translate_gc_report(payload)
77
+ report_data = {
78
+ process_id: payload['process_id'],
79
+ report_type: 'gc_profiler_report',
80
+ reported_at: payload['reported_at'],
81
+ gc_duration_ms: payload['payload']['gc_duration_since_last_check_ms'],
82
+ gc_invocation_count: payload['payload']['gc_invocation_count'],
83
+ created_at: Time.current,
84
+ updated_at: Time.current
85
+ }
86
+ @gc_report_batch << report_data
87
+ end
88
+
89
+ def translate_snapshot_report(payload)
90
+ snapshot_payload = payload['payload']
91
+
92
+ ActiveRecord::Base.transaction do
93
+ if snapshot_payload['living_objects_by_class']
94
+ object_counts_data = snapshot_payload['living_objects_by_class'].map do |class_name, count_details|
95
+ {
96
+ profiler_report_id: SecureRandom.uuid,
97
+ class_name: class_name,
98
+ count: count_details['count'],
99
+ is_platform_class: count_details['is_platform_class'],
100
+ created_at: Time.current,
101
+ updated_at: Time.current
102
+ }
103
+ end
104
+ end
105
+ end
106
+ puts "[UDPServer] Stored snapshot report for process #{payload['process_id']}."
107
+ end
108
+
109
+ def start_writer_thread
110
+ Thread.new do
111
+ while @running
112
+ sleep(BATCH_WRITE_INTERVAL)
113
+ write_gc_batch if @running # Only write if still supposed to be running
114
+ end
115
+ end
116
+ end
117
+
118
+ def write_gc_batch
119
+ return if @gc_report_batch.empty?
120
+
121
+ items_to_insert = []
122
+ items_to_insert << @gc_report_batch.pop until @gc_report_batch.empty? || items_to_insert.size >= BATCH_MAX_SIZE
123
+
124
+ return unless items_to_insert.any?
125
+
126
+ puts "[UDPServer] Writing batch of #{items_to_insert.size} GC reports to database..."
127
+ rescue => e
128
+ puts "[UDPServer] ERROR during batch insert: #{e.message}"
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,3 @@
1
+ module HeapPeriscopeUi
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,16 @@
1
+ require "heap_periscope_ui/version"
2
+ require "heap_periscope_ui/engine"
3
+
4
+ module HeapPeriscopeUi; end
5
+ # frozen_string_literal: true
6
+
7
+ module HeapPeriscopeUi
8
+ mattr_accessor :udp_host, :udp_port, :default_items_per_page
9
+ self.udp_host = '0.0.0.0' # Listen on all available interfaces by default
10
+ self.udp_port = 52525 # Default port used by heap_periscope_agent
11
+ self.default_items_per_page = 25
12
+
13
+ def self.setup
14
+ yield self
15
+ end
16
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :heap_periscope_ui do
3
+ # # Task goes here
4
+ # end
metadata ADDED
@@ -0,0 +1,100 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: heap_periscope_ui
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Jonathan Natanael Siahaan
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rails
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: 8.0.2
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: 8.0.2
26
+ description: 'HeapPeriscopeUi is a Rails engine designed to help developers monitor,
27
+ visualize, and diagnose memory-related issues within their Ruby applications. It
28
+ functions by listening for UDP packets containing GC profiler reports and heap snapshots,
29
+ typically transmitted by a companion agent (like `heap_periscope_agent`). The engine
30
+ efficiently processes this data: GC reports are batched for optimized database insertion,
31
+ while comprehensive heap snapshots, including detailed object counts by class, are
32
+ stored transactionally. Furthermore, HeapPeriscopeUi can broadcast incoming metrics
33
+ via ActionCable for real-time dashboard updates. Its integrated web interface allows
34
+ users to browse, filter, and analyze the collected profiler reports, facilitating
35
+ the identification of memory leaks, excessive object allocations, and opportunities
36
+ for performance optimization.'
37
+ email:
38
+ - js.jonathan.n@gmail.com
39
+ executables: []
40
+ extensions: []
41
+ extra_rdoc_files: []
42
+ files:
43
+ - MIT-LICENSE
44
+ - README.md
45
+ - Rakefile
46
+ - app/assets/config/heap_periscope_ui_manifest.js
47
+ - app/assets/stylesheets/heap_periscope_ui/application.css
48
+ - app/assets/stylesheets/heap_periscope_ui/visualizer.css
49
+ - app/channels/heap_periscope_ui/application_cable/channel.rb
50
+ - app/channels/heap_periscope_ui/application_cable/connection.rb
51
+ - app/channels/heap_periscope_ui/runtime_stats_channel.rb
52
+ - app/controllers/heap_periscope_ui/application_controller.rb
53
+ - app/controllers/heap_periscope_ui/dashboard_controller.rb
54
+ - app/helpers/heap_periscope_ui/application_helper.rb
55
+ - app/jobs/heap_periscope_ui/application_job.rb
56
+ - app/mailers/heap_periscope_ui/application_mailer.rb
57
+ - app/models/heap_periscope_ui/application_record.rb
58
+ - app/models/heap_periscope_ui/object_count.rb
59
+ - app/models/heap_periscope_ui/profiler_report.rb
60
+ - app/views/heap_periscope_ui/dashboard/index.html
61
+ - app/views/heap_periscope_ui/dashboard/index.html.bak
62
+ - app/views/layouts/heap_periscope_ui/application.html.erb
63
+ - config/initializers/heap_periscope_ui_start_udp_listener.rb
64
+ - config/routes.rb
65
+ - db/migrate/20250617144101_create_heap_periscope_ui_profiler_reports.rb
66
+ - db/migrate/20250617144853_create_heap_periscope_ui_object_counts.rb
67
+ - lib/generators/heap_periscope_ui/install/install_generator.rb
68
+ - lib/generators/heap_periscope_ui/install/templates/README_SETUP
69
+ - lib/generators/heap_periscope_ui/install/templates/initializer.rb
70
+ - lib/heap_periscope_ui.rb
71
+ - lib/heap_periscope_ui/engine.rb
72
+ - lib/heap_periscope_ui/udp_listener.rb
73
+ - lib/heap_periscope_ui/version.rb
74
+ - lib/tasks/heap_periscope_ui_tasks.rake
75
+ homepage: https://github.com/codepawpaw/heap_periscope_ui
76
+ licenses:
77
+ - MIT
78
+ metadata:
79
+ homepage_uri: https://github.com/codepawpaw/heap_periscope_ui
80
+ source_code_uri: https://github.com/codepawpaw/heap_periscope_ui
81
+ changelog_uri: https://github.com/codepawpaw/heap_periscope_ui/releases
82
+ rdoc_options: []
83
+ require_paths:
84
+ - lib
85
+ required_ruby_version: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ required_rubygems_version: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
95
+ requirements: []
96
+ rubygems_version: 3.6.9
97
+ specification_version: 4
98
+ summary: A Rails engine providing a UI to visualize heap memory and GC metrics from
99
+ Ruby applications.
100
+ test_files: []