Skip to content

Commit

Permalink
Improved performance by eliminating string allocation for logs
Browse files Browse the repository at this point in the history
  • Loading branch information
zbalkan committed Oct 3, 2024
1 parent 6557651 commit 95cd29b
Show file tree
Hide file tree
Showing 7 changed files with 245 additions and 77 deletions.
52 changes: 26 additions & 26 deletions Apps/LogExporterApp/App.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,20 +41,20 @@ public sealed class App : IDnsApplication, IDnsQueryLogger

private const int QUEUE_TIMER_INTERVAL = 10000;

private readonly ExportManager _exportManager = new ExportManager();
private readonly IReadOnlyList<DnsLogEntry> _emptyList = [];

private BlockingCollection<LogEntry> _logBuffer;
private readonly ExportManager _exportManager = new ExportManager();

private BufferManagementConfig? _config;

private IDnsServer _dnsServer;

private BlockingCollection<LogEntry> _logBuffer;

private Timer _queueTimer;

private bool disposedValue;

private readonly IReadOnlyList<DnsLogEntry> _emptyList = [];

#endregion variables

#region constructor
Expand Down Expand Up @@ -117,29 +117,8 @@ public Task InitializeAsync(IDnsServer dnsServer, string config)
RegisterExportTargets();
if (_exportManager.HasStrategy())
{
_queueTimer = new Timer(async (object _) =>
{
try
{
await ExportLogsAsync();
}
catch (Exception ex)
{
_dnsServer?.WriteLog(ex);
}
finally
{
try
{
_queueTimer.Change(QUEUE_TIMER_INTERVAL, Timeout.Infinite);
}
catch (ObjectDisposedException)
{ }
}
}, null, QUEUE_TIMER_INTERVAL, Timeout.Infinite);

_queueTimer = new Timer(HandleExportLogCallback, state: null, QUEUE_TIMER_INTERVAL, Timeout.Infinite);
}

return Task.CompletedTask;
}

Expand Down Expand Up @@ -176,6 +155,27 @@ private async Task ExportLogsAsync()
}
}

private async void HandleExportLogCallback(object? state)
{
try
{
await ExportLogsAsync().ConfigureAwait(false);
}
catch (Exception ex)
{
_dnsServer?.WriteLog(ex);
}
finally
{
try
{
_queueTimer.Change(QUEUE_TIMER_INTERVAL, Timeout.Infinite);
}
catch (ObjectDisposedException)
{ }
}
}

private void RegisterExportTargets()
{
// Helper function to register an export strategy if the target is enabled
Expand Down
132 changes: 132 additions & 0 deletions Apps/LogExporterApp/GrowableBuffer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
using System;
using System.Buffers;

namespace LogExporter
{
public class GrowableBuffer<T> : IBufferWriter<T>, IDisposable
{
// Gets the current length of the buffer contents
public int Length => _position;

// Initial capacity to be used in the constructor
private const int DefaultInitialCapacity = 256;

private Memory<T> _buffer;

private int _position;

private bool disposedValue;

public GrowableBuffer(int initialCapacity = DefaultInitialCapacity)
{
_buffer = new Memory<T>(ArrayPool<T>.Shared.Rent(initialCapacity));
_position = 0;
}

// IBufferWriter<T> implementation
public void Advance(int count)
{
if (count < 0 || _position + count > _buffer.Length)
throw new ArgumentOutOfRangeException(nameof(count));

_position += count;
}

// Appends a single element to the buffer
public void Append(T item)
{
EnsureCapacity(1);
_buffer.Span[_position++] = item;
}

// Appends a span of elements to the buffer
public void Append(ReadOnlySpan<T> span)
{
EnsureCapacity(span.Length);
span.CopyTo(_buffer.Span[_position..]);
_position += span.Length;
}

// Clears the buffer for reuse without reallocating
public void Clear() => _position = 0;

public Memory<T> GetMemory(int sizeHint = 0)
{
EnsureCapacity(sizeHint);
return _buffer[_position..];
}

public Span<T> GetSpan(int sizeHint = 0)
{
EnsureCapacity(sizeHint);
return _buffer.Span[_position..];
}

// Returns the buffer contents as an array
public T[] ToArray()
{
T[] result = new T[_position];
_buffer.Span[.._position].CopyTo(result);
return result;
}

// Returns the buffer contents as a ReadOnlySpan<T>
public ReadOnlySpan<T> ToSpan() => _buffer.Span[.._position];

public override string ToString() => _buffer.Span[.._position].ToString();

// Ensures the buffer has enough capacity to add more elements
private void EnsureCapacity(int additionalCapacity)
{
if (_position + additionalCapacity > _buffer.Length)
{
GrowBuffer(_position + additionalCapacity);
}
}

// Grows the buffer to accommodate the required capacity
private void GrowBuffer(int requiredCapacity)
{
int newCapacity = Math.Max(_buffer.Length * 2, requiredCapacity);

// Rent a larger buffer from the pool
T[] newArray = ArrayPool<T>.Shared.Rent(newCapacity);
Memory<T> newBuffer = new Memory<T>(newArray);

// Copy current contents to the new buffer
_buffer.Span[.._position].CopyTo(newBuffer.Span);

// Return old buffer to the pool
ArrayPool<T>.Shared.Return(_buffer.ToArray());

// Assign the new buffer
_buffer = newBuffer;
}

#region IDisposable

public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: true);
GC.SuppressFinalize(this);
}

protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing)
{
ArrayPool<T>.Shared.Return(_buffer.ToArray());
_buffer = Memory<T>.Empty;
_position = 0;
}
}

disposedValue = true;
}

#endregion IDisposable
}
}
103 changes: 68 additions & 35 deletions Apps/LogExporterApp/LogEntry.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text.Json.Serialization;
using System.Text;
using System.Text.Json;
using TechnitiumLibrary.Net.Dns;
using TechnitiumLibrary.Net.Dns.ResourceRecords;
using System.Collections.Generic;

namespace LogExporter
{
Expand Down Expand Up @@ -43,7 +43,7 @@ public LogEntry(DateTime timestamp, IPEndPoint remoteEP, DnsTransportProtocol pr
QuestionName = questionRecord.Name,
QuestionType = questionRecord.Type,
QuestionClass = questionRecord.Class,
Size = questionRecord.UncompressedLength
Size = questionRecord.UncompressedLength,
}));
}

Expand All @@ -58,7 +58,7 @@ public LogEntry(DateTime timestamp, IPEndPoint remoteEP, DnsTransportProtocol pr
RecordClass = record.Class,
RecordTtl = record.TTL,
Size = record.UncompressedLength,
DnssecStatus = record.DnssecStatus
DnssecStatus = record.DnssecStatus,
}));
}

Expand All @@ -73,11 +73,6 @@ public LogEntry(DateTime timestamp, IPEndPoint remoteEP, DnsTransportProtocol pr
}
}

public override string ToString()
{
return JsonSerializer.Serialize(this, DnsLogSerializerOptions.Default);
}

public class Question
{
public string QuestionName { get; set; }
Expand All @@ -95,36 +90,74 @@ public class Answer
public int Size { get; set; }
public DnssecStatus DnssecStatus { get; set; }
}
}

// Custom DateTime converter to handle UTC serialization in ISO 8601 format
public class JsonDateTimeConverter : JsonConverter<DateTime>
{
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
public ReadOnlySpan<char> AsSpan()
{
var dts = reader.GetString();
return dts == null ? DateTime.MinValue : DateTime.Parse(dts);
}
// Initialize a ValueStringBuilder with some initial capacity
var buffer = new GrowableBuffer<byte>(256);

public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ"));
using var writer = new Utf8JsonWriter(buffer);

// Manually serialize the LogEntry as JSON
writer.WriteStartObject();

writer.WriteString("timestamp", Timestamp.ToUniversalTime().ToString("O"));
writer.WriteString("clientIp", ClientIp);
writer.WriteNumber("clientPort", ClientPort);
writer.WriteBoolean("dnssecOk", DnssecOk);
writer.WriteString("protocol", Protocol.ToString());
writer.WriteString("responseCode", ResponseCode.ToString());

// Write Questions array
writer.WriteStartArray("questions");
foreach (var question in Questions)
{
writer.WriteStartObject();
writer.WriteString("questionName", question.QuestionName);
writer.WriteString("questionType", question.QuestionType.ToString());
writer.WriteString("questionClass", question.QuestionClass.ToString());
writer.WriteNumber("size", question.Size);
writer.WriteEndObject();
}
writer.WriteEndArray();

// Write Answers array (if exists)
if (Answers != null && Answers.Count > 0)
{
writer.WriteStartArray("answers");
foreach (var answer in Answers)
{
writer.WriteStartObject();
writer.WriteString("recordType", answer.RecordType.ToString());
writer.WriteString("recordData", answer.RecordData);
writer.WriteString("recordClass", answer.RecordClass.ToString());
writer.WriteNumber("recordTtl", answer.RecordTtl);
writer.WriteNumber("size", answer.Size);
writer.WriteString("dnssecStatus", answer.DnssecStatus.ToString());
writer.WriteEndObject();
}
writer.WriteEndArray();
}

writer.WriteEndObject();
writer.Flush();

return ConvertBytesToChars(buffer.ToSpan());
}
}

// Setup reusable options with a single instance
public static class DnsLogSerializerOptions
{
public static readonly JsonSerializerOptions Default = new JsonSerializerOptions
public static Span<char> ConvertBytesToChars(ReadOnlySpan<byte> byteSpan)
{
WriteIndented = false, // Newline delimited logs should not be multiline
PropertyNamingPolicy = JsonNamingPolicy.CamelCase, // Convert properties to camelCase
Converters = { new JsonStringEnumConverter(), new JsonDateTimeConverter() }, // Handle enums and DateTime conversion
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping, // For safe encoding
NumberHandling = JsonNumberHandling.Strict,
AllowTrailingCommas = true, // Allow trailing commas in JSON
DictionaryKeyPolicy = JsonNamingPolicy.CamelCase, // Convert dictionary keys to camelCase
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull // Ignore null values
};
}
// Calculate the maximum required length for the char array
int maxCharCount = Encoding.UTF8.GetCharCount(byteSpan);

// Allocate a char array large enough to hold the converted characters
char[] charArray = new char[maxCharCount];

// Decode the byteSpan into the char array
int actualCharCount = Encoding.UTF8.GetChars(byteSpan, charArray);

// Return a span of only the relevant portion of the char array
return new Span<char>(charArray, 0, actualCharCount);
}
};
}
2 changes: 1 addition & 1 deletion Apps/LogExporterApp/Strategy/ExportManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public async Task ImplementStrategyForAsync(List<LogEntry> logs)
{
foreach (var strategy in _exportStrategies.Values)
{
await strategy.ExportAsync(logs);
await strategy.ExportAsync(logs).ConfigureAwait(false);
}
}

Expand Down
Loading

0 comments on commit 95cd29b

Please sign in to comment.