fix(pdf-roll): survive a revert that lands during a page render - #362
fix(pdf-roll): survive a revert that lands during a page render#362alberti42 wants to merge 6 commits into
Conversation
|
I can see the problem but I think this is the wrong way of solve it: it is much simpler if Does the following diff fix this? diff --git a/lisp/pdf-roll.el b/lisp/pdf-roll.el
index 44b7100..411d561 100644
--- a/lisp/pdf-roll.el
+++ b/lisp/pdf-roll.el
@@ -52,6 +52,8 @@
(defvar pdf-roll--state nil
"Local variable that tracks window, point and vscroll to handle changes.")
+(defvar pdf-roll--delay-revert)
+
;;; Utility Macros and functions
(defsubst pdf-roll-page-to-pos (page)
"Get the buffer position displaying PAGE."
@@ -233,7 +235,8 @@ It should be added to `pre-redisplay-functions' buffer locally."
(with-demoted-errors "Error in image roll pre-redisplay: %S"
(unless (pdf-roll-page-overlay 1 win)
(pdf-roll-new-window-function win))
- (let* ((state (alist-get win pdf-roll--state))
+ (let* ((pdf-roll--delay-revert t)
+ (state (alist-get win pdf-roll--state))
(pscrolling (memq last-command
'( pixel-scroll-precision pixel-scroll-start-momentum
pixel-scroll-interpolate-up pixel-scroll-interpolate-down
@@ -405,16 +408,18 @@ If PIXELS is non-nil N is number of pixels instead of lines."
It is also added to `revert-buffer-function'.
It erases the buffer and adds one line containing a space for each page."
- (image-mode-window-put 'displayed-pages nil)
- (setq pdf-roll--state nil)
- (remove-overlays)
- (let ((pages (pdf-cache-number-of-pages))
- (inhibit-read-only t))
- (erase-buffer)
- (dotimes (_i (* 2 (+ pages 1)))
- (insert " \n"))
- (delete-char -1)
- (set-buffer-modified-p nil)))
+ (if pdf-roll--delay-revert
+ (run-at-time 0 nil #'pdf-roll-initialize)
+ (image-mode-window-put 'displayed-pages nil)
+ (setq pdf-roll--state nil)
+ (remove-overlays)
+ (let ((pages (pdf-cache-number-of-pages))
+ (inhibit-read-only t))
+ (erase-buffer)
+ (dotimes (_i (* 2 (+ pages 1)))
+ (insert " \n"))
+ (delete-char -1)
+ (set-buffer-modified-p nil))))
(defvar pdf-view-roll-minor-mode-map
(let ((map (make-sparse-keymap)))
I think this is fixed in #338. |
Rendering a page waits on the epdfinfo process, and `accept-process-output' runs timers, process filters and sentinels while it waits. Any of them can revert the buffer -- a recompiled LaTeX document reaches its viewer through the TeX process sentinel -- and the revert calls `pdf-roll-initialize', which removes every overlay and builds a new set. The render then returns to overlays that are no longer there. Rather than have each caller cope with overlays that disappeared under it, keep `pdf-roll-pre-redisplay' free of that possibility: while it runs, `pdf-roll-initialize' postpones its work to a timer that runs once the render has finished. Patch by aikrahguzar, proposed in vedang#362 (comment), transposed from the `upstream-child-frame-preview' branch onto `master'.
Two corrections to the previous commit.
`(defvar pdf-roll--delay-revert)` with no value form declares the
variable special but leaves it void, so reading it outside the binding
signals:
(void-variable pdf-roll--delay-revert)
`pdf-roll-initialize' is reached from the `revert-buffer-function'
advice and from the body of `pdf-view-roll-minor-mode', neither of them
inside the binding, so an ordinary `revert-buffer' and turning the mode
on both failed. Give it a nil default.
`run-at-time' runs its function with whatever buffer happens to be
current when the timer fires, which is not necessarily the one that was
reverted, and `pdf-roll-initialize' calls `remove-overlays' and
`erase-buffer' on the current buffer under `inhibit-read-only'. Capture
the buffer and check it is still live.
…play `pdf-roll-pre-redisplay' is not the only caller that renders a page. `pdf-roll-scroll-forward' and `pdf-roll-scroll-backward' call `pdf-roll-display-page' as they walk from one page to the next, and they run as commands rather than as part of redisplay, so a revert arriving while one of their renders waited on the server could still remove the overlays. Bind `pdf-roll--delay-revert' in `pdf-roll-display-page', which holds the only call to `pdf-view-create-page' in the file. The binding in `pdf-roll-pre-redisplay' stays: it covers the whole redisplay pass rather than the renders alone, and `pdf-roll-display-pages' also waits on the server between them, through `pdf-cache-number-of-pages'.
`pdf-roll-undisplay-pages' is given the difference between the `displayed-pages' a window remembers and the pages just drawn, and that list outlives the overlays. `pdf-roll-initialize' clears `displayed-pages' with `image-mode-window-put' and no window argument, so only the selected window is cleared -- which after the change to a timer is not necessarily the window showing the PDF, and never the other windows showing the same buffer. A document that is shorter after the revert then leaves those windows remembering a page the buffer no longer holds an overlay for, inside the buffer or past its end. Independent of when the revert runs, so it stays a guard rather than something the postponement takes care of.
`pdf-roll-initialize' called while a render is waiting has to leave the buffer and its overlays alone and queue the work instead. The path that does the work needs a document and a running server, so only the postponement is covered here.
6a6457f to
d9812cf
Compare
Postponing `pdf-roll-initialize' is not enough on its own: it is an
`:after' advice on `revert-buffer-function', and by the time it runs
`revert-buffer' has already replaced the buffer text. A page overlay
carries `evaporate', so the replacement collapses it. On entry to
`pdf-roll-initialize' the buffer holds the raw PDF and has no page
overlays left:
(initialize :delay t :overlays nil :point-max 284930) ; file is 284929 bytes
So the postponement delays the rebuild, not the destruction, and a render
that was waiting still returns to an overlay that is gone.
Measured on a 23-page document, with the revert forced into the middle of
a render from a window other than the one showing the PDF:
master 2/2 Error in image roll pre-redisplay
postponement only 5/5 Error in image roll pre-redisplay
guards only 0/2
postponement and guards 0/3
`pdf-roll-display-page' reads the overlay and `pdf-roll-display-image'
writes to it; both skip a page whose overlay is missing.
`pdf-roll-pre-redisplay' needs the same care for a different reason: it
records page, height, width and vscroll after `pdf-roll-display-pages'
returns, and recorded after a revert that state tells the next redisplay
nothing has changed, so the window stays on the empty buffer. Guarding
the record on the window still having overlays keeps the recovery
redisplay working.
The postponement stays. It stops `pdf-roll-initialize' rebuilding the
overlays part-way through a render, which is a real thing to avoid even
though it is not what causes the error.
|
Thanks for reviewing the PR so quickly! I built the branch around your patch, but testing it turned up something that changes the conclusion, so it is now in the branch together with the guards rather than in place of them. Your commit is first, with you as the author, adjusted to
I also bound the flag in On its own the postponement does not stop the error. I forced the race deterministically, with a ;; -*- lexical-binding: t; -*-
;; One-shot: make a revert land inside the next page render.
(defvar my-pdf-race-armed t)
(defun my-pdf-race (&rest _)
(when my-pdf-race-armed
(setq my-pdf-race-armed nil)
(let* ((buf (current-buffer))
(pdfwin (get-buffer-window buf t))
;; Any other window on the PDF's frame. With AUCTeX the revert
;; comes from the TeX process sentinel, so the selected window is
;; the one showing the LaTeX source, not the one being drawn.
(win (car (seq-remove (lambda (w) (eq w pdfwin))
(window-list (window-frame pdfwin))))))
(unless win (user-error "Split the PDF's frame first"))
(run-at-time 0 nil
(lambda ()
(when (buffer-live-p buf)
(with-selected-window win
(with-current-buffer buf (revert-buffer t t)))))))))
(advice-add 'pdf-view-create-page :before #'my-pdf-race)Open a PDF with Four details matter. The 23-page document, counting only The reason is that The buffer already holds the raw PDF and has no page overlays left. So the postponement delays the rebuild, not the collapse, and a render that was waiting still returns to an overlay that is gone. I kept it all the same, for the reason you gave: it stops On #338: you are right that One more thing the same test showed, which I have left alone: |
What happens
Open a PDF with
pdf-view-roll-minor-modeand let something revert it — AUCTeX'sTeX-after-compilation-finished-functionswithTeX-revert-document-bufferin it,auto-revert-mode, a file-notification watcher. Recompiling the document while the PDF is on screen then often ends in:pdf-roll-pre-redisplaywraps its body inwith-demoted-errors, which iscondition-case-unless-debug. So withdebug-on-erroroff this shows up as a page that fails to paint: the overlay keeps the(space :width 25 :height 1000)placeholder it was built with, so the window shows blank space where the page should be, and goes on showing it until the page, the window size or the vscroll changes. Withdebug-on-erroron you get the backtrace above.Why
pdf-roll-display-pagerenders the page and hands the image topdf-roll-display-image, which puts it on the overlay holding that page. Rendering is a synchronous query, andpdf-info-querywaits for the server like this:accept-process-outputruns timers, process filters and sentinels while it waits, and a revert of the PDF buffer arrives through exactly those: AUCTeX reverts from the TeX process sentinel,auto-revert-modefrom a timer, file notifications from the event loop. So the buffer can be reverted in the middle ofpdf-roll-display-pages, while that is inside redisplay.Two separate things then happen to the overlays, and it matters which is which.
revert-bufferreplaces the buffer text. A page overlay carriesevaporate, so the replacement collapses it. This happens insiderevert-bufferitself, beforepdf-roll-initialize— which is an:afteradvice onrevert-buffer-function— is reached at all. Tracing the entry topdf-roll-initializeon a 285 kB document:pdf-roll-initializethen rebuilds the overlays, and it builds them for(selected-window)— inside a sentinel or a timer, whatever window happened to be selected, which with AUCTeX is the window of the LaTeX source rather than the window being drawn.When the render returns, the overlay it was going to write to is gone, and
overlay-putgets nil.The fix
Two parts, and both are needed.
Postpone the rebuild. While a render is in progress,
pdf-roll-initializeputs its work on a timer that runs once the render has finished, so it does not replace a window's overlays part-way through a redisplay pass. The approach and the first commit here are @aikrahguzar's, from the review below, transposed ontomasterwherepdf-roll-initializediffers a little. Two corrections came with it:(defvar pdf-roll--delay-revert)with no value form leaves the variable void rather than nil, sopdf-roll-initializesignalled(void-variable pdf-roll--delay-revert)on an ordinaryrevert-bufferand when the mode was turned on; andrun-at-timeruns its function with whatever buffer is current when it fires, whilepdf-roll-initializecallsremove-overlaysanderase-bufferon the current buffer underinhibit-read-only, so the buffer has to be captured and checked for liveness. The flag is bound inpdf-roll-display-pageas well as inpdf-roll-pre-redisplay, becausepdf-roll-scroll-forwardandpdf-roll-scroll-backwardalso render as they walk from page to page and run as commands rather than as part of redisplay.Skip the overlays that are already gone. The postponement does not prevent the error on its own, because the collapse happens in
revert-bufferbefore the postponement can apply.pdf-roll-display-pagereads the overlay andpdf-roll-display-imagewrites to it; both now skip a page whose overlay is missing. The revert changed the buffer, so a redisplay follows that draws the page again.For that redisplay to draw anything, the window state may only be remembered when the pages really were drawn.
pdf-roll-pre-redisplayrecords page, height, width and vscroll afterpdf-roll-display-pagesreturns; recorded after a revert, that state tells the next redisplay nothing has changed, and the window stays on the empty buffer. Guarding the record on the window still having overlays keeps the recovery redisplay working.pdf-roll-undisplay-pagesneeds the same care for its own reason: it is handed the difference between thedisplayed-pagesa window remembers and the pages just drawn, and that list outlives the overlays.pdf-roll-initializeclears it withimage-mode-window-putand no window argument, so only the selected window is cleared, and a document that is shorter after the revert leaves the others naming a page with no overlay, inside the buffer or past its end.Measurements
The race was forced deterministically: a
run-at-time 0revert scheduled from a:beforeadvice onpdf-view-create-page, so it fires while that render waits on the server, with a window other than the PDF's selected. 23-page document, counting onlyError in image roll pre-redisplay: (wrong-type-argument overlayp nil):lisp/pdf-roll.elmasterTests
Two in
test/pdf-roll-test.el: thatpdf-roll-initializecalled while a render is in progress leaves the buffer and its overlays alone and queues the work instead, and thatpdf-roll-undisplay-pagesskips a page with no overlay. The paths that render need a live window and a running server, so they are not covered in batch, in keeping with the note at the top of that file.No new byte-compile warnings.
Not fixed here
The same forced revert also produces
Error during redisplay: (eval (pdf-misc-size-indication) t) signaled (wrong-type-argument overlayp nil), 4 to 6 times per run.pdf-misc-size-indicationis only the caller, through the mode line's:eval; the unguarded read is inpdf-view-image-size, which in roll mode ends in(overlay-get (pdf-roll-page-overlay page window) 'display). It has the same cause as the above and is present onmaster.Guarding that read is not sufficient on its own:
image-sizeandimage-display-sizethen fail on nil, andpdf-misc-size-indicationneeds a number to format a percentage. Whatpdf-view-image-sizeshould report when the page has no image yet is a decision of its own, so it is not in this branch.Relation to #361, #364 and #338
Independent of #361 and #364, and branched off
masterlike both of them. Onlytest/pdf-roll-test.elconflicts, because each appends a section at the end of the file.#338 does drop the
pdf-roll-new-window-functioncall frompdf-roll-initialize, so the overlays are no longer rebuilt for whatever window happened to be selected. The error survives that:pdf-roll-display-pagethere still reads the overlay unguarded andpdf-roll-display-imagestill ends in an unguardedoverlay-put, and the collapse inrevert-bufferis untouched by it. This branch should apply to #338 with little change.