Skip to content

Commit

Permalink
Batch updates
Browse files Browse the repository at this point in the history
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
  • Loading branch information
vimpostor committed Apr 12, 2024
1 parent c1b25c1 commit 5dd3832
Show file tree
Hide file tree
Showing 2 changed files with 30 additions and 10 deletions.
3 changes: 2 additions & 1 deletion autoload/tpipeline.vim
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
37 changes: 28 additions & 9 deletions tests/test_general.vim
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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])
Expand Down Expand Up @@ -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

0 comments on commit 5dd3832

Please sign in to comment.