rails_console_pro 0.1.2 → 0.1.3
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/.rspec_status +261 -240
- data/CHANGELOG.md +4 -0
- data/QUICK_START.md +8 -0
- data/README.md +16 -0
- data/docs/FORMATTING.md +5 -0
- data/docs/MODEL_STATISTICS.md +4 -0
- data/docs/OBJECT_DIFFING.md +6 -0
- data/docs/PROFILING.md +91 -0
- data/docs/QUEUE_INSIGHTS.md +82 -0
- data/docs/SCHEMA_INSPECTION.md +5 -0
- data/docs/SNIPPETS.md +71 -0
- data/lib/rails_console_pro/commands/jobs_command.rb +212 -0
- data/lib/rails_console_pro/commands/profile_command.rb +84 -0
- data/lib/rails_console_pro/commands/snippets_command.rb +141 -0
- data/lib/rails_console_pro/commands.rb +15 -0
- data/lib/rails_console_pro/configuration.rb +39 -0
- data/lib/rails_console_pro/format_exporter.rb +8 -0
- data/lib/rails_console_pro/global_methods.rb +12 -0
- data/lib/rails_console_pro/initializer.rb +23 -0
- data/lib/rails_console_pro/model_validator.rb +1 -1
- data/lib/rails_console_pro/printers/profile_printer.rb +180 -0
- data/lib/rails_console_pro/printers/queue_insights_printer.rb +150 -0
- data/lib/rails_console_pro/printers/snippet_collection_printer.rb +68 -0
- data/lib/rails_console_pro/printers/snippet_printer.rb +64 -0
- data/lib/rails_console_pro/profile_result.rb +109 -0
- data/lib/rails_console_pro/pry_commands.rb +106 -0
- data/lib/rails_console_pro/queue_insights_result.rb +110 -0
- data/lib/rails_console_pro/serializers/profile_serializer.rb +73 -0
- data/lib/rails_console_pro/services/profile_collector.rb +245 -0
- data/lib/rails_console_pro/services/queue_action_service.rb +176 -0
- data/lib/rails_console_pro/services/queue_insight_fetcher.rb +600 -0
- data/lib/rails_console_pro/services/snippet_repository.rb +191 -0
- data/lib/rails_console_pro/snippets/collection_result.rb +44 -0
- data/lib/rails_console_pro/snippets/single_result.rb +30 -0
- data/lib/rails_console_pro/snippets/snippet.rb +112 -0
- data/lib/rails_console_pro/snippets.rb +12 -0
- data/lib/rails_console_pro/version.rb +1 -1
- data/rails_console_pro.gemspec +1 -1
- metadata +26 -8
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsConsolePro
|
|
4
|
+
module Commands
|
|
5
|
+
# Command for managing, searching, and listing reusable console snippets
|
|
6
|
+
class SnippetsCommand < BaseCommand
|
|
7
|
+
DEFAULT_LIST_LIMIT = 10
|
|
8
|
+
|
|
9
|
+
def execute(action = :list, *args, **kwargs, &block)
|
|
10
|
+
return disabled_message unless enabled?
|
|
11
|
+
|
|
12
|
+
action = infer_action(action, args)
|
|
13
|
+
case action
|
|
14
|
+
when :list
|
|
15
|
+
list_snippets(**kwargs)
|
|
16
|
+
when :search
|
|
17
|
+
search_snippets(*args, **kwargs)
|
|
18
|
+
when :add, :create
|
|
19
|
+
add_snippet(*args, **kwargs, &block)
|
|
20
|
+
when :show
|
|
21
|
+
show_snippet(*args)
|
|
22
|
+
when :delete, :remove
|
|
23
|
+
delete_snippet(*args)
|
|
24
|
+
when :favorite
|
|
25
|
+
toggle_favorite(*args, value: true)
|
|
26
|
+
when :unfavorite
|
|
27
|
+
toggle_favorite(*args, value: false)
|
|
28
|
+
else
|
|
29
|
+
pastel.red("Unknown snippets action: #{action}")
|
|
30
|
+
end
|
|
31
|
+
rescue ArgumentError => e
|
|
32
|
+
pastel.red("Snippets error: #{e.message}")
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def enabled?
|
|
38
|
+
RailsConsolePro.config.enabled && RailsConsolePro.config.snippets_command_enabled
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def disabled_message
|
|
42
|
+
pastel.yellow('Snippets command is disabled. Enable via RailsConsolePro.configure { |c| c.snippets_command_enabled = true }')
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def infer_action(action, args)
|
|
46
|
+
return action.to_sym if action.respond_to?(:to_sym)
|
|
47
|
+
return :search if args.any?
|
|
48
|
+
|
|
49
|
+
:list
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def list_snippets(limit: DEFAULT_LIST_LIMIT, favorites: false)
|
|
53
|
+
snippets = repository.all(limit: limit)
|
|
54
|
+
snippets = snippets.select(&:favorite?) if favorites
|
|
55
|
+
|
|
56
|
+
Snippets::CollectionResult.new(
|
|
57
|
+
snippets: snippets,
|
|
58
|
+
limit: limit,
|
|
59
|
+
total_count: repository.all.size,
|
|
60
|
+
tags: favorites ? ['favorite'] : []
|
|
61
|
+
)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def search_snippets(term = nil, tags: nil, limit: DEFAULT_LIST_LIMIT)
|
|
65
|
+
search_result = repository.search(term: term, tags: tags, limit: limit)
|
|
66
|
+
snippets = search_result[:results]
|
|
67
|
+
Snippets::CollectionResult.new(
|
|
68
|
+
snippets: snippets,
|
|
69
|
+
query: term,
|
|
70
|
+
tags: Array(tags),
|
|
71
|
+
limit: limit,
|
|
72
|
+
total_count: search_result[:total_count]
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def add_snippet(body = nil, id: nil, tags: nil, description: nil, favorite: false, metadata: nil, &block)
|
|
77
|
+
snippet_body = extract_body(body, &block)
|
|
78
|
+
|
|
79
|
+
snippet = repository.add(
|
|
80
|
+
body: snippet_body,
|
|
81
|
+
id: id,
|
|
82
|
+
tags: tags,
|
|
83
|
+
description: description,
|
|
84
|
+
favorite: favorite,
|
|
85
|
+
metadata: metadata
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
Snippets::SingleResult.new(snippet: snippet, action: :add, created: true)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def show_snippet(id)
|
|
92
|
+
snippet = repository.find(id)
|
|
93
|
+
return pastel.yellow("No snippet found for '#{id}'") unless snippet
|
|
94
|
+
|
|
95
|
+
Snippets::SingleResult.new(snippet: snippet, action: :show, created: false)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def delete_snippet(id)
|
|
99
|
+
if repository.delete(id)
|
|
100
|
+
pastel.green("Removed snippet '#{id}'")
|
|
101
|
+
else
|
|
102
|
+
pastel.yellow("No snippet found for '#{id}'")
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def toggle_favorite(id, value:)
|
|
107
|
+
snippet = repository.update(id, favorite: value)
|
|
108
|
+
return pastel.yellow("No snippet found for '#{id}'") unless snippet
|
|
109
|
+
|
|
110
|
+
status = value ? 'marked as favorite' : 'removed from favorites'
|
|
111
|
+
message = pastel.green("Snippet '#{id}' #{status}.")
|
|
112
|
+
Snippets::SingleResult.new(
|
|
113
|
+
snippet: snippet,
|
|
114
|
+
action: value ? :favorite : :unfavorite,
|
|
115
|
+
created: false,
|
|
116
|
+
message: message
|
|
117
|
+
)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def extract_body(body, &block)
|
|
121
|
+
content = if block
|
|
122
|
+
result = block.call
|
|
123
|
+
result.respond_to?(:join) ? result.join("\n") : result.to_s
|
|
124
|
+
else
|
|
125
|
+
body
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
raise ArgumentError, 'Snippet body is required' if content.nil? || content.to_s.strip.empty?
|
|
129
|
+
|
|
130
|
+
content.to_s
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def repository
|
|
134
|
+
@repository ||= RailsConsolePro::Services::SnippetRepository.new(
|
|
135
|
+
store_path: RailsConsolePro.config.snippet_store_path
|
|
136
|
+
)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
@@ -30,5 +30,20 @@ module RailsConsolePro
|
|
|
30
30
|
def diff(object1, object2)
|
|
31
31
|
DiffCommand.new.execute(object1, object2)
|
|
32
32
|
end
|
|
33
|
+
|
|
34
|
+
# Profiling command
|
|
35
|
+
def profile(target = nil, *args, **kwargs, &block)
|
|
36
|
+
ProfileCommand.new.execute(target, *args, **kwargs, &block)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Queue insights command
|
|
40
|
+
def jobs(options = {})
|
|
41
|
+
JobsCommand.new.execute(options)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Snippets command
|
|
45
|
+
def snippets(action = :list, *args, **kwargs, &block)
|
|
46
|
+
SnippetsCommand.new.execute(action, *args, **kwargs, &block)
|
|
47
|
+
end
|
|
33
48
|
end
|
|
34
49
|
end
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'tmpdir'
|
|
4
|
+
require 'pathname'
|
|
5
|
+
|
|
3
6
|
module RailsConsolePro
|
|
4
7
|
# Configuration class for Enhanced Console Printer
|
|
5
8
|
class Configuration
|
|
@@ -45,10 +48,14 @@ module RailsConsolePro
|
|
|
45
48
|
attr_accessor :navigate_command_enabled
|
|
46
49
|
attr_accessor :stats_command_enabled
|
|
47
50
|
attr_accessor :diff_command_enabled
|
|
51
|
+
attr_accessor :snippets_command_enabled
|
|
52
|
+
attr_accessor :profile_command_enabled
|
|
53
|
+
attr_accessor :queue_command_enabled
|
|
48
54
|
attr_accessor :active_record_printer_enabled
|
|
49
55
|
attr_accessor :relation_printer_enabled
|
|
50
56
|
attr_accessor :collection_printer_enabled
|
|
51
57
|
attr_accessor :export_enabled
|
|
58
|
+
attr_accessor :snippet_store_path
|
|
52
59
|
|
|
53
60
|
# Color customization
|
|
54
61
|
attr_accessor :color_scheme
|
|
@@ -76,6 +83,11 @@ module RailsConsolePro
|
|
|
76
83
|
attr_accessor :stats_large_table_threshold
|
|
77
84
|
attr_accessor :stats_skip_distinct_threshold
|
|
78
85
|
|
|
86
|
+
# Profiling settings
|
|
87
|
+
attr_accessor :profile_slow_query_threshold
|
|
88
|
+
attr_accessor :profile_duplicate_query_threshold
|
|
89
|
+
attr_accessor :profile_max_saved_queries
|
|
90
|
+
|
|
79
91
|
def initialize
|
|
80
92
|
# Default feature toggles - all enabled
|
|
81
93
|
@enabled = true
|
|
@@ -84,10 +96,14 @@ module RailsConsolePro
|
|
|
84
96
|
@navigate_command_enabled = true
|
|
85
97
|
@stats_command_enabled = true
|
|
86
98
|
@diff_command_enabled = true
|
|
99
|
+
@snippets_command_enabled = true
|
|
100
|
+
@profile_command_enabled = true
|
|
101
|
+
@queue_command_enabled = true
|
|
87
102
|
@active_record_printer_enabled = true
|
|
88
103
|
@relation_printer_enabled = true
|
|
89
104
|
@collection_printer_enabled = true
|
|
90
105
|
@export_enabled = true
|
|
106
|
+
@snippet_store_path = default_snippet_store_path
|
|
91
107
|
|
|
92
108
|
# Default color scheme
|
|
93
109
|
@color_scheme = :dark
|
|
@@ -138,6 +154,11 @@ module RailsConsolePro
|
|
|
138
154
|
# Default stats calculation settings
|
|
139
155
|
@stats_large_table_threshold = 10_000 # Consider table large if it has 10k+ records
|
|
140
156
|
@stats_skip_distinct_threshold = 10_000 # Skip distinct count for tables with 10k+ records
|
|
157
|
+
|
|
158
|
+
# Default profiling settings
|
|
159
|
+
@profile_slow_query_threshold = 100.0 # milliseconds
|
|
160
|
+
@profile_duplicate_query_threshold = 2
|
|
161
|
+
@profile_max_saved_queries = 10
|
|
141
162
|
end
|
|
142
163
|
|
|
143
164
|
# Set color scheme (dark, light, or custom)
|
|
@@ -190,6 +211,7 @@ module RailsConsolePro
|
|
|
190
211
|
@navigate_command_enabled = false
|
|
191
212
|
@stats_command_enabled = false
|
|
192
213
|
@diff_command_enabled = false
|
|
214
|
+
@queue_command_enabled = false
|
|
193
215
|
@active_record_printer_enabled = false
|
|
194
216
|
@relation_printer_enabled = false
|
|
195
217
|
@collection_printer_enabled = false
|
|
@@ -204,6 +226,7 @@ module RailsConsolePro
|
|
|
204
226
|
@navigate_command_enabled = true
|
|
205
227
|
@stats_command_enabled = true
|
|
206
228
|
@diff_command_enabled = true
|
|
229
|
+
@queue_command_enabled = true
|
|
207
230
|
@active_record_printer_enabled = true
|
|
208
231
|
@relation_printer_enabled = true
|
|
209
232
|
@collection_printer_enabled = true
|
|
@@ -214,6 +237,22 @@ module RailsConsolePro
|
|
|
214
237
|
def reset
|
|
215
238
|
initialize
|
|
216
239
|
end
|
|
240
|
+
|
|
241
|
+
private
|
|
242
|
+
|
|
243
|
+
def default_snippet_store_path
|
|
244
|
+
base_path =
|
|
245
|
+
if defined?(Rails) && Rails.respond_to?(:root) && Rails.root
|
|
246
|
+
Rails.root.join('tmp', 'rails_console_pro')
|
|
247
|
+
else
|
|
248
|
+
File.expand_path(File.join(Dir.respond_to?(:pwd) ? Dir.pwd : Dir.tmpdir, 'tmp', 'rails_console_pro'))
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
base_path = Pathname.new(base_path) unless base_path.is_a?(Pathname)
|
|
252
|
+
(base_path + 'snippets.yml').to_s
|
|
253
|
+
rescue StandardError
|
|
254
|
+
File.join(Dir.tmpdir, 'rails_console_pro', 'snippets.yml')
|
|
255
|
+
end
|
|
217
256
|
end
|
|
218
257
|
end
|
|
219
258
|
|
|
@@ -81,6 +81,8 @@ module RailsConsolePro
|
|
|
81
81
|
serialize_diff_result(data)
|
|
82
82
|
when ExplainResult
|
|
83
83
|
serialize_explain_result(data)
|
|
84
|
+
when ProfileResult
|
|
85
|
+
serialize_profile_result(data)
|
|
84
86
|
when ActiveRecord::Base
|
|
85
87
|
serialize_active_record(data)
|
|
86
88
|
when ActiveRecord::Relation
|
|
@@ -131,6 +133,10 @@ module RailsConsolePro
|
|
|
131
133
|
Serializers::DiffSerializer.serialize(result, self)
|
|
132
134
|
end
|
|
133
135
|
|
|
136
|
+
def serialize_profile_result(result)
|
|
137
|
+
Serializers::ProfileSerializer.serialize(result, self)
|
|
138
|
+
end
|
|
139
|
+
|
|
134
140
|
def serialize_active_record(record)
|
|
135
141
|
Serializers::ActiveRecordSerializer.serialize(record, self)
|
|
136
142
|
end
|
|
@@ -388,6 +394,8 @@ module RailsConsolePro
|
|
|
388
394
|
"Diff Comparison: #{data.object1_type} vs #{data.object2_type}"
|
|
389
395
|
when ExplainResult
|
|
390
396
|
"SQL Explain Analysis"
|
|
397
|
+
when ProfileResult
|
|
398
|
+
"Profile: #{data.label || 'Session'}"
|
|
391
399
|
when ActiveRecord::Base
|
|
392
400
|
"#{data.class.name} ##{data.id}"
|
|
393
401
|
when ActiveRecord::Relation
|
|
@@ -40,3 +40,15 @@ def diff(object1, object2)
|
|
|
40
40
|
RailsConsolePro::Commands.diff(object1, object2)
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
+
def profile(target = nil, *args, **kwargs, &block)
|
|
44
|
+
RailsConsolePro::Commands.profile(target, *args, **kwargs, &block)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def jobs(options = {})
|
|
48
|
+
RailsConsolePro::Commands.jobs(options)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def snippets(action = :list, *args, **kwargs, &block)
|
|
52
|
+
RailsConsolePro::Commands.snippets(action, *args, **kwargs, &block)
|
|
53
|
+
end
|
|
54
|
+
|
|
@@ -31,11 +31,14 @@ module RailsConsolePro
|
|
|
31
31
|
autoload :ExplainResult, "rails_console_pro/explain_result"
|
|
32
32
|
autoload :StatsResult, "rails_console_pro/stats_result"
|
|
33
33
|
autoload :DiffResult, "rails_console_pro/diff_result"
|
|
34
|
+
autoload :ProfileResult, "rails_console_pro/profile_result"
|
|
35
|
+
autoload :QueueInsightsResult, "rails_console_pro/queue_insights_result"
|
|
34
36
|
autoload :AssociationNavigator, "rails_console_pro/association_navigator"
|
|
35
37
|
autoload :Commands, "rails_console_pro/commands"
|
|
36
38
|
autoload :FormatExporter, "rails_console_pro/format_exporter"
|
|
37
39
|
autoload :ErrorHandler, "rails_console_pro/error_handler"
|
|
38
40
|
autoload :Paginator, "rails_console_pro/paginator"
|
|
41
|
+
autoload :Snippets, "rails_console_pro/snippets"
|
|
39
42
|
|
|
40
43
|
module Printers
|
|
41
44
|
autoload :ActiveRecordPrinter, "rails_console_pro/printers/active_record_printer"
|
|
@@ -45,6 +48,10 @@ module RailsConsolePro
|
|
|
45
48
|
autoload :ExplainPrinter, "rails_console_pro/printers/explain_printer"
|
|
46
49
|
autoload :StatsPrinter, "rails_console_pro/printers/stats_printer"
|
|
47
50
|
autoload :DiffPrinter, "rails_console_pro/printers/diff_printer"
|
|
51
|
+
autoload :ProfilePrinter, "rails_console_pro/printers/profile_printer"
|
|
52
|
+
autoload :SnippetCollectionPrinter, "rails_console_pro/printers/snippet_collection_printer"
|
|
53
|
+
autoload :SnippetPrinter, "rails_console_pro/printers/snippet_printer"
|
|
54
|
+
autoload :QueueInsightsPrinter, "rails_console_pro/printers/queue_insights_printer"
|
|
48
55
|
end
|
|
49
56
|
|
|
50
57
|
# Main dispatcher - optimized with early returns
|
|
@@ -88,6 +95,14 @@ module RailsConsolePro
|
|
|
88
95
|
return Printers::ExplainPrinter if value.is_a?(ExplainResult)
|
|
89
96
|
return Printers::StatsPrinter if value.is_a?(StatsResult)
|
|
90
97
|
return Printers::DiffPrinter if value.is_a?(DiffResult)
|
|
98
|
+
return Printers::ProfilePrinter if value.is_a?(ProfileResult)
|
|
99
|
+
return Printers::QueueInsightsPrinter if value.is_a?(QueueInsightsResult)
|
|
100
|
+
if defined?(Snippets::CollectionResult) && value.is_a?(Snippets::CollectionResult)
|
|
101
|
+
return Printers::SnippetCollectionPrinter
|
|
102
|
+
end
|
|
103
|
+
if defined?(Snippets::SingleResult) && value.is_a?(Snippets::SingleResult)
|
|
104
|
+
return Printers::SnippetPrinter
|
|
105
|
+
end
|
|
91
106
|
|
|
92
107
|
# Fallback to base printer
|
|
93
108
|
BasePrinter
|
|
@@ -139,6 +154,10 @@ require_relative 'services/stats_calculator'
|
|
|
139
154
|
require_relative 'services/table_size_calculator'
|
|
140
155
|
require_relative 'services/index_analyzer'
|
|
141
156
|
require_relative 'services/column_stats_calculator'
|
|
157
|
+
require_relative 'services/profile_collector'
|
|
158
|
+
require_relative 'services/snippet_repository'
|
|
159
|
+
require_relative 'services/queue_action_service'
|
|
160
|
+
require_relative 'services/queue_insight_fetcher'
|
|
142
161
|
|
|
143
162
|
# Load command classes (needed by Commands module)
|
|
144
163
|
require_relative 'commands/base_command'
|
|
@@ -147,6 +166,9 @@ require_relative 'commands/explain_command'
|
|
|
147
166
|
require_relative 'commands/stats_command'
|
|
148
167
|
require_relative 'commands/diff_command'
|
|
149
168
|
require_relative 'commands/export_command'
|
|
169
|
+
require_relative 'commands/snippets_command'
|
|
170
|
+
require_relative 'commands/profile_command'
|
|
171
|
+
require_relative 'commands/jobs_command'
|
|
150
172
|
|
|
151
173
|
# Load Commands module (uses command classes)
|
|
152
174
|
require_relative 'commands'
|
|
@@ -157,6 +179,7 @@ require_relative 'serializers/schema_serializer'
|
|
|
157
179
|
require_relative 'serializers/stats_serializer'
|
|
158
180
|
require_relative 'serializers/explain_serializer'
|
|
159
181
|
require_relative 'serializers/diff_serializer'
|
|
182
|
+
require_relative 'serializers/profile_serializer'
|
|
160
183
|
require_relative 'serializers/active_record_serializer'
|
|
161
184
|
require_relative 'serializers/relation_serializer'
|
|
162
185
|
require_relative 'serializers/array_serializer'
|
|
@@ -27,7 +27,7 @@ module RailsConsolePro
|
|
|
27
27
|
# Check if model is abstract
|
|
28
28
|
def abstract_class?(model_class)
|
|
29
29
|
return false unless valid_model?(model_class)
|
|
30
|
-
model_class.abstract_class?
|
|
30
|
+
!!model_class.abstract_class?
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
# Check if model uses Single Table Inheritance (STI)
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsConsolePro
|
|
4
|
+
module Printers
|
|
5
|
+
# Printer for profile results
|
|
6
|
+
class ProfilePrinter < BasePrinter
|
|
7
|
+
def print
|
|
8
|
+
print_header
|
|
9
|
+
print_summary
|
|
10
|
+
print_sql_breakdown
|
|
11
|
+
print_cache_breakdown
|
|
12
|
+
print_instantiation_info
|
|
13
|
+
print_slow_queries
|
|
14
|
+
print_duplicate_queries
|
|
15
|
+
print_query_samples
|
|
16
|
+
print_result_preview
|
|
17
|
+
print_error_info
|
|
18
|
+
print_footer
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def print_header
|
|
24
|
+
label = value.label || 'Profiling Session'
|
|
25
|
+
header_color = config.get_color(:header)
|
|
26
|
+
border_length = config.header_width
|
|
27
|
+
|
|
28
|
+
output.puts bold_color(header_color, '═' * border_length)
|
|
29
|
+
output.puts bold_color(header_color, "🧪 PROFILE: #{label}")
|
|
30
|
+
output.puts bold_color(header_color, '═' * border_length)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def print_summary
|
|
34
|
+
output.puts bold_color(config.get_color(:warning), "\n⏱ Execution Summary:")
|
|
35
|
+
output.puts " #{summary_line('Total time', format_ms(value.duration_ms), config.get_color(:success))}"
|
|
36
|
+
output.puts " #{summary_line('SQL time', format_ms(value.total_sql_duration_ms), config.get_color(:attribute_value_numeric))}"
|
|
37
|
+
sql_ratio = ratio(value.total_sql_duration_ms, value.duration_ms)
|
|
38
|
+
output.puts color(config.get_color(:info), " (#{sql_ratio}% of total time spent in SQL)") if sql_ratio
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def print_sql_breakdown
|
|
42
|
+
output.puts bold_color(config.get_color(:warning), "\n🗂 Query Breakdown:")
|
|
43
|
+
output.puts " #{summary_line('Total queries', value.query_count, config.get_color(:attribute_value_numeric))}"
|
|
44
|
+
output.puts " #{summary_line('Read queries', value.read_query_count, config.get_color(:attribute_value_string))}"
|
|
45
|
+
output.puts " #{summary_line('Write queries', value.write_query_count, config.get_color(:attribute_value_numeric))}"
|
|
46
|
+
output.puts " #{summary_line('Cached queries', value.cached_query_count, config.get_color(:info))}"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def print_cache_breakdown
|
|
50
|
+
return unless value.cache_activity?
|
|
51
|
+
|
|
52
|
+
output.puts bold_color(config.get_color(:warning), "\n🧮 Cache Activity:")
|
|
53
|
+
output.puts " #{summary_line('Cache hits', value.cache_hits, config.get_color(:success))}"
|
|
54
|
+
output.puts " #{summary_line('Cache misses', value.cache_misses, config.get_color(:error))}"
|
|
55
|
+
output.puts " #{summary_line('Cache writes', value.cache_writes, config.get_color(:attribute_value_numeric))}"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def print_instantiation_info
|
|
59
|
+
return if value.instantiation_count.zero?
|
|
60
|
+
|
|
61
|
+
output.puts bold_color(config.get_color(:warning), "\n📦 Records Instantiated:")
|
|
62
|
+
output.puts " #{summary_line('ActiveRecord objects', value.instantiation_count, config.get_color(:attribute_value_numeric))}"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def print_slow_queries
|
|
66
|
+
return unless value.slow_queries?
|
|
67
|
+
|
|
68
|
+
output.puts bold_color(config.get_color(:warning), "\n🐢 Slow Queries (#{config.profile_slow_query_threshold}ms+):")
|
|
69
|
+
value.slow_queries.each_with_index do |query, index|
|
|
70
|
+
print_query_item(index + 1, query, highlight: config.get_color(:error))
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def print_duplicate_queries
|
|
75
|
+
return unless value.duplicate_queries?
|
|
76
|
+
|
|
77
|
+
output.puts bold_color(config.get_color(:warning), "\n♻️ Possible N+1 Queries:")
|
|
78
|
+
value.duplicate_queries.each_with_index do |duplicate, index|
|
|
79
|
+
sql_preview = truncate_sql(duplicate.sql)
|
|
80
|
+
duration = format_ms(duplicate.total_duration_ms)
|
|
81
|
+
output.puts " #{index + 1}. #{bold_color(config.get_color(:error), "#{duplicate.count}x")} "\
|
|
82
|
+
"#{color(config.get_color(:attribute_value_string), sql_preview)} "\
|
|
83
|
+
"#{color(config.get_color(:info), "(#{duration} total)")} "\
|
|
84
|
+
"#{color(config.get_color(:warning), "[#{duplicate.fingerprint.hash}]")}"
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def print_query_samples
|
|
89
|
+
return unless value.query_samples?
|
|
90
|
+
|
|
91
|
+
output.puts bold_color(config.get_color(:warning), "\n🔍 Query Samples:")
|
|
92
|
+
value.query_samples.each_with_index do |query, index|
|
|
93
|
+
print_query_item(index + 1, query)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def print_result_preview
|
|
98
|
+
preview = format_result_preview(value.result)
|
|
99
|
+
output.puts bold_color(config.get_color(:warning), "\n🧾 Result Preview:")
|
|
100
|
+
output.puts " #{preview}"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def print_error_info
|
|
104
|
+
return unless value.error?
|
|
105
|
+
|
|
106
|
+
error = value.error
|
|
107
|
+
output.puts bold_color(config.get_color(:error), "\n⚠️ Error Raised During Profiling:")
|
|
108
|
+
output.puts " #{color(config.get_color(:error), "#{error.class}: #{error.message}")}"
|
|
109
|
+
backtrace = Array(error.backtrace).first(3)
|
|
110
|
+
backtrace.each do |line|
|
|
111
|
+
output.puts color(config.get_color(:info), " ↳ #{line}")
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def print_footer
|
|
116
|
+
footer_color = config.get_color(:footer)
|
|
117
|
+
output.puts bold_color(footer_color, "\n" + '═' * config.header_width)
|
|
118
|
+
if value.started_at && value.finished_at
|
|
119
|
+
output.puts color(:dim, "Started: #{value.started_at.strftime('%Y-%m-%d %H:%M:%S')}")
|
|
120
|
+
output.puts color(:dim, "Finished: #{value.finished_at.strftime('%Y-%m-%d %H:%M:%S')}")
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def summary_line(label, value, color_value)
|
|
125
|
+
"#{label.ljust(18)} #{bold_color(color_value, value.to_s)}"
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def format_ms(duration_ms)
|
|
129
|
+
return '0.00 ms' if duration_ms.nil?
|
|
130
|
+
|
|
131
|
+
format('%.2f ms', duration_ms)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def ratio(value, total)
|
|
135
|
+
return nil if total.to_f.zero?
|
|
136
|
+
|
|
137
|
+
((value.to_f / total.to_f) * 100).round(2)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def truncate_sql(sql)
|
|
141
|
+
sql.to_s.gsub(/\s+/, ' ').strip.truncate(120)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def print_query_item(index, query, highlight: config.get_color(:attribute_value_numeric))
|
|
145
|
+
duration = format_ms(query.duration_ms)
|
|
146
|
+
cached_label = query.cached ? color(config.get_color(:info), '[cache] ') : ''
|
|
147
|
+
name_label = query.name.to_s.empty? ? '' : color(config.get_color(:attribute_key), "(#{query.name}) ")
|
|
148
|
+
binds = format_binds(query.binds)
|
|
149
|
+
|
|
150
|
+
output.puts " #{index}. #{bold_color(highlight, duration)} #{cached_label}#{name_label}"\
|
|
151
|
+
"#{color(config.get_color(:attribute_value_string), truncate_sql(query.sql))}"
|
|
152
|
+
output.puts " #{color(:dim, binds)}" if binds
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def format_binds(binds)
|
|
156
|
+
return nil if binds.nil? || binds.empty?
|
|
157
|
+
|
|
158
|
+
"binds: #{binds.map { |b| b.nil? ? 'nil' : b.inspect }.join(', ')}"
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def format_result_preview(result)
|
|
162
|
+
case result
|
|
163
|
+
when NilClass
|
|
164
|
+
color(config.get_color(:attribute_value_nil), 'nil')
|
|
165
|
+
when Array
|
|
166
|
+
color(config.get_color(:attribute_value_numeric), "#{result.class.name} (#{result.size} items)")
|
|
167
|
+
when ActiveRecord::Relation
|
|
168
|
+
color(config.get_color(:attribute_value_numeric), "#{result.klass.name} relation (#{result.count} records)")
|
|
169
|
+
when ActiveRecord::Base
|
|
170
|
+
color(config.get_color(:attribute_value_string), "#{result.class.name}##{result.id || 'new'}")
|
|
171
|
+
else
|
|
172
|
+
color(config.get_color(:attribute_value_string), result.inspect.truncate(120))
|
|
173
|
+
end
|
|
174
|
+
rescue => e
|
|
175
|
+
color(config.get_color(:error), "Error previewing result: #{e.message}")
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|