Skip to content

mlua-sys: bind LuaJIT's native luaL_traceback instead of the compat53 port - #739

Open
ross wants to merge 1 commit into
mlua-rs:mainfrom
ross:luajit-native-traceback
Open

ross wants to merge 1 commit into
mlua-rs:mainfrom
ross:luajit-native-traceback

Conversation

@ross

@ross ross commented Oct 3, 2026

Copy link
Copy Markdown

Under the luajit and luajit52 features, luaL_traceback resolves to the lua-compat-5.3 port
in mlua-sys/src/lua51/compat.rs. LuaJIT exports its own, and the port's cost grows with the
size of the script's global tables. mlua builds a traceback for every callback error and every
error that escapes to Rust, including errors a script catches with pcall, so ordinary error
handling pays it.

This PR binds LuaJIT's luaL_traceback and compiles the port only for Lua 5.1.

Why the port is slow

To name a frame, compat53_pushfuncname calls compat53_findfield over _G to depth 2: every
global and every field of every global table. On main it tries the global name first for every
frame, so every traceback walks _G, and a frame that isn't a global function (the main chunk,
for one) walks all of it. Lua 5.4 searches package.loaded instead. LuaJIT searches nothing: it
prints the call-site name, or the C function's address.

Measurements

Per caught error, min to max over 24 fresh VMs, each holding a 20,000-entry global table. Release
build, LuaJIT 2.1 from luajit-src, x86-64 Linux. rust.refuse is a Rust function that returns
Err.

mlua main (mlua-sys 0.13.0):

Case Port Native luaL_traceback
pcall(error, "x") (control, no callback) 1.0-2.7 µs 1.1-1.6 µs
pcall(rust.refuse) 242-925 µs 3.2-4.6 µs
pcall(function() rust.refuse() end) 515-1,039 µs 3.6-5.7 µs

mlua 0.9.9 (mlua-sys 0.6.8):

Case Port Native luaL_traceback
pcall(error, "x") (control, no callback) 1.7-2.6 µs 1.7-2.5 µs
pcall(rust.refuse) 6.6-596 µs 3.9-4.8 µs
An error from a Rust __newindex metamethod 5.2-8.6 µs 4.7-5.3 µs

The control row's spread comes from the host's mixed core types.

Change

  • lua51/lauxlib.rs: declare luaL_traceback in the extern block under
    #[cfg(feature = "luajit")]. The signature matches the Lua 5.2-5.4 declarations, so no caller
    changes.
  • lua51/compat.rs: put #[cfg(not(feature = "luajit"))] on the port's luaL_traceback, the
    helpers only it uses (compat53_countlevels, compat53_findfield,
    compat53_pushglobalfuncname, compat53_pushfuncname, COMPAT53_LEVELS1,
    COMPAT53_LEVELS2), and the CStr and mem imports.

Compatibility

  • LuaJIT versions. LuaJIT added luaL_traceback before 2.0.0 (commit "From Lua 5.2: Add
    luaL_traceback().", 2012-10-01, v2.0 branch), so every version build/find_normal.rs accepts
    from pkg-config (2.0.4 to 2.2) exports it, as does the luajit-src 2.1 vendored build.

  • Stack use. LuaJIT grows the stack as it pushes and concatenates every 15 pieces, so
    LUA_TRACEBACK_STACK still covers it.

  • Traceback text. Output follows LuaJIT's format, the one debug.traceback already uses under
    LuaJIT. Code that parses traceback text for a qualified name gets the call-site name instead:

    Frame Port (main) LuaJIT
    Unnamed C function [C]: in function 'rust.refuse' if found in _G, else [C]: in ? [C]: at 0x55cd55a80520
    Unnamed LuaJIT builtin [C]: in function 'NAME' if found in _G, else [C]: in ? [builtin#N]:
    Named C function [C]: in function 'table.sort' [C]: in function 'sort'
    Named Lua function in local 'f', in method 'm' in function 'f'

Testing

  • main: cargo test --features luajit,vendored,async,serde,macros passes before and after (358
    passed, 8 ignored). cargo clippy on mlua-sys is clean under luajit,vendored,
    luajit52,vendored, and lua51,vendored.
  • mlua-sys 0.6.8 with mlua 0.9.9: the same change through [patch.crates-io] passes a downstream
    application's 323 tests, including ones that assert on traceback content.

🤖 Generated with Claude Code

Under the luajit features, luaL_traceback resolved to the lua-compat-5.3
port, whose compat53_findfield walks every global and every field of
every global table to name a frame. mlua builds a traceback for every
callback error, so a pcall'd error on a VM with large global tables cost
hundreds of microseconds. LuaJIT exports luaL_traceback itself (since
before 2.0.0) and names frames from the call site without searching.

Declare the native symbol under the luajit feature and compile the port
and its helpers only for Lua 5.1.
@ross

ross commented Oct 3, 2026

Copy link
Copy Markdown
Author

Ran into this when i was seeing huge variability in test run times, a specific test case with that was sometimes a few seconds, others 1.5 minutes.

/cc ross/logit#499

This branch has not been deployed

No deployments
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