Skip to content
This repository was archived by the owner on Jan 23, 2023. It is now read-only.

Commit d38496f

Browse files
authored
Add basic ArrayPool cleaning tests (#29043)
* Add basic ArrayPool cleaning tests * Move the event tests to a separate process Want to disable collection when testing the existing events so that we don't get GC callback events. * Add a test to check the polling event * Rebase and other tweaks * Move tests to outer loop * Fix UseShellEx property set * Move test to stress mode
1 parent 21db3f9 commit d38496f

File tree

4 files changed

+323
-57
lines changed

4 files changed

+323
-57
lines changed
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System.Diagnostics;
6+
using System.Diagnostics.Tracing;
7+
using System.Threading;
8+
9+
namespace System.Buffers.ArrayPool.Tests
10+
{
11+
public abstract class ArrayPoolTest : RemoteExecutorTestBase
12+
{
13+
protected const string TrimSwitchName = "DOTNET_SYSTEM_BUFFERS_ARRAYPOOL_TRIMSHARED";
14+
15+
protected static class EventIds
16+
{
17+
public const int BufferRented = 1;
18+
public const int BufferAllocated = 2;
19+
public const int BufferReturned = 3;
20+
public const int BufferTrimmed = 4;
21+
public const int BufferTrimPoll = 5;
22+
}
23+
24+
protected static int RunWithListener(Action body, EventLevel level, Action<EventWrittenEventArgs> callback)
25+
{
26+
using (TestEventListener listener = new TestEventListener("System.Buffers.ArrayPoolEventSource", level))
27+
{
28+
int count = 0;
29+
listener.RunWithCallback(e =>
30+
{
31+
Interlocked.Increment(ref count);
32+
callback(e);
33+
}, body);
34+
return count;
35+
}
36+
}
37+
38+
protected static void RemoteInvokeWithTrimming(Action action, bool trim = false)
39+
{
40+
RemoteInvokeOptions options = new RemoteInvokeOptions();
41+
options.StartInfo.UseShellExecute = false;
42+
options.StartInfo.EnvironmentVariables.Add(TrimSwitchName, trim.ToString());
43+
44+
RemoteInvoke(action).Dispose();
45+
}
46+
47+
protected static void RemoteInvokeWithTrimming(Func<string, int> method, bool trim = false, int timeout = FailWaitTimeoutMilliseconds)
48+
{
49+
RemoteInvokeOptions options = new RemoteInvokeOptions
50+
{
51+
TimeOut = timeout
52+
};
53+
54+
options.StartInfo.UseShellExecute = false;
55+
options.StartInfo.EnvironmentVariables.Add(TrimSwitchName, trim.ToString());
56+
57+
RemoteInvoke(method, trim.ToString(), options).Dispose();
58+
}
59+
}
60+
}
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System.Collections.Generic;
6+
using System.Diagnostics.Tracing;
7+
using System.Linq;
8+
using System.Reflection;
9+
using System.Runtime.InteropServices;
10+
using System.Threading;
11+
using Xunit;
12+
13+
namespace System.Buffers.ArrayPool.Tests
14+
{
15+
[SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework)]
16+
public class CollectionTests : ArrayPoolTest
17+
{
18+
[OuterLoop("This is a long running test (over 2 minutes)")]
19+
[Theory,
20+
InlineData(true),
21+
InlineData(false)]
22+
public void BuffersAreCollectedWhenStale(bool trim)
23+
{
24+
RemoteInvokeWithTrimming((trimString) =>
25+
{
26+
// Check that our environment is as we expect
27+
Assert.Equal(trimString, Environment.GetEnvironmentVariable(TrimSwitchName));
28+
29+
const int BufferCount = 8;
30+
const int BufferSize = 1025;
31+
32+
// Get the pool and check our trim setting
33+
var pool = ArrayPool<int>.Shared;
34+
bool parsedTrim = ValidateTrimState(pool, trimString);
35+
36+
List<int[]> rentedBuffers = new List<int[]>();
37+
38+
// Rent and return a set of buffers
39+
for (int i = 0; i < BufferCount; i++)
40+
{
41+
rentedBuffers.Add(pool.Rent(BufferSize));
42+
}
43+
for (int i = 0; i < BufferCount; i++)
44+
{
45+
pool.Return(rentedBuffers[i]);
46+
}
47+
48+
// Rent what we returned and ensure they are the same
49+
for (int i = 0; i < BufferCount; i++)
50+
{
51+
var buffer = pool.Rent(BufferSize);
52+
Assert.Contains(rentedBuffers, item => ReferenceEquals(item, buffer));
53+
}
54+
for (int i = 0; i < BufferCount; i++)
55+
{
56+
pool.Return(rentedBuffers[i]);
57+
}
58+
59+
// Now wait a little over a minute and force a GC to get some buffers returned
60+
Console.WriteLine("Waiting a minute for buffers to go stale...");
61+
Thread.Sleep(61 * 1000);
62+
GC.Collect(2);
63+
GC.WaitForPendingFinalizers();
64+
bool foundNewBuffer = false;
65+
for (int i = 0; i < BufferCount; i++)
66+
{
67+
var buffer = pool.Rent(BufferSize);
68+
if (!rentedBuffers.Any(item => ReferenceEquals(item, buffer)))
69+
{
70+
foundNewBuffer = true;
71+
}
72+
}
73+
74+
// Should only have found a new buffer if we're trimming
75+
Assert.Equal(parsedTrim, foundNewBuffer);
76+
return SuccessExitCode;
77+
}, trim, 3 * 60 * 1000); // This test has to wait for the buffers to go stale (give it three minutes)
78+
}
79+
80+
// This test can cause problems for other tests run in parallel (from other assemblies) as
81+
// it pushes the physical memory usage above 80% temporarily.
82+
[ConditionalTheory(typeof(TestEnvironment), nameof(TestEnvironment.IsStressModeEnabled)),
83+
InlineData(true),
84+
InlineData(false)]
85+
public unsafe void ThreadLocalIsCollectedUnderHighPressure(bool trim)
86+
{
87+
RemoteInvokeWithTrimming((trimString) =>
88+
{
89+
// Check that our environment is as we expect
90+
Assert.Equal(trimString, Environment.GetEnvironmentVariable(TrimSwitchName));
91+
92+
// Get the pool and check our trim setting
93+
var pool = ArrayPool<byte>.Shared;
94+
bool parsedTrim = ValidateTrimState(pool, trimString);
95+
96+
// Create our buffer, return it, re-rent it and ensure we have the same one
97+
const int BufferSize = 4097;
98+
var buffer = pool.Rent(BufferSize);
99+
pool.Return(buffer);
100+
Assert.Same(buffer, pool.Rent(BufferSize));
101+
102+
// Return it and put memory pressure on to get it cleared
103+
pool.Return(buffer);
104+
105+
const int AllocSize = 1024 * 1024 * 64;
106+
int PageSize = Environment.SystemPageSize;
107+
var pressureMethod = pool.GetType().GetMethod("GetMemoryPressure", BindingFlags.Static | BindingFlags.NonPublic);
108+
do
109+
{
110+
Span<byte> native = new Span<byte>(Marshal.AllocHGlobal(AllocSize).ToPointer(), AllocSize);
111+
112+
// Touch the pages to bring them into physical memory
113+
for (int i = 0; i < native.Length; i += PageSize)
114+
{
115+
native[i] = 0xEF;
116+
}
117+
118+
GC.Collect(2);
119+
} while ((int)pressureMethod.Invoke(null, null) != 2);
120+
121+
GC.WaitForPendingFinalizers();
122+
if (parsedTrim)
123+
{
124+
// Should have a new buffer now
125+
Assert.NotSame(buffer, pool.Rent(BufferSize));
126+
}
127+
else
128+
{
129+
// Disabled, should not have trimmed buffer
130+
Assert.Same(buffer, pool.Rent(BufferSize));
131+
}
132+
133+
return SuccessExitCode;
134+
}, trim);
135+
}
136+
137+
private static bool ValidateTrimState(object pool, string trimString)
138+
{
139+
Assert.StartsWith("TlsOverPerCoreLockedStacksArrayPool", pool.GetType().Name);
140+
bool parsedTrim = bool.Parse(trimString);
141+
var trimField = pool.GetType().GetField("s_trimBuffers", BindingFlags.Static | BindingFlags.NonPublic);
142+
Assert.Equal(parsedTrim, (bool)trimField.GetValue(null));
143+
return parsedTrim;
144+
}
145+
146+
[Theory,
147+
InlineData(true),
148+
InlineData(false)]
149+
public void PollingEventFires(bool trim)
150+
{
151+
RemoteInvokeWithTrimming((trimString) =>
152+
{
153+
var pool = ArrayPool<float>.Shared;
154+
bool parsedTrim = ValidateTrimState(pool, trimString);
155+
bool pollEventFired = false;
156+
var buffer = pool.Rent(10);
157+
158+
// Polling doesn't start until the thread locals are created for a pool.
159+
// Try before the return then after.
160+
161+
RunWithListener(() =>
162+
{
163+
GC.Collect(2);
164+
GC.WaitForPendingFinalizers();
165+
},
166+
EventLevel.Informational,
167+
e =>
168+
{
169+
if (e.EventId == EventIds.BufferTrimPoll)
170+
pollEventFired = true;
171+
});
172+
173+
Assert.False(pollEventFired, "collection isn't hooked up until the first item is returned");
174+
pool.Return(buffer);
175+
176+
RunWithListener(() =>
177+
{
178+
GC.Collect(2);
179+
GC.WaitForPendingFinalizers();
180+
},
181+
EventLevel.Informational,
182+
e =>
183+
{
184+
if (e.EventId == EventIds.BufferTrimPoll)
185+
pollEventFired = true;
186+
});
187+
188+
// Polling events should only fire when trimming is enabled
189+
Assert.Equal(parsedTrim, pollEventFired);
190+
return SuccessExitCode;
191+
}, trim);
192+
}
193+
}
194+
}

0 commit comments

Comments
 (0)