%% %% %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 <- lists:seq(1, ?GENERATED_SBC_BLOCK_COUNT)], MBCs = [<> || 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 <- 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.