.NET Tools
Essential productivity kit for .NET and game developers
The Road to Out-of-Process ReSharper: Asynchronous Typing
Taking ReSharper out of process is a monumental undertaking. We covered the magnitude of this in a previous blog post and posted some updates on YouTrack along the way.
In the simplest terms, we’re tasked with separating code that requires the Microsoft Visual Studio (VS) API from code that works with the structures ReSharper uses to store information about your solution (akin to Roslyn’s workspace).
The overarching goal is to leave the part of code that requires the VS API within the Visual Studio process, and to take the ReSharper-bound code out into a separate process, with the two sides communicating through the open-source Rd-protocol that also powers Rider and several of our remote development tools. Many components have already been made compatible with this protocol, and the code that belongs to the editor is next in line.
In this article we’re going to go in-depth on a single task within the larger scope of work we’ve been tackling lately. A task that – if executed successfully – will reduce typing lag for ReSharper, both in its current integrated state and once it has gone out of process.
Note: Asynchronous typing has been enabled for ReSharper 2024.2 and newer.
Give it a try!
The complicated backstory
ReSharper uses a mutable model of your solution, synced to the version the user sees on their screen. This type of model is protected by a shared readers-writer lock. This means that whenever any sort of action is performed within the editor (even as small as typing a character), ReSharper has to stop all of its activities under the read lock, request permission to write in order to edit the model and – having received said permission – sync the models and resume all interrupted activities.
On average, it takes less than 20 milliseconds to process a single action. However, some outlier cases can occur where interrupting the background data processing operation is complicated. We constantly monitor the system for such operations to ensure that interruptions happen as quickly as possible.
To understand how difficult it is to untangle and separate the different operations involved in even the simplest editing actions, it’s necessary to clearly understand the role ReSharper plays in editing. ReSharper embeds itself into the chain of user input handlers and each keystroke triggers the following sequence of actions:
- Requests a blocking write-lock to update the data structures.
- Receives write-lock and pauses all async operations, like file analysis, as the results of these operations can no longer be accurate.
- Provides typing assistance (code formatting, smart code completion, etc.) along with the user typing.
- Requests a calculation for the updated completion items.
- Releases the blocking write-lock.
For predictable model changes, ReSharper performs the synchronous refresh reasonably quickly and immediately launches asynchronous code completion calculations and error analysis, as well as restarts the interrupted activities if needed. Things get trickier when typing assistance is involved.
Whenever a user inputs a symbol, ReSharper evaluates the symbol to see if there’s any assistance it has to offer based on that symbol. If there is, a complex interaction between ReSharper and Visual Studio must occur. ReSharper synchronously comes up with the action it needs to perform after or instead of the expected symbol and passes that call down the chain of handlers, which leads to the appearance of an appropriate symbol in the editor, and only then does ReSharper complete the evaluation. The algorithm looks something like this:
However, the code executed by ReSharper to complete an action in the editor can be quite complex. For example, if the file that’s being edited is large and the user adds a curly bracket, ReSharper’s search for the spot where the second bracket should go may require reparsing most of the file, which can be time-consuming. And if it’s the closing bracket that’s being put in – that kicks the difficulty up another notch.
The interaction between ReSharper and Visual Studio takes extra time and causes lag in typing and code completion. That’s why it’s important to come up with an asynchronous way to handle the changes made to the file in the editor.
Solutions and obstacles
We believe that the answer to the problem is to make the file editing in Visual Studio and code model editing in ReSharper asynchronous.
Doing so will free us from the need to request a blocking write-lock the moment the user starts typing in the editor. In most scenarios, this will decrease typing lag. It will also eliminate the need to wait for the background activities in the same thread to break. We won’t get in the way of the editor displaying the input symbol immediately. And once the write-lock is lifted, writing will be immediately available as there won’t be any operations requiring a read-lock.
This is where we run into our first obstacle. We need a clear understanding of how the changes made to the document on the Visual Studio side match up against the changes we make on our side. For example, what does the editor do when the user enters two closing brackets? We can’t evaluate the result of those two actions on Visual Studio’s part, because we don’t have a parser and a formatter in context here to complete this operation.
That’s why we need to pass this input data on to the second process, which can apply the changes to a virtual document and then play it back in the user’s interface once the changes have been calculated. But the trouble comes when a second set of formatting inputs comes in before the calculations for the first one can be completed, rendering the work obsolete as the second input is dependent on the outcome of the first.
To solve this problem, we’re implementing a change log and logging our document changes according to Operational Transformations consistency models. In doing so, a scenario where two brackets are entered in a close sequence can be resolved by having the operation of adding a second symbol be logged, reversed, and then re-applied to the changed model once the result of the first operation is completed.
The resulting flow looks admittedly overwhelming…
…but it essentially boils down to this: ReSharper’s typing processing becomes asynchronous to Visual Studio’s backend, saving you some precious milliseconds on the back-and-forth that can produce lag and graphical artifacts.
Are you with us so far? Because we’re about to go even deeper down this OOP rabbit hole.
The challenges ahead
We’re excited to share that we do have an internal prototype at this time. However, the prototype is not of stable release quality, and here’s why:
1. Some of ReSharper’s features support the “double undo” principle. For example, the user can use Ctrl+Z to undo the formatting and then use it again to remove the symbol it was applied to. This complicates the operation, and there is no way to be sure if the change will be applied or rejected.
2. To reduce the amount of falsely shown intermediate frames, which are almost unavoidable while waiting for feedback from another process, we’re trying to predict how the changes will unfold. Such predictions are impossible to make with code formatting or complex code assistance. However, it’s achievable for simpler cases. Currently, we don’t employ any oracles, but they are factored into the architecture.
3. Implementing asynchronous typing brings a major paradigm shift for ReSharper. This means we’re moving away from the paradigm that we’ve been building and enforcing for years. To make sure we don’t accidentally break existing features and development flows, we have put this functionality under a feature flag so we can easily test-drive asynchronous typing without bothering you until we’re confident that we’ve covered all cases. This does mean that we have to support both synchronous and asynchronous typing during the migration period, at least until we’re ready to make this feature flag the default.
Conclusion
ReSharper has a large and loyal user base who rely on our product to perform at their highest level, and we can’t bring ourselves to offer anything less than a stable build that we can be proud of. We hope that this post paints a clearer picture of the many obstacles we have yet to overcome on our journey out of Visual Studio’s process. We thank you for your patience and welcome any questions you may have.
Special thanks to Alexander Ulitin for spearheading this project, as well as to Sergey Kuks and Alexander Kurakin for their help in putting this article together.