//----------------------------------------------------------------------- // ----------------------------------------------------------------------- // file="TestL4Behaviour" // Project: StgSharp // AuthorGroup: Nitload // Copyright (c) Nitload. All rights reserved. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // // ----------------------------------------------------------------------- // ----------------------------------------------------------------------- using StgSharp.HighPerformance.Memory; using System; using System.Collections.Generic; using System.Runtime.CompilerServices; namespace StgSharp { // ==================================================================== // TrackingPredictor // // 每次 Predict 返回全部地址(幂等); // Prefetch 用可配置字节填充缓存行; // WriteBack 记录被回写的 origin,供驱逐测试断言。 // ==================================================================== internal sealed unsafe class TrackingPredictor : IL4Predict { private readonly nuint[] _addresses; public byte FillByte = 0xAB; public readonly List WriteBackLog = []; public TrackingPredictor( nuint[] addresses ) { _addresses = addresses; } public bool Predict( Span node, out int count ) { count = Math.Min(_addresses.Length, node.Length); for (int i = 0; i < count; i++) { node[i] = new CacheLineDescription { Address = _addresses[i], MapPolicy = 0, }; } return count > 0; } public void Prefetch( nuint origin, ulong policy, Span cache ) { cache.Fill(FillByte); } public void WriteBack( nuint origin, ulong policy, ReadOnlySpan cache ) { WriteBackLog.Add(origin); } } // ==================================================================== // P1 行为测试 // ==================================================================== internal static partial class Program { // ---------------------------------------------------------------- // 入口 // ---------------------------------------------------------------- public static void TestL4Behavior() { _l4FailCount = 0; Console.WriteLine("=== L4 Behavior Tests (P1) ==="); Console.WriteLine(); L4Test_MultipleRef_SameLine(); L4Test_AgeAll_SaturatesAt3(); L4Test_Evict_RemovesStaleLines(); Console.WriteLine(); if (_l4FailCount == 0) { Console.WriteLine("=== ALL P1 TESTS PASSED ==="); } else { Console.WriteLine($"=== {_l4FailCount} TEST(S) FAILED ==="); } } // ---------------------------------------------------------------- // 辅助:读取 CacheLine 第一个字节 // ---------------------------------------------------------------- private static unsafe byte L4ReadFirstByte( L4.CacheLine line ) { return *(byte*)line.Address; } // ================================================================ // P1-2 AgeAll_SaturatesAt3 // // 目标:连续触发多次 AgeAll 后,所有已分配行的 RRPV 字节 // 均饱和在 3,不溢出、不归零。 // // 策略: // 1. 预取 K 条行并立即 Dispose(refCount=0,RRPV 初始为 0) // 2. 调用 ForceAgeAll() 4 次(通过新增的 Debug 接口直接触发) // 3. 读取 DebugSnapshot.RrpvSnapshot,断言所有已分配行 == 3 // // 注意:AgeAll 是 private,测试依赖下方"在 L4.Debug.cs 追加 // public void DebugAgeAll()" 的接口,见文末说明。 // ================================================================ private static void L4Test_AgeAll_SaturatesAt3() { Console.Write("[P1-2] AgeAll_SaturatesAt3 ... "); try { const int K = 4; nuint[] addresses = MakeL4FakeAddresses(K); using L4 l4 = new L4(); l4.PrefetchPredict = new TrackingPredictor(addresses); // Step 1: 装填 K 条行,全部 Dispose(refCount=0) for (int i = 0; i < K; i++) { L4.CacheLine line = l4.Map(addresses[i]); line.Dispose(); } // 装填后 RRPV 应全为 0(Map 命中时 eviction[id] = 0) { L4.DebugSnapshot snap = l4.GetDebugSnapshot(); for (int i = 0; i < snap.MaxId; i++) { L4Assert( snap.RrpvSnapshot[i] == 0, $"{$"RrpvSnapshot[{i}] should be 0 right after Map, "}{$"got {snap.RrpvSnapshot[i]}"}"); } } // Step 2: AgeAll 4 次(饱和值为 3,4 次足够触顶) for (int age = 0; age < 4; age++) { l4.DebugAgeAll(); } // Step 3: 所有已分配行 RRPV == 3 { L4.DebugSnapshot snap = l4.GetDebugSnapshot(); L4Assert( snap.MaxId == K, $"MaxId should be {K}, got {snap.MaxId}"); bool allSaturated = true; for (int i = 0; i < snap.MaxId; i++) { if (snap.RrpvSnapshot[i] != 3) { L4Assert(false, $"{$"RrpvSnapshot[{i}] == {snap.RrpvSnapshot[i]}, "}expected 3 after 4x AgeAll (saturation failure or overflow)"); allSaturated = false; } } if (allSaturated) { Console.WriteLine($"PASS (MaxId={snap.MaxId}, all RRPV==3)"); } } } catch (Exception ex) { Console.WriteLine($"FAIL ({ex.GetType().Name}: {ex.Message})"); _l4FailCount++; } } // ================================================================ // P1-3 Evict_RemovesStaleLines // // 目标:RRPV==3 且 refCount==0 的行在 Evict 后: // a. WriteBack 被调用,origin 是 batch1 中的地址 // b. DebugContains(evictedOrigin) == false(SwissTable 条目已移除) // c. 该地址重新 Map 后,缓存行被重新 Prefetch(填充字节变为新值) // // 策略: // 1. batch1(K 条) 装填 → Dispose → DebugAgeAll × 4(RRPV 饱和到 3) // 2. 换 Predictor(FillByte 改为 0xCC),地址列表扩充为 batch1 + 1 条新地址 // 3. Map 新地址 → 触发 Prefetch → Prefetch 内部 Evict → WriteBack // 4. 三项断言 // ================================================================ private static void L4Test_Evict_RemovesStaleLines() { Console.Write("[P1-3] Evict_RemovesStaleLines ... "); try { const int K = 4; nuint[] batch1 = MakeL4FakeAddresses(K); TrackingPredictor predictor1 = new TrackingPredictor(batch1) { FillByte = 0xAA }; using L4 l4 = new L4(); l4.PrefetchPredict = predictor1; // ---------- Phase 1: 装填 batch1,全部 Dispose(refCount=0)---------- for (int i = 0; i < K; i++) { L4.CacheLine line = l4.Map(batch1[i]); L4Assert( L4ReadFirstByte(line) == 0xAA, $"Initial fill should be 0xAA, got 0x{L4ReadFirstByte(line):X2}"); line.Dispose(); } // 确认 K 条行均在 SwissTable 中 for (int i = 0; i < K; i++) { L4Assert( l4.DebugContains(batch1[i]), $"batch1[{i}] should be in SwissTable after prefetch"); } // ---------- Phase 2: AgeAll × 4,使 RRPV 饱和到 3 ---------- for (int age = 0; age < 4; age++) { l4.DebugAgeAll(); } L4.DebugSnapshot snap = l4.GetDebugSnapshot(); for (int i = 0; i < snap.MaxId; i++) { L4Assert( snap.RrpvSnapshot[i] == 3, $"Pre-evict: RrpvSnapshot[{i}] should be 3, got {snap.RrpvSnapshot[i]}"); } // ---------- Phase 3: 直接调用 DebugEvict(1),驱逐 1 条 ---------- // 此时 refCount==0 且 RRPV==3,Evict 应能找到候选 TrackingPredictor predictor2 = new TrackingPredictor(batch1) { FillByte = 0xCC }; l4.PrefetchPredict = predictor2; // 换 predictor 以捕获 WriteBack 日志 int remain = l4.DebugEvict(1); // 断言 a: remain==0,说明成功驱逐了 1 条 L4Assert( remain == 0, $"DebugEvict(1) should return 0 (all evicted), got remain={remain}"); // 断言 b: WriteBack 被调用,origin 属于 batch1 L4Assert( predictor2.WriteBackLog.Count > 0, "Evict should have called WriteBack for the evicted line"); nuint evictedOrigin = nuint.Zero; foreach (nuint logged in predictor2.WriteBackLog) { foreach (nuint b1 in batch1) { if (logged == b1) { evictedOrigin = logged; break; } } if (evictedOrigin != nuint.Zero) { break; } } L4Assert( evictedOrigin != nuint.Zero, $"{$"WriteBack origin should be a batch1 address, "}{$"WriteBackLog=[{string.Join(", ", predictor2.WriteBackLog)}]"}"); // 断言 c: SwissTable 中已不含该 origin L4Assert( !l4.DebugContains(evictedOrigin), $"DebugContains(0x{evictedOrigin:X}) should be false after eviction"); // 断言 d: 被驱逐的地址重新 Map → 重新 Prefetch → 填充 0xCC(非旧值 0xAA) // 注意:此时需要 predictor2 的 Predict 也包含 evictedOrigin, // 使 Map 触发 Prefetch 时能重新装填该行 L4.CacheLine remapped = l4.Map(evictedOrigin); byte refill = L4ReadFirstByte(remapped); remapped.Dispose(); L4Assert( refill == 0xCC, $"{$"Re-mapped evicted line should be re-prefetched with 0xCC, "}{$"got 0x{refill:X2} (0xAA = stale data not evicted)"}"); if (_l4FailCount == 0) { Console.WriteLine( $"{$"PASS (evicted 0x{evictedOrigin:X}, remain={remain}, "}{$"WriteBack×{predictor2.WriteBackLog.Count})"}"); } } catch (Exception ex) { Console.WriteLine($"FAIL ({ex.GetType().Name}: {ex.Message})"); _l4FailCount++; } } // ================================================================ // P1-1 MultipleRef_SameLine // // 同一 origin 同时持有 N 个句柄时: // a. 所有句柄 Address 非零 // b. 所有句柄 Address 相同(同一缓存行) // c. 全部 Dispose 后 ActiveCount 归零 // d. 再次 Map 同一地址仍然成功(refCount 归零不破坏可访问性) // ================================================================ private static void L4Test_MultipleRef_SameLine() { Console.Write("[P1-1] MultipleRef_SameLine ... "); const int N = 8; try { nuint[] addresses = MakeL4FakeAddresses(1); using L4 l4 = new L4(); l4.PrefetchPredict = new TrackingPredictor(addresses); nuint target = addresses[0]; // ref struct 不能形成数组,展开为 N 个具名局部变量同时持有 L4.CacheLine l0 = l4.Map(target); L4.CacheLine l1 = l4.Map(target); L4.CacheLine l2 = l4.Map(target); L4.CacheLine l3 = l4.Map(target); L4.CacheLine l4b = l4.Map(target); L4.CacheLine l5 = l4.Map(target); L4.CacheLine l6 = l4.Map(target); L4.CacheLine l7 = l4.Map(target); // a. 所有句柄 Address 非零 L4Assert(l0.Address != nuint.Zero, "l0.Address should not be zero"); L4Assert(l1.Address != nuint.Zero, "l1.Address should not be zero"); L4Assert(l2.Address != nuint.Zero, "l2.Address should not be zero"); L4Assert(l3.Address != nuint.Zero, "l3.Address should not be zero"); L4Assert(l4b.Address != nuint.Zero, "l4b.Address should not be zero"); L4Assert(l5.Address != nuint.Zero, "l5.Address should not be zero"); L4Assert(l6.Address != nuint.Zero, "l6.Address should not be zero"); L4Assert(l7.Address != nuint.Zero, "l7.Address should not be zero"); // b. 所有句柄 Address 相同(同一缓存行) nuint first = l0.Address; L4Assert(l1.Address == first, $"l1 0x{l1.Address:X} != l0 0x{first:X}"); L4Assert(l2.Address == first, $"l2 0x{l2.Address:X} != l0 0x{first:X}"); L4Assert(l3.Address == first, $"l3 0x{l3.Address:X} != l0 0x{first:X}"); L4Assert(l4b.Address == first, $"l4b 0x{l4b.Address:X} != l0 0x{first:X}"); L4Assert(l5.Address == first, $"l5 0x{l5.Address:X} != l0 0x{first:X}"); L4Assert(l6.Address == first, $"l6 0x{l6.Address:X} != l0 0x{first:X}"); L4Assert(l7.Address == first, $"l7 0x{l7.Address:X} != l0 0x{first:X}"); // c. 持有期间 ActiveCount == N { L4.DebugSnapshot snap = l4.GetDebugSnapshot(); L4Assert( snap.ActiveCount == N, $"ActiveCount should be {N} while {N} handles live, got {snap.ActiveCount}"); } // 全部 Dispose l0.Dispose(); l1.Dispose(); l2.Dispose(); l3.Dispose(); l4b.Dispose(); l5.Dispose(); l6.Dispose(); l7.Dispose(); // c(续). Dispose 后 ActiveCount 归零 { L4.DebugSnapshot snap = l4.GetDebugSnapshot(); L4Assert( snap.ActiveCount == 0, $"ActiveCount should be 0 after all disposed, got {snap.ActiveCount}"); } // d. 再次 Map 仍然成功 L4.CacheLine again = l4.Map(target); L4Assert(again.Address != nuint.Zero, "Map after all refs disposed should still return valid address"); again.Dispose(); Console.WriteLine("PASS"); } catch (Exception ex) { Console.WriteLine($"FAIL ({ex.GetType().Name}: {ex.Message})"); _l4FailCount++; } } } }