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>
This commit is contained in:
@@ -0,0 +1,332 @@
|
||||
/*
|
||||
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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user