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 }