ruby_method_tracer 0.3.0 → 0.3.2
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 +17 -0
- data/README.md +11 -0
- 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 +6 -1
- data/lib/ruby_method_tracer/version.rb +1 -1
- 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: c68cde18a5b8ae25abfbefdee213491e5f325aa7d47308e406fac073a4311e1d
|
|
4
|
+
data.tar.gz: 44a7c4e3ee69f0c34747e1a9f6b3b69ea0cbb12732c3b25d44471163ed7620cd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3f7ba3648b90cabfc0752f40e2f3b50b0fdb261162f67ad80e2832f8df922700e8837e04c70f874e3b6e3eb201df79b827f516cd74792b63753ba70424f3dd1f
|
|
7
|
+
data.tar.gz: 3031618de6d982cc269e3b1f7220156ce67f648fcd1699c9dba10df3cd9e344d6eeb4e72550a03dcbf259fdb6caa5fae895a8c7d9718fbb9e6ba8a22f761860d
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.3.2] - 2025-11-22
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- Fixed `SystemStackError: stack level too deep` with Ruby 3.4+ by improving keyword argument forwarding in method wrapper
|
|
7
|
+
|
|
8
|
+
## [0.3.1] - 2025-11-22
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- Fixed file permissions for `call_tree.rb`, `enhanced_tracer.rb`, and formatter files to be world-readable
|
|
12
|
+
- Gem now correctly includes all files when installed (previously missing EnhancedTracer and formatters)
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
- Code coverage reporting with SimpleCov (99% line coverage, 84% branch coverage)
|
|
16
|
+
- Codecov integration for CI/CD coverage tracking
|
|
17
|
+
- Comprehensive test suite for BaseFormatter and TreeFormatter (18 new tests)
|
|
18
|
+
- Coverage badge in README
|
|
19
|
+
|
|
3
20
|
## [0.3.0] - 2025-11-19
|
|
4
21
|
|
|
5
22
|
### 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
|
|
|
@@ -253,6 +254,16 @@ After checking out the repo, run `bin/setup` to install dependencies. Then run `
|
|
|
253
254
|
|
|
254
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`.
|
|
255
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
|
+
|
|
256
267
|
## Contributing
|
|
257
268
|
|
|
258
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
|
|
@@ -116,7 +116,12 @@ module RubyMethodTracer
|
|
|
116
116
|
def build_wrapper(aliased, method_name, key, tracer)
|
|
117
117
|
proc do |*args, **kwargs, &block| # Captures args and block exactly like original.
|
|
118
118
|
tracer.__send__(:wrap_call, method_name, key) do # Delegates to wrapper to handle timing and flag.
|
|
119
|
-
|
|
119
|
+
# Ruby 3+ compatible keyword argument forwarding
|
|
120
|
+
if kwargs.empty?
|
|
121
|
+
__send__(aliased, *args, &block) # Calls without kwargs to avoid Ruby warnings
|
|
122
|
+
else
|
|
123
|
+
__send__(aliased, *args, **kwargs, &block) # Calls the original aliased implementation.
|
|
124
|
+
end
|
|
120
125
|
end
|
|
121
126
|
end
|
|
122
127
|
end
|
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.3.
|
|
4
|
+
version: 0.3.2
|
|
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
|