ruby_method_tracer 0.3.1 → 0.3.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/CHANGELOG.md +16 -0
- data/lib/ruby_method_tracer/call_tree.rb +51 -39
- data/lib/ruby_method_tracer/enhanced_tracer.rb +43 -35
- data/lib/ruby_method_tracer/simple_tracer.rb +14 -21
- data/lib/ruby_method_tracer/version.rb +1 -1
- data/lib/ruby_method_tracer.rb +1 -1
- metadata +3 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7d467252ce5eedf8fe0111fe1afb3983d0d4a80ad7558940908543804e963b58
|
|
4
|
+
data.tar.gz: e650becb6eee60e5bc118cf287a8dc05cbafbcb404536f1b2ab2aba2072f88f9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 890623622e3f183687caba11a65577d20a82f9fc0032da94f8471b2947a2a0c805df3492b686623c3384584e338d625710531b37ca35adf7a251451fb0907e9f
|
|
7
|
+
data.tar.gz: 5cac3c1469df35c933e45a525e9384a7411553059633e98762fffdc1d56afe2eca1ab73b74d12c975a1d6dc2d6a7cd87720f3619aec9beffdf38d41a67961f0c
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.3.3] - 2026-06-08
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
- `CallTree` now stores its call stack in per-thread storage (keyed per instance) instead of a single shared `@call_stack`, so concurrent callers each track their own nesting depth. `@calls` and `@root_calls` remain shared and `Mutex`-guarded.
|
|
7
|
+
- `SimpleTracer` and `EnhancedTracer` now use a per-instance reentrancy key (`__ruby_method_tracer_in_trace_<object_id>`) so separate tracer instances no longer interfere with each other's re-entry guards.
|
|
8
|
+
- `EnhancedTracer` hierarchy tracking extracted into a dedicated `run_with_hierarchy` method; the call-tree entry is now always closed in an `ensure` block (even for non-`StandardError` exceptions) to prevent the per-thread call stack from becoming corrupted.
|
|
9
|
+
- `SimpleTracer` now delegates `format_time` and `colorize` to a `Formatters::BaseFormatter` instance instead of duplicating the formatting logic inline.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
- Keyword-argument forwarding in `EnhancedTracer`'s wrapper now avoids passing `**{}`, preventing `SystemStackError` on Ruby 3.4+ (mirrors the `SimpleTracer` fix from 0.3.2).
|
|
13
|
+
|
|
14
|
+
## [0.3.2] - 2025-11-22
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- Fixed `SystemStackError: stack level too deep` with Ruby 3.4+ by improving keyword argument forwarding in method wrapper
|
|
18
|
+
|
|
3
19
|
## [0.3.1] - 2025-11-22
|
|
4
20
|
|
|
5
21
|
### Fixed
|
|
@@ -4,45 +4,48 @@ module RubyMethodTracer
|
|
|
4
4
|
# CallTree manages the hierarchical structure of method calls,
|
|
5
5
|
# tracking parent-child relationships and call depths.
|
|
6
6
|
#
|
|
7
|
-
# It uses a
|
|
7
|
+
# It uses a per-thread stack to manage nested calls and builds
|
|
8
8
|
# a tree structure showing the complete call hierarchy.
|
|
9
|
+
#
|
|
10
|
+
# Note: @calls and @root_calls are shared across threads and protected
|
|
11
|
+
# by a Mutex. The call stack is stored in thread-local storage so that
|
|
12
|
+
# concurrent callers each maintain their own independent call depth.
|
|
9
13
|
class CallTree
|
|
10
14
|
attr_reader :calls, :root_calls
|
|
11
15
|
|
|
12
16
|
def initialize
|
|
13
|
-
@calls = [] # All recorded calls (flat list)
|
|
14
|
-
@
|
|
15
|
-
@
|
|
16
|
-
@
|
|
17
|
+
@calls = [] # All recorded calls (flat list, shared)
|
|
18
|
+
@root_calls = [] # Top-level calls (depth 0, shared)
|
|
19
|
+
@lock = Mutex.new # Protects @calls and @root_calls
|
|
20
|
+
@thread_key = :"__ruby_method_tracer_call_stack_#{object_id}" # per-instance thread-local key
|
|
17
21
|
end
|
|
18
22
|
|
|
19
23
|
# Start tracking a method call
|
|
20
24
|
#
|
|
21
25
|
# @param method_name [String] The name of the method being called
|
|
22
26
|
# @return [Hash] The call record that was pushed to the stack
|
|
23
|
-
def start_call(method_name)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
27
|
+
def start_call(method_name)
|
|
28
|
+
stack = thread_call_stack
|
|
29
|
+
|
|
30
|
+
call_record = {
|
|
31
|
+
method_name: method_name,
|
|
32
|
+
start_time: monotonic_time,
|
|
33
|
+
depth: stack.size,
|
|
34
|
+
children: [],
|
|
35
|
+
status: nil,
|
|
36
|
+
error: nil,
|
|
37
|
+
execution_time: nil,
|
|
38
|
+
timestamp: Time.now
|
|
39
|
+
}
|
|
36
40
|
|
|
37
|
-
|
|
38
|
-
|
|
41
|
+
# Add as child to parent if we're nested
|
|
42
|
+
stack.last[:children] << call_record if stack.any?
|
|
39
43
|
|
|
40
|
-
|
|
41
|
-
|
|
44
|
+
# Track root-level calls (lock required since @root_calls is shared)
|
|
45
|
+
@lock.synchronize { @root_calls << call_record } if stack.empty?
|
|
42
46
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
end
|
|
47
|
+
stack.push(call_record)
|
|
48
|
+
call_record
|
|
46
49
|
end
|
|
47
50
|
|
|
48
51
|
# End tracking a method call
|
|
@@ -51,24 +54,23 @@ module RubyMethodTracer
|
|
|
51
54
|
# @param error [Exception, nil] The exception if status is :error
|
|
52
55
|
# @return [Hash, nil] The completed call record
|
|
53
56
|
def end_call(status = :success, error = nil)
|
|
54
|
-
|
|
55
|
-
|
|
57
|
+
stack = thread_call_stack
|
|
58
|
+
return nil if stack.empty?
|
|
56
59
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
60
|
+
call_record = stack.pop
|
|
61
|
+
call_record[:status] = status
|
|
62
|
+
call_record[:error] = error
|
|
63
|
+
call_record[:execution_time] = monotonic_time - call_record[:start_time]
|
|
61
64
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
end
|
|
65
|
+
@lock.synchronize { @calls << call_record }
|
|
66
|
+
call_record
|
|
65
67
|
end
|
|
66
68
|
|
|
67
|
-
# Get the current call depth
|
|
69
|
+
# Get the current call depth for the calling thread
|
|
68
70
|
#
|
|
69
71
|
# @return [Integer] The current nesting level
|
|
70
72
|
def current_depth
|
|
71
|
-
|
|
73
|
+
thread_call_stack.size
|
|
72
74
|
end
|
|
73
75
|
|
|
74
76
|
# Get call hierarchy as nested structure
|
|
@@ -100,23 +102,33 @@ module RubyMethodTracer
|
|
|
100
102
|
end
|
|
101
103
|
|
|
102
104
|
# Clear all recorded calls and reset state
|
|
105
|
+
#
|
|
106
|
+
# Note: only the current thread's call stack is cleared; other threads
|
|
107
|
+
# that are mid-trace retain their stacks.
|
|
103
108
|
def clear
|
|
104
109
|
@lock.synchronize do
|
|
105
110
|
@calls.clear
|
|
106
|
-
@call_stack.clear
|
|
107
111
|
@root_calls.clear
|
|
108
112
|
end
|
|
113
|
+
thread_call_stack.clear
|
|
109
114
|
end
|
|
110
115
|
|
|
111
|
-
# Check if
|
|
116
|
+
# Check if the current thread has no active calls
|
|
112
117
|
#
|
|
113
118
|
# @return [Boolean]
|
|
114
119
|
def empty?
|
|
115
|
-
|
|
120
|
+
thread_call_stack.empty?
|
|
116
121
|
end
|
|
117
122
|
|
|
118
123
|
private
|
|
119
124
|
|
|
125
|
+
# Returns the call stack for the current thread, creating it if needed.
|
|
126
|
+
# Using a per-instance key prevents interference between multiple CallTree
|
|
127
|
+
# instances running in the same thread.
|
|
128
|
+
def thread_call_stack
|
|
129
|
+
Thread.current[@thread_key] ||= []
|
|
130
|
+
end
|
|
131
|
+
|
|
120
132
|
def monotonic_time
|
|
121
133
|
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
122
134
|
end
|
|
@@ -39,7 +39,7 @@ module RubyMethodTracer
|
|
|
39
39
|
@target_class.send(:alias_method, aliased, method_name)
|
|
40
40
|
|
|
41
41
|
tracer = self
|
|
42
|
-
key =
|
|
42
|
+
key = @tracer_key # unique per tracer instance; prevents cross-tracer interference
|
|
43
43
|
|
|
44
44
|
# Build wrapper that tracks hierarchy
|
|
45
45
|
@target_class.define_method(method_name, &build_enhanced_wrapper(aliased, method_name, key, tracer))
|
|
@@ -83,51 +83,59 @@ module RubyMethodTracer
|
|
|
83
83
|
|
|
84
84
|
private
|
|
85
85
|
|
|
86
|
-
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
87
86
|
def build_enhanced_wrapper(aliased, method_name, key, tracer)
|
|
88
87
|
track_hierarchy = tracer.instance_variable_get(:@track_hierarchy)
|
|
89
88
|
# Use method-specific key to prevent only SELF-recursion, not all nested calls
|
|
90
89
|
method_key = :"#{key}_#{method_name}"
|
|
91
90
|
|
|
92
91
|
proc do |*args, **kwargs, &block|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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)
|
|
92
|
+
# Ruby 3+ compatible forwarding helper (avoids passing **{} which caused
|
|
93
|
+
# SystemStackError with Ruby 3.4+ keyword argument forwarding)
|
|
94
|
+
call_aliased = lambda do
|
|
95
|
+
kwargs.empty? ? __send__(aliased, *args, &block) : __send__(aliased, *args, **kwargs, &block)
|
|
96
|
+
end
|
|
119
97
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
Thread.current[method_key] = false
|
|
123
|
-
end
|
|
98
|
+
if track_hierarchy
|
|
99
|
+
tracer.__send__(:run_with_hierarchy, method_name, method_key, call_aliased)
|
|
124
100
|
else
|
|
125
|
-
tracer.__send__(:wrap_call, method_name, key)
|
|
126
|
-
__send__(aliased, *args, **kwargs, &block)
|
|
127
|
-
end
|
|
101
|
+
tracer.__send__(:wrap_call, method_name, key) { call_aliased.call }
|
|
128
102
|
end
|
|
129
103
|
end
|
|
130
104
|
end
|
|
105
|
+
|
|
106
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
107
|
+
def run_with_hierarchy(method_name, method_key, call_aliased)
|
|
108
|
+
# Prevent only recursive calls to the SAME method
|
|
109
|
+
return call_aliased.call if Thread.current[method_key]
|
|
110
|
+
|
|
111
|
+
Thread.current[method_key] = true
|
|
112
|
+
full_method_name = "#{@target_class}##{method_name}"
|
|
113
|
+
|
|
114
|
+
# Start tracking in call tree before entering the timed section
|
|
115
|
+
@call_tree.start_call(full_method_name)
|
|
116
|
+
|
|
117
|
+
start = monotonic_time
|
|
118
|
+
call_status = :success
|
|
119
|
+
call_error = nil
|
|
120
|
+
|
|
121
|
+
begin
|
|
122
|
+
result = call_aliased.call
|
|
123
|
+
execution_time = monotonic_time - start
|
|
124
|
+
record_call(method_name, execution_time, :success)
|
|
125
|
+
result
|
|
126
|
+
rescue StandardError => e
|
|
127
|
+
call_status = :error
|
|
128
|
+
call_error = e
|
|
129
|
+
execution_time = monotonic_time - start
|
|
130
|
+
record_call(method_name, execution_time, :error, e)
|
|
131
|
+
raise
|
|
132
|
+
ensure
|
|
133
|
+
Thread.current[method_key] = false
|
|
134
|
+
# Always end the call tree entry, even for non-StandardError exceptions,
|
|
135
|
+
# to prevent the per-thread call stack from becoming corrupted.
|
|
136
|
+
@call_tree.end_call(call_status, call_error)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
131
139
|
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
132
140
|
|
|
133
141
|
def default_options
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "set"
|
|
4
4
|
require "logger"
|
|
5
|
+
require_relative "formatters/base_formatter"
|
|
5
6
|
|
|
6
7
|
module RubyMethodTracer
|
|
7
8
|
# SimpleTracer wraps instance methods on a target class and records
|
|
@@ -19,7 +20,7 @@ module RubyMethodTracer
|
|
|
19
20
|
# tracer = RubyMethodTracer::SimpleTracer.new(MyClass, threshold: 0.005)
|
|
20
21
|
# tracer.trace_method(:expensive_call)
|
|
21
22
|
# results = tracer.fetch_results
|
|
22
|
-
class SimpleTracer
|
|
23
|
+
class SimpleTracer
|
|
23
24
|
def initialize(target_class, **options)
|
|
24
25
|
@target_class = target_class
|
|
25
26
|
@options = default_options.merge(options)
|
|
@@ -27,6 +28,9 @@ module RubyMethodTracer
|
|
|
27
28
|
@lock = Mutex.new # Mutex to make writes to @calls thread safe.
|
|
28
29
|
@wrapped_methods = Set.new
|
|
29
30
|
@logger = @options[:logger] || Logger.new($stdout)
|
|
31
|
+
# Unique per instance so separate tracers don't interfere with each other.
|
|
32
|
+
@tracer_key = :"__ruby_method_tracer_in_trace_#{object_id}"
|
|
33
|
+
@formatter = Formatters::BaseFormatter.new
|
|
30
34
|
end
|
|
31
35
|
|
|
32
36
|
def trace_method(name)
|
|
@@ -39,7 +43,7 @@ module RubyMethodTracer
|
|
|
39
43
|
@target_class.send(:alias_method, aliased, method_name) # Aliases original implementation to our private name.
|
|
40
44
|
|
|
41
45
|
tracer = self
|
|
42
|
-
key =
|
|
46
|
+
key = @tracer_key # unique per tracer instance; prevents cross-tracer interference
|
|
43
47
|
|
|
44
48
|
# Defines a new method with the original name that delegates to our wrapper.
|
|
45
49
|
@target_class.define_method(method_name, &build_wrapper(aliased, method_name, key, tracer))
|
|
@@ -116,7 +120,12 @@ module RubyMethodTracer
|
|
|
116
120
|
def build_wrapper(aliased, method_name, key, tracer)
|
|
117
121
|
proc do |*args, **kwargs, &block| # Captures args and block exactly like original.
|
|
118
122
|
tracer.__send__(:wrap_call, method_name, key) do # Delegates to wrapper to handle timing and flag.
|
|
119
|
-
|
|
123
|
+
# Ruby 3+ compatible keyword argument forwarding
|
|
124
|
+
if kwargs.empty?
|
|
125
|
+
__send__(aliased, *args, &block) # Calls without kwargs to avoid Ruby warnings
|
|
126
|
+
else
|
|
127
|
+
__send__(aliased, *args, **kwargs, &block) # Calls the original aliased implementation.
|
|
128
|
+
end
|
|
120
129
|
end
|
|
121
130
|
end
|
|
122
131
|
end
|
|
@@ -156,27 +165,11 @@ module RubyMethodTracer
|
|
|
156
165
|
end
|
|
157
166
|
|
|
158
167
|
def format_time(seconds)
|
|
159
|
-
|
|
160
|
-
"#{seconds.round(3)}s"
|
|
161
|
-
elsif seconds >= 0.001
|
|
162
|
-
"#{(seconds * 1000).round(1)}ms"
|
|
163
|
-
else
|
|
164
|
-
"#{(seconds * 1_000_000).round(0)}µs"
|
|
165
|
-
end
|
|
168
|
+
@formatter.format_time(seconds)
|
|
166
169
|
end
|
|
167
170
|
|
|
168
171
|
def colorize(text, color)
|
|
169
|
-
|
|
170
|
-
red: "31",
|
|
171
|
-
green: "32",
|
|
172
|
-
yellow: "33",
|
|
173
|
-
blue: "34",
|
|
174
|
-
magenta: "35",
|
|
175
|
-
cyan: "36",
|
|
176
|
-
white: "37",
|
|
177
|
-
reset: "0"
|
|
178
|
-
}
|
|
179
|
-
"\e[#{colors[color]}m#{text}\e[#{colors[:reset]}m"
|
|
172
|
+
@formatter.colorize(text, color)
|
|
180
173
|
end
|
|
181
174
|
end
|
|
182
175
|
end
|
data/lib/ruby_method_tracer.rb
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "ruby_method_tracer/version"
|
|
4
|
+
require_relative "ruby_method_tracer/formatters/base_formatter"
|
|
4
5
|
require_relative "ruby_method_tracer/simple_tracer"
|
|
5
6
|
require_relative "ruby_method_tracer/call_tree"
|
|
6
7
|
require_relative "ruby_method_tracer/enhanced_tracer"
|
|
7
|
-
require_relative "ruby_method_tracer/formatters/base_formatter"
|
|
8
8
|
require_relative "ruby_method_tracer/formatters/tree_formatter"
|
|
9
9
|
|
|
10
10
|
# Public: Mixin that adds lightweight method tracing to classes.
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
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.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Seun Adekunle
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: exe
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies: []
|
|
13
12
|
description: A developer-friendly gem for tracing method calls, execution times, with
|
|
14
13
|
minimal overhead.
|
|
@@ -42,7 +41,6 @@ metadata:
|
|
|
42
41
|
source_code_uri: https://github.com/Seunadex/ruby_method_tracer
|
|
43
42
|
changelog_uri: https://github.com/Seunadex/ruby_method_tracer/blob/main/CHANGELOG.md
|
|
44
43
|
rubygems_mfa_required: 'true'
|
|
45
|
-
post_install_message:
|
|
46
44
|
rdoc_options: []
|
|
47
45
|
require_paths:
|
|
48
46
|
- lib
|
|
@@ -57,8 +55,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
57
55
|
- !ruby/object:Gem::Version
|
|
58
56
|
version: '0'
|
|
59
57
|
requirements: []
|
|
60
|
-
rubygems_version: 3.
|
|
61
|
-
signing_key:
|
|
58
|
+
rubygems_version: 3.6.7
|
|
62
59
|
specification_version: 4
|
|
63
60
|
summary: Lightweight method tracing for Ruby applications
|
|
64
61
|
test_files: []
|