%%%-------------------------------------------------------------------
%%% File    : p1_prof.erl
%%% Author  : Evgeniy Khramtsov <ekhramtsov@process-one.net>
%%% Description : Handy wrapper around eprof and fprof
%%%
%%% Created : 23 Jan 2010 by Evgeniy Khramtsov <ekhramtsov@process-one.net>
%%%
%%%
%%% ejabberd, Copyright (C) 2002-2025   ProcessOne
%%%
%%% This program is free software; you can redistribute it and/or
%%% modify it under the terms of the GNU General Public License as
%%% published by the Free Software Foundation; either version 2 of the
%%% License, or (at your option) any later version.
%%%
%%% This program is distributed in the hope that it will be useful,
%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
%%% General Public License for more details.
%%%
%%% You should have received a copy of the GNU General Public License along
%%% with this program; if not, write to the Free Software Foundation, Inc.,
%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
%%%
%%%-------------------------------------------------------------------
-module(p1_prof).

%% API
-export([eprof_start/0, eprof_stop/0,
	 eprof_start/1, fprof_apply/3,
	 fprof_start/0, fprof_start/1,
	 fprof_stop/0, fprof_analyze/0,
	 queue/0, queue/1, memory/0, memory/1,
	 reds/0, reds/1, trace/1, help/0,
	 q/0, m/0, r/0, q/1, m/1, r/1,
	 locks/0, locks/1]).

-define(TRACE_FILE, "/tmp/fprof.trace").
-define(ANALYSIS_FILE, "/tmp/fprof.analysis").

%%====================================================================
%% API
%%====================================================================
eprof_start() ->
    eprof_start(get_procs()).

eprof_start(Duration) when is_integer(Duration) ->
    eprof_start(get_procs()),
    timer:sleep(timer:seconds(Duration)),
    eprof_stop();
eprof_start([]) ->
    {error, no_procs_found};
eprof_start(Procs) ->
    eprof:start(),
    eprof:start_profiling(Procs).

fprof_apply(M, F, A) ->
    fprof:apply(M, F, A, [{file, ?TRACE_FILE}]),
    fprof_analyze().

fprof_start() ->
    fprof_start(0).

fprof_start(Duration) ->
    case get_procs() of
        [] ->
            {error, no_procs_found};
        Procs ->
            case fprof:trace([start, {procs, Procs}, {file, ?TRACE_FILE}]) of
                ok ->
                    io:format("Profiling started, writing trace data to ~s~n",
                              [?TRACE_FILE]),
                    if Duration > 0 ->
                            timer:sleep(Duration*1000),
                            fprof:trace([stop]),
                            fprof:stop();
                       true->
                            ok
                    end;
                Err ->
                    io:format("Couldn't start profiling: ~p~n", [Err]),
                    Err
            end
    end.

fprof_stop() ->
    fprof:trace([stop]),
    case fprof:profile([{file, ?TRACE_FILE}]) of
        ok ->
            case fprof:analyse([totals, no_details, {sort, own},
                                no_callers, {dest, ?ANALYSIS_FILE}]) of
                ok ->
                    fprof:stop(),
                    format_fprof_analyze();
                Err ->
                    io:format("Couldn't analyze: ~p~n", [Err]),
                    Err
            end;
        Err ->
            io:format("Couldn't compile a trace into profile data: ~p~n",
                      [Err]),
            Err
    end.

fprof_analyze() ->
    fprof_stop().

eprof_stop() ->
    eprof:stop_profiling(),
    eprof:analyze(total).

help() ->
    M = ?MODULE,
    io:format("Brief help:~n"
	      "~p:queue(N) - show top N pids sorted by queue length~n"
	      "~p:queue() - shorthand for ~p:queue(10)~n"
	      "~p:memory(N) - show top N pids sorted by memory usage~n"
	      "~p:memory() - shorthand for ~p:memory(10)~n"
	      "~p:reds(N) - show top N pids sorted by reductions~n"
	      "~p:reds() - shorthand for ~p:reds(10)~n"
	      "~p:q(N)|~p:q() - same as ~p:queue(N)|~p:queue()~n"
	      "~p:m(N)|~p:m() - same as ~p:memory(N)|~p:memory()~n"
	      "~p:r(N)|~p:r() - same as ~p:reds(N)|~p:reds()~n"
	      "~p:trace(Pid) - trace Pid; to stop tracing close "
	      "Erlang shell with Ctrl+C~n"
	      "~p:eprof_start() - start eprof on all available pids; "
	      "DO NOT use on production system!~n"
	      "~p:eprof_stop() - stop eprof and print result~n"
	      "~p:fprof_start() - start fprof on all available pids; "
	      "DO NOT use on production system!~n"
	      "~p:fprof_stop() - stop eprof and print formatted result~n"
	      "~p:fprof_start(N) - start and run fprof for N seconds; "
	      "use ~p:fprof_analyze() to analyze collected statistics and "
	      "print formatted result; use on production system with CARE~n"
	      "~p:fprof_analyze() - analyze previously collected statistics "
	      "using ~p:fprof_start(N) and print formatted result~n"
	      "~p:help() - print this help~n",
	      lists:duplicate(31, M)).

q() ->
    queue().

q(N) ->
    queue(N).

m() ->
    memory().

m(N) ->
    memory(N).

r() ->
    reds().

r(N) ->
    reds(N).

queue() ->
    queue(10).

memory() ->
    memory(10).

reds() ->
    reds(10).

queue(N) ->
    dump(N, lists:reverse(lists:ukeysort(1, all_pids(queue)))).

memory(N) ->
    dump(N, lists:reverse(lists:ukeysort(2, all_pids(memory)))).

reds(N) ->
    dump(N, lists:reverse(lists:ukeysort(3, all_pids(reductions)))).

trace(Pid) ->
    erlang:trace(Pid, true, [send, 'receive']),
    trace_loop().

trace_loop() ->
    receive
	M ->
	    io:format("~p~n", [M]),
	    trace_loop()
    end.

%%====================================================================
%% Internal functions
%%====================================================================
get_procs() ->
    processes().

format_fprof_analyze() ->
    case file:consult(?ANALYSIS_FILE) of
	{ok, [_, [{totals, _, _, TotalOWN}] | Rest]} ->
	    OWNs = lists:flatmap(
		     fun({MFA, _, _, OWN}) ->
			     Percent = OWN*100/TotalOWN,
			     case round(Percent) of
				 0 ->
				     [];
				 _ ->
				     [{mfa_to_list(MFA), Percent}]
			     end
		     end, Rest),
	    ACCs = collect_accs(Rest),
	    MaxACC = find_max(ACCs),
	    MaxOWN = find_max(OWNs),
	    io:format("=== Sorted by OWN:~n"),
	    lists:foreach(
	      fun({MFA, Per}) ->
		      L = length(MFA),
		      S = lists:duplicate(MaxOWN - L + 2, $ ),
		      io:format("~s~s~.2f%~n", [MFA, S, Per])
	      end, lists:reverse(lists:keysort(2, OWNs))),
	    io:format("~n=== Sorted by ACC:~n"),
	    lists:foreach(
	      fun({MFA, Per}) ->
		      L = length(MFA),
		      S = lists:duplicate(MaxACC - L + 2, $ ),
		      io:format("~s~s~.2f%~n", [MFA, S, Per])
	      end, lists:reverse(lists:keysort(2, ACCs)));
	Err ->
	    Err
    end.

mfa_to_list({M, F, A}) ->
    atom_to_list(M) ++ ":" ++ atom_to_list(F) ++ "/" ++ integer_to_list(A);
mfa_to_list(F) when is_atom(F) ->
    atom_to_list(F).

find_max(List) ->
    find_max(List, 0).

find_max([{V, _}|Tail], Acc) ->
    find_max(Tail, lists:max([length(V), Acc]));
find_max([], Acc) ->
    Acc.

collect_accs(List) ->
    List1 = lists:filter(
	      fun({MFA, _, _, _}) ->
		      case MFA of
			  {sys, _, _} ->
			      false;
			  suspend ->
			      false;
			  {gen_fsm, _, _} ->
			      false;
			  {p1_fsm, _, _} ->
			      false;
			  {gen, _, _} ->
			      false;
			  {gen_server, _, _} ->
			      false;
			  {proc_lib, _, _} ->
			      false;
			  _ ->
			      true
		      end
	      end, List),
    TotalACC = lists:sum([A || {_, _, A, _} <- List1]),
    lists:flatmap(
      fun({MFA, _, ACC, _}) ->
	      Percent = ACC*100/TotalACC,
	      case round(Percent) of
		  0 ->
		      [];
		  _ ->
		      [{mfa_to_list(MFA), Percent}]
	      end
      end, List1).

all_pids(Type) ->
    lists:foldl(
      fun(P, Acc) when P == self() ->
	      %% exclude ourself from statistics
	      Acc;
	 (P, Acc) ->
	      case catch process_info(
			   P,
			   [message_queue_len,
			    status,
			    memory,
			    reductions,
			    dictionary,
			    current_function,
			    registered_name]) of
		  [{_, QLen}, {_, Status}, {_, Memory}, {_, Reds},
		   {_, Dict}, {_, CurFun}, {_, RegName}] ->
		      Dict1 = filter_dict(Dict, RegName),
                      {IntQLen, Dict2} =
			  case lists:keytake('$internal_queue_len', 1, Dict1) of
			      {value, {_, N}, D} ->
				  {N, D};
			      false ->
				  {0, Dict1}
			  end,
		      Len = QLen + IntQLen,
		      if Type == queue andalso Len == 0 ->
			      Acc;
			 true ->
                              Dict3 = [{message_queue_len, Len},
				       {status, Status},
				       {memory, Memory},
				       {reductions, Reds},
				       {current_function, CurFun},
				       {registered_name, RegName}|Dict2],
			      [{Len, Memory, Reds, P, Dict3}|Acc]
		      end;
		  _ ->
		      Acc
	      end
      end, [], processes()).

dump(N, Rs) ->
    lists:foreach(
      fun({_, _, _, Pid, Properties}) ->
	      PidStr = pid_to_list(Pid),
	      [_, Maj, Min] = string:tokens(
				string:substr(
				  PidStr, 2, length(PidStr) - 2), "."),
	      io:put_chars(
		[io_lib:format("** pid: pid(0,~s,~s)~n", [Maj, Min]),
		 [io_lib:format("** ~s: ~p~n", [Key, Val])
		  || {Key, Val} <- Properties], io_lib:nl()])
      end, nthhead(N, Rs)).

nthhead(N, L) ->
    lists:reverse(nthhead(N, L, [])).

nthhead(0, _L, Acc) ->
    Acc;
nthhead(N, [H|T], Acc) ->
    nthhead(N-1, T, [H|Acc]);
nthhead(_N, [], Acc) ->
    Acc.

filter_dict(Dict, RegName) ->
    lists:filter(
      fun({'$internal_queue_len', _}) -> true;
	 ({'$initial_call', _}) -> RegName == [];
	 ({'$ancestors', _}) -> RegName == [];
	 (_) -> false
      end, Dict).

% output in the console counts of locks, optionally waiting for few seconds before collect
locks() ->
    locks(5).
locks(Time) ->
    lcnt:rt_opt({copy_save, true}),
    lcnt:start(),
    lcnt:clear(),
    timer:sleep(Time*1000),
    lcnt:collect(),
    lcnt:conflicts(),
    lcnt:stop(),
    lcnt:rt_opt({copy_save, false}),
    ok.
