%% %% %CopyrightBegin% %% %% Copyright Ericsson AB 2010-2017. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. %% You may obtain a copy of the License at %% %% http://www.apache.org/licenses/LICENSE-2.0 %% %% Unless required by applicable law or agreed to in writing, software %% distributed under the License is distributed on an "AS IS" BASIS, %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. %% See the License for the specific language governing permissions and %% limitations under the License. %% %% %CopyrightEnd% %% -module(dirty_nif_SUITE). %%-define(line_trace,true). -define(CHECK(Exp,Got), check(Exp,Got,?LINE)). %%-define(CHECK(Exp,Got), Exp = Got). -include_lib("common_test/include/ct.hrl"). -export([all/0, suite/0, init_per_suite/1, end_per_suite/1, init_per_testcase/2, end_per_testcase/2, dirty_nif/1, dirty_nif_send/1, dirty_nif_exception/1, call_dirty_nif_exception/1, dirty_scheduler_exit/1, dirty_call_while_terminated/1, dirty_heap_access/1, dirty_process_info/1, dirty_process_register/1, dirty_process_trace/1, code_purge/1, dirty_nif_send_traced/1, nif_whereis/1, nif_whereis_parallel/1, nif_whereis_proxy/1]). -define(nif_stub,nif_stub_error(?LINE)). suite() -> [{ct_hooks,[ts_install_cth]}]. all() -> [dirty_nif, dirty_nif_send, dirty_nif_exception, dirty_scheduler_exit, dirty_call_while_terminated, dirty_heap_access, dirty_process_info, dirty_process_register, dirty_process_trace, code_purge, dirty_nif_send_traced, nif_whereis, nif_whereis_parallel]. init_per_suite(Config) -> case erlang:system_info(dirty_cpu_schedulers) of N when N > 0 -> case lib_loaded() of false -> ok = erlang:load_nif( filename:join(?config(data_dir, Config), "dirty_nif_SUITE"), []); true -> ok end, Config; _ -> {skipped, "No dirty scheduler support"} end. end_per_suite(_Config) -> ok. init_per_testcase(Case, Config) -> [{testcase, Case} | Config]. end_per_testcase(_Case, _Config) -> ok. dirty_nif(Config) when is_list(Config) -> Val1 = 42, Val2 = "Erlang", Val3 = list_to_binary([Val2, 0]), {Val1, Val2, Val3} = call_dirty_nif(Val1, Val2, Val3), LargeArray = lists:duplicate(1000, ok), LargeArray = call_dirty_nif_zero_args(), ok. dirty_nif_send(Config) when is_list(Config) -> Parent = self(), Pid = spawn_link(fun() -> Self = self(), {ok, Self} = receive_any(), Parent ! {ok, Self} end), {ok, Pid} = send_from_dirty_nif(Pid), {ok, Pid} = receive_any(), ok. dirty_nif_exception(Config) when is_list(Config) -> try %% this checks that the expected exception occurs when the %% dirty NIF returns the result of enif_make_badarg %% directly call_dirty_nif_exception(1), ct:fail(expected_badarg) catch error:badarg:Stk1 -> [{?MODULE,call_dirty_nif_exception,[1],_}|_] = Stk1, ok end, try %% this checks that the expected exception occurs when the %% dirty NIF calls enif_make_badarg at some point but then %% returns a value that isn't an exception call_dirty_nif_exception(0), ct:fail(expected_badarg) catch error:badarg:Stk2 -> [{?MODULE,call_dirty_nif_exception,[0],_}|_] = Stk2, ok end, %% this checks that a dirty NIF can raise various terms as %% exceptions ok = nif_raise_exceptions(call_dirty_nif_exception). nif_raise_exceptions(NifFunc) -> ExcTerms = [{error, test}, "a string", <<"a binary">>, 42, [1,2,3,4,5], [{p,1},{p,2},{p,3}]], lists:foldl(fun(Term, ok) -> try erlang:apply(?MODULE,NifFunc,[Term]), ct:fail({expected,Term}) catch error:Term:Stk -> [{?MODULE,NifFunc,[Term],_}|_] = Stk, ok end end, ok, ExcTerms). dirty_scheduler_exit(Config) when is_list(Config) -> {ok, Node} = start_node(Config, "+SDio 1"), Path = proplists:get_value(data_dir, Config), NifLib = filename:join(Path, atom_to_list(?MODULE)), [ok] = mcall(Node, [fun() -> ok = erlang:load_nif(NifLib, []), %% Perform a dry run to ensure that all required code %% is loaded. Otherwise the test will fail since code %% loading is done through dirty IO and it won't make %% any progress during this test. _DryRun = test_dirty_scheduler_exit(), Start = erlang:monotonic_time(millisecond), ok = test_dirty_scheduler_exit(), End = erlang:monotonic_time(millisecond), io:format("Time=~p ms~n", [End-Start]), ok end]), stop_node(Node), ok. test_dirty_scheduler_exit() -> process_flag(trap_exit,true), test_dse(10,[]). test_dse(0,Pids) -> timer:sleep(100), kill_dse(Pids,[]); test_dse(N,Pids) -> Pid = spawn_link(fun dirty_sleeper/0), test_dse(N-1,[Pid|Pids]). kill_dse([],Killed) -> wait_dse(Killed, ok); kill_dse([Pid|Pids],AlreadyKilled) -> exit(Pid,kill), kill_dse(Pids,[Pid|AlreadyKilled]). wait_dse([], Result) -> Result; wait_dse([Pid|Pids], Result) -> receive {'EXIT', Pid, killed} -> wait_dse(Pids, Result); {'EXIT', Pid, _Other} -> wait_dse(Pids, failed) end. dirty_call_while_terminated(Config) when is_list(Config) -> Me = self(), Bin = list_to_binary(lists:duplicate(4711, $r)), {value, {BinAddr, 4711, 1}} = lists:keysearch(4711, 2, element(2, process_info(self(), binary))), {Dirty, DM} = spawn_opt(fun () -> dirty_call_while_terminated_nif(Me), blipp:blupp(Bin) end, [monitor,link]), receive {dirty_alive, _Pid} -> ok end, {value, {BinAddr, 4711, 2}} = lists:keysearch(4711, 2, element(2, process_info(self(), binary))), Reason = die_dirty_process, OT = process_flag(trap_exit, true), exit(Dirty, Reason), receive {'DOWN', DM, process, Dirty, R0} -> R0 = Reason end, receive {'EXIT', Dirty, R1} -> R1 = Reason end, undefined = process_info(Dirty), undefined = process_info(Dirty, status), false = erlang:is_process_alive(Dirty), false = lists:member(Dirty, processes()), %% Binary still referred by Dirty process not yet cleaned up %% since the dirty nif has not yet returned... {value, {BinAddr, 4711, 2}} = lists:keysearch(4711, 2, element(2, process_info(self(), binary))), receive after 2000 -> ok end, receive Msg -> ct:fail({unexpected_message, Msg}) after 0 -> ok end, {value, {BinAddr, 4711, 1}} = lists:keysearch(4711, 2, element(2, process_info(self(), binary))), process_flag(trap_exit, OT), try blipp:blupp(Bin) catch _ : _ -> ok end. dirty_heap_access(Config) when is_list(Config) -> {ok, Node} = start_node(Config), Me = self(), RGL = rpc:call(Node,erlang,whereis,[init]), Ref = rpc:call(Node,erlang,make_ref,[]), Dirty = spawn_link(fun () -> Res = dirty_heap_access_nif(Ref), garbage_collect(), Me ! {self(), Res}, receive after infinity -> ok end end), {N, R} = access_dirty_heap(Dirty, RGL, 0, 0), receive {_Pid, Res} -> 1000 = length(Res), lists:foreach(fun (X) -> Ref = X end, Res) end, unlink(Dirty), exit(Dirty, kill), stop_node(Node), {comment, integer_to_list(N) ++ " GL change loops; " ++ integer_to_list(R) ++ " while running dirty"}. access_dirty_heap(Dirty, RGL, N, R) -> case process_info(Dirty, status) of {status, waiting} -> {N, R}; {status, Status} -> {group_leader, GL} = process_info(Dirty, group_leader), true = group_leader(RGL, Dirty), {group_leader, RGL} = process_info(Dirty, group_leader), true = group_leader(GL, Dirty), {group_leader, GL} = process_info(Dirty, group_leader), access_dirty_heap(Dirty, RGL, N+1, case Status of running -> R+1; _ -> R end) end. %% These tests verify that processes that access a process executing a %% dirty NIF where the main lock is needed for that access do not get %% blocked. Each test passes its pid to dirty_sleeper, which sends a %% 'ready' message when it's running on a dirty scheduler and just before %% it starts a 2 second sleep. When it receives the message, it verifies %% that access to the dirty process is as it expects. After the dirty %% process finishes its 2 second sleep but before it returns from the dirty %% scheduler, it sends a 'done' message. If the tester already received %% that message, the test fails because it means attempting to access the %% dirty process waited for that process to return to a regular scheduler, %% so verify that we haven't received that message, and also verify that %% the dirty process is still alive immediately after accessing it. dirty_process_info(Config) when is_list(Config) -> access_dirty_process( Config, fun() -> ok end, fun(NifPid) -> PI = process_info(NifPid), {current_function,{?MODULE,dirty_sleeper,1}} = lists:keyfind(current_function, 1, PI), ok end, fun(_) -> ok end). dirty_process_register(Config) when is_list(Config) -> access_dirty_process( Config, fun() -> ok end, fun(NifPid) -> register(test_dirty_process_register, NifPid), NifPid = whereis(test_dirty_process_register), unregister(test_dirty_process_register), false = lists:member(test_dirty_process_register, registered()), ok end, fun(_) -> ok end). dirty_process_trace(Config) when is_list(Config) -> access_dirty_process( Config, fun() -> erlang:trace_pattern({?MODULE,dirty_sleeper,1}, [{'_',[],[{return_trace}]}], [local,meta]), ok end, fun(NifPid) -> erlang:trace(NifPid, true, [call,timestamp]), ok end, fun(NifPid) -> receive done -> receive {trace_ts,NifPid,call,{?MODULE,dirty_sleeper,_},_} -> ok after 0 -> error(missing_trace_call_message) end, receive {trace_ts,NifPid,return_from,{?MODULE,dirty_sleeper,1}, ok,_} -> ok after 100 -> error(missing_trace_return_message) end after 2500 -> error(missing_done_message) end, ok end). dirty_code_test_code() -> " -module(dirty_code_test). -export([func/1]). func(Fun) -> Fun(), blipp:blapp(). ". code_purge(Config) when is_list(Config) -> Path = ?config(data_dir, Config), File = filename:join(Path, "dirty_code_test.erl"), ok = file:write_file(File, dirty_code_test_code()), {ok, dirty_code_test, Bin} = compile:file(File, [binary]), {module, dirty_code_test} = erlang:load_module(dirty_code_test, Bin), Start = erlang:monotonic_time(), {Pid1, Mon1} = spawn_monitor(fun () -> dirty_code_test:func(fun () -> %% Sleep for 2 seconds %% in dirty nif... dirty_sleeper() end) end), {module, dirty_code_test} = erlang:load_module(dirty_code_test, Bin), {Pid2, Mon2} = spawn_monitor(fun () -> dirty_code_test:func(fun () -> %% Sleep for 2 seconds %% in dirty nif... dirty_sleeper() end) end), receive {'DOWN', Mon1, process, Pid1, _} -> ct:fail(premature_death) after 100 -> ok end, true = erlang:purge_module(dirty_code_test), receive {'DOWN', Mon1, process, Pid1, Reason1} -> killed = Reason1 end, receive {'DOWN', Mon2, process, Pid2, _} -> ct:fail(premature_death) after 100 -> ok end, true = erlang:delete_module(dirty_code_test), receive {'DOWN', Mon2, process, Pid2, _} -> ct:fail(premature_death) after 100 -> ok end, true = erlang:purge_module(dirty_code_test), receive {'DOWN', Mon2, process, Pid2, Reason2} -> killed = Reason2 end, End = erlang:monotonic_time(), Time = erlang:convert_time_unit(End-Start, native, milli_seconds), io:format("Time=~p~n", [Time]), true = Time =< 1000, ok. dirty_nif_send_traced(Config) when is_list(Config) -> Parent = self(), Rcvr = spawn_link(fun() -> Self = self(), receive {ok, Self} -> ok end, Parent ! {Self, received} end), Sndr = spawn_link(fun () -> receive {Parent, go} -> ok end, {ok, Rcvr} = send_wait_from_dirty_nif(Rcvr), Parent ! {self(), sent} end), 1 = erlang:trace(Sndr, true, [send]), Start = erlang:monotonic_time(), Sndr ! {self(), go}, receive {trace, Sndr, send, {ok, Rcvr}, Rcvr} -> ok end, receive {Rcvr, received} -> ok end, End1 = erlang:monotonic_time(), Time1 = erlang:convert_time_unit(End1-Start, native, 1000), io:format("Time1: ~p milliseconds~n", [Time1]), true = Time1 < 500, receive {Sndr, sent} -> ok end, End2 = erlang:monotonic_time(), Time2 = erlang:convert_time_unit(End2-Start, native, 1000), io:format("Time2: ~p milliseconds~n", [Time2]), true = Time2 >= 1900, ok. %% %% Internal... %% access_dirty_process(Config, Start, Test, Finish) -> {ok, Node} = start_node(Config, ""), [ok] = mcall(Node, [fun() -> Path = ?config(data_dir, Config), Lib = atom_to_list(?MODULE), ok = erlang:load_nif(filename:join(Path,Lib), []), ok = test_dirty_process_access(Start, Test, Finish) end]), stop_node(Node), ok. test_dirty_process_access(Start, Test, Finish) -> ok = Start(), Self = self(), NifPid = spawn_link(fun() -> ok = dirty_sleeper(Self) end), ok = receive ready -> ok = Test(NifPid), receive done -> error(dirty_process_info_blocked) after 0 -> true = erlang:is_process_alive(NifPid), ok end after 1000 -> error(timeout) end, ok = Finish(NifPid). receive_any() -> receive M -> M end. start_node(Config) -> start_node(Config, ""). start_node(Config, Args) when is_list(Config) -> Pa = filename:dirname(code:which(?MODULE)), Name = list_to_atom(atom_to_list(?MODULE) ++ "-" ++ atom_to_list(proplists:get_value(testcase, Config)) ++ "-" ++ integer_to_list(erlang:system_time(second)) ++ "-" ++ integer_to_list(erlang:unique_integer([positive]))), test_server:start_node(Name, slave, [{args, "-pa "++Pa++" "++Args}]). stop_node(Node) -> test_server:stop_node(Node). mcall(Node, Funs) -> Parent = self(), Refs = lists:map(fun (Fun) -> Ref = make_ref(), spawn_link(Node, fun () -> Res = Fun(), unlink(Parent), Parent ! {Ref, Res} end), Ref end, Funs), lists:map(fun (Ref) -> receive {Ref, Res} -> Res end end, Refs). %% Test enif_whereis_... %% These tests are mostly identical to their counterparts in nif_SUITE.erl, %% with just name and count changes in the first few lines. nif_whereis(Config) when is_list(Config) -> erl_ddll:try_load(?config(data_dir, Config), echo_drv, []), RegName = dirty_nif_whereis_test_thing, undefined = erlang:whereis(RegName), false = whereis_term(pid, RegName), Mgr = self(), Ref = make_ref(), ProcMsg = {Ref, ?LINE}, PortMsg = ?MODULE_STRING " whereis hello\n", {Pid, Mon} = spawn_monitor(?MODULE, nif_whereis_proxy, [Ref]), true = register(RegName, Pid), Pid = erlang:whereis(RegName), Pid = whereis_term(pid, RegName), false = whereis_term(port, RegName), false = whereis_term(pid, [RegName]), ok = whereis_send(pid, RegName, {forward, Mgr, ProcMsg}), ok = receive ProcMsg -> ok end, Pid ! {Ref, quit}, ok = receive {'DOWN', Mon, process, Pid, normal} -> ok end, undefined = erlang:whereis(RegName), false = whereis_term(pid, RegName), Port = open_port({spawn, echo_drv}, [eof]), true = register(RegName, Port), Port = erlang:whereis(RegName), Port = whereis_term(port, RegName), false = whereis_term(pid, RegName), false = whereis_term(port, [RegName]), ok = whereis_send(port, RegName, PortMsg), ok = receive {Port, {data, PortMsg}} -> ok end, port_close(Port), undefined = erlang:whereis(RegName), false = whereis_term(port, RegName), ok. nif_whereis_parallel(Config) when is_list(Config) -> %% try to be at least a little asymetric NProcs = trunc(3.5 * erlang:system_info(schedulers)), NSeq = lists:seq(1, NProcs), Names = [list_to_atom("dirty_nif_whereis_proc_" ++ integer_to_list(N)) || N <- NSeq], Mgr = self(), Ref = make_ref(), NotReg = fun(Name) -> erlang:whereis(Name) == undefined end, PidReg = fun({Name, Pid, _Mon}) -> erlang:whereis(Name) == Pid andalso whereis_term(pid, Name) == Pid end, RecvDown = fun({_Name, Pid, Mon}) -> receive {'DOWN', Mon, process, Pid, normal} -> true after 1500 -> false end end, RecvNum = fun(N) -> receive {N, Ref} -> true after 1500 -> false end end, true = lists:all(NotReg, Names), %% {Name, Pid, Mon} Procs = lists:map( fun(N) -> Name = lists:nth(N, Names), Prev = lists:nth((if N == 1 -> NProcs; true -> (N - 1) end), Names), Next = lists:nth((if N == NProcs -> 1; true -> (N + 1) end), Names), {Pid, Mon} = spawn_monitor( ?MODULE, nif_whereis_proxy, [{N, Ref, Mgr, [Prev, Next]}]), true = register(Name, Pid), {Name, Pid, Mon} end, NSeq), true = lists:all(PidReg, Procs), %% tell them all to 'fire' as fast as we can [P ! {Ref, send_proc} || {_, P, _} <- Procs], %% each gets forwarded through two processes true = lists:all(RecvNum, NSeq), true = lists:all(RecvNum, NSeq), %% tell them all to 'quit' by name [N ! {Ref, quit} || {N, _, _} <- Procs], true = lists:all(RecvDown, Procs), true = lists:all(NotReg, Names), ok. %% exported to be spawned by MFA by whereis tests nif_whereis_proxy({N, Ref, Mgr, Targets} = Args) -> receive {forward, To, Data} -> To ! Data, nif_whereis_proxy(Args); {Ref, quit} -> ok; {Ref, send_port} -> Msg = ?MODULE_STRING " whereis " ++ integer_to_list(N) ++ "\n", lists:foreach( fun(T) -> ok = whereis_send(port, T, Msg) end, Targets), nif_whereis_proxy(Args); {Ref, send_proc} -> lists:foreach( fun(T) -> ok = whereis_send(pid, T, {forward, Mgr, {N, Ref}}) end, Targets), nif_whereis_proxy(Args) end; nif_whereis_proxy(Ref) -> receive {forward, To, Data} -> To ! Data, nif_whereis_proxy(Ref); {Ref, quit} -> ok end. %% The NIFs: lib_loaded() -> false. call_dirty_nif(_,_,_) -> ?nif_stub. send_from_dirty_nif(_) -> ?nif_stub. send_wait_from_dirty_nif(_) -> ?nif_stub. call_dirty_nif_exception(_) -> ?nif_stub. call_dirty_nif_zero_args() -> ?nif_stub. dirty_call_while_terminated_nif(_) -> ?nif_stub. dirty_sleeper() -> ?nif_stub. dirty_sleeper(_) -> ?nif_stub. dirty_heap_access_nif(_) -> ?nif_stub. whereis_term(_Type,_Name) -> ?nif_stub. whereis_send(_Type,_Name,_Msg) -> ?nif_stub. nif_stub_error(Line) -> exit({nif_not_loaded,module,?MODULE,line,Line}).