Skip to content

Add NerdbankMessagePackFormatter - #1100

Merged
Andrew Arnott (AArnott) merged 10 commits into
microsoft:mainfrom
trippwill:dev/trippwill/nbmp-formatter
Aug 5, 2025
Merged

Add NerdbankMessagePackFormatter#1100
Andrew Arnott (AArnott) merged 10 commits into
microsoft:mainfrom
trippwill:dev/trippwill/nbmp-formatter

Conversation

@trippwill

@trippwillCharles Willis (trippwill) commented Dec 20, 2024

Copy link
Copy Markdown
Member

Add an IJsonRpcMessageFormatter implementation that uses Nerdbank.MessagePack. This serializer has better startup time and is AOT-safe. This alone won't make StreamJsonRpc as a whole AOT safe, but it's a step in that direction.

@AArnottAndrew Arnott (AArnott) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for doing all this work. You're providing invaluable feedback for the Nerdbank.MessagePack library's feature set as well.

Comment threadDirectory.Packages.props Outdated
Comment threadDirectory.Packages.props Outdated
Comment threadsrc/StreamJsonRpc/NerdbankMessagePackFormatter.CommonString.cs Outdated
Comment threadsrc/StreamJsonRpc/Protocol/JsonRpcRequest.cs Outdated
Comment threadsrc/StreamJsonRpc/NerdbankMessagePackFormatter.cs Outdated
Comment threadsrc/StreamJsonRpc/NerdbankMessagePackFormatter.cs Outdated
Comment threadsrc/StreamJsonRpc/NerdbankMessagePackFormatter.cs Outdated
Comment threadsrc/StreamJsonRpc/NerdbankMessagePackFormatter.FormatterContextBuilder.cs Outdated
Comment threadsrc/StreamJsonRpc/NerdbankMessagePackFormatter.cs
@trippwill

Copy link
Copy Markdown
MemberAuthor

Andrew Arnott (@AArnott) : Current test stats:

 2157 Passed
195 Failed
13 Skipped

@trippwill

Charles Willis (trippwill) commented Dec 28, 2024

Copy link
Copy Markdown
MemberAuthor

Current test results:
2182 Passed
170 Failed
13 Skipped

Andrew Arnott (@AArnott) - Currently seeing a lot of test failures around exceptions and having difficulty tracing the issues. Many errors are caused by an NB.MP throwing b/c an exception is not serializable.

While adding additional formatter configuration for the tests to pass, I'm seeing some
opportunities to make the builder API a bit nicer if that's the pattern we want to go with.

The need to pre-register converters in the serializer without the ability to resolve and cache at runtime like legacy MessagePack-CSharp resolvers, is currently the cause of the most repeated builder calls. I suppose we could make a converter act similar to a resolver, or make some converters more open to supporting a wider set of types?

Edit
Unrelated thought: Do we really need two different serializer instances? Maybe it would better to register all the envelope and support type shapes and converters in the default user data configuration, and let the user add additional converters and shapes via the builder. Instead of switching back and forth between an "rpc" configuration and a "userData" configuration.

@trippwill

Copy link
Copy Markdown
MemberAuthor

Benchmarks:


BenchmarkDotNet v0.13.10, Windows 11 (10.0.26100.2605) (Hyper-V)
Intel Xeon Platinum 8370C CPU 2.80GHz, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.402
[Host] : .NET 8.0.11 (8.0.1124.51707), X64 RyuJIT AVX2
Job-GLSONT : .NET 8.0.11 (8.0.1124.51707), X64 RyuJIT AVX2
Job-VLZGJF : .NET Framework 4.8.1 (4.8.9282.0), X64 RyuJIT VectorSize=256
MethodRuntimeFormatterMeanErrorStdDevRatioRatioSDGen0Gen1AllocatedAlloc Ratio
InvokeAsync_NoArgs.NET 8.0JSON77.92 μs1.471 μs1.913 μs0.880.030.4883-13.09 KB0.84
InvokeAsync_NoArgs.NET Framework 4.7.2JSON88.86 μs0.743 μs0.659 μs1.000.002.44140.122115.67 KB1.00
InvokeAsync_NoArgs.NET 8.0MessagePack46.29 μs0.631 μs0.591 μs1.090.020.1221-4.07 KB0.63
InvokeAsync_NoArgs.NET Framework 4.7.2MessagePack42.43 μs0.611 μs0.571 μs1.000.001.03760.06106.43 KB1.00
InvokeAsync_NoArgs.NET 8.0NerdbankMessagePack51.66 μs0.589 μs0.551 μs0.880.01--4.06 KB0.63
InvokeAsync_NoArgs.NET Framework 4.7.2NerdbankMessagePack58.85 μs0.218 μs0.170 μs1.000.001.03760.06106.44 KB1.00

BenchmarkDotNet v0.13.10, Windows 11 (10.0.26100.2605) (Hyper-V)
Intel Xeon Platinum 8370C CPU 2.80GHz, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.402
[Host] : .NET 8.0.11 (8.0.1124.51707), X64 RyuJIT AVX2
Job-GLSONT : .NET 8.0.11 (8.0.1124.51707), X64 RyuJIT AVX2
Job-VLZGJF : .NET Framework 4.8.1 (4.8.9282.0), X64 RyuJIT VectorSize=256
MethodRuntimeFormatterMeanErrorStdDevRatioGen0Gen1AllocatedAlloc Ratio
NotifyAsync_NoArgs.NET 8.0JSON2.821 μs0.0177 μs0.0157 μs0.440.0877-2216 B0.94
NotifyAsync_NoArgs.NET Framework 4.7.2JSON6.347 μs0.0327 μs0.0306 μs1.000.37380.00762359 B1.00
NotifyAsync_NoArgs.NET 8.0MessagePack1.877 μs0.0122 μs0.0114 μs0.630.0038-168 B0.68
NotifyAsync_NoArgs.NET Framework 4.7.2MessagePack2.961 μs0.0345 μs0.0306 μs1.000.03810.0038248 B1.00
NotifyAsync_NoArgs.NET 8.0NerdbankMessagePack1.891 μs0.0134 μs0.0125 μs0.560.0057-168 B0.67
NotifyAsync_NoArgs.NET Framework 4.7.2NerdbankMessagePack3.347 μs0.0272 μs0.0255 μs1.000.03810.0038250 B1.00

BenchmarkDotNet v0.13.10, Windows 11 (10.0.26100.2605) (Hyper-V)
Intel Xeon Platinum 8370C CPU 2.80GHz, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.402
[Host] : .NET 8.0.11 (8.0.1124.51707), X64 RyuJIT AVX2
Job-GLSONT : .NET 8.0.11 (8.0.1124.51707), X64 RyuJIT AVX2
Job-VLZGJF : .NET Framework 4.8.1 (4.8.9282.0), X64 RyuJIT VectorSize=256
MethodRuntimeFormatterMeanErrorStdDevRatioRatioSDGen0Gen1AllocatedAlloc Ratio
CreateConnectionAndInvokeOnce.NET 8.0JSON68.88 μs0.650 μs0.576 μs0.390.002.0000-49.85 KB0.86
CreateConnectionAndInvokeOnce.NET Framework 4.7.2JSON175.96 μs2.712 μs2.537 μs1.000.009.3333-58.21 KB1.00
CreateConnectionAndInvokeOnce.NET 8.0MessagePack49.62 μs0.977 μs1.711 μs0.370.011.5000-36.55 KB0.83
CreateConnectionAndInvokeOnce.NET Framework 4.7.2MessagePack133.71 μs2.591 μs2.983 μs1.000.007.0000-44.21 KB1.00
CreateConnectionAndInvokeOnce.NET 8.0NerdbankMessagePack184.48 μs2.625 μs2.455 μs0.400.022.0000-54.96 KB0.88
CreateConnectionAndInvokeOnce.NET Framework 4.7.2NerdbankMessagePack484.65 μs14.761 μs43.523 μs1.000.0010.00003.000062.52 KB1.00

@AArnott

Copy link
Copy Markdown
Member

Currently seeing a lot of test failures around exceptions and having difficulty tracing the issues. Many errors are caused by an NB.MP throwing b/c an exception is not serializable.

Seeing the exception type, message and stack trace usually helps me understand and suggest fixes for issues like this.
I don't think either msgpack library includes built-in support for exception serialization, so I'm not sure what NB.MP failure would be to blame here.

I'm seeing some opportunities to make the builder API a bit nicer if that's the pattern we want to go with.

FYI I haven't looked closely at the core of your pull request yet, since I'm trying to stay 'OOF' 😉 so I can't make a firm recommendation at this point.

The need to pre-register converters in the serializer without the ability to resolve and cache at runtime like legacy MessagePack-CSharp resolvers, is currently the cause of the most repeated builder calls. I suppose we could make a converter act similar to a resolver, or make some converters more open to supporting a wider set of types?

I recently removed the exception that is thrown when you register more converters after serialization has happened. But it still resets the converter cache so it's a runtime hit that should be avoided if possible.

Is your concern mostly around generics? I'm still working on a better solution for that from NB.MP that I hope will make this simpler for you.

Unrelated thought: Do we really need two different serializer instances? Maybe it would better to register all the envelope and support type shapes and converters in the default user data configuration, and let the user add additional converters and shapes via the builder. Instead of switching back and forth between an "rpc" configuration and a "userData" configuration.

Historically, separating envelope from user data has been useful in protecting the integrity of the JSON-RPC protocol and our extensions, while allowing the user to have flexibility in how types are serialized. If we have just one serializer object, it seems the user will either be disallowed to customize how types are serialized that we internally must serialize, or the user's customizations could invalidate the objects we serialize for the envelope. And this set of sensitive types may change over time as we change what or how we serialize objects as part of the core protocol or the exotic types.

If I'm missing the mark of what you're thinking of, please correct me.
Otherwise, I'm curious what cost (complexity, run time, etc.) you're thinking would benefit from consolidating the serializer instances.

... benchmarks...

Most of those look pretty good. The last one seems to mix in making the connection itself, which seems likely to be noisy. But I have no idea why NB.MP would make the process of connecting so much slower than the other libraries. Do you?

@trippwill

Copy link
Copy Markdown
MemberAuthor

Historically, separating envelope from user data has been useful in protecting the integrity of the JSON-RPC protocol and our extensions, while allowing the user to have flexibility in how types are serialized.

Totally makes sense.

Is your concern mostly around generics?

Mostly, yes. With MP-CS resolvers, the right formatter is found by working through an ordered list of resolvers for any given type. This allows for behaviors like a resolver intercepting intrinsic rpc-marhalables before they matched to a more conventional formatter. Same for things like Pipes and Streams as arguments or return types. And IAsyncEnumerable as a property on a class. With NB.MP, a type like MyClass : IObserver<int> has to have an RpcMarhsalable converter registered explicitly for it. Currently, I have this done as part of builder. Maybe the better solution would be to expose an ObserverConverter<T> from StreamJsonRpc that the user can apply to a custom type like MyClass?

Basically, not being able to intercept the exotic types as they are seen (for best perf) means the StreamJsonRpc consumer needs to be very explicit up-front.

But I have no idea why NB.MP would make the process of connecting so much slower than the other libraries. Do you?

Not yet :-)

FYI I haven't looked closely at the core of your pull request yet, since I'm trying to stay 'OOF' 😉

Totally understandable. I should probably step away from this myself for awhile.

@trippwill

Copy link
Copy Markdown
MemberAuthor

Latest Test Results:
2205 Passed
147 Failed
13 Skipped

@trippwill

Copy link
Copy Markdown
MemberAuthor

Latest Test Results:
2270 Passed
82 Failed
13 Skipped

@trippwill

Copy link
Copy Markdown
MemberAuthor

Test Results:

 2275 Passed
77 Failed
13 Skipped

@trippwill

Copy link
Copy Markdown
MemberAuthor
MethodRuntimeFormatterMeanErrorStdDevRatioRatioSDGen0Gen1AllocatedAlloc Ratio
CreateConnectionAndInvokeOnce.NET 8.0JSON75.15 μs1.493 μs2.767 μs0.410.022.0000-49.83 KB0.86
CreateConnectionAndInvokeOnce.NET Framework 4.7.2JSON182.33 μs3.626 μs7.805 μs1.000.009.3333-58.28 KB1.00
CreateConnectionAndInvokeOnce.NET 8.0MessagePack49.67 μs0.737 μs1.126 μs0.380.011.3333-36.58 KB0.83
CreateConnectionAndInvokeOnce.NET Framework 4.7.2MessagePack130.67 μs2.109 μs1.973 μs1.000.007.0000-44.25 KB1.00
CreateConnectionAndInvokeOnce.NET 8.0NerdbankMessagePack243.71 μs4.817 μs5.735 μs0.500.022.0000-66.71 KB0.79
CreateConnectionAndInvokeOnce.NET Framework 4.7.2NerdbankMessagePack492.03 μs9.718 μs15.413 μs1.000.0013.00003.000084.54 KB1.00

Latest benchmarks hint that there is a correlation between time and number of serializer runtime register calls. In the latest commit, many more converters and new sub type maps are added in the ctor of NerdbankMessagePackFormatter. The time could also be a result of the calls to converter resolvers.

@AArnott

Copy link
Copy Markdown
Member

Does this benchmark include the RegisterConverter calls themselves? If so, we should probably isolate that in a "connection creation" benchmark, leaving a benchmark that measures only the incremental cost of an RPC call.

Comment threadsrc/StreamJsonRpc/NerdbankMessagePackFormatter.RequestIdConverter.cs Outdated
Comment threadsrc/StreamJsonRpc/Reflection/MessageFormatterRpcMarshaledContextTracker.cs Outdated
@AArnott

Copy link
Copy Markdown
Member

FYI I've been building more on your branch with my own branch name. I'll push a bunch of commits at once soon, since I have only one last test failure. 🎈

@AArnott

Copy link
Copy Markdown
Member

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@AArnott

Copy link
Copy Markdown
Member

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@AArnott

Copy link
Copy Markdown
Member

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@AArnott

Copy link
Copy Markdown
Member

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

Named args are frequently used with C# anonymous types, for which 'type shapes' cannot be created. Since they were passed in via parameters declared as `object`, reflecting over them is not trim-safe either (at least, the linker doesn't think so).
The new `NamedArgs.Create` API makes it easy to wrap any object (including anonymous types) such that it is trim-safe and avoids individual formatters having to understand how to reflect over them.
I also added `[RequiresUnreferencedCode]` to the method overloads that just take `object` since resorting to reflection will never be trim safe. But for the callers that were passing in _dictionaries_ (which _are_ always trim safe), I added overloads _without_ the new attribute so that they can use them in trimmed apps.
Fixed the last trim warning
@AArnott
Andrew Arnott (AArnott)force-pushed the dev/trippwill/nbmp-formatter branch from 3da5ee2 to 367cc6bCompareJuly 29, 2025 13:31
@AArnott

Copy link
Copy Markdown
Member

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@AArnott
Andrew Arnott (AArnott) marked this pull request as ready for review July 29, 2025 13:44
@AArnott

Copy link
Copy Markdown
Member

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@AArnott
Andrew Arnott (AArnott) merged commit 6901ba0 into microsoft:mainAug 5, 2025
6 of 7 checks passed
@znakeeye

Copy link
Copy Markdown

No nuget package available for this? My project doesn't recognize NerdbankMessagePackFormatter. Tried both 2.22.11 and 2.23.32-alpha.

@AArnott

Copy link
Copy Markdown
Member

True. We haven't released this change in a package. In fact, we recently decided to delay shipping this another release, so you can watch #1280 for when we bring it back. Hopefully in about a month.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@trippwill@AArnott@znakeeye@RyanToth3