2022-08-16 ,

org-modeの起動時間を短縮する(org-babel-load-languages編)

org-modeの読み込みは非常に遅い。

原因は色々あるが、その一つが org-babel-load-languages の読み込みである。 org-babel-do-load-languages 関数によって、 org-babel-load-languages で指定されている全ての言語バックエンド(ob-???.el)を起動時に読み込んでしまい、その結果数秒も待たされることがある。

解決策はいくつか考えられる。一つはorg-mode起動後にタイマーによって少しずつ読み込んでいく方法。もう一つは必要になってから必要な言語だけ読み込む方法。

今回は後者を実現する。

ただし、この方法はorg-modeのバージョンアップによって機能しなくなる可能性が前者の方法より高い。org-modeは起動時に全ての言語バックエンドが読み込まれていることが前提で書かれており、言語バックエンドが必要なところで必ず呼び出す関数などは存在しない。強いて言えば、 (intern (concat "org-babel-???:" lang)) のようなコードによって org-babel-???:??? のようなシンボルを生成している箇所があちこちに存在する(grep '"org-babel-.*:'等で検索すると良い)。今回はそのような場所を詳しく調査することで半ば場当たり的に言語バックエンドが必要な箇所に処理を挟んで遅延読み込み処理を追加した。従ってorg-modeのバージョンアップに弱くなっている。

しかしながら調査した結果、大半は一つのパターンで対処が可能であることが分かった。ほとんどの場合、言語バックエンドを必要とする処理の前にはソースブロックの言語名を取得する処理が入っており、それは (org-element-property :language element) のような形になっている。このコードはorg-elementで解析した構文要素オブジェクトから言語名プロパティを取得するものだ。このコードで返すのは#+begin_srcの後の言語名なので、 org-element-property にadviceをかけて戻り値の言語名を元に言語バックエンドを読み込んでしまえば良い。 org-element-property は頻繁に呼ばれる関数なのであまり気は進まないが、少なくとも改変の影響はorg-modeの範囲に留まる(intern等に引っかけるよりはマシである)。

org-babel-get-src-block-infoorg-babel-lob-get-info のようなソースブロックの情報を返す関数も、結局はorg-elementで解析を行い要素の:languageプロパティを取得している。

まれにそれ以外の方法で言語名を生成している場合がある。例えばob-table.elには"emacs-lisp"のように言語名をハードコードしている箇所がある。また、 org-babel-enter-header-arg-w-completion 関数に対して (match-string) の値を言語名として引き渡している箇所もある。このようなケースには個別に対処する必要がある。

;; 使い方:
;; 1. org-babel-load-languagesの値はCustomizeの方ではnilにしておくこと。
;; 2. (with-eval-after-load "org"
;;      (load "このコードを含むファイル")) などとする。

;; 使用する言語名とそれを提供するelファイル名の一覧。
(defvar my-org-babel-languages
  ;;(言語名 . ob-ファイル名.el)
  '((elisp . emacs-lisp)
    (emacs-lisp . emacs-lisp)
    (makefile . makefile)
    (ditaa . ditaa)
    (dot . dot)
    (plantuml . plantuml)
    (perl . perl)
    (cpp . C)
    (C++ . C)
    (D . C)
    (C . C)
    (js . js)
    (java . java)
    (org . org)
    (R . R)
    (python . python)
    (shell . shell)
    (sh . shell)
    (bash . shell)
    (zsh . shell)
    (fish . shell)
    (csh . shell)
    (ash . shell)
    (dash . shell)
    (ksh . shell)
    (mksh . shell)
    (posh . shell)))

(defun my-org-babel-language-files ()
  "重複しない全ての言語バックエンドファイル名を返す。"
  (seq-uniq (mapcar #'cdr my-org-babel-languages)))

;; my-org-babel-languagesからorg-babel-load-languagesを設定する。
;; org-lintやorg-pcompleteにorg-babel-load-languagesを使った処理がある
;; ようなので。
;; このときcustom-set-variablesを使わないようにすること。
;; org-babel-do-load-languagesが呼ばれて全部読み込まれてしまうので。
(setq org-babel-load-languages
      (mapcar (lambda (lang) (cons lang t)) ;;(emacs-lisp . t)のような形式
              (my-org-babel-language-files)))

(defun my-org-require-lang-file (lang-file-name)
  "ob-LANG-FILE-NAME.elを読み込む。"
  (when lang-file-name
    (require (intern (format "ob-%s" lang-file-name)) nil t)))

(defun my-org-require-lang (lang)
  "LANGを読み込む。"
  (my-org-require-lang-file
   (alist-get
    (if (stringp lang) (intern lang) lang)
    my-org-babel-languages)))

(defun my-org-require-lang-all ()
  "全ての言語を読み込む。"
  (mapc #'my-org-require-lang-file
        (my-org-babel-language-files)))

;; org-elementで言語名を返す時、その言語をロードする。
(advice-add #'org-element-property :around #'my-org-element-property)
(defun my-org-element-property (original-fun property element)
  (let ((value (funcall original-fun property element)))
    (when (eq property :language)
      (my-org-require-lang value))
    value))

;; ob-table.elに(org-babel-execute-src-block nil (list "emacs-lisp" "results" params))のような呼び出し方をする所があるので。
(advice-add #'org-babel-execute-src-block :around
            #'my-org-babel-execute-src-block)
(defun my-org-babel-execute-src-block (original-fun
                                       &optional arg info params)
  (my-org-require-lang (nth 0 info))
  (funcall original-fun arg info params))

;; (match-string)の値を直接langとして渡しているので。
(advice-add #'org-babel-enter-header-arg-w-completion :around
            #'my-org-babel-enter-header-arg-w-completion)
(defun my-org-babel-enter-header-arg-w-completion (original-fun
                                                   lang)
  (my-org-require-lang lang)
  (funcall original-fun lang))

;; org-lint(org-lint-wrong-header-argument, org-lint-wrong-header-value)内で参照しているので。
;; 面倒なので全部読み込んでしまう。
(advice-add #'org-lint :around #'my-org-lint)
(defun my-org-lint (original-fun &rest args)
  (my-org-require-lang-all)
  (apply original-fun args))

;; 他にもinfoやlangを引数に取るような関数がある。
;; my-org-element-propertyやorg-babel-get-src-block-info等を使ってlangや
;; infoを取得していれば問題ないが、予期していない方法でlangやinfoを取得し
;; ている場合は対処する必要がある。

と、書いた後にorg-modeのメーリングリストに次のような投稿を見つけた。

Load Org Babel Languages on Demand

見たところ org-src--get-lang-modeorg-babel-confirm-evaluate にadviceを追加して似たようなことをしている。 org-src--get-lang-mode 関数は現在見当たらないが何か変更があったのだろうか( org-src-get-lang-mode はあるので改名された?)。

2020年の投稿だがその後どうなったのかは不明。

ちゃんと修正するのであれば、まずはあちこちに散らばっている org-babel-*:* シンボルを組み立てる処理を関数にまとめるのが良さそう。そしてその関数から言語名に対応するob-ファイルを読み込むようにすれば良い。有効言語の設定変数をどうするかは迷い所だが org-babel-load-languages の形式を拡張できるかもしれないし、あるいは別に変数を用意しても良さそうだ。