mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-20 15:04:37 +00:00
P3: Add explicit bounded read-only diagnosis surfaces for all symptom classes: - FailoverDiagnostic: volume-oriented failover state with per-volume DeferredPromotion/PendingRebuild entries and proper timer lifecycle - PublicationDiagnostic: two-read coherence check (LookupBlockVolume vs registry authority) with computed Coherent verdict - RecoveryDiagnostic: minimal ActiveTasks surface (Path A) - Blocker ledger: 3 diagnosed + 3 unresolved, finite, from actual file - Runbook references only exposed surfaces, no internal state P4: Add bounded performance floor + rollout-gate package: - Engine-local floor measurement with explicit IOPS gates per workload - Cost characterization: WAL 2x write amp, -56% replication tax - Rollout gates with semantic cross-checks against cited evidence (baseline numbers, transport/network matrix, blocker counts) - Launch envelope tightened to actually measured combinations only Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
583 lines
19 KiB
Go
583 lines
19 KiB
Go
package weed_server
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"fmt"
|
|
"math"
|
|
mrand "math/rand"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/blockvol"
|
|
)
|
|
|
|
// ============================================================
|
|
// Phase 12 P4: Performance Floor — Bounded Measurement Package
|
|
//
|
|
// Workload envelope:
|
|
// Topology: RF=2 sync_all accepted chosen path
|
|
// Operations: 4K random write, 4K random read, 4K sequential write, 4K sequential read
|
|
// Runtime: no failover, no disturbance, steady-state
|
|
// Environment: unit test harness (single-process, local disk, engine-local I/O)
|
|
//
|
|
// What this measures:
|
|
// Engine I/O floor for the accepted chosen path. WriteLBA/ReadLBA through
|
|
// the full fencing path (epoch, role, lease, writeGate, WAL, dirtyMap).
|
|
// No transport layer (iSCSI/NVMe). No cross-machine replication.
|
|
//
|
|
// What this does NOT measure:
|
|
// Transport throughput, cross-machine replication tax, multi-client concurrency,
|
|
// failover-under-load, degraded mode. Production floor with replication is
|
|
// documented in baseline-roce-20260401.md.
|
|
//
|
|
// NOT performance tuning. NOT broad benchmark.
|
|
// ============================================================
|
|
|
|
const (
|
|
perfBlockSize = 4096
|
|
perfVolumeSize = 64 * 1024 * 1024 // 64MB
|
|
perfWALSize = 16 * 1024 * 1024 // 16MB
|
|
perfOps = 1000 // ops per measurement run
|
|
perfWarmupOps = 200 // warmup ops (discarded from measurement)
|
|
perfIterations = 3 // run N times, report worst as floor
|
|
)
|
|
|
|
// Minimum acceptable floor thresholds (engine-local, single-writer).
|
|
//
|
|
// These are regression gates, not performance targets. Set conservatively
|
|
// so any reasonable hardware passes, but catastrophic regressions
|
|
// (accidental serialization, O(n^2) scan, broken WAL path) are caught.
|
|
//
|
|
// Rationale for values:
|
|
// Measured on dev SSD: rand-write ~10K, rand-read ~80K, seq-write ~30K, seq-read ~180K.
|
|
// Thresholds set at ~10% of measured to tolerate slow CI machines and VMs.
|
|
// Write P99 ceiling at 100ms catches deadlocks/stalls without false-positiving
|
|
// on slow storage.
|
|
var perfFloorGates = map[string]struct {
|
|
MinIOPS float64
|
|
MaxWriteP99 time.Duration // 0 = no ceiling (reads)
|
|
}{
|
|
"rand-write": {MinIOPS: 1000, MaxWriteP99: 100 * time.Millisecond},
|
|
"rand-read": {MinIOPS: 5000},
|
|
"seq-write": {MinIOPS: 2000, MaxWriteP99: 100 * time.Millisecond},
|
|
"seq-read": {MinIOPS: 10000},
|
|
}
|
|
|
|
// perfResult holds measurements for one workload run.
|
|
type perfResult struct {
|
|
Workload string
|
|
Ops int
|
|
Elapsed time.Duration
|
|
IOPS float64
|
|
MBps float64
|
|
LatSamples []int64 // per-op latency in nanoseconds
|
|
}
|
|
|
|
func (r *perfResult) latPct(pct float64) time.Duration {
|
|
if len(r.LatSamples) == 0 {
|
|
return 0
|
|
}
|
|
sorted := make([]int64, len(r.LatSamples))
|
|
copy(sorted, r.LatSamples)
|
|
sort.Slice(sorted, func(i, j int) bool { return sorted[i] < sorted[j] })
|
|
idx := int(math.Ceil(pct/100.0*float64(len(sorted)))) - 1
|
|
if idx < 0 {
|
|
idx = 0
|
|
}
|
|
if idx >= len(sorted) {
|
|
idx = len(sorted) - 1
|
|
}
|
|
return time.Duration(sorted[idx])
|
|
}
|
|
|
|
func (r *perfResult) latAvg() time.Duration {
|
|
if len(r.LatSamples) == 0 {
|
|
return 0
|
|
}
|
|
var sum int64
|
|
for _, s := range r.LatSamples {
|
|
sum += s
|
|
}
|
|
return time.Duration(sum / int64(len(r.LatSamples)))
|
|
}
|
|
|
|
// setupPerfVolume creates a BlockVol configured as Primary for perf measurement.
|
|
func setupPerfVolume(t *testing.T) *blockvol.BlockVol {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
volPath := filepath.Join(dir, "perf.blk")
|
|
vol, err := blockvol.CreateBlockVol(volPath, blockvol.CreateOptions{
|
|
VolumeSize: perfVolumeSize,
|
|
BlockSize: perfBlockSize,
|
|
WALSize: perfWALSize,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Set up as Primary with long lease so writes are allowed.
|
|
if err := vol.HandleAssignment(1, blockvol.RolePrimary, 10*time.Minute); err != nil {
|
|
vol.Close()
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { vol.Close() })
|
|
return vol
|
|
}
|
|
|
|
// maxLBAs returns the number of addressable 4K blocks in the extent area.
|
|
func maxLBAs() uint64 {
|
|
// Volume size minus WAL, divided by block size, with safety margin.
|
|
return (perfVolumeSize - perfWALSize) / perfBlockSize / 2
|
|
}
|
|
|
|
// runPerfWorkload executes one workload measurement and returns the result.
|
|
func runPerfWorkload(t *testing.T, vol *blockvol.BlockVol, workload string, ops int) perfResult {
|
|
t.Helper()
|
|
data := make([]byte, perfBlockSize)
|
|
rand.Read(data)
|
|
max := maxLBAs()
|
|
|
|
samples := make([]int64, 0, ops)
|
|
start := time.Now()
|
|
|
|
for i := 0; i < ops; i++ {
|
|
var lba uint64
|
|
switch {
|
|
case strings.HasPrefix(workload, "rand"):
|
|
lba = uint64(mrand.Int63n(int64(max)))
|
|
default: // sequential
|
|
lba = uint64(i) % max
|
|
}
|
|
|
|
opStart := time.Now()
|
|
switch {
|
|
case strings.HasSuffix(workload, "write"):
|
|
if err := vol.WriteLBA(lba, data); err != nil {
|
|
t.Fatalf("%s op %d: WriteLBA(%d): %v", workload, i, lba, err)
|
|
}
|
|
case strings.HasSuffix(workload, "read"):
|
|
if _, err := vol.ReadLBA(lba, perfBlockSize); err != nil {
|
|
t.Fatalf("%s op %d: ReadLBA(%d): %v", workload, i, lba, err)
|
|
}
|
|
}
|
|
samples = append(samples, time.Since(opStart).Nanoseconds())
|
|
}
|
|
|
|
elapsed := time.Since(start)
|
|
iops := float64(ops) / elapsed.Seconds()
|
|
mbps := iops * float64(perfBlockSize) / (1024 * 1024)
|
|
|
|
return perfResult{
|
|
Workload: workload,
|
|
Ops: ops,
|
|
Elapsed: elapsed,
|
|
IOPS: iops,
|
|
MBps: mbps,
|
|
LatSamples: samples,
|
|
}
|
|
}
|
|
|
|
// floorOf returns the worst (lowest) IOPS and worst (highest) P99 across iterations.
|
|
type perfFloor struct {
|
|
Workload string
|
|
FloorIOPS float64
|
|
FloorMBps float64
|
|
WorstAvg time.Duration
|
|
WorstP50 time.Duration
|
|
WorstP99 time.Duration
|
|
WorstMax time.Duration
|
|
}
|
|
|
|
func computeFloor(results []perfResult) perfFloor {
|
|
f := perfFloor{
|
|
Workload: results[0].Workload,
|
|
FloorIOPS: math.MaxFloat64,
|
|
FloorMBps: math.MaxFloat64,
|
|
}
|
|
for _, r := range results {
|
|
if r.IOPS < f.FloorIOPS {
|
|
f.FloorIOPS = r.IOPS
|
|
}
|
|
if r.MBps < f.FloorMBps {
|
|
f.FloorMBps = r.MBps
|
|
}
|
|
avg := r.latAvg()
|
|
if avg > f.WorstAvg {
|
|
f.WorstAvg = avg
|
|
}
|
|
p50 := r.latPct(50)
|
|
if p50 > f.WorstP50 {
|
|
f.WorstP50 = p50
|
|
}
|
|
p99 := r.latPct(99)
|
|
if p99 > f.WorstP99 {
|
|
f.WorstP99 = p99
|
|
}
|
|
pmax := r.latPct(100)
|
|
if pmax > f.WorstMax {
|
|
f.WorstMax = pmax
|
|
}
|
|
}
|
|
return f
|
|
}
|
|
|
|
// --- Test 1: PerformanceFloor_Bounded ---
|
|
|
|
func TestP12P4_PerformanceFloor_Bounded(t *testing.T) {
|
|
vol := setupPerfVolume(t)
|
|
|
|
workloads := []string{"rand-write", "rand-read", "seq-write", "seq-read"}
|
|
floors := make([]perfFloor, 0, len(workloads))
|
|
|
|
for _, wl := range workloads {
|
|
// Warmup: populate volume with data (needed for reads).
|
|
if strings.HasSuffix(wl, "read") {
|
|
warmupData := make([]byte, perfBlockSize)
|
|
rand.Read(warmupData)
|
|
for i := 0; i < int(maxLBAs()); i++ {
|
|
if err := vol.WriteLBA(uint64(i), warmupData); err != nil {
|
|
break // WAL full is acceptable during warmup
|
|
}
|
|
}
|
|
time.Sleep(200 * time.Millisecond) // let flusher drain
|
|
}
|
|
|
|
// Warmup ops (discarded).
|
|
runPerfWorkload(t, vol, wl, perfWarmupOps)
|
|
|
|
// Measurement: N iterations, take floor.
|
|
var results []perfResult
|
|
for iter := 0; iter < perfIterations; iter++ {
|
|
r := runPerfWorkload(t, vol, wl, perfOps)
|
|
results = append(results, r)
|
|
}
|
|
|
|
floor := computeFloor(results)
|
|
floors = append(floors, floor)
|
|
}
|
|
|
|
// Report structured floor table.
|
|
t.Log("")
|
|
t.Log("=== P12P4 Performance Floor (engine-local, single-writer) ===")
|
|
t.Log("")
|
|
t.Logf("%-12s %10s %8s %10s %10s %10s %10s",
|
|
"Workload", "Floor IOPS", "MB/s", "Avg Lat", "P50 Lat", "P99 Lat", "Max Lat")
|
|
t.Logf("%-12s %10s %8s %10s %10s %10s %10s",
|
|
"--------", "----------", "------", "-------", "-------", "-------", "-------")
|
|
for _, f := range floors {
|
|
t.Logf("%-12s %10.0f %8.2f %10s %10s %10s %10s",
|
|
f.Workload, f.FloorIOPS, f.FloorMBps, f.WorstAvg, f.WorstP50, f.WorstP99, f.WorstMax)
|
|
}
|
|
t.Log("")
|
|
t.Logf("Config: volume=%dMB WAL=%dMB block=%dB ops=%d warmup=%d iterations=%d",
|
|
perfVolumeSize/(1024*1024), perfWALSize/(1024*1024), perfBlockSize, perfOps, perfWarmupOps, perfIterations)
|
|
t.Log("Method: worst of N iterations (floor, not peak)")
|
|
t.Log("Scope: engine-local only; production RF=2 floor in baseline-roce-20260401.md")
|
|
|
|
// Gate: floor values must meet minimum acceptable thresholds.
|
|
// These are regression gates — if any floor drops below the gate,
|
|
// the test fails, blocking rollout.
|
|
t.Log("")
|
|
t.Log("=== Floor Gate Validation ===")
|
|
allGatesPassed := true
|
|
for _, f := range floors {
|
|
gate, ok := perfFloorGates[f.Workload]
|
|
if !ok {
|
|
t.Fatalf("no floor gate defined for workload %s", f.Workload)
|
|
}
|
|
passed := true
|
|
if f.FloorIOPS < gate.MinIOPS {
|
|
t.Errorf("GATE FAIL: %s floor IOPS %.0f < minimum %.0f", f.Workload, f.FloorIOPS, gate.MinIOPS)
|
|
passed = false
|
|
}
|
|
if gate.MaxWriteP99 > 0 && f.WorstP99 > gate.MaxWriteP99 {
|
|
t.Errorf("GATE FAIL: %s worst P99 %s > ceiling %s", f.Workload, f.WorstP99, gate.MaxWriteP99)
|
|
passed = false
|
|
}
|
|
status := "PASS"
|
|
if !passed {
|
|
status = "FAIL"
|
|
allGatesPassed = false
|
|
}
|
|
t.Logf(" %-12s min=%6.0f IOPS → floor=%6.0f [%s]", f.Workload, gate.MinIOPS, f.FloorIOPS, status)
|
|
}
|
|
|
|
if !allGatesPassed {
|
|
t.Fatal("P12P4 PerformanceFloor: FAIL — one or more floor gates not met")
|
|
}
|
|
t.Log("P12P4 PerformanceFloor: PASS — all floor gates met")
|
|
}
|
|
|
|
// --- Test 2: CostCharacterization_Bounded ---
|
|
|
|
func TestP12P4_CostCharacterization_Bounded(t *testing.T) {
|
|
vol := setupPerfVolume(t)
|
|
|
|
// Measure write latency breakdown: WriteLBA includes WAL append + group commit.
|
|
data := make([]byte, perfBlockSize)
|
|
rand.Read(data)
|
|
max := maxLBAs()
|
|
|
|
const costOps = 500
|
|
var writeLatSum int64
|
|
for i := 0; i < costOps; i++ {
|
|
lba := uint64(mrand.Int63n(int64(max)))
|
|
start := time.Now()
|
|
if err := vol.WriteLBA(lba, data); err != nil {
|
|
t.Fatalf("write op %d: %v", i, err)
|
|
}
|
|
writeLatSum += time.Since(start).Nanoseconds()
|
|
}
|
|
avgWriteLat := time.Duration(writeLatSum / costOps)
|
|
|
|
// Measure read latency for comparison.
|
|
// Populate first.
|
|
for i := 0; i < int(max/2); i++ {
|
|
vol.WriteLBA(uint64(i), data)
|
|
}
|
|
time.Sleep(200 * time.Millisecond) // let flusher drain
|
|
|
|
var readLatSum int64
|
|
for i := 0; i < costOps; i++ {
|
|
lba := uint64(mrand.Int63n(int64(max / 2)))
|
|
start := time.Now()
|
|
if _, err := vol.ReadLBA(lba, perfBlockSize); err != nil {
|
|
t.Fatalf("read op %d: %v", i, err)
|
|
}
|
|
readLatSum += time.Since(start).Nanoseconds()
|
|
}
|
|
avgReadLat := time.Duration(readLatSum / costOps)
|
|
|
|
// Cost statement.
|
|
t.Log("")
|
|
t.Log("=== P12P4 Cost Characterization (engine-local) ===")
|
|
t.Log("")
|
|
t.Logf("Average write latency: %s (includes WAL append + group commit sync)", avgWriteLat)
|
|
t.Logf("Average read latency: %s (dirtyMap lookup + WAL/extent read)", avgReadLat)
|
|
t.Log("")
|
|
t.Log("Bounded cost statement:")
|
|
t.Log(" WAL write amplification: 2x minimum (WAL write + eventual extent flush)")
|
|
t.Log(" Group commit: amortizes fdatasync across batched writers (1 sync per batch)")
|
|
t.Log(" Replication tax (production RF=2 sync_all): -56% vs RF=1 (barrier round-trip)")
|
|
t.Log(" Replication tax source: baseline-roce-20260401.md, measured on 25Gbps RoCE")
|
|
t.Log("")
|
|
t.Logf("Write/read ratio: %.1fx (write is %.1fx slower than read)",
|
|
float64(avgWriteLat)/float64(avgReadLat),
|
|
float64(avgWriteLat)/float64(avgReadLat))
|
|
t.Log("")
|
|
t.Logf("Config: volume=%dMB WAL=%dMB block=%dB ops=%d",
|
|
perfVolumeSize/(1024*1024), perfWALSize/(1024*1024), perfBlockSize, costOps)
|
|
|
|
// Proof: cost values are finite and positive.
|
|
if avgWriteLat <= 0 || avgReadLat <= 0 {
|
|
t.Fatal("latency values must be positive")
|
|
}
|
|
if avgWriteLat < avgReadLat {
|
|
t.Log("Note: write faster than read in this run (possible due to WAL cache hits)")
|
|
}
|
|
|
|
t.Log("P12P4 CostCharacterization: PASS — bounded cost statement produced")
|
|
}
|
|
|
|
// --- Test 3: RolloutGate_Bounded ---
|
|
|
|
func TestP12P4_RolloutGate_Bounded(t *testing.T) {
|
|
floorPath := "../../sw-block/.private/phase/phase-12-p4-floor.md"
|
|
gatesPath := "../../sw-block/.private/phase/phase-12-p4-rollout-gates.md"
|
|
baselinePath := "../../learn/projects/sw-block/test/results/baseline-roce-20260401.md"
|
|
blockerPath := "../../sw-block/.private/phase/phase-12-p3-blockers.md"
|
|
|
|
// --- Read all cited evidence sources ---
|
|
|
|
floorData, err := os.ReadFile(floorPath)
|
|
if err != nil {
|
|
t.Fatalf("floor doc must exist at %s: %v", floorPath, err)
|
|
}
|
|
floorContent := string(floorData)
|
|
|
|
gatesData, err := os.ReadFile(gatesPath)
|
|
if err != nil {
|
|
t.Fatalf("rollout-gates doc must exist at %s: %v", gatesPath, err)
|
|
}
|
|
gatesContent := string(gatesData)
|
|
|
|
baselineData, err := os.ReadFile(baselinePath)
|
|
if err != nil {
|
|
t.Fatalf("cited baseline must exist at %s: %v", baselinePath, err)
|
|
}
|
|
baselineContent := string(baselineData)
|
|
|
|
blockerData, err := os.ReadFile(blockerPath)
|
|
if err != nil {
|
|
t.Fatalf("cited blocker ledger must exist at %s: %v", blockerPath, err)
|
|
}
|
|
blockerContent := string(blockerData)
|
|
|
|
// --- Structural validation (shape) ---
|
|
|
|
// Floor doc: workload envelope, floor table, non-claims.
|
|
for _, required := range []string{"RF=2", "sync_all", "4K random write", "4K random read", "sequential write", "sequential read"} {
|
|
if !strings.Contains(floorContent, required) {
|
|
t.Fatalf("floor doc missing required content: %q", required)
|
|
}
|
|
}
|
|
if !strings.Contains(floorContent, "Floor") || !strings.Contains(floorContent, "IOPS") {
|
|
t.Fatal("floor doc must contain floor table with IOPS")
|
|
}
|
|
if !strings.Contains(floorContent, "does NOT") {
|
|
t.Fatal("floor doc must contain explicit non-claims")
|
|
}
|
|
|
|
// Gates doc: gates table, launch envelope, exclusions, non-claims.
|
|
if !strings.Contains(gatesContent, "Gate") || !strings.Contains(gatesContent, "Status") {
|
|
t.Fatal("rollout-gates doc must contain gates table")
|
|
}
|
|
if !strings.Contains(gatesContent, "Launch Envelope") {
|
|
t.Fatal("rollout-gates doc must contain launch envelope")
|
|
}
|
|
if !strings.Contains(gatesContent, "Exclusion") {
|
|
t.Fatal("rollout-gates doc must contain exclusions")
|
|
}
|
|
if !strings.Contains(gatesContent, "does NOT") {
|
|
t.Fatal("rollout-gates doc must contain explicit non-claims")
|
|
}
|
|
|
|
// Count gates — must be finite.
|
|
gateLines := 0
|
|
for _, line := range strings.Split(gatesContent, "\n") {
|
|
trimmed := strings.TrimSpace(line)
|
|
if strings.HasPrefix(trimmed, "| G") || strings.HasPrefix(trimmed, "| E") {
|
|
gateLines++
|
|
}
|
|
}
|
|
if gateLines == 0 {
|
|
t.Fatal("rollout-gates doc has no gate items")
|
|
}
|
|
if gateLines > 20 {
|
|
t.Fatalf("rollout-gates doc should be finite, got %d items", gateLines)
|
|
}
|
|
|
|
// --- Semantic cross-checks (evidence alignment) ---
|
|
|
|
// 1. G6 cites "28.4K write IOPS" — baseline must contain this number.
|
|
if strings.Contains(gatesContent, "28.4K write IOPS") || strings.Contains(gatesContent, "28,4") {
|
|
// The gates doc cites write IOPS from baseline. Verify the baseline has it.
|
|
if !strings.Contains(baselineContent, "28,") {
|
|
t.Fatal("G6 cites write IOPS but baseline does not contain matching value")
|
|
}
|
|
}
|
|
// More precise: baseline must contain the specific numbers cited in G6.
|
|
if !strings.Contains(baselineContent, "28,347") && !strings.Contains(baselineContent, "28,429") &&
|
|
!strings.Contains(baselineContent, "28,453") {
|
|
t.Fatal("baseline must contain RF=2 sync_all write IOPS data (28,3xx-28,4xx range)")
|
|
}
|
|
if !strings.Contains(baselineContent, "136,648") {
|
|
t.Fatal("baseline must contain RF=2 read IOPS data (136,648)")
|
|
}
|
|
|
|
// 2. G5 cites "-56% replication tax" — baseline must contain this.
|
|
if strings.Contains(gatesContent, "-56%") {
|
|
if !strings.Contains(baselineContent, "-56%") {
|
|
t.Fatal("G5 cites -56% replication tax but baseline does not contain -56%")
|
|
}
|
|
}
|
|
|
|
// 3. Launch envelope claims specific transport/network combos — verify against baseline.
|
|
// Claimed: NVMe-TCP @ 25Gbps RoCE
|
|
if strings.Contains(gatesContent, "NVMe-TCP @ 25Gbps RoCE") {
|
|
if !strings.Contains(baselineContent, "NVMe-TCP") || !strings.Contains(baselineContent, "RoCE") {
|
|
t.Fatal("launch envelope claims NVMe-TCP @ RoCE but baseline has no such data")
|
|
}
|
|
}
|
|
// Claimed: iSCSI @ 25Gbps RoCE
|
|
if strings.Contains(gatesContent, "iSCSI @ 25Gbps RoCE") {
|
|
if !strings.Contains(baselineContent, "iSCSI") || !strings.Contains(baselineContent, "RoCE") {
|
|
t.Fatal("launch envelope claims iSCSI @ RoCE but baseline has no such data")
|
|
}
|
|
}
|
|
// Claimed: iSCSI @ 1Gbps
|
|
if strings.Contains(gatesContent, "iSCSI @ 1Gbps") {
|
|
if !strings.Contains(baselineContent, "iSCSI") || !strings.Contains(baselineContent, "1Gbps") {
|
|
t.Fatal("launch envelope claims iSCSI @ 1Gbps but baseline has no such data")
|
|
}
|
|
}
|
|
// Exclusion: NVMe-TCP @ 1Gbps must NOT be claimed as supported.
|
|
if strings.Contains(gatesContent, "NOT included") {
|
|
// Verify baseline indeed lacks NVMe-TCP @ 1Gbps.
|
|
hasNvme1g := strings.Contains(baselineContent, "NVMe-TCP") && strings.Contains(baselineContent, "| NVMe-TCP | 1Gbps")
|
|
if hasNvme1g {
|
|
t.Fatal("baseline contains NVMe-TCP @ 1Gbps data but gates doc excludes it — resolve mismatch")
|
|
}
|
|
}
|
|
|
|
// 4. G7 cites blocker ledger counts — verify against actual ledger.
|
|
if strings.Contains(gatesContent, "3 diagnosed") {
|
|
diagCount := 0
|
|
for _, line := range strings.Split(blockerContent, "\n") {
|
|
if strings.HasPrefix(strings.TrimSpace(line), "| B") {
|
|
diagCount++
|
|
}
|
|
}
|
|
if diagCount != 3 {
|
|
t.Fatalf("G7 claims 3 diagnosed blockers but ledger has %d", diagCount)
|
|
}
|
|
}
|
|
if strings.Contains(gatesContent, "3 unresolved") {
|
|
unresCount := 0
|
|
for _, line := range strings.Split(blockerContent, "\n") {
|
|
if strings.HasPrefix(strings.TrimSpace(line), "| U") {
|
|
unresCount++
|
|
}
|
|
}
|
|
if unresCount != 3 {
|
|
t.Fatalf("G7 claims 3 unresolved blockers but ledger has %d", unresCount)
|
|
}
|
|
}
|
|
|
|
// 5. Floor doc gate thresholds must match code-defined gates.
|
|
for workload, gate := range perfFloorGates {
|
|
// The doc uses comma-formatted numbers (e.g., "1,000" or "5,000").
|
|
minInt := int(gate.MinIOPS)
|
|
// Check for both comma-formatted and plain forms.
|
|
found := false
|
|
for _, form := range []string{
|
|
fmt.Sprintf("%d", minInt), // "1000"
|
|
fmt.Sprintf("%d,%03d", minInt/1000, minInt%1000), // "1,000"
|
|
} {
|
|
if strings.Contains(floorContent, form) {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Errorf("floor doc gate for %s should cite minimum %d IOPS but doesn't", workload, minInt)
|
|
}
|
|
}
|
|
|
|
t.Logf("P12P4 RolloutGate: floor doc %d bytes, gates doc %d bytes, %d gate items",
|
|
len(floorData), len(gatesData), gateLines)
|
|
t.Log("P12P4 RolloutGate: semantic cross-checks passed (baseline, blocker ledger, gate thresholds)")
|
|
t.Log("P12P4 RolloutGate: PASS — bounded launch envelope with verified evidence alignment")
|
|
}
|
|
|
|
// --- Helpers ---
|
|
|
|
func init() {
|
|
// Seed random for reproducible LBA patterns within a test run.
|
|
mrand.Seed(time.Now().UnixNano())
|
|
}
|
|
|
|
// formatDuration formats a duration for table display.
|
|
func formatDuration(d time.Duration) string {
|
|
if d < time.Microsecond {
|
|
return fmt.Sprintf("%dns", d.Nanoseconds())
|
|
}
|
|
if d < time.Millisecond {
|
|
return fmt.Sprintf("%.1fus", float64(d.Nanoseconds())/1000.0)
|
|
}
|
|
return fmt.Sprintf("%.2fms", float64(d.Nanoseconds())/1e6)
|
|
}
|