-
Notifications
You must be signed in to change notification settings - Fork 6
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
Zero allocating StatsDPublisher #104
Conversation
Open questions:
|
Due to the purpose of this pr as it is 3rd in a series. Infrastructural code, as in this repository, must run as fast as possible. Which in .net realities does mean it should also allocate as less as possible. We got tools to do so and therefore should. |
@@ -2,13 +2,16 @@ | |||
using BenchmarkDotNet.Attributes; | |||
using JustEat.StatsD; | |||
using JustEat.StatsD.EndpointLookups; | |||
using JustEat.StatsD.V2; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps the new publisher, transport types and namespaces should have a name other than V2, naming stuff is hard though.
Something that indicates it offers better perf / zero allocations.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1, V2 is confusing given that the library is at version 3.
This PR is fantastic work @dv00d00, zero allocations and it's not a breaking change! 💯 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall this looks good, but there's some TODOs to deal with and I think this would be better done without all the "v2" duplication by making the existing concrete types support the "new" and "old" way.
I'd also probably argue that this would be v4, so we could finally make the pooled implementation the main implementation so we don't have to ship two, further reducing the duplication caused by making the pooled transport in 3.1 not be a breaking change.
Directory.Build.props
Outdated
@@ -24,5 +24,6 @@ | |||
<RepositoryType>git</RepositoryType> | |||
<RepositoryUrl>$(PackageProjectUrl).git</RepositoryUrl> | |||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors> | |||
<LangVersion>7.3</LangVersion> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is redundant, we already use the latest version here: https://github.com/justeat/JustEat.StatsD/blob/a3ee3711594b048e94be4068fba54dffd6c63094/Directory.Build.props#L14
var message = StatsDMessage.Timing(128, "bucket"); | ||
Check(message, 0.5, "prefix.bucket:128|ms|@0.5"); | ||
} | ||
|
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
private static void Check(StatsDMessage message, double sampleRate, string expected) | ||
{ | ||
Formatter.TryFormat(message, sampleRate, Buffer, out int written).ShouldBe(true); | ||
var result = Encoding.UTF8.GetString(Buffer.AsSpan().Slice(0, written)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think there's an AsSpan()
overload that lets you do this in one:
var result = Encoding.UTF8.GetString(Buffer.AsSpan(0, written));
|
||
[assembly: ComVisible(false)] | ||
[assembly: Guid("8f4ff09e-4130-4872-a50f-b290e9ccb04b")] | ||
|
||
[assembly:InternalsVisibleTo("JustEat.StatsD.Tests")] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Spaces after colons.
case StatsDMessage.Kind.Timing: | ||
{ | ||
if (!buffer.TryWriteLong(magnitudeIntegral)) return false; | ||
if (!buffer.TryWriteBytes((byte)'|', (byte)'m', (byte)'s')) return false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Constants?
if (!buffer.TryWriteDouble(msg.Magnitude)) return false;; | ||
} | ||
|
||
if (!buffer.TryWriteBytes((byte)'|', (byte)'g')) return false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Constants?
|
||
if (sampleRate < 1.0 && sampleRate > 0.0) | ||
{ | ||
if (!buffer.TryWriteBytes((byte)'|', (byte)'@')) return false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Constants?
written = buffer.Written; | ||
return true; | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Extra new line.
_endpointSource = endPointSource ?? throw new ArgumentNullException(nameof(endPointSource)); | ||
} | ||
|
||
public void Send(ArraySegment<byte> metric) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As per previous comments, we should be able to make the existing types implement both ways.
I am going to address V2 naming and structure in upcoming commits |
Ok, I think I am done with refactoring and addressing issues, would appreciate another review |
using Shouldly; | ||
using Xunit; | ||
|
||
#pragma warning disable xUnit1026 // Theory methods should use all of their parameters disabled to render test case name |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you provide a screenshot of the before and after with this warning disabled?
} | ||
|
||
public delegate IStatsDPublisher Factory(StatsDConfiguration config); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you not just use Func<StatsDConfiguration, IStatsDPublisher>
?
|
||
public void Increment(long value, double sampleRate, params string[] buckets) | ||
{ | ||
if (buckets == null) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could also shortcut if buckets.Length == 0
to remove the need to allocate the enumerator.
{ | ||
// so we was not able to write to resized buffer | ||
// that means there is a bug in formatter | ||
throw new Exception("Utf8 Formatting Error. This is a bug." + |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
<PackageReference Include="System.Memory" Version="4.5.1" /> | ||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="2.0.0" /> | ||
</ItemGroup> | ||
<ItemGroup Condition=" '$(TargetFramework)' == 'netcoreapp2.1' "> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could change this to Condition=" '$(TargetFramework)' != 'net451' "
and then remove the need to reference it twice for both netstandard2.0
and netcoreapp2.1
.
|
||
public StatsDPublisher(StatsDConfiguration configuration, IStatsDTransport transport) | ||
public StatsDPublisher(StatsDConfiguration configuration, IStatsDTransport transport, bool preferBufferedTransport = false) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a binary breaking change - is that intentional?
} | ||
|
||
public StatsDPublisher(StatsDConfiguration configuration) | ||
public StatsDPublisher(StatsDConfiguration configuration, bool preferBufferedTransport = false) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As above.
There is currently no way to specify a constructor on StasDPublisher which would accept IBufferedStatsDTransport, it needs to have the separate signature, otherwise, compiler complains over Ambiguous invocation. |
{ | ||
} | ||
|
||
public StatsDPublisher(StatsDConfiguration configuration, bool preferBufferedTransport) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the reasoning behind making "better performance" opt-in? I think it would be better to add a constructor to accept the new interface, and have the existing two try use the better implementation, if possible, by default.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As I have added new transport interface implementation to the existing transports, adding a constructor with signature accepting transport would result in a compilation error for the consumer.
I will make buffered implementation a default though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure whether it's a good idea or not, but if you swapped the parameter order for the new one, it wouldn't be ambiguous to the compiler?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The compiler will accept it without issues. 😸
Looks hacky though.
Other options I can think of:
- Static factory methods on StatsDPublisher
- Dedicated factory class
- Dedicated transports per interface
- Expose new public implementation of BufferdPublisher, String publisher will retain the old name
@@ -2,7 +2,7 @@ | |||
|
|||
namespace JustEat.StatsD.Buffered |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this would be better in JustEat.StatsD
rather than JustEat.StatsD.Buffered
to make it more discoverable.
So I think just two things left on this for me:
|
|
||
var transport = new PooledUdpTransport(endpointSource); | ||
|
||
if (preferBufferedTransport) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As alluded to in my comment, maybe we could have if (configuration.PreferBufferedTransport)
instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now it seems so obvious, thx @martincostello
pushed updates as requested |
_onError = configuration.OnError; | ||
switch (transport) | ||
{ | ||
case IStatsDBufferedTransport transportV2 when configuration.PreferBufferedTransport: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now we've dropped V2, maybe this should be called bufferedTransport
?
private readonly StatsDMessageFormatter _formatter; | ||
private readonly IStatsDTransport _transport; | ||
private readonly Func<Exception, bool> _onError; | ||
private readonly IStatsDPublisher _publisher; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As this class is now more of a wrapper, maybe call this _inner
?
I think I'm basically happy with this now as a Can we get one last set of performance test results from the tip of this branch? |
This run of benchmarks is taken from the different laptop. So could not be compared directly.
|
{ | ||
internal ref struct Buffer | ||
{ | ||
public Buffer(Span<byte> source) : this() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Last minute thought, what's with : this()
, isn't that redundant?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thx, good catch
As discussed with marticC Look good, might need some trials under load. Not sure if this will be V3.2 with no / minimal breaking changes; or 4.0 with other things tidied up as well. When we can have breaking changes, we don't need both code paths: if we have two classes that work the same, and we know that they both work well in production loads, then we only need one of them (the faster / lower allocation one), there is no real user choice here. So that extra bool There still in a choice of Udp vs. Ip transports, we need both for different cases (e.g. AWS lambda). The idea of "a new factory type that could simplify the transport creation and hide the details" sounds worthwhile (though there is something like that in |
Any updates on this one @martincostello @AnthonySteele ? |
I think we just need to decide whether this is a |
If we go for a |
So which are we doing, |
Myself and @AnthonySteele discussed this offline, and we're going to release this as a |
I've added a proof of concept implementation of buffer based statsd publisher.
Key points:
Utf8FormatterBench
StatSendingBenchmark