Project

General

Profile

Actions

Bug #22218

closed

Line TracePoint misses executed loop condition after a guard

Bug #22218: Line TracePoint misses executed loop condition after a guard

Added by ioquatix (Samuel Williams) 2 days ago. Updated 2 days ago.

Status:
Closed
Target version:
-
[ruby-core:<unknown>]

Description

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, 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

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

[1, 3]

Expected result

[1, 2, 3]

The same problem occurs with a non-constant predicate, for example:

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.

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.

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


Related issues 1 (0 open1 closed)

Related to Ruby - Bug #15980: Coverage shows while/until after raise if/unless as uncovered lineClosedmame (Yusuke Endoh)Actions

Updated by ioquatix (Samuel Williams) 2 days ago Actions #1

  • Related to Bug #15980: Coverage shows while/until after raise if/unless as uncovered line added

Updated by Anonymous 2 days ago Actions #2

  • Status changed from Open to Closed

Applied in changeset git|79e6d37b5569be4c772e9d41e8a5447e82c09683.


Preserve line events across jump optimization. (#18122)

[Bug #22218]

Actions

Also available in: PDF Atom