Skip to content

FFM CLibrary: per-syscall Arena.ofAuto allocation churns the JVM Cleaner #1872

Description

@BryanSant

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:

  1. There's no reported user-visible symptom.
  2. Without numbers, the change is premature optimization.
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions