Как мы обычно отлаживаем программу, если она не работает? Традиционный и самый простой способ — напичкать ее отладочным выводом, запустить, и посмотреть, что происходит. Чего уж греха таить. Однако в Erlang вы можете с легкостью сделать практически то же самое, не трогая исходный код программы, с помощью трассировщика dbg.
Чем dbg лучше отладочного вывода:
- Вам не нужно тратить время на редактирование исходного кода;
- Трассировщик можно натравить даже на работающее в боевом окружении приложение, не останавливая его;
- Вы не рискуете по ошибке запушить код с отладочным выводом в master (а такое бывает чаще, чем вы могли бы подумать, сам видел);
- Не нужно перекомпилировать программу ради еще одной строчки отладочного вывода;
- Трассировщик dbg предлагает вам намного больше возможностей, нежели отладочный вывод;
Пользоваться dbg очень просто. Рассмотрим самый простой вариант. Программа работает не так, как вам хочется, и вы решили посмотреть, с какими аргументами вызывается некая функция. Пусть это будет io : format / 2
. Нет ничего проще:
{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
, вы будите видеть, с какими аргументами она вызывалась:
Hello, Alex
(<0.36.0>) call io:format(«Hello, ~s~n»,[«Alex»])
ok
Само собой разумеется, это работает не только для вызовов, произведенных вручную. Чтобы остановить трассировщик, наберите:
ok
Если в двух словах, это все. Совсем не сложно, правда? Теперь перейдем к деталям.
Во-первых, можно одновременно трейсить сразу несколько функций, сделав, соответственно, несколько вызовов dbg : tp / 4
. Во-вторых, помимо dbg : tp / 4
есть также функции dbg : tp / 2
и dbg : tp / 3
. С их помощью вы можете, например, наблюдать за вызовами всех функций определенного модуля или всех одноименных функций заданного модуля независимо от их арности. Подробности вы найдете в erl -man dbg
(или в его онлайн-версии ). В-третьих, есть аналогичные функции dbg : tpl / N
, которые, в отличие от dbg : tp / N
, следят за вызовом не только экспортируемых, но и локальных (отсюда буква L в названии) функций модуля.
Выше я упомянул о трассировке приложения, работающего в боевом окружении, без его остановки. Это делается очень просто:
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 . С его помощью вы можете определить, какие вызовы функции, в зависимости от переданных параметров, следует трассировать, а какие не следует:
{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
Также с помощью этого аргумента вы можете отслеживать значения, возвращаемые функцией:
{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