TrackingTests.cs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512
  1. using NUnit.Framework;
  2. using Ryujinx.Memory;
  3. using Ryujinx.Memory.Tracking;
  4. using System;
  5. using System.Collections.Generic;
  6. using System.Diagnostics;
  7. using System.Threading;
  8. namespace Ryujinx.Tests.Memory
  9. {
  10. public class TrackingTests
  11. {
  12. private const int RndCnt = 3;
  13. private const ulong MemorySize = 0x8000;
  14. private const int PageSize = 4096;
  15. private MemoryBlock _memoryBlock;
  16. private MemoryTracking _tracking;
  17. private MockVirtualMemoryManager _memoryManager;
  18. [SetUp]
  19. public void Setup()
  20. {
  21. _memoryBlock = new MemoryBlock(MemorySize);
  22. _memoryManager = new MockVirtualMemoryManager(MemorySize, PageSize);
  23. _tracking = new MemoryTracking(_memoryManager, PageSize);
  24. }
  25. [TearDown]
  26. public void Teardown()
  27. {
  28. _memoryBlock.Dispose();
  29. }
  30. private bool TestSingleWrite(RegionHandle handle, ulong address, ulong size)
  31. {
  32. handle.Reprotect();
  33. _tracking.VirtualMemoryEvent(address, size, true);
  34. return handle.Dirty;
  35. }
  36. [Test]
  37. public void SingleRegion()
  38. {
  39. RegionHandle handle = _tracking.BeginTracking(0, PageSize, 0);
  40. (ulong address, ulong size)? readTrackingTriggered = null;
  41. handle.RegisterAction((address, size) =>
  42. {
  43. readTrackingTriggered = (address, size);
  44. });
  45. bool dirtyInitial = handle.Dirty;
  46. Assert.True(dirtyInitial); // Handle starts dirty.
  47. handle.Reprotect();
  48. bool dirtyAfterReprotect = handle.Dirty;
  49. Assert.False(dirtyAfterReprotect); // Handle is no longer dirty.
  50. _tracking.VirtualMemoryEvent(PageSize * 2, 4, true);
  51. _tracking.VirtualMemoryEvent(PageSize * 2, 4, false);
  52. bool dirtyAfterUnrelatedReadWrite = handle.Dirty;
  53. Assert.False(dirtyAfterUnrelatedReadWrite); // Not dirtied, as the write was to an unrelated address.
  54. Assert.IsNull(readTrackingTriggered); // Hasn't been triggered yet
  55. _tracking.VirtualMemoryEvent(0, 4, false);
  56. bool dirtyAfterRelatedRead = handle.Dirty;
  57. Assert.False(dirtyAfterRelatedRead); // Only triggers on write.
  58. Assert.AreEqual(readTrackingTriggered, (0UL, 4UL)); // Read action was triggered.
  59. readTrackingTriggered = null;
  60. _tracking.VirtualMemoryEvent(0, 4, true);
  61. bool dirtyAfterRelatedWrite = handle.Dirty;
  62. Assert.True(dirtyAfterRelatedWrite); // Dirty flag should now be set.
  63. _tracking.VirtualMemoryEvent(4, 4, true);
  64. bool dirtyAfterRelatedWrite2 = handle.Dirty;
  65. Assert.True(dirtyAfterRelatedWrite2); // Dirty flag should still be set.
  66. handle.Reprotect();
  67. bool dirtyAfterReprotect2 = handle.Dirty;
  68. Assert.False(dirtyAfterReprotect2); // Handle is no longer dirty.
  69. handle.Dispose();
  70. bool dirtyAfterDispose = TestSingleWrite(handle, 0, 4);
  71. Assert.False(dirtyAfterDispose); // Handle cannot be triggered when disposed
  72. }
  73. [Test]
  74. public void OverlappingRegions()
  75. {
  76. RegionHandle allHandle = _tracking.BeginTracking(0, PageSize * 16, 0);
  77. allHandle.Reprotect();
  78. (ulong address, ulong size)? readTrackingTriggeredAll = null;
  79. void RegisterReadAction()
  80. {
  81. readTrackingTriggeredAll = null;
  82. allHandle.RegisterAction((address, size) =>
  83. {
  84. readTrackingTriggeredAll = (address, size);
  85. });
  86. }
  87. RegisterReadAction();
  88. // Create 16 page sized handles contained within the allHandle.
  89. RegionHandle[] containedHandles = new RegionHandle[16];
  90. for (int i = 0; i < 16; i++)
  91. {
  92. containedHandles[i] = _tracking.BeginTracking((ulong)i * PageSize, PageSize, 0);
  93. containedHandles[i].Reprotect();
  94. }
  95. for (int i = 0; i < 16; i++)
  96. {
  97. // No handles are dirty.
  98. Assert.False(allHandle.Dirty);
  99. Assert.IsNull(readTrackingTriggeredAll);
  100. for (int j = 0; j < 16; j++)
  101. {
  102. Assert.False(containedHandles[j].Dirty);
  103. }
  104. _tracking.VirtualMemoryEvent((ulong)i * PageSize, 1, true);
  105. // Only the handle covering the entire range and the relevant contained handle are dirty.
  106. Assert.True(allHandle.Dirty);
  107. Assert.AreEqual(readTrackingTriggeredAll, ((ulong)i * PageSize, 1UL)); // Triggered read tracking
  108. for (int j = 0; j < 16; j++)
  109. {
  110. if (j == i)
  111. {
  112. Assert.True(containedHandles[j].Dirty);
  113. }
  114. else
  115. {
  116. Assert.False(containedHandles[j].Dirty);
  117. }
  118. }
  119. // Clear flags and reset read action.
  120. RegisterReadAction();
  121. allHandle.Reprotect();
  122. containedHandles[i].Reprotect();
  123. }
  124. }
  125. [Test]
  126. public void PageAlignment(
  127. [Values(1ul, 512ul, 2048ul, 4096ul, 65536ul)][Random(1ul, 65536ul, RndCnt)] ulong address,
  128. [Values(1ul, 4ul, 1024ul, 4096ul, 65536ul)][Random(1ul, 65536ul, RndCnt)] ulong size)
  129. {
  130. ulong alignedStart = (address / PageSize) * PageSize;
  131. ulong alignedEnd = ((address + size + PageSize - 1) / PageSize) * PageSize;
  132. ulong alignedSize = alignedEnd - alignedStart;
  133. RegionHandle handle = _tracking.BeginTracking(address, size, 0);
  134. // Anywhere inside the pages the region is contained on should trigger.
  135. bool originalRangeTriggers = TestSingleWrite(handle, address, size);
  136. Assert.True(originalRangeTriggers);
  137. bool alignedRangeTriggers = TestSingleWrite(handle, alignedStart, alignedSize);
  138. Assert.True(alignedRangeTriggers);
  139. bool alignedStartTriggers = TestSingleWrite(handle, alignedStart, 1);
  140. Assert.True(alignedStartTriggers);
  141. bool alignedEndTriggers = TestSingleWrite(handle, alignedEnd - 1, 1);
  142. Assert.True(alignedEndTriggers);
  143. // Outside the tracked range should not trigger.
  144. bool alignedBeforeTriggers = TestSingleWrite(handle, alignedStart - 1, 1);
  145. Assert.False(alignedBeforeTriggers);
  146. bool alignedAfterTriggers = TestSingleWrite(handle, alignedEnd, 1);
  147. Assert.False(alignedAfterTriggers);
  148. }
  149. [Test, Explicit, Timeout(1000)]
  150. public void Multithreading()
  151. {
  152. // Multithreading sanity test
  153. // Multiple threads can easily read/write memory regions from any existing handle.
  154. // Handles can also be owned by different threads, though they should have one owner thread.
  155. // Handles can be created and disposed at any time, by any thread.
  156. // This test should not throw or deadlock due to invalid state.
  157. const int ThreadCount = 1;
  158. const int HandlesPerThread = 16;
  159. long finishedTime = 0;
  160. RegionHandle[] handles = new RegionHandle[ThreadCount * HandlesPerThread];
  161. Random globalRand = new();
  162. for (int i = 0; i < handles.Length; i++)
  163. {
  164. handles[i] = _tracking.BeginTracking((ulong)i * PageSize, PageSize, 0);
  165. handles[i].Reprotect();
  166. }
  167. List<Thread> testThreads = new();
  168. // Dirty flag consumer threads
  169. int dirtyFlagReprotects = 0;
  170. for (int i = 0; i < ThreadCount; i++)
  171. {
  172. int randSeed = i;
  173. testThreads.Add(new Thread(() =>
  174. {
  175. int handleBase = randSeed * HandlesPerThread;
  176. while (Stopwatch.GetTimestamp() < finishedTime)
  177. {
  178. Random random = new(randSeed);
  179. RegionHandle handle = handles[handleBase + random.Next(HandlesPerThread)];
  180. if (handle.Dirty)
  181. {
  182. handle.Reprotect();
  183. Interlocked.Increment(ref dirtyFlagReprotects);
  184. }
  185. }
  186. }));
  187. }
  188. // Write trigger threads
  189. int writeTriggers = 0;
  190. for (int i = 0; i < ThreadCount; i++)
  191. {
  192. int randSeed = i;
  193. testThreads.Add(new Thread(() =>
  194. {
  195. Random random = new(randSeed);
  196. ulong handleBase = (ulong)(randSeed * HandlesPerThread * PageSize);
  197. while (Stopwatch.GetTimestamp() < finishedTime)
  198. {
  199. _tracking.VirtualMemoryEvent(handleBase + (ulong)random.Next(PageSize * HandlesPerThread), PageSize / 2, true);
  200. Interlocked.Increment(ref writeTriggers);
  201. }
  202. }));
  203. }
  204. // Handle create/delete threads
  205. int handleLifecycles = 0;
  206. for (int i = 0; i < ThreadCount; i++)
  207. {
  208. int randSeed = i;
  209. testThreads.Add(new Thread(() =>
  210. {
  211. int maxAddress = ThreadCount * HandlesPerThread * PageSize;
  212. Random random = new(randSeed + 512);
  213. while (Stopwatch.GetTimestamp() < finishedTime)
  214. {
  215. RegionHandle handle = _tracking.BeginTracking((ulong)random.Next(maxAddress), (ulong)random.Next(65536), 0);
  216. handle.Dispose();
  217. Interlocked.Increment(ref handleLifecycles);
  218. }
  219. }));
  220. }
  221. finishedTime = Stopwatch.GetTimestamp() + Stopwatch.Frequency / 2; // Run for 500ms;
  222. foreach (Thread thread in testThreads)
  223. {
  224. thread.Start();
  225. }
  226. foreach (Thread thread in testThreads)
  227. {
  228. thread.Join();
  229. }
  230. Assert.Greater(dirtyFlagReprotects, 10);
  231. Assert.Greater(writeTriggers, 10);
  232. Assert.Greater(handleLifecycles, 10);
  233. }
  234. [Test]
  235. public void ReadActionThreadConsumption()
  236. {
  237. // Read actions should only be triggered once for each registration.
  238. // The implementation should use an interlocked exchange to make sure other threads can't get the action.
  239. RegionHandle handle = _tracking.BeginTracking(0, PageSize, 0);
  240. int triggeredCount = 0;
  241. int registeredCount = 0;
  242. int signalThreadsDone = 0;
  243. bool isRegistered = false;
  244. void RegisterReadAction()
  245. {
  246. registeredCount++;
  247. handle.RegisterAction((address, size) =>
  248. {
  249. isRegistered = false;
  250. Interlocked.Increment(ref triggeredCount);
  251. });
  252. }
  253. const int ThreadCount = 16;
  254. const int IterationCount = 10000;
  255. Thread[] signalThreads = new Thread[ThreadCount];
  256. for (int i = 0; i < ThreadCount; i++)
  257. {
  258. int randSeed = i;
  259. signalThreads[i] = new Thread(() =>
  260. {
  261. Random random = new(randSeed);
  262. for (int j = 0; j < IterationCount; j++)
  263. {
  264. _tracking.VirtualMemoryEvent((ulong)random.Next(PageSize), 4, false);
  265. }
  266. Interlocked.Increment(ref signalThreadsDone);
  267. });
  268. }
  269. for (int i = 0; i < ThreadCount; i++)
  270. {
  271. signalThreads[i].Start();
  272. }
  273. while (signalThreadsDone != -1)
  274. {
  275. if (signalThreadsDone == ThreadCount)
  276. {
  277. signalThreadsDone = -1;
  278. }
  279. if (!isRegistered)
  280. {
  281. isRegistered = true;
  282. RegisterReadAction();
  283. }
  284. }
  285. // The action should trigger exactly once for every registration,
  286. // then we register once after all the threads signalling it cease.
  287. Assert.AreEqual(registeredCount, triggeredCount + 1);
  288. }
  289. [Test]
  290. public void DisposeHandles()
  291. {
  292. // Ensure that disposed handles correctly remove their virtual and physical regions.
  293. RegionHandle handle = _tracking.BeginTracking(0, PageSize, 0);
  294. handle.Reprotect();
  295. Assert.AreEqual(1, _tracking.GetRegionCount());
  296. handle.Dispose();
  297. Assert.AreEqual(0, _tracking.GetRegionCount());
  298. // Two handles, small entirely contains big.
  299. // We expect there to be three regions after creating both, one for the small region and two covering the big one around it.
  300. // Regions are always split to avoid overlapping, which is why there are three instead of two.
  301. RegionHandle handleSmall = _tracking.BeginTracking(PageSize, PageSize, 0);
  302. RegionHandle handleBig = _tracking.BeginTracking(0, PageSize * 4, 0);
  303. Assert.AreEqual(3, _tracking.GetRegionCount());
  304. // After disposing the big region, only the small one will remain.
  305. handleBig.Dispose();
  306. Assert.AreEqual(1, _tracking.GetRegionCount());
  307. handleSmall.Dispose();
  308. Assert.AreEqual(0, _tracking.GetRegionCount());
  309. }
  310. [Test]
  311. public void ReadAndWriteProtection()
  312. {
  313. MemoryPermission protection = MemoryPermission.ReadAndWrite;
  314. _memoryManager.OnProtect += (va, size, newProtection) =>
  315. {
  316. Assert.AreEqual((0, PageSize), (va, size)); // Should protect the exact region all the operations use.
  317. protection = newProtection;
  318. };
  319. RegionHandle handle = _tracking.BeginTracking(0, PageSize, 0);
  320. // After creating the handle, there is no protection yet.
  321. Assert.AreEqual(MemoryPermission.ReadAndWrite, protection);
  322. bool dirtyInitial = handle.Dirty;
  323. Assert.True(dirtyInitial); // Handle starts dirty.
  324. handle.Reprotect();
  325. // After a reprotect, there is write protection, which will set a dirty flag when any write happens.
  326. Assert.AreEqual(MemoryPermission.Read, protection);
  327. (ulong address, ulong size)? readTrackingTriggered = null;
  328. handle.RegisterAction((address, size) =>
  329. {
  330. readTrackingTriggered = (address, size);
  331. });
  332. // Registering an action adds read/write protection.
  333. Assert.AreEqual(MemoryPermission.None, protection);
  334. bool dirtyAfterReprotect = handle.Dirty;
  335. Assert.False(dirtyAfterReprotect); // Handle is no longer dirty.
  336. // First we should read, which will trigger the action. This _should not_ remove write protection on the memory.
  337. _tracking.VirtualMemoryEvent(0, 4, false);
  338. bool dirtyAfterRead = handle.Dirty;
  339. Assert.False(dirtyAfterRead); // Not dirtied, as this was a read.
  340. Assert.AreEqual(readTrackingTriggered, (0UL, 4UL)); // Read action was triggered.
  341. Assert.AreEqual(MemoryPermission.Read, protection); // Write protection is still present.
  342. readTrackingTriggered = null;
  343. // Now, perform a write.
  344. _tracking.VirtualMemoryEvent(0, 4, true);
  345. bool dirtyAfterWriteAfterRead = handle.Dirty;
  346. Assert.True(dirtyAfterWriteAfterRead); // Should be dirty.
  347. Assert.AreEqual(MemoryPermission.ReadAndWrite, protection); // All protection is now be removed from the memory.
  348. Assert.IsNull(readTrackingTriggered); // Read tracking was removed when the action fired, as it can only fire once.
  349. handle.Dispose();
  350. }
  351. [Test]
  352. public void PreciseAction()
  353. {
  354. RegionHandle handle = _tracking.BeginTracking(0, PageSize, 0);
  355. (ulong address, ulong size, bool write)? preciseTriggered = null;
  356. handle.RegisterPreciseAction((address, size, write) =>
  357. {
  358. preciseTriggered = (address, size, write);
  359. return true;
  360. });
  361. (ulong address, ulong size)? readTrackingTriggered = null;
  362. handle.RegisterAction((address, size) =>
  363. {
  364. readTrackingTriggered = (address, size);
  365. });
  366. handle.Reprotect();
  367. _tracking.VirtualMemoryEvent(0, 4, false, precise: true);
  368. Assert.IsNull(readTrackingTriggered); // Hasn't been triggered - precise action returned true.
  369. Assert.AreEqual(preciseTriggered, (0UL, 4UL, false)); // Precise action was triggered.
  370. _tracking.VirtualMemoryEvent(0, 4, true, precise: true);
  371. Assert.IsNull(readTrackingTriggered); // Still hasn't been triggered.
  372. bool dirtyAfterPreciseActionTrue = handle.Dirty;
  373. Assert.False(dirtyAfterPreciseActionTrue); // Not dirtied - precise action returned true.
  374. Assert.AreEqual(preciseTriggered, (0UL, 4UL, true)); // Precise action was triggered.
  375. // Handle is now dirty.
  376. handle.Reprotect(true);
  377. preciseTriggered = null;
  378. _tracking.VirtualMemoryEvent(4, 4, true, precise: true);
  379. Assert.AreEqual(preciseTriggered, (4UL, 4UL, true)); // Precise action was triggered even though handle was dirty.
  380. handle.Reprotect();
  381. handle.RegisterPreciseAction((address, size, write) =>
  382. {
  383. preciseTriggered = (address, size, write);
  384. return false; // Now, we return false, which indicates that the regular read/write behaviours should trigger.
  385. });
  386. _tracking.VirtualMemoryEvent(8, 4, true, precise: true);
  387. Assert.AreEqual(readTrackingTriggered, (8UL, 4UL)); // Read action triggered, as precise action returned false.
  388. bool dirtyAfterPreciseActionFalse = handle.Dirty;
  389. Assert.True(dirtyAfterPreciseActionFalse); // Dirtied, as precise action returned false.
  390. Assert.AreEqual(preciseTriggered, (8UL, 4UL, true)); // Precise action was triggered.
  391. }
  392. }
  393. }