Skip to content

Commit ba1e0c2

Browse files
authored
Merge pull request #5465 from tui-cs/copilot/fix-memory-leak
Bound UICatalog in-memory log capture to stop long-run memory growth
2 parents 0c8f194 + c579374 commit ba1e0c2

2 files changed

Lines changed: 104 additions & 0 deletions

File tree

Examples/UICatalog/ScenarioLogCapture.cs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ namespace UICatalog;
1010
/// </summary>
1111
public class ScenarioLogCapture : ILoggerProvider
1212
{
13+
private const int MaxBufferChars = 256_000;
14+
private const int TrimTargetChars = 192_000;
15+
1316
private readonly object _lock = new ();
1417
private readonly StringBuilder _buffer = new ();
1518

@@ -113,6 +116,7 @@ internal void Log (LogLevel logLevel, string message)
113116
lock (_lock)
114117
{
115118
_buffer.AppendLine ($"[{logLevel}] {message}");
119+
TrimIfNeeded ();
116120

117121
if (logLevel >= LogLevel.Error)
118122
{
@@ -121,6 +125,40 @@ internal void Log (LogLevel logLevel, string message)
121125
}
122126
}
123127

128+
private void TrimIfNeeded ()
129+
{
130+
if (_buffer.Length <= MaxBufferChars)
131+
{
132+
return;
133+
}
134+
135+
var removeCount = _buffer.Length - TrimTargetChars;
136+
137+
if (removeCount <= 0)
138+
{
139+
return;
140+
}
141+
142+
int nextLineBreak = -1;
143+
144+
for (int i = removeCount; i < _buffer.Length; i++)
145+
{
146+
if (_buffer [i] == '\n')
147+
{
148+
nextLineBreak = i;
149+
break;
150+
}
151+
}
152+
153+
if (nextLineBreak >= 0)
154+
{
155+
removeCount = nextLineBreak + 1;
156+
}
157+
158+
_buffer.Remove (0, removeCount);
159+
_scenarioStartPosition = Math.Max (0, _scenarioStartPosition - removeCount);
160+
}
161+
124162
/// <inheritdoc />
125163
public void Dispose ()
126164
{
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
using Microsoft.Extensions.Logging;
2+
using UICatalog;
3+
4+
namespace IntegrationTests;
5+
6+
public class ScenarioLogCaptureTests
7+
{
8+
[Fact]
9+
public void LogBuffer_IsTrimmed_WhenItGrowsTooLarge ()
10+
{
11+
ScenarioLogCapture capture = new ();
12+
ILogger logger = capture.CreateLogger ("test");
13+
string message = new ('x', 4096);
14+
15+
for (var i = 0; i < 120; i++)
16+
{
17+
logger.LogInformation ("{Message}", message);
18+
}
19+
20+
Assert.True (capture.GetAllLogs ().Length <= 256_000);
21+
}
22+
23+
[Fact]
24+
public void GetScenarioLogs_RespectsScenarioStart_AfterTrim ()
25+
{
26+
ScenarioLogCapture capture = new ();
27+
ILogger logger = capture.CreateLogger ("test");
28+
string message = new ('y', 4096);
29+
30+
// Fill close to the cap so the next few entries will trigger a trim.
31+
while (capture.GetAllLogs ().Length < 250_000)
32+
{
33+
logger.LogInformation ("{Message}", message);
34+
}
35+
36+
logger.LogInformation ("before-start");
37+
38+
capture.MarkScenarioStart ();
39+
40+
int lengthBefore = capture.GetAllLogs ().Length;
41+
bool trimmed = false;
42+
43+
for (int i = 0; i < 50; i++)
44+
{
45+
logger.LogInformation ("{Message}", message);
46+
47+
int lengthAfter = capture.GetAllLogs ().Length;
48+
49+
if (lengthAfter < lengthBefore)
50+
{
51+
trimmed = true;
52+
break;
53+
}
54+
55+
lengthBefore = lengthAfter;
56+
}
57+
58+
Assert.True (trimmed);
59+
60+
logger.LogInformation ("after-start");
61+
62+
string logs = capture.GetScenarioLogs ();
63+
Assert.DoesNotContain ("before-start", logs);
64+
Assert.Contains ("after-start", logs);
65+
}
66+
}

0 commit comments

Comments
 (0)