1-module(ibrowse_load_test).
2%%-compile(export_all).
3-export([
4         random_seed/0,
5         start/3,
6         query_state/0,
7         shutdown/0,
8         start_1/3,
9         calculate_timings/0,
10         get_mmv/2,
11         spawn_workers/2,
12         spawn_workers/4,
13         wait_for_workers/1,
14         worker_loop/2,
15         update_unknown_counter/2
16        ]).
17
18-ifdef(new_rand).
19
20-define(RAND, rand).
21random_seed() ->
22	ok.
23
24-else.
25
26-define(RAND, random).
27random_seed() ->
28	random:seed(os:timestamp()).
29
30-endif.
31
32-define(ibrowse_load_test_counters, ibrowse_load_test_counters).
33
34start(Num_workers, Num_requests, Max_sess) ->
35    proc_lib:spawn(fun() ->
36                           start_1(Num_workers, Num_requests, Max_sess)
37                   end).
38
39query_state() ->
40    ibrowse_load_test ! query_state.
41
42shutdown() ->
43    ibrowse_load_test ! shutdown.
44
45start_1(Num_workers, Num_requests, Max_sess) ->
46    register(ibrowse_load_test, self()),
47    application:start(ibrowse),
48    application:set_env(ibrowse, inactivity_timeout, 5000),
49    Ulimit = os:cmd("ulimit -n"),
50    case catch list_to_integer(string:strip(Ulimit, right, $\n)) of
51        X when is_integer(X), X > 3000 ->
52            ok;
53        X ->
54            io:format("Load test not starting. {insufficient_value_for_ulimit, ~p}~n", [X]),
55            exit({insufficient_value_for_ulimit, X})
56    end,
57    ets:new(?ibrowse_load_test_counters, [named_table, public]),
58    ets:new(ibrowse_load_timings, [named_table, public]),
59    try
60        ets:insert(?ibrowse_load_test_counters, [{success, 0},
61                                                 {failed, 0},
62                                                 {timeout, 0},
63                                                 {retry_later, 0},
64                                                 {one_request_only, 0}
65                                                ]),
66        ibrowse:set_max_sessions("localhost", 8081, Max_sess),
67        Start_time    = os:timestamp(),
68        Workers       = spawn_workers(Num_workers, Num_requests),
69        erlang:send_after(1000, self(), print_diagnostics),
70        ok            = wait_for_workers(Workers),
71        End_time      = os:timestamp(),
72        Time_in_secs  = trunc(round(timer:now_diff(End_time, Start_time) / 1000000)),
73        Req_count     = Num_workers * Num_requests,
74        [{_, Success_count}] = ets:lookup(?ibrowse_load_test_counters, success),
75        case Success_count == Req_count of
76            true ->
77                io:format("Test success. All requests succeeded~n", []);
78            false when Success_count > 0 ->
79                io:format("Test failed. Some successes~n", []);
80            false ->
81                io:format("Test failed. ALL requests FAILED~n", [])
82        end,
83        case Time_in_secs > 0 of
84            true ->
85                io:format("Reqs/sec achieved : ~p~n", [trunc(round(Success_count / Time_in_secs))]);
86            false ->
87                ok
88        end,
89        io:format("Load test results:~n~p~n", [ets:tab2list(?ibrowse_load_test_counters)]),
90        io:format("Timings: ~p~n", [calculate_timings()])
91    catch Err ->
92            io:format("Err: ~p~n", [Err])
93    after
94        ets:delete(?ibrowse_load_test_counters),
95        ets:delete(ibrowse_load_timings),
96        unregister(ibrowse_load_test)
97    end.
98
99calculate_timings() ->
100    {Max, Min, Mean} = get_mmv(ets:first(ibrowse_load_timings), {0, 9999999, 0}),
101    Variance = trunc(round(ets:foldl(fun({_, X}, X_acc) ->
102                                             (X - Mean)*(X-Mean) + X_acc
103                                     end, 0, ibrowse_load_timings) / ets:info(ibrowse_load_timings, size))),
104    Std_dev = trunc(round(math:sqrt(Variance))),
105    {ok, [{max, Max},
106          {min, Min},
107          {mean, Mean},
108          {variance, Variance},
109          {standard_deviation, Std_dev}]}.
110
111get_mmv('$end_of_table', {Max, Min, Total}) ->
112    Mean = trunc(round(Total / ets:info(ibrowse_load_timings, size))),
113    {Max, Min, Mean};
114get_mmv(Key, {Max, Min, Total}) ->
115    [{_, V}] = ets:lookup(ibrowse_load_timings, Key),
116    get_mmv(ets:next(ibrowse_load_timings, Key), {max(Max, V), min(Min, V), Total + V}).
117
118
119spawn_workers(Num_w, Num_r) ->
120    spawn_workers(Num_w, Num_r, self(), []).
121
122spawn_workers(0, _Num_requests, _Parent, Acc) ->
123    lists:reverse(Acc);
124spawn_workers(Num_workers, Num_requests, Parent, Acc) ->
125    Pid_ref = spawn_monitor(fun() ->
126                                    random_seed(),
127                                    case catch worker_loop(Parent, Num_requests) of
128                                        {'EXIT', Rsn} ->
129                                            io:format("Worker crashed with reason: ~p~n", [Rsn]);
130                                        _ ->
131                                            ok
132                                    end
133                            end),
134    spawn_workers(Num_workers - 1, Num_requests, Parent, [Pid_ref | Acc]).
135
136wait_for_workers([]) ->
137    ok;
138wait_for_workers([{Pid, Pid_ref} | T] = Pids) ->
139    receive
140        {done, Pid} ->
141            wait_for_workers(T);
142        {done, Some_pid} ->
143            wait_for_workers([{Pid, Pid_ref} | lists:keydelete(Some_pid, 1, T)]);
144        print_diagnostics ->
145            io:format("~1000.p~n", [ibrowse:get_metrics()]),
146            erlang:send_after(1000, self(), print_diagnostics),
147            wait_for_workers(Pids);
148        query_state ->
149            io:format("Waiting for ~p~n", [Pids]),
150            wait_for_workers(Pids);
151        shutdown ->
152            io:format("Shutting down on command. Still waiting for ~p workers~n", [length(Pids)]);
153        {'DOWN', _, process, _, normal} ->
154            wait_for_workers(Pids);
155        {'DOWN', _, process, Down_pid, Rsn} ->
156            io:format("Worker ~p died. Reason: ~p~n", [Down_pid, Rsn]),
157            wait_for_workers(lists:keydelete(Down_pid, 1, Pids));
158        X ->
159            io:format("Recvd unknown msg: ~p~n", [X]),
160            wait_for_workers(Pids)
161    end.
162
163worker_loop(Parent, 0) ->
164    Parent ! {done, self()};
165worker_loop(Parent, N) ->
166    Delay = ?RAND:uniform(100),
167    Url = case Delay rem 10 of
168              %% Change 10 to some number between 0-9 depending on how
169              %% much chaos you want to introduce into the server
170              %% side. The higher the number, the more often the
171              %% server will close a connection after serving the
172              %% first request, thereby forcing the client to
173              %% retry. Any number of 10 or higher will disable this
174              %% chaos mechanism
175              10 ->
176                  ets:update_counter(?ibrowse_load_test_counters, one_request_only, 1),
177                  "http://localhost:8081/ibrowse_handle_one_request_only";
178              _ ->
179                  "http://localhost:8081/blah"
180          end,
181    Start_time = os:timestamp(),
182    Res = ibrowse:send_req(Url, [], get),
183    End_time = os:timestamp(),
184    Time_taken = trunc(round(timer:now_diff(End_time, Start_time) / 1000)),
185    ets:insert(ibrowse_load_timings, {os:timestamp(), Time_taken}),
186    case Res of
187        {ok, "200", _, _} ->
188            ets:update_counter(?ibrowse_load_test_counters, success, 1);
189        {error, req_timedout} ->
190            ets:update_counter(?ibrowse_load_test_counters, timeout, 1);
191        {error, retry_later} ->
192            ets:update_counter(?ibrowse_load_test_counters, retry_later, 1);
193        {error, Reason} ->
194            update_unknown_counter(Reason, 1);
195        _ ->
196            io:format("~p -- Res: ~p~n", [self(), Res]),
197            ets:update_counter(?ibrowse_load_test_counters, failed, 1)
198    end,
199    timer:sleep(Delay),
200    worker_loop(Parent, N - 1).
201
202update_unknown_counter(Counter, Inc_val) ->
203    case catch ets:update_counter(?ibrowse_load_test_counters, Counter, Inc_val) of
204        {'EXIT', _} ->
205            ets:insert_new(?ibrowse_load_test_counters, {Counter, 0}),
206            update_unknown_counter(Counter, Inc_val);
207        _ ->
208            ok
209    end.
210