fiber-profiler 0.6.1 → 0.7.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1a4ef2a5d3b1f833810953f991fbbe8aac342b9e757c5ebfadfd695d87036558
4
- data.tar.gz: '012498a134f5b2c22fad0761090514d577d3ee36143ef568c5e0617b74a46d24'
3
+ metadata.gz: e3d1446f213526360f5368bfb8a08884033484f4ab868160340709652349c25c
4
+ data.tar.gz: 844c5319c20f8b0929342978790efe2d1aeb4dd37ae260cd073ff36c82c730fd
5
5
  SHA512:
6
- metadata.gz: 04c655663d123aa94f15f83d7b6a70ab9d81074fe18fb1d870f358756d19a7e91b4eaac4b0b832ed6c649397bf10e4e41b2342abfec1e7bb8e0c652f98630334
7
- data.tar.gz: 3529cf9a591bcfdd81dcf1a07d71401ab07322e475ec7a0b34b1d3d73080595cbc2b81419c1c7d9abe6341d163bcaaf1ac2017aed9e5d0d7af1847d15d704a88
6
+ metadata.gz: '08e564d3dc9c9abfba57056f3ff5a7e0fce516a5969ee014820ff2dac72b60fd51603c258c21cbee80c46d686f9ca9bbd2376a11b1202635371d0201b8a3b372'
7
+ data.tar.gz: 88d96fba98c5b9578683ad7f13cb898d70e8da83ce11a3b756b15d29b4cbc29649a61c7ec2d589cb9a1185e9646bee4ed018bbcbb1946defdd2e7163ae951aa8
checksums.yaml.gz.sig CHANGED
Binary file
@@ -0,0 +1,80 @@
1
+ # Capture Mode
2
+
3
+ This guide explains how to trace fiber execution and analyze call timings with the capture profiler.
4
+
5
+ Use capture mode when you need to investigate the calls made during a stall. It records call timings for sampled fiber executions and reports after the fiber switches away. For sampled backtraces while a stall is still in progress, see [Watchdog Mode](../watchdog-mode/index).
6
+
7
+ ## Usage
8
+
9
+ Add `fiber-profiler` to your application's bundle as described in [Getting Started](../getting-started/index), then select capture mode when starting Async or Falcon:
10
+
11
+ ```bash
12
+ FIBER_PROFILER=capture bundle exec falcon serve
13
+ ```
14
+
15
+ Async starts and stops capture automatically. The legacy `FIBER_PROFILER_CAPTURE=true` setting also enables capture when `FIBER_PROFILER` is unset. An explicit `FIBER_PROFILER` value takes precedence.
16
+
17
+ ### Manual Instrumentation
18
+
19
+ To reproduce a stall without a scheduler, save this as `capture.rb`. It starts a {ruby Fiber::Profiler::Capture} and simulates a blocking operation inside a fiber:
20
+
21
+ ```ruby
22
+ require "fiber/profiler"
23
+
24
+ profiler = Fiber::Profiler::Capture.new
25
+
26
+ begin
27
+ profiler.start
28
+
29
+ # Simulate a blocking operation without a scheduler:
30
+ Fiber.new(blocking: false) do
31
+ sleep 0.1
32
+ end.resume
33
+ ensure
34
+ profiler.stop
35
+ end
36
+ ```
37
+
38
+ ```bash
39
+ bundle exec ruby capture.rb
40
+ ```
41
+
42
+ This example starts the profiler explicitly, so it does not need `FIBER_PROFILER=capture`. The report should include `Kernel#sleep` and its elapsed duration. Start and stop profiling on the same thread.
43
+
44
+ ## Configuration
45
+
46
+ Set these environment variables before loading `fiber-profiler`; the native extension reads them when it loads:
47
+
48
+ | Variable | Default | Meaning |
49
+ | --- | --- | --- |
50
+ | `FIBER_PROFILER_CAPTURE_STALL_THRESHOLD` | `0.01` | Minimum execution duration in seconds to exceed before reporting a stall. |
51
+ | `FIBER_PROFILER_CAPTURE_FILTER_THRESHOLD` | 10% of the stall threshold | Filter calls shorter than this duration in seconds. |
52
+ | `FIBER_PROFILER_CAPTURE_TRACK_CALLS` | `true` | Record call timings. Set to `false` to report stall durations without tracing calls. |
53
+ | `FIBER_PROFILER_CAPTURE_SAMPLE_RATE` | `1.0` | Fraction of eligible fiber executions to sample: `1.0` samples all, `0.1` samples approximately 10%. |
54
+
55
+ These settings apply to capture mode only. The sample rate controls selection at fiber switches; it is not a time interval between backtrace samples.
56
+
57
+ For explicit instrumentation, override the defaults with `Fiber::Profiler::Capture.new(stall_threshold: 0.05, filter_threshold: 0.005, track_calls: true, sample_rate: 0.1)`. Pass `output:` to select a writable IO; the default is standard error.
58
+
59
+ ## Reading Reports
60
+
61
+ When standard error is a terminal, capture prints a readable call log. Redirected output uses one JSON object per stall, including the execution `duration` and a `calls` array. Each retained call includes its source location, class, method, duration, and nesting information.
62
+
63
+ To collect the example's reports and summarize call timings:
64
+
65
+ ```bash
66
+ bundle exec ruby capture.rb 2> capture.ndjson
67
+ bundle exec bake input --file capture.ndjson fiber:profiler:analyze output
68
+ ```
69
+
70
+ The analyzer aggregates call durations by source location and sorts the summary by total duration. Feed it capture reports; unrelated application output on standard error must be separated from the JSON first. With `track_calls: false`, reports contain no call timings to aggregate.
71
+
72
+ Call durations can include time spent in nested calls, so adding durations across different locations does not give total application runtime. Short or uninformative calls may be filtered from the report.
73
+
74
+ ## Interpretation and Limits
75
+
76
+ Capture samples non-blocking fibers and excludes blocking fibers, including the event-loop fiber. It measures wall time between fiber switches, so an ordinary scheduler-aware wait that yields does not count its entire wait time as a stall. A blocking operation that keeps the same fiber executing can count toward a stall.
77
+
78
+ Reports are emitted when the fiber switches away. An operation that never yields or returns can therefore prevent its capture report from appearing. Use [Watchdog Mode](../watchdog-mode/index) to investigate an ongoing stall when Ruby thread scheduling is still possible.
79
+
80
+ Call tracing can substantially affect performance. Reduce the sample rate to trace fewer executions, or disable call tracking if you only need stall durations. Use the [Falcon overhead benchmark](https://github.com/socketry/fiber-profiler/tree/main/benchmark/falcon) to compare modes, and measure with your application's Ruby/JIT configuration and workload.
@@ -1,6 +1,6 @@
1
1
  # Getting Started
2
2
 
3
- This guide explains how to detect stalls using the fiber profiler.
3
+ This guide explains how to install the fiber profiler and choose a mode for diagnosing event-loop stalls.
4
4
 
5
5
  ## Installation
6
6
 
@@ -10,63 +10,34 @@ Add the gem to your project:
10
10
  $ bundle add fiber-profiler
11
11
  ```
12
12
 
13
- ## Usage
13
+ ## Choose a Mode
14
14
 
15
- Instrument your code using the default profiler:
15
+ A fiber that runs for too long without yielding prevents other work on the same event loop from progressing. The profiler offers two ways to investigate it:
16
16
 
17
- ```ruby
18
- #!/usr/bin/env ruby
17
+ | Mode | Use it to | Reports |
18
+ | --- | --- | --- |
19
+ | [Watchdog](../watchdog-mode/index) | Find where a fiber is spending time during an ongoing stall. | Periodically sampled backtraces while the fiber is still executing. |
20
+ | [Capture](../capture-mode/index) | Investigate individual call timings during a fiber's execution. | Traced calls and their durations after the fiber switches away. |
19
21
 
20
- require "fiber/profiler"
22
+ Watchdog avoids method-call tracing and is a useful starting point for observing sustained stalls. Capture provides more detail at a higher instrumentation cost. Both affect the application being measured; see the [Falcon overhead benchmark](https://github.com/socketry/fiber-profiler/tree/main/benchmark/falcon) and measure with your own workload.
21
23
 
22
- profiler = Fiber::Profiler.default
24
+ ## Integration with Async and Falcon
23
25
 
24
- profiler&.start
25
-
26
- # Your application code:
27
- Fiber.new do
28
- sleep 0.1
29
- end.resume
30
-
31
- profiler&.stop
32
- ```
33
-
34
- Running this program will output the following:
26
+ Select the profiling mode when starting your application:
35
27
 
36
28
  ```bash
37
- $ FIBER_PROFILER_CAPTURE=true bundle exec ./test.rb
38
- Fiber stalled for 0.105 seconds
39
- /Users/samuel/Developer/socketry/fiber-profiler/test.rb:11 in c-call 'Kernel#sleep' (0.105s)
29
+ FIBER_PROFILER=watchdog bundle exec falcon serve
40
30
  ```
41
31
 
42
- ## Integration with Async
43
-
44
- The fiber profiler is optionally supported by `Async`. Simply enable the profiler using `FIBER_PROFILER_CAPTURE=true` to capture and report stalls.
45
-
46
- ## Default Environment Variables
47
-
48
- ### `FIBER_PROFILER_CAPTURE`
49
-
50
- Set to `true` to enable capturing of stalled fibers.
51
-
52
- ### `FIBER_PROFILER_CAPTURE_STALL_THRESHOLD`
32
+ 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.
53
33
 
54
- Set the threshold in seconds for reporting a stalled fiber. Default is `0.01`.
34
+ | Setting | Behaviour |
35
+ | --- | --- |
36
+ | `FIBER_PROFILER=watchdog` | Periodically sample stacks during sustained fiber execution. |
37
+ | `FIBER_PROFILER=capture` | Trace calls and report fibers that exceed the capture threshold. |
38
+ | `FIBER_PROFILER=false` | Disable the default profiler, even if the legacy capture flag is enabled. |
39
+ | `FIBER_PROFILER` unset | Preserve the existing `FIBER_PROFILER_CAPTURE=true` behaviour. |
55
40
 
56
- ### `FIBER_PROFILER_CAPTURE_TRACK_CALLS`
57
-
58
- Set to `true` to track calls within the fiber. Default is `true`. This can be disabled to reduce overhead.
59
-
60
- ### `FIBER_PROFILER_CAPTURE_SAMPLE_RATE`
61
-
62
- Set the sample rate of the profiler as a percentage of all context switches. The default is 1.0 (100%).
63
-
64
- ## Analyzing Logs
65
-
66
- If you collect your logs in a file (e.g. as `ndjson`) you can analyze them using the included `bake` commands:
67
-
68
- ```bash
69
- $ bundle exec bake input --file samples.ndjson fiber:profiler:analyze output
70
- ```
41
+ 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.
71
42
 
72
- This will aggregate all the call logs and generate a short summary, ordered by duration.
43
+ Each scheduler obtains its own profiler, 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.
data/context/index.yaml CHANGED
@@ -8,4 +8,13 @@ metadata:
8
8
  files:
9
9
  - path: getting-started.md
10
10
  title: Getting Started
11
- description: This guide explains how to detect stalls using the fiber profiler.
11
+ description: This guide explains how to install the fiber profiler and choose a
12
+ mode for diagnosing event-loop stalls.
13
+ - path: watchdog-mode.md
14
+ title: Watchdog Mode
15
+ description: This guide explains how to sample ongoing fiber stalls with the watchdog
16
+ profiler.
17
+ - path: capture-mode.md
18
+ title: Capture Mode
19
+ description: This guide explains how to trace fiber execution and analyze call timings
20
+ with the capture profiler.
@@ -0,0 +1,100 @@
1
+ # Watchdog Mode
2
+
3
+ This guide explains how to sample ongoing fiber stalls with the watchdog profiler.
4
+
5
+ Use watchdog mode to find where a fiber is spending time while it prevents other fibers from running. It reports during the stall, so you can investigate an operation that has not returned yet. For individual call timings, see [Capture Mode](../capture-mode/index).
6
+
7
+ ## Usage
8
+
9
+ Add `fiber-profiler` to your application's bundle as described in [Getting Started](../getting-started/index), then select watchdog mode when starting Async or Falcon:
10
+
11
+ ```bash
12
+ FIBER_PROFILER=watchdog bundle exec falcon serve
13
+ ```
14
+
15
+ Async starts and stops the watchdog automatically. Each scheduler gets its own profiler, which binds to the thread when profiling starts.
16
+
17
+ To reproduce a stall, save this as `stall.rb`:
18
+
19
+ ```ruby
20
+ require "async"
21
+
22
+ Sync do
23
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 2
24
+ while Process.clock_gettime(Process::CLOCK_MONOTONIC) < deadline
25
+ end
26
+ end
27
+ ```
28
+
29
+ ```bash
30
+ FIBER_PROFILER=watchdog bundle exec ruby stall.rb
31
+ ```
32
+
33
+ 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.
34
+
35
+ ## How Sampling Works
36
+
37
+ 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.
38
+
39
+ 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. The measured duration covers one uninterrupted fiber execution.
40
+
41
+ ## Configuration
42
+
43
+ Set these environment variables before starting your application. They are read when the default watchdog is constructed:
44
+
45
+ | Variable | Default | Meaning |
46
+ | --- | --- | --- |
47
+ | `FIBER_PROFILER_WATCHDOG_STALL_THRESHOLD` | `0.5` | Minimum execution duration in seconds before reporting, and minimum interval between repeated reports. |
48
+ | `FIBER_PROFILER_WATCHDOG_SAMPLE_INTERVAL` | `0.1` | Delay in seconds between stack samples. |
49
+
50
+ 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.
51
+
52
+ ### Manual Instrumentation
53
+
54
+ When managing fibers directly, construct a {ruby Fiber::Profiler::Watchdog} and stop it in an `ensure` block:
55
+
56
+ ```ruby
57
+ require "fiber/profiler/watchdog"
58
+
59
+ watchdog = Fiber::Profiler::Watchdog.new(
60
+ stall_threshold: 0.5,
61
+ sample_interval: 0.1,
62
+ max_samples: 5,
63
+ output: $stderr
64
+ )
65
+
66
+ begin
67
+ watchdog.start
68
+
69
+ # Simulate an application fiber blocked on an operation:
70
+ Fiber.new do
71
+ sleep 1
72
+ end.resume
73
+ ensure
74
+ watchdog.stop
75
+ end
76
+ ```
77
+
78
+ This example starts the profiler explicitly, so it does not need `FIBER_PROFILER=watchdog`. Start and stop profiling on the monitored thread. `max_samples` must be a positive integer; there is no environment variable for it. The output must support writes from the watchdog thread. Stop the profiler before closing its output.
79
+
80
+ 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.
81
+
82
+ 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.
83
+
84
+ ## Reading Reports
85
+
86
+ Terminal output contains readable stacks. Redirected output uses one JSON object per report:
87
+
88
+ ```bash
89
+ FIBER_PROFILER=watchdog bundle exec ruby stall.rb 2> watchdog.ndjson
90
+ ```
91
+
92
+ Each report contains `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.
93
+
94
+ Repeated frames identify code worth investigating: optimize expensive work, add cooperative yield points, or offload suitable work to a bounded thread pool. Watchdog reports contain sampled backtraces, so the capture-mode `fiber:profiler:analyze` command does not aggregate them.
95
+
96
+ ## Interpretation and Limits
97
+
98
+ 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.
99
+
100
+ Sampling still has overhead. Use the [Falcon overhead benchmark](https://github.com/socketry/fiber-profiler/tree/main/benchmark/falcon) as a starting point for measuring your own application before enabling continuous monitoring.
@@ -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
- free(capture);
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
- rb_scan_args(argc, argv, ":", &options);
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,44 +4,11 @@
4
4
  # Copyright, 2025-2026, by Samuel Williams.
5
5
 
6
6
  require_relative "native"
7
- require_relative "fork_handler"
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
- # Hook into Process._fork to handle fork events automatically.
14
- ::Process.singleton_class.prepend(ForkHandler)
15
-
16
- # Private module that wraps start/stop to manage thread-local storage.
17
- module ThreadLocalCapture
18
- # Start profiling and store the capture in the current thread's thread-local storage.
19
- def start
20
- result = super
21
-
22
- if result
23
- Thread.current.fiber_profiler_capture = self
24
- end
25
-
26
- return result
27
- end
28
-
29
- # Stop profiling and clear the capture from the current thread's thread-local storage.
30
- def stop
31
- result = super
32
-
33
- if result
34
- Thread.current.fiber_profiler_capture = nil
35
- end
36
-
37
- return result
38
- end
39
- end
40
-
41
- private_constant :ThreadLocalCapture
42
-
43
10
  # Represents a running profiler capture.
44
11
  class Capture
45
- prepend ThreadLocalCapture
12
+ prepend ThreadLocalProfiler
46
13
  end
47
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
@@ -7,6 +7,6 @@
7
7
  class Fiber
8
8
  # @namespace
9
9
  module Profiler
10
- VERSION = "0.6.1"
10
+ VERSION = "0.7.1"
11
11
  end
12
12
  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
@@ -9,11 +9,25 @@ require_relative "profiler/capture"
9
9
  module Fiber::Profiler
10
10
  # The default profiler to use, if any.
11
11
  #
12
- # Use the `FIBER_PROFILER_CAPTURE=true` environment variable to enable profiling.
12
+ # Set `FIBER_PROFILER` to `watchdog`, `capture`, or `false`. When unset,
13
+ # the legacy `FIBER_PROFILER_CAPTURE=true` setting enables capture mode.
13
14
  #
14
- # @returns [Capture | Nil]
15
+ # @returns [Capture | Watchdog | Nil]
16
+ # @raises [ArgumentError] If the requested mode is unknown.
15
17
  def self.default
16
- Capture.default
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
17
31
  end
18
32
 
19
33
  # Execute the given block with the {default} profiler, if any.
data/readme.md CHANGED
@@ -12,12 +12,20 @@ Migrating existing applications to the event loop can be tricky. One of the most
12
12
 
13
13
  Please see the [project documentation](https://socketry.github.io/fiber-profiler/) for more details.
14
14
 
15
- - [Getting Started](https://socketry.github.io/fiber-profiler/guides/getting-started/index) - This guide explains how to detect stalls using the fiber profiler.
15
+ - [Getting Started](https://socketry.github.io/fiber-profiler/guides/getting-started/index) - This guide explains how to install the fiber profiler and choose a mode for diagnosing event-loop stalls.
16
+
17
+ - [Watchdog Mode](https://socketry.github.io/fiber-profiler/guides/watchdog-mode/index) - This guide explains how to sample ongoing fiber stalls with the watchdog profiler.
18
+
19
+ - [Capture Mode](https://socketry.github.io/fiber-profiler/guides/capture-mode/index) - This guide explains how to trace fiber execution and analyze call timings with the capture profiler.
16
20
 
17
21
  ## Releases
18
22
 
19
23
  Please see the [project releases](https://socketry.github.io/fiber-profiler/releases/index) for all releases.
20
24
 
25
+ ### v0.7.0
26
+
27
+ - Sign releases with the Socketry Ruby Gems certificate.
28
+
21
29
  ### v0.6.0
22
30
 
23
31
  - Fixed compatibility with `Process.fork`.
@@ -50,12 +58,14 @@ bundle exec sus
50
58
 
51
59
  ### Making Releases
52
60
 
53
- To make a new release:
61
+ To prepare a release branch and open a pull request from an up-to-date `main`:
54
62
 
55
63
  ``` shell
56
- bundle exec bake gem:release:patch # or minor or major
64
+ bundle exec bake gem:github:release:patch # or minor or major
57
65
  ```
58
66
 
67
+ 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.
68
+
59
69
  ### Developer Certificate of Origin
60
70
 
61
71
  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
@@ -1,5 +1,9 @@
1
1
  # Releases
2
2
 
3
+ ## v0.7.0
4
+
5
+ - Sign releases with the Socketry Ruby Gems certificate.
6
+
3
7
  ## v0.6.0
4
8
 
5
9
  - Fixed compatibility with `Process.fork`.
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.6.1
4
+ version: 0.7.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -9,43 +9,55 @@ bindir: bin
9
9
  cert_chain:
10
10
  - |
11
11
  -----BEGIN CERTIFICATE-----
12
- MIIE2DCCA0CgAwIBAgIBATANBgkqhkiG9w0BAQsFADBhMRgwFgYDVQQDDA9zYW11
13
- ZWwud2lsbGlhbXMxHTAbBgoJkiaJk/IsZAEZFg1vcmlvbnRyYW5zZmVyMRIwEAYK
14
- CZImiZPyLGQBGRYCY28xEjAQBgoJkiaJk/IsZAEZFgJuejAeFw0yMjA4MDYwNDUz
15
- MjRaFw0zMjA4MDMwNDUzMjRaMGExGDAWBgNVBAMMD3NhbXVlbC53aWxsaWFtczEd
16
- MBsGCgmSJomT8ixkARkWDW9yaW9udHJhbnNmZXIxEjAQBgoJkiaJk/IsZAEZFgJj
17
- bzESMBAGCgmSJomT8ixkARkWAm56MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIB
18
- igKCAYEAomvSopQXQ24+9DBB6I6jxRI2auu3VVb4nOjmmHq7XWM4u3HL+pni63X2
19
- 9qZdoq9xt7H+RPbwL28LDpDNflYQXoOhoVhQ37Pjn9YDjl8/4/9xa9+NUpl9XDIW
20
- sGkaOY0eqsQm1pEWkHJr3zn/fxoKPZPfaJOglovdxf7dgsHz67Xgd/ka+Wo1YqoE
21
- e5AUKRwUuvaUaumAKgPH+4E4oiLXI4T1Ff5Q7xxv6yXvHuYtlMHhYfgNn8iiW8WN
22
- XibYXPNP7NtieSQqwR/xM6IRSoyXKuS+ZNGDPUUGk8RoiV/xvVN4LrVm9upSc0ss
23
- RZ6qwOQmXCo/lLcDUxJAgG95cPw//sI00tZan75VgsGzSWAOdjQpFM0l4dxvKwHn
24
- tUeT3ZsAgt0JnGqNm2Bkz81kG4A2hSyFZTFA8vZGhp+hz+8Q573tAR89y9YJBdYM
25
- zp0FM4zwMNEUwgfRzv1tEVVUEXmoFCyhzonUUw4nE4CFu/sE3ffhjKcXcY//qiSW
26
- xm4erY3XAgMBAAGjgZowgZcwCQYDVR0TBAIwADALBgNVHQ8EBAMCBLAwHQYDVR0O
27
- BBYEFO9t7XWuFf2SKLmuijgqR4sGDlRsMC4GA1UdEQQnMCWBI3NhbXVlbC53aWxs
28
- aWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MC4GA1UdEgQnMCWBI3NhbXVlbC53aWxs
29
- aWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MA0GCSqGSIb3DQEBCwUAA4IBgQB5sxkE
30
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
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
44
54
  extra_rdoc_files: []
45
55
  files:
46
56
  - bake/fiber/profiler/analyze.rb
57
+ - context/capture-mode.md
47
58
  - context/getting-started.md
48
59
  - context/index.yaml
60
+ - context/watchdog-mode.md
49
61
  - ext/extconf.rb
50
62
  - ext/fiber/profiler/capture.c
51
63
  - ext/fiber/profiler/capture.h
@@ -60,9 +72,12 @@ files:
60
72
  - lib/fiber/profiler/capture.rb
61
73
  - lib/fiber/profiler/fork_handler.rb
62
74
  - lib/fiber/profiler/native.rb
75
+ - lib/fiber/profiler/thread_local.rb
63
76
  - lib/fiber/profiler/version.rb
77
+ - lib/fiber/profiler/watchdog.rb
64
78
  - license.md
65
79
  - readme.md
80
+ - release.cert
66
81
  - releases.md
67
82
  homepage: https://github.com/socketry/fiber-profiler
68
83
  licenses:
@@ -84,7 +99,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
84
99
  - !ruby/object:Gem::Version
85
100
  version: '0'
86
101
  requirements: []
87
- rubygems_version: 4.0.6
102
+ rubygems_version: 4.0.21
88
103
  specification_version: 4
89
104
  summary: A fiber stall profiler.
90
105
  test_files: []
metadata.gz.sig CHANGED
@@ -1 +1,3 @@
1
- �<dW� } ��{��~�IL3)�P�G��H(�s0��ly����z��[�8ɧ4ރ���Ŵ�L��c���y%8�mL���o����F2�*���m��!�O�i��AS�=?@�|���J�Է�R bmJ�}�_�a�P{��e5������8���q/�W�&5ym�����9 j`�*=��>��>m����λ�@�:�P�~�ֱ{1u�>l2ɔM��4�m����ױR��ٚK�Úg�u%�0 S�C�~��|n�O���g�����<��%��K�ڬnݐ�]իI���$��9
1
+ �I(-r[���-�������������/Bkh�ѻ��.��Ԗx�Y�h�3� +,T.B����`*VK�:�$Y�Z� F6�B�3��&9�{���5ҏ��8��Ϲ�!xוE���/h�҂&h�u�I�H� ."�D�j�T���߫~X͠��c�.����L��x�ݲ�U_�KS*�x��hF�M�Q����"v�iC~���`GW
2
+ 5�t���P@8�G�Z���J�FC�w��L2��}C]���7��U1Fv.u��1_@�e���V��*| :��~Ͻ 3�d��å�̎�p�=]]Jx#�n� @�� �5PT���̾
3
+ |�2���io5EB#��K�[ H&\��G�Œ��PQ������s�?/��<u����mမ