From 84adefa331c4159d432d22840663c38f155cd4c1 Mon Sep 17 00:00:00 2001 From: Erlang/OTP Date: Fri, 20 Nov 2009 14:54:40 +0000 Subject: The R13B03 release. --- lib/et/src/Makefile | 135 ++++ lib/et/src/et.app.src | 34 + lib/et/src/et.appup.src | 23 + lib/et/src/et.erl | 140 ++++ lib/et/src/et_collector.erl | 1204 ++++++++++++++++++++++++++++ lib/et/src/et_contents_viewer.erl | 591 ++++++++++++++ lib/et/src/et_internal.hrl | 23 + lib/et/src/et_selector.erl | 529 ++++++++++++ lib/et/src/et_viewer.erl | 1602 +++++++++++++++++++++++++++++++++++++ lib/et/src/modules.mk | 32 + 10 files changed, 4313 insertions(+) create mode 100644 lib/et/src/Makefile create mode 100644 lib/et/src/et.app.src create mode 100644 lib/et/src/et.appup.src create mode 100644 lib/et/src/et.erl create mode 100644 lib/et/src/et_collector.erl create mode 100644 lib/et/src/et_contents_viewer.erl create mode 100644 lib/et/src/et_internal.hrl create mode 100644 lib/et/src/et_selector.erl create mode 100644 lib/et/src/et_viewer.erl create mode 100644 lib/et/src/modules.mk (limited to 'lib/et/src') diff --git a/lib/et/src/Makefile b/lib/et/src/Makefile new file mode 100644 index 0000000000..c590852625 --- /dev/null +++ b/lib/et/src/Makefile @@ -0,0 +1,135 @@ +# +# %CopyrightBegin% +# +# Copyright Ericsson AB 2000-2009. 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% +# + +# +include $(ERL_TOP)/make/target.mk + +ERL_COMPILE_FLAGS += -W +warn_unused_vars + +ifeq ($(TYPE),debug) +ERL_COMPILE_FLAGS += -Ddebug +endif + +# This is temporary until the et app is actually moved... +EBIN = ../ebin + +include $(ERL_TOP)/make/$(TARGET)/otp.mk + +# ---------------------------------------------------- +# Application version +# ---------------------------------------------------- +include ../vsn.mk +VSN=$(ET_VSN) + +# ---------------------------------------------------- +# Release directory specification +# ---------------------------------------------------- +RELSYSDIR = $(RELEASE_PATH)/lib/et-$(VSN) + + +# ---------------------------------------------------- +# Target Specs +# ---------------------------------------------------- + +include modules.mk + +ERL_FILES = $(MODULES:%=%.erl) + +TARGET_FILES = \ + $(ERL_FILES:%.erl=$(EBIN)/%.$(EMULATOR)) + +APP_FILE = et.app +APP_SRC = $(APP_FILE).src +APP_TARGET = $(EBIN)/$(APP_FILE) + +APPUP_FILE = et.appup +APPUP_SRC = $(APPUP_FILE).src +APPUP_TARGET = $(EBIN)/$(APPUP_FILE) + +# ---------------------------------------------------- +# FLAGS +# ---------------------------------------------------- +ERL_COMPILE_FLAGS += -pa $(ERL_TOP)/lib/et/ebin -I../include + +# ---------------------------------------------------- +# Special Build Targets +# ---------------------------------------------------- + +# ---------------------------------------------------- +# Targets +# ---------------------------------------------------- + +debug: + @${MAKE} TYPE=debug opt + +opt: $(TARGET_FILES) $(APP_TARGET) $(APPUP_TARGET) + +clean: + rm -f $(TARGET_FILES) $(APP_TARGET) $(APPUP_TARGET) + rm -f errs core *~ + +docs: + + +# ---------------------------------------------------- +# Special Build Targets +# ---------------------------------------------------- + +$(APP_TARGET): $(APP_SRC) ../vsn.mk + sed -e 's;%VSN%;$(VSN);' $< > $@ + +$(APPUP_TARGET): $(APPUP_SRC) ../vsn.mk + sed -e 's;%VSN%;$(VSN);' $< > $@ + +# ---------------------------------------------------- +# Release Target +# ---------------------------------------------------- + +include $(ERL_TOP)/make/otp_release_targets.mk + +release_spec: opt + $(INSTALL_DIR) $(RELSYSDIR)/ebin + $(INSTALL_DATA) $(TARGET_FILES) $(RELSYSDIR)/ebin + $(INSTALL_DATA) $(APP_TARGET) $(APPUP_TARGET) $(RELSYSDIR)/ebin + $(INSTALL_DIR) $(RELSYSDIR)/src + $(INSTALL_DATA) $(ERL_FILES) $(RELSYSDIR)/src + $(INSTALL_DATA) $(INTERNAL_HRL_FILES) $(RELSYSDIR)/src + $(INSTALL_DIR) $(RELSYSDIR)/include + $(INSTALL_DATA) $(HRL_FILES) $(RELSYSDIR)/include + + +release_docs_spec: + + +# ---------------------------------------------------- +# Include dependencies +# ---------------------------------------------------- + +$(EBIN)/et.$(EMULATOR): et.erl + +$(EBIN)/et_selector.$(EMULATOR): et_selector.erl ../include/et.hrl + +$(EBIN)/et_contents_viewer.$(EMULATOR): et_contents_viewer.erl ../include/et.hrl et_internal.hrl + +$(EBIN)/et_collector.$(EMULATOR): et_collector.erl ../include/et.hrl et_internal.hrl + +$(EBIN)/et_viewer.$(EMULATOR): et_viewer.erl ../include/et.hrl et_internal.hrl + + + diff --git a/lib/et/src/et.app.src b/lib/et/src/et.app.src new file mode 100644 index 0000000000..0c7bef7c3d --- /dev/null +++ b/lib/et/src/et.app.src @@ -0,0 +1,34 @@ +%% This is an -*- erlang -*- file. +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2002-2009. 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% + +{application, et, + [{description, "Event Tracer"}, + {vsn, "%VSN%"}, + {modules, + [ + et, + et_collector, + et_contents_viewer, + et_selector, + et_viewer + ]}, + {registered, [et_collector]}, + {applications, [stdlib, kernel]}, + {env, []} + ]}. diff --git a/lib/et/src/et.appup.src b/lib/et/src/et.appup.src new file mode 100644 index 0000000000..b6344a9387 --- /dev/null +++ b/lib/et/src/et.appup.src @@ -0,0 +1,23 @@ +%% This is an -*- erlang -*- file. +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2002-2009. 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% + +{"%VSN%", + [ ] +}. + diff --git a/lib/et/src/et.erl b/lib/et/src/et.erl new file mode 100644 index 0000000000..9c0a7f8f49 --- /dev/null +++ b/lib/et/src/et.erl @@ -0,0 +1,140 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2000-2009. 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: Main API module for Event Tracer +%%---------------------------------------------------------------------- +%% +%% The Event Tracer (et) uses the built-in trace mechanism in Erlang and +%% provides tools for collection and graphical viewing of trace data. +%% +%% et_collector +%% +%% An Erlang trace client which collects and stores trace data. +%% Provides hooks for trace data filtering and group communication +%% between processes (such as et_viewer-processes). The trace data +%% is preferably traced et-module calls, but may in fact be any +%% Erlang trace data. +%% +%% It do also provide functionality for global control of trace +%% pattern settings. If used, the one et_collector-process is +%% registered globally. On all connected Erlang nodes, it starts an +%% Erlang tracer process which sends its trace data to a local port +%% (the port number is generated). On the node where the global +%% et_collector is running, the corresponding Erlang trace client +%% processes are started (one for each node), configured to +%% transform the trace data into event records and possibly hand +%% them over to the collector. Whenever new nodes are +%% (dis)connected, this is monitored and new tracer/client pair of +%% processes are automatically started and eventually the trace +%% pattern are set on these nodes. +%% +%% Trace data can also be loaded from one or more files. +%% +%% et_viewer +%% +%% A graphical sequence chart tool. It is connected to a +%% et_collector-process, which it polls regulary for more trace +%% events to display. Before the event is displayed a user defined +%% filter function is applied in order to skip, accept as is or +%% transform the event. Several et_viewer-processes may share the +%% same et_collector in order to provide different simultaneous +%% views of the same trace data. +%% +%% et_contents_viewer +%% +%% A graphical tool which displays a detailed view of one trace +%% event. Normally started from the et_viewer. +%% +%% et_selector +%% +%% A library module with low level functions for activation of +%% Erlang trace patterns. It do also implement a default filter +%% function which transforms the raw trace data into the event +%% record data structure that is used as internal format by the rest +%% of the application. Customized transform functions can be +%% alternatively be used (by et_viewer, et_contents_viewer and +%% et_collector), if needed. +%% +%% et +%% +%% A library module with a few event report functions that are +%% intended to be invoked from other applications. The functions are +%% extremely light weight as they do nothing besides returning an +%% atom. These functions are specifically designed to be traced +%% for. The global trace patterns in et_collector defaults to trace +%% on these functions. +%%---------------------------------------------------------------------- + +-module(et). + +-export([ + phone_home/4, report_event/4, + phone_home/5, report_event/5 + ]). + +%%---------------------------------------------------------------------- +%% Reports an event, such as a message +%% +%% report_event(DetailLevel, FromTo, Label, Contents) -> hopefully_traced +%% report_event(DetailLevel, From, To, Label, Contents) -> hopefully_traced +%% phone_home(DetailLevel, FromTo, Label, Contents) -> hopefully_traced +%% phone_home(DetailLevel, From, To, Label, Contents) -> hopefully_traced +%% +%% DetailLevel = integer(X) when X =< 0, X >= 100 +%% From = actor() +%% To = actor() +%% FromTo = actor() +%% Label = atom() | string() | term() +%% Contents = [{Key, Value}] | term() +%% +%% actor() = term() +%% +%% These functions are intended to be invoked at strategic places +%% in user applications in order to enable simplified tracing. +%% The functions are extremely light weight as they do nothing +%% besides returning an atom. These functions are designed for +%% being traced. The global tracing mechanism in et_collector +%% defaults to set its trace pattern to these functions. +%% +%% The label is intended to provide a brief summary of the event. +%% A simple tag would do. +%% +%% The contents can be any term but in order to simplify +%% post processing of the traced events, a plain list +%% of {Key, Value} tuples is preferred. +%% +%% Some events, such as messages, are directed from some actor to another. +%% Other events (termed actions) may be undirected and only have one actor. +%%---------------------------------------------------------------------- + +phone_home(DetailLevel, FromTo, Label, Contents) -> + %% N.B External call + ?MODULE:report_event(DetailLevel, FromTo, FromTo, Label, Contents). + +phone_home(DetailLevel, From, To, Label, Contents) -> + %% N.B External call + ?MODULE:report_event(DetailLevel, From, To, Label, Contents). + +report_event(DetailLevel, FromTo, Label, Contents) -> + %% N.B External call + ?MODULE:report_event(DetailLevel, FromTo, FromTo, Label, Contents). + +report_event(DetailLevel, _From, _To, _Label, _Contents) + when integer(DetailLevel) -> + hopefully_traced. diff --git a/lib/et/src/et_collector.erl b/lib/et/src/et_collector.erl new file mode 100644 index 0000000000..ea23c188f7 --- /dev/null +++ b/lib/et/src/et_collector.erl @@ -0,0 +1,1204 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2000-2009. 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: Collect trace events and provide a backing storage +%% appropriate for iteration +%%---------------------------------------------------------------------- + +-module(et_collector). + +-behaviour(gen_server). + +%% External exports +-export([ + start_link/1, + stop/1, + + report/2, + report_event/5, + report_event/6, + + iterate/3, + iterate/5, + + start_trace_client/3, + start_trace_port/1, + %% load_event_file/2, + save_event_file/3, + clear_table/1, + + get_global_pid/0, + %% get_table_handle/1, + change_pattern/2, + make_key/2, + + dict_insert/3, + dict_delete/2, + dict_lookup/2, + dict_match/2, + multicast/2 + ]). + +%% gen_server callbacks +-export([init/1,terminate/2, code_change/3, + handle_call/3, handle_cast/2, handle_info/2]). + +-include("../include/et.hrl"). + +-record(state, {parent_pid, + event_tab, + dict_tab, + event_order, + subscribers, + file, + trace_pattern, + trace_port, + trace_max_queue, + trace_nodes, + trace_global}). + +-record(file, {name, desc, event_opt, file_opt, table_opt}). + +-record(table_handle, {collector_pid, event_tab, event_order, filter}). + +-record(trace_ts, {trace_ts, event_ts}). +-record(event_ts, {event_ts, trace_ts}). + +%%%---------------------------------------------------------------------- +%%% Client side +%%%---------------------------------------------------------------------- + +%%---------------------------------------------------------------------- +%% start_link(Options) -> {ok, CollectorPid} | {error, Reason} +%% +%% Start a collector process +%% +%% The collector collects trace events and keeps them ordered by their +%% timestamp. The timestamp may either reflect the time when the +%% actual trace data was generated (trace_ts) or when the trace data +%% was transformed into an event record (event_ts). If the time stamp +%% is missing in the trace data (missing timestamp option to +%% erlang:trace/4) the trace_ts will be set to the event_ts. +%% +%% Events are reported to the collector directly with the report +%% function or indirectly via one or more trace clients. All reported +%% events are first filtered thru the collector filter before they are +%% stored by the collector. By replacing the default collector filter +%% with a customized dito it is possible to allow any trace data as +%% input. The collector filter is a dictionary entry with the +%% predefined key {filter, collector} and the value is a fun of +%% arity 1. See et_selector:parse_event/2 for interface details, +%% such as which erlang:trace/1 tuples that are accepted. +%% +%% The collector has a built-in dictionary service. Any term may be +%% stored as value in the dictionary and bound to a unique key. When +%% new values are inserted with an existing key, the new values will +%% overwrite the existing ones. Processes may subscribe on dictionary +%% updates by using {subscriber, pid()} as dictionary key. All +%% dictionary updates will be propagated to the subscriber processes +%% matching the pattern {{subscriber, '_'}, '_'} where the first '_' +%% is interpreted as a pid(). +%% +%% In global trace mode, the collector will automatically start +%% tracing on all connected Erlang nodes. When a node connects, a port +%% tracer will be started on that node and a corresponding trace +%% client on the collector node. By default the global trace pattern +%% is 'max'. +%% +%% Options = [option()] +%% +%% option() = +%% {parent_pid, pid()} | +%% {event_order, event_order()} | +%% {dict_insert, {filter, collector}, collector_fun()} | +%% {dict_insert, {filter, event_filter_name()}, event_filter_fun()} | +%% {dict_insert, {subscriber, pid()}, dict_val()} | +%% {dict_insert, dict_key(), dict_val()} | +%% {dict_delete, dict_key()} | +%% {trace_client, trace_client()} | +%% {trace_global, boolean()} | +%% {trace_pattern, trace_pattern()} | +%% {trace_port, integer()} | +%% {trace_max_queue, integer()} +%% +%% event_order() = trace_ts | event_ts +%% trace_pattern() = detail_level() | dbg_match_spec() +%% detail_level() = min | max | integer(X) when X =< 0, X >= 100 +%% trace_client() = +%% {event_file, file_name()} | +%% {dbg_trace_type(), dbg_trace_parameters()} +%% file_name() = string() +%% collector_fun() = trace_filter_fun() | event_filter_fun() +%% trace_filter_fun() = fun(TraceData) -> false | true | {true, NewEvent} +%% event_filter_fun() = fun(Event) -> false | true | {true, NewEvent} +%% event_filter_name() = atom() +%% TraceData = erlang_trace_data() +%% Event = NewEvent = record(event) +%% dict_key() = term() +%% dict_val() = term() +%% +%% CollectorPid = pid() +%% Reason = term() +%%---------------------------------------------------------------------- + +start_link(Options) -> + case parse_opt(Options, default_state(), [], []) of + {ok, S, Dict2, Clients} when S#state.trace_global == false -> + case gen_server:start_link(?MODULE, [S, Dict2], []) of + {ok, Pid} when S#state.parent_pid /= self() -> + unlink(Pid), + start_clients(Pid, Clients); + {ok,Pid} -> + start_clients(Pid, Clients); + {error, Reason} -> + {error, Reason} + end; + {ok, S, Dict2, Clients} when S#state.trace_global == true -> + case gen_server:start_link({global, ?MODULE}, ?MODULE, [S, Dict2], []) of + {ok, Pid} when S#state.parent_pid /= self() -> + unlink(Pid), + start_clients(Pid, Clients); + {ok,Pid} -> + start_clients(Pid, Clients); + {error, Reason} -> + {error, Reason} + end; + {error, Reason} -> + {error, Reason} + end. + +default_state() -> + #state{parent_pid = self(), + event_order = trace_ts, + subscribers = [], + trace_global = false, + trace_pattern = undefined, + trace_nodes = [], + trace_port = 4711, + trace_max_queue = 50}. + +parse_opt([], S, Dict, Clients) -> + {Mod, Pattern} = et_selector:make_pattern(S#state.trace_pattern), + Fun = fun(E) -> et_selector:parse_event(Mod, E) end, + Default = {dict_insert, {filter, collector}, Fun}, + {ok, S#state{trace_pattern = {Mod, Pattern}}, [Default | Dict], Clients}; +parse_opt([H | T], S, Dict, Clients) -> + case H of + {parent_pid, Parent} when Parent == undefined -> + parse_opt(T, S#state{parent_pid = Parent}, Dict, Clients); + {parent_pid, Parent} when pid(Parent) -> + parse_opt(T, S#state{parent_pid = Parent}, Dict, Clients); + {event_order, Order} when Order == trace_ts -> + parse_opt(T, S#state{event_order = Order}, Dict, Clients); + {event_order, Order} when Order == event_ts -> + parse_opt(T, S#state{event_order = Order}, Dict, Clients); + {dict_insert, {filter, Name}, Fun} -> + if + atom(Name), function(Fun) -> + parse_opt(T, S, Dict ++ [H], Clients); + true -> + {error, {bad_option, H}} + end; + {dict_insert, {subscriber, Pid}, _Val} -> + if + pid(Pid) -> + parse_opt(T, S, Dict ++ [H], Clients); + true -> + {error, {bad_option, H}} + end; + {dict_insert, _Key, _Val} -> + parse_opt(T, S, Dict ++ [H], Clients); + {dict_delete, _Key} -> + parse_opt(T, S, Dict ++ [H], Clients); + {trace_client, Client = {_, _}} -> + parse_opt(T, S, Dict, Clients ++ [Client]); + {trace_global, Bool} when Bool == false -> + parse_opt(T, S#state{trace_global = Bool}, Dict, Clients); + {trace_global, Bool} when Bool == true -> + parse_opt(T, S#state{trace_global = Bool}, Dict, Clients); + {trace_pattern, {Mod, _} = Pattern} when atom(Mod) -> + parse_opt(T, S#state{trace_pattern = Pattern}, Dict, Clients); + {trace_pattern, undefined = Pattern} -> + parse_opt(T, S#state{trace_pattern = Pattern}, Dict, Clients); + {trace_port, Port} when integer(Port) -> + parse_opt(T, S#state{trace_port = Port}, Dict, Clients); + {trace_max_queue, MaxQueue} when integer(MaxQueue) -> + parse_opt(T, S#state{trace_port = MaxQueue}, Dict, Clients); + Bad -> + {error, {bad_option, Bad}} + end; +parse_opt(BadList, _S, _Dict, _Clients) -> + {error, {bad_option_list, BadList}}. + +start_clients(CollectorPid, [{Type, Parameters} | T]) -> + start_trace_client(CollectorPid, Type, Parameters), + start_clients(CollectorPid, T); +start_clients(CollectorPid, []) -> + {ok, CollectorPid}. + +%%---------------------------------------------------------------------- +%% stop(CollectorPid) -> ok +%% +%% Stop a collector process +%% +%% CollectorPid = pid() +%%---------------------------------------------------------------------- + +stop(CollectorPid) -> + call(CollectorPid, stop). + +%%---------------------------------------------------------------------- +%% save_event_file(CollectorPid, FileName, Options) -> ok | {error, Reason} +%% +%% Saves the events to a file +%% +%% CollectorPid = pid() +%% FileName = string() +%% Options = [option()] +%% Reason = term() +%% +%% option() = event_option() | file_option() | table_option() +%% event_option() = existing +%% file_option() = write | append +%% table_option() = keep | clear +%% +%% By default the currently stored events (existing) are +%% written to a brand new file (write) and the events are +%% kept (keep) after they have been written to the file. +%% +%% Instead of keeping the events after writing them to file, +%% it is possible to remove all stored events after they +%% have successfully written to file (clear). +%% +%% The options defaults to existing, write and keep. +%%---------------------------------------------------------------------- + +save_event_file(CollectorPid, FileName, Options) -> + call(CollectorPid, {save_event_file, FileName, Options}). + +%%---------------------------------------------------------------------- +%% load_event_file(CollectorPid, FileName) ->{ok, BadBytes} | exit(Reason) +%% +%% Load the event table from a file +%% +%% CollectorPid = pid() +%% FileName = string() +%% BadBytes = integer(X) where X >= 0 +%% Reason = term() +%%---------------------------------------------------------------------- + +load_event_file(CollectorPid, FileName) -> + Fd = make_ref(), + Args = [{file, FileName}, {name, Fd}, {repair, true}, {mode, read_only}], + Fun = fun(Event, {ok, TH}) -> report(TH, Event) end, + case disk_log:open(Args) of + {ok, _} -> + do_load_event_file(Fun, Fd, start, {ok, CollectorPid}, FileName, 0); + {repaired, _, _, BadBytes} -> + do_load_event_file(Fun, Fd, start, {ok, CollectorPid}, FileName, BadBytes); + {error, Reason} -> + exit({disk_log_open, FileName, Reason}) + end. + +do_load_event_file(Fun, Fd, Cont, Acc, FileName, BadBytes) -> + case disk_log:chunk(Fd, Cont) of + eof -> + {ok, BadBytes}; + {error, Reason} -> + exit({bad_disk_log_chunk, FileName, Reason}); + {Cont2, Events} -> + Acc2 = lists:foldl(Fun, Acc, Events), + do_load_event_file(Fun, Fd, Cont2, Acc2, FileName, BadBytes); + {Cont2, Events, More} -> + Acc2 = lists:foldl(Fun, Acc, Events), + do_load_event_file(Fun, Fd, Cont2, Acc2, FileName, BadBytes + More) + end. + +%%---------------------------------------------------------------------- +%% report(Handle, TraceOrEvent) +%% +%% Report an event to the collector +%% +%% All events are filtered thru the collector filter, which +%% optionally may transform or discard the event. The first +%% call should use the pid of the collector process as +%% report handle, while subsequent calls should use the +%% table handle. +%% +%% Handle = Initial | Continuation +%% Initial = collector_pid() +%% collector_pid() = pid() +%% Continuation = record(table_handle) +%% +%% TraceOrEvent = record(event) | dbg_trace_tuple() | end_of_trace +%% Reason = term() +%% +%% Returns: {ok, Continuation} | exit(Reason) +%%---------------------------------------------------------------------- + +report(CollectorPid, TraceOrEvent) when pid(CollectorPid) -> + case get_table_handle(CollectorPid) of + {ok, TH} when record(TH, table_handle) -> + report(TH, TraceOrEvent); + {error, Reason} -> + exit(Reason) + end; +report(TH, TraceOrEvent) when record(TH, table_handle) -> + Fun = TH#table_handle.filter, + case Fun(TraceOrEvent) of + false -> + {ok, TH}; + true when record(TraceOrEvent, event) -> + Key = make_key(TH, TraceOrEvent), + case catch ets:insert(TH#table_handle.event_tab, {Key, TraceOrEvent}) of + true -> + {ok, TH}; + {'EXIT', _Reason} -> + %% Refresh the report handle and try again + report(TH#table_handle.collector_pid, TraceOrEvent) + end; + {true, Event} when record(Event, event) -> + Key = make_key(TH, Event), + case catch ets:insert(TH#table_handle.event_tab, {Key, Event}) of + true -> + {ok, TH}; + {'EXIT', _Reason} -> + %% Refresh the report handle and try again + report(TH#table_handle.collector_pid, TraceOrEvent) + end; + BadEvent -> + TS = erlang:now(), + Contents = [{trace, TraceOrEvent}, {reason, BadEvent}, {filter, Fun}], + Event = #event{detail_level = 0, + trace_ts = TS, + event_ts = TS, + from = bad_filter, + to = bad_filter, + label = bad_filter, + contents = Contents}, + Key = make_key(TH, Event), + case catch ets:insert(TH#table_handle.event_tab, {Key, Event}) of + true -> + {ok, TH}; + {'EXIT', _Reason} -> + %% Refresh the report handle and try again + report(TH#table_handle.collector_pid, TraceOrEvent) + end + end; +report(TH, end_of_trace) when record(TH, table_handle) -> + {ok, TH}; +report(_, Bad) -> + exit({bad_event, Bad}). + +report_event(CollectorPid, DetailLevel, FromTo, Label, Contents) -> + report_event(CollectorPid, DetailLevel, FromTo, FromTo, Label, Contents). + +report_event(CollectorPid, DetailLevel, From, To, Label, Contents) + when integer(DetailLevel), DetailLevel >= 0, DetailLevel =< 100, list(Contents) -> + TS= erlang:now(), + E = #event{detail_level = DetailLevel, + trace_ts = TS, + event_ts = TS, + from = From, + to = To, + label = Label, + contents = Contents}, + report(CollectorPid, E). + +%%---------------------------------------------------------------------- +%% make_key(Type, Stuff) -> Key +%% +%% Makes a key out of an event record or an old key +%% +%% Type = record(table_handle) | trace_ts | event_ts +%% Stuff = record(event) | Key +%% Key = record(event_ts) | record(trace_ts) +%%---------------------------------------------------------------------- + +make_key(TH, Stuff) when record(TH, table_handle) -> + make_key(TH#table_handle.event_order, Stuff); +make_key(trace_ts, Stuff) -> + if + record(Stuff, event) -> + #event{trace_ts = R, event_ts = P} = Stuff, + #trace_ts{trace_ts = R, event_ts = P}; + record(Stuff, trace_ts) -> + Stuff; + record(Stuff, event_ts) -> + #event_ts{trace_ts = R, event_ts = P} = Stuff, + #trace_ts{trace_ts = R, event_ts = P} + end; +make_key(event_ts, Stuff) -> + if + record(Stuff, event) -> + #event{trace_ts = R, event_ts = P} = Stuff, + #event_ts{trace_ts = R, event_ts = P}; + record(Stuff, event_ts) -> + Stuff; + record(Stuff, trace_ts) -> + #trace_ts{trace_ts = R, event_ts = P} = Stuff, + #event_ts{trace_ts = R, event_ts = P} + end. + +%%---------------------------------------------------------------------- +%% get_table_handle(CollectorPid) -> Handle +%% +%% Return a table handle +%% +%% CollectorPid = pid() +%% Handle = record(table_handle) +%%---------------------------------------------------------------------- + +get_table_handle(CollectorPid) when pid(CollectorPid) -> + call(CollectorPid, get_table_handle). + +%%---------------------------------------------------------------------- +%% get_global_pid() -> CollectorPid | exit(Reason) +%% +%% Return a the identity of the globally registered collector +%% if there is any +%% +%% CollectorPid = pid() +%% Reason = term() +%%---------------------------------------------------------------------- + +get_global_pid() -> + case global:whereis_name(?MODULE) of + CollectorPid when pid(CollectorPid) -> + CollectorPid; + undefined -> + exit(global_collector_not_started) + end. + +%%---------------------------------------------------------------------- +%% change_pattern(CollectorPid, RawPattern) -> {old_pattern, TracePattern} +%% +%% Change active trace pattern globally on all trace nodes +%% +%% CollectorPid = pid() +%% RawPattern = {report_module(), extended_dbg_match_spec()} +%% report_module() = atom() | undefined +%% extended_dbg_match_spec()() = detail_level() | dbg_match_spec() +%% RawPattern = detail_level() +%% detail_level() = min | max | integer(X) when X =< 0, X >= 100 +%% TracePattern = {report_module(), dbg_match_spec_match_spec()} +%%---------------------------------------------------------------------- + +change_pattern(CollectorPid, RawPattern) -> + Pattern = et_selector:make_pattern(RawPattern), + call(CollectorPid, {change_pattern, Pattern}). + +%%---------------------------------------------------------------------- +%% dict_insert(CollectorPid, {filter, collector}, FilterFun) -> ok +%% dict_insert(CollectorPid, {subscriber, SubscriberPid}, Void) -> ok +%% dict_insert(CollectorPid, Key, Val) -> ok +%% +%% Insert a dictionary entry +%% and send a {et, {dict_insert, Key, Val}} tuple +%% to all registered subscribers. +%% +%% If the entry is a new subscriber, it will imply that +%% the new subscriber process first will get one message +%% for each already stored dictionary entry, before it +%% and all old subscribers will get this particular entry. +%% The collector process links to and then supervises the +%% subscriber process. If the subscriber process dies it +%% will imply that it gets unregistered as with a normal +%% dict_delete/2. +%% +%% CollectorPid = pid() +%% FilterFun = filter_fun() +%% SubscriberPid = pid() +%% Void = term() +%% Key = term() +%% Val = term() +%%---------------------------------------------------------------------- + +dict_insert(CollectorPid, Key = {filter, Name}, Fun) -> + if + atom(Name), function(Fun) -> + call(CollectorPid, {dict_insert, Key, Fun}); + true -> + exit({badarg, Key}) + end; +dict_insert(CollectorPid, Key = {subscriber, Pid}, Val) -> + if + pid(Pid) -> + call(CollectorPid, {dict_insert, Key, Val}); + true -> + exit({badarg, Key}) + end; +dict_insert(CollectorPid, Key, Val) -> + call(CollectorPid, {dict_insert, Key, Val}). + +%%---------------------------------------------------------------------- +%% dict_lookup(CollectorPid, Key) -> [Val] +%% +%% Lookup a dictionary entry and return zero or one value +%% +%% CollectorPid = pid() +%% Key = term() +%% Val = term() +%%---------------------------------------------------------------------- + +dict_lookup(CollectorPid, Key) -> + call(CollectorPid, {dict_lookup, Key}). + +%%---------------------------------------------------------------------- +%% Ddict_delete(CollectorPid, Key) -> ok +%% +%% elete a dictionary entry +%% and send a {et, {dict_delete, Key}} tuple +%% to all registered subscribers. +%% +%% If the deleted entry is a registered subscriber, it will +%% imply that the subscriber process gets is unregistered as +%% subscriber as well as it gets it final message. +%% +%% dict_delete(CollectorPid, {subscriber, SubscriberPid}) +%% dict_delete(CollectorPid, Key) +%% +%% CollectorPid = pid() +%% SubscriberPid = pid() +%% Key = term() +%%---------------------------------------------------------------------- + +dict_delete(CollectorPid, Key) -> + call(CollectorPid, {dict_delete, Key}). + +%%---------------------------------------------------------------------- +%% dict_match(CollectorPid, Pattern) -> [Match] +%% +%% Match some dictionary entries +%% +%% CollectorPid = pid() +%% Pattern = '_' | {key_pattern(), val_pattern()} +%% key_pattern() = ets_match_object_pattern() +%% val_pattern() = ets_match_object_pattern() +%% Match = {key(), val()} +%% key() = term() +%% val() = term() +%%---------------------------------------------------------------------- + +dict_match(CollectorPid, Pattern) -> + call(CollectorPid, {dict_match, Pattern}). + +%%---------------------------------------------------------------------- +%% multicast(_CollectorPid, Msg) -> ok +%% +%% Sends a message to all registered subscribers +%% +%% CollectorPid = pid() +%% Msg = term() +%%---------------------------------------------------------------------- + +multicast(_CollectorPid, Msg = {dict_insert, _Key, _Val}) -> + exit({badarg, Msg}); +multicast(_CollectorPid, Msg = {dict_delete, _Key}) -> + exit({badarg, Msg}); +multicast(CollectorPid, Msg) -> + call(CollectorPid, {multicast, Msg}). + +%%---------------------------------------------------------------------- +%% start_trace_client(CollectorPid, Type, Parameters) -> +%% file_loaded | {trace_client_pid, pid()} | exit(Reason) +%% +%% Load raw Erlang trace from a file, port or process. +%% +%% Type = dbg_trace_client_type() +%% Parameters = dbg_trace_client_parameters() +%% Pid = dbg_trace_client_pid() +%%---------------------------------------------------------------------- + +start_trace_client(CollectorPid, Type, FileName) when Type == event_file -> + load_event_file(CollectorPid, FileName); +start_trace_client(CollectorPid, Type, FileName) when Type == file -> + WaitFor = {make_ref(), end_of_trace}, + EventFun = fun(E, {ReplyTo, {ok, TH}}) -> {ReplyTo, report(TH, E)} end, + EndFun = fun({ReplyTo, {ok, _TH}}) -> ReplyTo ! WaitFor, ReplyTo end, + Spec = trace_spec_wrapper(EventFun, EndFun, {self(), {ok, CollectorPid}}), + Pid = dbg:trace_client(Type, FileName, Spec), + unlink(Pid), + Ref = erlang:monitor(process, Pid), + receive + WaitFor -> + erlang:demonitor(Ref), + receive + {'DOWN', Ref, _, _, _} -> + file_loaded + after 0 -> + file_loaded + end; + {'DOWN', Ref, _, _, Reason} -> + exit(Reason) + end; +start_trace_client(CollectorPid, Type, Parameters) -> + EventFun = fun(Event, {ok, TH}) -> report(TH, Event) end, + EndFun = fun(Acc) -> Acc end, + Spec = trace_spec_wrapper(EventFun, EndFun, {ok, CollectorPid}), + Pid = dbg:trace_client(Type, Parameters, Spec), + CollectorPid ! {register_trace_client, Pid}, + unlink(Pid), + {trace_client_pid, Pid}. + +trace_spec_wrapper(EventFun, EndFun, EventInitialAcc) + when function(EventFun), function(EndFun) -> + {fun(Trace, Acc) -> + case Trace == end_of_trace of + true -> EndFun(Acc); + false -> EventFun(Trace, Acc) + end + end, + EventInitialAcc}. + +start_trace_port(Parameters) -> + dbg:tracer(port, dbg:trace_port(ip, Parameters)). + +%%---------------------------------------------------------------------- +%% iterate(Handle, Prev, Limit) -> +%% iterate(Handle, Prev, Limit, undefined, Prev) +%% +%% Iterates over the currently stored events +%% +%% Short for iterate/5. +%%---------------------------------------------------------------------- + +iterate(Handle, Prev, Limit) -> + iterate(Handle, Prev, Limit, undefined, Prev). + +%%---------------------------------------------------------------------- +%% iterate(Handle, Prev, Limit, Fun, Acc) -> NewAcc +%% +%% Iterates over the currently stored events and apply a function for +%% each event. The iteration may be performed forwards or backwards +%% and may be limited to a maximum number of events (abs(Limit)). +%% +%% Handle = collector_pid() | table_handle() +%% Prev = first | last | event_key() +%% Limit = done() | forward() | backward() +%% collector_pid() = pid() +%% table_handle() = record(table_handle) +%% event_key() = +%% done() = 0 +%% forward() = infinity | integer(X) where X > 0 +%% backward() = '-infinity' | integer(X) where X < 0 +%% Fun = fun(Event, Acc) -> NewAcc +%% Acc = NewAcc = term() +%%---------------------------------------------------------------------- + +iterate(_, _, Limit, _, Acc) when Limit == 0 -> + Acc; +iterate(CollectorPid, Prev, Limit, Fun, Acc) when pid(CollectorPid) -> + case get_table_handle(CollectorPid) of + {ok, TH} when record(TH, table_handle) -> + iterate(TH, Prev, Limit, Fun, Acc); + {error, Reason} -> + exit(Reason) + end; +iterate(TH, Prev, Limit, Fun, Acc) when record(TH, table_handle) -> + if + Limit == infinity -> + next_iterate(TH, Prev, Limit, Fun, Acc); + integer(Limit), Limit > 0 -> + next_iterate(TH, Prev, Limit, Fun, Acc); + Limit == '-infinity' -> + prev_iterate(TH, Prev, Limit, Fun, Acc); + integer(Limit), Limit < 0 -> + prev_iterate(TH, Prev, Limit, Fun, Acc) + end. + +next_iterate(TH, Prev = first, Limit, Fun, Acc) -> + Tab = TH#table_handle.event_tab, + case catch ets:first(Tab) of + '$end_of_table' -> + Acc; + {'EXIT', _} = Error -> + io:format("~p(~p): First ~p~n", [?MODULE, ?LINE, Error]), + iterate(TH#table_handle.collector_pid, Prev, Limit, Fun, Acc); + First -> + lookup_and_apply(TH, Prev, First, Limit, -1, Fun, Acc) + end; +next_iterate(TH, Prev = last, Limit, Fun, Acc) -> + Tab = TH#table_handle.event_tab, + case catch ets:last(Tab) of + '$end_of_table' -> + Acc; + {'EXIT', _} = Error -> + io:format("~p(~p): Last ~p~n", [?MODULE, ?LINE, Error]), + iterate(TH#table_handle.collector_pid, Prev, Limit, Fun, Acc); + Last -> + lookup_and_apply(TH, Prev, Last, Limit, -1, Fun, Acc) + end; +next_iterate(TH, Prev, Limit, Fun, Acc) -> + Tab = TH#table_handle.event_tab, + Key = make_key(TH, Prev), + case catch ets:next(Tab, Key) of + '$end_of_table' -> + Acc; + {'EXIT', _} = Error -> + io:format("~p(~p): Next ~p -> ~p~n", [?MODULE, ?LINE, Key, Error]), + iterate(TH#table_handle.collector_pid, Prev, Limit, Fun, Acc); + Next -> + lookup_and_apply(TH, Prev, Next, Limit, -1, Fun, Acc) + end. + +prev_iterate(TH, Prev = first, Limit, Fun, Acc) -> + Tab = TH#table_handle.event_tab, + case catch ets:first(Tab) of + '$end_of_table' -> + Acc; + {'EXIT', _} = Error -> + io:format("~p(~p): First ~p~n", [?MODULE, ?LINE, Error]), + iterate(TH#table_handle.collector_pid, Prev, Limit, Fun, Acc); + First -> + lookup_and_apply(TH, Prev, First, Limit, 1, Fun, Acc) + end; +prev_iterate(TH, Prev = last, Limit, Fun, Acc) -> + Tab = TH#table_handle.event_tab, + case catch ets:last(Tab) of + '$end_of_table' -> + Acc; + {'EXIT', _} = Error -> + io:format("~p(~p): Last ~p~n", [?MODULE, ?LINE, Error]), + iterate(TH#table_handle.collector_pid, Prev, Limit, Fun, Acc); + Last -> + lookup_and_apply(TH, Prev, Last, Limit, 1, Fun, Acc) + end; +prev_iterate(TH, Prev, Limit, Fun, Acc) -> + Tab = TH#table_handle.event_tab, + Key = make_key(TH, Prev), + case catch ets:prev(Tab, Key) of + '$end_of_table' -> + Acc; + {'EXIT', _} = Error -> + io:format("~p(~p): Prev ~p -> ~p~n", [?MODULE, ?LINE, Key, Error]), + iterate(TH#table_handle.collector_pid, Prev, Limit, Fun, Acc); + Next -> + lookup_and_apply(TH, Prev, Next, Limit, 1, Fun, Acc) + end. + +lookup_and_apply(TH, _Prev, Next, Limit, Incr, Fun, _Acc) when Fun == undefined -> + Limit2 = incr(Limit, Incr), + iterate(TH, Next, Limit2, Fun, Next); +lookup_and_apply(TH, Prev, Next, Limit, Incr, Fun, Acc) -> + Tab = TH#table_handle.event_tab, + case catch ets:lookup_element(Tab, Next, 2) of + {'EXIT', _} -> + iterate(TH#table_handle.collector_pid, Prev, Limit, Fun, Acc); + E when record(E, event) -> + Acc2 = Fun(E, Acc), + Limit2 = incr(Limit, Incr), + iterate(TH, Next, Limit2, Fun, Acc2) + end. + +incr(Val, Incr) -> + if + Val == infinity -> Val; + Val == '-infinity' -> Val; + integer(Val) -> Val + Incr + end. + +%%---------------------------------------------------------------------- +%% clear_table(Handle) -> ok +%% +%% Clear the event table +%% +%% Handle = collector_pid() | table_handle() +%% collector_pid() = pid() +%% table_handle() = record(table_handle) +%%---------------------------------------------------------------------- + +clear_table(CollectorPid) when pid(CollectorPid) -> + call(CollectorPid, clear_table); +clear_table(TH) when record(TH, table_handle) -> + clear_table(TH#table_handle.collector_pid). + +call(CollectorPid, Request) -> + gen_server:call(CollectorPid, Request, infinity). + +%%%---------------------------------------------------------------------- +%%% Callback functions from gen_server +%%%---------------------------------------------------------------------- + +%%---------------------------------------------------------------------- +%% Func: init/1 +%% Returns: {ok, State} | +%% {ok, State, Timeout} | +%% ignore | +%% {stop, Reason} +%%---------------------------------------------------------------------- + +init([InitialS, Dict]) -> + process_flag(trap_exit, true), + case InitialS#state.parent_pid of + undefined -> + ignore; + Pid when pid(Pid) -> + link(Pid) + end, + Funs = [fun init_tables/1, + fun init_global/1, + fun(S) -> lists:foldl(fun do_dict_insert/2, S, Dict) end], + {ok, lists:foldl(fun(F, S) -> F(S) end, InitialS, Funs)}. + +init_tables(S) -> + EventTab = ets:new(et_events, [ordered_set, {keypos, 1}, public]), + DictTab = ets:new(et_dict, [ordered_set, {keypos, 1}, public]), + S#state{event_tab = EventTab, dict_tab = DictTab}. + +init_global(S) -> + case S#state.trace_global of + true -> + EventFun = fun(Event, {ok, TH}) -> report(TH, Event) end, + EndFun = fun(Acc) -> Acc end, + Spec = trace_spec_wrapper(EventFun, EndFun, {ok, self()}), + dbg:tracer(process, Spec), + et_selector:change_pattern(S#state.trace_pattern), + net_kernel:monitor_nodes(true), + lists:foreach(fun(N) -> self() ! {nodeup, N} end, nodes()), + S#state{trace_nodes = [node()]}; + false -> + S + end. + +%%---------------------------------------------------------------------- +%% 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({multicast, Msg}, _From, S) -> + do_multicast(S#state.subscribers, Msg), + {reply, ok, S}; + +handle_call(Msg = {dict_insert, _Key, _Val}, _From, S) -> + S2 = do_dict_insert(Msg, S), + {reply, ok, S2}; + +handle_call(Msg = {dict_delete, _Key}, _From, S) -> + S2 = do_dict_delete(Msg, S), + {reply, ok, S2}; + +handle_call({dict_lookup, Key}, _From, S) -> + Reply = ets:lookup(S#state.dict_tab, Key), + {reply, Reply, S}; + +handle_call({dict_match, Pattern}, _From, S) -> + case catch ets:match_object(S#state.dict_tab, Pattern) of + {'EXIT', _Reason} -> + {reply, [], S}; + Matching -> + {reply, Matching, S} + end; + +handle_call(get_table_handle, _From, S) -> + [{_, TableFilter}] = ets:lookup(S#state.dict_tab, {filter, collector}), + TH = #table_handle{collector_pid = self(), + event_tab = S#state.event_tab, + event_order = S#state.event_order, + filter = TableFilter}, + {reply, {ok, TH}, S}; + +handle_call(close, _From, S) -> + case S#state.file of + undefined -> + {reply, {error, file_not_open}, S}; + F -> + Reply = disk_log:close(F#file.desc), + S2 = S#state{file = undefined}, + {reply, Reply, S2} + end; +handle_call({save_event_file, FileName, Options}, _From, S) -> + Default = #file{name = FileName, + event_opt = existing, + file_opt = write, + table_opt = keep}, + case parse_file_options(Default, Options) of + {ok, F} when record(F, file) -> + case file_open(F) of + {ok, Fd} -> + F2 = F#file{desc = Fd}, + {Reply2, S3} = + case F2#file.event_opt of + %% new -> + %% Reply = ok, + %% S2 = S#state{file = F}, + %% {Reply, S2}; + %% + %% insert() -> + %% case S2#state.file of + %% undefined -> + %% ignore; + %% F -> + %% Fd = F#file.desc, + %% ok = disk_log:log(Fd, Event) + %% end. + existing -> + Fun = fun({_, E}, A) -> ok = disk_log:log(Fd, E), A end, + Tab = S#state.event_tab, + Reply = tab_iterate(Fun, Tab, ets:first(Tab), ok), + disk_log:close(Fd), + {Reply, S} + %% all -> + %% Reply = tab_iterate(WriteFun, Tab, ok), + %% S2 = S#state{file = F}, + %% {Reply, S2} + end, + case F2#file.table_opt of + keep -> + {reply, Reply2, S3}; + clear -> + S4 = do_clear_table(S3), + {reply, Reply2, S4} + end; + {error, Reason} -> + {reply, {error, {file_open, Reason}}, S} + end; + {error, Reason} -> + {reply, {error, Reason}, S} + end; + +handle_call({change_pattern, Pattern}, _From, S) -> + Ns = S#state.trace_nodes, + rpc:multicall(Ns, et_selector, change_pattern, [Pattern]), + Reply = {old_pattern, S#state.trace_pattern}, + S2 = S#state{trace_pattern = Pattern}, + {reply, Reply, S2}; + +handle_call(clear_table, _From, S) -> + S2 = do_clear_table(S), + {reply, ok, S2}; + +handle_call(stop, _From, S) -> + do_multicast(S#state.subscribers, close), + case S#state.trace_global of + true -> rpc:multicall(S#state.trace_nodes, dbg, stop, []); + false -> ignore + end, + {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}}, 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({nodeup, Node}, S) -> + Port = S#state.trace_port, + MaxQueue = S#state.trace_max_queue, + case rpc:call(Node, ?MODULE, start_trace_port, [{Port, MaxQueue}]) of + {ok, _} -> + listen_on_trace_port(Node, Port, S); + {error, Reason} when Reason == already_started-> + ok = error_logger:format("~p(~p): producer ignored(~p:~p):~n ~p~n", + [?MODULE, self(), Node, Port, Reason]), + S2 = S#state{trace_port = Port + 1}, + {noreply, S2}; + {badrpc, Reason} -> + ok = error_logger:format("~p(~p): producer ignored(~p:~p):~n ~p~n", + [?MODULE, self(), Node, Port, Reason]), + S2 = S#state{trace_port = Port + 1}, + {noreply, S2}; + {error, Reason} -> + self() ! {nodeup, Node}, + ok = error_logger:format("~p(~p): producer retry(~p:~p):~n ~p~n", + [?MODULE, self(), Node, Port, Reason]), + S2 = S#state{trace_port = Port + 1}, + {noreply, S2} + end; + +handle_info({nodedown, Node}, S) -> + {noreply, S#state{trace_nodes = S#state.trace_nodes -- [Node]}}; + +handle_info({register_trace_client, Pid}, S) -> + link(Pid), + {noreply, S}; + +handle_info({'EXIT', Pid, Reason}, S) when Pid == S#state.parent_pid -> + {stop, Reason, S}; +handle_info(Info = {'EXIT', Pid, _Reason}, S) -> + OldSubscribers = S#state.subscribers, + case lists:member(Pid, OldSubscribers) of + true -> + S2 = do_dict_delete({dict_delete, {subscriber, Pid}}, S), + {noreply, S2}; + false -> + ok = error_logger:format("~p(~p): handle_info(~p, ~p)~n", + [?MODULE, self(), Info, S]), + {noreply, S} + end; +handle_info(Info, S) -> + ok = error_logger:format("~p(~p): handle_info(~p, ~p)~n", + [?MODULE, self(), Info, S]), + {noreply, S}. + +listen_on_trace_port(Node, Port, S) -> + [_Name, Host] = string:tokens(atom_to_list(Node), [$@]), + case catch start_trace_client(self(), ip, {Host, Port}) of + {trace_client_pid, RemotePid} -> + rpc:call(Node, et_selector, change_pattern, [S#state.trace_pattern]), + link(RemotePid), + S2 = S#state{trace_nodes = [Node | S#state.trace_nodes], + trace_port = Port + 1}, + {noreply, S2}; + {'EXIT', Reason} when Reason == already_started-> + ok = error_logger:format("~p(~p): consumer ignored(~p:~p): ~p~n", + [?MODULE, self(), Node, Port, Reason]), + S2 = S#state{trace_port = Port + 1}, + {noreply, S2}; + {'EXIT', Reason} -> + self() ! {nodeup, Node}, + ok = error_logger:format("~p(~p): consumer retry(~p:~p):~n ~p~n", + [?MODULE, self(), Node, Port, Reason]), + S2 = S#state{trace_port = Port + 1}, + {noreply, S2} + end. + +%%---------------------------------------------------------------------- +%% Func: terminate/2 +%% Purpose: Shutdown the server +%% Returns: any (ignored by gen_server) +%%---------------------------------------------------------------------- + +terminate(Reason, S) -> + Fun = fun(Pid) -> exit(Pid, Reason) end, + lists:foreach(Fun, S#state.subscribers). + +%%---------------------------------------------------------------------- +%% Func: code_change/3 +%% Purpose: Convert process state when code is changed +%% Returns: {ok, NewState} +%%---------------------------------------------------------------------- + +code_change(_OldVsn, S, _Extra) -> + {ok, S}. + +%%%---------------------------------------------------------------------- +%%% Internal functions +%%%---------------------------------------------------------------------- + +do_clear_table(S) -> + OldTab = S#state.event_tab, + ets:delete(OldTab), + NewTab = ets:new(et_events, [ordered_set, {keypos, 1}, public]), + S#state{event_tab = NewTab}. + +do_dict_insert(Msg = {dict_insert, Key = {subscriber, Pid}, Val}, S) when pid(Pid) -> + OldSubscribers = S#state.subscribers, + NewSubscribers = + case lists:member(Pid, OldSubscribers) of + true -> + OldSubscribers; + false -> + link(Pid), + All = ets:match_object(S#state.dict_tab, '_'), + lists:foreach(fun({K, V}) -> Pid ! {et, {dict_insert, K, V}} end, All), + [Pid | OldSubscribers] + end, + do_multicast(NewSubscribers, Msg), + ets:insert(S#state.dict_tab, {Key, Val}), + S#state{subscribers = NewSubscribers}; +do_dict_insert(Msg = {dict_insert, Key, Val}, S) -> + do_multicast(S#state.subscribers, Msg), + ets:insert(S#state.dict_tab, {Key, Val}), + S. + +do_dict_delete(Msg = {dict_delete, Key = {subscriber, Pid}}, S) -> + OldSubscribers = S#state.subscribers, + do_multicast(OldSubscribers, Msg), + ets:delete(S#state.dict_tab, Key), + case lists:member(Pid, OldSubscribers) of + true -> + unlink(Pid), + S#state{subscribers = OldSubscribers -- [Pid]}; + false -> + S + end; +do_dict_delete({dict_delete, {filter, collector}}, S) -> + S; +do_dict_delete(Msg = {dict_delete, Key}, S) -> + do_multicast(S#state.subscribers, Msg), + ets:delete(S#state.dict_tab, Key), + S. + +tab_iterate(_Fun, _Tab, '$end_of_table', Acc) -> + Acc; +tab_iterate(Fun, Tab, Key, Acc) -> + Acc2 = lists:foldl(Fun, Acc, ets:lookup(Tab, Key)), + tab_iterate(Fun, Tab, ets:next(Tab, Key), Acc2). + +file_open(F) -> + Fd = make_ref(), + case F#file.file_opt of + write -> file:rename(F#file.name, F#file.name ++ ".OLD"); + append -> ignore + end, + Args = [{file, F#file.name}, {name, Fd}, + {repair, true}, {mode, read_write}], + case disk_log:open(Args) of + {ok, _} -> + {ok, Fd}; + {repaired, _, _, BadBytes} -> + ok = error_logger:format("~p: Skipped ~p bad bytes in file: ~p~n", + [?MODULE, BadBytes, F#file.name]), + {ok, Fd}; + {error,Reason} -> + {error,Reason} + end. + +parse_file_options(F, [H | T]) -> + case H of + existing -> parse_file_options(F#file{event_opt = existing} , T); + %%new -> parse_file_options(F#file{event_opt = new} , T); + all -> parse_file_options(F#file{event_opt = all} , T); + write -> parse_file_options(F#file{file_opt = write} , T); + append -> parse_file_options(F#file{file_opt = append} , T); + keep -> parse_file_options(F#file{table_opt = keep} , T); + clear -> parse_file_options(F#file{table_opt = clear} , T); + Bad -> {error, {bad_file_option, Bad}} + end; +parse_file_options(F, []) -> + {ok, F}. + +do_multicast([Pid | Pids], Msg) -> + Pid ! {et, Msg}, + do_multicast(Pids, Msg); +do_multicast([], _Msg) -> + ok. diff --git a/lib/et/src/et_contents_viewer.erl b/lib/et/src/et_contents_viewer.erl new file mode 100644 index 0000000000..29ca93ca64 --- /dev/null +++ b/lib/et/src/et_contents_viewer.erl @@ -0,0 +1,591 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2000-2009. 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_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 = collector, + filters = [#filter{name = collector, function = fun(E) -> E end}], + 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}}; + atom(Name) -> + case lists:keysearch(Name, #filter.name, Filters) of + {value, F} when 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 pid(ParentPid) -> + parse_opt(T, S#state{parent_pid = ParentPid}); + {viewer_pid, ViewerPid} when 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 record(Event, event) -> + parse_opt(T, S#state{event = Event}); + {active_filter, Name} when atom(Name) -> + parse_opt(T, S#state{active_filter = Name}); + F when record(F, filter), + atom(F#filter.name), + 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 integer(Width), Width > 0 -> + parse_opt(T, S#state{width = Width}); + {height, Height} when 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 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 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(collector, #filter.name, S#state.filters) of + {value, F} when 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 integer(Int), Int > 0, Int =< 9 -> + case catch lists:nth(Int, S#state.filters) of + F when 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 == collector-> + 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 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 integer(Mega), integer(Sec), 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 atom(Atom) -> + pad_string(atom_to_list(Atom), MinLen); +pad_string(String, MinLen) when 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 pid(ViewerPid) -> + ViewerPid ! {et, Event}; + undefined -> + ignore + end. diff --git a/lib/et/src/et_internal.hrl b/lib/et/src/et_internal.hrl new file mode 100644 index 0000000000..b6f84f5b4b --- /dev/null +++ b/lib/et/src/et_internal.hrl @@ -0,0 +1,23 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2002-2009. 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: Definition of internal data structures +%%---------------------------------------------------------------------- + +-record(filter, {name, function}). diff --git a/lib/et/src/et_selector.erl b/lib/et/src/et_selector.erl new file mode 100644 index 0000000000..845359622d --- /dev/null +++ b/lib/et/src/et_selector.erl @@ -0,0 +1,529 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2001-2009. 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: Define event transforms and trace patterns +%%---------------------------------------------------------------------- + +-module(et_selector). + +-export([ + make_pattern/1, + change_pattern/1, + parse_event/2 + ]). + +-include("../include/et.hrl"). + +%%---------------------------------------------------------------------- +%% make_pattern(RawPattern) -> TracePattern +%% +%% Makes a trace pattern suitable to feed change_pattern/1 +%% See also erlang:trace_pattern/2 for more info about its match_spec() +%% +%% RawPattern = detail_level() +%% TracePattern = erlang_trace_pattern_match_spec() +%% +%% detail_level() = min | max | integer(X) when X =< 0, X >= 100 +%% +%% min - minimum level of tracing (ignore calls to report_event/4,5) +%% max - maximum level of tracing (all calls to report_event/4,5) +%% integer() - explicit detail level of tracing +%%---------------------------------------------------------------------- + +make_pattern(undefined) -> + {undefined, undefined}; +make_pattern({Mod, Pattern}) when atom(Mod) -> + case Pattern of + min -> + {Mod, []}; + max -> + Head = ['$1', '_', '_', '_', '_'], + Body = [], + Cond = [], + {Mod, [{Head, Cond, Body}]}; + DetailLevel when integer(DetailLevel) -> + Head = ['$1', '_', '_', '_', '_'], + Body = [], + Cond = [{ '<', '$1', DetailLevel}], + {Mod, [{Head, Cond, Body}]}; + undefined -> + {Mod, undefined}; + _ -> + exit({bad_pattern, Pattern}) + end. + +%%---------------------------------------------------------------------- +%% change_pattern(Pattern) -> ok +%% +%% Activates/deactivates tracing by changing the current trace pattern +%% +%% Pattern = detail_level() | +%% empty_match_spec() | +%% erlang_trace_pattern_match_spec() +%% +%% detail_level() = min | max | integer(X) when X =<0, X >= 100 +%% empty_match_spec() = [] +%% +%% Min detail level deactivates tracing of calls to report_event/4,5 +%% +%% Max detail level activates tracing of all calls to report_event/4,5 +%% +%% integer(X) detail level activates tracing of all calls to +%% report_event/4,5 whose detail level argument is lesser than X. +%% +%% An empty match spec deactivates tracing of calls to report_event/4,5 +%% +%% Other match specs activates tracing of calls to report_event/4,5 +%% accordlingly with erlang:trace_pattern/2. +%%---------------------------------------------------------------------- + +change_pattern({Mod, Pattern}) when atom(Mod) -> + MFA = {Mod, report_event, 5}, + case Pattern of + undefined -> + ignore; + [] -> + error_to_exit(dbg:ctp(MFA)), + error_to_exit(dbg:p(all, clear)); + List when list(List) -> + error_to_exit(dbg:ctp(MFA)), + error_to_exit(dbg:tp(MFA, Pattern)), + error_to_exit(dbg:p(all, [call, timestamp])); + Other -> + change_pattern(make_pattern({Mod, Other})) + end, + ok. + +error_to_exit({error, Reason}) -> + exit(Reason); +error_to_exit({ok, Res}) -> + Res. + +%%---------------------------------------------------------------------- +%% parse_event(Mod, ValidTraceData) -> false | true | {true, Event} +%% +%% Transforms trace data and makes an event record out of it +%% +%% ValidTraceData = erlang_trace_data() | record(event) +%% Mod = module_name() | undefined +%% module_name() = atom() +%% +%% erlang_trace_data() = +%% +%% {trace, Pid, Label, Info} | +%% {trace, Pid, Label, Info, Extra} | +%% {trace_ts, Pid, Label, Info, ReportedTS} | +%% {trace_ts, Pid, Label, Info, Extra, ReportedTS} | +%% {seq_trace, Label, Info} | +%% {seq_trace, Label, Info, ReportedTS} | +%% {drop, NumberOfDroppedItems} +%% +%% See erlang:trace/3 for more info about the semantics of +%% the trace data. +%% +%% An event record consists of the following fields: +%% +%% detail_level - Noise has a high level as opposed to essentials. +%% trace_ts - Time when the trace was generated. +%% Same as event_ts if omitted in trace data. +%% event_ts - Time when the event record was created. +%% from - From actor, such as sender of a message. +%% to - To actor, such as receiver of message. +%% label - Label intended to provide a brief event summary. +%% contents - All nitty gritty details of the event. +%% +%% See et:report_event/4 and et:report_event/5 for details. +%% +%% Returns: +%% +%% {true, Event} - where Event is an #event{} record representing the +%% trace data +%% true - means that the trace data already is an event +%% record and that it is valid as it is. +%% No transformation is needed. +%% false - means that the trace data is uninteresting and +%% should be dropped +%%---------------------------------------------------------------------- + +parse_event(_Mod, E) when record(E, event) -> + true; +parse_event(Mod, Trace) -> + ParsedTS = erlang:now(), + case Trace of + {trace, Pid, Label, Info} -> + parse_event(Mod, Trace, ParsedTS, ParsedTS, Pid, Label, [Info]); + {trace, Pid, Label, Info, Extra} -> + parse_event(Mod, Trace, ParsedTS, ParsedTS, Pid, Label, [Info, Extra]); + {trace_ts, Pid, Label, Info, ReportedTS} -> + parse_event(Mod, Trace, ParsedTS, ReportedTS, Pid, Label, [Info]); + {trace_ts, Pid, Label, Info, Extra, ReportedTS} -> + parse_event(Mod, Trace, ParsedTS, ReportedTS, Pid, Label, [Info, Extra]); + {seq_trace, Label, Info} -> + parse_seq_event(Trace, ParsedTS, ParsedTS, Label, Info); + {seq_trace, Label, Info, ReportedTS} -> + parse_seq_event(Trace, ParsedTS, ReportedTS, Label, Info); + {drop, NumberOfDroppedItems} -> + DetailLevel = 20, + {true, #event{detail_level = DetailLevel, + trace_ts = ParsedTS, + event_ts = ParsedTS, + from = undefined, + to = undefined, + label = drop, + contents = [{label, drop}, + {detail_level, DetailLevel}, + {from, undefined}, + {to, undefined}, + {drop, NumberOfDroppedItems}]}}; + _ -> + error_logger:format("~p(~p): Ignoring unknown trace type -> ~p~n~n", + [?MODULE, ?LINE, Trace]), + false + end. + +parse_seq_event(Trace, ParsedTS, ReportedTS, Label, Info) -> + case Info of + {send, Serial, From, To, Msg} -> + DetailLevel = 15, + {true, #event{detail_level = DetailLevel, + trace_ts = ReportedTS, + event_ts = ParsedTS, + from = From, + to = To, + label = {seq_send, Label}, + contents = [{label, {seq_send, Label}}, + {detail_level, DetailLevel}, + {from, From}, + {to, To}, + {serial, Serial}, + {msg, Msg}]}}; + {'receive', Serial, From, To, Msg} -> + DetailLevel = 10, + {true, #event{detail_level = DetailLevel, + trace_ts = ReportedTS, + event_ts = ParsedTS, + from = From, + to = To, + label = {seq_receive, Label}, + contents = [{label, {seq_receive, Label}}, + {detail_level, DetailLevel}, + {from, From}, + {to, To}, + {serial, Serial}, + {msg, Msg}]}}; + {print, Serial, From, _, UserInfo} -> + DetailLevel = 5, + {true, #event{detail_level = DetailLevel, + trace_ts = ReportedTS, + event_ts = ParsedTS, + from = From, + to = From, + label = {seq_print, Label}, + contents = [{label, {seq_print, Label}}, + {detail_level, DetailLevel}, + {from, From}, + {to, From}, + {serial, Serial}, + {user_info, UserInfo}]}}; + _ -> + error_logger:format("~p(~p): Ignoring unknown trace type -> ~p~n~n", + [?MODULE, ?LINE, Trace]), + false + end. + +parse_event(Mod, Trace, ParsedTS, ReportedTS, From, Label, Contents) -> + case Label of + 'receive' -> + DetailLevel = 35, + [Msg] = Contents, + {true, #event{detail_level = DetailLevel, + trace_ts = ReportedTS, + event_ts = ParsedTS, + from = From, + to = From, + label = Label, + contents = [{label, Label}, + {detail_level, DetailLevel}, + {from, From}, + {to, From}, + {msg, Msg}]}}; + send -> + DetailLevel = 40, + [Msg, To] = Contents, + {true, #event{detail_level = DetailLevel, + trace_ts = ReportedTS, + event_ts = ParsedTS, + from = From, + to = To, + label = Label, + contents = [{label, Label}, + {detail_level, DetailLevel}, + {from, From}, + {to, To}, + {msg, Msg}]}}; + send_to_non_existing_process -> + DetailLevel = 40, + [Msg, To] = Contents, + {true, #event{detail_level = DetailLevel, + trace_ts = ReportedTS, + event_ts = ParsedTS, + from = From, + to = To, + label = Label, + contents = [{label, Label}, + {detail_level, DetailLevel}, + {from, From}, + {to, To}, + {msg, Msg}]}}; + call -> + case Contents of + [{M, report_event, [UserDetailLevel, UserFrom, UserTo, UserLabel, UserContents]}] when M == Mod, Mod /= undefined -> + {true, #event{detail_level = UserDetailLevel, + trace_ts = ReportedTS, + event_ts = ParsedTS, + from = UserFrom, + to = UserTo, + label = UserLabel, + contents = UserContents}}; % Term + [MFA] -> + DetailLevel = 45, + {true, #event{detail_level = DetailLevel, + trace_ts = ReportedTS, + event_ts = ParsedTS, + from = From, + to = From, + label = Label, + contents = [{label, Label}, + {detail_level, DetailLevel}, + {from, From}, + {to, From}, + {mfa, MFA}]}}; + [MFA, PamResult] -> + DetailLevel = 45, + {true, #event{detail_level = DetailLevel, + trace_ts = ReportedTS, + event_ts = ParsedTS, + from = From, + to = From, + label = Label, + contents = [{label, Label}, + {detail_level, DetailLevel}, + {from, From}, + {to, From}, + {mfa, MFA}, + {pam_result, PamResult}]}} + end; + return_to -> + DetailLevel = 50, + [MFA] = Contents, + {true, #event{detail_level = DetailLevel, + trace_ts = ReportedTS, + event_ts = ParsedTS, + from = From, + to = From, + label = Label, + contents = [{label, Label}, + {detail_level, DetailLevel}, + {from, From}, + {to, From}, + {mfa, MFA}]}}; + return_from -> + DetailLevel = 52, + [MFA, ReturnValue] = Contents, + {true, #event{detail_level = DetailLevel, + trace_ts = ReportedTS, + event_ts = ParsedTS, + from = From, + to = From, + label = Label, + contents = [{label, Label}, + {detail_level, DetailLevel}, + {from, From}, + {to, From}, + {mfa, MFA}, + {return, ReturnValue}]}}; + spawn -> + DetailLevel = 25, + [NewPid, MFA] = Contents, + {true, #event{detail_level = DetailLevel, + trace_ts = ReportedTS, + event_ts = ParsedTS, + from = From, + to = NewPid, + label = Label, + contents = [{label, Label}, + {detail_level, DetailLevel}, + {from, From}, + {to, NewPid}, + {mfa, MFA}]}}; % MFA | Term + exit -> + DetailLevel = 30, + [Reason] = Contents, + {true, #event{detail_level = DetailLevel, + trace_ts = ReportedTS, + event_ts = ParsedTS, + from = From, + to = From, + label = Label, + contents = [{label, Label}, + {detail_level, DetailLevel}, + {from, From}, + {to, From}, + {reason, Reason}]}}; + link -> + DetailLevel = 55, + [LinkTo] = Contents, + {true, #event{detail_level = DetailLevel, + trace_ts = ReportedTS, + event_ts = ParsedTS, + from = From, + to = LinkTo, + label = Label, + contents = [{label, Label}, + {detail_level, DetailLevel}, + {from, From}, + {to, LinkTo}]}}; + unlink -> + DetailLevel = 60, + [UnlinkFrom] = Contents, + {true, #event{detail_level = DetailLevel, + trace_ts = ReportedTS, + event_ts = ParsedTS, + from = From, + to = UnlinkFrom, + label = Label, + contents = [{label, Label}, + {detail_level, DetailLevel}, + {from, From}, + {to, UnlinkFrom}]}}; + getting_linked -> + DetailLevel = 65, + [LinkTo] = Contents, + {true, #event{detail_level = DetailLevel, + trace_ts = ReportedTS, + event_ts = ParsedTS, + from = From, + to = LinkTo, + label = Label, + contents = [{label, Label}, + {detail_level, DetailLevel}, + {from, From}, + {to, LinkTo}]}}; + getting_unlinked -> + DetailLevel = 67, + [UnlinkFrom] = Contents, + {true, #event{detail_level = DetailLevel, + trace_ts = ReportedTS, + event_ts = ParsedTS, + from = From, + to = UnlinkFrom, + label = Label, + contents = [{label, Label}, + {detail_level, DetailLevel}, + {from, From}, + {to, UnlinkFrom}]}}; + register -> + DetailLevel = 70, + [Name] = Contents, + {true, #event{detail_level = DetailLevel, + trace_ts = ReportedTS, + event_ts = ParsedTS, + from = From, + to = From, + label = Label, + contents = [{label, Label}, + {detail_level, DetailLevel}, + {from, From}, + {to, From}, + {name, Name}]}}; + unregister -> + DetailLevel = 75, + [Name] = Contents, + {true, #event{detail_level = DetailLevel, + trace_ts = ReportedTS, + event_ts = ParsedTS, + from = From, + to = From, + label = Label, + contents = [{label, Label}, + {detail_level, DetailLevel}, + {from, From}, + {to, From}, + {name, Name}]}}; + in -> + DetailLevel = 90, + [MFA] = Contents, + {true, #event{detail_level = DetailLevel, + trace_ts = ReportedTS, + event_ts = ParsedTS, + from = From, + to = From, + label = Label, + contents = [{label, Label}, + {detail_level, DetailLevel}, + {from, From}, + {to, From}, + {mfa, MFA}]}}; % MFA | 0 + out -> + DetailLevel = 95, + [MFA] = Contents, + {true, #event{detail_level = DetailLevel, + trace_ts = ReportedTS, + event_ts = ParsedTS, + from = From, + to = From, + label = Label, + contents = [{label, Label}, + {detail_level, DetailLevel}, + {from, From}, + {to, From}, + {mfa, MFA}]}}; % MFA | 0 + gc_start -> + DetailLevel = 80, + [GcKeyValueList] = Contents, + {true, #event{detail_level = DetailLevel, + trace_ts = ReportedTS, + event_ts = ParsedTS, + from = From, + to = From, + label = Label, + contents = [{label, Label}, + {detail_level, DetailLevel}, + {from, From}, + {to, From}, + {gc_items, GcKeyValueList}]}}; + gc_end -> + DetailLevel = 85, + [GcKeyValueList] = Contents, + {true, #event{detail_level = DetailLevel, + trace_ts = ReportedTS, + event_ts = ParsedTS, + from = From, + to = From, + label = Label, + contents = [{label, Label}, + {detail_level, DetailLevel}, + {from, From}, + {to, From}, + {gc_items, GcKeyValueList}]}}; + _ -> + error_logger:format("~p(~p): Ignoring unknown trace type -> ~p~n~n", + [?MODULE, ?LINE, Trace]), + false + end. diff --git a/lib/et/src/et_viewer.erl b/lib/et/src/et_viewer.erl new file mode 100644 index 0000000000..ede2c401eb --- /dev/null +++ b/lib/et/src/et_viewer.erl @@ -0,0 +1,1602 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2000-2009. 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 a sequence chart for trace events (messages/actions) +%%---------------------------------------------------------------------- + +-module(et_viewer). + +-behaviour(gen_server). + +%% External exports +-export([file/1, + start/0, + start/1, + start_link/1, + stop/1, + get_collector_pid/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"). + +-define(unknown, "UNKNOWN"). + +-record(state, + {parent_pid, % Pid of parent process + collector_pid, % Pid of collector process + event_order, % Field to be used as primary key + trace_pattern, % Collector trace pattern + active_filter, % Name of the active filter + filters, % List of possible filters + selected_actor, % Actor selected by user + first_event, % Key of first event (regardless of visibility) + last_event, % Key of last event (regardless of visibility) + max_events, % Maximum number of shown events + events, % Queue containg all event keys (regardless of visibility) + max_actors, % Maximum number of shown actors + actors, % List of known actors + refresh_needed, % Refresh is needed in order to show all actors + display_mode, % Display all or only matching actors + detail_level, % Show only events with lesser detail level + hide_actions, % Hide/show events where to == from actor (bool) + hide_unknown, % Hide/show events with unknown actor (bool) + is_suspended, % Suspend viewer updates (bool) + title, % GUI: Window title + win, % GUI: Window object + menubar, % GUI: Menu bar object + packer, % GUI: Packer object + width, % GUI: Window width + height, % GUI: Window height + scale, % GUI: Scaling factor on canvas + font, % GUI: Font to be used on text labels + canvas_width, % GUI: Canvas width + canvas_height, % GUI: Canvas height + canvas, % GUI: Canvas object + y_pos}). % GUI: Current y position on canvas + +-record(actor, {name, string}). + +-define(initial_x, 10). +-define(incr_x, 60). +-define(initial_y, 15). +-define(incr_y, 15). +-define(detail_level_min, 0). +-define(detail_level_max, 100). + +%%%---------------------------------------------------------------------- +%%% Client side +%%%---------------------------------------------------------------------- + +%%---------------------------------------------------------------------- +%% file(FileName) -> {ok, ViewerPid} | {error, Reason} +%% +%% Start a new event viewer and a corresponding collector +%% and load them with trace events from a trace file. +%% +%% FileName() = string() +%% ViewerPid = pid() +%% Reason = term() +%%---------------------------------------------------------------------- + +file(FileName) -> + start_link([{trace_client, {file, FileName}}]). + +%%---------------------------------------------------------------------- +%% start() -> ok +%% +%% Simplified start of a sequence chart viewer with +%% global tracing activated. +%% +%% Convenient to be used from the command line +%% (erl -s et_viewer) as both the viewer and collector +%% processes are unlinked from the calling process. +%%---------------------------------------------------------------------- + +start() -> + start([{trace_global, true}]). + +%%---------------------------------------------------------------------- +%% start(Options) -> {ok, ViewerPid} | {error, Reason} +%%---------------------------------------------------------------------- + +start(Options) -> + start_link([{parent_pid, undefined} | Options]). + +%%---------------------------------------------------------------------- +%% start_link(Options) -> {ok, ViewerPid} | {error, Reason} +%% +%% Start a sequence chart viewer for trace events (messages/actions) +%% +%% Options = [option() | collector_option()] +%% +%% option() = +%% {parent_pid, extended_pid()} | +%% {title, term()} | +%% {detail_level, detail_level()} | +%% {is_suspended, boolean()} | +%% {scale, integer()} | +%% {width, integer()} | +%% {height, integer()} | +%% {collector_pid, extended_pid()} | +%% {event_order, event_order()} | +%% {active_filter, atom()} | +%% {max_events, extended_integer()} | +%% {max_actors, extended_integer()} | +%% {trace_global, et_collector_trace_global()} | +%% {trace_pattern, et_collector_trace_pattern()} | +%% {trace_port, et_collector_trace_port()} | +%% {trace_max_queue, et_collector_trace_max_queue()} | +%% {trace_client, et_collector_trace_client()} | +%% {dict_insert, {filter, filter_name()}, event_filter_fun()} | +%% {dict_insert, et_collector_dict_key(), et_collector_dict_val()} | +%% {dict_delete, {filter, filter_name()}} | +%% {dict_delete, et_collector_dict_key()} | +%% {actors, actors()} | +%% {first_event, first_key()} | +%% {hide_unknown, boolean()} | +%% {hide_actions, boolean()} | +%% {display_mode, display_mode()} +%% +%% extended_pid() = pid() | undefined +%% detail_level() = min | max | integer(X) when X >=0, X =< 100 +%% event_order() = trace_ts | event_ts +%% extended_integer() = integer() | infinity +%% display_mode() = all | {search_actors, direction(), first_key(), actors()} +%% direction() = forward | reverse +%% first_key() = event_key() +%% actors() = [term()] +%% +%% filter_name() = atom() +%% filter_fun() = fun(Event) -> false | true | {true, NewEvent} +%% Event = NewEvent = record(event) +%% +%% ViewerPid = pid() +%% Reason = term() +%% +%% A filter_fun() takes an event record as sole argument +%% and returns false | true | {true, NewEvent}. +%%---------------------------------------------------------------------- + +start_link(Options) -> + case parse_opt(Options, default_state(), []) of + {ok, S, CollectorOpt} -> + case S#state.collector_pid of + CollectorPid when pid(CollectorPid) -> + case gen_server:start_link(?MODULE, [S], []) of + {ok, Pid} when S#state.parent_pid /= self() -> + unlink(Pid), + {ok, Pid}; + Other -> + Other + end; + undefined -> + case et_collector:start_link(CollectorOpt) of + {ok, CollectorPid} -> + S2 = S#state{collector_pid = CollectorPid}, + case gen_server:start_link(?MODULE, [S2], []) of + {ok, Pid} when S#state.parent_pid /= self() -> + unlink(Pid), + {ok, Pid}; + Other -> + Other + end; + {error, Reason} -> + {error, {et_collector, Reason}} + end + end; + {error, Reason} -> + {error, Reason} + end. + +default_state() -> + #state{parent_pid = self(), + collector_pid = undefined, + detail_level = ?detail_level_max, + active_filter = collector, + filters = [#filter{name = collector, function = fun(E) -> E end}], + event_order = trace_ts, + is_suspended = false, + max_events = 100, + first_event = first, + last_event = first, + events = queue_new(), + max_actors = 5, + actors = [create_actor(?unknown)], + selected_actor = ?unknown, + hide_actions = false, + hide_unknown = false, + refresh_needed = false, + display_mode = all, + scale = 2, + canvas_height = 0, + canvas_width = 0, + width = 800, + height = 600}. + +parse_opt([], S, CollectorOpt) -> + {ok, S, [{parent_pid, S#state.parent_pid} | CollectorOpt]}; +parse_opt([H | T], S, CollectorOpt) -> + case H of + {parent_pid, Parent} when Parent == undefined -> + CollectorOpt2 = [H | CollectorOpt], + parse_opt(T, S#state{parent_pid = Parent}, CollectorOpt2); + {parent_pid, Parent} when pid(Parent) -> + CollectorOpt2 = [H | CollectorOpt], + parse_opt(T, S#state{parent_pid = Parent}, CollectorOpt2); + {title, Title} -> + parse_opt(T, S#state{title = name_to_string(Title)}, CollectorOpt); + {detail_level, Level} when integer(Level), + Level >= ?detail_level_min, + Level =< ?detail_level_max -> + parse_opt(T, S#state{detail_level = Level}, CollectorOpt); + {detail_level, max} -> + parse_opt(T, S#state{detail_level = ?detail_level_max}, CollectorOpt); + {detail_level, min} -> + parse_opt(T, S#state{detail_level = ?detail_level_min}, CollectorOpt); + {is_suspended, true} -> + parse_opt(T, S#state{is_suspended = true}, CollectorOpt); + {is_suspended, false} -> + parse_opt(T, S#state{is_suspended = false}, CollectorOpt); + {scale, Scale} when integer(Scale), Scale > 0 -> + parse_opt(T, S#state{scale = Scale}, CollectorOpt); + {width, W} when integer(W), W > 0 -> + parse_opt(T, S#state{width = W, canvas_width = W}, CollectorOpt); + {height, WH} when integer(WH), WH > 0 -> + parse_opt(T, S#state{height = WH, canvas_height = WH}, CollectorOpt); + {collector_pid, Pid} when pid(Pid) -> + parse_opt(T, S#state{collector_pid = Pid}, CollectorOpt); + {collector_pid, undefined} -> + parse_opt(T, S#state{collector_pid = undefined}, CollectorOpt); + {active_filter, Name} when atom(Name) -> + parse_opt(T, S#state{active_filter = Name}, CollectorOpt); + {event_order, trace_ts} -> %% BUGBUG: Verify event_order with collector + CollectorOpt2 = [H | CollectorOpt], + parse_opt(T, S#state{event_order = trace_ts}, CollectorOpt2); + {event_order, event_ts} -> %% BUGBUG: Verify event_order with collector + CollectorOpt2 = [H | CollectorOpt], + parse_opt(T, S#state{event_order = event_ts}, CollectorOpt2); + {trace_port, _Port} -> + CollectorOpt2 = [H | CollectorOpt], + parse_opt(T, S, CollectorOpt2); + {trace_max_queue, _Queue} -> + CollectorOpt2 = [H | CollectorOpt], + parse_opt(T, S, CollectorOpt2); + {trace_pattern, _Pattern} -> + CollectorOpt2 = [H | CollectorOpt], + parse_opt(T, S, CollectorOpt2); + {trace_global, _Boolean} -> + CollectorOpt2 = [H | CollectorOpt], + parse_opt(T, S, CollectorOpt2); + {trace_client, _Client} -> + CollectorOpt2 = [H | CollectorOpt], + parse_opt(T, S, CollectorOpt2); + {dict_insert, {filter, Name}, Fun} -> + if + atom(Name), function(Fun) -> + F = #filter{name = Name, function = Fun}, + Filters = lists:keydelete(Name, #filter.name, S#state.filters), + CollectorOpt2 = [H | CollectorOpt], + parse_opt(T, S#state{filters = Filters ++ [F]}, CollectorOpt2); + true -> + {error, {bad_option, H}} + end; + {dict_insert, {subscriber, Pid}, _Val} -> + if + pid(Pid) -> + CollectorOpt2 = [H | CollectorOpt], + parse_opt(T, S, CollectorOpt2); + true -> + {error, {bad_option, H}} + end; + {dict_insert, _Key, _Val} -> + CollectorOpt2 = [H | CollectorOpt], + parse_opt(T, S, CollectorOpt2); + {dict_delete, {filter, Name}} -> + Filters = lists:keydelete(Name, #filter.name, S#state.filters), + CollectorOpt2 = [H | CollectorOpt], + parse_opt(T, S#state{filters = Filters}, CollectorOpt2); + {dict_delete, _Key} -> + CollectorOpt2 = [H | CollectorOpt], + parse_opt(T, S, CollectorOpt2); + {max_events, Max} when integer(Max), Max > 0-> + parse_opt(T, S#state{max_events = Max}, CollectorOpt); + {max_events, Max} when Max == infinity -> + parse_opt(T, S#state{max_events = Max}, CollectorOpt); + {max_actors, Max} when integer(Max), Max >= 0-> + parse_opt(T, S#state{max_actors = Max}, CollectorOpt); + {max_actors, Max} when Max == infinity -> + parse_opt(T, S#state{max_actors = Max}, CollectorOpt); + {actors, ActorNames} when list(ActorNames) -> + ActorNames2 = + case lists:member(?unknown, ActorNames) of + false -> [?unknown | ActorNames]; + true -> ActorNames + end, + Actors = [create_actor(Name) || Name <- ActorNames2], + parse_opt(T, S#state{actors = Actors}, CollectorOpt); + {first_event, First} -> + parse_opt(T, S#state{first_event = First}, CollectorOpt); + {hide_unknown, Bool} when Bool == false -> + parse_opt(T, S#state{hide_unknown = Bool}, CollectorOpt); + {hide_unknown, Bool} when Bool == true -> + parse_opt(T, S#state{hide_unknown = Bool}, CollectorOpt); + {hide_actions, Bool} when Bool == false -> + parse_opt(T, S#state{hide_actions = Bool}, CollectorOpt); + {hide_actions, Bool} when Bool == true -> + parse_opt(T, S#state{hide_actions = Bool}, CollectorOpt); + {display_mode, Mode = all} -> + parse_opt(T, S#state{display_mode = Mode}, CollectorOpt); + {display_mode, Mode = {search_actors, Dir, _Key, Actors}} when list(Actors), Dir == forward -> + parse_opt(T, S#state{display_mode = Mode}, CollectorOpt); + {display_mode, Mode = {search_actors, Dir, _Key, Actors}} when list(Actors), Dir == reverse -> + parse_opt(T, S#state{display_mode = Mode}, CollectorOpt); + + Bad -> + {error, {bad_option, Bad}} + end; +parse_opt(BadList, _S, _CollectorOpt) -> + {error, {bad_option_list, BadList}}. + +do_dict_insert({filter, Name}, Fun, S) when atom(Name), function(Fun) -> + F = #filter{name = Name, function = Fun}, + Filters = lists:keydelete(Name, #filter.name, S#state.filters), + Filters2 = lists:keysort(#filter.name, [F | Filters]), + gs:destroy(filter_menu), + create_filter_menu(S#state.active_filter, Filters2), + S#state{filters = Filters2}; +do_dict_insert(_Key, _Val, S) -> + %% ok = error_logger:format("~p(~p): handle_info({et, {dict_insert, ~p, ~p}})~n", + %% [?MODULE, self(), Key, Val]), + S. + +do_dict_delete({filter, Name}, S) when atom(Name), Name /= S#state.active_filter -> + Filters = lists:keydelete(Name, #filter.name, S#state.filters), + gs:destroy(filter_menu), + create_filter_menu(S#state.active_filter, Filters), + S#state{filters = Filters}; +do_dict_delete(_Key, S) -> + %% ok = error_logger:format("~p(~p): handle_info({et, {dict_delete, ~p}})~n", + %% [?MODULE, self(), Key]), + S. + +%%---------------------------------------------------------------------- +%% get_collector_pid(ViewerPid) -> CollectorPid +%% +%% Returns the identifier of the collector process +%% +%% ViewerPid = pid() +%% CollectorPid = pid() +%%---------------------------------------------------------------------- + +get_collector_pid(ViewerPid) -> + call(ViewerPid, get_collector_pid). + +%%---------------------------------------------------------------------- +%% stop(ViewerPid) -> ok +%% +%% Stops a viewer +%% +%% ViewerPid = pid() +%%---------------------------------------------------------------------- + +stop(ViewerPid) -> + call(ViewerPid, stop). + +call(ViewerPid, Request) -> + gen_server:call(ViewerPid, Request, infinity). + +%%%---------------------------------------------------------------------- +%%% Callback functions from gen_server +%%%---------------------------------------------------------------------- + +%%---------------------------------------------------------------------- +%% Func: init/1 +%% Returns: {ok, State} | +%% {ok, State, Timeout} | +%% ignore | +%% {stop, Reason} +%%---------------------------------------------------------------------- + +init([S]) when record(S, state) -> + process_flag(trap_exit, true), + InitialTimeout = 0, + case S#state.parent_pid of + undefined -> + ignore; + Pid when pid(Pid) -> + link(Pid) + end, + et_collector:dict_insert(S#state.collector_pid, + {subscriber, self()}, + ?MODULE), + {ok, create_main_window(S), InitialTimeout}. + +%%---------------------------------------------------------------------- +%% 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(get_collector_pid, _From, S) -> + Reply = S#state.collector_pid, + reply(Reply, S); +handle_call(stop, _From, S) -> + gs:destroy(S#state.win), + {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({et, {insert_actors, ActorNames}}, S) when list(ActorNames) -> + Fun = fun(N, Actors) -> + case lists:keymember(N, #actor.name, Actors) of + true -> Actors; + false -> Actors ++ [create_actor(N)] + end + end, + Actors = lists:foldl(Fun, S#state.actors, ActorNames), + S2 = refresh_main_window(S#state{actors = Actors}), + noreply(S2); +handle_info({et, {delete_actors, ActorNames}}, S) when list(ActorNames)-> + Fun = fun(N, Actors) when N == ?unknown -> + Actors; + (N, Actors) -> + lists:keydelete(N, #actor.name, Actors) + end, + New = lists:foldl(Fun, S#state.actors, ActorNames), + S2 = refresh_main_window(S#state{actors = New}), + noreply(S2); +handle_info({et, {dict_insert, Key, Val}}, S) -> + S2 = do_dict_insert(Key, Val, S), + noreply(S2); +handle_info({et, {dict_delete, Key}}, S) -> + S2 = do_dict_delete(Key, S), + noreply(S2); +handle_info({et, first}, S) -> + S2 = scroll_first(S), + noreply(S2); +handle_info({et, prev}, S) -> + S2 = scroll_prev(S), + noreply(S2); +handle_info({et, next}, S) -> + S2 = scroll_next(S), + noreply(S2); +handle_info({et, last}, S) -> + S2 = scroll_last(S), + noreply(S2); +handle_info({et, refresh}, S) -> + S2 = refresh_main_window(S), + noreply(S2); +handle_info({et, {display_mode, Mode}}, S) -> + S2 = change_display_mode(Mode, S), + noreply(S2); +handle_info({et, close}, S) -> + gs:destroy(S#state.win), + {stop, shutdown, S}; +handle_info({gs, Button, click, Data, Other} = Click, S) -> + CollectorPid = S#state.collector_pid, + case Button of + close -> + gs:destroy(S#state.win), + {stop, shutdown, S}; + suspended -> + case Other of + [_Text, _Group, Bool | _] when Bool == true -> + S2 = do_suspend(S), + noreply(S2); + [_Text, _Group, Bool | _] when Bool == false -> + S2 = do_resume(S), + noreply(S2); + _ -> + click_error(Click, S), + noreply(S) + end; + hide_actions -> + case Other of + [_Text, _Group, Bool | _] when Bool == true -> + S2 = refresh_main_window(S#state{hide_actions = Bool}), + noreply(S2); + [_Text, _Group, Bool | _] when Bool == false -> + S2 = refresh_main_window(S#state{hide_actions = Bool}), + noreply(S2); + _ -> + click_error(Click, S), + noreply(S) + end; + hide_unknown -> + case Other of + [_Text, _Group, Bool | _] when Bool == true -> + S2 = refresh_main_window(S#state{hide_unknown = Bool}), + noreply(S2); + [_Text, _Group, Bool | _] when Bool == false -> + S2 = refresh_main_window(S#state{hide_unknown = Bool}), + noreply(S2); + _ -> + click_error(Click, S), + noreply(S) + end; + up -> + S2 = scroll_up(S), + noreply(S2); + down -> + S2 = scroll_down(S), + noreply(S2); + first -> + S2 = scroll_first(S), + noreply(S2); + prev -> + S2 = scroll_prev(S), + noreply(S2); + next -> + S2 = scroll_next(S), + noreply(S2); + last -> + S2 = scroll_last(S), + noreply(S2); + refresh -> + S2 = refresh_main_window(S), + noreply(S2); + {display_mode, Mode} -> + S2 = change_display_mode(Mode, S), + noreply(S2); + close_all -> + close_all(S); + close_all_others -> + close_all_others(S); + first_all -> + et_collector:multicast(CollectorPid, first), + noreply(S); + prev_all -> + et_collector:multicast(CollectorPid, prev), + noreply(S); + next_all -> + et_collector:multicast(CollectorPid, next), + noreply(S); + last_all -> + et_collector:multicast(CollectorPid, last), + noreply(S); + refresh_all -> + et_collector:multicast(CollectorPid, refresh), + noreply(S); + clear_all -> + et_collector:clear_table(CollectorPid), + et_collector:multicast(CollectorPid, refresh), + noreply(S); + load_all -> + et_collector:start_trace_client(CollectorPid, event_file, "et_viewer.log"), + noreply(S); + save_all -> + et_collector:save_event_file(CollectorPid, + "et_viewer.log", + [existing, write, keep]), + noreply(S); + {open_viewer, Scale} -> + Actors = [A#actor.name || A <- S#state.actors], + open_viewer(Scale, S#state.active_filter, Actors, S), + noreply(S); + _Level when Data == detail_level, integer(hd(Other)), + hd(Other) >= ?detail_level_min, + hd(Other) =< ?detail_level_max -> + S2 = S#state{detail_level = hd(Other)}, + noreply(S2); + _PopupMenuItem when record(Data, filter) -> + open_viewer(S#state.scale, Data#filter.name, [?unknown], S), + noreply(S); + _ -> + click_error(Click, S), + noreply(S) + end; +handle_info({gs, _Obj, destroy,_, _}, S) -> + gs:destroy(S#state.win), + {stop, shutdown, S}; +handle_info({gs, _Obj, buttonpress, _, [_Button, X, Y | _]}, S) -> + S3 = + case y_to_n(Y, S) of + actor -> + %% Actor click + case S#state.actors of + [] -> + S; + _ -> + N = x_to_n(X, S), + A = lists:nth(N, S#state.actors), + S#state{selected_actor = A} + end; + {event, N} -> + %% Event click + List = queue_to_list(S#state.events), + S2 = S#state{events = list_to_queue(List)}, + + Key = lists:nth(N, List), + Pid = S#state.collector_pid, + Fun = fun create_contents_window/2, + case et_collector:iterate(Pid, Key, -1) of + Prev when Prev == Key -> + et_collector:iterate(Pid, first, 1, Fun, S2); + Prev -> + et_collector:iterate(Pid, Prev, 1, Fun, S2) + end + end, + noreply(S3); +handle_info({gs, _Obj, buttonrelease, _, [_Button, X, Y | _]}, S) -> + S2 = + case y_to_n(Y, S) of + actor -> + %% Actor click + case S#state.actors of + [] -> + S; + Actors -> + N = x_to_n(X, S), + New = lists:nth(N, S#state.actors), + Old = S#state.selected_actor, + case New#actor.name == Old#actor.name of + true -> + A = S#state.selected_actor, + toggle_search_for_actor(A#actor.name, S); + false -> + move_actor(Old, New, Actors, S) + end + end; + {event, _N} -> + %% Event click ignored + S + end, + noreply(S2); +handle_info({gs, _Obj, keypress, _, [KeySym, _Keycode, _Shift, _Control | _]} = Key, S) -> + case KeySym of + 'c' -> + close_all_others(S); + 'C' -> + close_all(S); + 'Up' -> + S2 = scroll_up(S), + noreply(S2); + 'Down' -> + S2 = scroll_down(S), + noreply(S2); + 'f' -> + S2 = scroll_first(S), + noreply(S2); + 'p' -> + S2 = scroll_prev(S), + noreply(S2); + 'Prior' -> + S2 = scroll_prev(S), + noreply(S2); + 'n' -> + S2 = scroll_next(S), + noreply(S2); + 'Next' -> + S2 = scroll_next(S), + noreply(S2); + 'l' -> + S2 = scroll_last(S), + noreply(S2); + 'r' -> + S2 = refresh_main_window(S), + noreply(S2); + 'F' -> + et_collector:multicast(S#state.collector_pid, first), + noreply(S); + 'P' -> + et_collector:multicast(S#state.collector_pid, prev), + noreply(S); + 'N' -> + et_collector:multicast(S#state.collector_pid, next), + noreply(S); + 'L' -> + et_collector:multicast(S#state.collector_pid, last), + noreply(S); + 'R' -> + et_collector:multicast(S#state.collector_pid, refresh), + noreply(S); + + 'a' -> + S2 = S#state{display_mode = all}, + S3 = refresh_main_window(S2), + noreply(S3); + + 'equal' -> + Scale = S#state.scale, + Actors = [A#actor.name || A <- S#state.actors], + open_viewer(Scale, S#state.active_filter, Actors, S), + noreply(S); + 'plus' -> + Scale = S#state.scale + 1, + Actors = [A#actor.name || A <- S#state.actors], + open_viewer(Scale, S#state.active_filter, Actors, S), + noreply(S); + 'minus' -> + case S#state.scale of + 1 -> + gs:config(S#state.canvas, beep); + Scale -> + Actors = [A#actor.name || A <- S#state.actors], + open_viewer(Scale - 1, S#state.active_filter, Actors, S) + end, + noreply(S); + 0 -> + case lists:keysearch(collector, #filter.name, S#state.filters) of + {value, F} when record(F, filter) -> + open_viewer(S#state.scale, F#filter.name, [?unknown], S); + false -> + gs:config(S#state.canvas, beep) + end, + noreply(S); + Int when integer(Int), Int > 0, Int =< 9 -> + case catch lists:nth(Int, S#state.filters) of + F when record(F, filter) -> + open_viewer(S#state.scale, F#filter.name, [?unknown], S); + {'EXIT', _} -> + gs:config(S#state.canvas, beep) + end, + noreply(S); + + 'Shift_L' -> + noreply(S); + 'Shift_R' -> + noreply(S); + 'Caps_Lock' -> + noreply(S); + + _ -> + click_error(Key, S), + 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(timeout, S) -> + Try = + case S#state.display_mode of + {search_actors, reverse, _, _} -> + -10; + _ -> + 10 + end, + if + S#state.is_suspended == true -> + {noreply, S, infinity}; + S#state.max_events == infinity -> + display_more_events(Try, S); + true -> + Needed = S#state.max_events - queue_length(S#state.events), + if + Needed =< 0 -> {noreply, S, infinity}; + Needed > 10 -> display_more_events(Try, S); + Needed =< 10 -> display_more_events(Needed, S) + end + end; + +handle_info({'EXIT', Pid, Reason}, S) -> + if + Pid == S#state.collector_pid -> + unlink(Pid), + gs:destroy(S#state.win), + {stop, Reason, S}; + Pid == S#state.parent_pid -> + unlink(Pid), + gs:destroy(S#state.win), + {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 suspend/resume +%%%---------------------------------------------------------------------- + +reply(Reply, S) -> + case queue_length(S#state.events) of + _ when S#state.is_suspended == true -> + {reply, Reply, S, infinity}; + _ when S#state.max_events == infinity -> + {reply, Reply, S, 500}; + N when N >= S#state.max_events -> + {reply, Reply, S, infinity}; + _ -> + {reply, Reply, S, 0} + end. + +noreply(S) -> + case queue_length(S#state.events) of + _ when S#state.is_suspended == true -> + {noreply, S, infinity}; + _ when S#state.max_events == infinity -> + {noreply, S, 500}; + N when N >= S#state.max_events -> + {noreply, S, infinity}; + _ -> + {noreply, S, 0} + end. + +do_suspend(S) -> + config_suspend(S#state{is_suspended = true}). + +do_resume(S) -> + config_suspend(S#state{is_suspended = false}). + +config_suspend(S) -> + Suspended = S#state.is_suspended, + gs:config(refresh, [{enable, not Suspended}]), + gs:config(refresh_all, [{enable, not Suspended}]), + gs:config(clear_all, [{enable, not Suspended}]), + S. + +refresh_main_window(S) -> + Pid = S#state.collector_pid, + Key = S#state.first_event, + case et_collector:iterate(Pid, Key, -1) of + Prev when Prev == Key -> + scroll_first(S); + _Prev -> + S2 = S#state{last_event = S#state.first_event}, + clear_canvas(S2) + end. + +scroll_first(S) -> + S2 = S#state{first_event = first, last_event = first}, + clear_canvas(S2). + +scroll_prev(S) -> + Try = + case S#state.max_events of + infinity -> -10; + Max -> -Max + end, + Key = et_collector:iterate(S#state.collector_pid, S#state.first_event, Try), + S2 = S#state{first_event = Key, last_event = Key}, + clear_canvas(S2). + +scroll_next(S) -> + S2 = S#state{first_event = S#state.last_event}, + clear_canvas(S2). + +scroll_up(S) -> + Key = et_collector:iterate(S#state.collector_pid, S#state.first_event, -5), + S2 = S#state{first_event = Key, last_event = Key}, + clear_canvas(S2). + +scroll_down(S) -> + Key = et_collector:iterate(S#state.collector_pid, S#state.first_event, 5), + S2 = S#state{first_event = Key, last_event = Key}, + clear_canvas(S2). + +scroll_last(S) -> + S2 = S#state{first_event = last, last_event = last}, + clear_canvas(S2). + +change_display_mode(Mode, S) -> + case Mode of + all -> + S2 = S#state{display_mode = Mode}, + refresh_main_window(S2); + {search_actors, _Dir, _Key, []} -> + S2 = S#state{display_mode = all}, + refresh_main_window(S2); + {search_actors, _Dir, Key, Actors} when list(Actors) -> + Pid = S#state.collector_pid, + Prev = et_collector:iterate(Pid, Key, -1), + S2 = S#state{first_event = Prev, + last_event = Prev, + display_mode = Mode}, + clear_canvas(S2) + end. + +close_all(S) -> + et_collector:multicast(S#state.collector_pid, close), + timer:sleep(timer:seconds(1)), + spawn(et_collector, stop, [S#state.collector_pid]), + gs:destroy(S#state.win), + {stop, shutdown, S}. + +close_all_others(S) -> + Fun = + fun({{subscriber, Pid}, _}) -> + if + Pid == self() -> + ignore; + true -> + unlink(Pid), + Pid ! {et, close} + end + end, + All = et_collector:dict_match(S#state.collector_pid, + {{subscriber, '_'}, '_'}), + lists:foreach(Fun, All), + noreply(S). + +click_error(Click, S) -> + gs:config(S#state.canvas, beep), + io:format("~p: ignored: ~p~n", [?MODULE, Click]). + +%%%---------------------------------------------------------------------- +%%% Clone viewer +%%%---------------------------------------------------------------------- + +open_viewer(Scale, FilterName, Actors, S) -> + Filters = [{dict_insert, {filter, F#filter.name}, F#filter.function} + || F <- S#state.filters], + Options = + [{parent_pid, S#state.parent_pid}, + {title, S#state.title}, + {collector_pid, S#state.collector_pid}, + {is_suspended, S#state.is_suspended}, + {detail_level, S#state.detail_level}, + {active_filter, FilterName}, + {event_order, S#state.event_order}, + {first_event, S#state.first_event}, + {max_events, S#state.max_events}, + {max_actors, S#state.max_actors}, + {hide_actions, S#state.hide_actions}, + {hide_unknown, S#state.hide_unknown}, + {is_suspended, S#state.is_suspended}, + {actors, Actors}, + {scale, Scale}, + {width, S#state.width}, + {height, S#state.height} | Filters], + case start_link(Options) of + {ok, ViewerPid} -> + unlink(ViewerPid), + ok; + {error, Reason} -> + ok = error_logger:format("~p: Failed to start a new window: ~p~n", + [?MODULE, Reason]) + end. + +%%%---------------------------------------------------------------------- +%%% Handle graphics +%%%---------------------------------------------------------------------- + +create_main_window(S) -> + Font = select_font(S#state.scale), + GS = gs:start(), + Name = name_to_string(S#state.active_filter), + Title = case S#state.title of + undefined -> atom_to_list(?MODULE); + Explicit -> name_to_string(Explicit) + end, + WinOpt = [{title, Title ++ " (filter: " ++ Name ++ ")"}, + {configure, true}, + {width, S#state.width}, + {height, S#state.height}], + Win = gs:window(GS, WinOpt), + Bar = gs:menubar(Win, []), + + create_file_menu(Bar), + create_viewer_menu(Bar), + create_collector_menu(Bar), + gs:menubutton(filter_button, Bar, [{label, {text, "Filter"}}]), + create_filter_menu(S#state.active_filter, S#state.filters), + create_help_menu(Bar), + + config_suspend(S), + + PackerOpt = [{packer_x, [{fixed, 5}, {fixed, 40}, {fixed, 40}, + {stretch, 1}, {fixed, 5}]}, + {packer_y, [{fixed, 30}, {fixed, 30}, + {stretch, 1}, {fixed, 30}]}, + {x, 0}, {y, 30}], + Packer = gs:frame(Win, PackerOpt), + gs:checkbutton(suspended, Packer, [{label,{text,"Freeze"}}, + {x, 10}, {y, 0}, + {width, 120}, {align, w}, + {select, S#state.is_suspended}]), + gs:checkbutton(hide_actions, Packer, [{label,{text,"Hide From=To"}}, + {x, 10}, {y, 20}, + {width, 120}, {align, w}, + {select, S#state.hide_actions}]), + gs:checkbutton(hide_unknown, Packer, [{label,{text,"Hide Unknown"}}, + {x, 10}, {y, 40}, + {width, 120}, {align, w}, + {select, S#state.hide_unknown}]), + gs:scale(Packer, [{text,"Detail Level"}, + {range, {?detail_level_min, ?detail_level_max}}, + {orient, horizontal}, + {x, 150}, {y, 0}, {height, 65}, {width, 200}, + {pos, S#state.detail_level}, {data, detail_level}]), + CanvasW = calc_canvas_width(S), + CanvasH = calc_canvas_height(S), + CanOpt = [{pack_xy, {{2, 4}, 3}}, {vscroll, right}, {hscroll, bottom}, + {scrollregion, {2, 2, CanvasW, CanvasH}}], + Canvas = gs:canvas(Packer, CanOpt), + gs:config(Canvas, [{buttonpress, true}, {buttonrelease, true}]), + gs:config(Packer, [{width, S#state.width}, {height, S#state.height}]), + gs:config(Win, [{map, true}, {keypress, true}]), + S2 = S#state{title = Title, + win = Win, font = Font, packer = Packer, + canvas_width = CanvasW, canvas_height = CanvasH, + canvas = Canvas, + y_pos = ?initial_y * S#state.scale}, + draw_all_actors(S2). + +select_font(Scale) when integer(Scale) -> + case Scale of + 1 -> {courier, 7}; + 2 -> {courier, 10}; + 3 -> {courier, 12}; + 4 -> {courier, 14}; + S -> {courier, S * 4} + end. + +create_file_menu(Bar) -> + Button = gs:menubutton(Bar, [{label, {text, "File"}}]), + Menu = gs:menu(Button, []), + gs:menuitem(close_all, Menu, [{label, {text, "Close Collector and all Viewers (C) "}}]), + gs:menuitem(close_all_others, Menu, [{label, {text, "Close other Viewers, but keep Collector (c)"}}]), + gs:menuitem(close, Menu, [{label, {text, "Close this Viewer, but keep Collector"}}]), + gs:menuitem(Menu, [{itemtype, separator}]), + + gs:menuitem(clear_all, Menu, [{label, {text, "Clear Collector"}}]), + gs:menuitem(load_all, Menu, [{label, {text, "Load Collector from the file \"et_viewer.log\""}}]), + gs:menuitem(save_all, Menu, [{label, {text, "Save Collector to the file \"et_viewer.log\""}}]). + +create_viewer_menu(Bar) -> + Button = gs:menubutton(Bar, [{label, {text, "Viewer"}}]), + Menu = gs:menu(Button, []), + gs:menuitem(Menu, [{label, {text, "Scroll this Viewer"}}, {bg, lightblue}, {enable,false}]), + gs:menuitem(Menu, [{itemtype, separator}]), + gs:menuitem(first, Menu, [{label, {text, "First (f)"}}]), + gs:menuitem(prev, Menu, [{label, {text, "Prev (p)"}}]), + gs:menuitem(next, Menu, [{label, {text, "Next (n)"}}]), + gs:menuitem(last, Menu, [{label, {text, "Last (l)"}}]), + gs:menuitem(refresh, Menu, [{label, {text, "Refresh (r)"}}]), + gs:menuitem(Menu, [{itemtype, separator}]), + gs:menuitem(up, Menu, [{label, {text, "Up 5 (Up)"}}]), + gs:menuitem(down, Menu, [{label, {text, "Down 5 (Down)"}}]), + gs:menuitem(Menu, [{itemtype, separator}]), + gs:menuitem(Menu, [{label, {text, "Search in this Viewer"}}, {bg, lightblue}, {enable,false}]), + gs:menuitem(Menu, [{itemtype, separator}]), + gs:menuitem({mode, all}, Menu, [{label, {text, "Abort search. Display all (a)"}}]). + +create_collector_menu(Bar) -> + Button = gs:menubutton(Bar, [{label, {text, "Collector"}}]), + Menu = gs:menu(Button, []), + gs:menuitem(Menu, [{label, {text, "Scroll all Viewers"}}, {bg, lightblue}, {enable,false}]), + gs:menuitem(Menu, [{itemtype, separator}]), + gs:menuitem(first_all, Menu, [{label, {text, "First (F)"}}]), + gs:menuitem(prev_all, Menu, [{label, {text, "Prev (P)"}}]), + gs:menuitem(next_all, Menu, [{label, {text, "Next (N)"}}]), + gs:menuitem(last_all, Menu, [{label, {text, "Last (L)"}}]), + gs:menuitem(refresh_all, Menu, [{label, {text, "Refresh (R)"}}]). + +create_filter_menu(ActiveFilterName, Filters) -> + Menu = gs:menu(filter_menu, filter_button, []), + Item = fun(F, N) when F#filter.name == collector -> + Label = lists:concat([pad_string(F#filter.name, 20), "(0)"]), + gs:menuitem(Menu, [{label, {text, Label}}, {data, F}]), + N + 1; + (F, N) -> + Label = lists:concat([pad_string(F#filter.name, 20), "(", N, ")"]), + gs:menuitem(Menu, [{label, {text, Label}}, {data, F}]), + N + 1 + end, + gs:menuitem(Menu, [{label, {text, "Same Filter New Scale"}}, {bg, lightblue}, {enable,false}]), + gs:menuitem(Menu, [{itemtype, separator}]), + {value, Filter} = lists:keysearch(ActiveFilterName, #filter.name, Filters), + Same = lists:concat([pad_string(ActiveFilterName, 20), "(=)"]), + Larger = lists:concat([pad_string(ActiveFilterName, 20), "(+)"]), + Smaller = lists:concat([pad_string(ActiveFilterName, 20), "(-)"]), + gs:menuitem(Menu, [{label, {text, Same}}, {data, Filter}]), + gs:menuitem(Menu, [{label, {text, Smaller}}, {data, Filter}]), + gs:menuitem(Menu, [{label, {text, Larger}}, {data, Filter}]), + gs:menuitem(Menu, [{itemtype, separator}]), + gs:menuitem(Menu, [{label, {text, "New Filter Same Scale"}}, {bg, lightblue}, {enable,false}]), + gs:menuitem(Menu, [{itemtype, separator}]), + lists:foldl(Item, 1, Filters). + +create_help_menu(Bar) -> + Button = gs:menubutton(Bar, [{label, {text, "Help"}}]), + Menu = gs:menu(Button, []), + gs:menuitem(Menu, [{label, {text, "Display details of an event"}}, + {bg, lightblue}, {enable,false}]), + gs:menuitem(Menu, [{label, {text, " Single click on the name tag or the arrow (Mouse-1)"}}, + {enable,false}]), + gs:menuitem(Menu, [{itemtype, separator}]), + gs:menuitem(Menu, [{label, {text, "Toggle actor search"}}, + {bg, lightblue}, {enable,false}]), + gs:menuitem(Menu, [{label, {text, " Single click on the name tag (Mouse-1)"}}, + {enable,false}]), + gs:menuitem(Menu, [{itemtype, separator}]), + gs:menuitem(Menu, [{label, {text, "Move actor"}}, + {bg, lightblue}, {enable,false}]), + gs:menuitem(Menu, [{label, {text, " se drag and drop on name tag (Mouse-1)"}}, + {enable,false}]). + +clear_canvas(S) -> + gs:destroy(S#state.canvas), + CanvasW = calc_canvas_width(S), + CanvasH = calc_canvas_height(S), + CanOpt = [{pack_xy, {{2, 4}, 3}}, {vscroll, right}, {hscroll, bottom}, + {scrollregion, {2, 2, CanvasW, CanvasH}}], + Canvas = gs:canvas(S#state.packer, CanOpt), + gs:config(S#state.packer, [{width, S#state.width}, {height, S#state.height}]), + gs:config(Canvas, [{buttonpress, true}, {buttonrelease, true}]), + S2 = S#state{refresh_needed = false, + y_pos = ?initial_y * S#state.scale, + canvas = Canvas, + canvas_width = CanvasW, + canvas_height = CanvasH, + events = queue_new()}, + draw_all_actors(S2). + +calc_canvas_width(S) -> + Min = calc_min_actors(S), + CanvasW = ((2 * ?initial_x) + (Min * ?incr_x)) * S#state.scale, + lists:max([CanvasW, S#state.width - (15 * S#state.scale), S#state.canvas_width]). + +calc_canvas_height(S) -> + Min = calc_min_events(S), + CanvasH = ((2 * ?initial_y) + (Min * ?incr_y)) * S#state.scale, + lists:max([CanvasH, S#state.height - (4 * 30), S#state.canvas_height]). + +calc_min_actors(S) -> + Max = S#state.max_actors, + N = length(S#state.actors), + if + Max == infinity -> + N * 2; + Max < N -> + N; + true -> + Max + end. + +calc_min_events(S) -> + Max = S#state.max_events, + N = queue_length(S#state.events), + if + Max == infinity -> + N * 2; + Max < N -> + N; + true -> + Max + end. + +display_more_events(Try, S) -> + Name = S#state.active_filter, + {value, F} = lists:keysearch(Name, #filter.name, S#state.filters), + FilterFun = F#filter.function, + Fun = fun(Event, State) -> + case catch FilterFun(Event) of + true -> + State2 = ensure_key(Event, State), + opt_display_event(Event, State2); + {true, Event2} -> + State2 = ensure_key(Event2, State), + opt_display_event(Event2, State2); + false -> + ensure_key(Event, State); + Bad -> + Contents = {bad_filter, Name, Bad, Event}, + Event2 = Event#event{contents = Contents, + from = bad_filter, + to = bad_filter}, + State2 = ensure_key(Event2, State), + opt_display_event(Event2, State2) + end + end, + Pid = S#state.collector_pid, + S2 = et_collector:iterate(Pid, S#state.last_event, Try, Fun, S), + case queue_length(S2#state.events) - queue_length(S#state.events) of + Diff when Diff == Try -> + %% Got as much as requested, look for more + %% io:format("Done: ~p~n", [{Try, Diff}]), + {noreply, S2, 0}; + _Diff when S2#state.first_event == S#state.first_event, + S2#state.last_event == S#state.last_event -> + %% Got lesser than requested, wait a while before looking for more + %% io:format("More: ~p~n", [{Try, Diff}]), + {noreply, S2, 500}; + _Diff -> + %% Got lesser than requested, look for more + %% io:format("More2: ~p~n", [{Try, Diff}]), + {noreply, S2, 0} + end. + +ensure_key(E, S) when record(E, event), record(S, state) -> + Key = et_collector:make_key(S#state.event_order, E), + case S#state.first_event of + first -> + S#state{first_event = Key, last_event = Key}; + last -> + S#state{first_event = Key, last_event = Key}; + _ -> + S#state{last_event = Key} + end. + +opt_display_event(E, S) -> + case S#state.display_mode of + all -> + display_event(E, S); + {search_actors, _Dir, _FirstKey, Actors} -> + %% Key = S#state.last_event, + From = select_actor_name(E#event.from, S), + case lists:member(From, Actors) of + true -> + display_event(E, S); + false -> + To = select_actor_name(E#event.to, S), + case lists:member(To, Actors) of + true -> + display_event(E, S); + false -> + S + end + end + end. + +select_actor_name(Name, S) -> + case lists:keymember(Name, #actor.name, S#state.actors) of + true -> Name; + false -> ?unknown + end. + +display_event(E, S) when E#event.detail_level < S#state.detail_level -> + {FromRefresh, From} = ensure_actor(E#event.from, S), + {FromName, FromPos, S2} = From, + {ToRefresh, To} = ensure_actor(E#event.to, S2), + {ToName, ToPos, S3} = To, + if + FromRefresh /= false, ToRefresh /= false -> + Key = S#state.last_event, + refresh_beep(S), + S3#state{refresh_needed = true, + events = queue_in(Key, S3#state.events)}; + FromName == ToName -> + case S#state.hide_actions of + true -> + S3; + false -> + Label = name_to_string(E#event.label), + draw_named_arrow(Label, FromName, FromPos, ToName, ToPos, S3) + end; + true -> + Label = name_to_string(E#event.label), + draw_named_arrow(Label, FromName, FromPos, ToName, ToPos, S3) + end; +display_event(_, S) -> + S. + +draw_named_arrow(Label, FromName, FromPos, ToName, ToPos, S) -> + Key = S#state.last_event, + case S#state.y_pos + (?incr_y * S#state.scale) of + _ when S#state.hide_unknown == true, FromName == ?unknown -> + S; + _ when S#state.hide_unknown == true, ToName == ?unknown -> + S; + Y when Y > S#state.canvas_height -> + refresh_beep(S), + S#state{refresh_needed = true, + events = queue_in(Key, S#state.events)}; + Y -> + S2 = S#state{y_pos = Y, events = queue_in(Key, S#state.events)}, + S3 = draw_arrow(FromPos, ToPos, S2), + draw_label(Label, FromName, ToName, FromPos, ToPos, S3) + end. + +refresh_beep(S) -> + case S#state.refresh_needed of + false -> + gs:config(S#state.canvas, beep), + gs:config(S#state.canvas, beep), + gs:config(S#state.canvas, beep); + true -> + ignore + end. + +draw_arrow(Pos, Pos, S) -> + S; +draw_arrow(FromPos, ToPos, S) -> + Y = S#state.y_pos, + CanOpts = [{coords, [{FromPos , Y}, {ToPos, Y}]}, + {arrow, last},{width, 1}, {fg, black}], + gs:line(S#state.canvas, CanOpts), + S. + +draw_label(Label, FromName, ToName, FromPos, ToPos, S) -> + Colour = + if + FromName == ?unknown, + ToName == ?unknown -> blue; %turquoise; + FromName == ?unknown -> orange; + ToName == ?unknown -> orange; + FromPos == ToPos -> blue; + true -> red + end, + Scale = S#state.scale, + X = lists:min([FromPos, ToPos]) + (6 * Scale), + Y = S#state.y_pos, + write_text(Label, X, Y, Colour, S), + S. + +draw_all_actors(State) -> + Scale = State#state.scale, + Fun = fun(A, X) -> + draw_actor(A, X, State), + X + (?incr_x * Scale) + end, + lists:foldl(Fun, ?initial_x * Scale, State#state.actors), + State. + +%% Returns: {NeedsRefreshBool, {ActorPos, NewsS, NewActors}} +ensure_actor(Name, S) -> + do_ensure_actor(Name, S, S#state.actors, 0). + +do_ensure_actor(Name, S, [H | _], N) when H#actor.name == Name -> + Pos = (?initial_x + (N * ?incr_x)) * S#state.scale, + {false, {Name, Pos, S}}; +do_ensure_actor(Name, S, [_ | T], N) -> + do_ensure_actor(Name, S, T, N + 1); +do_ensure_actor(Name, S, [], N) -> + %% A brand new actor, let's see if it does fit + Pos = (?initial_x + (N * ?incr_x)) * S#state.scale, + MaxActors = S#state.max_actors, + if + integer(MaxActors), N > MaxActors -> + %% Failed on max_actors limit, put into unknown + %% Assume that unknown always is in actor list + ensure_actor(?unknown, S); + Pos > (S#state.canvas_width - ((?initial_x - 15) * S#state.scale)) -> + %% New actor does not fit in canvas, refresh needed + A = create_actor(Name), + draw_actor(A, Pos, S), + {true, {Name, Pos, S#state{actors = S#state.actors ++ [A]}}}; + true -> + %% New actor fits in canvas. Draw the new actor. + A = create_actor(Name), + draw_actor(A, Pos, S), + {false, {Name, Pos, S#state{actors = S#state.actors ++ [A]}}} + end. + +draw_actor(A, LineX, S) -> + Scale = S#state.scale, + TextX = LineX - (5 * Scale), + TextY = ?initial_y * Scale, + LineTopY = TextY + ((?incr_y / 2) * Scale), + LineBotY = S#state.canvas_height - ((?incr_y / 2) * Scale), + Colour = case A#actor.name of + ?unknown -> orange; + _ -> red + end, + write_text(A#actor.string, TextX, TextY, Colour, S), + LineOpt = [{coords, [{LineX, LineTopY}, {LineX, LineBotY}]}, + {width, 1}, {fg, Colour}], + gs:line(S#state.canvas, LineOpt). + +toggle_search_for_actor(ActorName,S) -> + case S#state.display_mode of + all -> + io:format("~p: search for: ~p ++ ~p~n", [?MODULE, [], [ActorName]]), + %% Search for this actor + Key = S#state.first_event, + Actors = [ActorName], + Mode = {search_actors, forward, Key, Actors}, + change_display_mode(Mode, S); + {search_actors, Dir, Key, Actors}-> + Actors2 = + case lists:member(ActorName, Actors) of + true -> + io:format("~p: search for: ~p -- ~p~n", [?MODULE, Actors, [ActorName]]), + %% Remove actor from search list + Actors -- [ActorName]; + false -> + io:format("~p: search for: ~p ++ ~p~n", [?MODULE, Actors, [ActorName]]), + %% Add actor from search list + [ActorName | Actors] + end, + Mode2 = {search_actors, Dir, Key, Actors2}, + change_display_mode(Mode2, S) + end. + +move_actor(From, To, Actors, S) -> + Pos = #actor.name, + ToName = To#actor.name, + FromName = From#actor.name, + ToIx = actor_index(ToName, Pos, Actors), + FromIx = actor_index(FromName, Pos, Actors), + if + FromIx /= 0, ToIx /= 0, ToIx > FromIx -> + Actors2 = lists:keydelete(FromName, Pos, Actors), + Actors3 = insert_actor_after(From, To, Actors2), + S2 = S#state{actors = Actors3}, + refresh_main_window(S2); + FromIx /= 0, ToIx /= 0 -> + Actors2 = lists:keydelete(FromName, Pos, Actors), + Actors3 = insert_actor_before(From, To, Actors2), + S2 = S#state{actors = Actors3}, + refresh_main_window(S2); + true -> + %% Ignore + S + end. + +insert_actor_after(From, To, [H | T]) -> + case To#actor.name == H#actor.name of + true -> [H, From | T]; + false -> [H | insert_actor_after(From, To, T)] + end; +insert_actor_after(_From, _To, []) -> + []. + +insert_actor_before(From, To, [H | T]) -> + case To#actor.name == H#actor.name of + true -> [From, H | T]; + false -> [H | insert_actor_before(From, To, T)] + end; +insert_actor_before(_From, _To, []) -> + []. + +actor_index(_Key, _Pos, []) -> + 0; +actor_index(Key, Pos, [H | T]) -> + case Key == element(Pos, H) of + false -> actor_index(Key, Pos, T) + 1; + true -> 1 + end. + +y_to_n(Y, S) -> + Y2 = ((Y / S#state.scale) - ?initial_y + (?incr_y / 2)), + N = round(Y2 / ?incr_y - 0.2), + MaxN = queue_length(S#state.events), + if + N =< 0 -> actor; + N > MaxN -> actor; + true -> {event, N} + end. + +x_to_n(X, S) -> + Scale = S#state.scale, + Len = length(S#state.actors), + X2 = X - (?initial_x * Scale), + N = X2 / (?incr_x * Scale), + N2 = trunc(N + 1.5), + if + N2 > Len -> Len; + N2 < 1 -> 1; + true -> N2 + end. + +write_text(Text, X, Y, Colour, S) -> + Opt = [{coords, [{X, Y - (?incr_y * S#state.scale / 2)}]}, + {font, S#state.font}, {fg, Colour}, {text, Text}], + gs:text(S#state.canvas, Opt). + +create_contents_window(Event, S) -> + Options = [{viewer_pid, self()}, + {event, Event}, + {event_order, S#state.event_order}, + {active_filter, S#state.active_filter} + | S#state.filters], + case et_contents_viewer:start_link(Options) of + {ok, _Pid} -> + S; + {error, Reason} -> + ok = error_logger:format("~p(~p): create_contents_window(~p) ->~n ~p~n", + [?MODULE, self(), Options, Reason]), + S + end. + +%%%---------------------------------------------------------------------- +%%% String padding of actors +%%%---------------------------------------------------------------------- + +create_actor(Name) -> + String = name_to_string(Name), + PaddedString = pad_string(String, 8), + #actor{name = Name, string = PaddedString}. + +name_to_string(Name) -> + case catch io_lib:format("~s", [Name]) of + {'EXIT', _} -> lists:flatten(io_lib:format("~w", [Name])); + GoodString -> lists:flatten(GoodString) + end. + +pad_string(Atom, MinLen) when atom(Atom) -> + pad_string(atom_to_list(Atom), MinLen); +pad_string(String, MinLen) when integer(MinLen), MinLen >= 0 -> + Len = length(String), + case Len >= MinLen of + true -> + String; + false -> + String ++ lists:duplicate(MinLen - Len, $ ) + end. + +%%%---------------------------------------------------------------------- +%%% Queue management +%%%---------------------------------------------------------------------- + +queue_new() -> + {0, [], []}. + +queue_in(X, {Size, In, Out}) -> + {Size + 1, [X | In], Out}. + +%% queue_out(Q) -> +%% case Q of +%% {Size, In, [H | Out]} -> {{value, H}, {Size - 1, In, Out}}; +%% {Size, [], []} -> {empty, {Size, [], []}}; +%% {Size, In, _} -> queue_out({Size, [], lists:reverse(In)}) +%% end. + +queue_to_list({_Size, [], Out}) -> + Out; +queue_to_list({_Size, In, Out}) -> + Out ++ lists:reverse(In). + +queue_length({Size, _In, _Out}) -> + Size. + +list_to_queue(List) when list(List) -> + {length(List), [], List}. diff --git a/lib/et/src/modules.mk b/lib/et/src/modules.mk new file mode 100644 index 0000000000..8e9dd1a386 --- /dev/null +++ b/lib/et/src/modules.mk @@ -0,0 +1,32 @@ +#-*-makefile-*- ; force emacs to enter makefile-mode +# %CopyrightBegin% +# +# Copyright Ericsson AB 2001-2009. 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% + +MODULES = \ + et \ + et_collector \ + et_contents_viewer \ + et_selector \ + et_viewer + +HRL_FILES = \ + ../include/et.hrl + +INTERNAL_HRL_FILES = \ + et_internal.hrl + + -- cgit v1.2.3