%%
%% %CopyrightBegin%
%%
%% Copyright Ericsson AB 2017-2021. 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(lcnt_SUITE).

-include_lib("common_test/include/ct.hrl").

-export(
    [all/0, suite/0,
     init_per_suite/1, end_per_suite/1]).

-export(
    [toggle_lock_counting/1, error_on_invalid_category/1, preserve_locks/1,
     registered_processes/1, registered_db_tables/1]).

suite() ->
    [{ct_hooks,[ts_install_cth]},
     {timetrap, {seconds, 10}}].

all() ->
    [toggle_lock_counting, error_on_invalid_category, preserve_locks,
     registered_processes, registered_db_tables].

init_per_suite(Config) ->
    case erlang:system_info(lock_counting) of
        true ->
            %% The tests will run straight over these properties, so we have to
            %% preserve them to avoid tainting the other tests.
            OldCopySave = erts_debug:lcnt_control(copy_save),
            OldMask = erts_debug:lcnt_control(mask),
            [{lcnt_SUITE, {OldCopySave, OldMask}} | Config];
        _ ->
            {skip, "Lock counting is not enabled"}
    end.

end_per_suite(Config) ->
    {OldCopySave, OldMask} = proplists:get_value(lcnt_SUITE, Config),

    erts_debug:lcnt_control(copy_save, OldCopySave),
    OldCopySave = erts_debug:lcnt_control(copy_save),

    erts_debug:lcnt_control(mask, OldMask),
    OldMask = erts_debug:lcnt_control(mask),

    erts_debug:lcnt_clear(),
    ok.

disable_lock_counting() ->
    ok = erts_debug:lcnt_control(copy_save, false),
    ok = erts_debug:lcnt_control(mask, []),
    ok = erts_debug:lcnt_clear(),

    %% Sanity check.
    false = erts_debug:lcnt_control(copy_save),
    [] = erts_debug:lcnt_control(mask),

    %% The above commands rely on some lazy operations, so we'll have to wait
    %% for the list to clear.
    ok = wait_for_empty_lock_list().

wait_for_empty_lock_list() ->
    wait_for_empty_lock_list(10).
wait_for_empty_lock_list(Tries) when Tries > 0 ->
    try_flush_cleanup_ops(),
    [{duration, _}, {locks, Locks}] = erts_debug:lcnt_collect(),
    case remove_untoggleable_locks(Locks) of
        [] ->
            ok;
        _ ->
            timer:sleep(50),
            wait_for_empty_lock_list(Tries - 1)
    end;
wait_for_empty_lock_list(0) ->
    [{duration, _}, {locks, Locks0}] = erts_debug:lcnt_collect(),
    Locks = remove_untoggleable_locks(Locks0),
    ct:fail("Lock list failed to clear after disabling lock counting.~n\t~p",
            [Locks]).

%% Queue up a lot of thread progress cleanup ops in a vain attempt to
%% flush the lock list.
try_flush_cleanup_ops() ->
    false = lists:member(process, erts_debug:lcnt_control(mask)),
    [spawn(fun() -> ok end) || _ <- lists:seq(1, 1000)].

%%
%% Test cases
%%

toggle_lock_counting(Config) when is_list(Config) ->
    ok = disable_lock_counting(),

    Categories =
        [allocator, db, debug, distribution, generic, io, process, scheduler],
    lists:foreach(
        fun(Category) ->
            Locks = get_lock_info_for(Category),
            if
                Locks =/= [] ->
                    disable_lock_counting();
                Locks =:= [] ->
                    ct:fail("Failed to toggle ~p locks.", [Category])
            end
        end, Categories).

get_lock_info_for(Categories) when is_list(Categories) ->
    ok = erts_debug:lcnt_control(mask, Categories),
    [{duration, _}, {locks, Locks}] = erts_debug:lcnt_collect(),
    remove_untoggleable_locks(Locks);

get_lock_info_for(Category) when is_atom(Category) ->
    get_lock_info_for([Category]).

preserve_locks(Config) when is_list(Config) ->
    ok = disable_lock_counting(),

    erts_debug:lcnt_control(mask, [process]),

    erts_debug:lcnt_control(copy_save, true),
    [spawn(fun() -> ok end) || _ <- lists:seq(1, 1000)],

    %% Wait for the processes to be fully destroyed before disabling copy_save,
    %% then remove all active locks from the list. (There's no foolproof method
    %% to do this; sleeping before/after is the best way we have)
    timer:sleep(500),

    erts_debug:lcnt_control(copy_save, false),
    erts_debug:lcnt_control(mask, []),

    try_flush_cleanup_ops(),
    timer:sleep(500),

    case erts_debug:lcnt_collect() of
        [{duration, _}, {locks, Locks}] when length(Locks) > 0 ->
            ct:pal("Preserved ~p locks.", [length(Locks)]);
        [{duration, _}, {locks, []}] ->
            ct:fail("copy_save didn't preserve any locks.")
    end.

error_on_invalid_category(Config) when is_list(Config) ->
    ok = disable_lock_counting(),

    {error, badarg, q_invalid} = erts_debug:lcnt_control(mask, [q_invalid]),
    ok.

registered_processes(Config) when is_list(Config) ->
    ok = disable_lock_counting(),

    %% There ought to be at least one registered process (init/code_server)
    erts_debug:lcnt_control(mask, [process]),
    [_, {locks, ProcLocks}] = erts_debug:lcnt_collect(),
    true = lists:any(
        fun
            ({proc_main, RegName, _, _}) when is_atom(RegName) -> true;
            (_Lock) -> false
        end, ProcLocks),
    ok.

registered_db_tables(Config) when is_list(Config) ->
    ok = disable_lock_counting(),

    %% There ought to be at least one registered table (code)
    erts_debug:lcnt_control(mask, [db]),
    [_, {locks, DbLocks}] = erts_debug:lcnt_collect(),
    true = lists:any(
        fun
            ({db_tab, RegName, _, _}) when is_atom(RegName) -> true;
            (_Lock) -> false
        end, DbLocks),
    ok.

%% Not all locks can be toggled on or off due to technical limitations, so we
%% need to filter them out when checking whether we successfully disabled lock
%% counting.
remove_untoggleable_locks([]) ->
    [];
remove_untoggleable_locks([{resource_monitors, _, _, _} | T]) ->
    remove_untoggleable_locks(T);
remove_untoggleable_locks([{nif_load, _, _, _} | T]) ->
    remove_untoggleable_locks(T);
remove_untoggleable_locks([{'esock.gcnt', _, _, _} | T]) ->
    %% Global lock used by socket NIF
    remove_untoggleable_locks(T);
remove_untoggleable_locks([{'esock.protocols', _, _, _} | T]) ->
    %% Global lock used by socket NIF
    remove_untoggleable_locks(T);
remove_untoggleable_locks([H | T]) ->
    [H | remove_untoggleable_locks(T)].
