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