reduced allocations

This commit is contained in:
2026-09-16 15:06:27 +01:00
parent ec2ed17f43
commit ba6b2c700b
3 changed files with 71 additions and 28 deletions
+5 -6
View File
@@ -3,7 +3,7 @@ Simple SDK to interact with BeamNG.drive OutGauge data.
## Development ## Development
### Performance ### Performance
`go test -bench=BenchmarkReadData -benchmem -memprofile=mem.pprof` `go test -bench=BenchmarkUpdate -benchmem -memprofile=mem.pprof`
replace the function to be tested replace the function to be tested
Use `go tool pprof` to analyze the results Use `go tool pprof` to analyze the results
@@ -15,18 +15,17 @@ goos: linux
goarch: amd64 goarch: amd64
pkg: github.com/ESilva15/gobngsdk pkg: github.com/ESilva15/gobngsdk
cpu: AMD Ryzen 7 5800X3D 8-Core Processor 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 PASS
ok github.com/ESilva15/gobngsdk 1.345s ok github.com/ESilva15/gobngsdk 1.194s
# New footprint # New footprint
goos: linux goos: linux
goarch: amd64 goarch: amd64
pkg: github.com/ESilva15/gobngsdk pkg: github.com/ESilva15/gobngsdk
cpu: AMD Ryzen 7 5800X3D 8-Core Processor 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 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 # Pretty good enough. I can finally go be productive instead of "procrastinating" here
``` ```
+29 -13
View File
@@ -3,35 +3,51 @@ package bngsdk
import ( import (
"bytes" "bytes"
"encoding/binary" "encoding/binary"
"io"
"log/slog"
"net" "net"
"testing" "testing"
) )
func BenchmarkReadData(b *testing.B) { func BenchmarkUpdate(b *testing.B) {
// Spin up an UDP server // Silence logging output so slog calls don't pollute benchmark stats
sdk, err := Init("127.0.0.1", 0) 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 { if err != nil {
b.Fatalf("Failed to initialize SDK: %v", err) b.Fatalf("Failed to initialize SDK: %v", err)
} }
defer sdk.Close() defer sdk.Close()
// Retrieve the actual assigned UDP address // Access the underlying reader connection to determine the dynamically bound port
localAddr := sdk.Conn.LocalAddr().(*net.UDPAddr) 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 serverAddr := ogReader.udpConnection.connection.LocalAddr().(*net.UDPAddr)
clientConn, err := net.DialUDP("udp", nil, localAddr)
// Dial the UDP socket as a client to send test data
clientConn, err := net.DialUDP("udp", nil, serverAddr)
if err != nil { 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() defer clientConn.Close()
// Pre serialize some data // Pre-serialize a dummy Outgauge struct matching the required byte layout
dummyOutgauge := Outgauge{ dummyOutgauge := Outgauge{
Time: 424242, Time: 424242,
Car: [4]byte{'P', 'E', 'R', 'F'}, Car: [4]byte{'P', 'E', 'R', 'F'},
Speed: 45.2, Speed: 45.2,
RPM: 3500.0, RPM: 3500.0,
} }
var buf bytes.Buffer var buf bytes.Buffer
if err := binary.Write(&buf, binary.LittleEndian, dummyOutgauge); err != nil { if err := binary.Write(&buf, binary.LittleEndian, dummyOutgauge); err != nil {
b.Fatalf("Failed to serialize dummy struct: %v", err) b.Fatalf("Failed to serialize dummy struct: %v", err)
@@ -42,16 +58,16 @@ func BenchmarkReadData(b *testing.B) {
b.ReportAllocs() b.ReportAllocs()
for i := 0; i < b.N; i++ { 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) _, err := clientConn.Write(packetBytes)
if err != nil { if err != nil {
b.Fatalf("Failed to write to UDP socket: %v", err) b.Fatalf("Failed to write to UDP socket: %v", err)
} }
// Execute the target function // Run the main API loop method
err = sdk.ReadData() _, err = sdk.Update()
if err != nil { if err != nil {
b.Fatalf("ReadData failed at iteration %d: %v", i, err) b.Fatalf("Update failed at iteration %d: %v", i, err)
} }
} }
} }
+37 -9
View File
@@ -4,6 +4,7 @@ import (
"errors" "errors"
"log/slog" "log/slog"
"net" "net"
"sync"
"time" "time"
"unsafe" "unsafe"
) )
@@ -12,12 +13,25 @@ import (
var ( var (
ErrNoData = errors.New("no new data available") ErrNoData = errors.New("no new data available")
readTimeout = (time.Second / 60) * 5 // N missed frames at 60fps 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 { type UDPTransport struct {
address *net.UDPAddr address *net.UDPAddr
connection *net.UDPConn connection *net.UDPConn
dataChan chan []byte dataChan chan frame
} }
func NewUDPReader(ip string, port int) (*UDPTransport, error) { func NewUDPReader(ip string, port int) (*UDPTransport, error) {
@@ -35,7 +49,7 @@ func NewUDPReader(ip string, port int) (*UDPTransport, error) {
udpT := UDPTransport{ udpT := UDPTransport{
address: addr, address: addr,
connection: conn, connection: conn,
dataChan: make(chan []byte, 1), dataChan: make(chan frame, 1),
} }
go udpT.udpSink() go udpT.udpSink()
@@ -69,17 +83,28 @@ func (ut *UDPTransport) udpSink() {
for { for {
ut.connection.SetReadDeadline(time.Now().Add(readTimeout)) ut.connection.SetReadDeadline(time.Now().Add(readTimeout))
buf := make([]byte, unsafe.Sizeof(Outgauge{})) bufPtr := packetPool.Get().(*packetBuffer)
nBytes, _, err := ut.connection.ReadFromUDP(buf)
nBytes, _, err := ut.connection.ReadFromUDP(bufPtr[:])
if err != nil { if err != nil {
return return
} }
frame := frame{
Buf: bufPtr,
Len: nBytes,
}
select { select {
case ut.dataChan <- buf[:nBytes]: case ut.dataChan <- frame:
// Packet sent successfuly
default: default:
<-ut.dataChan select {
ut.dataChan <- buf[:nBytes] 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) { func (ut *UDPTransport) Read(buffer []byte) (int, error) {
data, ok := <-ut.dataChan latestFrame, ok := <-ut.dataChan
if !ok { if !ok {
slog.Error(ErrNoData.Error()) slog.Error(ErrNoData.Error())
return 0, ErrNoData 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 { func (ut *UDPTransport) Close() error {