%%
%% %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_bif_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_bif/1, dirty_bif_exception/1,
dirty_bif_multischedule/1,
dirty_bif_multischedule_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,
otp_15688/1]).
suite() -> [{ct_hooks,[ts_install_cth]}].
%%
%% All these tests utilize the debug BIFs:
%% - erts_debug:dirty_cpu/2 - Statically determined
%% to (begin to) execute on a dirty CPU scheduler.
%% - erts_debug:dirty_io/2 - Statically determined
%% to (begin to) execute on a dirty IO scheduler.
%% - erts_debug:dirty/3
%% Their implementations are located in
%% $ERL_TOP/erts/emulator/beam/beam_debug.c
%%
all() ->
[dirty_bif,
dirty_bif_multischedule,
dirty_bif_exception,
dirty_bif_multischedule_exception,
dirty_scheduler_exit,
dirty_call_while_terminated,
dirty_heap_access,
dirty_process_info,
dirty_process_register,
dirty_process_trace,
code_purge,
otp_15688].
init_per_suite(Config) ->
case erlang:system_info(dirty_cpu_schedulers) of
N when N > 0 ->
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_bif(Config) when is_list(Config) ->
dirty_cpu = erts_debug:dirty_cpu(scheduler,type),
dirty_io = erts_debug:dirty_io(scheduler,type),
normal = erts_debug:dirty(normal,scheduler,type),
dirty_cpu = erts_debug:dirty(dirty_cpu,scheduler,type),
dirty_io = erts_debug:dirty(dirty_io,scheduler,type),
ok.
dirty_bif_multischedule(Config) when is_list(Config) ->
ok = erts_debug:dirty_cpu(reschedule,1000),
ok = erts_debug:dirty_io(reschedule,1000),
ok = erts_debug:dirty(normal,reschedule,1000),
ok.
dirty_bif_exception(Config) when is_list(Config) ->
lists:foreach(fun (Error) ->
ErrorType = case Error of
_ when is_atom(Error) -> Error;
_ -> badarg
end,
try
erts_debug:dirty_cpu(error, Error),
ct:fail(expected_exception)
catch
error:ErrorType:Stk1 ->
[{erts_debug,dirty_cpu,[error, Error],_}|_] = Stk1,
ok
end,
try
apply(erts_debug,dirty_cpu,[error, Error]),
ct:fail(expected_exception)
catch
error:ErrorType:Stk2 ->
[{erts_debug,dirty_cpu,[error, Error],_}|_] = Stk2,
ok
end,
try
erts_debug:dirty_io(error, Error),
ct:fail(expected_exception)
catch
error:ErrorType:Stk3 ->
[{erts_debug,dirty_io,[error, Error],_}|_] = Stk3,
ok
end,
try
apply(erts_debug,dirty_io,[error, Error]),
ct:fail(expected_exception)
catch
error:ErrorType:Stk4 ->
[{erts_debug,dirty_io,[error, Error],_}|_] = Stk4,
ok
end,
try
erts_debug:dirty(normal, error, Error),
ct:fail(expected_exception)
catch
error:ErrorType:Stk5 ->
[{erts_debug,dirty,[normal, error, Error],_}|_] = Stk5,
ok
end,
try
apply(erts_debug,dirty,[normal, error, Error]),
ct:fail(expected_exception)
catch
error:ErrorType:Stk6 ->
[{erts_debug,dirty,[normal, error, Error],_}|_] = Stk6,
ok
end,
try
erts_debug:dirty(dirty_cpu, error, Error),
ct:fail(expected_exception)
catch
error:ErrorType:Stk7 ->
[{erts_debug,dirty,[dirty_cpu, error, Error],_}|_] = Stk7,
ok
end,
try
apply(erts_debug,dirty,[dirty_cpu, error, Error]),
ct:fail(expected_exception)
catch
error:ErrorType:Stk8 ->
[{erts_debug,dirty,[dirty_cpu, error, Error],_}|_] = Stk8,
ok
end,
try
erts_debug:dirty(dirty_io, error, Error),
ct:fail(expected_exception)
catch
error:ErrorType:Stk9 ->
[{erts_debug,dirty,[dirty_io, error, Error],_}|_] = Stk9,
ok
end,
try
apply(erts_debug,dirty,[dirty_io, error, Error]),
ct:fail(expected_exception)
catch
error:ErrorType:Stk10 ->
[{erts_debug,dirty,[dirty_io, error, Error],_}|_] = Stk10,
ok
end
end,
[badarg, undef, badarith, system_limit, noproc,
make_ref(), {another, "heap", term_to_binary("term")}]),
ok.
dirty_bif_multischedule_exception(Config) when is_list(Config) ->
try
erts_debug:dirty_cpu(reschedule,1001)
catch
error:badarg:Stk1 ->
[{erts_debug,dirty_cpu,[reschedule, 1001],_}|_] = Stk1,
ok
end,
try
erts_debug:dirty_io(reschedule,1001)
catch
error:badarg:Stk2 ->
[{erts_debug,dirty_io,[reschedule, 1001],_}|_] = Stk2,
ok
end,
try
erts_debug:dirty(normal,reschedule,1001)
catch
error:badarg:Stk3 ->
[{erts_debug,dirty,[normal,reschedule,1001],_}|_] = Stk3,
ok
end.
dirty_scheduler_exit(Config) when is_list(Config) ->
{ok, Node} = start_node(Config, "+SDio 1"),
[ok] = mcall(Node,
[fun() ->
%% 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 () -> erts_debug:dirty_io(wait, 1000) end),
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 () ->
erts_debug:dirty_cpu(alive_waitexiting, Me),
blipp:blupp(Bin)
end,
[monitor,link]),
receive {alive, Dirty} -> 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 refered by Dirty process not yet cleaned up
%% since the dirty bif 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 = erts_debug:dirty_cpu(copy, 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 BIF where the main lock is needed for that access do not get
%% blocked. Each test passes its pid to dirty_sleeper, which sends an
%% 'alive' message when it's running on a dirty scheduler and just before
%% it starts a 6 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 6 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(BifPid) ->
PI = process_info(BifPid),
{current_function,{erts_debug,dirty_io,2}} =
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(BifPid) ->
register(test_dirty_process_register, BifPid),
BifPid = 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({erts_debug,dirty_io,2},
[{'_',[],[{return_trace}]}],
[local,meta]),
ok
end,
fun(BifPid) ->
erlang:trace(BifPid, true, [call,timestamp]),
ok
end,
fun(BifPid) ->
receive
{done, BifPid} ->
receive
{trace_ts,BifPid,call,{erts_debug,dirty_io,_},_} ->
ok
after
0 ->
error(missing_trace_call_message)
end %%,
%% receive
%% {trace_ts,BifPid,return_from,{erts_debug,dirty_io,2},
%% ok,_} ->
%% ok
%% after
%% 100 ->
%% error(missing_trace_return_message)
%% end
after
6500 ->
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 6 seconds
%% in dirty bif...
erts_debug:dirty_io(wait,6000)
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 6 seconds
%% in dirty bif...
erts_debug:dirty_io(wait,6000)
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.
otp_15688(Config) when is_list(Config) ->
ImBack = make_ref(),
{See, SeeMon} = spawn_monitor(fun () ->
erts_debug:dirty_io(wait, 2000),
exit(ImBack)
end),
wait_until(fun () ->
[{current_function, {erts_debug, dirty_io, 2}},
{status, running}]
== process_info(See,
[current_function, status])
end),
{Ser1, Ser1Mon} = spawn_monitor(fun () ->
erlang:suspend_process(See,
[asynchronous])
end),
erlang:suspend_process(See, [asynchronous]),
receive {'DOWN', Ser1Mon, process, Ser1, normal} -> ok end,
%% Verify that we sent the suspend request while it was executing dirty...
[{current_function, {erts_debug, dirty_io, 2}},
{status, running}] = process_info(See, [current_function, status]),
wait_until(fun () ->
{status, suspended} == process_info(See, status)
end),
erlang:resume_process(See),
receive
{'DOWN', SeeMon, process, See, Reason} ->
ImBack = Reason
after 4000 ->
%% Resume bug seems to have hit us...
PI = process_info(See),
exit(See, kill),
ct:fail({suspendee_stuck, PI})
end.
%%
%% Internal...
%%
wait_until(Fun) ->
case Fun() of
true ->
ok;
_ ->
receive after 100 -> ok end,
wait_until(Fun)
end.
access_dirty_process(Config, Start, Test, Finish) ->
{ok, Node} = start_node(Config, ""),
[ok] = mcall(Node,
[fun() ->
ok = test_dirty_process_access(Start, Test, Finish)
end]),
stop_node(Node),
ok.
test_dirty_process_access(Start, Test, Finish) ->
ok = Start(),
Self = self(),
BifPid = spawn_link(fun() ->
ok = erts_debug:dirty_io(ready_wait6_done, Self)
end),
ok = receive
{ready, BifPid} ->
ok = Test(BifPid),
receive
{done, BifPid} ->
error(dirty_process_info_blocked)
after
0 ->
true = erlang:is_process_alive(BifPid),
ok
end
after
3000 ->
error(timeout)
end,
ok = Finish(BifPid).
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).