Class: Scryer::QueryWatcher
- Inherits:
-
Object
- Object
- Scryer::QueryWatcher
- Defined in:
- lib/scryer/query_watcher.rb
Overview
Runtime detector for two of the problems Bullet (github.com/flyerhzm/bullet)
is best known for — a collection query followed by one repeat query per
row ("N+1"), and an .includes/.preload/.eager_load association that
gets fetched but never actually read ("unused eager loading") — built
independently, on a different mechanism than Bullet's: SQL-shape
correlation via ActiveSupport::Notifications, plus a Module#prepend on
the two public entry points identified below (QueryMethods#includes
et al., and Association#reader), rather than Bullet's own per-request
association bookkeeping. No Bullet source was read or copied to build
this — see the README's "Runtime query watcher" section for the
conceptual write-up this was built from.
Unlike the rest of Scryer (a one-shot static Ripper scan of source
files with no Rails required), this module instruments a running app:
it needs ActiveRecord loaded and real queries executing to find
anything. It does nothing until Scryer::QueryWatcher.enable! is
called (typically from an initializer, gated to non-production
environments — see README) and a scope is opened with .watch { } (the
Rack middleware below opens one per request automatically).
Defined Under Namespace
Modules: AccessTracking, EagerLoadTracking Classes: Finding, Middleware, Scope
Class Method Summary collapse
- .call_site ⇒ Object
- .current_scope ⇒ Object
-
.enable!(logger: nil, n_plus_one_threshold: 2) ⇒ Object
Turns the watcher on for the life of the process.
- .enabled? ⇒ Boolean
-
.watch ⇒ Object
Opens a fresh per-thread scope, runs the block, then reports (and returns) whatever was found.
Class Method Details
.call_site ⇒ Object
143 144 145 146 |
# File 'lib/scryer/query_watcher.rb', line 143 def call_site loc = caller_locations.find { |l| !l.path.include?("/gems/") && !l.path.include?("lib/scryer/") } loc ? "#{loc.path}:#{loc.lineno}" : "unknown" end |
.current_scope ⇒ Object
139 140 141 |
# File 'lib/scryer/query_watcher.rb', line 139 def current_scope Thread.current[:scryer_query_watcher_scope] end |
.enable!(logger: nil, n_plus_one_threshold: 2) ⇒ Object
Turns the watcher on for the life of the process. Idempotent — safe
to call more than once (later calls are no-ops). logger receives a
warning line per finding as it's detected; n_plus_one_threshold is
how many repeats of the same query shape from the same call site
count as N+1 (default 2 — the second occurrence is already one
more than a single collection load needs).
108 109 110 111 112 113 114 115 116 117 118 |
# File 'lib/scryer/query_watcher.rb', line 108 def enable!(logger: nil, n_plus_one_threshold: 2) return if @enabled @logger = logger || default_logger @n_plus_one_threshold = n_plus_one_threshold @enabled = true require "active_support/notifications" subscribe_to_queries patch_active_record end |
.enabled? ⇒ Boolean
120 121 122 |
# File 'lib/scryer/query_watcher.rb', line 120 def enabled? !!@enabled end |
.watch ⇒ Object
Opens a fresh per-thread scope, runs the block, then reports (and returns) whatever was found. The bundled Rack middleware calls this once per request; call it directly to watch a Sidekiq job, a rake task, or anything else that isn't an HTTP request.
128 129 130 131 132 133 134 135 136 137 |
# File 'lib/scryer/query_watcher.rb', line 128 def watch raise "Scryer::QueryWatcher.enable! was never called" unless enabled? previous = Thread.current[:scryer_query_watcher_scope] scope = Thread.current[:scryer_query_watcher_scope] = Scope.new yield report(scope) ensure Thread.current[:scryer_query_watcher_scope] = previous end |