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