aboutsummaryrefslogtreecommitdiffstats
path: root/lib/tools/test/instrument_SUITE.erl
blob: f474669836a042bdead0b47be48d24e2adbe595a (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
%%
%% %CopyrightBegin%
%%
%% Copyright Ericsson AB 1998-2018. 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(instrument_SUITE).

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

-export([allocations_enabled/1, allocations_disabled/1, allocations_ramv/1,
         carriers_enabled/1, carriers_disabled/1]).

-export([test_all_alloc/2, test_per_alloc/2, test_format/3, test_abort/1,
         generate_test_blocks/0, churn_memory/0]).

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

suite() ->
    [{ct_hooks,[ts_install_cth]},
     {timetrap,{minutes,5}}].

all() -> 
    [allocations_enabled, allocations_disabled, allocations_ramv,
     carriers_enabled, carriers_disabled].

-define(GENERATED_SBC_BLOCK_COUNT, 1000).
-define(GENERATED_MBC_BLOCK_COUNT, ?GENERATED_SBC_BLOCK_COUNT).

-define(GENERATED_BLOCK_COUNT, (?GENERATED_SBC_BLOCK_COUNT +
                                ?GENERATED_MBC_BLOCK_COUNT)).
-define(GENERATED_CARRIER_COUNT, ?GENERATED_SBC_BLOCK_COUNT).

allocations_test(Args, Plain, PerAlloc) ->
    run_test(Args, fun(Node) ->
                 ok = rpc:call(Node, ?MODULE, test_all_alloc,
                               [fun instrument:allocations/0, Plain]),
                 ok = rpc:call(Node, ?MODULE, test_per_alloc,
                               [fun instrument:allocations/1, PerAlloc]),
                 ok = rpc:call(Node, ?MODULE, test_format,
                               [#{ histogram_start => 512,
                                   histogram_width => 4 },
                                fun instrument:allocations/1,
                                fun verify_allocations_output/2]),
                 ok = rpc:call(Node, ?MODULE, test_abort,
                               [fun erts_internal:gather_alloc_histograms/1])
             end).

allocations_enabled(Config) when is_list(Config) ->
    allocations_test("+Meamax +Muatags true",
                     fun verify_allocations_enabled/1,
                     fun verify_allocations_enabled/2).

allocations_disabled(Config) when is_list(Config) ->
    allocations_test("+Meamax +Muatags false",
                     fun verify_allocations_disabled/1,
                     fun verify_allocations_disabled/2).

allocations_ramv(Config) when is_list(Config) ->
    allocations_test("+Meamax +Muatags true +Muramv true",
                     fun verify_allocations_enabled/1,
                     fun verify_allocations_enabled/2).

verify_allocations_disabled(_AllocType, Result) ->
    verify_allocations_disabled(Result).

verify_allocations_disabled({ok, {_HistStart, _UnscannedBytes, Allocs}}) ->
    true = Allocs =:= #{};
verify_allocations_disabled({error, not_enabled}) ->
    ok.

%% Skip types that have unstable results or are unaffected by +Muatags
verify_allocations_enabled(literal_alloc, _Result) -> ok;
verify_allocations_enabled(exec_alloc, _Result) -> ok;
verify_allocations_enabled(temp_alloc, _Result) -> ok;
verify_allocations_enabled(sl_alloc, _Result) -> ok;
verify_allocations_enabled(_AllocType, Result) ->
    verify_allocations_enabled(Result).

verify_allocations_enabled({ok, {_HistStart, _UnscannedBytes, Allocs}}) ->
    true = Allocs =/= #{}.

verify_allocations_output(#{}, {ok, {_, _, Allocs}}) when Allocs =:= #{} ->
    %% This happens when the allocator is enabled but tagging is disabled. If
    %% there's an error that causes Allocs to always be empty when enabled it
    %% will be caught by verify_allocations_enabled.
    ok;
verify_allocations_output(#{}, {error, not_enabled}) ->
    ok;
verify_allocations_output(#{ histogram_start := HistStart,
                             histogram_width := HistWidth },
                    {ok, {HistStart, _UnscannedBytes, ByOrigin}}) ->
    AllHistograms = lists:flatten([maps:values(ByType) ||
                                   ByType <- maps:values(ByOrigin)]),

    %% Do the histograms look alright?
    HistogramSet = ordsets:from_list(AllHistograms),
    Verified = [H || H <- HistogramSet,
                tuple_size(H) =:= HistWidth,
                hist_sum(H) >= 1],
    [] = ordsets:subtract(HistogramSet, Verified),

    %% Do we have at least as many blocks as we've generated?
    BlockCount = lists:foldl(fun(Hist, Acc) ->
                                 hist_sum(Hist) + Acc
                             end, 0, AllHistograms),
    GenTotalBlockCount = ?GENERATED_BLOCK_COUNT,
    GenSBCBlockCount = ?GENERATED_SBC_BLOCK_COUNT,
    if
        BlockCount < GenSBCBlockCount ->
            ct:fail("Found ~p blocks, required at least ~p (SB)." ,
                    [BlockCount, GenSBCBlockCount]);
        BlockCount >= GenTotalBlockCount ->
            ct:pal("Found ~p blocks, expected at least ~p (SB + MB).",
                    [BlockCount, GenTotalBlockCount]);
        BlockCount < GenTotalBlockCount ->
            ct:pal("Found ~p blocks, expected at least ~p (SB + MB), but this "
                    "may be due to MBCs being skipped if they're about to be "
                    "scanned just as they're fetched from the carrier pool.",
                    [BlockCount, GenTotalBlockCount])
    end,

    ok.

%% %% %% %% %% %%

carriers_test(Args, Plain, PerAlloc) ->
    run_test(Args, fun(Node) ->
                 ok = rpc:call(Node, ?MODULE, test_all_alloc,
                               [fun instrument:carriers/0, Plain]),
                 ok = rpc:call(Node, ?MODULE, test_per_alloc,
                               [fun instrument:carriers/1, PerAlloc]),
                 ok = rpc:call(Node, ?MODULE, test_format,
                               [#{ histogram_start => 1024,
                                   histogram_width => 4 },
                                fun instrument:carriers/1,
                                fun verify_carriers_output/2]),
                 ok = rpc:call(Node, ?MODULE, test_abort,
                               [fun erts_internal:gather_carrier_info/1])
             end).

carriers_enabled(Config) when is_list(Config) ->
    carriers_test("+Meamax",
                  fun verify_carriers_enabled/1,
                  fun verify_carriers_enabled/2).

carriers_disabled(Config) when is_list(Config) ->
    carriers_test("+Meamin",
                  fun verify_carriers_disabled/1,
                  fun verify_carriers_disabled/2).

verify_carriers_disabled(_AllocType, Result) ->
    verify_carriers_disabled(Result).

verify_carriers_disabled({error, not_enabled}) ->
    ok;
verify_carriers_disabled({ok, {_HistStart, Carriers}}) ->
    verify_carriers_disabled_1(Carriers).

verify_carriers_disabled_1([]) ->
    ok;
%% literal_alloc, exec_alloc, and temp_alloc can't be disabled, so we have to
%% accept their presence in carriers_disabled/test_all_alloc.
verify_carriers_disabled_1([Carrier | Rest]) when
        element(1, Carrier) =:= literal_alloc;
        element(1, Carrier) =:= exec_alloc;
        element(1, Carrier) =:= temp_alloc ->
    verify_carriers_disabled_1(Rest).

%% exec_alloc only has a carrier if it's actually used.
verify_carriers_enabled(exec_alloc, _Result) -> ok;
verify_carriers_enabled(_AllocType, Result) -> verify_carriers_enabled(Result).

verify_carriers_enabled({ok, {_HistStart, Carriers}}) when Carriers =/= [] ->
    ok.

verify_carriers_output(#{ histogram_start := HistStart,
                          histogram_width := HistWidth },
                       {ok, {HistStart, AllCarriers}}) ->

    %% Do the carriers look alright?
    CarrierSet = ordsets:from_list(AllCarriers),
    Verified = [C || {AllocType,
                      TotalSize,
                      UnscannedSize,
                      AllocatedSize,
                      AllocatedCount,
                      InPool,
                      FreeBlockHist} = C <- CarrierSet,
                is_atom(AllocType),
                is_integer(TotalSize), TotalSize >= 1,
                is_integer(UnscannedSize), UnscannedSize < TotalSize,
                                           UnscannedSize >= 0,
                is_integer(AllocatedSize), AllocatedSize < TotalSize,
                                           AllocatedSize >= 0,
                is_integer(AllocatedCount), AllocatedCount =< AllocatedSize,
                                            AllocatedCount >= 0,
                is_boolean(InPool),
                tuple_size(FreeBlockHist) =:= HistWidth,
                carrier_block_check(AllocatedCount, FreeBlockHist)],
    [] = ordsets:subtract(CarrierSet, Verified),

    %% Do we have at least as many carriers as we've generated?
    CarrierCount = length(AllCarriers),
    GenSBCCount = ?GENERATED_SBC_BLOCK_COUNT,
    if
        CarrierCount < GenSBCCount ->
            ct:fail("Carrier count is ~p, expected at least ~p (SBC).",
                [CarrierCount, GenSBCCount]);
        CarrierCount >= GenSBCCount ->
            ct:pal("Found ~p carriers, required at least ~p (SBC)." ,
                [CarrierCount, GenSBCCount])
    end,

    ok;
verify_carriers_output(#{}, {error, not_enabled}) ->
    ok.

carrier_block_check(AllocCount, FreeHist) ->
    %% A carrier must contain at least one block, and th. number of free blocks
    %% must not exceed the number of allocated blocks + 1.
    FreeCount = hist_sum(FreeHist),

    (AllocCount + FreeCount) >= 1 andalso FreeCount =< (AllocCount + 1).

%% %% %% %% %% %%

test_all_alloc(Gather, Verify) ->
    Verify(Gather()),
    ok.

test_per_alloc(Gather, Verify) ->
    [begin
         Verify(T, Gather(#{ allocator_types => [T] }))
     end || T <- erlang:system_info(alloc_util_allocators)],
    ok.

test_format(#{ allocator_types := _ }, _, _) ->
    error(badarg);
test_format(Options0, Gather, Verify) ->
    %% We limit format checking to binary_alloc since we generated the test
    %% vectors there.
    Options = Options0#{ allocator_types => [binary_alloc] },
    Verify(Options, Gather(Options)),
    ok.

test_abort(Gather) ->
    %% There's no way for us to tell whether this actually aborted or ran to
    %% completion, but it might catch a few segfaults.
    %% This testcase is mostly useful when run in an debug emulator as it needs
    %% the modified reduction count to trigger the odd trap scenarios
    Runner = self(),
    Ref = make_ref(),
    spawn_opt(fun() ->
                      [begin
                           Ref2 = make_ref(),
                           [Gather({Type, SchedId, 1, 1, Ref2}) ||
                               Type <- erlang:system_info(alloc_util_allocators),
                               SchedId <- lists:seq(0, erlang:system_info(schedulers))]
                       end || _ <- lists:seq(1,100)],
                      Runner ! Ref
              end, [{priority, max}]),
    receive
        Ref -> ok
    end.

hist_sum(H) -> hist_sum_1(H, tuple_size(H), 0).
hist_sum_1(_H, 0, A) -> A;
hist_sum_1(H, N, A) -> hist_sum_1(H, N - 1, element(N, H) + A).

%%

run_test(Args0, Test) ->
    %% Override single-block carrier threshold for binaries to ensure we have
    %% coverage for that path. generate_test_blocks builds a few binaries that
    %% crosses this threshold.
    %%
    %% We also set the abandon carrier threshold to 70% to provoke more
    %% activity in the carrier pool.
    Args = Args0 ++ " +MBsbct 1 +Muacul 70",
    Node = start_slave(Args),

    ok = rpc:call(Node, ?MODULE, generate_test_blocks, []),
    ok = Test(Node),

    ok = rpc:call(Node, ?MODULE, churn_memory, []),
    ok = Test(Node),

    true = test_server:stop_node(Node).

start_slave(Args) ->
    MicroSecs = erlang:monotonic_time(),
    Name = "instr" ++ integer_to_list(MicroSecs),
    Pa = filename:dirname(code:which(?MODULE)),

    %% We pass arguments through ZFLAGS as the nightly tests rotate
    %% +Meamax/+Meamin which breaks the _enabled and _disabled tests unless
    %% overridden.
    ZFlags = os:getenv("ERL_ZFLAGS", ""),
    {ok, Node} = try
                     os:putenv("ERL_ZFLAGS", ZFlags ++ [" " | Args]),
                     test_server:start_node(list_to_atom(Name),
                                            slave,
                                            [{args, "-pa " ++ Pa}])
                 after
                     os:putenv("ERL_ZFLAGS", ZFlags)
                 end,
    Node.

generate_test_blocks() ->
    Runner = self(),
    Ref = make_ref(),
    spawn(fun() ->
              %% We've set the single-block carrier threshold to 1KB so one
              %% ought to land in a SBC and the other in a MBC. Both are kept
              %% alive forever.
              SBCs = [<<I, 0:(1 bsl 10)/unit:8>> ||
                      I <- lists:seq(1, ?GENERATED_SBC_BLOCK_COUNT)],
              MBCs = [<<I, 0:64/unit:8>> ||
                      I <- lists:seq(1, ?GENERATED_MBC_BLOCK_COUNT)],
              Runner ! Ref,
              receive
                   gurka -> gaffel ! {SBCs, MBCs}
              end
          end),
    receive
        Ref -> ok
    end.

churn_memory() ->
    %% All processes spawned from here on have 'low' priority to avoid starving
    %% others (e.g. the rpc process) which could cause the test to time out.
    [begin
         churn_list_to_binary(),
         churn_processes(),
         churn_ets()
     end || _ <- lists:seq(1, erlang:system_info(schedulers))],
    ok.

churn_processes() ->
    Pid = spawn_opt(fun churn_processes/0, [{priority, low}]),
    [Pid ! <<I, 0:128/unit:8>> || I <- lists:seq(1, 128)].

%% Nearly all types have a few allocations at all times but sl_alloc is
%% often empty. list_to_binary on large inputs will yield and spill the
%% state into an 'estack' which is allocated through sl_alloc.
%%
%% This is inherently unstable so we skip the verification step for this
%% type, but there's still a point to hammering it.
churn_list_to_binary() ->
    List = binary_to_list(<<0:(1 bsl 20)/unit:8>>),
    spawn_opt(fun() -> churn_list_to_binary_1(List) end, [{priority, low}]).

churn_list_to_binary_1(List) ->
    _ = id(list_to_binary(List)),
    churn_list_to_binary_1(List).

churn_ets() ->
    spawn_opt(fun() -> churn_ets_1(ets:new(gurka, [])) end, [{priority, low}]).

churn_ets_1(Tab) ->
    ets:insert(Tab, {gaffel, lists:seq(1, 16)}),
    ets:delete_all_objects(Tab),
    churn_ets_1(Tab).

id(I) ->
    I.