Reduce flydiff-update calls in diff-hl-flydiff-mode#278
Conversation
When diff-hl-flydiff-mode is enabled, ensure that diff-hl-update function is only called when there are changes in the buffer and not every time the idle timer triggers. This reduces the number of calls to diff-hl-update and thus allows diff-hl-mode to run smoother on weaker (or older hardware). The function diff-hl-flydiff/modified-p is rewritten so it uses buffer-chars-modified-tick internally instead of relying on buffer-modified-p. It is thus the only function that need to call buffer-chars-modified-tick and all other calls have been removed. Furthermore the buffer-local variable diff-hl-flydiff-modified-tick is only updated when it differs from buffer-chars-modified-tick. Note that when diff-hl-flydiff/modified-p is called there is a guard that checks if the given state is one of 'added' or 'missing'. Unless one of those two states is given this will trigger a check using buffer-chars-modified-tick. This means that a nil state will trigger such a check. The idle timer calls the diff-hl-update function, but an :around advice is used. That is the diff-hl-flydiff-update function decides if and when the original diff-hl-update function is called. To simplify the logic the existing 'unless' is replaced with 'when' and thus all checks are inverted; only when all conditions are met the update function diff-hl-update is called. Customizeable variable diff-hl-flydiff-delay added to existing group diff-hl-flydiff. Documentation strings written for existing functions diff-hl-flydiff-update and diff-hl-flydiff/modified-p. NOTE: diff-hl-dired.el had a hard dependency to the vc-run-delayed-success function introduced in Emacs 31. A call to 'static-if' is added to check if such a function is bound or not. If it is not bound, then a direct call to vc-hg-after-dir-status is made instead. Not sure if this is the best way to support older Emacs versions.
dgutov
left a comment
There was a problem hiding this comment.
Things look great otherwise.
Could I ask if you are willing to sign FSF's copyright papers?
You submitted two PRs now, and while I could probably squeeze this one in, the two together are over the allowed limit for not assigned code.
| "The idle delay in seconds before highlighting is updated." | ||
| :type 'number) | ||
| :type 'number | ||
| :group 'diff-hl-flydiff) |
There was a problem hiding this comment.
:group is usually not needed if the last defgroup in the file is the same.
| Calls ORIG-FUN only when buffer has been modified since last | ||
| call, buffer exists as a file, and it is not a remote file." | ||
| (when (and diff-hl-mode | ||
| (diff-hl-flydiff/modified-p nil) |
There was a problem hiding this comment.
What happens if subsequent checks resolve to nil? But diff-hl-flydiff/modified-p already updated diff-hl-flydiff-modified-tick. Maybe we should do it here, before calling orig-fun.
There was a problem hiding this comment.
Not entirely sure I follow you here. The construct looks like this
(when (and diff-hl-mode
(diff-hl-flydiff/modified-p nil)
(stringp buffer-file-name)
(not (file-remote-p default-directory))
(file-exists-p buffer-file-name))
(apply orig-fun nil))
Since the 'and' function in Emacs LISP short-circuits, the following checks are never called if 'diff-hl-flydiff/modified-p' return 'nil'.
Are you thinking of a scenario where a buffer is created that is not associated with any file and then it is written to a file (function 'buffer-file-name' goes from returning 'nil' to a string containing name of file)? If you then insert/modify a character in the file 'diff-hl-flydiff/modified-p' would return non-'nil' and 'orig-fun' is called.
There might be a race condition though where a buffer is modified, the timer triggers a check, 'diff-hl-flydiff/modified-p' return non-'nil' and then the file is moved which causes the 'and'-clause to return 'nil' (due to that 'file-exists-p' would return 'nil'). If the file is moved back and the timer fires again the 'and'-clause would still return 'nil' due to that 'diff-hl-flydiff/modified-p' return 'nil' in this case.
One way of removing this race-condition would be to introduce an additional buffer-local variable (for instance 'diff-hl-flydiff--update-state') that is cleared after 'orig-fun' returns and 'diff-hl-flydiff/modified-p' do an 'or' between the value of this new variable and 'buffer-chars-modified-tick' like this
(unless (memq state '(added missing))
(let ((buffer-state (not (eq diff-hl-flydiff-modified-tick
(buffer-chars-modified-tick)))))
(if (or buffer-state diff-hl-flydiff--update-state)
(progn
(setq diff-hl-flydiff-modified-tick (buffer-chars-modified-tick))
(setq diff-hl-flydiff--update-state t)
(setq buffer-state diff-hl-flydiff--update-state)))
buffer-state)))
That is, if you time above attempt on causing a race condition as described the next time the timer fires 'diff-hl-flydiff-modified-tick' is still updated (even though the value has not changed) and non-'nil' is returned the next time the function is called. The following tick would then result in an update where 'orig-fun' is called and 'diff-hl-flydiff--update-state' is set to 'nil', that is
(when (and diff-hl-mode
(diff-hl-flydiff/modified-p nil)
(stringp buffer-file-name)
(not (file-remote-p default-directory))
(file-exists-p buffer-file-name))
(apply orig-fun nil)
(setq diff-hl-flydiff--update-state nil))
Perhaps you were thinking of something else that I missed?
|
|
|
Great! Here you go: |
When diff-hl-flydiff-mode is enabled, ensure that diff-hl-update function is only called when there are changes in the buffer and not every time the idle timer triggers. This reduces the number of calls to diff-hl-update and thus allows diff-hl-mode to run smoother on weaker (or older hardware).
The function diff-hl-flydiff/modified-p is rewritten so it uses buffer-chars-modified-tick internally instead of relying on buffer-modified-p. It is thus the only function that need to call buffer-chars-modified-tick and all other calls have been removed. Furthermore the buffer-local variable diff-hl-flydiff-modified-tick is only updated when it differs from buffer-chars-modified-tick.
Note that when diff-hl-flydiff/modified-p is called there is a guard that checks if the given state is one of 'added' or 'missing'. Unless one of those two states is given this will trigger a check using buffer-chars-modified-tick. This means that a nil state will trigger such a check.
The idle timer calls the diff-hl-update function, but an :around advice is used. That is the diff-hl-flydiff-update function decides if and when the original diff-hl-update function is called. To simplify the logic the existing 'unless' is replaced with 'when' and thus all checks are inverted; only when all conditions are met the update function diff-hl-update is called.
Customizeable variable diff-hl-flydiff-delay added to existing group diff-hl-flydiff.
Documentation strings written for existing functions diff-hl-flydiff-update and diff-hl-flydiff/modified-p.
NOTE: diff-hl-dired.el had a hard dependency to the vc-run-delayed-success function introduced in Emacs 31. A call to 'static-if' is added to check if such a function is bound or not. If it is not bound, then a direct call to vc-hg-after-dir-status is made instead. Not sure if this is the best way to support older Emacs versions.