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

Switch Roslyn protocol types to System.Text.Json serialization #73207

Merged
merged 5 commits into from
May 15, 2024

Conversation

dibarbet
Copy link
Member

@dibarbet dibarbet commented Apr 24, 2024

Switches the Roslyn server to use System.Text.Json serialization for all LSP messages.

I suggest reviewing one commit at a time

  1. 6046471 is almost entirely mechanical find/replace of DataContract/DataMember/other newtonsoft attributes with the STJ version inside the Protocol/Protocol folder. I also copied the new converters https://devdiv.visualstudio.com/DevDiv/_git/VSLanguageServerClient/pullrequest/530064 (adjusted for style warnings).
  2. b292a51 is the meat of the change - switching streamjsonrpc to use STJ and implementing STJ support in the roslyn language server. It also covers modifying custom Roslyn protocol types to use STJ attributes.
  3. 1a4c3f4 - I apologize for this commit, its very unfortunate. Basically we have not opted the client side into STJ serialization, its still using newtonsoft (blocked on Razor transitioning to new APIs). This means that when we make document symbol LSP requests for document outline, we need to use newtonsoft compatible types, otherwise the client can't serialize it. Rather than 'poisoning' the rest of the roslyn protocol with newtonsoft attributes, I created duplicate types that we can delete as soon we as swtich to STJ on the client side. I also updated the tests to actually serialize using newtonsoft on the client side (server side still uses STJ).
  4. cade388 - small random cleanup

Tested C#/Razor/F#/TS in VS, document symbols in VS, and C#/Razor in VSCode.

TODO - PR validation in progress.
Initial results are showing an almost 25% reduction in allocations in the lightbulb speedometer tests. There are some methods JIT regressions, currently testing new optprof data to see if that helps.
image

Resolves #72867

@dibarbet dibarbet force-pushed the lsp_stj branch 3 times, most recently from d0f8a35 to 84d40a8 Compare May 2, 2024 18:59
@dibarbet dibarbet force-pushed the lsp_stj branch 2 times, most recently from 3fa1ec0 to 16e8689 Compare May 3, 2024 00:10
@dibarbet dibarbet changed the title [WIP] Switch Roslyn protocol types to System.Text.Json serialization Switch Roslyn protocol types to System.Text.Json serialization May 3, 2024
@dibarbet dibarbet marked this pull request as ready for review May 3, 2024 00:11
@dibarbet dibarbet requested review from a team as code owners May 3, 2024 00:11

// Retrieve the language of the request so we know how to deserialize it.
var language = _target.GetLanguageForRequest(_method, request);
public abstract MethodInfo GetEntryPoint(bool hasParameter);
Copy link
Member Author

Choose a reason for hiding this comment

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

I created two subclasses of AbstractLanguageServer, one for Newtonsoft and one for STJ (we still support webtools/razor using clasp using newtonsoft).

This can all be simplified once we move Razor/XAML over to our protocol types and no longer need to manually handle serialization

@CyrusNajmabadi
Copy link
Member

Any early numbers about it this improves anything?

/// StreamJsonRpc entry point for all handler methods.
/// The optional parameters allow StreamJsonRpc to call into the same method for any kind of request / notification (with any number of params or response).
/// </summary>
private async Task<JToken?> ExecuteRequestAsync(JToken? request = null, CancellationToken cancellationToken = default)
Copy link
Member Author

Choose a reason for hiding this comment

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

this is the same as before, just moved into its own subclass


namespace Microsoft.CommonLanguageServerProtocol.Framework;

internal abstract class SystemTextJsonLanguageServer<TRequestContext>(JsonRpc jsonRpc, JsonSerializerOptions options, ILspLogger logger) : AbstractLanguageServer<TRequestContext>(jsonRpc, logger)
Copy link
Member Author

Choose a reason for hiding this comment

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

this is all new for STJ support.

/// <summary>
/// StreamJsonRpc entry point for handlers with parameters (and any response) type.
/// </summary>
private async Task<JsonElement?> ExecuteRequestAsync(JsonElement? request, CancellationToken cancellationToken = default)
Copy link
Member Author

@dibarbet dibarbet May 3, 2024

Choose a reason for hiding this comment

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

STJ uses JsonElement, which is a struct, whereas newtonsoft uses JToken (a class). This caused all sorts of nullability differences between the two impls, so 100x easier to separate them (even if there is a small amount of duplication) here.

And as mentioned, all of this is simplified once we switch Razor/XAML to our types and remove the custom serialization logic here.


namespace Roslyn.LanguageServer.Protocol
{
using Newtonsoft.Json;
Copy link
Member Author

Choose a reason for hiding this comment

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

unnecessary, deleted in followup commit.

/// <summary>
/// Converter used to deserialize objects dropping any <see cref="IProgress{T}"/> property.
/// </summary>
internal class DropProgressConverter : JsonConverter
Copy link
Member Author

Choose a reason for hiding this comment

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

unnecessary, delted in followup

@@ -99,7 +99,7 @@
<PackageVersion Include="Microsoft.VisualStudio.Language" Version="$(MicrosoftVisualStudioCoreVersion)" />
<PackageVersion Include="Microsoft.VisualStudio.Language.NavigateTo.Interfaces" Version="$(MicrosoftVisualStudioCoreVersion)" />
<PackageVersion Include="Microsoft.VisualStudio.Language.StandardClassification" Version="$(MicrosoftVisualStudioCoreVersion)" />
<PackageVersion Include="Microsoft.VisualStudio.LanguageServer.Client" Version="17.10.11-preview" />
<PackageVersion Include="Microsoft.VisualStudio.LanguageServer.Client" Version="17.10.72-preview" />
Copy link
Member Author

@dibarbet dibarbet May 3, 2024

Choose a reason for hiding this comment

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

switching to newer version that has APIs using concrete types instead of JToken. Will make the transition to STJ on the client side easier later.

internal bool CallInitialized { get; init; } = true;
internal object? ClientTarget { get; init; } = null;
internal string? Locale { get; init; } = null;
internal IEnumerable<DiagnosticAnalyzer>? AdditionalAnalyzers { get; init; } = null;
internal IJsonRpcMessageFormatter? ClientMessageFormatter { get; init; } = null;
Copy link
Member Author

Choose a reason for hiding this comment

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

Allows us to use newtonsoft on the client side in tests for serialization

@@ -23,43 +25,37 @@ namespace Microsoft.VisualStudio.LanguageServices.DocumentOutline;
using LspDocumentSymbol = DocumentSymbol;
using Range = Roslyn.LanguageServer.Protocol.Range;

internal delegate Task<ManualInvocationResponse?> LanguageServiceBrokerCallback(
ITextBuffer textBuffer, Func<JToken, bool> capabilitiesFilter, string languageServerName, string method, Func<ITextSnapshot, JToken> parameterFactory, CancellationToken cancellationToken);
internal delegate Task<TResponse?> LanguageServiceBrokerCallback<TRequest, TResponse>(Request<TRequest, TResponse> request, CancellationToken cancellationToken);
Copy link
Member Author

Choose a reason for hiding this comment

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

switches to the new lsp client API

string textViewFilePath,
CancellationToken cancellationToken)
{
ITextSnapshot? requestSnapshot = null;
JToken ParameterFunction(ITextSnapshot snapshot)

var request = new DocumentRequest<DocumentSymbolNewtonsoft.NewtonsoftRoslynDocumentSymbolParams, DocumentSymbolNewtonsoft.NewtonsoftRoslynDocumentSymbol[]>()
Copy link
Member Author

Choose a reason for hiding this comment

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

Even though we switched to the new LSP client API that uses concrete types instead of JTOken - the streamjsonrpc instance on the client side is still newtonsoft. Therefore we have to use type definitions compatible with newtonsoft serialization

/// https://github.com/dotnet/roslyn/pull/72675 tracks opting in the client to STJ.
/// TODO - everything in this type should be deleted once the client side is using STJ.
/// </summary>
internal class DocumentSymbolNewtonsoft
Copy link
Member Author

Choose a reason for hiding this comment

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

I tried to contain the spread of newtonsoft here by just copying the types we need and adding the newtonsoft attributes to them.

These can be deleted as soon as we switch to STJ on the client side

/// </summary>
private async Task<JsonElement?> ExecuteRequestAsync(JsonElement? request, CancellationToken cancellationToken = default)
{
var queue = target.GetRequestExecutionQueue();
Copy link
Contributor

Choose a reason for hiding this comment

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

target

@CyrusNajmabadi

Off topic: What's the general consensus on primary constructor usage?

I like them when they are accessed during field initializations (outside methods). However, I strongly like being able to easily differentiate class vs local data within methods. I guess if I had my druthers, for any primary ctor arg needed in a method we'd still have the field declaration using the underscore syntax that sets itself to the primary constructor arg.

Copy link
Member

Choose a reason for hiding this comment

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

currently no rules. there are mixed feelings. I personally have no issues with captures. but i'm fairly used to lambdas, local functions, and JS/TS-style capture development. I can understand the desire around fields though.

Maybe a general rule would be that it's ok for fairly simple types with few methods. But once it gets complex enough, we assign to fields?

@dibarbet
Copy link
Member Author

dibarbet commented May 6, 2024

blocked on microsoft/vs-streamjsonrpc#1038 (found in test insertion)

@DoctorKrolic
Copy link
Contributor

Fixes #72867 or at least is related to that issue, if we consider that there are still Newtonsoft-based parts for razor compatibility

@dibarbet
Copy link
Member Author

dibarbet commented May 13, 2024

blocked on microsoft/vs-streamjsonrpc#1038 (found in test insertion)

unblocked. Now pending investigation of method JIT regressions

@dibarbet
Copy link
Member Author

Updated PR validation is here https://dev.azure.com/devdiv/DevDiv/_git/VS/pullrequest/550816 (internal only)

JIT regressions are approved, will merge as soon as CI completes.

@dibarbet dibarbet enabled auto-merge May 15, 2024 01:16
@dibarbet dibarbet merged commit e5d344a into dotnet:main May 15, 2024
28 checks passed
@dibarbet dibarbet deleted the lsp_stj branch May 15, 2024 18:05
@dotnet-policy-service dotnet-policy-service bot added this to the Next milestone May 15, 2024
@CyrusNajmabadi
Copy link
Member

Woohoo!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area-IDE untriaged Issues and PRs which have not yet been triaged by a lead
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Roslyn needs to move to System.Text.Json for LSP messages.
5 participants