emacs-lsp / lsp-mode

Emacs client/library for the Language Server Protocol
https://emacs-lsp.github.io/lsp-mode
GNU General Public License v3.0
4.72k stars 861 forks source link

Make loading yasnippet lazier #4409

Closed daanturo closed 3 months ago

daanturo commented 3 months ago

Currently, loading lsp-mode when yasnippet-snippets is installed delays startup significantly.

https://github.com/emacs-lsp/lsp-mode/blob/0038e09912f9715af52eeeff92f61e7598c8eb5a/lsp-mode.el#L59

Code to reproduce from vanilla Emacs:

;; -*- lexical-binding: t; -*-

(defmacro my-profiler-cpu-progn (&rest body)
  (declare (debug t))
  `(benchmark-progn
     (profiler-start 'cpu)
     (unwind-protect
         (progn ,@body)
       (profiler-stop)
       (save-window-excursion
         (profiler-report)))))

;;; Execute

(with-eval-after-load 'package
  (add-to-list 'package-archives '("melpa" . "https://melpa.org/packages/")
               'append))

(mapc
 (lambda (pkg)
   (unless (package-installed-p pkg)
     (condition-case _
         (package-install pkg)
       (error (package-refresh-contents) (package-install pkg)))))
 '(lsp-mode
   yasnippet yasnippet-snippets
   ))

;; defer GC to see the impacts of each package better
(dlet (
       (gc-cons-threshold most-positive-fixnum) (gc-cons-percentage 0.75)
       )
  (my-profiler-cpu-progn (require 'lsp-mode)))

Elapsed time: 0.426193s

Profiler result:

         417 100% - command-execute
         417 100%  - funcall-interactively
         417 100%   - elisp-eval-region-or-buffer
         417 100%    - let
         417 100%     - let
         417 100%      - let
         417 100%       - progn
         417 100%        - unwind-protect
         415  99%         - progn
         408  97%          - require
         407  97%           - byte-code
         363  87%            - require
         301  72%             - do-after-load-evaluation                                
         300  71%              - eval-after-load-helper
         300  71%               - #<lambda 0x1e2219b>
         299  71%                - yasnippet-snippets-initialize                <- here
         299  71%                 - byte-code
         299  71%                  - eval-after-load
         299  71%                   - #<compiled 0x1980069eeacd>
         299  71%                    - yasnippet-snippets-initialize
         299  71%                     - yas--load-snippet-dirs
         294  70%                      - yas-load-directory
         194  46%                       - yas--compute-major-mode-and-parents
         194  46%                        - cl-some
         194  46%                         - #<compiled -0xf5f693796a8fbf3>
          12   2%                            locate-dominating-file
          91  21%                       - yas--load-pending-jits
          91  21%                        - #<compiled -0x1e5683358b795d96>
          91  21%                         - apply
          90  21%                          - yas--load-directory-1
          84  20%                           - yas--load-directory-2
          69  16%                            - yas--parse-template
          69  16%                             - yas--calculate-group
           4   0%                                locate-dominating-file
           7   1%                            - yas--subdirs
           7   1%                             - cl-remove-if
           7   1%                              - cl-remove
           7   1%                                 #<compiled 0x59ff63d256345a1>
           3   0%                            + set-auto-coding
           2   0%                            + yas-define-snippets
           4   0%                       + yas--subdirs
           1   0%              + elisp--font-lock-flush-elisp-buffers
          53  12%             - byte-code
           7   1%              - require
           5   1%               - byte-code
           1   0%                + custom-declare-variable
           1   0%               + defvar
           1   0%               + custom-declare-face
           1   0%              + custom-declare-variable
           1   0%                cl-generic-define-method
           7   1%             - defvar
           7   1%              - byte-code
           7   1%               - mapatoms
           5   1%                  #<compiled -0x7cc32180bcf36fe>
           1   0%            + advice-add
           1   0%           + advice-add
           0   0% + ...

yasnippet-snippets-initialize usually takes around 70~75% of lsp-mode's loading time, while it doesn't see usage through LSP most of the time. Uninstalling the yasnippet* packages make loading lsp-mode noticeably faster.

Grepping lsp-mode's code base, I only see yasnippet's usage at lsp-mode.el/lsp--expand-snippet, and lsp-vscode-snippets.el. Maybe we can make lsp--expand-snippet only require yasnippet when needed? The (featurep 'yasnippet) calls can be replaced by (fboundp 'yas-minor-mode).

kiennq commented 3 months ago

This one can be a nice perf optimization. I put a PR there, please test if it's work for you.

daanturo commented 3 months ago

This one can be a nice perf optimization. I put a PR there, please test if it's work for you.

Thank you, that really works for me. It shaved the said benchmark time from 0.72s to 0.36s!

Searching the whole emacs-lsp organization, looks like https://github.com/emacs-lsp/lsp-dart/blob/13c981eece9a0cfd9eda3e20a304f0d82d7f450c/lsp-dart.el#L171 also needs updating.