reduced allocations
This commit is contained in:
@@ -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
@@ -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
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user