Start every investigation the same way:
TOFFLOAD_STATS=stderr:1 LD_PRELOAD=/path/to/libtransparent.so ./your-serverOne counter line per second tells you which of the situations below you are in.
No [toffload] runtime 0.1.0 active banner.
Wrong path. LD_PRELOAD needs a path the loader can resolve, and it fails
silently. Use an absolute path and confirm:
LD_DEBUG=libs LD_PRELOAD=$PWD/build/lib/libtransparent.so ./your-server 2>&1 | grep transparentSetuid or setgid binary. The loader ignores LD_PRELOAD for privileged
binaries. Nothing to do but run an unprivileged copy.
TOFFLOAD_QUIET is set. Then there is no banner by design; check the stats
line instead.
Statically linked application. There are no dynamic symbols to interpose. The runtime cannot help a static binary.
The most common outcome, and usually not a bug. Fiberization starts only after
the first successful accept(), and only for threads created after it.
- Event-loop server (nginx, redis, memcached). There is no per-connection thread. Expected, safe, and no overlap is available transparently. See is-my-server-supported.md.
- Thread pool created at startup. The workers predate the first
accept(), so they stay real threads. TryTOFFLOAD_POOL=1, and test carefully: it collapses the pool onto the carrier and changes the server's concurrency model. - Process-per-connection (
fork()per connection, like PostgreSQL). There is nopthread_createto interpose. - The server never accepts on a socket you expected — check it is really serving.
Fibers are running; the offload is not being intercepted. The runtime resolves
accel_run, then falls back to accel_encrypt.
nm -D --defined-only /path/to/your-backend.so | grep accel_You should see accel_submit, accel_done, accel_release and one of
accel_run / accel_encrypt.
- C++ backend without
extern "C". The names get mangled and nothing resolves. Every exported symbol needs C linkage. - Preload order. The backend must come before the runtime:
LD_PRELOAD="./libaccel_mine.so ./libtransparent.so". - Statically linked backend. If the offload is compiled into the
application rather than a shared object, there is no dynamic symbol to
interpose. Build the backend as a
.so.
Symptom: the runtime is clearly loaded and fibers_created and offloads
climb, but io_yields is 0 and, with TOFFLOAD_ENFORCE=1, serialized is
also 0 and unlocked shared state still loses updates.
The application's socket reads are not reaching the runtime. A distribution
that compiles with _FORTIFY_SOURCE=2 or higher under gcc 12 or newer
rewrites read(fd, buf, n) into __read_chk(fd, buf, n, size) whenever the
destination size can be determined, and glibc exports that under its own
name. Ubuntu 24.04 does this by default. The same applies to recv,
recvfrom and poll.
The runtime interposes all four _chk forms, so an up-to-date build handles
this. Confirm yours does:
nm -D --defined-only build/lib/libtransparent.so | grep _chkYou should see __read_chk, __recv_chk, __recvfrom_chk and __poll_chk.
If they are absent the build predates the fix. As a stopgap you can rebuild
the application with -U_FORTIFY_SOURCE, but upgrading the runtime is the
real answer. Note that the write direction has no _chk form: glibc fortifies
only calls that write into a caller's buffer.
Compare on an equal core budget. This is the usual answer. A stock thread-per-connection server on a 32-core machine spreads 64 OS threads over 32 cores; the runtime multiplexes them onto one carrier. Unpinned, you are measuring the machine, not the mechanism:
taskset -c 4,6 ./your-server # stock
taskset -c 4,6 env LD_PRELOAD=... ./your-server # runtimeYour offload may be too short. Below roughly 50 µs on ordinary hardware,
parking and resuming a fiber costs about as much as the wait it avoids. Our own
demo shows 0.97× at 20 µs. Check with ACCEL_LAT_US if you are using the
emulated backend.
The accelerator may be the bottleneck. Overlap helps when the CPU would
stall, not when the device is saturated. With ACCEL_OP=zip on the
single-threaded emulated device, throughput is ~1.05× because the device itself
is the limit. Look at device utilization before blaming the runtime.
serialized is climbing. Enforce mode is doing its job, which costs
throughput by design. If det_conflicts is 0, you do not need enforcement —
turn it off.
Check serialized and fibers_live. If fibers_live dropped and progress
stopped, something is holding a lock it never released. That exact bug shipped
once (the handler lock at EOF) and is fixed; a recurrence is a bug report.
Sub-libc blocking. A fiber that enters a raw futex or io_uring_enter
blocks the carrier and everything on it. Confirm with:
strace -f -c -e trace=futex,io_uring_enter -p $(pgrep your-server)Heavy futex traffic means category 3 — the runtime cannot yield through it. See is-my-server-supported.md.
TOFFLOAD_RT=1 is set. The realtime carrier can starve a real OS thread
that a fiber is waiting on, giving priority-inversion deadlock. Do not use it
outside microbenchmarks.
TOFFLOAD_REALMUTEX=1 is set. That selects the naive mutex, which
deadlocks the carrier on purpose to demonstrate why the fiber-aware one exists.
Bisect with the diagnostic knobs:
TOFFLOAD_PASSIVE=1 ... # fibers on, every other interposer off
TOFFLOAD_NOFIBER=1 ... # everything off; if this fixes it, it is fiberization
TOFFLOAD_NOMUTEX=1 ... # narrow to the mutex
TOFFLOAD_NOCOND=1 ... # narrow to condition variables
TOFFLOAD_NOIO=1 ... # narrow to socket I/OAt startup, in a database. Almost certainly versioned-symbol
interposition. Interposing pthread_cond_* without matching glibc's
GLIBC_2.3.2 versions crashes MariaDB at startup — measured on 1 of 5 stock
binaries. The runtime handles this with src/transparent.map and dlvsym. If
you built without the version script, that is the cause. Confirm:
objdump -T build/lib/libtransparent.so | grep pthread_cond_wait
# must show GLIBC_2.3.2Wrong errno or OpenSSL errors. If per-fiber save/restore were disabled
(TOFFLOAD_NOTLS=1) this is expected. Otherwise it is a bug — please report
it.
Stack overflow in a handler. Fibers get 1 MB stacks, less than the 8 MB default thread stack. A handler with deep recursion or a large stack array can overflow. This shows up as a crash inside the handler with a plausible-looking but wrong backtrace.
A correctness bug, not a tuning issue. Please open an issue.
Before you do, check whether the runtime is even implicated:
TOFFLOAD_NOFIBER=1 LD_PRELOAD=... ./your-server # runtime loaded, fibers offIf bad > 0 persists with fibers off, the problem is in your application or
backend, not the scheduler. If it disappears, it is ours — include the stats
line, your knobs, and the server's concurrency model.
One likely application-side cause: unlocked shared state read-modify-written
across the offload. That is a real hazard the runtime creates; see
safety.md. Run with the detector and look at det_conflicts
before concluding the runtime is broken.
Open an issue with:
- the exact command line, including every
TOFFLOAD_*andACCEL_*variable TOFFLOAD_STATS=stderr:1output- distribution, kernel, glibc, compiler
- whether
TOFFLOAD_NOFIBER=1changes the behaviour