From 5dd3832bd6e239feccb11cadca583cdcf9d5bda1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20Gro=C3=9F?= Date: Sat, 13 Apr 2024 00:13:28 +0200 Subject: [PATCH] Batch updates While the performance of tpipeline itself has been optimized quite a while for now, it is still possible for a bottleneck to appear, if the downstream consumer is too slow. The problem does not really show up when using tmux, but when using other external integrations (e.g. kitty), it is possible that the external statusline update can become a bottleneck. Kitty's custom tabbar integration being written in Python does not really help with this. It should be noted that this has never caused any delays in vim itself, given that this bottleneck only happens in the part, that is already running completely asynchronous from vim itself. But it could cause some hilarious problems, where the external statusline starts to lag behind noticably behind the live statusline in vim. To fix this we batch updates by depleting stdin completely before dispatching the external statusline update. Unfortunately there is not really a very good way to test if stdin contains more data just with shell code. We opt to use a hack with "read -t", which will fail immediately if stdin is empty: -t timeout time out and return failure if a complete line of input is not read within TIMEOUT seconds. The value of the TMOUT variable is the default timeout. TIMEOUT may be a fractional number. If TIMEOUT is 0, read returns immediately, without trying to read any data, returning success only if input is available on the specified file descriptor. The exit status is greater than 128 if the timeout is exceeded This is a complete hack, because "-t" is not really portable across sh implementations, e.g. the slightly widely used dash does not support it. We are very lucky with this specific codepath though, because "sh does not support -t" and "there is no more stdin data ready" have the same exit code, thus sh implementations not supporting this flag will automatically fallback to the old unbatched update path, i.e. they will never enter the inner while loop. If this however still ever causes problems in the future, we could think about using bash instead of sh. Both are basically preinstalled everywhere anyway. With this we can also lower the catchup time in the performance test, as the lagging behind is completely fixed now. In any case this also adds a completely new unit test that tests the lag-behind after updating the statusline every millisecond for 3 seconds. Fixes #64 --- autoload/tpipeline.vim | 3 ++- tests/test_general.vim | 37 ++++++++++++++++++++++++++++--------- 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/autoload/tpipeline.vim b/autoload/tpipeline.vim index 797eef3..1d7e265 100644 --- a/autoload/tpipeline.vim +++ b/autoload/tpipeline.vim @@ -174,7 +174,8 @@ func tpipeline#fork_job() let s:restore_right = systemlist("sh -c 'echo \"\"; tmux display-message -p \"#{status-right}\"'")[-1] endif let script = printf("export IFS='$\\n'; while read -r l; do%s", g:tpipeline_split ? " read -r r;" : "") - let script .= printf(" echo \"$l\" > '%s'%s", s:tpipeline_filepath, g:tpipeline_split ? printf("; echo \"$r\" > '%s'", s:tpipeline_right_filepath) : "") + let script .= printf(" while read -t 0 _; do read -r l%s; done", g:tpipeline_split ? "; read -r r" : "") " batch updates + let script .= printf("; echo \"$l\" > '%s'%s", s:tpipeline_filepath, g:tpipeline_split ? printf("; echo \"$r\" > '%s'", s:tpipeline_right_filepath) : "") if g:tpipeline_usepane " end early if file was truncated so as not to overwrite any titles of panes we may switch to let script .= "; if [ -z \"$l\" ]; then continue; fi" diff --git a/tests/test_general.vim b/tests/test_general.vim index 20859cf..2f34fe9 100644 --- a/tests/test_general.vim +++ b/tests/test_general.vim @@ -26,6 +26,15 @@ func Read_socket() endif endfunc +func Scroll() + if line(".") - 1 + norm k + else + norm G + endif + call tpipeline#update() +endfunc + func Test_loaded() call assert_equal(1, g:loaded_tpipeline) endfunc @@ -143,21 +152,13 @@ func Test_performance() exec printf("profile start %s", log_file) profile func tpipeline#update " simulate someone scrolling at 120FPS - func Scroll() - if line(".") - 1 - norm k - else - norm G - endif - call tpipeline#update() - endfunc let timer = timer_start(float2nr(individual_threshold * 1000), {-> Scroll()}, {'repeat': -1}) exec "sleep " . test_duration profile stop call timer_stop(timer) " wait for tmux to catch up - sleep 2 + sleep 200m let log = readfile(log_file, '', 5) call assert_equal('FUNCTION tpipeline#update()', log[0]) @@ -220,3 +221,21 @@ func Test_minwid_padded() call assert_match("a$", s:left) call assert_true(empty(s:right)) endfunc + +func Test_lag_behind() + " statusline should not lag behind even after rapid fire updates + let g:tpipeline_statusline = "%!tpipeline#stl#line()" + let test_duration = "3" + norm 99o + " update literally every single ms + let timer = timer_start(1, {-> Scroll()}, {'repeat': -1}) + exec "sleep " . test_duration + call timer_stop(timer) + + " now set a new statusline, the new value should appear immediately without any lag + let g:tpipeline_statusline = "RAPIDFIRE" + call Read_socket() + call assert_equal('RAPIDFIRE', Strip_hl(s:left)) + + bd! +endfunc