Emacs performance problems in python-mode
when using project-rootfile
and TRAMP. Updated on
May 24, 2025 to wrap up the saga.
Back in March I wrote about using project-rootfile
to handle nested projects in Eglot. At the
time I said:
Hopefully there are no downsides to this setup with any of my existing projects.
It's been 2 months, but unfortunately I have now stumbled on an existing project that behaves very poorly with this setup. I use TRAMP to access the Linux kernel project on a remote Fedora host and happily use Eglot over TRAMP with the clangd language server. There is a python project called YNL that lives in the kernel git repository and it is this project that is giving me grief.
I have been contributing to the YNL project for a couple of years and Emacs has behaved faultlessly all that time. This week I came back to look at the python code and was immediately derailed by multi-second freeze-ups while trying to work with python files.
There seems to be a really bad interaction between project-rootfile
, python-mode
and TRAMP.
- Editing the same files from a local copy of the project is fine
- Only python files over TRAMP cause the issue
- Commenting out the
project-rootfile
config resolves the issue
; (add-to-list 'project-find-functions #'project-rootfile-try-detect)
I used the Emacs built-in profiler to try and identify the root cause:
9810 84% - #<byte-code-function 975> 9810 84% - eldoc-print-current-symbol-info 9810 84% - eldoc--invoke-strategy 9810 84% - eldoc-documentation-default 9810 84% - run-hook-wrapped 9810 84% - #<byte-code-function B8B> 9810 84% - python-eldoc-function 9810 84% - python-eldoc--get-doc-at-point 9810 84% - python-shell-get-process 9810 84% - python-shell-get-buffer 9810 84% - seq-some 9810 84% - seq-do 9810 84% - mapc 9810 84% - #<byte-code-function 4C4> 9810 84% - #<byte-code-function 8BB> 9810 84% - python-shell-get-process-name 9808 84% - project-current 9808 84% - project--find-in-directory 9808 84% - run-hook-with-args-until-success 9808 84% - project-rootfile-try-detect 9808 84% - locate-dominating-file 9629 82% - #<byte-code-function 0A5> 9629 82% - project-rootfile--root-p 9062 77% - seq-some 9062 77% - seq-do 9062 77% - mapc 9062 77% - #<byte-code-function 5A1> 9062 77% - #<byte-code-function 5BA> 9043 77% - file-exists-p 9043 77% - tramp-file-name-handler 9039 77% - apply 9038 77% - tramp-sh-file-name-handler 9037 77% - apply 9032 77% - tramp-sh-handle-file-exists-p 9005 77% - tramp-send-command-and-check 9004 77% - tramp-send-command 9004 77% - apply 9004 77% - #<byte-code-function F39> 8777 75% + tramp-wait-for-output 227 1% + tramp-send-string 1 0% string-match 19 0% + expand-file-name
Firstly, this confirms that it is the combination of python-mode
, project-rootfile
and TRAMP
that is causing Emacs to freeze. Secondly, it shows that the problem is triggered by
eldoc-mode
when its idle timer fires. Right enough, when I turn off eldoc-mode
the problem
goes away. I don't even need the mode enabled, it's just on by default in python-mode
.
The fix for now is to turn off eldoc-mode
when in python-mode
.
(add-hook 'python-mode-hook
(lambda ()
(eldoc-mode -1)))
Sanity preserved!
Redux, redux
Yeah? Nope. It turns out that completion is also borked so I actually spent some time debugging
the issue. First question: why does this work for (project-try-vc)
, the default implementation
for project-find-functions
. It's clear that (project-current)
isn't caching anything and it
gets called an awful lot from python.el
.
Digging into (project-try-vc)
, I discover here's where the magic happens:
(defun project-try-vc (dir)
;; FIXME: Learn to invalidate when the value of
;; `project-vc-merge-submodules' or `project-vc-extra-root-markers'
;; changes.
(or (vc-file-getprop dir 'project-vc)
(let* ((backend-markers
;;
;; ...
;;
(vc-file-setprop dir 'project-vc project)
project))))
I'm about to add the same caching behaviour to project-rootfile.el
, when I notice
project-vc-extra-root-markers
is an existing extension mechanism for (project-try-vc)
.
(setq project-vc-extra-root-markers '("Cargo.toml" "pyproject.toml" "requirements.txt" "go.mod"))
After that interesting excursion, I've reverted the project-rootfile
experiment and configured
the project
native behaviour that I need.
Happy days.
The project-vc-extra-root-markers
feature was added about 2½ years ago, first appearing in
Emacs 29. That's a good reminder that it is worth reading through the NEWS for each new release.