スタックトレース
StackTraces
モジュールは人間と機械の両方にとって読みやすい簡単なスタックトレースを提供します。
スタックトレースの表示
スタックトレースの表示で使う中心的な関数は stacktrace
です。この関数が返すスタックトレースの例を示します:
6-element Array{Base.StackTraces.StackFrame,1}:
top-level scope
eval at boot.jl:317 [inlined]
eval(::Module, ::Expr) at REPL.jl:5
eval_user_input(::Any, ::REPL.REPLBackend) at REPL.jl:85
macro expansion at REPL.jl:116 [inlined]
(::getfield(REPL, Symbol("##28#29")){REPL.REPLBackend})() at event.jl:92
stacktrace()
は StackTraces.StackFrame
のベクトルを返します。Vector{StackFrame}
には簡単のため別名 StackTraces.StackTrace
が用意されています。この章の実行例では、コードを実行する環境によって異なる出力は [...]
で表し、関数をインライン化しないよう必要に応じて関数定義の前に @noinline
マクロを付けます:
julia> example() = stacktrace()
example (generic function with 1 method)
julia> example()
7-element Array{Base.StackTraces.StackFrame,1}:
example() at REPL[1]:1
top-level scope
eval at boot.jl:317 [inlined]
[...]
julia> @noinline child() = stacktrace()
child (generic function with 1 method)
julia> @noinline parent() = child()
parent (generic function with 1 method)
julia> grandparent() = parent()
grandparent (generic function with 1 method)
julia> grandparent()
9-element Array{Base.StackTraces.StackFrame,1}:
child() at REPL[3]:1
parent() at REPL[4]:1
grandparent() at REPL[5]:1
[...]
通常 stacktrace()
が返すスタックトレースは eval at boot.jl
が付いたフレームを含みます。REPL から stacktrace()
を呼んだ場合には、この他に REPL.jl
内の関数を指すフレームがいくつか加わります:
julia> example() = stacktrace()
example (generic function with 1 method)
julia> example()
7-element Array{Base.StackTraces.StackFrame,1}:
example() at REPL[1]:1
top-level scope
eval at boot.jl:317 [inlined]
eval(::Module, ::Expr) at REPL.jl:5
eval_user_input(::Any, ::REPL.REPLBackend) at REPL.jl:85
macro expansion at REPL.jl:116 [inlined]
(::getfield(REPL, Symbol("##28#29")){REPL.REPLBackend})() at event.jl:92
スタックトレースから情報を取り出す
StackTraces.StackFrame
型の値には次の情報が含まれます:
func
: 関数名file
: ファイル名line
: 行番号linfo
: 関数に関する情報inlined
: フレームがインライン化されたかどうかを示すフラグfrom_c
: C 関数かどうかを示すフラグ (デフォルトではスタックトレースに C 関数は表示されない)-
pointer
:backtrace
が返すポインタの整数表現
例を示します:
julia> frame = stacktrace()[3]
eval(::Module, ::Expr) at REPL.jl:5
julia> frame.func
:eval
julia> frame.file
Symbol("~/julia/usr/share/julia/stdlib/v0.7/REPL/src/REPL.jl")
julia> frame.line
5
julia> frame.linfo
MethodInstance for eval(::Module, ::Expr)
julia> frame.inlined
false
julia> frame.from_c
false
julia> frame.pointer
0x00007f92d6293171
このフィールドを使えば、スタックトレースの情報をログやエラー処理などでプログラムから利用できます。
エラー処理
現在のコールスタックの情報へ簡単にアクセスできる機能が役立つ場面は多くありますが、最も明らかな応用はエラー処理とデバッグです。簡単な例を示します:
julia> @noinline bad_function() = undeclared_variable
bad_function (generic function with 1 method)
julia> @noinline example() = try
bad_function()
catch
stacktrace()
end
example (generic function with 1 method)
julia> example()
7-element Array{Base.StackTraces.StackFrame,1}:
example() at REPL[2]:4
top-level scope
eval at boot.jl:317 [inlined]
[...]
出力されるスタックフレームが指すのが stacktrace
を呼んだ四行目であり、エラーが起きた関数 bad_function
を呼んだ二行目でないことに気が付くでしょう。また bad_function
の名前はスタックフレームに含まれていません。こうなっているのは、stacktrace
が catch
の文脈で実行されているためです。この例ではエラーの発生場所を簡単に見つけられますが、複雑な場合にはエラーの発生場所の発見が自明でない処理となります。
エラーが起きた時点のスタックトレースは catch_backtrace()
の返り値を stacktrace
に渡すことで取得できます。catch_backtrace
は最も最近の例外が発生した時点におけるコールスタック情報を返すので、これを現在の文脈におけるコールスタック情報の代わりに stacktrace
へ渡せば正しいエラー処理となります:
julia> @noinline bad_function() = undeclared_variable
bad_function (generic function with 1 method)
julia> @noinline example() = try
bad_function()
catch
stacktrace(catch_backtrace())
end
example (generic function with 1 method)
julia> example()
8-element Array{Base.StackTraces.StackFrame,1}:
bad_function() at REPL[1]:1
example() at REPL[2]:2
[...]
スタックトレースが適切な行番号とフレームを持っていることを確認してください。もう少し複雑な例を示します:
julia> @noinline child() = error("Whoops!")
child (generic function with 1 method)
julia> @noinline parent() = child()
parent (generic function with 1 method)
julia> @noinline function grandparent()
try
parent()
catch err
println("ERROR: ", err.msg)
stacktrace(catch_backtrace())
end
end
grandparent (generic function with 1 method)
julia> grandparent()
ERROR: Whoops!
10-element Array{Base.StackTraces.StackFrame,1}:
error at error.jl:33 [inlined]
child() at REPL[1]:1
parent() at REPL[2]:1
grandparent() at REPL[3]:3
[...]
catch_stack
と例外スタック
例外スタックの利用には最低でも Julia 1.1 が必要です。
例外を処理しているときに他の例外が発生する可能性があります。そういった場合に全ての例外を調べられれば、問題の原因を調べるのに役立ちます。Julia ランタイムは内部の例外スタック (exception stack) に例外をプッシュすることでこの機能を実現します。コードが catch
ブロックの実行を通常通り終えると、対応する try
ブロックで発生し例外スタックにプッシュされた例外に対するエラー処理は成功したとみなされ、その例外はスタックから削除されます。
例外スタックには実験的な Base.catch_stack
関数を使ってアクセスします:
julia> try
error("(A) The root cause")
catch
try
error("(B) An exception while handling the exception")
catch
for (exc, bt) in Base.catch_stack()
showerror(stdout, exc, bt)
println()
end
end
end
(A) The root cause
Stacktrace:
[1] error(::String) at error.jl:33
[2] top-level scope at REPL[7]:2
[3] eval(::Module, ::Any) at boot.jl:319
[4] eval_user_input(::Any, ::REPL.REPLBackend) at REPL.jl:85
[5] macro expansion at REPL.jl:117 [inlined]
[6] (::getfield(REPL, Symbol("##26#27")){REPL.REPLBackend})() at task.jl:259
(B) An exception while handling the exception
Stacktrace:
[1] error(::String) at error.jl:33
[2] top-level scope at REPL[7]:5
[3] eval(::Module, ::Any) at boot.jl:319
[4] eval_user_input(::Any, ::REPL.REPLBackend) at REPL.jl:85
[5] macro expansion at REPL.jl:117 [inlined]
[6] (::getfield(REPL, Symbol("##26#27")){REPL.REPLBackend})() at task.jl:259
この例では最初に起こった例外 (A)
がまずスタックにプッシュされ、その後 (B)
もプッシュされます。二つの catch
ブロックを通常終了する (例外を出さずに抜ける) と、全ての例外はスタックから取り除かれアクセスできなくなります。
例外スタックは例外が起こった Task
上に保存されます。タスクが捕捉されない例外を出して失敗したときは、catch_stack(task)
でそのタスクに対する例外スタックを調べることができます。
バックトレースとスタックトレースの比較
backtrace
関数を呼ぶと Union{Ptr{Nothing}, Base.InterpreterIP}
のベクトルが返ります。これは stacktrace
関数に渡すとスタックトレースに変換できます:
julia> trace = backtrace()
18-element Array{Union{Ptr{Nothing}, Base.InterpreterIP},1}:
Ptr{Nothing} @0x00007fd8734c6209
Ptr{Nothing} @0x00007fd87362b342
Ptr{Nothing} @0x00007fd87362c136
Ptr{Nothing} @0x00007fd87362c986
Ptr{Nothing} @0x00007fd87362d089
Base.InterpreterIP(CodeInfo(:(begin
Core.SSAValue(0) = backtrace()
trace = Core.SSAValue(0)
return Core.SSAValue(0)
end)), 0x0000000000000000)
Ptr{Nothing} @0x00007fd87362e4cf
[...]
julia> stacktrace(trace)
6-element Array{Base.StackTraces.StackFrame,1}:
top-level scope
eval at boot.jl:317 [inlined]
eval(::Module, ::Expr) at REPL.jl:5
eval_user_input(::Any, ::REPL.REPLBackend) at REPL.jl:85
macro expansion at REPL.jl:116 [inlined]
(::getfield(REPL, Symbol("##28#29")){REPL.REPLBackend})() at event.jl:92
backtrace()
が返すベクトルの要素数が 18 なのに対して、stacktrace
が返すベクトルの要素数は 6 しかないことに注目してください。こうなる理由は、デフォルトでは stacktrace
が低レベルの C 関数をスタックから取り除くためです。C 関数のスタックフレームを含めるには、stacktrace
の第二引数に true
を渡します:
julia> stacktrace(trace, true)
21-element Array{Base.StackTraces.StackFrame,1}:
jl_apply_generic at gf.c:2167
do_call at interpreter.c:324
eval_value at interpreter.c:416
eval_body at interpreter.c:559
jl_interpret_toplevel_thunk_callback at interpreter.c:798
top-level scope
jl_interpret_toplevel_thunk at interpreter.c:807
jl_toplevel_eval_flex at toplevel.c:856
jl_toplevel_eval_in at builtins.c:624
eval at boot.jl:317 [inlined]
eval(::Module, ::Expr) at REPL.jl:5
jl_apply_generic at gf.c:2167
eval_user_input(::Any, ::REPL.REPLBackend) at REPL.jl:85
jl_apply_generic at gf.c:2167
macro expansion at REPL.jl:116 [inlined]
(::getfield(REPL, Symbol("##28#29")){REPL.REPLBackend})() at event.jl:92
jl_fptr_trampoline at gf.c:1838
jl_apply_generic at gf.c:2167
jl_apply at julia.h:1540 [inlined]
start_task at task.c:268
ip:0xffffffffffffffff
backtrace
が返すポインタを一つずつ StackFrame
型へ変換するには、lookup
を使います:
julia> pointer = backtrace()[1];
julia> frame = StackTraces.lookup(pointer)
1-element Array{Base.StackTraces.StackFrame,1}:
jl_apply_generic at gf.c:2167
julia> println("The top frame is from $(frame[1].func)!")
The top frame is from jl_apply_generic!