%%
%% %CopyrightBegin%
%%
%% Copyright Ericsson AB 2014-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%
%%

%%
%% @doc Microstate accounting utility function
%%
%%	This module provides a user interface for analysing
%%      erlang:statistics(microstate_accounting) data.
%%

-module(msacc).
-export([available/0, start/0, start/1, stop/0, reset/0, to_file/1,
         from_file/1, stats/0, stats/2, print/0, print/1, print/2,
         print/3]).

-type msacc_data() :: [msacc_data_thread()].

-type msacc_data_thread() :: #{ '$type' := msacc_data,
                                type := msacc_type(), id := msacc_id(),
                                counters := msacc_data_counters() }.
-type msacc_data_counters() :: #{ msacc_state() => non_neg_integer()}.

-type msacc_stats() :: [msacc_stats_thread()].
-type msacc_stats_thread() :: #{ '$type' := msacc_stats,
                                 type := msacc_type(), id := msacc_id(),
                                 system := float(),
                                 counters := msacc_stats_counters()}.
-type msacc_stats_counters() :: #{ msacc_state() => #{ thread := float(),
                                                       system := float()}}.


-type msacc_type() :: aux | async | dirty_cpu_scheduler
                    | dirty_io_scheduler | poll | scheduler.
-type msacc_id() :: non_neg_integer().
-type msacc_state() :: alloc | aux | bif | busy_wait | check_io |
                       emulator | ets | gc | gc_fullsweep | nif |
                       other | port | send | sleep | timers.

-type msacc_print_options() :: #{ system => boolean() }.

-spec available() -> boolean().
available() ->
    try
        [_|_] = erlang:statistics(microstate_accounting),
        true
    catch _:_ ->
            false
    end.

-spec start() -> boolean().
start() ->
    erlang:system_flag(microstate_accounting, true).

-spec stop() -> boolean().
stop() ->
    erlang:system_flag(microstate_accounting, false).

-spec reset() -> boolean().
reset() ->
    erlang:system_flag(microstate_accounting, reset).

-spec start(Time) -> true when
      Time :: timeout().
start(Tmo) ->
    stop(), reset(), start(),
    timer:sleep(Tmo),
    stop().

-spec to_file(Filename) -> ok | {error, file:posix()} when
      Filename :: file:name_all().
to_file(Filename) ->
    file:write_file(Filename, io_lib:format("~p.~n",[stats()])).

-spec from_file(Filename) -> msacc_data() when
      Filename :: file:name_all().
from_file(Filename) ->
    {ok, [Stats]} = file:consult(Filename),
    Stats.

-spec print() -> ok.
print() ->
    print(stats()).

-spec print(DataOrStats) -> ok when
      DataOrStats :: msacc_data() | msacc_stats().
print(Stats) ->
    print(Stats, #{}).

-spec print(DataOrStats, Options) -> ok when
      DataOrStats :: msacc_data() | msacc_stats(),
      Options :: msacc_print_options().
print(Stats, Options) ->
    print(group_leader(), Stats, Options).

-spec print(FileOrDevice, DataOrStats, Options) -> ok when
      FileOrDevice :: file:filename() | io:device(),
      DataOrStats :: msacc_data() | msacc_stats(),
      Options :: msacc_print_options().
print(Filename, Stats, Options) when is_list(Filename) ->
    case file:open(Filename,[write]) of
        {ok, D} -> print(D, Stats, Options),file:close(D);
        Error -> Error
    end;
print(Device, Stats, Options) ->
    DefaultOpts = #{ system => false },
    print_int(Device, Stats, maps:merge(DefaultOpts, Options)).
print_int(Device, [#{ '$type' := msacc_data, id := _Id }|_] = Stats, Options) ->
    TypeStats = stats(type, Stats),
    io:format(Device, "~s", [print_stats_overview(Stats, Options)]),
    io:format(Device, "~s", [print_stats_header(Stats, Options)]),
    io:format(Device, "~s", [print_stats_threads(
                               stats(realtime, Stats), Options)]),
    io:format(Device, "~s", [print_stats_type(
                               stats(realtime, TypeStats), Options)]);
print_int(Device, [#{ '$type' := msacc_data }|_] = Stats, Options) ->
    io:format(Device, "~s", [print_stats_header(Stats, Options)]),
    io:format(Device, "~s", [print_stats_type(
                               stats(realtime, Stats), Options)]);
print_int(Device, [#{ '$type' := msacc_stats, id := _Id }|_] = Stats,Options) ->
    io:format(Device, "~s", [print_stats_header(Stats, Options)]),
    io:format(Device, "~s", [print_stats_threads(Stats, Options)]),
    io:format(Device, "~s", [print_stats_type(
                               msacc:stats(type, Stats), Options)]);
print_int(Device, [#{ '$type' := msacc_stats }|_] = Stats, Options) ->
    io:format(Device, "~s", [print_stats_header(Stats, Options)]),
    io:format(Device, "~s", [print_stats_type(Stats, Options)]).


-spec stats() -> msacc_data().
stats() ->
    Fun = fun F(K,{PerfCount,StateCount}) ->
                %% Need to handle ERTS_MSACC_STATE_COUNTERS
                {F(K,PerfCount),StateCount};
              F(_K,PerfCount) ->
                erlang:convert_time_unit(PerfCount, perf_counter, 1000000)
        end,
    UsStats = lists:map(
                fun(#{ counters := Cnt } = M) ->
                        UsCnt = maps:map(Fun,Cnt),
                        M#{ '$type' => msacc_data, counters := UsCnt }
                end, erlang:statistics(microstate_accounting)),
    statssort(UsStats).

-spec stats(Analysis, Stats) -> non_neg_integer() when
      Analysis :: system_realtime | system_runtime,
      Stats :: msacc_data();
           (Analysis, Stats) -> msacc_stats() when
      Analysis :: realtime | runtime,
      Stats :: msacc_data();
           (Analysis, StatsOrData) -> msacc_data() | msacc_stats() when
      Analysis :: type,
      StatsOrData :: msacc_data() | msacc_stats().
stats(system_realtime, Stats) ->
    lists:foldl(fun(#{ counters := Cnt }, Acc) ->
                        get_total(Cnt, Acc)
                end, 0, Stats);
stats(system_runtime, Stats) ->
    lists:foldl(fun(#{ counters := Cnt }, Acc) ->
                        get_total(maps:remove(sleep, Cnt), Acc)
                end, 0, Stats);
stats(realtime, Stats) ->
    RealTime = stats(system_realtime, Stats),
    statssort([get_thread_perc(Thread, RealTime) || Thread <- Stats]);
stats(runtime, Stats) ->
    RunTime = stats(system_runtime, Stats),
    statssort([get_thread_perc(T#{ counters := maps:remove(sleep,Cnt)}, RunTime)
                               || T = #{ counters := Cnt } <- Stats]);
stats(type, Stats) ->
    statssort(merge_threads(Stats, [])).

print_stats_overview(Stats, _Options) ->
    RunTime = stats(system_runtime, Stats),
    RealTime = stats(system_realtime, Stats) div length(Stats),
    SchedStats = [S || #{ type := scheduler } = S <- Stats],
    AvgSchedRunTime = stats(system_runtime, SchedStats) div length(SchedStats),
    NumSize = if
                  RealTime > RunTime -> length(integer_to_list(RealTime));
                  true -> length(integer_to_list(RunTime))
              end,
    [io_lib:format("Average thread real-time    : ~*B us~n",
                   [NumSize, RealTime]),
     io_lib:format("Accumulated system run-time : ~*B us~n",
                   [NumSize, RunTime]),
     io_lib:format("Average scheduler run-time  : ~*B us~n",
                   [NumSize, AvgSchedRunTime]),
     io_lib:format("~n",[])].

print_stats_threads(Stats, Options) ->
    [io_lib:format("~nStats per thread:~n", []),
     [print_thread_info(Thread, Options) || Thread <- Stats]].

print_stats_type(Stats, Options) ->
    [io_lib:format("~nStats per type:~n", []),
     [print_thread_info(Thread, Options) || Thread <- Stats]].


print_stats_header([#{ counters := Cnt }|_], #{ system := PrintSys }) ->
    [io_lib:format("~14s", ["Thread"]),
     map(fun(Counter, _) when PrintSys->
                 io_lib:format("~9s     ", [atom_to_list(Counter)]);
            (Counter, _) ->
                 io_lib:format("~9s", [atom_to_list(Counter)])
         end, Cnt),
     io_lib:format("~n",[])].

print_thread_info(#{ '$type' := msacc_stats,
                     counters := Cnt } = Thread, #{ system := PrintSys }) ->
    [case maps:find(id, Thread) of
        error ->
            io_lib:format("~14s", [atom_to_list(maps:get(type, Thread))]);
        {ok, Id} ->
            io_lib:format("~10s(~2B)", [atom_to_list(maps:get(type,Thread)),Id])
    end,
    map(fun(_Key, #{ thread := ThreadPerc, system := SystemPerc }) when PrintSys ->
                io_lib:format("~6.2f%(~4.1f%)", [ThreadPerc, SystemPerc]);
           (_Key, #{ thread := ThreadPerc }) ->
                io_lib:format("~8.2f%", [ThreadPerc])
             end, Cnt),
    io_lib:format("~n",[])].

get_total(Cnt, Base) ->
    maps:fold(fun(_, {Val,_}, Time) ->
                      %% Have to handle ERTS_MSACC_STATE_COUNTERS
                      Time + Val;
                 (_, Val, Time) -> Time + Val
              end, Base, Cnt).

get_thread_perc(#{ '$type' := msacc_data, counters := Cnt } = Thread,
                SystemTime) ->
    ThreadTime = get_total(Cnt, 0),
    Thread#{ '$type' := msacc_stats,
             system => percentage(ThreadTime,SystemTime),
             counters => get_thread_perc(Cnt, ThreadTime, SystemTime)}.
get_thread_perc(Cnt, ThreadTime, SystemTime) ->
    maps:map(fun F(Key, {Val, C}) ->
                    M = F(Key, Val),
                    M#{ cnt => C };
                 F(_Key, Val) ->
                    #{ thread => percentage(Val, ThreadTime),
                       system => percentage(Val, SystemTime) }
            end, Cnt).

%% This code is a little bit messy as it has to be able to deal with
%% both [msacc_data()] and [msacc_stats()].
merge_threads([#{ '$type' := msacc_stats,
                  type := Type,
                  counters := Cnt } = M0|R], Acc) ->
    case keyfind(type, Type, Acc) of
        false ->
            merge_threads(R, [maps:remove(id,M0#{ threads => 1 })|Acc]);
        #{ '$type' := msacc_stats, counters := Cnt0,
           threads := Threads, system := System } = M ->
            NewMap = M#{ counters := add_counters(Cnt, Cnt0),
                         system := System + maps:get(system, M0),
                         threads := Threads + 1},
            NewAcc = keyreplace(type, Type, NewMap, Acc),
            merge_threads(R, NewAcc)
    end;
merge_threads([], [#{ '$type' := msacc_stats,
                      system := System,
                      threads := Threads,
                      counters := Cnt} = M0|R]) ->
    Counters = maps:map(fun(_,#{ thread := Thr } = Map) ->
                                Map#{ thread := Thr / Threads }
                        end, Cnt),
    M = maps:remove(threads, M0),
    [M#{ system := System, counters := Counters} | merge_threads([],R)];
merge_threads([], []) ->
    [];
%% The clauses below deal with msacc_data()
merge_threads([#{ '$type' := msacc_data,
                  type := Type,
                  counters := Cnt } = M0|R], Acc) ->
    case keyfind(type, Type, Acc) of
        false ->
            merge_threads(R, [maps:remove(id,M0)|Acc]);
        #{ '$type' := msacc_data, counters := Cnt0 } = M ->
            NewMap = M#{ counters := add_counters(Cnt, Cnt0) },
            NewAcc = keyreplace(type, Type, NewMap, Acc),
            merge_threads(R, NewAcc)
    end;
merge_threads([], Acc) ->
    Acc.

add_counters(M1, M2) ->
    maps:map(
      fun(Key, #{ thread := Thr1, system := Sys1, cnt := Cnt1}) ->
              %% Have to handle ERTS_MSACC_STATE_COUNTERS
              #{ thread := Thr2, system := Sys2, cnt := Cnt2} = maps:get(Key, M2),
              #{ thread => Thr1 + Thr2, system => Sys1 + Sys2,
                 cnt => Cnt1 + Cnt2 };
         (Key, #{ thread := Thr1, system := Sys1}) ->
              #{ thread := Thr2, system := Sys2} = maps:get(Key, M2),
              #{ thread => Thr1 + Thr2, system => Sys1 + Sys2};
         (Key, {V1,C1}) ->
              %% Have to handle ERTS_MSACC_STATE_COUNTERS
              {V2,C2} = maps:get(Key, M2),{V1+V2,C1+C2};
         (Key, V1) -> maps:get(Key, M2) + V1
      end, M1).

percentage(Divident, Divisor) ->
    if Divisor == 0 andalso Divident /= 0 ->
            100.0;
        Divisor == 0 ->
            0.0;
       true ->
            Divident / Divisor * 100
    end.

keyfind(Key, Value, [H|T]) ->
    case maps:find(Key, H) of
        {ok, Value} ->
            H;
        _ ->
            keyfind(Key, Value, T)
    end;
keyfind(_, _, []) ->
    false.

keyreplace(Key, Value, NewMap, [H|T]) ->
    case maps:find(Key, H) of
        {ok, Value} ->
            [NewMap|T];
        _ ->
            [H|keyreplace(Key, Value, NewMap, T)]
    end;
keyreplace(_, _, _, []) ->
    [].

statssort(Stats) ->
    lists:sort(fun(#{ type := Type1, id := Id1},
                   #{ type := Type2, id := Id2}) ->
                       {Type1, Id1} < {Type2, Id2};
                  (#{ type := Type1}, #{ type := Type2}) ->
                       Type1 < Type2
               end, Stats).

map(Fun,Map) ->
    [ Fun(K,V) || {K,V} <- maps:to_list(Map) ].
