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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +28 -0
- data/Rakefile +8 -0
- data/app/assets/config/heap_periscope_ui_manifest.js +3 -0
- data/app/assets/stylesheets/heap_periscope_ui/application.css +15 -0
- data/app/assets/stylesheets/heap_periscope_ui/visualizer.css +11 -0
- data/app/channels/heap_periscope_ui/application_cable/channel.rb +7 -0
- data/app/channels/heap_periscope_ui/application_cable/connection.rb +12 -0
- data/app/channels/heap_periscope_ui/runtime_stats_channel.rb +14 -0
- data/app/controllers/heap_periscope_ui/application_controller.rb +4 -0
- data/app/controllers/heap_periscope_ui/dashboard_controller.rb +11 -0
- data/app/helpers/heap_periscope_ui/application_helper.rb +4 -0
- data/app/jobs/heap_periscope_ui/application_job.rb +4 -0
- data/app/mailers/heap_periscope_ui/application_mailer.rb +6 -0
- data/app/models/heap_periscope_ui/application_record.rb +5 -0
- data/app/models/heap_periscope_ui/object_count.rb +13 -0
- data/app/models/heap_periscope_ui/profiler_report.rb +15 -0
- data/app/views/heap_periscope_ui/dashboard/index.html +1132 -0
- data/app/views/heap_periscope_ui/dashboard/index.html.bak +862 -0
- data/app/views/layouts/heap_periscope_ui/application.html.erb +14 -0
- data/config/initializers/heap_periscope_ui_start_udp_listener.rb +15 -0
- data/config/routes.rb +8 -0
- data/db/migrate/20250617144101_create_heap_periscope_ui_profiler_reports.rb +17 -0
- data/db/migrate/20250617144853_create_heap_periscope_ui_object_counts.rb +14 -0
- data/lib/generators/heap_periscope_ui/install/install_generator.rb +26 -0
- data/lib/generators/heap_periscope_ui/install/templates/README_SETUP +17 -0
- data/lib/generators/heap_periscope_ui/install/templates/initializer.rb +4 -0
- data/lib/heap_periscope_ui/engine.rb +13 -0
- data/lib/heap_periscope_ui/udp_listener.rb +131 -0
- data/lib/heap_periscope_ui/version.rb +3 -0
- data/lib/heap_periscope_ui.rb +16 -0
- data/lib/tasks/heap_periscope_ui_tasks.rake +4 -0
- 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,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,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,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
|
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: []
|