mirror of https://github.com/jetkvm/kvm.git
refactor: enhance keep-alive handling and jitter compensation in HID RPC
- Implemented staleness guard to ignore outdated keep-alive packets. - Added jitter compensation logic to adjust timer extensions based on packet arrival times. - Introduced new methods for managing keep-alive state and reset functionality in the Session struct. - Updated auto-release delay mechanism to use dynamic durations based on keep-alive timing. - Adjusted keep-alive interval in the UI to improve responsiveness.
This commit is contained in:
parent
c394fc559d
commit
23c79941d2
58
hidrpc.go
58
hidrpc.go
|
@ -39,7 +39,63 @@ func handleHidRPCMessage(message hidrpc.Message, session *Session) {
|
|||
rpcCancelKeyboardMacro()
|
||||
return
|
||||
case hidrpc.TypeKeypressKeepAliveReport:
|
||||
gadget.DelayAutoRelease()
|
||||
session.keepAliveJitterLock.Lock()
|
||||
defer session.keepAliveJitterLock.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
|
||||
// Tunables
|
||||
// Keep in mind
|
||||
// macOS default: 15 * 15 = 225ms https://discussions.apple.com/thread/1316947?sortBy=rank
|
||||
// Linux default: 250ms https://man.archlinux.org/man/kbdrate.8.en
|
||||
// Windows default: 1s `HKEY_CURRENT_USER\Control Panel\Accessibility\Keyboard Response\AutoRepeatDelay`
|
||||
|
||||
const expectedRate = 50 * time.Millisecond // expected keepalive interval
|
||||
const maxLateness = 50 * time.Millisecond // max jitter we'll tolerate OR jitter budget
|
||||
const baseExtension = expectedRate + maxLateness // 100ms extension on perfect tick
|
||||
|
||||
const maxStaleness = 225 * time.Millisecond // discard ancient packets outright
|
||||
|
||||
// 1) Staleness guard: ensures packets that arrive far beyond the life of a valid key hold
|
||||
// (e.g. after a network stall, retransmit burst, or machine sleep) are ignored outright.
|
||||
// This prevents “zombie” keepalives from reviving a key that should already be released.
|
||||
if !session.lastTimerResetTime.IsZero() && now.Sub(session.lastTimerResetTime) > maxStaleness {
|
||||
return
|
||||
}
|
||||
|
||||
validTick := true
|
||||
timerExtension := baseExtension
|
||||
|
||||
if !session.lastKeepAliveArrivalTime.IsZero() {
|
||||
timeSinceLastTick := now.Sub(session.lastKeepAliveArrivalTime)
|
||||
lateness := timeSinceLastTick - expectedRate
|
||||
|
||||
if lateness > 0 {
|
||||
if lateness <= maxLateness {
|
||||
// --- Small lateness (within jitterBudget) ---
|
||||
// This is normal jitter (e.g., Wi-Fi contention).
|
||||
// We still accept the tick, but *reduce the extension*
|
||||
// so that the total hold time stays aligned with REAL client side intent.
|
||||
timerExtension -= lateness
|
||||
} else {
|
||||
// --- Large lateness (beyond jitterBudget) ---
|
||||
// This is likely a retransmit stall or ordering delay.
|
||||
// We reject the tick entirely and DO NOT extend,
|
||||
// so the auto-release still fires on time.
|
||||
validTick = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if validTick {
|
||||
// Only valid ticks update our state and extend the timer.
|
||||
session.lastKeepAliveArrivalTime = now
|
||||
session.lastTimerResetTime = now
|
||||
if ug := getUsbGadget(); ug != nil {
|
||||
ug.DelayAutoReleaseWithDuration(timerExtension)
|
||||
}
|
||||
}
|
||||
// On a miss: do not advance any state — keeps baseline stable.
|
||||
case hidrpc.TypePointerReport:
|
||||
pointerReport, err := message.PointerReport()
|
||||
if err != nil {
|
||||
|
|
|
@ -26,11 +26,6 @@ var keyboardConfig = gadgetConfigItem{
|
|||
reportDesc: keyboardReportDesc,
|
||||
}
|
||||
|
||||
// macOS default: 15 * 15 = 225ms https://discussions.apple.com/thread/1316947?sortBy=rank
|
||||
// Linux default: 250ms https://man.archlinux.org/man/kbdrate.8.en
|
||||
// Windows default: 1s `HKEY_CURRENT_USER\Control Panel\Accessibility\Keyboard Response\AutoRepeatDelay`
|
||||
const autoReleaseKeyboardInterval = time.Millisecond * 225
|
||||
|
||||
// Source: https://www.kernel.org/doc/Documentation/usb/gadget_hid.txt
|
||||
var keyboardReportDesc = []byte{
|
||||
0x05, 0x01, /* USAGE_PAGE (Generic Desktop) */
|
||||
|
@ -158,6 +153,10 @@ func (u *UsbGadget) SetOnKeysDownChange(f func(state KeysDownState)) {
|
|||
u.onKeysDownChange = &f
|
||||
}
|
||||
|
||||
func (u *UsbGadget) SetOnKeepAliveReset(f func()) {
|
||||
u.onKeepAliveReset = &f
|
||||
}
|
||||
|
||||
func (u *UsbGadget) scheduleAutoRelease(key byte) {
|
||||
u.kbdAutoReleaseLock.Lock()
|
||||
defer unlockWithLog(&u.kbdAutoReleaseLock, u.log, "autoRelease scheduled")
|
||||
|
@ -166,7 +165,10 @@ func (u *UsbGadget) scheduleAutoRelease(key byte) {
|
|||
u.kbdAutoReleaseTimers[key].Stop()
|
||||
}
|
||||
|
||||
u.kbdAutoReleaseTimers[key] = time.AfterFunc(autoReleaseKeyboardInterval, func() {
|
||||
// TODO: This shouldn't use the global autoReleaseKeyboardStartInterval
|
||||
// but rather the baseExtension from the keepalive jitter compensation logic.
|
||||
// Make them global as they will in the future likely be variable.
|
||||
u.kbdAutoReleaseTimers[key] = time.AfterFunc(100*time.Millisecond, func() {
|
||||
u.performAutoRelease(key)
|
||||
})
|
||||
}
|
||||
|
@ -179,10 +181,15 @@ func (u *UsbGadget) cancelAutoRelease(key byte) {
|
|||
timer.Stop()
|
||||
u.kbdAutoReleaseTimers[key] = nil
|
||||
delete(u.kbdAutoReleaseTimers, key)
|
||||
|
||||
// Reset keep-alive timing when key is released
|
||||
if u.onKeepAliveReset != nil {
|
||||
(*u.onKeepAliveReset)()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (u *UsbGadget) DelayAutoRelease() {
|
||||
func (u *UsbGadget) DelayAutoReleaseWithDuration(resetDuration time.Duration) {
|
||||
u.kbdAutoReleaseLock.Lock()
|
||||
defer unlockWithLog(&u.kbdAutoReleaseLock, u.log, "autoRelease delayed")
|
||||
|
||||
|
@ -190,9 +197,11 @@ func (u *UsbGadget) DelayAutoRelease() {
|
|||
return
|
||||
}
|
||||
|
||||
u.log.Debug().Dur("reset_duration", resetDuration).Msg("delaying auto-release with dynamic duration")
|
||||
|
||||
for _, timer := range u.kbdAutoReleaseTimers {
|
||||
if timer != nil {
|
||||
timer.Reset(autoReleaseKeyboardInterval)
|
||||
timer.Reset(resetDuration)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -88,6 +88,7 @@ type UsbGadget struct {
|
|||
|
||||
onKeyboardStateChange *func(state KeyboardState)
|
||||
onKeysDownChange *func(state KeysDownState)
|
||||
onKeepAliveReset *func()
|
||||
|
||||
log *zerolog.Logger
|
||||
|
||||
|
|
|
@ -262,7 +262,7 @@ export default function useKeyboard() {
|
|||
}, [rpcHidReady, cancelOngoingKeyboardMacroHidRpc, abortController]);
|
||||
};
|
||||
|
||||
const KEEPALIVE_INTERVAL = 75; // TODO: use an adaptive interval based on RTT later
|
||||
const KEEPALIVE_INTERVAL = 50;
|
||||
|
||||
const cancelKeepAlive = useCallback(() => {
|
||||
if (keepAliveTimerRef.current) {
|
||||
|
@ -277,9 +277,6 @@ export default function useKeyboard() {
|
|||
clearInterval(keepAliveTimerRef.current);
|
||||
}
|
||||
|
||||
sendKeypressKeepAliveHidRpc();
|
||||
|
||||
// Create new interval timer
|
||||
keepAliveTimerRef.current = setInterval(() => {
|
||||
sendKeypressKeepAliveHidRpc();
|
||||
}, KEEPALIVE_INTERVAL);
|
||||
|
|
10
usb.go
10
usb.go
|
@ -8,6 +8,10 @@ import (
|
|||
|
||||
var gadget *usbgadget.UsbGadget
|
||||
|
||||
func getUsbGadget() *usbgadget.UsbGadget {
|
||||
return gadget
|
||||
}
|
||||
|
||||
// initUsbGadget initializes the USB gadget.
|
||||
// call it only after the config is loaded.
|
||||
func initUsbGadget() {
|
||||
|
@ -37,6 +41,12 @@ func initUsbGadget() {
|
|||
}
|
||||
})
|
||||
|
||||
gadget.SetOnKeepAliveReset(func() {
|
||||
if currentSession != nil {
|
||||
currentSession.resetKeepAliveTime()
|
||||
}
|
||||
})
|
||||
|
||||
// open the keyboard hid file to listen for keyboard events
|
||||
if err := gadget.OpenKeyboardHidFile(); err != nil {
|
||||
usbLogger.Error().Err(err).Msg("failed to open keyboard hid file")
|
||||
|
|
18
webrtc.go
18
webrtc.go
|
@ -7,6 +7,7 @@ import (
|
|||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/coder/websocket"
|
||||
"github.com/coder/websocket/wsjson"
|
||||
|
@ -28,13 +29,23 @@ type Session struct {
|
|||
|
||||
rpcQueue chan webrtc.DataChannelMessage
|
||||
|
||||
hidRPCAvailable bool
|
||||
hidQueueLock sync.Mutex
|
||||
hidQueue []chan hidQueueMessage
|
||||
hidRPCAvailable bool
|
||||
lastKeepAliveArrivalTime time.Time // Track when last keep-alive packet arrived
|
||||
lastTimerResetTime time.Time // Track when auto-release timer was last reset
|
||||
keepAliveJitterLock sync.Mutex // Protect jitter compensation timing state
|
||||
hidQueueLock sync.Mutex
|
||||
hidQueue []chan hidQueueMessage
|
||||
|
||||
keysDownStateQueue chan usbgadget.KeysDownState
|
||||
}
|
||||
|
||||
func (s *Session) resetKeepAliveTime() {
|
||||
s.keepAliveJitterLock.Lock()
|
||||
defer s.keepAliveJitterLock.Unlock()
|
||||
s.lastKeepAliveArrivalTime = time.Time{} // Reset keep-alive timing tracking
|
||||
s.lastTimerResetTime = time.Time{} // Reset auto-release timer tracking
|
||||
}
|
||||
|
||||
type hidQueueMessage struct {
|
||||
webrtc.DataChannelMessage
|
||||
channel string
|
||||
|
@ -148,6 +159,7 @@ func getOnHidMessageHandler(session *Session, scopedLogger *zerolog.Logger, chan
|
|||
|
||||
l.Trace().Msg("received data in HID RPC message handler")
|
||||
|
||||
|
||||
// Enqueue to ensure ordered processing
|
||||
queueIndex := hidrpc.GetQueueIndex(hidrpc.MessageType(msg.Data[0]))
|
||||
if queueIndex >= len(session.hidQueue) || queueIndex < 0 {
|
||||
|
|
Loading…
Reference in New Issue