Better sdk #1

Merged
esilva merged 4 commits from better-sdk into master 2026-09-16 19:07:40 +01:00
3 changed files with 71 additions and 28 deletions
Showing only changes of commit ba6b2c700b - Show all commits
+5 -6
View File
@@ -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
```
+29 -13
View File
@@ -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)
}
}
}
+37 -9
View File
@@ -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 {