Skip to content

Commit b3ce174

Browse files
committed
Harden V3 event ingestion pipeline
1 parent 52a1904 commit b3ce174

124 files changed

Lines changed: 11687 additions & 908 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

benchmarks/Exceptionless.Benchmarks/Exceptionless.Benchmarks.csproj

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,10 @@
1010
<ItemGroup>
1111
<ProjectReference Include="..\..\src\Exceptionless.Core\Exceptionless.Core.csproj" />
1212
</ItemGroup>
13+
14+
<ItemGroup>
15+
<!-- Compile the production reader source so the microbenchmark cannot drift to a substitute parser. -->
16+
<Compile Include="..\..\src\Exceptionless.Web\Utility\EventIngestionV3StreamReader.cs"
17+
Link="Production\EventIngestionV3StreamReader.cs" />
18+
</ItemGroup>
1319
</Project>
Lines changed: 171 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,81 +1,222 @@
11
using System.IO.Pipelines;
22
using System.Text;
33
using System.Text.Json;
4+
using System.Text.Json.Serialization;
45
using BenchmarkDotNet.Attributes;
6+
using Exceptionless.Core.Extensions;
7+
using Exceptionless.Core.Models;
58
using Exceptionless.Core.Models.Ingestion;
69
using Exceptionless.Core.Serialization;
10+
using Exceptionless.Web.Utility;
711

812
namespace Exceptionless.Benchmarks.Serialization;
913

1014
[MemoryDiagnoser]
1115
public class EventIngestionDeserializationBenchmarks
1216
{
13-
private byte[] _arrayPayload = null!;
14-
private byte[] _streamPayload = null!;
17+
private const int MaximumEventSize = 512 * 1024;
18+
private byte[] _v2Payload = null!;
19+
private byte[] _v3StreamPayload = null!;
20+
private JsonSerializerOptions _v2JsonOptions = null!;
1521

1622
[Params(1, 100, 1000)]
1723
public int EventCount { get; set; }
1824

1925
[GlobalSetup]
2026
public void Setup()
2127
{
22-
var events = new EventIngestionV3Event[EventCount];
23-
for (int index = 0; index < events.Length; index++)
28+
_v2JsonOptions = new JsonSerializerOptions().ConfigureExceptionlessDefaults();
29+
_v2JsonOptions.RespectNullableAnnotations = false;
30+
var v2Events = new V2BenchmarkEvent[EventCount];
31+
var v3Events = new EventIngestionV3Event[EventCount];
32+
for (int index = 0; index < v3Events.Length; index++)
2433
{
25-
events[index] = new EventIngestionV3Event
34+
const string message = "Operation failed";
35+
const string exceptionType = "System.InvalidOperationException";
36+
const string stackTrace = " at Example.Service.Run() in Service.cs:line 42";
37+
v2Events[index] = new V2BenchmarkEvent(
38+
Event.KnownTypes.Error,
39+
DateTimeOffset.UnixEpoch.AddSeconds(index),
40+
message,
41+
new V2BenchmarkData(new V2BenchmarkError(exceptionType, message, stackTrace)));
42+
v3Events[index] = new EventIngestionV3Event
2643
{
2744
Id = $"01J0000000000000000000{index:D4}",
2845
Type = "error",
2946
Date = DateTimeOffset.UnixEpoch.AddSeconds(index),
30-
Message = "Operation failed",
31-
ExceptionType = "System.InvalidOperationException",
32-
StackTrace = " at Example.Service.Run() in Service.cs:line 42"
47+
Message = message,
48+
ExceptionType = exceptionType,
49+
StackTrace = stackTrace
3350
};
3451
}
3552

36-
_arrayPayload = JsonSerializer.SerializeToUtf8Bytes(
37-
events,
38-
EventIngestionJsonContext.Default.EventIngestionV3EventArray);
53+
_v2Payload = EventCount == 1
54+
? JsonSerializer.SerializeToUtf8Bytes(v2Events[0])
55+
: JsonSerializer.SerializeToUtf8Bytes(v2Events);
3956

40-
using var stream = new MemoryStream(_arrayPayload.Length);
41-
for (int index = 0; index < events.Length; index++)
57+
using var stream = new MemoryStream(_v2Payload.Length);
58+
for (int index = 0; index < v3Events.Length; index++)
4259
{
4360
JsonSerializer.Serialize(
4461
stream,
45-
events[index],
62+
v3Events[index],
4663
EventIngestionJsonContext.Default.EventIngestionV3Event);
4764
stream.WriteByte((byte)'\n');
4865
}
4966

50-
_streamPayload = stream.ToArray();
67+
_v3StreamPayload = stream.ToArray();
5168
}
5269

5370
[Benchmark(Baseline = true)]
54-
public int DeserializeArray()
71+
public int DeserializeV2Payload()
5572
{
56-
var events = JsonSerializer.Deserialize(
57-
_arrayPayload,
58-
EventIngestionJsonContext.Default.EventIngestionV3EventArray);
59-
60-
return events?.Length ?? 0;
73+
string input = Encoding.UTF8.GetString(_v2Payload);
74+
return input.GetJsonType() switch
75+
{
76+
JsonType.Object => JsonSerializer.Deserialize<PersistentEvent>(input, _v2JsonOptions) is null ? 0 : 1,
77+
JsonType.Array => JsonSerializer.Deserialize<PersistentEvent[]>(input, _v2JsonOptions)?.Length ?? 0,
78+
_ => 0
79+
};
6180
}
6281

6382
[Benchmark]
64-
public async Task<int> DeserializeTopLevelValuesAsync()
83+
public Task<int> FrameAndRouteV3NdjsonAsync() => ReadV3NdjsonAsync(materializeSurvivors: false);
84+
85+
[Benchmark]
86+
public Task<int> FrameRouteAndMaterializeV3SurvivorsAsync() => ReadV3NdjsonAsync(materializeSurvivors: true);
87+
88+
private async Task<int> ReadV3NdjsonAsync(bool materializeSurvivors)
6589
{
66-
using var stream = new MemoryStream(_streamPayload, writable: false);
90+
using var stream = new MemoryStream(_v3StreamPayload, writable: false);
6791
var reader = PipeReader.Create(stream);
6892
int count = 0;
69-
70-
await foreach (var _ in JsonSerializer.DeserializeAsyncEnumerable(
71-
reader,
72-
EventIngestionJsonContext.Default.EventIngestionV3Event,
73-
topLevelValues: true))
93+
try
7494
{
75-
count++;
95+
while (await EventIngestionV3StreamReader.ReadAsync(reader, MaximumEventSize, CancellationToken.None) is { } record)
96+
{
97+
try
98+
{
99+
EventIngestionV3Event parsed = materializeSurvivors
100+
? record.BufferedRecord.Materialize()
101+
: record.Event;
102+
if (!String.IsNullOrEmpty(parsed.Id))
103+
count++;
104+
}
105+
finally
106+
{
107+
record.BufferedRecord.Dispose();
108+
}
109+
}
110+
}
111+
finally
112+
{
113+
await reader.CompleteAsync();
76114
}
77115

78-
await reader.CompleteAsync();
79116
return count;
80117
}
118+
119+
private sealed record V2BenchmarkEvent(
120+
[property: JsonPropertyName("type")] string Type,
121+
[property: JsonPropertyName("date")] DateTimeOffset Date,
122+
[property: JsonPropertyName("message")] string Message,
123+
[property: JsonPropertyName("data")] V2BenchmarkData Data);
124+
125+
private sealed record V2BenchmarkData(
126+
[property: JsonPropertyName("@simple_error")] V2BenchmarkError SimpleError);
127+
128+
private sealed record V2BenchmarkError(
129+
[property: JsonPropertyName("type")] string Type,
130+
[property: JsonPropertyName("message")] string Message,
131+
[property: JsonPropertyName("stack_trace")] string StackTrace);
132+
}
133+
134+
/// <summary>
135+
/// Keeps large raw stacks visible in allocation results. One accepted error is enough to expose
136+
/// duplicate LOH strings without multiplying the benchmark process's retained payload by a batch.
137+
/// </summary>
138+
[MemoryDiagnoser]
139+
public class LargeStackEventIngestionDeserializationBenchmarks
140+
{
141+
private const int MaximumEventSize = 512 * 1024;
142+
private byte[] _v2Payload = null!;
143+
private byte[] _v3Payload = null!;
144+
private JsonSerializerOptions _v2JsonOptions = null!;
145+
146+
[Params(16 * 1024, 128 * 1024)]
147+
public int StackTraceLength { get; set; }
148+
149+
[GlobalSetup]
150+
public void Setup()
151+
{
152+
string stackTrace = new('x', StackTraceLength);
153+
_v2JsonOptions = new JsonSerializerOptions().ConfigureExceptionlessDefaults();
154+
_v2JsonOptions.RespectNullableAnnotations = false;
155+
_v2Payload = JsonSerializer.SerializeToUtf8Bytes(new Dictionary<string, object?>
156+
{
157+
["type"] = "error",
158+
["message"] = "Operation failed",
159+
["data"] = new Dictionary<string, object?>
160+
{
161+
["@simple_error"] = new
162+
{
163+
type = "Example.Exception",
164+
message = "Operation failed",
165+
stack_trace = stackTrace
166+
}
167+
}
168+
});
169+
_v3Payload = JsonSerializer.SerializeToUtf8Bytes(new
170+
{
171+
id = "large-stack-event",
172+
type = "error",
173+
message = "Operation failed",
174+
exception_type = "Example.Exception",
175+
stack_trace = stackTrace
176+
});
177+
}
178+
179+
[Benchmark(Baseline = true)]
180+
public PersistentEvent? DeserializeV2Payload()
181+
{
182+
string input = Encoding.UTF8.GetString(_v2Payload);
183+
return JsonSerializer.Deserialize<PersistentEvent>(input, _v2JsonOptions);
184+
}
185+
186+
[Benchmark]
187+
public Task<int> FrameAndRouteV3NdjsonAsync() => ReadV3NdjsonAsync(materializeSurvivor: false);
188+
189+
[Benchmark]
190+
public Task<int> FrameRouteAndMaterializeV3SurvivorAsync() => ReadV3NdjsonAsync(materializeSurvivor: true);
191+
192+
private async Task<int> ReadV3NdjsonAsync(bool materializeSurvivor)
193+
{
194+
using var stream = new MemoryStream(_v3Payload, writable: false);
195+
var reader = PipeReader.Create(stream);
196+
try
197+
{
198+
EventIngestionV3StreamRecord? record = await EventIngestionV3StreamReader.ReadAsync(
199+
reader,
200+
MaximumEventSize,
201+
CancellationToken.None);
202+
if (record is null)
203+
return 0;
204+
205+
try
206+
{
207+
EventIngestionV3Event parsed = materializeSurvivor
208+
? record.Value.BufferedRecord.Materialize()
209+
: record.Value.Event;
210+
return parsed.StackTrace?.Length ?? 0;
211+
}
212+
finally
213+
{
214+
record.Value.BufferedRecord.Dispose();
215+
}
216+
}
217+
finally
218+
{
219+
await reader.CompleteAsync();
220+
}
221+
}
81222
}

0 commit comments

Comments
 (0)