L4 briefly pass unit test

This commit is contained in:
Youmu Liu
2026-03-17 20:31:06 +08:00
parent 98d711a3ca
commit dccb815811
13 changed files with 1710 additions and 32 deletions
+431
View File
@@ -0,0 +1,431 @@
//-----------------------------------------------------------------------
// -----------------------------------------------------------------------
// 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 : IPrefetchPredict
{
private readonly nuint[] _addresses;
public byte FillByte = 0xAB;
public readonly List<nuint> WriteBackLog = [];
public TrackingPredictor(
nuint[] addresses
)
{
_addresses = addresses;
}
public bool Predict(
Span<CacheLinePrediction> node,
out int count
)
{
count = Math.Min(_addresses.Length, node.Length);
for (int i = 0; i < count; i++) {
node[i] = new CacheLinePrediction
{
Address = _addresses[i],
MapPolicy = 0,
};
}
return count > 0;
}
public void Prefetch(
nuint origin,
ulong policy,
Span<byte> cache
)
{
cache.Fill(FillByte);
}
public void WriteBack(
nuint origin,
ulong policy,
ReadOnlySpan<byte> 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
) => *(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++;
}
}
}
}