%%
%% %CopyrightBegin%
%% 
%% Copyright Ericsson AB 2007-2011. All Rights Reserved.
%% 
%% The contents of this file are subject to the Erlang Public License,
%% Version 1.1, (the "License"); you may not use this file except in
%% compliance with the License. You should have received a copy of the
%% Erlang Public License along with this software. If not, it can be
%% retrieved online at http://www.erlang.org/.
%% 
%% Software distributed under the License is distributed on an "AS IS"
%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
%% the License for the specific language governing rights and limitations
%% under the License.
%% 
%% %CopyrightEnd%
%%


%%% Purpose : Tests system_profile BIF

-module(system_profile_SUITE).

-export([all/0, suite/0,groups/0,init_per_suite/1, end_per_suite/1, 
	 init_per_group/2,end_per_group/2,
	system_profile_on_and_off/1,
	runnable_procs/1,
	runnable_ports/1,
	scheduler/1
        ]).

-export([init_per_testcase/2, end_per_testcase/2]).

-export([profiler_process/1, ring_loop/1, port_echo_start/0, 
	 list_load/0, run_load/2]).

-include_lib("test_server/include/test_server.hrl").

-define(default_timeout, ?t:minutes(1)).

init_per_testcase(_Case, Config) ->
    ?line Dog=?t:timetrap(?default_timeout),
    [{watchdog, Dog}|Config].
end_per_testcase(_Case, Config) ->
    Dog=?config(watchdog, Config),
    ?t:timetrap_cancel(Dog),
    ok.

suite() -> [{ct_hooks,[ts_install_cth]}].

all() -> 
    [system_profile_on_and_off, runnable_procs,
     runnable_ports, scheduler].

groups() -> 
    [].

init_per_suite(Config) ->
    Config.

end_per_suite(_Config) ->
    ok.

init_per_group(_GroupName, Config) ->
    Config.

end_per_group(_GroupName, Config) ->
    Config.


%% No specification clause needed for an init function in a conf case!!!

%% Test switching system_profiling on and off.
system_profile_on_and_off(suite) ->
    [];
system_profile_on_and_off(doc) ->
    ["Tests switching system_profiling on and off."];
system_profile_on_and_off(Config) when is_list(Config) ->
    ?line Pid = start_profiler_process(),
    
    % Test runnable_ports on and off
    ?line undefined = erlang:system_profile(Pid, [runnable_ports]),
    ?line {Pid, [runnable_ports]} = erlang:system_profile(),
    ?line {Pid, [runnable_ports]} = erlang:system_profile(undefined, []),

    % Test runnable_procs on and off
    ?line undefined = erlang:system_profile(Pid, [runnable_procs]),
    ?line {Pid, [runnable_procs]} = erlang:system_profile(),
    ?line {Pid, [runnable_procs]} = erlang:system_profile(undefined, []),

    % Test scheduler on and off
    ?line undefined = erlang:system_profile(Pid, [scheduler]),
    ?line {Pid, [scheduler]} = erlang:system_profile(),
    ?line {Pid, [scheduler]} = erlang:system_profile(undefined, []),

    % Test combined runnable_ports, runnable_procs, scheduler; on and off
    ?line undefined = erlang:system_profile(Pid, [scheduler, runnable_procs, runnable_ports]),
    ?line {Pid, [scheduler,runnable_procs,runnable_ports]} = erlang:system_profile(),
    ?line {Pid, [scheduler,runnable_procs,runnable_ports]} = erlang:system_profile(undefined, []),

    % Test turned off and kill process
    ?line undefined = erlang:system_profile(),
    ?line exit(Pid,kill),
    ok.

%% Test runnable_procs

runnable_procs(suite) ->
    [];
runnable_procs(doc) ->
    ["Tests system_profiling with runnable_procs."];
runnable_procs(Config) when is_list(Config) ->
    ?line Pid = start_profiler_process(),
    % start a ring of processes
    % FIXME: Set #laps and #nodes in config file
    Nodes = 10,
    Laps = 10,
    ?line Master = ring(Nodes),
    ?line undefined = erlang:system_profile(Pid, [runnable_procs]),
    % loop a message
    ?line ok = ring_message(Master, message, Laps),
    ?line Events = get_profiler_events(),
    ?line kill_em_all = kill_ring(Master),
    ?line erlang:system_profile(undefined, []),
    put(master, Master),
    put(laps, Laps),
    ?line true = has_runnable_event(Events),
    Pids = sort_events_by_pid(Events),
    ?line ok = check_events(Pids),
    erase(),
    ?line exit(Pid,kill),
    ok.

runnable_ports(suite) ->
    [];
runnable_ports(doc) ->
    ["Tests system_profiling with runnable_port."];
runnable_ports(Config) when is_list(Config) ->
    ?line Pid = start_profiler_process(),
    ?line undefined = erlang:system_profile(Pid, [runnable_ports]),
    ?line EchoPid = echo(Config),
    % FIXME: Set config to number_of_echos
    Laps = 10,
    put(laps, Laps),
    ?line ok = echo_message(EchoPid, Laps, message),
    ?line Events = get_profiler_events(),
    ?line kill_em_all = kill_echo(EchoPid),
    ?line erlang:system_profile(undefined, []),
    ?line true = has_runnable_event(Events),
    Pids = sort_events_by_pid(Events),
    ?line ok = check_events(Pids),
    erase(),
    ?line exit(Pid,kill),
    ok.

scheduler(suite) ->
    [];
scheduler(doc) ->
    ["Tests system_profiling with scheduler."];
scheduler(Config) when is_list(Config) ->
    case {erlang:system_info(smp_support), erlang:system_info(schedulers_online)} of
	{false,_} -> ?line {skipped, "No need for scheduler test when smp support is disabled."};
	{_,    1} -> ?line {skipped, "No need for scheduler test when only one scheduler online."};
	_ ->
	    Nodes = 10,
	    ?line ok = check_block_system(Nodes),
	    ?line ok = check_multi_scheduling_block(Nodes),
	    ok
    end.

%%% Check scheduler profiling

check_multi_scheduling_block(Nodes) ->
    ?line Pid = start_profiler_process(),
    ?line undefined = erlang:system_profile(Pid, [scheduler]),
    ?line {ok, Supervisor} = start_load(Nodes),
    ?line erlang:system_flag(multi_scheduling, block),
    ?line erlang:system_flag(multi_scheduling, unblock),
    ?line {Pid, [scheduler]} = erlang:system_profile(undefined, []),
    ?line Events = get_profiler_events(),
    ?line true = has_scheduler_event(Events),
    stop_load(Supervisor),
    ?line exit(Pid,kill),
    erase(),
    ok.

check_block_system(Nodes) ->
    ?line Dummy = spawn(?MODULE, profiler_process, [[]]),
    ?line Pid = start_profiler_process(),
    ?line undefined = erlang:system_profile(Pid, [scheduler]),
    ?line {ok, Supervisor} = start_load(Nodes),
    % FIXME: remove wait !!
    wait(300),
    ?line undefined = erlang:system_monitor(Dummy, [busy_port]),
    ?line {Dummy, [busy_port]} = erlang:system_monitor(undefined, []),
    ?line {Pid, [scheduler]} = erlang:system_profile(undefined, []),
    ?line Events = get_profiler_events(),
    ?line true = has_scheduler_event(Events),
    stop_load(Supervisor),
    ?line exit(Pid,kill),
    ?line exit(Dummy,kill),
    erase(),
    ok.

%%% Check events

check_events([]) -> ok;
check_events([Pid | Pids]) ->
    Master = get(master),
    Laps = get(laps),
    CheckPids = get(pids),
    {Events, N} = get_pid_events(Pid),
    ?line ok = check_event_flow(Events),
    ?line ok = check_event_ts(Events),
    IsMember = lists:member(Pid, CheckPids),
    case Pid of
    	Master ->
	    io:format("Expected ~p and got ~p profile events from ~p: ok~n", [Laps*2+2, N, Pid]),
	    ?line N = Laps*2 + 2,
    	    check_events(Pids);
	Pid when IsMember == true ->
	    io:format("Expected ~p and got ~p profile events from ~p: ok~n", [Laps*2, N, Pid]),
	    ?line N = Laps*2,
    	    check_events(Pids);
	Pid ->
	    check_events(Pids)
    end.

%% timestamp consistency check for descending timestamps

check_event_ts(Events) ->
    check_event_ts(Events, undefined).
check_event_ts([], _) -> ok;
check_event_ts([Event | Events], undefined) ->
    check_event_ts(Events, Event);
check_event_ts([{Pid, _, _, TS1}=Event | Events], {Pid,_,_,TS0}) ->
    Time = timer:now_diff(TS1, TS0),
    if 
    	Time < 0.0 -> timestamp_error;
    	true -> check_event_ts(Events, Event)
    end.

%% consistency check for active vs. inactive activity (runnable)

check_event_flow(Events) ->
    check_event_flow(Events, undefined).
check_event_flow([], _) -> ok;
check_event_flow([Event | PidEvents], undefined) ->
    check_event_flow(PidEvents, Event);
check_event_flow([{Pid,Act,_,_}=Event | Events], PrevEvent) ->
    case PrevEvent of
    	{Pid, Act, _MFA, _TS} -> consistency_error;
	_ -> check_event_flow(Events, Event)
    end.
    


get_pid_events(Pid) -> 
    Events = get({pid_events, Pid}),
    {Events, length(Events)}.

sort_events_by_pid(Events) ->
    sort_events_by_pid(lists:reverse(Events), []).
sort_events_by_pid([], Pids) -> Pids;
sort_events_by_pid([Event | Events],Pids) ->
    case Event of 
    	{profile,Pid,Act,MFA,TS} ->
	    case get({pid_events, Pid}) of 
	    	undefined ->
		    put({pid_events, Pid}, [{Pid,Act,MFA,TS}]),
		    sort_events_by_pid(Events, [Pid | Pids]);
		PidEvents ->
		    put({pid_events, Pid}, [{Pid,Act,MFA,TS}|PidEvents]),
		    sort_events_by_pid(Events, Pids)
	    end
    end.


%%%
%% Process ring
%%%

%% API

% Returns master pid
ring(N) -> 
    Pids = build_ring(N, []),
    put(pids, Pids),
    setup_ring(Pids).

ring_message(Master, Message, Laps) ->
    Master ! {message, Master, Laps, Message},
    receive
    	{laps_complete, Master} -> ok
    end.

kill_ring(Master) -> Master ! kill_em_all.

%% Process ring helpers

build_ring(0, Pids) -> Pids;
build_ring(N, Pids) ->
    build_ring(N - 1, [spawn_link(?MODULE, ring_loop, [undefined]) | Pids]).

setup_ring([Master | Relayers]) ->
    % Relayers may not include the master pid
    Master ! {setup_ring, Relayers, self()},
    receive
        {setup_complete, Master} -> Master
    end.

ring_loop(RelayTo) ->
    receive
        kill_em_all ->
            RelayTo ! kill_em_all;
        {setup_ring, [Pid | Pids], Supervisor} ->
            put(supervisor, Supervisor),
            Pid ! {relay_to, Pids, self()},
            ring_loop(Pid);
        {setup_complete, _} ->
            get(supervisor) ! {setup_complete, self()},
            ring_loop(RelayTo);
        {relay_to, [], Master} ->
            Master ! {setup_complete, self()},
            ring_loop(Master);
        {relay_to, [Pid | Pids], Master} ->
            Pid ! {relay_to, Pids, Master},
            ring_loop(Pid);
        {message, Master, Lap, Msg}=Message ->
            case {self(), Lap} of
                {Master, 0} ->
                    get(supervisor) ! {laps_complete, self()},
                    ring_loop(RelayTo);
                {Master, Lap} ->
                    RelayTo ! {message, Master, Lap - 1, Msg},
                    ring_loop(RelayTo);
                _ ->
                    RelayTo ! Message,
                    ring_loop(RelayTo)
            end
    end.

%%%
%% Echo driver
%%%

%% API

echo(Config) ->
    Path = ?config(data_dir, Config),
    erl_ddll:load_driver(Path, echo_drv),
    Pid = spawn_link(?MODULE, port_echo_start, []),
    Pid ! {self(), get_ports},
    receive
	{port, Port} ->
	    put(pids, [Port]),
    	    put(master, Port),
	    Pid
    end.

echo_message(Pid, N, Msg) -> 
    Pid ! {start_echo, self(), N, Msg},
    receive
	{echo_complete, Pid} -> ok
    end.

kill_echo(Pid) -> Pid ! kill_em_all.


%% Echo driver helpers
port_echo_start() ->
    Port = open_port({spawn,echo_drv}, [eof,binary]),
    receive
	{Pid, get_ports} ->
    	    Pid ! {port, Port},
	    port_echo_loop(Port)
    end.

port_echo_loop(Port) ->
    receive
	{start_echo, Pid, Echos, Msg} ->
	    port_command(Port, term_to_binary({Pid, Echos, Msg})),
	    port_echo_loop(Port);
	{Port, {data, Data}} ->
	    {Pid, Echos, Msg} = binary_to_term(Data),
	    case Echos of
	    	0 ->
		    Pid ! {echo_complete, self()},
		    port_echo_loop(Port);
		Echos ->
	    	    port_command(Port, term_to_binary({Pid, Echos - 1, Msg})),
		    port_echo_loop(Port)
	    end;
	kill_em_all -> 
	    port_close(Port),
	    ok
    end.



%%%
%% Helpers
%%%

start_load(N) ->
   Pid = spawn_link(?MODULE, run_load, [N, []]),
   {ok, Pid}.


stop_load(Supervisor) ->
    erlang:unlink(Supervisor),
    exit(Supervisor, kill).

run_load(0, _Pids) ->
    receive
	    % wait for an exit signal or a message then kill
	    % all associated processes.
	    _ -> exit(annihilated)
    end;
run_load(N, Pids) ->
    Pid = spawn_link(?MODULE, list_load, []),
    run_load(N - 1, [Pid | Pids]).

list_load() -> 
    ok = case math:sin(random:uniform(32451)) of
    	A when is_float(A) -> ok;
	_ -> ok
    end,
    list_load().


has_scheduler_event(Events) ->
    lists:any(
    	fun (Pred) ->
	    case Pred of 
	    	{profile, scheduler, _ID, _Activity, _NR, _TS} -> true;
		_ -> false
	    end
	end, Events).

has_runnable_event(Events) ->
    lists:any(
    	fun (Pred) ->
	    case Pred of
	    	{profile, _Pid, _Activity, _MFA, _TS} -> true;
	    	_ -> false
	    end
        end, Events).

wait(Time) -> receive after Time -> ok end.

%%%
%%  Receivers
%%%

%% Process receiver


get_profiler_events() ->
    Pid = get(profiler),
    Pid ! {self(), get_events},
    receive
    	Events -> Events
    end.

start_profiler_process() ->
    Pid = spawn(?MODULE, profiler_process, [[]]),
    put(profiler, Pid),
    Pid.

profiler_process(Events) ->
    receive 
	{Pid, get_events} -> 
	    Ref = erlang:trace_delivered(all),
	    profiler_process_followup(Pid, Events, Ref);
	Event -> 
	    profiler_process([Event | Events])
    end.

profiler_process_followup(Pid, Events, Ref) ->
    receive
	{trace_delivered,all,Ref} ->
	    Pid ! lists:reverse(Events);
	Event -> 
	    profiler_process_followup(Pid, [Event | Events], Ref)
    end.

%% Port receiver


