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