%%% Test suite for recon_alloc. Because many of the tests in
%%% here depend on memory allocation and this is *not* transparent,
%%% the tests are rather weak and more or less check for interface
%%% conformance and obvious changes more than anything.
-module(recon_alloc_SUITE).
-include_lib("common_test/include/ct.hrl").
-compile(export_all).

all() -> [memory, fragmentation, cache_hit_rates, average_block_sizes,
          sbcs_to_mbcs, allocators, allocators_merged, snapshots, units].

memory(_Config) ->
    %% Freeze memory values for tests
    recon_alloc:snapshot(),
    %% memory returns values for 'used', 'allocated', 'unused', and 'usage'.
    Used = recon_alloc:memory(used),
    Alloc = recon_alloc:memory(allocated),
    Unused = recon_alloc:memory(unused),
    Usage = recon_alloc:memory(usage),
    Types = recon_alloc:memory(allocated_types),
    Instances = recon_alloc:memory(allocated_instances),
    %% equivalent to memory(_, current)
    Used = recon_alloc:memory(used, current),
    Alloc = recon_alloc:memory(allocated, current),
    Unused = recon_alloc:memory(unused, current),
    Usage = recon_alloc:memory(usage, current),
    Types = recon_alloc:memory(allocated_types, current),
    Instances = recon_alloc:memory(allocated_instances, current),
    %% relationships, and variation rates
    Alloc = Used + Unused,
    Usage = Used/Alloc,
    true = Alloc > 0,
    true = Usage > 0,
    true = Unused > 0,
    %% Current vs. Max
    true = Used =< recon_alloc:memory(used, max),
    true = Alloc =< recon_alloc:memory(allocated, max),
    true = Types =< recon_alloc:memory(allocated_types, max),
    true = Instances =< recon_alloc:memory(allocated_instances, max),
    %% Key presences in allocateds
    [{binary_alloc,_},
     {driver_alloc,_},
     {eheap_alloc,_},
     {ets_alloc,_},
     {fix_alloc,_},
     {ll_alloc,_},
     {sl_alloc,_},
     {std_alloc,_},
     {temp_alloc,_}] = Types,
    MaxInstance = lists:max(proplists:get_keys(Instances)),
    true = lists:sort(proplists:get_keys(Instances))
         =:= lists:seq(0,MaxInstance),
    %% allocate a bunch of memory and see if the memory used goes
    %% up and relationships change accordingly.
    [spawn(fun() -> lists:seq(1,1000), timer:sleep(1000) end)
     || _ <- lists:seq(1,100)],
    recon_alloc:snapshot(),
    true = Used < recon_alloc:memory(used),
    true = Alloc < recon_alloc:memory(allocated)
         orelse Usage < recon_alloc:memory(usage),
    %% Cleanup
    recon_alloc:snapshot_clear().

fragmentation(_Config) ->
    %% Values returned are of the format [{K, V}] and supports both
    %% searches by 'current' and 'max'
    Current = recon_alloc:fragmentation(current),
    Max = recon_alloc:fragmentation(max),
    true = allocdata(Current),
    true = allocdata(Max),
    true = Max =/= Current,
    Keys = [sbcs_usage, sbcs_block_size, sbcs_carriers_size, mbcs_usage,
            mbcs_block_size, mbcs_carriers_size],
    true = each_keys(Keys, Current),
    true = each_keys(Keys, Max).

cache_hit_rates(_Config) ->
    Cache = recon_alloc:cache_hit_rates(),
    true = allocdata(Cache),
    true = each_keys([hit_rate, hits, calls], Cache).

average_block_sizes(_Config) ->
    CurrentSizes = recon_alloc:average_block_sizes(current),
    MaxSizes = recon_alloc:average_block_sizes(max),
    true = lists:all(fun({K,V}) -> is_atom(K) andalso is_list(V) end,
                     CurrentSizes),
    true = each_keys([mbcs, sbcs], CurrentSizes),
    true = lists:all(fun({K,V}) -> is_atom(K) andalso is_list(V) end,
                     MaxSizes),
    true = each_keys([mbcs, sbcs], MaxSizes).

sbcs_to_mbcs(_Config) ->
    Check = fun(Ratio) ->
        true = lists:all(fun({{Alloc,N},_}) ->
                            is_atom(Alloc) andalso is_integer(N)
                        end,
                        Ratio),
        true = lists:all(fun({_,infinity}) -> true;
                            ({_,0}) -> true;
                            ({_,N}) -> is_float(N)
                        end,
                        Ratio)
    end,
    Check(recon_alloc:sbcs_to_mbcs(current)),
    Check(recon_alloc:sbcs_to_mbcs(max)).


allocators(_Config) ->
    true = allocdata(recon_alloc:allocators()).

allocators_merged(_Config) ->
    true = merged_allocdata(recon_alloc:allocators(types)).

merged_allocdata(L) ->
    Validate = fun({{Allocator, Ns}, List}) ->
        is_atom(Allocator) andalso is_list(Ns)
        andalso
        lists:all(fun({_,_}) -> true; (_) -> false end, List)
    end,
    lists:all(Validate, L).

snapshots(Config) ->
    File = filename:join(?config(priv_dir, Config), "snapshot"),
    undefined = recon_alloc:snapshot(),
    true = is_snapshot(recon_alloc:snapshot()),
    true = is_snapshot(recon_alloc:snapshot_clear()),
    undefined = recon_alloc:snapshot_clear(),
    ok = recon_alloc:snapshot_print(),
    ok = recon_alloc:snapshot_save(File),
    undefined = recon_alloc:snapshot_get(),
    undefined = recon_alloc:snapshot(),
    ok = recon_alloc:snapshot_print(),
    ok = recon_alloc:snapshot_save(File),
    _ = recon_alloc:snapshot_clear(),
    undefined = recon_alloc:snapshot_load(File),
    true = is_snapshot(recon_alloc:snapshot_load(File)),
    true = is_snapshot(recon_alloc:snapshot_get()),
    %% Also supporting another dump format
    file:write_file(
      File,
      io_lib:format("~p.~n", [{erlang:memory(),
                                [{A,erlang:system_info({allocator,A})}
                                 || A <- erlang:system_info(alloc_util_allocators)++[sys_alloc,mseg_alloc]]}
                             ])),
    _ = recon_alloc:snapshot_clear(),
    undefined = recon_alloc:snapshot_load(File),
    true = is_snapshot(recon_alloc:snapshot_get()),
    recon_alloc:snapshot_clear().

units(_Config) ->
    recon_alloc:snapshot(),
    ByteMem = recon_alloc:memory(used),
    ByteAvg = [X || {_,[{_,X}|_]} <- recon_alloc:average_block_sizes(max)],
    recon_alloc:set_unit(byte),
    ByteMem = recon_alloc:memory(used),
    ByteAvg = [X || {_,[{_,X}|_]} <- recon_alloc:average_block_sizes(max)],
    recon_alloc:set_unit(kilobyte),
    true = ByteMem/1024 == recon_alloc:memory(used),
    true = [X/1024 || X <- ByteAvg]
        == [X || {_,[{_,X}|_]} <- recon_alloc:average_block_sizes(max)],
    recon_alloc:set_unit(megabyte),
    true = ByteMem/(1024*1024) == recon_alloc:memory(used),
    true = [X/(1024*1024) || X <- ByteAvg]
        == [X || {_,[{_,X}|_]} <- recon_alloc:average_block_sizes(max)],
    recon_alloc:set_unit(gigabyte),
    true = ByteMem/(1024*1024*1024) == recon_alloc:memory(used),
    true = [X/(1024*1024*1024) || X <- ByteAvg]
        == [X || {_,[{_,X}|_]} <- recon_alloc:average_block_sizes(max)],
    recon_alloc:snapshot_clear().

%%% Helpers
allocdata(L) ->
    Validate = fun({{Allocator,N}, List}) ->
        is_atom(Allocator) andalso is_integer(N)
        andalso
        lists:all(fun({_,_}) -> true; (_) -> false end, List)
    end,
    lists:all(Validate, L).

each_keys(Keys,ListOfLists) ->
    lists:all(fun({_K,L}) ->
                lists:all(fun(Key) ->
                             undefined =/= proplists:get_value(Key,L)
                          end,
                          Keys)
              end,
              ListOfLists).

is_snapshot({Mem,Snap}) ->
    lists:all(fun({K,V}) -> is_atom(K) andalso is_integer(V) end, Mem)
    andalso
    allocdata(Snap).
