Как мы обычно отлаживаем программу, если она не работает? Традиционный и самый простой способ — напичкать ее отладочным выводом, запустить, и посмотреть, что происходит. Чего уж греха таить. Однако в 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) говорим выводить все вызовы перехватываемых функций ( p rint all c alls). Пусть вас не пугают такие странные имена функций. Они специально сокращены, потому что эрлангистам приходится довольно часто их набирать.

Теперь каждый раз, когда вызывается функция 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

EnglishRussianUkrainian