Skip to main content

Module async_precmd

Module async_precmd 

Source
Expand description

async_precmd submodule — run precmd-style hooks on the worker pool so they don’t block prompt rendering (writes into the shared param table). async_precmd hook — run precmd-style functions on a POOL WORKER THREAD so they never block prompt rendering.

zsh’s precmd hooks run synchronously before the prompt paints, so a slow hook stalls the prompt. async_precmd is a new lifecycle hook (no zsh equivalent): its functions run on the shared worker pool AFTER the prompt is built/rendered, writing their results into the shared, RwLock-synchronized global param table. The prompt reads whatever is currently there and never waits — a slow segment simply updates a prompt or two later.

Registration mirrors zsh’s hook arrays:

  • a function literally named async_precmd, and/or
  • members of the async_precmd_functions array.

Built on the Phase-1 crate::vm_helper::ShellExecutor::new_worker lightweight worker executor. No isolation is needed here: async_precmd WANTS its typeset -g writes to land in the shared table.

§The batch never overlaps shell code on the shell thread

A hook is a shell FUNCTION, and running one moves state C keeps for a single thread of execution: locallevel (Src/params.c:5837 locallevel++ in startparamscope, :5856 locallevel-- in endparamscope) and the level-stamped entries of the one paramtab, which scanendscope (Src/params.c:5904-5907, if (pm->level > locallevel)) restores or deletes on every scope exit. The worker’s pushes and pops land on the same counter and the same table as the shell thread’s.

The batch is fired from preprompt(), and the first thing the shell thread does after the prompt paints is usually run a widget: a shell-function widget brackets its body with startparamscope(); makezleparams(0); … endparamscope(); (Src/Zle/zle_main.c:1533-1540), and makezleparams stamps $BUFFER and its family with the scope level (Src/Zle/zle_params.c:206). A worker scope exit that lands inside that window deletes the widget’s $BUFFER, and the write-back in zle_param_sync::sync_from_paramtab then copies the now-empty value into the editor: every character typed so far on the line disappears. Measured with a self-insert wrapper (zle -N self-insert f; f() { zle .self-insert }, the shape zpwr’s zpwrSelfInsert has) and a hook that runs for half a second: most typed lines lost a prefix, and a trace showed the worker’s endparamscope removing BUFFER at the widget’s level immediately before the editor was overwritten with “”. A top-level typeset X=1 typed during the batch was stamped with the worker’s level and vanished when the hook returned.

So shell code on the shell thread waits for a batch that is already running (quiesce), and withdraws one the pool has not started yet. Plain typing is not held up: builtin widgets never open a scope. The first scope usually opens after the prompt is on screen; a zle-line-init widget is the exception, because this port’s zleread runs it before its first zrefresh (C paints first, c:1353, then calls the hook, c:1357), so a running batch delays that paint by the hook’s runtime.

Functions§

fire_async_precmd
Fire the async_precmd hooks on a worker thread. Called from preprompt() AFTER precmd + prompt render, so the prompt is already on screen. Returns immediately (non-blocking): it submits ONE closure to the shared worker pool and lets it run in the background. Debounced via [BATCH].
quiesce
Make the shell thread safe to run shell code: wait for a batch a worker is running, or withdraw one that is only queued (its hooks run on the next prompt instead). Returns at once when no batch is in flight, and on any thread but the shell’s — a worker must never wait on its own batch.
set_session_pool
Publish the session worker pool. Called once from ShellExecutor::new().