Files
seaweedfs/weed/storage/blockvol/testrunner/actions/bench_test.go
T
Ping QiuandClaude Opus 4.6 4c5f9f2b9d feat: CP10B-1 NVMe/TCP RX/TX split + CP10B-2 bench/profiling fixes
RX/TX split: rxLoop reads PDUs, txLoop writes responses via respCh.
Handlers refactored to void + enqueueResponse pattern. IOCCSZ fix
enables inline write data (100K IOPS vs 15K before). R2T deadlock
fix via completeWaiters. Shutdown cleans up pendingCapsules buffers.

Bench: ParseFioMetric accepts plain/quoted numbers for aggregated
medians. Profiling actions: pprof_capture, vmstat_capture, iostat_capture.

196 NVMe tests, 92 testrunner actions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 15:09:41 -07:00

413 lines
9.8 KiB
Go

package actions
import (
"math"
"testing"
)
// Realistic fio JSON output for testing parse logic.
const fioWriteJSON = `{
"fio version": "fio-3.33",
"jobs": [{
"jobname": "bench",
"read": {
"iops": 0,
"bw_bytes": 0,
"lat_ns": {"mean": 0, "percentile": {}}
},
"write": {
"iops": 49832.5,
"bw_bytes": 204113920,
"lat_ns": {
"mean": 19823.4,
"percentile": {
"50.000000": 18000,
"99.000000": 45000,
"99.900000": 82000
}
}
}
}]
}`
const fioReadJSON = `{
"jobs": [{
"jobname": "bench",
"read": {
"iops": 62100.0,
"bw_bytes": 254361600,
"lat_ns": {
"mean": 15200.0,
"percentile": {
"50.000000": 14000,
"99.000000": 32000,
"99.900000": 58000
}
}
},
"write": {
"iops": 0,
"bw_bytes": 0,
"lat_ns": {"mean": 0, "percentile": {}}
}
}]
}`
const fioMixedJSON = `{
"jobs": [{
"jobname": "bench",
"read": {
"iops": 35000.0,
"bw_bytes": 143360000,
"lat_ns": {
"mean": 22000.0,
"percentile": {
"50.000000": 20000,
"99.000000": 55000,
"99.900000": 95000
}
}
},
"write": {
"iops": 15000.0,
"bw_bytes": 61440000,
"lat_ns": {
"mean": 28000.0,
"percentile": {
"50.000000": 25000,
"99.000000": 65000,
"99.900000": 120000
}
}
}
}]
}`
func TestParseFioMetric_WriteIOPS(t *testing.T) {
val, err := ParseFioMetric(fioWriteJSON, "iops", "")
if err != nil {
t.Fatalf("parse: %v", err)
}
if val != 49832.5 {
t.Fatalf("iops = %f, want 49832.5", val)
}
}
func TestParseFioMetric_WriteBW(t *testing.T) {
val, err := ParseFioMetric(fioWriteJSON, "bw_mb", "")
if err != nil {
t.Fatalf("parse: %v", err)
}
expected := 204113920.0 / (1024 * 1024)
if math.Abs(val-expected) > 0.1 {
t.Fatalf("bw_mb = %f, want %f", val, expected)
}
}
func TestParseFioMetric_WriteLatency(t *testing.T) {
val, err := ParseFioMetric(fioWriteJSON, "lat_mean_us", "")
if err != nil {
t.Fatalf("parse: %v", err)
}
expected := 19823.4 / 1000 // ns to µs
if math.Abs(val-expected) > 0.01 {
t.Fatalf("lat_mean_us = %f, want %f", val, expected)
}
}
func TestParseFioMetric_WriteP99(t *testing.T) {
val, err := ParseFioMetric(fioWriteJSON, "lat_p99_us", "")
if err != nil {
t.Fatalf("parse: %v", err)
}
expected := 45000.0 / 1000 // 45 µs
if math.Abs(val-expected) > 0.01 {
t.Fatalf("lat_p99_us = %f, want %f", val, expected)
}
}
func TestParseFioMetric_ReadIOPS(t *testing.T) {
val, err := ParseFioMetric(fioReadJSON, "iops", "")
if err != nil {
t.Fatalf("parse: %v", err)
}
if val != 62100.0 {
t.Fatalf("iops = %f, want 62100.0", val)
}
}
func TestParseFioMetric_ExplicitDirection(t *testing.T) {
// Mixed workload, explicitly request read.
val, err := ParseFioMetric(fioMixedJSON, "iops", "read")
if err != nil {
t.Fatalf("parse: %v", err)
}
if val != 35000.0 {
t.Fatalf("read iops = %f, want 35000.0", val)
}
// Explicitly request write.
val, err = ParseFioMetric(fioMixedJSON, "iops", "write")
if err != nil {
t.Fatalf("parse: %v", err)
}
if val != 15000.0 {
t.Fatalf("write iops = %f, want 15000.0", val)
}
}
func TestParseFioMetric_AutoDetect(t *testing.T) {
// Write-only JSON: auto should pick write.
val, err := ParseFioMetric(fioWriteJSON, "iops", "")
if err != nil {
t.Fatalf("parse: %v", err)
}
if val != 49832.5 {
t.Fatalf("auto-detect write: iops = %f, want 49832.5", val)
}
// Read-only JSON: auto should pick read (write IOPS=0).
val, err = ParseFioMetric(fioReadJSON, "iops", "")
if err != nil {
t.Fatalf("parse: %v", err)
}
if val != 62100.0 {
t.Fatalf("auto-detect read: iops = %f, want 62100.0", val)
}
}
func TestParseFioMetric_UnknownMetric(t *testing.T) {
_, err := ParseFioMetric(fioWriteJSON, "nonexistent", "")
if err == nil {
t.Fatal("expected error for unknown metric")
}
}
func TestParseFioMetric_PlainNumber(t *testing.T) {
val, err := ParseFioMetric("15322.00", "iops", "")
if err != nil {
t.Fatalf("parse plain number: %v", err)
}
if val != 15322.00 {
t.Fatalf("got %f, want 15322.00", val)
}
}
func TestParseFioMetric_PlainInteger(t *testing.T) {
val, err := ParseFioMetric("36853", "iops", "")
if err != nil {
t.Fatalf("parse plain integer: %v", err)
}
if val != 36853 {
t.Fatalf("got %f, want 36853", val)
}
}
func TestParseFioMetric_QuotedNumber(t *testing.T) {
val, err := ParseFioMetric(`"15322.00"`, "iops", "")
if err != nil {
t.Fatalf("parse quoted number: %v", err)
}
if val != 15322.00 {
t.Fatalf("got %f, want 15322.00", val)
}
}
func TestParseFioMetric_NumberWithWhitespace(t *testing.T) {
val, err := ParseFioMetric(" 42000.50 \n", "iops", "")
if err != nil {
t.Fatalf("parse number with whitespace: %v", err)
}
if val != 42000.50 {
t.Fatalf("got %f, want 42000.50", val)
}
}
func TestParseFioMetric_InvalidInput(t *testing.T) {
_, err := ParseFioMetric("not a number or json", "iops", "")
if err == nil {
t.Fatal("expected error for invalid input")
}
}
func TestParseFioMetric_InvalidJSON(t *testing.T) {
_, err := ParseFioMetric("{invalid json}", "iops", "")
if err == nil {
t.Fatal("expected error for invalid JSON")
}
}
func TestParseFioMetric_EmptyJobs(t *testing.T) {
_, err := ParseFioMetric(`{"jobs":[]}`, "iops", "")
if err == nil {
t.Fatal("expected error for empty jobs")
}
}
func TestComputeBenchResult_ThroughputPass(t *testing.T) {
r := ComputeBenchResult("4k-randwrite", "iops", 49000, 52000, 1.0)
if !r.Pass {
t.Fatalf("expected pass: ratio=%.3f", r.Ratio)
}
if r.Ratio < 1.0 {
t.Fatalf("ratio = %.3f, want >= 1.0", r.Ratio)
}
}
func TestComputeBenchResult_ThroughputFail(t *testing.T) {
r := ComputeBenchResult("4k-randwrite", "iops", 49000, 40000, 1.0)
if r.Pass {
t.Fatal("expected fail: candidate < baseline")
}
}
func TestComputeBenchResult_ThroughputWarn(t *testing.T) {
// candidate = 92% of baseline, gate = 1.0 → fail but ratio >= 0.9
r := ComputeBenchResult("4k-randwrite", "iops", 50000, 46000, 1.0)
if r.Pass {
t.Fatal("expected fail")
}
if r.Ratio < 0.9 {
t.Fatalf("ratio = %.3f, expected >= 0.9 for WARN", r.Ratio)
}
}
func TestComputeBenchResult_LatencyPass(t *testing.T) {
// Latency: lower candidate is better. baseline=45µs, candidate=32µs → good.
r := ComputeBenchResult("4k-randwrite", "lat_p99_us", 45.0, 32.0, 1.0)
if !r.Pass {
t.Fatalf("expected pass: candidate latency lower. ratio=%.3f", r.Ratio)
}
// Ratio should be baseline/candidate = 45/32 ≈ 1.406
if r.Ratio < 1.0 {
t.Fatalf("ratio = %.3f, want > 1.0 (latency decreased)", r.Ratio)
}
}
func TestComputeBenchResult_LatencyFail(t *testing.T) {
// Latency: candidate is higher → bad.
r := ComputeBenchResult("4k-randwrite", "lat_p99_us", 45.0, 60.0, 1.0)
if r.Pass {
t.Fatal("expected fail: candidate latency higher")
}
}
func TestComputeBenchResult_ZeroBaseline(t *testing.T) {
r := ComputeBenchResult("test", "iops", 0, 100, 1.0)
if r.Pass {
t.Fatal("expected fail with zero baseline")
}
}
func TestFormatBenchReport(t *testing.T) {
results := []BenchResult{
ComputeBenchResult("4k-rw j=1 qd=1", "iops", 12000, 14000, 1.0),
ComputeBenchResult("4k-rw j=4 qd=32", "iops", 49000, 62000, 1.0),
ComputeBenchResult("4k-rw j=4 qd=32", "lat_p99_us", 45.0, 32.0, 1.0),
}
report := FormatBenchReport(results)
if report == "" {
t.Fatal("empty report")
}
// Should contain all three workloads.
for _, r := range results {
if !contains(report, r.Workload) {
t.Errorf("report missing workload %q", r.Workload)
}
}
// All should pass.
for _, r := range results {
if !r.Pass {
t.Errorf("expected pass for %s", r.Workload)
}
}
}
func contains(s, substr string) bool {
return len(s) > 0 && len(substr) > 0 && findSubstr(s, substr)
}
func findSubstr(s, substr string) bool {
for i := 0; i <= len(s)-len(substr); i++ {
if s[i:i+len(substr)] == substr {
return true
}
}
return false
}
func TestParsePgbenchTPS(t *testing.T) {
tests := []struct {
name string
output string
want string
}{
{
"standard TPC-B output",
`pgbench (PostgreSQL 16.1)
starting vacuum...end.
transaction type: <builtin: TPC-B (sort of)>
scaling factor: 10
query mode: simple
number of clients: 16
number of threads: 16
maximum number of seconds of each test: 30
number of transactions actually processed: 45678
number of failed transactions: 0 (0.000%)
latency average = 10.500 ms
initial connection time = 12.345 ms
tps = 1522.600000 (without initial connection time)`,
"1522.600000",
},
{
"select only",
`tps = 89456.123456 (without initial connection time)`,
"89456.123456",
},
{
"no match",
"some random output",
"",
},
{
"skip initial connection line",
`initial connection time = 5.678 ms
tps = 2345.678901 (without initial connection time)`,
"2345.678901",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := parsePgbenchTPS(tt.output)
if got != tt.want {
t.Errorf("parsePgbenchTPS() = %q, want %q", got, tt.want)
}
})
}
}
func TestTrimValues(t *testing.T) {
// 10 values, trim 20% = remove 2 from each end, keep 6
values := []float64{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
trimmed := trimValues(values, 20)
if len(trimmed) != 6 {
t.Fatalf("trimValues(10, 20%%) = %d values, want 6", len(trimmed))
}
// Should be [3, 4, 5, 6, 7, 8]
if trimmed[0] != 3 || trimmed[len(trimmed)-1] != 8 {
t.Errorf("trimmed = %v, want [3..8]", trimmed)
}
}
func TestTargetSpecNQN(t *testing.T) {
// Test is in actions package — import testrunner types.
// TargetSpec is in testrunner package, so we test the NQN suffix logic
// by verifying the format.
nqn := "nqn.2024-01.com.seaweedfs:vol." + "bench-vol"
if nqn != "nqn.2024-01.com.seaweedfs:vol.bench-vol" {
t.Fatalf("NQN format wrong: %s", nqn)
}
}