Current section
Files
Jump to
Current section
Files
src/observer_cli_process.erl
-module(observer_cli_process).
-include("observer_cli.hrl").
-dialyzer([
{nowarn_function, [
render_worker/8,
render_process_sections/3,
render_reduction_memory/4,
get_chart_format/1,
chart_format/2
]}
]).
-export([start/3]).
-ifdef(TEST).
-export([
parse_cmd_str/1,
collect_process_info/1,
collect_process_messages/1,
collect_process_dictionary/1,
collect_process_stack/1,
collect_process_state/1,
chart_format/2,
replace_first_line/2,
render_process_sections/3,
render_process_info/1,
render_process_messages/1,
render_process_dictionary/1,
render_process_stack/1,
render_process_state/2,
render_stateless_view/4,
render_link_monitor/3,
render_reduction_memory/4,
render_menu/3,
render_footer/0,
state_footer_text/1,
render_worker/8,
render_state/3,
output_die_view/3,
state_nav/1,
state_title/1,
state_footer/2,
truncate_str/2,
format_mod/1,
format/1,
collect_process_extra/2,
binary_refs_summary/1,
format_suspending/1,
wait_for_state_view/4,
bounded_process_detail/3
]).
-endif.
%% lists:foldl(fun(_X, Acc) -> queue:in('NaN', Acc) end, queue:new(), lists:seq(1, 5))
-define(INIT_QUEUE, {['NaN', 'NaN', 'NaN', 'NaN'], ['NaN']}).
-define(DETAIL_TIMEOUT_MS, 5000).
-define(DETAIL_MAX_HEAP_WORDS, 512 * 1024).
-define(DETAIL_MAX_TERM_BYTES, 64 * 1024).
-define(DETAIL_MAX_OUTPUT_CHARS, 64 * 1024).
-define(DETAIL_MAX_OUTPUT_BYTES, 64 * 1024).
-define(DETAIL_FORMAT_DEPTH, 32).
-spec start(Type, pid(), view_opts()) -> no_return() when Type :: home | plugin.
start(Type, Pid, Opts) ->
#view_opts{process = #process{interval = RefreshMs}} = Opts,
ManagerPid = self(),
RenderPid = spawn_link(fun() ->
?output(?CLEAR),
render_worker(
info,
Type,
RefreshMs,
Pid,
?INIT_TIME_REF,
?INIT_QUEUE,
?INIT_QUEUE,
ManagerPid
)
end),
manager(RenderPid, Type, Pid, Opts).
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%% Private
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
manager(RenderPid, Type, Pid, Opts) ->
handle_action(parse_cmd(), RenderPid, Type, Pid, Opts).
handle_action(quit, RenderPid, _Type, _Pid, _Opts) ->
erlang:exit(RenderPid, stop);
handle_action(
{new_interval, NewInterval}, RenderPid, Type, Pid, #view_opts{process = ProcOpts} = Opts
) ->
erlang:send(RenderPid, {new_interval, NewInterval}),
NewOpt = Opts#view_opts{process = ProcOpts#process{interval = NewInterval}},
manager(RenderPid, Type, Pid, NewOpt);
handle_action(home, RenderPid, _Type, _Pid, Opts) ->
open_home(RenderPid, Opts);
handle_action(back, RenderPid, home, _Pid, Opts) ->
open_home(RenderPid, Opts);
handle_action(back, RenderPid, plugin, _Pid, Opts) ->
open_plugin(RenderPid, Opts);
handle_action(state_view, RenderPid, Type, Pid, Opts) ->
erlang:send(RenderPid, state_view),
wait_for_state_view(RenderPid, Type, Pid, Opts);
handle_action(ViewAction, RenderPid, Type, Pid, Opts) ->
erlang:send(RenderPid, ViewAction),
manager(RenderPid, Type, Pid, Opts).
open_plugin(RenderPid, Opts) ->
erlang:exit(RenderPid, stop),
observer_cli_plugin:start(Opts).
open_home(RenderPid, Opts) ->
erlang:exit(RenderPid, stop),
observer_cli:start(Opts).
wait_for_state_view(RenderPid, Type, Pid, Opts) ->
receive
{state_view_done, {ok, none}} ->
manager(RenderPid, Type, Pid, Opts);
{state_view_done, {ok, Action}} ->
handle_action(Action, RenderPid, Type, Pid, Opts);
{state_view_done, error} ->
manager(RenderPid, Type, Pid, Opts)
end.
render_worker(info, Type, Interval, Pid, TimeRef, RedQ, MemQ, ManagerPid) ->
case collect_process_info(Pid) of
dead ->
output_die_view(Pid, Type, Interval),
next_draw_view(info, Type, TimeRef, Interval, Pid, RedQ, MemQ, ManagerPid);
ProcessInfo ->
{NewRedQ, NewMemQ, Lines} = render_process_sections(ProcessInfo, RedQ, MemQ),
Menu = render_menu(info, Type, Interval),
LastLine = render_footer(),
?output([?CURSOR_TOP, Menu, Lines, LastLine]),
next_draw_view(info, Type, TimeRef, Interval, Pid, NewRedQ, NewMemQ, ManagerPid)
end;
render_worker(message, Type, Interval, Pid, TimeRef, RedQ, MemQ, ManagerPid) ->
case bounded_process_detail(message, Pid) of
{ok, Line} ->
?output([?CURSOR_TOP, render_stateless_view(message, Type, Interval, Line)]),
next_draw_view(message, Type, TimeRef, Interval, Pid, RedQ, MemQ, ManagerPid);
dead ->
render_worker(
info,
Type,
Interval,
Pid,
?INIT_TIME_REF,
?INIT_QUEUE,
?INIT_QUEUE,
ManagerPid
)
end;
render_worker(dict, Type, Interval, Pid, TimeRef, RedQ, MemQ, ManagerPid) ->
case bounded_process_detail(dict, Pid) of
{ok, Line} ->
?output([?CURSOR_TOP, render_stateless_view(dict, Type, Interval, Line)]),
next_draw_view(dict, Type, TimeRef, Interval, Pid, RedQ, MemQ, ManagerPid);
dead ->
render_worker(
info,
Type,
Interval,
Pid,
?INIT_TIME_REF,
?INIT_QUEUE,
?INIT_QUEUE,
ManagerPid
)
end;
render_worker(stack, Type, Interval, Pid, TimeRef, RedQ, MemQ, ManagerPid) ->
case collect_process_stack(Pid) of
{ok, #{pid := Pid, stack := Stack}} ->
Prompt = io_lib:format("erlang:process_info(~p, current_stacktrace). ~n", [Pid]),
Line = [Prompt, render_process_stack(Stack)],
?output([?CURSOR_TOP, render_stateless_view(stack, Type, Interval, Line)]),
next_draw_view(stack, Type, TimeRef, Interval, Pid, RedQ, MemQ, ManagerPid);
dead ->
render_worker(
info,
Type,
Interval,
Pid,
?INIT_TIME_REF,
?INIT_QUEUE,
?INIT_QUEUE,
ManagerPid
)
end;
render_worker(state, Type, Interval, Pid, TimeRef, RedQ, MemQ, ManagerPid) ->
Result = render_state(Pid, Type, Interval),
erlang:send(ManagerPid, {state_view_done, Result}),
case Result of
{ok, _Action} ->
next_draw_view(state, Type, TimeRef, Interval, Pid, RedQ, MemQ, ManagerPid);
error ->
next_draw_view_2(state, Type, TimeRef, Interval, Pid, RedQ, MemQ, ManagerPid)
end.
render_process_sections(
#{
process := ProcessView,
links := Link,
monitors := Monitors,
monitored_by := MonitoredBy,
reductions := Reductions,
memory := Memory
},
RedQ,
MemQ
) ->
ProcessSection = render_process_info(ProcessView),
LinkSection = render_link_monitor(Link, Monitors, MonitoredBy),
{NewRedQ, NewMemQ, ReductionSection} = render_reduction_memory(Reductions, Memory, RedQ, MemQ),
{NewRedQ, NewMemQ, [ProcessSection, LinkSection, ReductionSection]}.
render_stateless_view(View, Type, Interval, Line) ->
Menu = render_menu(View, Type, Interval),
LastLine = render_footer(),
[Menu, Line, LastLine].
collect_process_info(Pid) ->
ProcessInfo = recon:info(Pid, [
registered_name,
group_leader,
status,
links,
monitors,
monitored_by,
trap_exit,
initial_call,
memory,
message_queue_len,
heap_size,
total_heap_size,
garbage_collection,
reductions
]),
case ProcessInfo of
undefined ->
dead;
_ ->
WordSize = erlang:system_info(wordsize),
RegisteredName = proplists:get_value(registered_name, ProcessInfo),
GroupLeader = proplists:get_value(group_leader, ProcessInfo),
Status = proplists:get_value(status, ProcessInfo),
Link = proplists:get_value(links, ProcessInfo),
Monitors = proplists:get_value(monitors, ProcessInfo),
MonitoredBy = proplists:get_value(monitored_by, ProcessInfo),
TrapExit = proplists:get_value(trap_exit, ProcessInfo),
InitialCall = proplists:get_value(initial_call, ProcessInfo),
Memory = proplists:get_value(memory, ProcessInfo),
MessageQueueLen = proplists:get_value(message_queue_len, ProcessInfo),
HeapSize = proplists:get_value(heap_size, ProcessInfo, 0) * WordSize,
TotalHeapSize = proplists:get_value(total_heap_size, ProcessInfo, 0) * WordSize,
GarbageCollection = collect_process_gc(
proplists:get_value(garbage_collection, ProcessInfo), WordSize
),
Reductions = proplists:get_value(reductions, ProcessInfo),
case collect_process_extra(Pid, WordSize) of
dead ->
dead;
Extra ->
ProcessView = maps:merge(
#{
pid => Pid,
registered_name => RegisteredName,
group_leader => GroupLeader,
status => Status,
trap_exit => TrapExit,
initial_call => InitialCall,
message_queue_len => MessageQueueLen,
heap_size => HeapSize,
total_heap_size => TotalHeapSize,
garbage_collection => GarbageCollection
},
Extra
),
#{
process => ProcessView,
links => Link,
monitors => Monitors,
monitored_by => MonitoredBy,
reductions => Reductions,
memory => Memory
}
end
end.
collect_process_extra(Pid, WordSize) ->
case
erlang:process_info(Pid, [
priority,
stack_size,
binary,
catchlevel,
suspending,
error_handler
])
of
undefined ->
dead;
Info ->
Binaries = proplists:get_value(binary, Info, []),
#{
priority => proplists:get_value(priority, Info),
stack_size => proplists:get_value(stack_size, Info, 0) * WordSize,
binary_refs => binary_refs_summary(Binaries),
catchlevel => proplists:get_value(catchlevel, Info),
suspending => proplists:get_value(suspending, Info, []),
error_handler => proplists:get_value(error_handler, Info)
}
end.
binary_refs_summary(Binaries) ->
Bytes = lists:foldl(
fun
({_, Size, _}, Acc) when is_integer(Size) -> Acc + Size;
(_, Acc) -> Acc
end,
0,
Binaries
),
{erlang:length(Binaries), Bytes}.
collect_process_gc(GarbageCollection, WordSize) ->
#{
min_bin_vheap_size =>
proplists:get_value(min_bin_vheap_size, GarbageCollection) * WordSize,
min_heap_size => proplists:get_value(min_heap_size, GarbageCollection) * WordSize,
fullsweep_after => proplists:get_value(fullsweep_after, GarbageCollection),
minor_gcs => proplists:get_value(minor_gcs, GarbageCollection)
}.
collect_process_messages(Pid) ->
case erlang:process_info(Pid, message_queue_len) of
{message_queue_len, Len} when Len =:= 0 ->
{ok, #{pid => Pid, message_queue_len => Len, messages => []}};
{message_queue_len, Len} when Len > 10000 ->
{ok, #{pid => Pid, message_queue_len => Len, too_many => true, messages => []}};
{message_queue_len, Len} ->
{messages, Messages} = recon:info(Pid, messages),
{ok, #{pid => Pid, message_queue_len => Len, messages => Messages}};
undefined ->
dead
end.
render_process_messages(#{message_queue_len := 0}) ->
"\e[32;1mNo messages were found.\e[0m\n";
render_process_messages(#{too_many := true}) ->
"\e[31mtoo_large\e[0m\n";
render_process_messages(#{pid := Pid, message_queue_len := Len, messages := Messages}) ->
[
io_lib:format("~p Message Len:~p~n", [Pid, Len]),
truncate_str(Pid, Messages)
].
collect_process_dictionary(Pid) ->
case erlang:process_info(Pid, dictionary) of
{dictionary, List} ->
{ok, #{pid => Pid, dictionary => List}};
undefined ->
dead
end.
render_process_dictionary(#{pid := Pid, dictionary := List}) ->
Len = erlang:length(List),
Line1 = io_lib:format(
"erlang:process_info(~p, dictionary). dictionary_len:~p ~n",
[Pid, Len]
),
Line2 =
case Len of
0 -> "\e[32;1mNo dictionary was found\e[0m\n";
_ -> truncate_str(Pid, List)
end,
[Line1, Line2].
collect_process_stack(Pid) ->
case erlang:process_info(Pid, current_stacktrace) of
{current_stacktrace, Stack} ->
{ok, #{pid => Pid, stack => Stack}};
undefined ->
dead
end.
render_process_stack(Stack) ->
{_, Line} =
lists:foldr(
fun({Mod, Func, Arity, Location}, {Nth, Acc}) ->
Mfa = observer_cli_lib:mfa_to_list({Mod, Func, Arity}),
File = proplists:get_value(file, Location, "undefined"),
LineNo = proplists:get_value(line, Location, 0),
FileLine = File ++ ":" ++ erlang:integer_to_list(LineNo),
case Nth =:= 1 of
false -> {Nth + 1, [?W(Mfa, 66), ?W(FileLine, 62), ?NEW_LINE | Acc]};
true -> {Nth + 1, [?W(Mfa, 66), ?W(FileLine, 62) | Acc]}
end
end,
{1, []},
lists:sublist(Stack, 30)
),
?render(Line).
collect_process_state(Pid) ->
recon:get_state(Pid, 2500).
render_process_state(Pid, State) ->
Line = truncate_str(Pid, State),
replace_first_line(Line, state_title(Pid)).
bounded_process_detail(View, Pid) ->
bounded_process_detail(View, Pid, ?DETAIL_TIMEOUT_MS).
bounded_process_detail(View, Pid, Timeout) ->
bounded_detail(
Pid,
fun() -> collect_and_render_process_detail(View, Pid) end,
Timeout
).
collect_and_render_process_detail(message, Pid) ->
case collect_process_messages(Pid) of
{ok, Info} -> {ok, render_process_messages(Info)};
dead -> dead
end;
collect_and_render_process_detail(dict, Pid) ->
case collect_process_dictionary(Pid) of
{ok, Info} -> {ok, render_process_dictionary(Info)};
dead -> dead
end;
collect_and_render_process_detail(state, Pid) ->
try collect_process_state(Pid) of
State -> {ok, render_process_state(Pid, State)}
catch
_:_ -> state_error
end.
bounded_detail(Pid, Fun, Timeout) ->
Parent = self(),
Ref = make_ref(),
{Worker, Monitor} = spawn_opt(
fun() -> Parent ! {Ref, self(), bounded_detail_result(Fun)} end,
[
monitor,
{max_heap_size, #{
size => ?DETAIL_MAX_HEAP_WORDS,
kill => true,
error_logger => false,
include_shared_binaries => true
}}
]
),
{ok, KillTimer} = timer:kill_after(Timeout, Worker),
await_bounded_detail(Pid, Worker, Monitor, Ref, KillTimer, Timeout).
bounded_detail_result(Fun) ->
try Fun() of
{ok, Output} -> bounded_detail_output(Output);
dead -> dead;
state_error -> state_error
catch
_:_ -> error
end.
bounded_detail_output(Output) ->
try unicode:characters_to_binary(Output) of
Binary when
is_binary(Binary),
byte_size(Binary) =< ?DETAIL_MAX_OUTPUT_BYTES
->
Characters = unicode:characters_to_list(Binary),
case erlang:length(Characters) =< ?DETAIL_MAX_OUTPUT_CHARS of
true -> {ok, Binary};
false -> too_large
end;
_ ->
too_large
catch
_:_ -> too_large
end.
await_bounded_detail(Pid, Worker, Monitor, Ref, KillTimer, Timeout) ->
receive
{Ref, Worker, Result} ->
timer:cancel(KillTimer),
erlang:demonitor(Monitor, [flush]),
normalize_bounded_detail(Pid, Result);
{'DOWN', Monitor, process, Worker, killed} ->
timer:cancel(KillTimer),
normalize_bounded_detail(Pid, too_large);
{'DOWN', Monitor, process, Worker, _Reason} ->
timer:cancel(KillTimer),
normalize_bounded_detail(Pid, error)
after Timeout ->
timer:cancel(KillTimer),
exit(Worker, kill),
receive
{'DOWN', Monitor, process, Worker, _Reason} -> ok
after 1000 ->
erlang:demonitor(Monitor, [flush])
end,
receive
{Ref, Worker, _Result} -> ok
after 0 ->
ok
end,
normalize_bounded_detail(Pid, too_large)
end.
normalize_bounded_detail(_Pid, {ok, Binary}) ->
{ok, Binary};
normalize_bounded_detail(_Pid, dead) ->
dead;
normalize_bounded_detail(_Pid, state_error) ->
error;
normalize_bounded_detail(Pid, error) ->
normalize_bounded_detail(Pid, too_large);
normalize_bounded_detail(Pid, too_large) ->
{ok, unicode:characters_to_binary(detail_too_large(Pid))}.
detail_too_large(Pid) ->
io_lib:format("Process: ~p~n~ntoo_large~n", [Pid]).
%% state_view is static. user left state view and may stay long after. no need for redraw
next_draw_view(state, Type, TimeRef, Interval, Pid, NewRedQ, NewMemQ, ManagerPid) ->
observer_cli_lib:flush_redraw_timer(TimeRef),
next_draw_view_2(state, Type, TimeRef, Interval, Pid, NewRedQ, NewMemQ, ManagerPid);
next_draw_view(Status, Type, TimeRef, Interval, Pid, NewRedQ, NewMemQ, ManagerPid) ->
NewTimeRef = observer_cli_lib:next_redraw(TimeRef, Interval),
next_draw_view_2(Status, Type, NewTimeRef, Interval, Pid, NewRedQ, NewMemQ, ManagerPid).
next_draw_view_2(Status, Type, TimeRef, Interval, Pid, NewRedQ, NewMemQ, ManagerPid) ->
receive
quit ->
exit(stop);
{new_interval, NewInterval} ->
?output(?CLEAR),
render_worker(Status, Type, NewInterval, Pid, TimeRef, NewRedQ, NewMemQ, ManagerPid);
info_view ->
?output(?CLEAR),
render_worker(info, Type, Interval, Pid, TimeRef, NewRedQ, NewMemQ, ManagerPid);
message_view ->
?output(?CLEAR),
render_worker(message, Type, Interval, Pid, TimeRef, NewRedQ, NewMemQ, ManagerPid);
dict_view ->
?output(?CLEAR),
render_worker(dict, Type, Interval, Pid, TimeRef, NewRedQ, NewMemQ, ManagerPid);
stack_view ->
?output(?CLEAR),
render_worker(stack, Type, Interval, Pid, TimeRef, NewRedQ, NewMemQ, ManagerPid);
state_view ->
%% state view is static. user chose state view - no need for redraw
observer_cli_lib:flush_redraw_timer(TimeRef),
?output(?CLEAR),
render_worker(state, Type, Interval, Pid, TimeRef, NewRedQ, NewMemQ, ManagerPid);
redraw ->
?output(?CLEAR),
render_worker(Status, Type, Interval, Pid, TimeRef, NewRedQ, NewMemQ, ManagerPid)
end.
render_process_info(ProcessView) ->
Meta = process_meta_fields(ProcessView),
Memory = process_memory_fields(ProcessView),
GC = process_gc_fields(ProcessView),
Widths = process_info_widths([16, 42, 16, 12, 18, 12]),
[
render_process_info_title(Widths),
render_process_info_rows(Meta, Memory, GC, Widths)
].
process_meta_fields(#{
pid := Pid,
registered_name := RegisteredName,
group_leader := GroupLeader,
status := Status,
initial_call := InitialCall,
priority := Priority,
catchlevel := CatchLevel,
suspending := Suspending,
error_handler := ErrorHandler
}) ->
PidStr = erlang:pid_to_list(Pid),
Name =
case RegisteredName of
"" -> PidStr;
_ -> PidStr ++ "/" ++ erlang:atom_to_list(RegisteredName)
end,
#{
registered_name => Name,
initial_call => observer_cli_lib:mfa_to_list(InitialCall),
group_leader => erlang:pid_to_list(GroupLeader),
status => Status,
priority => Priority,
catchlevel => CatchLevel,
suspending => format_suspending(Suspending),
error_handler => ErrorHandler
}.
process_memory_fields(#{
message_queue_len := MessageQueueLen,
heap_size := HeapSize,
total_heap_size := TotalHeapSize,
stack_size := StackSize,
binary_refs := BinaryRefs,
trap_exit := TrapExit
}) ->
MessageQueueLenColor =
case MessageQueueLen > 0 of
true -> ?RED;
false -> ?GREEN
end,
#{
message_queue_len => {erlang:integer_to_list(MessageQueueLen), MessageQueueLenColor},
heap_size => HeapSize,
total_heap_size => TotalHeapSize,
stack_size => StackSize,
binary_refs => BinaryRefs,
trap_exit => TrapExit
}.
format_binary_refs({Count, Bytes}) ->
[erlang:integer_to_list(Count), $/, observer_cli_lib:to_byte(Bytes)].
format_suspending([]) ->
"0";
format_suspending(Suspending) ->
Items = [observer_cli_lib:to_list(P) || P <- lists:sublist(Suspending, 3)],
[erlang:integer_to_list(erlang:length(Suspending)), $:, string:join(Items, ",")].
process_gc_fields(#{garbage_collection := GarbageCollection}) ->
#{
min_bin_vheap_size => maps:get(min_bin_vheap_size, GarbageCollection),
min_heap_size => maps:get(min_heap_size, GarbageCollection),
fullsweep_after => maps:get(fullsweep_after, GarbageCollection),
minor_gcs => integer_to_list(maps:get(minor_gcs, GarbageCollection))
}.
render_process_info_title([MetaW, MetaValueW, MemoryW, MemoryValueW, GcW, GcValueW]) ->
?render([
?GRAY_BG,
?W("Meta", MetaW),
?W("Value", MetaValueW),
?W("Memory Used", MemoryW),
?W("Value", MemoryValueW),
?W("Garbage Collection", GcW),
?W("Value", GcValueW)
]).
render_process_info_rows(Meta, Memory, GC, [
MetaW, MetaValueW, MemoryW, MemoryValueW, GcW, GcValueW
]) ->
#{
registered_name := Name,
initial_call := InitialCallStr,
group_leader := GroupLeaderStr,
status := Status,
priority := Priority,
catchlevel := CatchLevel,
suspending := Suspending,
error_handler := ErrorHandler
} = Meta,
#{
message_queue_len := {MessageQueueLenStr, MessageQueueLenColor},
heap_size := HeapSize,
total_heap_size := TotalHeapSize,
stack_size := StackSize,
binary_refs := BinaryRefs,
trap_exit := TrapExit
} = Memory,
#{
min_bin_vheap_size := MinBinVHeapSize,
min_heap_size := MinHeapSize,
fullsweep_after := FullSweepAfter,
minor_gcs := MinorGcs
} = GC,
?render([
?W("registered_name", MetaW),
?W(Name, MetaValueW),
?W("msg_queue_len", MemoryW),
?W2(MessageQueueLenColor, MessageQueueLenStr, MemoryValueW + 1),
" ",
?W("min_bin_vheap_size", GcW),
?W({byte, MinBinVHeapSize}, GcValueW),
?NEW_LINE,
?W("initial_call", MetaW),
?W(InitialCallStr, MetaValueW),
?W("heap_size", MemoryW),
?W({byte, HeapSize}, MemoryValueW),
?W("min_heap_size", GcW),
?W({byte, MinHeapSize}, GcValueW),
?NEW_LINE,
?W("group_leader", MetaW),
?W(GroupLeaderStr, MetaValueW),
?W("total_heap_size", MemoryW),
?W({byte, TotalHeapSize}, MemoryValueW),
?W("fullsweep_after", GcW),
?W(FullSweepAfter, GcValueW),
?NEW_LINE,
?W("status", MetaW),
?W(Status, MetaValueW),
?W("trap_exit", MemoryW),
?W(TrapExit, MemoryValueW),
?W("minor_gcs", GcW),
?W(MinorGcs, GcValueW),
?NEW_LINE,
?W("priority", MetaW),
?W(Priority, MetaValueW),
?W("stack_size", MemoryW),
?W({byte, StackSize}, MemoryValueW),
?W("", GcW),
?W("", GcValueW),
?NEW_LINE,
?W("catchlevel", MetaW),
?W(CatchLevel, MetaValueW),
?W("binary_refs", MemoryW),
?W(format_binary_refs(BinaryRefs), MemoryValueW),
?W("", GcW),
?W("", GcValueW),
?NEW_LINE,
?W("suspending", MetaW),
?W(Suspending, MetaValueW),
?W("", MemoryW),
?W("", MemoryValueW),
?W("", GcW),
?W("", GcValueW),
?NEW_LINE,
?W("error_handler", MetaW),
?W(ErrorHandler, MetaValueW),
?W("", MemoryW),
?W("", MemoryValueW),
?W("", GcW),
?W("", GcValueW)
]).
render_link_monitor(Link, Monitors, MonitoredBy) ->
LinkStr = [
begin
observer_cli_lib:to_list(P)
end
|| P <- lists:sublist(Link, 30)
],
MonitorsStr = [
begin
case P of
{process, {RegName, Node}} ->
observer_cli_lib:to_list(RegName) ++ "/" ++ observer_cli_lib:to_list(Node);
{process, Pid} ->
observer_cli_lib:to_list(Pid);
{port, {RegName, Node}} ->
observer_cli_lib:to_list(RegName) ++ "/" ++ observer_cli_lib:to_list(Node);
{port, Port} ->
observer_cli_lib:to_list(Port)
end
end
|| P <- lists:sublist(Monitors, 30)
],
MonitoredByStr = [
begin
observer_cli_lib:to_list(P)
end
|| P <- lists:sublist(MonitoredBy, 30)
],
LinkInfo = "Links(" ++ erlang:integer_to_list(erlang:length(Link)) ++ ")",
MonitorInfo = "Monitors(" ++ erlang:integer_to_list(erlang:length(Monitors)) ++ ")",
MonitoredByInfo = "MonitoredBy(" ++ erlang:integer_to_list(erlang:length(MonitoredBy)) ++ ")",
LinkValueW = 112 + observer_cli_lib:layout_extra_width() + wide_fill(5),
?render([
?W(LinkInfo, 16),
?W(LinkStr, LinkValueW),
?NEW_LINE,
?W(MonitorInfo, 16),
?W(MonitorsStr, LinkValueW),
?NEW_LINE,
?W(MonitoredByInfo, 16),
?W(MonitoredByStr, LinkValueW)
]).
process_info_widths(Base) ->
fill_last(observer_cli_lib:weighted_widths(Base, [0, 4, 0, 1, 0, 1]), wide_fill(5)).
render_reduction_memory(Reduction, Memory, ReductionQ, MemoryQ) ->
{NewRed, NewMem} =
case queue:len(ReductionQ) >= 20 of
true ->
RestRed = queue:tail(ReductionQ),
RestMem = queue:tail(MemoryQ),
{queue:in(Reduction, RestRed), queue:in(Memory, RestMem)};
false ->
{queue:in(Reduction, ReductionQ), queue:in(Memory, MemoryQ)}
end,
Extra = observer_cli_lib:layout_extra_width(),
RedWidth = 120 + Extra + wide_fill(5),
MemWidth = 124 + Extra + wide_fill(5),
View = [
io_lib:format("|Reductions: ~*.*s|~n", [RedWidth, RedWidth, get_chart_format(NewRed)]),
io_lib:format("|Memory: ~*.*s|~n", [MemWidth, MemWidth, get_chart_format(NewMem)])
],
{NewRed, NewMem, View}.
wide_fill(Amount) ->
case observer_cli_lib:layout_extra_width() of
0 -> 0;
_ -> Amount
end.
fill_last([Last], Amount) ->
[Last + Amount];
fill_last([Width | Rest], Amount) ->
[Width | fill_last(Rest, Amount)].
render_footer() ->
observer_cli_lib:render_footer("q(quit)").
get_chart_format(Queue) ->
List = queue:to_list(Queue),
chart_format(List, "").
chart_format([_R], Lines) ->
Lines;
chart_format([R, R | RestRed], Lines) ->
chart_format([R | RestRed], Lines ++ observer_cli_lib:to_list(R) ++ "->");
chart_format([R1, R2 | RestRed], Lines) when R1 > R2 ->
chart_format([R2 | RestRed], Lines ++ observer_cli_lib:to_list(R1) ++ "->");
chart_format([R1, R2 | RestRed], Lines) when R1 < R2 ->
chart_format([R2 | RestRed], Lines ++ observer_cli_lib:to_list(R1) ++ "->").
render_menu(Type, Menu, Interval) ->
Text = "Interval: " ++ integer_to_list(Interval) ++ "ms",
Title = get_menu_title(Type, Menu),
UpTime = observer_cli_lib:uptime(),
TitleWidth =
observer_cli_lib:layout_base_width() + 99 - erlang:length(UpTime) +
observer_cli_lib:layout_extra_width(),
observer_cli_lib:render_menu_header(Title, Text, TitleWidth).
get_menu_title(Type, Menu) ->
MenuStr =
case Menu of
home -> "Home(H)";
plugin -> "Back(B)"
end,
observer_cli_lib:menu_items(Type, [
{back, MenuStr},
{info, "Process Info(P)"},
{message, "Messages(M)"},
{dict, "Dictionary(D)"},
{stack, "Current Stack(C)"},
{state, "State(S)"}
]).
parse_cmd() ->
parse_cmd_str(observer_cli_lib:read_cmd()).
parse_cmd_str(Key) ->
case Key of
Cmd when Cmd =:= "q\n"; Cmd =:= "Q\n" ->
quit;
"P\n" ->
info_view;
"M\n" ->
message_view;
"D\n" ->
dict_view;
"C\n" ->
stack_view;
"S\n" ->
state_view;
"H\n" ->
home;
"B\n" ->
back;
%% {error, estale}|{error, terminated}
{error, _Reason} ->
quit;
Number ->
observer_cli_command:parse_integer(Number)
end.
render_state(Pid, Type, Interval) ->
Menu = render_menu(state, Type, Interval),
PromptRes = io_lib:format("recon:get_state(~p, 2500). ~n", [Pid]),
PromptBefore = [
observer_cli_lib:ansi_green(
io_lib:format("Waiting recon:get_state(~p, 2500) return...", [Pid])
),
"\n"
],
LastLine = render_footer(),
?output([?CURSOR_TOP, Menu, PromptBefore]),
try
{ok, BoundedLine} = bounded_process_detail(state, Pid),
Nav = state_nav(Type),
Line = unicode:characters_to_list(BoundedLine),
Footer = state_footer(Menu, Nav),
Action = print_with_less(Line, Menu, Nav, Footer),
case Action of
quit ->
{ok, quit};
_ ->
?output([?CURSOR_TOP, Menu, PromptRes, "", LastLine]),
{ok, Action}
end
catch
Class:Reason:Stacktrace ->
log_render_state_error(Class, Reason, Stacktrace, Pid, Type, Interval),
Error =
"Information could not be retrieved, system messages may not be handled by this process.\n",
?output([?CURSOR_TOP, Menu, PromptRes, Error, LastLine]),
error
end.
log_render_state_error(Class, Reason, Stacktrace, Pid, Type, Interval) ->
error_logger:warning_msg(
"observer_cli render_state failed: class=~p reason=~p stacktrace=~p pid=~p type=~p interval=~p~n",
[Class, Reason, Stacktrace, Pid, Type, Interval]
).
output_die_view(Pid, Type, Interval) ->
Menu = render_menu(info, Type, Interval),
Line = [observer_cli_lib:ansi_red(io_lib:format("Process(~p) has already died.", [Pid])), "\n"],
LastLine = render_footer(),
?output([?CURSOR_TOP, Menu, Line, LastLine]).
print_with_less(Input, Menu, Nav, Footer) ->
observer_cli_lib:pipe({Input, Menu, Nav, Footer}, [
fun less_client:init/1,
fun less_client:main/1
]).
state_nav(Type) ->
Base = #{
"q\n" => quit,
"Q\n" => quit,
"H\n" => home,
"P\n" => info_view,
"M\n" => message_view,
"D\n" => dict_view,
"C\n" => stack_view
},
case Type of
plugin -> maps:put("B\n", back, Base);
home -> Base
end.
state_title(Pid) ->
io_lib:format("recon:get_state(~p, 2500).", [Pid]).
replace_first_line(Line, NewLine) ->
case string:split(Line, "\n", leading) of
[_First, Rest] -> NewLine ++ "\n" ++ Rest;
[_Only] -> NewLine ++ "\n"
end.
state_footer(_Menu, Nav) ->
observer_cli_lib:render_footer(state_footer_text(Nav)).
state_footer_text(_Nav) ->
"q(quit) F/B(page forward/back)".
truncate_str(Pid, Term) ->
case detail_term_within_limits(Term) of
true ->
State = #{
pid => Pid,
term => Term,
%% we need default mod, cause user can override conf
formatter_default => observer_cli_formatter_default,
formatter => undefined
},
observer_cli_lib:pipe(State, [
fun format_mod/1,
fun format/1
]);
false ->
detail_too_large(Pid)
end.
detail_term_within_limits(Term) ->
try
erlang:external_size(Term) =< ?DETAIL_MAX_TERM_BYTES andalso
detail_term_within_depth(Term, ?DETAIL_FORMAT_DEPTH)
catch
_:_ -> false
end.
detail_term_within_depth([], _Depth) ->
true;
detail_term_within_depth(Term, _Depth) when is_bitstring(Term) ->
true;
detail_term_within_depth(Term, _Depth) when
not is_list(Term),
not is_tuple(Term),
not is_map(Term)
->
true;
detail_term_within_depth(_Term, 0) ->
false;
detail_term_within_depth([Head | Tail], Depth) ->
detail_term_within_depth(Head, Depth - 1) andalso
detail_list_tail_within_depth(Tail, Depth);
detail_term_within_depth(Tuple, Depth) when is_tuple(Tuple) ->
detail_tuple_within_depth(Tuple, 1, Depth - 1);
detail_term_within_depth(Map, Depth) when is_map(Map) ->
detail_map_within_depth(maps:iterator(Map), Depth - 1).
detail_list_tail_within_depth([], _Depth) ->
true;
detail_list_tail_within_depth([Head | Tail], Depth) ->
detail_term_within_depth(Head, Depth - 1) andalso
detail_list_tail_within_depth(Tail, Depth);
detail_list_tail_within_depth(Tail, Depth) ->
detail_term_within_depth(Tail, Depth - 1).
detail_tuple_within_depth(Tuple, Index, _Depth) when Index > tuple_size(Tuple) ->
true;
detail_tuple_within_depth(Tuple, Index, Depth) ->
detail_term_within_depth(element(Index, Tuple), Depth) andalso
detail_tuple_within_depth(Tuple, Index + 1, Depth).
detail_map_within_depth(Iterator, Depth) ->
case maps:next(Iterator) of
{Key, Value, NextIterator} ->
detail_term_within_depth(Key, Depth) andalso
detail_term_within_depth(Value, Depth) andalso
detail_map_within_depth(NextIterator, Depth);
none ->
true
end.
format_mod(State) ->
#{formatter_default := FormatModDefault} = State,
observer_cli_lib:pipe(State, [
fun(StateAcc) ->
Formatter = application:get_env(observer_cli, formatter, ?DEFAULT_FORMATTER),
StateAcc#{formatter => Formatter}
end,
fun(StateAcc) ->
#{formatter := Formatter} = StateAcc,
StateAcc#{formatter => maps:get(mod, Formatter, FormatModDefault)}
end
]).
format(
State = #{formatter := FormatModDefault, formatter_default := FormatModDefault}
) ->
#{pid := Pid, term := Term} = State,
observer_cli_formatter:format(FormatModDefault, Pid, Term);
format(State) ->
#{
pid := Pid,
term := Term,
formatter := FormatMod,
formatter_default := FormatModDefault
} =
State,
try
observer_cli_formatter:format(FormatMod, Pid, Term)
catch
_:_ -> observer_cli_formatter:format(FormatModDefault, Pid, Term)
end.