Guide

Diagnose input latency

Record input and settlement timings without application contents.

Diagnose input latency

Add this option to the JVM that runs your app:

-Dtui4s.diagnostics.file=/tmp/tui4s-diagnostics.json

Type while the workload runs. Copy the JSON file during the delay, or after you quit. Give each app process a different file path. The parent directory must exist and be writable. Remove the JVM option to turn diagnostics off.

This release adds measurements. It does not change the settlement rules. Background scheduler work can still delay a frame. Continuous notifications can still extend settlement past its configured maximum.

What the report measures

The input clock starts when tui4s receives an event from the terminal reader. It includes the wait before dispatch and the wait in the runner's backlog. A completed input sample ends when its runner frame pass completes, before warning logs or frame-stat delivery. A pass can finish without painting. It is not proof that the expected text appeared. The external screen check described below measures that separate condition.

The report includes the current runner phase and its age. It also includes pending input, backlog size, notification counts, settlement observations, timeouts, and recent completed input timings. A frame need not finish for the report to update. Check pending input and phase age when completed samples are absent. observationAgeNanos measures the last hook observation; a small value does not prove useful progress. framePasses counts runner passes, including passes with no screen output.

Check that elapsedNanos advances while the app runs. A missing or stale file is not evidence that input is healthy. The exporter retries write failures; writeFailures shows their count if a later write succeeds.

The terminal reader offers input in batches. A batch acceptance time means that the whole batch was accepted. An earlier event can already have run. Do not subtract that batch time from each event's dispatch time and call the result its queue delay.

The recorder keeps at most 1,024 pending input timings and 64 recent completed samples. It counts inputs that it cannot track. Recent samples do not form a complete latency distribution. A small recent average cannot establish that all input completed. Compare received and completed counts, pending ages, and untracked counts too.

Cost and contents

Diagnostics are off by default. When enabled, one daemon thread outside the Kyo scheduler exports a snapshot every 250 ms. File output runs on that thread. The latest snapshot replaces the previous snapshot; the file does not grow with the duration of the run. Session cleanup requests a final snapshot and waits at most 200 ms for the exporter. Slow file output can outlast that wait on the daemon thread. Measurement has a cost: compare runs with it enabled and disabled.

The report contains durations, sequence numbers, phases and counts. It does not record key characters, pasted text, chat content, signal values or application logs. The output stays in the file that you selected.

Reproduce locally

Build the example launchers with JDK 25:

sbt examples/launchers
mkdir -p work

The PTY kit sends keys on a schedule and observes the correct input text. It sends the next key without waiting for the previous frame. Start with a quiet run, then add unrelated runnable work or notification traffic:

python3 examples/ptycheck/latency.py --rate 10 --keys 60 --deadline-ms 100 --output work/quiet.json --command examples/target/bin/settlement
python3 examples/ptycheck/latency.py --rate 10 --keys 60 --deadline-ms 100 --require-load-running --output work/busy.json --command examples/target/bin/settlement --busy --duration=15
python3 examples/ptycheck/latency.py --rate 10 --keys 60 --deadline-ms 100 --require-updates --require-load-running --output work/updates.json --command examples/target/bin/settlement --updates=1000 --duration=15
python3 examples/ptycheck/latency.py --rate 10 --keys 60 --deadline-ms 100 --require-updates --require-load-running --output work/combined.json --command examples/target/bin/settlement --busy --updates=1000 --duration=15

The update rate is a requested maximum. The screen counter shows actual updates. The finite load starts before the first frame, so startup consumes part of its duration. Keep the run shorter than the load. The fixture's input is 100 columns wide; keep the sent text below 100 characters.

The load checks require visible running markers and advancing update counts. A screen marker can be stale during a stall. The example's lifecycle tests also check the producer state directly, during input and after shutdown.

The command exits with a failure when input is missing, incorrect, or late. The loaded runs can fail on this release: they expose the settlement issue. Keep their reports as the baseline for a fix. The 100 ms value above is an explicit test deadline, not a claim about supported performance.

The PTY report separates actual send-to-screen time from planned send-to-screen time. The deadline uses planned send time, so a late sender cannot hide a delay. The endpoint is the decoded terminal screen with the expected input. It includes transport and queueing. It does not measure a physical terminal's painting. Report sender delay, missing input and the maximum delay beside percentiles. Do not use FrameStats.nanos or the layout-only PerfProbe as a substitute.

tui4sA terminal runner for kyo-ui.