diff --git a/Tests/NFUnitTestGC/NFUnitTestGC.nfproj b/Tests/NFUnitTestGC/NFUnitTestGC.nfproj new file mode 100644 index 00000000..31f73663 --- /dev/null +++ b/Tests/NFUnitTestGC/NFUnitTestGC.nfproj @@ -0,0 +1,49 @@ + + + + $(MSBuildExtensionsPath)\nanoFramework\v1.0\ + + + + + + + Debug + AnyCPU + {11A8DD76-328B-46DF-9F39-F559912D0360};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 768be3b6-41c6-4dfb-8a2d-443b2113f5ad + Library + Properties + 512 + NFUnitTestGC + NFUnitTest + False + true + UnitTest + v1.0 + + + + + + + + + + + + + ..\..\packages\nanoFramework.CoreLibrary.1.14.2\lib\mscorlib.dll + + + ..\..\packages\nanoFramework.TestFramework.2.1.85\lib\nanoFramework.TestFramework.dll + + + ..\..\packages\nanoFramework.TestFramework.2.1.85\lib\nanoFramework.UnitTestLauncher.exe + + + + + + + \ No newline at end of file diff --git a/Tests/NFUnitTestGC/Properties/AssemblyInfo.cs b/Tests/NFUnitTestGC/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..a3735af6 --- /dev/null +++ b/Tests/NFUnitTestGC/Properties/AssemblyInfo.cs @@ -0,0 +1,31 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyCopyright("Copyright (c) 2021 nanoFramework contributors")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Tests/NFUnitTestGC/TestGC.cs b/Tests/NFUnitTestGC/TestGC.cs new file mode 100644 index 00000000..0ea02bba --- /dev/null +++ b/Tests/NFUnitTestGC/TestGC.cs @@ -0,0 +1,48 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + + +using nanoFramework.TestFramework; +using System; + +namespace NFUnitTestGC +{ + [TestClass] + public class TestGC + { + [TestMethod] + public void TestGCStress() + { + int maxArraySize = 1024 * 32; + object[] arrays = new object[600]; + + OutputHelper.WriteLine("Starting TestGCStress"); + + for (int loop = 0; loop < 100; loop++) + { + OutputHelper.WriteLine($"Running iteration {loop}"); + + for (int i = 0; i < arrays.Length - 1;) + { + OutputHelper.WriteLine($"Alloc array of {maxArraySize} bytes @ pos {i}"); + arrays[i++] = new byte[maxArraySize]; ; + + OutputHelper.WriteLine($"Alloc array of 64 bytes @ pos {i}"); + arrays[i++] = new byte[64]; + } + + arrays[0] = new byte[maxArraySize]; + + for (int i = 0; i < arrays.Length; i++) + { + arrays[i] = null; + } + } + + OutputHelper.WriteLine("Completed TestGCStress"); + } + } +} diff --git a/Tests/NFUnitTestGC/TestGCWithByteArrays.cs b/Tests/NFUnitTestGC/TestGCWithByteArrays.cs new file mode 100644 index 00000000..579d7ca8 --- /dev/null +++ b/Tests/NFUnitTestGC/TestGCWithByteArrays.cs @@ -0,0 +1,89 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +using nanoFramework.TestFramework; +using System; + +namespace NFUnitTestGC +{ + [TestClass] + public class TestGCWithByteArrays + { + [TestMethod] + public void TestCompactionForNotFixedArray() + { + OutputHelper.WriteLine("Starting TestCompactionForNotFixedArray"); + + for (int loop = 0; loop < 10; loop++) + { + OutputHelper.WriteLine($"Starting iteration {loop}"); + + // First we create byte and holes that keeps some space that could be used by compaction + + // Small count so compaction does not happen + byte[] arrayOfArrays = new byte[10]; + + RunAllocations(arrayOfArrays); + + // This is the array that we expect to move in during compaction. + byte[] testNativeBuffer = new byte[100]; + + // Fill it, so it is not optimized out + Random random = new(); + var baseValue = random.Next(2); + + for (int i = 0; i < testNativeBuffer.Length; i++) + { + testNativeBuffer[i] = (byte)(i * baseValue); + } + + // trigger compaction + InitiateCompaction(); + + int index = 0; + + // Check that array content is not corrupted + foreach (var item in testNativeBuffer) + { + Assert.AreEqual(index * baseValue, item, $"Array content comparison failed at position {index}. Expecting {(index * baseValue)}, found {item}"); + index++; + } + + OutputHelper.WriteLine("No corruption detected in array"); + } + + OutputHelper.WriteLine("Completed TestCompactionForNotFixedArray"); + } + + void RunAllocations(byte[] arrObj) + { + for (int i = 1; i < arrObj.Length; i++) + { + // Creates referenced byte, which stays in memory until InitiateCompaction exits + arrObj[i] = new byte(); + + // Tries to create larger object that would be later hole . + // This object could be garbage collected on each "i" cycle. + byte[] arr = new byte[50 * i]; + + // Creates some usage for arr, so it is not optimized out. + arr[0] = 1; + arr[1] = 2; + + OutputHelper.WriteLine($"On Cycle {i:D3} Array of {arr[1]} was allocated"); + } + } + + // This method causes compaction to occur. + // It is not so trivial as it need to fragment heap with referenced byte array. + void InitiateCompaction() + { + // large count, so compaction happens during call to RunAllocations + byte[] arrayOfArrays = new byte[1500]; + RunAllocations(arrayOfArrays); + } + } +} diff --git a/Tests/NFUnitTestGC/TestGCWithDateTimeArrays.cs b/Tests/NFUnitTestGC/TestGCWithDateTimeArrays.cs new file mode 100644 index 00000000..e78cd0c5 --- /dev/null +++ b/Tests/NFUnitTestGC/TestGCWithDateTimeArrays.cs @@ -0,0 +1,214 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +using nanoFramework.TestFramework; +using System; + +namespace NFUnitTestGC +{ + [TestClass] + public class TestGCWithDateTimeArrays + { + [TestMethod] + public void TestCompactionForNotFixedDateTimeArray() + { + OutputHelper.WriteLine("Starting TestCompactionForNotFixedDateTimeArray"); + + for (int loop = 0; loop < 10; loop++) + { + OutputHelper.WriteLine($"Starting iteration {loop}"); + // First we create objects and holes that keeps some space that could be used by compaction. + // Small count so compaction does not happen. + HolderForDateTime[] arrayOfArrays = new HolderForDateTime[10]; + RunDateTimeAllocations(arrayOfArrays); + + // This is the array that we expect to move in during compaction. + HolderForDateTime[] testNativeBuffer = new HolderForDateTime[100]; + // Fill it, so it is not optimized out + for (int i = 0; i < testNativeBuffer.Length; i++) + { + testNativeBuffer[i] = new HolderForDateTime(GetRandomDateTime()); + } + + OutputHelper.WriteLine("Large HolderForDateTime array created"); + OutputHelper.WriteLine("Forcing compaction to occurr"); + + // Causes compaction + InitiateDateTimeCompaction(); + + OutputHelper.WriteLine("Compaction occurred"); + OutputHelper.WriteLine("Checking arrays for corrupted data..."); + + int index = 0; + + // Check that array content is not corrupted + foreach (HolderForDateTime holder in testNativeBuffer) + { + Assert.AreEqual(holder.StoredTicks, holder.DtValue.Ticks, $"Array content comparison failed at position {index}. Expecting {holder.StoredTicks}, found {holder.DtValue.Ticks}"); + index++; + } + + OutputHelper.WriteLine("No corruption detected in array"); + } + + OutputHelper.WriteLine("Completed TestCompactionForNotFixedArray"); + } + + // This function cause compaction to occur. + // It is not so trivial as it need to fragment heap with referenced objects. + void InitiateDateTimeCompaction() + { + // Large count, so compaction happens during RunAllocations. + HolderForDateTime[] arrayOfArrays = new HolderForDateTime[500]; + RunDateTimeAllocations(arrayOfArrays); + } + + private void RunDateTimeAllocations(HolderForDateTime[] arrObj) + { + for (int i = 1; i < arrObj.Length; i++) + { + // Creates referenced object, which stays in memory until InitiateCompaction exits + arrObj[i] = new HolderForDateTime(DateTime_btwn_1801_And_2801()); + + // Tries to create larger object that would be later hole + // This object could be garbage collected on each "i" cycle + HolderForDateTime[] arr = new HolderForDateTime[50 * i]; + + // Creates some usage for array elements, so it is not optimized out + arr[0] = new HolderForDateTime(DateTime.MinValue); + arr[1] = new HolderForDateTime(DateTime.MaxValue); + + Console.WriteLine($"On Cycle {i:D3} DateTime holder allocated"); + } + } + + private class HolderForDateTime + { + public long StoredTicks { get; } + public DateTime DtValue { get; } + + public HolderForDateTime(DateTime dt) + { + StoredTicks = dt.Ticks; + DtValue = dt; + } + } + + static int year, month, day, hour, minute, second, millisec; + static long ticks; + + static int[] leapYear = new int[] {2000, 2004, 2008, 2012, 2016, 2020, 2024, 2028, 2032, 2036, 2040, 2044, 2048, + 2052, 2056, 2060, 2064, 2068, 2072, 2076, 2080, 2084, 2088, 2092, 2096}; + + // computing our constants here, as these are not accessible + // equivalent to DateTime.TicksPerSecond + const int _TicksPerSecond = 10000 * 1000; + + private DateTime[] Get_ArrayOfRandomDateTimes() + { + OutputHelper.WriteLine(DateTime_btwn_1801_And_2801().ToString()); + OutputHelper.WriteLine(GetLeapYearDateTime().ToString()); + DateTime[] _dateTimeArr = new DateTime[] {DateTime.UtcNow, + DateTime_btwn_1801_And_2801(), DateTime_btwn_1801_And_2801(), DateTime_btwn_1801_And_2801(), + GetLeapYearDateTime(), GetLeapYearDateTime() , GetLeapYearDateTime(), + DateTime_btwn_1801_And_2801(), DateTime_btwn_1801_And_2801(), DateTime_btwn_1801_And_2801(), + GetLeapYearDateTime(), GetLeapYearDateTime(), GetLeapYearDateTime()}; + + return _dateTimeArr; + } + + private DateTime DateTime_btwn_1801_And_2801() + { + //Generates random DateTime b/n 1000 and 9000 + Random random = new Random(); + year = random.Next(999) + 1801; + month = random.Next(12) + 1; + if (month == 2 && IsLeapYear(year)) + day = random.Next(29) + 1; + else if (month == 2 && (!IsLeapYear(year))) + day = random.Next(28) + 1; + else if (((month <= 7) && ((month + 1) % 2 == 0)) || + ((month > 7) && ((month % 2) == 0))) + day = random.Next(31) + 1; + else + day = random.Next(30) + 1; + hour = random.Next(24); + minute = random.Next(60); + second = random.Next(60); + millisec = random.Next(1000); + + return new DateTime(year, month, day, hour, minute, second, millisec); + } + + private DateTime GetRandomDateTime() + { + //Generates random DateTime + Random random = new Random(); + year = random.Next(1399) + 1601; + month = random.Next(12) + 1; + + if (month == 2 && IsLeapYear(year)) + { + day = random.Next(29) + 1; + } + else if (month == 2 && (!IsLeapYear(year))) + { + day = random.Next(28) + 1; + } + else if (((month <= 7) && ((month + 1) % 2 == 0)) + || ((month > 7) && ((month % 2) == 0))) + { + day = random.Next(31) + 1; + } + else + { + day = random.Next(30) + 1; + } + + hour = random.Next(24); + minute = random.Next(60); + second = random.Next(60); + millisec = random.Next(1000); + + DateTime dt = new(year, month, day, hour, minute, second, millisec); + + // fill in random ticks value so we can have a fully filled ticks value + ticks = dt.Ticks + random.Next(1000_000); + + dt = new(ticks); + + // need to update minutesm, millisec and second because it could have changed with new ticks value + millisec = dt.Millisecond; + second = dt.Second; + minute = dt.Minute; + + return dt; + } + + private DateTime GetLeapYearDateTime() + { + Random random = new Random(); + year = leapYear[random.Next(leapYear.Length)]; + month = random.Next(12) + 1; + day = random.Next(29) + 1; + hour = random.Next(24); + minute = random.Next(60); + second = random.Next(60); + millisec = random.Next(1000); + OutputHelper.WriteLine($"{year} {month} {day} {hour} {minute} {second} {millisec}"); + return new DateTime(year, month, day, hour, minute, second, millisec); + } + + private bool IsLeapYear(int yr) + { + if ((yr % 400 == 0) || ((yr % 100 != 0) && (yr % 4 == 0))) + return true; + else + return false; + } + + } +} diff --git a/Tests/NFUnitTestGC/TestGCWithObjectArrays.cs b/Tests/NFUnitTestGC/TestGCWithObjectArrays.cs new file mode 100644 index 00000000..df0b9a3d --- /dev/null +++ b/Tests/NFUnitTestGC/TestGCWithObjectArrays.cs @@ -0,0 +1,89 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +using nanoFramework.TestFramework; +using System; + +namespace NFUnitTestGC +{ + [TestClass] + public class TestGCWithObjectArrays + { + [TestMethod] + public void TestCompactionForNotFixedArray() + { + OutputHelper.WriteLine("Starting TestCompactionForNotFixedArray"); + + for (int loop = 0; loop < 10; loop++) + { + OutputHelper.WriteLine($"Starting iteration {loop}"); + + // First we create objects and holes that keeps some space that could be used by compaction + + // Small count so compaction does not happen + object[] arrayOfArrays = new object[10]; + + RunAllocations(arrayOfArrays); + + // This is the array that we expect to move in during compaction. + int[] testNativeBuffer = new int[100]; + + // Fill it, so it is not optimized out + Random random = new Random(); + var baseValue = random.Next(); + + for (int i = 0; i < testNativeBuffer.Length; i++) + { + testNativeBuffer[i] = i * baseValue; + } + + // trigger compaction + InitiateCompaction(); + + int index = 0; + + // Check that array content is not corrupted + foreach (var item in testNativeBuffer) + { + Assert.AreEqual(index * baseValue, item, $"Array content comparison failed at position {index}. Expecting {(index * baseValue)}, found {item}"); + index++; + } + + OutputHelper.WriteLine("No corruption detected in array"); + } + + OutputHelper.WriteLine("Completed TestCompactionForNotFixedArray"); + } + + void RunAllocations(object[] arrObj) + { + for (int i = 1; i < arrObj.Length; i++) + { + // Creates referenced interger object, which stays in memory until InitiateCompaction exits + arrObj[i] = new int(); + + // Tries to create larger object that would be later hole . + // This object could be garbage collected on each "i" cycle. + int[] arr = new int[50 * i]; + + // Creates some usage for arr, so it is not optimized out. + arr[0] = i; + arr[1] = 50 * i; + + OutputHelper.WriteLine($"On Cycle {i:D3} Array of {arr[1]} was allocated"); + } + } + + // This method causes compaction to occur. + // It is not so trivial as it need to fragment heap with referenced objects. + void InitiateCompaction() + { + // large count, so compaction happens during call to RunAllocations + object[] arrayOfArrays = new object[1500]; + RunAllocations(arrayOfArrays); + } + } +} diff --git a/Tests/NFUnitTestGC/TestGCWithTimeSpanArrays.cs b/Tests/NFUnitTestGC/TestGCWithTimeSpanArrays.cs new file mode 100644 index 00000000..1294deda --- /dev/null +++ b/Tests/NFUnitTestGC/TestGCWithTimeSpanArrays.cs @@ -0,0 +1,109 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +using nanoFramework.TestFramework; +using System; + +namespace NFUnitTestGC +{ + [TestClass] + public class TestGCWithTimeSpanArrays + { + [TestMethod] + public void TestCompactionForNotFixedTimeSpanArray() + { + OutputHelper.WriteLine("Starting TestCompactionForNotFixedTimeSpanArray"); + + for (int loop = 0; loop < 10; loop++) + { + OutputHelper.WriteLine($"Starting iteration {loop}"); + + // First we create objects and holes that keeps some space that could be used by compaction. + // Small count so compaction does not happen. + HolderForTimeSpan[] arrayOfArrays = new HolderForTimeSpan[10]; + RunTimeSpanAllocations(arrayOfArrays); + + // This is the array that we expect to move in during compaction. + HolderForTimeSpan[] testNativeBuffer = new HolderForTimeSpan[100]; + // Fill it, so it is not optimized out + for (int i = 0; i < testNativeBuffer.Length; i++) + { + testNativeBuffer[i] = new HolderForTimeSpan(GetRandomTimeSpan()); + } + + OutputHelper.WriteLine("Large HolderForTimeSpan array created"); + OutputHelper.WriteLine("Forcing compaction to occurr"); + + // Causes compaction + InitiateTimeSpanCompaction(); + + OutputHelper.WriteLine("Compaction occurred"); + OutputHelper.WriteLine("Checking arrays for corrupted data..."); + + int index = 0; + + // Check that array content is not corrupted + foreach (HolderForTimeSpan holder in testNativeBuffer) + { + Assert.AreEqual(holder.StoredTicks, holder.TsValue.Ticks, $"Array content comparison failed at position {index}. Expecting {holder.StoredTicks}, found {holder.TsValue.Ticks}"); + index++; + } + + OutputHelper.WriteLine("No corruption detected in array"); + } + + OutputHelper.WriteLine("Completed TestCompactionForNotFixedArray"); + } + + private TimeSpan GetRandomTimeSpan() + { + Random rand = new(); + + return TimeSpan.FromTicks(rand.Next() * 1000_000); + } + + // This function cause compaction to occur. + // It is not so trivial as it need to fragment heap with referenced objects. + void InitiateTimeSpanCompaction() + { + // Large count, so compaction happens during RunAllocations. + HolderForTimeSpan[] arrayOfArrays = new HolderForTimeSpan[500]; + RunTimeSpanAllocations(arrayOfArrays); + } + + private void RunTimeSpanAllocations(HolderForTimeSpan[] arrObj) + { + + for (int i = 1; i < arrObj.Length; i++) + { + // Creates referenced object, which stays in memory until InitiateCompaction exits + arrObj[i] = new HolderForTimeSpan(GetRandomTimeSpan()); + + // Tries to create larger object that would be later hole + // This object could be garbage collected on each "i" cycle + HolderForTimeSpan[] arr = new HolderForTimeSpan[50 * i]; + + // Creates some usage for array elements, so it is not optimized out + arr[0] = new HolderForTimeSpan(TimeSpan.MinValue); + arr[1] = new HolderForTimeSpan(TimeSpan.MaxValue); + + Console.WriteLine($"On Cycle {i:D3} DateTime holder allocated"); + } + } + + private class HolderForTimeSpan + { + public long StoredTicks { get; } + public TimeSpan TsValue { get; } + + public HolderForTimeSpan(TimeSpan ts) + { + StoredTicks = ts.Ticks; + TsValue = ts; + } + } + } +} diff --git a/Tests/NFUnitTestGC/packages.config b/Tests/NFUnitTestGC/packages.config new file mode 100644 index 00000000..7704bd8d --- /dev/null +++ b/Tests/NFUnitTestGC/packages.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/nanoFramework.CoreLibrary.sln b/nanoFramework.CoreLibrary.sln index a2a1bbe7..9e41b209 100644 --- a/nanoFramework.CoreLibrary.sln +++ b/nanoFramework.CoreLibrary.sln @@ -67,6 +67,8 @@ Project("{11A8DD76-328B-46DF-9F39-F559912D0360}") = "NFUnitTestRecords", "Tests\ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "nanoFramework.TestAdapter", "nanoFramework.TestFramework\source\TestAdapter\nanoFramework.TestAdapter.csproj", "{6D740F0A-D435-4ACF-AD27-D702A599F229}" EndProject +Project("{11A8DD76-328B-46DF-9F39-F559912D0360}") = "NFUnitTestGC", "Tests\NFUnitTestGC\NFUnitTestGC.nfproj", "{768BE3B6-41C6-4DFB-8A2D-443B2113F5AD}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -221,6 +223,12 @@ Global {6D740F0A-D435-4ACF-AD27-D702A599F229}.Debug|Any CPU.Build.0 = Debug|Any CPU {6D740F0A-D435-4ACF-AD27-D702A599F229}.Release|Any CPU.ActiveCfg = Release|Any CPU {6D740F0A-D435-4ACF-AD27-D702A599F229}.Release|Any CPU.Build.0 = Release|Any CPU + {768BE3B6-41C6-4DFB-8A2D-443B2113F5AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {768BE3B6-41C6-4DFB-8A2D-443B2113F5AD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {768BE3B6-41C6-4DFB-8A2D-443B2113F5AD}.Debug|Any CPU.Deploy.0 = Debug|Any CPU + {768BE3B6-41C6-4DFB-8A2D-443B2113F5AD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {768BE3B6-41C6-4DFB-8A2D-443B2113F5AD}.Release|Any CPU.Build.0 = Release|Any CPU + {768BE3B6-41C6-4DFB-8A2D-443B2113F5AD}.Release|Any CPU.Deploy.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -250,6 +258,7 @@ Global {55F048B5-6739-43C5-A93D-DB61DB8E912F} = {0BAE286A-5434-4F56-A9F1-41B72056170E} {0BE498D1-CB3E-4D1E-BA4C-2C49AE30432D} = {0BAE286A-5434-4F56-A9F1-41B72056170E} {6D740F0A-D435-4ACF-AD27-D702A599F229} = {0BAE286A-5434-4F56-A9F1-41B72056170E} + {768BE3B6-41C6-4DFB-8A2D-443B2113F5AD} = {0BAE286A-5434-4F56-A9F1-41B72056170E} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {8DE44407-9B41-4459-97D2-FCE54B1F4300}