Module: Sixty::Stack
- Defined in:
- lib/sixty/stack.rb
Overview
Where in your code did this operation come from.
A finding you cannot locate in your own repository is much less actionable — "select:orders returns 30,000 rows" is only useful once you know which file writes that query. In a Rails app that file is almost never the one that executes the statement: ActiveRecord does, through six frames of adapter, relation and log subscriber. So the frames kept here are the application's own, and the ORM's are skipped.
── Why this is affordable ────────────────────────────────────────────────
Capturing a backtrace costs microseconds, which is unacceptable per query. But a call site is a property of the operation, not of the call: the same query is issued from the same place every time. So the stack is captured on the first sighting of a normalized statement and never again. The cost amortises to nothing over a process lifetime and is bounded by operation cardinality, which is already capped.
── Why several frames, not one ───────────────────────────────────────────
Applications funnel queries through helpers — a scope, a query object, a concern. The top application frame is therefore that helper for every query, which is useless. Rather than guess which frame is "the real caller" with a heuristic that will often be wrong, the top few application frames are kept and shown. The developer recognises their own code immediately, and no guess has to be correct.
Constant Summary collapse
- MAX_FRAMES =
4- MAX_TRACKED =
matches the aggregator's operation cap
2000- NOT_USER_CODE =
Frames from Ruby itself, from installed gems, and from this agent are never the answer — the caller wants their own code.
<internal:covers the frames Ruby's own prelude contributes. %r{ /gems/ | /ruby/\d | /rubygems/ | ^<internal: | /bundler/ | /packages/ruby/lib/sixty/ }x.freeze
Class Attribute Summary collapse
-
.root ⇒ Object
Returns the value of attribute root.
Class Method Summary collapse
-
.capture(key) ⇒ Array<Hash>?
Frames, once per key, nil every time after.
- .mutex ⇒ Object
-
.relativise(path) ⇒ Object
Repository-relative paths, so a frame is comparable across machines and can be turned into a link into the commit that produced it.
-
.reset! ⇒ Object
Exported for tests: forget what has been seen.
- .seen ⇒ Object
Class Attribute Details
.root ⇒ Object
Returns the value of attribute root.
42 43 44 |
# File 'lib/sixty/stack.rb', line 42 def root @root end |
Class Method Details
.capture(key) ⇒ Array<Hash>?
Returns frames, once per key, nil every time after.
54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
# File 'lib/sixty/stack.rb', line 54 def capture(key) return nil if key.nil? || key.empty? mutex.synchronize do return nil if seen.key?(key) || seen.size >= MAX_TRACKED seen[key] = true end frames = [] # Deep enough to get past ActiveRecord's adapter stack, not so deep that # building the array becomes the expensive part. caller_locations(2, 40).each do |location| path = location.absolute_path || location.path next if path.nil? || NOT_USER_CODE.match?(path) frames << { file: relativise(path), line: location.lineno, fn: location.label.to_s[0, 80] } break if frames.length >= MAX_FRAMES end frames.empty? ? nil : frames end |
.mutex ⇒ Object
48 49 50 |
# File 'lib/sixty/stack.rb', line 48 def mutex @mutex ||= Mutex.new end |
.relativise(path) ⇒ Object
Repository-relative paths, so a frame is comparable across machines and can be turned into a link into the commit that produced it. An absolute deploy path is meaningless to a reader and discloses the layout of the host it was built on.
81 82 83 84 85 86 87 |
# File 'lib/sixty/stack.rb', line 81 def relativise(path) prefix = root return path if prefix.nil? || prefix.empty? prefix = "#{prefix}/" unless prefix.end_with?('/') path.start_with?(prefix) ? path[prefix.length..] : path end |
.reset! ⇒ Object
Exported for tests: forget what has been seen.
90 91 92 |
# File 'lib/sixty/stack.rb', line 90 def reset! mutex.synchronize { @seen = {} } end |
.seen ⇒ Object
44 45 46 |
# File 'lib/sixty/stack.rb', line 44 def seen @seen ||= {} end |