株式会社ACCESS Advent Calendar 2021 22日目の記事だよー!
はじめに
- 最近リリースされた Elixir 1.13.0 では
mix xref
の新しいサブコマンドにtrace
が追加されました。- Advent Calendar の担当日を待っているうちに v1.13.1 がリリースされました。
- 実装 を見てみたところ、Elixir 1.10 で 追加 された compilation tracers という機能を使っているようです。
- Elixir には macro があるので、ファイルを見ただけではどの関数を呼び出しているか分からないこともあるのですが、compilation tracers を使うと比較的簡単に分かりそうに思ったので調べてみました。
環境
$ iex
Erlang/OTP 22 [erts-10.7.2.15] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1]
Interactive Elixir (1.13.0) - press Ctrl+C to exit (type h() ENTER for help)
Tracer module を作ってみる
defmodule CompilationTracer do
def run(file, apps) do
set =
for app <- apps,
modules = Application.spec(app, :modules),
module <- modules,
into: MapSet.new(),
do: module
old = Code.compiler_options(ignore_module_conflict: true, tracers: [__MODULE__])
ets = :ets.new(__MODULE__, [:named_table, :duplicate_bag, :public])
:ets.insert(ets, [{:config, set, trace_label(nil)}])
try do
Code.compile_file(file)
else
_ ->
:ets.delete(ets, :modules)
print_traces(Enum.sort(:ets.lookup_element(__MODULE__, :entry, 2)))
after
:ets.delete(ets)
Code.compiler_options(old)
end
end
# 以下に下記のコードを丸ごとコピーする
# https://github.com/elixir-lang/elixir/blob/v1.13.0/lib/mix/lib/mix/tasks/xref.ex#L509-L585
ほとんど mix xref trace
のコードから持ってきています。
元のコードと違う点は以下です。
- 検出対象の application を設定できるようにしています。
-
apps
で指定した application 内の関数や macro の呼び出しが検出されます。 -
mix xref trace
では自分の Mix project で管理している application が対象になります。-
--include-siblings
オプションにより、同じ umbrella projects 内の application を対象にすることもできます。
-
-
- 使わないオプションに関連するコードを削除してあります。
Tracer module を使ってみる
テスト用の Mix project
Macro を多用している gettext
を使った Mix project を準備します。
$ mix new gettext_example
して、gettext
を deps に入れて、以下 2 つのファイルを書きます。
# lib/gettext_example/gettext.ex
defmodule GettextExample.Gettext do
use Gettext, otp_app: :gettext_example
end
# lib/gettext_example.ex
defmodule GettextExample do
import GettextExample.Gettext
def hello do
gettext("Here is one string to translate")
end
end
Compilation tracing してみる
少しは面白い結果になるように、gettext
のバージョンを v0.17.1
と v0.18.2
の 2 通り使って実行してみます。
# gettext = v0.17.1 の場合
iex(2)> Mix.Project.config()[:elixirc_paths] |> Mix.Utils.extract_files([:ex]) |> Enum.each(&CompilationTracer.run(&1, [:gettext]))
lib/gettext_example.ex:9: call Gettext.dgettext/4 (runtime)
lib/gettext_example/gettext.ex:1: alias Gettext.Backend (compile)
lib/gettext_example/gettext.ex:1: require Gettext.Backend (export)
lib/gettext_example/gettext.ex:1: call Gettext.Compiler.__before_compile__/1 (compile)
lib/gettext_example/gettext.ex:1: call Gettext.Compiler.append_extracted_comment/1 (runtime)
lib/gettext_example/gettext.ex:1: call Gettext.Compiler.expand_to_binary/4 (runtime)
lib/gettext_example/gettext.ex:1: call Gettext.Compiler.expand_to_binary/4 (runtime)
lib/gettext_example/gettext.ex:1: call Gettext.Compiler.expand_to_binary/4 (runtime)
lib/gettext_example/gettext.ex:1: call Gettext.Compiler.expand_to_binary/4 (runtime)
lib/gettext_example/gettext.ex:1: call Gettext.Compiler.expand_to_binary/4 (runtime)
lib/gettext_example/gettext.ex:1: call Gettext.Compiler.expand_to_binary/4 (runtime)
lib/gettext_example/gettext.ex:1: call Gettext.Compiler.get_and_flush_extracted_comments/0 (runtime)
lib/gettext_example/gettext.ex:1: call Gettext.Compiler.get_and_flush_extracted_comments/0 (runtime)
lib/gettext_example/gettext.ex:1: call Gettext.Extractor.extract/5 (runtime)
lib/gettext_example/gettext.ex:1: call Gettext.Extractor.extract/5 (runtime)
lib/gettext_example/gettext.ex:1: call Gettext.Extractor.extracting?/0 (compile)
lib/gettext_example/gettext.ex:1: call Gettext.Extractor.extracting?/0 (compile)
lib/gettext_example/gettext.ex:1: call Gettext.Extractor.extracting?/0 (runtime)
lib/gettext_example/gettext.ex:1: call Gettext.Extractor.extracting?/0 (runtime)
lib/gettext_example/gettext.ex:1: call Gettext.ExtractorAgent.add_backend/1 (compile)
lib/gettext_example/gettext.ex:1: call Gettext.ExtractorAgent.add_backend/1 (compile)
lib/gettext_example/gettext.ex:2: require Gettext (export)
lib/gettext_example/gettext.ex:2: require Gettext.Interpolation (export)
lib/gettext_example/gettext.ex:2: require Gettext.Interpolation (export)
lib/gettext_example/gettext.ex:2: call Gettext.__using__/1 (compile)
lib/gettext_example/gettext.ex:2: call Gettext.Compiler.warn_if_domain_contains_slashes/1 (runtime)
lib/gettext_example/gettext.ex:2: call Gettext.Compiler.warn_if_domain_contains_slashes/1 (runtime)
lib/gettext_example/gettext.ex:2: call Gettext.Interpolation.interpolate/2 (runtime)
lib/gettext_example/gettext.ex:2: call Gettext.Interpolation.interpolate/2 (runtime)
lib/gettext_example/gettext.ex:2: import Gettext.Interpolation.interpolate/2 (runtime)
lib/gettext_example/gettext.ex:2: import Gettext.Interpolation.interpolate/2 (runtime)
lib/gettext_example/gettext.ex:2: call Gettext.Interpolation.to_interpolatable/1 (runtime)
lib/gettext_example/gettext.ex:2: call Gettext.Interpolation.to_interpolatable/1 (runtime)
lib/gettext_example/gettext.ex:2: import Gettext.Interpolation.to_interpolatable/1 (runtime)
lib/gettext_example/gettext.ex:2: import Gettext.Interpolation.to_interpolatable/1 (runtime)
# gettext = v0.18.2 の場合
iex(2)> Mix.Project.config()[:elixirc_paths] |> Mix.Utils.extract_files([:ex]) |> Enum.each(&CompilationTracer.run(&1, [:gettext]))
lib/gettext_example.ex:9: call Gettext.dpgettext/5 (runtime)
lib/gettext_example/gettext.ex:1: alias Gettext.Backend (compile)
lib/gettext_example/gettext.ex:1: require Gettext.Backend (export)
lib/gettext_example/gettext.ex:1: call Gettext.Compiler.__before_compile__/1 (compile)
lib/gettext_example/gettext.ex:1: call Gettext.Compiler.append_extracted_comment/1 (runtime)
lib/gettext_example/gettext.ex:1: call Gettext.Compiler.expand_to_binary/4 (runtime)
lib/gettext_example/gettext.ex:1: call Gettext.Compiler.expand_to_binary/4 (runtime)
lib/gettext_example/gettext.ex:1: call Gettext.Compiler.expand_to_binary/4 (runtime)
lib/gettext_example/gettext.ex:1: call Gettext.Compiler.expand_to_binary/4 (runtime)
lib/gettext_example/gettext.ex:1: call Gettext.Compiler.expand_to_binary/4 (runtime)
lib/gettext_example/gettext.ex:1: call Gettext.Compiler.expand_to_binary/4 (runtime)
lib/gettext_example/gettext.ex:1: call Gettext.Compiler.expand_to_binary/4 (runtime)
lib/gettext_example/gettext.ex:1: call Gettext.Compiler.expand_to_binary/4 (runtime)
lib/gettext_example/gettext.ex:1: call Gettext.Compiler.get_and_flush_extracted_comments/0 (runtime)
lib/gettext_example/gettext.ex:1: call Gettext.Compiler.get_and_flush_extracted_comments/0 (runtime)
lib/gettext_example/gettext.ex:1: call Gettext.Extractor.extract/6 (runtime)
lib/gettext_example/gettext.ex:1: call Gettext.Extractor.extract/6 (runtime)
lib/gettext_example/gettext.ex:1: call Gettext.Extractor.extracting?/0 (compile)
lib/gettext_example/gettext.ex:1: call Gettext.Extractor.extracting?/0 (compile)
lib/gettext_example/gettext.ex:1: call Gettext.Extractor.extracting?/0 (runtime)
lib/gettext_example/gettext.ex:1: call Gettext.Extractor.extracting?/0 (runtime)
lib/gettext_example/gettext.ex:1: call Gettext.ExtractorAgent.add_backend/1 (compile)
lib/gettext_example/gettext.ex:1: call Gettext.ExtractorAgent.add_backend/1 (compile)
lib/gettext_example/gettext.ex:2: require Gettext (export)
lib/gettext_example/gettext.ex:2: require Gettext.Interpolation (export)
lib/gettext_example/gettext.ex:2: require Gettext.Interpolation (export)
lib/gettext_example/gettext.ex:2: call Gettext.__using__/1 (compile)
lib/gettext_example/gettext.ex:2: call Gettext.Compiler.warn_if_domain_contains_slashes/1 (runtime)
lib/gettext_example/gettext.ex:2: call Gettext.Compiler.warn_if_domain_contains_slashes/1 (runtime)
lib/gettext_example/gettext.ex:2: call Gettext.Interpolation.interpolate/2 (runtime)
lib/gettext_example/gettext.ex:2: call Gettext.Interpolation.interpolate/2 (runtime)
lib/gettext_example/gettext.ex:2: import Gettext.Interpolation.interpolate/2 (runtime)
lib/gettext_example/gettext.ex:2: import Gettext.Interpolation.interpolate/2 (runtime)
lib/gettext_example/gettext.ex:2: call Gettext.Interpolation.to_interpolatable/1 (runtime)
lib/gettext_example/gettext.ex:2: call Gettext.Interpolation.to_interpolatable/1 (runtime)
lib/gettext_example/gettext.ex:2: import Gettext.Interpolation.to_interpolatable/1 (runtime)
lib/gettext_example/gettext.ex:2: import Gettext.Interpolation.to_interpolatable/1 (runtime)
gettext
のバージョンを更新したことにより、呼び出している関数や macro が変わったことが分かります。
1c1
< lib/gettext_example.ex:9: call Gettext.dgettext/4 (runtime)
---
> lib/gettext_example.ex:9: call Gettext.dpgettext/5 (runtime)
11a12,13
> lib/gettext_example/gettext.ex:1: call Gettext.Compiler.expand_to_binary/4 (runtime)
> lib/gettext_example/gettext.ex:1: call Gettext.Compiler.expand_to_binary/4 (runtime)
14,15c16,17
< lib/gettext_example/gettext.ex:1: call Gettext.Extractor.extract/5 (runtime)
< lib/gettext_example/gettext.ex:1: call Gettext.Extractor.extract/5 (runtime)
---
> lib/gettext_example/gettext.ex:1: call Gettext.Extractor.extract/6 (runtime)
> lib/gettext_example/gettext.ex:1: call Gettext.Extractor.extract/6 (runtime)
できたこと
ライブラリの macro を使っている場合、自分のコードは全く変えていないのに、呼び出している関数や macro が変わっているケースがあります。
もちろんコードを追えばどのような変化があったのか分かるのですが、compilation tracers を利用することによって、苦労せずに見やすい形でその変化を確認することができました。
(実際にこのような変化を確認したいケース自体があまりありませんが。。。)
(おまけ)ある application の関数と macro の一覧を取得する
mfa_list =
for m <- Application.spec(:gettext, :modules),
{f, a} <- m.module_info()[:exports],
do: {m, f, a}
gettext
の v0.17.1
と v0.18.2
で diff を取ってみると、以下のようになりました。
5d4
< {Gettext, :dngettext, 5}
6a6,8
> {Gettext, :dpgettext, 4}
> {Gettext, :dpngettext, 6}
> {Gettext, :dpngettext, 7}
13a16,18
> {Gettext, :pgettext, 3}
> {Gettext, :pgettext, 4}
> {Gettext, :pngettext, 6}
19a25
> {Gettext, :dpgettext, 5}
31,34d36
< {Gettext.Compiler, :append_extracted_comment, 1}
< {Gettext.Compiler, :expand_to_binary, 4}
< {Gettext.Compiler, :get_and_flush_extracted_comments, 0}
< {Gettext.Compiler, :warn_if_domain_contains_slashes, 1}
36a39,42
> {Gettext.Compiler, :warn_if_domain_contains_slashes, 1}
> {Gettext.Compiler, :append_extracted_comment, 1}
> {Gettext.Compiler, :get_and_flush_extracted_comments, 0}
> {Gettext.Compiler, :expand_to_binary, 4}
40d45
< {Gettext.Error, :exception, 1}
43a49
> {Gettext.Error, :exception, 1}
47,48d52
< {Gettext.Extractor, :extract, 5}
< {Gettext.Extractor, :extracting?, 0}
53a58,59
> {Gettext.Extractor, :extract, 6}
> {Gettext.Extractor, :extracting?, 0}
75,77d80
< {Gettext.Interpolation, :interpolate, 2}
< {Gettext.Interpolation, :keys, 1}
< {Gettext.Interpolation, :to_interpolatable, 1}
79a83,85
> {Gettext.Interpolation, :keys, 1}
> {Gettext.Interpolation, :interpolate, 2}
> {Gettext.Interpolation, :to_interpolatable, 1}
96d101
< {Gettext.PO, :dump, 2}
98,99d102
< {Gettext.PO, :parse_file!, 1}
< {Gettext.PO, :parse_string, 1}
102a106,108
> {Gettext.PO, :parse_string, 1}
> {Gettext.PO, :dump, 2}
> {Gettext.PO, :parse_file!, 1}
115d120
< {Gettext.PO.SyntaxError, :exception, 1}
118a124
> {Gettext.PO.SyntaxError, :exception, 1}
120d125
< {Gettext.PO.Tokenizer, :tokenize, 1}
122a128
> {Gettext.PO.Tokenizer, :tokenize, 1}
129,132d134
< {Gettext.PO.Translations, :autogenerated?, 1}
< {Gettext.PO.Translations, :find, 2}
< {Gettext.PO.Translations, :mark_as_fuzzy, 1}
< {Gettext.PO.Translations, :protected?, 2}
135a138,141
> {Gettext.PO.Translations, :mark_as_fuzzy, 1}
> {Gettext.PO.Translations, :autogenerated?, 1}
> {Gettext.PO.Translations, :protected?, 2}
> {Gettext.PO.Translations, :find, 2}
137a144
> {Gettext.Plural, :nplurals, 1}
142d148
< {Gettext.Plural, :nplurals, 1}
146d151
< {Gettext.Plural.UnknownLocaleError, :exception, 1}
149a155,162
> {Gettext.Plural.UnknownLocaleError, :exception, 1}
> {Gettext.PluralFormError, :__info__, 1}
> {Gettext.PluralFormError, :__struct__, 0}
> {Gettext.PluralFormError, :__struct__, 1}
> {Gettext.PluralFormError, :exception, 1}
> {Gettext.PluralFormError, :message, 1}
> {Gettext.PluralFormError, :module_info, 0}
> {Gettext.PluralFormError, :module_info, 1}
例えば、バージョンアップで Gettext.Extractor.extract/5
が無くなっているので、v0.17.1
で compile した gettext_example
を v0.18.2
の環境で使おうとしても無理ということが分かります。
おわりに
Compilation tracers は色々な使い方ができそうですね!
23 日の担当は @Hiroshi_Tsukamoto さんです。
投稿楽しみにしています。