Subject: bug#77620: thingatpt can be incredibly slow in python-mode buffers

From: kobarity <[email-masked]>
Date: Mon, 21 Apr 2025 00:05:37 +0900

Eli Zaretskii wrote:
> 
> > Date: Thu, 10 Apr 2025 00:33:03 +0900
> > From: kobarity <[email-masked]>
> > Cc: JD Smith <[email-masked]>,
> > 	[email-masked]
> > 
> > Eli Zaretskii wrote:
> > > 
> > > > In #77588 this was shown to lead to long intermittent pauses with eglot in large python buffers, but the issue is more general for any packages using thingatpt in python buffers.
> > > 
> > > The Lisp profile, which seems to point to the likely culprit, is
> > > below.
> > > 
> > > Maybe kobarity (CC'ed) will have some ideas or suggestions.
> > > 
> > >          345  89% - ...
> > >          343  89%  - cond
> > >          343  89%   - let
> > >          343  89%    - condition-case
> > >          343  89%     - progn
> > >          343  89%      - save-excursion
> > >          173  44%       - funcall
> > >          173  44%        - thing-at-point--end-of-sexp
> > >          173  44%         - let
> > >          173  44%          - if
> > >          173  44%           - condition-case
> > >          173  44%            - forward-sexp
> > >          173  44%             - python-nav-forward-sexp
> > >          173  44%              - python-nav--forward-sexp
> > >          173  44%               - python-info-statement-ends-block-p
> > >          173  44%                - python-nav-end-of-block
> > >           96  24%                 - python-nav-end-of-statement
> > >           69  17%                  - syntax-ppss
> > >           67  17%                     parse-partial-sexp
> > >           27   7%                  - python-info-line-ends-backslash-p
> > >           27   7%                   - syntax-ppss
> > >           27   7%                      parse-partial-sexp
> > >           76  19%                 - python-nav-beginning-of-block
> > >           76  19%                  - python-nav-backward-block
> > >           76  19%                   - python-nav-forward-block
> > >           75  19%                    - python-syntax-context-type
> > >           75  19%                     - syntax-ppss
> > >           74  19%                        parse-partial-sexp
> > >            1   0%                    - python-nav-beginning-of-statement
> > >            1   0%                       point-marker
> > >            1   0%                   current-indentation
> > >          170  44%       - let
> > >          170  44%        - if
> > >          170  44%         - funcall
> > >          170  44%          - thing-at-point--beginning-of-sexp
> > >          170  44%           - let
> > >          170  44%            - if
> > >          170  44%             - forward-sexp
> > >          170  44%              - python-nav-forward-sexp
> > >          170  44%               - python-nav--forward-sexp
> > >          170  44%                - python-info-statement-ends-block-p
> > >          170  44%                 - python-nav-end-of-block
> > >           95  24%                  - python-nav-end-of-statement
> > >           63  16%                   - syntax-ppss
> > >           62  16%                      parse-partial-sexp
> > >           31   8%                   - python-info-line-ends-backslash-p
> > >           31   8%                    - syntax-ppss
> > >           30   7%                       parse-partial-sexp
> > >            1   0%                     re-search-forward
> > >           75  19%                  - python-nav-beginning-of-block
> > >           75  19%                   - python-nav-backward-block
> > >           75  19%                    - python-nav-forward-block
> > >           72  18%                     - python-syntax-context-type
> > >           72  18%                      - syntax-ppss
> > >           71  18%                         parse-partial-sexp
> > >            2   0%                       re-search-backward
> > >            1   0%                     - python-nav-beginning-of-statement
> > >            1   0%                      - python-info-line-ends-backslash-p
> > >            1   0%                       - syntax-ppss
> > >            1   0%                          syntax-ppss--data
> > >            2   0%  - python-shell-get-process
> > >            2   0%   - python-shell-get-buffer
> > >            2   0%    - seq-some
> > >            2   0%     + seq-do
> > >           32   8%   Automatic GC
> > >            6   1% - command-execute
> > >            6   1%  - call-interactively
> > >            6   1%   - byte-code
> > >            6   1%    - read-extended-command
> > >            6   1%     - read-extended-command-1
> > >            6   1%      - completing-read
> > >            6   1%       - completing-read-default
> > >            6   1%        - read-from-minibuffer
> > >            1   0%           redisplay_internal (C function)
> > >            2   0%   redisplay_internal (C function)
> > 
> > Please give me some time as I'm trying to improve performance.
> 
> Sure, there's no rush.  TIA.

In the process of looking into #77620, I noticed bugs in the
end-of-block detection functions.  So I submitted a patch to #77941.
Please note that the patch attached to this mail assumes the #77941
patch.

In my environment, the next test shows about 2 seconds on _axes.py's
L8817.

> (/ (car (benchmark-run 10 (thing-at-point 'sexp))) 10)

But there are worse cases.  Let's add the next two lines to the end of
_axes.py with 4-character indent.

    a = 1
    b = 2

The same measurement at the end of line "a = 1" gives 6 seconds; at
the end of line "b = 2" it gives 7 seconds.  This is because it calls
time-consuming functions many times.

`python-nav-beginning-of-block' and `python-nav-end-of-block' are very
time consuming if the block is large.  In the case of _axes.py, "class
Axes" is a large block with over 8000 lines.

However, `python-nav-forward-sexp' that (thing-at-point 'sexp) calls
does not necessarily have to perform these functions.  They are just
called by `python-info-statement-ends-block-p': (Before #77941 patch)

(defun python-info-statement-ends-block-p ()
  "Return non-nil if point is at end of block."
  (let ((end-of-block-pos (save-excursion
                            (python-nav-end-of-block)))
        (end-of-statement-pos (save-excursion
                                (python-nav-end-of-statement))))
    (and end-of-block-pos end-of-statement-pos
         (= end-of-block-pos end-of-statement-pos))))

The logic is that if the end of the current statement is the end of
the current block, then it is the last line of the block.  This is not
wrong, but the problem is that it takes time to move to the end of the
block.

Fortunately, in many cases it is much easier to determine that it is
not the end of the block.  Specifically, if the indentation of the
next statement is equal to or greater than the indentation of the
current statement, then the current statement cannot be the end of the
block.  The attached patch
0001-Performance-optimization-of-python-info-statement-en.patch takes
advantage of this to improve the performance of
`python-info-statement-ends-block-p'.  Please remember that it assumes
the #77941 patch is applied.

0001-Performance-optimization-of-python-info-statement-en.patch solves
the performance problem with _axes.py, but does not have much effect
on the problem with lines "a = 1" and "b = 2" added above.  The
attached patch
0002-Add-cache-to-Python-block-navigation-functions.patch is an
attempt to introduce a cache to alleviate this problem.

I measured the following to disable the cache for each benchmark.

(/ (car (benchmark-run 10 (progn (thing-at-point 'sexp) (setq python-nav-cache nil)))) 10)

It shows about 1.6 seconds for both "a = 1" and "b = 2" lines.

I believe 0002-Add-cache-to-Python-block-navigation-functions.patch is
a PoC level patch, so I welcome opinions on what you think of this
direction.