unofficial mirror of bug-gnu-emacs@gnu.org 
 help / color / mirror / code / Atom feed
* bug#53432: [PATCH] Avoid losing keyboard input when inotify is too busy
@ 2022-01-21 23:36 Ian Jackson
  2022-01-22  0:38 ` Po Lu via Bug reports for GNU Emacs, the Swiss army knife of text editors
  2022-01-22  6:45 ` bug#53432: [PATCH] Avoid losing keyboard input when inotify is too busy Eli Zaretskii
  0 siblings, 2 replies; 32+ messages in thread
From: Ian Jackson @ 2022-01-21 23:36 UTC (permalink / raw)
  To: 53432

[-- Attachment #1: message body text --]
[-- Type: text/plain, Size: 1342 bytes --]

I experienced a bug where my Emacs (Debian's emacs-lucid) would lose
keystrokes under pathological conditions.  I tracked this down to the
keyboard input buffer filling up with inotify events.

The attached patches have been rebased onto emacs-28 (fbc9b121e062)
and I have tested before-and-after versions in parallel to verify that
the bug appears in the upstream branch and has been fixed with my
patches.

The bug was really quite severe in my (rather abusive) situation, with
Debian's emacs-lucid 26.1.  With the upstream emacs-28 branch it
requires more effort, and loses fewer keys.  I suspect that some more
sophisticated approach to inotify requests means it's harder to get
the buffer to fill up with the newer version.  Unfortunately I am not
able to share my whole repro, but a key part is to visit the output
file from this
  while yes | dd of=t bs=1 count=1000000; do : ;
with global-auto-revert-mode enabled.

With my patches it is still possible to get emacs to perform very
poorly by abusing it this way, but at least all the keys arrive
eventually.  I added some FIXMEs for further work that would be good,
but is not critical to fixing the bug.

I hope the commit messages are in the expected format.  I think I
probably have GNU copyright paperwork on file already, perhaps under a
different email address.

Regards,
Ian.


[-- Attachment #2: 0001-inotify.c-Document-hazard-of-unlimited-buffering.patch --]
[-- Type: application/octet-stream, Size: 939 bytes --]

From a90b9c3c54eabe4e9429911417a8f3196b9036c2 Mon Sep 17 00:00:00 2001
From: Ian Jackson <ijackson@chiark.greenend.org.uk>
Date: Fri, 21 Jan 2022 20:28:05 +0000
Subject: [PATCH 1/4] * inotify.c: Document hazard of unlimited buffering

---
 src/inotify.c | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/src/inotify.c b/src/inotify.c
index e92ad40abc..b676ea216a 100644
--- a/src/inotify.c
+++ b/src/inotify.c
@@ -316,6 +316,9 @@ inotify_callback (int fd, void *_)
   if (ioctl (fd, FIONREAD, &to_read) < 0)
     report_file_notify_error ("Error while retrieving file system events",
 			      Qnil);
+  // FIXME: feeding the value from FIONREAD to a memory allocator is unwise
+  // Sadly the inotify event buffer parsing algorithm is not capable of
+  // handling the case where we read only part of a kernel event.
   USE_SAFE_ALLOCA;
   char *buffer = SAFE_ALLOCA (to_read);
   ssize_t n = read (fd, buffer, to_read);
-- 
2.20.1


[-- Attachment #3: 0002-inotify.c-Use-a-buffer-from-the-heap.patch --]
[-- Type: application/octet-stream, Size: 2382 bytes --]

From 6a5dd5766a5806258d665cab343a103b5705bf32 Mon Sep 17 00:00:00 2001
From: Ian Jackson <ijackson@chiark.greenend.org.uk>
Date: Fri, 21 Jan 2022 19:42:43 +0000
Subject: [PATCH 2/4] * inotify.c: Use a buffer from the heap

We are going to need this buffer to be persistent, because we may need
to stop processing it halfway through (if the main input event buffer
fills up).

Also, getting a value from FIONREAD and then feeding it to alloca is
even less wise than feeding it to malloc.
---
 src/inotify.c | 17 +++++++++++------
 1 file changed, 11 insertions(+), 6 deletions(-)

diff --git a/src/inotify.c b/src/inotify.c
index b676ea216a..ad02164b37 100644
--- a/src/inotify.c
+++ b/src/inotify.c
@@ -43,6 +43,9 @@ Copyright (C) 2012-2022 Free Software Foundation, Inc.
 /* File handle for inotify.  */
 static int inotifyfd = -1;
 
+static size_t inotify_buffer_len = 0;
+static char *inotify_buffer = NULL;
+
 /* Alist of files being watched.  We want the returned descriptor to
    be unique for every watch, but inotify returns the same descriptor
    WD for multiple calls to inotify_add_watch with the same file.
@@ -319,9 +322,13 @@ inotify_callback (int fd, void *_)
   // FIXME: feeding the value from FIONREAD to a memory allocator is unwise
   // Sadly the inotify event buffer parsing algorithm is not capable of
   // handling the case where we read only part of a kernel event.
-  USE_SAFE_ALLOCA;
-  char *buffer = SAFE_ALLOCA (to_read);
-  ssize_t n = read (fd, buffer, to_read);
+  if (to_read > inotify_buffer_len)
+    {
+      inotify_buffer = xrealloc(inotify_buffer, to_read);
+      inotify_buffer_len = to_read;
+    }
+
+  ssize_t n = read (fd, inotify_buffer, to_read);
   if (n < 0)
     report_file_notify_error ("Error while reading file system events", Qnil);
 
@@ -331,7 +338,7 @@ inotify_callback (int fd, void *_)
 
   for (ssize_t i = 0; i < n; )
     {
-      struct inotify_event *ev = (struct inotify_event *) &buffer[i];
+      struct inotify_event *ev = (struct inotify_event *) &inotify_buffer[i];
       Lisp_Object descriptor = INT_TO_INTEGER (ev->wd);
       Lisp_Object prevtail = find_descriptor (descriptor);
 
@@ -351,8 +358,6 @@ inotify_callback (int fd, void *_)
         }
       i += sizeof (*ev) + ev->len;
     }
-
-  SAFE_FREE ();
 }
 
 DEFUN ("inotify-add-watch", Finotify_add_watch, Sinotify_add_watch, 3, 3, 0,
-- 
2.20.1


[-- Attachment #4: 0003-inotify.c-Break-out-inotify_process_buffer.patch --]
[-- Type: application/octet-stream, Size: 1679 bytes --]

From bc46afb34524d97a354a636c476544daedf560cd Mon Sep 17 00:00:00 2001
From: Ian Jackson <ijackson@chiark.greenend.org.uk>
Date: Fri, 21 Jan 2022 19:51:25 +0000
Subject: [PATCH 3/4] * inotify.c: Break out inotify_process_buffer

We pass the buffer fullness in a global variable.  This is weird right
now, but we are going to want this to persist so it is convenient to
do it now.
---
 src/inotify.c | 13 ++++++++++++-
 1 file changed, 12 insertions(+), 1 deletion(-)

diff --git a/src/inotify.c b/src/inotify.c
index ad02164b37..8a3685f2f1 100644
--- a/src/inotify.c
+++ b/src/inotify.c
@@ -44,6 +44,7 @@ Copyright (C) 2012-2022 Free Software Foundation, Inc.
 static int inotifyfd = -1;
 
 static size_t inotify_buffer_len = 0;
+static size_t inotify_buffer_full = 0;
 static char *inotify_buffer = NULL;
 
 /* Alist of files being watched.  We want the returned descriptor to
@@ -71,6 +72,9 @@ Copyright (C) 2012-2022 Free Software Foundation, Inc.
 
 static Lisp_Object watch_list;
 
+static void
+inotify_process_buffer (void);
+
 static Lisp_Object
 mask_to_aspects (uint32_t mask)
 {
@@ -332,11 +336,18 @@ inotify_callback (int fd, void *_)
   if (n < 0)
     report_file_notify_error ("Error while reading file system events", Qnil);
 
+  inotify_buffer_full = n;
+  inotify_process_buffer ();
+}
+
+static void
+inotify_process_buffer (void)
+{
   struct input_event event;
   EVENT_INIT (event);
   event.kind = FILE_NOTIFY_EVENT;
 
-  for (ssize_t i = 0; i < n; )
+  for (ssize_t i = 0; i < inotify_buffer_full; )
     {
       struct inotify_event *ev = (struct inotify_event *) &inotify_buffer[i];
       Lisp_Object descriptor = INT_TO_INTEGER (ev->wd);
-- 
2.20.1


[-- Attachment #5: 0004-inotify-Pause-when-keyboard-input-is-suspended.patch --]
[-- Type: application/octet-stream, Size: 7206 bytes --]

From 7b94f182683ae12e3f39a82a9957239d9b35832e Mon Sep 17 00:00:00 2001
From: Ian Jackson <ijackson@chiark.greenend.org.uk>
Date: Fri, 21 Jan 2022 19:56:13 +0000
Subject: [PATCH 4/4] inotify: Pause when keyboard input is suspended

If the emacs main thread cannot process events quickly enough, the
input buffer can become full.  If that happens, events can get lost.

There is a mechanism to detect this in keyboard.c, and prevent people
from putting more stuff into the buffer, but it was not wired up to
the inotify machinery.  So heavy inotify load can cause lost
keystrokes!

Fix this.

* inotify.c: Be able to pause and resume the reading, retaining the
buffered events as necessary.
* keyboard.c: Call stop_inotify_input and resume_inotify_input.

Details of the approach are:

 - The `i` loop variable in `inotify_process_buffer` becomes a global
   variable, so that we can retain our knowledge of where we were.

 - Enabling or disabling reading from the inotify fd is now done in a
   single idempotent function which checks whether (i) input is
   suspended (ii) the buffer of unprocssed inotify events is empty.
   Happily add_read_fd and delete_read_fd are idempotent.

 - New hooks are provided by which keyboard.c tells inotify.c when
   input is being suspended and unsuspended.  Suspending input is
   straightforward.  Unsuspending it may involve processing buffered
   events (and that might cause the input to be suspended again!)

I have checked that this fixes the lost input bug, which I can
reproduce with a crazy test setup.  With my patch, when emacs is
processing too many inotify events, it can become rather unresponsive
- but when it recovers, all the events are there.

Tested-by: Ian Jackson <ijackson@chiark.greenend.org.uk>
---
 src/inotify.c  | 57 ++++++++++++++++++++++++++++++++++++++++++++++----
 src/keyboard.c |  2 ++
 src/keyboard.h |  9 ++++++++
 3 files changed, 64 insertions(+), 4 deletions(-)

diff --git a/src/inotify.c b/src/inotify.c
index 8a3685f2f1..97c0fd9efa 100644
--- a/src/inotify.c
+++ b/src/inotify.c
@@ -43,8 +43,15 @@ Copyright (C) 2012-2022 Free Software Foundation, Inc.
 /* File handle for inotify.  */
 static int inotifyfd = -1;
 
+/* Buffer is like this:
+ *   |oooo|======================|---------|
+ *   0    done                   full      len
+ * "oooo" is events we have processed into the keyboard buffer.
+ * "====" is events we have read from the kernel but not processed.
+ * "----" is spare space that was used on a previous occasion  */
 static size_t inotify_buffer_len = 0;
 static size_t inotify_buffer_full = 0;
+static size_t inotify_buffer_done = 0;
 static char *inotify_buffer = NULL;
 
 /* Alist of files being watched.  We want the returned descriptor to
@@ -74,6 +81,8 @@ Copyright (C) 2012-2022 Free Software Foundation, Inc.
 
 static void
 inotify_process_buffer (void);
+static void
+inotify_add_or_remove_reading_fd (void);
 
 static Lisp_Object
 mask_to_aspects (uint32_t mask)
@@ -319,6 +328,14 @@ remove_watch (Lisp_Object descriptor, Lisp_Object id)
 static void
 inotify_callback (int fd, void *_)
 {
+  if ( inotify_buffer_done != inotify_buffer_full )
+    {
+      inotify_add_or_remove_reading_fd ();
+      return;
+    }
+
+  // Buffer is empty.  We will reset done and full after reading into it.
+
   int to_read;
   if (ioctl (fd, FIONREAD, &to_read) < 0)
     report_file_notify_error ("Error while retrieving file system events",
@@ -337,9 +354,14 @@ inotify_callback (int fd, void *_)
     report_file_notify_error ("Error while reading file system events", Qnil);
 
   inotify_buffer_full = n;
+  inotify_buffer_done = 0;
   inotify_process_buffer ();
+  inotify_add_or_remove_reading_fd ();
 }
 
+/* Idempotently process events from our buffer of things we have read
+ * from the kernel, into the input event buffer.  Call sites should
+ * call inotify_add_or_remove_reading_fd afterwards.  */
 static void
 inotify_process_buffer (void)
 {
@@ -347,9 +369,14 @@ inotify_process_buffer (void)
   EVENT_INIT (event);
   event.kind = FILE_NOTIFY_EVENT;
 
-  for (ssize_t i = 0; i < inotify_buffer_full; )
+  // FIXME: Ideally in the future some arrangement would be made to
+  // prioritise keyboard input over inotify events, which would
+  // involve using a tighter condition than kbd_on_hold_p.
+
+  while( inotify_buffer_done < inotify_buffer_full && !kbd_on_hold_p () )
     {
-      struct inotify_event *ev = (struct inotify_event *) &inotify_buffer[i];
+      struct inotify_event *ev =
+	(struct inotify_event *) &inotify_buffer[inotify_buffer_done];
       Lisp_Object descriptor = INT_TO_INTEGER (ev->wd);
       Lisp_Object prevtail = find_descriptor (descriptor);
 
@@ -367,10 +394,32 @@ inotify_process_buffer (void)
           if (ev->mask & IN_IGNORED)
 	    remove_descriptor (prevtail, true);
         }
-      i += sizeof (*ev) + ev->len;
+      inotify_buffer_done += sizeof (*ev) + ev->len;
     }
 }
 
+/* Idempotently start, or stop, reading from the inotify fd, according
+ * to what is required.  */
+static void inotify_add_or_remove_reading_fd (void) {
+  if (inotifyfd >= 0) {
+    if ( kbd_on_hold_p () || inotify_buffer_done != inotify_buffer_full )
+      delete_read_fd (inotifyfd);
+    else
+      add_read_fd (inotifyfd, &inotify_callback, NULL);
+  }
+}
+
+/* Called when keyboard buffer is getting full, kbd_on_hold_p becomes true. */
+void stop_inotify_input (void) {
+  inotify_add_or_remove_reading_fd ();
+}
+
+/* Called when buffer can take more events, kbd_on_hold_p becomes false */
+void resume_inotify_input (void) {
+  inotify_process_buffer();
+  inotify_add_or_remove_reading_fd ();
+}
+
 DEFUN ("inotify-add-watch", Finotify_add_watch, Sinotify_add_watch, 3, 3, 0,
        doc: /* Add a watch for FILE-NAME to inotify.
 
@@ -447,7 +496,7 @@ renames (moved-from and moved-to).
       if (inotifyfd < 0)
 	report_file_notify_error ("File watching is not available", Qnil);
       watch_list = Qnil;
-      add_read_fd (inotifyfd, &inotify_callback, NULL);
+      inotify_add_or_remove_reading_fd ();
     }
 
   encoded_file_name = ENCODE_FILE (filename);
diff --git a/src/keyboard.c b/src/keyboard.c
index 9865bc9add..171bdcf1d5 100644
--- a/src/keyboard.c
+++ b/src/keyboard.c
@@ -3657,6 +3657,7 @@ kbd_buffer_store_buffered_event (union buffered_input_event *event,
           hold_keyboard_input ();
           unrequest_sigio ();
           stop_polling ();
+	  stop_inotify_input ();
         }
 #endif	/* subprocesses */
     }
@@ -3849,6 +3850,7 @@ kbd_buffer_get_event (KBOARD **kbp,
       unhold_keyboard_input ();
       request_sigio ();
       start_polling ();
+      resume_inotify_input ();
     }
 #endif	/* subprocesses */
 
diff --git a/src/keyboard.h b/src/keyboard.h
index 03aa96ad4b..a832d08c2b 100644
--- a/src/keyboard.h
+++ b/src/keyboard.h
@@ -499,4 +499,13 @@ kbd_buffer_store_event_hold (struct input_event *event,
 
 INLINE_HEADER_END
 
+/* Defined in inotify.c */
+#ifdef HAVE_INOTIFY
+extern void stop_inotify_input (void);
+extern void resume_inotify_input (void);
+#else
+INLINE void stop_inotify_input (void) { }
+INLINE void resume_inotify_input (void) { }
+#endif
+
 #endif /* EMACS_KEYBOARD_H */
-- 
2.20.1


[-- Attachment #6: .signature --]
[-- Type: text/plain, Size: 218 bytes --]



-- 
Ian Jackson <ijackson@chiark.greenend.org.uk>   These opinions are my own.  

Pronouns: they/he.  If I emailed you from @fyvzl.net or @evade.org.uk,
that is a private address which bypasses my fierce spamfilter.

^ permalink raw reply related	[flat|nested] 32+ messages in thread

end of thread, other threads:[~2022-01-25 14:50 UTC | newest]

Thread overview: 32+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2022-01-21 23:36 bug#53432: [PATCH] Avoid losing keyboard input when inotify is too busy Ian Jackson
2022-01-22  0:38 ` Po Lu via Bug reports for GNU Emacs, the Swiss army knife of text editors
2022-01-22 19:34   ` bug#53432: [PATCH] Avoid losing keyboard input when inotify is too busy [and 1 more messages] Ian Jackson
2022-01-22 19:48     ` Eli Zaretskii
2022-01-23  2:37       ` Dmitry Gutov
2022-01-23  2:47         ` Po Lu via Bug reports for GNU Emacs, the Swiss army knife of text editors
2022-01-23  2:49           ` Dmitry Gutov
2022-01-23  2:53             ` Po Lu via Bug reports for GNU Emacs, the Swiss army knife of text editors
2022-01-23  3:23               ` Dmitry Gutov
2022-01-23  3:26                 ` Po Lu via Bug reports for GNU Emacs, the Swiss army knife of text editors
2022-01-23  3:36                   ` Dmitry Gutov
2022-01-23  3:48                     ` Po Lu via Bug reports for GNU Emacs, the Swiss army knife of text editors
2022-01-23  4:07                       ` Dmitry Gutov
2022-01-23  5:50                   ` Eli Zaretskii
2022-01-23 16:34                     ` Michael Albinus
2022-01-23  1:00     ` Po Lu via Bug reports for GNU Emacs, the Swiss army knife of text editors
2022-01-23 13:38       ` Ian Jackson
2022-01-23 16:37         ` Michael Albinus
2022-01-23 18:31           ` Ian Jackson
2022-01-24  0:42             ` Po Lu via Bug reports for GNU Emacs, the Swiss army knife of text editors
2022-01-24 14:42             ` Michael Albinus
2022-01-24 14:57               ` Eli Zaretskii
2022-01-24 15:35                 ` Michael Albinus
2022-01-24 16:52                   ` Eli Zaretskii
2022-01-24 17:12                     ` Michael Albinus
2022-01-24 17:25                       ` Eli Zaretskii
2022-01-24 15:45               ` Ian Jackson
2022-01-24 17:05                 ` Michael Albinus
2022-01-24 18:59                   ` Ian Jackson
2022-01-25 14:50                     ` Michael Albinus
2022-01-24  0:26         ` Po Lu via Bug reports for GNU Emacs, the Swiss army knife of text editors
2022-01-22  6:45 ` bug#53432: [PATCH] Avoid losing keyboard input when inotify is too busy Eli Zaretskii

Code repositories for project(s) associated with this public inbox

	https://git.savannah.gnu.org/cgit/emacs.git

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).