fiber-profiler 0.6.0 → 0.7.0
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
- checksums.yaml.gz.sig +0 -0
- data/context/getting-started.md +85 -9
- data/ext/fiber/profiler/capture.c +4 -2
- data/lib/fiber/profiler/capture.rb +2 -32
- data/lib/fiber/profiler/fork_handler.rb +0 -3
- data/lib/fiber/profiler/thread_local.rb +34 -0
- data/lib/fiber/profiler/version.rb +1 -1
- data/lib/fiber/profiler/watchdog.rb +185 -0
- data/lib/fiber/profiler.rb +18 -5
- data/readme.md +8 -2
- data/release.cert +24 -0
- data/releases.md +4 -0
- data.tar.gz.sig +0 -0
- metadata +42 -29
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 907a8aaa882e1eb3d59e994e89b6c7181b85e5a1795f7bddab05cae9bf32afe8
|
|
4
|
+
data.tar.gz: 412d12500a7bb74047b883669c70575360222fcf7df7a8b3fc6ecf5f73efc6b9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e5347ee515d48af4c75bbe85b32b8a71019ad8f48392a641d47e213012ed41bf2ee9a6112c11cbe46ecf66da23d9682d770ead22f73180eaa2950a636c00ce39
|
|
7
|
+
data.tar.gz: dceff8570643161dc70ec0b02bf72504cc5d554c4d5db201f6c9efdc63f64e0e6b90fa3b234a5b56174d67aab4d6cb98b4b8360250466aba46752fe9f5a5c1c6
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/context/getting-started.md
CHANGED
|
@@ -12,6 +12,25 @@ $ bundle add fiber-profiler
|
|
|
12
12
|
|
|
13
13
|
## Usage
|
|
14
14
|
|
|
15
|
+
Select the profiling mode when starting your application:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
FIBER_PROFILER=watchdog bundle exec falcon serve
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
With Async, including Falcon, the scheduler starts and stops the selected profiler automatically. Include `fiber-profiler` in your application's bundle; no initializer or per-request instrumentation is needed.
|
|
22
|
+
|
|
23
|
+
| Setting | Behaviour |
|
|
24
|
+
| --- | --- |
|
|
25
|
+
| `FIBER_PROFILER=watchdog` | Periodically sample stacks during sustained fiber execution. |
|
|
26
|
+
| `FIBER_PROFILER=capture` | Trace calls and report fibers that exceed the capture threshold. |
|
|
27
|
+
| `FIBER_PROFILER=false` | Disable the default profiler, even if the legacy capture flag is enabled. |
|
|
28
|
+
| `FIBER_PROFILER` unset | Preserve the existing `FIBER_PROFILER_CAPTURE=true` behaviour. |
|
|
29
|
+
|
|
30
|
+
Unknown modes raise `ArgumentError`. Set environment variables before loading the gem: the native capture settings are read when the extension loads. The new mode selector and watchdog settings are read when `Fiber::Profiler.default` is called.
|
|
31
|
+
|
|
32
|
+
### Manual Instrumentation
|
|
33
|
+
|
|
15
34
|
Instrument your code using the default profiler:
|
|
16
35
|
|
|
17
36
|
```ruby
|
|
@@ -21,14 +40,16 @@ require "fiber/profiler"
|
|
|
21
40
|
|
|
22
41
|
profiler = Fiber::Profiler.default
|
|
23
42
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
43
|
+
begin
|
|
44
|
+
profiler&.start
|
|
45
|
+
|
|
46
|
+
# Your application code:
|
|
47
|
+
Fiber.new do
|
|
48
|
+
sleep 0.1
|
|
49
|
+
end.resume
|
|
50
|
+
ensure
|
|
51
|
+
profiler&.stop
|
|
52
|
+
end
|
|
32
53
|
```
|
|
33
54
|
|
|
34
55
|
Running this program will output the following:
|
|
@@ -41,10 +62,63 @@ Fiber stalled for 0.105 seconds
|
|
|
41
62
|
|
|
42
63
|
## Integration with Async
|
|
43
64
|
|
|
44
|
-
The fiber profiler is optionally supported by `Async`.
|
|
65
|
+
The fiber profiler is optionally supported by `Async`. Enable either mode using `FIBER_PROFILER=watchdog` or `FIBER_PROFILER=capture`. The legacy `FIBER_PROFILER_CAPTURE=true` setting continues to enable capture mode when `FIBER_PROFILER` is unset.
|
|
66
|
+
|
|
67
|
+
Each scheduler obtains its own profiler. Watchdog binds to the thread when profiling starts, so schedulers in separate worker processes or threads are monitored independently. After `fork`, inherited profiling is stopped in the child; a new scheduler starts a new profiler normally.
|
|
68
|
+
|
|
69
|
+
## Watchdog Mode
|
|
70
|
+
|
|
71
|
+
Watchdog uses a thread-specific `:fiber_switch` tracepoint to track which fiber is executing. A separate Ruby thread periodically captures the monitored thread's backtrace. It does not install method-call tracing hooks or require a heartbeat task.
|
|
72
|
+
|
|
73
|
+
By default, it samples every 100 ms, retains at most five recent stacks, and reports when the same fiber execution lasts at least 500 ms. A continued stall produces at most one report per additional threshold interval. Returning to the event loop or switching to another fiber resets the sample window. This measures individual uninterrupted fiber executions, rather than starvation of a scheduled heartbeat.
|
|
74
|
+
|
|
75
|
+
To reproduce a stall, save this as `stall.rb`:
|
|
76
|
+
|
|
77
|
+
```ruby
|
|
78
|
+
require "async"
|
|
79
|
+
|
|
80
|
+
Sync do
|
|
81
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 2
|
|
82
|
+
while Process.clock_gettime(Process::CLOCK_MONOTONIC) < deadline
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
FIBER_PROFILER=watchdog bundle exec ruby stall.rb
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The reports should point to the busy loop. Replacing the loop with `sleep(2)` allows Async to keep scheduling fibers and should produce no reports.
|
|
92
|
+
|
|
93
|
+
Terminal output contains readable stacks. Redirected output uses one JSON object per report, with `mode: "watchdog"`, process/thread/fiber identifiers, elapsed `duration`, and a bounded `samples` array. Each sample contains its elapsed time and `backtrace`. Duration is elapsed wall time since the fiber resumed, and a report is emitted while the fiber is still executing.
|
|
94
|
+
|
|
95
|
+
### Configuration
|
|
96
|
+
|
|
97
|
+
| Variable | Default | Meaning |
|
|
98
|
+
| --- | --- | --- |
|
|
99
|
+
| `FIBER_PROFILER_WATCHDOG_STALL_THRESHOLD` | `0.5` | Minimum execution duration in seconds before reporting, and minimum interval between repeated reports. |
|
|
100
|
+
| `FIBER_PROFILER_WATCHDOG_SAMPLE_INTERVAL` | `0.1` | Delay in seconds between stack samples. |
|
|
101
|
+
|
|
102
|
+
Both values must be finite and positive. Actual sampling intervals depend on Ruby and operating-system scheduling. Capture-specific settings, including its sample rate, do not configure watchdog mode.
|
|
103
|
+
|
|
104
|
+
For explicit instrumentation, use `Fiber::Profiler::Watchdog.new(stall_threshold: 0.5, sample_interval: 0.1, max_samples: 5, output: $stderr)` and the same `start`/`stop` lifecycle as capture. The output must support writes from the watchdog thread. Stop the profiler before closing its output.
|
|
105
|
+
|
|
106
|
+
If sampling or reporting raises a `StandardError`, the watchdog disables its tracepoint and stops sampling. It retains the exception in `watchdog.error` and attempts one warning to standard error. Failure to write that warning is also contained. These errors do not escape through `stop` or replace application errors. Call `stop` normally to finish cleanup; a subsequent `start` clears the error and resumes monitoring.
|
|
107
|
+
|
|
108
|
+
### Interpretation and Limits
|
|
109
|
+
|
|
110
|
+
Repeated frames identify code worth investigating: optimize expensive work, add cooperative yield points, or offload suitable work to a bounded thread pool. Sampling still has overhead; measure it for your workload.
|
|
111
|
+
|
|
112
|
+
The repository includes a [Falcon overhead benchmark](https://github.com/socketry/fiber-profiler/tree/main/benchmark/falcon) comparing disabled, watchdog, and capture modes. It measures throughput, latency, server CPU time, and Ruby allocations across repeated runs. Use it as a starting point for measuring your own application before enabling continuous monitoring.
|
|
113
|
+
|
|
114
|
+
The watchdog requires Ruby thread scheduling. Native code that holds the GVL without allowing other threads to run can prevent sampling entirely. Short stalls can occur between samples, and normal scheduler/OS delays can extend measured execution time. A report is a diagnostic lead, not proof that every sampled frame is expensive.
|
|
115
|
+
|
|
116
|
+
When profiling starts on a blocking event-loop fiber, that fiber is excluded from monitoring, including its normal idle waits. Other fibers, including blocking application fibers, are monitored. When profiling starts inside a non-blocking application fiber, blocking fibers are excluded because the event-loop fiber is not known. Start and stop profiling on the monitored thread.
|
|
45
117
|
|
|
46
118
|
## Default Environment Variables
|
|
47
119
|
|
|
120
|
+
The following settings apply to capture mode only.
|
|
121
|
+
|
|
48
122
|
### `FIBER_PROFILER_CAPTURE`
|
|
49
123
|
|
|
50
124
|
Set to `true` to enable capturing of stalled fibers.
|
|
@@ -70,3 +144,5 @@ $ bundle exec bake input --file samples.ndjson fiber:profiler:analyze output
|
|
|
70
144
|
```
|
|
71
145
|
|
|
72
146
|
This will aggregate all the call logs and generate a short summary, ordered by duration.
|
|
147
|
+
|
|
148
|
+
The analyzer consumes capture-mode call timings. Watchdog reports contain sampled backtraces instead of call timings and are not aggregated by this command.
|
|
@@ -184,7 +184,7 @@ static void Fiber_Profiler_Capture_free(void *ptr) {
|
|
|
184
184
|
Fiber_Profiler_Stream_free(&capture->stream);
|
|
185
185
|
Fiber_Profiler_Deque_free(&capture->calls);
|
|
186
186
|
|
|
187
|
-
|
|
187
|
+
xfree(capture);
|
|
188
188
|
}
|
|
189
189
|
|
|
190
190
|
static size_t Fiber_Profiler_Capture_memsize(const void *ptr) {
|
|
@@ -271,7 +271,9 @@ VALUE Fiber_Profiler_Capture_initialize(int argc, VALUE *argv, VALUE self) {
|
|
|
271
271
|
|
|
272
272
|
VALUE arguments[5] = {0};
|
|
273
273
|
VALUE options = Qnil;
|
|
274
|
-
|
|
274
|
+
if (argc > 0) {
|
|
275
|
+
rb_scan_args(argc, argv, ":", &options);
|
|
276
|
+
}
|
|
275
277
|
rb_get_kwargs(options, Fiber_Profiler_Capture_initialize_options, 0, 5, arguments);
|
|
276
278
|
|
|
277
279
|
if (arguments[0] != Qundef) {
|
|
@@ -4,41 +4,11 @@
|
|
|
4
4
|
# Copyright, 2025-2026, by Samuel Williams.
|
|
5
5
|
|
|
6
6
|
require_relative "native"
|
|
7
|
-
require_relative "
|
|
7
|
+
require_relative "thread_local"
|
|
8
8
|
|
|
9
9
|
module Fiber::Profiler
|
|
10
|
-
# Thread-local storage for the active profiler capture.
|
|
11
|
-
::Thread.attr_accessor :fiber_profiler_capture
|
|
12
|
-
|
|
13
|
-
# Private module that wraps start/stop to manage thread-local storage.
|
|
14
|
-
module ThreadLocalCapture
|
|
15
|
-
# Start profiling and store the capture in the current thread's thread-local storage.
|
|
16
|
-
def start
|
|
17
|
-
result = super
|
|
18
|
-
|
|
19
|
-
if result
|
|
20
|
-
Thread.current.fiber_profiler_capture = self
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
return result
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
# Stop profiling and clear the capture from the current thread's thread-local storage.
|
|
27
|
-
def stop
|
|
28
|
-
result = super
|
|
29
|
-
|
|
30
|
-
if result
|
|
31
|
-
Thread.current.fiber_profiler_capture = nil
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
return result
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
private_constant :ThreadLocalCapture
|
|
39
|
-
|
|
40
10
|
# Represents a running profiler capture.
|
|
41
11
|
class Capture
|
|
42
|
-
prepend
|
|
12
|
+
prepend ThreadLocalProfiler
|
|
43
13
|
end
|
|
44
14
|
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2025-2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require_relative "fork_handler"
|
|
7
|
+
|
|
8
|
+
module Fiber::Profiler
|
|
9
|
+
# Thread-local storage for the active profiler, shared by both implementations.
|
|
10
|
+
::Thread.attr_accessor :fiber_profiler_capture
|
|
11
|
+
|
|
12
|
+
::Process.singleton_class.prepend(ForkHandler)
|
|
13
|
+
|
|
14
|
+
# Manages the active profiler so it can be stopped after a fork.
|
|
15
|
+
module ThreadLocalProfiler
|
|
16
|
+
# Start profiling on the current thread.
|
|
17
|
+
def start
|
|
18
|
+
result = super
|
|
19
|
+
Thread.current.fiber_profiler_capture = self if result
|
|
20
|
+
result
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Stop profiling and clear the current thread's reference.
|
|
24
|
+
def stop
|
|
25
|
+
super
|
|
26
|
+
ensure
|
|
27
|
+
if Thread.current.fiber_profiler_capture.equal?(self)
|
|
28
|
+
Thread.current.fiber_profiler_capture = nil
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private_constant :ThreadLocalProfiler
|
|
34
|
+
end
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require_relative "version"
|
|
7
|
+
require_relative "thread_local"
|
|
8
|
+
require "json"
|
|
9
|
+
|
|
10
|
+
module Fiber::Profiler
|
|
11
|
+
# Samples stacks while a fiber runs without switching back to the event loop.
|
|
12
|
+
class Watchdog
|
|
13
|
+
prepend ThreadLocalProfiler
|
|
14
|
+
|
|
15
|
+
# Build a watchdog using environment configuration.
|
|
16
|
+
# @parameter env [Hash] The environment to read configuration from.
|
|
17
|
+
# @returns [Watchdog] The configured watchdog.
|
|
18
|
+
def self.default(env = ENV)
|
|
19
|
+
new(
|
|
20
|
+
stall_threshold: Float(env.fetch("FIBER_PROFILER_WATCHDOG_STALL_THRESHOLD", "0.5")),
|
|
21
|
+
sample_interval: Float(env.fetch("FIBER_PROFILER_WATCHDOG_SAMPLE_INTERVAL", "0.1"))
|
|
22
|
+
)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Initialize a watchdog. Monitoring begins when {start} is called.
|
|
26
|
+
# @parameter stall_threshold [Float] Seconds without a fiber switch before reporting, and the minimum interval between reports for the same execution.
|
|
27
|
+
# @parameter sample_interval [Float] Seconds between stack samples.
|
|
28
|
+
# @parameter max_samples [Integer] Maximum number of recent stacks retained per execution.
|
|
29
|
+
# @parameter output [IO] The destination for stall reports.
|
|
30
|
+
def initialize(stall_threshold: 0.5, sample_interval: 0.1, max_samples: 5, output: $stderr)
|
|
31
|
+
@stall_threshold = Float(stall_threshold)
|
|
32
|
+
@sample_interval = Float(sample_interval)
|
|
33
|
+
|
|
34
|
+
unless @stall_threshold.finite? && @stall_threshold.positive? && @sample_interval.finite? && @sample_interval.positive?
|
|
35
|
+
raise ArgumentError, "Watchdog intervals must be finite and positive"
|
|
36
|
+
end
|
|
37
|
+
unless max_samples.is_a?(Integer) && max_samples.positive?
|
|
38
|
+
raise ArgumentError, "max_samples must be a positive integer"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
@max_samples = max_samples
|
|
42
|
+
@output = output
|
|
43
|
+
@running = false
|
|
44
|
+
@stalls = 0
|
|
45
|
+
@error = nil
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @attribute [Float] The minimum execution duration before reporting a stall.
|
|
49
|
+
attr_reader :stall_threshold
|
|
50
|
+
|
|
51
|
+
# @attribute [Float] The delay between samples.
|
|
52
|
+
attr_reader :sample_interval
|
|
53
|
+
|
|
54
|
+
# @attribute [Integer] The number of reports written.
|
|
55
|
+
attr_reader :stalls
|
|
56
|
+
|
|
57
|
+
# @attribute [StandardError | Nil] The sampling or reporting failure, retained until monitoring starts again.
|
|
58
|
+
attr_reader :error
|
|
59
|
+
|
|
60
|
+
# Start monitoring application fibers on the calling thread.
|
|
61
|
+
# @returns [Watchdog | false] Self, or false if already running.
|
|
62
|
+
def start
|
|
63
|
+
return false if @running
|
|
64
|
+
|
|
65
|
+
@thread = Thread.current
|
|
66
|
+
@pid = Process.pid
|
|
67
|
+
@mutex = Mutex.new
|
|
68
|
+
@condition = ConditionVariable.new
|
|
69
|
+
@running = true
|
|
70
|
+
@execution = nil
|
|
71
|
+
@error = nil
|
|
72
|
+
|
|
73
|
+
# When started on the event loop, distinguish it from blocking application fibers:
|
|
74
|
+
@loop = Fiber.current if Fiber.current.blocking?
|
|
75
|
+
@tracepoint = TracePoint.new(:fiber_switch){record_execution}
|
|
76
|
+
@tracepoint.enable(target_thread: @thread)
|
|
77
|
+
record_execution
|
|
78
|
+
|
|
79
|
+
@watchdog = Thread.new{watch}
|
|
80
|
+
self
|
|
81
|
+
rescue Exception
|
|
82
|
+
stop
|
|
83
|
+
raise
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Stop monitoring and wait for the sampling thread to finish.
|
|
87
|
+
# @returns [Watchdog | false] Self, or false if already stopped.
|
|
88
|
+
def stop
|
|
89
|
+
return false unless @running
|
|
90
|
+
|
|
91
|
+
@tracepoint&.disable
|
|
92
|
+
|
|
93
|
+
if @pid == Process.pid
|
|
94
|
+
@mutex.synchronize do
|
|
95
|
+
@running = false
|
|
96
|
+
@condition.broadcast
|
|
97
|
+
end
|
|
98
|
+
@watchdog&.join
|
|
99
|
+
else
|
|
100
|
+
# The sampling thread does not survive fork; avoid inherited synchronization:
|
|
101
|
+
@running = false
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
self
|
|
105
|
+
ensure
|
|
106
|
+
@watchdog = @thread = @execution = @loop = @tracepoint = nil
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
private
|
|
110
|
+
|
|
111
|
+
def now
|
|
112
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def record_execution
|
|
116
|
+
fiber = Fiber.current
|
|
117
|
+
idle = @loop ? fiber.equal?(@loop) : fiber.blocking?
|
|
118
|
+
|
|
119
|
+
@mutex.synchronize do
|
|
120
|
+
@execution = idle ? nil : [fiber, now]
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def wait
|
|
125
|
+
@mutex.synchronize do
|
|
126
|
+
@condition.wait(@mutex, @sample_interval) if @running
|
|
127
|
+
@running
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def watch
|
|
132
|
+
execution = nil
|
|
133
|
+
samples = []
|
|
134
|
+
next_report = @stall_threshold
|
|
135
|
+
|
|
136
|
+
while wait
|
|
137
|
+
current = @mutex.synchronize{@execution}
|
|
138
|
+
unless current.equal?(execution)
|
|
139
|
+
execution = current
|
|
140
|
+
samples.clear
|
|
141
|
+
next_report = @stall_threshold
|
|
142
|
+
end
|
|
143
|
+
next unless execution
|
|
144
|
+
|
|
145
|
+
backtrace = @thread.backtrace
|
|
146
|
+
duration = now - execution[1]
|
|
147
|
+
|
|
148
|
+
# Discard samples if the target switched fibers while capturing its stack:
|
|
149
|
+
next unless backtrace && @mutex.synchronize{@execution.equal?(execution)}
|
|
150
|
+
|
|
151
|
+
samples << {elapsed: duration, backtrace: backtrace}
|
|
152
|
+
samples.shift if samples.size > @max_samples
|
|
153
|
+
|
|
154
|
+
if duration >= next_report
|
|
155
|
+
report(execution[0], duration, samples)
|
|
156
|
+
@stalls += 1
|
|
157
|
+
next_report = duration + @stall_threshold
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
rescue StandardError => error
|
|
161
|
+
# A diagnostic failure must not escape into the monitored application:
|
|
162
|
+
@error = error
|
|
163
|
+
@tracepoint.disable
|
|
164
|
+
warn_failure(error)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def warn_failure(error)
|
|
168
|
+
warn "Fiber::Profiler::Watchdog disabled: #{error.class}: #{error.message}"
|
|
169
|
+
rescue StandardError
|
|
170
|
+
# The diagnostic destination may also be unavailable; retain the original error:
|
|
171
|
+
nil
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def report(fiber, duration, samples)
|
|
175
|
+
if @output.respond_to?(:tty?) && @output.tty?
|
|
176
|
+
message = "## Fiber stalled for at least %.3f seconds (watchdog, pid=%d, thread=%d, fiber=%d)\n" % [duration, @pid, @thread.object_id, fiber.object_id]
|
|
177
|
+
message << samples.map{|sample| sample[:backtrace].join("\n")}.join("\n\n") << "\n"
|
|
178
|
+
else
|
|
179
|
+
message = JSON.generate(mode: "watchdog", pid: @pid, thread_id: @thread.object_id, fiber_id: fiber.object_id, duration: duration, samples: samples) << "\n"
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
@output.write(message)
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
data/lib/fiber/profiler.rb
CHANGED
|
@@ -4,17 +4,30 @@
|
|
|
4
4
|
# Copyright, 2025-2026, by Samuel Williams.
|
|
5
5
|
|
|
6
6
|
require_relative "profiler/version"
|
|
7
|
-
require_relative "profiler/
|
|
8
|
-
require_relative "profiler/fork_handler"
|
|
7
|
+
require_relative "profiler/capture"
|
|
9
8
|
|
|
10
9
|
module Fiber::Profiler
|
|
11
10
|
# The default profiler to use, if any.
|
|
12
11
|
#
|
|
13
|
-
#
|
|
12
|
+
# Set `FIBER_PROFILER` to `watchdog`, `capture`, or `false`. When unset,
|
|
13
|
+
# the legacy `FIBER_PROFILER_CAPTURE=true` setting enables capture mode.
|
|
14
14
|
#
|
|
15
|
-
# @returns [Capture | Nil]
|
|
15
|
+
# @returns [Capture | Watchdog | Nil]
|
|
16
|
+
# @raises [ArgumentError] If the requested mode is unknown.
|
|
16
17
|
def self.default
|
|
17
|
-
|
|
18
|
+
case mode = ENV["FIBER_PROFILER"]
|
|
19
|
+
when nil
|
|
20
|
+
Capture.default
|
|
21
|
+
when "capture"
|
|
22
|
+
Capture.new
|
|
23
|
+
when "watchdog"
|
|
24
|
+
require_relative "profiler/watchdog"
|
|
25
|
+
Watchdog.default
|
|
26
|
+
when "false"
|
|
27
|
+
nil
|
|
28
|
+
else
|
|
29
|
+
raise ArgumentError, "Unknown FIBER_PROFILER mode: #{mode.inspect} (expected watchdog, capture, or false)"
|
|
30
|
+
end
|
|
18
31
|
end
|
|
19
32
|
|
|
20
33
|
# Execute the given block with the {default} profiler, if any.
|
data/readme.md
CHANGED
|
@@ -18,6 +18,10 @@ Please see the [project documentation](https://socketry.github.io/fiber-profiler
|
|
|
18
18
|
|
|
19
19
|
Please see the [project releases](https://socketry.github.io/fiber-profiler/releases/index) for all releases.
|
|
20
20
|
|
|
21
|
+
### v0.7.0
|
|
22
|
+
|
|
23
|
+
- Sign releases with the Socketry Ruby Gems certificate.
|
|
24
|
+
|
|
21
25
|
### v0.6.0
|
|
22
26
|
|
|
23
27
|
- Fixed compatibility with `Process.fork`.
|
|
@@ -50,12 +54,14 @@ bundle exec sus
|
|
|
50
54
|
|
|
51
55
|
### Making Releases
|
|
52
56
|
|
|
53
|
-
To
|
|
57
|
+
To prepare a release branch and open a pull request from an up-to-date `main`:
|
|
54
58
|
|
|
55
59
|
``` shell
|
|
56
|
-
bundle exec bake gem:release:patch # or minor or major
|
|
60
|
+
bundle exec bake gem:github:release:patch # or minor or major
|
|
57
61
|
```
|
|
58
62
|
|
|
63
|
+
After the release PR is merged, GitHub Actions automatically publishes the verified gem through the `rubygems` environment. See [bake-gem-github](https://github.com/socketry/bake-gem-github) for setup, remote releases, and recovery.
|
|
64
|
+
|
|
59
65
|
### Developer Certificate of Origin
|
|
60
66
|
|
|
61
67
|
In order to protect users of this project, we require all contributors to comply with the [Developer Certificate of Origin](https://developercertificate.org/). This ensures that all contributions are properly licensed and attributed.
|
data/release.cert
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
-----BEGIN CERTIFICATE-----
|
|
2
|
+
MIIEFDCCAnygAwIBAgIBATANBgkqhkiG9w0BAQsFADAwMREwDwYDVQQKDAhTb2Nr
|
|
3
|
+
ZXRyeTEbMBkGA1UEAwwSU29ja2V0cnkgUnVieSBHZW1zMB4XDTI2MDkyMTA3NTgx
|
|
4
|
+
NFoXDTI3MDkyMTA3NTgxNFowMDERMA8GA1UECgwIU29ja2V0cnkxGzAZBgNVBAMM
|
|
5
|
+
ElNvY2tldHJ5IFJ1YnkgR2VtczCCAaIwDQYJKoZIhvcNAQEBBQADggGPADCCAYoC
|
|
6
|
+
ggGBAM/QgjVgDzDo/xJEQoFvAzcVFP7msnswkhPJB2UDsbxXCZmers5jV512TM5s
|
|
7
|
+
NwLHfzJiC4DcI5ax9ZYKM9Q+dS21YYagNqjtg3YPyDqR6phoibEA0VoMuInUQW68
|
|
8
|
+
i5OkCJzYKGD2pYYVZnuFNqyM2ECUvXh/fBmvPoHbncAHhWPaCBH8mQJ8sRNc6+RV
|
|
9
|
+
SoZZu1Yo/aW1zQ+SpZwad7s1ZmigfDNHKhJ22KwZHk+/5Zw1QXVfcajjswV0Mm2P
|
|
10
|
+
irgjKN3fyNVkwlly63EBlR4VydT+g9QtJtqh7ee0ThhI+v4wYovf4dbLNcEBTHu1
|
|
11
|
+
x1pGbXGx/2fBAST5eVwaXIx1V8VU22AJcBk1H2o5/1F0HfbD2HQjmFOA112KUwBW
|
|
12
|
+
9TtspZD3g6rCQFX53XvL9h6j2y1ukl7s5AdEzOe/x6r1MO8tSDtgcYoF89wgF/88
|
|
13
|
+
Q/Eski0FckQ+znfESZcFcpzs2sPDoYW6SNVX4tZcNty85Y6WBHfHKio14x63PeyC
|
|
14
|
+
CG1gMwIDAQABozkwNzAJBgNVHRMEAjAAMAsGA1UdDwQEAwIEsDAdBgNVHQ4EFgQU
|
|
15
|
+
SuMc0x24Xshtaa8SQVkBr2Z5WzIwDQYJKoZIhvcNAQELBQADggGBABI62ey5lDWm
|
|
16
|
+
j8jsTQaNBMBb7LbKS3XkBwyL1UNGEwlicp399rJDeuNgVSHqBCZ/nE4yleNjgW9Y
|
|
17
|
+
N6DKdxXCvXmFUoHvUINeAvThHxmlEGhSfXl1x55xHEig0rEP4jgdjnYdQI4fqeOM
|
|
18
|
+
zIDN0F3ruArke4Y62SQgWVN7vspdAA41hBIRl1vsvs0KWG8DIVQ8cmR+TG6zjOmK
|
|
19
|
+
iUiUZGNPnStV1O1xW83c+Ba4Am0krP6/forxDPhRvTehqkVYvMdPfYcICNb5IM58
|
|
20
|
+
5vaYvuj5AtfZuZWN0F/0KtSphhNC17rh6h8QmuYdMJ1clvlrkiEaO1UjPVwjgtxC
|
|
21
|
+
x/ARh5X6q0/YTXl4r7EfSJEeqW7qhgu9EUj3mGIPHYdX0Dqih/NgK20F3GvQtTfB
|
|
22
|
+
7sse0duKBHXONYVwKAsceUVvqtCfyF608bCilCkbhw3QaInRj+BkRQKJ5Tuj2PZR
|
|
23
|
+
76a/hwaV2wWn5RtAFBaUQzQdo/xTqJ7IkwaFjOZe/QyvyoRjCK9Rdg==
|
|
24
|
+
-----END CERTIFICATE-----
|
data/releases.md
CHANGED
data.tar.gz.sig
CHANGED
|
Binary file
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: fiber-profiler
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Samuel Williams
|
|
@@ -9,35 +9,45 @@ bindir: bin
|
|
|
9
9
|
cert_chain:
|
|
10
10
|
- |
|
|
11
11
|
-----BEGIN CERTIFICATE-----
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
JkxfQWC+jBVeG9ZtPhQgZpfhvh+6hMhraUYRQ6XGyvBqEUe+yo6DKIT3MtGE2+CP
|
|
35
|
-
eX9i9ZWBydWb8/rvmwmX2kkcBbX0hZS1rcR593hGc61JR6lvkGYQ2MYskBveyaxt
|
|
36
|
-
Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
|
|
37
|
-
voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
|
|
12
|
+
MIIEFDCCAnygAwIBAgIBATANBgkqhkiG9w0BAQsFADAwMREwDwYDVQQKDAhTb2Nr
|
|
13
|
+
ZXRyeTEbMBkGA1UEAwwSU29ja2V0cnkgUnVieSBHZW1zMB4XDTI2MDkyMTA3NTgx
|
|
14
|
+
NFoXDTI3MDkyMTA3NTgxNFowMDERMA8GA1UECgwIU29ja2V0cnkxGzAZBgNVBAMM
|
|
15
|
+
ElNvY2tldHJ5IFJ1YnkgR2VtczCCAaIwDQYJKoZIhvcNAQEBBQADggGPADCCAYoC
|
|
16
|
+
ggGBAM/QgjVgDzDo/xJEQoFvAzcVFP7msnswkhPJB2UDsbxXCZmers5jV512TM5s
|
|
17
|
+
NwLHfzJiC4DcI5ax9ZYKM9Q+dS21YYagNqjtg3YPyDqR6phoibEA0VoMuInUQW68
|
|
18
|
+
i5OkCJzYKGD2pYYVZnuFNqyM2ECUvXh/fBmvPoHbncAHhWPaCBH8mQJ8sRNc6+RV
|
|
19
|
+
SoZZu1Yo/aW1zQ+SpZwad7s1ZmigfDNHKhJ22KwZHk+/5Zw1QXVfcajjswV0Mm2P
|
|
20
|
+
irgjKN3fyNVkwlly63EBlR4VydT+g9QtJtqh7ee0ThhI+v4wYovf4dbLNcEBTHu1
|
|
21
|
+
x1pGbXGx/2fBAST5eVwaXIx1V8VU22AJcBk1H2o5/1F0HfbD2HQjmFOA112KUwBW
|
|
22
|
+
9TtspZD3g6rCQFX53XvL9h6j2y1ukl7s5AdEzOe/x6r1MO8tSDtgcYoF89wgF/88
|
|
23
|
+
Q/Eski0FckQ+znfESZcFcpzs2sPDoYW6SNVX4tZcNty85Y6WBHfHKio14x63PeyC
|
|
24
|
+
CG1gMwIDAQABozkwNzAJBgNVHRMEAjAAMAsGA1UdDwQEAwIEsDAdBgNVHQ4EFgQU
|
|
25
|
+
SuMc0x24Xshtaa8SQVkBr2Z5WzIwDQYJKoZIhvcNAQELBQADggGBABI62ey5lDWm
|
|
26
|
+
j8jsTQaNBMBb7LbKS3XkBwyL1UNGEwlicp399rJDeuNgVSHqBCZ/nE4yleNjgW9Y
|
|
27
|
+
N6DKdxXCvXmFUoHvUINeAvThHxmlEGhSfXl1x55xHEig0rEP4jgdjnYdQI4fqeOM
|
|
28
|
+
zIDN0F3ruArke4Y62SQgWVN7vspdAA41hBIRl1vsvs0KWG8DIVQ8cmR+TG6zjOmK
|
|
29
|
+
iUiUZGNPnStV1O1xW83c+Ba4Am0krP6/forxDPhRvTehqkVYvMdPfYcICNb5IM58
|
|
30
|
+
5vaYvuj5AtfZuZWN0F/0KtSphhNC17rh6h8QmuYdMJ1clvlrkiEaO1UjPVwjgtxC
|
|
31
|
+
x/ARh5X6q0/YTXl4r7EfSJEeqW7qhgu9EUj3mGIPHYdX0Dqih/NgK20F3GvQtTfB
|
|
32
|
+
7sse0duKBHXONYVwKAsceUVvqtCfyF608bCilCkbhw3QaInRj+BkRQKJ5Tuj2PZR
|
|
33
|
+
76a/hwaV2wWn5RtAFBaUQzQdo/xTqJ7IkwaFjOZe/QyvyoRjCK9Rdg==
|
|
38
34
|
-----END CERTIFICATE-----
|
|
39
35
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
40
|
-
dependencies:
|
|
36
|
+
dependencies:
|
|
37
|
+
- !ruby/object:Gem::Dependency
|
|
38
|
+
name: json
|
|
39
|
+
requirement: !ruby/object:Gem::Requirement
|
|
40
|
+
requirements:
|
|
41
|
+
- - ">="
|
|
42
|
+
- !ruby/object:Gem::Version
|
|
43
|
+
version: '0'
|
|
44
|
+
type: :runtime
|
|
45
|
+
prerelease: false
|
|
46
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
47
|
+
requirements:
|
|
48
|
+
- - ">="
|
|
49
|
+
- !ruby/object:Gem::Version
|
|
50
|
+
version: '0'
|
|
41
51
|
executables: []
|
|
42
52
|
extensions:
|
|
43
53
|
- ext/extconf.rb
|
|
@@ -60,9 +70,12 @@ files:
|
|
|
60
70
|
- lib/fiber/profiler/capture.rb
|
|
61
71
|
- lib/fiber/profiler/fork_handler.rb
|
|
62
72
|
- lib/fiber/profiler/native.rb
|
|
73
|
+
- lib/fiber/profiler/thread_local.rb
|
|
63
74
|
- lib/fiber/profiler/version.rb
|
|
75
|
+
- lib/fiber/profiler/watchdog.rb
|
|
64
76
|
- license.md
|
|
65
77
|
- readme.md
|
|
78
|
+
- release.cert
|
|
66
79
|
- releases.md
|
|
67
80
|
homepage: https://github.com/socketry/fiber-profiler
|
|
68
81
|
licenses:
|
|
@@ -84,7 +97,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
84
97
|
- !ruby/object:Gem::Version
|
|
85
98
|
version: '0'
|
|
86
99
|
requirements: []
|
|
87
|
-
rubygems_version: 4.0.
|
|
100
|
+
rubygems_version: 4.0.21
|
|
88
101
|
specification_version: 4
|
|
89
102
|
summary: A fiber stall profiler.
|
|
90
103
|
test_files: []
|
metadata.gz.sig
CHANGED
|
Binary file
|