Add GVL contention metrics via gvltools (#2538)

* Add GVL contention metrics via gvltools and Yabeda

Expose per-request and process-wide GVL wait time as Prometheus metrics
to diagnose suspected GVL contention from Solid Cable and Action Cable
threads in Puma workers.

Metrics: gvl_request_wait_seconds (histogram), gvl_waiting_threads
(gauge), gvl_global_timer_total_seconds (gauge).

* Add unit: :seconds to GVL histogram for consistency
This commit is contained in:
Jeremy Daer
2026-02-12 16:28:29 -08:00
committed by GitHub
parent 8aa934a594
commit a5a5a62169
5 changed files with 61 additions and 0 deletions
+1
View File
@@ -24,3 +24,4 @@ gem "yabeda-puma-plugin"
gem "yabeda-rails"
gem "webrick" # required for yabeda-prometheus metrics server
gem "prometheus-client-mmap", "~> 1.3"
gem "gvltools"
+2
View File
@@ -282,6 +282,7 @@ GEM
addressable (>= 2.5.0)
globalid (1.3.0)
activesupport (>= 6.1)
gvltools (0.4.0)
hashdiff (1.2.1)
i18n (1.14.8)
concurrent-ruby (~> 1.0)
@@ -657,6 +658,7 @@ DEPENDENCIES
faker
fizzy-saas!
geared_pagination (~> 1.2)
gvltools
image_processing (~> 1.14)
importmap-rails
jbuilder
+8
View File
@@ -1,6 +1,7 @@
require_relative "transaction_pinning"
require_relative "signup"
require_relative "authorization"
require_relative "gvl_instrumentation"
require_relative "../../rails_ext/active_record_tasks_database_tasks.rb"
module Fizzy
@@ -51,6 +52,10 @@ module Fizzy
app.config.middleware.insert_after(ActiveRecord::Middleware::DatabaseSelector, TransactionPinning::Middleware)
end
initializer "fizzy_saas.gvl_instrumentation" do |app|
app.config.middleware.insert_before(Rack::Runtime, GvlInstrumentation)
end
initializer "fizzy_saas.solid_queue" do
SolidQueue.on_start do
Process.warmup
@@ -117,6 +122,9 @@ module Fizzy
config.channel_class_name = "ActionCable::Channel::Base"
end
require "yabeda/gvl"
Yabeda::GVL.install!
require_relative "metrics"
end
@@ -0,0 +1,20 @@
module Fizzy
module Saas
class GvlInstrumentation
def initialize(app)
@app = app
end
def call(env)
GVLTools::LocalTimer.enable
before = GVLTools::LocalTimer.monotonic_time
result = @app.call(env)
gvl_wait_ns = GVLTools::LocalTimer.monotonic_time - before
Yabeda.gvl.request_wait_seconds.measure({}, gvl_wait_ns / 1_000_000_000.0)
result
ensure
GVLTools::LocalTimer.disable
end
end
end
end
+30
View File
@@ -0,0 +1,30 @@
module Yabeda
module GVL
WAIT_HISTOGRAM_BUCKETS = [ 0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10 ]
def self.install!
GVLTools::GlobalTimer.enable
GVLTools::WaitingThreads.enable
Yabeda.configure do
group :gvl do
gauge :waiting_threads,
comment: "Number of threads currently waiting to acquire the GVL"
gauge :global_timer_total_seconds,
comment: "Total time all threads spent waiting on the GVL (seconds)"
histogram :request_wait_seconds,
unit: :seconds,
comment: "GVL wait time experienced during a single request (seconds)",
buckets: WAIT_HISTOGRAM_BUCKETS
end
collect do
gvl.waiting_threads.set({}, GVLTools::WaitingThreads.count)
gvl.global_timer_total_seconds.set({}, GVLTools::GlobalTimer.monotonic_time / 1_000_000_000.0)
end
end
end
end
end