Skip to content

Commit 8032584

Browse files
committed
add benchmarks
1 parent d5bf707 commit 8032584

File tree

9 files changed

+196
-32
lines changed

9 files changed

+196
-32
lines changed

Directory.Packages.props

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,21 @@
66
<ItemGroup>
77
<PackageVersion Include="AssemblyMetadata.Generators" Version="2.1.0" />
88
<PackageVersion Include="AwesomeAssertions" Version="9.3.0" />
9+
<PackageVersion Include="BenchmarkDotNet" Version="0.15.6" />
910
<PackageVersion Include="dbup-sqlserver" Version="6.0.16" />
1011
<PackageVersion Include="MicroSoft.Data.SqlClient" Version="6.1.3" />
1112
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="10.0.0" />
1213
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="10.0.0" />
1314
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
15+
<PackageVersion Include="Microsoft.VisualStudio.DiagnosticsHub.BenchmarkDotNetDiagnosers" Version="18.3.36726.2" />
1416
<PackageVersion Include="MinVer" Version="6.0.0" />
1517
<PackageVersion Include="Polyfill" Version="9.3.0" />
1618
<PackageVersion Include="Serilog" Version="4.3.0" />
1719
<PackageVersion Include="Serilog.AspNetCore" Version="10.0.0" />
1820
<PackageVersion Include="Serilog.Extensions.Hosting" Version="10.0.0" />
1921
<PackageVersion Include="Serilog.Settings.Configuration" Version="10.0.0" />
2022
<PackageVersion Include="Serilog.Sinks.Console" Version="6.1.1" />
23+
<PackageVersion Include="Serilog.Sinks.MSSqlServer" Version="9.0.2" />
2124
<PackageVersion Include="System.Buffers" Version="4.6.1" />
2225
<PackageVersion Include="System.CommandLine" Version="2.0.0" />
2326
<PackageVersion Include="System.Memory" Version="4.6.3" />

README.md

Lines changed: 36 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,42 @@ A high-performance [Serilog](https://serilog.net/) sink that writes log events t
1616
- **Distributed Tracing**: Built-in support for TraceId and SpanId
1717
- **Auto Truncation**: Automatically truncates string values to match column size constraints, preventing insert errors
1818

19+
## Performance Comparison
20+
21+
This sink is designed to be a **high-performance, lightweight alternative** to `Serilog.Sinks.MSSqlServer` with significant improvements in speed and memory efficiency.
22+
23+
### Benchmark Results
24+
25+
Based on `Serilog.Sinks.SqlServer.Benchmark` tests (100 log events per batch):
26+
27+
| Method | Mean | Rank | Gen0 | Gen1 | Allocated |
28+
| --------------- | -------: | ---: | -------: | ------: | ---------: |
29+
| SqlServerSink | 2.082 ms | 1 | 7.8125 | - | 438.31 KB |
30+
| MSSqlServerSink | 2.666 ms | 2 | 117.1875 | 27.3438 | 5773.93 KB |
31+
32+
**Key Performance Benefits:**
33+
34+
- **~22% faster** execution time (2.082 ms vs 2.666 ms)
35+
- **~92% fewer allocations** (438 KB vs 5,774 KB per batch)
36+
- **Significantly reduced GC pressure** from 13x lower memory allocations
37+
- **Optimized bulk copy** operations with minimal overhead
38+
39+
### Why This Sink is Faster
40+
41+
1. **Streamlined Architecture**: Focused solely on high-performance SQL Server logging without legacy compatibility layers
42+
2. **Efficient Memory Usage**: Minimal allocations through careful use of `ArrayBufferWriter`, `Span<T>`, and modern .NET APIs
43+
3. **Optimized JSON Serialization**: Custom `JsonWriter` using `Utf8JsonWriter` for zero-copy serialization
44+
4. **Direct Bulk Copy**: Simplified data pipeline from log events to `SqlBulkCopy` with fewer intermediate transformations
45+
5. **No Reflection Overhead**: Uses pre-defined mappings with delegate-based value extraction
46+
6. **Avoids DataTable**: Uses lightweight `IDataReader` implementation instead of `DataTable`, eliminating the overhead of DataTable's internal structures
47+
48+
### Simplified Codebase
49+
50+
- **Fewer dependencies**: Minimal external packages (only `Serilog`, `Microsoft.Data.SqlClient`, and polyfills)
51+
- **Smaller footprint**: Focused implementation without legacy features
52+
- **Easier to understand**: Clear, modern C# code using latest language features
53+
- **Better maintainability**: Single-purpose design makes updates and fixes straightforward
54+
1955
## Installation
2056

2157
Install the sink via NuGet:
@@ -280,38 +316,6 @@ The `Properties` column stores log event properties as a JSON object. Property v
280316
}
281317
```
282318

283-
**Dictionaries:**
284-
285-
```json
286-
{
287-
"Headers": {
288-
"Content-Type": "application/json",
289-
"Authorization": "Bearer token"
290-
}
291-
}
292-
```
293-
294-
**Complex nested structures:**
295-
296-
```json
297-
{
298-
"Request": {
299-
"Method": "POST",
300-
"Path": "/api/users",
301-
"Headers": {
302-
"Content-Type": "application/json",
303-
"User-Agent": "MyApp/1.0"
304-
},
305-
"Body": {
306-
"Users": [
307-
{ "Id": 1, "Name": "Alice" },
308-
{ "Id": 2, "Name": "Bob" }
309-
]
310-
}
311-
}
312-
}
313-
```
314-
315319
**Supported scalar types:**
316320

317321
- Primitive types: `string`, `bool`, `int`, `long`, `double`, `float`, `decimal`, `byte`, `short`, etc.

Serilog.Sinks.SqlServer.slnx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
<File Path="src/Directory.Build.props" />
77
</Folder>
88
<Project Path="src/Serilog.Sinks.SqlServer/Serilog.Sinks.SqlServer.csproj" Id="8a1163d7-c953-47db-9e98-ad719a7d07ef" />
9+
<Project Path="test/Serilog.Sinks.SqlServer.Benchmark/Serilog.Sinks.SqlServer.Benchmark.csproj" Id="3997769d-3bc8-4aa0-92fc-f1504f40a409" />
910
<Project Path="test/Serilog.Sinks.SqlServer.Sample/Serilog.Sinks.SqlServer.Sample.csproj" Id="6167d9cb-9972-41af-8cd0-5ba90af583d1" />
1011
<Project Path="test/Serilog.Sinks.SqlServer.Tests/Serilog.Sinks.SqlServer.Tests.csproj" Id="fab4ab2c-a4ca-4937-96b7-1e3d61f2dbbf" />
1112
</Solution>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using BenchmarkDotNet.Running;
2+
3+
namespace Serilog.Sinks.SqlServer.Benchmark;
4+
5+
internal class Program
6+
{
7+
static void Main(string[] args)
8+
{
9+
var _ = BenchmarkRunner.Run(typeof(Program).Assembly);
10+
}
11+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
CREATE TABLE [LogsMSSqlServerBatch] (
2+
3+
[Id] int IDENTITY(1,1) NOT NULL,
4+
[Message] nvarchar(max) NULL,
5+
[MessageTemplate] nvarchar(max) NULL,
6+
[Level] nvarchar(max) NULL,
7+
[TimeStamp] datetime NULL,
8+
[Exception] nvarchar(max) NULL,
9+
[Properties] nvarchar(max) NULL
10+
11+
CONSTRAINT [PK_LogsMSSqlServerBatch] PRIMARY KEY CLUSTERED ([Id] ASC)
12+
);
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
CREATE TABLE [dbo].[LogsSqlServerBatch]
2+
(
3+
[Id] INT IDENTITY(1,1) NOT NULL,
4+
[Timestamp] DATETIMEOFFSET NOT NULL,
5+
[Level] NVARCHAR(50) NOT NULL,
6+
[Message] NVARCHAR(MAX) NULL,
7+
[TraceId] NVARCHAR(100) NULL,
8+
[SpanId] NVARCHAR(100) NULL,
9+
[Exception] NVARCHAR(MAX) NULL,
10+
[Properties] NVARCHAR(MAX) NULL,
11+
CONSTRAINT [PK_LogsSqlServerBatch] PRIMARY KEY CLUSTERED ([Id] ASC)
12+
);
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net9.0</TargetFramework>
5+
<OutputType>Exe</OutputType>
6+
</PropertyGroup>
7+
8+
<ItemGroup>
9+
<PackageReference Include="BenchmarkDotNet" />
10+
<PackageReference Include="Serilog.Sinks.MSSqlServer" />
11+
</ItemGroup>
12+
13+
<ItemGroup>
14+
<ProjectReference Include="..\..\src\Serilog.Sinks.SqlServer\Serilog.Sinks.SqlServer.csproj" />
15+
</ItemGroup>
16+
17+
</Project>
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Threading.Tasks;
4+
5+
using BenchmarkDotNet.Attributes;
6+
using BenchmarkDotNet.Columns;
7+
using BenchmarkDotNet.Order;
8+
9+
using Serilog.Events;
10+
11+
namespace Serilog.Sinks.SqlServer.Benchmark;
12+
13+
[MemoryDiagnoser]
14+
[RankColumn]
15+
[Orderer(SummaryOrderPolicy.FastestToSlowest)]
16+
[HideColumns(Column.Error, Column.StdDev)]
17+
public class SinkBenchmark
18+
{
19+
private const string SqlConnectionString = "Data Source=(local);Initial Catalog=SerilogBenchmark;Integrated Security=True;TrustServerCertificate=True;";
20+
private const int BatchSize = 100;
21+
22+
private MSSqlServer.MSSqlServerSink _msSqlServerSink;
23+
private SqlServerSink _sqlServerSink;
24+
25+
private LogEvent[] _logEvents;
26+
27+
[GlobalSetup]
28+
public void Setup()
29+
{
30+
// Create a sample exception to include in log events
31+
var exception = new InvalidOperationException("Benchmark exception");
32+
33+
// Create log events to batch
34+
_logEvents = new LogEvent[BatchSize];
35+
for (int i = 0; i < BatchSize; i++)
36+
{
37+
var template = new Parsing.MessageTemplateParser()
38+
.Parse("Batch message {Counter} user {UserId} action {Action}");
39+
40+
var properties = new List<LogEventProperty>
41+
{
42+
new("Counter", new ScalarValue(i)),
43+
new("UserId", new ScalarValue($"user{i}")),
44+
new("Action", new ScalarValue("BatchTest")),
45+
new("SourceContext", new ScalarValue("Serilog.Sinks.SqlServer.Benchmark.SinkBenchmark"))
46+
};
47+
48+
_logEvents[i] = new LogEvent(
49+
DateTimeOffset.UtcNow,
50+
LogEventLevel.Information,
51+
exception,
52+
template,
53+
properties);
54+
}
55+
56+
57+
// Serilog.Sinks.MSSqlServer
58+
_msSqlServerSink = new MSSqlServer.MSSqlServerSink(
59+
connectionString: SqlConnectionString,
60+
sinkOptions: new MSSqlServer.MSSqlServerSinkOptions
61+
{
62+
TableName = "LogsMSSqlServerBatch",
63+
AutoCreateSqlTable = false,
64+
BatchPostingLimit = BatchSize,
65+
BatchPeriod = TimeSpan.FromSeconds(2),
66+
UseSqlBulkCopy = true
67+
});
68+
69+
// Serilog.Sinks.SqlServer
70+
var options = new SqlServerSinkOptions
71+
{
72+
ConnectionString = SqlConnectionString,
73+
TableName = "LogsSqlServerBatch",
74+
BatchSizeLimit = BatchSize,
75+
BufferingTimeLimit = TimeSpan.FromSeconds(2)
76+
};
77+
78+
// Exclude SourceContext column for fair comparison
79+
options.Mappings.RemoveAll(m => m.ColumnName == "SourceContext");
80+
81+
_sqlServerSink = new SqlServerSink(options);
82+
}
83+
84+
[GlobalCleanup]
85+
public void Cleanup()
86+
{
87+
(_msSqlServerSink as IDisposable)?.Dispose();
88+
(_sqlServerSink as IDisposable)?.Dispose();
89+
}
90+
91+
92+
[Benchmark]
93+
public async Task MSSqlServerSink()
94+
{
95+
await _msSqlServerSink.EmitBatchAsync(_logEvents);
96+
}
97+
98+
[Benchmark]
99+
public async Task SqlServerSink()
100+
{
101+
await _sqlServerSink.EmitBatchAsync(_logEvents);
102+
}
103+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
dotnet run -c Release --framework net9.0

0 commit comments

Comments
 (0)