ruby_method_tracer 0.2.0 → 0.3.1
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/CHANGELOG.md +32 -0
- data/README.md +126 -1
- data/lib/ruby_method_tracer/call_tree.rb +167 -0
- data/lib/ruby_method_tracer/enhanced_tracer.rb +137 -0
- data/lib/ruby_method_tracer/formatters/base_formatter.rb +49 -0
- data/lib/ruby_method_tracer/formatters/tree_formatter.rb +164 -0
- data/lib/ruby_method_tracer/simple_tracer.rb +1 -1
- data/lib/ruby_method_tracer/version.rb +1 -1
- data/lib/ruby_method_tracer.rb +4 -0
- metadata +6 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 35c8ff07ff1fff38a22779065d538545ec642a043b723da76b784ee97d9a24c8
|
|
4
|
+
data.tar.gz: a391814be17a97f5b6820355ba21f71eb8b3ffe381589f11ddccc41f6be7c691
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a2b2c43903841a2603c5dbd2032faa11facf0c3654bb8af6b32ab88a85777e095976f4ffbfd9448efd8a9644f5be5fde4e37524ac1e4cf733fbe078b1b93ea73
|
|
7
|
+
data.tar.gz: c0f7126d5a885e1f4943e00651fc51ff97b614f17b86035e6fa81c083a6a74705376762f7d87a006f4e5691cdd68bc900210836a675e5ecd58d4f6e6d47f3b24
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.3.1] - 2025-11-22
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- Fixed file permissions for `call_tree.rb`, `enhanced_tracer.rb`, and formatter files to be world-readable
|
|
7
|
+
- Gem now correctly includes all files when installed (previously missing EnhancedTracer and formatters)
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- Code coverage reporting with SimpleCov (99% line coverage, 84% branch coverage)
|
|
11
|
+
- Codecov integration for CI/CD coverage tracking
|
|
12
|
+
- Comprehensive test suite for BaseFormatter and TreeFormatter (18 new tests)
|
|
13
|
+
- Coverage badge in README
|
|
14
|
+
|
|
15
|
+
## [0.3.0] - 2025-11-19
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- **NEW: Hierarchical Call Tree Visualization** - `EnhancedTracer` class for tracking nested method calls
|
|
19
|
+
- `CallTree` class for managing call hierarchy with parent-child relationships
|
|
20
|
+
- `TreeFormatter` for beautiful tree visualization with proper indentation and tree characters
|
|
21
|
+
- Statistics calculation: slowest methods, most called methods, max call depth
|
|
22
|
+
- `print_tree()` method for outputting formatted call trees
|
|
23
|
+
- `format_tree()` method for programmatic access to tree visualization
|
|
24
|
+
- `fetch_enhanced_results()` for combined flat and hierarchical data
|
|
25
|
+
- Thread-safe call stack management
|
|
26
|
+
- Error tracking in call tree with full error messages
|
|
27
|
+
- Color-coded tree output for better readability
|
|
28
|
+
- 24 new comprehensive tests covering call tree functionality
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
- Main module now auto-loads `EnhancedTracer` and formatting classes
|
|
32
|
+
- README updated with call tree examples and usage guide
|
|
33
|
+
- Documentation expanded with decision guide for choosing between SimpleTracer and EnhancedTracer
|
|
34
|
+
|
|
3
35
|
## [0.2.0] - 2025-11-19
|
|
4
36
|
|
|
5
37
|
### Added
|
data/README.md
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
[](https://github.com/Seunadex/ruby_method_tracer/actions/workflows/main.yml)
|
|
2
|
+
[](https://codecov.io/gh/Seunadex/ruby_method_tracer)
|
|
2
3
|
|
|
3
4
|
# RubyMethodTracer
|
|
4
5
|
|
|
@@ -7,6 +8,7 @@ RubyMethodTracer is a lightweight Ruby mixin for targeted method tracing. It wra
|
|
|
7
8
|
## Highlights
|
|
8
9
|
- Wrap only the methods you care about; public, protected, and private methods are supported.
|
|
9
10
|
- Records duration, success/error state, and timestamps with thread-safe storage.
|
|
11
|
+
- **NEW: Hierarchical call tree visualization** to understand nested method calls and dependencies.
|
|
10
12
|
- Configurable threshold to ignore fast calls and optional log streaming via `Logger`.
|
|
11
13
|
- Zero dependencies beyond the Ruby standard library, keeping overhead minimal.
|
|
12
14
|
|
|
@@ -125,20 +127,143 @@ tracer.trace_method(:expensive_operation)
|
|
|
125
127
|
tracer.clear_results
|
|
126
128
|
```
|
|
127
129
|
|
|
130
|
+
### Example 4: Call Tree Visualization (NEW!)
|
|
128
131
|
|
|
129
|
-
|
|
132
|
+
Visualize hierarchical method call relationships with the `EnhancedTracer`:
|
|
133
|
+
|
|
134
|
+
```ruby
|
|
135
|
+
class OrderProcessor
|
|
136
|
+
def process_order(order)
|
|
137
|
+
validate_order(order)
|
|
138
|
+
charge_payment(order)
|
|
139
|
+
send_confirmation(order)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def validate_order(order)
|
|
143
|
+
check_inventory(order.items)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def charge_payment(order)
|
|
147
|
+
# Payment processing...
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def send_confirmation(order)
|
|
151
|
+
# Email sending...
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
private
|
|
155
|
+
|
|
156
|
+
def check_inventory(items)
|
|
157
|
+
# Inventory check...
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Use EnhancedTracer for call tree tracking
|
|
162
|
+
tracer = RubyMethodTracer::EnhancedTracer.new(OrderProcessor, threshold: 0.0)
|
|
163
|
+
tracer.trace_method(:process_order)
|
|
164
|
+
tracer.trace_method(:validate_order)
|
|
165
|
+
tracer.trace_method(:charge_payment)
|
|
166
|
+
tracer.trace_method(:send_confirmation)
|
|
167
|
+
tracer.trace_method(:check_inventory)
|
|
168
|
+
|
|
169
|
+
processor = OrderProcessor.new
|
|
170
|
+
processor.process_order(order)
|
|
171
|
+
|
|
172
|
+
# Print beautiful call tree visualization
|
|
173
|
+
tracer.print_tree
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
This outputs a hierarchical tree showing nested calls:
|
|
177
|
+
|
|
178
|
+
```
|
|
179
|
+
METHOD CALL TREE
|
|
180
|
+
============================================================
|
|
181
|
+
└── OrderProcessor#process_order (125.3ms)
|
|
182
|
+
├── OrderProcessor#validate_order (15.2ms)
|
|
183
|
+
│ └── OrderProcessor#check_inventory (12.1ms)
|
|
184
|
+
├── OrderProcessor#charge_payment (85.4ms)
|
|
185
|
+
└── OrderProcessor#send_confirmation (24.7ms)
|
|
186
|
+
============================================================
|
|
187
|
+
|
|
188
|
+
STATISTICS
|
|
189
|
+
------------------------------------------------------------
|
|
190
|
+
Total Calls: 5
|
|
191
|
+
Total Time: 250.6ms
|
|
192
|
+
Unique Methods: 5
|
|
193
|
+
Max Depth: 2
|
|
194
|
+
|
|
195
|
+
Slowest Methods (by average time):
|
|
196
|
+
1. OrderProcessor#process_order - 125.3ms
|
|
197
|
+
2. OrderProcessor#charge_payment - 85.4ms
|
|
198
|
+
3. OrderProcessor#send_confirmation - 24.7ms
|
|
199
|
+
4. OrderProcessor#validate_order - 15.2ms
|
|
200
|
+
5. OrderProcessor#check_inventory - 12.1ms
|
|
201
|
+
|
|
202
|
+
Most Called Methods:
|
|
203
|
+
1. OrderProcessor#process_order - 1 calls
|
|
204
|
+
2. OrderProcessor#validate_order - 1 calls
|
|
205
|
+
...
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
**Call Tree Features:**
|
|
209
|
+
- Shows parent-child relationships between methods
|
|
210
|
+
- Visual tree structure with proper indentation
|
|
211
|
+
- Execution times for each method call
|
|
212
|
+
- Statistics summary (slowest methods, most called, max depth)
|
|
213
|
+
- Error indicators with full error messages
|
|
214
|
+
- Color-coded output for better readability
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
### Options (SimpleTracer)
|
|
130
218
|
|
|
131
219
|
- `threshold` (Float, default `0.001`): minimum duration (in seconds) to record.
|
|
132
220
|
- `auto_output` (Boolean, default `false`): emit a log line using `Logger` for each recorded call.
|
|
133
221
|
- `max_calls` (Integer, default `1000`): maximum number of calls to store in memory. When exceeded, the oldest calls are automatically removed to prevent memory leaks.
|
|
134
222
|
- `logger` (Logger, default `Logger.new($stdout)`): custom logger instance for output. Useful for directing logs to files or custom log handlers.
|
|
135
223
|
|
|
224
|
+
### Options (EnhancedTracer)
|
|
225
|
+
|
|
226
|
+
EnhancedTracer supports all SimpleTracer options plus:
|
|
227
|
+
|
|
228
|
+
- `track_hierarchy` (Boolean, default `true`): enable call tree tracking. Set to `false` to use EnhancedTracer like SimpleTracer.
|
|
229
|
+
|
|
230
|
+
### API Methods (EnhancedTracer)
|
|
231
|
+
|
|
232
|
+
- `print_tree(options = {})` - Print formatted call tree to stdout
|
|
233
|
+
- Options: `colorize: true/false`, `show_errors: true/false`
|
|
234
|
+
- `format_tree(options = {})` - Get formatted call tree as string
|
|
235
|
+
- `fetch_enhanced_results` - Get hash with `:flat_calls`, `:call_hierarchy`, and `:statistics`
|
|
236
|
+
- `clear_results` - Clear both flat results and call tree
|
|
237
|
+
|
|
238
|
+
## Choosing Between SimpleTracer and EnhancedTracer
|
|
239
|
+
|
|
240
|
+
**Use SimpleTracer when:**
|
|
241
|
+
- You only need flat timing data
|
|
242
|
+
- You want minimal overhead
|
|
243
|
+
- You're tracing independent methods
|
|
244
|
+
|
|
245
|
+
**Use EnhancedTracer when:**
|
|
246
|
+
- You need to understand call hierarchies
|
|
247
|
+
- You want to visualize nested method calls
|
|
248
|
+
- You're debugging complex call flows
|
|
249
|
+
- You need statistics on method relationships
|
|
250
|
+
|
|
136
251
|
## Development
|
|
137
252
|
|
|
138
253
|
After checking out the repo, run `bin/setup` to install dependencies. Then run `rake spec` to execute the test suite. You can also run `bin/console` for an interactive prompt.
|
|
139
254
|
|
|
140
255
|
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version in `lib/ruby_method_tracer/version.rb`, and then run `bundle exec rake release`.
|
|
141
256
|
|
|
257
|
+
### Code Coverage
|
|
258
|
+
|
|
259
|
+
This project uses [SimpleCov](https://github.com/simplecov-ruby/simplecov) for code coverage analysis. Coverage reports are automatically generated when running tests:
|
|
260
|
+
|
|
261
|
+
```bash
|
|
262
|
+
bundle exec rspec
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
After running the tests, open `coverage/index.html` in your browser to view the detailed coverage report. The project maintains a minimum coverage threshold of 95% line coverage and 80% branch coverage.
|
|
266
|
+
|
|
142
267
|
## Contributing
|
|
143
268
|
|
|
144
269
|
Bug reports and pull requests are welcome on GitHub at https://github.com/Seunadex/ruby_method_tracer. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/Seunadex/ruby_method_tracer/blob/main/CODE_OF_CONDUCT.md).
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyMethodTracer
|
|
4
|
+
# CallTree manages the hierarchical structure of method calls,
|
|
5
|
+
# tracking parent-child relationships and call depths.
|
|
6
|
+
#
|
|
7
|
+
# It uses a stack-based approach to manage nested calls and builds
|
|
8
|
+
# a tree structure showing the complete call hierarchy.
|
|
9
|
+
class CallTree
|
|
10
|
+
attr_reader :calls, :root_calls
|
|
11
|
+
|
|
12
|
+
def initialize
|
|
13
|
+
@calls = [] # All recorded calls (flat list)
|
|
14
|
+
@call_stack = [] # Current execution stack
|
|
15
|
+
@root_calls = [] # Top-level calls (depth 0)
|
|
16
|
+
@lock = Mutex.new # Thread safety
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Start tracking a method call
|
|
20
|
+
#
|
|
21
|
+
# @param method_name [String] The name of the method being called
|
|
22
|
+
# @return [Hash] The call record that was pushed to the stack
|
|
23
|
+
def start_call(method_name) # rubocop:disable Metrics/MethodLength
|
|
24
|
+
@lock.synchronize do
|
|
25
|
+
call_record = {
|
|
26
|
+
method_name: method_name,
|
|
27
|
+
start_time: monotonic_time,
|
|
28
|
+
depth: @call_stack.size,
|
|
29
|
+
parent: @call_stack.last,
|
|
30
|
+
children: [],
|
|
31
|
+
status: nil,
|
|
32
|
+
error: nil,
|
|
33
|
+
execution_time: nil,
|
|
34
|
+
timestamp: Time.now
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
# Add as child to parent if we're nested
|
|
38
|
+
@call_stack.last[:children] << call_record if @call_stack.any?
|
|
39
|
+
|
|
40
|
+
# Track root-level calls
|
|
41
|
+
@root_calls << call_record if @call_stack.empty?
|
|
42
|
+
|
|
43
|
+
@call_stack.push(call_record)
|
|
44
|
+
call_record
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# End tracking a method call
|
|
49
|
+
#
|
|
50
|
+
# @param status [Symbol] :success or :error
|
|
51
|
+
# @param error [Exception, nil] The exception if status is :error
|
|
52
|
+
# @return [Hash, nil] The completed call record
|
|
53
|
+
def end_call(status = :success, error = nil)
|
|
54
|
+
@lock.synchronize do
|
|
55
|
+
return nil if @call_stack.empty?
|
|
56
|
+
|
|
57
|
+
call_record = @call_stack.pop
|
|
58
|
+
call_record[:status] = status
|
|
59
|
+
call_record[:error] = error
|
|
60
|
+
call_record[:execution_time] = monotonic_time - call_record[:start_time]
|
|
61
|
+
|
|
62
|
+
@calls << call_record
|
|
63
|
+
call_record
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Get the current call depth
|
|
68
|
+
#
|
|
69
|
+
# @return [Integer] The current nesting level
|
|
70
|
+
def current_depth
|
|
71
|
+
@lock.synchronize { @call_stack.size }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Get call hierarchy as nested structure
|
|
75
|
+
#
|
|
76
|
+
# @return [Array<Hash>] Root calls with nested children
|
|
77
|
+
def call_hierarchy
|
|
78
|
+
@lock.synchronize { @root_calls.dup }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Calculate statistics from recorded calls
|
|
82
|
+
#
|
|
83
|
+
# @return [Hash] Statistics including total calls, time, slowest methods, etc.
|
|
84
|
+
def statistics
|
|
85
|
+
@lock.synchronize do
|
|
86
|
+
return default_statistics if @calls.empty?
|
|
87
|
+
|
|
88
|
+
method_stats = calculate_method_stats
|
|
89
|
+
|
|
90
|
+
{
|
|
91
|
+
total_calls: @calls.size,
|
|
92
|
+
total_time: @calls.sum { |c| c[:execution_time] },
|
|
93
|
+
unique_methods: method_stats.size,
|
|
94
|
+
slowest_methods: slowest_methods(method_stats),
|
|
95
|
+
most_called_methods: most_called_methods(method_stats),
|
|
96
|
+
average_time_per_method: average_times(method_stats),
|
|
97
|
+
max_depth: @calls.map { |c| c[:depth] }.max || 0
|
|
98
|
+
}
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Clear all recorded calls and reset state
|
|
103
|
+
def clear
|
|
104
|
+
@lock.synchronize do
|
|
105
|
+
@calls.clear
|
|
106
|
+
@call_stack.clear
|
|
107
|
+
@root_calls.clear
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Check if call stack is empty (no active calls)
|
|
112
|
+
#
|
|
113
|
+
# @return [Boolean]
|
|
114
|
+
def empty?
|
|
115
|
+
@lock.synchronize { @call_stack.empty? }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
private
|
|
119
|
+
|
|
120
|
+
def monotonic_time
|
|
121
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def default_statistics
|
|
125
|
+
{
|
|
126
|
+
total_calls: 0,
|
|
127
|
+
total_time: 0.0,
|
|
128
|
+
unique_methods: 0,
|
|
129
|
+
slowest_methods: [],
|
|
130
|
+
most_called_methods: [],
|
|
131
|
+
average_time_per_method: {},
|
|
132
|
+
max_depth: 0
|
|
133
|
+
}
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def calculate_method_stats
|
|
137
|
+
method_stats = Hash.new { |h, k| h[k] = { calls: 0, total_time: 0.0, times: [] } }
|
|
138
|
+
|
|
139
|
+
@calls.each do |call|
|
|
140
|
+
stats = method_stats[call[:method_name]]
|
|
141
|
+
stats[:calls] += 1
|
|
142
|
+
stats[:total_time] += call[:execution_time]
|
|
143
|
+
stats[:times] << call[:execution_time]
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
method_stats
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def slowest_methods(method_stats)
|
|
150
|
+
method_stats
|
|
151
|
+
.map { |name, stats| { method: name, avg_time: stats[:total_time] / stats[:calls] } }
|
|
152
|
+
.sort_by { |m| -m[:avg_time] }
|
|
153
|
+
.take(10)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def most_called_methods(method_stats)
|
|
157
|
+
method_stats
|
|
158
|
+
.map { |name, stats| { method: name, count: stats[:calls] } }
|
|
159
|
+
.sort_by { |m| -m[:count] }
|
|
160
|
+
.take(10)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def average_times(method_stats)
|
|
164
|
+
method_stats.transform_values { |stats| stats[:total_time] / stats[:calls] }
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "simple_tracer"
|
|
4
|
+
require_relative "call_tree"
|
|
5
|
+
require_relative "formatters/tree_formatter"
|
|
6
|
+
|
|
7
|
+
module RubyMethodTracer
|
|
8
|
+
# EnhancedTracer extends SimpleTracer with hierarchical call tracking
|
|
9
|
+
#
|
|
10
|
+
# In addition to the basic tracing functionality, this tracer maintains
|
|
11
|
+
# a call tree that captures parent-child relationships between method calls,
|
|
12
|
+
# enabling visualization of complex call hierarchies.
|
|
13
|
+
#
|
|
14
|
+
# Options:
|
|
15
|
+
# - All options from SimpleTracer
|
|
16
|
+
# - :track_hierarchy (Boolean): Enable call tree tracking; defaults to true
|
|
17
|
+
#
|
|
18
|
+
# Usage:
|
|
19
|
+
# tracer = RubyMethodTracer::EnhancedTracer.new(MyClass, threshold: 0.005)
|
|
20
|
+
# tracer.trace_method(:expensive_call)
|
|
21
|
+
# tracer.print_tree
|
|
22
|
+
class EnhancedTracer < SimpleTracer
|
|
23
|
+
attr_reader :call_tree
|
|
24
|
+
|
|
25
|
+
def initialize(target_class, **options)
|
|
26
|
+
super
|
|
27
|
+
@call_tree = CallTree.new
|
|
28
|
+
@track_hierarchy = @options.fetch(:track_hierarchy, true)
|
|
29
|
+
@formatter = Formatters::TreeFormatter.new
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def trace_method(name)
|
|
33
|
+
method_name = name.to_sym
|
|
34
|
+
visibility = method_visibility(method_name)
|
|
35
|
+
return unless visibility
|
|
36
|
+
return unless mark_wrapped?(method_name)
|
|
37
|
+
|
|
38
|
+
aliased = alias_for(method_name)
|
|
39
|
+
@target_class.send(:alias_method, aliased, method_name)
|
|
40
|
+
|
|
41
|
+
tracer = self
|
|
42
|
+
key = :__ruby_method_tracer_in_trace
|
|
43
|
+
|
|
44
|
+
# Build wrapper that tracks hierarchy
|
|
45
|
+
@target_class.define_method(method_name, &build_enhanced_wrapper(aliased, method_name, key, tracer))
|
|
46
|
+
|
|
47
|
+
@target_class.send(visibility, method_name)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Print the call tree visualization
|
|
51
|
+
#
|
|
52
|
+
# @param options [Hash] Formatting options
|
|
53
|
+
# @option options [Boolean] :show_errors (true) Include error information
|
|
54
|
+
# @option options [Boolean] :colorize (true) Apply colors to output
|
|
55
|
+
def print_tree(options = {})
|
|
56
|
+
puts @formatter.format(@call_tree, options)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Get call tree as string without printing
|
|
60
|
+
#
|
|
61
|
+
# @param options [Hash] Formatting options
|
|
62
|
+
# @return [String] Formatted call tree
|
|
63
|
+
def format_tree(options = {})
|
|
64
|
+
@formatter.format(@call_tree, options)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Get enhanced results including both flat list and hierarchy
|
|
68
|
+
#
|
|
69
|
+
# @return [Hash] Results with call tree and statistics
|
|
70
|
+
def fetch_enhanced_results
|
|
71
|
+
{
|
|
72
|
+
flat_calls: fetch_results,
|
|
73
|
+
call_hierarchy: @call_tree.call_hierarchy,
|
|
74
|
+
statistics: @call_tree.statistics
|
|
75
|
+
}
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Clear both simple tracer results and call tree
|
|
79
|
+
def clear_results
|
|
80
|
+
super
|
|
81
|
+
@call_tree.clear
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
87
|
+
def build_enhanced_wrapper(aliased, method_name, key, tracer)
|
|
88
|
+
track_hierarchy = tracer.instance_variable_get(:@track_hierarchy)
|
|
89
|
+
# Use method-specific key to prevent only SELF-recursion, not all nested calls
|
|
90
|
+
method_key = :"#{key}_#{method_name}"
|
|
91
|
+
|
|
92
|
+
proc do |*args, **kwargs, &block|
|
|
93
|
+
if track_hierarchy
|
|
94
|
+
# Prevent only recursive calls to the SAME method
|
|
95
|
+
return __send__(aliased, *args, **kwargs, &block) if Thread.current[method_key]
|
|
96
|
+
|
|
97
|
+
Thread.current[method_key] = true
|
|
98
|
+
full_method_name = "#{tracer.instance_variable_get(:@target_class)}##{method_name}"
|
|
99
|
+
|
|
100
|
+
# Start tracking in call tree
|
|
101
|
+
tracer.call_tree.start_call(full_method_name)
|
|
102
|
+
|
|
103
|
+
start = tracer.__send__(:monotonic_time)
|
|
104
|
+
begin
|
|
105
|
+
result = __send__(aliased, *args, **kwargs, &block)
|
|
106
|
+
execution_time = tracer.__send__(:monotonic_time) - start
|
|
107
|
+
|
|
108
|
+
# Record in both places
|
|
109
|
+
tracer.__send__(:record_call, method_name, execution_time, :success)
|
|
110
|
+
tracer.call_tree.end_call(:success)
|
|
111
|
+
|
|
112
|
+
result
|
|
113
|
+
rescue StandardError => e
|
|
114
|
+
execution_time = tracer.__send__(:monotonic_time) - start
|
|
115
|
+
|
|
116
|
+
# Record in both places
|
|
117
|
+
tracer.__send__(:record_call, method_name, execution_time, :error, e)
|
|
118
|
+
tracer.call_tree.end_call(:error, e)
|
|
119
|
+
|
|
120
|
+
raise
|
|
121
|
+
ensure
|
|
122
|
+
Thread.current[method_key] = false
|
|
123
|
+
end
|
|
124
|
+
else
|
|
125
|
+
tracer.__send__(:wrap_call, method_name, key) do
|
|
126
|
+
__send__(aliased, *args, **kwargs, &block)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
132
|
+
|
|
133
|
+
def default_options
|
|
134
|
+
super.merge(track_hierarchy: true)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyMethodTracer
|
|
4
|
+
module Formatters
|
|
5
|
+
# Base class for formatting trace output
|
|
6
|
+
class BaseFormatter
|
|
7
|
+
# Format timing value into human-readable string
|
|
8
|
+
#
|
|
9
|
+
# @param seconds [Float] Time in seconds
|
|
10
|
+
# @return [String] Formatted time string
|
|
11
|
+
def format_time(seconds)
|
|
12
|
+
if seconds >= 1.0
|
|
13
|
+
"#{seconds.round(3)}s"
|
|
14
|
+
elsif seconds >= 0.001
|
|
15
|
+
"#{(seconds * 1000).round(1)}ms"
|
|
16
|
+
else
|
|
17
|
+
"#{(seconds * 1_000_000).round(0)}µs"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Apply color to text using ANSI escape codes
|
|
22
|
+
#
|
|
23
|
+
# @param text [String] Text to colorize
|
|
24
|
+
# @param color [Symbol] Color name
|
|
25
|
+
# @return [String] Colorized text
|
|
26
|
+
def colorize(text, color)
|
|
27
|
+
colors = {
|
|
28
|
+
red: "31",
|
|
29
|
+
green: "32",
|
|
30
|
+
yellow: "33",
|
|
31
|
+
blue: "34",
|
|
32
|
+
magenta: "35",
|
|
33
|
+
cyan: "36",
|
|
34
|
+
white: "37",
|
|
35
|
+
reset: "0"
|
|
36
|
+
}
|
|
37
|
+
"\e[#{colors[color]}m#{text}\e[#{colors[:reset]}m"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Abstract method to be implemented by subclasses
|
|
41
|
+
#
|
|
42
|
+
# @param _data [Object] Data to format
|
|
43
|
+
# @raise [NotImplementedError] Must be implemented by subclass
|
|
44
|
+
def format(_data)
|
|
45
|
+
raise NotImplementedError, "#{self.class} must implement #format"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base_formatter"
|
|
4
|
+
|
|
5
|
+
module RubyMethodTracer
|
|
6
|
+
module Formatters
|
|
7
|
+
# TreeFormatter generates hierarchical tree visualizations of method calls
|
|
8
|
+
class TreeFormatter < BaseFormatter
|
|
9
|
+
# Format call tree into hierarchical string representation
|
|
10
|
+
#
|
|
11
|
+
# @param call_tree [CallTree] The call tree to format
|
|
12
|
+
# @param options [Hash] Formatting options
|
|
13
|
+
# @option options [Boolean] :show_errors (true) Include error information
|
|
14
|
+
# @option options [Boolean] :colorize (true) Apply colors to output
|
|
15
|
+
# @return [String] Formatted tree visualization
|
|
16
|
+
# rubocop:disable Metrics/AbcSize
|
|
17
|
+
def format(call_tree, options = {})
|
|
18
|
+
opts = default_options.merge(options)
|
|
19
|
+
root_calls = call_tree.call_hierarchy
|
|
20
|
+
|
|
21
|
+
return "No method calls recorded.\n" if root_calls.empty?
|
|
22
|
+
|
|
23
|
+
output = []
|
|
24
|
+
output << header
|
|
25
|
+
output << separator
|
|
26
|
+
|
|
27
|
+
root_calls.each_with_index do |call, index|
|
|
28
|
+
is_last_root = index == root_calls.size - 1
|
|
29
|
+
output << format_call_node(call, "", is_last_root, opts)
|
|
30
|
+
output << "" unless is_last_root # Blank line between root calls
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
output << separator
|
|
34
|
+
output << format_statistics(call_tree.statistics, opts)
|
|
35
|
+
|
|
36
|
+
output.join("\n")
|
|
37
|
+
end
|
|
38
|
+
# rubocop:enable Metrics/AbcSize
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def default_options
|
|
43
|
+
{
|
|
44
|
+
show_errors: true,
|
|
45
|
+
colorize: true
|
|
46
|
+
}
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def header
|
|
50
|
+
"METHOD CALL TREE"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def separator
|
|
54
|
+
"=" * 60
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Recursively format a call node and its children
|
|
58
|
+
#
|
|
59
|
+
# @param call [Hash] Call record
|
|
60
|
+
# @param prefix [String] Current line prefix for indentation
|
|
61
|
+
# @param is_last [Boolean] Whether this is the last sibling
|
|
62
|
+
# @param opts [Hash] Formatting options
|
|
63
|
+
# @return [String] Formatted call tree section
|
|
64
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
|
65
|
+
def format_call_node(call, prefix, is_last, opts)
|
|
66
|
+
lines = []
|
|
67
|
+
|
|
68
|
+
# Format the current call
|
|
69
|
+
lines << format_call_line(call, prefix, is_last, opts)
|
|
70
|
+
|
|
71
|
+
# Format error details if present
|
|
72
|
+
if call[:status] == :error && call[:error] && opts[:show_errors]
|
|
73
|
+
error_prefix = prefix + (is_last ? " " : "│ ")
|
|
74
|
+
lines << format_error_line(call[:error], error_prefix, opts)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Format children
|
|
78
|
+
unless call[:children].empty?
|
|
79
|
+
child_prefix = prefix + (is_last ? " " : "│ ")
|
|
80
|
+
call[:children].each_with_index do |child, index|
|
|
81
|
+
is_last_child = index == call[:children].size - 1
|
|
82
|
+
lines << format_call_node(child, child_prefix, is_last_child, opts)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
lines.join("\n")
|
|
87
|
+
end
|
|
88
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
|
89
|
+
|
|
90
|
+
# Format a single call line
|
|
91
|
+
#
|
|
92
|
+
# @param call [Hash] Call record
|
|
93
|
+
# @param prefix [String] Current line prefix
|
|
94
|
+
# @param is_last [Boolean] Whether this is the last sibling
|
|
95
|
+
# @param opts [Hash] Formatting options
|
|
96
|
+
# @return [String] Formatted call line
|
|
97
|
+
def format_call_line(call, prefix, is_last, opts)
|
|
98
|
+
connector = is_last ? "└── " : "├── "
|
|
99
|
+
tree_part = prefix + connector
|
|
100
|
+
|
|
101
|
+
method_name = call[:method_name]
|
|
102
|
+
time_str = format_time(call[:execution_time])
|
|
103
|
+
status_indicator = call[:status] == :error ? " [ERROR]" : ""
|
|
104
|
+
|
|
105
|
+
if opts[:colorize]
|
|
106
|
+
method_name = colorize(method_name, :cyan)
|
|
107
|
+
time_str = colorize("(#{time_str})", :yellow)
|
|
108
|
+
status_indicator = colorize(status_indicator, :red) unless status_indicator.empty?
|
|
109
|
+
else
|
|
110
|
+
time_str = "(#{time_str})"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
"#{tree_part}#{method_name} #{time_str}#{status_indicator}"
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Format error information
|
|
117
|
+
#
|
|
118
|
+
# @param error [Exception] The error object
|
|
119
|
+
# @param prefix [String] Current line prefix
|
|
120
|
+
# @param opts [Hash] Formatting options
|
|
121
|
+
# @return [String] Formatted error line
|
|
122
|
+
def format_error_line(error, prefix, opts)
|
|
123
|
+
error_msg = "└─ Error: #{error.class}: #{error.message}"
|
|
124
|
+
error_msg = colorize(error_msg, :red) if opts[:colorize]
|
|
125
|
+
"#{prefix}#{error_msg}"
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Format statistics summary
|
|
129
|
+
#
|
|
130
|
+
# @param stats [Hash] Statistics hash from CallTree
|
|
131
|
+
# @param opts [Hash] Formatting options
|
|
132
|
+
# @return [String] Formatted statistics
|
|
133
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
134
|
+
def format_statistics(stats, _opts)
|
|
135
|
+
lines = []
|
|
136
|
+
lines << "\nSTATISTICS"
|
|
137
|
+
lines << ("-" * 60)
|
|
138
|
+
|
|
139
|
+
lines << "Total Calls: #{stats[:total_calls]}"
|
|
140
|
+
lines << "Total Time: #{format_time(stats[:total_time])}"
|
|
141
|
+
lines << "Unique Methods: #{stats[:unique_methods]}"
|
|
142
|
+
lines << "Max Depth: #{stats[:max_depth]}"
|
|
143
|
+
|
|
144
|
+
unless stats[:slowest_methods].empty?
|
|
145
|
+
lines << "\nSlowest Methods (by average time):"
|
|
146
|
+
stats[:slowest_methods].take(5).each_with_index do |method, index|
|
|
147
|
+
time_str = format_time(method[:avg_time])
|
|
148
|
+
lines << " #{index + 1}. #{method[:method]} - #{time_str}"
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
unless stats[:most_called_methods].empty?
|
|
153
|
+
lines << "\nMost Called Methods:"
|
|
154
|
+
stats[:most_called_methods].take(5).each_with_index do |method, index|
|
|
155
|
+
lines << " #{index + 1}. #{method[:method]} - #{method[:count]} calls"
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
lines.join("\n")
|
|
160
|
+
end
|
|
161
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
data/lib/ruby_method_tracer.rb
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "ruby_method_tracer/version"
|
|
4
4
|
require_relative "ruby_method_tracer/simple_tracer"
|
|
5
|
+
require_relative "ruby_method_tracer/call_tree"
|
|
6
|
+
require_relative "ruby_method_tracer/enhanced_tracer"
|
|
7
|
+
require_relative "ruby_method_tracer/formatters/base_formatter"
|
|
8
|
+
require_relative "ruby_method_tracer/formatters/tree_formatter"
|
|
5
9
|
|
|
6
10
|
# Public: Mixin that adds lightweight method tracing to classes.
|
|
7
11
|
#
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ruby_method_tracer
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Seun Adekunle
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2025-11-
|
|
11
|
+
date: 2025-11-22 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: A developer-friendly gem for tracing method calls, execution times, with
|
|
14
14
|
minimal overhead.
|
|
@@ -26,6 +26,10 @@ files:
|
|
|
26
26
|
- README.md
|
|
27
27
|
- Rakefile
|
|
28
28
|
- lib/ruby_method_tracer.rb
|
|
29
|
+
- lib/ruby_method_tracer/call_tree.rb
|
|
30
|
+
- lib/ruby_method_tracer/enhanced_tracer.rb
|
|
31
|
+
- lib/ruby_method_tracer/formatters/base_formatter.rb
|
|
32
|
+
- lib/ruby_method_tracer/formatters/tree_formatter.rb
|
|
29
33
|
- lib/ruby_method_tracer/simple_tracer.rb
|
|
30
34
|
- lib/ruby_method_tracer/version.rb
|
|
31
35
|
- sig/ruby_method_tracer.rbs
|