Error in undo-list-transfer-to-tree while saving or undoing in large Org file

What happened?

When I save my monolithic todo.org file (257k) or use undo-tree to undo a change, Emacs often freezes. With other (smaller) Org files, this doesn’t usually happen. I am mentioning both the “save” and the “undo” because the backtraces that I’ve managed to get are identical; the error happens in the undo-list-transfer-to-tree function. Spamming C-g during the hang does nothing, but after adding (toggle-debug-on-quit) to my config, I was able to generate these backtraces (spamming C-g, pressing it once didn’t immediately trigger the debugger):

Hang on save and undo generate an identical backtrace:

Debugger entered--Lisp error: (quit)
  #<subr undo-list-transfer-to-tree>()
  apply(#<subr undo-list-transfer-to-tree> nil)
  undo-list-transfer-to-tree()
  #<subr undo-tree-save-history>(nil overwrite)
  apply(#<subr undo-tree-save-history> (nil overwrite))
  (prog1 (apply fn args) (message ""))
  (let ((inhibit-message t) (save-silently t)) (prog1 (apply fn args) (message "")))
  (if init-file-debug (progn (apply fn args)) (let ((inhibit-message t) (save-silently t)) (prog1 (apply fn args) (message ""))))
  doom-shut-up-a(#<subr undo-tree-save-history> nil overwrite)
  apply(doom-shut-up-a #<subr undo-tree-save-history> (nil overwrite))
  undo-tree-save-history(nil overwrite)
  undo-tree-save-history-from-hook()
  run-hook-with-args-until-success(undo-tree-save-history-from-hook)
  basic-save-buffer(nil)
  #<subr save-buffer>()
  funcall(#<subr save-buffer>)
  (let ((apheleia-mode (and apheleia-mode (memq arg '(nil 1))))) (funcall orig-fn))
  +format--inhibit-reformat-on-prefix-arg-a(#<subr save-buffer>)
  apply(+format--inhibit-reformat-on-prefix-arg-a #<subr save-buffer> nil)
  save-buffer()
  evil-write(nil nil nil nil nil)
  funcall-interactively(evil-write nil nil nil nil nil)
  evil-ex-call-command(nil "w" nil)
  evil-ex(nil)
  funcall-interactively(evil-ex nil)
  command-execute(evil-ex)

It’s possible the freeze on undo is related to garbage collection, because I also got this backtrace once:

;; Debugger entered--entering a function:
* undo-tree-post-gc()
  garbage-collect()
  gcmh-idle-garbage-collect()
  apply(gcmh-idle-garbage-collect nil)
  timer-event-handler([t 26059 39268 866324 nil gcmh-idle-garbage-collect nil nil 840999 nil])

Values of:

  • gcmh-mode: t
  • gcmh-low-cons-threshold: 800000
  • gcmh-high-cons-threshold: 33554432

I was able to get a CPU and memory profile during the undo-list-transfer-to-tree error (starting the profiler right before I tried to reproduce the error, and ending it right after the hang finished).

CPU:


Loading data dump...

Memory:


Loading data dump...

What did you expect to happen?

Org file should save almost instantly. Undo should work almost instantly, or at least not hang.

Steps to reproduce

Saving:

  1. Start Emacs (new instance)
  2. Open todo.org
  3. Make a change
  4. Save :w

Undoing:

  1. Make change to org file
  2. Undo the change (u)

System information


Loading data dump...