asynchronize your life: shell commands 10x faster

I use the window manager StumpWM, and I love it. If you've never experienced Stump, the easiest way to describe it is the emacs of WMs. Plentiful with features, but most importantly super hackable. But today I just wanted to talk about how shell-command calls are done in Stump, and how with a few lines of code I made it 10x faster.

Stump has a very useful function called (run-shell-command) for running commands with sh, it takes a command and an optional boolean if you want it to return the string the command returns. (run-shell-command) is built on top of (run-program) from, uiop a popular portable way to use shell commands in Common Lisp. So what's wrong? It's pretty slow. Internally (run-program) runs a new instance of sh, sends the command, and then returns the value and terminates the shell. This has quite an overhead cost, especially when you are running a lot of these at once; such as in a mode-line.

Like the emacs by which Stump is very influenced, we have a mode-line. When I first got into Stump, I filled this baby up with functions depending on (run-shell-command). What I didn't know at first was that the mode-line is redrawn every time you run any Stump command. This means when you have lots of relatively slow functions you have an a sluggish and unresponsive window manager.

Asynchronizing with launch-program.

Another useful set of functions in uiop is (launch-program) and it's supporting functions. (launch-program) runs an asynchronous command, so the idea here is to run an asynchronous shell, then send commands to it and receive the output. The first step is opening up an instance of bash:
(defparameter *async-shell* (uiop:launch-program "bash" :input :stream :output :stream))
We use those :input and :output options to allow us to access the input and output as streams by using (process-info-output) and (process-info-input) respectively. Those functions take an instance of (launch-program) and return the associated input or output streams. Now we can start our function to send and receive some input and output. The first thing we need to do it send a command:
(write-line "echo hi" (uiop:process-info-input *async-shell*))
(force-output (uiop:process-info-input *async-shell*))
This sends the input into our shell and flushes the stream through. There are other ways such as (finish-output) which waits to return until the string has been flushed to the stream, but here it doesn't much matter either way. Now that our shell has been sent some input, let's see if it worked:
(read-line (uiop:process-info-output *async-shell*))
;; "hi"
;; NIL
It works! Let's wrap it in a function:
(defun async-run (command) (write-line command (uiop:process-info-input *async-shell*)) (force-output (uiop:process-info-input *async-shell*)) (let* ((output-string (read-line (uiop:process-info-output *async-shell*))) (stream (uiop:process-info-output *async-shell*))) (if (listen stream) (loop while (listen stream) do (setf output-string (concatenate 'string output-string '(#\Newline) (read-line stream))))) output-string))
Basically this function forces whatever command you give it into the open bash, then returns the output, with properly concatenated newlines at the ends of the lines. There's an important note here to be made about how (read-line) works, and why the (async-run) function needs to be given a command that will return a value. (read-line) will sit on the stream waiting for a new line to read forever, so unless you wrap it with a conditional using a (sleep) or something it will hang. For my purposes I didn't need to build in this conditional, because I only use this for commands that always return. If your use case needs even faster non-returning commands, I would look into sb-thread or bt for threading to implement a time based conditional. With that said let's test the speed of this versus the pre-existing (run-shell-command) and see if we've achieved the desired speedup.
STUMPWM> (time (loop for i from 1 to 1000 do (run-shell-command "ls"))) Evaluation took: 9.778 seconds of real time 4.849494 seconds of total run time (0.157795 user, 4.691699 system) 49.59% CPU 21,589,308,018 processor cycles 14,307,504 bytes consed NIL STUMPWM> (time (loop for i from 1 to 1000 do (async-run "ls"))) Evaluation took: 0.903 seconds of real time 0.060049 seconds of total run time (0.032583 user, 0.027466 system) [ Run times consist of 0.009 seconds GC time, and 0.052 seconds non-GC time. ] 6.64% CPU 1,994,328,392 processor cycles 4,249,760 bytes consed
See! I wasn't lying about a ten times increase! I would say that my mode-line responsiveness has increased, but I've actually already replaced all my mode-line functions that used to call shell commands with ones that use built in CL functions or that slurp in files on my system (which turns out to be even faster than using shell commands). I'll soon write a post about that too. Hopefully some anonymous reader will benefit from the speed increase of asynchronizing shell commands.