diff options
author | HÃ¥kan Mattsson <[email protected]> | 2010-02-03 08:59:06 +0000 |
---|---|---|
committer | Erlang/OTP <[email protected]> | 2010-02-03 08:59:06 +0000 |
commit | 43f3482adf5eee657e5ba922733dfff6600c4e14 (patch) | |
tree | 7ea7b32a171de1a7690102c403a8a946e8a382a8 /lib/et/src/et_gs_contents_viewer.erl | |
parent | 768da5a5f6312496b9b8a09cca5ea1d6b89a2c1c (diff) | |
download | otp-43f3482adf5eee657e5ba922733dfff6600c4e14.tar.gz otp-43f3482adf5eee657e5ba922733dfff6600c4e14.tar.bz2 otp-43f3482adf5eee657e5ba922733dfff6600c4e14.zip |
OTP-8058 The GUI parts are rewritten to use wxWidgets. Thanks Olle
Mattsson!
For the time being it is still possible to use the old GS based
version of the tool, but it is deprecated. The wxWidgets based
version is started by default.
A new tutorial has been added to the documentation. It is based
on Jayson Vantuyl's article
http://souja.net/2009/04/making-sense-of-erlangs-event-tracer.htm
l.
The functions et:trace_me/4 and et:trace_me/5 has been introduced
in order to replace the deprecated functions et:report_event/4
and et:report_event/5. Hopefully the new names makes it a little
more obvious what the intended usage of the functions are.
A print function has been added to the GUI, in order to enable
printing of sequence charts.
More functionality for hiding unwanted events has been added to
the GUI.
The max_events, hide_unknown and display_mode configuration
parameters to et_viewer is not used any more. Now the event cache
in the Viewer only contains those events that actually are
displayed in the GUI.
Diffstat (limited to 'lib/et/src/et_gs_contents_viewer.erl')
-rw-r--r-- | lib/et/src/et_gs_contents_viewer.erl | 591 |
1 files changed, 591 insertions, 0 deletions
diff --git a/lib/et/src/et_gs_contents_viewer.erl b/lib/et/src/et_gs_contents_viewer.erl new file mode 100644 index 0000000000..f6a87bd608 --- /dev/null +++ b/lib/et/src/et_gs_contents_viewer.erl @@ -0,0 +1,591 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2000-2010. All Rights Reserved. +%% +%% The contents of this file are subject to the Erlang Public License, +%% Version 1.1, (the "License"); you may not use this file except in +%% compliance with the License. You should have received a copy of the +%% Erlang Public License along with this software. If not, it can be +%% retrieved online at http://www.erlang.org/. +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and limitations +%% under the License. +%% +%% %CopyrightEnd% +%% +%%---------------------------------------------------------------------- +%% Purpose: Displays details of a trace event +%%---------------------------------------------------------------------- + +-module(et_gs_contents_viewer). + +-behaviour(gen_server). + +%% External exports +-export([start_link/1, + stop/1]). + +%% gen_server callbacks +-export([init/1, terminate/2, code_change/3, + handle_call/3, handle_cast/2, handle_info/2]). + +-include("../include/et.hrl"). +-include("et_internal.hrl"). + +-record(state, {parent_pid, % Pid of parent process + viewer_pid, % Pid of viewer process + event_order, % Field to be used as primary key + event, % The original event + filtered_event, % Event processed by active filter + active_filter, % Name of the active filter + filters, % List of possible filters + win, % GUI: Window object + packer, % GUI: Packer object + width, % GUI: Window width + height}). % GUI: Window height + +%%%---------------------------------------------------------------------- +%%% Client side +%%%---------------------------------------------------------------------- + +%%---------------------------------------------------------------------- +%% start_link(Options) -> {ok, ContentsPid} | {error, Reason} +%% +%% Start an viewer for the event contents as window in GS +%% +%% Options = [option()] +%% +%% option() = +%% +%% {parent_pid, pid()} | % Pid of parent process +%% {viewer_pid, pid()} | % Pid of viewer process +%% {event_order, event_order()} | % Field to be used as primary key +%% {active_filter, atom()} | % Name of the active filter +%% {filter, atom(), fun()} % A named filter fun +%% +%% event_order() = 'trace_ts' | 'event_ts' +%% ContentsPid = pid() +%% Reason = term() +%%---------------------------------------------------------------------- + +start_link(Options) -> + case parse_opt(Options, default_state()) of + {ok, S} -> + case gen_server:start_link(?MODULE, [S], []) of + {ok, ContentsPid} when S#state.parent_pid =/= self() -> + unlink(ContentsPid), + {ok, ContentsPid}; + Other -> + Other + end; + {error, Reason} -> + {error, Reason} + end. + +default_state() -> + #state{parent_pid = self(), + viewer_pid = undefined, + active_filter = ?DEFAULT_FILTER_NAME, + filters = [?DEFAULT_FILTER], + width = 600, + height = 300}. + +parse_opt([], S) -> + Name = S#state.active_filter, + Filters = S#state.filters, + if + S#state.event =:= undefined -> + {error, {badarg, no_event}}; + is_atom(Name) -> + case lists:keysearch(Name, #filter.name, Filters) of + {value, F} when is_record(F, filter) -> + {ok, S#state{active_filter = Name}}; + false -> + {error, {badarg, {no_such_filter, Name, Filters}}} + end + end; +parse_opt([H | T], S) -> + case H of + {parent_pid, ParentPid} when is_pid(ParentPid) -> + parse_opt(T, S#state{parent_pid = ParentPid}); + {viewer_pid, ViewerPid} when is_pid(ViewerPid) -> + parse_opt(T, S#state{viewer_pid = ViewerPid}); + {event_order, trace_ts} -> + parse_opt(T, S#state{event_order = trace_ts}); + {event_order, event_ts} -> + parse_opt(T, S#state{event_order = event_ts}); + {event, Event} when is_record(Event, event) -> + parse_opt(T, S#state{event = Event}); + {active_filter, Name} when is_atom(Name) -> + parse_opt(T, S#state{active_filter = Name}); + F when is_record(F, filter), + is_atom(F#filter.name), + is_function(F#filter.function) -> + Filters = lists:keydelete(F#filter.name, #filter.name, S#state.filters), + Filters2 = lists:keysort(#filter.name, [F | Filters]), + parse_opt(T, S#state{filters = Filters2}); + {width, Width} when is_integer(Width), Width > 0 -> + parse_opt(T, S#state{width = Width}); + {height, Height} when is_integer(Height), Height > 0 -> + parse_opt(T, S#state{height = Height}); + Bad -> + {error, {bad_option, Bad}} + end; +parse_opt(BadList, _S) -> + {error, {bad_option_list, BadList}}. + +%%---------------------------------------------------------------------- +%% stop(ContentsPid) -> ok +%% +%% Stops a contents viewer process +%% +%% ContentsPid = pid() +%%---------------------------------------------------------------------- + +stop(ContentsPid) -> + unlink(ContentsPid), + call(ContentsPid, stop). + +call(ContentsPid, Request) -> + gen_server:call(ContentsPid, Request, infinity). + +%%%---------------------------------------------------------------------- +%%% Callback functions from gen_server +%%%---------------------------------------------------------------------- + +%%---------------------------------------------------------------------- +%% Func: init/1 +%% Returns: {ok, State} | +%% {ok, State, Timeout} | +%% ignore | +%% {stop, Reason} +%%---------------------------------------------------------------------- + +init([S]) when is_record(S, state) -> + process_flag(trap_exit, true), + S2 = create_window(S), + {ok, S2}. + +%%---------------------------------------------------------------------- +%% Func: handle_call/3 +%% Returns: {reply, Reply, State} | +%% {reply, Reply, State, Timeout} | +%% {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, Reply, State} | (terminate/2 is called) +%% {stop, Reason, State} (terminate/2 is called) +%%---------------------------------------------------------------------- + +handle_call(stop, _From, S) -> + unlink(S#state.parent_pid), + {stop, shutdown, ok, S}; +handle_call(Request, From, S) -> + ok = error_logger:format("~p(~p): handle_call(~p, ~p, ~p)~n", + [?MODULE, self(), Request, From, S]), + Reply = {error, {bad_request, Request}}, + {reply, Reply, S}. + +%%---------------------------------------------------------------------- +%% Func: handle_cast/2 +%% Returns: {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, State} (terminate/2 is called) +%%---------------------------------------------------------------------- + +handle_cast(Msg, S) -> + ok = error_logger:format("~p(~p): handle_cast(~p, ~p)~n", + [?MODULE, self(), Msg, S]), + {noreply, S}. + +%%---------------------------------------------------------------------- +%% Func: handle_info/2 +%% Returns: {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, State} (terminate/2 is called) +%%---------------------------------------------------------------------- + +handle_info({gs, Button, click, Data, _Other}, S) -> + case Button of + close -> + gs:destroy(S#state.win), + {stop, normal, S}; + save -> + Event = S#state.event, + Bin = list_to_binary(event_to_string(Event, S#state.event_order)), + TimeStamp = + case S#state.event_order of + trace_ts -> Event#event.trace_ts; + event_ts -> Event#event.event_ts + end, + FileName = ["et_contents_viewer_", now_to_string(TimeStamp), ".save"], + file:write_file(lists:flatten(FileName), Bin), + {noreply, S}; + _PopupMenuItem when is_record(Data, filter) -> + F = Data, + ChildState= S#state{active_filter = F#filter.name}, + case gen_server:start_link(?MODULE, [ChildState], []) of + {ok, Pid} when S#state.parent_pid =/= self() -> + unlink(Pid), + {noreply, S}; + _ -> + {noreply, S} + end; + {hide, Actors} -> + send_viewer_event(S, {delete_actors, Actors}), + {noreply, S}; + {show, Actors} -> + send_viewer_event(S, {insert_actors, Actors}), + {noreply, S}; + {mode, Mode} -> + send_viewer_event(S, {mode, Mode}), + {noreply, S}; + Nyi -> + ok = error_logger:format("~p: click ~p ignored (nyi)~n", + [?MODULE, Nyi]), + {noreply, S} + end; +handle_info({gs, _Obj, destroy,_, _}, S) -> + unlink(S#state.parent_pid), + gs:destroy(S#state.win), + {stop, normal, S}; +handle_info({gs, _Obj, keypress, _, [KeySym, _Keycode, _Shift, _Control | _]}, S) -> + case KeySym of + 'c' -> + gs:destroy(S#state.win), + {stop, normal, S}; + + 'f' -> + E = S#state.filtered_event, + From = E#event.from, + send_viewer_event(S, {delete_actors, [From]}), + {noreply, S}; + 't' -> + E = S#state.filtered_event, + To = E#event.to, + send_viewer_event(S, {delete_actors, [To]}), + {noreply, S}; + 'b' -> + E = S#state.filtered_event, + From = E#event.from, + To = E#event.to, + send_viewer_event(S, {delete_actors, [From, To]}), + {noreply, S}; + + 'F' -> + E = S#state.filtered_event, + From = E#event.from, + send_viewer_event(S, {insert_actors, [From]}), + {noreply, S}; + 'T' -> + E = S#state.filtered_event, + To = E#event.to, + send_viewer_event(S, {insert_actors, [To]}), + {noreply, S}; + 'B' -> + E = S#state.filtered_event, + From = E#event.from, + To = E#event.to, + send_viewer_event(S, {insert_actors, [From, To]}), + {noreply, S}; + + 's' -> + E = S#state.filtered_event, + From = E#event.from, + To = E#event.to, + First = et_collector:make_key(S#state.event_order, E), + Mode = {search_actors, forward, First, [From, To]}, + send_viewer_event(S, {mode, Mode}), + {noreply, S}; + 'r' -> + E = S#state.filtered_event, + From = E#event.from, + To = E#event.to, + First = et_collector:make_key(S#state.event_order, E), + Mode = {search_actors, reverse, First, [From, To]}, + send_viewer_event(S, {mode, Mode}), + {noreply, S}; + 'a' -> + send_viewer_event(S, {mode, all}), + {noreply, S}; + + 0 -> + case lists:keysearch(?DEFAULT_FILTER_NAME, #filter.name, S#state.filters) of + {value, F} when is_record(F, filter) -> + ChildState= S#state{active_filter = F#filter.name}, + case gen_server:start_link(?MODULE, [ChildState], []) of + {ok, Pid} when S#state.parent_pid =/= self() -> + unlink(Pid); + _ -> + ignore + end; + false -> + ignore + end, + {noreply, S}; + Int when is_integer(Int), Int > 0, Int =< 9 -> + case catch lists:nth(Int, S#state.filters) of + F when is_record(F, filter) -> + ChildState= S#state{active_filter = F#filter.name}, + case gen_server:start_link(?MODULE, [ChildState], []) of + {ok, Pid} when S#state.parent_pid =/= self() -> + unlink(Pid); + _ -> + ignore + end; + {'EXIT', _} -> + ignore + end, + {noreply, S}; + + 'Shift_L' -> + {noreply, S}; + 'Shift_R' -> + {noreply, S}; + 'Caps_Lock' -> + {noreply, S}; + _ -> + io:format("~p: ignored: ~p~n", [?MODULE, KeySym]), + {noreply, S} + end; +handle_info({gs, _Obj, configure, [], [W, H | _]}, S) -> + gs:config(S#state.packer, [{width, W},{height, H}]), + S2 = S#state{width = W, height = H}, + {noreply, S2}; +handle_info({'EXIT', Pid, Reason}, S) -> + if + Pid =:= S#state.parent_pid -> + unlink(Pid), + {stop, Reason, S}; + true -> + {noreply, S} + end; +handle_info(Info, S) -> + ok = error_logger:format("~p(~p): handle_info(~p, ~p)~n", + [?MODULE, self(), Info, S]), + {noreply, S}. + +%%---------------------------------------------------------------------- +%% Func: terminate/2 +%% Purpose: Shutdown the server +%% Returns: any (ignored by gen_server) +%%---------------------------------------------------------------------- + +terminate(_Reason, _S) -> + ignore. + +%%---------------------------------------------------------------------- +%% Func: code_change/3 +%% Purpose: Convert process state when code is changed +%% Returns: {ok, NewState} +%%---------------------------------------------------------------------- + +code_change(_OldVsn, S, _Extra) -> + {ok, S}. + +%%%---------------------------------------------------------------------- +%%% Handle graphics +%%%---------------------------------------------------------------------- + +create_window(S) -> + H = S#state.height, + W = S#state.width, + Name = S#state.active_filter, + Title = lists:concat([?MODULE, " (filter: ", Name, ")"]), + WinOpt = [{title, Title}, {configure, true}, + {width, W}, {height, H}], + GS = gs:start(), + Win = gs:window(GS, WinOpt), + Bar = gs:menubar(Win, []), + create_file_menu(Bar), + PackerOpt = [{packer_x, [{stretch, 1}]}, + {packer_y, [{stretch, 1}, {fixed, 25}]}, + {x, 0}, {y, 25}], + Packer = gs:frame(Win, PackerOpt), + EditorOpt = [{pack_xy, {1, 1}}, {vscroll, right}, {hscroll, bottom}, + {wrap, none}, + {bg, lightblue}, {font, {courier, 12}}], + Editor = gs:editor(Packer, EditorOpt), + FilteredEvent = config_editor(Editor, S), + S2 = S#state{win = Win, packer = Packer, filtered_event = FilteredEvent}, + create_hide_menu(Bar, S2), + create_search_menu(Bar, S2), + create_filter_menu(Bar, S#state.filters), + gs:config(Packer, [{width, W}, {height, H}]), + gs:config(Win, [{map,true}, {keypress, true}]), + S2. + +create_file_menu(Bar) -> + Button = gs:menubutton(Bar, [{label, {text, "File"}}]), + Menu = gs:menu(Button, []), + gs:menuitem(close, Menu, [{label, {text,"Close (c)"}}]), + gs:menuitem(save, Menu, [{label, {text,"Save"}}]). + +create_filter_menu(Bar, Filters) -> + Button = gs:menubutton(Bar, [{label, {text, "Filters"}}]), + Menu = gs:menu(Button, []), + gs:menuitem(Menu, [{label, {text, "Select Filter"}}, {bg, lightblue}, {enable, false}]), + gs:menuitem(Menu, [{itemtype, separator}]), + Item = fun(F, N) when F#filter.name =:= ?DEFAULT_FILTER_NAME-> + Label = lists:concat([pad_string(F#filter.name, 20), "(0)"]), + gs:menuitem(Menu, [{label, {text, Label}}, {data, F}]), + N + 1; + (F, N) -> + Name = F#filter.name, + Label = lists:concat([pad_string(Name, 20), "(", N, ")"]), + gs:menuitem(Menu, [{label, {text, Label}}, {data, F}]), + N + 1 + end, + Filters2 = lists:keysort(#filter.name, Filters), + lists:foldl(Item, 1, Filters2), + Menu. + +create_hide_menu(Bar, S) -> + Button = gs:menubutton(Bar, [{label, {text, "Hide"}}]), + Menu = gs:menu(Button, []), + E = S#state.filtered_event, + From = E#event.from, + To = E#event.to, + if + S#state.viewer_pid =:= undefined -> + ignore; + From =:= To -> + gs:menuitem(Menu, [{label, {text, "Hide actor in Viewer "}}, {bg, lightblue}, {enable, false}]), + gs:menuitem(Menu, [{itemtype, separator}]), + gs:menuitem({hide, [From]}, Menu, [{label, {text,"From=To (f|t|b)"}}]), + gs:menuitem(Menu, [{itemtype, separator}]), + gs:menuitem(Menu, [{label, {text, "Show actor in Viewer "}}, {bg, lightblue}, {enable, false}]), + gs:menuitem(Menu, [{itemtype, separator}]), + gs:menuitem({show, [From]}, Menu, [{label, {text,"From=To (F|T|B)"}}]); + true -> + gs:menuitem(Menu, [{label, {text, "Hide actor in Viewer "}}, {bg, lightblue}, {enable, false}]), + gs:menuitem(Menu, [{itemtype, separator}]), + gs:menuitem({hide, [From]}, Menu, [{label, {text,"From (f)"}}]), + gs:menuitem({hide, [To]}, Menu, [{label, {text,"To (t)"}}]), + gs:menuitem({hide, [From, To]}, Menu, [{label, {text,"Both (b)"}}]), + gs:menuitem(Menu, [{itemtype, separator}]), + gs:menuitem(Menu, [{label, {text, "Show actor in Viewer "}}, {bg, lightblue}, {enable, false}]), + gs:menuitem(Menu, [{itemtype, separator}]), + gs:menuitem({show, [From]}, Menu, [{label, {text,"From (F)"}}]), + gs:menuitem({show, [To]}, Menu, [{label, {text,"To (T)"}}]), + gs:menuitem({show, [From, To]}, Menu, [{label, {text,"Both (B)"}}]) + end. + +create_search_menu(Bar, S) -> + Button = gs:menubutton(Bar, [{label, {text, "Search"}}]), + Menu = gs:menu(Button, []), + E = S#state.filtered_event, + From = E#event.from, + To = E#event.to, + gs:menuitem(Menu, [{label, {text, "Search in Viewer "}}, + {bg, lightblue}, {enable, false}]), + gs:menuitem(Menu, [{itemtype, separator}]), + if + S#state.viewer_pid =:= undefined -> + S; + From =:= To -> + Key = et_collector:make_key(S#state.event_order, E), + ModeS = {search_actors, forward, Key, [From]}, + ModeR = {search_actors, reverse, Key, [From]}, + gs:menuitem({mode, ModeS}, Menu, [{label, {text,"Forward from this event (s)"}}]), + gs:menuitem({mode, ModeR}, Menu, [{label, {text,"Reverse from this event (r)"}}]); + true -> + Key = et_collector:make_key(S#state.event_order, E), + ModeS = {search_actors, forward, Key, [From, To]}, + ModeR = {search_actors, reverse, Key, [From, To]}, + gs:menuitem({mode, ModeS}, Menu, [{label, {text,"Forward from this event (s)"}}]), + gs:menuitem({mode, ModeR}, Menu, [{label, {text,"Reverse from this event (r)"}}]) + end, + gs:menuitem({mode, all}, Menu, [{label, {text,"Abort search. Display all (a)"}}]). + +config_editor(Editor, S) -> + Event = S#state.event, + Name = S#state.active_filter, + {value, F} = lists:keysearch(Name, #filter.name, S#state.filters), + FilterFun = F#filter.function, + case catch FilterFun(Event) of + true -> + do_config_editor(Editor, Event, lightblue, S#state.event_order); + {true, Event2} when is_record(Event2, event) -> + do_config_editor(Editor, Event2, lightblue, S#state.event_order); + false -> + do_config_editor(Editor, Event, red, S#state.event_order); + Bad -> + Contents = {bad_filter, Name, Bad}, + BadEvent = Event#event{contents = Contents}, + do_config_editor(Editor, BadEvent, red, S#state.event_order) + end. + +do_config_editor(Editor, Event, Colour, TsKey) -> + String = event_to_string(Event, TsKey), + gs:config(Editor, {insert, {'end', String}}), + gs:config(Editor, {enable, false}), + gs:config(Editor, {bg, Colour}), + Event. + +%%%---------------------------------------------------------------------- +%%% String handling +%%%---------------------------------------------------------------------- + +term_to_string(Term) -> + case catch io_lib:format("~s", [Term]) of + {'EXIT', _} -> io_lib:format("~p", [Term]); + GoodString -> GoodString + end. + +now_to_string({Mega, Sec, Micro} = Now) + when is_integer(Mega), is_integer(Sec), is_integer(Micro) -> + {{Y, Mo, D}, {H, Mi, S}} = calendar:now_to_universal_time(Now), + lists:concat([Y, "-", Mo, "-", D, " ", H, ".", Mi, ".", S, ".", Micro]); +now_to_string(Other) -> + term_to_string(Other). + +event_to_string(Event, TsKey) -> + ReportedTs = Event#event.trace_ts, + ParsedTs = Event#event.event_ts, + Deep = + ["DETAIL LEVEL: ", term_to_string(Event#event.detail_level), + "\nLABEL: ", term_to_string(Event#event.label), + case Event#event.from =:= Event#event.to of + true -> + ["\nACTOR: ", term_to_string(Event#event.from)]; + false -> + ["\nFROM: ", term_to_string(Event#event.from), + "\nTO: ", term_to_string(Event#event.to)] + end, + case ReportedTs =:= ParsedTs of + true -> + ["\nPARSED: ", now_to_string(ParsedTs)]; + false -> + case TsKey of + trace_ts -> + ["\nTRACE_TS: ", now_to_string(ReportedTs), + "\nEVENT_TS: ", now_to_string(ParsedTs)]; + event_ts -> + ["\nEVENT_TS: ", now_to_string(ParsedTs), + "\nTRACE_TS: ", now_to_string(ReportedTs)] + end + end, + "\nCONTENTS:\n\n", term_to_string(Event#event.contents)], + lists:flatten(Deep). + +pad_string(Atom, MinLen) when is_atom(Atom) -> + pad_string(atom_to_list(Atom), MinLen); +pad_string(String, MinLen) when is_integer(MinLen), MinLen >= 0 -> + Len = length(String), + case Len >= MinLen of + true -> + String; + false -> + String ++ lists:duplicate(MinLen - Len, $ ) + end. + +send_viewer_event(S, Event) -> + case S#state.viewer_pid of + ViewerPid when is_pid(ViewerPid) -> + ViewerPid ! {et, Event}; + undefined -> + ignore + end. |