Emacs 26.1のc-modeのFont Lockが重い問題に立ち向かう
最近、c-modeが超絶重い問題にぶち当たるファイルを弄ってる事が多くストレスマッハなので真面目に調べることにした。といっても、調べだすとキリがないので深みにはまらないように気を付ける。
まずはfont-lock-support-mode変数を評価して、Font Lockの現在の動作モードを調べる。デフォルトではjit-lock-modeになっているハズ。歴史的(?)に他のモードもあるようだけど、今は殆どJit Lockのようだ。詳細→emacs/font-lock.el
JIT Lockはその名の通り、バッファに表示中の部分のみハイライト処理を行う。M-x customize-apropos RET jit RETでJIT関連の設定項目を表示してみる。
更にそこから関係ありそうな設定を抜き出したのが以下。
- jit-lock-chunk-size
- jit-lock-context-time
- jit-lock-contextually
- jit-lock-defer-time
- jit-lock-stealth-load
- jit-lock-stealth-nice
- jit-lock-stealth-time
説明を読んでjit-lock-context-timeとjit-lock-defer-timeが効きそうかなーと思い適当に値設定してみたけど、目立った改善は見られず…。前回のプロファイル結果と合わせて考えると、Font Lockが重いのではなくFont Lockに付随するc-modeの構文解析が重い予感?
ここまでで2時間ほど費やしてしまったので、今日はここまで。仕事しないと…。
Emacs 26.1のc-modeのシンタックスハイライトが重いでござる(未解決)
EmacsでC++を書いてるとバッファが超重くなることがある。キー入力やスクロールがツーテンポくらい遅れる感じ。他のバッファには影響しないので物凄く困ってるわけではないが、地味にストレスでござる。
発生条件がさっぱりわからんのが困りどころ。重くなるファイルでも開いた直後は大丈夫だったりして、編集しているうちに何かの拍子で重くなる。ファイルの大きさ自体はさほど関係ないようだ(起きないやつは行数が多くても起きないし、起きるやつは100行程度のソースでも起きる)。何となく、コメントないしマルチバイト文字成分多めのファイル、C++11なコードで発生しやすいような感じ。
ほぼ間違いないのはc-modeが原因であるということ。他のmodeでは発生しないし…。恐らくFont Lockがらみが原因と思われる
(2019-07-09 追記)
M-x font-lock-modeでFont Lockの有無を切り替えると明らかに速度が変わるので、Font Lockがらみなのも間違いない。
未だ解決には至ってないが、毎度profileとるのが面倒なのでメモがてら記事にしとく。
Function CPU samples %
- redisplay_internal (C function) 736 68%
- jit-lock-function 731 68%
- jit-lock-fontify-now 731 68%
- jit-lock--run-functions 725 67%
- run-hook-wrapped 725 67%
- #<compiled 0x1b94275> 725 67%
- font-lock-fontify-region 725 67%
- c-font-lock-fontify-region 722 67%
- font-lock-default-fontify-region 658 61%
- font-lock-fontify-keywords-region 647 60%
- c-font-lock-declarations 320 29%
- c-find-decl-spots 313 29%
- #<compiled 0x1c22bc1> 234 21%
- c-get-fontification-context 125 11%
- c-back-over-member-initializers 64 5%
+ c-just-after-func-arglist-p 18 1%
+ c-parse-state 16 1%
+ c-back-over-compound-identifier 14 1%
+ c-at-toplevel-p 10 0%
+ c-backward-sws 5 0%
+ c-looking-at-or-maybe-in-bracelist 47 4%
+ c-backward-token-2 6 0%
+ c-back-over-compound-identifier 3 0%
+ c-forward-decl-or-cast-1 60 5%
+ c-font-lock-single-decl 15 1%
+ c-backward-sws 14 1%
+ c-backward-token-2 4 0%
c-syntactic-re-search-forward 2 0%
+ c-forward-label 1 0%
+ c-bs-at-toplevel-p 52 4%
+ c-beginning-of-macro 5 0%
+ c-forward-sws 3 0%
+ c-literal-start 1 0%
c-font-lock-<>-arglists 77 7%
+ #<compiled 0x1c2637d> 44 4%
+ c-font-lock-cut-off-declarators 41 3%
+ c-font-lock-enclosing-decls 34 3%
+ c-font-lock-enum-body 24 2%
#<compiled 0x1c26351> 19 1%
+ c-font-lock-complex-decl-prepare 11 1%
+ c-font-lock-enum-tail 8 0%
#<compiled 0x1c272e7> 8 0%
+ c-font-lock-invalid-single-quotes 7 0%
#<compiled 0x1c272af> 6 0%
#<compiled 0x1c2631b> 6 0%
#<compiled 0x1c27285> 4 0%
#<compiled 0x1c2633d> 4 0%
#<compiled 0x1c263a3> 4 0%
#<compiled 0x1c2630b> 3 0%
+ c-font-lock-raw-strings 3 0%
whitespace-trailing-regexp 1 0%
+ font-lock-fontify-syntactically-region 7 0%
+ font-lock-unfontify-region 1 0%
+ c-before-context-fl-expand-region 64 5%
+ file-remote-p 4 0%
+ eval 1 0%
- ... 232 21%
Automatic GC 232 21%
+ command-execute 103 9%
御覧の通り、c-modeのFont Lockが原因なのは間違いないんだよなー。解決方法分からんけど。
以前はそれほど気にならなかったし設定も大して変えてないので、単にマシンがしょぼいだけって線も捨てきれない。一応マシンスペックも書いておく。
| CPU | Core i5-5200U (2.2GHz/2C4T) |
| RAM | DDR3-1600 8GBx1 |
| GPU | HD Graphics 5500 |
| HDD | MQ01ABF050 |
前はUnreal Engine 4での開発に耐えられるようなデスクトップマシンだったのよねー。
EmacsのTRAMPのrgrepでfind: paths must precede expression: ^^!^が出るようになった顛末
Windows 10 + PuTTY(plink) + Emacs 26.1な環境でLinuxターゲットのSSH TRAMPがいい感じに使えてたんだけど、PuTTYをmsys2のものに置き換えたら、rgrepが「find: paths must precede expression: ^^!^」なるエラーを吐いて使えなくなってしまった。これじゃ仕事にならん!ってなもんで前のPuTTYに戻したものの状況は変わらず…オワタ\(^o^)/
とりあえずemacs/share/emacs/26.1/lisp/progmodes/grep.el.gzの1153行目付近、-prune -oと(shell-quote-argument “!”)から文字列を結合してるあたりを削り、M-x byte-compile-file grep.el.gzして、超無理やり解決した。
同様の問題が起きた時の参考として、解決に至るまでの顛末をメモ。
- lgrepやgrep-findは動くのでrgrepの問題と判断。
- エラーメッセージと*grep*バッファのコマンドログから -prune -o ^“^!^” -type d あたりが怪しいと判断。実際、^“^!^”の先頭のサーカムフレックスは引数の文字としておかしくない?
- M-x describe-function rgrepでrgrep関数の定義を探す→grep.el.gzと判明
- ファイル内を「rgrep」をキーワードに流し読み。rgrep-default-command関数で実行するコマンドを組み立ててる予感!
- 一応scratchバッファで(rgrep-default-command “XXX” “YYY” “ZZZ”)を実行して結果を見てみる→あたり!
- rgrep-default-command関数の中で「!」を使ってるところを探す→(shell-quote-argument “!”)発見
- scratchバッファで評価してみる→変なエスケープのされ方をされている!
- 「!」は結合しない方向で、findの引数の関係が正しくなるように前後の処理を調整
- M-x byte-compile-file grep.el.gzしてEmacs再起動
- rgrepが使えるようになっていることを確認
根本原因はshell-quote-argumentの挙動が変わった(?)ことっぽいけど、なぜ変わったのかは不明のまま。PuTTYを差し替えたこと以外、何もいじってないはずなんだけどな…。
同様にshell-quote-argumentの実装を見てみても、OSによって処理を分岐させてるだけで変なところはない。……ん?待てよ、TRAMPの場合はOSはどういう扱いになるのこれ?ちゃんとリモート側のシステムにあわせてsystem-typeの中身が変わるのこれ…?
2018-12-04 追記
サーカムフレックスはコマンドプロンプトでのエスケープシーケンスらしいので、TRAMPしてるにもかかわらずshell-quote-argumentがWindows用の挙動を示すのが根本原因のようだ。
先の回避策では除外ディレクトリの指定が効かなくなってしまうので、grep.el.gzの1153行目あたりを以下のように変更した。
(and grep-find-ignored-files (concat (shell-quote-argument "!") " -type d "
↓
(and grep-find-ignored-files (concat " '!' -type d "
EmacsのM-x compileで自動的にMakefileを探し出すようにする
Emacs上でソースコードのコンパイルを行うにはM-x compileコマンドを使う。するとmakeが走り、結果がCompilatinバッファに表示され、M-x next-errorなどでエラー行に一発ジャンプ出来て超便利ッ……という所までは良く語られる。が、しかーし、肝心のMakefileの指定方法に触れている解説が少ない。
実際のところ、M-x compileのmakeの引数指定のところで書いてやればいいのだが、カレントパスや取り掛かっているプロジェクトごとにアレコレ考えてMakefileを指定するのは現実的ではない。それする位なら、Compilationの利点を捨て、ターミナルのコマンドヒストリーでコンパイルした方がよっぽどマシである。たいてい、Makefileはソースフォルダのトップなりに置くので、編集中のファイルから上に辿り、見つかったMakefileでmakeしてくれれば済む話。
で、ようやくそれを実現する方法を見つけた。Recursively go up to find Makefile and compileに載ってるelispを使う。万が一消えた時用に転載。
(defun desperately-compile () "Traveling up the path, find a Makefile and `compile'." (interactive) (when (locate-dominating-file default-directory "Makefile") (with-temp-buffer (cd (locate-dominating-file default-directory "Makefile")) (compile "make -k")))) ; C-x mでMakefileを探してコンパイル (global-set-key (kbd "C-x m") 'desperately-compile)
上記サイトの回答に書いてあることだが、指定ファイルが見つかるまでディレクトリを遡って検索する打って付けのlocate-dominating-fileなる関数があるそうで。今回の用途だけではなく、様々な場面で役に立ちそう。
EmacsのCompletionsバッファを新規ウィンドウではなく既存ウィンドウに表示させる
通常、EmacsでTAB補完の時とかに表示されるCompletionsバッファは、フレーム1)下部を分割した一時的なウィンドウ2)として表示される。言葉じゃわかりにくいので、スクショを張っとくと↓こんな感じね。
自分のEmacsの使い方は垂直分割した2つのウィンドウ表示が基本で、Completionsバッファは非アクティブな方のウィンドウに出て欲しい。その方が補完候補の一覧性が圧倒的だし、ウィンドウがパカパカしないのがいい。スクショを張(ry
ここの回答に載ってるelispを参考に、ウィンドウが1つの時はウィンドウ幅に応じて分割方向を変えるようにしてみた。
(defun display-on-side (buffer &optional not-this-window frame) (let* ((window (or (minibuffer-selected-window) (selected-window))) (display-buffer-function nil) (pop-up-windows nil)) (with-selected-window (or window (error "display-on-side")) (when (one-window-p t) (if (> (window-pixel-width) (window-pixel-height)) (split-window-horizontally) (split-window-vertically)) ) (display-buffer buffer not-this-window frame)))) (setq display-buffer-function 'display-on-side)
水平2分割した状態で使うと、非アクティブなウィンドウの方がCompletions表示時に勝手にリサイズされる問題があったりする…。自分は水平分割使わないので放置してます、すいません。えらいひと直して教えてください。
