Subject: [PATCH] Speed up asynchronous man page fontifying

From: Stefan Kangas <[email-masked]>
Date: Sat, 4 Jan 2025 01:22:03 -0600

Severity: wishlist

The below patch makes asynchronous fontifying of man pages faster.

After typing `M-x man RET gcc-14 RET`, it takes around 5 minutes on this
fast MacBook Pro M2 (2022) machine until the whole 1.43 MiB man page is
displayed.  With a hot cache, the best time I have managed to measure is
173.432 seconds.

We seem to call `man--maybe-fontify-manpage` many times on very small
chunks: on my machine, it processes ~30 characters at a time.  Things
get substantially faster if we make `Man-bgproc-filter` fontify the
buffer in much larger chunks.  The main drawback of doing this is that
we risk very briefly seeing an incorrect display flash by in the man
buffer (e.g., with `end-of-buffer`).

My measurements show that 32 KiB might be a good choice, and gives a
~95% speedup:

| Chunk size (KiB) | Completion time (s) |
|------------------+---------------------|
|                4 |                29.3 |
|                8 |                18.2 |
|               16 |                12.7 |
|               32 |                 7.4 |
|               64 |                 6.5 |
|              128 |                 5.4 |

Is this the best approach to optimize something like this, or am I
overlooking something obvious?  Note that I didn't add a variable for
the chunk size, but we could easily add one, if that'd be useful.

When I set `Man-prefer-synchronous-call` to t, it takes ~1.8 seconds to
process the same page.  I guess that this is the lower bound for how
fast we could make the asynchronous call.

Please see the attached.
From 2f677cf4457ae4a2fdf02cee658988da3a27dafc Mon Sep 17 00:00:00 2001
From: Stefan Kangas <[email-masked]>
Date: Sat, 4 Jan 2025 07:38:33 +0100
Subject: [PATCH] Fontify man page in 32 KiB chunks

* lisp/man.el (man--bgproc-filter-fontify): New function.
(Man-bgproc-filter): Use above new function; fontify in 32 KiB chunks.
(Man-bgproc-sentinel): Use above new function.
(man--bgproc-filter-last-pos): New variable.
(Man-getpage-in-background): Set above new variable to 0.
---
 lisp/man.el | 32 +++++++++++++++++++++-----------
 1 file changed, 21 insertions(+), 11 deletions(-)

diff --git a/lisp/man.el b/lisp/man.el
index 54ca8cbae9f..470d6790ba2 100644
--- a/lisp/man.el
+++ b/lisp/man.el
@@ -417,6 +417,7 @@ Man-reverse
 (defvar-local Man-original-frame nil)
 (defvar-local Man-arguments nil)
 (put 'Man-arguments 'permanent-local t)
+(defvar-local man--bgproc-filter-last-pos nil)
 
 (defvar-local Man--sections nil)
 (defvar-local Man--refpages nil)
@@ -1243,8 +1244,9 @@ Man-getpage-in-background
 			  (Man-shell-file-name)
 			  shell-command-switch
 			  (format (Man-build-man-command) man-args))))
-	       (set-process-sentinel proc 'Man-bgproc-sentinel)
-	       (set-process-filter proc 'Man-bgproc-filter))
+               (set-process-sentinel proc #'Man-bgproc-sentinel)
+               (set-process-filter proc #'Man-bgproc-filter)
+               (setq man--bgproc-filter-last-pos 0))
 	   (let* ((inhibit-read-only t)
 		  (exit-status
 		   (process-file
@@ -1515,6 +1517,19 @@ man--maybe-fontify-manpage
       (Man-fontify-manpage)
     (Man-cleanup-manpage)))
 
+(defun man--bgproc-filter-fontify ()
+  (let ((inhibit-read-only t))
+    (save-restriction
+      (narrow-to-region
+       (save-excursion
+         (goto-char man--bgproc-filter-last-pos)
+         ;; Process whole sections (Bug#36927).
+         (Man-previous-section 1)
+         (point))
+       (point))
+      (setq man--bgproc-filter-last-pos (point))
+      (man--maybe-fontify-manpage))))
+
 (defun Man-bgproc-filter (process string)
   "Manpage background process filter.
 When manpage command is run asynchronously, PROCESS is the process
@@ -1532,15 +1547,9 @@ Man-bgproc-filter
 	    (save-excursion
 	      (goto-char beg)
 	      (insert string)
-	      (save-restriction
-		(narrow-to-region
-		 (save-excursion
-		   (goto-char beg)
-                   ;; Process whole sections (Bug#36927).
-                   (Man-previous-section 1)
-                   (point))
-		 (point))
-		(man--maybe-fontify-manpage))
+              ;; Chunk the processing.
+              (when (>= (- beg man--bgproc-filter-last-pos) 0)
+                (man--bgproc-filter-fontify))
 	      (set-marker (process-mark process) (point-max)))))))))
 
 (defun Man-bgproc-sentinel (process msg)
@@ -1560,6 +1569,7 @@ Man-bgproc-sentinel
 	    (set-process-buffer process nil))
 
       (with-current-buffer Man-buffer
+        (man--bgproc-filter-fontify)
 	(save-excursion
 	  (let ((case-fold-search nil)
                 (inhibit-read-only t))
-- 
2.47.1