perf(usbgadget): reduce input latency by pre-opening HID files and removing throttling

Pre-open HID files during initialization to minimize I/O overhead during operation. Remove mouse event throttling mechanism to improve input responsiveness. Keep HID files open on write errors to avoid repeated file operations.
This commit is contained in:
Alex P 2025-08-12 10:07:58 +00:00
parent 5f905e7eee
commit 4b693b4279
7 changed files with 36 additions and 76 deletions

View File

@ -201,6 +201,9 @@ func (u *UsbGadget) Init() error {
return u.logError("unable to initialize USB stack", err)
}
// Pre-open HID files to reduce input latency
u.PreOpenHidFiles()
return nil
}

View File

@ -203,8 +203,7 @@ func (u *UsbGadget) keyboardWriteHidFile(data []byte) error {
_, err := u.keyboardHidFile.Write(data)
if err != nil {
u.logWithSuppression("keyboardWriteHidFile", 100, u.log, err, "failed to write to hidg0")
u.keyboardHidFile.Close()
u.keyboardHidFile = nil
// Keep file open on write errors to reduce I/O overhead
return err
}
u.resetLogSuppressionCounter("keyboardWriteHidFile")

View File

@ -77,8 +77,7 @@ func (u *UsbGadget) absMouseWriteHidFile(data []byte) error {
_, err := u.absMouseHidFile.Write(data)
if err != nil {
u.logWithSuppression("absMouseWriteHidFile", 100, u.log, err, "failed to write to hidg1")
u.absMouseHidFile.Close()
u.absMouseHidFile = nil
// Keep file open on write errors to reduce I/O overhead
return err
}
u.resetLogSuppressionCounter("absMouseWriteHidFile")

View File

@ -60,15 +60,14 @@ func (u *UsbGadget) relMouseWriteHidFile(data []byte) error {
var err error
u.relMouseHidFile, err = os.OpenFile("/dev/hidg2", os.O_RDWR, 0666)
if err != nil {
return fmt.Errorf("failed to open hidg1: %w", err)
return fmt.Errorf("failed to open hidg2: %w", err)
}
}
_, err := u.relMouseHidFile.Write(data)
if err != nil {
u.logWithSuppression("relMouseWriteHidFile", 100, u.log, err, "failed to write to hidg2")
u.relMouseHidFile.Close()
u.relMouseHidFile = nil
// Keep file open on write errors to reduce I/O overhead
return err
}
u.resetLogSuppressionCounter("relMouseWriteHidFile")

View File

@ -95,6 +95,33 @@ func NewUsbGadget(name string, enabledDevices *Devices, config *Config, logger *
return newUsbGadget(name, defaultGadgetConfig, enabledDevices, config, logger)
}
// PreOpenHidFiles opens all HID files to reduce input latency
func (u *UsbGadget) PreOpenHidFiles() {
if u.enabledDevices.Keyboard {
if err := u.openKeyboardHidFile(); err != nil {
u.log.Debug().Err(err).Msg("failed to pre-open keyboard HID file")
}
}
if u.enabledDevices.AbsoluteMouse {
if u.absMouseHidFile == nil {
var err error
u.absMouseHidFile, err = os.OpenFile("/dev/hidg1", os.O_RDWR, 0666)
if err != nil {
u.log.Debug().Err(err).Msg("failed to pre-open absolute mouse HID file")
}
}
}
if u.enabledDevices.RelativeMouse {
if u.relMouseHidFile == nil {
var err error
u.relMouseHidFile, err = os.OpenFile("/dev/hidg2", os.O_RDWR, 0666)
if err != nil {
u.log.Debug().Err(err).Msg("failed to pre-open relative mouse HID file")
}
}
}
}
func newUsbGadget(name string, configMap map[string]gadgetConfigItem, enabledDevices *Devices, config *Config, logger *zerolog.Logger) *UsbGadget {
if logger == nil {
logger = defaultLogger

View File

@ -10,7 +10,6 @@ import (
"path/filepath"
"reflect"
"strconv"
"sync"
"time"
"github.com/pion/webrtc/v4"
@ -19,73 +18,7 @@ import (
"github.com/jetkvm/kvm/internal/usbgadget"
)
// Mouse event processing with single worker
var (
mouseEventChan = make(chan mouseEventData, 100) // Buffered channel for mouse events
mouseWorkerOnce sync.Once
)
type mouseEventData struct {
message webrtc.DataChannelMessage
session *Session
}
// startMouseWorker starts a single worker goroutine for processing mouse events
func startMouseWorker() {
go func() {
ticker := time.NewTicker(16 * time.Millisecond) // ~60 FPS
defer ticker.Stop()
var latestMouseEvent *mouseEventData
for {
select {
case event := <-mouseEventChan:
// Always keep the latest mouse event
latestMouseEvent = &event
case <-ticker.C:
// Process the latest mouse event at regular intervals
if latestMouseEvent != nil {
onRPCMessage(latestMouseEvent.message, latestMouseEvent.session)
latestMouseEvent = nil
}
}
}
}()
}
// onRPCMessageThrottled handles RPC messages with special throttling for mouse events
func onRPCMessageThrottled(message webrtc.DataChannelMessage, session *Session) {
var request JSONRPCRequest
err := json.Unmarshal(message.Data, &request)
if err != nil {
onRPCMessage(message, session)
return
}
// Check if this is a mouse event that should be throttled
if isMouseEvent(request.Method) {
// Start the mouse worker if not already started
mouseWorkerOnce.Do(startMouseWorker)
// Send to mouse worker (non-blocking)
select {
case mouseEventChan <- mouseEventData{message: message, session: session}:
// Event queued successfully
default:
// Channel is full, drop the event (this prevents blocking)
}
} else {
// Non-mouse events are processed immediately
go onRPCMessage(message, session)
}
}
// isMouseEvent checks if the RPC method is a mouse-related event
func isMouseEvent(method string) bool {
return method == "absMouseReport" || method == "relMouseReport"
}
// Direct RPC message handling for optimal input responsiveness
type JSONRPCRequest struct {
JSONRPC string `json:"jsonrpc"`

View File

@ -119,7 +119,7 @@ func newSession(config SessionConfig) (*Session, error) {
case "rpc":
session.RPCChannel = d
d.OnMessage(func(msg webrtc.DataChannelMessage) {
go onRPCMessageThrottled(msg, session)
go onRPCMessage(msg, session)
})
triggerOTAStateUpdate()
triggerVideoStateUpdate()