Skip to content

Reduce flydiff-update calls in diff-hl-flydiff-mode#278

Open
ermingol23 wants to merge 1 commit into
dgutov:masterfrom
ermingol23:reduce-update-calls
Open

Reduce flydiff-update calls in diff-hl-flydiff-mode#278
ermingol23 wants to merge 1 commit into
dgutov:masterfrom
ermingol23:reduce-update-calls

Conversation

@ermingol23
Copy link
Copy Markdown

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.

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.
Copy link
Copy Markdown
Owner

@dgutov dgutov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread diff-hl-flydiff.el
"The idle delay in seconds before highlighting is updated."
:type 'number)
:type 'number
:group 'diff-hl-flydiff)
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:group is usually not needed if the last defgroup in the file is the same.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll remove the ':group'!

Comment thread diff-hl-flydiff.el
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)
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

@ermingol23
Copy link
Copy Markdown
Author

Could I ask if you are willing to sign FSF's copyright papers?
Sure! No problems! Please let me know how to do it (what forms to fill in, where to send them, and so on).

@dgutov
Copy link
Copy Markdown
Owner

dgutov commented May 25, 2026

Great! Here you go:

Please email the following information to assign@gnu.org, and we
will send you the assignment form for your past and future changes.

Please use your full legal name (in ASCII characters) as the subject
line of the message.
----------------------------------------------------------------------
REQUEST: SEND FORM FOR PAST AND FUTURE CHANGES

[What is the name of the program or package you're contributing to?]
Emacs

[Did you copy any files or text written by someone else in these changes?
Even if that material is free software, we need to know about it.]


[Do you have an employer who might have a basis to claim to own
your changes?  Do you attend a school which might make such a claim?]


[For the copyright registration, what country are you a citizen of?]


[What year were you born?]


[Please write your email address here.]


[Please write your postal address here.]





[Which files have you changed so far, and which new files have you written
so far?]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants