22
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

VimAdvent Calendar 2024

Day 20

あなたは多分、Neovim の本当の起動速度を知らない

Posted at

前回からのあらすじ

以前アドベントカレンダーで「爆速で起動する Neovim を作る」という記事を書いたことがありました。あれから Neovim 界では様々なことがありましたが、みなさんも僕と同じように感謝の起動速度測定 10000 回を日課としておられることと思います。

この 2 年間で起こった、起動速度測定界隈(そんなものがあるの?)での一番重要な事件はこれでしょう。

思わず太字にしてしまいましたが、前回の記事が出てすぐに packer.nvim がオワコン化したことはショックでした。「packer.nvim では init.lua をコンパイルしてバイトコードで保存してるから起動速度が爆速~」なんて書いてたのに、そんな複雑な機構が存在しない lazy.nvim の方がより早く、より変更に強い init.lua を書けたのです。これは起動速度測定界隈(そんなものが(略))にとっては驚天動地の大事件でした。

それは本当に起動速度なの?

さて、本題に入ります。今日も日課の Neovim HEAD インストールからの nvim --startuptime をやって起動速度が変化しないか見る作業をしていたのですが、ふと気になりました。この値、本当に正しいの?

前回の記事で「起動速度を測る方法」として列挙したのは以下の 3 つです。

厳密には、「方法(3)」は起動速度を測る方法ではありません。また、「方法(2)」についても、ソースを見ると --startuptime オプションを利用していることが分かります。つまり、--startuptime オプションの実装を調べればこの「起動速度」が何を指しているのか判別できそうです。

--startuptime オプションの結果は何を表しているのか

まず、計測方法をおさらいしましょう。この方法では、nvim --startuptime hoge.log +q のように指定すると、hoge.log に Neovim の起動プロセスについて詳細なログを吐いて終了するのでした。この時 hoge.log には以下のような文字列が書き込まれています(最後の部分だけ表示します)。

……

148.366  001.148: opening buffers
148.851  000.346  000.346: sourcing /Users/jinnouchi.yasushi/.local/share/nvim/lazy/direnv.vim/autoload/direnv/extra_vimrc.vim
148.955  000.243: BufEnter autocommands
148.957  000.002: editing files in windows
148.975  000.019: --- NVIM STARTED ---

この 148.975 が起動速度です。この文字列はどのようにして吐かれているのか Neovim のソースを見てみましょう1

起動速度の正体

このファイルの文字列をどこで吐いているのか調査するため、--- NVIM STARTED --- という文字列でリポジトリを検索してみました。すると、吐いているのは、profile.ctime_finish() という小さな関数です。

TIME_MSG というのはマクロですね。これの展開元を見てみると、

以下、実際に左側の時刻(148.975)を生成しているコードを調べていきます。

最後に、uv_hrtime() に辿り着きました。これは libuv の関数です。その実装は libuv/libuv にあります。uv_hrtime() の実装は OS によって引数が異なるものの、uv__hrtime() に引き継がれています。

そして uv__hrtime() ですが、これは Linux や macOS では以下のようなロジックです(Windows は詳しくないのでパス)。

やっと分かりました。CLOCK_MONOTONIC は単調増加する時刻で、ベンチマーク用途などに良く使われます(mach_continuous_time() も似たようなもんです)。ある時点でこれの差分を取って起動速度(148.75)を出していたのです。

--- NVIM STARTED --- の正体

ではこの、--- NVIM STARTED --- はどこで吐かれているのでしょう。time_finish() という関数が吐いているのは分かったのですが、この関数の呼び出し元は複数あって分かりにくいです。

結論から言うと、このメッセージは Neovim の起動方法によって出力箇所が異なります。つまり、--- NVIM STARTED --- が示す「起動速度」をそのまま使うと、実際に体感できるそれとは違った値を指している可能性があります。

+q 付きで起動する場合(「方法(1)」)

「方法(1)」では --startuptime の後ろに +q を付けて、測定が終わったら即時に Neovim を終了していました。起動時に指定された +, -c オプション、及び -S オプションは以下の部分で処理されます。

+q 付きで起動した場合ここで Neovim が終了しまいます。周辺のソースを見ると、BufEnter イベント実行されるものの、VimEnter イベントUIEnter イベント実行されないことが分かります2

vim-startuptime の場合(「方法(2)」)

vim-startuptime コマンドの場合は更に事情が異なります。Neovim を起動しているソースは以下の部分です。

-c qall! を付けて起動しているのは上記の +q と同じですので、time_finish() の呼び出し箇所は同じです。もう一つ重要なことは --headless を付けて実行しているため、UI プロセス自体がアタッチされていません。ベンチマークを取るという目的なら問題無いかも知れませんが、その計測された値は実際に Neovim を利用する際の感覚とはズレてしまう可能性があります。

--startuptime は実際に Neovim を操作する時の実態を表していない(かも知れない)

という訳で、あなたの init.lua に少なくとも VimEnterUIEnter を契機として実行されるコードがある場合、--startuptime に示される値は実際の使用感とは懸け離れたものになっている恐れがあります。例えば lazy.nvim を利用している場合、以下のような記述をしているプラグインが該当します。

  • event = { "VimEnter" }
  • event = { "UIEnter" }
  • event = { "VeryLazy" }

最後の VeryLazy イベントは lazy.nvim 独自のものですが、これは UIEnter の後に実行されるようです。人気のディストリビューション LazyVim のレポジトリをVeryLazy」で検索した所、結構な数のプラグインがヒットしましたので、この事象に該当する人も多いのではないでしょうか。

「本当の起動速度」を測る

じゃあどうしたらいいのか。となるのですが、これに関して簡単な解決法はありません。起動処理が完了し、プラグインの初期化処理が終わった時点というのを判定する方法が無いからです。

ただし先ほど例に挙げました LazyVim のように、ダッシュボード(起動画面)を表示している人はこれを判定できる可能性があります。例えば LazyVim は folke/snacks.nvim(以前は nvimdev/dashboard-nvim)というダッシュボードプラグインを利用していました。これらのプラグインはダッシュボードの表示が完了した際、以下のようなイベントを実行するようになっています。

プラグイン イベント名 ソース
snacks.nvim SnacksDashboardOpened lua/snacks/dashboard.lua#L229
dashboard-nvim DashboardLoaded lua/dashboard/theme/hyper.lua#L534-L537

ダッシュボードの表示が完了しているんだったら、ユーザーが操作可能になっていると見做して良いのではないでしょうか。以下では例として、dashboard-nvim で「本当の起動速度」を測定する方法を見ていきましょう。

「本当の起動速度」をファイルに保存するスクリプト

最初に、init.lua の冒頭に以下のコードを挿入します。

if vim.env.LOGFILE or vim.env.WARMUP then
  local start = vim.uv.hrtime()
  vim.api.nvim_create_autocmd("User", {
    once = true,
    -- snacks.nvim の場合はここを
    -- "SnacksDashboardOpened" とします。
    pattern = "DashboardLoaded",
    callback = function()
      if vim.env.LOGFILE then
        local finish = vim.uv.hrtime()
        vim.fn.writefile({ tostring((finish - start) / 1e6) }, vim.env.LOGFILE, "a")
      end
      vim.schedule_wrap(vim.cmd.qall) { bang = true }
    end,
  })
end

これを以下のようにして使うと、「本当の起動速度」が /tmp/hoge.log に記録されます。

env LOGFILE=/tmp/hoge.log nvim

init.lua 冒頭で起動時刻を保存し(local start = vim.uv.hrtime())、ダッシュボードの表示が完了した時起こるイベント DashboardLoaded までの時間を環境変数 $LOGFILE に指定されたファイルに保存しています。全部終わったら :qall! を実行して Neovim を終了します。環境変数 $WARMUP が指定された時はファイルに保存する操作を行わず、DashboardLoaded に到達したらすぐに終了するようにしています。

これにより startfinish の差で表される「本当の起動速度」が分かるという訳です。ただし init.lua を使う以上しょうがないことなのですが、この値には Neovim が起動してから init.lua の評価が始まるまでの時間が含まれません。ソースコードで言うと source_startup_scripts() の呼び出し時までの間です。

nvim --startuptime の結果によると、ここまでの時間は僕の環境では 5ms 程度です(init default mappings & autocommands が出力されるまでの間です)。最終的な結果にこの程度の時間は上乗せされることを覚えておきましょう3

保存した「本当の起動速度」を集計する

普通に env LOGFILE=/tmp/hoge.log nvim やるだけだと 1 回実行して終わりです。これを vim-startuptime のように複数回実行して集計するスクリプトを作りました。

特に複雑なことはしていないので中身は解説しません。以下のようにして使います。

~ ❯❯❯ bench-nvim -h
Usage:
        bench-nvim
        # Run nvim 10 times after 1 warmup

        bench-nvim -c 100 -w 5
        # Run nvim 100 times after 5 warmups

        bench-nvim -l /tmp/foo.log
        # Store results into /tmp/foo.log

        bench-nvim -h
        # See this help

vim-startuptime と実行結果を比較してみます。

~ ❯❯❯ vim-startuptime -vimpath nvim -count 100 | head -20
Extra options: []
Measured: 100 times

Total Average: 69.743620 msec
Total Max:     79.262000 msec
Total Min:     66.035000 msec

~ ❯❯❯ bench-nvim -c 100
Measured 100 times

Total Average: 138.887355 msec
Total Max:     185.870666 msec
Total Min:     129.550209 msec

随分違った値になりましたね。僕の環境では VimEnter / UIEnter / VeryLazy で起動するプラグインが多いため、このような結果になったのでしょう。Min と Max の差が大きいことも含めて実際の使用感にあった値になっていると思います。

ダッシュボードに表示される起動速度の正体

さて、前節でこの記事の目的はあらかた達したのですが、一つここで疑問があります。LazyVim で利用されているダッシュボード(snacks.nvim や dashboard-nvim によるもの)には起動速度と思しき値が表示されています。この値(下記の画像では 34.51ms)はどのようにして計測しているのでしょうか。

LazyVim.png

これは snacks.nvim、dashboard-nvim、共に、lazy.nvim の一機能である require("lazy.stats").stats().startuptime という値を利用しています。

この値は startuptime という名前をしていますが、実は、以下の二つの理由から「起動速度」と言える値ではありません。

  • 理由(1) 実時間ではなく CPU 時間を計測している
  • 理由(2) 計測終了時点が UIEnter イベントである

理由(1) 実時間ではなく CPU 時間を計測している

lazy.nvim のソースを見てみますと、この値は C の clock_gettime(2) を直接呼んで取得しています。

これ、「起動速度の正体」の節で見た、uv__hrtime() の実装に似ていますね。しかし大きな違いがありまして、uv__hrtime()CLOCK_MONOTONIC を使っていたのに、ここでは CLOCK_PROCESS_CPUTIME_ID を利用しています。これらの間には man ページによると以下のような違いがあります。

説明(抜粋)
CLOCK_MONOTONIC ある開始時点からの単調増加の時間で 表現されるクロック
CLOCK_PROCESS_CPUTIME_ID プロセス単位の CPU タイムクロック

つまり、ダッシュボードに表示される起動速度のような数値(lazy.nvim の startuptime)は実時間では無く、それまでに CPU が消費した時間を表しています。具体的には、プラグインが外部のファイルを実行していたり、ストレージ、ネットワークによる I/O が発生していたとしても、掛かった時間は計測されないのです。

理由(2) 計測終了時点が UIEnter イベントである

これも lazy.nvim のソースを見ると分かります。startuptime の値は UIEnter イベントで記録されています。

snacks.nvim や dashboard-nvim といったダッシュボードは UIEnter で描画を開始します。そのため、startuptime の値にはダッシュボードの表示、また、他の UIEnter イベント(及び VeryLazy イベント)で起動するプラグインで掛かった時間は含まれていないことになります。


これらの実装仕様は意図的なものだと思われます。lazy.nvim はプラグインマネージャであって、require "lazy.stats" はその動作速度を計測するモジュールです。lazy.nvim 自身のプロファイリングに使えるようなデータを提供するのが目的なのでしょう。

という訳で、ダッシュボードに表示される startuptime はあくまで参考程度、本当の起動速度を知りたい時は前節のようなスクリプトを利用した方が良いという結論になります。

まとめ

本記事では Neovim の --startuptime オプションについて動作を実装レベルで理解し、それが「本当の起動速度」と異なる可能性について説明しました。また、snacks.nvim や dashboard-nvim などのダッシュボードプラグインで表示される値が Neovim のチューニングに役立たないことも分かりました。

これからもみなさんが vim-startuptime で計測した値や、ダッシュボードに表示される値に振り回されることなく、「真に高速に起動する Neovim」を求めて精進されるよう、日々感謝の Neovim 起動 10000 回をすることを誓って、この記事を終わりたいと思います。

  1. 以下、Neovim のソースは記事執筆時点の HEAD のものを利用しています。

  2. これ以降を読み進めるためにはこれで十分なのですが、念のため、実際に +q--- NVIM STARTED --- が表示されるロジックを追っておきます。:quit コマンドの実装はこの部分にあります。最後の方で実行されている getout(0) が実際にプロセスを終了させる関数です。ここtime_finish() が呼ばれています。

  3. ちなみに、nvim --startuptime の起点も厳密には「Neovim プロセスの開始時点」ではありません。main.c のこの部分で値が初期化されていますので、この部分より前の時間は計測できていないことになります。

22
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
22
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?