changed README.md
 
@@ -27,6 +27,9 @@ all stable changes of the first version of Recon.
27
27
28
28
*2.x*
29
29
30
+ - 2.5.0
31
+ - Optional formatting of records in traces (thanks to @bartekgorny)
32
+ - Basic support for OTP-22 in `recon_alloc` (base handling of `foreign_blocks` type)
30
33
- 2.4.0
31
34
- Optional formatting of records in traces (thanks to @bartekgorny)
32
35
- 2.3.6
changed hex_metadata.config
 
@@ -1,17 +1,16 @@
1
- {<<"name">>,<<"recon">>}.
2
- {<<"version">>,<<"2.4.0">>}.
3
- {<<"requirements">>,#{}}.
4
1
{<<"app">>,<<"recon">>}.
5
- {<<"maintainers">>,[<<"Fred Hebert">>]}.
6
- {<<"precompiled">>,false}.
2
+ {<<"build_tools">>,[<<"mix">>,<<"rebar3">>]}.
7
3
{<<"description">>,<<"Diagnostic tools for production use">>}.
8
4
{<<"files">>,
9
- [<<"src/recon.app.src">>,<<"LICENSE">>,<<"README.md">>,<<"rebar.lock">>,
10
- <<"script/app_deps.erl">>,<<"script/erl_crashdump_analyzer.sh">>,
11
- <<"script/queue_fun.awk">>,<<"src/recon.erl">>,<<"src/recon_alloc.erl">>,
12
- <<"src/recon_lib.erl">>,<<"src/recon_rec.erl">>,<<"src/recon_trace.erl">>]}.
5
+ [<<"src/recon.app.src">>,<<"LICENSE">>,<<"README.md">>,<<"mix.exs">>,
6
+ <<"rebar.lock">>,<<"script/app_deps.erl">>,
7
+ <<"script/erl_crashdump_analyzer.sh">>,<<"script/queue_fun.awk">>,
8
+ <<"src/recon.erl">>,<<"src/recon_alloc.erl">>,<<"src/recon_lib.erl">>,
9
+ <<"src/recon_map.erl">>,<<"src/recon_rec.erl">>,<<"src/recon_trace.erl">>]}.
13
10
{<<"licenses">>,[<<"BSD">>]}.
14
11
{<<"links">>,
15
- [{<<"Github">>,<<"https://github.com/ferd/recon/">>},
16
- {<<"Documentation">>,<<"http://ferd.github.io/recon/">>}]}.
17
- {<<"build_tools">>,[<<"rebar3">>]}.
12
+ [{<<"Documentation">>,<<"http://ferd.github.io/recon/">>},
13
+ {<<"Github">>,<<"https://github.com/ferd/recon/">>}]}.
14
+ {<<"name">>,<<"recon">>}.
15
+ {<<"requirements">>,[]}.
16
+ {<<"version">>,<<"2.5.0">>}.
added mix.exs
 
@@ -0,0 +1,13 @@
1
+ defmodule Recon.MixProject do
2
+ use Mix.Project
3
+
4
+ def project do
5
+ [
6
+ app: :recon,
7
+ description: "Diagnostic tools for production use",
8
+ version: "2.5.0",
9
+ language: :erlang,
10
+ deps: []
11
+ ]
12
+ end
13
+ end
changed src/recon.app.src
 
@@ -1,11 +1,12 @@
1
1
{application,recon,
2
2
[{description,"Diagnostic tools for production use"},
3
- {vsn,"2.4.0"},
3
+ {vsn,"2.5.0"},
4
4
{modules,[recon,recon_alloc,recon_lib,recon_trace,recon_rec]},
5
5
{registered,[]},
6
6
{applications,[kernel,stdlib]},
7
- {maintainers,["Fred Hebert"]},
8
7
{licenses,["BSD"]},
9
8
{links,[{"Github","https://github.com/ferd/recon/"},
10
9
{"Documentation","http://ferd.github.io/recon/"}]},
11
- {files,["src/","script/","rebar.lock","README.md","LICENSE"]}]}.
10
+ {build_tools,["mix","rebar3"]},
11
+ {files,["src/","script/","rebar.lock","mix.exs","README.md",
12
+ "LICENSE"]}]}.
changed src/recon_alloc.erl
 
@@ -420,6 +420,9 @@ merge_values([{Key,Vs}|T1], [{Key,OVs}|T2]) when Key =:= calls;
420
420
%% value is very rarely important so leave it
421
421
%% like this for now.
422
422
{K, lists:max([V1,V2])};
423
+ ({{K,V1}, {K,V2}}) when K =:= foreign_blocks ->
424
+ %% foreign blocks are just merged as a bigger list.
425
+ {K, V1++V2};
423
426
({{K,V1}, {K,V2}}) ->
424
427
{K, V1 + V2};
425
428
({{K,C1,L1,M1}, {K,C2,L2,M2}}) ->
added src/recon_map.erl
 
@@ -0,0 +1,208 @@
1
+ %%%-------------------------------------------------------------------
2
+ %%% @author bartlomiej.gorny@erlang-solutions.com
3
+ %%% @doc
4
+ %%% This module handles formatting maps.
5
+ %% It allows for trimming output to selected fields, or to nothing at all. It also adds a label
6
+ %% to a printout.
7
+ %% To set up a limit for a map, you need to give recon a way to tell the map you want to
8
+ %% trim from all the other maps, so you have to provide something like a 'type definition'.
9
+ %% It can be either another map which is compared to the arg, or a fun.
10
+ %%% @end
11
+ %%%-------------------------------------------------------------------
12
+ -module(recon_map).
13
+ -author("bartlomiej.gorny@erlang-solutions.com").
14
+ %% API
15
+
16
+ -export([limit/3, list/0, is_active/0, clear/0, remove/1, rename/2]).
17
+ -export([process_map/1]).
18
+
19
+ -type map_label() :: atom().
20
+ -type pattern() :: map() | function().
21
+ -type limit() :: all | none | atom() | binary() | [any()].
22
+
23
+ %% @doc quickly check if we want to do any record formatting
24
+ -spec is_active() -> boolean().
25
+ is_active() ->
26
+ case whereis(recon_ets_maps) of
27
+ undefined -> false;
28
+ _ -> true
29
+ end.
30
+
31
+ %% @doc remove all imported definitions, destroy the table, clean up
32
+ clear() ->
33
+ maybe_kill(recon_ets_maps),
34
+ ok.
35
+
36
+ %% @doc Limit output to selected keys of a map (can be 'none', 'all', a key or a list of keys).
37
+ %% Pattern selects maps to process: a "pattern" is just a map, and if all key/value pairs of a pattern
38
+ %% are present in a map (in other words, the pattern is a subset), then we say the map matches
39
+ %% and we process it accordingly (apply the limit).
40
+ %%
41
+ %% Patterns are applied in alphabetical order, until a match is found.
42
+ %%
43
+ %% Instead of a pattern you can also provide a function which will take a map and return a boolean.
44
+ %% @end
45
+ -spec limit(map_label(), pattern(), limit()) -> ok | {error, any()}.
46
+ limit(Label, #{} = Pattern, Limit) when is_atom(Label) ->
47
+ store_pattern(Label, Pattern, Limit);
48
+ limit(Label, Pattern, Limit) when is_atom(Label), is_function(Pattern) ->
49
+ store_pattern(Label, Pattern, Limit).
50
+
51
+ %% @doc prints out all "known" map definitions and their limit settings.
52
+ %% Printout tells a map's name, the matching fields required, and the limit options.
53
+ %% @end
54
+ list() ->
55
+ ensure_table_exists(),
56
+ io:format("~nmap definitions and limits:~n"),
57
+ list(ets:tab2list(patterns_table_name())).
58
+
59
+ %% @doc remove a given map entry
60
+ -spec remove(map_label()) -> true.
61
+ remove(Label) ->
62
+ ensure_table_exists(),
63
+ ets:delete(patterns_table_name(), Label).
64
+
65
+ %% @doc rename a given map entry, which allows to to change priorities for
66
+ %% matching. The first argument is the current name, and the second
67
+ %% argument is the new name.
68
+ -spec rename(map_label(), map_label()) -> renamed | missing.
69
+ rename(Name, NewName) ->
70
+ ensure_table_exists(),
71
+ case ets:lookup(patterns_table_name(), Name) of
72
+ [{Name, Pattern, Limit}] ->
73
+ ets:insert(patterns_table_name(), {NewName, Pattern, Limit}),
74
+ ets:delete(patterns_table_name(), Name),
75
+ renamed;
76
+ [] ->
77
+ missing
78
+ end.
79
+
80
+ %% @doc prints out all "known" map filter definitions and their settings.
81
+ %% Printout tells the map's label, the matching patterns, and the limit options
82
+ %% @end
83
+ list([]) ->
84
+ io:format("~n"),
85
+ ok;
86
+ list([{Label, Pattern, Limit} | Rest]) ->
87
+ io:format("~p: ~p -> ~p~n", [Label, Pattern, Limit]),
88
+ list(Rest).
89
+
90
+ %% @private given a map, scans saved patterns for one that matches; if found, returns a label
91
+ %% and a map with limits applied; otherwise returns 'none' and original map.
92
+ %% Pattern can be:
93
+ %% <ul>
94
+ %% <li> a map - then each key in pattern is checked for equality with the map in question</li>
95
+ %% <li> a fun(map()) -> boolean()</li>
96
+ %% </ul>
97
+ -spec process_map(map()) -> map() | {atom(), map()}.
98
+ process_map(M) ->
99
+ process_map(M, ets:tab2list(patterns_table_name())).
100
+
101
+ process_map(M, []) ->
102
+ M;
103
+ process_map(M, [{Label, Pattern, Limit} | Rest]) ->
104
+ case map_matches(M, Pattern) of
105
+ true ->
106
+ {Label, apply_map_limits(Limit, M)};
107
+ false ->
108
+ process_map(M, Rest)
109
+ end.
110
+
111
+ map_matches(#{} = M, Pattern) when is_function(Pattern) ->
112
+ Pattern(M);
113
+ map_matches(_, []) ->
114
+ true;
115
+ map_matches(M, [{K, V} | Rest]) ->
116
+ case maps:is_key(K, M) of
117
+ true ->
118
+ case maps:get(K, M) of
119
+ V ->
120
+ map_matches(M, Rest);
121
+ _ ->
122
+ false
123
+ end;
124
+ false ->
125
+ false
126
+ end.
127
+
128
+ apply_map_limits(none, M) ->
129
+ M;
130
+ apply_map_limits(all, _) ->
131
+ #{};
132
+ apply_map_limits(Fields, M) ->
133
+ maps:with(Fields, M).
134
+
135
+ patterns_table_name() -> recon_map_patterns.
136
+
137
+ store_pattern(Label, Pattern, Limit) ->
138
+ ensure_table_exists(),
139
+ ets:insert(patterns_table_name(), {Label, prepare_pattern(Pattern), prepare_limit(Limit)}),
140
+ ok.
141
+
142
+ prepare_limit(all) -> all;
143
+ prepare_limit(none) -> none;
144
+ prepare_limit(Limit) when is_binary(Limit) -> [Limit];
145
+ prepare_limit(Limit) when is_atom(Limit) -> [Limit];
146
+ prepare_limit(Limit) when is_list(Limit) -> Limit.
147
+
148
+ prepare_pattern(Pattern) when is_function(Pattern) -> Pattern;
149
+ prepare_pattern(Pattern) when is_map(Pattern) -> maps:to_list(Pattern).
150
+
151
+
152
+ ensure_table_exists() ->
153
+ case ets:info(patterns_table_name()) of
154
+ undefined ->
155
+ case whereis(recon_ets_maps) of
156
+ undefined ->
157
+ Parent = self(),
158
+ Ref = make_ref(),
159
+ %% attach to the currently running session
160
+ {Pid, MonRef} = spawn_monitor(fun() ->
161
+ register(recon_ets_maps, self()),
162
+ ets:new(patterns_table_name(), [ordered_set, public, named_table]),
163
+ Parent ! Ref,
164
+ ets_keeper()
165
+ end),
166
+ receive
167
+ Ref ->
168
+ erlang:demonitor(MonRef, [flush]),
169
+ Pid;
170
+ {'DOWN', MonRef, _, _, Reason} ->
171
+ error(Reason)
172
+ end;
173
+ Pid ->
174
+ Pid
175
+ end;
176
+ Pid ->
177
+ Pid
178
+ end.
179
+
180
+ ets_keeper() ->
181
+ receive
182
+ stop -> ok;
183
+ _ -> ets_keeper()
184
+ end.
185
+
186
+ %%%%%%%%%%%%%%%
187
+ %%% HELPERS %%%
188
+ %%%%%%%%%%%%%%%
189
+
190
+ maybe_kill(Name) ->
191
+ case whereis(Name) of
192
+ undefined ->
193
+ ok;
194
+ Pid ->
195
+ unlink(Pid),
196
+ exit(Pid, kill),
197
+ wait_for_death(Pid, Name)
198
+ end.
199
+
200
+ wait_for_death(Pid, Name) ->
201
+ case is_process_alive(Pid) orelse whereis(Name) =:= Pid of
202
+ true ->
203
+ timer:sleep(10),
204
+ wait_for_death(Pid, Name);
205
+ false ->
206
+ ok
207
+ end.
208
+
changed src/recon_rec.erl
 
@@ -15,7 +15,8 @@
15
15
%% API
16
16
17
17
-export([is_active/0]).
18
- -export([import/1, format_tuple/1, clear/1, clear/0, list/0, get_list/0, limit/3]).
18
+ -export([import/1, clear/1, clear/0, list/0, get_list/0, limit/3]).
19
+ -export([format_tuple/1]).
19
20
20
21
-ifdef(TEST).
21
22
-export([lookup_record/2]).
 
@@ -54,7 +55,7 @@ is_active() ->
54
55
55
56
%% @doc remove definitions imported from a module.
56
57
clear(Module) ->
57
- lists:map(fun(R) -> rem_for_module(R, Module) end, ets:tab2list(ets_table_name())).
58
+ lists:map(fun(R) -> rem_for_module(R, Module) end, ets:tab2list(records_table_name())).
58
59
59
60
%% @doc remove all imported definitions, destroy the table, clean up
60
61
clear() ->
 
@@ -62,7 +63,7 @@ clear() ->
62
63
ok.
63
64
64
65
%% @doc prints out all "known" (imported) record definitions and their limit settings.
65
- %% Print out tells module a record originates from, its name and a list of field names,
66
+ %% Printout tells module a record originates from, its name and a list of field names,
66
67
%% plus the record's arity (may be handy if handling big records) and a list of field it
67
68
%% limits its output to, if set.
68
69
%% @end
 
@@ -80,20 +81,20 @@ list() ->
80
81
-spec get_list() -> [listentry()].
81
82
get_list() ->
82
83
ensure_table_exists(),
83
- Lst = lists:map(fun make_list_entry/1, ets:tab2list(ets_table_name())),
84
+ Lst = lists:map(fun make_list_entry/1, ets:tab2list(records_table_name())),
84
85
lists:sort(Lst).
85
86
86
87
%% @doc Limit output to selected fields of a record (can be 'none', 'all', a field or a list of fields).
87
88
%% Limit set to 'none' means there is no limit, and all fields are displayed; limit 'all' means that
88
89
%% all fields are squashed and only record name will be shown.
89
90
%% @end
90
- -spec limit(record_name(), arity(), limit()) -> ok | {error, record_unknown}.
91
- limit(Name, Arity, Limit) ->
91
+ -spec limit(record_name(), arity(), limit()) -> ok | {error, any()}.
92
+ limit(Name, Arity, Limit) when is_atom(Name), is_integer(Arity) ->
92
93
case lookup_record(Name, Arity) of
93
94
[] ->
94
95
{error, record_unknown};
95
96
[{Key, Fields, Mod, _}] ->
96
- ets:insert(ets_table_name(), {Key, Fields, Mod, Limit}),
97
+ ets:insert(records_table_name(), {Key, Fields, Mod, Limit}),
97
98
ok
98
99
end.
99
100
 
@@ -127,10 +128,10 @@ store_record(Rec, Module, ResultList) ->
127
128
Arity = length(Fields),
128
129
Result = case lookup_record(Name, Arity) of
129
130
[] ->
130
- ets:insert(ets_table_name(), rec_info(Rec, Module)),
131
+ ets:insert(records_table_name(), rec_info(Rec, Module)),
131
132
{imported, Module, Name, Arity};
132
133
[{_, _, Module, _}] ->
133
- ets:insert(ets_table_name(), rec_info(Rec, Module)),
134
+ ets:insert(records_table_name(), rec_info(Rec, Module)),
134
135
{overwritten, Module, Name, Arity};
135
136
[{_, _, Mod, _}] ->
136
137
{ignored, Module, Name, Arity, Mod}
 
@@ -148,10 +149,11 @@ get_record(_, Acc) -> Acc.
148
149
%% @private
149
150
lookup_record(RecName, FieldCount) ->
150
151
ensure_table_exists(),
151
- ets:lookup(ets_table_name(), {RecName, FieldCount}).
152
+ ets:lookup(records_table_name(), {RecName, FieldCount}).
152
153
154
+ %% @private
153
155
ensure_table_exists() ->
154
- case ets:info(ets_table_name()) of
156
+ case ets:info(records_table_name()) of
155
157
undefined ->
156
158
case whereis(recon_ets) of
157
159
undefined ->
 
@@ -160,7 +162,7 @@ ensure_table_exists() ->
160
162
%% attach to the currently running session
161
163
{Pid, MonRef} = spawn_monitor(fun() ->
162
164
register(recon_ets, self()),
163
- ets:new(ets_table_name(), [set, public, named_table]),
165
+ ets:new(records_table_name(), [set, public, named_table]),
164
166
Parent ! Ref,
165
167
ets_keeper()
166
168
end),
 
@@ -178,13 +180,13 @@ ensure_table_exists() ->
178
180
Pid
179
181
end.
180
182
181
- ets_table_name() -> recon_record_definitions.
183
+ records_table_name() -> recon_record_definitions.
182
184
183
185
rec_info({Name, Fields}, Module) ->
184
186
{{Name, length(Fields)}, field_names(Fields), Module, none}.
185
187
186
188
rem_for_module({_, _, Module, _} = Rec, Module) ->
187
- ets:delete_object(ets_table_name(), Rec);
189
+ ets:delete_object(records_table_name(), Rec);
188
190
rem_for_module(_, _) ->
189
191
ok.
190
192
 
@@ -233,6 +235,7 @@ format_record(Rec, {{Name, Arity}, Fields, _, Limits}) ->
233
235
end.
234
236
235
237
format_kv(Key, Val) ->
238
+ %% Some messy mutually recursive calls we can't avoid
236
239
[recon_trace:format_trace_output(true, Key), "=", recon_trace:format_trace_output(true, Val)].
237
240
238
241
apply_limits(List, none) -> List;
changed src/recon_trace.erl
 
@@ -185,7 +185,7 @@
185
185
-export([format/1]).
186
186
187
187
%% Internal exports
188
- -export([count_tracer/1, rate_tracer/2, formatter/5, format_trace_output/2]).
188
+ -export([count_tracer/1, rate_tracer/2, formatter/5, format_trace_output/1, format_trace_output/2]).
189
189
190
190
-type matchspec() :: [{[term()], [term()], [term()]}].
191
191
-type shellfun() :: fun((_) -> term()).
 
@@ -607,36 +607,76 @@ to_hms(_) ->
607
607
format_args(Arity) when is_integer(Arity) ->
608
608
[$/, integer_to_list(Arity)];
609
609
format_args(Args) when is_list(Args) ->
610
- Active = recon_rec:is_active(),
611
- [$(, join(", ", [format_trace_output(Active, Arg) || Arg <- Args]), $)].
610
+ [$(, join(", ", [format_trace_output(Arg) || Arg <- Args]), $)].
612
611
613
612
614
613
%% @doc formats call arguments and return values - most types are just printed out, except for
615
614
%% tuples recognised as records, which mimic the source code syntax
616
615
%% @end
617
616
format_trace_output(Args) ->
618
- format_trace_output(recon_rec:is_active(), Args).
617
+ format_trace_output(recon_rec:is_active(), recon_map:is_active(), Args).
619
618
620
- format_trace_output(true, Args) when is_tuple(Args) ->
619
+ format_trace_output(Recs, Args) ->
620
+ format_trace_output(Recs, recon_map:is_active(), Args).
621
+
622
+ format_trace_output(true, _, Args) when is_tuple(Args) ->
621
623
recon_rec:format_tuple(Args);
622
- format_trace_output(true, Args) when is_list(Args) ->
624
+ format_trace_output(false, true, Args) when is_tuple(Args) ->
625
+ format_tuple(false, true, Args);
626
+ format_trace_output(Recs, Maps, Args) when is_list(Args), Recs orelse Maps ->
623
627
case io_lib:printable_list(Args) of
624
628
true ->
625
629
io_lib:format("~p", [Args]);
626
630
false ->
627
- L = lists:map(fun(A) -> format_trace_output(true, A) end, Args),
628
- [$[, join(", ", L), $]]
631
+ format_maybe_improper_list(Recs, Maps, Args)
629
632
end;
630
- format_trace_output(true, Args) when is_map(Args) ->
633
+ format_trace_output(Recs, true, Args) when is_map(Args) ->
634
+ {Label, Map} = case recon_map:process_map(Args) of
635
+ {L, M} -> {atom_to_list(L), M};
636
+ M -> {"", M}
637
+ end,
638
+ ItemList = maps:to_list(Map),
639
+ [Label,
640
+ "#{",
641
+ join(", ", [format_kv(Recs, true, Key, Val) || {Key, Val} <- ItemList]),
642
+ "}"];
643
+ format_trace_output(Recs, false, Args) when is_map(Args) ->
631
644
ItemList = maps:to_list(Args),
632
645
["#{",
633
- join(", ", [format_kv(Key, Val) || {Key, Val} <- ItemList]),
646
+ join(", ", [format_kv(Recs, false, Key, Val) || {Key, Val} <- ItemList]),
634
647
"}"];
635
- format_trace_output(_, Args) ->
648
+ format_trace_output(_, _, Args) ->
636
649
io_lib:format("~p", [Args]).
637
650
638
- format_kv(Key, Val) ->
639
- [format_trace_output(true, Key), "=", format_trace_output(true, Val)].
651
+ format_kv(Recs, Maps, Key, Val) ->
652
+ [format_trace_output(Recs, Maps, Key), "=>", format_trace_output(Recs, Maps, Val)].
653
+
654
+
655
+ format_tuple(Recs, Maps, Tup) ->
656
+ [${ | format_tuple_(Recs, Maps, tuple_to_list(Tup))].
657
+
658
+ format_tuple_(_Recs, _Maps, []) ->
659
+ "}";
660
+ format_tuple_(Recs, Maps, [H|T]) ->
661
+ [format_trace_output(Recs, Maps, H), $,,
662
+ format_tuple_(Recs, Maps, T)].
663
+
664
+
665
+ format_maybe_improper_list(Recs, Maps, List) ->
666
+ [$[ | format_maybe_improper_list_(Recs, Maps, List)].
667
+
668
+ format_maybe_improper_list_(_, _, []) ->
669
+ "]";
670
+ format_maybe_improper_list_(Recs, Maps, [H|[]]) ->
671
+ [format_trace_output(Recs, Maps, H), $]];
672
+ format_maybe_improper_list_(Recs, Maps, [H|T]) when is_list(T) ->
673
+ [format_trace_output(Recs, Maps, H), $,,
674
+ format_maybe_improper_list_(Recs, Maps, T)];
675
+ format_maybe_improper_list_(Recs, Maps, [H|T]) when not is_list(T) ->
676
+ %% Handling improper lists
677
+ [format_trace_output(Recs, Maps, H), $|,
678
+ format_trace_output(Recs, Maps, T), $]].
679
+
640
680
641
681
%%%%%%%%%%%%%%%
642
682
%%% HELPERS %%%
 
@@ -678,6 +718,7 @@ fun_to_ms(ShellFun) when is_function(ShellFun) ->
678
718
exit(shell_funs_only)
679
719
end.
680
720
721
+
681
722
-ifdef(OTP_RELEASE).
682
723
-spec join(term(), [term()]) -> [term()].
683
724
join(Sep, List) ->