%%% @author Fred Hebert %%% [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: %% https://www.erlang.org/doc/apps/erts/erl_ext_dist.html#port_ext %% https://www.erlang.org/doc/apps/erts/erl_ext_dist.html#new_port_ext Name = iolist_to_binary(atom_to_list(node())), NameLen = iolist_size(Name), Vsn = binary:last(term_to_binary(self())), {PortTag, VsnSize} = if Vsn > 3 -> {89, 4}; true -> {102, 1} end, Bin = <<131, % term encoding value PortTag, % port tag, 102 or 89 100, % atom ext tag, used for node name NameLen:2/unit:8, Name:NameLen/binary, N:4/unit:8, % actual counter value Vsn:VsnSize/unit: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)).