Skip to content

Commit c11cd23

Browse files
Prefer cached fast path for buffer writes.
1 parent a19093c commit c11cd23

7 files changed

Lines changed: 479 additions & 93 deletions

File tree

guides/getting-started/readme.md

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,22 +16,27 @@ $ bundle add async-utilization
1616

1717
The key components are:
1818

19-
- {ruby Async::Utilization::Interface}: Thread-local singleton for emitting metrics
20-
- {ruby Async::Utilization::Schema}: Defines the binary layout for serialization
21-
- {ruby Async::Utilization::Observer}: Writes metrics to shared memory using the schema
19+
- {ruby Async::Utilization::Interface}: Thread-local singleton for emitting metrics.
20+
- {ruby Async::Utilization::Schema}: Defines the binary layout for serialization.
21+
- {ruby Async::Utilization::Observer}: Writes metrics to shared memory using the schema.
22+
- {ruby Async::Utilization::Metric}: Caches metric value and fast path for direct buffer updates.
2223

2324
## Basic Usage
2425

25-
The simplest way to use `async-utilization` is to emit metrics directly:
26+
The recommended way to use `async-utilization` is to get a cached metric reference and use it:
2627

2728
```ruby
2829
require "async/utilization"
2930

30-
# Increment a metric
31-
Async::Utilization.increment(:total_requests)
31+
# Get metrics:
32+
total_requests = Async::Utilization.metric(:total_requests)
33+
active_requests = Async::Utilization.metric(:active_requests)
3234

33-
# Increment with auto-decrement
34-
Async::Utilization.increment(:active_requests) do
35+
# Increment a metric:
36+
total_requests.increment
37+
38+
# Increment with auto-decrement:
39+
active_requests.increment do
3540
# Handle request - automatically decrements when block completes
3641
end
3742
```
@@ -63,8 +68,9 @@ observer = Async::Utilization::Observer.open(
6368
# Set observer - metrics will now be written to shared memory
6469
Async::Utilization.observer = observer
6570

66-
# Now all metrics are written to shared memory
67-
Async::Utilization.increment(:total_requests)
71+
# All metrics are written directly to shared memory:
72+
total_requests = Async::Utilization.metric(:total_requests)
73+
total_requests.increment
6874
```
6975

7076
The observer automatically handles page alignment requirements for memory mapping, so you can use any segment size and offset. The supervisor process can then read these metrics from shared memory to aggregate utilization across all workers.

lib/async/utilization.rb

Lines changed: 30 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -7,50 +7,50 @@
77
require_relative "utilization/schema"
88
require_relative "utilization/interface"
99
require_relative "utilization/observer"
10+
require_relative "utilization/metric"
1011

12+
# @namespace
1113
module Async
14+
# Provides high-performance utilization metrics for Async services using shared memory.
15+
#
16+
# This module provides a convenient interface for tracking utilization metrics
17+
# that can be synchronized to shared memory for inter-process communication.
18+
# Each thread gets its own instance of the underlying {Interface}, providing
19+
# thread-local behavior.
20+
#
21+
# See the {file:guides/getting-started/readme.md Getting Started} guide for usage examples.
1222
module Utilization
13-
# Increment a field value, optionally with a block that auto-decrements.
14-
#
15-
# Delegates to the thread-local {Interface} instance.
23+
# Set the observer for utilization metrics.
1624
#
17-
# @parameter field [Symbol] The field name to increment.
18-
# @yield Optional block - if provided, decrements the field after the block completes.
19-
# @returns [Integer] The new value of the field.
20-
def self.increment(...)
21-
Interface.instance.increment(...)
22-
end
23-
24-
# Decrement a field value.
25+
# When an observer is set, it is notified of all current metric values
26+
# so it can sync its state. The observer must implement `set(field, value)`.
2527
#
2628
# Delegates to the thread-local {Interface} instance.
2729
#
28-
# @parameter field [Symbol] The field name to decrement.
29-
# @returns [Integer] The new value of the field.
30-
def self.decrement(...)
31-
Interface.instance.decrement(...)
30+
# @parameter observer [#set] The observer to set.
31+
def self.observer=(observer)
32+
Interface.instance.observer = observer
3233
end
3334

34-
# Set a field value.
35+
# Get a cached metric reference for a field.
3536
#
36-
# Delegates to the thread-local {Interface} instance.
37+
# Returns a {Metric} instance that caches all details needed for fast writes
38+
# to shared memory, avoiding hash lookups on the fast path.
3739
#
38-
# @parameter field [Symbol] The field name to set.
39-
# @parameter value [Numeric] The value to set.
40-
def self.set(...)
41-
Interface.instance.set(...)
42-
end
43-
44-
# Set the observer for utilization metrics.
45-
#
46-
# When an observer is set, it is notified of all current values
47-
# so it can sync its state. The observer must implement `set(field, value)`.
40+
# This is the recommended way to access metrics for optimal performance.
4841
#
4942
# Delegates to the thread-local {Interface} instance.
5043
#
51-
# @parameter observer [#set] The observer to set.
52-
def self.observer=(observer)
53-
Interface.instance.observer = observer
44+
# @parameter field [Symbol] The field name to get a metric for.
45+
# @returns [Metric] A metric instance for the given field.
46+
# @example
47+
# current_requests = Async::Utilization.metric(:current_requests)
48+
# current_requests.increment
49+
# current_requests.increment do
50+
# # Handle request - auto-decrements when block completes
51+
# end
52+
def self.metric(field)
53+
Interface.instance.metric(field)
5454
end
5555
end
5656
end

lib/async/utilization/interface.rb

Lines changed: 43 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -41,94 +41,94 @@ class Interface
4141
# Initialize a new interface.
4242
def initialize
4343
@observer = nil
44-
@values = Hash.new(0)
44+
@metrics = {}
4545

4646
@guard = Mutex.new
4747
end
4848

4949
# @attribute [Object | Nil] The registered observer.
5050
attr :observer
5151

52-
# @attribute [Hash] The current values for all fields.
53-
attr :values
52+
# @attribute [Mutex] The mutex for thread safety.
53+
attr :guard
54+
55+
# Get the current values for all metrics.
56+
#
57+
# @returns [Hash] Hash mapping field names to their current values.
58+
def values
59+
@metrics.transform_values do |metric|
60+
metric.guard.synchronize { metric.value }
61+
end
62+
end
5463

5564
# Set the observer for the interface.
5665
#
57-
# When an observer is set, it is notified of all current values
66+
# When an observer is set, it is notified of all current metric values
5867
# so it can sync its state. The observer must implement `set(field, value)`.
68+
# All cached metrics are invalidated when the observer changes.
5969
#
6070
# @parameter observer [#set] The observer to set.
6171
def observer=(observer)
6272
@guard.synchronize do
63-
@observer = observer
64-
65-
@values.each do |field, value|
66-
observer.set(field, value)
73+
# Invalidate all cached metrics
74+
@metrics.each_value do |metric|
75+
metric.invalidate
6776
end
77+
78+
@observer = observer
79+
end
80+
81+
# Notify observer of all current metric values (outside guard to avoid deadlock)
82+
@metrics.each do |name, metric|
83+
value = metric.guard.synchronize { metric.value }
84+
observer.set(name, value)
6885
end
6986
end
7087

7188
# Set a field value.
7289
#
73-
# Updates the interface's value and notifies the registered observer.
90+
# Delegates to the metric instance for the given field.
7491
#
7592
# @parameter field [Symbol] The field name to set.
7693
# @parameter value [Numeric] The value to set.
7794
def set(field, value)
78-
field = field.to_sym
79-
80-
@guard.synchronize do
81-
@values[field] = value
82-
@observer&.set(field, value)
83-
end
95+
metric(field).set(value)
8496
end
8597

8698
# Increment a field value, optionally with a block that auto-decrements.
8799
#
88-
# Updates the interface's value and notifies the registered observer.
100+
# Delegates to the metric instance for the given field.
89101
#
90102
# @parameter field [Symbol] The field name to increment.
91103
# @yield Optional block - if provided, decrements the field after the block completes.
92104
# @returns [Integer] The new value of the field.
93-
def increment(field)
94-
field = field.to_sym
95-
96-
new_value = nil
97-
@guard.synchronize do
98-
new_value = @values[field] + 1
99-
@values[field] = new_value
100-
@observer&.set(field, new_value)
101-
end
102-
103-
if block_given?
104-
begin
105-
yield
106-
ensure
107-
# Decrement after block completes
108-
decrement(field)
109-
end
110-
end
111-
112-
new_value
105+
def increment(field, &block)
106+
metric(field).increment(&block)
113107
end
114108

115109
# Decrement a field value.
116110
#
117-
# Updates the interface's value and notifies the registered observer.
111+
# Delegates to the metric instance for the given field.
118112
#
119113
# @parameter field [Symbol] The field name to decrement.
120114
# @returns [Integer] The new value of the field.
121115
def decrement(field)
116+
metric(field).decrement
117+
end
118+
119+
# Get a cached metric reference for a field.
120+
#
121+
# Returns a {Metric} instance that caches all details needed for fast writes.
122+
# Metrics are cached per field and invalidated when the observer changes.
123+
#
124+
# @parameter field [Symbol] The field name to get a metric for.
125+
# @returns [Metric] A metric instance for the given field.
126+
def metric(field)
122127
field = field.to_sym
123128

124-
new_value = nil
125129
@guard.synchronize do
126-
new_value = @values[field] - 1
127-
@values[field] = new_value
128-
@observer&.set(field, new_value)
130+
@metrics[field] ||= Metric.new(field, self)
129131
end
130-
131-
new_value
132132
end
133133
end
134134
end

0 commit comments

Comments
 (0)