-module(ibrowse_load_test). -compile(export_all). -define(ibrowse_load_test_counters, ibrowse_load_test_counters). start(Num_workers, Num_requests, Max_sess) -> proc_lib:spawn(fun() -> start_1(Num_workers, Num_requests, Max_sess) end). query_state() -> ibrowse_load_test ! query_state. shutdown() -> ibrowse_load_test ! shutdown. start_1(Num_workers, Num_requests, Max_sess) -> register(ibrowse_load_test, self()), application:start(ibrowse), application:set_env(ibrowse, inactivity_timeout, 5000), Ulimit = os:cmd("ulimit -n"), case catch list_to_integer(string:strip(Ulimit, right, $\n)) of X when is_integer(X), X > 3000 -> ok; X -> io:format("Load test not starting. {insufficient_value_for_ulimit, ~p}~n", [X]), exit({insufficient_value_for_ulimit, X}) end, ets:new(?ibrowse_load_test_counters, [named_table, public]), ets:new(ibrowse_load_timings, [named_table, public]), try ets:insert(?ibrowse_load_test_counters, [{success, 0}, {failed, 0}, {timeout, 0}, {retry_later, 0}, {one_request_only, 0} ]), ibrowse:set_max_sessions("localhost", 8081, Max_sess), Start_time = now(), Workers = spawn_workers(Num_workers, Num_requests), erlang:send_after(1000, self(), print_diagnostics), ok = wait_for_workers(Workers), End_time = now(), Time_in_secs = trunc(round(timer:now_diff(End_time, Start_time) / 1000000)), Req_count = Num_workers * Num_requests, [{_, Success_count}] = ets:lookup(?ibrowse_load_test_counters, success), case Success_count == Req_count of true -> io:format("Test success. All requests succeeded~n", []); false when Success_count > 0 -> io:format("Test failed. Some successes~n", []); false -> io:format("Test failed. ALL requests FAILED~n", []) end, case Time_in_secs > 0 of true -> io:format("Reqs/sec achieved : ~p~n", [trunc(round(Success_count / Time_in_secs))]); false -> ok end, io:format("Load test results:~n~p~n", [ets:tab2list(?ibrowse_load_test_counters)]), io:format("Timings: ~p~n", [calculate_timings()]) catch Err -> io:format("Err: ~p~n", [Err]) after ets:delete(?ibrowse_load_test_counters), ets:delete(ibrowse_load_timings), unregister(ibrowse_load_test) end. calculate_timings() -> {Max, Min, Mean} = get_mmv(ets:first(ibrowse_load_timings), {0, 9999999, 0}), Variance = trunc(round(ets:foldl(fun({_, X}, X_acc) -> (X - Mean)*(X-Mean) + X_acc end, 0, ibrowse_load_timings) / ets:info(ibrowse_load_timings, size))), Std_dev = trunc(round(math:sqrt(Variance))), {ok, [{max, Max}, {min, Min}, {mean, Mean}, {variance, Variance}, {standard_deviation, Std_dev}]}. get_mmv('$end_of_table', {Max, Min, Total}) -> Mean = trunc(round(Total / ets:info(ibrowse_load_timings, size))), {Max, Min, Mean}; get_mmv(Key, {Max, Min, Total}) -> [{_, V}] = ets:lookup(ibrowse_load_timings, Key), get_mmv(ets:next(ibrowse_load_timings, Key), {max(Max, V), min(Min, V), Total + V}). spawn_workers(Num_w, Num_r) -> spawn_workers(Num_w, Num_r, self(), []). spawn_workers(0, _Num_requests, _Parent, Acc) -> lists:reverse(Acc); spawn_workers(Num_workers, Num_requests, Parent, Acc) -> Pid_ref = spawn_monitor(fun() -> random:seed(now()), case catch worker_loop(Parent, Num_requests) of {'EXIT', Rsn} -> io:format("Worker crashed with reason: ~p~n", [Rsn]); _ -> ok end end), spawn_workers(Num_workers - 1, Num_requests, Parent, [Pid_ref | Acc]). wait_for_workers([]) -> ok; wait_for_workers([{Pid, Pid_ref} | T] = Pids) -> receive {done, Pid} -> wait_for_workers(T); {done, Some_pid} -> wait_for_workers([{Pid, Pid_ref} | lists:keydelete(Some_pid, 1, T)]); print_diagnostics -> io:format("~1000.p~n", [ibrowse:get_metrics()]), erlang:send_after(1000, self(), print_diagnostics), wait_for_workers(Pids); query_state -> io:format("Waiting for ~p~n", [Pids]), wait_for_workers(Pids); shutdown -> io:format("Shutting down on command. Still waiting for ~p workers~n", [length(Pids)]); {'DOWN', _, process, _, normal} -> wait_for_workers(Pids); {'DOWN', _, process, Down_pid, Rsn} -> io:format("Worker ~p died. Reason: ~p~n", [Down_pid, Rsn]), wait_for_workers(lists:keydelete(Down_pid, 1, Pids)); X -> io:format("Recvd unknown msg: ~p~n", [X]), wait_for_workers(Pids) end. worker_loop(Parent, 0) -> Parent ! {done, self()}; worker_loop(Parent, N) -> Delay = random:uniform(100), Url = case Delay rem 10 of %% Change 10 to some number between 0-9 depending on how %% much chaos you want to introduce into the server %% side. The higher the number, the more often the %% server will close a connection after serving the %% first request, thereby forcing the client to %% retry. Any number of 10 or higher will disable this %% chaos mechanism 10 -> ets:update_counter(?ibrowse_load_test_counters, one_request_only, 1), "http://localhost:8081/ibrowse_handle_one_request_only"; _ -> "http://localhost:8081/blah" end, Start_time = now(), Res = ibrowse:send_req(Url, [], get), End_time = now(), Time_taken = trunc(round(timer:now_diff(End_time, Start_time) / 1000)), ets:insert(ibrowse_load_timings, {now(), Time_taken}), case Res of {ok, "200", _, _} -> ets:update_counter(?ibrowse_load_test_counters, success, 1); {error, req_timedout} -> ets:update_counter(?ibrowse_load_test_counters, timeout, 1); {error, retry_later} -> ets:update_counter(?ibrowse_load_test_counters, retry_later, 1); {error, Reason} -> update_unknown_counter(Reason, 1); _ -> io:format("~p -- Res: ~p~n", [self(), Res]), ets:update_counter(?ibrowse_load_test_counters, failed, 1) end, timer:sleep(Delay), worker_loop(Parent, N - 1). update_unknown_counter(Counter, Inc_val) -> case catch ets:update_counter(?ibrowse_load_test_counters, Counter, Inc_val) of {'EXIT', _} -> ets:insert_new(?ibrowse_load_test_counters, {Counter, 0}), update_unknown_counter(Counter, Inc_val); _ -> ok end.