Skip to content

Shopify omnibus refresh: JIT frames + skip_native_resume - #45

Closed
dalehamel wants to merge 22 commits into
mainfrom
dale/omnibus-37-38-refresh-20260430
Closed

dalehamel wants to merge 22 commits into
mainfrom
dale/omnibus-37-38-refresh-20260430

Conversation

@dalehamel

@dalehamel dalehamel commented Apr 30, 2026 •

Copy link
Copy Markdown
Member

Summary

Current head: e74cbe6dfd415c5349ee524af4df74771cfff7b5

Validation

Profiles-processor bump PR: https://github.com/Shopify/profiles-processor/pull/1204

  • profiles-processor Buildkite #4775 passed on 20e828d6dfca4c20273f66eabfa7d448fb8e786c.
  • profiles-processor code-scanner #4690 passed on 20e828d6dfca4c20273f66eabfa7d448fb8e786c.

Production QA

Short dev profile runs with prophiler image tag 20e828d6dfca4c20273f66eabfa7d448fb8e786c / image digest sha256:8dcf943c8bbd15f6e022905d232488234497b9f19310e9bd2952a65e21d19161.

Queries are native_profiles CPU scoped to process.executable.name="ruby" and each node.

App Variant Cluster Node Env var Job Observe Samples ERROR_4012: native_no_pid_page_mapping <unknown jit code>
Core Control core-us-ce1b-sy3 gke-core-us-ce1b-sy3-core-web-1-04315b31-2797 not set ebpf-profiler-20260506-140535 https://observe.shopify.io/a/investigate-profiles/id/3080cd14d350a115 24,244 0.24% self / 0.38% total not rendered; graph dominated by other at 90.55% self
Core Treatment core-us-ce1b-sy3 gke-core-us-ce1b-sy3-core-web-1-04315b31-2d77 OTEL_EBPF_RUBY_SKIP_NATIVE_RESUME=true ebpf-profiler-20260506-140633 https://observe.shopify.io/a/investigate-profiles/id/abaee6516bfb1307 17,928 0.50% self / 0.71% total 12.66% self / 60.37% total
SFR Control storefront-us-ce1b-tk2 gke-storefront-us-ce-storefront-rende-75b8f29d-26cp not set ebpf-profiler-20260506-140708 https://observe.shopify.io/a/investigate-profiles/id/062c919e808c28a9 45,168 0.21% self / 0.38% total 9.52% self / 86.32% total
SFR Treatment storefront-us-ce1b-tk2 gke-storefront-us-ce-storefront-rende-75b8f29d-2ml6 OTEL_EBPF_RUBY_SKIP_NATIVE_RESUME=true ebpf-profiler-20260506-140742 https://observe.shopify.io/a/investigate-profiles/id/77cb22fe728e085d 45,668 0.20% self / 0.39% total 9.54% self / 85.46% total

All Kubernetes jobs completed. The wide fallback continues to keep ERROR_4012 low versus the pre-fallback Core results (~10-11% self and ~72-96% total).

dalehamel and others added 22 commits May 6, 2026 09:39
… cleanup

- Extract JIT region detection into standalone findJITRegion() function
  that handles both prctl-labeled mappings (spanning full reserved area
  including holes) and heuristic fallback (first anonymous executable mapping)
- Add comprehensive unit tests for JIT region detection: no mappings,
  file-backed only, labeled single/multiple, heuristic fallback, precedence
- Fix Detach() to clean up PidInterpreterMapping prefixes (previously only
  deleted proc data, leaking eBPF map entries)
- Remove dead code: m.Vaddr < jitMapping.Vaddr branch that could never be
  true since /proc/pid/maps is sorted by address
- Fix JIT region check to use >= for lower bound (half-open interval [start, end))
  matching V8 and HotSpot conventions
- Reuse map lookup value to avoid double hashing of RawMapping struct key
- Wrap CalculatePrefixList error for better debugging
- Log stale prefix deletion errors instead of silently discarding
- Move prctl(PR_SET_VMA) named anonymous mapping support from process/process.go
  (was in PR #36 but is needed here for findJITRegion labeled detection)
- Remove stale 'assume all anonymous' comment
- Rebuild BPF blobs for >= fix
- Add 'Register LPM prefixes' comment to SynchronizeMappings loop
- Extract in_jit bool variable from inline if condition for clarity
- Improve JIT detection comments (read_ruby_frame and walk_ruby_stack)
- Fix whitespace: add blank line after FRAMES_PER_WALK_RUBY_STACK define,
  normalize MAX_EP_CHECKS spacing
- Rebuild BPF blobs
Ruby 3.4.7 with --yjit on arm64. Verifies that:
- JIT code region is detected from anonymous executable mappings
- A JIT frame is pushed for the YJIT-compiled code
- Ruby VM frames (is_prime, sum_of_primes, loop) are properly unwound
  after the JIT frame

On main, this coredump fails with native_no_pid_page_mapping because
the profiler doesn't know how to handle the anonymous JIT mapping.

Moduledata tar for CI upload: ~/coredump-uploads/ruby-yjit-moduledata.tar
Ruby 3.4.7 with --yjit on x86_64. Same verification as arm64 test:
- JIT code region detected from anonymous executable mappings
- JIT frame pushed for YJIT-compiled code
- Full Ruby VM stack unwound (is_prime, sum_of_primes, loop)

Moduledata tar: ~/coredump-uploads/ruby-yjit-amd64-moduledata.tar
On systems with CONFIG_ANON_VMA_NAME=y, Ruby labels its JIT memory
region via prctl(PR_SET_VMA), giving it a path like
'[anon:Ruby:rb_yjit_reserve_addr_space]'. Without this fix, IsFileBacked()
returns true for these mappings (non-empty Path), causing IsAnonymous()
to return false. This prevents the LPM prefix registration loop in
SynchronizeMappings from registering JIT executable pages for eBPF
dispatch.

Add IsPrctlNamed() helper and exclude prctl-named mappings from
IsFileBacked(), so they are correctly classified as anonymous.
Co-authored-by: Florian Lehner <florianl@users.noreply.github.com>
Adapt SynchronizeMappings to work with uint32 values instead of *uint32
pointers: since generations are no longer shared via pointer, both mappings
and their prefixes must be updated independently each cycle. Use an isNew
flag to only call UpdatePidInterpreterMapping for newly discovered mappings.
Address florianl's concern about unlimited growth: entries are pruned
each SynchronizeMappings call via generation tracking, and the map size
is bounded by the number of executable anonymous mappings per process.
Only push the JIT frame when trace->num_frames == 0 (leaf position),
restoring the original behavior. Without this guard, when native frames
were already unwound before the Ruby unwinder runs, the JIT frame was
pushed deep in the stack instead of at the leaf, causing it to appear
as the root frame in flamegraphs.
walk_ruby_stack is re-entered via tail calls to process more frames.
On re-entry, in_jit was recomputed from record->state.pc which hasn't
changed (non-FP path), causing the JIT frame to be pushed again on
every tail call. Guard with !jit_detected so the JIT frame is only
pushed once on the first entry.
When the Ruby unwinder encounters a cfunc (C-implemented Ruby method),
it normally transitions back to the native unwinder to unwind through
the C code, then re-enters the Ruby unwinder when it detects the Ruby
interpreter loop. Each such round-trip costs 2-3 tail calls out of a
budget of 29, so a Ruby stack with 10+ cfuncs can easily truncate.

This adds a skip_native_resume flag to RubyProcInfo that, when set,
pushes cfunc frames inline without transitioning to the native unwinder.
This trades native frames within cfuncs (e.g., the C call chain inside
Array#sort) for complete Ruby-level traces without truncation.

The flag is controlled by OTEL_EBPF_RUBY_SKIP_NATIVE_RESUME (default:
false). Set to 'true' to enable.
- Correct inline comment that incorrectly stated 'Default to skipping'
  when the actual default is false (requires explicit opt-in)
- Add note that skipNativeResume() is evaluated per-process at attach time
When skip_native_resume is true, the end-of-stack check should stop
instead of resuming native unwinding. Without this, the native unwinder
can re-enter the Ruby unwinder from JIT anonymous mappings, pushing
frames in wrong positions.
@dalehamel
dalehamel force-pushed the dale/omnibus-37-38-refresh-20260430 branch from 78728b7 to e74cbe6 Compare May 6, 2026 13:42
@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown

This PR was marked stale due to lack of activity. It will be closed in 14 days.

@github-actions github-actions Bot added the Stale label Jun 4, 2026
@github-actions

Copy link
Copy Markdown

Closed as inactive. Feel free to reopen if this PR is still being worked on.

@github-actions github-actions Bot closed this Jun 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant