@ -0,0 +1,152 @@ | |||
recon | |||
===== | |||
Recon wants to be a set of tools usable in production to diagnose Erlang problems or inspect production environment safely. | |||
To build the library: | |||
rebar3 compile | |||
Documentation for the library can be obtained at http://ferd.github.io/recon/ | |||
It is recommended that you use tags if you do not want bleeding edge and development content for this library. | |||
Current Status | |||
-------------- | |||
[](https://travis-ci.org/ferd/recon) | |||
Versions supported: OTP-17 and up. Support of R16B03-1 down to R15B02 is best effort. Builds with Rebar3 require OTP-17.1 and up because that's what the tool supports. | |||
Changelog | |||
--------- | |||
Branches are organized by version. `master` contains the bleeding edge, `2.x` | |||
contains all stable changes up to the latest release of v2, and `1.x` contains | |||
all stable changes of the first version of Recon. | |||
*2.x* | |||
- 2.5.1 | |||
- Fix support for extra messages in traces (thanks to Péter Gömöri) | |||
- Fix some typespecs for match specs (thanks to @chenduo) | |||
- Support OTP-23 change of format in allocator blocks related to carrier migration support | |||
- 2.5.0 | |||
- Optional formatting of records in traces (thanks to @bartekgorny) | |||
- Basic support for OTP-22 in `recon_alloc` (base handling of `foreign_blocks` type) | |||
- 2.4.0 | |||
- Optional formatting of records in traces (thanks to @bartekgorny) | |||
- 2.3.6 | |||
- Adapting for OTP-21. Includes the 'deprecation' of `recon:files/0` | |||
since OTP-21 no longer supports listing all file descriptors, and | |||
removing `error_logger_queue_len` from node stats since a new | |||
logging mechanism was introduced in-process instead. | |||
- 2.3.5 | |||
- fixing timefold's first iteration to prevent errors at call-site | |||
by sleeping before sampling | |||
- 2.3.4 | |||
- fixing edoc tag that broke some downstream packaging attempts | |||
- 2.3.3 | |||
- fixing `bin_leak` arith errors | |||
- fixes to `recon_alloc:allocators/1` (incl. R16 compatibility) | |||
- fix errors in scheduler wall time calculations | |||
- `term_to_pid` supports binaries | |||
- 2.3.2 | |||
- Allow the `return_to` option in `recon_trace` | |||
- More efficient sorting function for procs and ports attributes | |||
(thanks to @zhongwencool and @pichi) | |||
- Allow the usage of `return_trace` in `recon_trace:calls/2-3` instead | |||
of `fun(_) -> return_trace() end`. | |||
- 2.3.1 | |||
- Updated `app_deps` script to run with rebar3 dependencies | |||
- Minor docs update | |||
- 2.3.0 | |||
- Doc made clearer around semantics of `recon:proc_count` and | |||
`recon:proc_window`. | |||
- Fix doc typos | |||
- Fix potential race condition on waiting for death of tracing process | |||
- Add an option which allows sending tracing output somewhere other than | |||
group_leader() (thanks @djnym) | |||
- Add ability to pass custom formatter function when tracing (thanks @iilyak) | |||
- 2.2.1 | |||
- Fixing type specs for `recon:port_types/0` and `recon_lib:count/1`, | |||
thanks to @lucafavatella | |||
- Minor documentation fixes. | |||
- 2.2.0: | |||
- Adding scheduler info metrics to get a more accurate picture than what | |||
top and CPU gives. | |||
- Broadening `recon_trace:calls/2` interface to allow multiple match specs, | |||
which was currently only allowed for `calls/3`. | |||
- Support for `mbcs_pool` data in `erts_alloc`, and some internal refactoring, | |||
thanks to Lukas Larsson. | |||
- 2.1.2: | |||
- Fixing tests for R15B02 and up | |||
- Fixing a backwards compatibility for R15B03 on `recon_alloc` operations | |||
with dumps on disk | |||
- 2.1.1: | |||
- Renaming `recon_trace:mfa()` type to `recon_trace:tspec()` to avoid | |||
issues in older Erlang versions regarding redefining an existing type | |||
(Thanks Roberto Aloi) | |||
- 2.1.0: | |||
- Adding `recon_trace` script to allow safe tracing of function calls | |||
on production nodes. | |||
- Adding `queue_fun.awk` script to inspect running functions of processes | |||
with large mailboxes in a crash dump. | |||
- 2.0.2: | |||
- Preventing crashes in `recon_alloc` when certain expected allocators | |||
do not return results (Thanks to Michal Ptaszek) | |||
- 2.0.1: | |||
- Add support for R16B03 in `recon_alloc`. | |||
- 2.0.0: | |||
- Test suite added | |||
- Major rewrite of `recon_alloc`, thanks to Lukas Larsson. Things that changed include: | |||
- `average_sizes/0` is renamed `average_block_sizes/1` and now takes | |||
the keywords `current` and `max`. | |||
- Documentation updates. | |||
- `memory/1` has new options in `allocated_types` and `allocated_instances`. | |||
- `memory/2` has been added, which allows to choose between `current` and | |||
`max` values. `memory(Term)` is made equivalent to `memory(Term, current)`. | |||
- Allow `sbcs_to_mbcs/0` to take the arguments `current` and `max`. | |||
- Added unit conversion function `set_unit/1`, which allows to get the | |||
`recon_alloc` results in bytes (default), kilobytes, megabytes, and | |||
gigabytes, to help with readability. | |||
- Updated the internal rebar version, if anybody were to use it. | |||
- `recon:port_info/1` no longer includes the `parallelism` option by default | |||
within the `meta` category as this would hurt backwards compatibility on | |||
some installations. | |||
- `recon:get_state/2` is added in order to specify timeouts. | |||
`recon:get_state/1` keeps its 5000 milliseconds timeout. | |||
- Addition of a fake attribute called `binary_memory`, which is callable in | |||
`recon:info/2,4`, `recon:proc_count/2`, and `recon:proc_window/3`. This | |||
attribute allows to fetch the amount of memory used by refc binaries for | |||
a process, and to sort by that value for counts and windows. | |||
*1.x* | |||
- 1.2.0: | |||
- add `recon_alloc:snapshot*` functions, which allow memory allocation | |||
snapshots to be taken, saved on disk, reloaded, and analyzed on-demand. | |||
Thanks to Lukas Larsson for this functionality. | |||
- remove `parallelism` data from `port_info` for better OTP backwards | |||
compatibility with little loss of information. | |||
- 1.1.0: | |||
- add `recon_lib:term_to_port` to convert a string back to a | |||
usable port. | |||
- add `recon:port_info/1` and `recon:port_info/2` | |||
- add `recon_alloc` module | |||
- 1.0.0: add `info/2` and `info/4`. The `memory` info type thus gets renamed | |||
to `memory_used`, in order to avoid conflicts when picking between a type | |||
and a specific process attribute. Types exported by the module also get | |||
updated. | |||
- 0.4.2: extended `app_deps.erl` to read apps/ directories for releases | |||
- 0.4.1: fixed bug where nodes with lots of processes could see the GC call | |||
fail if said processes failed between long calls within the `bin_leak` | |||
function call. | |||
- 0.4.0: fixed bug where nodes with lots of processes or ports could see their | |||
count or window functions fail because a process or socket closed between the | |||
time the function started and before it finished. This ends up changing the | |||
API in `recon_lib` for the window and count functions that take a specific | |||
pid as an argument. | |||
- 0.3.1: factored out some logic from `recon:info/1` into `recon_lib:term_to_pid` | |||
and allowed arbitrary terms to be used for pids in `recon:get_state/1`. |
@ -0,0 +1,71 @@ | |||
#!/usr/bin/env escript | |||
%% -*- erlang -*- | |||
%%% Run with 'escript app_deps.erl' | |||
%%% Change the path in filelib:wildcard/1 as required to capture | |||
%%% all your dependencies. | |||
%%% | |||
%%% Rectangular nodes will represent library apps (no processes | |||
%%% involved) and the circular nodes will represent regular apps. | |||
%%% An arrow going from 'A -> B' means 'A depends on B'. | |||
%%% | |||
%%% This script depends on graphviz being present on the system. | |||
-module(app_deps). | |||
-export([main/1]). | |||
main(_) -> | |||
AppFiles = filelib:wildcard("deps/*/ebin/*.app") | |||
++ | |||
filelib:wildcard("apps/*/ebin/*.app") | |||
++ | |||
filelib:wildcard("ebin/*.app") | |||
++ | |||
filelib:wildcard("_build/default/lib/*/ebin/*.app"), | |||
to_graphviz(read_deps(AppFiles)). | |||
read_deps(AppFiles) -> | |||
[{App, | |||
proplists:get_value(applications, Props, []), | |||
apptype(Props)} | |||
|| {ok, [{_,App,Props}]} <- | |||
[file:consult(AppFile) || AppFile <- AppFiles]]. | |||
apptype(Props) -> | |||
case proplists:get_value(mod, Props) of | |||
undefined -> library; | |||
_ -> regular | |||
end. | |||
to_graphviz(Deps) -> | |||
AllApps = lists:usort(lists:flatten( | |||
[[{App,Type},DepList] || {App,DepList,Type} <- Deps] | |||
)), | |||
Bytes = ["digraph G { ", | |||
"K=0.25; ratio=0.75; overlap=\"9:prism\"; ", | |||
[io_lib:format("~p [shape=box] ", [App]) | |||
|| App <- libapps(AllApps -- [kernel,stdlib])], | |||
[[io_lib:format("~p->~p ", [App,Dep]) | |||
|| Dep <- DepList -- [kernel, stdlib]] | |||
|| {App, DepList, _} <- Deps], | |||
"}"], | |||
file:write_file("app-deps.dot", Bytes), | |||
os:cmd("dot app-deps.dot -Tpng -o app-deps.png"). | |||
libapps([]) -> []; | |||
libapps([{App,library}|Apps]) -> [App|libapps(Apps)]; | |||
libapps([{_,_}|Apps]) -> libapps(Apps); | |||
libapps([App|Apps]) -> | |||
Dir = case code:lib_dir(App) of | |||
{error, _} -> ""; % not an OTP app | |||
DirPath -> DirPath | |||
end, | |||
Path = filename:join([Dir, "ebin", atom_to_list(App)++".app"]), | |||
case lists:prefix(code:lib_dir(), Path) of | |||
false -> | |||
[App|libapps(Apps)]; % not OTP app, we don't care | |||
true -> % deps of OTP deps: we don't care either. | |||
{ok, [{_,App,Props}]} = file:consult(Path), | |||
case apptype(Props) of | |||
library -> [App | libapps(Apps)]; | |||
regular -> libapps(Apps) | |||
end | |||
end. |
@ -0,0 +1,69 @@ | |||
#!/usr/bin/env bash | |||
DUMP=$1 | |||
echo -e "analyzing $DUMP, generated on: " `head -2 $DUMP | tail -1` "\n" | |||
### SLOGAN ### | |||
grep Slogan: $DUMP -m 1 | |||
### MEMORY ### | |||
echo -e "\nMemory:\n===" | |||
M=`grep -m 1 'processes' $DUMP | sed "s/processes: //"` | |||
let "m=$M/(1024*1024)" | |||
echo " processes: $m Mb" | |||
M=`grep -m 1 'processes_used' $DUMP | sed "s/processes_used: //"` | |||
let "m=$M/(1024*1024)" | |||
echo " processes_used: $m Mb" | |||
M=`grep -m 1 'system' $DUMP | sed "s/system: //"` | |||
let "m=$M/(1024*1024)" | |||
echo " system: $m Mb" | |||
M=`grep -m 1 'atom' $DUMP | sed "s/atom: //"` | |||
let "m=$M/(1024*1024)" | |||
echo " atom: $m Mb" | |||
M=`grep -m 1 'atom_used' $DUMP | sed "s/atom_used: //"` | |||
let "m=$M/(1024*1024)" | |||
echo " atom_used: $m Mb" | |||
M=`grep -m 1 'binary' $DUMP | sed "s/binary: //"` | |||
let "m=$M/(1024*1024)" | |||
echo " binary: $m Mb" | |||
M=`grep -m 1 'code' $DUMP | sed "s/code: //"` | |||
let "m=$M/(1024*1024)" | |||
echo " code: $m Mb" | |||
M=`grep -m 1 'ets' $DUMP | sed "s/ets: //"` | |||
let "m=$M/(1024*1024)" | |||
echo " ets: $m Mb" | |||
M=`grep -m 1 'total' $DUMP | sed "s/total: //"` | |||
let "m=$M/(1024*1024)" | |||
echo -e " ---\n total: $m Mb" | |||
### PROCESS MESSAGE QUEUES LENGTHS ### | |||
echo -e "\nDifferent message queue lengths (5 largest different):\n===" | |||
grep 'Message queue len' $DUMP | sed 's/Message queue length: //g' | sort -n -r | uniq -c | head -5 | |||
### ERROR LOGGER QUEUE LENGTH ### | |||
echo -e "\nError logger queue length:\n===" | |||
grep -C 10 'Name: error_logger' $DUMP -m 1| grep 'Message queue length' | sed 's/Message queue length: //g' | |||
### PORT/FILE DESCRIPTOR INFO ### | |||
echo -e "\nFile descriptors open:\n===" | |||
echo -e " UDP: " `grep 'Port controls linked-in driver:' $DUMP | grep 'udp_inet' | wc -l` | |||
echo -e " TCP: " `grep 'Port controls linked-in driver:' $DUMP | grep 'tcp_inet' | wc -l` | |||
echo -e " Files: " `grep 'Port controls linked-in driver:' $DUMP | grep -vi 'udp_inet' | grep -vi 'tcp_inet' | wc -l` | |||
echo -e " ---\n Total: " `grep 'Port controls linked-in driver:' $DUMP | wc -l` | |||
### NUMBER OF PROCESSES ### | |||
echo -e "\nNumber of processes:\n===" | |||
grep '=proc:' $DUMP | wc -l | |||
### PROC HEAPS+STACK ### | |||
echo -e "\nProcesses Heap+Stack memory sizes (words) used in the VM (5 largest different):\n===" | |||
grep 'Stack+heap' $DUMP | sed "s/Stack+heap: //g" | sort -n -r | uniq -c | head -5 | |||
### PROC OLDHEAP ### | |||
echo -e "\nProcesses OldHeap memory sizes (words) used in the VM (5 largest different):\n===" | |||
grep 'OldHeap' $DUMP | sed "s/OldHeap: //g" | sort -n -r | uniq -c | head -5 | |||
### PROC STATES ### | |||
echo -e "\nProcess States when crashing (sum): \n===" | |||
grep 'State: ' $DUMP | sed "s/State: //g" | sort | uniq -c |
@ -0,0 +1,36 @@ | |||
# Parse Erlang Crash Dumps and correlate mailbox size to the currently running | |||
# function. | |||
# | |||
# Once in the procs section of the dump, all processes are displayed with | |||
# =proc:<0.M.N> followed by a list of their attributes, which include the | |||
# message queue length and the program counter (what code is currently | |||
# executing). | |||
# | |||
# Run as: | |||
# | |||
# $ awk -v threshold=$THRESHOLD -f queue_fun.awk $CRASHDUMP | |||
# | |||
# Where $THRESHOLD is the smallest mailbox you want inspects. Default value | |||
# is 1000. | |||
BEGIN { | |||
if (threshold == "") { | |||
threshold = 1000 # default mailbox size | |||
} | |||
procs = 0 # are we in the =procs entries? | |||
print "MESSAGE QUEUE LENGTH: CURRENT FUNCTION" | |||
print "======================================" | |||
} | |||
# Only bother with the =proc: entries. Anything else is useless. | |||
procs == 0 && /^=proc/ { procs = 1 } # entering the =procs entries | |||
procs == 1 && /^=/ && !/^=proc/ { exit 0 } # we're done | |||
# Message queue length: 1210 | |||
# 1 2 3 4 | |||
/^Message queue length: / && $4 >= threshold { flag=1; ct=$4 } | |||
/^Message queue length: / && $4 < threshold { flag=0 } | |||
# Program counter: 0x00007f5fb8cb2238 (io:wait_io_mon_reply/2 + 56) | |||
# 1 2 3 4 5 6 | |||
flag == 1 && /^Program counter: / { print ct ":", substr($4,2) } |
@ -0,0 +1,713 @@ | |||
%%% @author Fred Hebert <mononcqc@ferd.ca> | |||
%%% [http://ferd.ca/] | |||
%%% @doc Recon, as a module, provides access to the high-level functionality | |||
%%% contained in the Recon application. | |||
%%% | |||
%%% It has functions in five main categories: | |||
%%% | |||
%%% <dl> | |||
%%% <dt>1. State information</dt> | |||
%%% <dd>Process information is everything that has to do with the | |||
%%% general state of the node. Functions such as {@link info/1} | |||
%%% and {@link info/3} are wrappers to provide more details than | |||
%%% `erlang:process_info/1', while providing it in a production-safe | |||
%%% manner. They have equivalents to `erlang:process_info/2' in | |||
%%% the functions {@link info/2} and {@link info/4}, respectively.</dd> | |||
%%% <dd>{@link proc_count/2} and {@link proc_window/3} are to be used | |||
%%% when you require information about processes in a larger sense: | |||
%%% biggest consumers of given process information (say memory or | |||
%%% reductions), either absolutely or over a sliding time window, | |||
%%% respectively.</dd> | |||
%%% <dd>{@link bin_leak/1} is a function that can be used to try and | |||
%%% see if your Erlang node is leaking refc binaries. See the function | |||
%%% itself for more details.</dd> | |||
%%% <dd>Functions to access node statistics, in a manner somewhat similar | |||
%%% to what <a href="https://github.com/ferd/vmstats">vmstats</a> | |||
%%% provides as a library. There are 3 of them: | |||
%%% {@link node_stats_print/2}, which displays them, | |||
%%% {@link node_stats_list/2}, which returns them in a list, and | |||
%%% {@link node_stats/4}, which provides a fold-like interface | |||
%%% for stats gathering. For CPU usage specifically, see | |||
%%% {@link scheduler_usage/1}.</dd> | |||
%%% | |||
%%% <dt>2. OTP tools</dt> | |||
%%% <dd>This category provides tools to interact with pieces of OTP | |||
%%% more easily. At this point, the only function included is | |||
%%% {@link get_state/1}, which works as a wrapper around | |||
%%% {@link get_state/2}, which works as a wrapper around | |||
%%% `sys:get_state/1' in R16B01, and provides the required | |||
%%% functionality for older versions of Erlang.</dd> | |||
%%% | |||
%%% <dt>3. Code Handling</dt> | |||
%%% <dd>Specific functions are in `recon' for the sole purpose | |||
%%% of interacting with source and compiled code. | |||
%%% {@link remote_load/1} and {@link remote_load/2} will allow | |||
%%% to take a local module, and load it remotely (in a diskless | |||
%%% manner) on another Erlang node you're connected to.</dd> | |||
%%% <dd>{@link source/1} allows to print the source of a loaded module, | |||
%%% in case it's not available in the currently running node.</dd> | |||
%%% | |||
%%% <dt>4. Ports and Sockets</dt> | |||
%%% <dd>To make it simpler to debug some network-related issues, | |||
%%% recon contains functions to deal with Erlang ports (raw, file | |||
%%% handles, or inet). Functions {@link tcp/0}, {@link udp/0}, | |||
%%% {@link sctp/0}, {@link files/0}, and {@link port_types/0} will | |||
%%% list all the Erlang ports of a given type. The latter function | |||
%%% prints counts of all individual types.</dd> | |||
%%% <dd>Port state information can be useful to figure out why certain | |||
%%% parts of the system misbehave. Functions such as | |||
%%% {@link port_info/1} and {@link port_info/2} are wrappers to provide | |||
%%% more similar or more details than `erlang:port_info/1-2', and, for | |||
%%% inet ports, statistics and options for each socket.</dd> | |||
%%% <dd>Finally, the functions {@link inet_count/2} and {@link inet_window/3} | |||
%%% provide the absolute or sliding window functionality of | |||
%%% {@link proc_count/2} and {@link proc_count/3} to inet ports | |||
%%% and connections currently on the node.</dd> | |||
%%% | |||
%%% <dt>5. RPC</dt> | |||
%%% <dd>These are wrappers to make RPC work simpler with clusters of | |||
%%% Erlang nodes. Default RPC mechanisms (from the `rpc' module) | |||
%%% make it somewhat painful to call shell-defined funs over node | |||
%%% boundaries. The functions {@link rpc/1}, {@link rpc/2}, and | |||
%%% {@link rpc/3} will do it with a simpler interface.</dd> | |||
%%% <dd>Additionally, when you're running diagnostic code on remote | |||
%%% nodes and want to know which node evaluated what result, using | |||
%%% {@link named_rpc/1}, {@link named_rpc/2}, and {@link named_rpc/3} | |||
%%% will wrap the results in a tuple that tells you which node it's | |||
%%% coming from, making it easier to identify bad nodes.</dd> | |||
%%% </dl> | |||
%%% @end | |||
-module(recon). | |||
-export([info/1, info/2, info/3, info/4, | |||
proc_count/2, proc_window/3, | |||
bin_leak/1, | |||
node_stats_print/2, node_stats_list/2, node_stats/4, | |||
scheduler_usage/1]). | |||
-export([get_state/1, get_state/2]). | |||
-export([remote_load/1, remote_load/2, | |||
source/1]). | |||
-export([tcp/0, udp/0, sctp/0, files/0, port_types/0, | |||
inet_count/2, inet_window/3, | |||
port_info/1, port_info/2]). | |||
-export([rpc/1, rpc/2, rpc/3, | |||
named_rpc/1, named_rpc/2, named_rpc/3]). | |||
%%%%%%%%%%%%% | |||
%%% TYPES %%% | |||
%%%%%%%%%%%%% | |||
-type proc_attrs() :: {pid(), | |||
Attr::_, | |||
[Name::atom() | |||
|{current_function, mfa()} | |||
|{initial_call, mfa()}, ...]}. | |||
-type inet_attrs() :: {port(), | |||
Attr::_, | |||
[{atom(), term()}]}. | |||
-type pid_term() :: pid() | atom() | string() | |||
| {global, term()} | {via, module(), term()} | |||
| {non_neg_integer(), non_neg_integer(), non_neg_integer()}. | |||
-type info_type() :: meta | signals | location | memory_used | work. | |||
-type info_meta_key() :: registered_name | dictionary | group_leader | status. | |||
-type info_signals_key() :: links | monitors | monitored_by | trap_exit. | |||
-type info_location_key() :: initial_call | current_stacktrace. | |||
-type info_memory_key() :: memory | message_queue_len | heap_size | |||
| total_heap_size | garbage_collection. | |||
-type info_work_key() :: reductions. | |||
-type info_key() :: info_meta_key() | info_signals_key() | info_location_key() | |||
| info_memory_key() | info_work_key(). | |||
-type port_term() :: port() | string() | atom() | pos_integer(). | |||
-type port_info_type() :: meta | signals | io | memory_used | specific. | |||
-type port_info_meta_key() :: registered_name | id | name | os_pid. | |||
-type port_info_signals_key() :: connected | links | monitors. | |||
-type port_info_io_key() :: input | output. | |||
-type port_info_memory_key() :: memory | queue_size. | |||
-type port_info_specific_key() :: atom(). | |||
-type port_info_key() :: port_info_meta_key() | port_info_signals_key() | |||
| port_info_io_key() | port_info_memory_key() | |||
| port_info_specific_key(). | |||
-export_type([proc_attrs/0, inet_attrs/0, pid_term/0, port_term/0]). | |||
-export_type([info_type/0, info_key/0, | |||
info_meta_key/0, info_signals_key/0, info_location_key/0, | |||
info_memory_key/0, info_work_key/0]). | |||
-export_type([port_info_type/0, port_info_key/0, | |||
port_info_meta_key/0, port_info_signals_key/0, port_info_io_key/0, | |||
port_info_memory_key/0, port_info_specific_key/0]). | |||
%%%%%%%%%%%%%%%%%% | |||
%%% PUBLIC API %%% | |||
%%%%%%%%%%%%%%%%%% | |||
%%% Process Info %%% | |||
%% @doc Equivalent to `info(<A.B.C>)' where `A', `B', and `C' are integers part | |||
%% of a pid | |||
-spec info(N,N,N) -> [{info_type(), [{info_key(),term()}]},...] when | |||
N :: non_neg_integer(). | |||
info(A,B,C) -> info(recon_lib:triple_to_pid(A,B,C)). | |||
%% @doc Equivalent to `info(<A.B.C>, Key)' where `A', `B', and `C' are integers part | |||
%% of a pid | |||
-spec info(N,N,N, Key) -> term() when | |||
N :: non_neg_integer(), | |||
Key :: info_type() | [atom()] | atom(). | |||
info(A,B,C, Key) -> info(recon_lib:triple_to_pid(A,B,C), Key). | |||
%% @doc Allows to be similar to `erlang:process_info/1', but excludes fields | |||
%% such as the mailbox, which have a tendency to grow and be unsafe when called | |||
%% in production systems. Also includes a few more fields than what is usually | |||
%% given (`monitors', `monitored_by', etc.), and separates the fields in a more | |||
%% readable format based on the type of information contained. | |||
%% | |||
%% Moreover, it will fetch and read information on local processes that were | |||
%% registered locally (an atom), globally (`{global, Name}'), or through | |||
%% another registry supported in the `{via, Module, Name}' syntax (must have a | |||
%% `Module:whereis_name/1' function). Pids can also be passed in as a string | |||
%% (`"<0.39.0>"') or a triple (`{0,39,0}') and will be converted to be used. | |||
-spec info(pid_term()) -> [{info_type(), [{info_key(), Value}]},...] when | |||
Value :: term(). | |||
info(PidTerm) -> | |||
Pid = recon_lib:term_to_pid(PidTerm), | |||
[info(Pid, Type) || Type <- [meta, signals, location, memory_used, work]]. | |||
%% @doc Allows to be similar to `erlang:process_info/2', but allows to | |||
%% sort fields by safe categories and pre-selections, avoiding items such | |||
%% as the mailbox, which may have a tendency to grow and be unsafe when | |||
%% called in production systems. | |||
%% | |||
%% Moreover, it will fetch and read information on local processes that were | |||
%% registered locally (an atom), globally (`{global, Name}'), or through | |||
%% another registry supported in the `{via, Module, Name}' syntax (must have a | |||
%% `Module:whereis_name/1' function). Pids can also be passed in as a string | |||
%% (`"<0.39.0>"') or a triple (`{0,39,0}') and will be converted to be used. | |||
%% | |||
%% Although the type signature doesn't show it in generated documentation, | |||
%% a list of arguments or individual arguments accepted by | |||
%% `erlang:process_info/2' and return them as that function would. | |||
%% | |||
%% A fake attribute `binary_memory' is also available to return the | |||
%% amount of memory used by refc binaries for a process. | |||
-spec info(pid_term(), info_type()) -> {info_type(), [{info_key(), term()}]} | |||
; (pid_term(), [atom()]) -> [{atom(), term()}] | |||
; (pid_term(), atom()) -> {atom(), term()}. | |||
info(PidTerm, meta) -> | |||
info_type(PidTerm, meta, [registered_name, dictionary, group_leader, | |||
status]); | |||
info(PidTerm, signals) -> | |||
info_type(PidTerm, signals, [links, monitors, monitored_by, trap_exit]); | |||
info(PidTerm, location) -> | |||
info_type(PidTerm, location, [initial_call, current_stacktrace]); | |||
info(PidTerm, memory_used) -> | |||
info_type(PidTerm, memory_used, [memory, message_queue_len, heap_size, | |||
total_heap_size, garbage_collection]); | |||
info(PidTerm, work) -> | |||
info_type(PidTerm, work, [reductions]); | |||
info(PidTerm, Keys) -> | |||
proc_info(recon_lib:term_to_pid(PidTerm), Keys). | |||
%% @private makes access to `info_type()' calls simpler. | |||
-spec info_type(pid_term(), info_type(), [info_key()]) -> | |||
{info_type(), [{info_key(), term()}]}. | |||
info_type(PidTerm, Type, Keys) -> | |||
Pid = recon_lib:term_to_pid(PidTerm), | |||
{Type, proc_info(Pid, Keys)}. | |||
%% @private wrapper around `erlang:process_info/2' that allows special | |||
%% attribute handling for items like `binary_memory'. | |||
proc_info(Pid, binary_memory) -> | |||
{binary, Bins} = erlang:process_info(Pid, binary), | |||
{binary_memory, recon_lib:binary_memory(Bins)}; | |||
proc_info(Pid, Term) when is_atom(Term) -> | |||
erlang:process_info(Pid, Term); | |||
proc_info(Pid, List) when is_list(List) -> | |||
case lists:member(binary_memory, List) of | |||
false -> | |||
erlang:process_info(Pid, List); | |||
true -> | |||
Res = erlang:process_info(Pid, replace(binary_memory, binary, List)), | |||
proc_fake(List, Res) | |||
end. | |||
%% @private Replace keys around | |||
replace(_, _, []) -> []; | |||
replace(H, Val, [H|T]) -> [Val | replace(H, Val, T)]; | |||
replace(R, Val, [H|T]) -> [H | replace(R, Val, T)]. | |||
proc_fake([], []) -> | |||
[]; | |||
proc_fake([binary_memory|T1], [{binary,Bins}|T2]) -> | |||
[{binary_memory, recon_lib:binary_memory(Bins)} | |||
| proc_fake(T1,T2)]; | |||
proc_fake([_|T1], [H|T2]) -> | |||
[H | proc_fake(T1,T2)]. | |||
%% @doc Fetches a given attribute from all processes (except the | |||
%% caller) and returns the biggest `Num' consumers. | |||
-spec proc_count(AttributeName, Num) -> [proc_attrs()] when | |||
AttributeName :: atom(), | |||
Num :: non_neg_integer(). | |||
proc_count(AttrName, Num) -> | |||
recon_lib:sublist_top_n_attrs(recon_lib:proc_attrs(AttrName), Num). | |||
%% @doc Fetches a given attribute from all processes (except the | |||
%% caller) and returns the biggest entries, over a sliding time window. | |||
%% | |||
%% This function is particularly useful when processes on the node | |||
%% are mostly short-lived, usually too short to inspect through other | |||
%% tools, in order to figure out what kind of processes are eating | |||
%% through a lot resources on a given node. | |||
%% | |||
%% It is important to see this function as a snapshot over a sliding | |||
%% window. A program's timeline during sampling might look like this: | |||
%% | |||
%% `--w---- [Sample1] ---x-------------y----- [Sample2] ---z--->' | |||
%% | |||
%% Some processes will live between `w' and die at `x', some between `y' and | |||
%% `z', and some between `x' and `y'. These samples will not be too significant | |||
%% as they're incomplete. If the majority of your processes run between a time | |||
%% interval `x'...`y' (in absolute terms), you should make sure that your | |||
%% sampling time is smaller than this so that for many processes, their | |||
%% lifetime spans the equivalent of `w' and `z'. Not doing this can skew the | |||
%% results: long-lived processes, that have 10 times the time to accumulate | |||
%% data (say reductions) will look like bottlenecks when they're not one. | |||
%% | |||
%% Warning: this function depends on data gathered at two snapshots, and then | |||
%% building a dictionary with entries to differentiate them. This can take a | |||
%% heavy toll on memory when you have many dozens of thousands of processes. | |||
-spec proc_window(AttributeName, Num, Milliseconds) -> [proc_attrs()] when | |||
AttributeName :: atom(), | |||
Num :: non_neg_integer(), | |||
Milliseconds :: pos_integer(). | |||
proc_window(AttrName, Num, Time) -> | |||
Sample = fun() -> recon_lib:proc_attrs(AttrName) end, | |||
{First,Last} = recon_lib:sample(Time, Sample), | |||
recon_lib:sublist_top_n_attrs(recon_lib:sliding_window(First, Last), Num). | |||
%% @doc Refc binaries can be leaking when barely-busy processes route them | |||
%% around and do little else, or when extremely busy processes reach a stable | |||
%% amount of memory allocated and do the vast majority of their work with refc | |||
%% binaries. When this happens, it may take a very long while before references | |||
%% get deallocated and refc binaries get to be garbage collected, leading to | |||
%% Out Of Memory crashes. | |||
%% This function fetches the number of refc binary references in each process | |||
%% of the node, garbage collects them, and compares the resulting number of | |||
%% references in each of them. The function then returns the `N' processes | |||
%% that freed the biggest amount of binaries, potentially highlighting leaks. | |||
%% | |||
%% See <a href="http://www.erlang.org/doc/efficiency_guide/binaryhandling.html#id65722">The efficiency guide</a> | |||
%% for more details on refc binaries | |||
-spec bin_leak(pos_integer()) -> [proc_attrs()]. | |||
bin_leak(N) -> | |||
Procs = recon_lib:sublist_top_n_attrs([ | |||
try | |||
{ok, {_,Pre,Id}} = recon_lib:proc_attrs(binary, Pid), | |||
erlang:garbage_collect(Pid), | |||
{ok, {_,Post,_}} = recon_lib:proc_attrs(binary, Pid), | |||
{Pid, length(Pre) - length(Post), Id} | |||
catch | |||
_:_ -> {Pid, 0, []} | |||
end || Pid <- processes() | |||
], N), | |||
[{Pid, -Val, Id} ||{Pid, Val, Id} <-Procs]. | |||
%% @doc Shorthand for `node_stats(N, Interval, fun(X,_) -> io:format("~p~n",[X]) end, nostate)'. | |||
-spec node_stats_print(Repeat, Interval) -> term() when | |||
Repeat :: non_neg_integer(), | |||
Interval :: pos_integer(). | |||
node_stats_print(N, Interval) -> | |||
node_stats(N, Interval, fun(X, _) -> io:format("~p~n", [X]) end, ok). | |||
%% @doc Because Erlang CPU usage as reported from `top' isn't the most | |||
%% reliable value (due to schedulers doing idle spinning to avoid going | |||
%% to sleep and impacting latency), a metric exists that is based on | |||
%% scheduler wall time. | |||
%% | |||
%% For any time interval, Scheduler wall time can be used as a measure | |||
%% of how 'busy' a scheduler is. A scheduler is busy when: | |||
%% | |||
%% <ul> | |||
%% <li>executing process code</li> | |||
%% <li>executing driver code</li> | |||
%% <li>executing NIF code</li> | |||
%% <li>executing BIFs</li> | |||
%% <li>garbage collecting</li> | |||
%% <li>doing memory management</li> | |||
%% </ul> | |||
%% | |||
%% A scheduler isn't busy when doing anything else. | |||
-spec scheduler_usage(Millisecs) -> undefined | [{SchedulerId, Usage}] when | |||
Millisecs :: non_neg_integer(), | |||
SchedulerId :: pos_integer(), | |||
Usage :: number(). | |||
scheduler_usage(Interval) when is_integer(Interval) -> | |||
%% We start and stop the scheduler_wall_time system flag if | |||
%% it wasn't in place already. Usually setting the flag should | |||
%% have a CPU impact (making it higher) only when under low usage. | |||
FormerFlag = erlang:system_flag(scheduler_wall_time, true), | |||
First = erlang:statistics(scheduler_wall_time), | |||
timer:sleep(Interval), | |||
Last = erlang:statistics(scheduler_wall_time), | |||
erlang:system_flag(scheduler_wall_time, FormerFlag), | |||
recon_lib:scheduler_usage_diff(First, Last). | |||
%% @doc Shorthand for `node_stats(N, Interval, fun(X,Acc) -> [X|Acc] end, [])' | |||
%% with the results reversed to be in the right temporal order. | |||
-spec node_stats_list(Repeat, Interval) -> [Stats] when | |||
Repeat :: non_neg_integer(), | |||
Interval :: pos_integer(), | |||
Stats :: {[Absolutes::{atom(),term()}], | |||
[Increments::{atom(),term()}]}. | |||
node_stats_list(N, Interval) -> | |||
lists:reverse(node_stats(N, Interval, fun(X, Acc) -> [X|Acc] end, [])). | |||
%% @doc Gathers statistics `N' time, waiting `Interval' milliseconds between | |||
%% each run, and accumulates results using a folding function `FoldFun'. | |||
%% The function will gather statistics in two forms: Absolutes and Increments. | |||
%% | |||
%% Absolutes are values that keep changing with time, and are useful to know | |||
%% about as a datapoint: process count, size of the run queue, error_logger | |||
%% queue length in versions before OTP-21 or those thar run it explicitely, | |||
%% and the memory of the node (total, processes, atoms, binaries, | |||
%% and ets tables). | |||
%% | |||
%% Increments are values that are mostly useful when compared to a previous | |||
%% one to have an idea what they're doing, because otherwise they'd never | |||
%% stop increasing: bytes in and out of the node, number of garbage colelctor | |||
%% runs, words of memory that were garbage collected, and the global reductions | |||
%% count for the node. | |||
-spec node_stats(N, Interval, FoldFun, Acc) -> Acc when | |||
N :: non_neg_integer(), | |||
Interval :: pos_integer(), | |||
FoldFun :: fun((Stats, Acc) -> Acc), | |||
Acc :: term(), | |||
Stats :: {[Absolutes::{atom(),term()}], | |||
[Increments::{atom(),term()}]}. | |||
node_stats(N, Interval, FoldFun, Init) -> | |||
Logger = case whereis(error_logger) of | |||
undefined -> logger; | |||
_ -> error_logger | |||
end, | |||
%% Turn on scheduler wall time if it wasn't there already | |||
FormerFlag = erlang:system_flag(scheduler_wall_time, true), | |||
%% Stats is an ugly fun, but it does its thing. | |||
Stats = fun({{OldIn,OldOut},{OldGCs,OldWords,_}, SchedWall}) -> | |||
%% Absolutes | |||
ProcC = erlang:system_info(process_count), | |||
RunQ = erlang:statistics(run_queue), | |||
LogQ = case Logger of | |||
error_logger -> | |||
{_,LogQLen} = process_info(whereis(error_logger), | |||
message_queue_len), | |||
LogQLen; | |||
_ -> | |||
undefined | |||
end, | |||
%% Mem (Absolutes) | |||
Mem = erlang:memory(), | |||
Tot = proplists:get_value(total, Mem), | |||
ProcM = proplists:get_value(processes_used,Mem), | |||
Atom = proplists:get_value(atom_used,Mem), | |||
Bin = proplists:get_value(binary, Mem), | |||
Ets = proplists:get_value(ets, Mem), | |||
%% Incremental | |||
{{input,In},{output,Out}} = erlang:statistics(io), | |||
GC={GCs,Words,_} = erlang:statistics(garbage_collection), | |||
BytesIn = In-OldIn, | |||
BytesOut = Out-OldOut, | |||
GCCount = GCs-OldGCs, | |||
GCWords = Words-OldWords, | |||
{_, Reds} = erlang:statistics(reductions), | |||
SchedWallNew = erlang:statistics(scheduler_wall_time), | |||
SchedUsage = recon_lib:scheduler_usage_diff(SchedWall, SchedWallNew), | |||
%% Stats Results | |||
{{[{process_count,ProcC}, {run_queue,RunQ}] ++ | |||
[{error_logger_queue_len,LogQ} || LogQ =/= undefined] ++ | |||
[{memory_total,Tot}, | |||
{memory_procs,ProcM}, {memory_atoms,Atom}, | |||
{memory_bin,Bin}, {memory_ets,Ets}], | |||
[{bytes_in,BytesIn}, {bytes_out,BytesOut}, | |||
{gc_count,GCCount}, {gc_words_reclaimed,GCWords}, | |||
{reductions,Reds}, {scheduler_usage, SchedUsage}]}, | |||
%% New State | |||
{{In,Out}, GC, SchedWallNew}} | |||
end, | |||
{{input,In},{output,Out}} = erlang:statistics(io), | |||
Gc = erlang:statistics(garbage_collection), | |||
SchedWall = erlang:statistics(scheduler_wall_time), | |||
Result = recon_lib:time_fold( | |||
N, Interval, Stats, | |||
{{In,Out}, Gc, SchedWall}, | |||
FoldFun, Init), | |||
%% Set scheduler wall time back to what it was | |||
erlang:system_flag(scheduler_wall_time, FormerFlag), | |||
Result. | |||
%%% OTP & Manipulations %%% | |||
%% @doc Shorthand call to `recon:get_state(PidTerm, 5000)' | |||
-spec get_state(pid_term()) -> term(). | |||
get_state(PidTerm) -> get_state(PidTerm, 5000). | |||
%% @doc Fetch the internal state of an OTP process. | |||
%% Calls `sys:get_state/2' directly in R16B01+, and fetches | |||
%% it dynamically on older versions of OTP. | |||
-spec get_state(pid_term(), Ms::non_neg_integer() | 'infinity') -> term(). | |||
get_state(PidTerm, Timeout) -> | |||
Proc = recon_lib:term_to_pid(PidTerm), | |||
try | |||
sys:get_state(Proc, Timeout) | |||
catch | |||
error:undef -> | |||
case sys:get_status(Proc, Timeout) of | |||
{status,_Pid,{module,gen_server},Data} -> | |||
{data, Props} = lists:last(lists:nth(5, Data)), | |||
proplists:get_value("State", Props); | |||
{status,_Pod,{module,gen_fsm},Data} -> | |||
{data, Props} = lists:last(lists:nth(5, Data)), | |||
proplists:get_value("StateData", Props) | |||
end | |||
end. | |||
%%% Code & Stuff %%% | |||
%% @equiv remote_load(nodes(), Mod) | |||
-spec remote_load(module()) -> term(). | |||
remote_load(Mod) -> remote_load(nodes(), Mod). | |||
%% @doc Loads one or more modules remotely, in a diskless manner. Allows to | |||
%% share code loaded locally with a remote node that doesn't have it | |||
-spec remote_load(Nodes, module()) -> term() when | |||
Nodes :: [node(),...] | node(). | |||
remote_load(Nodes=[_|_], Mod) when is_atom(Mod) -> | |||
{Mod, Bin, File} = code:get_object_code(Mod), | |||
rpc:multicall(Nodes, code, load_binary, [Mod, File, Bin]); | |||
remote_load(Nodes=[_|_], Modules) when is_list(Modules) -> | |||
[remote_load(Nodes, Mod) || Mod <- Modules]; | |||
remote_load(Node, Mod) -> | |||
remote_load([Node], Mod). | |||
%% @doc Obtain the source code of a module compiled with `debug_info'. | |||
%% The returned list sadly does not allow to format the types and typed | |||
%% records the way they look in the original module, but instead goes to | |||
%% an intermediary form used in the AST. They will still be placed | |||
%% in the right module attributes, however. | |||
%% @todo Figure out a way to pretty-print typespecs and records. | |||
-spec source(module()) -> iolist(). | |||
source(Module) -> | |||
Path = code:which(Module), | |||
{ok,{_,[{abstract_code,{_,AC}}]}} = beam_lib:chunks(Path, [abstract_code]), | |||
erl_prettypr:format(erl_syntax:form_list(AC)). | |||
%%% Ports Info %%% | |||
%% @doc returns a list of all TCP ports (the data type) open on the node. | |||
-spec tcp() -> [port()]. | |||
tcp() -> recon_lib:port_list(name, "tcp_inet"). | |||
%% @doc returns a list of all UDP ports (the data type) open on the node. | |||
-spec udp() -> [port()]. | |||
udp() -> recon_lib:port_list(name, "udp_inet"). | |||
%% @doc returns a list of all SCTP ports (the data type) open on the node. | |||
-spec sctp() -> [port()]. | |||
sctp() -> recon_lib:port_list(name, "sctp_inet"). | |||
%% @doc returns a list of all file handles open on the node. | |||
%% @deprecated Starting with OTP-21, files are implemented as NIFs | |||
%% and can no longer be listed. This function returns an empty list | |||
%% in such a case. | |||
-spec files() -> [port()]. | |||
files() -> recon_lib:port_list(name, "efile"). | |||
%% @doc Shows a list of all different ports on the node with their respective | |||
%% types. | |||
-spec port_types() -> [{Type::string(), Count::pos_integer()}]. | |||
port_types() -> | |||
lists:usort( | |||
%% sorts by biggest count, smallest type | |||
fun({KA,VA}, {KB,VB}) -> {VA,KB} > {VB,KA} end, | |||
recon_lib:count([Name || {_, Name} <- recon_lib:port_list(name)]) | |||
). | |||
%% @doc Fetches a given attribute from all inet ports (TCP, UDP, SCTP) | |||
%% and returns the biggest `Num' consumers. | |||
%% | |||
%% The values to be used can be the number of octets (bytes) sent, received, | |||
%% or both (`send_oct', `recv_oct', `oct', respectively), or the number | |||
%% of packets sent, received, or both (`send_cnt', `recv_cnt', `cnt', | |||
%% respectively). Individual absolute values for each metric will be returned | |||
%% in the 3rd position of the resulting tuple. | |||
-spec inet_count(AttributeName, Num) -> [inet_attrs()] when | |||
AttributeName :: 'recv_cnt' | 'recv_oct' | 'send_cnt' | 'send_oct' | |||
| 'cnt' | 'oct', | |||
Num :: non_neg_integer(). | |||
inet_count(Attr, Num) -> | |||
recon_lib:sublist_top_n_attrs(recon_lib:inet_attrs(Attr), Num). | |||
%% @doc Fetches a given attribute from all inet ports (TCP, UDP, SCTP) | |||
%% and returns the biggest entries, over a sliding time window. | |||
%% | |||
%% Warning: this function depends on data gathered at two snapshots, and then | |||
%% building a dictionary with entries to differentiate them. This can take a | |||
%% heavy toll on memory when you have many dozens of thousands of ports open. | |||
%% | |||
%% The values to be used can be the number of octets (bytes) sent, received, | |||
%% or both (`send_oct', `recv_oct', `oct', respectively), or the number | |||
%% of packets sent, received, or both (`send_cnt', `recv_cnt', `cnt', | |||
%% respectively). Individual absolute values for each metric will be returned | |||
%% in the 3rd position of the resulting tuple. | |||
-spec inet_window(AttributeName, Num, Milliseconds) -> [inet_attrs()] when | |||
AttributeName :: 'recv_cnt' | 'recv_oct' | 'send_cnt' | 'send_oct' | |||
| 'cnt' | 'oct', | |||
Num :: non_neg_integer(), | |||
Milliseconds :: pos_integer(). | |||
inet_window(Attr, Num, Time) when is_atom(Attr) -> | |||
Sample = fun() -> recon_lib:inet_attrs(Attr) end, | |||
{First,Last} = recon_lib:sample(Time, Sample), | |||
recon_lib:sublist_top_n_attrs(recon_lib:sliding_window(First, Last), Num). | |||
%% @doc Allows to be similar to `erlang:port_info/1', but allows | |||
%% more flexible port usage: usual ports, ports that were registered | |||
%% locally (an atom), ports represented as strings (`"#Port<0.2013>"'), | |||
%% or through an index lookup (`2013', for the same result as | |||
%% `"#Port<0.2013>"'). | |||
%% | |||
%% Moreover, the function will try to fetch implementation-specific | |||
%% details based on the port type (only inet ports have this feature | |||
%% so far). For example, TCP ports will include information about the | |||
%% remote peer, transfer statistics, and socket options being used. | |||
%% | |||
%% The information-specific and the basic port info are sorted and | |||
%% categorized in broader categories ({@link port_info_type()}). | |||
-spec port_info(port_term()) -> [{port_info_type(), | |||
[{port_info_key(), term()}]},...]. | |||
port_info(PortTerm) -> | |||
Port = recon_lib:term_to_port(PortTerm), | |||
[port_info(Port, Type) || Type <- [meta, signals, io, memory_used, | |||
specific]]. | |||
%% @doc Allows to be similar to `erlang:port_info/2', but allows | |||
%% more flexible port usage: usual ports, ports that were registered | |||
%% locally (an atom), ports represented as strings (`"#Port<0.2013>"'), | |||
%% or through an index lookup (`2013', for the same result as | |||
%% `"#Port<0.2013>"'). | |||
%% | |||
%% Moreover, the function allows to to fetch information by category | |||
%% as defined in {@link port_info_type()}, and although the type signature | |||
%% doesn't show it in the generated documentation, individual items | |||
%% accepted by `erlang:port_info/2' are accepted, and lists of them too. | |||
-spec port_info(port_term(), port_info_type()) -> {port_info_type(), | |||
[{port_info_key(), _}]} | |||
; (port_term(), [atom()]) -> [{atom(), term()}] | |||
; (port_term(), atom()) -> {atom(), term()}. | |||
port_info(PortTerm, meta) -> | |||
{meta, List} = port_info_type(PortTerm, meta, [id, name, os_pid]), | |||
case port_info(PortTerm, registered_name) of | |||
[] -> {meta, List}; | |||
Name -> {meta, [Name | List]} | |||
end; | |||
port_info(PortTerm, signals) -> | |||
port_info_type(PortTerm, signals, [connected, links, monitors]); | |||
port_info(PortTerm, io) -> | |||
port_info_type(PortTerm, io, [input, output]); | |||
port_info(PortTerm, memory_used) -> | |||
port_info_type(PortTerm, memory_used, [memory, queue_size]); | |||
port_info(PortTerm, specific) -> | |||
Port = recon_lib:term_to_port(PortTerm), | |||
Props = case erlang:port_info(Port, name) of | |||
{_,Type} when Type =:= "udp_inet"; | |||
Type =:= "tcp_inet"; | |||
Type =:= "sctp_inet" -> | |||
case inet:getstat(Port) of | |||
{ok, Stats} -> [{statistics, Stats}]; | |||
_ -> [] | |||
end ++ | |||
case inet:peername(Port) of | |||
{ok, Peer} -> [{peername, Peer}]; | |||
{error, _} -> [] | |||
end ++ | |||
case inet:sockname(Port) of | |||
{ok, Local} -> [{sockname, Local}]; | |||
{error, _} -> [] | |||
end ++ | |||
case inet:getopts(Port, [active, broadcast, buffer, delay_send, | |||
dontroute, exit_on_close, header, | |||
high_watermark, ipv6_v6only, keepalive, | |||
linger, low_watermark, mode, nodelay, | |||
packet, packet_size, priority, | |||
read_packets, recbuf, reuseaddr, | |||
send_timeout, sndbuf]) of | |||
{ok, Opts} -> [{options, Opts}]; | |||
{error, _} -> [] | |||
end; | |||
{_,"efile"} -> | |||
%% would be nice to support file-specific info, but things | |||
%% are too vague with the file_server and how it works in | |||
%% order to make this work efficiently | |||
[]; | |||
_ -> | |||
[] | |||
end, | |||
{type, Props}; | |||
port_info(PortTerm, Keys) when is_list(Keys) -> | |||
Port = recon_lib:term_to_port(PortTerm), | |||
[erlang:port_info(Port,Key) || Key <- Keys]; | |||
port_info(PortTerm, Key) when is_atom(Key) -> | |||
erlang:port_info(recon_lib:term_to_port(PortTerm), Key). | |||
%% @private makes access to `port_info_type()' calls simpler. | |||
%-spec port_info_type(pid_term(), port_info_type(), [port_info_key()]) -> | |||
% {port_info_type(), [{port_info_key(), term()}]}. | |||
port_info_type(PortTerm, Type, Keys) -> | |||
Port = recon_lib:term_to_port(PortTerm), | |||
{Type, [erlang:port_info(Port,Key) || Key <- Keys]}. | |||
%%% RPC Utils %%% | |||
%% @doc Shorthand for `rpc([node()|nodes()], Fun)'. | |||
-spec rpc(fun(() -> term())) -> {[Success::_],[Fail::_]}. | |||
rpc(Fun) -> | |||
rpc([node()|nodes()], Fun). | |||
%% @doc Shorthand for `rpc(Nodes, Fun, infinity)'. | |||
-spec rpc(node()|[node(),...], fun(() -> term())) -> {[Success::_],[Fail::_]}. | |||
rpc(Nodes, Fun) -> | |||
rpc(Nodes, Fun, infinity). | |||
%% @doc Runs an arbitrary fun (of arity 0) over one or more nodes. | |||
-spec rpc(node()|[node(),...], fun(() -> term()), timeout()) -> {[Success::_],[Fail::_]}. | |||
rpc(Nodes=[_|_], Fun, Timeout) when is_function(Fun,0) -> | |||
rpc:multicall(Nodes, erlang, apply, [Fun,[]], Timeout); | |||
rpc(Node, Fun, Timeout) when is_atom(Node) -> | |||
rpc([Node], Fun, Timeout). | |||
%% @doc Shorthand for `named_rpc([node()|nodes()], Fun)'. | |||
-spec named_rpc(fun(() -> term())) -> {[Success::_],[Fail::_]}. | |||
named_rpc(Fun) -> | |||
named_rpc([node()|nodes()], Fun). | |||
%% @doc Shorthand for `named_rpc(Nodes, Fun, infinity)'. | |||
-spec named_rpc(node()|[node(),...], fun(() -> term())) -> {[Success::_],[Fail::_]}. | |||
named_rpc(Nodes, Fun) -> | |||
named_rpc(Nodes, Fun, infinity). | |||
%% @doc Runs an arbitrary fun (of arity 0) over one or more nodes, and returns the | |||
%% name of the node that computed a given result along with it, in a tuple. | |||
-spec named_rpc(node()|[node(),...], fun(() -> term()), timeout()) -> {[Success::_],[Fail::_]}. | |||
named_rpc(Nodes=[_|_], Fun, Timeout) when is_function(Fun,0) -> | |||
rpc:multicall(Nodes, erlang, apply, [fun() -> {node(),Fun()} end,[]], Timeout); | |||
named_rpc(Node, Fun, Timeout) when is_atom(Node) -> | |||
named_rpc([Node], Fun, Timeout). | |||
@ -0,0 +1,778 @@ | |||
%%% @author Fred Hebert <mononcqc@ferd.ca> | |||
%%% [http://ferd.ca/] | |||
%%% @author Lukas Larsson <lukas@erlang.org> | |||
%%% @doc Functions to deal with | |||
%%% <a href="http://www.erlang.org/doc/man/erts_alloc.html">Erlang's memory | |||
%%% allocators</a>, or particularly, to try to present the allocator data | |||
%%% in a way that makes it simpler to discover possible problems. | |||
%%% | |||
%%% Tweaking Erlang memory allocators and their behaviour is a very tricky | |||
%%% ordeal whenever you have to give up the default settings. This module | |||
%%% (and its documentation) will try and provide helpful pointers to help | |||
%%% in this task. | |||
%%% | |||
%%% This module should mostly be helpful to figure out <em>if</em> there is | |||
%%% a problem, but will offer little help to figure out <em>what</em> is wrong. | |||
%%% | |||
%%% To figure this out, you need to dig deeper into the allocator data | |||
%%% (obtainable with {@link allocators/0}), and/or have some precise knowledge | |||
%%% about the type of load and work done by the VM to be able to assess what | |||
%%% each reaction to individual tweak should be. | |||
%%% | |||
%%% A lot of trial and error might be required to figure out if tweaks have | |||
%%% helped or not, ultimately. | |||
%%% | |||
%%% In order to help do offline debugging of memory allocator problems | |||
%%% recon_alloc also has a few functions that store snapshots of the | |||
%%% memory statistics. | |||
%%% These snapshots can be used to freeze the current allocation values so that | |||
%%% they do not change during analysis while using the regular functionality of | |||
%%% this module, so that the allocator values can be saved, or that | |||
%%% they can be shared, dumped, and reloaded for further analysis using files. | |||
%%% See {@link snapshot_load/1} for a simple use-case. | |||
%%% | |||
%%% Glossary: | |||
%%% <dl> | |||
%%% <dt>sys_alloc</dt> | |||
%%% <dd>System allocator, usually just malloc</dd> | |||
%%% | |||
%%% <dt>mseg_alloc</dt> | |||
%%% <dd>Used by other allocators, can do mmap. Caches allocations</dd> | |||
%%% | |||
%%% <dt>temp_alloc</dt> | |||
%%% <dd>Used for temporary allocations</dd> | |||
%%% | |||
%%% <dt>eheap_alloc</dt> | |||
%%% <dd>Heap data (i.e. process heaps) allocator</dd> | |||
%%% | |||
%%% <dt>binary_alloc</dt> | |||
%%% <dd>Global binary heap allocator</dd> | |||
%%% | |||
%%% <dt>ets_alloc</dt> | |||
%%% <dd>ETS data allocator</dd> | |||
%%% | |||
%%% <dt>driver_alloc</dt> | |||
%%% <dd>Driver data allocator</dd> | |||
%%% | |||
%%% <dt>sl_alloc</dt> | |||
%%% <dd>Short-lived memory blocks allocator</dd> | |||
%%% | |||
%%% <dt>ll_alloc</dt> | |||
%%% <dd>Long-lived data (i.e. Erlang code itself) allocator</dd> | |||
%%% | |||
%%% <dt>fix_alloc</dt> | |||
%%% <dd>Frequently used fixed-size data allocator</dd> | |||
%%% | |||
%%% <dt>std_alloc</dt> | |||
%%% <dd>Allocator for other memory blocks</dd> | |||
%%% | |||
%%% <dt>carrier</dt> | |||
%%% <dd>When a given area of memory is allocated by the OS to the | |||
%%% VM (through sys_alloc or mseg_alloc), it is put into a 'carrier'. There | |||
%%% are two kinds of carriers: multiblock and single block. The default | |||
%%% carriers data is sent to are multiblock carriers, owned by a specific | |||
%%% allocator (ets_alloc, binary_alloc, etc.). The specific allocator can | |||
%%% thus do allocation for specific Erlang requirements within bits of | |||
%%% memory that has been preallocated before. This allows more reuse, | |||
%%% and we can even measure the cache hit rates {@link cache_hit_rates/0}. | |||
%%% | |||
%%% There is however a threshold above which an item in memory won't fit | |||
%%% a multiblock carrier. When that happens, the specific allocator does | |||
%%% a special allocation to a single block carrier. This is done by the | |||
%%% allocator basically asking for space directly from sys_alloc or | |||
%%% mseg_alloc rather than a previously multiblock area already obtained | |||
%%% before. | |||
%%% | |||
%%% This leads to various allocation strategies where you decide to | |||
%%% choose: | |||
%%% <ol> | |||
%%% <li>which multiblock carrier you're going to (if at all)</li> | |||
%%% <li>which block in that carrier you're going to</li> | |||
%%% </ol> | |||
%%% | |||
%%% See <a href="http://www.erlang.org/doc/man/erts_alloc.html">the official | |||
%%% documentation on erts_alloc</a> for more details. | |||
%%% </dd> | |||
%%% | |||
%%% <dt>mbcs</dt> | |||
%%% <dd>Multiblock carriers.</dd> | |||
%%% | |||
%%% <dt>sbcs</dt> | |||
%%% <dd>Single block carriers.</dd> | |||
%%% | |||
%%% <dt>lmbcs</dt> | |||
%%% <dd>Largest multiblock carrier size</dd> | |||
%%% | |||
%%% <dt>smbcs</dt> | |||
%%% <dd>Smallest multiblock carrier size</dd> | |||
%%% | |||
%%% <dt>sbct</dt> | |||
%%% <dd>Single block carrier threshold</dd> | |||
%%% </dl> | |||
%%% | |||
%%% By default all sizes returned by this module are in bytes. You can change | |||
%%% this by calling {@link set_unit/1}. | |||
%%% | |||
-module(recon_alloc). | |||
-define(UTIL_ALLOCATORS, [temp_alloc, | |||
eheap_alloc, | |||
binary_alloc, | |||
ets_alloc, | |||
driver_alloc, | |||
sl_alloc, | |||
ll_alloc, | |||
fix_alloc, | |||
std_alloc | |||
]). | |||
-type allocator() :: temp_alloc | eheap_alloc | binary_alloc | ets_alloc | |||
| driver_alloc | sl_alloc | ll_alloc | fix_alloc | |||
| std_alloc. | |||
-type instance() :: non_neg_integer(). | |||
-type allocdata(T) :: {{allocator(), instance()}, T}. | |||
-type allocdata_types(T) :: {{allocator(), [instance()]}, T}. | |||
-export_type([allocator/0, instance/0, allocdata/1]). | |||
-define(CURRENT_POS, 2). % pos in sizes tuples for current value | |||
-define(MAX_POS, 4). % pos in sizes tuples for max value | |||
-export([memory/1, memory/2, fragmentation/1, cache_hit_rates/0, | |||
average_block_sizes/1, sbcs_to_mbcs/1, allocators/0, | |||
allocators/1]). | |||
%% Snapshot handling | |||
-type memory() :: [{atom(),atom()}]. | |||
-type snapshot() :: {memory(),[allocdata(term())]}. | |||
-export_type([memory/0, snapshot/0]). | |||
-export([snapshot/0, snapshot_clear/0, | |||
snapshot_print/0, snapshot_get/0, | |||
snapshot_save/1, snapshot_load/1]). | |||
%% Unit handling | |||
-export([set_unit/1]). | |||
%%%%%%%%%%%%%% | |||
%%% Public %%% | |||
%%%%%%%%%%%%%% | |||
%% @doc Equivalent to `memory(Key, current)'. | |||
-spec memory(used | allocated | unused) -> pos_integer() | |||
; (usage) -> number() | |||
; (allocated_types | allocated_instances) -> | |||
[{allocator(), pos_integer()}]. | |||
memory(Key) -> memory(Key, current). | |||
%% @doc reports one of multiple possible memory values for the entire | |||
%% node depending on what is to be reported: | |||
%% | |||
%% <ul> | |||
%% <li>`used' reports the memory that is actively used for allocated | |||
%% Erlang data;</li> | |||
%% <li>`allocated' reports the memory that is reserved by the VM. It | |||
%% includes the memory used, but also the memory yet-to-be-used but still | |||
%% given by the OS. This is the amount you want if you're dealing with | |||
%% ulimit and OS-reported values. </li> | |||
%% <li>`allocated_types' report the memory that is reserved by the | |||
%% VM grouped into the different util allocators.</li> | |||
%% <li>`allocated_instances' report the memory that is reserved | |||
%% by the VM grouped into the different schedulers. Note that | |||
%% instance id 0 is the global allocator used to allocate data from | |||
%% non-managed threads, i.e. async and driver threads.</li> | |||
%% <li>`unused' reports the amount of memory reserved by the VM that | |||
%% is not being allocated. | |||
%% Equivalent to `allocated - used'.</li> | |||
%% <li>`usage' returns a percentage (0.0 .. 1.0) of `used/allocated' | |||
%% memory ratios.</li> | |||
%% </ul> | |||
%% | |||
%% The memory reported by `allocated' should roughly | |||
%% match what the OS reports. If this amount is different by a large margin, | |||
%% it may be the sign that someone is allocating memory in C directly, outside | |||
%% of Erlang's own allocator -- a big warning sign. There are currently | |||
%% three sources of memory alloction that are not counted towards this value: | |||
%% The cached segments in the mseg allocator, any memory allocated as a | |||
%% super carrier, and small pieces of memory allocated during startup | |||
%% before the memory allocators are initialized. | |||
%% | |||
%% Also note that low memory usages can be the sign of fragmentation in | |||
%% memory, in which case exploring which specific allocator is at fault | |||
%% is recommended (see {@link fragmentation/1}) | |||
-spec memory(used | allocated | unused, current | max) -> pos_integer() | |||
; (usage, current | max) -> number() | |||
; (allocated_types|allocated_instances, current | max) -> | |||
[{allocator(),pos_integer()}]. | |||
memory(used,Keyword) -> | |||
lists:sum(lists:map(fun({_,Prop}) -> | |||
container_size(Prop,Keyword,blocks_size) | |||
end,util_alloc())); | |||
memory(allocated,Keyword) -> | |||
lists:sum(lists:map(fun({_,Prop}) -> | |||
container_size(Prop,Keyword,carriers_size) | |||
end,util_alloc())); | |||
memory(allocated_types,Keyword) -> | |||
lists:foldl(fun({{Alloc,_N},Props},Acc) -> | |||
CZ = container_size(Props,Keyword,carriers_size), | |||
orddict:update_counter(Alloc,CZ,Acc) | |||
end,orddict:new(),util_alloc()); | |||
memory(allocated_instances,Keyword) -> | |||
lists:foldl(fun({{_Alloc,N},Props},Acc) -> | |||
CZ = container_size(Props,Keyword,carriers_size), | |||
orddict:update_counter(N,CZ,Acc) | |||
end,orddict:new(),util_alloc()); | |||
memory(unused,Keyword) -> | |||
memory(allocated,Keyword) - memory(used,Keyword); | |||
memory(usage,Keyword) -> | |||
memory(used,Keyword) / memory(allocated,Keyword). | |||
%% @doc Compares the block sizes to the carrier sizes, both for | |||
%% single block (`sbcs') and multiblock (`mbcs') carriers. | |||
%% | |||
%% The returned results are sorted by a weight system that is | |||
%% somewhat likely to return the most fragmented allocators first, | |||
%% based on their percentage of use and the total size of the carriers, | |||
%% for both `sbcs' and `mbcs'. | |||
%% | |||
%% The values can both be returned for `current' allocator values, and | |||
%% for `max' allocator values. The current values hold the present allocation | |||
%% numbers, and max values, the values at the peak. Comparing both together | |||
%% can give an idea of whether the node is currently being at its memory peak | |||
%% when possibly leaky, or if it isn't. This information can in turn | |||
%% influence the tuning of allocators to better fit sizes of blocks and/or | |||
%% carriers. | |||
-spec fragmentation(current | max) -> [allocdata([{atom(), term()}])]. | |||
fragmentation(Keyword) -> | |||
WeighedData = [begin | |||
BlockSbcs = container_value(Props, Keyword, sbcs, blocks_size), | |||
CarSbcs = container_value(Props, Keyword, sbcs, carriers_size), | |||
BlockMbcs = container_value(Props, Keyword, mbcs, blocks_size), | |||
CarMbcs = container_value(Props, Keyword, mbcs, carriers_size), | |||
{Weight, Vals} = weighed_values({BlockSbcs,CarSbcs}, | |||
{BlockMbcs,CarMbcs}), | |||
{Weight, {Allocator,N}, Vals} | |||
end || {{Allocator, N}, Props} <- util_alloc()], | |||
[{Key,Val} || {_W, Key, Val} <- lists:reverse(lists:sort(WeighedData))]. | |||
%% @doc looks at the `mseg_alloc' allocator (allocator used by all the | |||
%% allocators in {@link allocator()}) and returns information relative to | |||
%% the cache hit rates. Unless memory has expected spiky behaviour, it should | |||
%% usually be above 0.80 (80%). | |||
%% | |||
%% Cache can be tweaked using three VM flags: `+MMmcs', `+MMrmcbf', and | |||
%% `+MMamcbf'. | |||
%% | |||
%% `+MMmcs' stands for the maximum amount of cached memory segments. Its | |||
%% default value is '10' and can be anything from 0 to 30. Increasing | |||
%% it first and verifying if cache hits get better should be the first | |||
%% step taken. | |||
%% | |||
%% The two other options specify what are the maximal values of a segment | |||
%% to cache, in relative (in percent) and absolute terms (in kilobytes), | |||
%% respectively. Increasing these may allow more segments to be cached, but | |||
%% should also add overheads to memory allocation. An Erlang node that has | |||
%% limited memory and increases these values may make things worse on | |||
%% that point. | |||
%% | |||
%% The values returned by this function are sorted by a weight combining | |||
%% the lower cache hit joined to the largest memory values allocated. | |||
-spec cache_hit_rates() -> [{{instance,instance()}, [{Key,Val}]}] when | |||
Key :: hit_rate | hits | calls, | |||
Val :: term(). | |||
cache_hit_rates() -> | |||
WeighedData = [begin | |||
Mem = proplists:get_value(memkind, Props), | |||
{_,Hits} = lists:keyfind(cache_hits, 1, proplists:get_value(status,Mem)), | |||
{_,Giga,Ones} = lists:keyfind(mseg_alloc,1,proplists:get_value(calls,Mem)), | |||
Calls = 1000000000*Giga + Ones, | |||
HitRate = usage(Hits,Calls), | |||
Weight = (1.00 - HitRate)*Calls, | |||
{Weight, {instance,N}, [{hit_rate,HitRate}, {hits,Hits}, {calls,Calls}]} | |||
end || {{_, N}, Props} <- alloc([mseg_alloc])], | |||
[{Key,Val} || {_W,Key,Val} <- lists:reverse(lists:sort(WeighedData))]. | |||
%% @doc Checks all allocators in {@link allocator()} and returns the average | |||
%% block sizes being used for `mbcs' and `sbcs'. This value is interesting | |||
%% to use because it will tell us how large most blocks are. | |||
%% This can be related to the VM's largest multiblock carrier size | |||
%% (`lmbcs') and smallest multiblock carrier size (`smbcs') to specify | |||
%% allocation strategies regarding the carrier sizes to be used. | |||
%% | |||
%% This function isn't exceptionally useful unless you know you have some | |||
%% specific problem, say with sbcs/mbcs ratios (see {@link sbcs_to_mbcs/0}) | |||
%% or fragmentation for a specific allocator, and want to figure out what | |||
%% values to pick to increase or decrease sizes compared to the currently | |||
%% configured value. | |||
%% | |||
%% Do note that values for `lmbcs' and `smbcs' are going to be rounded up | |||
%% to the next power of two when configuring them. | |||
-spec average_block_sizes(current | max) -> [{allocator(), [{Key,Val}]}] when | |||
Key :: mbcs | sbcs, | |||
Val :: number(). | |||
average_block_sizes(Keyword) -> | |||
Dict = lists:foldl(fun({{Instance,_},Props},Dict0) -> | |||
CarSbcs = container_value(Props, Keyword, sbcs, blocks), | |||
SizeSbcs = container_value(Props, Keyword, sbcs, blocks_size), | |||
CarMbcs = container_value(Props, Keyword, mbcs, blocks), | |||
SizeMbcs = container_value(Props, Keyword, mbcs, blocks_size), | |||
Dict1 = dict:update_counter({Instance,sbcs,count},CarSbcs,Dict0), | |||
Dict2 = dict:update_counter({Instance,sbcs,size},SizeSbcs,Dict1), | |||
Dict3 = dict:update_counter({Instance,mbcs,count},CarMbcs,Dict2), | |||
Dict4 = dict:update_counter({Instance,mbcs,size},SizeMbcs,Dict3), | |||
Dict4 | |||
end, | |||
dict:new(), | |||
util_alloc()), | |||
average_group(average_calc(lists:sort(dict:to_list(Dict)))). | |||
%% @doc compares the amount of single block carriers (`sbcs') vs the | |||
%% number of multiblock carriers (`mbcs') for each individual allocator in | |||
%% {@link allocator()}. | |||
%% | |||
%% When a specific piece of data is allocated, it is compared to a threshold, | |||
%% called the 'single block carrier threshold' (`sbct'). When the data is | |||
%% larger than the `sbct', it gets sent to a single block carrier. When the | |||
%% data is smaller than the `sbct', it gets placed into a multiblock carrier. | |||
%% | |||
%% mbcs are to be preferred to sbcs because they basically represent pre- | |||
%% allocated memory, whereas sbcs will map to one call to sys_alloc | |||
%% or mseg_alloc, which is more expensive than redistributing | |||
%% data that was obtained for multiblock carriers. Moreover, the VM is able to | |||
%% do specific work with mbcs that should help reduce fragmentation in ways | |||
%% sys_alloc or mmap usually won't. | |||
%% | |||
%% Ideally, most of the data should fit inside multiblock carriers. If | |||
%% most of the data ends up in `sbcs', you may need to adjust the multiblock | |||
%% carrier sizes, specifically the maximal value (`lmbcs') and the threshold | |||
%% (`sbct'). On 32 bit VMs, `sbct' is limited to 8MBs, but 64 bit VMs can go | |||
%% to pretty much any practical size. | |||
%% | |||
%% Given the value returned is a ratio of sbcs/mbcs, the higher the value, | |||
%% the worst the condition. The list is sorted accordingly. | |||
-spec sbcs_to_mbcs(max | current) -> [allocdata(term())]. | |||
sbcs_to_mbcs(Keyword) -> | |||
WeightedList = [begin | |||
Sbcs = container_value(Props, Keyword, sbcs, blocks), | |||
Mbcs = container_value(Props, Keyword, mbcs, blocks), | |||
Ratio = case {Sbcs, Mbcs} of | |||
{0,0} -> 0; | |||
{_,0} -> infinity; % that is bad! | |||
{_,_} -> Sbcs / Mbcs | |||
end, | |||
{Ratio, {Allocator,N}} | |||
end || {{Allocator, N}, Props} <- util_alloc()], | |||
[{Alloc,Ratio} || {Ratio,Alloc} <- lists:reverse(lists:sort(WeightedList))]. | |||
%% @doc returns a dump of all allocator settings and values | |||
-spec allocators() -> [allocdata(term())]. | |||
allocators() -> | |||
UtilAllocators = erlang:system_info(alloc_util_allocators), | |||
Allocators = [sys_alloc,mseg_alloc|UtilAllocators], | |||
[{{A,N}, format_alloc(A, Props)} || | |||
A <- Allocators, | |||
Allocs <- [erlang:system_info({allocator,A})], | |||
Allocs =/= false, | |||
{_,N,Props} <- Allocs]. | |||
format_alloc(Alloc, Props) -> | |||
%% {versions,_,_} is implicitly deleted in order to allow the use of the | |||
%% orddict api, and never really having come across a case where it was | |||
%% useful to know. | |||
[{K, format_blocks(Alloc, K, V)} || {K, V} <- lists:sort(Props)]. | |||
format_blocks(_, _, []) -> | |||
[]; | |||
format_blocks(Alloc, Key, [{blocks, L} | List]) when is_list(L) -> | |||
%% OTP-22 introduces carrier migrations across types, and OTP-23 changes the | |||
%% format of data reported to be a bit richer; however it's not compatible | |||
%% with most calculations made for this library. | |||
%% So what we do here for `blocks' is merge all the info into the one the | |||
%% library expects (`blocks' and `blocks_size'), then keep the original | |||
%% one in case it is further needed. | |||
%% There were further changes to `mbcs_pool' changing `foreign_blocks', | |||
%% `blocks' and `blocks_size' into just `blocks' with a proplist, so we're breaking | |||
%% up to use that one too. | |||
%% In the end we go from `{blocks, [{Alloc, [...]}]}' to: | |||
%% - `{blocks, ...}' (4-tuple in mbcs and sbcs, 2-tuple in mbcs_pool) | |||
%% - `{blocks_size, ...}' (4-tuple in mbcs and sbcs, 2-tuple in mbcs_pool) | |||
%% - `{foreign_blocks, [...]}' (just append lists =/= `Alloc') | |||
%% - `{raw_blocks, [...]}' (original value) | |||
Foreign = lists:filter(fun({A, _Props}) -> A =/= Alloc end, L), | |||
Type = case Key of | |||
mbcs_pool -> int; | |||
_ -> quadruple | |||
end, | |||
MergeF = fun(K) -> | |||
fun({_A, Props}, Acc) -> | |||
case lists:keyfind(K, 1, Props) of | |||
{K,Cur,Last,Max} -> {Cur, Last, Max}; | |||
{K,V} -> Acc+V | |||
end | |||
end | |||
end, | |||
%% Since tuple sizes change, hack around it using tuple_to_list conversion | |||
%% and set the accumulator to a list so it defaults to not putting anything | |||
{Blocks, BlocksSize} = case Type of | |||
int -> | |||
{{blocks, lists:foldl(MergeF(count), 0, L)}, | |||
{blocks_size, lists:foldl(MergeF(size), 0, L)}}; | |||
quadruple -> | |||
{list_to_tuple([blocks | tuple_to_list(lists:foldl(MergeF(count), {0,0,0}, L))]), | |||
list_to_tuple([blocks_size | tuple_to_list(lists:foldl(MergeF(size), {0,0,0}, L))])} | |||
end, | |||
[Blocks, BlocksSize, {foreign_blocks, Foreign}, {raw_blocks, L} | |||
| format_blocks(Alloc, Key, List)]; | |||
format_blocks(Alloc, Key, [H | T]) -> | |||
[H | format_blocks(Alloc, Key, T)]. | |||
%% @doc returns a dump of all allocator settings and values modified | |||
%% depending on the argument. | |||
%% <ul> | |||
%% <li>`types' report the settings and accumulated values for each | |||
%% allocator type. This is useful when looking for anomalies | |||
%% in the system as a whole and not specific instances.</li> | |||
%% </ul> | |||
-spec allocators(types) -> [allocdata_types(term())]. | |||
allocators(types) -> | |||
allocators_types(alloc(), []). | |||
allocators_types([{{Type,No},Vs}|T], As) -> | |||
case lists:keytake(Type, 1, As) of | |||
false -> | |||
allocators_types(T,[{Type,[No],sort_values(Type, Vs)}|As]); | |||
{value,{Type,Nos,OVs},NAs} -> | |||
MergedValues = merge_values(sort_values(Type, Vs),OVs), | |||
allocators_types(T,[{Type,[No|Nos],MergedValues}|NAs]) | |||
end; | |||
allocators_types([], As) -> | |||
[{{Type,Nos},Vs} || {Type, Nos, Vs} <- As]. | |||
merge_values([{Key,Vs}|T1], [{Key,OVs}|T2]) when Key =:= memkind -> | |||
[{Key, merge_values(Vs, OVs)} | merge_values(T1, T2)]; | |||
merge_values([{Key,Vs}|T1], [{Key,OVs}|T2]) when Key =:= calls; | |||
Key =:= fix_types; | |||
Key =:= sbmbcs; | |||
Key =:= mbcs; | |||
Key =:= mbcs_pool; | |||
Key =:= sbcs; | |||
Key =:= status -> | |||
[{Key,lists:map( | |||
fun({{K,MV1,V1}, {K,MV2,V2}}) -> | |||
%% Merge the MegaVs + Vs into one | |||
V = MV1 * 1000000 + V1 + MV2 * 1000000 + V2, | |||
{K, V div 1000000, V rem 1000000}; | |||
({{K,V1}, {K,V2}}) when K =:= segments_watermark -> | |||
%% We take the maximum watermark as that is | |||
%% a value that we can use somewhat. Ideally | |||
%% maybe the average should be used, but the | |||
%% value is very rarely important so leave it | |||
%% like this for now. | |||
{K, lists:max([V1,V2])}; | |||
({{K,V1}, {K,V2}}) when K =:= foreign_blocks; K =:= raw_blocks -> | |||
%% foreign blocks are just merged as a bigger list. | |||
{K, V1++V2}; | |||
({{K,V1}, {K,V2}}) -> | |||
{K, V1 + V2}; | |||
({{K,C1,L1,M1}, {K,C2,L2,M2}}) -> | |||
%% Merge the Curr, Last, Max into one | |||
{K, C1+C2, L1+L2, M1+M2} | |||
end, lists:zip(Vs,OVs))} | merge_values(T1,T2)]; | |||
merge_values([{Type,_Vs}=E|T1], T2) when Type =:= mbcs_pool -> | |||
%% For values never showing up in instance 0 but in all other | |||
[E|merge_values(T1,T2)]; | |||
merge_values(T1, [{Type,_Vs}=E|T2]) when Type =:= fix_types -> | |||
%% For values only showing up in instance 0 | |||
[E|merge_values(T1,T2)]; | |||
merge_values([E|T1], [E|T2]) -> | |||
%% For values that are constant | |||
[E|merge_values(T1,T2)]; | |||
merge_values([{options,_Vs1}|T1], [{options,_Vs2} = E|T2]) -> | |||
%% Options change a but in between instance 0 and the other, | |||
%% We show the others as they are the most interesting. | |||
[E|merge_values(T1,T2)]; | |||
merge_values([],[]) -> | |||
[]. | |||
sort_values(mseg_alloc, Vs) -> | |||
{value, {memkind, MemKindVs}, OVs} = lists:keytake(memkind, 1, Vs), | |||
lists:sort([{memkind, lists:sort(MemKindVs)} | OVs]); | |||
sort_values(_Type, Vs) -> | |||
lists:sort(Vs). | |||
%%%%%%%%%%%%%%%%%%%%%%%%% | |||
%%% Snapshot handling %%% | |||
%%%%%%%%%%%%%%%%%%%%%%%%% | |||
%% @doc Take a new snapshot of the current memory allocator statistics. | |||
%% The snapshot is stored in the process dictionary of the calling process, | |||
%% with all the limitations that it implies (i.e. no garbage-collection). | |||
%% To unsert the snapshot, see {@link snapshot_clear/1}. | |||
-spec snapshot() -> snapshot() | undefined. | |||
snapshot() -> | |||
put(recon_alloc_snapshot, snapshot_int()). | |||
%% @doc clear the current snapshot in the process dictionary, if present, | |||
%% and return the value it had before being unset. | |||
%% @end | |||
%% Maybe we should use erlang:delete(Key) here instead? | |||
-spec snapshot_clear() -> snapshot() | undefined. | |||
snapshot_clear() -> | |||
put(recon_alloc_snapshot, undefined). | |||
%% @doc print a dump of the current snapshot stored by {@link snapshot/0} | |||
%% Prints `undefined' if no snapshot has been taken. | |||
-spec snapshot_print() -> ok. | |||
snapshot_print() -> | |||
io:format("~p.~n",[snapshot_get()]). | |||
%% @doc returns the current snapshot stored by {@link snapshot/0}. | |||
%% Returns `undefined' if no snapshot has been taken. | |||
-spec snapshot_get() -> snapshot() | undefined. | |||
snapshot_get() -> | |||
get(recon_alloc_snapshot). | |||
%% @doc save the current snapshot taken by {@link snapshot/0} to a file. | |||
%% If there is no current snapshot, a snaphot of the current allocator | |||
%% statistics will be written to the file. | |||
-spec snapshot_save(Filename) -> ok when | |||
Filename :: file:name(). | |||
snapshot_save(Filename) -> | |||
Snapshot = case snapshot_get() of | |||
undefined -> | |||
snapshot_int(); | |||
Snap -> | |||
Snap | |||
end, | |||
case file:write_file(Filename,io_lib:format("~p.~n",[Snapshot])) of | |||
ok -> ok; | |||
{error,Reason} -> | |||
erlang:error(Reason,[Filename]) | |||
end. | |||
%% @doc load a snapshot from a given file. The format of the data in the | |||
%% file can be either the same as output by {@link snapshot_save()}, | |||
%% or the output obtained by calling | |||
%% `{erlang:memory(),[{A,erlang:system_info({allocator,A})} || A <- erlang:system_info(alloc_util_allocators)++[sys_alloc,mseg_alloc]]}.' | |||
%% and storing it in a file. | |||
%% If the latter option is taken, please remember to add a full stop at the end | |||
%% of the resulting Erlang term, as this function uses `file:consult/1' to load | |||
%% the file. | |||
%% | |||
%% Example usage: | |||
%% | |||
%%```On target machine: | |||
%% 1> recon_alloc:snapshot(). | |||
%% undefined | |||
%% 2> recon_alloc:memory(used). | |||
%% 18411064 | |||
%% 3> recon_alloc:snapshot_save("recon_snapshot.terms"). | |||
%% ok | |||
%% | |||
%% On other machine: | |||
%% 1> recon_alloc:snapshot_load("recon_snapshot.terms"). | |||
%% undefined | |||
%% 2> recon_alloc:memory(used). | |||
%% 18411064''' | |||
%% | |||
-spec snapshot_load(Filename) -> snapshot() | undefined when | |||
Filename :: file:name(). | |||
snapshot_load(Filename) -> | |||
{ok,[Terms]} = file:consult(Filename), | |||
Snapshot = | |||
case Terms of | |||
%% We handle someone using | |||
%% {erlang:memory(), | |||
%% [{A,erlang:system_info({allocator,A})} || | |||
%% A <- erlang:system_info(alloc_util_allocators)++[sys_alloc,mseg_alloc]]} | |||
%% to dump data. | |||
{M,[{Alloc,_D}|_] = Allocs} when is_atom(Alloc) -> | |||
{M,[{{A,N},lists:sort(proplists:delete(versions,Props))} || | |||
{A,Instances = [_|_]} <- Allocs, | |||
{_, N, Props} <- Instances]}; | |||
%% We assume someone used recon_alloc:snapshot() to store this one | |||
{M,Allocs} -> | |||
{M,[{AN,lists:sort(proplists:delete(versions,Props))} || | |||
{AN, Props} <- Allocs]} | |||
end, | |||
put(recon_alloc_snapshot,Snapshot). | |||
%%%%%%%%%%%%%%%%%%%%%%%%% | |||
%%% Handling of units %%% | |||
%%%%%%%%%%%%%%%%%%%%%%%%% | |||
%% @doc set the current unit to be used by recon_alloc. This effects all | |||
%% functions that return bytes. | |||
%% | |||
%% Eg. | |||
%% ```1> recon_alloc:memory(used,current). | |||
%% 17548752 | |||
%% 2> recon_alloc:set_unit(kilobyte). | |||
%% undefined | |||
%% 3> recon_alloc:memory(used,current). | |||
%% 17576.90625''' | |||
%% | |||
-spec set_unit(byte | kilobyte | megabyte | gigabyte) -> ok. | |||
set_unit(byte) -> | |||
put(recon_alloc_unit,undefined); | |||
set_unit(kilobyte) -> | |||
put(recon_alloc_unit,1024); | |||
set_unit(megabyte) -> | |||
put(recon_alloc_unit,1024*1024); | |||
set_unit(gigabyte) -> | |||
put(recon_alloc_unit,1024*1024*1024). | |||
conv({Mem,Allocs} = D) -> | |||
case get(recon_alloc_unit) of | |||
undefined -> | |||
D; | |||
Factor -> | |||
{conv_mem(Mem,Factor),conv_alloc(Allocs,Factor)} | |||
end. | |||
conv_mem(Mem,Factor) -> | |||
[{T,M / Factor} || {T,M} <- Mem]. | |||
conv_alloc([{{sys_alloc,_I},_Props} = Alloc|R], Factor) -> | |||
[Alloc|conv_alloc(R,Factor)]; | |||
conv_alloc([{{mseg_alloc,_I} = AI,Props}|R], Factor) -> | |||
MemKind = orddict:fetch(memkind,Props), | |||
Status = orddict:fetch(status,MemKind), | |||
{segments_size,Curr,Last,Max} = lists:keyfind(segments_size,1,Status), | |||
NewSegSize = {segments_size,Curr/Factor,Last/Factor,Max/Factor}, | |||
NewStatus = lists:keyreplace(segments_size,1,Status,NewSegSize), | |||
NewProps = orddict:store(memkind,orddict:store(status,NewStatus,MemKind), | |||
Props), | |||
[{AI,NewProps}|conv_alloc(R,Factor)]; | |||
conv_alloc([{AI,Props}|R], Factor) -> | |||
FactorFun = fun({T,Curr}) when | |||
T =:= blocks_size; T =:= carriers_size -> | |||
{T,Curr/Factor}; | |||
({T,Curr,Last,Max}) when | |||
T =:= blocks_size; T =:= carriers_size; | |||
T =:= mseg_alloc_carriers_size; | |||
T =:= sys_alloc_carriers_size -> | |||
{T,Curr/Factor,Last/Factor,Max/Factor}; | |||
(T) -> | |||
T | |||
end, | |||
NewMbcsProp = [FactorFun(Prop) || Prop <- orddict:fetch(mbcs,Props)], | |||
NewSbcsProp = [FactorFun(Prop) || Prop <- orddict:fetch(sbcs,Props)], | |||
NewProps = orddict:store(sbcs,NewSbcsProp, | |||
orddict:store(mbcs,NewMbcsProp,Props)), | |||
case orddict:find(mbcs_pool,Props) of | |||
error -> | |||
[{AI,NewProps}|conv_alloc(R,Factor)]; | |||
{ok,MbcsPoolProps} -> | |||
NewMbcsPoolProp = [FactorFun(Prop) || Prop <- MbcsPoolProps], | |||
NewPoolProps = orddict:store(mbcs_pool,NewMbcsPoolProp,NewProps), | |||
[{AI,NewPoolProps}|conv_alloc(R,Factor)] | |||
end; | |||
conv_alloc([],_Factor) -> | |||
[]. | |||
%%%%%%%%%%%%%%% | |||
%%% Private %%% | |||
%%%%%%%%%%%%%%% | |||
%% Sort on small usage vs large size. | |||
%% The weight cares about both the sbcs and mbcs values, and also | |||
%% returns a proplist of possibly interesting values. | |||
weighed_values({SbcsBlockSize, SbcsCarrierSize}, | |||
{MbcsBlockSize, MbcsCarrierSize}) -> | |||
SbcsUsage = usage(SbcsBlockSize, SbcsCarrierSize), | |||
MbcsUsage = usage(MbcsBlockSize, MbcsCarrierSize), | |||
SbcsWeight = (1.00 - SbcsUsage)*SbcsCarrierSize, | |||
MbcsWeight = (1.00 - MbcsUsage)*MbcsCarrierSize, | |||
Weight = SbcsWeight + MbcsWeight, | |||
{Weight, [{sbcs_usage, SbcsUsage}, | |||
{mbcs_usage, MbcsUsage}, | |||
{sbcs_block_size, SbcsBlockSize}, | |||
{sbcs_carriers_size, SbcsCarrierSize}, | |||
{mbcs_block_size, MbcsBlockSize}, | |||
{mbcs_carriers_size, MbcsCarrierSize}]}. | |||
%% Returns the `BlockSize/CarrierSize' as a 0.0 -> 1.0 percentage, | |||
%% but also takes 0/0 to be 100% to make working with sorting and | |||
%% weights simpler. | |||
usage(0,0) -> 1.00; | |||
usage(0.0,0.0) -> 1.00; | |||
%usage(N,0) -> ???; | |||
usage(Block,Carrier) -> Block/Carrier. | |||
%% Calculation for the average of blocks being used. | |||
average_calc([]) -> | |||
[]; | |||
average_calc([{{Instance,Type,count},Ct},{{Instance,Type,size},Size}|Rest]) -> | |||
case {Size,Ct} of | |||
{_,0} when Size == 0 -> [{Instance, Type, 0} | average_calc(Rest)]; | |||
_ -> [{Instance,Type,Size/Ct} | average_calc(Rest)] | |||
end. | |||
%% Regrouping/merging values together in proplists | |||
average_group([]) -> []; | |||
average_group([{Instance,Type1,N},{Instance,Type2,M} | Rest]) -> | |||
[{Instance,[{Type1,N},{Type2,M}]} | average_group(Rest)]. | |||
%% Get the total carrier size | |||
container_size(Props, Keyword, Container) -> | |||
Sbcs = container_value(Props, Keyword, sbcs, Container), | |||
Mbcs = container_value(Props, Keyword, mbcs, Container), | |||
Sbcs+Mbcs. | |||
container_value(Props, Keyword, Type, Container) | |||
when is_atom(Keyword) -> | |||
container_value(Props, key2pos(Keyword), Type, Container); | |||
container_value(Props, Pos, mbcs = Type, Container) | |||
when Pos == ?CURRENT_POS, | |||
((Container =:= blocks) or (Container =:= blocks_size) | |||
or (Container =:= carriers) or (Container =:= carriers_size))-> | |||
%% We include the mbcs_pool into the value for mbcs. | |||
%% The mbcs_pool contains carriers that have been abandoned | |||
%% by the specific allocator instance and can therefore be | |||
%% grabbed by another instance of the same type. | |||
%% The pool was added in R16B02 and enabled by default in 17.0. | |||
%% See erts/emulator/internal_docs/CarrierMigration.md in | |||
%% Erlang/OTP repo for more details. | |||
Pool = case proplists:get_value(mbcs_pool, Props) of | |||
PoolProps when PoolProps =/= undefined -> | |||
element(Pos,lists:keyfind(Container, 1, PoolProps)); | |||
_ -> 0 | |||
end, | |||
TypeProps = proplists:get_value(Type, Props), | |||
Pool + element(Pos,lists:keyfind(Container, 1, TypeProps)); | |||
container_value(Props, Pos, Type, Container) | |||
when Type =:= sbcs; Type =:= mbcs -> | |||
TypeProps = proplists:get_value(Type, Props), | |||
element(Pos,lists:keyfind(Container, 1, TypeProps)). | |||
%% Create a new snapshot | |||
snapshot_int() -> | |||
{erlang:memory(),allocators()}. | |||
%% If no snapshot has been taken/loaded then we use current values | |||
snapshot_get_int() -> | |||
case snapshot_get() of | |||
undefined -> | |||
conv(snapshot_int()); | |||
Snapshot -> | |||
conv(Snapshot) | |||
end. | |||
%% Get the alloc part of a snapshot | |||
alloc() -> | |||
{_Mem,Allocs} = snapshot_get_int(), | |||
Allocs. | |||
alloc(Type) -> | |||
[{{T,Instance},Props} || {{T,Instance},Props} <- alloc(), | |||
lists:member(T,Type)]. | |||
%% Get only alloc_util allocs | |||
util_alloc() -> | |||
alloc(?UTIL_ALLOCATORS). | |||
key2pos(current) -> | |||
?CURRENT_POS; | |||
key2pos(max) -> | |||
?MAX_POS. |
@ -0,0 +1,285 @@ | |||
%%% @author Fred Hebert <mononcqc@ferd.ca> | |||
%%% [http://ferd.ca/] | |||
%%% @doc Regroups useful functionality used by recon when dealing with data | |||
%%% from the node. The functions in this module allow quick runtime access | |||
%%% to fancier behaviour than what would be done using recon module itself. | |||
%%% @end | |||
-module(recon_lib). | |||
-export([sliding_window/2, sample/2, count/1, | |||
port_list/1, port_list/2, | |||
proc_attrs/1, proc_attrs/2, | |||
inet_attrs/1, inet_attrs/2, | |||
triple_to_pid/3, term_to_pid/1, | |||
term_to_port/1, | |||
time_map/5, time_fold/6, | |||
scheduler_usage_diff/2, | |||
sublist_top_n_attrs/2]). | |||
%% private exports | |||
-export([binary_memory/1]). | |||
-type diff() :: [recon:proc_attrs() | recon:inet_attrs()]. | |||
%% @doc Compare two samples and return a list based on some key. The type mentioned | |||
%% for the structure is `diff()' (`{Key,Val,Other}'), which is compatible with | |||
%% the {@link recon:proc_attrs()} type. | |||
-spec sliding_window(First::diff(), Last::diff()) -> diff(). | |||
sliding_window(First, Last) -> | |||
Dict = lists:foldl( | |||
fun({Key, {Current, Other}}, Acc) -> | |||
dict:update(Key, | |||
fun({Old,_Other}) -> {Current-Old, Other} end, | |||
{Current, Other}, | |||
Acc) | |||
end, | |||
dict:from_list([{K,{V,O}} || {K,V,O} <- First]), | |||
[{K,{V,O}} || {K,V,O} <- Last] | |||
), | |||
[{K,V,O} || {K,{V,O}} <- dict:to_list(Dict)]. | |||
%% @doc Runs a fun once, waits `Ms', runs the fun again, | |||
%% and returns both results. | |||
-spec sample(Ms::non_neg_integer(), fun(() -> term())) -> | |||
{First::term(), Second::term()}. | |||
sample(Delay, Fun) -> | |||
First = Fun(), | |||
timer:sleep(Delay), | |||
Second = Fun(), | |||
{First, Second}. | |||
%% @doc Takes a list of terms, and counts how often each of | |||
%% them appears in the list. The list returned is in no | |||
%% particular order. | |||
-spec count([term()]) -> [{term(), Count::integer()}]. | |||
count(Terms) -> | |||
Dict = lists:foldl( | |||
fun(Val, Acc) -> dict:update_counter(Val, 1, Acc) end, | |||
dict:new(), | |||
Terms | |||
), | |||
dict:to_list(Dict). | |||
%% @doc Returns a list of all the open ports in the VM, coupled with | |||
%% one of the properties desired from `erlang:port_info/1-2'. | |||
-spec port_list(Attr::atom()) -> [{port(), term()}]. | |||
port_list(Attr) -> | |||
[{Port,Val} || Port <- erlang:ports(), | |||
{_, Val} <- [erlang:port_info(Port, Attr)]]. | |||
%% @doc Returns a list of all the open ports in the VM, but only | |||
%% if the `Attr''s resulting value matches `Val'. `Attr' must be | |||
%% a property accepted by `erlang:port_info/2'. | |||
-spec port_list(Attr::atom(), term()) -> [port()]. | |||
port_list(Attr, Val) -> | |||
[Port || Port <- erlang:ports(), | |||
{Attr, Val} =:= erlang:port_info(Port, Attr)]. | |||
%% @doc Returns the attributes ({@link recon:proc_attrs()}) of | |||
%% all processes of the node, except the caller. | |||
-spec proc_attrs(term()) -> [recon:proc_attrs()]. | |||
proc_attrs(AttrName) -> | |||
Self = self(), | |||
[Attrs || Pid <- processes(), | |||
Pid =/= Self, | |||
{ok, Attrs} <- [proc_attrs(AttrName, Pid)] | |||
]. | |||
%% @doc Returns the attributes of a given process. This form of attributes | |||
%% is standard for most comparison functions for processes in recon. | |||
%% | |||
%% A special attribute is `binary_memory', which will reduce the memory used | |||
%% by the process for binary data on the global heap. | |||
-spec proc_attrs(term(), pid()) -> {ok, recon:proc_attrs()} | {error, term()}. | |||
proc_attrs(binary_memory, Pid) -> | |||
case process_info(Pid, [binary, registered_name, | |||
current_function, initial_call]) of | |||
[{_, Bins}, {registered_name,Name}, Init, Cur] -> | |||
{ok, {Pid, binary_memory(Bins), [Name || is_atom(Name)]++[Init, Cur]}}; | |||
undefined -> | |||
{error, undefined} | |||
end; | |||
proc_attrs(AttrName, Pid) -> | |||
case process_info(Pid, [AttrName, registered_name, | |||
current_function, initial_call]) of | |||
[{_, Attr}, {registered_name,Name}, Init, Cur] -> | |||
{ok, {Pid, Attr, [Name || is_atom(Name)]++[Init, Cur]}}; | |||
undefined -> | |||
{error, undefined} | |||
end. | |||
%% @doc Returns the attributes ({@link recon:inet_attrs()}) of | |||
%% all inet ports (UDP, SCTP, TCP) of the node. | |||
-spec inet_attrs(term()) -> [recon:inet_attrs()]. | |||
inet_attrs(AttrName) -> | |||
Ports = [Port || Port <- erlang:ports(), | |||
{_, Name} <- [erlang:port_info(Port, name)], | |||
Name =:= "tcp_inet" orelse | |||
Name =:= "udp_inet" orelse | |||
Name =:= "sctp_inet"], | |||
[Attrs || Port <- Ports, | |||
{ok, Attrs} <- [inet_attrs(AttrName, Port)]]. | |||
%% @doc Returns the attributes required for a given inet port (UDP, | |||
%% SCTP, TCP). This form of attributes is standard for most comparison | |||
%% functions for processes in recon. | |||
-spec inet_attrs(AttributeName, port()) -> {ok,recon:inet_attrs()} | |||
| {error,term()} when | |||
AttributeName :: 'recv_cnt' | 'recv_oct' | 'send_cnt' | 'send_oct' | |||
| 'cnt' | 'oct'. | |||
inet_attrs(Attr, Port) -> | |||
Attrs = case Attr of | |||
cnt -> [recv_cnt, send_cnt]; | |||
oct -> [recv_oct, send_oct]; | |||
_ -> [Attr] | |||
end, | |||
case inet:getstat(Port, Attrs) of | |||
{ok, Props} -> | |||
ValSum = lists:foldl(fun({_,X},Y) -> X+Y end, 0, Props), | |||
{ok, {Port,ValSum,Props}}; | |||
{error, Reason} -> | |||
{error, Reason} | |||
end. | |||
%% @doc Equivalent of `pid(X,Y,Z)' in the Erlang shell. | |||
-spec triple_to_pid(N,N,N) -> pid() when | |||
N :: non_neg_integer(). | |||
triple_to_pid(X, Y, Z) -> | |||
list_to_pid("<" ++ integer_to_list(X) ++ "." ++ | |||
integer_to_list(Y) ++ "." ++ | |||
integer_to_list(Z) ++ ">"). | |||
%% @doc Transforms a given term to a pid. | |||
-spec term_to_pid(recon:pid_term()) -> pid(). | |||
term_to_pid(Pid) when is_pid(Pid) -> Pid; | |||
term_to_pid(Name) when is_atom(Name) -> whereis(Name); | |||
term_to_pid(List = "<0."++_) -> list_to_pid(List); | |||
term_to_pid(Binary = <<"<0.", _/binary>>) -> list_to_pid(binary_to_list(Binary)); | |||
term_to_pid({global, Name}) -> global:whereis_name(Name); | |||
term_to_pid({via, Module, Name}) -> Module:whereis_name(Name); | |||
term_to_pid({X,Y,Z}) when is_integer(X), is_integer(Y), is_integer(Z) -> | |||
triple_to_pid(X,Y,Z). | |||
%% @doc Transforms a given term to a port | |||
-spec term_to_port(recon:port_term()) -> port(). | |||
term_to_port(Port) when is_port(Port) -> Port; | |||
term_to_port(Name) when is_atom(Name) -> whereis(Name); | |||
term_to_port("#Port<0."++Id) -> | |||
N = list_to_integer(lists:sublist(Id, length(Id)-1)), % drop trailing '>' | |||
term_to_port(N); | |||
term_to_port(N) when is_integer(N) -> | |||
%% We rebuild the term from the int received: | |||
%% http://www.erlang.org/doc/apps/erts/erl_ext_dist.html#id86892 | |||
Name = iolist_to_binary(atom_to_list(node())), | |||
NameLen = iolist_size(Name), | |||
Vsn = binary:last(term_to_binary(self())), | |||
Bin = <<131, % term encoding value | |||
102, % port tag | |||
100, % atom ext tag, used for node name | |||
NameLen:2/unit:8, | |||
Name:NameLen/binary, | |||
N:4/unit:8, % actual counter value | |||
Vsn:8>>, % version | |||
binary_to_term(Bin). | |||
%% @doc Calls a given function every `Interval' milliseconds and supports | |||
%% a map-like interface (each result is modified and returned) | |||
-spec time_map(N, Interval, Fun, State, MapFun) -> [term()] when | |||
N :: non_neg_integer(), | |||
Interval :: pos_integer(), | |||
Fun :: fun((State) -> {term(), State}), | |||
State :: term(), | |||
MapFun :: fun((_) -> term()). | |||
time_map(0, _, _, _, _) -> | |||
[]; | |||
time_map(N, Interval, Fun, State, MapFun) -> | |||
{Res, NewState} = Fun(State), | |||
timer:sleep(Interval), | |||
[MapFun(Res) | time_map(N-1,Interval,Fun,NewState,MapFun)]. | |||
%% @doc Calls a given function every `Interval' milliseconds and supports | |||
%% a fold-like interface (each result is modified and accumulated) | |||
-spec time_fold(N, Interval, Fun, State, FoldFun, Init) -> [term()] when | |||
N :: non_neg_integer(), | |||
Interval :: pos_integer(), | |||
Fun :: fun((State) -> {term(), State}), | |||
State :: term(), | |||
FoldFun :: fun((term(), Init) -> Init), | |||
Init :: term(). | |||
time_fold(0, _, _, _, _, Acc) -> | |||
Acc; | |||
time_fold(N, Interval, Fun, State, FoldFun, Init) -> | |||
timer:sleep(Interval), | |||
{Res, NewState} = Fun(State), | |||
Acc = FoldFun(Res,Init), | |||
time_fold(N-1,Interval,Fun,NewState,FoldFun,Acc). | |||
%% @doc Diffs two runs of erlang:statistics(scheduler_wall_time) and | |||
%% returns usage metrics in terms of cores and 0..1 percentages. | |||
-spec scheduler_usage_diff(SchedTime, SchedTime) -> undefined | [{SchedulerId, Usage}] when | |||
SchedTime :: [{SchedulerId, ActiveTime, TotalTime}], | |||
SchedulerId :: pos_integer(), | |||
Usage :: number(), | |||
ActiveTime :: non_neg_integer(), | |||
TotalTime :: non_neg_integer(). | |||
scheduler_usage_diff(First, Last) when First =:= undefined orelse Last =:= undefined -> | |||
undefined; | |||
scheduler_usage_diff(First, Last) -> | |||
lists:map( | |||
fun ({{I, _A0, T}, {I, _A1, T}}) -> {I, 0.0}; % Avoid divide by zero | |||
({{I, A0, T0}, {I, A1, T1}}) -> {I, (A1 - A0)/(T1 - T0)} | |||
end, | |||
lists:zip(lists:sort(First), lists:sort(Last)) | |||
). | |||
%% @doc Returns the top n element of a list of process or inet attributes | |||
-spec sublist_top_n_attrs([Attrs], pos_integer()) -> [Attrs] | |||
when Attrs :: recon:proc_attrs() | recon:inet_attrs(). | |||
sublist_top_n_attrs(_, 0) -> | |||
%% matching lists:sublist/2 behaviour | |||
[]; | |||
sublist_top_n_attrs(List, Len) -> | |||
pheap_fill(List, Len, []). | |||
%% @private crush binaries from process_info into their amount of place | |||
%% taken in memory. | |||
binary_memory(Bins) -> | |||
lists:foldl(fun({_,Mem,_}, Tot) -> Mem+Tot end, 0, Bins). | |||
%%%%%%%%%%%%%%% | |||
%%% PRIVATE %%% | |||
%%%%%%%%%%%%%%% | |||
pheap_fill(List, 0, Heap) -> | |||
pheap_full(List, Heap); | |||
pheap_fill([], _, Heap) -> | |||
pheap_to_list(Heap, []); | |||
pheap_fill([{Y, X, _} = H|T], N, Heap) -> | |||
pheap_fill(T, N-1, insert({{X, Y}, H}, Heap)). | |||
pheap_full([], Heap) -> | |||
pheap_to_list(Heap, []); | |||
pheap_full([{Y, X, _} = H|T], [{K, _}|HeapT] = Heap) -> | |||
case {X, Y} of | |||
N when N > K -> | |||
pheap_full(T, insert({N, H}, merge_pairs(HeapT))); | |||
_ -> | |||
pheap_full(T, Heap) | |||
end. | |||
pheap_to_list([], Acc) -> Acc; | |||
pheap_to_list([{_, H}|T], Acc) -> | |||
pheap_to_list(merge_pairs(T), [H|Acc]). | |||
-compile({inline, [insert/2, merge/2]}). | |||
insert(E, []) -> [E]; %% merge([E], H) | |||
insert(E, [E2|_] = H) when E =< E2 -> [E, H]; | |||
insert(E, [E2|H]) -> [E2, [E]|H]. | |||
merge(H1, []) -> H1; | |||
merge([E1|H1], [E2|_]=H2) when E1 =< E2 -> [E1, H2|H1]; | |||
merge(H1, [E2|H2]) -> [E2, H1|H2]. | |||
merge_pairs([]) -> []; | |||
merge_pairs([H]) -> H; | |||
merge_pairs([A, B|T]) -> merge(merge(A, B), merge_pairs(T)). | |||
@ -0,0 +1,208 @@ | |||
%%%------------------------------------------------------------------- | |||
%%% @author bartlomiej.gorny@erlang-solutions.com | |||
%%% @doc | |||
%%% This module handles formatting maps. | |||
%% It allows for trimming output to selected fields, or to nothing at all. It also adds a label | |||
%% to a printout. | |||
%% To set up a limit for a map, you need to give recon a way to tell the map you want to | |||
%% trim from all the other maps, so you have to provide something like a 'type definition'. | |||
%% It can be either another map which is compared to the arg, or a fun. | |||
%%% @end | |||
%%%------------------------------------------------------------------- | |||
-module(recon_map). | |||
-author("bartlomiej.gorny@erlang-solutions.com"). | |||
%% API | |||
-export([limit/3, list/0, is_active/0, clear/0, remove/1, rename/2]). | |||
-export([process_map/1]). | |||
-type map_label() :: atom(). | |||
-type pattern() :: map() | function(). | |||
-type limit() :: all | none | atom() | binary() | [any()]. | |||
%% @doc quickly check if we want to do any record formatting | |||
-spec is_active() -> boolean(). | |||
is_active() -> | |||
case whereis(recon_ets_maps) of | |||
undefined -> false; | |||
_ -> true | |||
end. | |||
%% @doc remove all imported definitions, destroy the table, clean up | |||
clear() -> | |||
maybe_kill(recon_ets_maps), | |||
ok. | |||
%% @doc Limit output to selected keys of a map (can be 'none', 'all', a key or a list of keys). | |||
%% Pattern selects maps to process: a "pattern" is just a map, and if all key/value pairs of a pattern | |||
%% are present in a map (in other words, the pattern is a subset), then we say the map matches | |||
%% and we process it accordingly (apply the limit). | |||
%% | |||
%% Patterns are applied in alphabetical order, until a match is found. | |||
%% | |||
%% Instead of a pattern you can also provide a function which will take a map and return a boolean. | |||
%% @end | |||
-spec limit(map_label(), pattern(), limit()) -> ok | {error, any()}. | |||
limit(Label, #{} = Pattern, Limit) when is_atom(Label) -> | |||
store_pattern(Label, Pattern, Limit); | |||
limit(Label, Pattern, Limit) when is_atom(Label), is_function(Pattern) -> | |||
store_pattern(Label, Pattern, Limit). | |||
%% @doc prints out all "known" map definitions and their limit settings. | |||
%% Printout tells a map's name, the matching fields required, and the limit options. | |||
%% @end | |||
list() -> | |||
ensure_table_exists(), | |||
io:format("~nmap definitions and limits:~n"), | |||
list(ets:tab2list(patterns_table_name())). | |||
%% @doc remove a given map entry | |||
-spec remove(map_label()) -> true. | |||
remove(Label) -> | |||
ensure_table_exists(), | |||
ets:delete(patterns_table_name(), Label). | |||
%% @doc rename a given map entry, which allows to to change priorities for | |||
%% matching. The first argument is the current name, and the second | |||
%% argument is the new name. | |||
-spec rename(map_label(), map_label()) -> renamed | missing. | |||
rename(Name, NewName) -> | |||
ensure_table_exists(), | |||
case ets:lookup(patterns_table_name(), Name) of | |||
[{Name, Pattern, Limit}] -> | |||
ets:insert(patterns_table_name(), {NewName, Pattern, Limit}), | |||
ets:delete(patterns_table_name(), Name), | |||
renamed; | |||
[] -> | |||
missing | |||
end. | |||
%% @doc prints out all "known" map filter definitions and their settings. | |||
%% Printout tells the map's label, the matching patterns, and the limit options | |||
%% @end | |||
list([]) -> | |||
io:format("~n"), | |||
ok; | |||
list([{Label, Pattern, Limit} | Rest]) -> | |||
io:format("~p: ~p -> ~p~n", [Label, Pattern, Limit]), | |||
list(Rest). | |||
%% @private given a map, scans saved patterns for one that matches; if found, returns a label | |||
%% and a map with limits applied; otherwise returns 'none' and original map. | |||
%% Pattern can be: | |||
%% <ul> | |||
%% <li> a map - then each key in pattern is checked for equality with the map in question</li> | |||
%% <li> a fun(map()) -> boolean()</li> | |||
%% </ul> | |||
-spec process_map(map()) -> map() | {atom(), map()}. | |||
process_map(M) -> | |||
process_map(M, ets:tab2list(patterns_table_name())). | |||
process_map(M, []) -> | |||
M; | |||
process_map(M, [{Label, Pattern, Limit} | Rest]) -> | |||
case map_matches(M, Pattern) of | |||
true -> | |||
{Label, apply_map_limits(Limit, M)}; | |||
false -> | |||
process_map(M, Rest) | |||
end. | |||
map_matches(#{} = M, Pattern) when is_function(Pattern) -> | |||
Pattern(M); | |||
map_matches(_, []) -> | |||
true; | |||
map_matches(M, [{K, V} | Rest]) -> | |||
case maps:is_key(K, M) of | |||
true -> | |||
case maps:get(K, M) of | |||
V -> | |||
map_matches(M, Rest); | |||
_ -> | |||
false | |||
end; | |||
false -> | |||
false | |||
end. | |||
apply_map_limits(none, M) -> | |||
M; | |||
apply_map_limits(all, _) -> | |||
#{}; | |||
apply_map_limits(Fields, M) -> | |||
maps:with(Fields, M). | |||
patterns_table_name() -> recon_map_patterns. | |||
store_pattern(Label, Pattern, Limit) -> | |||
ensure_table_exists(), | |||
ets:insert(patterns_table_name(), {Label, prepare_pattern(Pattern), prepare_limit(Limit)}), | |||
ok. | |||
prepare_limit(all) -> all; | |||
prepare_limit(none) -> none; | |||
prepare_limit(Limit) when is_binary(Limit) -> [Limit]; | |||
prepare_limit(Limit) when is_atom(Limit) -> [Limit]; | |||
prepare_limit(Limit) when is_list(Limit) -> Limit. | |||
prepare_pattern(Pattern) when is_function(Pattern) -> Pattern; | |||
prepare_pattern(Pattern) when is_map(Pattern) -> maps:to_list(Pattern). | |||
ensure_table_exists() -> | |||
case ets:info(patterns_table_name()) of | |||
undefined -> | |||
case whereis(recon_ets_maps) of | |||
undefined -> | |||
Parent = self(), | |||
Ref = make_ref(), | |||
%% attach to the currently running session | |||
{Pid, MonRef} = spawn_monitor(fun() -> | |||
register(recon_ets_maps, self()), | |||
ets:new(patterns_table_name(), [ordered_set, public, named_table]), | |||
Parent ! Ref, | |||
ets_keeper() | |||
end), | |||
receive | |||
Ref -> | |||
erlang:demonitor(MonRef, [flush]), | |||
Pid; | |||
{'DOWN', MonRef, _, _, Reason} -> | |||
error(Reason) | |||
end; | |||
Pid -> | |||
Pid | |||
end; | |||
Pid -> | |||
Pid | |||
end. | |||
ets_keeper() -> | |||
receive | |||
stop -> ok; | |||
_ -> ets_keeper() | |||
end. | |||
%%%%%%%%%%%%%%% | |||
%%% HELPERS %%% | |||
%%%%%%%%%%%%%%% | |||
maybe_kill(Name) -> | |||
case whereis(Name) of | |||
undefined -> | |||
ok; | |||
Pid -> | |||
unlink(Pid), | |||
exit(Pid, kill), | |||
wait_for_death(Pid, Name) | |||
end. | |||
wait_for_death(Pid, Name) -> | |||
case is_process_alive(Pid) orelse whereis(Name) =:= Pid of | |||
true -> | |||
timer:sleep(10), | |||
wait_for_death(Pid, Name); | |||
false -> | |||
ok | |||
end. | |||
@ -0,0 +1,279 @@ | |||
%%%------------------------------------------------------------------- | |||
%%% @author bartlomiej.gorny@erlang-solutions.com | |||
%%% @doc | |||
%%% This module handles formatting records for known record types. | |||
%%% Record definitions are imported from modules by user. Definitions are | |||
%%% distinguished by record name and its arity, if you have multiple records | |||
%%% of the same name and size, you have to choose one of them and some of your | |||
%%% records may be wrongly labelled. You can manipulate your definition list by | |||
%%% using import/1 and clear/1, and check which definitions are in use by executing | |||
%%% list/0. | |||
%%% @end | |||
%%%------------------------------------------------------------------- | |||
-module(recon_rec). | |||
-author("bartlomiej.gorny@erlang-solutions.com"). | |||
%% API | |||
-export([is_active/0]). | |||
-export([import/1, clear/1, clear/0, list/0, get_list/0, limit/3]). | |||
-export([format_tuple/1]). | |||
-ifdef(TEST). | |||
-export([lookup_record/2]). | |||
-endif. | |||
% basic types | |||
-type field() :: atom(). | |||
-type record_name() :: atom(). | |||
% compound | |||
-type limit() :: all | none | field() | [field()]. | |||
-type listentry() :: {module(), record_name(), [field()], limit()}. | |||
-type import_result() :: {imported, module(), record_name(), arity()} | |||
| {overwritten, module(), record_name(), arity()} | |||
| {ignored, module(), record_name(), arity(), module()}. | |||
%% @doc import record definitions from a module. If a record definition of the same name | |||
%% and arity has already been imported from another module then the new | |||
%% definition is ignored (returned info tells you from which module the existing definition was imported). | |||
%% You have to choose one and possibly remove the old one using | |||
%% clear/1. Supports importing multiple modules at once (by giving a list of atoms as | |||
%% an argument). | |||
%% @end | |||
-spec import(module() | [module()]) -> import_result() | [import_result()]. | |||
import(Modules) when is_list(Modules) -> | |||
lists:foldl(fun import/2, [], Modules); | |||
import(Module) -> | |||
import(Module, []). | |||
%% @doc quickly check if we want to do any record formatting | |||
-spec is_active() -> boolean(). | |||
is_active() -> | |||
case whereis(recon_ets) of | |||
undefined -> false; | |||
_ -> true | |||
end. | |||
%% @doc remove definitions imported from a module. | |||
clear(Module) -> | |||
lists:map(fun(R) -> rem_for_module(R, Module) end, ets:tab2list(records_table_name())). | |||
%% @doc remove all imported definitions, destroy the table, clean up | |||
clear() -> | |||
maybe_kill(recon_ets), | |||
ok. | |||
%% @doc prints out all "known" (imported) record definitions and their limit settings. | |||
%% Printout tells module a record originates from, its name and a list of field names, | |||
%% plus the record's arity (may be handy if handling big records) and a list of field it | |||
%% limits its output to, if set. | |||
%% @end | |||
list() -> | |||
F = fun({Module, Name, Fields, Limits}) -> | |||
Fnames = lists:map(fun atom_to_list/1, Fields), | |||
Flds = join(",", Fnames), | |||
io:format("~p: #~p(~p){~s} ~p~n", | |||
[Module, Name, length(Fields), Flds, Limits]) | |||
end, | |||
io:format("Module: #Name(Size){<Fields>} Limits~n==========~n", []), | |||
lists:foreach(F, get_list()). | |||
%% @doc returns a list of active record definitions | |||
-spec get_list() -> [listentry()]. | |||
get_list() -> | |||
ensure_table_exists(), | |||
Lst = lists:map(fun make_list_entry/1, ets:tab2list(records_table_name())), | |||
lists:sort(Lst). | |||
%% @doc Limit output to selected fields of a record (can be 'none', 'all', a field or a list of fields). | |||
%% Limit set to 'none' means there is no limit, and all fields are displayed; limit 'all' means that | |||
%% all fields are squashed and only record name will be shown. | |||
%% @end | |||
-spec limit(record_name(), arity(), limit()) -> ok | {error, any()}. | |||
limit(Name, Arity, Limit) when is_atom(Name), is_integer(Arity) -> | |||
case lookup_record(Name, Arity) of | |||
[] -> | |||
{error, record_unknown}; | |||
[{Key, Fields, Mod, _}] -> | |||
ets:insert(records_table_name(), {Key, Fields, Mod, Limit}), | |||
ok | |||
end. | |||
%% @private if a tuple is a known record, formats is as "#recname{field=value}", otherwise returns | |||
%% just a printout of a tuple. | |||
format_tuple(Tuple) -> | |||
ensure_table_exists(), | |||
First = element(1, Tuple), | |||
format_tuple(First, Tuple). | |||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% | |||
%% PRIVATE | |||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% | |||
make_list_entry({{Name, _}, Fields, Module, Limits}) -> | |||
FmtLimit = case Limits of | |||
[] -> none; | |||
Other -> Other | |||
end, | |||
{Module, Name, Fields, FmtLimit}. | |||
import(Module, ResultList) -> | |||
ensure_table_exists(), | |||
lists:foldl(fun(Rec, Res) -> store_record(Rec, Module, Res) end, | |||
ResultList, | |||
get_record_defs(Module)). | |||
store_record(Rec, Module, ResultList) -> | |||
{Name, Fields} = Rec, | |||
Arity = length(Fields), | |||
Result = case lookup_record(Name, Arity) of | |||
[] -> | |||
ets:insert(records_table_name(), rec_info(Rec, Module)), | |||
{imported, Module, Name, Arity}; | |||
[{_, _, Module, _}] -> | |||
ets:insert(records_table_name(), rec_info(Rec, Module)), | |||
{overwritten, Module, Name, Arity}; | |||
[{_, _, Mod, _}] -> | |||
{ignored, Module, Name, Arity, Mod} | |||
end, | |||
[Result | ResultList]. | |||
get_record_defs(Module) -> | |||
Path = code:which(Module), | |||
{ok,{_,[{abstract_code,{_,AC}}]}} = beam_lib:chunks(Path, [abstract_code]), | |||
lists:foldl(fun get_record/2, [], AC). | |||
get_record({attribute, _, record, Rec}, Acc) -> [Rec | Acc]; | |||
get_record(_, Acc) -> Acc. | |||
%% @private | |||
lookup_record(RecName, FieldCount) -> | |||
ensure_table_exists(), | |||
ets:lookup(records_table_name(), {RecName, FieldCount}). | |||
%% @private | |||
ensure_table_exists() -> | |||
case ets:info(records_table_name()) of | |||
undefined -> | |||
case whereis(recon_ets) of | |||
undefined -> | |||
Parent = self(), | |||
Ref = make_ref(), | |||
%% attach to the currently running session | |||
{Pid, MonRef} = spawn_monitor(fun() -> | |||
register(recon_ets, self()), | |||
ets:new(records_table_name(), [set, public, named_table]), | |||
Parent ! Ref, | |||
ets_keeper() | |||
end), | |||
receive | |||
Ref -> | |||
erlang:demonitor(MonRef, [flush]), | |||
Pid; | |||
{'DOWN', MonRef, _, _, Reason} -> | |||
error(Reason) | |||
end; | |||
Pid -> | |||
Pid | |||
end; | |||
Pid -> | |||
Pid | |||
end. | |||
records_table_name() -> recon_record_definitions. | |||
rec_info({Name, Fields}, Module) -> | |||
{{Name, length(Fields)}, field_names(Fields), Module, none}. | |||
rem_for_module({_, _, Module, _} = Rec, Module) -> | |||
ets:delete_object(records_table_name(), Rec); | |||
rem_for_module(_, _) -> | |||
ok. | |||
ets_keeper() -> | |||
receive | |||
stop -> ok; | |||
_ -> ets_keeper() | |||
end. | |||
field_names(Fields) -> | |||
lists:map(fun field_name/1, Fields). | |||
field_name({record_field, _, {atom, _, Name}}) -> Name; | |||
field_name({record_field, _, {atom, _, Name}, _Default}) -> Name; | |||
field_name({typed_record_field, Field, _Type}) -> field_name(Field). | |||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% | |||
%% FORMATTER | |||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% | |||
format_tuple(Name, Rec) when is_atom(Name) -> | |||
case lookup_record(Name, size(Rec) - 1) of | |||
[RecDef] -> format_record(Rec, RecDef); | |||
_ -> | |||
List = tuple_to_list(Rec), | |||
["{", join(", ", [recon_trace:format_trace_output(true, El) || El <- List]), "}"] | |||
end; | |||
format_tuple(_, Tuple) -> | |||
format_default(Tuple). | |||
format_default(Val) -> | |||
io_lib:format("~p", [Val]). | |||
format_record(Rec, {{Name, Arity}, Fields, _, Limits}) -> | |||
ExpectedLength = Arity + 1, | |||
case tuple_size(Rec) of | |||
ExpectedLength -> | |||
[_ | Values] = tuple_to_list(Rec), | |||
List = lists:zip(Fields, Values), | |||
LimitedList = apply_limits(List, Limits), | |||
["#", atom_to_list(Name), "{", | |||
join(", ", [format_kv(Key, Val) || {Key, Val} <- LimitedList]), | |||
"}"]; | |||
_ -> | |||
format_default(Rec) | |||
end. | |||
format_kv(Key, Val) -> | |||
%% Some messy mutually recursive calls we can't avoid | |||
[recon_trace:format_trace_output(true, Key), "=", recon_trace:format_trace_output(true, Val)]. | |||
apply_limits(List, none) -> List; | |||
apply_limits(_List, all) -> []; | |||
apply_limits(List, Field) when is_atom(Field) -> | |||
[{Field, proplists:get_value(Field, List)}, {more, '...'}]; | |||
apply_limits(List, Limits) -> | |||
lists:filter(fun({K, _}) -> lists:member(K, Limits) end, List) ++ [{more, '...'}]. | |||
%%%%%%%%%%%%%%% | |||
%%% HELPERS %%% | |||
%%%%%%%%%%%%%%% | |||
maybe_kill(Name) -> | |||
case whereis(Name) of | |||
undefined -> | |||
ok; | |||
Pid -> | |||
unlink(Pid), | |||
exit(Pid, kill), | |||
wait_for_death(Pid, Name) | |||
end. | |||
wait_for_death(Pid, Name) -> | |||
case is_process_alive(Pid) orelse whereis(Name) =:= Pid of | |||
true -> | |||
timer:sleep(10), | |||
wait_for_death(Pid, Name); | |||
false -> | |||
ok | |||
end. | |||
-ifdef(OTP_RELEASE). | |||
-spec join(term(), [term()]) -> [term()]. | |||
join(Sep, List) -> | |||
lists:join(Sep, List). | |||
-else. | |||
-spec join(string(), [string()]) -> string(). | |||
join(Sep, List) -> | |||
string:join(List, Sep). | |||
-endif. |
@ -0,0 +1,733 @@ | |||
%%% @author Fred Hebert <mononcqc@ferd.ca> | |||
%%% [http://ferd.ca/] | |||
%%% @doc | |||
%%% `recon_trace' is a module that handles tracing in a safe manner for single | |||
%%% Erlang nodes, currently for function calls only. Functionality includes: | |||
%%% | |||
%%% <ul> | |||
%%% <li>Nicer to use interface (arguably) than `dbg' or trace BIFs.</li> | |||
%%% <li>Protection against dumb decisions (matching all calls on a node | |||
%%% being traced, for example)</li> | |||
%%% <li>Adding safe guards in terms of absolute trace count or | |||
%%% rate-limitting</li> | |||
%%% <li>Nicer formatting than default traces</li> | |||
%%% </ul> | |||
%%% | |||
%%% == Tracing Erlang Code == | |||
%%% | |||
%%% The Erlang Trace BIFs allow to trace any Erlang code at all. They work in | |||
%%% two parts: pid specifications, and trace patterns. | |||
%%% | |||
%%% Pid specifications let you decide which processes to target. They can be | |||
%%% specific pids, `all' pids, `existing' pids, or `new' pids (those not | |||
%%% spawned at the time of the function call). | |||
%%% | |||
%%% The trace patterns represent functions. Functions can be specified in two | |||
%%% parts: specifying the modules, functions, and arguments, and then with | |||
%%% Erlang match specifications to add constraints to arguments (see | |||
%%% {@link calls/3} for details). | |||
%%% | |||
%%% What defines whether you get traced or not is the intersection of both: | |||
%%% | |||
%%% ``` | |||
%%% _,--------,_ _,--------,_ | |||
%%% ,-' `-,,-' `-, | |||
%%% ,-' ,-' '-, `-, | |||
%%% | Matching -' '- Matching | | |||
%%% | Pids | Getting | Trace | | |||
%%% | | Traced | Patterns | | |||
%%% | -, ,- | | |||
%%% '-, '-, ,-' ,-' | |||
%%% '-,_ _,-''-,_ _,-' | |||
%%% '--------' '--------' | |||
%%% ''' | |||
%%% | |||
%%% If either the pid specification excludes a process or a trace pattern | |||
%%% excludes a given call, no trace will be received. | |||
%%% | |||
%%% == Example Session == | |||
%%% | |||
%%% First let's trace the `queue:new' functions in any process: | |||
%%% | |||
%%% ``` | |||
%%% 1> recon_trace:calls({queue, new, '_'}, 1). | |||
%%% 1 | |||
%%% 13:14:34.086078 <0.44.0> queue:new() | |||
%%% Recon tracer rate limit tripped. | |||
%%% ''' | |||
%%% | |||
%%% The limit was set to `1' trace message at most, and `recon' let us | |||
%%% know when that limit was reached. | |||
%%% | |||
%%% Let's instead look for all the `queue:in/2' calls, to see what it is | |||
%%% we're inserting in queues: | |||
%%% | |||
%%% ``` | |||
%%% 2> recon_trace:calls({queue, in, 2}, 1). | |||
%%% 1 | |||
%%% 13:14:55.365157 <0.44.0> queue:in(a, {[],[]}) | |||
%%% Recon tracer rate limit tripped. | |||
%%% ''' | |||
%%% | |||
%%% In order to see the content we want, we should change the trace patterns | |||
%%% to use a `fun' that matches on all arguments in a list (`_') and returns | |||
%%% `return_trace()'. This last part will generate a second trace for each | |||
%%% call that includes the return value: | |||
%%% | |||
%%% ``` | |||
%%% 3> recon_trace:calls({queue, in, fun(_) -> return_trace() end}, 3). | |||
%%% 1 | |||
%%% | |||
%%% 13:15:27.655132 <0.44.0> queue:in(a, {[],[]}) | |||
%%% | |||
%%% 13:15:27.655467 <0.44.0> queue:in/2 --> {[a],[]} | |||
%%% | |||
%%% 13:15:27.757921 <0.44.0> queue:in(a, {[],[]}) | |||
%%% Recon tracer rate limit tripped. | |||
%%% ''' | |||
%%% | |||
%%% Matching on argument lists can be done in a more complex manner: | |||
%%% | |||
%%% ``` | |||
%%% 4> recon_trace:calls( | |||
%%% 4> {queue, '_', fun([A,_]) when is_list(A); is_integer(A) andalso A > 1 -> return_trace() end}, | |||
%%% 4> {10,100} | |||
%%% 4> ). | |||
%%% 32 | |||
%%% | |||
%%% 13:24:21.324309 <0.38.0> queue:in(3, {[],[]}) | |||
%%% | |||
%%% 13:24:21.371473 <0.38.0> queue:in/2 --> {[3],[]} | |||
%%% | |||
%%% 13:25:14.694865 <0.53.0> queue:split(4, {[10,9,8,7],[1,2,3,4,5,6]}) | |||
%%% | |||
%%% 13:25:14.695194 <0.53.0> queue:split/2 --> {{[4,3,2],[1]},{[10,9,8,7],[5,6]}} | |||
%%% | |||
%%% 5> recon_trace:clear(). | |||
%%% ok | |||
%%% ''' | |||
%%% | |||
%%% Note that in the pattern above, no specific function (<code>'_'</code>) was | |||
%%% matched against. Instead, the `fun' used restricted functions to those | |||
%%% having two arguments, the first of which is either a list or an integer | |||
%%% greater than `1'. | |||
%%% | |||
%%% The limit was also set using `{10,100}' instead of an integer, making the | |||
%%% rate-limitting at 10 messages per 100 milliseconds, instead of an absolute | |||
%%% value. | |||
%%% | |||
%%% Any tracing can be manually interrupted by calling `recon_trace:clear()', | |||
%%% or killing the shell process. | |||
%%% | |||
%%% Be aware that extremely broad patterns with lax rate-limitting (or very | |||
%%% high absolute limits) may impact your node's stability in ways | |||
%%% `recon_trace' cannot easily help you with. | |||
%%% | |||
%%% In doubt, start with the most restrictive tracing possible, with low | |||
%%% limits, and progressively increase your scope. | |||
%%% | |||
%%% See {@link calls/3} for more details and tracing possibilities. | |||
%%% | |||
%%% == Structure == | |||
%%% | |||
%%% This library is production-safe due to taking the following structure for | |||
%%% tracing: | |||
%%% | |||
%%% ``` | |||
%%% [IO/Group leader] <---------------------, | |||
%%% | | | |||
%%% [shell] ---> [tracer process] ----> [formatter] | |||
%%% ''' | |||
%%% | |||
%%% The tracer process receives trace messages from the node, and enforces | |||
%%% limits in absolute terms or trace rates, before forwarding the messages | |||
%%% to the formatter. This is done so the tracer can do as little work as | |||
%%% possible and never block while building up a large mailbox. | |||
%%% | |||
%%% The tracer process is linked to the shell, and the formatter to the | |||
%%% tracer process. The formatter also traps exits to be able to handle | |||
%%% all received trace messages until the tracer termination, but will then | |||
%%% shut down as soon as possible. | |||
%%% | |||
%%% In case the operator is tracing from a remote shell which gets | |||
%%% disconnected, the links between the shell and the tracer should make it | |||
%%% so tracing is automatically turned off once you disconnect. | |||
%%% | |||
%%% If sending output to the Group Leader is not desired, you may specify | |||
%%% a different pid() via the option `io_server' in the {@link calls/3} function. | |||
%%% For instance to write the traces to a file you can do something like | |||
%%% | |||
%%% ``` | |||
%%% 1> {ok, Dev} = file:open("/tmp/trace",[write]). | |||
%%% 2> recon_trace:calls({queue, in, fun(_) -> return_trace() end}, 3, [{io_server, Dev}]). | |||
%%% 1 | |||
%%% 3> | |||
%%% Recon tracer rate limit tripped. | |||
%%% 4> file:close(Dev). | |||
%%% ''' | |||
%%% | |||
%%% The only output still sent to the Group Leader is the rate limit being | |||
%%% tripped, and any errors. The rest will be sent to the other IO | |||
%%% server (see [http://erlang.org/doc/apps/stdlib/io_protocol.html]). | |||
%%% | |||
%%% == Record Printing == | |||
%%% | |||
%%% Thanks to code contributed by Bartek Górny, record printing can be added | |||
%%% to traces by first importing records in an active session with | |||
%%% `recon_rec:import([Module, ...])', after which the records declared in | |||
%%% the module list will be supported. | |||
%%% @end | |||
-module(recon_trace). | |||
%% API | |||
-export([clear/0, calls/2, calls/3]). | |||
-export([format/1]). | |||
%% Internal exports | |||
-export([count_tracer/1, rate_tracer/2, formatter/5, format_trace_output/1, format_trace_output/2]). | |||
-type matchspec() :: [{[term()] | '_', [term()], [term()]}]. | |||
-type shellfun() :: fun((_) -> term()). | |||
-type formatterfun() :: fun((_) -> iodata()). | |||
-type millisecs() :: non_neg_integer(). | |||
-type pidspec() :: all | existing | new | recon:pid_term(). | |||
-type max_traces() :: non_neg_integer(). | |||
-type max_rate() :: {max_traces(), millisecs()}. | |||
%% trace options | |||
-type options() :: [ {pid, pidspec() | [pidspec(),...]} % default: all | |||
| {timestamp, formatter | trace} % default: formatter | |||
| {args, args | arity} % default: args | |||
| {io_server, pid()} % default: group_leader() | |||
| {formatter, formatterfun()} % default: internal formatter | |||
| return_to | {return_to, boolean()} % default: false | |||
%% match pattern options | |||
| {scope, global | local} % default: global | |||
]. | |||
-type mod() :: '_' | module(). | |||
-type fn() :: '_' | atom(). | |||
-type args() :: '_' | 0..255 | return_trace | matchspec() | shellfun(). | |||
-type tspec() :: {mod(), fn(), args()}. | |||
-type max() :: max_traces() | max_rate(). | |||
-type num_matches() :: non_neg_integer(). | |||
-export_type([mod/0, fn/0, args/0, tspec/0, num_matches/0, options/0, | |||
max_traces/0, max_rate/0]). | |||
%%%%%%%%%%%%%% | |||
%%% PUBLIC %%% | |||
%%%%%%%%%%%%%% | |||
%% @doc Stops all tracing at once. | |||
-spec clear() -> ok. | |||
clear() -> | |||
erlang:trace(all, false, [all]), | |||
erlang:trace_pattern({'_','_','_'}, false, [local,meta,call_count,call_time]), | |||
erlang:trace_pattern({'_','_','_'}, false, []), % unsets global | |||
maybe_kill(recon_trace_tracer), | |||
maybe_kill(recon_trace_formatter), | |||
ok. | |||
%% @equiv calls({Mod, Fun, Args}, Max, []) | |||
-spec calls(tspec() | [tspec(),...], max()) -> num_matches(). | |||
calls({Mod, Fun, Args}, Max) -> | |||
calls([{Mod,Fun,Args}], Max, []); | |||
calls(TSpecs = [_|_], Max) -> | |||
calls(TSpecs, Max, []). | |||
%% @doc Allows to set trace patterns and pid specifications to trace | |||
%% function calls. | |||
%% | |||
%% The basic calls take the trace patterns as tuples of the form | |||
%% `{Module, Function, Args}' where: | |||
%% | |||
%% <ul> | |||
%% <li>`Module' is any atom representing a module</li> | |||
%% <li>`Function' is any atom representing a function, or the wildcard | |||
%% <code>'_'</code></li> | |||
%% <li>`Args' is either the arity of a function (`0..255'), a wildcard | |||
%% pattern (<code>'_'</code>), a | |||
%% <a href="http://learnyousomeerlang.com/ets#you-have-been-selected">match specification</a>, | |||
%% or a function from a shell session that can be transformed into | |||
%% a match specification</li> | |||
%% </ul> | |||
%% | |||
%% There is also an argument specifying either a maximal count (a number) | |||
%% of trace messages to be received, or a maximal frequency (`{Num, Millisecs}'). | |||
%% | |||
%% Here are examples of things to trace: | |||
%% | |||
%% <ul> | |||
%% <li>All calls from the `queue' module, with 10 calls printed at most: | |||
%% ``recon_trace:calls({queue, '_', '_'}, 10)''</li> | |||
%% <li>All calls to `lists:seq(A,B)', with 100 calls printed at most: | |||
%% `recon_trace:calls({lists, seq, 2}, 100)'</li> | |||
%% <li>All calls to `lists:seq(A,B)', with 100 calls per second at most: | |||
%% `recon_trace:calls({lists, seq, 2}, {100, 1000})'</li> | |||
%% <li>All calls to `lists:seq(A,B,2)' (all sequences increasing by two) | |||
%% with 100 calls at most: | |||
%% `recon_trace:calls({lists, seq, fun([_,_,2]) -> ok end}, 100)'</li> | |||
%% <li>All calls to `iolist_to_binary/1' made with a binary as an argument | |||
%% already (kind of useless conversion!): | |||
%% `recon_trace:calls({erlang, iolist_to_binary, fun([X]) when is_binary(X) -> ok end}, 10)'</li> | |||
%% <li>Calls to the queue module only in a given process `Pid', at a rate | |||
%% of 50 per second at most: | |||
%% ``recon_trace:calls({queue, '_', '_'}, {50,1000}, [{pid, Pid}])''</li> | |||
%% <li>Print the traces with the function arity instead of literal arguments: | |||
%% `recon_trace:calls(TSpec, Max, [{args, arity}])'</li> | |||
%% <li>Matching the `filter/2' functions of both `dict' and `lists' modules, | |||
%% across new processes only: | |||
%% `recon_trace:calls([{dict,filter,2},{lists,filter,2}], 10, [{pid, new}])'</li> | |||
%% <li>Tracing the `handle_call/3' functions of a given module for all new processes, | |||
%% and those of an existing one registered with `gproc': | |||
%% `recon_trace:calls({Mod,handle_call,3}, {10,100}, [{pid, [{via, gproc, Name}, new]}'</li> | |||
%% <li>Show the result of a given function call: | |||
%% `recon_trace:calls({Mod,Fun,fun(_) -> return_trace() end}, Max, Opts)' | |||
%% or | |||
%% ``recon_trace:calls({Mod,Fun,[{'_', [], [{return_trace}]}]}, Max, Opts)'', | |||
%% the important bit being the `return_trace()' call or the | |||
%% `{return_trace}' match spec value. | |||
%% A short-hand version for this pattern of 'match anything, trace everything' | |||
%% for a function is `recon_trace:calls({Mod, Fun, return_trace})'. </li> | |||
%% </ul> | |||
%% | |||
%% There's a few more combination possible, with multiple trace patterns per call, and more | |||
%% options: | |||
%% | |||
%% <ul> | |||
%% <li>`{pid, PidSpec}': which processes to trace. Valid options is any of | |||
%% `all', `new', `existing', or a process descriptor (`{A,B,C}', | |||
%% `"<A.B.C>"', an atom representing a name, `{global, Name}', | |||
%% `{via, Registrar, Name}', or a pid). It's also possible to specify | |||
%% more than one by putting them in a list.</li> | |||
%% <li>`{timestamp, formatter | trace}': by default, the formatter process | |||
%% adds timestamps to messages received. If accurate timestamps are | |||
%% required, it's possible to force the usage of timestamps within | |||
%% trace messages by adding the option `{timestamp, trace}'.</li> | |||
%% <li>`{args, arity | args}': whether to print arity in function calls | |||
%% or their (by default) literal representation.</li> | |||
%% <li>`{scope, global | local}': by default, only 'global' (fully qualified | |||
%% function calls) are traced, not calls made internally. To force tracing | |||
%% of local calls, pass in `{scope, local}'. This is useful whenever | |||
%% you want to track the changes of code in a process that isn't called | |||
%% with `Module:Fun(Args)', but just `Fun(Args)'.</li> | |||
%% <li>`{formatter, fun(Term) -> io_data() end}': override the default | |||
%% formatting functionality provided by recon.</li> | |||
%% <li>`{io_server, pid() | atom()}': by default, recon logs to the current | |||
%% group leader, usually the shell. This option allows to redirect | |||
%% trace output to a different IO server (such as a file handle).</li> | |||
%% <li>`return_to': If this option is set (in conjunction with the match | |||
%% option `{scope, local}'), the function to which the value is returned | |||
%% is output in a trace. Note that this is distinct from giving the | |||
%% *caller* since exception handling or calls in tail position may | |||
%% hide the original caller.</li> | |||
%% </ul> | |||
%% | |||
%% Also note that putting extremely large `Max' values (i.e. `99999999' or | |||
%% `{10000,1}') will probably negate most of the safe-guarding this library | |||
%% does and be dangerous to your node. Similarly, tracing extremely large | |||
%% amounts of function calls (all of them, or all of `io' for example) | |||
%% can be risky if more trace messages are generated than any process on | |||
%% the node could ever handle, despite the precautions taken by this library. | |||
%% @end | |||
-spec calls(tspec() | [tspec(),...], max(), options()) -> num_matches(). | |||
calls({Mod, Fun, Args}, Max, Opts) -> | |||
calls([{Mod,Fun,Args}], Max, Opts); | |||
calls(TSpecs = [_|_], {Max, Time}, Opts) -> | |||
Pid = setup(rate_tracer, [Max, Time], | |||
validate_formatter(Opts), validate_io_server(Opts)), | |||
trace_calls(TSpecs, Pid, Opts); | |||
calls(TSpecs = [_|_], Max, Opts) -> | |||
Pid = setup(count_tracer, [Max], | |||
validate_formatter(Opts), validate_io_server(Opts)), | |||
trace_calls(TSpecs, Pid, Opts). | |||
%%%%%%%%%%%%%%%%%%%%%%% | |||
%%% PRIVATE EXPORTS %%% | |||
%%%%%%%%%%%%%%%%%%%%%%% | |||
%% @private Stops when N trace messages have been received | |||
count_tracer(0) -> | |||
exit(normal); | |||
count_tracer(N) -> | |||
receive | |||
Msg -> | |||
recon_trace_formatter ! Msg, | |||
count_tracer(N-1) | |||
end. | |||
%% @private Stops whenever the trace message rates goes higher than | |||
%% `Max' messages in `Time' milliseconds. Note that if the rate | |||
%% proposed is higher than what the IO system of the formatter | |||
%% can handle, this can still put a node at risk. | |||
%% | |||
%% It is recommended to try stricter rates to begin with. | |||
rate_tracer(Max, Time) -> rate_tracer(Max, Time, 0, os:timestamp()). | |||
rate_tracer(Max, Time, Count, Start) -> | |||
receive | |||
Msg -> | |||
recon_trace_formatter ! Msg, | |||
Now = os:timestamp(), | |||
Delay = timer:now_diff(Now, Start) div 1000, | |||
if Delay > Time -> rate_tracer(Max, Time, 0, Now) | |||
; Max > Count -> rate_tracer(Max, Time, Count+1, Start) | |||
; Max =:= Count -> exit(normal) | |||
end | |||
end. | |||
%% @private Formats traces to be output | |||
formatter(Tracer, Parent, Ref, FormatterFun, IOServer) -> | |||
process_flag(trap_exit, true), | |||
link(Tracer), | |||
Parent ! {Ref, linked}, | |||
formatter(Tracer, IOServer, FormatterFun). | |||
formatter(Tracer, IOServer, FormatterFun) -> | |||
receive | |||
{'EXIT', Tracer, normal} -> | |||
io:format("Recon tracer rate limit tripped.~n"), | |||
exit(normal); | |||
{'EXIT', Tracer, Reason} -> | |||
exit(Reason); | |||
TraceMsg -> | |||
io:format(IOServer, FormatterFun(TraceMsg), []), | |||
formatter(Tracer, IOServer, FormatterFun) | |||
end. | |||
%%%%%%%%%%%%%%%%%%%%%%% | |||
%%% SETUP FUNCTIONS %%% | |||
%%%%%%%%%%%%%%%%%%%%%%% | |||
%% starts the tracer and formatter processes, and | |||
%% cleans them up before each call. | |||
setup(TracerFun, TracerArgs, FormatterFun, IOServer) -> | |||
clear(), | |||
Ref = make_ref(), | |||
Tracer = spawn_link(?MODULE, TracerFun, TracerArgs), | |||
register(recon_trace_tracer, Tracer), | |||
Format = spawn(?MODULE, formatter, [Tracer, self(), Ref, FormatterFun, IOServer]), | |||
register(recon_trace_formatter, Format), | |||
receive | |||
{Ref, linked} -> Tracer | |||
after 5000 -> | |||
error(setup_failed) | |||
end. | |||
%% Sets the traces in action | |||
trace_calls(TSpecs, Pid, Opts) -> | |||
{PidSpecs, TraceOpts, MatchOpts} = validate_opts(Opts), | |||
Matches = [begin | |||
{Arity, Spec} = validate_tspec(Mod, Fun, Args), | |||
erlang:trace_pattern({Mod, Fun, Arity}, Spec, MatchOpts) | |||
end || {Mod, Fun, Args} <- TSpecs], | |||
[erlang:trace(PidSpec, true, [call, {tracer, Pid} | TraceOpts]) | |||
|| PidSpec <- PidSpecs], | |||
lists:sum(Matches). | |||
%%%%%%%%%%%%%%%%%% | |||
%%% VALIDATION %%% | |||
%%%%%%%%%%%%%%%%%% | |||
validate_opts(Opts) -> | |||
PidSpecs = validate_pid_specs(proplists:get_value(pid, Opts, all)), | |||
Scope = proplists:get_value(scope, Opts, global), | |||
TraceOpts = case proplists:get_value(timestamp, Opts, formatter) of | |||
formatter -> []; | |||
trace -> [timestamp] | |||
end ++ | |||
case proplists:get_value(args, Opts, args) of | |||
args -> []; | |||
arity -> [arity] | |||
end ++ | |||
case proplists:get_value(return_to, Opts, undefined) of | |||
true when Scope =:= local -> | |||
[return_to]; | |||
true when Scope =:= global -> | |||
io:format("Option return_to only works with option {scope, local}~n"), | |||
%% Set it anyway | |||
[return_to]; | |||
_ -> | |||
[] | |||
end, | |||
MatchOpts = [Scope], | |||
{PidSpecs, TraceOpts, MatchOpts}. | |||
%% Support the regular specs, but also allow `recon:pid_term()' and lists | |||
%% of further pid specs. | |||
-spec validate_pid_specs(pidspec() | [pidspec(),...]) -> | |||
[all | new | existing | pid(), ...]. | |||
validate_pid_specs(all) -> [all]; | |||
validate_pid_specs(existing) -> [existing]; | |||
validate_pid_specs(new) -> [new]; | |||
validate_pid_specs([Spec]) -> validate_pid_specs(Spec); | |||
validate_pid_specs(PidTerm = [Spec|Rest]) -> | |||
%% can be "<a.b.c>" or [pidspec()] | |||
try | |||
[recon_lib:term_to_pid(PidTerm)] | |||
catch | |||
error:function_clause -> | |||
validate_pid_specs(Spec) ++ validate_pid_specs(Rest) | |||
end; | |||
validate_pid_specs(PidTerm) -> | |||
%% has to be `recon:pid_term()'. | |||
[recon_lib:term_to_pid(PidTerm)]. | |||
validate_tspec(Mod, Fun, Args) when is_function(Args) -> | |||
validate_tspec(Mod, Fun, fun_to_ms(Args)); | |||
%% helper to save typing for common actions | |||
validate_tspec(Mod, Fun, return_trace) -> | |||
validate_tspec(Mod, Fun, [{'_', [], [{return_trace}]}]); | |||
validate_tspec(Mod, Fun, Args) -> | |||
BannedMods = ['_', ?MODULE, io, lists], | |||
%% The banned mod check can be bypassed by using | |||
%% match specs if you really feel like being dumb. | |||
case {lists:member(Mod, BannedMods), Args} of | |||
{true, '_'} -> error({dangerous_combo, {Mod,Fun,Args}}); | |||
{true, []} -> error({dangerous_combo, {Mod,Fun,Args}}); | |||
_ -> ok | |||
end, | |||
case Args of | |||
'_' -> {'_', true}; | |||
_ when is_list(Args) -> {'_', Args}; | |||
_ when Args >= 0, Args =< 255 -> {Args, true} | |||
end. | |||
validate_formatter(Opts) -> | |||
case proplists:get_value(formatter, Opts) of | |||
F when is_function(F, 1) -> F; | |||
_ -> fun format/1 | |||
end. | |||
validate_io_server(Opts) -> | |||
proplists:get_value(io_server, Opts, group_leader()). | |||
%%%%%%%%%%%%%%%%%%%%%%%% | |||
%%% TRACE FORMATTING %%% | |||
%%%%%%%%%%%%%%%%%%%%%%%% | |||
%% Thanks Geoff Cant for the foundations for this. | |||
format(TraceMsg) -> | |||
{Type, Pid, {Hour,Min,Sec}, TraceInfo} = extract_info(TraceMsg), | |||
{FormatStr, FormatArgs} = case {Type, TraceInfo} of | |||
%% {trace, Pid, 'receive', Msg} | |||
{'receive', [Msg]} -> | |||
{"< ~p", [Msg]}; | |||
%% {trace, Pid, send, Msg, To} | |||
{send, [Msg, To]} -> | |||
{" > ~p: ~p", [To, Msg]}; | |||
%% {trace, Pid, send_to_non_existing_process, Msg, To} | |||
{send_to_non_existing_process, [Msg, To]} -> | |||
{" > (non_existent) ~p: ~p", [To, Msg]}; | |||
%% {trace, Pid, call, {M, F, Args}} | |||
{call, [{M,F,Args}]} -> | |||
{"~p:~p~s", [M,F,format_args(Args)]}; | |||
%% {trace, Pid, call, {M, F, Args}, Msg} | |||
{call, [{M,F,Args}, Msg]} -> | |||
{"~p:~p~s ~s", [M,F,format_args(Args), format_trace_output(Msg)]}; | |||
%% {trace, Pid, return_to, {M, F, Arity}} | |||
{return_to, [{M,F,Arity}]} -> | |||
{" '--> ~p:~p/~p", [M,F,Arity]}; | |||
%% {trace, Pid, return_from, {M, F, Arity}, ReturnValue} | |||
{return_from, [{M,F,Arity}, Return]} -> | |||
{"~p:~p/~p --> ~s", [M,F,Arity, format_trace_output(Return)]}; | |||
%% {trace, Pid, exception_from, {M, F, Arity}, {Class, Value}} | |||
{exception_from, [{M,F,Arity}, {Class,Val}]} -> | |||
{"~p:~p/~p ~p ~p", [M,F,Arity, Class, Val]}; | |||
%% {trace, Pid, spawn, Spawned, {M, F, Args}} | |||
{spawn, [Spawned, {M,F,Args}]} -> | |||
{"spawned ~p as ~p:~p~s", [Spawned, M, F, format_args(Args)]}; | |||
%% {trace, Pid, exit, Reason} | |||
{exit, [Reason]} -> | |||
{"EXIT ~p", [Reason]}; | |||
%% {trace, Pid, link, Pid2} | |||
{link, [Linked]} -> | |||
{"link(~p)", [Linked]}; | |||
%% {trace, Pid, unlink, Pid2} | |||
{unlink, [Linked]} -> | |||
{"unlink(~p)", [Linked]}; | |||
%% {trace, Pid, getting_linked, Pid2} | |||
{getting_linked, [Linker]} -> | |||
{"getting linked by ~p", [Linker]}; | |||
%% {trace, Pid, getting_unlinked, Pid2} | |||
{getting_unlinked, [Unlinker]} -> | |||
{"getting unlinked by ~p", [Unlinker]}; | |||
%% {trace, Pid, register, RegName} | |||
{register, [Name]} -> | |||
{"registered as ~p", [Name]}; | |||
%% {trace, Pid, unregister, RegName} | |||
{unregister, [Name]} -> | |||
{"no longer registered as ~p", [Name]}; | |||
%% {trace, Pid, in, {M, F, Arity} | 0} | |||
{in, [{M,F,Arity}]} -> | |||
{"scheduled in for ~p:~p/~p", [M,F,Arity]}; | |||
{in, [0]} -> | |||
{"scheduled in", []}; | |||
%% {trace, Pid, out, {M, F, Arity} | 0} | |||
{out, [{M,F,Arity}]} -> | |||
{"scheduled out from ~p:~p/~p", [M, F, Arity]}; | |||
{out, [0]} -> | |||
{"scheduled out", []}; | |||
%% {trace, Pid, gc_start, Info} | |||
{gc_start, [Info]} -> | |||
HeapSize = proplists:get_value(heap_size, Info), | |||
OldHeapSize = proplists:get_value(old_heap_size, Info), | |||
MbufSize = proplists:get_value(mbuf_size, Info), | |||
{"gc beginning -- heap ~p bytes", | |||
[HeapSize + OldHeapSize + MbufSize]}; | |||
%% {trace, Pid, gc_end, Info} | |||
{gc_end, [Info]} -> | |||
HeapSize = proplists:get_value(heap_size, Info), | |||
OldHeapSize = proplists:get_value(old_heap_size, Info), | |||
MbufSize = proplists:get_value(mbuf_size, Info), | |||
{"gc finished -- heap ~p bytes", | |||
[HeapSize + OldHeapSize + MbufSize]}; | |||
_ -> | |||
{"unknown trace type ~p -- ~p", [Type, TraceInfo]} | |||
end, | |||
io_lib:format("~n~p:~p:~9.6.0f ~p " ++ FormatStr ++ "~n", | |||
[Hour, Min, Sec, Pid] ++ FormatArgs). | |||
extract_info(TraceMsg) -> | |||
case tuple_to_list(TraceMsg) of | |||
[trace_ts, Pid, Type | Info] -> | |||
{TraceInfo, [Timestamp]} = lists:split(length(Info)-1, Info), | |||
{Type, Pid, to_hms(Timestamp), TraceInfo}; | |||
[trace, Pid, Type | TraceInfo] -> | |||
{Type, Pid, to_hms(os:timestamp()), TraceInfo} | |||
end. | |||
to_hms(Stamp = {_, _, Micro}) -> | |||
{_,{H, M, Secs}} = calendar:now_to_local_time(Stamp), | |||
Seconds = Secs rem 60 + (Micro / 1000000), | |||
{H,M,Seconds}; | |||
to_hms(_) -> | |||
{0,0,0}. | |||
format_args(Arity) when is_integer(Arity) -> | |||
[$/, integer_to_list(Arity)]; | |||
format_args(Args) when is_list(Args) -> | |||
[$(, join(", ", [format_trace_output(Arg) || Arg <- Args]), $)]. | |||
%% @doc formats call arguments and return values - most types are just printed out, except for | |||
%% tuples recognised as records, which mimic the source code syntax | |||
%% @end | |||
format_trace_output(Args) -> | |||
format_trace_output(recon_rec:is_active(), recon_map:is_active(), Args). | |||
format_trace_output(Recs, Args) -> | |||
format_trace_output(Recs, recon_map:is_active(), Args). | |||
format_trace_output(true, _, Args) when is_tuple(Args) -> | |||
recon_rec:format_tuple(Args); | |||
format_trace_output(false, true, Args) when is_tuple(Args) -> | |||
format_tuple(false, true, Args); | |||
format_trace_output(Recs, Maps, Args) when is_list(Args), Recs orelse Maps -> | |||
case io_lib:printable_list(Args) of | |||
true -> | |||
io_lib:format("~p", [Args]); | |||
false -> | |||
format_maybe_improper_list(Recs, Maps, Args) | |||
end; | |||
format_trace_output(Recs, true, Args) when is_map(Args) -> | |||
{Label, Map} = case recon_map:process_map(Args) of | |||
{L, M} -> {atom_to_list(L), M}; | |||
M -> {"", M} | |||
end, | |||
ItemList = maps:to_list(Map), | |||
[Label, | |||
"#{", | |||
join(", ", [format_kv(Recs, true, Key, Val) || {Key, Val} <- ItemList]), | |||
"}"]; | |||
format_trace_output(Recs, false, Args) when is_map(Args) -> | |||
ItemList = maps:to_list(Args), | |||
["#{", | |||
join(", ", [format_kv(Recs, false, Key, Val) || {Key, Val} <- ItemList]), | |||
"}"]; | |||
format_trace_output(_, _, Args) -> | |||
io_lib:format("~p", [Args]). | |||
format_kv(Recs, Maps, Key, Val) -> | |||
[format_trace_output(Recs, Maps, Key), "=>", format_trace_output(Recs, Maps, Val)]. | |||
format_tuple(Recs, Maps, Tup) -> | |||
[${ | format_tuple_(Recs, Maps, tuple_to_list(Tup))]. | |||
format_tuple_(_Recs, _Maps, []) -> | |||
"}"; | |||
format_tuple_(Recs, Maps, [H|T]) -> | |||
[format_trace_output(Recs, Maps, H), $,, | |||
format_tuple_(Recs, Maps, T)]. | |||
format_maybe_improper_list(Recs, Maps, List) -> | |||
[$[ | format_maybe_improper_list_(Recs, Maps, List)]. | |||
format_maybe_improper_list_(_, _, []) -> | |||
"]"; | |||
format_maybe_improper_list_(Recs, Maps, [H|[]]) -> | |||
[format_trace_output(Recs, Maps, H), $]]; | |||
format_maybe_improper_list_(Recs, Maps, [H|T]) when is_list(T) -> | |||
[format_trace_output(Recs, Maps, H), $,, | |||
format_maybe_improper_list_(Recs, Maps, T)]; | |||
format_maybe_improper_list_(Recs, Maps, [H|T]) when not is_list(T) -> | |||
%% Handling improper lists | |||
[format_trace_output(Recs, Maps, H), $|, | |||
format_trace_output(Recs, Maps, T), $]]. | |||
%%%%%%%%%%%%%%% | |||
%%% HELPERS %%% | |||
%%%%%%%%%%%%%%% | |||
maybe_kill(Name) -> | |||
case whereis(Name) of | |||
undefined -> | |||
ok; | |||
Pid -> | |||
unlink(Pid), | |||
exit(Pid, kill), | |||
wait_for_death(Pid, Name) | |||
end. | |||
wait_for_death(Pid, Name) -> | |||
case is_process_alive(Pid) orelse whereis(Name) =:= Pid of | |||
true -> | |||
timer:sleep(10), | |||
wait_for_death(Pid, Name); | |||
false -> | |||
ok | |||
end. | |||
%% Borrowed from dbg | |||
fun_to_ms(ShellFun) when is_function(ShellFun) -> | |||
case erl_eval:fun_data(ShellFun) of | |||
{fun_data,ImportList,Clauses} -> | |||
case ms_transform:transform_from_shell( | |||
dbg,Clauses,ImportList) of | |||
{error,[{_,[{_,_,Code}|_]}|_],_} -> | |||
io:format("Error: ~s~n", | |||
[ms_transform:format_error(Code)]), | |||
{error,transform_error}; | |||
Else -> | |||
Else | |||
end; | |||
false -> | |||
exit(shell_funs_only) | |||
end. | |||
-ifdef(OTP_RELEASE). | |||
-spec join(term(), [term()]) -> [term()]. | |||
join(Sep, List) -> | |||
lists:join(Sep, List). | |||
-else. | |||
-spec join(string(), [string()]) -> string(). | |||
join(Sep, List) -> | |||
string:join(List, Sep). | |||
-endif. |
@ -0,0 +1,327 @@ | |||
%%% Test suite for the recon module. Because many of the tests in | |||
%%% here depend on runtime properties and this is *not* transparent, | |||
%%% the tests are rather weak and more or less check for interface | |||
%%% conformance and obvious changes more than anything. | |||
-module(recon_SUITE). | |||
-include_lib("common_test/include/ct.hrl"). | |||
-include_lib("eunit/include/eunit.hrl"). | |||
-compile(export_all). | |||
-ifdef(OTP_RELEASE). | |||
-define(FILES_IMPL, nif). | |||
-define(ERROR_LOGGER_MATCH(_), ). | |||
-define(REDUCTIONS_MATCH(X), X). | |||
-else. | |||
-define(FILES_IMPL, port). | |||
-define(ERROR_LOGGER_MATCH(X), X,). | |||
-define(REDUCTIONS_MATCH(_), []). | |||
-endif. | |||
all() -> [{group,info}, proc_count, proc_window, bin_leak, | |||
node_stats_list, get_state, source, tcp, udp, files, port_types, | |||
inet_count, inet_window, binary_memory, scheduler_usage]. | |||
groups() -> [{info, [], [info3, info4, info1, info2, | |||
port_info1, port_info2]}]. | |||
init_per_group(info, Config) -> | |||
Self = self(), | |||
Pid = spawn(fun() -> | |||
{ok, TCP} = gen_tcp:listen(0, []), | |||
{ok, UDP} = gen_udp:open(0), | |||
Self ! {TCP, UDP}, | |||
timer:sleep(infinity) | |||
end), | |||
receive | |||
{TCP, UDP} -> [{pid, Pid}, {tcp, TCP}, {udp, UDP} | Config] | |||
end. | |||
end_per_group(info, Config) -> | |||
exit(?config(pid, Config), kill). | |||
init_per_testcase(files, Config) -> | |||
case ?FILES_IMPL of | |||
nif -> {skip, "files can no longer be listed in OTP-21 and above"}; | |||
port -> Config | |||
end; | |||
init_per_testcase(_, Config) -> | |||
Config. | |||
end_per_testcase(_, Config) -> | |||
Config. | |||
%%%%%%%%%%%%% | |||
%%% TESTS %%% | |||
%%%%%%%%%%%%% | |||
info3(Config) -> | |||
Pid = ?config(pid, Config), | |||
{A,B,C} = pid_to_triple(Pid), | |||
Info1 = recon:info(Pid), | |||
Info2 = recon:info(A,B,C), | |||
%% Reduction count is unreliable | |||
?assertMatch(?REDUCTIONS_MATCH( | |||
[{work, [{reductions,_}]}, {work, [{reductions,_}]}] | |||
), (Info1 -- Info2) ++ (Info2 -- Info1)). | |||
info4(Config) -> | |||
Pid = ?config(pid, Config), | |||
Keys = [meta, signals, location, memory_used, | |||
links, monitors, messages, | |||
[links, monitors, messages]], | |||
{A,B,C} = pid_to_triple(Pid), | |||
lists:map(fun(Key) -> | |||
Info = recon:info(Pid, Key), | |||
Info = recon:info(A,B,C, Key) | |||
end, | |||
Keys). | |||
info1(Config) -> | |||
Pid = ?config(pid, Config), | |||
Categories = [{meta, [registered_name, dictionary, group_leader, status]}, | |||
{signals, [links, monitors, monitored_by, trap_exit]}, | |||
{location, [initial_call, current_stacktrace]}, | |||
{memory_used, [memory, message_queue_len, heap_size, | |||
total_heap_size, garbage_collection]}, | |||
{work, [reductions]}], | |||
[] = lists:flatten( | |||
[K || {Cat,List} <- Categories, | |||
K <- List, | |||
Info <- [recon:info(Pid)], | |||
undefined == proplists:get_value(K, proplists:get_value(Cat,Info)) | |||
]), | |||
register(info1, Pid), | |||
Res1 = recon:info(info1), | |||
Res2 = recon:info(whereis(info1)), | |||
Res3 = recon:info(pid_to_triple(whereis(info1))), | |||
Res4 = recon:info(lists:flatten(io_lib:format("~p",[Pid]))), | |||
unregister(info1), | |||
L = lists:usort(Res1 ++ Res2 ++ Res3 ++ Res4), | |||
?assertMatch(?REDUCTIONS_MATCH([{work,[{reductions,_}]}, | |||
{work,[{reductions,_}]}, | |||
{work,[{reductions,_}]}]), L -- Res1), | |||
?assertMatch(?REDUCTIONS_MATCH([{work,[{reductions,_}]}, | |||
{work,[{reductions,_}]}, | |||
{work,[{reductions,_}]}]), L -- Res2), | |||
?assertMatch(?REDUCTIONS_MATCH([{work,[{reductions,_}]}, | |||
{work,[{reductions,_}]}, | |||
{work,[{reductions,_}]}]), L -- Res3), | |||
?assertMatch(?REDUCTIONS_MATCH([{work,[{reductions,_}]}, | |||
{work,[{reductions,_}]}, | |||
{work,[{reductions,_}]}]), L -- Res4), | |||
ok. | |||
info2(Config) -> | |||
Pid = ?config(pid, Config), | |||
Categories = [{meta, [registered_name, dictionary, group_leader, status]}, | |||
{signals, [links, monitors, monitored_by, trap_exit]}, | |||
{location, [initial_call, current_stacktrace]}, | |||
{memory_used, [memory, message_queue_len, heap_size, | |||
total_heap_size, garbage_collection]}, | |||
{work, [reductions]}], | |||
%% registered_name is special -- only returns | |||
%% [] when passed through by info/2. Because we pass terms through | |||
%% according to the docs, we have to respect that | |||
[] = recon:info(Pid, registered_name), | |||
%% Register to get the expected tuple | |||
register(info2, Pid), | |||
Keys = lists:flatten([K || {_,L} <- Categories, K <- L]), | |||
%% check that individual category call works for all terms | |||
[] = lists:flatten( | |||
[K || {Cat, List} <- Categories, | |||
K <- List, | |||
{GetCat,Info} <- [recon:info(Pid, Cat)], | |||
Cat =:= GetCat, | |||
undefined =:= proplists:get_value(K, Info)] | |||
), | |||
%% Can get a list of arguments | |||
true = lists:sort(Keys) | |||
=:= | |||
lists:sort(proplists:get_keys(recon:info(Pid, Keys))), | |||
true = length(Keys) | |||
=:= | |||
length([1 || K1 <- Keys, {K2,_} <- [recon:info(Pid, K1)], | |||
K1 == K2]), | |||
unregister(info2). | |||
proc_count(_Config) -> | |||
Res = recon:proc_count(memory, 10), | |||
true = proc_attrs(Res), | |||
%% greatest to smallest | |||
true = lists:usort(fun({P1,V1,_},{P2,V2,_}) -> {V1,P1} >= {V2,P2} end, | |||
Res) =:= Res, | |||
10 = length(Res), | |||
15 = length(recon:proc_count(reductions, 15)). | |||
proc_window(_Config) -> | |||
Res = recon:proc_window(reductions, 10, 100), | |||
true = proc_attrs(Res), | |||
%% we can't check order easily because stuff doesn't move | |||
%% fast enough on a test node to show up here | |||
10 = length(Res), | |||
15 = length(recon:proc_window(memory, 15, 100)). | |||
bin_leak(_Config) -> | |||
Res = recon:bin_leak(5), | |||
5 = length(Res), | |||
true = proc_attrs(Res), | |||
true = lists:any(fun({_,Val,_})-> Val =/= 0 end, Res), | |||
%% all results are =< 0, and ordered from smallest to biggest | |||
lists:foldl(fun({_,Current,_}, Next) when Current =< Next -> Current end, | |||
0, | |||
lists:reverse(Res)). | |||
%% This function implicitly tests node_stats/4 | |||
node_stats_list(_Config) -> | |||
Res = recon:node_stats_list(2,100), | |||
2 = length([1 || {[{process_count,_}, | |||
{run_queue,_}, | |||
?ERROR_LOGGER_MATCH({error_logger_queue_len,_}) | |||
{memory_total,_}, | |||
{memory_procs,_}, | |||
{memory_atoms,_}, | |||
{memory_bin,_}, | |||
{memory_ets,_}|_], | |||
[{bytes_in,_}, | |||
{bytes_out,_}, | |||
{gc_count,_}, | |||
{gc_words_reclaimed,_}, | |||
{reductions,_}, | |||
{scheduler_usage,[_|_]}|_]} <- Res]). | |||
get_state(_Config) -> | |||
Res = recon:get_state(kernel_sup), | |||
Res = recon:get_state(whereis(kernel_sup)), | |||
Res = recon:get_state(pid_to_triple(whereis(kernel_sup))), | |||
state = element(1,Res). | |||
%% Skip on remote-loading, too hard | |||
source(_Config) -> | |||
Pat = <<"find this sentence in this file's source">>, | |||
{_,_} = binary:match(iolist_to_binary(recon:source(?MODULE)), Pat). | |||
tcp(_Config) -> | |||
{ok, Listen} = gen_tcp:listen(0, []), | |||
true = lists:member(Listen, recon:tcp()). | |||
udp(_Config) -> | |||
{ok, Port} = gen_udp:open(0), | |||
true = lists:member(Port, recon:udp()). | |||
%% SCTP not supported everywhere, skipped. | |||
files(Config) -> | |||
Len = length(recon:files()), | |||
{ok, _IoDevice} = file:open(filename:join(?config(priv_dir, Config), "a"), | |||
[write]), | |||
true = Len + 1 =:= length(recon:files()). | |||
port_types(_Config) -> | |||
lists:all(fun({[_|_],N}) when is_integer(N), N > 0 -> true end, | |||
recon:port_types()). | |||
inet_count(_Config) -> | |||
[gen_tcp:listen(0,[]) || _ <- lists:seq(1,100)], | |||
Res = recon:inet_count(oct, 10), | |||
%% all results are =< 0, and ordered from biggest to smaller | |||
lists:foldl(fun({_,Current,_}, Next) when Current >= Next -> Current end, | |||
0, | |||
lists:reverse(Res)), | |||
true = inet_attrs(Res), | |||
%% greatest to smallest | |||
10 = length(Res), | |||
15 = length(recon:inet_count(cnt, 15)). | |||
inet_window(_Config) -> | |||
[gen_tcp:listen(0,[]) || _ <- lists:seq(1,100)], | |||
Res = recon:inet_window(oct, 10, 100), | |||
%% all results are =< 0, and ordered from biggest to smaller | |||
lists:foldl(fun({_,Current,_}, Next) when Current >= Next -> Current end, | |||
0, | |||
lists:reverse(Res)), | |||
true = inet_attrs(Res), | |||
%% greatest to smallest | |||
10 = length(Res), | |||
15 = length(recon:inet_window(cnt, 15, 100)). | |||
%% skip RPC | |||
port_info1(Config) -> | |||
TCP = ?config(tcp, Config), | |||
UDP = ?config(tcp, Config), | |||
TCPInfo = recon:port_info(TCP), | |||
UDPInfo = recon:port_info(UDP), | |||
%% type is the only specific value supported for now | |||
Cats = lists:sort([meta, signals, io, memory_used, type]), | |||
%% Too many options for inet stuff. | |||
Cats = lists:sort(proplists:get_keys(TCPInfo)), | |||
Cats = lists:sort(proplists:get_keys(UDPInfo)). | |||
port_info2(Config) -> | |||
TCP = ?config(tcp, Config), | |||
UDP = ?config(tcp, Config), | |||
%% Not testing the whole set, but heeeh. Good enough. | |||
{io, [{input,_},{output,_}]} = recon:port_info(TCP, io), | |||
{io, [{input,_},{output,_}]} = recon:port_info(UDP, io). | |||
%% binary_memory is a created attribute that counts the amount | |||
%% of memory held by refc binaries, usable in info/2-4 and | |||
%% in proc_count/proc_window. | |||
binary_memory(_Config) -> | |||
%% we just don't want them to crash like it happens with | |||
%% non-existing attributes. | |||
?assertError(_, recon:proc_count(fake_attribute, 10)), | |||
?assertError(_, recon:proc_window(fake_attribute, 10, 100)), | |||
recon:proc_count(binary_memory, 10), | |||
recon:proc_window(binary_memory, 10, 100), | |||
%% And now for info, it should work in lists, which can contain | |||
%% duplicates, or in single element calls. | |||
%% Note: we allocate the binary before spawning the process but | |||
%% use it in a closure to avoid race conditions on allocation for | |||
%% the test. | |||
Bin = <<1:999999>>, | |||
Pid1 = spawn_link(fun() -> timer:sleep(100000) end), | |||
Pid2 = spawn_link(fun() -> timer:sleep(100000), Bin end), | |||
{binary_memory, 0} = recon:info(Pid1, binary_memory), | |||
{binary_memory, N} = recon:info(Pid2, binary_memory), | |||
true = N > 0, | |||
Res1 = recon:info(Pid1, [binary, binary_memory, binary]), | |||
Res2 = recon:info(Pid2, [binary_memory, binary, binary_memory]), | |||
%% we expect everything to look as a single call to process_info/2 | |||
[{binary,X}, {binary_memory,_}, {binary,X}] = Res1, | |||
[{binary_memory,Y}, {binary,_}, {binary_memory,Y}] = Res2. | |||
%% Just check that we get many schedulers and that the usage values are all | |||
%% between 0 and 1 inclusively. We don't care for edge cases like a | |||
%% scheduler disappearing halfway through a run. | |||
scheduler_usage(_Config) -> | |||
List = recon:scheduler_usage(100), | |||
?assertEqual(length(List), length( | |||
[1 || {Id,Rate} <- List, | |||
is_integer(Id), Id > 0, | |||
Rate >= 0, Rate =< 1]) | |||
). | |||
%%%%%%%%%%%%%%% | |||
%%% HELPERS %%% | |||
%%%%%%%%%%%%%%% | |||
pid_to_triple(Pid) when is_pid(Pid) -> | |||
"<0."++Rest = lists:flatten(io_lib:format("~p",[Pid])), | |||
{B,C} = lists:foldl(fun($>, Acc) -> Acc; | |||
($., B) -> {B,0}; | |||
(N, {B,C}) -> {B,(C*10)+(N-$0)}; | |||
(N, B) -> (B*10)+(N-$0) | |||
end, | |||
0, | |||
Rest), | |||
{0,B,C}. | |||
proc_attrs(L) -> | |||
lists:all(fun({Pid,_Val,List}) -> is_pid(Pid) andalso is_list(List) end, | |||
L). | |||
inet_attrs(L) -> | |||
lists:all(fun({Port,_Val,List}) -> is_port(Port) andalso is_list(List) end, | |||
L). |
@ -0,0 +1,192 @@ | |||
%%% Test suite for recon_alloc. Because many of the tests in | |||
%%% here depend on memory allocation and this is *not* transparent, | |||
%%% the tests are rather weak and more or less check for interface | |||
%%% conformance and obvious changes more than anything. | |||
-module(recon_alloc_SUITE). | |||
-include_lib("common_test/include/ct.hrl"). | |||
-compile(export_all). | |||
all() -> [memory, fragmentation, cache_hit_rates, average_block_sizes, | |||
sbcs_to_mbcs, allocators, allocators_merged, snapshots, units]. | |||
memory(_Config) -> | |||
%% Freeze memory values for tests | |||
recon_alloc:snapshot(), | |||
%% memory returns values for 'used', 'allocated', 'unused', and 'usage'. | |||
Used = recon_alloc:memory(used), | |||
Alloc = recon_alloc:memory(allocated), | |||
Unused = recon_alloc:memory(unused), | |||
Usage = recon_alloc:memory(usage), | |||
Types = recon_alloc:memory(allocated_types), | |||
Instances = recon_alloc:memory(allocated_instances), | |||
%% equivalent to memory(_, current) | |||
Used = recon_alloc:memory(used, current), | |||
Alloc = recon_alloc:memory(allocated, current), | |||
Unused = recon_alloc:memory(unused, current), | |||
Usage = recon_alloc:memory(usage, current), | |||
Types = recon_alloc:memory(allocated_types, current), | |||
Instances = recon_alloc:memory(allocated_instances, current), | |||
%% relationships, and variation rates | |||
Alloc = Used + Unused, | |||
Usage = Used/Alloc, | |||
true = Alloc > 0, | |||
true = Usage > 0, | |||
true = Unused > 0, | |||
%% Current vs. Max | |||
true = Used =< recon_alloc:memory(used, max), | |||
true = Alloc =< recon_alloc:memory(allocated, max), | |||
true = Types =< recon_alloc:memory(allocated_types, max), | |||
true = Instances =< recon_alloc:memory(allocated_instances, max), | |||
%% Key presences in allocateds | |||
[{binary_alloc,_}, | |||
{driver_alloc,_}, | |||
{eheap_alloc,_}, | |||
{ets_alloc,_}, | |||
{fix_alloc,_}, | |||
{ll_alloc,_}, | |||
{sl_alloc,_}, | |||
{std_alloc,_}, | |||
{temp_alloc,_}] = Types, | |||
MaxInstance = lists:max(proplists:get_keys(Instances)), | |||
true = lists:sort(proplists:get_keys(Instances)) | |||
=:= lists:seq(0,MaxInstance), | |||
%% allocate a bunch of memory and see if the memory used goes | |||
%% up and relationships change accordingly. | |||
[spawn(fun() -> lists:seq(1,1000), timer:sleep(1000) end) | |||
|| _ <- lists:seq(1,100)], | |||
recon_alloc:snapshot(), | |||
true = Used < recon_alloc:memory(used), | |||
true = Alloc < recon_alloc:memory(allocated) | |||
orelse Usage < recon_alloc:memory(usage), | |||
%% Cleanup | |||
recon_alloc:snapshot_clear(). | |||
fragmentation(_Config) -> | |||
%% Values returned are of the format [{K, V}] and supports both | |||
%% searches by 'current' and 'max' | |||
Current = recon_alloc:fragmentation(current), | |||
Max = recon_alloc:fragmentation(max), | |||
true = allocdata(Current), | |||
true = allocdata(Max), | |||
true = Max =/= Current, | |||
Keys = [sbcs_usage, sbcs_block_size, sbcs_carriers_size, mbcs_usage, | |||
mbcs_block_size, mbcs_carriers_size], | |||
true = each_keys(Keys, Current), | |||
true = each_keys(Keys, Max). | |||
cache_hit_rates(_Config) -> | |||
Cache = recon_alloc:cache_hit_rates(), | |||
true = allocdata(Cache), | |||
true = each_keys([hit_rate, hits, calls], Cache). | |||
average_block_sizes(_Config) -> | |||
CurrentSizes = recon_alloc:average_block_sizes(current), | |||
MaxSizes = recon_alloc:average_block_sizes(max), | |||
true = lists:all(fun({K,V}) -> is_atom(K) andalso is_list(V) end, | |||
CurrentSizes), | |||
true = each_keys([mbcs, sbcs], CurrentSizes), | |||
true = lists:all(fun({K,V}) -> is_atom(K) andalso is_list(V) end, | |||
MaxSizes), | |||
true = each_keys([mbcs, sbcs], MaxSizes). | |||
sbcs_to_mbcs(_Config) -> | |||
Check = fun(Ratio) -> | |||
true = lists:all(fun({{Alloc,N},_}) -> | |||
is_atom(Alloc) andalso is_integer(N) | |||
end, | |||
Ratio), | |||
true = lists:all(fun({_,infinity}) -> true; | |||
({_,0}) -> true; | |||
({_,N}) -> is_float(N) | |||
end, | |||
Ratio) | |||
end, | |||
Check(recon_alloc:sbcs_to_mbcs(current)), | |||
Check(recon_alloc:sbcs_to_mbcs(max)). | |||
allocators(_Config) -> | |||
true = allocdata(recon_alloc:allocators()). | |||
allocators_merged(_Config) -> | |||
true = merged_allocdata(recon_alloc:allocators(types)). | |||
merged_allocdata(L) -> | |||
Validate = fun({{Allocator, Ns}, List}) -> | |||
is_atom(Allocator) andalso is_list(Ns) | |||
andalso | |||
lists:all(fun({_,_}) -> true; (_) -> false end, List) | |||
end, | |||
lists:all(Validate, L). | |||
snapshots(Config) -> | |||
File = filename:join(?config(priv_dir, Config), "snapshot"), | |||
undefined = recon_alloc:snapshot(), | |||
true = is_snapshot(recon_alloc:snapshot()), | |||
true = is_snapshot(recon_alloc:snapshot_clear()), | |||
undefined = recon_alloc:snapshot_clear(), | |||
ok = recon_alloc:snapshot_print(), | |||
ok = recon_alloc:snapshot_save(File), | |||
undefined = recon_alloc:snapshot_get(), | |||
undefined = recon_alloc:snapshot(), | |||
ok = recon_alloc:snapshot_print(), | |||
ok = recon_alloc:snapshot_save(File), | |||
_ = recon_alloc:snapshot_clear(), | |||
undefined = recon_alloc:snapshot_load(File), | |||
true = is_snapshot(recon_alloc:snapshot_load(File)), | |||
true = is_snapshot(recon_alloc:snapshot_get()), | |||
%% Also supporting another dump format | |||
file:write_file( | |||
File, | |||
io_lib:format("~p.~n", [{erlang:memory(), | |||
[{A,erlang:system_info({allocator,A})} | |||
|| A <- erlang:system_info(alloc_util_allocators)++[sys_alloc,mseg_alloc]]} | |||
])), | |||
_ = recon_alloc:snapshot_clear(), | |||
undefined = recon_alloc:snapshot_load(File), | |||
true = is_snapshot(recon_alloc:snapshot_get()), | |||
recon_alloc:snapshot_clear(). | |||
units(_Config) -> | |||
recon_alloc:snapshot(), | |||
ByteMem = recon_alloc:memory(used), | |||
ByteAvg = [X || {_,[{_,X}|_]} <- recon_alloc:average_block_sizes(max)], | |||
recon_alloc:set_unit(byte), | |||
ByteMem = recon_alloc:memory(used), | |||
ByteAvg = [X || {_,[{_,X}|_]} <- recon_alloc:average_block_sizes(max)], | |||
recon_alloc:set_unit(kilobyte), | |||
true = ByteMem/1024 == recon_alloc:memory(used), | |||
true = [X/1024 || X <- ByteAvg] | |||
== [X || {_,[{_,X}|_]} <- recon_alloc:average_block_sizes(max)], | |||
recon_alloc:set_unit(megabyte), | |||
true = ByteMem/(1024*1024) == recon_alloc:memory(used), | |||
true = [X/(1024*1024) || X <- ByteAvg] | |||
== [X || {_,[{_,X}|_]} <- recon_alloc:average_block_sizes(max)], | |||
recon_alloc:set_unit(gigabyte), | |||
true = ByteMem/(1024*1024*1024) == recon_alloc:memory(used), | |||
true = [X/(1024*1024*1024) || X <- ByteAvg] | |||
== [X || {_,[{_,X}|_]} <- recon_alloc:average_block_sizes(max)], | |||
recon_alloc:snapshot_clear(). | |||
%%% Helpers | |||
allocdata(L) -> | |||
Validate = fun({{Allocator,N}, List}) -> | |||
is_atom(Allocator) andalso is_integer(N) | |||
andalso | |||
lists:all(fun({_,_}) -> true; (_) -> false end, List) | |||
end, | |||
lists:all(Validate, L). | |||
each_keys(Keys,ListOfLists) -> | |||
lists:all(fun({_K,L}) -> | |||
lists:all(fun(Key) -> | |||
undefined =/= proplists:get_value(Key,L) | |||
end, | |||
Keys) | |||
end, | |||
ListOfLists). | |||
is_snapshot({Mem,Snap}) -> | |||
lists:all(fun({K,V}) -> is_atom(K) andalso is_integer(V) end, Mem) | |||
andalso | |||
allocdata(Snap). |
@ -0,0 +1,47 @@ | |||
-module(recon_lib_SUITE). | |||
-include_lib("common_test/include/ct.hrl"). | |||
-compile(export_all). | |||
all() -> [scheduler_usage_diff, sublist_top_n, term_to_pid]. | |||
scheduler_usage_diff(_Config) -> | |||
{Active0, Total0} = {1000, 2000}, | |||
SchedStat0 = {1, Active0, Total0}, | |||
% No active or total time has elapsed. Make sure we don't divide by zero. | |||
[{1, 0.0}] = recon_lib:scheduler_usage_diff([SchedStat0], [SchedStat0]), | |||
% Total time has elapsed, but no active time. Make sure we get 0 usage back. | |||
SchedStat1 = {1, Active0, Total0 * 2}, | |||
[{1, 0.0}] = recon_lib:scheduler_usage_diff([SchedStat0], [SchedStat1]), | |||
% Check for 100% usage | |||
SchedStat2 = {1, Active0 + 1000, Total0 + 1000}, | |||
[{1, 1.0}] = recon_lib:scheduler_usage_diff([SchedStat0], [SchedStat2]). | |||
sublist_top_n(_Config) -> | |||
L0 = [1,1,2,4,5,6,0,8,7,4,5,2,1,8,agbg,{t},3,[bah],"te",<<"bin">>,23.0, 23], | |||
L = [{make_ref(), Val, [{meta,data}]} || Val <- L0], | |||
%% former sort function used prior to integraton of sublist_top_n | |||
Sorted = lists:usort(fun({_,A,_},{_,B,_}) -> A > B end, L), | |||
[begin | |||
Sub = (catch lists:sublist(Sorted, N)), | |||
ct:pal("Sub ~p: ~p", [N, Sub]), | |||
Sub = (catch recon_lib:sublist_top_n_attrs(L, N)) | |||
end || N <- lists:seq(0, length(L)+1)], | |||
ok. | |||
term_to_pid(_Config) -> | |||
Pid = self(), | |||
Pid = recon_lib:term_to_pid(Pid), | |||
List = pid_to_list(Pid), | |||
Pid = recon_lib:term_to_pid(List), | |||
Binary = list_to_binary(List), | |||
Pid = recon_lib:term_to_pid(Binary), | |||
Name = foo, | |||
register(Name, Pid), | |||
Pid = recon_lib:term_to_pid(Name), | |||
yes = global:register_name(Name, Pid), | |||
Pid = recon_lib:term_to_pid({global, Name}), | |||
Sublist = lists:sublist(List, 2, length(List)-2), | |||
Ints = [ element(1, string:to_integer(T)) || T <- string:tokens(Sublist, ".")], | |||
Triple = list_to_tuple(Ints), | |||
Pid = recon_lib:term_to_pid(Triple), | |||
ok. |
@ -0,0 +1,72 @@ | |||
-module(recon_rec_SUITE). | |||
-include_lib("common_test/include/ct.hrl"). | |||
-compile(export_all). | |||
%%%%%%%%%% SETUP | |||
all() -> [record_defs, lists_and_limits]. | |||
init_per_testcase(_, Config) -> | |||
Res = recon_rec:import(records1), | |||
[{imported, records1, another, 3}, {imported, records1, state, 3}] = lists:sort(Res), | |||
Config. | |||
end_per_testcase(_, Config) -> | |||
recon_rec:clear(), | |||
Config. | |||
%%%%%%%%%% TESTS | |||
record_defs(_Config) -> | |||
has_record(state, 3), % make sure table was not wiped out | |||
has_record(another, 3), | |||
ImportRes = recon_rec:import(records2), %% one record is a duplicate | |||
[{imported,records2,another,4}, {ignored,records2,state,3,records1}] = lists:sort(ImportRes), | |||
has_record(state, 3), | |||
has_record(another, 3), | |||
has_record(another, 4), | |||
[Res] = recon_rec:lookup_record(state, 3), | |||
check_first_field(aaa, Res), | |||
recon_rec:clear(records1), | |||
no_record(state, 3), | |||
no_record(another, 3), | |||
has_record(another, 4), | |||
ImportRes2 = recon_rec:import(records2), | |||
[{imported,records2,state,3}, {overwritten,records2,another,4}] = lists:sort(ImportRes2), | |||
[Res1] = recon_rec:lookup_record(state, 3), | |||
check_first_field(one, Res1), | |||
recon_rec:clear(), | |||
no_record(state, 3), | |||
no_record(another, 3), | |||
no_record(another, 4), | |||
ok. | |||
lists_and_limits(_Config) -> | |||
recon_rec:import(records1), | |||
recon_rec:import(records2), | |||
List = recon_rec:get_list(), | |||
[{records1,another,[ddd,eee,fff],none}, | |||
{records1,state,[aaa,bbb,ccc],none}, | |||
{records2,another,[one,two,three,four],none}] = List, | |||
recon_rec:limit(another, 3, ddd), | |||
{records1,another,[ddd,eee,fff], ddd} = hd(recon_rec:get_list()), | |||
recon_rec:limit(another, 3, [ddd, eee]), | |||
{records1,another,[ddd,eee,fff], [ddd, eee]} = hd(recon_rec:get_list()), | |||
recon_rec:limit(another, 3, all), | |||
{records1,another,[ddd,eee,fff], all} = hd(recon_rec:get_list()), | |||
recon_rec:clear(records2), | |||
{error, record_unknown} = recon_rec:limit(another, 4, none), | |||
ok. | |||
%%%%%%%%%% HELPERS | |||
has_record(Name, Count) -> | |||
[_] = recon_rec:lookup_record(Name, Count). | |||
no_record(Name, Count) -> | |||
[] = recon_rec:lookup_record(Name, Count). | |||
check_first_field(F, Rec) -> | |||
{_, Fields, _, _} = Rec, | |||
F = hd(Fields). |
@ -0,0 +1,13 @@ | |||
-module(records1). | |||
-author("bartlomiej.gorny@erlang-solutions.com"). | |||
%% API | |||
-export([state/3, another/3]). | |||
-record(state, {aaa, bbb, ccc}). | |||
-record(another, {ddd, eee, fff}). | |||
state(A, B, C) -> | |||
#state{aaa = A, bbb = B, ccc = C}. | |||
another(D, E, F) -> | |||
#another{ddd = D, eee = E, fff = F}. |
@ -0,0 +1,13 @@ | |||
-module(records2). | |||
-author("bartlomiej.gorny@erlang-solutions.com"). | |||
%% API | |||
-export([state/3, another/4]). | |||
-record(state, {one, two, three}). | |||
-record(another, {one, two = something, three :: boolean(), four = 123 :: integer()}). | |||
state(A, B, C) -> | |||
#state{one = A, two = B, three = C}. | |||
another(A, B, C, D) -> | |||
#another{one = A, two = B, three = C, four = D}. |