catpm 0.7.0 → 0.8.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/README.md +1 -1
- data/lib/catpm/call_tracer.rb +6 -22
- data/lib/catpm/collector.rb +35 -3
- data/lib/catpm/configuration.rb +2 -2
- data/lib/catpm/fingerprint.rb +48 -8
- data/lib/catpm/middleware.rb +3 -8
- data/lib/catpm/request_segments.rb +18 -6
- data/lib/catpm/segment_subscribers.rb +3 -2
- data/lib/catpm/stack_sampler.rb +89 -2
- data/lib/catpm/version.rb +1 -1
- data/lib/catpm.rb +1 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5e79483c6546d21e4b6054d481f3625616eb0c4044f1251d11be907983dadeb7
|
|
4
|
+
data.tar.gz: 555afaca6bfb09499fea0c24359ee7709d8b982759fb6717a577882d64c543cc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2971732b88f54566048545753164728886136408fc0db9d82e20652c08b098973d0889a839fd66cd4a504def1a9c576cf2eab822dac9b381eda0f2e40d908123
|
|
7
|
+
data.tar.gz: c8c7b98c039e7c483e8d4e3e3a9e8c9f06a58e7cd396d303da5bd37c9771d6b463d84d62c9da5c9f87e3e1f688ebefe7bb8b016d7eac477030588513b2e0988c
|
data/README.md
CHANGED
data/lib/catpm/call_tracer.rb
CHANGED
|
@@ -4,25 +4,6 @@ module Catpm
|
|
|
4
4
|
class CallTracer
|
|
5
5
|
MAX_CALL_DEPTH = 64
|
|
6
6
|
|
|
7
|
-
# Global thread-safe path classification cache — avoids repeated Fingerprint.app_frame? calls
|
|
8
|
-
@global_path_cache = {}
|
|
9
|
-
@global_path_mutex = Mutex.new
|
|
10
|
-
|
|
11
|
-
class << self
|
|
12
|
-
def app_frame_cached?(path)
|
|
13
|
-
cached = @global_path_cache[path]
|
|
14
|
-
return cached unless cached.nil?
|
|
15
|
-
|
|
16
|
-
result = Fingerprint.app_frame?(path)
|
|
17
|
-
@global_path_mutex.synchronize do
|
|
18
|
-
# Cap cache to prevent unbounded growth across process lifetime
|
|
19
|
-
@global_path_cache.clear if @global_path_cache.size > 2000
|
|
20
|
-
@global_path_cache[path] = result
|
|
21
|
-
end
|
|
22
|
-
result
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
|
|
26
7
|
def initialize(request_segments:)
|
|
27
8
|
@request_segments = request_segments
|
|
28
9
|
@call_stack = []
|
|
@@ -60,7 +41,7 @@ module Catpm
|
|
|
60
41
|
@depth += 1
|
|
61
42
|
|
|
62
43
|
path = tp.path
|
|
63
|
-
app =
|
|
44
|
+
app = Fingerprint.app_frame?(path)
|
|
64
45
|
|
|
65
46
|
unless app
|
|
66
47
|
@call_stack.push(:skip)
|
|
@@ -97,11 +78,14 @@ module Catpm
|
|
|
97
78
|
end
|
|
98
79
|
|
|
99
80
|
def format_detail(defined_class, method_id)
|
|
100
|
-
|
|
81
|
+
name = defined_class.name
|
|
82
|
+
if name
|
|
83
|
+
"#{name}##{method_id}"
|
|
84
|
+
elsif defined_class.singleton_class?
|
|
101
85
|
owner = defined_class.attached_object
|
|
102
86
|
"#{owner.name || owner.inspect}.#{method_id}"
|
|
103
87
|
else
|
|
104
|
-
"#{defined_class.
|
|
88
|
+
"#{defined_class.inspect}##{method_id}"
|
|
105
89
|
end
|
|
106
90
|
end
|
|
107
91
|
end
|
data/lib/catpm/collector.rb
CHANGED
|
@@ -90,6 +90,20 @@ module Catpm
|
|
|
90
90
|
end
|
|
91
91
|
end
|
|
92
92
|
|
|
93
|
+
# Inject call tree segments from sampler (replaces TracePoint-based CallTracer)
|
|
94
|
+
ctrl_idx = segments.index { |s| s[:type] == 'controller' }
|
|
95
|
+
if Catpm.config.instrument_call_tree && req_segments
|
|
96
|
+
tree_segs = req_segments.call_tree_segments
|
|
97
|
+
if tree_segs.any?
|
|
98
|
+
base_idx = segments.size
|
|
99
|
+
tree_segs.each do |seg|
|
|
100
|
+
tree_parent = seg.delete(:_tree_parent)
|
|
101
|
+
seg[:parent_index] = tree_parent ? (tree_parent + base_idx) : (ctrl_idx || 0)
|
|
102
|
+
segments << seg
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
93
107
|
# Fill untracked controller time with sampler data or synthetic segment
|
|
94
108
|
ctrl_idx = segments.index { |s| s[:type] == 'controller' }
|
|
95
109
|
if ctrl_idx
|
|
@@ -246,6 +260,20 @@ module Catpm
|
|
|
246
260
|
end
|
|
247
261
|
segments.unshift(root_segment)
|
|
248
262
|
|
|
263
|
+
# Inject call tree segments from sampler
|
|
264
|
+
ctrl_idx = segments.index { |s| s[:type] == 'controller' }
|
|
265
|
+
if Catpm.config.instrument_call_tree && req_segments
|
|
266
|
+
tree_segs = req_segments.call_tree_segments
|
|
267
|
+
if tree_segs.any?
|
|
268
|
+
base_idx = segments.size
|
|
269
|
+
tree_segs.each do |seg|
|
|
270
|
+
tree_parent = seg.delete(:_tree_parent)
|
|
271
|
+
seg[:parent_index] = tree_parent ? (tree_parent + base_idx) : (ctrl_idx || 0)
|
|
272
|
+
segments << seg
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
|
|
249
277
|
# Fill untracked controller time with sampler data or synthetic segment
|
|
250
278
|
ctrl_idx = segments.index { |s| s[:type] == 'controller' }
|
|
251
279
|
if ctrl_idx
|
|
@@ -339,14 +367,18 @@ module Catpm
|
|
|
339
367
|
# ActiveSupport controller notification finishes.
|
|
340
368
|
# Mutates segments in place: removes the wrapper and re-indexes parent references.
|
|
341
369
|
def collapse_code_wrappers(segments)
|
|
370
|
+
# Pre-build set of parent indices that have a controller child — O(n)
|
|
371
|
+
parents_with_controller = {}
|
|
372
|
+
segments.each do |seg|
|
|
373
|
+
parents_with_controller[seg[:parent_index]] = true if seg[:type] == 'controller' && seg[:parent_index]
|
|
374
|
+
end
|
|
375
|
+
|
|
342
376
|
# Identify code spans to collapse: near-zero duration wrapping a controller child
|
|
343
377
|
collapse = {}
|
|
344
378
|
segments.each_with_index do |seg, i|
|
|
345
379
|
next unless seg[:type] == 'code'
|
|
346
380
|
next unless (seg[:duration] || 0).to_f < 1.0
|
|
347
|
-
|
|
348
|
-
has_controller_child = segments.any? { |s| s[:parent_index] == i && s[:type] == 'controller' }
|
|
349
|
-
next unless has_controller_child
|
|
381
|
+
next unless parents_with_controller[i]
|
|
350
382
|
|
|
351
383
|
collapse[i] = seg[:parent_index]
|
|
352
384
|
end
|
data/lib/catpm/configuration.rb
CHANGED
|
@@ -71,7 +71,7 @@ module Catpm
|
|
|
71
71
|
@instrument_stack_sampler = false
|
|
72
72
|
@instrument_middleware_stack = false
|
|
73
73
|
@max_segments_per_request = 50
|
|
74
|
-
@segment_source_threshold =
|
|
74
|
+
@segment_source_threshold = 5.0 # ms — capture caller_locations only for segments >= 5ms (set to 0.0 to capture all)
|
|
75
75
|
@max_sql_length = 200
|
|
76
76
|
@slow_threshold = 500 # milliseconds
|
|
77
77
|
@slow_threshold_per_kind = {}
|
|
@@ -98,7 +98,7 @@ module Catpm
|
|
|
98
98
|
@circuit_breaker_recovery_timeout = 60 # seconds
|
|
99
99
|
@sqlite_busy_timeout = 5_000 # milliseconds
|
|
100
100
|
@persistence_batch_size = 100
|
|
101
|
-
@backtrace_lines = nil
|
|
101
|
+
@backtrace_lines = 20 # frames per error backtrace (nil = unlimited)
|
|
102
102
|
@shutdown_timeout = 5 # seconds
|
|
103
103
|
@events_enabled = false
|
|
104
104
|
@events_max_samples_per_name = 20
|
data/lib/catpm/fingerprint.rb
CHANGED
|
@@ -4,6 +4,9 @@ require 'digest'
|
|
|
4
4
|
|
|
5
5
|
module Catpm
|
|
6
6
|
module Fingerprint
|
|
7
|
+
@path_cache = {}
|
|
8
|
+
@path_cache_mutex = Mutex.new
|
|
9
|
+
|
|
7
10
|
# Generates a stable fingerprint for error grouping.
|
|
8
11
|
# Includes kind so the same exception in HTTP vs job = different groups.
|
|
9
12
|
def self.generate(kind:, error_class:, backtrace:)
|
|
@@ -30,23 +33,60 @@ module Catpm
|
|
|
30
33
|
.join("\n")
|
|
31
34
|
end
|
|
32
35
|
|
|
33
|
-
#
|
|
36
|
+
# Cached wrapper — all callers benefit from the shared path cache.
|
|
34
37
|
def self.app_frame?(line)
|
|
35
|
-
|
|
36
|
-
return
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
cached = @path_cache[line]
|
|
39
|
+
return cached unless cached.nil?
|
|
40
|
+
|
|
41
|
+
result = _app_frame?(line)
|
|
42
|
+
@path_cache_mutex.synchronize do
|
|
43
|
+
@path_cache.clear if @path_cache.size > 4000
|
|
44
|
+
@path_cache[line] = result
|
|
45
|
+
end
|
|
46
|
+
result
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Cached Rails.root.to_s — computed once, never changes after boot.
|
|
50
|
+
def self.cached_rails_root
|
|
51
|
+
return @cached_rails_root if defined?(@cached_rails_root)
|
|
39
52
|
|
|
40
|
-
if defined?(Rails) && Rails.respond_to?(:root) && Rails.root
|
|
41
|
-
|
|
53
|
+
@cached_rails_root = if defined?(Rails) && Rails.respond_to?(:root) && Rails.root
|
|
54
|
+
Rails.root.to_s.freeze
|
|
42
55
|
end
|
|
56
|
+
end
|
|
43
57
|
|
|
44
|
-
|
|
58
|
+
# Cached "#{Rails.root}/" for path stripping.
|
|
59
|
+
def self.cached_rails_root_slash
|
|
60
|
+
return @cached_rails_root_slash if defined?(@cached_rails_root_slash)
|
|
61
|
+
|
|
62
|
+
root = cached_rails_root
|
|
63
|
+
@cached_rails_root_slash = root ? "#{root}/".freeze : nil
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def self.reset_caches!
|
|
67
|
+
@path_cache_mutex.synchronize { @path_cache.clear }
|
|
68
|
+
remove_instance_variable(:@cached_rails_root) if defined?(@cached_rails_root)
|
|
69
|
+
remove_instance_variable(:@cached_rails_root_slash) if defined?(@cached_rails_root_slash)
|
|
45
70
|
end
|
|
46
71
|
|
|
47
72
|
# Strips line numbers: "app/models/user.rb:42:in `validate'" → "app/models/user.rb:in `validate'"
|
|
48
73
|
def self.strip_line_number(line)
|
|
49
74
|
line.sub(/:\d+:in /, ':in ')
|
|
50
75
|
end
|
|
76
|
+
|
|
77
|
+
# The actual classification logic (uncached).
|
|
78
|
+
def self._app_frame?(line)
|
|
79
|
+
return false if line.include?('/gems/')
|
|
80
|
+
return false if line.include?('/ruby/')
|
|
81
|
+
return false if line.include?('<internal:')
|
|
82
|
+
return false if line.include?('/catpm/')
|
|
83
|
+
|
|
84
|
+
if (root = cached_rails_root)
|
|
85
|
+
return line.start_with?(root) if line.start_with?('/')
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
line.start_with?('app/') || line.include?('/app/')
|
|
89
|
+
end
|
|
90
|
+
private_class_method :_app_frame?
|
|
51
91
|
end
|
|
52
92
|
end
|
data/lib/catpm/middleware.rb
CHANGED
|
@@ -14,19 +14,15 @@ module Catpm
|
|
|
14
14
|
env['catpm.request_start'] = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
15
15
|
|
|
16
16
|
if Catpm.config.instrument_segments
|
|
17
|
+
use_sampler = Catpm.config.instrument_stack_sampler || Catpm.config.instrument_call_tree
|
|
17
18
|
req_segments = RequestSegments.new(
|
|
18
19
|
max_segments: Catpm.config.max_segments_per_request,
|
|
19
20
|
request_start: env['catpm.request_start'],
|
|
20
|
-
stack_sample:
|
|
21
|
+
stack_sample: use_sampler,
|
|
22
|
+
call_tree: Catpm.config.instrument_call_tree
|
|
21
23
|
)
|
|
22
24
|
env['catpm.segments'] = req_segments
|
|
23
25
|
Thread.current[:catpm_request_segments] = req_segments
|
|
24
|
-
|
|
25
|
-
if Catpm.config.instrument_call_tree
|
|
26
|
-
call_tracer = CallTracer.new(request_segments: req_segments)
|
|
27
|
-
call_tracer.start
|
|
28
|
-
env['catpm.call_tracer'] = call_tracer
|
|
29
|
-
end
|
|
30
26
|
end
|
|
31
27
|
|
|
32
28
|
@app.call(env)
|
|
@@ -35,7 +31,6 @@ module Catpm
|
|
|
35
31
|
raise
|
|
36
32
|
ensure
|
|
37
33
|
if Catpm.config.instrument_segments
|
|
38
|
-
env['catpm.call_tracer']&.stop
|
|
39
34
|
req_segments&.stop_sampler
|
|
40
35
|
Thread.current[:catpm_request_segments] = nil
|
|
41
36
|
end
|
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
module Catpm
|
|
4
4
|
class RequestSegments
|
|
5
|
+
# Pre-computed symbol pairs — each type computed once per process lifetime.
|
|
6
|
+
SUMMARY_KEYS = Hash.new { |h, k| h[k] = [:"#{k}_count", :"#{k}_duration"] }
|
|
7
|
+
|
|
5
8
|
attr_reader :segments, :summary, :request_start
|
|
6
9
|
|
|
7
|
-
def initialize(max_segments:, request_start: nil, stack_sample: false)
|
|
10
|
+
def initialize(max_segments:, request_start: nil, stack_sample: false, call_tree: false)
|
|
8
11
|
@max_segments = max_segments
|
|
9
12
|
@request_start = request_start || Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
10
13
|
@segments = []
|
|
@@ -12,17 +15,19 @@ module Catpm
|
|
|
12
15
|
@summary = Hash.new(0)
|
|
13
16
|
@span_stack = []
|
|
14
17
|
@tracked_ranges = []
|
|
18
|
+
@call_tree = call_tree
|
|
15
19
|
|
|
16
20
|
if stack_sample
|
|
17
|
-
@sampler = StackSampler.new(target_thread: Thread.current, request_start: @request_start)
|
|
21
|
+
@sampler = StackSampler.new(target_thread: Thread.current, request_start: @request_start, call_tree: call_tree)
|
|
18
22
|
@sampler.start
|
|
19
23
|
end
|
|
20
24
|
end
|
|
21
25
|
|
|
22
26
|
def add(type:, duration:, detail:, source: nil, started_at: nil)
|
|
23
27
|
type_key = type.to_sym
|
|
24
|
-
|
|
25
|
-
@summary[
|
|
28
|
+
count_key, dur_key = SUMMARY_KEYS[type_key]
|
|
29
|
+
@summary[count_key] += 1
|
|
30
|
+
@summary[dur_key] += duration
|
|
26
31
|
|
|
27
32
|
offset = started_at ? ((started_at - @request_start) * 1000.0).round(2) : nil
|
|
28
33
|
|
|
@@ -79,8 +84,9 @@ module Catpm
|
|
|
79
84
|
segment[:duration] = duration.round(2)
|
|
80
85
|
|
|
81
86
|
type_key = segment[:type].to_sym
|
|
82
|
-
|
|
83
|
-
@summary[
|
|
87
|
+
count_key, dur_key = SUMMARY_KEYS[type_key]
|
|
88
|
+
@summary[count_key] += 1
|
|
89
|
+
@summary[dur_key] += duration
|
|
84
90
|
end
|
|
85
91
|
|
|
86
92
|
def stop_sampler
|
|
@@ -88,9 +94,15 @@ module Catpm
|
|
|
88
94
|
end
|
|
89
95
|
|
|
90
96
|
def sampler_segments
|
|
97
|
+
return [] if @call_tree # call tree mode produces segments via call_tree_segments
|
|
91
98
|
@sampler&.to_segments(tracked_ranges: @tracked_ranges) || []
|
|
92
99
|
end
|
|
93
100
|
|
|
101
|
+
def call_tree_segments
|
|
102
|
+
return [] unless @sampler && @call_tree
|
|
103
|
+
@sampler.to_call_tree(tracked_ranges: @tracked_ranges)
|
|
104
|
+
end
|
|
105
|
+
|
|
94
106
|
def overflowed?
|
|
95
107
|
@overflow
|
|
96
108
|
end
|
|
@@ -33,8 +33,9 @@ module Catpm
|
|
|
33
33
|
return unless req_segments
|
|
34
34
|
|
|
35
35
|
identifier = payload[:identifier].to_s
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
root_slash = Fingerprint.cached_rails_root_slash
|
|
37
|
+
if root_slash
|
|
38
|
+
identifier = identifier.delete_prefix(root_slash)
|
|
38
39
|
end
|
|
39
40
|
|
|
40
41
|
started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
data/lib/catpm/stack_sampler.rb
CHANGED
|
@@ -4,6 +4,7 @@ module Catpm
|
|
|
4
4
|
class StackSampler
|
|
5
5
|
MS_PER_SECOND = 1000.0
|
|
6
6
|
MIN_SEGMENT_DURATION_MS = 1.0
|
|
7
|
+
CALL_TREE_SAMPLE_INTERVAL = 0.001 # 1ms — higher resolution for call tree reconstruction
|
|
7
8
|
SAMPLING_THREAD_PRIORITY = -1
|
|
8
9
|
|
|
9
10
|
# Single global thread that samples all active requests.
|
|
@@ -31,7 +32,12 @@ module Catpm
|
|
|
31
32
|
def start_thread
|
|
32
33
|
@thread = Thread.new do
|
|
33
34
|
loop do
|
|
34
|
-
|
|
35
|
+
interval = if Catpm.config.instrument_call_tree
|
|
36
|
+
[CALL_TREE_SAMPLE_INTERVAL, Catpm.config.stack_sample_interval].min
|
|
37
|
+
else
|
|
38
|
+
Catpm.config.stack_sample_interval
|
|
39
|
+
end
|
|
40
|
+
sleep(interval)
|
|
35
41
|
sample_all
|
|
36
42
|
end
|
|
37
43
|
end
|
|
@@ -51,10 +57,11 @@ module Catpm
|
|
|
51
57
|
attr_reader :loop
|
|
52
58
|
end
|
|
53
59
|
|
|
54
|
-
def initialize(target_thread:, request_start:)
|
|
60
|
+
def initialize(target_thread:, request_start:, call_tree: false)
|
|
55
61
|
@target = target_thread
|
|
56
62
|
@request_start = request_start
|
|
57
63
|
@samples = []
|
|
64
|
+
@call_tree = call_tree
|
|
58
65
|
end
|
|
59
66
|
|
|
60
67
|
def start
|
|
@@ -159,8 +166,88 @@ module Catpm
|
|
|
159
166
|
end
|
|
160
167
|
end
|
|
161
168
|
|
|
169
|
+
# Build a call tree from samples — replacement for TracePoint-based CallTracer.
|
|
170
|
+
# Returns flat array of segments with :_tree_parent (relative index or nil for top-level).
|
|
171
|
+
def to_call_tree(tracked_ranges: [])
|
|
172
|
+
return [] if @samples.size < 2
|
|
173
|
+
|
|
174
|
+
# Build a tree of app-frame call chains across all samples
|
|
175
|
+
tree = {} # key → {frame:, children: {}, count:, first_time:, last_time:}
|
|
176
|
+
|
|
177
|
+
@samples.each do |time, locs|
|
|
178
|
+
chain = extract_app_chain(locs)
|
|
179
|
+
next if chain.empty?
|
|
180
|
+
|
|
181
|
+
current_level = tree
|
|
182
|
+
chain.each do |frame|
|
|
183
|
+
key = frame_key(frame)
|
|
184
|
+
unless current_level.key?(key)
|
|
185
|
+
current_level[key] = {
|
|
186
|
+
frame: frame, children: {}, count: 0,
|
|
187
|
+
first_time: time, last_time: time
|
|
188
|
+
}
|
|
189
|
+
end
|
|
190
|
+
node = current_level[key]
|
|
191
|
+
node[:count] += 1
|
|
192
|
+
node[:last_time] = time
|
|
193
|
+
current_level = node[:children]
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Flatten tree into segments with relative parent references
|
|
198
|
+
segments = []
|
|
199
|
+
flatten_call_tree(tree, segments, nil)
|
|
200
|
+
segments
|
|
201
|
+
end
|
|
202
|
+
|
|
162
203
|
private
|
|
163
204
|
|
|
205
|
+
# Extract all app frames from a backtrace, ordered caller-first (outer → inner).
|
|
206
|
+
def extract_app_chain(locations)
|
|
207
|
+
frames = []
|
|
208
|
+
locations.each do |loc|
|
|
209
|
+
path = loc.path.to_s
|
|
210
|
+
next if path.start_with?('<internal:')
|
|
211
|
+
next if path.include?('/catpm/')
|
|
212
|
+
next if path.include?('/ruby/') && !path.include?('/gems/')
|
|
213
|
+
|
|
214
|
+
frames << loc if Fingerprint.app_frame?(path)
|
|
215
|
+
end
|
|
216
|
+
frames.reverse
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def flatten_call_tree(children_hash, segments, parent_idx)
|
|
220
|
+
children_hash.each_value do |node|
|
|
221
|
+
duration = call_tree_node_duration(node)
|
|
222
|
+
next if duration < MIN_SEGMENT_DURATION_MS
|
|
223
|
+
|
|
224
|
+
frame = node[:frame]
|
|
225
|
+
seg = {
|
|
226
|
+
type: 'code',
|
|
227
|
+
detail: build_app_detail(frame),
|
|
228
|
+
duration: duration.round(2),
|
|
229
|
+
offset: ((node[:first_time] - @request_start) * MS_PER_SECOND).round(2),
|
|
230
|
+
source: "#{frame.path}:#{frame.lineno}",
|
|
231
|
+
_tree_parent: parent_idx
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
idx = segments.size
|
|
235
|
+
segments << seg
|
|
236
|
+
|
|
237
|
+
flatten_call_tree(node[:children], segments, idx)
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def call_tree_node_duration(node)
|
|
242
|
+
interval = Catpm.config.instrument_call_tree ?
|
|
243
|
+
[CALL_TREE_SAMPLE_INTERVAL, Catpm.config.stack_sample_interval].min :
|
|
244
|
+
Catpm.config.stack_sample_interval
|
|
245
|
+
[
|
|
246
|
+
(node[:last_time] - node[:first_time]) * MS_PER_SECOND,
|
|
247
|
+
node[:count] * interval * MS_PER_SECOND
|
|
248
|
+
].max
|
|
249
|
+
end
|
|
250
|
+
|
|
164
251
|
# Walk the stack: find the leaf (deepest interesting frame)
|
|
165
252
|
# and the app_frame (nearest app code above the leaf)
|
|
166
253
|
def extract_frame_pair(locations)
|
data/lib/catpm/version.rb
CHANGED
data/lib/catpm.rb
CHANGED