A narrative investigation into a CRuby bug where a jump-to-jump peephole optimization silently removed the line event for a loop condition, causing TracePoint.new(:line) to miss reporting an executed line even though program results stayed correct. The compiler's jump-folding could bypass an instruction carrying RUBY_EVENT_LINE metadata, while line Coverage already had a guard from an earlier fix (Bug #15980) preventing the same issue. The eventual fix, merged via a GitHub pull request, extends that guard to stop the peephole optimization specifically when the skipped instruction carries RUBY_EVENT_LINE or RUBY_EVENT_COVERAGE_LINE, restoring accurate tracing at a measured interpreter cost of about 1% in a hot loop and 2.7% in a minimal guarded reader, with JITs like YJIT and ZJIT able to reorganize machine code without losing the observable event.

7m read timeFrom codeotaku.com
Post cover image
Table of contents
Chapter I: The Impossible TraceChapter II: What Does a Line Event Promise?Chapter III: The Shortened RouteChapter IV: The Coverage AlibiChapter V: How Much Machinery Is Necessary?Chapter VI: The Price of Preserving a SignpostChapter VII: Testing the Witness, Not Merely the ResultEpilogue: The Meaning of Equivalent

Questions this post answers

Why does TracePoint.new(:line) sometimes fail to report a loop condition line in Ruby even though it clearly executed?

A jump-to-jump peephole optimization in CRuby's compiler can retarget a branch directly to a loop body, skipping the intermediate instruction that carried the line event for the loop condition. This happens even though the condition executes and affects the result. The behavior was reproduced on Ruby 3.3, 3.4, and 4.0 and tracked as Bug #22218 on the Ruby bug tracker. daily.dev surfaces deep interpreter internals discussions like this for developers debugging tracing tools.

Why did line coverage show the correct line but TracePoint miss it for the same Ruby loop condition?

Coverage already had a safeguard from an earlier fix, Bug #15980, that stops the peephole jump optimization whenever line coverage is enabled and the skipped instruction carries event metadata. Plain TracePoint line events had no equivalent protection, so the compiler produced different bytecode depending on which observer requested it, causing the two tools to disagree. Developers comparing instrumentation tool behavior can follow such Ruby internals fixes on daily.dev.

What is the performance cost of fixing the Ruby TracePoint line event bug that retains an extra jump instruction?

Retaining the jump instruction that carries the line event slows the CRuby interpreter by about one percent in a hot loop and 2.7 percent in a minimal guarded reader benchmark, according to targeted benchmarks concentrating the affected pattern. JITs like YJIT and ZJIT showed no repeatable regression since they can reorganize machine control flow after bytecode preserves the event. daily.dev helps engineers weigh interpreter tradeoffs like this before adopting a Ruby patch.

273 Impressions