Памятка по трассировке в Erlang

18 февраля 2013

Как мы обычно отлаживаем программу, если она не работает? Традиционный и самый простой способ — напичкать ее отладочным выводом, запустить, и посмотреть, что происходит. Чего уж греха таить. Однако в Erlang вы можете с легкостью сделать практически то же самое, не трогая исходный код программы, с помощью трассировщика dbg.

Чем dbg лучше отладочного вывода:

  • Вам не нужно тратить время на редактирование исходного кода;
  • Трассировщик можно натравить даже на работающее в боевом окружении приложение, не останавливая его;
  • Вы не рискуете по ошибке запушить код с отладочным выводом в master (а такое бывает чаще, чем вы могли бы подумать, сам видел);
  • Не нужно перекомпилировать программу ради еще одной строчки отладочного вывода;
  • Трассировщик dbg предлагает вам намного больше возможностей, нежели отладочный вывод;

Пользоваться dbg очень просто. Рассмотрим самый простой вариант. Программа работает не так, как вам хочется, и вы решили посмотреть, с какими аргументами вызывается некая функция. Пусть это будет io:format/2. Нет ничего проще:

1> dbg:tracer().
{ok,<0.38.0>}
2> dbg:tp(io, format, 2, []).
{ok,[{matched,nonode@nohost,1}]}
3> dbg:p(all, c).
{ok,[{matched,nonode@nohost,29}]}

Здесь мы (1) запускаем трассировщик (2) перехватываем вызовы io:format/2, после чего (3) говорим выводить все вызовы перехватываемых функций (print all calls). Пусть вас не пугают такие странные имена функций. Они специально сокращены, потому что эрлангистам приходится довольно часто их набирать.

Теперь каждый раз, когда вызывается функция io:format/2, вы будите видеть, с какими аргументами она вызывалась:

4> io:format("Hello, ~s~n", ["Alex"]).
Hello, Alex
(<0.36.0>) call io:format("Hello, ~s~n",["Alex"])
ok

Само собой разумеется, это работает не только для вызовов, произведенных вручную. Чтобы остановить трассировщик, наберите:

5> dbg:stop_clear().
ok

Если в двух словах, это все. Совсем не сложно, правда? Теперь перейдем к деталям.

Во-первых, можно одновременно трейсить сразу несколько функций, сделав, соответственно, несколько вызовов dbg:tp/4. Во-вторых, помимо dbg:tp/4 есть также функции dbg:tp/2 и dbg:tp/3. С их помощью вы можете, например, наблюдать за вызовами всех функций определенного модуля или всех одноименных функций заданного модуля независимо от их арности. Подробности вы найдете в erl -man dbg (или в его онлайн-версии). В-третьих, есть аналогичные функции dbg:tpl/N, которые, в отличие от dbg:tp/N, следят за вызовом не только экспортируемых, но и локальных (отсюда буква L в названии) функций модуля.

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

$ erl -name afiskon-dbg -setcookie secretcookie
1> dbg:tracer().
{ok,<0.39.0>}
2> dbg:n('node@example.ru').
{ok,'node@example.ru'}

Здесь мы (1) запустили трассировщик и (2) подрубились к удаленной ноде. Теперь просто говорим dbg:tp(foo,bar,2,[]) и dbg:p(all,c), как делали это ранее. В консоли мы увидим все вызовы функции foo:bar/2 на удаленной ноде. Функция dbg:ln/0 выводит список трессируемых нод, а dbg:cn/1 — отменяет трассировку заданной ноды.

Трассировку функций также можно отменять с помощью dbg:ctp/N (отменяет как dbg:tp/N, так и dbg:tpl/N), dbg:ctpl/N (отменяет только dbg:tpl/N), dbg:ctpg/N (отменяет лишь dbg:tp/N), а также dbg:dtp/0 (отменяет все вызовы dbg:tp/N).

Наверняка вы обратили внимание на последний параметр у функции dbg:tp/4. Это так называемый match specification. С его помощью вы можете определить, какие вызовы функции, в зависимости от переданных параметров, следует трассировать, а какие не следует:

2> dbg:tp(io, format, 2, dbg:fun2ms(fun([Format, [Arg1|_]]) when Arg1 /= "Alex" -> true end)).
{ok,[{matched,nonode@nohost,1},{saved,1}]}
3> dbg:p(all,c).
{ok,[{matched,nonode@nohost,29}]}
4> io:format("Hello, ~s~n", ["Alex"]).
Hello, Alex
ok
5> io:format("Hello, ~s~n", ["Bob"]).
Hello, Bob
(<0.36.0>) call io:format("Hello, ~s~n",["Bob"])
ok

Также с помощью этого аргумента вы можете отслеживать значения, возвращаемые функцией:

6> dbg:tp(io, format, 2, dbg:fun2ms(fun(_) -> return_trace() end)).
{ok,[{matched,nonode@nohost,1},{saved,2}]}
7> io:format("Hello, ~s~n", ["Alex"]).                            
Hello, Alex
(<0.36.0>) call io:format("Hello, ~s~n",["Alex"])
(<0.36.0>) returned from io:format/2 -> ok
ok

Match specifications могут быть сохранены в текстовом файле, а затем загружены из него и использованы повторно с помощью функций dbg:wtp/1, dbg:rtp/1 и dbg:ltp/0. Первая из них сохраняет match specifications в файл, вторая — загружает из файла, третья — выводит список match specifications и их номера. Чтобы использовать match specification из этого списка, передайте функции dbg:tp/4 последним аргументом номер требуемой match specification.

Если вам нужно трассировать не все процессы, а только один конкретный, вместо dbg:p(all,c) скажите что-то вроде dbg:p(self(),c).

Помимо описанного в этой заметке dbg позволяет многое другое. Например, с его помощью можно отслеживать сообщения, получаемые (или принимаемые, или и те, и другие) заданным процессом, пересылать соответствующие уведомления процессу-трассировщику, запущенному на другом узле, который будет писать эти сообщения в текстовый лог-файл. Но это, пожалуй, тема для отдельного поста.

А как вы отлаживаете свои программы на Erlang?

Дополнение: Написал библиотеку для мемоизации в Erlang

Метки: , , .


Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.