2019-12-31 09:52:56 +00:00
|
|
|
-module(meter_memory_pressure).
|
|
|
|
|
|
|
|
-type microseconds() :: non_neg_integer().
|
|
|
|
-type words() :: non_neg_integer().
|
|
|
|
-type metrics() :: #{
|
|
|
|
minor_gcs := non_neg_integer(),
|
|
|
|
minor_gcs_duration := microseconds(),
|
|
|
|
major_gcs := non_neg_integer(),
|
|
|
|
minor_gcs_duration := microseconds(),
|
|
|
|
heap_reclaimed := words(),
|
|
|
|
offheap_bin_reclaimed := words(),
|
|
|
|
stack_min := words(),
|
|
|
|
stack_max := words()
|
|
|
|
}.
|
|
|
|
|
|
|
|
-export([measure/2]).
|
|
|
|
-export([export/3]).
|
|
|
|
|
|
|
|
-export_type([metrics/0]).
|
|
|
|
|
|
|
|
%%
|
|
|
|
|
|
|
|
-type runner() :: fun(() -> _).
|
|
|
|
-type opts() :: #{
|
2021-01-15 12:35:41 +00:00
|
|
|
iterations => pos_integer(),
|
|
|
|
spawn_opts => [{atom(), _}],
|
2019-12-31 09:52:56 +00:00
|
|
|
dump_traces => file:filename()
|
|
|
|
}.
|
|
|
|
|
|
|
|
-export_type([runner/0]).
|
|
|
|
-export_type([opts/0]).
|
|
|
|
|
2021-01-15 12:35:41 +00:00
|
|
|
-spec measure(runner(), opts()) -> metrics().
|
2019-12-31 09:52:56 +00:00
|
|
|
measure(Runner, Opts0) ->
|
|
|
|
Opts = maps:merge(get_default_opts(), Opts0),
|
|
|
|
Token = make_ref(),
|
|
|
|
Tracer = start_tracer(Token, Opts),
|
|
|
|
ok = run(Runner, Tracer, Opts),
|
|
|
|
Metrics = collect_metrics(Tracer, Token),
|
|
|
|
Metrics.
|
|
|
|
|
|
|
|
get_default_opts() ->
|
|
|
|
#{
|
|
|
|
iterations => 100,
|
|
|
|
spawn_opts => [{fullsweep_after, 0}]
|
|
|
|
}.
|
|
|
|
|
|
|
|
run(Runner, Tracer, Opts) ->
|
|
|
|
SpawnOpts = [monitor, {priority, high}] ++ maps:get(spawn_opts, Opts),
|
|
|
|
{Staging, MRef} = erlang:spawn_opt(
|
2021-01-15 12:35:41 +00:00
|
|
|
fun() -> run_staging(Runner, Tracer, Opts) end,
|
2019-12-31 09:52:56 +00:00
|
|
|
SpawnOpts
|
|
|
|
),
|
|
|
|
receive
|
|
|
|
{'DOWN', MRef, process, Staging, normal} ->
|
|
|
|
ok
|
|
|
|
end.
|
|
|
|
|
|
|
|
run_staging(Runner, Tracer, Opts) ->
|
|
|
|
N = maps:get(iterations, Opts),
|
|
|
|
TraceOpts = [garbage_collection, timestamp, {tracer, Tracer}],
|
|
|
|
_ = erlang:trace(self(), true, TraceOpts),
|
|
|
|
iterate(Runner, N).
|
|
|
|
|
|
|
|
iterate(Runner, N) when N > 0 ->
|
|
|
|
_ = Runner(),
|
|
|
|
iterate(Runner, N - 1);
|
|
|
|
iterate(_Runner, 0) ->
|
|
|
|
ok.
|
|
|
|
|
|
|
|
%%
|
|
|
|
|
|
|
|
start_tracer(Token, Opts) ->
|
|
|
|
Self = self(),
|
2021-01-15 12:35:41 +00:00
|
|
|
erlang:spawn_link(fun() -> run_tracer(Self, Token, Opts) end).
|
2019-12-31 09:52:56 +00:00
|
|
|
|
|
|
|
collect_metrics(Tracer, Token) ->
|
|
|
|
_ = Tracer ! Token,
|
|
|
|
receive
|
|
|
|
{?MODULE, {metrics, Metrics}} ->
|
|
|
|
Metrics
|
|
|
|
end.
|
|
|
|
|
|
|
|
run_tracer(MeterPid, Token, Opts) ->
|
2021-01-15 12:35:41 +00:00
|
|
|
_ =
|
|
|
|
receive
|
|
|
|
Token -> ok
|
|
|
|
end,
|
2019-12-31 09:52:56 +00:00
|
|
|
Traces = collect_traces(),
|
|
|
|
Metrics = analyze_traces(Traces),
|
|
|
|
ok = maybe_dump_traces(Traces, Opts),
|
|
|
|
MeterPid ! {?MODULE, {metrics, Metrics}}.
|
|
|
|
|
|
|
|
collect_traces() ->
|
|
|
|
collect_traces([]).
|
2021-01-15 12:35:41 +00:00
|
|
|
|
2019-12-31 09:52:56 +00:00
|
|
|
collect_traces(Acc) ->
|
|
|
|
receive
|
|
|
|
{trace_ts, _Pid, Trace, Info, Clock} ->
|
|
|
|
collect_traces([{Trace, Info, Clock} | Acc]);
|
|
|
|
Unexpected ->
|
|
|
|
error({unexpected, Unexpected})
|
2021-01-15 12:35:41 +00:00
|
|
|
after 0 -> lists:reverse(Acc)
|
2019-12-31 09:52:56 +00:00
|
|
|
end.
|
|
|
|
|
|
|
|
maybe_dump_traces(Traces, #{dump_traces := Filename}) ->
|
|
|
|
file:write_file(Filename, erlang:term_to_binary(Traces));
|
|
|
|
maybe_dump_traces(_, #{}) ->
|
|
|
|
ok.
|
|
|
|
|
|
|
|
analyze_traces(Traces) ->
|
|
|
|
analyze_traces(Traces, #{
|
|
|
|
minor_gcs => 0,
|
|
|
|
minor_gcs_duration => 0,
|
|
|
|
major_gcs => 0,
|
|
|
|
major_gcs_duration => 0,
|
|
|
|
heap_reclaimed => 0,
|
|
|
|
offheap_bin_reclaimed => 0
|
|
|
|
}).
|
|
|
|
|
|
|
|
analyze_traces([{gc_minor_start, InfoStart, C1}, {gc_minor_end, InfoEnd, C2} | Rest], M0) ->
|
|
|
|
M1 = increment(minor_gcs, M0),
|
|
|
|
M2 = increment(minor_gcs_duration, timer:now_diff(C2, C1), M1),
|
|
|
|
analyze_traces(Rest, analyze_gc(InfoStart, InfoEnd, M2));
|
|
|
|
analyze_traces([{gc_major_start, InfoStart, C1}, {gc_major_end, InfoEnd, C2} | Rest], M0) ->
|
|
|
|
M1 = increment(major_gcs, M0),
|
|
|
|
M2 = increment(major_gcs_duration, timer:now_diff(C2, C1), M1),
|
|
|
|
analyze_traces(Rest, analyze_gc(InfoStart, InfoEnd, M2));
|
|
|
|
analyze_traces([], M) ->
|
|
|
|
M.
|
|
|
|
|
|
|
|
analyze_gc(InfoStart, InfoEnd, M0) ->
|
|
|
|
M1 = increment(heap_reclaimed, difference(heap_size, InfoEnd, InfoStart), M0),
|
|
|
|
M2 = increment(offheap_bin_reclaimed, difference(bin_vheap_size, InfoEnd, InfoStart), M1),
|
|
|
|
M3 = update(stack_min, fun erlang:min/2, min(stack_size, InfoStart, InfoEnd), M2),
|
|
|
|
M4 = update(stack_max, fun erlang:max/2, max(stack_size, InfoStart, InfoEnd), M3),
|
|
|
|
M4.
|
|
|
|
|
|
|
|
difference(Name, Info1, Info2) ->
|
2021-01-15 12:35:41 +00:00
|
|
|
combine(Name, fun(V1, V2) -> erlang:max(0, V2 - V1) end, Info1, Info2).
|
2019-12-31 09:52:56 +00:00
|
|
|
|
|
|
|
min(Name, Info1, Info2) ->
|
|
|
|
combine(Name, fun erlang:min/2, Info1, Info2).
|
|
|
|
|
|
|
|
max(Name, Info1, Info2) ->
|
|
|
|
combine(Name, fun erlang:max/2, Info1, Info2).
|
|
|
|
|
|
|
|
combine(Name, Fun, Info1, Info2) ->
|
|
|
|
{_Name, V1} = lists:keyfind(Name, 1, Info1),
|
|
|
|
{_Name, V2} = lists:keyfind(Name, 1, Info2),
|
|
|
|
Fun(V1, V2).
|
|
|
|
|
|
|
|
increment(Name, Metrics) ->
|
|
|
|
increment(Name, 1, Metrics).
|
|
|
|
|
|
|
|
increment(Name, Delta, Metrics) ->
|
2021-01-15 12:35:41 +00:00
|
|
|
maps:update_with(Name, fun(V) -> V + Delta end, Metrics).
|
2019-12-31 09:52:56 +00:00
|
|
|
|
|
|
|
update(Name, Fun, I, Metrics) ->
|
2021-01-15 12:35:41 +00:00
|
|
|
maps:update_with(Name, fun(V) -> Fun(V, I) end, I, Metrics).
|
2019-12-31 09:52:56 +00:00
|
|
|
|
|
|
|
%%
|
|
|
|
|
|
|
|
-spec export(file:filename(), file:filename(), csv) -> ok.
|
|
|
|
export(FilenameIn, FilenameOut, Format) ->
|
|
|
|
{ok, Content} = file:read_file(FilenameIn),
|
|
|
|
Traces = erlang:binary_to_term(Content),
|
|
|
|
{ok, FileOut} = file:open(FilenameOut, [write, binary]),
|
|
|
|
ok = format_traces(Traces, Format, FileOut),
|
|
|
|
ok = file:close(FileOut).
|
|
|
|
|
|
|
|
format_traces(Traces, csv, FileOut) ->
|
|
|
|
_ = format_csv_header(FileOut),
|
2021-01-15 12:35:41 +00:00
|
|
|
_ = lists:foreach(fun(T) -> format_csv_trace(T, FileOut) end, Traces),
|
2019-12-31 09:52:56 +00:00
|
|
|
ok.
|
|
|
|
|
|
|
|
format_csv_header(Out) ->
|
|
|
|
Line = " ~s , ~s , ~s , ~s , ~s , ~s , ~s , ~s , ~s , ~s , ~s , ~s ~n",
|
|
|
|
io:fwrite(Out, Line, [
|
|
|
|
"Time",
|
|
|
|
"End?",
|
|
|
|
"Major?",
|
|
|
|
"Stack",
|
|
|
|
"Heap",
|
|
|
|
"HeapBlock",
|
|
|
|
"BinHeap",
|
|
|
|
"BinHeapBlock",
|
|
|
|
"OldHeap",
|
|
|
|
"OldHeapBlock",
|
|
|
|
"OldBinHeap",
|
|
|
|
"OldBinHeapBlock"
|
|
|
|
]).
|
|
|
|
|
|
|
|
format_csv_trace({Event, Info, Clock}, Out) ->
|
|
|
|
Line = " ~B , ~B , ~B , ~B , ~B , ~B , ~B , ~B , ~B , ~B , ~B , ~B ~n",
|
|
|
|
io:fwrite(Out, Line, [
|
|
|
|
clock_to_mcs(Clock),
|
|
|
|
bool_to_integer(lists:member(Event, [gc_minor_end, gc_major_end])),
|
|
|
|
bool_to_integer(lists:member(Event, [gc_major_start, gc_major_end])),
|
|
|
|
get_info(stack_size, Info),
|
|
|
|
get_info(heap_size, Info),
|
|
|
|
get_info(heap_block_size, Info),
|
|
|
|
get_info(bin_vheap_size, Info),
|
|
|
|
get_info(bin_vheap_block_size, Info),
|
|
|
|
get_info(old_heap_size, Info),
|
|
|
|
get_info(old_heap_block_size, Info),
|
|
|
|
get_info(bin_old_vheap_size, Info),
|
|
|
|
get_info(bin_old_vheap_block_size, Info)
|
|
|
|
]).
|
|
|
|
|
|
|
|
get_info(Name, Info) ->
|
2021-01-15 12:35:41 +00:00
|
|
|
{_Name, V} = lists:keyfind(Name, 1, Info),
|
|
|
|
V.
|
2019-12-31 09:52:56 +00:00
|
|
|
|
|
|
|
clock_to_mcs({MSec, Sec, USec}) ->
|
|
|
|
(MSec * 1000000 + Sec) * 1000000 + USec.
|
|
|
|
|
|
|
|
bool_to_integer(false) ->
|
|
|
|
0;
|
|
|
|
bool_to_integer(true) ->
|
|
|
|
1.
|