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 s. Для удобства можно использовать псевдоним StackTraces.StackTrace вместо 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. При вызове 4d61726b646f776e2e436f64652822222c2022737461636b747261636528292229_40726566 из REPL у вас также будет несколько дополнительных фреймов в стеке от 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 содержит имя функции, имя файла, номер строки, информацию о лямбде, флаг, указывающий, был ли кадр встроен, флаг, указывающий, является ли это 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]
[...]

Вы можете заметить, что в приведенном выше примере первый стековый фрейм указывает на строку 4, где вызывается stacktrace, а не на строку 2, где вызывается bad_function, и фрейм bad_function полностью отсутствует. Это вполне объяснимо, учитывая, что 4d61726b646f776e2e436f64652822222c2022737461636b74726163652229_40726566 вызывается в контексте catch. Хотя в этом примере довольно легко найти фактический источник ошибки, в сложных случаях отслеживание источника ошибки становится нетривиальным.

Это можно исправить, передав результат catch_backtrace в stacktrace. Вместо того чтобы возвращать информацию о стеке вызовов для текущего контекста, 4d61726b646f776e2e436f64652822222c202263617463685f6261636b74726163652229_40726566 возвращает информацию о стеке для контекста последнего исключения:

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
[...]

Exception stacks and current_exceptions

Julia 1.1

Исключения стека требуют как минимум Julia 1.1.

При обработке исключения могут возникать дополнительные исключения. Полезно просмотреть все эти исключения, чтобы определить коренную причину проблемы. Среда выполнения Julia поддерживает это, помещая каждое исключение в внутренний стек исключений по мере его возникновения. Когда код выходит из блока 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, где произошли исключения. Когда задача завершается с необработанными исключениями, current_exceptions(task) может быть использован для проверки стека исключений для этой задачи.

Comparison with backtrace

Вызов 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. Это происходит потому, что по умолчанию 4d61726b646f776e2e436f64652822222c2022737461636b74726163652229_40726566 удаляет любые функции 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.StackFrame s, передавая их в StackTraces.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!