Skip to content
This repository has been archived by the owner on Dec 18, 2018. It is now read-only.

Reduce the per message allocations #1630

Closed
wants to merge 3 commits into from

Conversation

davidfowl
Copy link
Member

@davidfowl davidfowl commented Mar 17, 2018

  • Introduce Utf8BufferTextReader which is a simple TextReader implemented over a utf8 buffer. It encodes directly into the buffer allocated by JSON.NET.
  • Use JSON.NET's IArrayPool feature and back it with the shared array pool.

Baseline

image

             Method |          Input | HubProtocol |         Mean |       Error |       StdDev |       Median |        Op/s |   Gen 0 |  Gen 1 | Allocated |
------------------- |--------------- |------------ |-------------:|------------:|-------------:|-------------:|------------:|--------:|-------:|----------:|
  ReadSingleMessage |   FewArguments |        Json |   6,748.6 ns |   385.87 ns |  1,107.14 ns |   6,381.7 ns |   148,179.5 |  0.7172 | 0.0076 |    8728 B |
  ReadSingleMessage | LargeArguments |        Json | 137,546.7 ns | 4,607.99 ns | 13,514.45 ns | 136,188.9 ns |     7,270.3 | 18.0664 | 4.6387 |  217408 B | 
  ReadSingleMessage |  ManyArguments |        Json |   8,444.5 ns |   165.59 ns |    154.89 ns |   8,435.7 ns |   118,420.3 |  0.8698 | 0.0153 |   10600 B |
  ReadSingleMessage |    NoArguments |        Json |   4,381.1 ns |   211.40 ns |    613.31 ns |   4,278.5 ns |   228,254.2 |  0.6332 |      - |    7648 B |

This PR

image

             Method |          Input | HubProtocol |         Mean |        Error |       StdDev |       Median |        Op/s |  Gen 0 |  Gen 1 | Allocated |
------------------- |--------------- |------------ |-------------:|-------------:|-------------:|-------------:|------------:|-------:|-------:|----------:|
  ReadSingleMessage |   FewArguments |        Json |   5,576.3 ns |   127.120 ns |   197.911 ns |   5,510.1 ns |   179,331.1 | 0.2594 |      - |    3264 B |
  ReadSingleMessage | LargeArguments |        Json | 109,498.0 ns | 3,127.507 ns | 8,973.404 ns | 107,669.3 ns |     9,132.6 | 4.8828 | 0.4883 |   60960 B |
  ReadSingleMessage |  ManyArguments |        Json |   8,756.9 ns |   323.938 ns |   934.636 ns |   8,294.2 ns |   114,196.0 | 0.4120 |      - |    5096 B |
  ReadSingleMessage |    NoArguments |        Json |   3,712.5 ns |   162.313 ns |   470.900 ns |   3,570.0 ns |   269,359.2 | 0.1793 |      - |    2192 B |

Fixes #1618

- Introduce CharArrayTextReader which is a simple TextReader
implemented over a utf8 buffer. It encodes directly into the
buffer allocated by JSON.NET.
@davidfowl davidfowl force-pushed the davidfowl/reduce-per-message branch from ac4e05d to 128ce47 Compare March 17, 2018 03:14
- Renamed CharArrayTextReader to Utf8BufferTextReader
- Fixed length issue on netstandard version
}
var textReader = new Utf8BufferTextReader(payload);
messages.Add(ParseMessage(textReader, binder));

Copy link
Member

Choose a reason for hiding this comment

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

nit: remove line

{
using (var reader = new JsonTextReader(new StreamReader(input)))
using (var reader = new JsonTextReader(input))
Copy link
Member

Choose a reason for hiding this comment

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

Add an internal helper method for creating a JsonTextReader with shared array pool?

{
private ReadOnlyMemory<byte> _utf8Buffer;

public Utf8BufferTextReader(ReadOnlyMemory<byte> utf8Buffer)
Copy link
Member

Choose a reason for hiding this comment

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

Every time I read HubEndPoint and see the ToArray line 150 I don't like it. See below.

What about if HubProtocol took a sequence, which was then passed to this text reader. The logic in Utf8BufferTextReader would stay largely the same expect it could read over each memory block in the sequence instead of just one. We can avoid copying the sequence in its entirety into a new array.

var result = await connection.Input.ReadAsync(connection.ConnectionAbortedToken);
var buffer = result.Buffer;
var consumed = buffer.End;
var examined = buffer.End;
try
{
if (!buffer.IsEmpty)
{
var hubMessages = new List<HubMessage>();
// TODO: Make this incremental
if (connection.Protocol.TryParseMessages(buffer.ToArray(), _dispatcher, hubMessages))
{
foreach (var hubMessage in hubMessages)
{
// Don't wait on the result of execution, continue processing other
// incoming messages on this connection.
_ = _dispatcher.DispatchMessageAsync(connection, hubMessage);
}
}
}
else if (result.IsCompleted)
{
break;
}
}
finally
{
connection.Input.AdvanceTo(consumed, examined);
}

Copy link
Member Author

@davidfowl davidfowl Mar 18, 2018

Choose a reason for hiding this comment

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

This change will be made after @anurse 's refactoring. The problem is that right now the client and server are asymmetrical and they don't both parse ReadOnlySequence<byte>. Client uses a byte[] and the server uses ReadOnlySequence<byte>. This will be done as part of #1508.

The plan is to:

  • Move the reconnect logic to HubConnection instead of IConnection
  • Make IConnection single use, single reader and writer
  • Change the IConnection client to use pipelines
  • Change the parsers to be incremental
    • Change Utf8TextReader to take the ReadOnySequence<byte>directly from the pipe

I'm hoping after we do that, we're be blazingly fast on the client and server and will allocate very little.

Next after that will be object pooling of various non buffer things.

Copy link
Member

@JamesNK JamesNK Mar 18, 2018

Choose a reason for hiding this comment

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

Could we make the server and Utf8BufferTextReader work with ReadOnlySequence<byte> now? On the client we can just wrap the byte[] in a ReadOnlySequence<byte> with little to no overhead before parsing, no? Memory and sequence are just structs.

Copy link
Member Author

@davidfowl davidfowl Mar 18, 2018

Choose a reason for hiding this comment

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

I don't really see the point. I mean I actually want to do it, just not prematurely.

Copy link
Member Author

@davidfowl davidfowl Mar 18, 2018

Choose a reason for hiding this comment

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

I also hate ToArray() with a passion but there is a master plan here and it's going to be glorious when it comes together. These are mostly incremental steps towards that shared goal. I expect to see dramatic performance improvements in the next couple of weeks.

@davidfowl
Copy link
Member Author

Closing in favor of #1635

@davidfowl davidfowl closed this Mar 18, 2018
Copy link
Contributor

@analogrelay analogrelay left a comment

Choose a reason for hiding this comment

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

LGTM (post-merge)

@@ -7,13 +7,13 @@
"@std/esm": {
"version": "0.18.0",
"resolved": "https://registry.npmjs.org/@std/esm/-/esm-0.18.0.tgz",
"integrity": "sha512-oeHSSVp/WxC08ngpKgyYR4LcI0+EBwZiJcB58jvIqyJnOGxudSkxTgAQKsVfpNsMXfOoILgu9PWhuzIZ8GQEjw==",
"integrity": "sha1-4hK1Zcdl+Tsp7FIqSToZLOeuK6Y=",
Copy link
Contributor

Choose a reason for hiding this comment

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

Upgrade your NPM bro

@davidfowl davidfowl deleted the davidfowl/reduce-per-message branch March 28, 2018 06:04
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants