Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 27 additions & 2 deletions python/ycm/client/messages_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from ycm.client.base_request import BaseRequest, BuildRequestData
from ycm.vimsupport import PostVimMessage

import json
import logging

_logger = logging.getLogger( __name__ )
Expand Down Expand Up @@ -56,8 +57,32 @@ def Poll( self, diagnostics_handler ):
# Nothing yet...
return True

response = self.HandleFuture( self._response_future,
display_message = False )
# Avoid HandleFuture() to prevent blocking in timer callbacks.
# HandleFuture() does:
# 1. Complex exception handling (UnknownExtraConf, DisplayServerException)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so? why is this a problem in this case? do we have a trivial/minimal repro case?

# 2. User dialogs that can block waiting for input
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No it doesn't, at least, not in any relevant codepath.

# 3. Vim UI updates during callback execution
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So does this code. I mean, that's its literal job : HandlePollResponse will do UI updates, that's what it's for.

# By extracting the response directly with minimal error handling, we avoid
# blocking vim's main thread. Note that response.read() is still technically
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

citation needed?

If response.done() is True then HandleFuture (which just calls JsonFromFuture) will just do the exact thing this code does, except this code skips HMAC validation, which is absolute Nope. Skipping HMAC validation is a security issue and we can't accept it.

I'm honestly not convinced by this change, and the explanation seems ... synthetic to me.

I'm willing to believe there is indeed an issue when there are very big notification messages (such as huge diagnostics) but I'd like to have a more convincing explanation and maybe the flame graph, profile, or whatever was mentioned in the description.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the detailed review. I'm sorry you're upset with a generated answer.

I didn't test the fix well enough. Most likely, I concluded it was fixed before the entire dataset was indexed by rust-analyzer, and it still freezes, but I didn't reproduce it before submitting the PR.

flamegraph-1.tar.gz

Here's the original flamegraph + perf.script data I used for it.

flamegraph-2.tar.gz

The second flamegraph looks similar, since I waited until the data was loaded. For the one that showed an improvement, I only have a flamegraph; the perf data has been overwritten.

flamegraph-3.tar.gz

I'll dig a bit deeper into it, but it looks like there's no easy solution.

Thank you for your time, and happy New Year 2026!

# blocking, but:
# - The future is already done(), data received from localhost ycmd server
# - Network I/O is complete, read() just copies from buffer to memory
# - No user interaction or complex processing
# The real performance issue was HandleFuture's heavy exception handling,
# not the I/O itself.
try:
response = self._response_future.result( timeout = 0 )
response_text = response.read()
response.close()
if response_text:
response = json.loads( response_text )
else:
response = None
except Exception:
_logger.exception( 'Error while handling server response in Poll' )
# Server returned an exception.
return False

if response is None:
# Server returned an exception.
return False
Expand Down
116 changes: 113 additions & 3 deletions python/ycm/tests/client/messages_request_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,16 @@
# You should have received a copy of the GNU General Public License
# along with YouCompleteMe. If not, see <http://www.gnu.org/licenses/>.

import json
from ycm.tests.test_utils import MockVimModule
MockVimModule()

from hamcrest import assert_that, equal_to
from unittest import TestCase
from unittest.mock import patch, call
from unittest.mock import patch, call, MagicMock

from ycm.client.messages_request import _HandlePollResponse
from ycm.tests.test_utils import ExtendedMock
from ycm.client.messages_request import _HandlePollResponse, MessagesPoll
from ycm.tests.test_utils import ExtendedMock, MockVimBuffers, VimBuffer


class MessagesRequestTest( TestCase ):
Expand Down Expand Up @@ -138,3 +139,112 @@ def test_HandlePollResponse_MultipleMessagesAndDiagnostics(
warning=False,
truncate=True ),
] )


def test_Poll_FirstCall_StartsRequest( self ):
test_buffer = VimBuffer( 'test_buffer', number = 1, contents = [ '' ] )

with MockVimBuffers( [ test_buffer ], [ test_buffer ] ):
poller = MessagesPoll( test_buffer )

# Mock the async request method to avoid actual HTTP call
mock_future = MagicMock()
with patch.object( poller, 'PostDataToHandlerAsync',
return_value = mock_future ) as mock_post:
# First poll should start request
result = poller.Poll( None )

assert_that( result, equal_to( True ) )
mock_post.assert_called_once()


def test_Poll_FutureNotDone_ReturnsTrue( self ):
test_buffer = VimBuffer( 'test_buffer', number = 1, contents = [ '' ] )

with MockVimBuffers( [ test_buffer ], [ test_buffer ] ):
poller = MessagesPoll( test_buffer )

# Mock future that is not done
mock_future = MagicMock()
mock_future.done.return_value = False
poller._response_future = mock_future

# Should return True without extracting result
result = poller.Poll( None )

assert_that( result, equal_to( True ) )
mock_future.result.assert_not_called()


def test_Poll_FutureReady_ExtractsResponseNonBlocking( self ):
test_buffer = VimBuffer( 'test_buffer', number = 1, contents = [ '' ] )

with MockVimBuffers( [ test_buffer ], [ test_buffer ] ):
poller = MessagesPoll( test_buffer )

# Mock completed future with response
mock_response = MagicMock()
mock_response.read.return_value = json.dumps(
[ { 'message': 'test' } ] ).encode()
mock_response.close = MagicMock()

mock_future = MagicMock()
mock_future.done.return_value = True
mock_future.result.return_value = mock_response
poller._response_future = mock_future

# Mock diagnostics handler
mock_handler = MagicMock()

# Should extract result with timeout=0 (non-blocking)
with patch( 'ycm.client.messages_request.PostVimMessage' ):
result = poller.Poll( mock_handler )

# Verify non-blocking extraction
mock_future.result.assert_called_once_with( timeout = 0 )
mock_response.read.assert_called_once()
mock_response.close.assert_called_once()
assert_that( result, equal_to( True ) )


def test_Poll_FutureException_ReturnsFalse( self ):
test_buffer = VimBuffer( 'test_buffer', number = 1, contents = [ '' ] )

with MockVimBuffers( [ test_buffer ], [ test_buffer ] ):
poller = MessagesPoll( test_buffer )

# Mock future that raises exception
mock_future = MagicMock()
mock_future.done.return_value = True
mock_future.result.side_effect = Exception( 'Connection error' )
poller._response_future = mock_future

# Should catch exception and return False
result = poller.Poll( None )

assert_that( result, equal_to( False ) )


def test_Poll_DoesNotCallHandleFuture( self ):
"""Verify that Poll() does NOT call HandleFuture() to avoid blocking."""
test_buffer = VimBuffer( 'test_buffer', number = 1, contents = [ '' ] )

with MockVimBuffers( [ test_buffer ], [ test_buffer ] ):
poller = MessagesPoll( test_buffer )

# Mock completed future
mock_response = MagicMock()
mock_response.read.return_value = json.dumps( True ).encode()
mock_response.close = MagicMock()

mock_future = MagicMock()
mock_future.done.return_value = True
mock_future.result.return_value = mock_response
poller._response_future = mock_future

# Spy on HandleFuture to ensure it's NOT called
with patch.object( poller, 'HandleFuture' ) as mock_handle_future:
# Poll should not call HandleFuture
poller.Poll( None )

mock_handle_future.assert_not_called()
2 changes: 1 addition & 1 deletion python/ycm/tests/mock_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ def done( self ):
return self._done


def result( self ):
def result( self, timeout = None ):
return self._result


Expand Down
Loading