From ba6b2c700b0d0790bdd4e8ace76275b8560abf9a Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Wed, 16 Sep 2026 15:06:27 +0100 Subject: [PATCH] reduced allocations --- README.md | 11 +++++------ bngsdk_test.go | 42 +++++++++++++++++++++++++++++------------- transport.go | 46 +++++++++++++++++++++++++++++++++++++--------- 3 files changed, 71 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 80cce45..eb35639 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ Simple SDK to interact with BeamNG.drive OutGauge data. ## Development ### Performance -`go test -bench=BenchmarkReadData -benchmem -memprofile=mem.pprof` +`go test -bench=BenchmarkUpdate -benchmem -memprofile=mem.pprof` replace the function to be tested Use `go tool pprof` to analyze the results @@ -15,18 +15,17 @@ goos: linux goarch: amd64 pkg: github.com/ESilva15/gobngsdk cpu: AMD Ryzen 7 5800X3D 8-Core Processor -BenchmarkReadData-16 320931 4058 ns/op 100 B/op 2 allocs/op +BenchmarkReadData-16 362514 3188 ns/op 4 B/op 1 allocs/op PASS -ok github.com/ESilva15/gobngsdk 1.345s +ok github.com/ESilva15/gobngsdk 1.194s # New footprint goos: linux goarch: amd64 pkg: github.com/ESilva15/gobngsdk cpu: AMD Ryzen 7 5800X3D 8-Core Processor -BenchmarkReadData-16 362514 3188 ns/op 4 B/op 1 allocs/op +BenchmarkUpdate-16 159170 7287 ns/op 4 B/op 1 allocs/op PASS -ok github.com/ESilva15/gobngsdk 1.194s - +ok github.com/ESilva15/gobngsdk 1.241s # Pretty good enough. I can finally go be productive instead of "procrastinating" here ``` diff --git a/bngsdk_test.go b/bngsdk_test.go index 4d17946..3ad85b5 100644 --- a/bngsdk_test.go +++ b/bngsdk_test.go @@ -3,35 +3,51 @@ package bngsdk import ( "bytes" "encoding/binary" + "io" + "log/slog" "net" "testing" ) -func BenchmarkReadData(b *testing.B) { - // Spin up an UDP server - sdk, err := Init("127.0.0.1", 0) +func BenchmarkUpdate(b *testing.B) { + // Silence logging output so slog calls don't pollute benchmark stats + slogger := slog.New(slog.NewTextHandler(io.Discard, nil)) + + // Initialize the SDK with port 0 to bind to an OS-assigned ephemeral port + sdk, err := NewBngSDK(Options{ + Logger: slogger, + SourceType: UDPData, + ImportUDPAddress: "127.0.0.1", + ImportUDPPort: 0, + }) if err != nil { b.Fatalf("Failed to initialize SDK: %v", err) } defer sdk.Close() - // Retrieve the actual assigned UDP address - localAddr := sdk.Conn.LocalAddr().(*net.UDPAddr) + // Access the underlying reader connection to determine the dynamically bound port + ogReader, ok := sdk.reader.(*OgUDPReader) + if !ok || ogReader.udpConnection == nil || ogReader.udpConnection.connection == nil { + b.Fatalf("Failed to retrieve underlying UDP connection") + } - // Start a client to stream data - clientConn, err := net.DialUDP("udp", nil, localAddr) + serverAddr := ogReader.udpConnection.connection.LocalAddr().(*net.UDPAddr) + + // Dial the UDP socket as a client to send test data + clientConn, err := net.DialUDP("udp", nil, serverAddr) if err != nil { - b.Fatalf("Failed to dial local UDP socket: %v", err) + b.Fatalf("Failed to dial UDP server: %v", err) } defer clientConn.Close() - // Pre serialize some data + // Pre-serialize a dummy Outgauge struct matching the required byte layout dummyOutgauge := Outgauge{ Time: 424242, Car: [4]byte{'P', 'E', 'R', 'F'}, Speed: 45.2, RPM: 3500.0, } + var buf bytes.Buffer if err := binary.Write(&buf, binary.LittleEndian, dummyOutgauge); err != nil { b.Fatalf("Failed to serialize dummy struct: %v", err) @@ -42,16 +58,16 @@ func BenchmarkReadData(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { - // Feed a packet into the network buffer right before reading it + // Feed a packet into the network transport socket _, err := clientConn.Write(packetBytes) if err != nil { b.Fatalf("Failed to write to UDP socket: %v", err) } - // Execute the target function - err = sdk.ReadData() + // Run the main API loop method + _, err = sdk.Update() if err != nil { - b.Fatalf("ReadData failed at iteration %d: %v", i, err) + b.Fatalf("Update failed at iteration %d: %v", i, err) } } } diff --git a/transport.go b/transport.go index cc91469..e3e9d1a 100644 --- a/transport.go +++ b/transport.go @@ -4,6 +4,7 @@ import ( "errors" "log/slog" "net" + "sync" "time" "unsafe" ) @@ -12,12 +13,25 @@ import ( var ( ErrNoData = errors.New("no new data available") readTimeout = (time.Second / 60) * 5 // N missed frames at 60fps + packetPool = sync.Pool{ + New: func() any { + var b packetBuffer + return &b + }, + } ) +type packetBuffer [unsafe.Sizeof(Outgauge{})]byte + +type frame struct { + Buf *packetBuffer + Len int +} + type UDPTransport struct { address *net.UDPAddr connection *net.UDPConn - dataChan chan []byte + dataChan chan frame } func NewUDPReader(ip string, port int) (*UDPTransport, error) { @@ -35,7 +49,7 @@ func NewUDPReader(ip string, port int) (*UDPTransport, error) { udpT := UDPTransport{ address: addr, connection: conn, - dataChan: make(chan []byte, 1), + dataChan: make(chan frame, 1), } go udpT.udpSink() @@ -69,17 +83,28 @@ func (ut *UDPTransport) udpSink() { for { ut.connection.SetReadDeadline(time.Now().Add(readTimeout)) - buf := make([]byte, unsafe.Sizeof(Outgauge{})) - nBytes, _, err := ut.connection.ReadFromUDP(buf) + bufPtr := packetPool.Get().(*packetBuffer) + + nBytes, _, err := ut.connection.ReadFromUDP(bufPtr[:]) if err != nil { return } + frame := frame{ + Buf: bufPtr, + Len: nBytes, + } + select { - case ut.dataChan <- buf[:nBytes]: + case ut.dataChan <- frame: + // Packet sent successfuly default: - <-ut.dataChan - ut.dataChan <- buf[:nBytes] + select { + case oldFrame := <-ut.dataChan: + packetPool.Put(oldFrame.Buf) + default: + } + ut.dataChan <- frame } } } @@ -89,13 +114,16 @@ func (ut *UDPTransport) Write(data []byte) (int, error) { } func (ut *UDPTransport) Read(buffer []byte) (int, error) { - data, ok := <-ut.dataChan + latestFrame, ok := <-ut.dataChan if !ok { slog.Error(ErrNoData.Error()) return 0, ErrNoData } - return copy(buffer, data), nil + nBytes := copy(buffer, latestFrame.Buf[:latestFrame.Len]) + packetPool.Put(latestFrame.Buf) + + return nBytes, nil } func (ut *UDPTransport) Close() error {