Reworked IO loop, simplification & removing restarts.

This commit is contained in:
Mihail Ivanchev 2025-01-28 11:02:30 +01:00
parent d54bebd3c4
commit 7f6c57f361
5 changed files with 110 additions and 205 deletions

View file

@ -66,9 +66,8 @@
;;;; multiplexer to actually run it.
(export '(io-channel-ioport io-channel-events io-channel-handle
*default-io-loop* *current-io-loop* *current-io-channel*
io-loop io-loop-add io-loop-remove io-loop-update
callback-channel callback-channel-stream callback-channel-events))
*default-io-loop* *current-io-loop*))
;;; General interface
(defgeneric io-channel-ioport (io-loop channel)
@ -84,6 +83,7 @@
An I/O channel may also return NIL to indicate that it is only
interested in purely virtual events, such as :TIMEOUT or :LOOP."))
(defgeneric io-channel-events (channel)
(:documentation
"Returns a list of events that CHANNEL is interested in. An event
@ -118,6 +118,7 @@
other point in time, it should use the IO-LOOP-UPDATE function to
notify the I/O loop of such changes. The I/O loop may also update
spuriously at any time, but such updates are not guaranteed."))
(defgeneric io-channel-handle (channel event &key &allow-other-keys)
(:documentation
"Called by the I/O loop to notify a channel that an event has
@ -129,12 +130,15 @@
(defgeneric io-loop-add (io-loop channel)
(:documentation "Add a channel to the given I/O multiplexer to be monitored."))
(defgeneric io-loop-remove (io-loop channel)
(:documentation "Unregister a channel from the I/O multiplexer."))
(defgeneric io-loop-update (io-loop channel)
(:documentation "Make the I/O loop update its knowledge of what
events CHANNEL is interested in. See the documentation for
IO-CHANNEL-EVENTS for more information."))
(defgeneric io-loop (io-loop &key &allow-other-keys)
(:documentation "Run the given I/O multiplexer, watching for events
on any channels registered with it. IO-LOOP will return when it has
@ -148,11 +152,6 @@
"Dynamically bound to the I/O loop currently running, providing an
easy way for event callbacks to register new channels.")
(defvar *current-io-channel* nil
"While processing an I/O channel, this variable is dynamically bound
to the channel in question. This is provided primarily for
error-handling code.")
;; Default methods for the above
(defmethod io-channel-handle (channel event &key &allow-other-keys)
(declare (ignore channel event)))
@ -193,197 +192,105 @@
(defmethod io-loop-update ((info sbcl-io-loop) channel)
(declare (ignore info channel)))
(defmethod io-loop ((info sbcl-io-loop) &key description)
;;; Calculates the maximum blocking time that can be spend waiting for
;;; IO activity before channels interested in timeout events (i.e. timers)
;;; need to be notified.
(defun get-max-blocking-time (lowest-timeout)
(declare (type (or null (integer 0)) lowest-timeout))
(if lowest-timeout
(let ((remaining (- lowest-timeout (get-internal-real-time))))
(if (> remaining 0)
(multiple-value-bind (whole-secs sub-sec)
(truncate (/ remaining internal-time-units-per-second))
(values whole-secs (nth-value 0 (truncate (* sub-sec 1000000)))))
(values 0 0)))
(values nil nil)))
(defmethod io-loop ((info sbcl-io-loop) &key &allow-other-keys)
(let ((*current-io-loop* info))
(with-simple-restart (:quit-ioloop "Quit I/O loop~A"
(if description
(format nil " (~A)" description)
""))
(block io-loop
(sb-alien:with-alien ((rfds (sb-alien:struct sb-unix:fd-set))
(wfds (sb-alien:struct sb-unix:fd-set))
(efds (sb-alien:struct sb-unix:fd-set)))
(loop
:do
(with-simple-restart (:restart-ioloop "Restart at I/O loop~A"
(if description
(format nil " (~A)" description)
""))
(macrolet ((with-channel-restarts ((channel &optional remove-code) &body body)
(let ((ch (gensym "CHANNEL")))
`(let* ((,ch ,channel)
(*current-io-channel* ,ch))
(restart-case
(progn ,@body)
(:skip-channel ()
:report (lambda (s)
(format s "Continue as if without channel ~S" ,ch))
nil)
(:remove-channel ()
:report (lambda (s)
(format s "Unregister channel ~S and continue" ,ch))
,(or remove-code `(io-loop-remove info ,ch))
nil))))))
(let ((ch-map (make-hash-table :test 'eql))
(timeouts '())
(loop-ch '())
(maxfd 0))
;; Since it is select(2)-based, this implementation
;; updates the entire set of interesting events once
;; every iteration.
(let ((remove '()))
(sb-alien:with-alien ((rfds (sb-alien:struct sb-unix:fd-set))
(wfds (sb-alien:struct sb-unix:fd-set))
(efds (sb-alien:struct sb-unix:fd-set)))
(loop
(let ((loop-channels nil)
(read-channels nil)
(write-channels nil)
(timeout-channels nil)
(inactive-channels nil)
(highest-fd 0)
(earliest-timeout nil))
(sb-unix:fd-zero rfds)
(sb-unix:fd-zero wfds)
(sb-unix:fd-zero efds)
(sb-unix:fd-zero rfds)
(sb-unix:fd-zero wfds)
(sb-unix:fd-zero efds)
;; Collect the file descriptors we need to wait on in
;; the next step and the channels without any events
;; so we can remove them. Also group the active events
;; in categories.
(dolist (ch (slot-value info 'channels))
(if-let ((evs (io-channel-events ch)))
(dolist (ev evs)
(let ((ev-type (if (consp ev) (car ev) ev))
(ev-data (if (consp ev) (car (cdr ev)) nil))
(ev-fd (io-channel-ioport info ch)))
(case ev-type
(:loop
(push ch loop-channels))
(:read
(setf highest-fd (max highest-fd ev-fd))
(sb-unix:fd-set ev-fd rfds)
(push (cons ch ev-fd) read-channels))
(:write
(setf highest-fd (max highest-fd ev-fd))
(sb-unix:fd-set ev-fd wfds)
(push (cons ch ev-fd) write-channels))
(:timeout
(setf earliest-timeout (min (or earliest-timeout ev-data) ev-data))
(push (cons ch ev-data) timeout-channels)))))
(push ch inactive-channels)))
(dolist (channel (slot-value info 'channels))
(with-channel-restarts (channel (push channel remove))
(let ((fd (io-channel-ioport info channel)))
(let ((events (io-channel-events channel)))
(if events
(dolist (event events)
(multiple-value-bind (event data)
(if (consp event)
(values (car event) (cdr event))
(values event nil))
(case event
(:read
(setf maxfd (max maxfd fd))
(sb-unix:fd-set fd rfds)
(push (cons :read channel)
(gethash fd ch-map '())))
(:write
(setf maxfd (max maxfd fd))
(sb-unix:fd-set fd wfds)
(push (cons :write channel)
(gethash fd ch-map '())))
(:timeout
(let ((timeout (car data)))
(check-type timeout real)
(push (cons timeout channel) timeouts)))
(:loop
(push channel loop-ch)))))
(push channel remove))))))
(dolist (channel remove)
(io-loop-remove info channel))
(unless (slot-value info 'channels)
(return-from io-loop)))
;; Call any :LOOP channels
(dolist (channel loop-ch)
(with-channel-restarts (channel)
(io-channel-handle channel :loop)))
(setf timeouts (sort timeouts '< :key 'car))
(flet ((compute-timeout ()
(if timeouts
(let* ((internal-time-of-timeout (car (first timeouts)))
(remaining-internal-time (- internal-time-of-timeout
(get-internal-real-time)))
(remaining-seconds (/ remaining-internal-time
internal-time-units-per-second))
(s-to-ms 1000000)
(remaining-ms (max
(round (* remaining-seconds
s-to-ms))
0)))
(floor remaining-ms 1000000))
(values nil nil))))
;; Actually block for events
(multiple-value-bind (rval errno)
(multiple-value-call #'sb-unix:unix-fast-select
(1+ maxfd)
(sb-alien:addr rfds)
(sb-alien:addr wfds)
(sb-alien:addr efds)
(compute-timeout))
(declare (ignore rval))
(cond ((and errno (plusp errno))
(unless (eql errno sb-unix:eintr)
(dformat 1
"Unexpected ~S error: ~A~%"
'sb-unix:unix-fast-select
(sb-int:strerror errno))))
(t
;; Notify channels for transpired events
(maphash (lambda (fd evs)
(let ((r (sb-unix:fd-isset fd rfds))
(w (sb-unix:fd-isset fd wfds))
(e (sb-unix:fd-isset fd efds)))
(dolist (ev evs)
(with-channel-restarts ((cdr ev))
(cond ((and (eq (car ev) :read)
(or r e))
(io-channel-handle (cdr ev) :read))
((and (eq (car ev) :write)
w)
(io-channel-handle (cdr ev) :write)))))))
ch-map)))))
;; Check for timeouts
(when timeouts
(block timeouts
(let ((now (get-internal-real-time)))
(dolist (to timeouts)
(if (<= (car to) now)
(with-channel-restarts ((cdr to))
(io-channel-handle (cdr to) :timeout))
(return-from timeouts)))))))))))))))
(dolist (ch inactive-channels)
(io-loop-remove info ch))
;;; IO-CHANNEL-IOPORT methods for support facilities
(defmethod io-channel-ioport (io-loop (channel sb-sys:fd-stream))
(declare (ignore io-loop))
(sb-sys:fd-stream-fd channel))
(defmethod io-channel-ioport ((io-loop sbcl-io-loop) (channel xlib:display))
(io-channel-ioport io-loop (xlib::display-input-stream channel)))
(unless (slot-value info 'channels)
(return))
;;; Default methods for widely supported objects
(defmethod io-channel-ioport (io-loop (channel synonym-stream))
(io-channel-ioport io-loop (symbol-value (synonym-stream-symbol channel))))
;; Notify the :LOOP channels before blocking.
(dolist (ch loop-channels)
(io-channel-handle ch :loop))
;;; Callback channel implementation
(defclass callback-channel ()
((current :initform nil)
(stream :initarg :stream :reader callback-channel-stream)
(read-function :initform nil :initarg :read)
(write-function :initform nil :initarg :write)
(events :initform :auto :initarg :events :accessor callback-channel-events))
(:documentation
"Implements a convenience I/O channel which takes an underlying I/O
facility and calls the given callback functions when an event
occurs on the channel. The :STREAM init-argument specifies the I/O
facility to monitor, :READ specifies a function to be called back
when a read event occurs, and :WRITE a corresponding function for
write events. Timeouts are not supported.
;; Block while waiting for something to happen on the
;; monitored file descriptors. This is implemented with
;; select(2). After that, notify the interested :READ
;; and :WRITE handlers.
(multiple-value-bind (secs usecs)
(get-max-blocking-time earliest-timeout)
(multiple-value-bind (count errno)
(sb-unix:unix-fast-select
(1+ highest-fd)
(sb-alien:addr rfds)
(sb-alien:addr wfds)
(sb-alien:addr efds)
secs
usecs)
(declare (ignore count))
(if (and errno (plusp errno))
(unless (eql errno sb-unix:eintr)
(dformat 1
"Unexpected ~S error: ~A~%"
'sb-unix:unix-fast-select
(sb-int:strerror errno)))
(progn
(loop :for (ch . fd) :in read-channels
:do (when (or (sb-unix:fd-isset fd rfds)
(sb-unix:fd-isset fd efds))
(io-channel-handle ch :read)))
(loop :for (ch . fd) :in write-channels
:do (when (sb-unix:fd-isset fd wfds)
(io-channel-handle ch :write)))))))
By default, the channel will listen for read events iff a read
callback function is given and correspondingly for write events,
but CALLBACK-CHANNEL-EVENTS can be SETF'd to specify events
explicitly in case certain events are only interesting
sporadically. To restore default behavior, set it to :AUTO."))
(defmethod io-loop-add :before (info (channel callback-channel))
(when (slot-value channel 'current)
(error "Callback channel is already registered with an I/O loop")))
(defmethod io-loop-add :after (info (channel callback-channel))
(setf (slot-value channel 'current) info))
(defmethod io-loop-remove :after (info (channel callback-channel))
(setf (slot-value channel 'current) nil))
(defmethod io-channel-ioport (io-loop (channel callback-channel))
(io-channel-ioport io-loop (slot-value channel 'stream)))
(defmethod io-channel-events ((channel callback-channel))
(with-slots (events) channel
(if (eq events :auto)
(let ((ret '()))
(when (slot-value channel 'read-function)
(push :read ret))
(when (slot-value channel 'write-function)
(push :write ret))
ret)
events)))
(defmethod io-channel-handle ((channel callback-channel) (event (eql :read)) &key)
(funcall (slot-value channel 'read-function) channel))
(defmethod (setf callback-channel-events) (events channel)
(setf (slot-value channel 'events) events)
(with-slots (current) channel
(when current (io-loop-update current channel))))
;; Notify all channels with now expired timeouts.
(loop :with now = (get-internal-real-time)
:for (ch . timeout) :in timeout-channels
:do (when (<= timeout now)
(io-channel-handle ch :timeout))))))))

View file

@ -9,7 +9,7 @@
(uiop/lisp-build:compile-warned-warning . 1)
(sb-int:type-style-warning . 1)
(sb-kernel:redefinition-with-defmacro . 34)
(sb-c::unknown-typep-note . 8)))
(sb-c::unknown-typep-note . 6)))
(actual-warnings
(mapcar (lambda (x) (cons (car x) 0)) expected-warnings))
(in-ci (< 0 (length (sb-ext:posix-getenv "STUMPWM_CI"))))

View file

@ -86,13 +86,6 @@ further up. "
(muffle-warning))
(defmethod handle-top-level-condition ((c serious-condition))
(when (and (find-restart :remove-channel)
(not (typep *current-io-channel*
'(or stumpwm-timer-channel
display-channel
request-channel))))
(message "Removed channel ~S due to uncaught error '~A'." *current-io-channel* c)
(invoke-restart :remove-channel))
(ecase *top-level-error-action*
(:message
(let ((s (format nil "~&Caught '~a' at the top level. Please report this." c)))
@ -121,7 +114,7 @@ further up. "
(defvar *request-channel* nil)
(defmethod io-channel-ioport (io-loop (channel request-channel))
(io-channel-ioport io-loop (request-channel-in channel)))
(sb-sys:fd-stream-fd (request-channel-in channel)))
(defmethod io-channel-events ((channel request-channel))
(list :read))
@ -165,9 +158,12 @@ further up. "
((display :initarg :display)))
(defmethod io-channel-ioport (io-loop (channel display-channel))
(io-channel-ioport io-loop (slot-value channel 'display)))
(sb-sys:fd-stream-fd
(xlib::display-input-stream (slot-value channel 'display))))
(defmethod io-channel-events ((channel display-channel))
(list :read :loop))
(flet ((dispatch-all (display)
(block handle
(loop

View file

@ -2788,7 +2788,6 @@ IO-LOOP-UPDATE (as described above). Call IO-LOOP on the
multiplexer to actually run it.
### *default-io-loop*
### *current-io-loop*
### *current-io-channel*
@@@ io-channel-ioport
@@@ io-channel-events
@@@ io-channel-handle

View file

@ -121,12 +121,15 @@ The action is to call FUNCTION with arguments ARGS."
(defmethod io-channel-ioport (io-loop (channel stumpwm-timer-channel))
(declare (ignore io-loop))
nil)
(defmethod io-channel-events ((channel stumpwm-timer-channel))
(sb-thread:with-mutex (*timer-list-lock*)
(if *timer-list*
`((:timeout ,(timer-time (car *timer-list*))))
'(:loop))))
(defmethod io-channel-handle ((channel stumpwm-timer-channel) (event (eql :timeout)) &key)
(run-expired-timers))
(defmethod io-channel-handle ((channel stumpwm-timer-channel) (event (eql :loop)) &key)
(run-expired-timers))