1%%
2%% %CopyrightBegin%
3%%
4%% Copyright Ericsson AB 2017-2018. All Rights Reserved.
5%%
6%% Licensed under the Apache License, Version 2.0 (the "License");
7%% you may not use this file except in compliance with the License.
8%% You may obtain a copy of the License at
9%%
10%%     http://www.apache.org/licenses/LICENSE-2.0
11%%
12%% Unless required by applicable law or agreed to in writing, software
13%% distributed under the License is distributed on an "AS IS" BASIS,
14%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15%% See the License for the specific language governing permissions and
16%% limitations under the License.
17%%
18%% %CopyrightEnd%
19%%
20
21-module(lcnt_SUITE).
22
23-include_lib("common_test/include/ct.hrl").
24
25-export(
26    [all/0, suite/0,
27     init_per_suite/1, end_per_suite/1,
28     init_per_testcase/2, end_per_testcase/2]).
29
30-export(
31    [toggle_lock_counting/1, error_on_invalid_category/1, preserve_locks/1,
32     registered_processes/1, registered_db_tables/1]).
33
34suite() ->
35    [{ct_hooks,[ts_install_cth]},
36     {timetrap, {seconds, 10}}].
37
38all() ->
39    [toggle_lock_counting, error_on_invalid_category, preserve_locks,
40     registered_processes, registered_db_tables].
41
42init_per_suite(Config) ->
43    case erlang:system_info(lock_counting) of
44        true ->
45            %% The tests will run straight over these properties, so we have to
46            %% preserve them to avoid tainting the other tests.
47            OldCopySave = erts_debug:lcnt_control(copy_save),
48            OldMask = erts_debug:lcnt_control(mask),
49            [{lcnt_SUITE, {OldCopySave, OldMask}} | Config];
50        _ ->
51            {skip, "Lock counting is not enabled"}
52    end.
53
54end_per_suite(Config) ->
55    {OldCopySave, OldMask} = proplists:get_value(lcnt_SUITE, Config),
56
57    erts_debug:lcnt_control(copy_save, OldCopySave),
58    OldCopySave = erts_debug:lcnt_control(copy_save),
59
60    erts_debug:lcnt_control(mask, OldMask),
61    OldMask = erts_debug:lcnt_control(mask),
62
63    erts_debug:lcnt_clear(),
64    ok.
65
66init_per_testcase(_Case, Config) ->
67    disable_lock_counting(),
68    Config.
69
70end_per_testcase(_Case, _Config) ->
71    ok.
72
73disable_lock_counting() ->
74    ok = erts_debug:lcnt_control(copy_save, false),
75    ok = erts_debug:lcnt_control(mask, []),
76    ok = erts_debug:lcnt_clear(),
77
78    %% Sanity check.
79    false = erts_debug:lcnt_control(copy_save),
80    [] = erts_debug:lcnt_control(mask),
81
82    %% The above commands rely on some lazy operations, so we'll have to wait
83    %% for the list to clear.
84    ok = wait_for_empty_lock_list().
85
86wait_for_empty_lock_list() ->
87    wait_for_empty_lock_list(10).
88wait_for_empty_lock_list(Tries) when Tries > 0 ->
89    try_flush_cleanup_ops(),
90    [{duration, _}, {locks, Locks}] = erts_debug:lcnt_collect(),
91    case remove_untoggleable_locks(Locks) of
92        [] ->
93            ok;
94        _ ->
95            timer:sleep(50),
96            wait_for_empty_lock_list(Tries - 1)
97    end;
98wait_for_empty_lock_list(0) ->
99    ct:fail("Lock list failed to clear after disabling lock counting.").
100
101%% Queue up a lot of thread progress cleanup ops in a vain attempt to
102%% flush the lock list.
103try_flush_cleanup_ops() ->
104    false = lists:member(process, erts_debug:lcnt_control(mask)),
105    [spawn(fun() -> ok end) || _ <- lists:seq(1, 1000)].
106
107%%
108%% Test cases
109%%
110
111toggle_lock_counting(Config) when is_list(Config) ->
112    Categories =
113        [allocator, db, debug, distribution, generic, io, process, scheduler],
114    lists:foreach(
115        fun(Category) ->
116            Locks = get_lock_info_for(Category),
117            if
118                Locks =/= [] ->
119                    disable_lock_counting();
120                Locks =:= [] ->
121                    ct:fail("Failed to toggle ~p locks.", [Category])
122            end
123        end, Categories).
124
125get_lock_info_for(Categories) when is_list(Categories) ->
126    ok = erts_debug:lcnt_control(mask, Categories),
127    [{duration, _}, {locks, Locks}] = erts_debug:lcnt_collect(),
128    remove_untoggleable_locks(Locks);
129
130get_lock_info_for(Category) when is_atom(Category) ->
131    get_lock_info_for([Category]).
132
133preserve_locks(Config) when is_list(Config) ->
134    erts_debug:lcnt_control(mask, [process]),
135
136    erts_debug:lcnt_control(copy_save, true),
137    [spawn(fun() -> ok end) || _ <- lists:seq(1, 1000)],
138
139    %% Wait for the processes to be fully destroyed before disabling copy_save,
140    %% then remove all active locks from the list. (There's no foolproof method
141    %% to do this; sleeping before/after is the best way we have)
142    timer:sleep(500),
143
144    erts_debug:lcnt_control(copy_save, false),
145    erts_debug:lcnt_control(mask, []),
146
147    try_flush_cleanup_ops(),
148    timer:sleep(500),
149
150    case erts_debug:lcnt_collect() of
151        [{duration, _}, {locks, Locks}] when length(Locks) > 0 ->
152            ct:pal("Preserved ~p locks.", [length(Locks)]);
153        [{duration, _}, {locks, []}] ->
154            ct:fail("copy_save didn't preserve any locks.")
155    end.
156
157error_on_invalid_category(Config) when is_list(Config) ->
158    {error, badarg, q_invalid} = erts_debug:lcnt_control(mask, [q_invalid]),
159    ok.
160
161registered_processes(Config) when is_list(Config) ->
162    %% There ought to be at least one registered process (init/code_server)
163    erts_debug:lcnt_control(mask, [process]),
164    [_, {locks, ProcLocks}] = erts_debug:lcnt_collect(),
165    true = lists:any(
166        fun
167            ({proc_main, RegName, _, _}) when is_atom(RegName) -> true;
168            (_Lock) -> false
169        end, ProcLocks),
170    ok.
171
172registered_db_tables(Config) when is_list(Config) ->
173    %% There ought to be at least one registered table (code)
174    erts_debug:lcnt_control(mask, [db]),
175    [_, {locks, DbLocks}] = erts_debug:lcnt_collect(),
176    true = lists:any(
177        fun
178            ({db_tab, RegName, _, _}) when is_atom(RegName) -> true;
179            (_Lock) -> false
180        end, DbLocks),
181    ok.
182
183%% Not all locks can be toggled on or off due to technical limitations, so we
184%% need to filter them out when checking whether we successfully disabled lock
185%% counting.
186remove_untoggleable_locks([]) ->
187    [];
188remove_untoggleable_locks([{resource_monitors, _, _, _} | T]) ->
189    remove_untoggleable_locks(T);
190remove_untoggleable_locks([H | T]) ->
191    [H | remove_untoggleable_locks(T)].
192