Class: Scryer::QueryWatcher

Inherits:
Object
  • Object
show all
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

Class Method Details

.call_siteObject



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_scopeObject



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

Returns:

  • (Boolean)


120
121
122
# File 'lib/scryer/query_watcher.rb', line 120

def enabled?
  !!@enabled
end

.watchObject

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