Summary
Every syscall wrapper in terminal-ffm's CLibrary allocates a fresh Arena.ofAuto() for its termios / winsize / openpty buffer. Each one parks ~10–60 bytes of native memory plus a Cleaner registration. For low-rate callers this is fine, but a caller that polls (e.g. terminal size checks per animation frame) could measurably stress the JVM Cleaner and grow native heap between GC cycles.
Affected call sites
terminal-ffm/src/main/java/org/jline/terminal/impl/ffm/CLibrary.java:
| Method |
Allocation |
winsize() { … } (constructor) |
Arena.ofAuto().allocate(LAYOUT) |
termios() { … } (constructor) |
Arena.ofAuto().allocate(LAYOUT) |
getTerminalSize(int fd) |
one winsize() per call |
setTerminalSize(int fd, Sized) |
one winsize() per call |
getAttributes(int fd) |
one termios() per call |
setAttributes(int fd, Attributes) |
one termios() per call |
ttyName(int fd) |
Arena.ofAuto().allocate(64) |
openpty(…) |
three separate Arena.ofAuto().allocate(…) |
Auto-arenas defer release to the JVM Cleaner. Allocations that arrive faster than the Cleaner can reclaim them stack up as native memory.
Why it's not (yet) a bug
I have no measured workload that hits this — every direct caller I've audited touches these methods rarely (terminal resize handler, attribute save/restore on raw-mode entry/exit, single openpty at construction). Filing this as an issue rather than a PR specifically because:
- There's no reported user-visible symptom.
- Without numbers, the change is premature optimization.
- The pattern has shipped for years.
But the shape is known to be fragile if a downstream library starts polling — e.g., a Live display that re-queries terminal.size per animation frame to detect resize without going through SIGWINCH.
Suggested fix
Replace Arena.ofAuto() with try-with-resources confined arenas at each call site:
static Size getTerminalSize(int fd) {
try (Arena arena = Arena.ofConfined()) {
MemorySegment seg = arena.allocate(winsize.LAYOUT);
ioctl.invoke(fd, (long) TIOCGWINSZ, seg);
return Size.of(seg); // copy fields out before close
} catch (Throwable e) { … }
}
Arena.ofConfined() releases deterministically on close — no Cleaner involvement, no native-memory accumulation. The trade-off is that the winsize / termios helper objects can no longer outlive the call; they have to copy primitives out before the arena closes. That's fine for all current callers; none of them hold the segment past the syscall return.
Six call sites, plus the constructors of winsize / termios need an arena parameter (or become free functions inside an arena-scoped block).
What would justify a PR
Either:
- A JMH microbenchmark showing measurable cleaner / native-memory pressure on a tight
terminal.getSize() loop, or
- A user report of cleaner-thread CPU spikes / native-heap growth attributable to JLine FFM.
If a maintainer agrees the pattern is worth fixing pre-emptively (treating it as a try (Arena) { … } migration rather than perf work), I'm happy to do the diff.
Context
Surfaced during a Konsole (Kotlin TUI) audit triggered by a separate flicker / memory-pressure report. The audit concluded JLine FFM wasn't the smoking gun for that user's symptoms, but flagged this pattern as the only structural concern in the FFM module.
Summary
Every syscall wrapper in
terminal-ffm'sCLibraryallocates a freshArena.ofAuto()for its termios / winsize / openpty buffer. Each one parks ~10–60 bytes of native memory plus a Cleaner registration. For low-rate callers this is fine, but a caller that polls (e.g. terminal size checks per animation frame) could measurably stress the JVM Cleaner and grow native heap between GC cycles.Affected call sites
terminal-ffm/src/main/java/org/jline/terminal/impl/ffm/CLibrary.java:winsize() { … }(constructor)Arena.ofAuto().allocate(LAYOUT)termios() { … }(constructor)Arena.ofAuto().allocate(LAYOUT)getTerminalSize(int fd)winsize()per callsetTerminalSize(int fd, Sized)winsize()per callgetAttributes(int fd)termios()per callsetAttributes(int fd, Attributes)termios()per callttyName(int fd)Arena.ofAuto().allocate(64)openpty(…)Arena.ofAuto().allocate(…)Auto-arenas defer release to the JVM Cleaner. Allocations that arrive faster than the Cleaner can reclaim them stack up as native memory.
Why it's not (yet) a bug
I have no measured workload that hits this — every direct caller I've audited touches these methods rarely (terminal resize handler, attribute save/restore on raw-mode entry/exit, single
openptyat construction). Filing this as an issue rather than a PR specifically because:But the shape is known to be fragile if a downstream library starts polling — e.g., a Live display that re-queries
terminal.sizeper animation frame to detect resize without going through SIGWINCH.Suggested fix
Replace
Arena.ofAuto()withtry-with-resourcesconfined arenas at each call site:Arena.ofConfined()releases deterministically on close — no Cleaner involvement, no native-memory accumulation. The trade-off is that thewinsize/termioshelper objects can no longer outlive the call; they have to copy primitives out before the arena closes. That's fine for all current callers; none of them hold the segment past the syscall return.Six call sites, plus the constructors of
winsize/termiosneed an arena parameter (or become free functions inside an arena-scoped block).What would justify a PR
Either:
terminal.getSize()loop, orIf a maintainer agrees the pattern is worth fixing pre-emptively (treating it as a
try (Arena) { … }migration rather than perf work), I'm happy to do the diff.Context
Surfaced during a Konsole (Kotlin TUI) audit triggered by a separate flicker / memory-pressure report. The audit concluded JLine FFM wasn't the smoking gun for that user's symptoms, but flagged this pattern as the only structural concern in the FFM module.