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

Fix verbose RPC message tracing #398

Merged
merged 1 commit into from
Dec 5, 2019
Merged

Conversation

AArnott
Copy link
Member

@AArnott AArnott commented Dec 5, 2019

We have JsonConverter and IMessagePackFormatter types that have side effects in order to support marshaling of special objects. This makes tracing the full JSON-RPC message hazardous if it means we run these custom serializers multiple times per message, and this is in fact happening lately.

The fix is to be sure we only serialize/deserialize a message once. This means that the result of the serialization must be retained long enough to trace (if tracing is turned on). It also means we need to retain the text that was deserialized long enough to be traced. We also don't want to allocate a string for the entire message unless tracing requires it.

The approach varies between formatters, as follows:

For the JsonMessageFormatter we have a super-convenient JToken instance that represents the entire message either after serialization or before deserialization. This type knows how to render a string representation of itself. So for this formatter we simply raise the trace events with the JToken as a parameter, which the string formatter will call ToString on if a trace listener wants it as a string.

For the MessagePackFormatter it's a little more complicated. There is no native JSON text representation at all (since msgpack is a binary encoding). But we still want to trace a JSON text representation for logging/diagnostics purposes. MessagePack has a converter that translates msgpack binary to JSON text, but this requires that we have the entire msgpack byte sequence. When deserializing this is easy enough: we have the sequence to deserialize from anyway. But when serializing, we're writing to an IBufferWriter<T> which doesn't give us access to access the serialized binary later. So seeing the full msgpack that was serialized in order to convert to JSON and trace it is something the formatter needs help with. The message handlers have access to the full msgpack to be written, so we arrange for the handlers to "call back" into the formatter after the formatter is done serializing in order to say "by the way, here's the full sequence you just wrote out" which the formatter can then use to raise the trace event with an object that will convert it to JSON text if/when the object's ToString() method is called. This call back from the handler into the formatter is through an optional interface that only the MessagePackFormatter needs to implement.

As part of this 'callback' from handler to formatter, the LengthHeaderMessageHandler needed a slightly revision: it didn't have a way to collect the entire serialized sequence written by the formatter because the PrefixingBufferWriter<T> doesn't expose a ReadOnlySequence<T> for all the written bytes like Sequence<T> does. So I had to switch to Sequence<T> in this handler. This means that small messages will have their buffer copied (even when not tracing) once before being transmitted where they weren't being copied before. But these are small messages so the impact is likely very small. Large messages were already getting copied anyway, so no difference there.

So with this change we now have safe and complete tracing of JSON-RPC messages for all handlers and formatters, and without nasty doubling of side-effects.

Fixes #386

We have JsonConverter and IMessagePackFormatter<T> types that have side effects in order to support marshaling of special objects. This makes tracing the full JSON-RPC message hazardous if it means we run these custom serializers multiple times per message, and this is in fact happening lately.

The fix is to be sure we only serialize/deserialize a message *once*. This means that the result of the serialization must be retained long enough to trace (if tracing is turned on). It also means we need to retain the text that was *de*serialized long enough to be traced. We also don't want to allocate a string for the entire message unless tracing requires it.

The approach varies between formatters, as follows:

For the `JsonMessageFormatter` we have a super-convenient `JToken` instance that represents the entire message either after serialization or before deserialization. This type knows how to render a string representation of itself. So for this formatter we simply raise the trace events with the `JToken` as a parameter, which the string formatter will call `ToString` on if a trace listener wants it as a string.

For the `MessagePackFormatter` it's a little more complicated. There is no native JSON text representation at all (since msgpack is a binary encoding). But we still want to trace a JSON text representation for logging/diagnostics purposes. MessagePack has a converter that translates msgpack binary to JSON text, but this requires that we have the entire msgpack byte sequence. When deserializing this is easy enough: we have the sequence to deserialize from anyway. But when serializing, we're writing to an `IBufferWriter<T>` which doesn't give us access to access the serialized binary later. So seeing the full msgpack that was serialized in order to convert to JSON and trace it is something the formatter needs help with. The message handlers have access to the full msgpack to be written, so we arrange for the handlers to "call back" into the formatter after the formatter is done serializing in order to say "by the way, here's the full sequence you just wrote out" which the formatter can then use to raise the trace event with an object that will convert it to JSON text if/when the object's ToString() method is called. This call back from the handler into the formatter is through an optional interface that only the `MessagePackFormatter` needs to implement.

As part of this 'callback' from handler to formatter, the `LengthHeaderMessageHandler` needed a slightly revision: it didn't have a way to collect the entire serialized sequence written by the formatter because the `PrefixingBufferWriter<T>` doesn't expose a `ReadOnlySequence<T>` for all the written bytes like `Sequence<T>` does. So I had to switch to `Sequence<T>` in this handler. This means that small messages will have their buffer copied (even when not tracing) once before being transmitted where they weren't being copied before. But these are small messages so the impact is likely very small. Large messages were already getting copied anyway, so no difference there.

So with this change we now have safe and complete tracing of JSON-RPC messages for all handlers and formatters, and without nasty doubling of side-effects.

Fixes microsoft#386
@AArnott AArnott requested review from milopezc and jepetty December 5, 2019 22:33
@AArnott AArnott self-assigned this Dec 5, 2019
@AArnott AArnott added this to the v2.3 milestone Dec 5, 2019
@codecov-io
Copy link

Codecov Report

Merging #398 into master will decrease coverage by 0.06%.
The diff coverage is 92.98%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #398      +/-   ##
==========================================
- Coverage   90.33%   90.27%   -0.07%     
==========================================
  Files          47       47              
  Lines        3581     3608      +27     
==========================================
+ Hits         3235     3257      +22     
- Misses        346      351       +5
Impacted Files Coverage Δ
src/StreamJsonRpc/JsonRpc.cs 92.93% <100%> (-0.05%) ⬇️
src/StreamJsonRpc/LengthHeaderMessageHandler.cs 100% <100%> (ø) ⬆️
src/StreamJsonRpc/Utilities.cs 100% <100%> (ø) ⬆️
src/StreamJsonRpc/HeaderDelimitedMessageHandler.cs 85.96% <50%> (-0.43%) ⬇️
src/StreamJsonRpc/WebSocketMessageHandler.cs 97.36% <50%> (-2.64%) ⬇️
src/StreamJsonRpc/MessagePackFormatter.cs 93.45% <92.3%> (-0.06%) ⬇️
src/StreamJsonRpc/JsonMessageFormatter.cs 92.82% <93.33%> (-0.12%) ⬇️
...nRpc/Reflection/MessageFormatterProgressTracker.cs 94.28% <0%> (-1.43%) ⬇️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 47a1325...288b633. Read the comment docs.

@AArnott AArnott merged commit 46d4758 into microsoft:master Dec 5, 2019
@AArnott AArnott deleted the fix386 branch December 5, 2019 23:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Improve/Fix GetJsonText on built-in formatters
3 participants