-module(bench_take_return).

-export([
    size_5_take_return_one/1,
    bench_size_5_take_return_one/2,
    size_1000_take_return_one/1,
    bench_size_1000_take_return_one/2,
    size_1_empty_take_no_members/1,
    bench_size_1_empty_take_no_members/2,
    size_5_take_return_all/1,
    bench_size_5_take_return_all/2,
    size_500_take_return_all/1,
    bench_size_500_take_return_all/2,
    size_500_clients_50_take_return_all/1,
    bench_size_500_clients_50_take_return_all/2,
    size_0_max_500_take_return_all/1,
    bench_size_0_max_500_take_return_all/2,
    size_0_max_500_clients_50_take_return_all/1,
    bench_size_0_max_500_clients_50_take_return_all/2
]).

%% @doc Pool of fixed size 5 - try to take just one member and instantly return
size_5_take_return_one(init) ->
    % init is called only once in the same process where benchmark will be running
    % at the very beginning of benchmark.
    % Value returned from this callback will be passed to server({input, State}),
    % bench_server(_, State) and server({stop, State})
    start_fixed(?FUNCTION_NAME, 5),
    ?FUNCTION_NAME;
size_5_take_return_one({input, _Server}) ->
    % This callback is called after `init` to generate benchmark input data.
    % Returned value will be passed to bench_server(Input, _)
    [];
size_5_take_return_one({stop, PoolName}) ->
    % Called only once at the very end of benchmark
    stop(PoolName).

bench_size_5_take_return_one(_Input, PoolName) ->
    Member = pooler:take_member(PoolName),
    true = is_pid(Member),
    pooler:return_member(PoolName, Member).

%% @doc Pool of fixed size 1000 - try to take just one member and instantly return
size_1000_take_return_one(init) ->
    start_fixed(?FUNCTION_NAME, 1000),
    ?FUNCTION_NAME;
size_1000_take_return_one({input, _Server}) ->
    % This callback is called after `init` to generate benchmark input data.
    % Returned value will be passed to bench_server(Input, _)
    [];
size_1000_take_return_one({stop, PoolName}) ->
    % Called only once at the very end of benchmark
    stop(PoolName).

bench_size_1000_take_return_one(_Input, PoolName) ->
    Member = pooler:take_member(PoolName),
    true = is_pid(Member),
    pooler:return_member(PoolName, Member).

%% @doc Fully satrurated pool of fixed size 1 - try to take just one member, get `error_no_members'
size_1_empty_take_no_members(init) ->
    start_fixed(?FUNCTION_NAME, 1),
    Worker = pooler:take_member(?FUNCTION_NAME),
    {?FUNCTION_NAME, Worker};
size_1_empty_take_no_members({input, _}) ->
    [];
size_1_empty_take_no_members({stop, {PoolName, Worker}}) ->
    pooler:return_member(PoolName, Worker),
    stop(PoolName).

bench_size_1_empty_take_no_members(_Input, {PoolName, _}) ->
    error_no_members = pooler:take_member(PoolName).

%% @doc Pool of fixed size 5 - take all members sequentially and instantly return them
size_5_take_return_all(init) ->
    start_fixed(?FUNCTION_NAME, 5),
    {?FUNCTION_NAME, 5};
size_5_take_return_all({input, {_Pool, _Size}}) ->
    [];
size_5_take_return_all({stop, {PoolName, _}}) ->
    stop(PoolName).

bench_size_5_take_return_all(_Input, {PoolName, Size}) ->
    Members = take_n(PoolName, Size),
    [pooler:return_member(PoolName, Member) || Member <- Members].

%% @doc Pool of fixed size 500 - take all members and instantly return them
size_500_take_return_all(init) ->
    start_fixed(?FUNCTION_NAME, 500),
    {?FUNCTION_NAME, 500};
size_500_take_return_all({input, {_Pool, Size}}) ->
    lists:seq(1, Size);
size_500_take_return_all({stop, {PoolName, _}}) ->
    stop(PoolName).

bench_size_500_take_return_all(_Input, {PoolName, Size}) ->
    Members = take_n(PoolName, Size),
    [pooler:return_member(PoolName, Member) || Member <- Members].

%% @doc Pool of fixed size 500 - take all members from 50 workers and let workers instantly return them
size_500_clients_50_take_return_all(init) ->
    PoolSize = 500,
    NumClients = 50,
    PerClient = PoolSize div NumClients,
    start_fixed(?FUNCTION_NAME, 500),
    Clients = [
        erlang:spawn_link(
            fun() ->
                client(?FUNCTION_NAME, 0, PerClient)
            end
        )
     || _ <- lists:seq(1, NumClients)
    ],
    {?FUNCTION_NAME, 500, Clients};
size_500_clients_50_take_return_all({input, {_Pool, _Size, _Clients}}) ->
    [];
size_500_clients_50_take_return_all({stop, {PoolName, _Size, Clients}}) ->
    [
        begin
            unlink(Pid),
            exit(Pid, shutdown)
        end
     || Pid <- Clients
    ],
    stop(PoolName).

bench_size_500_clients_50_take_return_all(_Input, {_PoolName, _Size, Clients}) ->
    Ref = erlang:make_ref(),
    Self = self(),
    lists:foreach(fun(C) -> C ! {do, Self, Ref} end, Clients),
    lists:foreach(
        fun(_C) ->
            receive
                {done, RecRef} ->
                    RecRef = Ref
            after 5000 ->
                error(timeout)
            end
        end,
        Clients
    ).

%% @doc Artificial example: pool with init_count=0, max_count=500 that is culled to 0 on each iteration.
%% Try to take 500 workers sequentially and instantly return them (and trigger culling).
%% This benchmark, while is quite unrealistic (why have pool that instantly kills workers), but it
%% helps to test worker spawn/kill routines.
size_0_max_500_take_return_all(init) ->
    Size = 500,
    start([
        {name, ?FUNCTION_NAME},
        {init_count, 0},
        {max_count, Size},
        {max_age, {0, sec}}
    ]),
    {?FUNCTION_NAME, Size};
size_0_max_500_take_return_all({input, {_Pool, _Size}}) ->
    [];
size_0_max_500_take_return_all({stop, {PoolName, _}}) ->
    stop(PoolName).

bench_size_0_max_500_take_return_all(_Input, {PoolName, Size}) ->
    Members = take_n(PoolName, 500, Size),
    [pooler:return_member(PoolName, Member) || Member <- Members],
    whereis(PoolName) ! cull_pool,
    Utilization = pooler:pool_utilization(PoolName),
    0 = proplists:get_value(free_count, Utilization),
    0 = proplists:get_value(in_use_count, Utilization).

%% @doc Pool with init_count=0, max_count=500 that is culled to 0 on each iteration, taken by 50 workers
%%
%% Artificial example (because pool is instantly culled), but with parallel consumers we can test concurrent
%% pool worker starter
size_0_max_500_clients_50_take_return_all(init) ->
    PoolSize = 500,
    NumClients = 50,
    PerClient = PoolSize div NumClients,
    start([
        {name, ?FUNCTION_NAME},
        {init_count, 0},
        {max_count, PoolSize},
        {max_age, {0, sec}},
        {queue_max, 500}
    ]),
    Clients = [
        erlang:spawn_link(
            fun() ->
                client(?FUNCTION_NAME, 500, PerClient)
            end
        )
     || _ <- lists:seq(1, NumClients)
    ],
    {?FUNCTION_NAME, 500, Clients};
size_0_max_500_clients_50_take_return_all({input, {_Pool, _Size, _Clients}}) ->
    [];
size_0_max_500_clients_50_take_return_all({stop, {PoolName, _Size, Clients}}) ->
    [
        begin
            unlink(Pid),
            exit(Pid, shutdown)
        end
     || Pid <- Clients
    ],
    stop(PoolName).

bench_size_0_max_500_clients_50_take_return_all(_Input, {PoolName, _Size, Clients}) ->
    Ref = erlang:make_ref(),
    Self = self(),
    lists:foreach(fun(C) -> C ! {do, Self, Ref} end, Clients),
    lists:foreach(
        fun(_C) ->
            receive
                {done, RecRef} ->
                    RecRef = Ref
            after 5000 ->
                error(timeout)
            end
        end,
        Clients
    ),
    whereis(PoolName) ! cull_pool,
    Utilization = pooler:pool_utilization(PoolName),
    0 = proplists:get_value(free_count, Utilization),
    0 = proplists:get_value(in_use_count, Utilization).

%% Internal

start_fixed(Name, Size) ->
    Conf = [
        {name, Name},
        {init_count, Size},
        {max_count, Size}
    ],
    start(Conf).

start(Conf0) ->
    Conf = [{start_mfa, {pooled_gs, start_link, [{"test"}]}} | Conf0],
    logger:set_handler_config(default, filters, []),
    {ok, _} = application:ensure_all_started(pooler),
    {ok, _} = pooler:new_pool(Conf).

stop(Name) ->
    pooler:rm_pool(Name),
    application:stop(pooler).

take_n(PoolName, N) ->
    take_n(PoolName, 0, N).

take_n(_, _, 0) ->
    [];
take_n(PoolName, Timeout, N) ->
    Member = pooler:take_member(PoolName, Timeout),
    true = is_pid(Member),
    [Member | take_n(PoolName, Timeout, N - 1)].

client(Pool, Timeout, N) ->
    receive
        {do, From, Ref} ->
            Taken = take_n(Pool, Timeout, N),
            [pooler:return_member(Pool, Member) || Member <- Taken],
            From ! {done, Ref}
    end,
    client(Pool, Timeout, N).