Class: Bulldogger::Record::Session
- Inherits:
-
Object
- Object
- Bulldogger::Record::Session
- Defined in:
- lib/bulldogger/record/session.rb
Overview
One recording session: a single TracePoint subscribed to :call/:return/:raise/:rescue, writing :call/:return/:raise as JSONL lines to one trace-NNN.jsonl file. The session captures and writes events; it does not query trace files.
:rescue is subscribed to but never written to the file -- it exists only to feed the raise-exit discriminator (see #on_return); WRITTEN_EVENTS is the actual shipped default the task report measures and docs describe.
Constant Summary collapse
- WRITTEN_EVENTS =
%w[call return raise].freeze
- SKIP_PATH_PREFIX =
A global (non-target) TracePoint is live for the entire process the instant #enable runs -- including the rest of this constructor after that line, and the entry into #stop before it disables anything. Without this filter, every session's trace opened with its own "Session#initialize returned" and "Record.start returned" lines (the latter dumping this session's own Config#inspect -- a value formatter never meant to be part of what a caller asked to observe), and closed with a "Session#stop" call line. Reusing FrameSource's already-measured skip_path_prefix (this library's own lib/ directory) filters out bulldogger's own frames the same way Capture already does for :raise.
Bulldogger::FrameSource.default_skip_path_prefix
- INTERNAL_TRACE_POINT_PATH =
Where Ruby reports TracePoint's own methods as defined.
"<internal:trace_point>"
Instance Method Summary collapse
-
#initialize(config:, run_dir:, sink: nil) ⇒ Session
constructor
sink: is an internal seam, not part of the public API a caller is meant to use.
-
#stop ⇒ Object
Idempotent, matching Bulldogger.start/stop and Run#finish elsewhere in this codebase: a second call must not re-close an already-closed IO (raises IOError) just because a caller defensively calls #stop from more than one place (an ensure block after an earlier explicit call, for example).
Constructor Details
#initialize(config:, run_dir:, sink: nil) ⇒ Session
sink: is an internal seam, not part of the public API a caller is meant to use. Bulldogger::Record.start never passes it; it exists only so the overhead benchmark (test/fixtures/record/ bench.rb) can isolate the cost of value capture (TracePoint dispatch, Formatter, Redactor) from the JSONL write that always follows it on the real path -- there is no production code path that captures without writing, so measuring that split needs a substitute writer to exist at all. When sink is given, run_dir is never touched.
49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 |
# File 'lib/bulldogger/record/session.rb', line 49 def initialize(config:, run_dir:, sink: nil) @config = config @enabled = config.enabled && !(run_dir.nil? && sink.nil?) return unless @enabled @redactor = Redactor.new(config.redact_patterns) @formatter = Formatter.new(config: config, redactor: @redactor) @writer = sink || Writer.new(run_dir: run_dir, header: header) @mutex = Mutex.new @sequence = 0 # Process-wide, not per-thread: it only ever needs to answer # "did a raise happen between this frame's :call and its # :return", and comparing the DELTA recorded at those two # points (not the counter's absolute value at :return alone) # makes that answer immune to an imbalance left over from # before this particular frame's call started. See the # contract's "raise で抜けたときの :return" note: an absolute # counter would misclassify a frame if some unrelated raise # elsewhere had already left the counter positive when this # frame's own :call fired. @raise_rescue_counter = 0 # One call stack per Thread. :call/:return for a given frame # always fire on the same thread that made the call, so keying # by Thread.current is what lets the delta comparison (and # "depth") match each :return to its own :call instead of some # other thread's -- a single shared stack would interleave # unrelated frames from concurrent threads and pop the wrong # entry. @call_stacks = Hash.new { |h, k| h[k] = [] } # Set just before the real TracePoint#disable call in #stop, # and checked first in #handle. Without it: TracePoint#disable # is itself a Ruby-level method (its own backtrace names # <internal:trace_point>), so calling it while this session's # TracePoint is still enabled fires one last :call/:return # through this same handler before the disable takes effect -- # a phantom "TracePoint#disable" line at the end of every # trace file (reproduced and confirmed while building this). @stopping = false # A Thread-local (not tp.disable) reentrancy guard. tp.disable # was tried first and rejected: TracePoint's enabled flag is # process-wide, so disabling it for the duration of one # thread's handler silently drops every other thread's events # that happen to fire in that same window -- reproduced with 4 # threads calling a traced method concurrently, which lost # about three-quarters of the expected :call/:return events # and corrupted #on_return's per-thread depth bookkeeping for # the survivors. A key unique to this Session (not a fixed # name) keeps two sessions -- e.g. Record.run nested inside # another -- from suppressing each other's events on the same # thread. @reentrant_key = :"__bulldogger_record_session_#{object_id}__" @trace_point = TracePoint.new(:call, :return, :raise, :rescue) { |tp| handle(tp) } @trace_point.enable end |
Instance Method Details
#stop ⇒ Object
Idempotent, matching Bulldogger.start/stop and Run#finish elsewhere in this codebase: a second call must not re-close an already-closed IO (raises IOError) just because a caller defensively calls #stop from more than one place (an ensure block after an earlier explicit call, for example).
109 110 111 112 113 114 115 116 |
# File 'lib/bulldogger/record/session.rb', line 109 def stop return nil unless @enabled return @result_path if @stopping @stopping = true @trace_point.disable @result_path = @writer.close end |