%% %% %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) ].