EMACS HOLISTIC LATENCY PROFILER Status: design Initial scope: POSIX termcap (`-nw`) sessions whose terminal input and output have file descriptors. GUI event delivery, the Windows console, MS-DOS, terminal-emulator rendering, compositors, and physical presentation timing are deliberately excluded. 1. Objective The profiler answers one question: after work becomes observable to Emacs, where does time pass before Emacs has emitted the resulting terminal output? The primary unit is an interaction, not a function sample. A source becomes observable when terminal input is reported readable or read, a timer becomes due, process output is reported readable, or a file notification is queued. An execution interaction begins when Emacs dispatches that source. It ends when synchronous dispatch finishes and the immediately associated redisplay is completed, skipped, or superseded. If that redisplay emits terminal bytes, the interaction also records the final successful stdio flush. Deferred work is a new interaction linked to the interaction that scheduled it; it does not keep the original interaction open indefinitely. This operational boundary is intentionally narrower than "all causally related work", which is not generally decidable in Emacs Lisp. The primary output is a latency distribution and a latency budget for each interaction class. CPU throughput is supporting evidence. The profiler must: * run continuously and silently while explicitly enabled; * cover TTY input, the command loop, timers and process events, GC, redisplay, and TTY output before adding function attribution; * retain every interaction and its aggregate semantic spans; * optionally write an append-only, compact, process-crash-recoverable file; * state the uncertainty and availability of every clock boundary; * account for its own overhead and all lost records; and * avoid recording keys, buffer contents, file names, process output, or other user data by default. This instrumentation can identify work whose removal would yield a large latency reduction. It cannot promise that such work exists. It measures only the portion controlled by Emacs: terminal-input readiness through a successful `fflush`. That means bytes have left the C library for the kernel; it does not mean a terminal, PTY peer, emulator, SSH client, or display has consumed or rendered them. `tcdrain` may be used only by an explicit diagnostic run, because waiting for it changes the latency being measured. 2. Terms and clocks All internal timestamps are unsigned 64-bit nanoseconds from one profiler-private monotonic clock selected when profiling starts. Prefer `CLOCK_MONOTONIC_RAW` where available, then `CLOCK_MONOTONIC`, with a platform-specific monotonic fallback. Profiling does not change the clocks used by normal Emacs timekeeping. The file records the clock identity, advertised resolution, measured read cost, and whether the clock includes suspend time. Wall-clock time appears only in session metadata. An unavailable boundary is absent, never zero. TTY input normally has no per-byte kernel timestamp. An optional native observer polls the input descriptor without reading it and records the first readable observation while the main thread may be busy. The main thread marks the descriptor empty only after its existing nonblocking drain finds no input; the observer then rearms. Closing or replacing a terminal descriptor must first detach it from the observer, and the observer never changes descriptor flags or consumes input. A read batch records the last known-empty time, first observed-readable time, and read begin/end. Input in that batch arrived no earlier than the known-empty time and no later than read completion. The first-readable observation is a tighter upper bound only when the decisive source range ends with the first byte after an empty state; it says nothing about later bytes received while the descriptor remained readable. If the observer is unavailable, the main thread's poll/read observation is a lower-fidelity fallback. Each queued TTY byte carries a sidecar provenance token: batch ID and byte offset. Decoding combines tokens into a source range; function-key translation and replay propagate that range. Fabricated, unread, macro, and synthetic events have explicit synthetic or unknown provenance. The trace never stores the bytes themselves. Each timestamp has one of these qualities: observer-ready, main-poll-ready, read, internal, write, flush, or inferred. An input arrival interval carries its width as uncertainty. A derived latency therefore has lower and upper bounds when exact input arrival is unavailable. 3. Causal model Every dispatched root of work receives a monotonically increasing 64-bit `cause_id`. Root kinds are TTY command, synthetic command, timer, process data, process sentinel, file notification, terminal resize, thread wakeup, and internal maintenance. Nested synchronous work keeps the current `cause_id` and a `parent_span_id`. A nested root also records `parent_cause_id`. Deferred work records `scheduled_by_cause_id` and receives a new `cause_id` when it eventually runs. A key sequence can contain user think time. It therefore records the source range of both the first physical event and the decisive final physical event. Command latency begins at the decisive event's arrival interval; prefix dwell time is reported separately. A command without physical provenance has no input-to-command latency. For commands, `read_char` associates the next requested redisplay with the command that just finished. If Emacs deliberately skips that redisplay because input was already pending, the interaction is `superseded` and has no input-to-flush latency. Timer and process roots similarly own redisplay done before the event loop waits again. A redisplay invoked synchronously inside any root already belongs to that root. Each redisplay attempt gets a `redisplay_id` and names its serving cause. The first nonempty write to the TTY stdio stream starts an `output_epoch`; a successful stdio flush completes the greatest written epoch. One flush may therefore complete several coalesced attempts. This attributes emitted work, not arbitrary buffer mutations: an interaction with no bytes written has completion latency but no input-to-flush latency. 4. Numbers recorded for every interaction The following are raw observations. Durations are derived on reload so a future reader can correct clock mappings without rewriting the trace. | Field | Meaning | |-------|---------| | `cause_id` | Stable identifier for this interaction. | | `parent_cause_id`, `scheduled_by_cause_id` | Optional synchronous and deferred causal links. | | `root_kind` | TTY command, synthetic command, timer, process, file notification, resize, thread wakeup, or maintenance. | | `terminal_id`, `frame_id`, `thread_id` | TTY execution context; opaque session-local identifiers. | | `first_source`, `decisive_source` | Batch and byte ranges for the first and decisive physical events. | | `t_input_earliest`, `t_input_latest` | Decisive-input arrival bounds; latest uses first-readable only when the decisive source is the first byte after empty. | | `t_input_ready` | First non-consuming readable observation for the batch, when available. | | `t_read_begin`, `t_read_end` | TTY read interval for the decisive batch. | | `t_queue_insert`, `t_queue_remove` | Queue interval for the decisive raw event. | | `t_key_decode_done` | Completion of key-sequence decoding and keymap lookup. | | `t_dispatch_begin`, `t_root_work_end` | Operational execution interval for the root. | | `t_pre_command_begin`, `t_pre_command_end` | Pre-command hook interval. | | `t_command_begin`, `t_command_end` | `command-execute` interval, including abnormal exit status. | | `t_post_command_begin`, `t_post_command_end` | Post-command hook interval. | | `first_redisplay_id`, `last_redisplay_id`, `final_output_epoch` | Associated presentation range, or zero. | | `t_redisplay_begin`, `t_redisplay_end` | C redisplay interval serving the cause. | | `t_output_begin`, `t_output_end` | Encoding and writes for the relevant output epoch. | | `t_flush_begin`, `t_flush_end` | Successful stdio `fflush` interval for the output epoch. | | `thread_cpu_begin`, `thread_cpu_end` | Main-thread CPU clock at interaction boundaries. | | `status` | Normal, quit, error, throw, superseded, dropped, or still open. | | `validity_mask` | Which optional timestamps and counters are present. | | `input_uncertainty_ns` | `t_input_latest - t_input_earliest`. | The reader derives at least these numbers: | Metric | Definition | |--------|------------| | `batch_ready_wait_ns` | `t_read_begin - t_input_ready`; time the batch waited after its first readable observation. | | `input_queue_ns` | `t_queue_remove - t_queue_insert`. | | `key_decode_ns` | `t_key_decode_done - t_queue_remove`. | | `pre_command_ns` | `t_pre_command_end - t_pre_command_begin`. | | `command_ns` | `t_command_end - t_command_begin`. | | `post_command_ns` | `t_post_command_end - t_post_command_begin`. | | `redisplay_wait_ns` | `t_redisplay_begin - t_root_work_end`. | | `redisplay_ns` | `t_redisplay_end - t_redisplay_begin`. | | `tty_emit_ns` | `t_flush_end - t_output_begin`; encoding through stdio flush, not terminal rendering. | | `input_to_command_min_ns` | `t_command_end - t_input_latest`. | | `input_to_command_max_ns` | `t_command_end - t_input_earliest`. | | `input_to_flush_min_ns` | `t_flush_end - t_input_latest`. | | `input_to_flush_max_ns` | `t_flush_end - t_input_earliest`. | | `main_cpu_ns` | `thread_cpu_end - thread_cpu_begin`. | | `main_off_cpu_ns` | Interaction wall time minus main-thread CPU time; inclusive of nested roots. | | `prefix_dwell_min_ns` | `max(0, decisive_earliest - first_latest)`; excluded from command latency. | | `prefix_dwell_max_ns` | `decisive_latest - first_earliest`; excluded from command latency. | Reports show count, minimum, median, p90, p95, p99, maximum, arithmetic mean, total time, and input-arrival uncertainty. They add p99.9 only when the sample count supports it. Means are never the only displayed latency statistic. Interactions without terminal output and superseded interactions are separate populations. Session comparison adds confidence intervals only after a stable replay workload exists. 5. Work and state recorded per interaction The first implementation records only counters already available at the hook or cheaply accumulated inside the measured loop: * input queue depth at insertion and removal, read-batch byte count, decoded event count, key-sequence length, prefix depth, and extra reads while decoding; * selected-buffer size and modification-tick delta, selected-window rows and columns, and window count; * GC count and GC wall time during the interaction; * redisplay attempts, preemptions and retry reasons, frames and windows considered and updated, rows generated and reused, glyphs emitted, terminal bytes requested, stdio calls, and flush failures; and * profiler records emitted and dropped, ring high-water mark, writer CPU time, clock-read time, and measured observer cost. Partial writes, `EAGAIN`, and kernel output-queue depth are not claimed while TTY output uses stdio, which hides those details. Syscall interposition, per-object allocation accounting, hardware counters, every wait kind, and every redisplay subphase are added only in a targeted diagnostic build after a coarser span identifies a need. Existing OS tools are preferred for hardware and scheduler counters. `SESSION` records the Emacs revision, executable and dump build IDs, configure and optimization settings, GC settings, OS and CPU identity, the profiler clock, TTY type/capabilities/size/coding/baud, local-device versus PTY, and profiler settings. It records whether an SSH-related variable is present, not its value, and never records the terminal device path. Symbol and feature names can reveal installed packages. The initial file contains no Lisp symbol or feature names. A later command-grouping or sampling phase may add them behind an explicit option. Startup profiling, loaded-feature inventories, and file-name hashes are outside the initial subsystem. 6. Spans Manual spans cover the semantic boundaries needed for the first report: * TTY readiness observation, read, queue insert/remove, key decoding, and command loop; * pre-command, command, and post-command; * GC as one span; * the central event poll; and * redisplay decision, desired-matrix construction, matrix comparison, terminal encoding/write, and flush. The profiler does not claim to observe blocking hidden in arbitrary Lisp, modules, libraries, or syscalls. Add a wait span at an Emacs wrapper only when the initial waterfall leaves material unknown off-CPU time. Timer, process, file-notification, and resize spans arrive with their root kinds in the second phase. A completed span stores `span_id`, `parent_span_id`, `cause_id`, kind, start time, wall duration, thread CPU duration, thread ID, status, detail ID, and the counter deltas relevant to that kind. Spans open during a crash are reconstructed from begin records and marked incomplete. 7. Function attribution Function attribution is not part of the first implementation. Phase timing comes first; controlled reruns use the existing Lisp profiler and an external OS profiler such as `perf` for function-level evidence. This reuses working machinery and avoids adding a second signal source before semantic boundaries have proved useful. If those reruns cannot explain a repeatable slow phase, a later phase may add one randomized on-CPU sampler. It may reuse the timer setup in `src/profiler.c`, but not its Lisp-object hash-table signal path. Samples go to a fixed numeric ring and carry the current `cause_id` and `span_id`. A mixed C/Lisp stack requires a separately validated shadow stack across interpreted, byte-code, native-compiled, primitive, module, and nonlocal-exit gateways. The signal path performs no allocation, hashing, Lisp call, symbol lookup, locking, or file I/O. Off-CPU time initially comes from wall time minus thread CPU time and known wait spans. A second wall-clock sampling signal is justified only if material unknown off-CPU time remains. Compiler-wide C/Lisp function tracing is out of scope: it changes the workload too much and duplicates targeted spans and existing external tools. 8. C and Lisp symbols The initial file contains no function identities. If command grouping or mixed-stack sampling is added later, signal records contain only module-relative PCs and fixed Lisp frame IDs. Symbolization happens off the signal path using executable/shared-object build IDs and opt-in Lisp names. Unknown frames remain explicit module-relative addresses. A prefix-tree stack dictionary is useful only after sample volume demonstrates that it saves more space than it costs in writer complexity. 9. Collection and overhead control The main thread and the optional input observer each write to a preallocated single-producer ring. Producers never allocate, lock, wait, symbolize, or do file I/O. A native writer thread drains the rings without taking the Lisp lock, forms chunks, and writes with `writev`. Its CPU time is reported. Because the first phase stores aggregate interactions and spans rather than function samples or per-glyph events, it retains every record and needs no flight recorder. Ring, chunk, and rotation sizes are configuration values chosen by a burst test, not architectural constants. The default file has no compression and no forced `fsync`; a process crash can lose a partial current chunk, while a machine or filesystem failure can lose more. Any loss emits a `LOSS` record with stream, first/last time, records, bytes, and reason. A producer increments an out-of-ring drop counter before it continues, so a full ring cannot hide the loss record it prevents. Silent loss invalidates a profile. The acceptance budget for continuous mode is less than 1% additional CPU, less than 1% median command latency, and less than 250 microseconds added to p99 command latency on the reference workload. These are gates, not presumed facts. The input observer is measured separately and disabled automatically when its platform implementation cannot meet the gate. 10. File format (`.emlat`) Integers are little-endian in fixed headers. Record integers use unsigned or signed LEB128. Timestamps are deltas from the first timestamp in their block. IDs start at one; zero means absent. Strings are UTF-8 byte strings. No Lisp reader syntax or dumped Lisp object appears in the file. IDs are stable only within one rotated session. Cross-session comparison uses explicit metadata, never coincidentally equal numeric IDs. 10.1 File header The fixed 64-byte header is: | Offset | Size | Field | |--------|------|-------| | `0` | `8` | Magic bytes `EMLAT\0\r\n`. | | `8` | `2` | Format major version. | | `10` | `2` | Format minor version. | | `12` | `4` | Header flags. | | `16` | `16` | Random session UUID. | | `32` | `8` | Rotated-file sequence number. | | `40` | `8` | Realtime start in Unix nanoseconds. | | `48` | `8` | Monotonic start tick in nanoseconds. | | `56` | `4` | CRC32C of bytes `0..55`. | | `60` | `4` | Reserved zero. | 10.2 Chunks The remainder is a sequence of independently valid chunks, padded with zeros to an eight-byte boundary. A 40-byte chunk header contains: | Offset | Size | Field | |--------|------|-------| | `0` | `4` | Chunk magic `CHNK`. | | `4` | `2` | Chunk type. | | `6` | `2` | Type-specific version. | | `8` | `4` | Flags, including compression. | | `12` | `4` | Header size, initially `40`. | | `16` | `4` | Stored payload bytes. | | `20` | `4` | Uncompressed payload bytes. | | `24` | `8` | Monotonic chunk sequence. | | `32` | `4` | Payload CRC32C after decompression. | | `36` | `4` | Header CRC32C with this field zeroed. | The core format requires no compression library. Delta encoding, varints, and dictionaries provide the baseline compactness. If compression is added, a numeric codec ID lives in the chunk flags so a reader can identify or skip the chunk without first decoding `SESSION`. Unsupported compression is reported as loss. Chunk types are: | Type | Payload | |------|---------| | `SESSION` | Build/configuration, clocks, OS, CPU, TTY capabilities, profiler settings, privacy policy, and feature flags. | | `STRING` | ID, byte length, bytes. | | `INPUT` | Read-batch bounds and byte-provenance ranges, without byte contents. | | `INTERACTION` | Cause links, output epochs, validity mask, raw timestamps, status, and fixed counters. | | `SPAN` | Completed or open semantic spans and their counter deltas. | | `LOSS` | Dropped/corrupt/unsupported data intervals. | Dictionary definitions precede their first use. Each record block begins with a base timestamp, thread ID when homogeneous, record count, and schema version. Records then use timestamp deltas and varint IDs. Unknown chunk types and newer type-specific versions are skipped by chunk length and reported, rather than guessed or misdecoded. Sampling can add `MODULE`, `SYMBOL`, `STACK`, and `SAMPLE` chunk types in a later format-minor version. The writer constructs a complete chunk in memory before appending its header and payload. On reload, scanning stops at the first incomplete header, impossible size, or failed CRC; every preceding chunk remains usable. An index can be added later if sequential scans become measurably slow. Rotation never reuses IDs within a session. 11. Embedded interface and reports Profiling starts after a termcap terminal exists, through a Lisp primitive or `--latency-profile[=FILE]`. Lisp primitives start, stop, rotate, and query status. Dump loading and earlier startup are outside the initial clock. The first loader scans chunks sequentially and materializes summaries, not one Lisp object per record. Memory mapping and lazy iterators are added only if profile size makes that necessary. The first reports are: 1. command-latency distributions, split into flushed, no-output, and superseded interactions; 2. a waterfall for one selected tail interaction, including input uncertainty; 3. redisplay work and emitted TTY bytes for that interaction; and 4. profiler overhead, loss, and unavailable-boundary diagnostics. The report distinguishes wall time, main-thread CPU time, measured wait time, unknown off-CPU time, and TTY emission time. Function rankings and session comparisons wait for the later attribution and replay phases. 12. Initial implementation map The semantic recorder belongs in a small native `latency` core; it does not modify `src/profiler.c` or install a sampling signal. The existing Lisp CPU and memory profiler remains unchanged. Only a later mixed-stack sampler may share timer setup with `src/profiler.c`. The first semantic hooks belong at these existing funnels: * `wait_reading_process_output` for poll start/end, ready descriptor classes, fallback readiness, and main-thread idle time; * `tty_read_avail_input`, `kbd_buffer_store_event`, `kbd_buffer_get_event`, `read_decoded_event_from_main_queue`, and `read_key_sequence` for TTY read batches, queue latency, provenance folding, and key decoding; * `command_loop_1` around pre-command hooks, `command-execute`, post-command hooks, and root completion; * the redisplay decision in `read_char`, which is where the command's following redisplay is actually performed or skipped; * `garbage_collect` for GC attribution; * `redisplay_internal`, `redisplay_window`, `display_line`, `update_frame`, and `update_window` for redisplay decisions and work volume; * one small TTY-output wrapper used by the glyph `fwrite` sites and `cmputc`, plus one flush wrapper used by `tty_update_end`, `flush_terminal`, and the other normal redisplay flush paths, for byte counts and stdio-flush latency. Timer callbacks, process filters/sentinels, file notifications, and resize roots are the next phase after the command path meets its overhead budget. The input observer follows only if main-thread input intervals are too wide to answer the latency question. Mixed-stack sampling follows only if semantic spans plus controlled `profiler.el`/OS-profiler reruns leave a repeatable tail unexplained. The native core exposes inline, allocation-free producer operations. Lisp report code is loaded only when a profile is opened. 13. Validation Before optimization work uses these files, the first phase needs: * round-trip tests for each initial record and dictionary type; * truncated, corrupted, unknown-version, and rotated-file tests; * fuzzing of the loader with strict allocation and record-count limits; * synthetic CPU, sleep, GC, redisplay, error, quit, and nonlocal-exit commands with known durations and causal relationships; * tests that force ring overflow and verify explicit `LOSS` records; * PTY tests that control read batching, queueing, redisplay, terminal output, backpressure, and stdio-flush timing; * privacy tests that scan default traces for injected keys, buffer text, file names, process output, terminal paths, and non-dumped symbol names; and * A/B runs of an identical replay workload to enforce the continuous-mode budget. The observer gets a separate A/B gate when added. Only after these checks should profile results choose optimization targets. The first target is the largest repeatable component of p95/p99 input-to-flush latency, not the hottest function in an average CPU profile.