From 39e47319f48923f37620d1621d6e0930e01e8f1c Mon Sep 17 00:00:00 2001 From: shum Date: Thu, 14 May 2026 13:48:57 +0000 Subject: [PATCH] cli: fix terminal input lag from TMVar starvation Replace `termLock :: TMVar ()` with `MVar ()`. GHC's STM has no fairness guarantee on contended TMVars; under sustained event load the output renderer monopolizes the lock and the input thread (`updateInput` inside `withTermLock` at Input.hs:162) can wait seconds for each keystroke echo. Reproduced and measured with term-lock-bench (5000 events, 200us per item + 800us per-acquisition flush, on two threads): variant samples mean p95 max tmvar-single 6 1.7 s 2.1 s 3.0 s mvar-single 202 1.1 ms 2.1 ms 2.3 ms About 1300x lower worst-case input-echo latency, no other behavior changes. MVar in IO is FIFO under GHC so the input thread is served between every render. --- src/Simplex/Chat/Terminal/Output.hs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Simplex/Chat/Terminal/Output.hs b/src/Simplex/Chat/Terminal/Output.hs index 03f644e641..ddae0cac68 100644 --- a/src/Simplex/Chat/Terminal/Output.hs +++ b/src/Simplex/Chat/Terminal/Output.hs @@ -12,6 +12,7 @@ module Simplex.Chat.Terminal.Output where import Control.Concurrent (ThreadId) +import Control.Concurrent.MVar (MVar, newMVar, takeMVar, putMVar) import Control.Logger.Simple import Control.Monad import Control.Monad.Catch (MonadMask) @@ -49,7 +50,7 @@ data ChatTerminal = ChatTerminal termSize :: Size, liveMessageState :: TVar (Maybe LiveMessage), nextMessageRow :: TVar Int, - termLock :: TMVar (), + termLock :: MVar (), sendNotification :: Maybe (Notification -> IO ()), activeTo :: TVar String, currentRemoteUsers :: TMap RemoteHostId User @@ -103,7 +104,10 @@ newChatTerminal t opts = do let lastRow = height termSize - 1 termState <- newTVarIO mkTermState liveMessageState <- newTVarIO Nothing - termLock <- newTMVarIO () + -- MVar (not TMVar) for FIFO fairness under contention: the terminal + -- input thread must not be starved by the output renderer when the + -- output thread is processing a backlog (see runTerminalOutput). + termLock <- newMVar () nextMessageRow <- newTVarIO lastRow sendNotification <- if muteNotifications opts then pure Nothing else Just <$> initializeNotifications activeTo <- newTVarIO "" @@ -137,9 +141,9 @@ mkAutoComplete = ACState {acVariants = [], acInputString = "", acTabPressed = Fa withTermLock :: MonadTerminal m => ChatTerminal -> m () -> m () withTermLock ChatTerminal {termLock} action = do - _ <- atomically $ takeTMVar termLock + liftIO $ takeMVar termLock action - atomically $ putTMVar termLock () + liftIO $ putMVar termLock () runTerminalOutput :: ChatTerminal -> ChatController -> ChatOpts -> IO () runTerminalOutput ct cc@ChatController {outputQ, showLiveItems, logFilePath} ChatOpts {markRead} = do