Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Documentation request: best practices for creating parts with high latency #2496

Open
0jrp0 opened this issue Dec 24, 2021 · 5 comments
Open

Comments

@0jrp0
Copy link
Contributor

0jrp0 commented Dec 24, 2021

This is a documentation request or at least a link to a vim-best practices doc for pseudo-asynchronous communication to populate a part from a high latency source. What follows is a description of my setup. Everything works great -- except vim slows for a bit while I am editing a file. I don't use neovim, just vim.

Overall, I'm writing a part that takes 150ms to 200ms (or longer) to compute.

I chose the following design:

Split the logic into two different vim plugins that talk to each other through a g: level variable.

Plugin 1 (foo)

  • This plugin uses airline#extensions#{name}#init(...) API to define a part function and condition.
  • It invokes a method to extract certain items found in a g: level variable populated with JSON data.

code snippet:

function! airline#extensions#foo#init(...)
  call airline#parts#define_function('bar-findings', 'foo#GetBarFindings')
  call airline#parts#define_condition('bar-findings', '&rtp =~ "bar" && 
      \ !empty(foo#GetIdentifier())')
  let g:airline_section_y = airline#section#create_right(['bar-findings'])
endfunction

Plugin 2 (bar)

  • This plugin has a polling job that runs every N seconds .. my plan is to run every 30 seconds. Right now, I run it every 5 seconds for debugging purposes.
  • This plugin talks to a locally run proxy daemon via Unix Domain Socket. Since I couldn't figure out how to do it through vimscript, I'm using python3.
  • After getting the json body back from the proxy daemon, the plugin populates the global variable via py3's vim.command API.

code snippet:

function! s:Poll(timer)
  if a:timer != g:bar_polling_id
    call timer_stop(g:bar_polling_id)
  endif
  py3 <<EOF
process(
  vim.eval('bazlib#GetPendingStuff()'),
  vim.eval('bazlib#GetRootDir()')
)
EOF
endfunction

function! bar#Init()
  if get(g:, 'bar_polling_id', -1) >= 0
    return
  endif
  let g:bar_polling_id = timer_start(5000, funcref('<sid>Poll'), {'repeat': -1})
endfunction

Observations

  • One thing that I noticed is that foo#GetBarFindings gets invoked on every keystroke, in addition to other kinds of events like buffer change. Luckily, it's just looking up data in a global vim variable. But practicably speaking, I know it won't change for another 30 seconds .. even then, it'll probably remain the same .. since it's tracking a state change that happens on a server.
    • Question: is it possible to tell airline to only update a part on significant events such as bufchange, win change, etc.?
  • The other thing that I observe is that since Vim (and Python) are single threaded, that socket call in py3 really gums things up when it takes 200ms.
    • Question: is there a better way to wrap this slow call?

Alternatives considered

I had considered writing Plugin 2 (bar) as a shell script and then write the data to a file. However, I do aspire to make this plugin available to others via git (internal). So, I thought implementing it solely in vim would make distribution and install much easier.

@chrisbra
Copy link
Member

Yeah, we have a differing goals here:

  • the statusline should be updated as often as possible to be accurate
  • sometimes you don't want to trigger expansive calls on every possible event.

What some plugins usually do, is to only perform some work if a buffer-local variable has been set and otherwise use the cached result. That buffer-local variable is then reset on different autocommands, e.g. when saving the buffer.

Check the branch extension or the wordcount extension.

That should give some ideas.

But in general, I agree, properly documenting would be good :)

@chrisbra
Copy link
Member

About question 2: I don't know. Would it be possible in python3 to start an extra python3 thread that way it shouldn't block vim I guess? But that sounds pretty complex architecture already :(

@0jrp0
Copy link
Contributor Author

0jrp0 commented Dec 28, 2021

I took at look at branch, wordcount, and then ultimately arrived at a best practice looking at vim-airline-clock. I spent several hours trying to use Python multithread, but kept running into segfaults. The single-threaded nature along with non-threadsafe code in vim core makes it impossible to use multithreading. Lesson learned: it's really important to understand to never do anything slow within vimscript or even embedded py3/lua/ruby.

So, I ended up using a combination of timer_start and job_start functions. timer_start defines how often you want to poll the external information. job_start defines how to asynchronously call an external process with a callback.

To start, define a timer rig in my_plugin/autoload/airline/extensions/my_plugin.vim:

function! s:timerfn(timer)
  call my_plugin#DoSomethingExpensive()
endfunction

" you can set 5000 to &updatetime or a higher time depending on sensitivity of the expensive thing.
let g:airline#extensions#my_plugin#updatetime = get(g:, 'airline#extensions#my_plugin#updatetime', 5000)

let g:airline#extensions#my_plugin#timer = timer_start(
      \ g:airline#extensions#my_plugin#updatetime,
      \ funcref('<sid>timerfn'), {'repeat': -1})

autocmd User AirlineToggledOff call timer_pause(g:airline#extensions#my_plugin#timer, 1)
autocmd User AirlineToggledOn call timer_pause(g:airline#extensions#my_plugin#timer, 0)

Then define the asynchronous interaction in my_plugin/autoload/my_plugin.vim:

let s:param4 = 0
let s:info = {}

function! my_plugin#InfoHandler(channel, msg)
  let s:info = json_decode(a:msg)
  " You could ask airline to refresh, but it slows things down when another event triggers refresh anyway.
  " :AirlineRefresh
endfunction

function! my_plugin#DoSomethingExpensive()
  " add any necessary checks to skip the slow op.
  if s:param1 > 0
    let s:job = job_start(['/path/to/binary', 'param1',
          \ 'param2', 'param3', '--', s:param4], {"out_cb": "my_plugin#InfoHandler"})
  endif
endfunction

" You can dynamically set parameters like this.
function! my_plugin#SetParam(value)
  let s:param4 = a:value
endfunction

@0jrp0
Copy link
Contributor Author

0jrp0 commented Dec 28, 2021

One more note. I will try to add an afk-detect mechanism in this to stop the timer when a user is not using vim .. either in background tmux window or they're just not using terminal at the moment. Probably something like this:

https://vi.stackexchange.com/questions/27180/does-vim-8-have-some-screensaver-like-mechanism

@chrisbra
Copy link
Member

Great stuff. I'll review and add something to the documentation then.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants