dial 0.1.3 → 0.1.4

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: dcc8b8f15acf035328c3330930476af9621529608cabcb511b240cc7da9a9ecf
4
- data.tar.gz: 05b4c3610edad8e1b6b4cb1cad387ea962d7e250c8cb165a845926bbd668e37c
3
+ metadata.gz: 61252b82f8769e8c0724a9cb40a8771afc59df61de01847add599037939b1e7d
4
+ data.tar.gz: 3944fe8e444853a4dfe4f2fb474bf6ed6a4b30814d55dc127df986e99c08bcf6
5
5
  SHA512:
6
- metadata.gz: 9bf37053b4f2bc59bd322646d7485326378c6d234d822571e59f14530ea4382830a6b842f514bef3f9fc7d6719b9b212251023b5c6a214304fb920aac0c4abd3
7
- data.tar.gz: c5688daab1bd6d82310bab1270a243e507eb54bbc12cbf187c2d3237e43fe1d5022df34fa7f105b642ca563f10f7b343181a74bc5c1864a1dd36d5cca6d15f85
6
+ metadata.gz: cdbc7c51f7392b280a56f56d232204c93ee77e436ee6891f7f88e9d9c39570b77b85af3ff07dc15a54c854757053be0ab8c85d7adb6c21ed23f438c43c0d4da7
7
+ data.tar.gz: 8b8b5400414355f6012d87b490b719ab3b79cfacc229705e752e8822162cc383979e8898711f76b0b1393c497ec0c5a0cd4a344f9050ab1832e1317b314c30bd
data/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.1.4] - 2024-12-27
4
+
5
+ - Use vernier memory usage and rails hooks by default
6
+ - Add support for N+1 detection with prosopite
7
+
3
8
  ## [0.1.3] - 2024-11-14
4
9
 
5
10
  - Enable allocation profiling by default
data/README.md CHANGED
@@ -7,17 +7,24 @@ WIP
7
7
 
8
8
  A modern profiler for Rails applications.
9
9
 
10
- https://github.com/user-attachments/assets/bae59681-ebeb-42b3-9489-9692c072c3dc
10
+ Check out the demo:
11
+ [![Demo](https://img.youtube.com/vi/LPXtfJ0c284/maxresdefault.jpg)](https://youtu.be/LPXtfJ0c284)
11
12
 
12
13
  ## Installation
13
14
 
14
- Install the gem and add it to your Rails application's Gemfile by executing:
15
+ 1. Install the gem and add it to your Rails application's Gemfile by executing:
15
16
 
16
17
  ```bash
17
18
  bundle add dial
18
19
  ```
19
20
 
20
- and everything should just work.
21
+ 2. Mount the engine in your `config/routes.rb` file:
22
+
23
+
24
+ ```ruby
25
+ # this will mount the engine at /dial
26
+ mount Dial::Engine, at: "/"
27
+ ```
21
28
 
22
29
  ## Development
23
30
 
data/dial.gemspec CHANGED
@@ -22,5 +22,7 @@ Gem::Specification.new do |spec|
22
22
  spec.add_dependency "railties", ">= 7", "< 8.2"
23
23
  spec.add_dependency "activerecord", ">= 7", "< 8.2"
24
24
  spec.add_dependency "actionpack", ">= 7", "< 8.2"
25
- spec.add_dependency "vernier", "~> 1.3"
25
+ spec.add_dependency "vernier", "~> 1.5"
26
+ spec.add_dependency "prosopite", "~> 1.4"
27
+ spec.add_dependency "pg_query", "~> 5.1"
26
28
  end
@@ -1,6 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "version"
4
+
3
5
  module Dial
4
6
  REQUEST_TIMING_HEADER = "dial_request_timing"
7
+
8
+ PROFILE_OUT_STALE_SECONDS = 60 * 60
5
9
  PROFILE_OUT_RELATIVE_DIRNAME = "tmp/dial/profile/"
10
+
11
+ PROSOPITE_IGNORE_QUERIES = [/schema_migrations/]
12
+ PROSOPITE_LOG_RELATIVE_PATHNAME = "log/dial/prosopite.log"
6
13
  end
@@ -5,7 +5,7 @@ require "uri"
5
5
  module Dial
6
6
  class Panel
7
7
  class << self
8
- def html env, profile_out_filename, ruby_vm_stat, gc_stat, gc_stat_heap, server_timing
8
+ def html env, profile_out_filename, query_logs, ruby_vm_stat, gc_stat, gc_stat_heap, server_timing
9
9
  <<~HTML
10
10
  <style>#{style}</style>
11
11
 
@@ -31,6 +31,13 @@ module Dial
31
31
  </div>
32
32
  </details>
33
33
 
34
+ <details>
35
+ <summary>N+1s</summary>
36
+ <div class="section query-logs">
37
+ #{formatted_query_logs query_logs}
38
+ </div>
39
+ </details>
40
+
34
41
  <details>
35
42
  <summary>RubyVM stat</summary>
36
43
  <div class="section">
@@ -92,6 +99,14 @@ module Dial
92
99
  margin: 0.25rem 0 0 0;
93
100
  }
94
101
 
102
+ .query-logs {
103
+ padding-left: 0.75rem;
104
+
105
+ details {
106
+ margin-top: 0;
107
+ }
108
+ }
109
+
95
110
  span {
96
111
  text-align: left;
97
112
  }
@@ -140,10 +155,10 @@ module Dial
140
155
  end
141
156
 
142
157
  def formatted_profile_output env, profile_out_filename
158
+ url_base = ::Rails.application.routes.url_helpers.dial_url host: env[::Rack::HTTP_HOST]
159
+ prefix = "/" unless url_base.end_with? "/"
143
160
  uuid = profile_out_filename.delete_suffix ".json"
144
- host = env[::Rack::HTTP_HOST]
145
- base_url = ::Rails.application.routes.url_helpers.dial_url host: host
146
- profile_out_url = URI.encode_www_form_component base_url + "dial/profile?uuid=#{uuid}"
161
+ profile_out_url = URI.encode_www_form_component url_base + "#{prefix}dial/profile?uuid=#{uuid}"
147
162
 
148
163
  "<a href='https://vernier.prof/from-url/#{profile_out_url}' target='_blank'>View profile</a>"
149
164
  end
@@ -171,6 +186,20 @@ module Dial
171
186
  end
172
187
  end
173
188
 
189
+ def formatted_query_logs query_logs
190
+ query_logs.map do |(queries, stack_lines)|
191
+ <<~HTML
192
+ <details>
193
+ <summary>#{queries.shift}</summary>
194
+ <div class="section query-logs">
195
+ #{queries.map { |query| "<span>#{query}</span>" }.join}
196
+ #{stack_lines.map { |stack_line| "<span>#{stack_line}</span>" }.join}
197
+ </div>
198
+ </details>
199
+ HTML
200
+ end.join
201
+ end
202
+
174
203
  def formatted_ruby_vm_stat ruby_vm_stat
175
204
  ruby_vm_stat.map { |key, value| "<span><b>#{key}:</b> #{value}</span>" }.join
176
205
  end
@@ -1,8 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "securerandom"
4
+
3
5
  require "vernier"
6
+ require "prosopite"
4
7
 
5
- require_relative "constants"
6
8
  require_relative "middleware/panel"
7
9
  require_relative "middleware/ruby_stat"
8
10
  require_relative "middleware/rails_stat"
@@ -19,38 +21,36 @@ module Dial
19
21
  def call env
20
22
  start_time = Process.clock_gettime Process::CLOCK_MONOTONIC
21
23
 
22
- ruby_vm_stat_before = RubyVM.stat
23
- gc_stat_before = GC.stat
24
- gc_stat_heap_before = GC.stat_heap
25
-
26
- profile_out_dirname = String ::Rails.root.join PROFILE_OUT_RELATIVE_DIRNAME
27
- FileUtils.mkdir_p profile_out_dirname
28
- profile_out_filename = "#{SecureRandom.uuid}.json"
29
- profile_out_pathname = "#{profile_out_dirname}#{profile_out_filename}"
24
+ profile_out_filename = "#{SecureRandom.uuid_v7}.json"
25
+ profile_out_pathname = "#{profile_out_dir_pathname}#{profile_out_filename}"
30
26
 
31
27
  status, headers, rack_body = nil
32
- ::Vernier.profile out: profile_out_pathname, interval: 500, allocation_interval: 1000 do
33
- status, headers, rack_body = @app.call env
28
+ ruby_vm_stat, gc_stat, gc_stat_heap = nil
29
+ ::Prosopite.scan do
30
+ ::Vernier.profile out: profile_out_pathname, interval: 500, allocation_interval: 1000, hooks: [:memory_usage, :rails] do
31
+ ruby_vm_stat, gc_stat, gc_stat_heap = with_diffed_ruby_stats do
32
+ status, headers, rack_body = @app.call env
33
+ end
34
+ end
34
35
  end
36
+ server_timing = server_timing headers
35
37
 
36
38
  unless headers[::Rack::CONTENT_TYPE]&.include? "text/html"
37
39
  File.delete profile_out_pathname if File.exist? profile_out_pathname
38
40
  return [status, headers, rack_body]
39
41
  end
40
42
 
43
+ query_logs = clear_query_logs!
44
+ remove_stale_profile_out_files!
45
+
41
46
  finish_time = Process.clock_gettime Process::CLOCK_MONOTONIC
42
47
  env[REQUEST_TIMING_HEADER] = ((finish_time - start_time) * 1_000).round 2
43
48
 
44
- ruby_vm_stat = ruby_vm_stat_diff ruby_vm_stat_before, RubyVM.stat
45
- gc_stat = gc_stat_diff gc_stat_before, GC.stat
46
- gc_stat_heap = gc_stat_heap_diff gc_stat_heap_before, GC.stat_heap
47
- server_timing = server_timing headers
48
-
49
49
  body = String.new.tap do |str|
50
50
  rack_body.each { |chunk| str << chunk }
51
51
  rack_body.close if rack_body.respond_to? :close
52
52
  end.sub "</body>", <<~HTML
53
- #{Panel.html env, profile_out_filename, ruby_vm_stat, gc_stat, gc_stat_heap, server_timing}
53
+ #{Panel.html env, profile_out_filename, query_logs, ruby_vm_stat, gc_stat, gc_stat_heap, server_timing}
54
54
  </body>
55
55
  HTML
56
56
 
@@ -58,5 +58,77 @@ module Dial
58
58
 
59
59
  [status, headers, [body]]
60
60
  end
61
+
62
+ private
63
+
64
+ def with_diffed_ruby_stats
65
+ ruby_vm_stat_before = RubyVM.stat
66
+ gc_stat_before = GC.stat
67
+ gc_stat_heap_before = GC.stat_heap
68
+ yield
69
+ [
70
+ ruby_vm_stat_diff(ruby_vm_stat_before, RubyVM.stat),
71
+ gc_stat_diff(gc_stat_before, GC.stat),
72
+ gc_stat_heap_diff(gc_stat_heap_before, GC.stat_heap)
73
+ ]
74
+ end
75
+
76
+ def remove_stale_profile_out_files!
77
+ stale_profile_out_files.each do |profile_out_file|
78
+ File.delete profile_out_file
79
+ end
80
+ end
81
+
82
+ def stale_profile_out_files
83
+ Dir.glob("#{profile_out_dir_pathname}/*.json").select do |profile_out_file|
84
+ timestamp = Util.uuid_v7_timestamp File.basename profile_out_file
85
+ timestamp < Time.now - PROFILE_OUT_STALE_SECONDS
86
+ end
87
+ end
88
+
89
+ def profile_out_dir_pathname
90
+ @_profile_out_dir_pathname ||= ::Rails.root.join PROFILE_OUT_RELATIVE_DIRNAME
91
+ end
92
+
93
+ def clear_query_logs!
94
+ [].tap do |query_logs|
95
+ File.open(query_log_pathname, "r+") do |file|
96
+ entry = reading_section = query_count = nil
97
+ file.each_line do |line|
98
+ case line
99
+ when /N\+1 queries detected/
100
+ entry = [[], []]
101
+ reading_section = :queries
102
+ query_count = 0
103
+ when /Call stack/
104
+ reading_section = :call_stack
105
+ if query_count > 5
106
+ entry.first << "+ #{query_count - 5} more queries"
107
+ end
108
+ else
109
+ case reading_section
110
+ when :queries
111
+ query_count += 1
112
+ entry.first << line.strip if query_count <= 5
113
+ when :call_stack
114
+ if line.strip.empty?
115
+ query_logs << entry
116
+ reading_section = nil
117
+ else
118
+ entry.last << line.strip
119
+ end
120
+ end
121
+ end
122
+ end
123
+
124
+ file.truncate 0
125
+ file.rewind
126
+ end
127
+ end
128
+ end
129
+
130
+ def query_log_pathname
131
+ @_query_log_dir_pathname ||= ::Rails.root.join PROSOPITE_LOG_RELATIVE_PATHNAME
132
+ end
61
133
  end
62
134
  end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dial
4
+ class ProsopiteLogger < Logger
5
+ end
6
+ end
data/lib/dial/railtie.rb CHANGED
@@ -1,13 +1,36 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "rails"
4
+ require "prosopite"
4
5
 
5
6
  require_relative "middleware"
7
+ require_relative "prosopite_logger"
6
8
 
7
9
  module Dial
8
10
  class Railtie < ::Rails::Railtie
9
11
  initializer "dial.use_middleware" do |app|
10
12
  app.middleware.insert_before 0, Middleware
11
13
  end
14
+
15
+ initializer "dial.set_up_vernier" do |app|
16
+ app.config.after_initialize do
17
+ FileUtils.mkdir_p ::Rails.root.join PROFILE_OUT_RELATIVE_DIRNAME
18
+ end
19
+ end
20
+
21
+ initializer "dial.set_up_prosopite" do |app|
22
+ app.config.after_initialize do
23
+ if ::ActiveRecord::Base.connection.adapter_name == "PostgreSQL"
24
+ require "pg_query"
25
+ end
26
+
27
+ prosopite_log_pathname = ::Rails.root.join PROSOPITE_LOG_RELATIVE_PATHNAME
28
+ FileUtils.mkdir_p File.dirname prosopite_log_pathname
29
+ FileUtils.touch prosopite_log_pathname
30
+ ::Prosopite.custom_logger = ProsopiteLogger.new prosopite_log_pathname
31
+
32
+ ::Prosopite.ignore_queries = PROSOPITE_IGNORE_QUERIES
33
+ end
34
+ end
12
35
  end
13
36
  end
data/lib/dial/util.rb ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dial
4
+ module Util
5
+ class << self
6
+ def uuid_v7_timestamp uuid
7
+ high_bits_hex = uuid.split("-").first(2).join[0, 12].to_i 16
8
+ Time.at high_bits_hex / 1000.0
9
+ end
10
+ end
11
+ end
12
+ end
data/lib/dial/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Dial
4
- VERSION = "0.1.3"
4
+ VERSION = "0.1.4"
5
5
  end
data/lib/dial.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "dial/version"
3
+ require_relative "dial/constants"
4
+ require_relative "dial/util"
5
+
4
6
  require_relative "dial/railtie"
5
7
  require_relative "dial/engine"
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dial
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joshua Young
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2024-11-14 00:00:00.000000000 Z
10
+ date: 2024-12-27 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: railties
@@ -75,14 +75,42 @@ dependencies:
75
75
  requirements:
76
76
  - - "~>"
77
77
  - !ruby/object:Gem::Version
78
- version: '1.3'
78
+ version: '1.5'
79
79
  type: :runtime
80
80
  prerelease: false
81
81
  version_requirements: !ruby/object:Gem::Requirement
82
82
  requirements:
83
83
  - - "~>"
84
84
  - !ruby/object:Gem::Version
85
- version: '1.3'
85
+ version: '1.5'
86
+ - !ruby/object:Gem::Dependency
87
+ name: prosopite
88
+ requirement: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - "~>"
91
+ - !ruby/object:Gem::Version
92
+ version: '1.4'
93
+ type: :runtime
94
+ prerelease: false
95
+ version_requirements: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - "~>"
98
+ - !ruby/object:Gem::Version
99
+ version: '1.4'
100
+ - !ruby/object:Gem::Dependency
101
+ name: pg_query
102
+ requirement: !ruby/object:Gem::Requirement
103
+ requirements:
104
+ - - "~>"
105
+ - !ruby/object:Gem::Version
106
+ version: '5.1'
107
+ type: :runtime
108
+ prerelease: false
109
+ version_requirements: !ruby/object:Gem::Requirement
110
+ requirements:
111
+ - - "~>"
112
+ - !ruby/object:Gem::Version
113
+ version: '5.1'
86
114
  email:
87
115
  - djry1999@gmail.com
88
116
  executables: []
@@ -102,7 +130,9 @@ files:
102
130
  - lib/dial/middleware/panel.rb
103
131
  - lib/dial/middleware/rails_stat.rb
104
132
  - lib/dial/middleware/ruby_stat.rb
133
+ - lib/dial/prosopite_logger.rb
105
134
  - lib/dial/railtie.rb
135
+ - lib/dial/util.rb
106
136
  - lib/dial/version.rb
107
137
  homepage: https://github.com/joshuay03/dial
108
138
  licenses:
@@ -124,7 +154,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
124
154
  - !ruby/object:Gem::Version
125
155
  version: '0'
126
156
  requirements: []
127
- rubygems_version: 3.6.0.dev
157
+ rubygems_version: 3.6.2
128
158
  specification_version: 4
129
159
  summary: A modern Rails profiler
130
160
  test_files: []