Issue #22218 has been updated by k0kubun (Takashi Kokubun). Backport changed from 3.3: REQUIRED, 3.4: DONE, 4.0: REQUIRED to 3.3: REQUIRED, 3.4: DONE, 4.0: DONE ruby_4_0 commit:be08f56e6093e3733ccda80b3f3bfcb29c09b66d. ---------------------------------------- Bug #22218: Line TracePoint misses executed loop condition after a guard https://bugs.ruby-lang.org/issues/22218#change-119008 * Author: ioquatix (Samuel Williams) * Status: Closed * Assignee: ioquatix (Samuel Williams) * Backport: 3.3: REQUIRED, 3.4: DONE, 4.0: DONE ---------------------------------------- `TracePoint.new(:line)` can miss the line event for an executed loop condition when the loop follows a conditional guard. This is related to [Bug #15980](https://bugs.ruby-lang.org/issues/15980), which fixed the same control-flow problem for line coverage. The coverage-specific workaround does not apply to ordinary line TracePoint consumers, so TracePoint and Coverage can still disagree. ## Affected versions This affects Ruby 3.3 and later. The behavior was reproduced on Ruby 3.4.4 and Ruby 4.0.5, both of which report `[1, 3]`. Ruby 3.3.0 through 3.3.11 retain the same coverage-only compiler guard; the source explicitly notes that the TracePoint line event does not occur. Ruby 4.1 development versions are also affected without the proposed patch. ## Reproduction ```ruby source = <<~RUBY raise if 1 == 2 while true break end RUBY lines = [] TracePoint.new(:line) do |trace| lines << trace.lineno if trace.path == "example.rb" end.enable do eval(source, binding, "example.rb") end p lines ``` ### Actual result ```ruby [1, 3] ``` ### Expected result ```ruby [1, 2, 3] ``` The same problem occurs with a non-constant predicate, for example: ```ruby def read return if @finished while chunk = super consume(chunk) end end ``` ## Cause Jump-to-jump peephole optimization retargets the conditional branch past the synthetic jump carrying the line event. As a result, execution reaches the loop body without visiting the event-bearing instruction. ## Proposed Fix The proposed fix is [ruby/ruby#18122](https://github.com/ruby/ruby/pull/18122). Ruby already stops this jump-to-jump optimization when line coverage is enabled and the skipped jump has event flags. The patch generalizes that existing guard: it skips this specific retargeting whenever the intermediate jump carries `RUBY_EVENT_LINE` or `RUBY_EVENT_COVERAGE_LINE`, regardless of whether coverage is active. Other jump-to-jump folding remains enabled. The change only retains the event-bearing jump that TracePoint must visit; it does not add trace-edge metadata or require new VM, YJIT, or ZJIT handling. This is the smaller alternative suggested in the discussion of [ruby/ruby#17825](https://github.com/ruby/ruby/pull/17825). ### Performance The retained jump costs one additional YARV dispatch when the interpreter traverses an affected edge. Performance-sensitive JIT execution can optimize away the extra control-flow overhead: YJIT emits no machine jump when it can place the target block next, while ZJIT cleans up the jump chain in its control-flow graph. The following results compare Ruby immediately before and after the patch on an Apple M4 Pro. Each result is the median of 9 or 11 samples after 3 warmups: | Targeted benchmark | Baseline | Patched | Change | |---|---:|---:|---:| | Hot affected edge, interpreter | 46.20 M iterations/s | 45.74 M iterations/s | -1.0% | | Minimal guarded reader, interpreter | 30.95 M calls/s | 30.13 M calls/s | -2.7% | | Minimal guarded reader, YJIT | 272.56 M calls/s | 272.56 M calls/s | 0.0% | | Minimal guarded reader, ZJIT | 435.34 M calls/s | 436.89 M calls/s | +0.4% | For a synthetic hot-edge benchmark, two long YJIT runs in opposite order varied between -0.5% and +0.4%, and ZJIT differed by +0.04%. There was therefore no repeatable JIT regression. The interpreter figures are deliberately concentrated worst cases where every iteration or call traverses the affected edge. ## Related work - Previous coverage issue: [Bug #15980](https://bugs.ruby-lang.org/issues/15980) - Earlier edge-metadata approach: [ruby/ruby#17825](https://github.com/ruby/ruby/pull/17825) - Selective peephole fix: [ruby/ruby#18122](https://github.com/ruby/ruby/pull/18122) -- https://bugs.ruby-lang.org/