How to fix this script so that it won't peg the CPU?
Asked Answered
S

5

8

On my home Kubuntu machine, I am running a script to beep on every keypress, no matter which window or application has focus, adapted from this insightful page

#!/usr/bin/env python 

from Xlib.display import Display
import os
import sys

ZERO=[]
for i in range(0,32):
        ZERO.append(0)
ignorelist=[ZERO]

def main():    
        if os.getuid()==0:
                os.system("modprobe pcspkr")
                print("Speaker enabled, start as normal user")
                sys.exit()

        print("If no beep is heard, then run as root to enable pcspkr")

        disp = Display()
        while 1:
                keymap=disp.query_keymap()
                if keymap not in ignorelist:
                        os.system("beep")

if __name__ == '__main__':
        main()

The script works great, but it pegs both CPUs of my dual-core Intel machine at around 80% each, so I can do little else with the machine. How can I reduce the CPU requirements of this simple script without interfering with its operation? In other words, it should still beep at the moment of keypress, no matter what window or application has focus.

If this is not possible in Python, what other technologies should I look at? C? I would assume that there exists some kernel component which notifies applications of keypresses: how else does KDE handle global shortcuts? How can I get my application to receive these notices as well?

The goal is to make a beep at the moment each key is pressed, as I am training my fingers to type on a mechanical keyboard without bottoming out yet without missing keypresses. I just graduated from Cherry Browns to Cherry Blues and the lack of tactical feedback takes some time to get used to.

Note that any solution must emit a beep no matter which window has focus. This program is intended to be used as a daemon that will run in the background of all applications that I use.

Thanks.

Shogun answered 16/8, 2012 at 15:22 Comment(2)
Have you checked to see what programs are pegging the CPU? It seems doubtful that a single threaded program such as this could be responsible by itself.Dene
Yes, I have checked and killing this script removes the CPU load. Restarting it creates the CPU load again. Interestingly, repeatedly pressing a key to beep actually reduces the load.Shogun
P
7

You can start your script using nice. The nice command will lower the priority of your script, so that it will only run when the system has nothing else to do. That way it will still eat CPU cycles, but you will be able to use your system normally for other tasks.

See the man page for details.

EDIT:

To reduce CPU usage, you could add a small delay, using time.sleep(0.01). This will reduce the CPU load, but will marginally increase the time between the keypress and the resulting beep.

Peracid answered 12/9, 2012 at 9:37 Comment(8)
Thanks. Starting the app with nice -n 19 does still peg the CPU but the system is more responsive at least. This approach has a drawback of still keeping the CPU monitor pegged, so it becomes useless for determining unrelated system loads. Furthermore, the temp stays very high with the CPU this active. So this is a good workaround, but I really need something that addresses the root issue.Shogun
Lanaru's first suggestion was to add a small sleep delay. If you use time.sleep(0.05) you will at most a 50ms delay between your your keypress and yout beep. This will reduce the CPU load a lot.Peracid
Thanks, reviewing that even a delay of 0.01 reduces the CPU use significantly without affecting the operation.Shogun
Why the downvote? I'm eager to learn what was wrong with my answer?Peracid
I upvoted, I sometimes never understand why some questions get downvotes without explanations. In fact, I would like to see SO punish downvotes with no comment.Shogun
The nice is solution only for CPU-bound processes to not destroy interactivity. A sleep is never a real solution either as it would only make the code miss presses. This is an IO-bound process; the original code uses the wrong X11 call that just queries the keyboard status, the real solution is to use a blocking call instead.Torment
Thank you Antti. Which blocking call might that be, or where might I find it?Shogun
This was a hard call, there were some terrific answers here. I really learned from them all. In the end, the best compromise seems to be a small 0.01 delay in the while loop, and a generous helping of nice. This makes for an application that uses less resources, and gives up the resources that it does use when other applications need it, but still provides real-time keypress notifications in all applications. Thank you!Shogun
M
12

Use events:

import Xlib
from Xlib.display import Display

display = Display()

screen = display.screen()
w = screen.root.create_window(0, 0, 100, 100, 1,
              screen.root_depth,
              event_mask = Xlib.X.KeyPressMask)
w.map()

while True:
    event = display.next_event()
    if event.type != Xlib.X.KeyPress:
        continue
    print "OHAI"

Details cadged from http://python-xlib.sourceforge.net/doc/html/python-xlib_10.html#SEC9

Major answered 16/8, 2012 at 15:31 Comment(6)
Thank you. Having simply replaced if keycode_requires_beep(event.detail) with else: and replaced # beep with os.system("beep"), the script runs and does not peg the CPU, but it does not beep on keypresses. I would have expected to many beeps since I'm using simple else but not no beeps! Have you any idea why this may be?Shogun
No idea, but there's some questions you can find answers to in order to find out. 1: Are you getting in to that beep section, and just not hearing a beep when you get there? 2: are you getting an event when you hit a key? 3: what is the type of that event? All these questions are pretty standard debugging, when you know that what you expect to have happening isn't what you're seeing. The idea is to continually ask "what is actually happening?" and "What do I expect to happen?" until you find where your expectation and the vicious reality of broken code aren't lining up.Major
Thanks. I see that print("1") before the line event = Display.next_event() gets printed, but print("2") after that line does not. It seems to be hanging on that line.Shogun
Well, that got hairy real fast. The reason for your problem was right there in the docs: "To avoid flooding the clients with events in which they have no interest, they must explicitly tell the server which events they are interested in." You do that when you create a window, which means we need to create a window. I've replaced the example code with one that does this (and that I've actually tested). The two changes are the prelude that grabs the display and creates the window, and a correction to the comparison to check the type of the events.Major
Thank you. Unfortunately, this method only works when the new window is focused. Obviously this will not help me in everyday usage. I don't see anything other than screen which might be appropriate. Are there any other options? This would otherwise be a good solution. In any case I learned much from this post. Thanks.Shogun
Hm. I don't know. screen.root is a window, and you might be able to call change_attributes(event_mask = Xlib.X.KeyPressMask) on it, but you still wouldn't own the window, so I don't know if you'd get the events or not. lwn.net/Articles/101475 does a screen.root.query_tree(), calls child.select_input(Xlib.X.KeyPressMask) on each child, then recurses. If you speak C, it would be but a small change to that code to get to where you want to be.Major
A
8

Your while loop is consuming all your CPU's cycles since it's executing as fast as possible. You could add a small sleep() delay, but you risk missing out on some key events. Alternatively, consider using the pyhook module to monitor key presses.

Asiatic answered 16/8, 2012 at 15:32 Comment(1)
Thank you. I am actually using Kubuntu Linux, not Windows. Nice find, though.Shogun
P
7

You can start your script using nice. The nice command will lower the priority of your script, so that it will only run when the system has nothing else to do. That way it will still eat CPU cycles, but you will be able to use your system normally for other tasks.

See the man page for details.

EDIT:

To reduce CPU usage, you could add a small delay, using time.sleep(0.01). This will reduce the CPU load, but will marginally increase the time between the keypress and the resulting beep.

Peracid answered 12/9, 2012 at 9:37 Comment(8)
Thanks. Starting the app with nice -n 19 does still peg the CPU but the system is more responsive at least. This approach has a drawback of still keeping the CPU monitor pegged, so it becomes useless for determining unrelated system loads. Furthermore, the temp stays very high with the CPU this active. So this is a good workaround, but I really need something that addresses the root issue.Shogun
Lanaru's first suggestion was to add a small sleep delay. If you use time.sleep(0.05) you will at most a 50ms delay between your your keypress and yout beep. This will reduce the CPU load a lot.Peracid
Thanks, reviewing that even a delay of 0.01 reduces the CPU use significantly without affecting the operation.Shogun
Why the downvote? I'm eager to learn what was wrong with my answer?Peracid
I upvoted, I sometimes never understand why some questions get downvotes without explanations. In fact, I would like to see SO punish downvotes with no comment.Shogun
The nice is solution only for CPU-bound processes to not destroy interactivity. A sleep is never a real solution either as it would only make the code miss presses. This is an IO-bound process; the original code uses the wrong X11 call that just queries the keyboard status, the real solution is to use a blocking call instead.Torment
Thank you Antti. Which blocking call might that be, or where might I find it?Shogun
This was a hard call, there were some terrific answers here. I really learned from them all. In the end, the best compromise seems to be a small 0.01 delay in the while loop, and a generous helping of nice. This makes for an application that uses less resources, and gives up the resources that it does use when other applications need it, but still provides real-time keypress notifications in all applications. Thank you!Shogun
H
5

Your program is hogging CPU because it is running an infinite loop that will keep your CPU busy with checking the keyboard status every millisecond or so, even if no keys are pressed. Since your computer has no indication that it can stop, it will keep on checking as often as possible, thereby burning resources.

As Iain suggested, the most elegant solution to this problem is to use Display.next_event(), which will tell the program to wait until a new event (e.g. a keypress) is received. During this waiting time, your program will not consume significant amounts of CPU and your load should go down dramatically.

Homorganic answered 16/8, 2012 at 15:37 Comment(1)
Thank you. Unfortunately, the Display.next_event() seems to require its own window, and only functions for keypresses inside that window. I need a global solution.Shogun
R
4

Working from this answer to a previously posted question, reveals a Python module called pyxhook that is part of a program called pykeylogger.

Using this pyxhook module, you should be able to beep on each keypress with the following code:

import pyxhook
import time
import os
import sys

def do_beep(event):
    os.system('beep')

hm = pyxhook.HookManager()
hm.HookKeyboard()
hm.KeyDown = do_beep
hm.start()
while True:
    try:
        time.sleep(10)
    except: 
        break
hm.cancel()

I can't properly test it because I don't have a beep command. It does, however, report sh: beep: command not found for each keypress.

Roofdeck answered 19/9, 2012 at 0:30 Comment(5)
Thank you for finding the Linux version of pyhook! This solution is much more reasonable with the CPU, but it queues beep commands that are run too fast, so it is not suitable for real-time typing indication.Shogun
You could try os.system('beep&') to run each beep in the background, although I'm not sure how they will interact with each other. In addition, some versions of the beep command also allow customization of the beep sound -- for example, reducing the duration.Roofdeck
That is a good idea. In fact, I have tried to change the beep sound but Kubuntu doesn't seem to acknowledge the change.Shogun
You seem to be changing the X11 bell settings, and then using the beep command to trigger the bell. I think the beep command interfaces with the PC speaker directly, and so is likely ignoring your X11 bell settings entirely. Try using the options of the beep command itself. The man page for beep has several examples, such as beep -f 400 -l 10. Note also that beep has -s and -c options, which you can use to avoid starting a new beep process for every single key press. This may also help (slightly) with delay.Roofdeck
Thank you nandhp! It did not even occur to me to use beep flags.Shogun

© 2022 - 2024 — McMap. All rights reserved.