diff --git a/.github/workflows/erlang.yml b/.github/workflows/erlang.yml index e1005ed..b5a945f 100644 --- a/.github/workflows/erlang.yml +++ b/.github/workflows/erlang.yml @@ -20,4 +20,6 @@ jobs: run: rebar3 compile - name: xref run: rebar3 xref + - name: dialyzer + run: rebar3 dialyzer diff --git a/README.md b/README.md index 55c0e71..d93faec 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ Visualize Erlang/Elixir Nodes On The Command Line base on [recon](https://github %% rebar.config {deps, [observer_cli]} %% erlang.mk -dep_observer_cli = hex 1.7.5 +dep_observer_cli = hex 1.8.0 ``` **Elixir** @@ -35,7 +35,7 @@ dep_observer_cli = hex 1.7.5 ```elixir # mix.exs def deps do - [{:observer_cli, "~> 1.7"}] + [{:observer_cli, "~> 1.8"}] end ``` @@ -89,7 +89,7 @@ _build/dev/rel/example/bin/example rpc ":observer_cli.start" ### DEMO -Home +Home ### How to write your own plugin? @@ -255,7 +255,12 @@ Support F/B to page up/down. --- ### Changelog - +- 1.8.0 + - Support ` 2.5.1"}, + {:recon, "~> 2.5.6"}, ] ] end diff --git a/mix.lock b/mix.lock index 7636f67..da4116d 100644 --- a/mix.lock +++ b/mix.lock @@ -1,3 +1,3 @@ %{ - "recon": {:hex, :recon, "2.5.1", "430ffa60685ac1efdfb1fe4c97b8767c92d0d92e6e7c3e8621559ba77598678a", [:mix, :rebar3], [], "hexpm"}, + "recon": {:hex, :recon, "2.5.6", "9052588e83bfedfd9b72e1034532aee2a5369d9d9343b61aeb7fbce761010741", [:mix, :rebar3], [], "hexpm", "96c6799792d735cc0f0fd0f86267e9d351e63339cbe03df9d162010cefc26bb0"}, } diff --git a/rebar.config b/rebar.config index 517203e..3f7fa9a 100644 --- a/rebar.config +++ b/rebar.config @@ -18,18 +18,12 @@ ]}. {deps_dir, "deps"}. {deps, [ - recon, - {eqwalizer_support, - {git_subdir, "https://github.com/whatsapp/eqwalizer.git", {branch, "main"}, - "eqwalizer_support"}} + recon ]}. {project_plugins, [ rebar3_format, - erlfmt, - {eqwalizer_rebar3, - {git_subdir, "https://github.com/whatsapp/eqwalizer.git", {branch, "main"}, - "eqwalizer_rebar3"}} + erlfmt ]}. {escript_main_app, observer_cli}. diff --git a/rebar.lock b/rebar.lock index 2621c54..91585f1 100644 --- a/rebar.lock +++ b/rebar.lock @@ -1,13 +1,8 @@ {"1.2.0", -[{<<"eqwalizer_support">>, - {git_subdir,"https://github.com/whatsapp/eqwalizer.git", - {ref,"0c099bbfa2fba1869d60e239c9b2cf2b6fb31ea9"}, - "eqwalizer_support"}, - 0}, - {<<"recon">>,{pkg,<<"recon">>,<<"2.5.5">>},0}]}. +[{<<"recon">>,{pkg,<<"recon">>,<<"2.5.6">>},0}]}. [ {pkg_hash,[ - {<<"recon">>, <<"C108A4C406FA301A529151A3BB53158CADC4064EC0C5F99B03DDB8C0E4281BDF">>}]}, + {<<"recon">>, <<"9052588E83BFEDFD9B72E1034532AEE2A5369D9D9343B61AEB7FBCE761010741">>}]}, {pkg_hash_ext,[ - {<<"recon">>, <<"632A6F447DF7CCC1A4A10BDCFCE71514412B16660FE59DECA0FCF0AA3C054404">>}]} + {<<"recon">>, <<"96C6799792D735CC0F0FD0F86267E9D351E63339CBE03DF9D162010CEFC26BB0">>}]} ]. diff --git a/src/observer_cli.app.src b/src/observer_cli.app.src index 406c247..fe17530 100644 --- a/src/observer_cli.app.src +++ b/src/observer_cli.app.src @@ -1,39 +1,39 @@ -{application, observer_cli, - [ - {description, "Visualize Erlang Nodes On The Command Line"}, - {vsn, "1.7.5"}, - {modules, [ - observer_cli - ]}, - {registered, []}, - {applications, [ - kernel, - stdlib, - recon - ]}, - {files, ["include", - "LICENSE*", - "mix.exs", - "mix.lock", - "README.md", - "rebar.config", - "rebar.lock", - "src"]}, - {build_tools, ["mix", "rebar3"]}, - {env, [ - {scheduler_usage, disable}, - {plugins, - [ - %% module - Specific module implements plugin behavior. It's mandatory. - %% title - Menu title. It's mandatory. - %% shortcut - Switch plugin by shortcut. It's mandatory. - %% interval - Refresh interval ms. It's options. default is 1500ms. - %% sort_column - Sort the sheet by this index. It's options default is 2. - - %% #{module => observer_cli_plug_1, title => "Example-1", interval => 1500, shortcut => "S", sort_column => 3}, - %% #{module => observer_cli_plug_2, title => "Example-2", interval => 1600, shortcut => "D", sort_column => 2} - ]} - ]}, - {licenses, ["MIT"]}, - {links, [{"Github", "https://github.com/zhongwencool/observer_cli"}]} - ]}. +{application, observer_cli, [ + {description, "Visualize Erlang Nodes On The Command Line"}, + {vsn, "1.8.0"}, + {modules, [ + observer_cli + ]}, + {registered, []}, + {applications, [ + kernel, + stdlib, + recon + ]}, + {files, [ + "include", + "LICENSE*", + "mix.exs", + "mix.lock", + "README.md", + "rebar.config", + "rebar.lock", + "src" + ]}, + {build_tools, ["mix", "rebar3"]}, + {env, [ + {scheduler_usage, disable}, + {plugins, [ + %% module - Specific module implements plugin behavior. It's mandatory. + %% title - Menu title. It's mandatory. + %% shortcut - Switch plugin by shortcut. It's mandatory. + %% interval - Refresh interval ms. It's options. default is 1500ms. + %% sort_column - Sort the sheet by this index. It's options default is 2. + + %% #{module => observer_cli_plug_1, title => "Example-1", interval => 1500, shortcut => "S", sort_column => 3}, + %% #{module => observer_cli_plug_2, title => "Example-2", interval => 1600, shortcut => "D", sort_column => 2} + ]} + ]}, + {licenses, ["MIT"]}, + {links, [{"Github", "https://github.com/zhongwencool/observer_cli"}]} +]}. diff --git a/src/observer_cli.erl b/src/observer_cli.erl index c1f3e40..c47d404 100644 --- a/src/observer_cli.erl +++ b/src/observer_cli.erl @@ -23,7 +23,8 @@ -spec start() -> no_return | {badrpc, term()}. start() -> start(#view_opts{}). --spec start(Node) -> no_return | {badrpc, term()} when Node :: atom() | non_neg_integer(). +-spec start(Node) -> no_return | {badrpc, term()} when + Node :: atom() | non_neg_integer() | #view_opts{}. start(Node) when Node =:= node() -> start(#view_opts{}); start(Node) when is_atom(Node) -> @@ -138,6 +139,9 @@ manager(StorePid, RenderPid, Opts, LastSchWallFlag) -> NewPages = observer_cli_lib:update_page_pos(StorePid, NewPage, Pages), clean(Resource), start(Opts#view_opts{home = Home#home{cur_page = NewPage, pages = NewPages}}); + {go_to_pid, Pid} -> + clean(Resource), + observer_cli_process:start(home, Pid, Opts); _ -> manager(StorePid, RenderPid, Opts, LastSchWallFlag) end. @@ -289,6 +293,7 @@ render_memory_process_line(MemSum, PortParallelism, Interval) -> AtomMem = proplists:get_value(atom_used, Mem), BinMem = proplists:get_value(binary, Mem), EtsMem = proplists:get_value(ets, Mem), + EtsLen = erlang:length(ets:all()), { BytesIn, BytesOut, @@ -349,7 +354,7 @@ render_memory_process_line(MemSum, PortParallelism, Interval) -> ?W("Gc Count", 20), ?W(GcCount, 24), ?NEW_LINE, - ?W("Ets", 10), + ?W("Ets/" ++ erlang:integer_to_list(EtsLen), 10), ?W({byte, EtsMem}, 12), ?W(EtsMemPercent, 6), ?W(LogKey, 25), @@ -777,6 +782,8 @@ choose_name(IsName) when is_atom(IsName) -> choose_name(_) -> undefined. +%% proc_lib:get_label/1 is not exported before OTP 27 +-dialyzer([{nowarn_function, [choose_lable/1]}]). choose_lable(Pid) -> case erlang:function_exported(proc_lib, get_label, 1) andalso diff --git a/src/observer_cli_application.erl b/src/observer_cli_application.erl index ff4d9a4..e03c79e 100644 --- a/src/observer_cli_application.erl +++ b/src/observer_cli_application.erl @@ -14,7 +14,7 @@ %% @doc List application info --spec start(ViewOpts) -> no_return when ViewOpts :: view_opts(). +-spec start(ViewOpts) -> quit when ViewOpts :: view_opts(). start(#view_opts{app = App, auto_row = AutoRow} = ViewOpts) -> Pid = spawn_link(fun() -> ?output(?CLEAR), diff --git a/src/observer_cli_help.erl b/src/observer_cli_help.erl index 6c218d6..b15f085 100644 --- a/src/observer_cli_help.erl +++ b/src/observer_cli_help.erl @@ -38,17 +38,21 @@ render_worker(Interval) -> Text = "Interval: " ++ integer_to_list(Interval) ++ "ms", Menu = observer_cli_lib:render_menu(doc, Text), ?output([?CURSOR_TOP, Menu]), + render_doc(Text), erlang:send_after(Interval, self(), redraw), receive redraw -> render_worker(Interval); quit -> - MenuQ = observer_cli_lib:render_menu(doc, Text), - HelpQ = render_help(), - LastLine = observer_cli_lib:render_last_line("q(quit)"), - ?output([?CURSOR_TOP, MenuQ, HelpQ, ?UNDERLINE, ?GRAY_BG, LastLine, ?RESET_BG, ?RESET]) + ok end. +render_doc(Text) -> + MenuQ = observer_cli_lib:render_menu(doc, Text), + HelpQ = render_help(), + LastLine = observer_cli_lib:render_last_line("q(quit)"), + ?output([?CURSOR_TOP, MenuQ, HelpQ, ?UNDERLINE, ?GRAY_BG, LastLine, ?RESET_BG, ?RESET]). + render_help() -> [ "|\e[44m1. Start Mode\e[49m \n", @@ -57,22 +61,24 @@ render_help() -> "| \e[48;2;80;80;80m1.3\e[0m observer_start:start(Node, Cookie).\n", "|\e[44m2. HOME(H) Commands\e[49m \n", - "| \e[48;2;80;80;80m` \e[0m enable/disable schedule usage.\n", - "| \e[48;2;80;80;80mPageDown \e[0m pd or F(forward).\n", - "| \e[48;2;80;80;80mPageUp \e[0m pu or B(back).\n", - "| \e[48;2;80;80;80mr \e[0m switch mode to reduction(proc_count).\n", - "| \e[48;2;80;80;80mrr \e[0m switch mode to reduction(proc_window).\n", - "| \e[48;2;80;80;80mm \e[0m switch mode to memory(proc_count).\n", - "| \e[48;2;80;80;80mmm \e[0m switch mode to memory(proc_window).\n", - "| \e[48;2;80;80;80mb \e[0m switch mode to bin memory(proc_count).\n", - "| \e[48;2;80;80;80mbb \e[0m switch mode to bin memory(proc_window).\n", - "| \e[48;2;80;80;80mmq \e[0m switch mode to message queue len(proc_count).\n", - "| \e[48;2;80;80;80mmmq \e[0m switch mode to message queue len(proc_window).\n", - "| \e[48;2;80;80;80mt \e[0m switch mode to total heap size(proc_count).\n", - "| \e[48;2;80;80;80mtt \e[0m switch mode to total heap size(proc_window).\n", - "| \e[48;2;80;80;80m3000 \e[0m set interval time to 3000ms, the integer must >= 1500.\n", - "| \e[48;2;80;80;80m13 \e[0m choose the 13th process(green line), the integer must in top list.\n", - "| \e[48;2;80;80;80mp \e[0m pause/unpause the view.\n", + "| \e[48;2;80;80;80m` \e[0m enable/disable schedule usage.\n", + "| \e[48;2;80;80;80mPageDown \e[0m pd or F(forward).\n", + "| \e[48;2;80;80;80mPageUp \e[0m pu or B(back).\n", + "| \e[48;2;80;80;80mr \e[0m switch mode to reduction(proc_count).\n", + "| \e[48;2;80;80;80mrr \e[0m switch mode to reduction(proc_window).\n", + "| \e[48;2;80;80;80mm \e[0m switch mode to memory(proc_count).\n", + "| \e[48;2;80;80;80mmm \e[0m switch mode to memory(proc_window).\n", + "| \e[48;2;80;80;80mb \e[0m switch mode to bin memory(proc_count).\n", + "| \e[48;2;80;80;80mbb \e[0m switch mode to bin memory(proc_window).\n", + "| \e[48;2;80;80;80mmq \e[0m switch mode to message queue len(proc_count).\n", + "| \e[48;2;80;80;80mmmq \e[0m switch mode to message queue len(proc_window).\n", + "| \e[48;2;80;80;80mt \e[0m switch mode to total heap size(proc_count).\n", + "| \e[48;2;80;80;80mtt \e[0m switch mode to total heap size(proc_window).\n", + "| \e[48;2;80;80;80m3000 \e[0m set interval time to 3000ms, the integer must >= 1500.\n", + "| \e[48;2;80;80;80m13 \e[0m choose the 13th process(green line), the integer must in top list.\n", + "| \e[48;2;80;80;80m<0.43.0> \e[0m choose the <0.43.0> process, the pid does not need to be in the top list.\n", + "| \e[48;2;80;80;80m<431 or >431 \e[0m choose the <0.431.0> process, the pid does not need to be in the top list.\n", + "| \e[48;2;80;80;80mp \e[0m pause/unpause the view.\n", "|\e[44m5. Reference\e[49m \n", "|More information about recon:proc_count/2 and recon:proc_window/3 \n", diff --git a/src/observer_cli_lib.erl b/src/observer_cli_lib.erl index bbdc963..bb13157 100644 --- a/src/observer_cli_lib.erl +++ b/src/observer_cli_lib.erl @@ -35,13 +35,11 @@ uptime() -> [?W(?GREEN, Time, 16)]. %% @doc 0.982342 -> 98.23%, 1 -> 100.0% --spec to_percent(float()) -> string(). to_percent(Float) when Float < 0.1 -> [$0, erlang:float_to_list(Float * 100, [{decimals, 2}]), $%]; to_percent(Float) when Float < 1 -> [erlang:float_to_list(Float * 100, [{decimals, 2}]), $%]; to_percent(undefined) -> "******"; to_percent(_) -> "100.0%". --spec to_list(term()) -> list(). to_list(Atom) when is_atom(Atom) -> atom_to_list(Atom); to_list(Integer) when is_integer(Integer) -> integer_to_list(Integer); to_list(Pid) when is_pid(Pid) -> erlang:pid_to_list(Pid); @@ -251,6 +249,10 @@ parse_cmd(ViewOpts, Module, Args) -> hide; "`\n" -> scheduler_usage; + [$< | PidStr] -> + to_pid(PidStr); + [$> | PidStr] -> + to_pid(PidStr); %% {error, estale}|{error, terminated} {error, _Reason} -> quit; @@ -258,7 +260,18 @@ parse_cmd(ViewOpts, Module, Args) -> parse_integer(Number) end. --spec next_redraw(reference(), pos_integer()) -> reference(). +-spec to_pid(string()) -> {go_to_pid, pid()} | quit. +to_pid(Str) -> + case string:tokens(Str, ".<>\n") of + [X, Y, Z] -> + {go_to_pid, list_to_pid("<" ++ X ++ "." ++ Y ++ "." ++ Z ++ ">")}; + [Y] -> + {go_to_pid, list_to_pid("<0." ++ Y ++ ".0>")}; + _ -> + quit + end. + +-spec next_redraw(reference() | ?INIT_TIME_REF, pos_integer()) -> reference(). next_redraw(LastTimeRef, Interval) -> LastTimeRef =/= ?INIT_TIME_REF andalso erlang:cancel_timer(LastTimeRef), erlang:send_after(Interval, self(), redraw). diff --git a/src/observer_cli_mnesia.erl b/src/observer_cli_mnesia.erl index 97987e3..1d232fd 100644 --- a/src/observer_cli_mnesia.erl +++ b/src/observer_cli_mnesia.erl @@ -187,6 +187,7 @@ get_table_list(HideSys, Attr) -> _ -> get_table_list2(Owner, HideSys, Attr) end. +-dialyzer([{nowarn_function, [get_table_list2/3]}]). get_table_list2(Owner, HideSys, Attr) -> {registered_name, RegName} = process_info(Owner, registered_name), WordSize = erlang:system_info(wordsize), @@ -213,6 +214,8 @@ get_table_list2(Owner, HideSys, Attr) -> end, lists:foldl(CollectFun, [], mnesia:system_info(tables)). +-dialyzer([{nowarn_function, [with_storage_type/2]}]). + with_storage_type(Id, Tab0) -> Storage = mnesia:table_info(Id, storage_type), with_storage_type(Id, Storage, Tab0). diff --git a/src/observer_cli_plugin.erl b/src/observer_cli_plugin.erl index defd9eb..21f903b 100644 --- a/src/observer_cli_plugin.erl +++ b/src/observer_cli_plugin.erl @@ -139,9 +139,7 @@ manager(ChildPid, SheetCache, ViewOpts) -> start(ViewOpts#view_opts{plug = PlugOpts#plug{plugs = NewPlugs}}); {error, _} -> manager(ChildPid, SheetCache, ViewOpts) - end; - _ -> - manager(ChildPid, SheetCache, ViewOpts) + end end. update_plugins(CurIndex, Lists, UpdateItems) -> diff --git a/src/observer_cli_process.erl b/src/observer_cli_process.erl index dc0c0d7..3625bc6 100644 --- a/src/observer_cli_process.erl +++ b/src/observer_cli_process.erl @@ -2,6 +2,12 @@ -include("observer_cli.hrl"). +-dialyzer([ + {nowarn_function, [ + render_worker/7, render_reduction_memory/4, get_chart_format/1, chart_format/2 + ]} +]). + -export([start/3]). %% lists:foldl(fun(_X, Acc) -> queue:in('NaN', Acc) end, queue:new(), lists:seq(1, 5)) diff --git a/src/observer_cli_system.erl b/src/observer_cli_system.erl index b47bc71..c80babb 100644 --- a/src/observer_cli_system.erl +++ b/src/observer_cli_system.erl @@ -2,6 +2,7 @@ -module(observer_cli_system). -include("observer_cli.hrl"). +-include_lib("kernel/include/net_address.hrl"). %% API -export([start/1]). @@ -66,9 +67,11 @@ render_worker(Cmd, Interval, LastTimeRef) -> SbcsToMbcsCurs, SbcsToMbcsMaxs ), + DistNodesInfo = get_dist_nodes_info(), + DistNodeView = render_dist_node_info(DistNodesInfo), HitView = render_cache_hit_rates(CacheHitInfo, erlang:length(CacheHitInfo)), LastLine = observer_cli_lib:render_last_line("q(quit)"), - ?output([?CURSOR_TOP, Menu, Sys, BlockView, HitView, LastLine]), + ?output([?CURSOR_TOP, Menu, Sys, BlockView, DistNodeView, HitView, LastLine]), NextTimeRef = observer_cli_lib:next_redraw(LastTimeRef, Interval), receive quit -> quit; @@ -76,6 +79,92 @@ render_worker(Cmd, Interval, LastTimeRef) -> redraw -> render_worker(Cmd, Interval, NextTimeRef) end. +get_dist_nodes_info() -> + case ets:info(sys_dist, size) of + undefined -> + []; + 0 -> + []; + _ -> + {ok, DistNodesInfo} = net_kernel:nodes_info(), + DistNodesInfo + end. + +render_dist_node_info([]) -> + []; +render_dist_node_info(DistNodesInfo) -> + Title = ?render([ + ?UNDERLINE, + ?GRAY_BG, + ?W("Node", 30), + ?W("Dist Node Queue Size Bytes", 20), + ?W("Percent", 7), + ?W("Address", 19), + ?W("In", 11), + ?W("Out", 11), + ?W("Type", 7), + ?W("State", 10) + ]), + Limit = erlang:system_info(dist_buf_busy_limit), + LimitStr = integer_to_list(Limit), + View = lists:map( + fun({Node, Info}) -> + State = proplists:get_value(state, Info), + Type = proplists:get_value(type, Info), + Address = get_address(Info), + In = proplists:get_value(in, Info), + Out = proplists:get_value(out, Info), + QueueSize = get_dist_queue_size(Node), + QueueSizeStr = observer_cli_lib:to_list(QueueSize), + QueueSizeLimitStr = QueueSizeStr ++ "/" ++ LimitStr, + Percent = + case is_integer(QueueSize) of + true -> + Float = QueueSize / Limit, + [erlang:float_to_list(Float * 100, [{decimals, 2}]), $%]; + false -> + "unsupported" + end, + ?render([ + ?W(Node, 30), + ?W(QueueSizeLimitStr, 20), + ?W(Percent, 7), + ?W(Address, 19), + ?W(In, 11), + ?W(Out, 11), + ?W(Type, 7), + ?W(State, 10) + ]) + end, + lists:sort(DistNodesInfo) + ), + [Title | View]. + +get_address(Info) -> + #net_address{address = Address} = proplists:get_value(address, Info, #net_address{}), + case Address of + {Addr, Port} -> + case inet:ntoa(Addr) of + {error, _} -> + AddrStr = lists:flatten(io_lib:format("~p", [Addr])), + AddrStr ++ ":" ++ integer_to_list(Port); + AddrStr -> + AddrStr ++ ":" ++ integer_to_list(Port) + end; + _ -> + "unknown" + end. + +get_dist_queue_size(Node) -> + case ets:lookup(sys_dist, Node) of + [] -> + not_found; + [Dist] -> + ConnId = element(3, Dist), + {ok, _, _, Size} = erlang:dist_get_stat(ConnId), + Size + end. + render_cache_hit_rates(CacheHitInfo, Len) when Len =< 8 -> Title = ?render([ ?UNDERLINE,