Skip to content

Commit

Permalink
WIP: cover: Use native coverage if enabled
Browse files Browse the repository at this point in the history
This is a crude modification to use the native coverage feature when
it is enabled in the runtime system when running the compiler test
suite.

Some rough measurements when running the compiler test suite on
my Intel iMac:

* Without coverage: 4 min 22 sec

* With -JPcover line_coverage: 5 min 42 sec

* With -JPcover line_counters: 6 min 5 sec

* With traditional coverage: 8 min 15 sec

Note that when coverage is enabled for the compiler test suite, each
test module will be recompiled, which does not happen when running
without coverage. Therefore, these measurements don't say anything
about the overhead for native coverage compared to uninstrumented
code.

To get a better idea of the overhead, I compiled the entire compiler
with the `line_coverage` option, and then run the test case
`compilation_SUITE:self_compile/1` with different configurations. I
ran each test case several times for each configuration and noted the
shortest time for each:

* -JPcover false: 11.5 sec

* -JPcover line_coverage: 11.5 sec

* -JPcover line_counters: 12.4 sec

I repeated the measurments on my M1 MacBook Pro and got the following
times:

* -JPcover false: 7.2 sec

* -JPcover line_coverage: 7.2 sec

* -JPcover line_counters: 8.0 sec

At least according to this benchmark, the overhead for the
line_coverage mode is negligible.
  • Loading branch information
bjorng committed Nov 5, 2023
1 parent f730cde commit e46015a
Showing 1 changed file with 91 additions and 26 deletions.
117 changes: 91 additions & 26 deletions lib/tools/src/cover.erl
Original file line number Diff line number Diff line change
Expand Up @@ -1373,11 +1373,16 @@ get_downs_r(Mons) ->
get_data_for_remote_loading({Module,File}) ->
[{Module,Code}] = ets:lookup(?BINARY_TABLE, Module),
%%! The InitialTable list will be long if the module is big - what to do??
Mapping = counters_mapping_table(Module),
InitialClauses = ets:lookup(?COVER_CLAUSE_TABLE,Module),
case has_native_coverage() of
true ->
#remote_data{module=Module,file=File,code=Code};
false ->
Mapping = counters_mapping_table(Module),
InitialClauses = ets:lookup(?COVER_CLAUSE_TABLE,Module),

#remote_data{module=Module,file=File,code=Code,
mapping=Mapping,clauses=InitialClauses}.
#remote_data{module=Module,file=File,code=Code,
mapping=Mapping,clauses=InitialClauses}
end.

%% Unload modules on remote nodes
remote_unload(Nodes,UnloadedModules) ->
Expand Down Expand Up @@ -1756,7 +1761,8 @@ 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,
Options = [report_errors|SourceInfo ++ UserOptions],

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

case code:load_binary(Module, ?TAG, Binary) of
Expand Down Expand Up @@ -1847,13 +1853,25 @@ create_counters(Mod) ->
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});
case has_native_coverage() of
true ->
_ = catch erlang:reset_coverage(Mod),
patch_code1(Forms, native_coverage);
false ->
A = erl_anno:new(0),
AbstrKey = {tuple,A,[{atom,A,?MODULE},{atom,A,Mod}]},
patch_code1(Forms, {distributed,AbstrKey})
end;
patch_code(Mod, Forms, true) ->
Cref = create_counters(Mod),
AbstrCref = cid_to_abstract(Cref),
patch_code1(Forms, {local_only,AbstrCref}).
case has_native_coverage() of
true ->
_ = catch erlang:reset_coverage(Mod),
patch_code1(Forms, native_coverage);
false ->
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 All @@ -1871,6 +1889,8 @@ patch_code1({executable_line,_Anno,Index}, {local_only,AbstrCref}) ->
A = element(2, AbstrCref),
{call,A,{remote,A,{atom,A,counters},{atom,A,add}},
[AbstrCref,{integer,A,Index},{integer,A,1}]};
patch_code1({executable_line,_Anno,_Index}=Line, native_coverage) ->
Line;
patch_code1({clauses,Cs}, Key) ->
{clauses,[patch_code1(El, Key) || El <- Cs]};
patch_code1({attribute, _, _, _} = Attribute, _Key) ->
Expand Down Expand Up @@ -1902,28 +1922,39 @@ cid_to_abstract(Cref0) ->
%% the main node. Also zero the counters.
send_counters(Mod, CollectorPid) ->
Process = fun(Chunk) -> send_chunk(CollectorPid, Chunk) end,
move_counters(Mod, Process).
Move = standard_move(Mod),
move_counters(Mod, Move, Process).

%% 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 = case has_native_coverage() of
true ->
native_move(Mod);
false ->
standard_move(Mod)
end,
move_counters(Mod, Move, Process),
case has_native_coverage() of
true ->
_ = catch erlang:reset_coverage(Mod),
ok;
false ->
ok
end.

move_counters(Mod, Process) ->
move_counters(Mod, Move, Process) ->
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({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(Matches, Move, Process).

move_counters1({Mappings,Continuation}, Move, Process) ->
Moved = [Move(Item) || Item <- Mappings],
%% io:format("~p\n", [Moved]),
Process(Moved),
move_counters1(ets:match_object(Continuation), Move, Process);
move_counters1('$end_of_table', _Move, _Process) ->
ok.

counters_mapping_table(Mod) ->
Expand All @@ -1946,8 +1977,39 @@ 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) ->
Coverage0 = erlang:get_line_coverage(Mod),
Coverage = maps:from_list(lists:sort(Coverage0)),
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) ->
case has_native_coverage() of
true ->
_ = catch erlang:reset_coverage(Mod),
ok;
false ->
ok
end,
Pattern = {#bump{module=Mod,_='_'},'$1'},
MatchSpec = [{Pattern,[],['$1']}],
Matches = ets:select(?COVER_MAPPING_TABLE,
Expand Down Expand Up @@ -2665,3 +2727,6 @@ html_encoding(latin1) ->
"iso-8859-1";
html_encoding(utf8) ->
"utf-8".

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

0 comments on commit e46015a

Please sign in to comment.