Skip to content

Commit 8dd823e

Browse files
committed
Add support for custom hook classes with Firefox profiler integration
1 parent 7107a6d commit 8dd823e

File tree

6 files changed

+230
-4
lines changed

6 files changed

+230
-4
lines changed

README.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,71 @@ ruby -r vernier -e 'Vernier.trace_retained(out: "irb_profile.json") { require "i
126126
> [!NOTE]
127127
> Retained-memory flamegraphs must be interpreted a little differently than a typical profiling flamegraph. In a retained-memory flamegraph, the x-axis represents a proportion of memory in bytes, _not time or samples_ The topmost boxes on the y-axis represent the retained objects, with their stacktrace below; their width represents the percentage of overall retained memory each object occupies.
128128
129+
### Hooks
130+
131+
Hooks automatically add markers to profiles based on application events:
132+
133+
```ruby
134+
Vernier.profile(hooks: [:activesupport, MyHook]) do
135+
# code to profile
136+
end
137+
```
138+
139+
#### Built-in hooks
140+
141+
- `:activesupport` (`:rails`) - ActiveSupport notifications
142+
- `:memory_usage` - Memory usage tracking
143+
144+
#### Custom hooks
145+
146+
Define a class with `initialize(collector)`, `enable`, and `disable` methods:
147+
148+
```ruby
149+
class MyHook
150+
def initialize(collector)
151+
@collector = collector
152+
end
153+
154+
def enable
155+
# Set up event listeners
156+
end
157+
158+
def disable
159+
# Clean up
160+
end
161+
end
162+
```
163+
164+
For Firefox profiler integration, add `firefox_marker_schema` or `firefox_counters` methods (see [Firefox profiler docs](https://profiler.firefox.com/docs/#/) for format details):
165+
166+
```ruby
167+
def firefox_marker_schema
168+
[{
169+
name: "my_event",
170+
display: ["marker-chart", "marker-table"],
171+
tooltipLabel: "{marker.data.name}",
172+
data: [
173+
{ key: "name", format: "string" }
174+
]
175+
}]
176+
end
177+
178+
def firefox_counters
179+
{
180+
name: "my_counter",
181+
category: "Custom",
182+
description: "My custom counter",
183+
samples: {
184+
time: [0, 1000, 2000],
185+
count: [10, 20, 30],
186+
length: 3
187+
}
188+
}
189+
end
190+
```
191+
192+
See [`examples/custom_hook.rb`](examples/custom_hook.rb) for a complete example.
193+
129194
### Options
130195

131196
| Option | Middleware Param | Description | Default (Middleware Default) |

examples/custom_hook.rb

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
#!/usr/bin/env ruby
2+
3+
require "vernier"
4+
5+
class CustomHook
6+
def initialize(collector)
7+
@collector = collector
8+
@events = []
9+
end
10+
11+
def enable
12+
# Simulate subscribing to application events
13+
@thread = Thread.new do
14+
3.times do |i|
15+
sleep 0.03
16+
start_time = @collector.current_time
17+
sleep 0.01 # Simulate work
18+
@collector.add_marker(
19+
name: "custom_event",
20+
start: start_time,
21+
finish: @collector.current_time,
22+
data: { type: "custom_event", event_id: i + 1 }
23+
)
24+
end
25+
end
26+
end
27+
28+
def disable
29+
@thread&.join
30+
end
31+
end
32+
33+
result = Vernier.profile(hooks: [CustomHook]) do
34+
sleep 0.15
35+
end
36+
37+
puts "Profile complete with custom events"

lib/vernier/collector.rb

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,17 @@ def initialize(mode, options = {})
3131
end
3232

3333
private def add_hook(hook)
34-
case hook.to_sym
34+
case (hook.to_sym if hook.respond_to?(:to_sym))
3535
when :rails, :activesupport
3636
@hooks << Vernier::Hooks::ActiveSupport.new(self)
3737
when :memory_usage
3838
@hooks << Vernier::Hooks::MemoryUsage.new(self)
3939
else
40-
warn "unknown hook: #{hook.inspect}"
40+
if hook.respond_to?(:new)
41+
@hooks << hook.new(self)
42+
else
43+
warn "unknown hook: #{hook.inspect}"
44+
end
4145
end
4246
end
4347

test/output/test_firefox.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
# frozen_string_literal: true
22

33
require "test_helper"
4-
require "firefox_test_helpers"
54

65
class TestOutputFirefox < Minitest::Test
76
include FirefoxTestHelpers

test/test_custom_hooks.rb

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
# frozen_string_literal: true
2+
3+
require "test_helper"
4+
5+
class TestCustomHooks < Minitest::Test
6+
include FirefoxTestHelpers
7+
8+
class SimpleHook
9+
def initialize(collector)
10+
@collector = collector
11+
end
12+
13+
def enable
14+
@start_time = @collector.current_time
15+
end
16+
17+
def disable
18+
@collector.add_marker(
19+
name: "Simple Hook",
20+
start: @start_time,
21+
finish: @collector.current_time,
22+
data: { type: "simple_hook" }
23+
)
24+
end
25+
end
26+
27+
class HookWithSchema
28+
def initialize(collector)
29+
@collector = collector
30+
end
31+
32+
def enable
33+
@collector.add_marker(
34+
name: "custom_event",
35+
start: @collector.current_time,
36+
finish: @collector.current_time + 1000,
37+
data: { type: "custom_event", event_name: "test" }
38+
)
39+
end
40+
41+
def disable
42+
end
43+
44+
def firefox_marker_schema
45+
[{
46+
name: "custom_event",
47+
display: ["marker-chart", "marker-table"],
48+
data: [{ key: "event_name", format: "string" }]
49+
}]
50+
end
51+
end
52+
53+
class HookWithCounters
54+
def initialize(collector)
55+
@collector = collector
56+
end
57+
58+
def enable
59+
end
60+
61+
def disable
62+
end
63+
64+
def firefox_counters
65+
{
66+
name: "test_counter",
67+
category: "Test",
68+
description: "Test counter",
69+
samples: {
70+
time: [0, 1000],
71+
count: [10, 20],
72+
length: 2
73+
}
74+
}
75+
end
76+
end
77+
78+
def test_custom_hook_class
79+
result = Vernier.profile(hooks: [SimpleHook]) do
80+
sleep 0.01
81+
end
82+
83+
output = Vernier::Output::Firefox.new(result).output
84+
assert_valid_firefox_profile(output)
85+
end
86+
87+
def test_custom_hook_with_schema
88+
result = Vernier.profile(hooks: [HookWithSchema]) do
89+
sleep 0.01
90+
end
91+
92+
output = Vernier::Output::Firefox.new(result).output
93+
assert_valid_firefox_profile(output)
94+
95+
data = JSON.parse(output)
96+
schemas = data.dig("meta", "markerSchema") || []
97+
assert schemas.any? { |s| s["name"] == "custom_event" }
98+
end
99+
100+
def test_mixed_hooks
101+
result = Vernier.profile(hooks: [:memory_usage, SimpleHook]) do
102+
sleep 0.01
103+
end
104+
105+
output = Vernier::Output::Firefox.new(result).output
106+
assert_valid_firefox_profile(output)
107+
assert_equal 2, result.hooks.size
108+
end
109+
110+
def test_custom_hook_with_counters
111+
result = Vernier.profile(hooks: [HookWithCounters]) do
112+
sleep 0.01
113+
end
114+
115+
output = Vernier::Output::Firefox.new(result).output
116+
assert_valid_firefox_profile(output)
117+
118+
data = JSON.parse(output)
119+
counters = data["counters"]
120+
assert counters.any? { |c| c["name"] == "test_counter" }
121+
end
122+
end

test/test_rack_middleware.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
# frozen_string_literal: true
22

33
require "test_helper"
4-
require "firefox_test_helpers"
54

65
require "rack"
76

0 commit comments

Comments
 (0)