Files
petrbalvin a9b8039ef7
Test / test (push) Successful in 2m4s
Release / gates (push) Successful in 2m5s
Release / build (amd64, freebsd) (push) Successful in 1m27s
Release / build (amd64, linux) (push) Successful in 1m22s
Release / build (amd64, netbsd) (push) Successful in 1m19s
Release / build (amd64, openbsd) (push) Successful in 1m20s
Release / build (arm64, darwin) (push) Successful in 1m21s
Release / build (arm64, freebsd) (push) Successful in 1m26s
Release / build (arm64, linux) (push) Successful in 1m25s
Release / build (arm64, netbsd) (push) Successful in 1m31s
Release / build (arm64, openbsd) (push) Successful in 1m27s
Release / build (loong64, linux) (push) Successful in 1m37s
Release / build (riscv64, linux) (push) Successful in 1m21s
Release / release (push) Successful in 40s
feat: full NFSv4.2 server and client in pure Go
Assisted-by: GLM 5.3 Flash
2026-09-21 18:51:17 +02:00

203 lines
5.5 KiB
Go

// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
// The wire level benchmarks: the server handler and the client library
// over a loopback connection, so one operation carries the whole stack,
// RPC and XDR and sessions included, on top of the backend.
package nfs4server_test
import (
"context"
"net"
"os"
"path/filepath"
"testing"
"sourcedock.dev/petrbalvin/nfs/internal/nfs4"
"sourcedock.dev/petrbalvin/nfs/internal/nfs4server"
"sourcedock.dev/petrbalvin/nfs/internal/nfsclient"
"sourcedock.dev/petrbalvin/nfs/internal/nfsfs"
"sourcedock.dev/petrbalvin/nfs/internal/server"
"sourcedock.dev/petrbalvin/nfs/internal/xdr"
)
const wireChunk = 64 << 10
// benchWire serves one export over loopback, dials the client library at
// it and returns the client and the handle of one regular file of the
// given size. Setup runs once, outside the measured region.
func benchWire(b *testing.B, fileSize int) (*nfsclient.Client, []byte) {
b.Helper()
root := b.TempDir()
backend, err := nfsfs.NewLocal(root)
if err != nil {
b.Fatalf("NewLocal: %v", err)
}
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
b.Fatalf("Listen: %v", err)
}
ctx, stop := context.WithCancel(context.Background())
srv := &server.Server{Handle: (&nfs4server.Handler{FS: backend}).HandleConn}
go func() { _ = srv.Serve(ctx, ln) }()
b.Cleanup(stop)
name := "benchfile"
if fileSize > 0 {
p := filepath.Join(root, name)
buf := make([]byte, 1<<20)
for i := range buf {
buf[i] = byte(i)
}
f, err := os.Create(p)
if err != nil {
b.Fatalf("Create: %v", err)
}
for written := 0; written < fileSize; written += len(buf) {
if _, err := f.Write(buf); err != nil {
b.Fatalf("Write: %v", err)
}
}
if err := f.Close(); err != nil {
b.Fatalf("Close: %v", err)
}
}
cl, err := nfsclient.Dial(ln.Addr().String())
if err != nil {
b.Fatalf("Dial: %v", err)
}
b.Cleanup(func() { _ = cl.Close() })
if err := cl.Establish("bench"); err != nil {
b.Fatalf("Establish: %v", err)
}
res, bodies, err := cl.Compound("bench-lookup", [][]byte{
nfs4.AppendPutRootfh(nil),
nfs4.AppendLookup(nil, name),
nfs4.AppendGetfh(nil),
})
if err != nil {
b.Fatalf("Compound: %v", err)
}
if res.Status != nfs4.ErrOK {
b.Fatalf("lookup status %d", res.Status)
}
fh, err := xdr.NewDecoder(bodies[len(bodies)-1]).VarOpaque()
if err != nil {
b.Fatalf("decode fh: %v", err)
}
return cl, fh
}
// compound runs one COMPOUND and requires NFS4ERR_SUCCESS of it.
func compound(b *testing.B, cl *nfsclient.Client, tag string, ops [][]byte) [][]byte {
b.Helper()
res, bodies, err := cl.Compound(tag, ops)
if err != nil {
b.Fatalf("%s: %v", tag, err)
}
if res.Status != nfs4.ErrOK {
b.Fatalf("%s: nfs status %d", tag, res.Status)
}
return bodies
}
// BenchmarkWireRead64K reads 64 KiB per COMPOUND from a 64 MiB file over
// the loopback connection, cycling through the offsets.
func BenchmarkWireRead64K(b *testing.B) {
cl, fh := benchWire(b, 64<<20)
off := uint64(0)
b.SetBytes(wireChunk)
b.ResetTimer()
for b.Loop() {
bodies := compound(b, cl, "bench-read", [][]byte{
nfs4.AppendPutfh(nil, fh),
nfs4.AppendReadArgs(nil, nfs4.Stateid{}, off, wireChunk),
})
d := xdr.NewDecoder(bodies[len(bodies)-1])
if _, err := d.Bool(); err != nil {
b.Fatalf("decode eof: %v", err)
}
if _, err := d.VarOpaque(); err != nil {
b.Fatalf("decode data: %v", err)
}
off += wireChunk
if off > 64<<20-wireChunk {
off = 0
}
}
}
// BenchmarkWireWrite64K writes 64 KiB per COMPOUND to a preallocated
// 64 MiB file, cycling through the offsets, one open around the run.
func BenchmarkWireWrite64K(b *testing.B) {
cl, _ := benchWire(b, 64<<20)
bodies := compound(b, cl, "bench-open", [][]byte{
nfs4.AppendPutRootfh(nil),
nfs4.AppendOpenArgs(nil, 0, []byte("bench"), nfs4.ShareAccessBoth, 0,
true, 0o644, "benchfile"),
nfs4.AppendGetfh(nil),
})
fh, err := xdr.NewDecoder(bodies[len(bodies)-1]).VarOpaque()
if err != nil {
b.Fatalf("decode fh: %v", err)
}
var st nfs4.Stateid
stateBody := bodies[len(bodies)-2]
if len(stateBody) < len(st) {
b.Fatalf("open result carries %d bytes of stateid", len(stateBody))
}
copy(st[:], stateBody)
buf := make([]byte, wireChunk)
off := uint64(0)
b.SetBytes(wireChunk)
b.ResetTimer()
for b.Loop() {
compound(b, cl, "bench-write", [][]byte{
nfs4.AppendPutfh(nil, fh),
nfs4.AppendWriteArgs(nil, st, off, nfs4.StableFileSync, buf),
})
off += wireChunk
if off > 64<<20-wireChunk {
off = 0
}
}
b.StopTimer()
compound(b, cl, "bench-close", [][]byte{
nfs4.AppendPutfh(nil, fh),
nfs4.AppendCloseArgs(nil, st),
})
}
// BenchmarkWireGetattr asks the attributes of one file per COMPOUND.
func BenchmarkWireGetattr(b *testing.B) {
cl, fh := benchWire(b, 1<<20)
request := nfs4.OfBits(nfs4.AttrType, nfs4.AttrSize, nfs4.AttrMode)
b.ResetTimer()
for b.Loop() {
bodies := compound(b, cl, "bench-getattr", [][]byte{
nfs4.AppendPutfh(nil, fh),
nfs4.AppendGetattr(nil, request),
})
d := xdr.NewDecoder(bodies[len(bodies)-1])
if _, err := nfs4.ReadBitmap(d); err != nil {
b.Fatalf("decode bitmap: %v", err)
}
if _, err := d.VarOpaque(); err != nil {
b.Fatalf("decode attrs: %v", err)
}
}
}
// BenchmarkWireLookup resolves one name under the root per COMPOUND.
func BenchmarkWireLookup(b *testing.B) {
cl, _ := benchWire(b, 1<<20)
b.ResetTimer()
for b.Loop() {
compound(b, cl, "bench-lookup", [][]byte{
nfs4.AppendPutRootfh(nil),
nfs4.AppendLookup(nil, "benchfile"),
nfs4.AppendGetfh(nil),
})
}
}