1%%
2%% %CopyrightBegin%
3%%
4%% Copyright Ericsson AB 2017-2020. 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
29-export(
30    [toggle_lock_counting/1, error_on_invalid_category/1, preserve_locks/1,
31     registered_processes/1, registered_db_tables/1]).
32
33suite() ->
34    [{ct_hooks,[ts_install_cth]},
35     {timetrap, {seconds, 10}}].
36
37all() ->
38    [toggle_lock_counting, error_on_invalid_category, preserve_locks,
39     registered_processes, registered_db_tables].
40
41init_per_suite(Config) ->
42    case erlang:system_info(lock_counting) of
43        true ->
44            %% The tests will run straight over these properties, so we have to
45            %% preserve them to avoid tainting the other tests.
46            OldCopySave = erts_debug:lcnt_control(copy_save),
47            OldMask = erts_debug:lcnt_control(mask),
48            [{lcnt_SUITE, {OldCopySave, OldMask}} | Config];
49        _ ->
50            {skip, "Lock counting is not enabled"}
51    end.
52
53end_per_suite(Config) ->
54    {OldCopySave, OldMask} = proplists:get_value(lcnt_SUITE, Config),
55
56    erts_debug:lcnt_control(copy_save, OldCopySave),
57    OldCopySave = erts_debug:lcnt_control(copy_save),
58
59    erts_debug:lcnt_control(mask, OldMask),
60    OldMask = erts_debug:lcnt_control(mask),
61
62    erts_debug:lcnt_clear(),
63    ok.
64
65disable_lock_counting() ->
66    ok = erts_debug:lcnt_control(copy_save, false),
67    ok = erts_debug:lcnt_control(mask, []),
68    ok = erts_debug:lcnt_clear(),
69
70    %% Sanity check.
71    false = erts_debug:lcnt_control(copy_save),
72    [] = erts_debug:lcnt_control(mask),
73
74    %% The above commands rely on some lazy operations, so we'll have to wait
75    %% for the list to clear.
76    ok = wait_for_empty_lock_list().
77
78wait_for_empty_lock_list() ->
79    wait_for_empty_lock_list(10).
80wait_for_empty_lock_list(Tries) when Tries > 0 ->
81    try_flush_cleanup_ops(),
82    [{duration, _}, {locks, Locks}] = erts_debug:lcnt_collect(),
83    case remove_untoggleable_locks(Locks) of
84        [] ->
85            ok;
86        _ ->
87            timer:sleep(50),
88            wait_for_empty_lock_list(Tries - 1)
89    end;
90wait_for_empty_lock_list(0) ->
91    [{duration, _}, {locks, Locks0}] = erts_debug:lcnt_collect(),
92    Locks = remove_untoggleable_locks(Locks0),
93    ct:fail("Lock list failed to clear after disabling lock counting.~n\t~p",
94            [Locks]).
95
96%% Queue up a lot of thread progress cleanup ops in a vain attempt to
97%% flush the lock list.
98try_flush_cleanup_ops() ->
99    false = lists:member(process, erts_debug:lcnt_control(mask)),
100    [spawn(fun() -> ok end) || _ <- lists:seq(1, 1000)].
101
102%%
103%% Test cases
104%%
105
106toggle_lock_counting(Config) when is_list(Config) ->
107    ok = disable_lock_counting(),
108
109    Categories =
110        [allocator, db, debug, distribution, generic, io, process, scheduler],
111    lists:foreach(
112        fun(Category) ->
113            Locks = get_lock_info_for(Category),
114            if
115                Locks =/= [] ->
116                    disable_lock_counting();
117                Locks =:= [] ->
118                    ct:fail("Failed to toggle ~p locks.", [Category])
119            end
120        end, Categories).
121
122get_lock_info_for(Categories) when is_list(Categories) ->
123    ok = erts_debug:lcnt_control(mask, Categories),
124    [{duration, _}, {locks, Locks}] = erts_debug:lcnt_collect(),
125    remove_untoggleable_locks(Locks);
126
127get_lock_info_for(Category) when is_atom(Category) ->
128    get_lock_info_for([Category]).
129
130preserve_locks(Config) when is_list(Config) ->
131    ok = disable_lock_counting(),
132
133    erts_debug:lcnt_control(mask, [process]),
134
135    erts_debug:lcnt_control(copy_save, true),
136    [spawn(fun() -> ok end) || _ <- lists:seq(1, 1000)],
137
138    %% Wait for the processes to be fully destroyed before disabling copy_save,
139    %% then remove all active locks from the list. (There's no foolproof method
140    %% to do this; sleeping before/after is the best way we have)
141    timer:sleep(500),
142
143    erts_debug:lcnt_control(copy_save, false),
144    erts_debug:lcnt_control(mask, []),
145
146    try_flush_cleanup_ops(),
147    timer:sleep(500),
148
149    case erts_debug:lcnt_collect() of
150        [{duration, _}, {locks, Locks}] when length(Locks) > 0 ->
151            ct:pal("Preserved ~p locks.", [length(Locks)]);
152        [{duration, _}, {locks, []}] ->
153            ct:fail("copy_save didn't preserve any locks.")
154    end.
155
156error_on_invalid_category(Config) when is_list(Config) ->
157    ok = disable_lock_counting(),
158
159    {error, badarg, q_invalid} = erts_debug:lcnt_control(mask, [q_invalid]),
160    ok.
161
162registered_processes(Config) when is_list(Config) ->
163    ok = disable_lock_counting(),
164
165    %% There ought to be at least one registered process (init/code_server)
166    erts_debug:lcnt_control(mask, [process]),
167    [_, {locks, ProcLocks}] = erts_debug:lcnt_collect(),
168    true = lists:any(
169        fun
170            ({proc_main, RegName, _, _}) when is_atom(RegName) -> true;
171            (_Lock) -> false
172        end, ProcLocks),
173    ok.
174
175registered_db_tables(Config) when is_list(Config) ->
176    ok = disable_lock_counting(),
177
178    %% There ought to be at least one registered table (code)
179    erts_debug:lcnt_control(mask, [db]),
180    [_, {locks, DbLocks}] = erts_debug:lcnt_collect(),
181    true = lists:any(
182        fun
183            ({db_tab, RegName, _, _}) when is_atom(RegName) -> true;
184            (_Lock) -> false
185        end, DbLocks),
186    ok.
187
188%% Not all locks can be toggled on or off due to technical limitations, so we
189%% need to filter them out when checking whether we successfully disabled lock
190%% counting.
191remove_untoggleable_locks([]) ->
192    [];
193remove_untoggleable_locks([{resource_monitors, _, _, _} | T]) ->
194    remove_untoggleable_locks(T);
195remove_untoggleable_locks([{'esock[gcnt]', _, _, _} | T]) ->
196    %% Global lock used by socket NIF
197    remove_untoggleable_locks(T);
198remove_untoggleable_locks([H | T]) ->
199    [H | remove_untoggleable_locks(T)].
200