Long read: Where we are with “out of process” ReSharper
A little over a year ago, we ran a series of blog posts describing performance improvements we were making to ReSharper. We’ve delivered a lot of improvements in the releases since then, but we haven’t yet delivered on the big one – running ReSharper out of process. We thought it was about time we gave you an update on where we are.
TL;DR: This isn’t an announcement post. We don’t have an ETA to share right now, although we have made significant progress on what is a massive problem. This post should give you an idea of the scale of the technical challenges we are working through in order to make ReSharper run out of process successfully.
So, grab a coffee, find a comfy chair, and we’ll explain exactly what’s going on with ReSharper out of process, why it’s taking so long, and what the future plans are.
What are we trying to achieve?
To recap, we want to push ReSharper out of process. Currently, all of ReSharper’s features – navigation, code completion, inspections, quick fixes and refactorings, unit testing, diagramming, find usages, etc. – all run inside the Visual Studio process.
This is a problem because Visual Studio is a 32-bit process, and realistically that means it can only use 2 to 2.5GB of memory. This might sound like a lot, but this memory space is also shared between your project files, the .NET runtime, ReSharper, and Visual Studio’s own features, including the WPF based UI, source control and of course any other installed plugins. And Visual Studio itself has added extra memory demands in recent versions – converting the ReSharper solution to the new SDK style .csproj files increased memory usage by 800MB!
The key point is that even if you have a monster development machine with 128 processors, SSD drives and 1TB of memory, Visual Studio will only ever use 2 to 2.5GB of memory. With larger projects, things can start to get a little cramped, garbage collection kicks in more frequently, throughput can suffer, and everyone gets frustrated – us included.
When we built Rider, we split the architecture of the IDE, admittedly out of necessity. The front end user interface is ultimately based on the open source IntelliJ Platform, as used in IntelliJ IDEA, WebStorm, PyCharm and so on. This is a JVM application we’ve been building since 2001 and has a ton of useful features that we wanted to take advantage of. Rider’s C# features reuse ReSharper, which we’ve been building since 2004, and we wanted to keep the breadth of functionality there, too. But those two pieces are not compatible – one is a JVM application and the other is .NET.
So Rider runs the ReSharper codebase out of process, using a very lightweight inter-process communication protocol. More importantly, both the IntelliJ and ReSharper processes are 64-bit, meaning we can make much better use of the resources available to your computer.
As anyone who has used Rider will confirm, this is a very rewarding strategy for performance.
And so we want to apply this architecture to ReSharper in Visual Studio, too, and push ReSharper out of process. We’ve proved the technology works with Rider, so it should be a quick and easy job to make it work with ReSharper too, right? Sadly, things are never that straightforward.
Why is it so hard?
Did I mention that we’ve been building ReSharper since 2004? That’s a lot of functionality to migrate, and the central architecture of ReSharper is built on the assumption of running inside Visual Studio. Essentially, we’ve spent the last 15 years tightly coupling a product to Visual Studio, and we’re now having to unpick it.
On the plus side, way back in ReSharper 2.0 we introduced interfaces to abstract away from Visual Studio. These have proved invaluable for supporting multiple versions of Visual Studio simultaneously, as well as allowing us to write hundreds of thousands of tests. However, these interfaces were originally designed as a “seam” to avoid being tightly coupled to specific versions of Visual Studio’s APIs, and for the most part, they make the same assumptions as the Visual Studio APIs – the interaction is fundamentally the same.
Unfortunately, the interactions required for an in-process API are not the same as what we need for out of process support. For one thing, communication needs to be asynchronous, and we all know how async can spread “virally” through a codebase. Secondly, we need the inter-process communication to be very lightweight. In fact, the protocol we’ve built for Rider, and which will be used when running ReSharper out of process, is essentially just a view model – all processing happens in the ReSharper host process, and only presentation data is sent to a thin layer running inside the IDE user interface process. This also has a big impact on the shape of the APIs we need when we move to out of process. (You can read more about Rider’s architecture in this Code magazine article.)
A great example here is a piece of work that spans previous and current release cycles on decoupling the project model. This is a phased set of refactorings that have been moving us to the right place for out of process, while still keeping everything working, and without having a risky “big bang” style rewrite.
ReSharper needs a project model – it can’t do anything without it. This is the list of projects in a solution, the list of files in a project, references, packages, compiler settings and so on. All very important context to tell ReSharper what to analyse and how to resolve external dependencies, amongst other things. It’s also a writable model, with support for adding or removing files, projects, references, settings, etc.
ReSharper’s project model is based on Visual Studio’s view of the solution, again abstracted by interfaces that provide a more friendly interface for ReSharper’s internals, and as a seam to support multiple versions of Visual Studio. But this introduces a number of problems, not all of which are related to our requirements for out of process. Significantly, creating the project model has a negative impact on solution load time, which we want to improve. This is mainly because we’re using Visual Studio COM APIs, which means we walk the entire solution structure, synchronously, on the UI thread – and the larger the project, the more expensive this becomes.
On top of that, the way we interact with these COM APIs is not a good fit for out of process support. Traditionally, we’ve been creating project model entities directly from the hierarchy APIs – instances of our IProject, IProjectFolder and IProjectFile interfaces. These types expose and manipulate the project model, and are used throughout the codebase to work with project files and to get access to text documents and the syntactic and semantic models. Moving to out of process means that these entities, and the information they represent, must live in the out of process “backend”, and not in the thin Visual Studio plugin “frontend”.
So our first step was to introduce lightweight “descriptor” objects that can quickly map between the project model entities in the backend, and the Visual Studio APIs in the frontend. Even though we’re still in-process, we now have a conceptual boundary of APIs that use the project model descriptors, while the existing backend code uses project model entities.
When ReSharper loads a solution, it actually reads the project model from a binary, disk based cache, if available. This is significantly faster than creating it from the Visual Studio APIs, but we still need to use the APIs to walk the solution to make sure that our caches are up to date – we need to pick up any changes from your last git pull.
And that means our next refactoring step is to create these descriptors as we iterate over the COM APIs, and compare these to the cached project model we’ve already loaded. All of this still happens on the UI thread, and is also under our global “write lock” that protects our project model from threading issues. We’re not seeing any improvements yet.
From this point, things start to get a little more interesting. The third step is to use the COM APIs to walk the solution, still synchronously, but only create our lightweight descriptor objects. We then send these, asynchronously, to the (conceptual, still in-process) “backend”. Right now, this just means running on a background thread, but it also means that our impact on the UI thread is significantly reduced. On the background thread, we compare our descriptors to our cached project model, and if there are any differences, marshal back to the UI thread to take the write lock, talk to the COM APIs and update just the changes. Things are getting better.
Finally, we get to where we are in this release cycle – and we can remove the usage of the COM APIs completely. We’ve previously looked at using Roslyn’s workspaces API, but this isn’t as rich as the COM APIs, and doesn’t give us all the information we need. Instead, we’ll take a leaf out of Rider’s book, and talk to MSBuild directly.
But again, this is anything but straightforward. We can’t use the MSBuild APIs inside Visual Studio, as this causes increased memory usage. So we have to host MSBuild ourselves, as another out of process application. But now we have to be very careful to avoid synchronisation and contention issues with Visual Studio’s own project evaluation. There are also significant challenges in making sure that the project model is writable – if we use our out of process MSBuild to add a reference and update the project files, we also need to call the Visual Studio APIs to avoid Visual Studio having to reload the solution.
The good news is that the work has been completed on this piece, and we’re currently putting it through its paces with a good round of testing. We’re hoping it will be the default option in ReSharper 2019.2, but that will only happen if we’re confident of the quality. If there are issues, we’ll hold it back for ReSharper 2019.3.
I hope you can see that there’s a huge amount of complexity involved in all of this. Basically, what we’re doing with ReSharper is like changing the wheels of a car doing 130 down the motorway. We’re fundamentally changing the architecture of a product that we’ve been building for 15 years, while it’s still (very much) in active development. And we can’t even postpone other features we’re working on, such as support for new C# language features, or new versions of Visual Studio.
In fact, recent changes to the way Visual Studio and .NET 4.8 handles per-monitor DPI has required the attention of a lot of the team in an effort to quickly get all of our tool windows working with the unexpected changes to APIs. And let’s not forget the Rider team implementing more ReSharper features, as well as porting the out of process ReSharper host to run on .NET Core!
But it already works with Rider?
This is a very good point. Rider was released back in August 2017, and had a lengthy EAP process before that. We proved we could run ReSharper out of process a long time ago. If we’ve already got it working in Rider, why does it take so much longer to get it working with Visual Studio? And if ReSharper is based on so many assumptions about interactions with Visual Studio, how does Rider work with IntelliJ?
A simplistic response to the first point is that not all of ReSharper’s features have been integrated into Rider yet, such as our type or project dependency diagrams, or remove unused references, and we can’t move ReSharper out of process until all of these features are available to an out of process host. This is true, but it’s not the full story.
The main answer, to both of these points, is also very simple – they’re different scenarios, and have different requirements. Running ReSharper as an out of process plugin to Visual Studio is a very different proposition to running ReSharper as the out of process core to a full IDE built on top of the IntelliJ Platform.
There are fundamental differences to the way IntelliJ and Visual Studio work, and how we integrate with both. In some respects, Rider’s out of process ReSharper host is a superset of the functionality in ReSharper for Visual Studio – the Solution Explorer is a good example here. Rider needs to provide a rich hierarchy of solution, projects, folders, files, references, dependencies, icons, properties, and so on. ReSharper doesn’t need to show anything – this tool window is provided by Visual Studio. So the out of process work done for Rider can’t be reused by ReSharper for Visual Studio.
But that sounds great! If Visual Studio provides the Solution Explorer, ReSharper running out of process doesn’t need to provide anything. Not so fast! While ReSharper doesn’t own or draw the Solution Explorer, it does need to track the selected item, provide context menu actions, calculate extra properties for the selected item and so on. So we can’t just reuse this part of the existing Rider out of process infrastructure, and have to refactor existing ReSharper functionality to create something new, for a new set of requirements. The worst of all worlds.
On the flip side, unit testing support is easy. Rider doesn’t use IntelliJ’s existing unit testing UI – ReSharper’s unit testing support is handled and presented very differently to how IntelliJ does it for other languages and frameworks. So the Rider team wrote their own custom version of the unit testing tool window that does expose all of ReSharper’s unit testing features, and that means that the hard work to show unit test results from an out of process ReSharper is already done – we just need to wire that up to Visual Studio.
Looking at another example, both ReSharper and Rider show the results of the Find Usages command in a tool window. This means ReSharper already supports sending a tree of results via the out of process communication protocol. However, Rider simply needs to fulfill an existing IntelliJ API, while ReSharper needs to create the tool window from scratch. The scenarios are different.
This also raises an interesting question – where does the user interface for this custom tool window live? ReSharper will obviously still need an in-process Visual Studio plugin – should this plugin create the user interface and populate it from out of process data? Or should we have some way of “remoting” a user interface from the backend out of process host? And what about settings pages and dialogs for refactorings – who creates and owns these? Do we create one user interface in Swing for IntelliJ and another in WPF for Visual Studio?
The good news here is that there is a comprehensive chunk of Rider work that we can reuse in this case, a feature we call BeControls (the “Be” stands for “backend”). This allows us to create a view, such as a dialog or panel, from a view model created by the out of process ReSharper host, transmitted to the front end user interface process (either IntelliJ or Visual Studio) and drawn natively. Rider is already using this extensively for Rider support, and we can now write a user interface that can be shared in both Rider and ReSharper for Visual Studio. And this will also work nicely for ReSharper running out of process.
But what about the problems listed above, that different scenarios require different interactions and the core interface we built to work with Visual Studio are no longer suitable for working out of process? How does Rider work if that’s such a big problem?
The answer to this one is fairly easy as well – with Rider, we own both sides of the interface contract. If there’s a problem with the front end, we can change the interfaces to work with both Visual Studio and Rider’s out of process host. And if we can’t easily change the interface, we can change the implementation or requirement in the IntelliJ side. We don’t have this flexibility with Visual Studio.
Looking at the project model, we don’t have to worry about any interaction with Visual Studio at all, so we can host MSBuild directly and take full ownership of the project model.
Or we can look at the “action” system. Each menu item, context menu item, toolbar button and keyboard shortcut is bound to an action, and ReSharper has hundreds of these. Both Visual Studio and IntelliJ have different systems for working with actions, with different requirements for registering actions, assigning shortcuts, customising presentation (icons and text) and calculating availability – some synchronous, some async, some requiring UI thread access, some done declaratively. While building Rider, we made changes to the IntelliJ Platform to make these different models more similar, so we could reuse ReSharper’s actions with only minor changes. But this required changes on both sides – ReSharper and IntelliJ.
Having full ownership of the entire IDE allows us to make any changes we need to get IntelliJ and Rider playing nicely together. But there is a cost to this strategy, too – the IntelliJ Platform is shared across 10 different IDEs inside JetBrains, not counting third party IDEs such as Android Studio. Any changes we make to IntelliJ must play nice with the IntelliJ ecosystem. Rider has been built from its own fork of the IntelliJ Platform since the very beginning, and after every release, we’ve had to backport and merge changes back upstream. In fact, Rider 2019.2 will be the first release which is built from IntelliJ’s master branch.
Where are we now?
Whew. That’s a very long detailed look at why ReSharper isn’t yet running out of process. So where does that leave us? How are things looking for the future?
Firstly: please be patient with us, but rest assured, it’s coming.
This is a massive architectural challenge, and is not something that can easily be done at speed, but it is also a big deal for us – we want it too. We’ve made a huge amount of progress, and have already got a lot of the pieces in place for running ReSharper out of process. As before, we’re not committing to an ETA of when we’ll see ReSharper running fully out of process. It’s coming, but it’s not there yet. We’ll keep you informed of progress and you’ll definitely hear about preview releases when the time comes.
But importantly, this isn’t all we’re doing to address performance in ReSharper. We understand how important it is, and continue to make improvements in every release. We’ve been targeting specific reported scenarios, as we covered in this series of posts from ReSharper 2018.2, or this post from ReSharper 2018.3.
And we’re also working on more general performance improvements for initial startup and solution load. For example, we’re hoping to ship the MSBuild based project model in ReSharper 2019.2. That should have a positive impact on solution load, similar to the now sadly deprecated Lightweight Solution Load from Visual Studio 2017.
Looking at future releases, we’re also working on changing our container construction strategy, by making it both lazy and asynchronous. Currently, we create all components at application startup and solution load, on the UI thread. This is done at low priority, frequently yielding back to the UI for responsiveness, but the impact can still be felt. We’re working to move this to a background thread, and only creating components when we need them. This is yet another massive undertaking with hidden complexities, requiring an audit of the entire codebase to see what components can be created lazily, and what must be created eagerly, as well as correctly fulfilling component dependencies on a background thread that require COM objects that can only be used on the UI thread! We’ll talk more about this in the future.
We hope you’ve enjoyed this deep dive into what’s going on with taking ReSharper out of process. We’re just as impatient as you are to get this implemented, and hope to have something to share sooner, rather than later.
In the meantime, if you do encounter performance issues with ReSharper, please use these instructions to provide us with a profiling snapshot that we can use to investigate and hopefully fix. And if you haven’t already, please try your solution with Rider – you might be pleasantly surprised!