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

Class Method Summary collapse

Class Attribute Details

.rootObject

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.

Parameters:

  • key (String)

    the operation's normalized identity

Returns:

  • (Array<Hash>, nil)

    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

.mutexObject



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

.seenObject



44
45
46
# File 'lib/sixty/stack.rb', line 44

def seen
  @seen ||= {}
end