DI: backfill CodeTracker registry with iseqs for pre-loaded files#5496
DI: backfill CodeTracker registry with iseqs for pre-loaded files#5496
Conversation
Typing analysisNote: Ignored files are excluded from the next sections.
|
3efa407 to
6a54ca2
Compare
|
✨ Fix all issues with BitsAI or with Cursor
|
BenchmarksBenchmark execution time: 2026-04-09 17:38:34 Comparing candidate commit 52f34fb in PR branch Found 0 performance improvements and 0 performance regressions! Performance is the same for 45 metrics, 1 unstable metrics.
|
8156b73 to
87ae582
Compare
92caa43 to
485e23f
Compare
a058463 to
d1a219d
Compare
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: cd1e2d3b72
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…oaded code The prior text said "Code loaded before the tracer initializes cannot be instrumented with line probes." This is no longer accurate: #5496 backfills whole-file iseqs from object space at startup, and #5501 adds per-method iseq fallback for files whose whole-file iseq was garbage collected. Together these cover most pre-loaded application and gem code. The small remaining exception is files where all iseqs (whole-file and per-method) were GC'd before DI started. The updated text documents this accurately without overpromising. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Remove the incorrect limitation "Code loaded before the tracer initializes cannot be instrumented with line probes." After #5496 (whole-file iseq backfill) and this PR (per-method iseq fallback), line probes work for application and gem code regardless of load order. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…ent section The section said "Code loaded before the tracer initializes cannot be instrumented with line probes." After #5496 (whole-file iseq backfill) and this PR (per-method iseq fallback), that limitation no longer exists. Remove the section entirely rather than rewrite it. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…ent section The section said "Code loaded before the tracer initializes cannot be instrumented with line probes." After #5496 (whole-file iseq backfill) and this PR (per-method iseq fallback), that limitation no longer exists. Remove the section entirely rather than rewrite it. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
|
@codex review |
…ent section The section said "Code loaded before the tracer initializes cannot be instrumented with line probes." After #5496 (whole-file iseq backfill) and this PR (per-method iseq fallback), that limitation no longer exists. Remove the section entirely rather than rewrite it. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a104ffd302
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| # captured at load time and are authoritative. | ||
| next if registry.key?(path) | ||
|
|
||
| registry[path] = iseq |
There was a problem hiding this comment.
Install pending line probes for backfilled files
When backfill_registry adds a pre-loaded file to registry, it never triggers probe_manager.install_pending_line_probes(path), unlike the :script_compiled path in start. This leaves line probes stuck in pending_probes if they were queued before tracking started (e.g., delayed/programmatic activate_tracking! or tracker restart), so the newly backfilled file remains uninstrumented until it is reloaded and emits another compile event. Calling pending-probe installation when a new backfilled path is inserted would make backfill behave consistently with runtime load tracking.
Useful? React with 👍 / 👎.
992e365 to
d160b98
Compare
When CodeTracker starts, use the all_iseqs C extension to populate the registry with instruction sequences for files that were loaded before tracking began. This enables line probes on third-party code and application code loaded at boot time. Only whole-file iseqs (first_lineno == 0) are backfilled — per-method iseqs require instrumenter changes to select the correct iseq for a target line and will be supported in a follow-up. Backfill does not overwrite entries from :script_compiled, which are authoritative. The C extension availability is checked via DI.respond_to?(:all_iseqs) so the code gracefully degrades when the extension is not compiled. - Added CodeTracker#backfill_registry - Called from CodeTracker#start after trace point is enabled - Added RBS signature - Added tests for backfill behavior and C extension fallback Co-Authored-By: Claude <noreply@anthropic.com>
- Added rescue block around backfill_registry so failures are best-effort (logged + telemetry) rather than propagating - Replaced all skip-based tests with mock-based tests that exercise backfill logic without requiring the compiled C extension - Added tests for: mixed iseq types, multiple files, error handling, suffix/exact lookup on backfilled entries, start ordering - 27 examples, 0 failures, 0 pending, 0 skipped Co-Authored-By: Claude <noreply@anthropic.com>
Tests the end-to-end flow: test class loaded before code tracking starts → CodeTracker#start triggers backfill via all_iseqs C extension → iseq recovered from object space → line probe installed on backfilled iseq → probe fires and captures local variables. Runs under rake spec:di_with_ext (requires compiled C extension). Three test cases: - Probe installs successfully on backfilled iseq - Probe fires when target line executes - Snapshot captures local variables from backfilled iseq Co-Authored-By: Claude <noreply@anthropic.com>
On macOS CI the C extension is compiled, so backfill_registry populates the CodeTracker registry with pre-loaded files during start. This broke existing tests that expect the registry to be empty after start or to contain exactly N explicitly-loaded files. Fix by stubbing backfill_registry in test contexts that exercise :script_compiled behavior. Backfill is tested separately in its own describe blocks. Affected contexts: - CodeTracker #start (before block) - CodeTracker shared context 'when code tracker is running' - CodeTracker #iseqs_for_path_suffix (around block) - Instrumenter shared context 'with code tracking' Co-Authored-By: Claude <noreply@anthropic.com>
…kfill The backfill filter used first_lineno == 0 to identify whole-file iseqs, but most whole-file iseqs from all_iseqs have first_lineno == 1. The new DI.iseq_type method reads the iseq type directly from the Ruby VM struct and returns a symbol (:top, :method, :block, :class, etc.). The backfill now filters by type == :top || type == :main, which correctly identifies whole-file iseqs regardless of first_lineno. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
rb_iseq_type is an internal Ruby function that only exists in Ruby 3.1+. On Ruby 2.7 and 3.0, referencing it causes an undefined symbol error at load time, crashing the entire C extension (including all_iseqs and exception_message which work fine on those versions). Use have_func in extconf.rb to detect rb_iseq_type at compile time, and wrap the iseq_type function + registration in #ifdef HAVE_RB_ISEQ_TYPE. The Ruby code in code_tracker.rb already handles the missing method via DI.respond_to?(:iseq_type) with a first_lineno fallback. Co-Authored-By: Claude <noreply@anthropic.com>
… require - Add doc comments for rb_iseqw_new and rb_iseqw_to_iseq prototypes in di.c (internal Ruby functions used without documentation) - Add error handling test coverage for backfill_registry: verify logger.debug is called with the error message and telemetry.report is called when DI.current_component is available - Add test coverage for the first_lineno == 0 fallback path when iseq_type is unavailable (Ruby versions without rb_iseq_type) - Add missing require "datadog/di/spec_helper" to iseq_type_spec.rb for consistency with other ext specs - Fix skip message: iseq_type availability depends on rb_iseq_type in the Ruby runtime, not on the DI C extension Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- di.c: Document that rb_iseq_type was added in Ruby 3.1, explain the HAVE_RB_ISEQ_TYPE compile-time guard, and note the fallback path - code_tracker.rb: Replace "first_lineno == 0" YARD doc with full description of both strategies (iseq_type on 3.1+, first_lineno heuristic on older Rubies) and their tradeoffs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two root causes: 1. code_tracker_spec.rb: iseq_type was stubbed with and_call_original, but the C function expects a real RubyVM::InstructionSequence, not a test double. Stub returns :top for first_lineno==0, :method otherwise. 2. backfill_integration_spec.rb: The top-level file iseq (first_lineno=0, type=:top) is not referenced by any constant or method after loading. GC could collect it between require_relative (file load time) and the before block's backfill_registry call. Move GC.disable to file level, immediately before require_relative, so the iseq survives until backfill walks the object space. Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
The ivar is initialized to nil to avoid Ruby 2.6/2.7 warnings. RBS type needs to reflect this. Silence false positive on << after ||= (Steep doesn't track that ||= guarantees non-nil). Co-Authored-By: Claude <noreply@anthropic.com>
Root cause: the top-level (:top) iseq for the test class file has no references after loading completes — only class/method child iseqs survive via BackfillIntegrationTestClass. The previous approach disabled GC at file load time and re-enabled it in the before block after backfill. This protected the first test, but after deactivate_tracking! cleared the registry (the only reference to the iseq), GC could collect it before the next test's backfill_registry walked object space. Fix: capture the top-level iseq in a constant (BACKFILL_TEST_TOP_ISEQ) immediately after loading, before GC can collect it. The constant keeps the iseq alive for the lifetime of the process, so backfill_registry can find it in every test regardless of GC activity. Verified: 0 failures across 8 consecutive full DI suite runs (714 examples each), vs ~20% failure rate before the fix. Co-Authored-By: Claude <noreply@anthropic.com>
Add "Iseq Lifecycle and GC" section to DynamicInstrumentationDevelopment.md covering: iseq types created on file load, which survive GC and why, implications for backfill_registry, and the correct test pattern for keeping top-level iseqs alive across tests. Add cross-reference from code_tracker.rb backfill_registry docstring. Co-Authored-By: Claude <noreply@anthropic.com>
The rescue branch already returns nil explicitly, but the happy path returned the last value from registry_lock.synchronize. Make both paths consistent with the void RBS signature. Co-Authored-By: Claude <noreply@anthropic.com>
Targeted TracePoints are bound to the specific iseq object, not the source location. A TracePoint on a compile_file iseq does NOT fire when the require-produced iseq for the same source executes — they are different objects with separate instruction bytes and hook lists (confirmed by reading vm_trace.c:rb_tracepoint_enable_for_target and iseq.c:iseq_add_local_tracepoint). If backfill_registry registered a compile_file iseq, probes installed against it would silently never fire. Fix: require first_lineno == 0 alongside iseq_type on Ruby 3.1+. compile_file produces :top iseqs with first_lineno=1, while require/load produces :top iseqs with first_lineno=0. Co-Authored-By: Claude <noreply@anthropic.com>
Tests the full lifecycle when Bootsnap serves cached iseqs via load_iseq instead of compiling from source: - CodeTracker registry captures Bootsnap-cached files - Registered iseq is the one Ruby executes (TracePoint fires) - Probe installs on Bootsnap-cached file - Probe fires when target line executes - Snapshot captures local variables correctly - Bootsnap cache files exist on disk (precondition verification) Requires bootsnap gem in the test environment. Skips gracefully if bootsnap is not available. Infrastructure changes needed (not in this commit): - Add :bootsnap to DI contrib list in Rakefile - Add gem 'bootsnap' to rails appraisals - Add di:bootsnap entry in Matrixfile Co-Authored-By: Claude <noreply@anthropic.com>
The primary DI code path (file loaded after tracking starts) had no end-to-end test. Unit tests verified the registry was populated but never installed a probe and verified it fired. Tests added: - CodeTracker captures the file in registry - Registered iseq is the one Ruby executes (TracePoint fires) - Probe installs on the file - Probe fires when target line executes - Snapshot captures local variables correctly Co-Authored-By: Claude <noreply@anthropic.com>
Infrastructure for the Bootsnap integration test: - Add :bootsnap to DI contrib list in Rakefile - Add gem 'bootsnap' to rails8-mysql2 appraisals (ruby 3.2-4.0) - Add di:bootsnap Matrixfile entry (rails8-mysql2, ruby 3.2+) Bootsnap is a standard Rails companion gem. Adding it to the rails8 appraisal does not affect other tests — it's an extra gem in the bundle that's only exercised by spec/datadog/di/contrib/bootsnap/. Co-Authored-By: Claude <noreply@anthropic.com>
- Remove load_iseq from prepended module in after block instead of leaving the prepend as a permanent global mutation. Uses the same pattern as the DI instrumenter for method probe cleanup: the module stays in the ancestor chain but becomes a no-op. - Regenerate ruby_3.2_rails8_mysql2.gemfile to include bootsnap. Other Ruby version gemfiles will be regenerated in their respective CI environments. Co-Authored-By: Claude <noreply@anthropic.com>
- Rewrite `unless` with mixed logical operators to `if` with negated conditions in backfill_registry (Style/UnlessLogicalOperators) - Remove redundant begin block and line continuations in Bootsnap test Co-Authored-By: Claude <noreply@anthropic.com>
…oads The skip guard only caught LoadError, but bootsnap can load successfully while its iseq cache fails to initialize (e.g. C extension not compiled for this environment). This caused test failures in CI jobs where bootsnap was available as a transitive dependency but couldn't install its iseq cache. Fix: probe-test the iseq cache in before(:all) — install it in a temp dir, clean up, and skip if any error occurs. Co-Authored-By: Claude <noreply@anthropic.com>
The require was inside before(:all), which loads bootsnap into the process at test execution time — after other test infrastructure has initialized. Moving it to file load time makes the side effect visible and predictable. If bootsnap isn't available or its iseq cache can't initialize, the constant BOOTSNAP_AVAILABLE is false and the entire describe block skips via before(:all). Also eliminates the standard:disable comment for Style/RedundantBegin since the rescue is now in a top-level begin/rescue/end, not a block. Co-Authored-By: Claude <noreply@anthropic.com>
… RBS type - compile_file_backfill_spec.rb: fix skip message "Ruby < 3.1" → "Ruby >= 3.1 only" - iseq_type_spec.rb: rename "eval with top-level code" → "compiled top-level code" (the test uses compile(), not eval) - code_tracker_spec.rb: remove two comments that just restate the expectation below them - code_tracker_spec.rb: rename "calls backfill_registry after trace point is enabled" test — it didn't actually verify ordering, only that active? is true after start - sig/datadog/di.rbs: iseq_type return type Symbol → Symbol? (C code returns Qnil when rb_iseqw_to_iseq returns NULL) Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
The guard fires in zero CI jobs — the di:bootsnap job always has bootsnap in the gemfile. It only fires when a developer runs the file manually without the correct gemfile, in which case a LoadError is the right signal. More importantly, the StandardError rescue around install!() could silently skip all tests if bootsnap's C extension fails on a new Ruby version, hiding a real compatibility break as green CI. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
The test "does not register compile_file iseqs" passed vacuously when the code was correct — the `if result` block meant zero assertions ran when result was nil. Add `expect(result).to be_nil` to directly assert the intended behavior. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
require "bootsnap" alone does not load Bootsnap::CompileCache::ISeq — that submodule must be required explicitly. Without it, the spec fails with NameError when referencing Bootsnap::CompileCache::ISeq. Also restores the BOOTSNAP_AVAILABLE skip guard that was removed, so the tests degrade gracefully when bootsnap or its ISeq cache is unavailable. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
The di:bootsnap CI task runs with rails8-mysql2 gemfile but does not compile the DI C extension (that is done by di_with_ext). When DI::Component.build calls environment_supported?, it finds DI.respond_to?(:exception_message) is false and calls logger.warn(). The strict instance_double rejects the unexpected warn call, failing the test before it can exercise any Bootsnap behavior. Add a skip guard parallel to the existing BOOTSNAP_AVAILABLE guard. Co-Authored-By: Claude <noreply@anthropic.com>
- Add @return [void] YARD tag to backfill_registry - Restructure compile_file_backfill_spec into two contexts: - "with iseq_type (Ruby 3.1+)" — existing test, skips on < 3.1 - "with first_lineno fallback (all Ruby versions)" — new test that stubs iseq_type away and verifies the first_lineno == 0 heuristic also excludes compile_file iseqs (which have first_lineno == 1) - Extract shared setup into helper method to avoid duplication Co-Authored-By: Claude <noreply@anthropic.com>
The previous fix added a skip guard for the DI C extension, which is a build dependency not a platform limitation. That guard silently skipped all bootsnap tests on every CI run. Correct fix: add the compile dependency to the di:bootsnap Rake task, matching the pattern used by di:di_with_ext. Remove the skip guard. Co-Authored-By: Claude <noreply@anthropic.com>
The di:bootsnap CI job runs against rails8-mysql2 gemfiles which include bootsnap. The skip guard fires in zero CI jobs — it only fires when a developer runs the file manually without bootsnap installed. That's a local dev convenience skip, not a platform limitation. If bootsnap can't load, the test should fail — that tells the developer they need the right gemfile. Requires and initialization now happen at file load time without rescue, so failures are immediate and visible. Co-Authored-By: Claude <noreply@anthropic.com>
The file-level Bootsnap::CompileCache::ISeq.install!(BOOTSNAP_VERIFY_DIR) set Bootsnap's internal cache directory, which was then deleted. Subsequent install!(cache_dir) calls in the before block may not update the internal state, causing cache files to be written to the deleted directory. Other bootsnap tests passed because they don't check for cache files on disk. Fix: remove the file-level install!/cleanup entirely. The requires at file level are sufficient to prove bootsnap loads. The test's before block handles install! with the correct cache directory. Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
31fa59b to
52f34fb
Compare
Bootsnap's load_iseq intentionally returns nil when Coverage is active (line 53 of bootsnap/compile_cache/iseq.rb), disabling iseq disk caching entirely. This means no cache files are written during CI runs with SimpleCov, causing the "Bootsnap cache was actually used" test to fail — it checks for cache files on disk. This is a genuine platform condition: Bootsnap + Coverage are incompatible for disk caching. Other bootsnap tests (iseq capture, TracePoint firing, probe installation, snapshot capture) pass because they test the :script_compiled path, not the disk cache. Root cause of the previous incorrect fix (removing file-level install!): the file-level install! was not the problem — Coverage was. Co-Authored-By: Claude <noreply@anthropic.com>
bd63696 to
45a9f83
Compare
This reverts commit 45a9f83.
Bootsnap's load_iseq intentionally returns nil when Coverage is running (SimpleCov in CI), disabling iseq disk caching entirely. This caused the "Bootsnap cache was actually used" test to fail — no cache files were written during the priming load. Fix: suspend Coverage for the duration of each bootsnap test example. Coverage.suspend (Ruby 3.2+) pauses coverage collection, allowing Bootsnap's load_iseq to proceed normally. di:bootsnap runs on 3.2+ only, so Coverage.suspend is always available. The suspend/resume is tracked via @coverage_was_suspended to avoid calling Coverage.resume in local dev where Coverage was never started (which would raise RuntimeError). Co-Authored-By: Claude <noreply@anthropic.com>
What does this PR do?
When CodeTracker starts, backfills the iseq registry with instruction sequences for files that were loaded before tracking began. Also adds a
DI.iseq_typeC extension (Ruby 3.1+) for precise whole-file iseq detection.Motivation:
CodeTracker only captures files loaded after DI starts via
:script_compiledTracePoint events. Most application code and all gems are loaded at boot time before DI activates, making them invisible to line probes. This change walks the Ruby object space via theall_iseqsC extension (#5111) and recovers whole-file iseqs for already-loaded code.Implementation:
CodeTracker#backfill_registrywalks the object space viaDI.file_iseqs, filters to whole-file iseqs, and stores them in the registry without overwriting entries from:script_compiled.DI.iseq_typeon Ruby 3.1+ (returns:topfor require/load,:mainfor the entry script). On Ruby < 3.1, falls back tofirst_lineno == 0— which produces the same result because Ruby'srb_iseq_new_topandrb_iseq_new_mainpassINT2FIX(0)while method/class/block definitions get their source line (>= 1).DI.iseq_typewraps the internalrb_iseq_type()function added in Ruby 3.1 (commit89a02d89by Koichi Sasada). Guarded behindhave_func('rb_iseq_type')in extconf.rb — only compiled when the symbol exists.startafter the trace point is enabled, so files loaded concurrently are captured by the trace point (backfill won't overwrite them).compile_fileiseqs are excluded viafirst_lineno == 0check on all Ruby versions. Targeted TracePoints are bound to the specific iseq object — acompile_fileiseq is distinct from therequire-produced iseq, so a probe installed against it silently never fires. Verified by readingvm_trace.candiseq.c.Change log entry
Yes. DI/LD: more third-party libraries are now instrumentable in DI/LD.
Additional Notes:
all_iseqsandexception_messageC extensions)di:bootsnapCI task with bootsnap gem in rails8 appraisals for Bootsnap integration testing:script_compiled(file loaded after tracking) and backfill (file loaded before tracking), including snapshot capture with local variablesDocumentation: Updated in #5501. While this PR qualitatively improves which third-party code is instrumentable, in our testing the gains from whole-file backfill alone are ~10% of pre-loaded files (the rest have GC'd top iseqs and require per-method backfill from #5501). To avoid churn, #5501 removes the entire "Code Tracking Requirement" section from
docs/DynamicInstrumentation.md; this PR intentionally keeps the existing docs as-is.How to test the change?
Unit tests for
backfill_registry(filtering, overwrite protection, error boundary, fallback path, compile_file exclusion). Integration tests: backfill path (load before tracking → probe installs and fires → snapshot captured) and:script_compiledpath (load after tracking → probe installs and fires → snapshot captured). Bootsnap integration test (requires bootsnap gem, runs indi:bootsnapCI job). TracePoint binding tests verifyingcompile_fileiseqs don't fire forrequire-produced code.Manually tested in gobo by inspecting the DI code tracer registry. Before:
After:
Manual testing via gobo targeting stdlib: