Background: I wrote a python script that uses evdev to read from the gamepad on a Steam Deck and use it to send keypresses to a game. From a functionality standpoint, it does everything perfectly.
The Problem: The script slows down over time: after it has run for 15 minutes the game starts stuttering, and at the 30 minute mark the game is juddering to the point of unplayability. The game runs smoothly when I'm not using the script, so the script is definitely the part that's at fault; I just don't know why yet (because I'm a python noob).
What I've tried: I've tried garbage-collecting on every thousand-th iteration of the loop, in case the slowdown is due to eating up memory. I've tried using pypy to make it run faster (but haven't been able to get pypy working on Steam Deck, and it would only be masking the underlying problem anyway). I've tried reading all the posts under the evdev tag on StackOverflow. Now I'm asking a question because I wasn't able to find anything that could solve the slowdown problem.
The Code:
#!/usr/bin/env python
import evdev
import struct
import os
import sys
import time
import subprocess
import gc
from evdev import ecodes
GAME_PROCESS_ID = sys.argv[1]
TRIGGER_THRESHOLD = 70
GAMEPAD_NAME = "Microsoft X-Box 360 pad"
devices = [evdev.InputDevice(path) for path in evdev.list_devices()]
for device in devices:
if GAMEPAD_NAME in device.name:
GAMEPAD_PATH = device.path
device = evdev.InputDevice(GAMEPAD_PATH)
isLeftTriggerDown = False
isRightTriggerDown = False
isStartDown = False
isCtrlDown = False
isDpadUpDown = False
isDpadDownDown = False
isDpadLeftDown = False
isDpadRightDown = False
southKey = "Return"
westKey = "KP_Subtract"
eastKey = "Escape"
northKey = "KP_Add"
dpadUpKey = "F1"
dpadRightKey = "F2"
dpadDownKey = "F3"
dpadLeftKey = "F4"
selectKey = "F9"
startKey = "F10"
def sendKeyDown(keycode):
subprocess.call(["xdotool", "keydown", "--window", GAME_PROCESS_ID, keycode])
def sendKeyUp(keycode):
subprocess.call(["xdotool", "keyup", "--window", GAME_PROCESS_ID, keycode])
iterations = 0
for event in device.read_loop():
iterations = iterations + 1
if iterations > 1000:
gc.collect()
iterations = 0
if event.type == ecodes.EV_ABS:
if event.code == ecodes.ABS_Z:
if event.value < TRIGGER_THRESHOLD:
if isLeftTriggerDown:
isLeftTriggerDown = False
sendKeyUp("F11")
else:
if not isLeftTriggerDown:
isLeftTriggerDown = True
if not isCtrlDown:
isCtrlDown = True
sendKeyDown("Control_L")
sendKeyDown("F11")
if event.code == ecodes.ABS_RZ:
if event.value < TRIGGER_THRESHOLD:
if isRightTriggerDown:
isRightTriggerDown = False
sendKeyUp("F12")
else:
if not isRightTriggerDown:
isRightTriggerDown = True
if not isCtrlDown:
isCtrlDown = True
sendKeyDown("Control_L")
sendKeyDown("F12")
if isCtrlDown and not (isLeftTriggerDown or isRightTriggerDown):
isCtrlDown = False
sendKeyUp("Control_L")
if isLeftTriggerDown or isRightTriggerDown:
southKey = "F5"
westKey = "F6"
eastKey = "F7"
northKey = "F8"
else:
southKey = "Return"
westKey = "KP_Subtract"
eastKey = "Escape"
northKey = "KP_Add"
if isLeftTriggerDown or isRightTriggerDown or isStartDown:
if event.code == ecodes.ABS_HAT0X:
if event.value == 1:
if not isDpadRightDown:
sendKeyDown(dpadRightKey)
isDpadRightDown = True
isDpadLeftDown = False
if isDpadLeftDown:
sendKeyUp(dpadLeftKey)
if isDpadUpDown:
sendKeyUp(dpadUpKey)
if isDpadLeftDown:
sendKeyUp(dpadDownKey)
elif event.value == -1:
if not isDpadLeftDown:
sendKeyDown(dpadLeftKey)
isDpadLeftDown = True
isDpadRightDown = False
if isDpadRightDown:
sendKeyUp(dpadRightKey)
if isDpadUpDown:
sendKeyUp(dpadUpKey)
if isDpadDownDown:
sendKeyUp(dpadDownKey)
else:
if isDpadRightDown:
sendKeyUp(dpadRightKey)
if isDpadLeftDown:
sendKeyUp(dpadLeftKey)
isDpadRightDown = False
isDpadLeftDown = False
if event.code == ecodes.ABS_HAT0Y:
if event.value == 1:
if not isDpadDownDown:
sendKeyDown(dpadDownKey)
isDpadDownDown = True
isDpadUpDown = False
if isDpadLeftDown:
sendKeyUp(dpadLeftKey)
if isDpadUpDown:
sendKeyUp(dpadUpKey)
if isDpadRightDown:
sendKeyUp(dpadRightKey)
elif event.value == -1:
if not isDpadUpDown:
sendKeyDown(dpadUpKey)
isDpadUpDown = True
isDpadDownDown = False
if isDpadLeftDown:
sendKeyUp(dpadLeftKey)
if isDpadRightDown:
sendKeyUp(dpadRightKey)
if isDpadLeftDown:
sendKeyUp(dpadDownKey)
else:
if isDpadDownDown:
sendKeyUp(dpadDownKey)
if isDpadUpDown:
sendKeyUp(dpadUpKey)
isDpadDownDown = False
isDpadUpDown = False
if event.type == ecodes.EV_KEY:
# For some reason, BTN_NORTH and BTN_WEST are switched on Steam Decks
if event.code == ecodes.BTN_NORTH:
if event.value == 1:
sendKeyDown(westKey)
else:
sendKeyUp(westKey)
if event.code == ecodes.BTN_SOUTH:
if event.value == 1:
sendKeyDown(southKey)
else:
sendKeyUp(southKey)
if event.code == ecodes.BTN_EAST:
if event.value == 1:
sendKeyDown(eastKey)
else:
sendKeyUp(eastKey)
# For some reason, BTN_NORTH and BTN_WEST are switched on Steam Decks
if event.code == ecodes.BTN_WEST:
if event.value == 1:
sendKeyDown(northKey)
else:
sendKeyUp(northKey)
if event.code == ecodes.BTN_START:
isStartDown = event.value == 1
if event.value == 1:
sendKeyDown("Control_L")
sendKeyDown(startKey)
else:
sendKeyUp(startKey)
sendKeyUp("Control_L")
if event.code == ecodes.BTN_SELECT:
if event.value == 1:
sendKeyDown("Control_L")
sendKeyDown(selectKey)
else:
sendKeyUp(selectKey)
sendKeyUp("Control_L")
Possible Suspects: The two things I suspect could be the cause of the slowdown are either 1) the subprocesses being called by sendKeyDown() and sendKeyUp() are for some reason not closing after sending a key, or 2) events objects are piling up because for event in device.read_loop(): is causing the events to not be garbage collected. I'm not sure how to verify these possibilities: it shouldn't be #1 because leaving the game (and gamepad) idle for 15+ minutes still results in slowdown, and it shouldn't be #2 because calling gc.collect() should take care that, right?
Any ideas what might be causing this to slow down over time, and/or how to test likely candidates?