mirror of https://github.com/jetkvm/kvm.git
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:
parent
5f905e7eee
commit
4b693b4279
|
@ -201,6 +201,9 @@ func (u *UsbGadget) Init() error {
|
||||||
return u.logError("unable to initialize USB stack", err)
|
return u.logError("unable to initialize USB stack", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pre-open HID files to reduce input latency
|
||||||
|
u.PreOpenHidFiles()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -203,8 +203,7 @@ func (u *UsbGadget) keyboardWriteHidFile(data []byte) error {
|
||||||
_, err := u.keyboardHidFile.Write(data)
|
_, err := u.keyboardHidFile.Write(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
u.logWithSuppression("keyboardWriteHidFile", 100, u.log, err, "failed to write to hidg0")
|
u.logWithSuppression("keyboardWriteHidFile", 100, u.log, err, "failed to write to hidg0")
|
||||||
u.keyboardHidFile.Close()
|
// Keep file open on write errors to reduce I/O overhead
|
||||||
u.keyboardHidFile = nil
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
u.resetLogSuppressionCounter("keyboardWriteHidFile")
|
u.resetLogSuppressionCounter("keyboardWriteHidFile")
|
||||||
|
|
|
@ -77,8 +77,7 @@ func (u *UsbGadget) absMouseWriteHidFile(data []byte) error {
|
||||||
_, err := u.absMouseHidFile.Write(data)
|
_, err := u.absMouseHidFile.Write(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
u.logWithSuppression("absMouseWriteHidFile", 100, u.log, err, "failed to write to hidg1")
|
u.logWithSuppression("absMouseWriteHidFile", 100, u.log, err, "failed to write to hidg1")
|
||||||
u.absMouseHidFile.Close()
|
// Keep file open on write errors to reduce I/O overhead
|
||||||
u.absMouseHidFile = nil
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
u.resetLogSuppressionCounter("absMouseWriteHidFile")
|
u.resetLogSuppressionCounter("absMouseWriteHidFile")
|
||||||
|
|
|
@ -60,15 +60,14 @@ func (u *UsbGadget) relMouseWriteHidFile(data []byte) error {
|
||||||
var err error
|
var err error
|
||||||
u.relMouseHidFile, err = os.OpenFile("/dev/hidg2", os.O_RDWR, 0666)
|
u.relMouseHidFile, err = os.OpenFile("/dev/hidg2", os.O_RDWR, 0666)
|
||||||
if err != nil {
|
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)
|
_, err := u.relMouseHidFile.Write(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
u.logWithSuppression("relMouseWriteHidFile", 100, u.log, err, "failed to write to hidg2")
|
u.logWithSuppression("relMouseWriteHidFile", 100, u.log, err, "failed to write to hidg2")
|
||||||
u.relMouseHidFile.Close()
|
// Keep file open on write errors to reduce I/O overhead
|
||||||
u.relMouseHidFile = nil
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
u.resetLogSuppressionCounter("relMouseWriteHidFile")
|
u.resetLogSuppressionCounter("relMouseWriteHidFile")
|
||||||
|
|
|
@ -95,6 +95,33 @@ func NewUsbGadget(name string, enabledDevices *Devices, config *Config, logger *
|
||||||
return newUsbGadget(name, defaultGadgetConfig, enabledDevices, 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 {
|
func newUsbGadget(name string, configMap map[string]gadgetConfigItem, enabledDevices *Devices, config *Config, logger *zerolog.Logger) *UsbGadget {
|
||||||
if logger == nil {
|
if logger == nil {
|
||||||
logger = defaultLogger
|
logger = defaultLogger
|
||||||
|
|
69
jsonrpc.go
69
jsonrpc.go
|
@ -10,7 +10,6 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pion/webrtc/v4"
|
"github.com/pion/webrtc/v4"
|
||||||
|
@ -19,73 +18,7 @@ import (
|
||||||
"github.com/jetkvm/kvm/internal/usbgadget"
|
"github.com/jetkvm/kvm/internal/usbgadget"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Mouse event processing with single worker
|
// Direct RPC message handling for optimal input responsiveness
|
||||||
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"
|
|
||||||
}
|
|
||||||
|
|
||||||
type JSONRPCRequest struct {
|
type JSONRPCRequest struct {
|
||||||
JSONRPC string `json:"jsonrpc"`
|
JSONRPC string `json:"jsonrpc"`
|
||||||
|
|
|
@ -119,7 +119,7 @@ func newSession(config SessionConfig) (*Session, error) {
|
||||||
case "rpc":
|
case "rpc":
|
||||||
session.RPCChannel = d
|
session.RPCChannel = d
|
||||||
d.OnMessage(func(msg webrtc.DataChannelMessage) {
|
d.OnMessage(func(msg webrtc.DataChannelMessage) {
|
||||||
go onRPCMessageThrottled(msg, session)
|
go onRPCMessage(msg, session)
|
||||||
})
|
})
|
||||||
triggerOTAStateUpdate()
|
triggerOTAStateUpdate()
|
||||||
triggerVideoStateUpdate()
|
triggerVideoStateUpdate()
|
||||||
|
|
Loading…
Reference in New Issue