Skip to content

LuaJIT: Merge Lua and native stacks in postprocessing - #127

Open
GitSparTV wants to merge 7 commits into
yandex:mainfrom
GitSparTV:luajit-stack-postprocessing
Open

GitSparTV wants to merge 7 commits into
yandex:mainfrom
GitSparTV:luajit-stack-postprocessing

Conversation

@GitSparTV

@GitSparTV GitSparTV commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

Depends on merge of #123

This PR merges LuaJIT frames into the native call stack. Before this, Lua frames were shown as a separate block at the bottom of the flamegraph, even below kernel, so the flamegraph didn’t show where execution crossed between Lua and C. VM dispatch frames such as lj_BC_TSETS also stayed visible even when the Lua frames already described what was running, making the flamegraph harder to read because of fragmentized nodes.

Changes

  • Record the LuaJIT VM address range and Lua unwind status in a metadata frame. It's carried with the stack of a sample.
  • Distinguish native C calls, assembly fast functions, and protected calls so the merger can find the right boundaries.
  • Merge Lua and native frames without relying on native symbol names. If the boundaries don’t line up, preserve all captured frames, but put frame before kernel frames at least (light postprocessing).
  • Remove the metadata frame from the output, even when Lua merging is disabled.

How it works

  1. Capture the VM range in eBPF. The Lua stack starts with a metadata frame containing the VM address range and unwind status. The agent gives it the same native mapping as the VM code so its addresses stay aligned with the native stack during profile conversion.

  2. Find where Lua calls into C. Native C call markers split the Lua stack into segments. Protected calls such as pcall and xpcall are special, they are C fast-functions, but they don't create a frame on a native stack, to prevent breaking merging FF frames hold the type. The merger walks from the root toward the leaf and matches the Lua segments to native frames whose addresses fall inside the VM range.

  3. Replace VM frames with Lua frames. A matched dispatch frame is replaced by its Lua segment. If the segment ends in an assembly fast function represented by the native VM frame, the merger keeps that native frame and drops the duplicate Lua builtin. For example, tostring can appear as Lua caller → lj_ff_tostring → lj_strfmt_num.

  4. Keep the original frames when a match is uncertain. If a stack is truncated or the boundaries don’t line up, the merger keeps both stacks and places the Lua frames between the native user-space frames and the kernel frames. Such situations are still not uncommon, potential heuristics might be added in the future.

Lua collection remains opt-in and thus doesn't support backwards compatibility with previously recorded samples. Lua postprocessing runs after the existing Python and PHP passes.

Testing

Regression tests cover interleaved Lua and C calls, native fast functions, fallback, disabled merging, and profile conversion. The Go unwinder and C++ native pipeline suites passed all 65 tests after the wire fixtures were updated.

image

@robot-magpie

robot-magpie Bot commented Nov 12, 2026

Copy link
Copy Markdown

✅ All contributors are covered under a CLA with Yandex

See CONTRIBUTING.md for more info about Yandex Contributor License Agreement.


The following contributors were found:
✅ 9a38844 Author: @GitSparTV (dev*******rtv@gmail.com)

(Only the first commit for a unique contributor is listed)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant