重点解説！ “DockerでLuaLaTeX”を遅くしない方法

:::note info
これは「[TeX & LaTeX Advent Caleandar 2023](https://adventar.org/calendars/9305)」の25日目の記事です。
（24日目は[k16shikano さん](https://zenn.dev/k16/articles/2305cafae40619)です。）
:::

## DockerでLuaLaTeXして遅くなる件

[今年のアドベントカレンダー][texadvent2023]の初日は次の記事でした。

* [重点解説！ LuaLaTeXを遅くしない方法][zr-old]（Qiita/@zr_tex8r）

この記事で「LuaLaTeXが**フツーより遅くなる**パターン」の2つ目として**​「Dockerの特性のせいでLuaLaTeXのキャッシュが効いていない」​**を挙げましたが、それについての詳細はdoraTeX氏の過去の記事にほぼ全て任せました。

* [TeX Live 2019 + 原ノ味フォント + llmk の Docker コンテナを作る][doratex-old]（Qiita/@doraTeX）  
※この記事の[「LuaTeX の初回フォントキャッシュ作成が遅い問題」][doratex-sec]の節。

その主な理由は<del>「時間切れになったから」</del>「doraTeX氏の記事で十分だと判断したから」ですが、その他に「問題の**原因がイメージを“作る側”にある**ので、詳細な解説をしてもイメージを“使う側”[^user-vs]は対処のしようがない」という点も挙げられます。

[^user-vs]: 少なくとも「LaTeXを使う」という用途に関しては、「イメージを **“使える”人**」と「イメージを **“作れる”人**」の間には大きなギャップがあると思っています。

記事の想定読者にイメージを作る能力を仮定**しない**のであれば、読者にできる対策は「問題への対策が済んでいる（ことが判っている）イメージを使う」ことしかありません。もちろん、例のdoraTeX氏の記事で紹介されているイメージは条件を満たすわけですが、LuaLaTeXを使うという用途を鑑みるとTeX Live 2019は少し古すぎて、これを推奨するのは躊躇われます。ネット上で紹介されているイメージで新しめの環境を使ったものを色々と調べてみたのですが、条件を満たすものが見つかりませんでした。自分の記事の「2つ目のパターン」の記述が中途半端で終わったのはこれが原因だったりします。

ところが実は、ある **“裏技”**:muscle:を使うと、ほとんどの（LuaLaTeXが使える）イメージについて“使う側”が対処して「LuaLaTeXが遅くなる」問題を解消することができます。本記事ではその“裏技”を解説します。

## 前提知識

* LuaLaTeXのキホンを把握している。
* Dockerのイメージを“使う”ことができる。
    * イメージを“作る”能力は仮定**しない**。

## イロイロ対策を考えてみる件

doraTeX氏の記事をよく読むと、「LuaLaTeXのフォント関連のキャッシュ」には次の2種類があることがわかります。ここで改めてまとめてみます。

* **フォントデータベース**
    * これは原則的に[^font-db]一度だけ生成すればよい。
    * `$TEXMFVAR/luatex-cache/generic/names`の下に生成される。
    * LuaLaTeXの初回使用時に自動的に生成されるが`luaotfload-tool`で明示的に生成することもできる。
    * 従って、“作る側”の対策としては「イメージ作成の際に`luaotfload-tool`を実行する」(①)ようにすればよい。
* **フォントキャッシュ**
    * 各々のフォントごとのキャッシュデータ。
    * `$TEXMFVAR/luatex-cache/generic/fonts/otl`の下に生成される。
    * LuaLaTeXで個々の書体を初めて使ったときに自動的に生成される。
    * フォントごとのデータなので“作る側”で対処しようとすると結局「使われうる全てのフォントのキャッシュを事前に作る」(②)方法しかない。それをするとイメージサイズが1GB以上増えてしまう。
    * “使う側”での対策として「キャッシュの保存先のディレクトリをボリュームにマウントして永続化させる」(③)という方法もある。

[^font-db]: OpenTypeフォントの加除や luaotfload パッケージの更新があった場合は更新する必要がある。

③について補足ですが、マウント先（コンテナ側）のディレクトリを`$TEXMFVAR/luatex-cache/`としておくと、フォントデータベースとフォントキャッシュの両方がその配下に保存されるため永続化されます。つまり、③はそれだけで完全な対策になっています。

①と②は“作る側”が行う対策なので“使う側”が自分の判断では行えません。対して③は確かに“使う側”が行う対策ですが、これを行うにはマウント先（`$TEXMFVAR/luatex-cache/`）が具体的にどこにあるのか[^cache-dir]を“使う側”が把握している必要があります。従って、“使う側”がフツーのLuaLaTeXを使えるには **“作る側”が次の2つの何れかを行っている**必要があるわけです。

* イメージを作る際に①と②の対策を実行している。
* “使う側”にマウント先ディレクトリの場所を伝えている。

[^cache-dir]: . `$TEXMFVAR`はTeX Liveが規定する基底ディレクトリの一つですが、その場所はディストリビューションに依存します。UNIX的OSの場合「本家のTeX Live」か「OSのパッケージ」かにより値が異なるのが普通です。TeX Live系以外のディストリビューションの場合はそもそも`$TEXMFVAR`という規定もないので、キャッシュのあるディレクトリは「ファイルシステムのどこかにある`luatex-cache/`」ということになります。

doraTeX氏は後者の方針をとっていて、マウント先ディレクトリが`/usr/local/texlive/2019/texmf-var/luatex-cache`であることを明示しています。

残念ながら、ネット上に公開されているLaTeX用のDockerイメージで“作る側”の対策が実施済のものは、doraTeX氏のもの以外にはなかなか見当たりません。しかし、もし“使う側”はイメージについて以下の2つの情報を得ることができるなら**自分で対策を行う**ことができるはずです。

* イメージを作る際に①と②の対策が行われているか。
* マウント先ディレクトリの場所がどこか。

## イメージの情報を取得する件

ここでは“使う側”が先述の“必要な情報”を**実験によって得る**方法を紹介します。

まず実験用の簡単なLuaLaTeX文書を用意します。

```tex:hello.tex
\documentclass{article}
\usepackage{fontspec}
\begin{document}
Hello, world!
\end{document}
```

さらに luaotfload のログ出力設定を一時的に詳細出力に変更するために、以下の設定ファイルを用意します。

```ini:luaotfload.conf
[run]
  log-level = 3
```

その上で、luaotfload.conf ファイルが（コンテナ側の）カレントディレクトリにある状態で hello.tex を`lualatex`でタイプセットします。ログファイル hello.log が出力されるはずなので、以下で述べる手順に従って**ログの中を調べる**ことで必要な情報を得られます。

:::note warn
なお、TeXのログ出力の仕様のせいで長い行に**自動的に改行文字が入ってしまう**ため、単純に文字列を検索したのでは見逃す可能性があります。注意してください。
:::

### ①が対策済であるか

①が対策済**でなければ**、ログファイルに次のメッセージが出力されます。

```text
luaotfload | db : Font names database not found, generating new one.
```

このメッセージが出力されなければ①は対策済と考えられます。

### ②が対策済であるか

②が対策済**でなければ**、ログファイルに次のメッセージが出力されます。

```text
luaotfload | system : saving lua file '/‹何らかのパス›/luatex-cache/generic/fonts/otl/lmroman10-regular.lua' :
```

このメッセージが出力されなければ②は対策済と考えられます。

### マウント先ディレクトリの場所

ログファイルに以下のようなメッセージがあるはずです。

```text
luaotfload | conf : Root cache directory is "/‹何らかのパス›/luatex-cache/generic/names".
```

このとき`/‹何らかのパス›/luatex-cache`が所望のマウント先ディレクトリになります。

### 実際に実験してみた例

ここでは、日本語LaTeX用のDockerイメージとして比較的有名だと思われる [paperist/alpine-texlive-ja][paperist] について、“必要な情報”を取得してみます。

※実験はWindows（コマンドプロンプト）上で行いました。

まず、前述の hello.tex と luaotfload.conf を同じディレクトリに置いて、そのディレクトリで以下のコマンドを実行します[^workdir]。

[^workdir]: カレントディレクトリのbindマウント先が`/workdir`であることはalpine-texlive-jaの仕様として明記されています。コンテナ側のカレントディレクトリは`/workdir`であるため、先に述べた“実験の前提条件”が満たされます。

```text
docker run --rm -it --mount type=bind,src=%CD%,dst=/workdir →
  paperist/alpine-texlive-ja lualatex hello.tex
```

※コマンド記述中の`→`は「実際には次の行とつなげて1行で入力する」ことを表すものとします。

:::note info
`%CD%`は「現在ディレクトリの絶対パス表記」に展開されるWindowsの環境変数です。bashの`$PWD`や`$(pwd)`に相当します。
:::

生成されたログファイル hello.log を調べると以下の記述が見つかります。

```text
luaotfload | db : Font names database not found, generating new one.
```

つまり①の対策は行われていません。

```text
luaotfload | system : saving lua file '/usr/local/texlive/2023/texmf-var/luatex-cache/generic/fonts/otl/lmroman10-regular.lua' :
```

②の対策も行われていません。

```text
luaotfload | conf : Root cache directory is "/usr/local/texlive/2023/texmf-var/luatex-cache/generic/names".
```

ボリュームのマウント先のディレクトリは`/usr/local/texlive/2023/texmf-var/luatex-cache`です。

## 結局どう対策すればよいか述べる件

①と②の両方が対策済である場合は“使う側”が対策する必要はありません。提供元の“指示通り”に使用すればフツーのLuaLaTeXが使えるはずです。

そうでない場合は **“使う側”が③の対策をする**必要があります。実験で判明した「マウント先ディレクトリ」に基づいて、`docker`実行時に以下のオプションを追加すればよいはずです。

```bash
--mount type=volume,src=ltcache,dst=‹マウント先ディレクトリ›
# "-v (--volume)"オプションで書く場合は↓の通り
# -v ltcache:‹マウント先ディレクトリ›
```

先の実験に用いた paperist/alpine-texlive-ja の場合、結局以下のように`docker`を実行すればフツーのLuaLaTeXが使えることになります。

```text
docker run --rm -it --mount type=bind,src=‹現在ディレクトリパス›,dst=/workdir →
  --mount type=volume,src=ltcache,dst=/usr/local/texlive/2023/texmf-var/luatex-cache →
  paperist/alpine-texlive-ja lualatex ‹ファイル名›.tex
```

## テスト文書は「猫」である件

ここまでに述べた対策によりフツーのLuaLaTeXが使えていることを、実際に短い和文文書をタイプセットして確かめてみましょう。

実験に使うのは例によって(?)『吾輩は猫である』の冒頭の文章です。

```tex:test.tex
% LuaLaTeX文書; 文字コードはUTF-8
\documentclass[a4paper]{ltjsarticle}
\usepackage{bxjalipsum}% 例のアレ
\begin{document}
\section{吾輩は猫である}
%『吾輩は猫である』の先頭の10段落を出力
\jalipsum[1-10]{wagahai}
\end{document}
```

このコードの出力は以下のようなA4判で2ページ半の文書になります。和文2書体（明朝体・ゴシック体1ウェイトずつ）を使ったごくフツーの文書です。

![nsdl-image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14550/c7883107-e73d-d76e-8375-bff181024233.png)

これを`lualatex`コマンドで（1回だけ）タイプセットする際の所要時間を調べます。

### まずはWindowsでやってみる

Dockerコンテナでの実験の前に、まずは「キャッシュ無効化の影響がどれほどか」を把握するために生のWindows環境で「キャッシュ無効」の状況をシミュレートして時間を測ってみます。

* ①：フツー（キャッシュ有効）にタイプセットを実行する。
* ②：タイプセットの前に`$TEXMFVAR/luatex-cache/generic/fonts`ディレクトリ全体を削除する。**フォントキャッシュが無効**になる。
* ③：タイプセットの前に`$TEXMFVAR/luatex-cache`ディレクトリ全体を削除する。フォントキャッシュも**フォントデータベースも無効**になる。

※10回実行して平均と標準偏差を求めました。

| テスト | 所要時間 |
|:--|--:|
| ①フツー                   |  6.336±0.049 秒 |
| ②フォントキャッシュ無効   | 12.974±0.071 秒 |
| ③フォントデータベース無効 | 24.537±0.069 秒 |

フォントキャッシュが無効だと2倍、さらにフォントデータベースが無効の状態だと4倍近くの時間がかかっています。キャッシュが無効であることの**影響はかなり大きい**ことが判りました。

### Dockerコンテナでやってみる

それでは本番の実験です。paperist/alpine-texlive-ja のコンテナで先のテスト文書を`lualatex`でタイプセットします。

* ①：何も対策をせずにキャッシュ無効の状態でタイプセットする。

```text
docker run --rm -it --mount type=bind,src=‹現在ディレクトリパス›,dst=/workdir →
  paperist/alpine-texlive-ja lualatex -halt-on-error test.tex
```
* ②：ltcache ボリュームをマウントしてキャッシュを有効にしてタイプセットする。LuaLaTeXとしてはこちらの状態が**フツー**である。

```text
docker run --rm -it --mount type=bind,src=‹現在ディレクトリパス›,dst=/workdir →
  --mount type=volume,src=ltcache,dst=/usr/local/texlive/2023/texmf-var/luatex-cache →
  paperist/alpine-texlive-ja lualatex -halt-on-error test.tex
```

結果は以下の通りです。

| テスト | 所要時間 |
|:--|--:|
| ①対策無し（キャッシュ無効） | 12.381±0.209 秒 |
| ②対策有り（フツー）         |  4.381±0.173 秒 |

キャッシュを有効にしてLuaLaTeXをフツーにすることで**3倍近く**速くなりました！:blush:

### まとめ

“DockerでLuaLaTeX”している皆さんも、やっぱり遅くない**フツー**のLuaLaTeXを使って**幸せ**:blush:になりましょう！:smiley:

[doratex-old]: https://qiita.com/doraTeX/items/f40f9597763ca63daabb
[doratex-sec]: https://qiita.com/doraTeX/items/f40f9597763ca63daabb#luatex-%E3%81%AE%E5%88%9D%E5%9B%9E%E3%83%95%E3%82%A9%E3%83%B3%E3%83%88%E3%82%AD%E3%83%A3%E3%83%83%E3%82%B7%E3%83%A5%E4%BD%9C%E6%88%90%E3%81%8C%E9%81%85%E3%81%84%E5%95%8F%E9%A1%8C
[paperist]: https://github.com/Paperist/texlive-ja
[texadvent2023]: https://adventar.org/calendars/9305
[zr-old]: https://qiita.com/zr_tex8r/items/e179b0327ddba7b17095