Erlang代码分析与调试全攻略
1. 分析与性能分析工具
在开发过程中,我们常常需要深入了解运行中的程序,查找潜在问题代码、调查性能问题以及检查死代码或已弃用函数的使用情况。以下是一些相关的工具和方法。
1.1 覆盖率分析
在测试代码时,我们不仅想知道哪些代码行被频繁执行,还想知道哪些代码行从未被执行。未执行的代码行可能是错误的潜在来源,因此找出这些代码行非常重要。我们可以使用覆盖率分析器来完成这项工作。
以下是一个覆盖率分析的示例:
1> cover:start().
%% start the coverage analyser
{ok,<0.34.0>}
2> cover:compile(shout).
%% compile shout.erl for coverage
{ok,shout}
3> shout:start().
%% run the program
<0.41.0>
Playing:<<"title: track018 performer: .. ">>
4> %% let the program run for a bit
4>
cover:analyse_to_file(shout).
%% analyse the results
{ok,"shout.COVER.out"}
%% this is the results file
覆盖率分析的结果会输出到一个文件中,文件左侧会显示每行代码的执行次数。标记为零的行特别值得关注,因为这些代码从未被执行,我们无法确定程序在这些部分是否正确。设计测试用例使所有覆盖率计数大于零是系统地查找程序中隐藏故障的有效方法。
1.2 性能分析工具
标准的Erlang发行版提供了三种性能分析工具:
| 工具名称 | 功能描述 | 适用场景 | 系统负载 |
| ---- | ---- | ---- | ---- |
| cprof | 统计每个函数的调用次数,轻量级分析器 | 实时系统 | 增加5% - 10%系统负载 |
| fprof | 显示调用和被调用函数的时间,输出到文件 | 实验室或模拟系统中的大型系统分析 | 显著增加系统负载 |
| eprof | 测量Erlang程序的时间使用情况,fprof的前身 | 小规模分析 | - |
以下是运行cprof的示例:
1> cprof:start().
%% start the profiler
4501
2> shout:start().
%% run the application
<0.35.0>
3> cprof:pause().
%% pause the profiler
4844
4> cprof:analyse(shout). %% analyse function calls
{shout,232,
[{{shout,split,2},73},
{{shout,write_data,4},33},
{{shout,the_header,1},33},
{{shout,send_file,6},33},
{{shout,bump,1},32},
{{shout,make_header1,1},5},
{{shout,'-got_request_from_client/3-fun-0-',1},4},
{{shout,songs_loop,1},2},
{{shout,par_connect,2},2},
{{shout,unpack_song_descriptor,1},1},
...
5> cprof:stop().
%% stop the profiler
4865
此外, cprof:analyse() 可以分析所有已收集统计信息的模块。更多关于cprof的详细信息,请参考 http://www.erlang.org/doc/man/cprof.html 。
1.3 xref交叉引用
可以使用xref模块生成交叉引用。xref仅在代码使用 debug_info 标志编译时有效。在开发程序时,偶尔对代码进行交叉引用检查是个好主意。
以下是对一个示例项目进行交叉引用检查的步骤:
1. 进入项目目录:
$ cd /home/joe/2007/vsg-1.6
- 删除所有
.beam文件:
$ rm *.beam
- 使用
debug_info标志编译所有.erl文件:
$ erlc +debug_info *.erl
- 启动Erlang shell并运行交叉引用检查:
1> xref:d('.')
[{deprecated,[]},
{undefined,[{{new,win1,0},{wish_manager,on_destroy,2}},
{{vsg,alpha_tag,0},{wish_manager,new_index,0}},
{{vsg,call,1},{wish,cmd,1}},
{{vsg,cast,1},{wish,cast,1}},
{{vsg,mkWindow,7},{wish,start,0}},
{{vsg,new_tag,0},{wish_manager,new_index,0}},
{{vsg,new_win_name,0},{wish_manager,new_index,0}},
{{vsg,on_click,2},{wish_manager,bind_event,2}},
{{vsg,on_move,2},{wish_manager,bind_event,2}},
{{vsg,on_move,2},{wish_manager,bind_tag,2}},
{{vsg,on_move,2},{wish_manager,new_index,0}}]},
{unused,[{vsg,new_tag,0},
{vsg_indicator_box,theValue,1},
{vsg_indicator_box,theValue,1}]}]
xref:d('.') 会对当前目录中使用调试标志编译的所有代码进行交叉引用分析,并生成已弃用、未定义和未使用函数的列表。
2. 调试
调试Erlang程序相对容易,这得益于其单赋值变量的特性。由于Erlang没有指针和可变状态(除了ETS表和进程字典),找出问题所在通常不是难事。
2.1 编译器诊断
当编译程序时,如果源代码存在语法错误,编译器会提供有用的错误消息。以下是一些常见的编译器错误:
- 头部不匹配 :如果函数定义的子句没有相同的名称和参数数量,会出现此错误。
%% bad.erl
Line 1
foo(1,2) ->
-
a;
-
foo(2,3,a) ->
-
b.
1> c(bad).
./bad.erl:3: head mismatch
- 未绑定变量 :代码中使用了未赋值的变量会导致此错误。
%% bad.erl
Line 1
foo(A, B) ->
-
bar(A, dothis(X), B),
-
baz(Y, X).
1> c(bad).
./bad.erl:2: variable 'X' is unbound
./bad.erl:3: variable 'Y' is unbound
- 未终止的字符串 :如果在字符串或原子中忘记了引号,会得到此错误消息。
- 不安全变量 :程序可能会导致变量未定义的情况,编译器会发出“不安全变量”错误消息。
%% bad.erl
Line 1
foo() ->
-
case bar() of
-
1 ->
-
X = 1,
5
Y = 2;
-
2 ->
-
X = 3
-
end,
-
b(X, Y).
> c(bad).
./bad.erl:9: variable 'Y' unsafe in 'case' (line 2)
{ok,bad}
- 变量遮蔽 :在函数内部使用与参数同名的变量可能会导致混淆,编译器会发出警告。
%% bad.erl
Line 1
foo(X, L) ->
-
lists:map(fun(X) -> 2*X end, L).
1> c(bad).
./bad.erl:1: Warning: variable 'X' is unused
./bad.erl:2: Warning: variable 'X' shadowed in 'fun'
{ok,bad}
可以通过重命名变量来解决此问题:
foo(X, L) ->
lists:map(fun(Z) -> 2*Z end, L).
2.2 运行时诊断
如果Erlang进程崩溃,可能会得到错误消息。要查看错误消息,需要有其他进程监控崩溃的进程,并在被监控进程死亡时打印错误消息。为了看到所有错误消息,最好始终使用 spawn_link 。
当与shell链接的进程崩溃时,会打印堆栈跟踪信息。以下是一个故意制造错误并查看堆栈跟踪的示例:
%% lib_misc.erl
deliberate_error(A) ->
bad_function(A, 12),
lists:reverse(A).
bad_function(A, _) ->
{ok, Bin} = file:open({abc,123}, A),
binary_to_list(Bin).
1> lib_misc:deliberate_error("file.erl").
** exited: {{badmatch,{error,einval}},
[{lib_misc,bad_function,2},
{lib_misc,deliberate_error,1},
{erl_eval,do_apply,5},
{shell,exprs,6},
{shell,eval_loop,3}]} **
堆栈跟踪信息从顶部开始,显示错误的类型和具体值,随后是函数调用的列表。需要注意的是,由于Erlang的尾调用优化,某些调用可能不会出现在堆栈跟踪中。
2.3 调试技术
Erlang程序员使用多种调试技术,以下是一些常见的方法:
- io:format调试 :在程序中添加 io:format(...) 语句来打印感兴趣变量的值。在调试并行程序时,在发送和接收消息前后打印消息是个好主意。
loop(...) ->
receive
Any ->
io:format("*** warning unexpected message:~p~n",[Any])
loop(...)
end
还可以使用宏来标记未实现的功能:
%% lib_misc.erl
-define(NYI(X),(begin
io:format("*** NYI ~p ~p ~p~n",[?MODULE, ?LINE, X]),
exit(nyi)
end)).
glurk(X, Y) ->
?NYI({glurk, X, Y}).
- 数据结构转储到文件 :如果感兴趣的数据结构很大,可以将其写入文件。
%% lib_misc.erl
dump(File, Term) ->
Out = File ++ ".tmp",
io:format("** dumping to ~s~n",[Out]),
{ok, S} = file:open(Out, [write]),
io:format(S, "~p.~n",[Term]),
file:close(S).
- 使用错误日志记录器 :可以创建一个配置文件,将错误消息记录到文本文件中。
%% elog5.config
%% text erorr log
[ {kernel,
[{error_logger,
{file, "/home/joe/error_logs/debug.log"}}]}].
启动Erlang时使用配置文件:
erl -config elog5.config
- 调试器 :标准的Erlang发行版包含一个调试器。启动调试器的步骤如下:
1> %% recompile lib_misc so we can debug it
1>
c(lib_misc, [debug_info]).
{ok, lib_misc}
2> im().
%% A window will pop up. Ignore it for now
<0.42.0>
3> ii(lib_misc).
{module,lib_misc}
4> iaa([init]).
true.
5> lib_misc:
启动调试器后,可以检查变量、单步执行代码、设置断点等。由于经常需要调试多个进程,调试器还可以生成自身的副本,为每个调试的进程提供一个调试窗口。
Erlang代码分析与调试全攻略(续)
2. 调试(续)
2.4 调试方法总结
为了更清晰地展示不同调试方法的特点和适用场景,我们可以用表格进行总结:
| 调试方法 | 特点 | 适用场景 |
| ---- | ---- | ---- |
| io:format调试 | 简单直接,通过打印变量值来跟踪程序执行 | 小型程序或快速定位问题 |
| 数据结构转储到文件 | 适用于处理大型数据结构,方便后续分析 | 数据量较大的程序 |
| 使用错误日志记录器 | 集中记录错误信息,便于长期监控和分析 | 复杂系统或需要持续运行的程序 |
| 调试器 | 功能强大,可进行单步执行、设置断点等操作 | 深入调试复杂问题 |
下面是一个简单的mermaid流程图,展示了选择调试方法的一般流程:
graph TD;
A[发现问题] --> B{问题类型};
B -->|小型问题或快速定位| C[io:format调试];
B -->|大型数据结构| D[数据结构转储到文件];
B -->|复杂系统或长期监控| E[使用错误日志记录器];
B -->|深入调试复杂问题| F[调试器];
3. 综合应用示例
假设我们正在开发一个简单的音乐播放程序 shout ,在开发过程中遇到了性能和逻辑问题,我们可以综合运用上述分析和调试工具来解决问题。
3.1 性能分析
首先,我们使用 cprof 来分析程序的性能,找出调用频繁的函数:
1> cprof:start().
%% start the profiler
4501
2> shout:start().
%% run the application
<0.35.0>
3> cprof:pause().
%% pause the profiler
4844
4> cprof:analyse(shout). %% analyse function calls
{shout,232,
[{{shout,split,2},73},
{{shout,write_data,4},33},
{{shout,the_header,1},33},
{{shout,send_file,6},33},
{{shout,bump,1},32},
{{shout,make_header1,1},5},
{{shout,'-got_request_from_client/3-fun-0-',1},4},
{{shout,songs_loop,1},2},
{{shout,par_connect,2},2},
{{shout,unpack_song_descriptor,1},1},
...
5> cprof:stop().
%% stop the profiler
4865
从 cprof 的输出中,我们可以看到 shout:split/2 函数的调用次数较多,可能是性能瓶颈所在,我们可以对该函数进行优化。
3.2 覆盖率分析
接着,我们使用覆盖率分析来找出未执行的代码:
1> cover:start().
%% start the coverage analyser
{ok,<0.34.0>}
2> cover:compile(shout).
%% compile shout.erl for coverage
{ok,shout}
3> shout:start().
%% run the program
<0.41.0>
Playing:<<"title: track018 performer: .. ">>
4> %% let the program run for a bit
4>
cover:analyse_to_file(shout).
%% analyse the results
{ok,"shout.COVER.out"}
%% this is the results file
查看 shout.COVER.out 文件,标记为零的行表示未执行的代码,我们可以设计测试用例来覆盖这些代码,确保程序的正确性。
3.3 交叉引用检查
在开发过程中,我们还可以使用 xref 进行交叉引用检查,确保代码中没有未定义或已弃用的函数:
$ cd /path/to/shout_project
$ rm *.beam
$ erlc +debug_info *.erl
$ erl
1> xref:d('.')
根据 xref 的输出,我们可以及时发现并修复代码中的问题。
3.4 调试逻辑问题
如果程序出现逻辑错误,我们可以使用 io:format 调试或调试器来定位问题。例如,在 shout 程序的接收循环中添加 io:format 语句:
loop(...) ->
receive
Any ->
io:format("*** warning unexpected message:~p~n",[Any])
loop(...)
end
如果问题比较复杂,我们可以使用调试器进行深入调试:
1> %% recompile shout so we can debug it
1>
c(shout, [debug_info]).
{ok, shout}
2> im().
%% A window will pop up. Ignore it for now
<0.42.0>
3> ii(shout).
{module,shout}
4> iaa([init]).
true.
5> shout:
4. 总结
通过本文的介绍,我们了解了Erlang中各种分析和调试工具的使用方法,包括覆盖率分析、性能分析、交叉引用检查和调试技术。在实际开发中,我们可以根据具体问题选择合适的工具和方法,综合运用这些工具可以帮助我们更高效地开发和维护高质量的Erlang程序。
为了方便大家记忆和参考,我们将主要工具和方法总结如下:
| 工具/方法 | 功能 | 使用步骤 |
| ---- | ---- | ---- |
| 覆盖率分析 | 找出未执行的代码 | 1. cover:start() ; 2. cover:compile(Module) ; 3. 运行程序; 4. cover:analyse_to_file(Module) |
| 性能分析(cprof) | 统计函数调用次数 | 1. cprof:start() ; 2. 运行程序; 3. cprof:pause() ; 4. cprof:analyse(Module) ; 5. cprof:stop() |
| 交叉引用检查(xref) | 检查未定义或已弃用的函数 | 1. 编译代码时使用 +debug_info 标志; 2. xref:d('.') |
| io:format调试 | 打印变量值 | 在关键位置添加 io:format(...) 语句 |
| 数据结构转储到文件 | 处理大型数据结构 | 调用 dump(File, Term) 函数 |
| 使用错误日志记录器 | 记录错误信息 | 1. 创建配置文件; 2. erl -config config_file |
| 调试器 | 深入调试复杂问题 | 1. 重新编译代码并添加 debug_info 标志; 2. 执行一系列启动命令 |
希望这些工具和方法能帮助你在Erlang开发中更加得心应手,快速解决遇到的问题。
超级会员免费看
18

被折叠的 条评论
为什么被折叠?



