If a plugin wants to grab all of the previously placed text properties that the same plugin has placed, the current prop_list requires some work:
type isn't perfect either.This results in a ton of vimscript API calls and is a similar thing to what lead to #8675.
Basically, what a plugin is running is this:
function! g:UpdateMatches() let properties = [] for line in range( line('$') ) call extend(properties, filter(prop_list( line + 1, { 'bufnr': 1 } ), 'v:val[ "type" ][ :3 ] ==# "Ycm"' )) endfor endfunction
The profile is here: https://gist.github.com/bstaletic/84c10b75be93c99f321a27af73278a8d
I'm not the biggest fan of neovim, but its API here is really nice.
Each plugin gets a unique namespace ID. Then, when getting (neovim's equivalent of) all the properties in a buffer, besides the buffer number, the ID is passed as well. Introducing this namespace ID thing to text property APIs would be perfect. Vim's sign_foo() family of functions already has the group thing.
A simpler solution is just introducing something like prop_list_range(), where not only a single line would be passed, but rather a range of lines. That way plugins could request all text properties from line N to line M. It would still leave filtering to the users.
Note that the two ideas aren't exactly exclusive.
I'm a maintainer of YouCompleteMe. The way YCM updates text properties for a specific buffer is basically described in the "problem" section. In more detail:
prop_add_list(). In the TODO list.The algorithm is made that way, because in common cases it's faster than removing all old and then placing all new - i.e. paying attention to overlap between the two sets is beneficial.
But I digress. The problem is that call to vimsupport.GetTextProperties( bufnr ). It's defined here:
https://github.com/ycm-core/YouCompleteMe/blob/master/python/ycm/vimsupport.py#L213-L226
Doing that on an extra long file is very inefficient. For a file that is 1'010'000 lines long, with no props either placed, or to place (i.e. all work is in GetTextProperties()), it takes ~1 minute on my machine.
—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub.
Triage notifications on the go with GitHub Mobile for iOS or Android.
![]()
One more possible alternative is "get all props of type N in buffer B". That way a plugin like YCM could get all the properties in just two vim.eval() calls.
The problem
If a plugin wants to grab all of the previously placed text properties that the same plugin has placed, the current
prop_listrequires some work:
- Loop over the lines of the buffer.
- Grab all the text properties from each line and aggregate in some list.
- Filter out all properties that are not produced by the plugin. A plugin might need to grab two types of properties, so making vim filter by
typeisn't perfect either.
—
You are receiving this because you commented.
@brammool @yegappan Thanks for replying.
How are you using the property dict value returned by prop_list()?
That's over here:
If you have a text property that spans multiple lines, then you cannot simply modify the value returned by prop_list_range() and place the text properties again.
I'm pretty sure we don't handle multiline properties 100% correctly. Is it okay if we ignore them for the sake of this discussion?
What is the overhead caused by? If it's calling prop_list() for every
line, we could add an entry in the {props} argument "linecount".
The test case was with zero actually placed properties, so there was nothing to actually filter. That suggests the repeated calls to prop_list() is the slow part.
As for returning (and subsequently filtering) tons of properties belonging to a different plugin, that seems like a much more unlikely situation. I'm leaning towards ignoring it until a user complains.
Whatever solution we pick, it is likely still slow for a very long file.
Yeah, for really long files we do bail out early and just refuse to even try. At least now we do - there was a bug where we tried do update text properties even for files where b:ycm_largefile was set.
Still, it feels like there should be a way to optimize the "get all props for buffer B placed by me" use case.
Have you considered only dealing with visible lines?
Can't say that I have, but it's definitely worth considering. It shouldn't even be a very intrusive change.
—
You are receiving this because you commented.
Have you tried putting the loop that calls prop_list() in a :def function? It would be interesting to know how much difference this makes.
—
You are receiving this because you commented.
Okay, wow... That's about 9 times faster. Scanned a buffer of million lines in about 10 seconds.
—
You are receiving this because you commented.
If I read the log correctly, it seems the slowest part comes from this line:
call extend(properties, filter(prop_list( line + 1, { 'bufnr': 1 } ), 'v:val[ "type" ][ : 3 ] ==# "Ycm"' ))
I don't know whether it will make a difference, but you could try to replace the eval string containing v:val with a Vim9 lambda. Something like this (untested):
properties->extend(filter(prop_list(line + 1, {bufnr: 1}), (_, v: dict<any>): bool => v['type'][: 3] == 'Ycm'))
You could also try to replace filter() with a :for loop and remove(). The reason why I say this is because sometimes a :for loop is significantly faster than map():
vim -es -Nu NONE -i NONE -U NONE -S <(cat <<'EOF'
vim9script
var mylist = pow(10, 6)->float2nr()->range()
def Lambda()
var time = reltime()
map(mylist, (_, v) => v + 1)
setline(1, reltime(time)->reltimestr()->matchstr('.*\..\{,3}') .. ' seconds to run Lambda()')
enddef
Lambda()
def ForLoop()
var time = reltime()
var i = 0
for _ in mylist
mylist[i] = mylist[i] + 1
i += 1
endfor
setline(2, reltime(time)->reltimestr()->matchstr('.*\..\{,3}') .. ' seconds to run ForLoop()')
enddef
ForLoop()
:%p
qa!
EOF
)
0.565 seconds to run Lambda()
0.169 seconds to run ForLoop()
So, maybe the same can be true for filter(); I don't know.
A few remarks:
:call\%(\w\|-\)\+)# suffix at the end of comparison operators ('ignorecase' is always ignored)Here is an example of refactoring for a legacy eval string:
v-----------------------v
:legacy echo getcompletion('', 'option')->filter('v:val =~ "func"')->join("\n")
→
:vim9 echo getcompletion('', 'option')->filter((_, v) => v =~ 'func')->join("\n")
^---------------------------^
Providing the types is not necessary, but it might improve the reliability of the code and maybe let the Vim compiler optimize it (not sure about that), which is why I wrote dict<any> and bool earlier:
properties->extend(filter(prop_list(line + 1, {bufnr: 1}), (_, v: dict<any>): bool => v['type'][: 3] == 'Ycm'))
^-------^ ^--^
If the type of the members of the v dictionary is more accurate, you can replace any with it. You can inspect the exact type of v with typename().
—
You are receiving this because you commented.
@brammool @yegappan Thanks for replying.
How are you using the property dict value returned by prop_list()?
That's over here:
- Gathering and filtering: https://github.com/ycm-core/YouCompleteMe/blob/master/python/ycm/vimsupport.py#L217-L224
- Skipping the ones we want to keep: https://github.com/ycm-core/YouCompleteMe/blob/master/python/ycm/diagnostic_interface.py#L149
- Dropping stale ones: https://github.com/ycm-core/YouCompleteMe/blob/master/python/ycm/diagnostic_interface.py#L158-L159
If you have a text property that spans multiple lines, then you cannot simply modify the value returned by prop_list_range() and place the text properties again.
I'm pretty sure we don't handle multiline properties 100% correctly. Is it okay if we ignore them for the sake of this discussion?
What is the overhead caused by? If it's calling prop_list() for every
line, we could add an entry in the {props} argument "linecount".The test case was with zero actually placed properties, so there was nothing to actually filter. That suggests the repeated calls to
prop_list()is the slow part.As for returning (and subsequently filtering) tons of properties belonging to a different plugin, that seems like a much more unlikely situation. I'm leaning towards ignoring it until a user complains.
Whatever solution we pick, it is likely still slow for a very long file.
Yeah, for really long files we do bail out early and just refuse to even try. At least now we do - there was a bug where we tried do update text properties even for files where
b:ycm_largefilewas set.Still, it feels like there should be a way to optimize the "get all props for buffer B placed by me" use case.
First of all, apologies for a very late reply.
I have just tested the pathological case that prompted me to open this feature request. The result? Vim is now able to complete the task in 0.2 seconds, which is astonishing.
@yegappan Thanks a lot for providing the feature so quickly! I'm off to update the plugin.
—
You are receiving this because you commented.