스택 트레이스
스택 트레이스 (Stack Traces)
StackTraces 모듈은 사람이 읽기 쉬우면서도 프로그램에서 쉽게 다룰 수 있는 간단한 스택 트레이스를 제공해요.
본문
스택 트레이스 보기 (Viewing a stack trace)
스택 트레이스를 얻는 데 쓰는 주요 함수는 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들의 벡터가 반환돼요. 편의를 위해 StackTraces.StackTrace라는 별칭(alias)을 Vector{StackFrame} 대신 쓸 수 있어요. ([...]가 섞인 예제들은 코드를 어떻게 실행하느냐에 따라 출력이 달라질 수 있다는 뜻이에요.)
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
유용한 정보 추출하기 (Extracting useful information)
각 StackTraces.StackFrame은 함수 이름, 파일 이름, 줄 번호, lambda 정보, 프레임이 인라인되었는지 나타내는 플래그, C 함수인지 나타내는 플래그(기본적으로 C 함수는 스택 트레이스에 나타나지 않아요), 그리고 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
이렇게 하면 스택 트레이스 정보를 로깅, 오류 처리 등에 프로그램적으로 활용할 수 있어요.
오류 처리 (Error handling)
호출 스택의 현재 상태에 대한 정보에 쉽게 접근할 수 있는 것은 많은 곳에서 도움이 되지만, 가장 명확한 활용처는 오류 처리와 디버깅이에요.
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]
[...]
위 예제에서 첫 스택 프레임이, bad_function이 호출되는 2번 줄이 아니라 stacktrace가 호출되는 4번 줄을 가리키고, bad_function의 프레임은 아예 빠져 있다는 걸 눈치챘을 거예요. 이는 stacktrace가 catch의 컨텍스트에서 호출되기 때문에 당연한 일이에요. 이 예제에서는 오류의 실제 출처를 찾기가 꽤 쉬운 편이지만, 복잡한 경우에는 오류 출처를 추적하는 게 결코 사소한 일이 아니에요.
이 문제는 catch_backtrace의 결과를 stacktrace에 넘겨주면 해결할 수 있어요. catch_backtrace는 현재 컨텍스트의 호출 스택 정보를 반환하는 대신, 가장 최근 예외의 컨텍스트에 대한 스택 정보를 반환해요.
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
[...]
예외 스택과 current_exceptions
이 기능은 최소 Julia 1.1이 필요해요.
예외를 처리하는 동안 추가 예외가 던져질 수 있어요. 문제의 근본 원인을 찾으려면 이 모든 예외를 살펴보는 게 유용할 수 있어요. Julia 런타임은 각 예외가 발생할 때마다 내부 *예외 스택(exception stack)*에 푸시해 이걸 지원해요. 코드가 catch 블록을 정상적으로(즉, 추가 예외를 던지지 않고) 빠져나오면, 연결된 try에서 스택에 푸시됐던 예외들은 성공적으로 처리된 것으로 간주되어 스택에서 제거돼요.
현재 예외의 스택은 current_exceptions 함수로 접근할 수 있어요. 예를 들어,
julia> try
error("(A) The root cause")
catch
try
error("(B) An exception while handling the exception")
catch
for (exc, bt) in current_exceptions()
showerror(stdout, exc, bt)
println(stdout)
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에 저장돼요. task가 처리되지 않은 예외로 실패하면, current_exceptions(task)로 그 task의 예외 스택을 확인할 수 있어요.
backtrace와의 비교 (Comparison with backtrace)
backtrace를 호출하면 Union{Ptr{Nothing}, Base.InterpreterIP}의 벡터가 반환되는데, 이걸 번역(translation)을 위해 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 호출의 스택 프레임도 포함하고 싶다면 이렇게 하면 돼요.
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가 반환한 개별 포인터는 StackTraces.lookup에 넘겨 StackTraces.StackFrame으로 번역할 수 있어요.
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!