Skip to content

Commit 23cab11

Browse files
authored
Merge pull request #35 from vonhoff/34-freezing-under-heavy-load
Refactor RichTextBoxSink and formatting classes to improve performance
2 parents 67b6e61 + a24bff9 commit 23cab11

File tree

6 files changed

+55
-59
lines changed

6 files changed

+55
-59
lines changed

CHANGES.md

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
1-
## Release Notes - Serilog.Sinks.RichTextBox.WinForms.Colored v3.1.2
1+
## Release Notes - Serilog.Sinks.RichTextBox.WinForms.Colored v3.1.3
22

33
### What Changed
44

5-
- Fixed a bug where cross-thread log messages during form construction caused `System.InvalidOperationException` due to premature access of the RichTextBox.
6-
7-
### Contributors
8-
9-
- [Steffen S. Hellestøl](https://github.com/SteffenSH)
5+
- Fixed performance issues when logging large numbers of complex messages that could cause application freezing.
6+
- Fixed `ArgumentOutOfRangeException` thrown from `RtfBuilder.Clear()`
107

118
### Resources
129

Serilog.Sinks.RichTextBox.WinForms.Colored/Serilog.Sinks.RichTextBox.WinForms.Colored.csproj

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,10 @@
2222
<TargetFrameworks>net462;net471;net6.0-windows;net8.0-windows;net9.0-windows;netcoreapp3.0-windows;netcoreapp3.1-windows</TargetFrameworks>
2323
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
2424
<UseWindowsForms>true</UseWindowsForms>
25-
<Version>3.1.2</Version>
25+
<Version>3.1.3</Version>
2626
<PackageReleaseNotes>
27-
- Fixed a bug where cross-thread log messages during form construction caused `System.InvalidOperationException` due to premature access of the RichTextBox.
27+
- Fixed performance issues when logging large numbers of complex messages that could cause application freezing.
28+
- Fixed `ArgumentOutOfRangeException` thrown from `RtfBuilder.Clear()`
2829

2930
See repository for more information:
3031
https://github.com/vonhoff/Serilog.Sinks.RichTextBox.WinForms.Colored

Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Formatting/DisplayValueFormatter.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public class DisplayValueFormatter : ValueFormatter
3030
{
3131
private readonly IFormatProvider? _formatProvider;
3232
private readonly StringBuilder _scalarBuilder = new();
33-
private readonly StringBuilder _stringBuilder = new();
33+
private readonly StringBuilder _literalBuilder = new(64);
3434
private JsonValueFormatter? _jsonValueFormatter;
3535

3636
public DisplayValueFormatter(Theme theme, IFormatProvider? formatProvider) : base(theme, formatProvider)
@@ -53,9 +53,9 @@ private void FormatLiteralValue(ScalarValue scalar, IRtfCanvas canvas, string? f
5353
return;
5454

5555
case byte[] bytes:
56-
_stringBuilder.Clear();
57-
_stringBuilder.Append('"').Append(Convert.ToBase64String(bytes)).Append('"');
58-
Theme.Render(canvas, StyleToken.String, _stringBuilder.ToString());
56+
_literalBuilder.Clear();
57+
_literalBuilder.Append('"').Append(Convert.ToBase64String(bytes)).Append('"');
58+
Theme.Render(canvas, StyleToken.String, _literalBuilder.ToString());
5959
return;
6060

6161
case bool b:
@@ -90,9 +90,9 @@ private void RenderString(string text, IRtfCanvas canvas, string? format, bool i
9090
}
9191
else
9292
{
93-
_stringBuilder.Clear();
94-
_stringBuilder.Append('"').Append(text.Replace("\"", "\\\"")).Append('"');
95-
Theme.Render(canvas, StyleToken.String, _stringBuilder.ToString());
93+
_literalBuilder.Clear();
94+
_literalBuilder.Append('"').Append(text.Replace("\"", "\\\"")).Append('"');
95+
Theme.Render(canvas, StyleToken.String, _literalBuilder.ToString());
9696
}
9797
}
9898

Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Formatting/JsonValueFormatter.cs

Lines changed: 13 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -218,65 +218,55 @@ private void FormatLiteralValue(ScalarValue scalar, IRtfCanvas canvas)
218218
}
219219
}
220220

221-
private static void WriteQuotedJsonString(string str, TextWriter output)
221+
private static void WriteQuotedJsonString(StringBuilder builder, string str)
222222
{
223-
output.Write('\"');
223+
builder.Append('\"');
224224

225225
foreach (var c in str)
226226
{
227227
switch (c)
228228
{
229229
case '"':
230-
output.Write("\\\"");
230+
builder.Append("\\\"");
231231
break;
232232

233233
case '\\':
234-
output.Write(@"\\");
234+
builder.Append(@"\\");
235235
break;
236236

237237
case '\n':
238-
output.Write("\\n");
238+
builder.Append("\\n");
239239
break;
240240

241241
case '\r':
242-
output.Write("\\r");
242+
builder.Append("\\r");
243243
break;
244244

245245
case '\f':
246-
output.Write("\\f");
246+
builder.Append("\\f");
247247
break;
248248

249249
case '\t':
250-
output.Write("\\t");
250+
builder.Append("\\t");
251251
break;
252252

253253
case < (char)32:
254-
output.Write("\\u");
255-
output.Write(((int)c).ToString("X4"));
254+
builder.Append("\\u");
255+
builder.Append(((int)c).ToString("X4"));
256256
break;
257257

258258
default:
259-
output.Write(c);
259+
builder.Append(c);
260260
break;
261261
}
262262
}
263-
output.Write('\"');
263+
builder.Append('\"');
264264
}
265265

266266
private string GetQuotedJsonString(string str)
267267
{
268268
_jsonStringBuilder.Clear();
269-
var estimatedCapacity = str.Length + 2;
270-
if (_jsonStringBuilder.Capacity < estimatedCapacity)
271-
{
272-
_jsonStringBuilder.Capacity = estimatedCapacity;
273-
}
274-
275-
using (var writer = new StringWriter(_jsonStringBuilder))
276-
{
277-
WriteQuotedJsonString(str, writer);
278-
}
279-
269+
WriteQuotedJsonString(_jsonStringBuilder, str);
280270
return _jsonStringBuilder.ToString();
281271
}
282272
}

Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/RichTextBoxSink.cs

Lines changed: 28 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ public class RichTextBoxSink : ILogEventSink, IDisposable
4040
private readonly CancellationTokenSource _tokenSource;
4141
private readonly Task _processingTask;
4242
private bool _disposed;
43-
private int _hasNewMessages;
4443

4544
public RichTextBoxSink(RichTextBox richTextBox, RichTextBoxSinkOptions options, ITokenRenderer? renderer = null)
4645
{
@@ -51,7 +50,6 @@ public RichTextBoxSink(RichTextBox richTextBox, RichTextBoxSinkOptions options,
5150

5251
_buffer = new ConcurrentCircularBuffer<LogEvent>(options.MaxLogLines);
5352
_signal = new AutoResetEvent(false);
54-
_hasNewMessages = 0;
5553

5654
richTextBox.Clear();
5755
richTextBox.ReadOnly = true;
@@ -78,24 +76,28 @@ public void Dispose()
7876
public void Emit(LogEvent logEvent)
7977
{
8078
_buffer.Add(logEvent);
81-
Update();
79+
_signal.Set();
8280
}
8381

8482
public void Clear()
8583
{
84+
if (_disposed)
85+
{
86+
return;
87+
}
88+
8689
_buffer.Clear();
87-
Update();
90+
_signal.Set();
8891
}
8992

9093
public void Restore()
9194
{
92-
_buffer.Restore();
93-
Update();
94-
}
95+
if (_disposed)
96+
{
97+
return;
98+
}
9599

96-
private void Update()
97-
{
98-
Interlocked.Exchange(ref _hasNewMessages, 1);
100+
_buffer.Restore();
99101
_signal.Set();
100102
}
101103

@@ -104,38 +106,43 @@ private void ProcessMessages(CancellationToken token)
104106
var builder = new RtfBuilder(_options.Theme);
105107
var snapshot = new System.Collections.Generic.List<LogEvent>(_options.MaxLogLines);
106108
var flushInterval = TimeSpan.FromMilliseconds(FlushIntervalMs);
107-
var lastFlush = DateTime.UtcNow;
109+
var lastFlush = DateTime.MinValue;
108110

109111
while (!token.IsCancellationRequested)
110112
{
111113
_signal.WaitOne();
112-
113-
if (Interlocked.CompareExchange(ref _hasNewMessages, 0, 1) != 1)
114+
if (token.IsCancellationRequested)
114115
{
115-
continue;
116+
break;
116117
}
117118

118119
var now = DateTime.UtcNow;
119-
var elapsed = now - lastFlush;
120-
if (elapsed < flushInterval)
120+
var elapsedSinceLastFlush = now - lastFlush;
121+
if (elapsedSinceLastFlush < flushInterval)
121122
{
122-
var remaining = flushInterval - elapsed;
123-
if (remaining > TimeSpan.Zero)
123+
var remainingTime = flushInterval - elapsedSinceLastFlush;
124+
if (token.WaitHandle.WaitOne(remainingTime))
124125
{
125-
Thread.Sleep(remaining);
126+
break;
126127
}
127128
}
128129

129-
Interlocked.Exchange(ref _hasNewMessages, 0);
130+
_signal.Reset();
130131
_buffer.TakeSnapshot(snapshot);
131132
builder.Clear();
132133
foreach (var evt in snapshot)
133134
{
134135
_renderer.Render(evt, builder);
135136
}
137+
138+
if (_richTextBox.IsDisposed || _richTextBox.Disposing)
139+
{
140+
continue;
141+
}
142+
136143
_richTextBox.SetRtf(builder.Rtf, _options.AutoScroll, token);
137144
lastFlush = DateTime.UtcNow;
138145
}
139146
}
140147
}
141-
}
148+
}

Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Rtf/RtfBuilder.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@ public void Dispose()
179179
public void Clear()
180180
{
181181
_body.Clear();
182+
_documentBuilder.Clear();
182183

183184
const int maxRetainedBuilderSize = 64 * 1024;
184185
if (_body.Capacity > maxRetainedBuilderSize)

0 commit comments

Comments
 (0)