|
|
@@ -558,3 +558,95 @@ func TestBufferPoolHotConcurrencyWithWork(t *testing.T) {
|
|
|
t.Logf("BufferPool RLock 真并行! 加速比 %.2fx", speedup)
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+// TestEvictBurstAfterCommit 采样淘汰的正确性: Commit 把一批脏页变干净留在池里,
|
|
|
+// 池子超出容量; 下一次写入必须在一轮内把容量压回来, 且被丢的只能是干净页。
|
|
|
+// (这正是旧实现 O(n×淘汰数) 退化的场景 —— 500 次淘汰 × 全表扫; 采样后 O(K×淘汰数))
|
|
|
+func TestEvictBurstAfterCommit(t *testing.T) {
|
|
|
+ d := &diskStub{pages: map[uint32][]byte{}}
|
|
|
+ cr := &countingRead{d: d}
|
|
|
+ const poolCap = 1000
|
|
|
+ bp := NewBufferPool(poolCap, cr.read, d.write)
|
|
|
+
|
|
|
+ // 塞 1500 张脏页: 全脏时允许临时超容量, 所以能超过 1000
|
|
|
+ for i := 1; i <= 1500; i++ {
|
|
|
+ if err := bp.WritePage(uint32(i), []byte{byte(i)}); err != nil {
|
|
|
+ t.Fatalf("WritePage(%d): %v", i, err)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if got := bp.Len(); got != 1500 {
|
|
|
+ t.Fatalf("全脏时应允许超容量, 实际 %d 页", got)
|
|
|
+ }
|
|
|
+
|
|
|
+ bp.ClearAll() // 模拟 Commit 后脏页变干净、留在池里
|
|
|
+
|
|
|
+ // 下一次写入: WritePage 先淘汰再插入 —— 一轮把 1500 压到 999,
|
|
|
+ // 插入新页后回到恰 capacity (这是池的稳态不变量)
|
|
|
+ if err := bp.WritePage(2000, []byte{0x7f}); err != nil {
|
|
|
+ t.Fatalf("触发淘汰的 WritePage: %v", err)
|
|
|
+ }
|
|
|
+ if got := bp.Len(); got != poolCap {
|
|
|
+ t.Fatalf("淘汰+插入后应恰为容量 %d, 实际 %d", poolCap, got)
|
|
|
+ }
|
|
|
+ // 触发写入的那页必须还在
|
|
|
+ if _, ok := bp.pages[2000]; !ok {
|
|
|
+ t.Fatalf("刚写入的页 2000 不应被淘汰")
|
|
|
+ }
|
|
|
+ // 剩下的页都能读回 —— 没有脏页被静默丢掉
|
|
|
+ for pid := range bp.pages {
|
|
|
+ if err := bp.ReadPage(pid, make([]byte, 1)); err != nil {
|
|
|
+ t.Fatalf("读回页 %d: %v", pid, err)
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// TestEvictSkipsDirtyPages 样本里的脏页绝不被淘汰: 池满时若没有干净候选,
|
|
|
+// 宁可临时超容量也不丢未落盘的修改。
|
|
|
+func TestEvictSkipsDirtyPages(t *testing.T) {
|
|
|
+ d := &diskStub{pages: map[uint32][]byte{}}
|
|
|
+ cr := &countingRead{d: d}
|
|
|
+ bp := NewBufferPool(4, cr.read, d.write)
|
|
|
+ for i := 1; i <= 4; i++ {
|
|
|
+ if err := bp.WritePage(uint32(i), []byte{byte(i)}); err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // 4 张全是脏页, 再写一张: 样本抽到的全是脏页 → 不做淘汰, 允许临时超容量
|
|
|
+ if err := bp.WritePage(5, []byte{5}); err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ if got := bp.Len(); got != 5 {
|
|
|
+ t.Fatalf("全脏时应允许临时超容量 (5 页), 实际 %d", got)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// BenchmarkEvictBurst 一轮淘汰 ~2000 页的成本 (白盒直接测淘汰循环):
|
|
|
+// 6000 页干净池 / 容量 4000, evictCleanForRoomLocked 一轮要淘汰 2001 页。
|
|
|
+// 全表精确最小 = 2001 × 6000 ≈ 1200 万次条目扫描 (~0.5-1s);
|
|
|
+// 采样 32 = 2001 × 32 ≈ 6.4 万次 (~0.3-0.6ms) —— 与池大小脱钩。
|
|
|
+func BenchmarkEvictBurst(b *testing.B) {
|
|
|
+ d := &diskStub{pages: map[uint32][]byte{}}
|
|
|
+ cr := &countingRead{d: d}
|
|
|
+ bp := NewBufferPool(4000, cr.read, d.write)
|
|
|
+ fillN := 1000000 // 单调发号: 用 len 当页号会在第二轮与幸存页碰撞 (死循环), 必须独立递增
|
|
|
+ fill := func(n int) {
|
|
|
+ bp.mu.Lock()
|
|
|
+ defer bp.mu.Unlock()
|
|
|
+ for len(bp.pages) < n {
|
|
|
+ pid := uint32(fillN)
|
|
|
+ fillN++
|
|
|
+ e := &pageEntry{data: []byte{1}, pageLen: 1}
|
|
|
+ bp.stampLocked(e)
|
|
|
+ bp.pages[pid] = e
|
|
|
+ bp.touchLocked(e)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ fill(6000)
|
|
|
+ b.ResetTimer()
|
|
|
+ for n := 0; n < b.N; n++ {
|
|
|
+ fill(6000) // 每轮先把池子重新撑到 6000, 再测一轮 2001 次采样淘汰
|
|
|
+ bp.mu.Lock()
|
|
|
+ bp.evictCleanForRoomLocked()
|
|
|
+ bp.mu.Unlock()
|
|
|
+ }
|
|
|
+}
|