Skip to content

Commit

Permalink
cover: Use native coverage if supported
Browse files Browse the repository at this point in the history
Update the `cover` tool to use the native coverage feature of
the runtime system. Currently at least, the runtime system only
supports coverage through the JIT.

TO DO:

* When running on multiple nodes, only use the native coverage
functionality if all connected Erlang nodes support it.

* When asked to compile a module and that module is already
loaded with line counters enabled, don't compile or load the
module. That would support running cover on modules that are
not possible to reload (such as many modules in Kernel or even
the preloaded modules for Erts).
  • Loading branch information
bjorng committed Nov 9, 2023
1 parent 9a11f62 commit db7b3f1
Showing 1 changed file with 100 additions and 41 deletions.
141 changes: 100 additions & 41 deletions lib/tools/src/cover.erl
Original file line number Diff line number Diff line change
Expand Up @@ -1268,7 +1268,7 @@ do_start_nodes(Nodes, State) ->
{_LoadedModules,Compiled} =
get_compiled_still_loaded(State#main_state.nodes,
State#main_state.compiled),
remote_load_compiled(StartedNodes,Compiled),
remote_load_compiled(StartedNodes, Compiled, State),

State1 =
State#main_state{nodes = State#main_state.nodes ++ StartedNodes,
Expand Down Expand Up @@ -1317,7 +1317,7 @@ sync_compiled(Node,State) ->
remote_unload([Node],Unload),
Load = [L || L <- Compiled,
false == lists:member(L,RemoteCompiled)],
remote_load_compiled([Node],Load),
remote_load_compiled([Node], Load, State),
State#main_state{compiled=Compiled, nodes=[Node|Nodes]}
end,
State1#main_state{lost_nodes=Lost--[Node]}.
Expand All @@ -1326,8 +1326,14 @@ sync_compiled(Node,State) ->
%% We do it ?MAX_MODS modules at a time so that we don't
%% run out of memory on the cover_server node.
-define(MAX_MODS, 10).
remote_load_compiled(Nodes,Compiled) ->
remote_load_compiled(Nodes, Compiled, [], 0).
remote_load_compiled(Nodes, Compiled, #main_state{local_only=LocalOnly}) ->
case LocalOnly of
true ->
ok;
false ->
remote_load_compiled(Nodes, Compiled, [], 0)
end.

remote_load_compiled(_Nodes, [], [], _ModNum) ->
ok;
remote_load_compiled(Nodes, Compiled, Acc, ModNum)
Expand Down Expand Up @@ -1639,7 +1645,7 @@ do_compile_beams(ModsAndFiles, State) ->
end,
ModsAndFiles),
Compiled = [{M,F} || {ok,M,F} <- Result0],
remote_load_compiled(State#main_state.nodes,Compiled),
remote_load_compiled(State#main_state.nodes, Compiled, State),
fix_state_and_result(Result0,State,[]).

do_compile_beam(Module,BeamFile0,State) ->
Expand Down Expand Up @@ -1680,7 +1686,7 @@ do_compile(Files, Options, State) ->
end,
Files),
Compiled = [{M,F} || {ok,M,F} <- Result0],
remote_load_compiled(State#main_state.nodes,Compiled),
remote_load_compiled(State#main_state.nodes, Compiled, State),
fix_state_and_result(Result0,State,[]).

do_compile1(File, Options, LocalOnly) ->
Expand Down Expand Up @@ -1756,7 +1762,9 @@ do_compile_beam2(Module,Beam,UserOptions,Forms0,MainFile,LocalOnly) ->
%% Compile and load the result.
%% It's necessary to check the result of loading since it may
%% fail, for example if Module resides in a sticky directory.
Options = SourceInfo ++ UserOptions,
Options0 = SourceInfo ++ UserOptions,
Options = [report_errors,force_line_counters|Options0],

{ok, Module, Binary} = compile:forms(Forms, Options),

case code:load_binary(Module, ?TAG, Binary) of
Expand Down Expand Up @@ -1798,7 +1806,7 @@ get_compile_info(Module, Beam) ->
end.

transform(Code, Module, _MainFile, LocalOnly) ->
{ok, MungedForms0,InitInfo} = sys_coverage:cover_transform(Code, fun counter_index/5),
{ok,MungedForms0,InitInfo} = sys_coverage:cover_transform(Code, fun counter_index/5),
MungedForms = patch_code(Module, MungedForms0, LocalOnly),
{MungedForms,InitInfo}.

Expand Down Expand Up @@ -1832,10 +1840,15 @@ counter_index(Mod, F, A, C, Line) ->

%% Create the counter array and store as a persistent term.
maybe_create_counters(Mod, true) ->
Cref = create_counters(Mod),
Key = {?MODULE,Mod},
persistent_term:put(Key, Cref),
ok;
case has_native_coverage() of
false ->
Cref = create_counters(Mod),
Key = {?MODULE,Mod},
persistent_term:put(Key, Cref),
ok;
true ->
ok
end;
maybe_create_counters(_Mod, false) ->
ok.

Expand All @@ -1846,14 +1859,20 @@ create_counters(Mod) ->
ets:insert(?COVER_MAPPING_TABLE, {{counters,Mod},Cref}),
Cref.

patch_code(Mod, Forms, false) ->
A = erl_anno:new(0),
AbstrKey = {tuple,A,[{atom,A,?MODULE},{atom,A,Mod}]},
patch_code1(Forms, {distributed,AbstrKey});
patch_code(Mod, Forms, true) ->
Cref = create_counters(Mod),
AbstrCref = cid_to_abstract(Cref),
patch_code1(Forms, {local_only,AbstrCref}).
patch_code(Mod, Forms, Local) ->
case has_native_coverage() of
true ->
_ = catch erlang:reset_coverage(Mod),
Forms;
false when Local =:= false ->
A = erl_anno:new(0),
AbstrKey = {tuple,A,[{atom,A,?MODULE},{atom,A,Mod}]},
patch_code1(Forms, {distributed,AbstrKey});
false when Local =:= true ->
Cref = create_counters(Mod),
AbstrCref = cid_to_abstract(Cref),
patch_code1(Forms, {local_only,AbstrCref})
end.

%% Go through the abstract code and replace 'executable_line' forms
%% with the actual code to increment the counters.
Expand Down Expand Up @@ -1907,30 +1926,37 @@ send_counters(Mod, CollectorPid) ->
%% Called on the main node. Collect the counters and consolidate
%% them into the collection table. Also zero the counters.
move_counters(Mod) ->
move_counters(Mod, fun insert_in_collection_table/1).
Process = fun insert_in_collection_table/1,
move_counters(Mod, Process).

move_counters(Mod, Process) ->
Move = case has_native_coverage() of
true ->
native_move(Mod);
false ->
standard_move(Mod)
end,
Pattern = {#bump{module=Mod,_='_'},'_'},
Matches = ets:match_object(?COVER_MAPPING_TABLE, Pattern, ?CHUNK_SIZE),
Cref = get_counters_ref(Mod),
move_counters1(Matches, Cref, Process).
move_counters1(Matches, Move, Process).

move_counters1({Mappings,Continuation}, Cref, Process) ->
Move = fun({Key,Index}) ->
Count = counters:get(Cref, Index),
ok = counters:sub(Cref, Index, Count),
{Key,Count}
end,
Process(lists:map(Move, Mappings)),
move_counters1(ets:match_object(Continuation), Cref, Process);
move_counters1('$end_of_table', _Cref, _Process) ->
move_counters1({Mappings,Continuation}, Move, Process) ->
Moved = [Move(Item) || Item <- Mappings],
Process(Moved),
move_counters1(ets:match_object(Continuation), Move, Process);
move_counters1('$end_of_table', _Move, _Process) ->
ok.

counters_mapping_table(Mod) ->
Mapping = counters_mapping(Mod),
Cref = get_counters_ref(Mod),
#{size:=Size} = counters:info(Cref),
[{Mod,Size}|Mapping].
case has_native_coverage() of
false ->
Cref = get_counters_ref(Mod),
#{size:=Size} = counters:info(Cref),
[{Mod,Size}|Mapping];
true ->
Mapping
end.

get_counters_ref(Mod) ->
ets:lookup_element(?COVER_MAPPING_TABLE, {counters,Mod}, 2).
Expand All @@ -1946,14 +1972,44 @@ clear_counters(Mod) ->
_ = ets:match_delete(?COVER_MAPPING_TABLE, Pattern),
ok.

standard_move(Mod) ->
Cref = get_counters_ref(Mod),
fun({Key,Index}) ->
Count = counters:get(Cref, Index),
ok = counters:sub(Cref, Index, Count),
{Key,Count}
end.

native_move(Mod) ->
Coverage = maps:from_list(erlang:get_line_coverage(Mod)),
_ = erlang:reset_coverage(Mod),
fun({#bump{line=Line}=Key,_Index}) ->
case Coverage of
#{Line := false} ->
{Key,0};
#{Line := true} ->
{Key,1};
#{Line := N} when is_integer(N), N >= 0 ->
{Key,N};
#{} ->
{Key,0}
end
end.

%% Reset counters (set counters to 0).
reset_counters(Mod) ->
Pattern = {#bump{module=Mod,_='_'},'$1'},
MatchSpec = [{Pattern,[],['$1']}],
Matches = ets:select(?COVER_MAPPING_TABLE,
MatchSpec, ?CHUNK_SIZE),
Cref = get_counters_ref(Mod),
reset_counters1(Matches, Cref).
case has_native_coverage() of
true ->
_ = catch erlang:reset_coverage(Mod),
ok;
false ->
Pattern = {#bump{module=Mod,_='_'},'$1'},
MatchSpec = [{Pattern,[],['$1']}],
Matches = ets:select(?COVER_MAPPING_TABLE,
MatchSpec, ?CHUNK_SIZE),
Cref = get_counters_ref(Mod),
reset_counters1(Matches, Cref)
end.

reset_counters1({Indices,Continuation}, Cref) ->
_ = [counters:put(Cref, N, 0) || N <- Indices],
Expand Down Expand Up @@ -2665,3 +2721,6 @@ html_encoding(latin1) ->
"iso-8859-1";
html_encoding(utf8) ->
"utf-8".

has_native_coverage() ->
erlang:system_info(coverage_support).

0 comments on commit db7b3f1

Please sign in to comment.