dial 0.1.3 → 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
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: []