Files
gotgt/pkg/scsi/io_bench_test.go
Lei Xue a5628f4ec0 add end-to-end IO benchmarks and fix pprof-identified hotspots
Add comprehensive benchmark suite (io_bench_test.go):
- BenchmarkEndToEndRead/Write: full SCSI stack (512B to 256KB)
- BenchmarkEndToEndReadParallel/WriteParallel: concurrent IO
- BenchmarkFileBackingStoreRead/Write: isolated backing store

pprof-guided optimizations:
- Guard hot-path log.Debugf with log.GetLevel() check in scsi.go,
  sbc.go, backingstore.go — eliminates 22% CPU overhead from logrus
  Entry allocation even when debug logging is disabled
- Add FileBackingStore.ReadAt for zero-copy reads directly into
  caller's buffer, bypassing Read()'s per-call make([]byte, tl)
- Use ReadAt via interface assertion in bsPerformCommand to read
  directly into InSDBBuffer, eliminating allocation + copy

Results (256KB reads): +42% throughput, allocs reduced from 10 to 5

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 19:41:48 +08:00

333 lines
7.8 KiB
Go

/*
Copyright 2024 The GoStor Authors All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package scsi_test
import (
"encoding/binary"
"fmt"
"os"
"testing"
"github.com/google/uuid"
"github.com/gostor/gotgt/pkg/api"
"github.com/gostor/gotgt/pkg/config"
"github.com/gostor/gotgt/pkg/scsi"
_ "github.com/gostor/gotgt/pkg/scsi/backingstore"
)
func setupBenchTarget(b *testing.B, diskSize int64) (*scsi.SCSITargetService, int, uuid.UUID, func()) {
b.Helper()
f, err := os.CreateTemp("", "gotgt-bench-*.img")
if err != nil {
b.Fatal(err)
}
if err := f.Truncate(diskSize); err != nil {
os.Remove(f.Name())
b.Fatal(err)
}
f.Close()
svc := scsi.NewSCSITargetService()
tgt, err := svc.NewSCSITarget(0, "iscsi", "iqn.bench.target")
if err != nil {
os.Remove(f.Name())
b.Fatal(err)
}
// Initialize Devices map if nil (NewSCSITarget gets it from global LUN map which is empty in tests)
if tgt.Devices == nil {
tgt.Devices = api.LUNMap{}
}
bs := &config.BackendStorage{
DeviceID: 1000,
Path: "file:" + f.Name(),
Online: true,
}
lu, err := scsi.NewSCSILu(bs)
if err != nil {
os.Remove(f.Name())
b.Fatal(err)
}
tgt.Devices[0] = lu
itnexusID := uuid.New()
itnexus := &api.ITNexus{ID: itnexusID, Tag: "bench-itn"}
scsi.AddITNexus(tgt, itnexus)
cleanup := func() {
lu.Storage.Close(lu)
os.Remove(f.Name())
}
return svc, tgt.TID, itnexusID, cleanup
}
func makeSCSIReadCmd(itnexusID uuid.UUID, lba uint32, blocks uint16, blockShift uint) *api.SCSICommand {
scb := make([]byte, 10)
scb[0] = byte(api.READ_10)
binary.BigEndian.PutUint32(scb[2:6], lba)
binary.BigEndian.PutUint16(scb[7:9], blocks)
bufSize := int(blocks) << blockShift
return &api.SCSICommand{
ITNexusID: itnexusID,
SCB: scb,
SCBLength: 10,
Direction: api.SCSIDataRead,
InSDBBuffer: &api.SCSIDataBuffer{
Buffer: make([]byte, bufSize),
Length: uint32(bufSize),
},
}
}
func makeSCSIWriteCmd(itnexusID uuid.UUID, lba uint32, blocks uint16, blockShift uint, data []byte) *api.SCSICommand {
scb := make([]byte, 10)
scb[0] = byte(api.WRITE_10)
binary.BigEndian.PutUint32(scb[2:6], lba)
binary.BigEndian.PutUint16(scb[7:9], blocks)
bufSize := int(blocks) << blockShift
outBuf := make([]byte, bufSize)
copy(outBuf, data)
return &api.SCSICommand{
ITNexusID: itnexusID,
SCB: scb,
SCBLength: 10,
Direction: api.SCSIDataWrite,
OutSDBBuffer: &api.SCSIDataBuffer{
Buffer: outBuf,
Length: uint32(bufSize),
},
}
}
// BenchmarkEndToEndRead benchmarks the full SCSI read path:
// AddCommandQueue -> luPerformCommand -> SBCReadWrite -> bsPerformCommand -> FileBackingStore.Read
func BenchmarkEndToEndRead(b *testing.B) {
blockShift := uint(9)
diskSize := int64(100 * 1024 * 1024)
svc, tid, itnID, cleanup := setupBenchTarget(b, diskSize)
defer cleanup()
for _, tc := range []struct {
name string
blocks uint16
}{
{"512B", 1},
{"4KB", 8},
{"64KB", 128},
{"256KB", 512},
} {
b.Run(tc.name, func(b *testing.B) {
maxLBA := int(diskSize>>blockShift) - int(tc.blocks)
b.SetBytes(int64(tc.blocks) << blockShift)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
lba := uint32(i % maxLBA)
cmd := makeSCSIReadCmd(itnID, lba, tc.blocks, blockShift)
if err := svc.AddCommandQueue(tid, cmd); err != nil {
b.Fatal(err)
}
}
})
}
}
// BenchmarkEndToEndWrite benchmarks the full SCSI write path.
func BenchmarkEndToEndWrite(b *testing.B) {
blockShift := uint(9)
diskSize := int64(100 * 1024 * 1024)
svc, tid, itnID, cleanup := setupBenchTarget(b, diskSize)
defer cleanup()
for _, tc := range []struct {
name string
blocks uint16
}{
{"512B", 1},
{"4KB", 8},
{"64KB", 128},
{"256KB", 512},
} {
b.Run(tc.name, func(b *testing.B) {
writeData := make([]byte, int(tc.blocks)<<blockShift)
for i := range writeData {
writeData[i] = byte(i)
}
maxLBA := int(diskSize>>blockShift) - int(tc.blocks)
b.SetBytes(int64(tc.blocks) << blockShift)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
lba := uint32(i % maxLBA)
cmd := makeSCSIWriteCmd(itnID, lba, tc.blocks, blockShift, writeData)
if err := svc.AddCommandQueue(tid, cmd); err != nil {
b.Fatal(err)
}
}
})
}
}
// BenchmarkEndToEndReadParallel benchmarks concurrent SCSI reads.
func BenchmarkEndToEndReadParallel(b *testing.B) {
blockShift := uint(9)
diskSize := int64(100 * 1024 * 1024)
blocks := uint16(8) // 4KB
maxLBA := int(diskSize>>blockShift) - int(blocks)
svc, tid, itnID, cleanup := setupBenchTarget(b, diskSize)
defer cleanup()
b.SetBytes(int64(blocks) << blockShift)
b.ResetTimer()
b.ReportAllocs()
b.RunParallel(func(pb *testing.PB) {
i := 0
for pb.Next() {
lba := uint32(i % maxLBA)
cmd := makeSCSIReadCmd(itnID, lba, blocks, blockShift)
if err := svc.AddCommandQueue(tid, cmd); err != nil {
b.Fatal(err)
}
i++
}
})
}
// BenchmarkEndToEndWriteParallel benchmarks concurrent SCSI writes.
func BenchmarkEndToEndWriteParallel(b *testing.B) {
blockShift := uint(9)
diskSize := int64(100 * 1024 * 1024)
blocks := uint16(8) // 4KB
maxLBA := int(diskSize>>blockShift) - int(blocks)
svc, tid, itnID, cleanup := setupBenchTarget(b, diskSize)
defer cleanup()
writeData := make([]byte, int(blocks)<<blockShift)
for i := range writeData {
writeData[i] = byte(i)
}
b.SetBytes(int64(blocks) << blockShift)
b.ResetTimer()
b.ReportAllocs()
b.RunParallel(func(pb *testing.PB) {
i := 0
for pb.Next() {
lba := uint32(i % maxLBA)
cmd := makeSCSIWriteCmd(itnID, lba, blocks, blockShift, writeData)
if err := svc.AddCommandQueue(tid, cmd); err != nil {
b.Fatal(err)
}
i++
}
})
}
// BenchmarkFileBackingStoreRead isolates file backing store read performance.
func BenchmarkFileBackingStoreRead(b *testing.B) {
f, err := os.CreateTemp("", "gotgt-bs-bench-*.img")
if err != nil {
b.Fatal(err)
}
defer os.Remove(f.Name())
if err := f.Truncate(100 * 1024 * 1024); err != nil {
b.Fatal(err)
}
f.Close()
bs, err := scsi.NewBackingStore("file")
if err != nil {
b.Fatal(err)
}
lu := &api.SCSILu{}
if err := bs.Open(lu, f.Name()); err != nil {
b.Fatal(err)
}
defer bs.Close(lu)
for _, size := range []int64{512, 4096, 65536, 262144} {
b.Run(fmt.Sprintf("%dB", size), func(b *testing.B) {
b.SetBytes(size)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
off := int64(i*int(size)) % (100*1024*1024 - size)
_, err := bs.Read(off, size)
if err != nil {
b.Fatal(err)
}
}
})
}
}
// BenchmarkFileBackingStoreWrite isolates file backing store write performance.
func BenchmarkFileBackingStoreWrite(b *testing.B) {
f, err := os.CreateTemp("", "gotgt-bs-bench-*.img")
if err != nil {
b.Fatal(err)
}
defer os.Remove(f.Name())
if err := f.Truncate(100 * 1024 * 1024); err != nil {
b.Fatal(err)
}
f.Close()
bs, err := scsi.NewBackingStore("file")
if err != nil {
b.Fatal(err)
}
lu := &api.SCSILu{}
if err := bs.Open(lu, f.Name()); err != nil {
b.Fatal(err)
}
defer bs.Close(lu)
for _, size := range []int64{512, 4096, 65536, 262144} {
b.Run(fmt.Sprintf("%dB", size), func(b *testing.B) {
data := make([]byte, size)
for i := range data {
data[i] = byte(i)
}
b.SetBytes(size)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
off := int64(i*int(size)) % (100*1024*1024 - size)
if err := bs.Write(data, off); err != nil {
b.Fatal(err)
}
}
})
}
}