diff options
Diffstat (limited to 'lib/observer/src')
-rw-r--r-- | lib/observer/src/Makefile | 132 | ||||
-rw-r--r-- | lib/observer/src/crashdump_viewer.erl | 2566 | ||||
-rw-r--r-- | lib/observer/src/crashdump_viewer.hrl | 132 | ||||
-rw-r--r-- | lib/observer/src/crashdump_viewer_html.erl | 1431 | ||||
-rw-r--r-- | lib/observer/src/etop.erl | 344 | ||||
-rw-r--r-- | lib/observer/src/etop_defs.hrl | 29 | ||||
-rw-r--r-- | lib/observer/src/etop_gui.erl | 362 | ||||
-rw-r--r-- | lib/observer/src/etop_tr.erl | 130 | ||||
-rw-r--r-- | lib/observer/src/etop_txt.erl | 101 | ||||
-rw-r--r-- | lib/observer/src/multitrace.erl | 256 | ||||
-rw-r--r-- | lib/observer/src/observer.app.src | 34 | ||||
-rw-r--r-- | lib/observer/src/observer.appup.src | 19 | ||||
-rw-r--r-- | lib/observer/src/ttb.erl | 1000 | ||||
-rw-r--r-- | lib/observer/src/ttb_et.erl | 267 |
14 files changed, 6803 insertions, 0 deletions
diff --git a/lib/observer/src/Makefile b/lib/observer/src/Makefile new file mode 100644 index 0000000000..dde1ea17be --- /dev/null +++ b/lib/observer/src/Makefile @@ -0,0 +1,132 @@ +# +# %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% +# +include $(ERL_TOP)/make/target.mk +include $(ERL_TOP)/make/$(TARGET)/otp.mk + +# ---------------------------------------------------- +# Application version +# ---------------------------------------------------- +include ../vsn.mk +VSN=$(OBSERVER_VSN) + +# ---------------------------------------------------- +# Release directory specification +# ---------------------------------------------------- +RELSYSDIR = $(RELEASE_PATH)/lib/observer-$(VSN) + +# ---------------------------------------------------- +# Common Macros +# ---------------------------------------------------- + +MODULES= \ + crashdump_viewer \ + crashdump_viewer_html \ + etop \ + etop_gui \ + etop_tr \ + etop_txt \ + ttb \ + ttb_et +HRL_FILES= \ + ../include/etop.hrl +INTERNAL_HRL_FILES= \ + crashdump_viewer.hrl \ + etop_defs.hrl +ERL_FILES= $(MODULES:%=%.erl) +EXAMPLE_FILES= multitrace.erl + +TARGET_FILES= $(MODULES:%=$(EBIN)/%.$(EMULATOR)) $(APP_TARGET) $(APPUP_TARGET) + +PRIVDIR= ../priv +WEBTOOLFILES= $(PRIVDIR)/crashdump_viewer.tool +BINDIR= $(PRIVDIR)/bin +EXECUTABLES= \ + $(BINDIR)/etop \ + $(BINDIR)/getop \ + $(BINDIR)/etop.bat \ + $(BINDIR)/getop.bat +CDVDIR= $(PRIVDIR)/crashdump_viewer +GIF_FILES= \ + $(CDVDIR)/collapsd.gif \ + $(CDVDIR)/exploded.gif + +APP_FILE= observer.app + +APP_SRC= $(APP_FILE).src +APP_TARGET= $(EBIN)/$(APP_FILE) + +APPUP_FILE= observer.appup + +APPUP_SRC= $(APPUP_FILE).src +APPUP_TARGET= $(EBIN)/$(APPUP_FILE) + +# ---------------------------------------------------- +# FLAGS +# ---------------------------------------------------- +ERL_COMPILE_FLAGS += \ + -I../include \ + -I ../../et/include \ + -I ../../../libraries/et/include + +# ---------------------------------------------------- +# Targets +# ---------------------------------------------------- + +debug opt: $(TARGET_FILES) + +clean: + rm -f $(TARGET_FILES) + rm -f errs core *~ + +$(APP_TARGET): $(APP_SRC) ../vsn.mk + sed -e 's;%VSN%;$(VSN);' $< > $@ + +$(APPUP_TARGET): $(APPUP_SRC) ../vsn.mk + sed -e 's;%VSN%;$(VSN);' $< > $@ + +docs: + +# ---------------------------------------------------- +# Release Target +# ---------------------------------------------------- +include $(ERL_TOP)/make/otp_release_targets.mk + +release_spec: opt + $(INSTALL_DIR) $(RELSYSDIR)/src + $(INSTALL_DATA) $(ERL_FILES) $(RELSYSDIR)/src + $(INSTALL_DATA) $(INTERNAL_HRL_FILES) $(RELSYSDIR)/src + $(INSTALL_DATA) $(EXAMPLE_FILES) $(RELSYSDIR)/src + $(INSTALL_DIR) $(RELSYSDIR)/include + $(INSTALL_DATA) $(HRL_FILES) $(RELSYSDIR)/include + $(INSTALL_DIR) $(RELSYSDIR)/ebin + $(INSTALL_DATA) $(TARGET_FILES) $(RELSYSDIR)/ebin + $(INSTALL_DIR) $(RELSYSDIR)/priv/bin + $(INSTALL_SCRIPT) $(EXECUTABLES) $(RELSYSDIR)/priv/bin + $(INSTALL_DIR) $(RELSYSDIR)/priv/crashdump_viewer + $(INSTALL_DATA) $(WEBTOOLFILES) $(RELSYSDIR)/priv + $(INSTALL_DATA) $(GIF_FILES) $(RELSYSDIR)/priv/crashdump_viewer + +release_docs_spec: + + + + + + + diff --git a/lib/observer/src/crashdump_viewer.erl b/lib/observer/src/crashdump_viewer.erl new file mode 100644 index 0000000000..b323d86ea4 --- /dev/null +++ b/lib/observer/src/crashdump_viewer.erl @@ -0,0 +1,2566 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2003-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% +%% +-module(crashdump_viewer). + +%% +%% This module is the main module in the crashdump viewer. It implements +%% the server started by webtool and the API for the crashdump viewer tool. +%% +%% All functions in the API except configData/0 and start_link/0 are +%% called from HTML pages via erl_scheme. +%% +%% Tables +%% ------ +%% cdv_menu_table: This table holds the menu which is presented in the left +%% frame of the crashdump viewer page. Each element in the table represents +%% one meny item, and the state of the item indicates if it is presently +%% visible or not. +%% +%% cdv_dump_index_table: This table holds all tags read from the crashdump. +%% Each tag indicates where the information about a specific item starts. +%% The table entry for a tag includes the start and end positions for +%% this item-information. All tags start with a "=" at the beginning of +%% a line. +%% +%% Process state +%% ------------- +%% file: The name of the crashdump currently viewed. +%% procs_summary: Process summary represented by a list of +%% #proc records. This is used for efficiency reasons when sorting +%% the process summary table instead of reading all processes from +%% the dump again. +%% sorted: atom(), indicated what item was last sorted in process summary. +%% This is needed so reverse sorting can be done. +%% shared_heap: 'true' if crashdump comes from a system running shared heap, +%% else 'false'. +%% wordsize: 4 | 8, the number of bytes in a word. +%% binaries: a gb_tree containing binaries or links to binaries in the dump +%% + +%% User API +-export([start/0,stop/0]). + +%% Webtool API +-export([configData/0, + start_link/0]). +-export([start_page/2, + read_file_frame/2, + read_file/2, + redirect/2, + filename_frame/2, + menu_frame/2, + initial_info_frame/2, + toggle/2, + general_info/2, + processes/2, + proc_details/2, + ports/2, + ets_tables/2, + timers/2, + fun_table/2, + atoms/2, + dist_info/2, + loaded_modules/2, + loaded_mod_details/2, + memory/2, + allocated_areas/2, + allocator_info/2, + hash_tables/2, + index_tables/2, + sort_procs/2, + expand/2, + expand_binary/2, + expand_memory/2, + next/2]). + + +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). + +%% Debug support +-export([debug/1,stop_debug/0]). + +-include("crashdump_viewer.hrl"). +-include_lib("kernel/include/file.hrl"). +-include_lib("stdlib/include/ms_transform.hrl"). + +-define(START_PAGE,"/cdv_erl/crashdump_viewer/start_page"). +-define(READ_FILE_PAGE,"/cdv_erl/crashdump_viewer/read_file?path="). +-define(SERVER, crashdump_viewer_server). +-define(call_timeout,3600000). +-define(chunk_size,1000). % number of bytes read from crashdump at a time +-define(max_line_size,100). % max number of bytes (i.e. characters) the + % line_head/1 function can return +-define(max_display_size,500). % max number of bytes that will be directly + % displayed. If e.g. msg_q is longer than + % this, it must be explicitly expanded. +-define(max_display_binary_size,50). % max size of a binary that will be + % directly displayed. + +-define(initial_proc_record(Pid), + #proc{pid=Pid, + %% msg_q_len, reds and stack_heap are integers because it must + %% be possible to sort on them. All other fields are strings + msg_q_len=0,reds=0,stack_heap=0, + %% for old dumps start_time, parent and number of heap frament + %% does not exist + start_time="unknown", + parent="unknown", + num_heap_frag="unknown", + %% current_func can be both "current function" and + %% "last scheduled in for" + current_func={"Current Function",?space}, + %% stack_dump, message queue and dictionaries should only be + %% displayed as a link to "Expand" (if dump is from OTP R9B + %% or newer) + _=?space}). + +-record(state,{file,procs_summary,sorted,shared_heap=false, + wordsize=4,num_atoms="unknown",binaries,bg_status}). + +%%%----------------------------------------------------------------- +%%% Debugging +%% Start tracing with +%% debug(Functions). +%% Functions = local | global | FunctionList +%% FunctionList = [Function] +%% Function = {FunctionName,Arity} | FunctionName +debug(F) -> + ttb:tracer(all,[{file,"cdv"}]), % tracing all nodes + ttb:p(all,[call,timestamp]), + MS = [{'_',[],[{return_trace},{message,{caller}}]}], + tp(F,MS), + ttb:ctp(?MODULE,stop_debug), % don't want tracing of the stop_debug func + ok. +tp([{M,F,A}|T],MS) -> % mod:func/arity + ttb:tpl(M,F,A,MS), + tp(T,MS); +tp([{M,F}|T],MS) -> % mod:func + ttb:tpl(M,F,MS), + tp(T,MS); +tp([M|T],MS) -> % mod + ttb:tp(M,MS), % only exported + tp(T,MS); +tp([],_MS) -> + ok. +stop_debug() -> + ttb:stop([format]). + +%%%----------------------------------------------------------------- +%%% User API +start() -> + webtool:start(), + receive after 1000 -> ok end, + webtool:start_tools([],"app=crashdump_viewer"), + receive after 1000 -> ok end, + ok. + +stop() -> + webtool:stop_tools([],"app=crashdump_viewer"), + webtool:stop(). + +%%%----------------------------------------------------------------- +%%% Return config data used by webtool +configData() -> + Dir = filename:join(code:priv_dir(observer),"crashdump_viewer"), + {crashdump_viewer, + [{web_data,{"CrashDumpViewer",?START_PAGE}}, + {alias,{"/crashdump_viewer",Dir}}, + {alias,{"/crashdump_erts_doc",erts_docdir()}}, + {alias,{"/crashdump_doc",cdv_docdir()}}, + {alias,{erl_alias,"/cdv_erl",[?MODULE]}}, + {start,{child,{{local,?SERVER}, + {?MODULE,start_link,[]}, + permanent,100,worker,[?MODULE]}}} + ]}. + +erts_docdir() -> + ErtsVsn = erlang:system_info(version), + RootDir = code:root_dir(), + VsnErtsDir = filename:join(RootDir,"erts-"++ErtsVsn), + DocDir = filename:join(["doc","html"]), + case filelib:is_dir(VsnErtsDir) of + true -> + filename:join(VsnErtsDir,DocDir); + false -> + %% So this can be run in clearcase + filename:join([RootDir,"erts",DocDir]) + end. + +cdv_docdir() -> + ObserverDir = code:lib_dir(observer), + filename:join([ObserverDir,"doc","html"]). + +%%==================================================================== +%% External functions +%%==================================================================== +%%%-------------------------------------------------------------------- +%%% Start the server +start_link() -> + case whereis(?SERVER) of + undefined -> + gen_server:start_link({local, ?SERVER}, ?MODULE, [], []); + Pid -> + {ok,Pid} + end. + +%%%----------------------------------------------------------------- +%%% If crashdump_viewer is just started, show welcome frame. Else +%%% show menu and general_info +start_page(_Env,_Input) -> + call(start_page). + +%%%----------------------------------------------------------------- +%%% Display the form for entering the file name for the crashdump +%%% to view. +read_file_frame(_Env,_Input) -> + crashdump_viewer_html:read_file_frame(). + +%%%----------------------------------------------------------------- +%%% Called when the 'ok' button is clicked after entering the dump +%%% file name. +read_file(_Env,Input) -> + call({read_file,Input}). + +%%%----------------------------------------------------------------- +%%% The topmost frame of the main page. Called when a crashdump is +%%% loaded. +filename_frame(_Env,_Input) -> + call(filename_frame). + +%%%----------------------------------------------------------------- +%%% The initial information frame. Called when a crashdump is loaded. +initial_info_frame(_Env,_Input) -> + call(initial_info_frame). + +%%%----------------------------------------------------------------- +%%% The left frame of the main page. Called when a crashdump is +%%% loaded. +menu_frame(_Env,_Input) -> + crashdump_viewer_html:menu_frame(). + +%%%----------------------------------------------------------------- +%%% Called when the collapsed or exploded picture in the menu is +%%% clicked. +toggle(_Env,Input) -> + call({toggle,Input}). + +%%%----------------------------------------------------------------- +%%% The following functions are called when menu items are clicked. +general_info(_Env,_Input) -> + call(general_info). +processes(_Env,_Input) -> + call(procs_summary). +ports(_Env,Input) -> % this is also called when a link to a port is clicked + call({ports,Input}). +ets_tables(_Env,Input) -> + call({ets_tables,Input}). +timers(_Env,Input) -> + call({timers,Input}). +fun_table(_Env,_Input) -> + call(funs). +atoms(_Env,_Input) -> + call(atoms). +dist_info(_Env,_Input) -> + call(dist_info). +loaded_modules(_Env,_Input) -> + call(loaded_mods). +loaded_mod_details(_Env,Input) -> + call({loaded_mod_details,Input}). +memory(_Env,_Input) -> + call(memory). +allocated_areas(_Env,_Input) -> + call(allocated_areas). +allocator_info(_Env,_Input) -> + call(allocator_info). +hash_tables(_Env,_Input) -> + call(hash_tables). +index_tables(_Env,_Input) -> + call(index_tables). + +%%%----------------------------------------------------------------- +%%% Called when a link to a process (Pid) is clicked. +proc_details(_Env,Input) -> + call({proc_details,Input}). + +%%%----------------------------------------------------------------- +%%% Called when one of the headings in the process summary table are +%%% clicked. It sorts the processes by the clicked heading. +sort_procs(_Env,Input) -> + call({sort_procs,Input}). + +%%%----------------------------------------------------------------- +%%% Called when the "Expand" link in a call stack (Last Calls) is +%%% clicked. +expand(_Env,Input) -> + call({expand,Input}). + +%%%----------------------------------------------------------------- +%%% Called when the "Expand" link in a stack dump, message queue or +%%% dictionary is clicked. +expand_memory(_Env,Input) -> + call({expand_memory,Input}). + +%%%----------------------------------------------------------------- +%%% Called when "<< xxx bytes>>" link in a stack dump, message queue or +%%% dictionary is clicked. +expand_binary(_Env,Input) -> + call({expand_binary,Input}). + +%%%----------------------------------------------------------------- +%%% Called when the "Next" link under atoms is clicked. +next(_Env,Input) -> + call({next,Input}). + +%%%----------------------------------------------------------------- +%%% Called on regular intervals while waiting for a dump to be read +redirect(_Env,_Input) -> + call(redirect). + +%%==================================================================== +%% Server functions +%%==================================================================== + +%%-------------------------------------------------------------------- +%% Function: init/1 +%% Description: Initiates the server +%% Returns: {ok, State} | +%% {ok, State, Timeout} | +%% ignore | +%% {stop, Reason} +%%-------------------------------------------------------------------- +init([]) -> + ets:new(cdv_menu_table,[set,named_table,{keypos,#menu_item.index},public]), + ets:new(cdv_dump_index_table,[bag,named_table,public]), + {ok, #state{}}. + +%%-------------------------------------------------------------------- +%% Function: handle_call/3 +%% Description: Handling call messages +%% 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(start_page,_From,State=#state{file=undefined,bg_status=undefined})-> + Reply = crashdump_viewer_html:welcome(), + {reply,Reply,State}; +handle_call(start_page, _From, State=#state{file=undefined,bg_status={done,Page}}) -> + {reply,Page,State}; +handle_call(start_page, _From, State=#state{file=undefined,bg_status=Status}) -> + Reply = crashdump_viewer_html:redirect(Status), + {reply,Reply,State}; +handle_call(start_page, _From, State) -> + Reply = crashdump_viewer_html:start_page(), + {reply,Reply,State}; +handle_call({read_file,Input}, _From, _State) -> + {ok,File0} = get_value("path",httpd:parse_query(Input)), + File = + case File0 of + [$"|FileAndSome] -> + %% Opera adds \"\" around the filename! + [$"|Elif] = lists:reverse(FileAndSome), + lists:reverse(Elif); + _ -> + File0 + end, + spawn_link(fun() -> read_file(File) end), + Status = background_status(reading,File), + Reply = crashdump_viewer_html:redirect(Status), + {reply, Reply, #state{bg_status=Status}}; +handle_call(redirect,_From, State=#state{bg_status={done,Page}}) -> + {reply, Page, State#state{bg_status=undefined}}; +handle_call(redirect,_From, State=#state{bg_status=Status}) -> + Reply = crashdump_viewer_html:redirect(Status), + {reply, Reply, State}; +handle_call(filename_frame,_From,State=#state{file=File}) -> + Reply = crashdump_viewer_html:filename_frame(File), + {reply,Reply,State}; +handle_call(initial_info_frame,_From,State=#state{file=File}) -> + GenInfo = general_info(File), + NumAtoms = GenInfo#general_info.num_atoms, + {WS,SH} = parse_vsn_str(GenInfo#general_info.system_vsn,4,false), + Reply = crashdump_viewer_html:general_info(GenInfo), + {reply,Reply,State#state{shared_heap=SH,wordsize=WS,num_atoms=NumAtoms}}; +handle_call({toggle,Input},_From,State) -> + {ok,Index} = get_value("index",httpd:parse_query(Input)), + do_toggle(list_to_integer(Index)), + Reply = crashdump_viewer_html:menu_frame(), + {reply,Reply,State}; +handle_call({expand,Input},_From,State=#state{file=File}) -> + [{"pos",Pos},{"size",Size},{"what",What},{"truncated",Truncated}] = + httpd:parse_query(Input), + Expanded = get_expanded(File,list_to_integer(Pos),list_to_integer(Size)), + TruncText = if Truncated=="true" -> "WARNING: This term is truncated!\n\n"; + true -> "" + end, + Reply = + case {Truncated,What} of + {_,"LastCalls"} -> + LastCalls = replace_all($ ,$\n,Expanded,[]), + crashdump_viewer_html:info_page(What,[TruncText,LastCalls]); + {_,"StackDump"} -> + crashdump_viewer_html:info_page(What,[TruncText,Expanded]); + {"false",_} -> + crashdump_viewer_html:pretty_info_page(What,Expanded); + {"true",_} -> + crashdump_viewer_html:info_page(What,[TruncText,Expanded]) + end, + {reply,Reply,State}; +handle_call({expand_memory,Input},_From,State=#state{file=File,binaries=B}) -> + [{"pid",Pid},{"what",What}] = httpd:parse_query(Input), + Reply = + case truncated_warning([{"=proc",Pid}]) of + [] -> + Expanded = expand_memory(File,What,Pid,B), + crashdump_viewer_html:expanded_memory(What,Expanded); + _TW -> + Info = + "The crashdump is truncated in the middle of this " + "process' memory information, so this information " + "can not be extracted.", + crashdump_viewer_html:info_page(What,Info) + end, + {reply,Reply,State}; +handle_call({expand_binary,Input},_From,State=#state{file=File}) -> + [{"pos",Pos0}] = httpd:parse_query(Input), + Pos = list_to_integer(Pos0), + Fd = open(File), + pos_bof(Fd,Pos), + {Bin,_Line} = get_binary(val(Fd)), + close(Fd), + Reply=crashdump_viewer_html:expanded_binary(io_lib:format("~p",[Bin])), + {reply,Reply,State}; +handle_call({next,Input},_From,State=#state{file=File}) -> + [{"pos",Pos},{"num",N},{"start",Start},{"what",What}] = + httpd:parse_query(Input), + Tags = related_tags(What), + TW = truncated_warning(Tags), + Next = get_next(File,list_to_integer(Pos),list_to_integer(N), + list_to_integer(Start),What), + Reply = crashdump_viewer_html:next(Next,TW), + {reply,Reply,State}; +handle_call(general_info,_From,State=#state{file=File}) -> + GenInfo=general_info(File), + Reply = crashdump_viewer_html:general_info(GenInfo), + {reply,Reply,State}; +handle_call(procs_summary,_From,State=#state{file=File,shared_heap=SH}) -> + ProcsSummary = + case State#state.procs_summary of + undefined -> procs_summary(File); + PS -> PS + end, + TW = truncated_warning(["=proc"]), + Reply = crashdump_viewer_html:procs_summary("pid",ProcsSummary,TW,SH), + {reply,Reply,State#state{procs_summary=ProcsSummary,sorted="pid"}}; +handle_call({sort_procs,Input}, _From, State=#state{shared_heap=SH}) -> + {ok,Sort} = get_value("sort",httpd:parse_query(Input)), + {ProcsSummary,Sorted} = do_sort_procs(Sort, + State#state.procs_summary, + State#state.sorted), + TW = truncated_warning(["=proc"]), + Reply = crashdump_viewer_html:procs_summary(Sort,ProcsSummary,TW,SH), + {reply,Reply,State#state{sorted=Sorted}}; +handle_call({proc_details,Input},_From,State=#state{file=File,shared_heap=SH}) -> + {ok,Pid} = get_value("pid",httpd:parse_query(Input)), + Reply = + case get_proc_details(File,Pid) of + {ok,Proc} -> + TW = truncated_warning([{"=proc",Pid}]), + crashdump_viewer_html:proc_details(Pid,Proc,TW,SH); + {other_node,Node} -> + TW = truncated_warning(["=visible_node", + "=hidden_node", + "=not_connected"]), + crashdump_viewer_html:nods(Node,TW); + not_found -> + crashdump_viewer_html:info_page(["Could not find process: ", + Pid],?space) + end, + {reply, Reply, State}; +handle_call({ports,Input},_From,State=#state{file=File}) -> + Reply = + case get_value("port",httpd:parse_query(Input)) of + {ok,P} -> + Id = [$#|P], + case get_port(File,Id) of + {ok,PortInfo} -> + TW = truncated_warning([{"=port",Id}]), + crashdump_viewer_html:ports(Id,[PortInfo],TW); + {other_node,Node} -> + TW = truncated_warning(["=visible_node", + "=hidden_node", + "=not_connected"]), + crashdump_viewer_html:nods(Node,TW); + not_found -> + crashdump_viewer_html:info_page( + ["Could not find port: ",Id],?space) + end; + error -> % no port identity in Input - get all ports + Ports=get_ports(File), + TW = truncated_warning(["=port"]), + crashdump_viewer_html:ports("Port Information",Ports,TW) + end, + {reply,Reply,State}; +handle_call({ets_tables,Input},_From,State=#state{file=File,wordsize=WS}) -> + {Pid,Heading,InternalEts} = + case get_value("pid",httpd:parse_query(Input)) of + {ok,P} -> + {P,["ETS Tables for Process ",P],[]}; + error -> + I = get_internal_ets_tables(File,WS), + {'_',"ETS Table Information",I} + end, + EtsTables = get_ets_tables(File,Pid,WS), + TW = truncated_warning(["=ets"]), + Reply = crashdump_viewer_html:ets_tables(Heading,EtsTables,InternalEts,TW), + {reply,Reply,State}; +handle_call({timers,Input},_From,State=#state{file=File}) -> + {Pid,Heading} = + case get_value("pid",httpd:parse_query(Input)) of + {ok,P} -> {P,["Timers for Process ",P]}; + error -> {'_',"Timer Information"} + end, + Timers=get_timers(File,Pid), + TW = truncated_warning(["=timer"]), + Reply = crashdump_viewer_html:timers(Heading,Timers,TW), + {reply,Reply,State}; +handle_call(dist_info,_From,State=#state{file=File}) -> + Nods=nods(File), + TW = truncated_warning(["=visible_node","=hidden_node","=not_connected"]), + Reply = crashdump_viewer_html:nods(Nods,TW), + {reply,Reply,State}; +handle_call(loaded_mods,_From,State=#state{file=File}) -> + LoadedMods=loaded_mods(File), + TW = truncated_warning(["=mod"]), + Reply = crashdump_viewer_html:loaded_mods(LoadedMods,TW), + {reply,Reply,State}; +handle_call({loaded_mod_details,Input},_From,State=#state{file=File}) -> + {ok,Mod} = get_value("mod",httpd:parse_query(Input)), + ModInfo = get_loaded_mod_details(File,Mod), + TW = truncated_warning([{"=mod",Mod}]), + Reply = crashdump_viewer_html:loaded_mod_details(ModInfo,TW), + {reply,Reply,State}; +handle_call(funs,_From,State=#state{file=File}) -> + Funs=funs(File), + TW = truncated_warning(["=fun"]), + Reply = crashdump_viewer_html:funs(Funs,TW), + {reply,Reply,State}; +handle_call(atoms,_From,State=#state{file=File,num_atoms=Num}) -> + Atoms=atoms(File), + TW = truncated_warning(["=atoms","=num_atoms"]), + Reply = crashdump_viewer_html:atoms(Atoms,Num,TW), + {reply,Reply,State}; +handle_call(memory,_From,State=#state{file=File}) -> + Memory=memory(File), + TW = truncated_warning(["=memory"]), + Reply = crashdump_viewer_html:memory(Memory,TW), + {reply,Reply,State}; +handle_call(allocated_areas,_From,State=#state{file=File}) -> + AllocatedAreas=allocated_areas(File), + TW = truncated_warning(["=allocated_areas"]), + Reply = crashdump_viewer_html:allocated_areas(AllocatedAreas,TW), + {reply,Reply,State}; +handle_call(allocator_info,_From,State=#state{file=File}) -> + SlAlloc=allocator_info(File), + TW = truncated_warning(["=allocator"]), + Reply = crashdump_viewer_html:allocator_info(SlAlloc,TW), + {reply,Reply,State}; +handle_call(hash_tables,_From,State=#state{file=File}) -> + HashTables=hash_tables(File), + TW = truncated_warning(["=hash_table","=index_table"]), + Reply = crashdump_viewer_html:hash_tables(HashTables,TW), + {reply,Reply,State}; +handle_call(index_tables,_From,State=#state{file=File}) -> + IndexTables=index_tables(File), + TW = truncated_warning(["=hash_table","=index_table"]), + Reply = crashdump_viewer_html:index_tables(IndexTables,TW), + {reply,Reply,State}. + + + +%%-------------------------------------------------------------------- +%% Function: handle_cast/2 +%% Description: Handling cast messages +%% Returns: {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, State} (terminate/2 is called) +%%-------------------------------------------------------------------- +handle_cast({background_done,{Page,File,Binaries},Dict}, State) -> + lists:foreach(fun({Key,Val}) -> put(Key,Val) end, Dict), + {noreply, State#state{file=File,binaries=Binaries,bg_status={done,Page}}}; +handle_cast({background_status,Status}, State) -> + {noreply, State#state{bg_status=Status}}. + +%%-------------------------------------------------------------------- +%% Function: handle_info/2 +%% Description: Handling all non call/cast messages +%% Returns: {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, State} (terminate/2 is called) +%%-------------------------------------------------------------------- +handle_info(_Info, State) -> + {noreply, State}. + +%%-------------------------------------------------------------------- +%% Function: terminate/2 +%% Description: Shutdown the server +%% Returns: any (ignored by gen_server) +%%-------------------------------------------------------------------- +terminate(_Reason, _State) -> + ok. + +%%-------------------------------------------------------------------- +%% Func: code_change/3 +%% Purpose: Convert process state when code is changed +%% Returns: {ok, NewState} +%%-------------------------------------------------------------------- +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%-------------------------------------------------------------------- +%%% Internal functions +%%-------------------------------------------------------------------- +call(Request) -> + gen_server:call(?SERVER,Request,?call_timeout). + +cast(Msg) -> + gen_server:cast(?SERVER,Msg). + +unexpected(_Fd,{eof,_LastLine},_Where) -> + ok; % truncated file +unexpected(Fd,{part,What},Where) -> + skip_rest_of_line(Fd), + io:format("WARNING: Found unexpected line in ~s:~n~s ...~n",[Where,What]); +unexpected(_Fd,What,Where) -> + io:format("WARNING: Found unexpected line in ~s:~n~s~n",[Where,What]). + +truncated_warning([]) -> + []; +truncated_warning([Tag|Tags]) -> + case truncated_here(Tag) of + true -> truncated_warning(); + false -> truncated_warning(Tags) + end. +truncated_warning() -> + ["WARNING: The crash dump is truncated here. " + "Some information might be missing."]. + +truncated_here(Tag) -> + case get(truncated) of + true -> + case get(last_tag) of + Tag -> % Tag == {TagType,Id} + true; + {Tag,_Id} -> + true; + _LastTag -> + truncated_earlier(Tag) + end; + false -> + false + end. + + +%% Check if the dump was truncated with the same tag, but earlier id. +%% Eg if this is {"=proc","<0.30.0>"}, we should warn if the dump was +%% truncated in {"=proc","<0.29.0>"} or earlier +truncated_earlier({"=proc",Pid}) -> + compare_pid(Pid,get(truncated_proc)); +truncated_earlier(_Tag) -> + false. + +compare_pid("<"++Id,"<"++OtherId) -> + Id>=OtherId; +compare_pid(_,_) -> + false. + +background_status(Action,File) -> + SizeInfo = filesizeinfo(File), + background_status(Action,File,SizeInfo). + +background_status(processing,File,SizeInfo) -> + "Processing " ++ File ++ SizeInfo; +background_status(reading,File,SizeInfo) -> + "Reading file " ++ File ++ SizeInfo. + +filesizeinfo(File) -> + case file:read_file_info(File) of + {ok,#file_info{size=Size}} -> + " (" ++ integer_to_list(Size) ++ " bytes)"; + _X -> + "" + end. + + +open(File) -> + {ok,Fd} = file:open(File,[read,read_ahead,raw,binary]), + Fd. +close(Fd) -> + erase(chunk), + file:close(Fd). +pos_bof(Fd,Pos) -> + reset_chunk(), + file:position(Fd,{bof,Pos}). + +get_chunk(Fd) -> + case erase(chunk) of + undefined -> + case read(Fd) of + eof -> + put_pos(Fd), + eof; + Other -> + Other + end; + Bin -> + {ok,Bin} + end. + +read(Fd) -> + file:read(Fd,?chunk_size). + +put_chunk(Fd,Bin) -> + {ok,Pos0} = file:position(Fd,cur), + Pos = Pos0 - byte_size(Bin), + put(chunk,Bin), + put(pos,Pos). + +put_pos(Fd) -> + {ok,Pos} = file:position(Fd,cur), + put(pos,Pos). + +reset_chunk() -> + erase(chunk), + erase(pos). + +line_head(Fd) -> + case get_chunk(Fd) of + {ok,Bin} -> line_head(Fd,Bin,[],0); + eof -> {eof,[]} + end. +line_head(Fd,Bin,Acc,?max_line_size) -> + put_chunk(Fd,Bin), + {part,lists:reverse(Acc)}; +line_head(Fd,<<$\n:8,Bin/binary>>,Acc,_N) -> + put_chunk(Fd,Bin), + lists:reverse(Acc); +line_head(Fd,<<$::8,$\r:8,$\n:8,Bin/binary>>,Acc,_N) -> + put_chunk(Fd,Bin), + lists:reverse(Acc); +line_head(Fd,<<$::8,$\r:8>>,Acc,N) -> + case get_chunk(Fd) of + {ok,Bin} -> line_head(Fd,<<$:,Bin/binary>>,Acc,N); + eof -> {eof,lists:reverse(Acc)} + end; +line_head(Fd,<<$::8>>,Acc,N) -> + case get_chunk(Fd) of + {ok,Bin} -> line_head(Fd,<<$:,Bin/binary>>,Acc,N); + eof -> {eof,lists:reverse(Acc)} + end; +line_head(Fd,<<$::8,Space:8,Bin/binary>>,Acc,_N) when Space=:=$ ;Space=:=$\n -> + put_chunk(Fd,Bin), + lists:reverse(Acc); +line_head(Fd,<<$::8,Bin/binary>>,Acc,_N) -> + put_chunk(Fd,Bin), + lists:reverse(Acc); +line_head(Fd,<<$\r:8,Bin/binary>>,Acc,N) -> + line_head(Fd,Bin,Acc,N+1); +line_head(Fd,<<Char:8,Bin/binary>>,Acc,N) -> + line_head(Fd,Bin,[Char|Acc],N+1); +line_head(Fd,<<>>,Acc,N) -> + case get_chunk(Fd) of + {ok,Bin} -> line_head(Fd,Bin,Acc,N); + eof -> {eof,lists:reverse(Acc)} + end. + +skip_rest_of_line(Fd) -> + case get_chunk(Fd) of + {ok,Bin} -> skip(Fd,Bin); + eof -> ok + end. +skip(Fd,<<$\n:8,Bin/binary>>) -> + put_chunk(Fd,Bin), + ok; +skip(Fd,<<_Char:8,Bin/binary>>) -> + skip(Fd,Bin); +skip(Fd,<<>>) -> + case get_chunk(Fd) of + {ok,Bin} -> skip(Fd,Bin); + eof -> ok + end. + + +val(Fd) -> + case get_rest_of_line(Fd) of + {eof,[]} -> "-1"; + [] -> "-1"; + {eof,Val} -> Val; + Val -> Val + end. + +get_rest_of_line(Fd) -> + case get_chunk(Fd) of + {ok,Bin} -> get_rest_of_line_1(Fd, Bin, []); + eof -> {eof,[]} + end. + +get_rest_of_line_1(Fd, <<$\n:8,Bin/binary>>, Acc) -> + put_chunk(Fd, Bin), + lists:reverse(Acc); +get_rest_of_line_1(Fd, <<$\r:8,Rest/binary>>, Acc) -> + get_rest_of_line_1(Fd, Rest, Acc); +get_rest_of_line_1(Fd, <<Char:8,Rest/binary>>, Acc) -> + get_rest_of_line_1(Fd, Rest, [Char|Acc]); +get_rest_of_line_1(Fd, <<>>, Acc) -> + case get_chunk(Fd) of + {ok,Bin} -> get_rest_of_line_1(Fd, Bin, Acc); + eof -> {eof,lists:reverse(Acc)} + end. + +count_rest_of_line(Fd) -> + case get_chunk(Fd) of + {ok,Bin} -> count_rest_of_line(Fd,Bin,0); + eof -> {eof,0} + end. +count_rest_of_line(Fd,<<$\n:8,Bin/binary>>,N) -> + put_chunk(Fd,Bin), + N; +count_rest_of_line(Fd,<<$\r:8,Bin/binary>>,N) -> + count_rest_of_line(Fd,Bin,N); +count_rest_of_line(Fd,<<_Char:8,Bin/binary>>,N) -> + count_rest_of_line(Fd,Bin,N+1); +count_rest_of_line(Fd,<<>>,N) -> + case get_chunk(Fd) of + {ok,Bin} -> count_rest_of_line(Fd,Bin,N); + eof -> {eof,N} + end. + +get_n_lines_of_tag(Fd,N) -> + case get_chunk(Fd) of + {ok,Bin} -> + {AllOrPart,Rest,Lines} = get_n_lines_of_tag(Fd,N,Bin,[]), + {AllOrPart,N-Rest,Lines}; + eof -> + empty + end. +get_n_lines_of_tag(Fd,N,<<"\n=",_/binary>>=Bin,Acc) -> + put_chunk(Fd,Bin), + {all,N-1,lists:reverse(Acc)}; +get_n_lines_of_tag(Fd,0,Bin,Acc) -> + put_chunk(Fd,Bin), + {part,0,lists:reverse(Acc)}; +get_n_lines_of_tag(Fd,N,<<$\n:8,Bin/binary>>,Acc) -> + get_n_lines_of_tag(Fd,N-1,Bin,[$\n|Acc]); +get_n_lines_of_tag(Fd,N,<<$\r:8,Bin/binary>>,Acc) -> + get_n_lines_of_tag(Fd,N,Bin,Acc); +get_n_lines_of_tag(Fd,N,<<Char:8,Bin/binary>>,Acc) -> + get_n_lines_of_tag(Fd,N,Bin,[Char|Acc]); +get_n_lines_of_tag(Fd,N,<<>>,Acc) -> + case get_chunk(Fd) of + {ok,Bin} -> + get_n_lines_of_tag(Fd,N,Bin,Acc); + eof -> + case Acc of + [$\n|_] -> + {all,N,lists:reverse(Acc)}; + _ -> + {all,N-1,lists:reverse(Acc)} + end + end. + +count_rest_of_tag(Fd) -> + case get_chunk(Fd) of + {ok,Bin} -> count_rest_of_tag(Fd,Bin,0); + eof -> 0 + end. +count_rest_of_tag(Fd,<<"\n=",Bin/binary>>,N) -> + put_chunk(Fd,Bin), + N; +count_rest_of_tag(Fd,<<$\r:8,Bin/binary>>,N) -> + count_rest_of_tag(Fd,Bin,N); +count_rest_of_tag(Fd,<<_Char:8,Bin/binary>>,N) -> + count_rest_of_tag(Fd,Bin,N+1); +count_rest_of_tag(Fd,<<>>,N) -> + case get_chunk(Fd) of + {ok,Bin} -> count_rest_of_tag(Fd,Bin,N); + eof -> N + end. + +split(Str) -> + split($ ,Str,[]). +split(Char,Str) -> + split(Char,Str,[]). +split(Char,[Char|Str],Acc) -> % match Char + {lists:reverse(Acc),Str}; +split(_Char,[$\r,$\n|Str],Acc) -> % new line + {lists:reverse(Acc),Str}; +split(_Char,[$\n|Str],Acc) -> % new line + {lists:reverse(Acc),Str}; +split(Char,[H|T],Acc) -> + split(Char,T,[H|Acc]); +split(_Char,[],Acc) -> + {lists:reverse(Acc),[]}. + +size_or_term(Fd) -> + size_or_term(Fd,get(pos)). +size_or_term(Fd,Pos) -> + case count_rest_of_line(Fd) of + {eof,Size} -> + {size,true,Size,Pos}; + Size when Size > ?max_display_size -> + {size,false,Size,Pos}; + _Size -> + {ok,Pos} = pos_bof(Fd,Pos), + val(Fd) + end. + +%%%----------------------------------------------------------------- +%%% +get_value(Key,List) -> + case lists:keysearch(Key,1,List) of + {value,{Key,Value}} -> {ok,Value}; + false -> error + end. + +parse_vsn_str([],WS,false) -> + %% If the log is translated, crashdump_translate might have written + %% shared_heap=true in dictionary. + case erase(shared_heap) of + true -> {WS,true}; + _ -> {WS,false} + end; +parse_vsn_str([],WS,SH) -> + {WS,SH}; +parse_vsn_str(Str,WS,SH) -> + case Str of + "[64-bit]" ++ Rest -> + case SH of + false -> + parse_vsn_str(Rest,8,false); + _ -> + {8,SH} + end; + "[shared heap]" ++ Rest -> + case WS of + 4 -> + parse_vsn_str(Rest,WS,true); + _ -> + {WS,true} + end; + [_Char|Rest] -> + parse_vsn_str(Rest,WS,SH) + end. + + +%%%----------------------------------------------------------------- +%%% +initial_menu() -> + insert_items( + [menu_item(0, {"./general_info","General information"},0), + menu_item(0, {"./processes","Processes"}, 0), + menu_item(0, {"./ports","Ports"}, 0), + menu_item(0, {"./ets_tables","ETS tables"}, 0), + menu_item(0, {"./timers","Timers"}, 0), + menu_item(0, {"./fun_table","Fun table"}, 0), + menu_item(0, {"./atoms","Atoms"}, 0), + menu_item(0, {"./dist_info","Distribution information"}, 0), + menu_item(0, {"./loaded_modules","Loaded modules"}, 0), + menu_item(2, "Internal Tables", 0), + menu_item(0, {"./hash_tables","Hash tables"}, 1), + menu_item(0, {"./index_tables","Index tables"}, 1), + menu_item(3, "Memory information", 0), + menu_item(0, {"./memory","Memory"}, 1), + menu_item(0, {"./allocated_areas","Allocated areas"}, 1), + menu_item(0, {"./allocator_info","Allocator information"}, 1), + menu_item(2, "Documentation", 0), + menu_item(0, {"/crashdump_doc/crashdump_help.html", + "Crashdump Viewer help"}, 1,"doc"), + menu_item(0, {"/crashdump_erts_doc/crash_dump.html", + "How to interpret Erlang crashdumps"}, 1,"doc")]). + +menu_item(Children,Text,Depth) -> + menu_item(Children,Text,Depth,"main"). +menu_item(Children,Text,Depth,Target) -> + #menu_item{picture=get_pic(Children), + text=Text, + depth=Depth, + children=Children, + state=if Depth==0 -> true; true -> false end, + target=Target}. + +insert_items(Items) -> + insert_items(Items,1). +insert_items([Item|Items],Index) -> + ets:insert(cdv_menu_table,Item#menu_item{index=Index}), + insert_items(Items,Index+1); +insert_items([],_) -> + ok. + +get_pic(0) -> + ""; +get_pic(_) -> + "/crashdump_viewer/collapsd.gif". + +do_toggle(Index) -> + [Item]= ets:lookup(cdv_menu_table,Index), + case toggle_children(Index,Index+Item#menu_item.children, + Item#menu_item.depth+1,undefined) of + true -> + ets:insert(cdv_menu_table, + Item#menu_item{picture= + "/crashdump_viewer/exploded.gif"}); + false -> + ets:insert(cdv_menu_table, + Item#menu_item{picture= + "/crashdump_viewer/collapsd.gif"}) + end. + +toggle_children(Index,Max,_Depth,ToggleState) when Index>Max-> + ToggleState; +toggle_children(Index,Max,Depth,ToggleState) -> + case ets:lookup(cdv_menu_table,Index+1) of + [#menu_item{depth=Depth}=Child] -> + NewState = not Child#menu_item.state, + ets:insert(cdv_menu_table,Child#menu_item{state=NewState}), + toggle_children(Index+1,Max,Depth,NewState); + _ -> + toggle_children(Index+1,Max,Depth,ToggleState) + end. + +%%%----------------------------------------------------------------- +%%% Traverse crash dump and insert index in table for each heading +%%% +%%% This function is executed in a background process in order to +%%% avoid a timeout in the web browser. The browser displays "Please +%%% wait..." while this is going on. +%%% +%%% Variable written to process dictionary in this function are copied +%%% to the crashdump_viewer_server when the function is completed (see +%%% background_done/1). +read_file(File) -> + case file:read_file_info(File) of + {ok,#file_info{type=regular,access=FileA}} when FileA=:=read; + FileA=:=read_write -> + Fd = open(File), + case read(Fd) of + {ok,<<$=:8,TagAndRest/binary>>} -> + {Tag,Id,Rest,N1} = tag(Fd,TagAndRest,1), + case Tag of + "=erl_crash_dump" -> + ets:delete_all_objects(cdv_dump_index_table), + ets:insert(cdv_dump_index_table,{Tag,Id,N1+1}), + put(last_tag,{Tag,""}), + Status = background_status(processing,File), + background_status(Status), + indexify(Fd,Rest,N1), + check_if_truncated(), + initial_menu(), + Binaries = read_binaries(Fd), + R = crashdump_viewer_html:start_page(), + close(Fd), + background_done({R,File,Binaries}); + _Other -> + R = crashdump_viewer_html:error( + "~s is not an Erlang crash dump~n", + [File]), + close(Fd), + background_done({R,undefined,undefined}) + end; + {ok,<<"<Erlang crash dump>",_Rest/binary>>} -> + %% old version - no longer supported + R = crashdump_viewer_html:error( + "The crashdump ~s is in the pre-R10B format, " + "which is no longer supported.~n", + [File]), + close(Fd), + background_done({R,undefined,undefined}); + _Other -> + R = crashdump_viewer_html:error( + "~s is not an Erlang crash dump~n", + [File]), + close(Fd), + background_done({R,undefined,undefined}) + end; + _other -> + R = crashdump_viewer_html:error("~s is not an Erlang crash dump~n", + [File]), + background_done({R,undefined,undefined}) + end. + +indexify(Fd,<<"\n=",TagAndRest/binary>>,N) -> + {Tag,Id,Rest,N1} = tag(Fd,TagAndRest,N+2), + ets:insert(cdv_dump_index_table,{Tag,Id,N1+1}), % +1 to get past newline + put(last_tag,{Tag,Id}), + indexify(Fd,Rest,N1); +indexify(Fd,<<>>,N) -> + case read(Fd) of + {ok,Chunk} when is_binary(Chunk) -> + indexify(Fd,Chunk,N); + eof -> + eof + end; +indexify(Fd,<<$\n>>,N) -> + %% This clause is needed in case the chunk ends with a newline and + %% the next chunk starts with a tag (i.e. "\n=....") + case read(Fd) of + {ok,Chunk} when is_binary(Chunk) -> + indexify(Fd,<<$\n,Chunk/binary>>,N); + eof -> + eof + end; +indexify(Fd,<<_Char:8,Rest/binary>>,N) -> + indexify(Fd,Rest,N+1). + +tag(Fd,Bin,N) -> + tag(Fd,Bin,N,[],[],tag). +tag(_Fd,<<$\n:8,_/binary>>=Rest,N,Gat,Di,_Now) -> + {[$=|lists:reverse(Gat)],lists:reverse(Di),Rest,N}; +tag(Fd,<<$\r:8,Rest/binary>>,N,Gat,Di,Now) -> + tag(Fd,Rest,N+1,Gat,Di,Now); +tag(Fd,<<$::8,IdAndRest/binary>>,N,Gat,Di,tag) -> + tag(Fd,IdAndRest,N+1,Gat,Di,id); +tag(Fd,<<Char:8,Rest/binary>>,N,Gat,Di,tag) -> + tag(Fd,Rest,N+1,[Char|Gat],Di,tag); +tag(Fd,<<Char:8,Rest/binary>>,N,Gat,Di,id) -> + tag(Fd,Rest,N+1,Gat,[Char|Di],id); +tag(Fd,<<>>,N,Gat,Di,Now) -> + case read(Fd) of + {ok,Chunk} when is_binary(Chunk) -> + tag(Fd,Chunk,N,Gat,Di,Now); + eof -> + {[$=|lists:reverse(Gat)],lists:reverse(Di),<<>>,N} + end. + +check_if_truncated() -> + case get(last_tag) of + {"=end",_} -> + put(truncated,false), + put(truncated_proc,false); + TruncatedTag -> + put(truncated,true), + find_truncated_proc(TruncatedTag) + end. + +find_truncated_proc({"=atom",_Id}) -> + put(truncated_proc,false); +find_truncated_proc({Tag,Pid}) -> + case is_proc_tag(Tag) of + true -> + put(truncated_proc,Pid); + false -> + %% This means that the dump is truncated between "=proc" and + %% "=proc_heap" => memory info is missing for all procs. + put(truncated_proc,"<0.0.0>") + end. + +is_proc_tag(Tag) when Tag=="=proc"; + Tag=="=proc_dictionary"; + Tag=="=proc_messages"; + Tag=="=proc_dictionary"; + Tag=="=debug_proc_dictionary"; + Tag=="=proc_stack"; + Tag=="=proc_heap" -> + true; +is_proc_tag(_) -> + false. + +related_tags("Atoms") -> + ["=atoms","=num_atoms"]. + +%%% Inform the crashdump_viewer_server that a background job is completed. +background_done(Result) -> + Dict = get(), + cast({background_done,Result,Dict}). + +background_status(Status) -> + cast({background_status,Status}). + +%%%----------------------------------------------------------------- +%%% Functions for reading information from the dump +general_info(File) -> + [{"=erl_crash_dump",_Id,Start}] = + ets:lookup(cdv_dump_index_table,"=erl_crash_dump"), + Fd = open(File), + pos_bof(Fd,Start), + Created = case get_rest_of_line(Fd) of + {eof,SomeOfLine} -> SomeOfLine; + WholeLine -> WholeLine + end, + + GI0 = get_general_info(Fd,#general_info{created=Created,_=?space}), + GI = case GI0#general_info.num_atoms of + ?space -> GI0#general_info{num_atoms=get_num_atoms(Fd)}; + _ -> GI0 + end, + + {MemTot,MemMax} = + case ets:lookup(cdv_dump_index_table,"=memory") of + [{"=memory",_,MemStart}] -> + pos_bof(Fd,MemStart), + Memory = get_meminfo(Fd,[]), + Tot = case lists:keysearch("total",1,Memory) of + {value,{_,T}} -> T; + false -> "" + end, + Max = case lists:keysearch("maximum",1,Memory) of + {value,{_,M}} -> M; + false -> "" + end, + {Tot,Max}; + _ -> + {"",""} + end, + + close(Fd), + {NumProcs,NumEts,NumFuns} = count(), + NodeName = + case ets:lookup(cdv_dump_index_table,"=node") of + [{"=node",N,_Start}] -> + N; + [] -> + case ets:lookup(cdv_dump_index_table,"=no_distribution") of + [_] -> "nonode@nohost"; + [] -> "unknown" + end + end, + + InstrInfo = + case ets:member(cdv_dump_index_table,"=old_instr_data") of + true -> + old_instr_data; + false -> + case ets:member(cdv_dump_index_table,"=instr_data") of + true -> + instr_data; + false -> + false + end + end, + GI#general_info{node_name=NodeName, + num_procs=integer_to_list(NumProcs), + num_ets=integer_to_list(NumEts), + num_fun=integer_to_list(NumFuns), + mem_tot=MemTot, + mem_max=MemMax, + instr_info=InstrInfo}. + +get_general_info(Fd,GenInfo) -> + case line_head(Fd) of + "Slogan" -> + get_general_info(Fd,GenInfo#general_info{slogan=val(Fd)}); + "System version" -> + get_general_info(Fd,GenInfo#general_info{system_vsn=val(Fd)}); + "Compiled" -> + get_general_info(Fd,GenInfo#general_info{compile_time=val(Fd)}); + "Atoms" -> + get_general_info(Fd,GenInfo#general_info{num_atoms=val(Fd)}); + "=" ++ _next_tag -> + GenInfo; + Other -> + unexpected(Fd,Other,"general information"), + GenInfo + end. + +get_num_atoms(Fd) -> + case ets:match(cdv_dump_index_table,{"=hash_table","atom_tab",'$1'}) of + [[Pos]] -> + pos_bof(Fd,Pos), + skip_rest_of_line(Fd), % size + skip_rest_of_line(Fd), % used + case line_head(Fd) of + "objs" -> + val(Fd); + _1 -> + get_num_atoms2() + end; + [] -> + get_num_atoms2() + end. +get_num_atoms2() -> + case ets:lookup(cdv_dump_index_table,"=num_atoms") of + [] -> + ?space; + [{"=num_atoms",NA,_Pos}] -> + %% If dump is translated this will exist + case get(truncated) of + true -> + [NA," (visible in dump)"]; % might be more + false -> + NA + end + end. + +count() -> + {ets:select_count(cdv_dump_index_table,count_ms("=proc")), + ets:select_count(cdv_dump_index_table,count_ms("=ets")), + ets:select_count(cdv_dump_index_table,count_ms("=fun"))}. + +count_ms(Tag) -> + [{{Tag,'_','_'},[],[true]}]. + + +procs_summary(File) -> + AllProcs = ets:lookup(cdv_dump_index_table,"=proc"), + Fd = open(File), + R = lists:map(fun({"=proc",Pid,Start}) -> + pos_bof(Fd,Start), + get_procinfo(Fd,fun main_procinfo/4, + ?initial_proc_record(Pid)) + end, + AllProcs), + close(Fd), + R. + +get_proc_details(File,Pid) -> + DumpVsn = ets:lookup_element(cdv_dump_index_table,"=erl_crash_dump",2), + case ets:match(cdv_dump_index_table,{"=proc",Pid,'$1'}) of + [[Start]] -> + Fd = open(File), + pos_bof(Fd,Start), + Proc0 = + case DumpVsn of + "0.0" -> + %% Old version (translated) + ?initial_proc_record(Pid); + _ -> + (?initial_proc_record(Pid))#proc{ + stack_dump=if_exist("=proc_stack",Pid), + msg_q=if_exist("=proc_messages",Pid), + dict=if_exist("=proc_dictionary",Pid), + debug_dict=if_exist("=debug_proc_dictionary",Pid)} + end, + Proc = get_procinfo(Fd,fun all_procinfo/4,Proc0), + close(Fd), + {ok,Proc}; + _ -> + case maybe_other_node(File,Pid) of + {other_node,Type,Node} -> + Info = "The process you are searching for was residing on " + "a remote node. No process information is available. " + "Information about the remote node is show below.", + {other_node,{Type,Info,Node}}; + not_found -> + not_found + end + end. + +if_exist(Tag,Key) -> + case ets:select_count(cdv_dump_index_table,[{{Tag,Key,'_'},[],[true]}]) of + 0 -> + Tag1 = + case is_proc_tag(Tag) of + true -> "=proc"; + false -> Tag + end, + case truncated_here({Tag1,Key}) of + true -> truncated; + false -> ?space + end; + _ -> + expand + end. + +get_procinfo(Fd,Fun,Proc) -> + case line_head(Fd) of + "State" -> + State = case val(Fd) of + "Garbing" -> "Garbing\n(limited info)"; + State0 -> State0 + end, + get_procinfo(Fd,Fun,Proc#proc{state=State}); + "Name" -> + get_procinfo(Fd,Fun,Proc#proc{name=val(Fd)}); + "Spawned as" -> + IF = val(Fd), + case Proc#proc.name of + ?space -> + get_procinfo(Fd,Fun,Proc#proc{name=IF,init_func=IF}); + _ -> + get_procinfo(Fd,Fun,Proc#proc{init_func=IF}) + end; + "Spawned by" -> + case val(Fd) of + "[]" -> + get_procinfo(Fd,Fun,Proc); + Parent -> + get_procinfo(Fd,Fun,Proc#proc{parent=Parent}) + end; + "Started" -> + get_procinfo(Fd,Fun,Proc#proc{start_time=val(Fd)}); + "Last scheduled in for" -> + get_procinfo(Fd,Fun,Proc#proc{current_func= + {"Last scheduled in for", + val(Fd)}}); + "Current call" -> + get_procinfo(Fd,Fun,Proc#proc{current_func={"Current call", + val(Fd)}}); + "Message queue length" -> + %% stored as integer so we can sort on it + get_procinfo(Fd,Fun,Proc#proc{msg_q_len=list_to_integer(val(Fd))}); + "Reductions" -> + %% stored as integer so we can sort on it + get_procinfo(Fd,Fun,Proc#proc{reds=list_to_integer(val(Fd))}); + "Number of heap fragments" -> + get_procinfo(Fd,Fun,Proc#proc{num_heap_frag=val(Fd)}); + "Heap fragment data" -> + get_procinfo(Fd,Fun,Proc#proc{heap_frag_data=val(Fd)}); + Stack when Stack=:="Stack+heap"; Stack=:="Stack" -> + %% stored as integer so we can sort on it + get_procinfo(Fd,Fun,Proc#proc{stack_heap= + list_to_integer(val(Fd))}); + "OldHeap" -> + get_procinfo(Fd,Fun,Proc#proc{old_heap=val(Fd)}); + "Heap unused" -> + get_procinfo(Fd,Fun,Proc#proc{heap_unused=val(Fd)}); + "OldHeap unused" -> + get_procinfo(Fd,Fun,Proc#proc{old_heap_unused=val(Fd)}); + "New heap start" -> + get_procinfo(Fd,Fun,Proc#proc{new_heap_start=val(Fd)}); + "New heap top" -> + get_procinfo(Fd,Fun,Proc#proc{new_heap_top=val(Fd)}); + "Stack top" -> + get_procinfo(Fd,Fun,Proc#proc{stack_top=val(Fd)}); + "Stack end" -> + get_procinfo(Fd,Fun,Proc#proc{stack_end=val(Fd)}); + "Old heap start" -> + get_procinfo(Fd,Fun,Proc#proc{old_heap_start=val(Fd)}); + "Old heap top" -> + get_procinfo(Fd,Fun,Proc#proc{old_heap_top=val(Fd)}); + "Old heap end" -> + get_procinfo(Fd,Fun,Proc#proc{old_heap_end=val(Fd)}); + {eof,_} -> + Proc; % truncated file + Other -> + Fun(Fd,Fun,Proc,Other) + end. + +main_procinfo(Fd,Fun,Proc,LineHead) -> + case LineHead of + "Stack dump" -> + %% This is the last element in older dumps (DumpVsn=0.0) + Proc; + "=" ++ _next_tag -> + %% DumpVsn=0.1 or newer: No stack dump here + Proc; + "arity = " ++ _ -> + %%! Temporary workaround + get_procinfo(Fd,Fun,Proc); + _Other -> + skip_rest_of_line(Fd), + get_procinfo(Fd,Fun,Proc) + end. +all_procinfo(Fd,Fun,Proc,LineHead) -> + case LineHead of + "Message queue" -> + get_procinfo(Fd,Fun,Proc#proc{msg_q=size_or_term(Fd)}); + "Last calls" -> + R = case size_or_term(Fd) of + SizeThing when is_tuple(SizeThing) -> + Proc#proc{last_calls=SizeThing}; + Term -> + Proc#proc{last_calls=replace_all($ ,$\n,Term,[])} + end, + get_procinfo(Fd,Fun,R); + "Link list" -> + get_procinfo(Fd,Fun,Proc#proc{links=val(Fd)}); + "Program counter" -> + get_procinfo(Fd,Fun,Proc#proc{prog_count=val(Fd)}); + "CP" -> + get_procinfo(Fd,Fun,Proc#proc{cp=val(Fd)}); + "arity = " ++ Arity -> + %%! Temporary workaround + get_procinfo(Fd,Fun,Proc#proc{arity=Arity--"\r\n"}); + "Dictionary" -> + get_procinfo(Fd,Fun,Proc#proc{dict=size_or_term(Fd)}); + "$Dictionary" -> + get_procinfo(Fd,Fun,Proc#proc{debug_dict=size_or_term(Fd)}); + "Stack dump" -> + %% This is the last element in older dumps (DumpVsn=0.0) + get_stack_dump(Fd,Proc); + "=" ++ _next_tag -> + %% DumpVsn=0.1 or newer: No stack dump here + Proc; + Other -> + unexpected(Fd,Other,"process info"), + get_procinfo(Fd,Fun,Proc) + end. + +get_stack_dump(Fd,Proc) -> + %% Always show stackdump as "Expand" link + Pos = get(pos), + Size = count_rest_of_tag(Fd), + Proc#proc{stack_dump={size,true,Size,Pos}}. + +maybe_other_node(File,Id) -> + Channel = + case split($.,Id) of + {"<" ++ N, _Rest} -> + N; + {"#Port<" ++ N, _Rest} -> + N + end, + Ms = ets:fun2ms( + fun({Tag,Id,Start}) when Tag=:="=visible_node", Id=:=Channel -> + {"Visible Node",Start}; + ({Tag,Id,Start}) when Tag=:="=hidden_node", Id=:=Channel -> + {"Hidden Node",Start}; + ({Tag,Id,Start}) when Tag=:="=not_connected", Id=:=Channel -> + {"Not Connected Node",Start} + end), + case ets:select(cdv_dump_index_table,Ms) of + [] -> + not_found; + [{Type,Pos}] -> + Fd = open(File), + NodeInfo = get_nodeinfo(Fd,Channel,Pos), + close(Fd), + {other_node,Type,NodeInfo} + end. + +expand_memory(File,What,Pid,Binaries) -> + Fd = open(File), + put(fd,Fd), + Dict = read_heap(Fd,Pid,Binaries), + Expanded = + case What of + "StackDump" -> read_stack_dump(Fd,Pid,Dict); + "MsgQueue" -> read_messages(Fd,Pid,Dict); + "Dictionary" -> read_dictionary(Fd,"=proc_dictionary",Pid,Dict); + "DebugDictionary" -> read_dictionary(Fd,"=debug_proc_dictionary",Pid,Dict) + end, + erase(fd), + close(Fd), + Expanded. + +%%% +%%% Read binaries. +%%% +read_binaries(Fd) -> + AllBinaries = ets:match(cdv_dump_index_table,{"=binary",'$1','$2'}), + read_binaries(Fd,AllBinaries, gb_trees:empty()). + +read_binaries(Fd,[[Addr0,Pos]|Bins],Dict0) -> + pos_bof(Fd,Pos), + {Addr,_} = get_hex(Addr0), + Dict = + case line_head(Fd) of + {eof,_} -> + gb_trees:enter(Addr,'#CDVTruncatedBinary',Dict0); + Size0 -> + {Size,_} = get_hex(Size0), + if Size > ?max_display_binary_size -> + gb_trees:enter(Addr,{'#CDVTooBig',binary,Pos},Dict0); + true -> + pos_bof(Fd,Pos), + Line = val(Fd), + parse_binary(Addr,Line,Dict0) + end + end, + read_binaries(Fd,Bins,Dict); +read_binaries(_Fd,[],Dict) -> + Dict. + +parse_binary(Addr, Line0, Dict) -> + case get_hex(Line0) of + {N,":"++Line1} -> + {Bin,Line} = get_binary(N, Line1, []), + [] = skip_blanks(Line), + gb_trees:enter(Addr, Bin, Dict); + {_N,[]} -> + %% If the dump is truncated before the ':' in this line, then + %% line_head/1 might not discover it (if a \n has been inserted + %% somehow???) + gb_trees:enter(Addr,'#CDVTruncatedBinary',Dict) + end. + + + +%%% +%%% Read top level section. +%%% + +read_stack_dump(Fd,Pid,Dict) -> + case ets:match(cdv_dump_index_table,{"=proc_stack",Pid,'$1'}) of + [[Start]] -> + pos_bof(Fd,Start), + read_stack_dump1(Fd,Dict,[]); + [] -> + [] + end. +read_stack_dump1(Fd,Dict,Acc) -> + %% This function is never called if the dump is truncated in "=proc_heap:Pid" + case val(Fd) of + "=" ++ _next_tag -> + lists:reverse(Acc); + Line -> + Stack = parse_top(Line,Dict), + read_stack_dump1(Fd,Dict,[Stack|Acc]) + end. + +parse_top(Line0, D) -> + {Label,Line1} = get_label(Line0), + {Term,Line,D} = parse_term(Line1, D), + [] = skip_blanks(Line), + {Label,Term}. + +%%% +%%% Read message queue. +%%% + +read_messages(Fd,Pid,Dict) -> + case ets:match(cdv_dump_index_table,{"=proc_messages",Pid,'$1'}) of + [[Start]] -> + pos_bof(Fd,Start), + read_messages1(Fd,Dict,[]); + [] -> + [] + end. +read_messages1(Fd,Dict,Acc) -> + %% This function is never called if the dump is truncated in "=proc_heap:Pid" + case val(Fd) of + "=" ++ _next_tag -> + lists:reverse(Acc); + Line -> + Msg = parse_message(Line,Dict), + read_messages1(Fd,Dict,[Msg|Acc]) + end. + +parse_message(Line0, D) -> + {Msg,":"++Line1,_} = parse_term(Line0, D), + {Token,Line,_} = parse_term(Line1, D), + [] = skip_blanks(Line), + {Msg,Token}. + +%%% +%%% Read process dictionary +%%% + +read_dictionary(Fd,Tag,Pid,Dict) -> + case ets:match(cdv_dump_index_table,{Tag,Pid,'$1'}) of + [[Start]] -> + pos_bof(Fd,Start), + read_dictionary1(Fd,Dict,[]); + [] -> + [] + end. +read_dictionary1(Fd,Dict,Acc) -> + %% This function is never called if the dump is truncated in "=proc_heap:Pid" + case val(Fd) of + "=" ++ _next_tag -> + lists:reverse(Acc); + Line -> + Msg = parse_dictionary(Line,Dict), + read_dictionary1(Fd,Dict,[Msg|Acc]) + end. + +parse_dictionary(Line0, D) -> + {Entry,Line,_} = parse_term(Line0, D), + [] = skip_blanks(Line), + Entry. + +%%% +%%% Read heap data. +%%% + +read_heap(Fd,Pid,Dict0) -> + case ets:match(cdv_dump_index_table,{"=proc_heap",Pid,'$2'}) of + [[Pos]] -> + pos_bof(Fd,Pos), + read_heap(Dict0); + [] -> + Dict0 + end. + +read_heap(Dict0) -> + %% This function is never called if the dump is truncated in "=proc_heap:Pid" + case get(fd) of + end_of_heap -> + Dict0; + Fd -> + case val(Fd) of + "=" ++ _next_tag -> + put(fd, end_of_heap), + Dict0; + Line -> + Dict = parse(Line,Dict0), + read_heap(Dict) + end + end. + +parse(Line0, Dict0) -> + {Addr,":"++Line1} = get_hex(Line0), + {_Term,Line,Dict} = parse_heap_term(Line1, Addr, Dict0), + [] = skip_blanks(Line), + Dict. + + +do_sort_procs("state",Procs,"state") -> + {lists:reverse(lists:keysort(#proc.state,Procs)),"rstate"}; +do_sort_procs("state",Procs,_) -> + {lists:keysort(#proc.state,Procs),"state"}; +do_sort_procs("pid",Procs,"pid") -> + {lists:reverse(Procs),"rpid"}; +do_sort_procs("pid",Procs,_) -> + {Procs,"pid"}; +do_sort_procs("msg_q_len",Procs,"msg_q_len") -> + {lists:keysort(#proc.msg_q_len,Procs),"rmsg_q_len"}; +do_sort_procs("msg_q_len",Procs,_) -> + {lists:reverse(lists:keysort(#proc.msg_q_len,Procs)),"msg_q_len"}; +do_sort_procs("reds",Procs,"reds") -> + {lists:keysort(#proc.reds,Procs),"rreds"}; +do_sort_procs("reds",Procs,_) -> + {lists:reverse(lists:keysort(#proc.reds,Procs)),"reds"}; +do_sort_procs("mem",Procs,"mem") -> + {lists:keysort(#proc.stack_heap,Procs),"rmem"}; +do_sort_procs("mem",Procs,_) -> + {lists:reverse(lists:keysort(#proc.stack_heap,Procs)),"mem"}; +do_sort_procs("init_func",Procs,"init_func") -> + {lists:reverse(lists:keysort(#proc.init_func,Procs)),"rinit_func"}; +do_sort_procs("init_func",Procs,_) -> + {lists:keysort(#proc.init_func,Procs),"init_func"}; +do_sort_procs("name_func",Procs,"name_func") -> + {lists:reverse(lists:keysort(#proc.name,Procs)),"rname_func"}; +do_sort_procs("name_func",Procs,_) -> + {lists:keysort(#proc.name,Procs),"name_func"}; +do_sort_procs("name",Procs,Sorted) -> + {No,Yes} = + lists:foldl(fun(P,{N,Y}) -> + case P#proc.name of + ?space -> {[P|N],Y}; + _other -> {N,[P|Y]} + end + end, + {[],[]}, + Procs), + Result = lists:keysort(#proc.name,Yes) ++ No, + case Sorted of + "name" -> {lists:reverse(Result),"rname"}; + _ -> {Result,"name"} + end. + + +get_port(File,Port) -> + case ets:match(cdv_dump_index_table,{"=port",Port,'$1'}) of + [[Start]] -> + Fd = open(File), + R = get_portinfo(Fd,Port,Start), + close(Fd), + {ok,R}; + [] -> + case maybe_other_node(File,Port) of + {other_node,Type,Node} -> + Info = "The port you are searching for was residing on " + "a remote node. No port information is available. " + "Information about the remote node is show below.", + {other_node,{Type,Info,Node}}; + not_found -> + not_found + end + end. + +get_ports(File) -> + Ports = ets:lookup(cdv_dump_index_table,"=port"), + Fd = open(File), + R = lists:map(fun({"=port",Id,Start}) -> get_portinfo(Fd,Id,Start) end, + Ports), + close(Fd), + R. + + +get_portinfo(Fd,Id,Start) -> + pos_bof(Fd,Start), + get_portinfo(Fd,#port{id=Id,_=?space}). + +get_portinfo(Fd,Port) -> + case line_head(Fd) of + "Slot" -> + get_portinfo(Fd,Port#port{slot=val(Fd)}); + "Connected" -> + get_portinfo(Fd,Port#port{connected=val(Fd)}); + "Links" -> + get_portinfo(Fd,Port#port{links=val(Fd)}); + "Port controls linked-in driver" -> + get_portinfo(Fd,Port#port{controls=["Linked in driver: " | + val(Fd)]}); + "Port controls external process" -> + get_portinfo(Fd,Port#port{controls=["External proc: " | val(Fd)]}); + "Port is a file" -> + get_portinfo(Fd,Port#port{controls=["File: "| val(Fd)]}); + "Port is UNIX fd not opened by emulator" -> + get_portinfo(Fd,Port#port{ + controls=["UNIX fd not opened by emulator: "| + val(Fd)]}); + "=" ++ _next_tag -> + Port; + Other -> + unexpected(Fd,Other,"port info"), + Port + end. + +get_ets_tables(File,Pid,WS) -> + EtsTables = ets:match_object(cdv_dump_index_table,{"=ets",Pid,'_'}), + Fd = open(File), + R = lists:map(fun({"=ets",P,Start}) -> + get_etsinfo(Fd,P,Start,WS) + end, + EtsTables), + close(Fd), + R. + +get_internal_ets_tables(File,WS) -> + InternalEts = ets:match_object(cdv_dump_index_table, + {"=internal_ets",'_','_'}), + Fd = open(File), + R = lists:map(fun({"=internal_ets",Descr,Start}) -> + {Descr,get_etsinfo(Fd,undefined,Start,WS)} + end, + InternalEts), + close(Fd), + R. + +get_etsinfo(Fd,Pid,Start,WS) -> + pos_bof(Fd,Start), + get_etsinfo(Fd,#ets_table{pid=Pid,type="hash",_=?space},WS). + +get_etsinfo(Fd,EtsTable,WS) -> + case line_head(Fd) of + "Slot" -> + get_etsinfo(Fd,EtsTable#ets_table{slot=val(Fd)},WS); + "Table" -> + get_etsinfo(Fd,EtsTable#ets_table{id=val(Fd)},WS); + "Name" -> + get_etsinfo(Fd,EtsTable#ets_table{name=val(Fd)},WS); + "Ordered set (AVL tree), Elements" -> + skip_rest_of_line(Fd), + get_etsinfo(Fd,EtsTable#ets_table{type="tree",buckets="-"},WS); + "Buckets" -> + get_etsinfo(Fd,EtsTable#ets_table{buckets=val(Fd)},WS); + "Objects" -> + get_etsinfo(Fd,EtsTable#ets_table{size=val(Fd)},WS); + "Words" -> + Words = list_to_integer(val(Fd)), + Bytes = + case Words of + -1 -> "-1"; % probably truncated + _ -> integer_to_list(Words * WS) + end, + get_etsinfo(Fd,EtsTable#ets_table{memory=Bytes},WS); + "=" ++ _next_tag -> + EtsTable; + Other -> + unexpected(Fd,Other,"ETS info"), + EtsTable + end. + +get_timers(File,Pid) -> + Timers = ets:match_object(cdv_dump_index_table,{"=timer",Pid,'$1'}), + Fd = open(File), + R = lists:map(fun({"=timer",P,Start}) -> + get_timerinfo(Fd,P,Start) + end, + Timers), + close(Fd), + R. + +get_timerinfo(Fd,Pid,Start) -> + pos_bof(Fd,Start), + get_timerinfo(Fd,#timer{pid=Pid,_=?space}). + +get_timerinfo(Fd,Timer) -> + case line_head(Fd) of + "Message" -> + get_timerinfo(Fd,Timer#timer{msg=val(Fd)}); + "Time left" -> + get_timerinfo(Fd,Timer#timer{time=val(Fd)}); + "=" ++ _next_tag -> + Timer; + Other -> + unexpected(Fd,Other,"timer info"), + Timer + end. + +nods(File) -> + case ets:lookup(cdv_dump_index_table,"=no_distribution") of + [] -> + V = ets:lookup(cdv_dump_index_table,"=visible_node"), + H = ets:lookup(cdv_dump_index_table,"=hidden_node"), + N = ets:lookup(cdv_dump_index_table,"=not_connected"), + Fd = open(File), + Visible = lists:map( + fun({"=visible_node",Channel,Start}) -> + get_nodeinfo(Fd,Channel,Start) + end, + V), + Hidden = lists:map( + fun({"=hidden_node",Channel,Start}) -> + get_nodeinfo(Fd,Channel,Start) + end, + H), + NotConnected = lists:map( + fun({"=not_connected",Channel,Start}) -> + get_nodeinfo(Fd,Channel,Start) + end, + N), + close(Fd), + {Visible,Hidden,NotConnected}; + [_] -> + no_distribution + end. + +get_nodeinfo(Fd,Channel,Start) -> + pos_bof(Fd,Start), + get_nodeinfo(Fd,#nod{channel=Channel,_=?space}). + +get_nodeinfo(Fd,Nod) -> + case line_head(Fd) of + "Name" -> + get_nodeinfo(Fd,Nod#nod{name=val(Fd)}); + "Controller" -> + get_nodeinfo(Fd,Nod#nod{controller=val(Fd)}); + "Creation" -> + get_nodeinfo(Fd,Nod#nod{creation=val(Fd)}); + "Remote link" -> + Procs = val(Fd), % e.g. "<0.31.0> <4322.54.0>" + RemoteLinks = Nod#nod.remote_links, + get_nodeinfo(Fd,Nod#nod{remote_links=[split(Procs)|RemoteLinks]}); + "Remote monitoring" -> + Procs = val(Fd), % e.g. "<0.31.0> <4322.54.0>" + RemoteMon = Nod#nod.remote_mon, + get_nodeinfo(Fd,Nod#nod{remote_mon=[split(Procs)|RemoteMon]}); + "Remotely monitored by" -> + Procs = val(Fd), % e.g. "<0.31.0> <4322.54.0>" + RemoteMonBy = Nod#nod.remote_mon_by, + get_nodeinfo(Fd,Nod#nod{remote_mon_by=[split(Procs)|RemoteMonBy]}); + "Error" -> + get_nodeinfo(Fd,Nod#nod{error=val(Fd)}); + "=" ++ _next_tag -> + Nod; + Other -> + unexpected(Fd,Other,"node info"), + Nod + end. + +loaded_mods(File) -> + case ets:lookup(cdv_dump_index_table,"=loaded_modules") of + [{"=loaded_modules",_,StartTotal}] -> + Fd = open(File), + pos_bof(Fd,StartTotal), + {CC,OC} = get_loaded_mod_totals(Fd,{"unknown","unknown"}), + + Mods = ets:lookup(cdv_dump_index_table,"=mod"), + LM = lists:map(fun({"=mod",M,Start}) -> + pos_bof(Fd,Start), + InitLM = #loaded_mod{mod=M,_=?space}, + get_loaded_mod_info(Fd,InitLM, + fun main_modinfo/3) + end, + Mods), + close(Fd), + {CC,OC,LM}; + [] -> + {"unknown","unknown",[]} + end. + +get_loaded_mod_totals(Fd,{CC,OC}) -> + case line_head(Fd) of + "Current code" -> + get_loaded_mod_totals(Fd,{val(Fd),OC}); + "Old code" -> + get_loaded_mod_totals(Fd,{CC,val(Fd)}); + "=" ++ _next_tag -> + {CC,OC}; + Other -> + unexpected(Fd,Other,"loaded modules info"), + {CC,OC} % truncated file + end. + +get_loaded_mod_details(File,Mod) -> + [[Start]] = ets:match(cdv_dump_index_table,{"=mod",Mod,'$1'}), + Fd = open(File), + pos_bof(Fd,Start), + InitLM = #loaded_mod{mod=Mod,old_size="No old code exists", + _="No information available"}, + ModInfo = get_loaded_mod_info(Fd,InitLM,fun all_modinfo/3), + close(Fd), + ModInfo. + +get_loaded_mod_info(Fd,LM,Fun) -> + case line_head(Fd) of + "Current size" -> + get_loaded_mod_info(Fd,LM#loaded_mod{current_size=val(Fd)},Fun); + "Old size" -> + get_loaded_mod_info(Fd,LM#loaded_mod{old_size=val(Fd)},Fun); + "=" ++ _next_tag -> + LM; + {eof,_} -> + LM; % truncated file + Other -> + LM1 = Fun(Fd,LM,Other), + get_loaded_mod_info(Fd,LM1,Fun) + end. + +main_modinfo(_Fd,LM,_LineHead) -> + LM. +all_modinfo(Fd,LM,LineHead) -> + case LineHead of + "Current attributes" -> + Str = hex_to_str(val(Fd)), + LM#loaded_mod{current_attrib=Str}; + "Current compilation info" -> + Str = hex_to_str(val(Fd)), + LM#loaded_mod{current_comp_info=Str}; + "Old attributes" -> + Str = hex_to_str(val(Fd)), + LM#loaded_mod{old_attrib=Str}; + "Old compilation info" -> + Str = hex_to_str(val(Fd)), + LM#loaded_mod{old_comp_info=Str}; + Other -> + unexpected(Fd,Other,"loaded modules info"), + LM + end. + + +hex_to_str(Hex) -> + Term = hex_to_term(Hex,[]), + io_lib:format("~p~n",[Term]). + +hex_to_term([X,Y|Hex],Acc) -> + MS = hex_to_dec([X]), + LS = hex_to_dec([Y]), + Z = 16*MS+LS, + hex_to_term(Hex,[Z|Acc]); +hex_to_term([],Acc) -> + Bin = list_to_binary(lists:reverse(Acc)), + case catch binary_to_term(Bin) of + {'EXIT',_Reason} -> + {"WARNING: The term is probably truncated!", + "I can not do binary_to_term.", + Bin}; + Term -> + Term + end. + +hex_to_dec("F") -> 15; +hex_to_dec("E") -> 14; +hex_to_dec("D") -> 13; +hex_to_dec("C") -> 12; +hex_to_dec("B") -> 11; +hex_to_dec("A") -> 10; +hex_to_dec(N) -> list_to_integer(N). + + + +funs(File) -> + case ets:lookup(cdv_dump_index_table,"=fun") of + [] -> + []; + AllFuns -> + Fd = open(File), + R = lists:map(fun({"=fun",_,Start}) -> + get_funinfo(Fd,Start) + end, + AllFuns), + close(Fd), + R + end. + +get_funinfo(Fd,Start) -> + pos_bof(Fd,Start), + get_funinfo1(Fd,#fu{_=?space}). + +get_funinfo1(Fd,Fu) -> + case line_head(Fd) of + "Module" -> + get_funinfo1(Fd,Fu#fu{module=val(Fd)}); + "Uniq" -> + get_funinfo1(Fd,Fu#fu{uniq=val(Fd)}); + "Index" -> + get_funinfo1(Fd,Fu#fu{index=val(Fd)}); + "Address" -> + get_funinfo1(Fd,Fu#fu{address=val(Fd)}); + "Native_address" -> + get_funinfo1(Fd,Fu#fu{native_address=val(Fd)}); + "Refc" -> + get_funinfo1(Fd,Fu#fu{refc=val(Fd)}); + "=" ++ _next_tag -> + Fu; + Other -> + unexpected(Fd,Other,"fun info"), + Fu + end. + +atoms(File) -> + case ets:lookup(cdv_dump_index_table,"=atoms") of + [{_atoms,_Id,Start}] -> + Fd = open(File), + pos_bof(Fd,Start), + R = case get_n_lines_of_tag(Fd,100) of + {all,N,Lines} -> + {n_lines,1,N,"Atoms",Lines}; + {part,100,Lines} -> + {n_lines,1,100,"Atoms",Lines,get(pos)}; + empty -> + [] + end, + close(Fd), + R; + _ -> + [] + end. + +memory(File) -> + case ets:lookup(cdv_dump_index_table,"=memory") of + [{"=memory",_,Start}] -> + Fd = open(File), + pos_bof(Fd,Start), + R = get_meminfo(Fd,[]), + close(Fd), + R; + _ -> + [] + end. + +get_meminfo(Fd,Acc) -> + case line_head(Fd) of + "=" ++ _next_tag -> + lists:reverse(Acc); + {eof,_last_line} -> + lists:reverse(Acc); + Key -> + get_meminfo(Fd,[{Key,val(Fd)}|Acc]) + end. + +allocated_areas(File) -> + case ets:lookup(cdv_dump_index_table,"=allocated_areas") of + [{"=allocated_areas",_,Start}] -> + Fd = open(File), + pos_bof(Fd,Start), + R = get_allocareainfo(Fd,[]), + close(Fd), + R; + _ -> + [] + end. + +get_allocareainfo(Fd,Acc) -> + case line_head(Fd) of + "=" ++ _next_tag -> + lists:reverse(Acc); + {eof,_last_line} -> + lists:reverse(Acc); + Key -> + Val = val(Fd), + AllocInfo = + case split(Val) of + {Alloc,[]} -> + {Key,Alloc,?space}; + {Alloc,Used} -> + {Key,Alloc,Used} + end, + get_allocareainfo(Fd,[AllocInfo|Acc]) + end. + +allocator_info(File) -> + case ets:lookup(cdv_dump_index_table,"=allocator") of + [] -> + []; + AllAllocators -> + Fd = open(File), + R = lists:map(fun({"=allocator",Heading,Start}) -> + {Heading,get_allocatorinfo(Fd,Start)} + end, + AllAllocators), + close(Fd), + R + end. + +get_allocatorinfo(Fd,Start) -> + pos_bof(Fd,Start), + get_allocatorinfo1(Fd,[]). + +get_allocatorinfo1(Fd,Acc) -> + case line_head(Fd) of + "=" ++ _next_tag -> + lists:reverse(Acc); + {eof,_last_line} -> + lists:reverse(Acc); + Key -> + Values = get_all_vals(val(Fd),[]), + get_allocatorinfo1(Fd,[{Key,Values}|Acc]) + end. + +get_all_vals([$ |Rest],Acc) -> + [lists:reverse(Acc)|get_all_vals(Rest,[])]; +get_all_vals([],Acc) -> + [lists:reverse(Acc)]; +get_all_vals([Char|Rest],Acc) -> + get_all_vals(Rest,[Char|Acc]). + + +hash_tables(File) -> + case ets:lookup(cdv_dump_index_table,"=hash_table") of + [] -> + []; + AllHashTables -> + Fd = open(File), + R = lists:map(fun({"=hash_table",Name,Start}) -> + get_hashtableinfo(Fd,Name,Start) + end, + AllHashTables), + close(Fd), + R + end. + +get_hashtableinfo(Fd,Name,Start) -> + pos_bof(Fd,Start), + get_hashtableinfo1(Fd,#hash_table{name=Name,_=?space}). + +get_hashtableinfo1(Fd,HashTable) -> + case line_head(Fd) of + "size" -> + get_hashtableinfo1(Fd,HashTable#hash_table{size=val(Fd)}); + "used" -> + get_hashtableinfo1(Fd,HashTable#hash_table{used=val(Fd)}); + "objs" -> + get_hashtableinfo1(Fd,HashTable#hash_table{objs=val(Fd)}); + "depth" -> + get_hashtableinfo1(Fd,HashTable#hash_table{depth=val(Fd)}); + "=" ++ _next_tag -> + HashTable; + Other -> + unexpected(Fd,Other,"hash table information"), + HashTable + end. + +index_tables(File) -> + case ets:lookup(cdv_dump_index_table,"=index_table") of + [] -> + []; + AllIndexTables -> + Fd = open(File), + R = lists:map(fun({"=index_table",Name,Start}) -> + get_indextableinfo(Fd,Name,Start) + end, + AllIndexTables), + close(Fd), + R + end. + +get_indextableinfo(Fd,Name,Start) -> + pos_bof(Fd,Start), + get_indextableinfo1(Fd,#index_table{name=Name,_=?space}). + +get_indextableinfo1(Fd,IndexTable) -> + case line_head(Fd) of + "size" -> + get_indextableinfo1(Fd,IndexTable#index_table{size=val(Fd)}); + "used" -> + get_indextableinfo1(Fd,IndexTable#index_table{used=val(Fd)}); + "limit" -> + get_indextableinfo1(Fd,IndexTable#index_table{limit=val(Fd)}); + "rate" -> + get_indextableinfo1(Fd,IndexTable#index_table{rate=val(Fd)}); + "=" ++ _next_tag -> + IndexTable; + Other -> + unexpected(Fd,Other,"index table information"), + IndexTable + end. + + + + + +get_expanded(File,Pos,Size) -> + Fd = open(File), + R = case file:pread(Fd,Pos,Size) of + {ok,Bin}-> + binary_to_list(Bin); + eof -> + ?space + end, + close(Fd), + R. + + +get_next(File,Pos,N0,Start,What) -> + Fd = open(File), + pos_bof(Fd,Pos), + R = case get_n_lines_of_tag(Fd,N0) of + {all,N,Lines} -> + {n_lines,Start,N,What,Lines}; + {part,N,Lines} -> + {n_lines,Start,N,What,Lines,get(pos)} + end, + close(Fd), + R. + + + +replace_all(From,To,[From|Rest],Acc) -> + replace_all(From,To,Rest,[To|Acc]); +replace_all(From,To,[Char|Rest],Acc) -> + replace_all(From,To,Rest,[Char|Acc]); +replace_all(_From,_To,[],Acc) -> + lists:reverse(Acc). + + +%%%----------------------------------------------------------------- +%%% Parse memory in crashdump version 0.1 and newer +%%% +parse_heap_term([$l|Line0], Addr, D0) -> %Cons cell. + {H,"|"++Line1,D1} = parse_term(Line0, D0), + {T,Line,D2} = parse_term(Line1, D1), + Term = [H|T], + D = gb_trees:insert(Addr, Term, D2), + {Term,Line,D}; +parse_heap_term([$t|Line0], Addr, D) -> %Tuple + {N,":"++Line} = get_hex(Line0), + parse_tuple(N, Line, Addr, D, []); +parse_heap_term([$F|Line0], Addr, D0) -> %Float + {N,":"++Line1} = get_hex(Line0), + {Chars,Line} = get_chars(N, Line1), + Term = list_to_float(Chars), + D = gb_trees:insert(Addr, Term, D0), + {Term,Line,D}; +parse_heap_term("B16#"++Line0, Addr, D0) -> %Positive big number. + {Term,Line} = get_hex(Line0), + D = gb_trees:insert(Addr, Term, D0), + {Term,Line,D}; +parse_heap_term("B-16#"++Line0, Addr, D0) -> %Negative big number + {Term0,Line} = get_hex(Line0), + Term = -Term0, + D = gb_trees:insert(Addr, Term, D0), + {Term,Line,D}; +parse_heap_term("B"++Line0, Addr, D0) -> %Decimal big num (new in R10B-something). + case string:to_integer(Line0) of + {Int,Line} when is_integer(Int) -> + D = gb_trees:insert(Addr, Int, D0), + {Int,Line,D} + end; +parse_heap_term([$P|Line0], Addr, D0) -> % External Pid. + {Pid0,Line} = get_id(Line0), + Pid = "#CDVPid"++Pid0, + D = gb_trees:insert(Addr, Pid, D0), + {Pid,Line,D}; +parse_heap_term([$p|Line0], Addr, D0) -> % External Port. + {Port0,Line} = get_id(Line0), + Port = "#CDVPort"++Port0, + D = gb_trees:insert(Addr, Port, D0), + {Port,Line,D}; +parse_heap_term("E"++Line0, Addr, D0) -> %Term encoded in external format. + {Bin,Line} = get_binary(Line0), + Term = binary_to_term(Bin), + D = gb_trees:insert(Addr, Term, D0), + {Term,Line,D}; +parse_heap_term("Yh"++Line0, Addr, D0) -> %Heap binary. + {Term,Line} = get_binary(Line0), + D = gb_trees:insert(Addr, Term, D0), + {Term,Line,D}; +parse_heap_term("Yc"++Line0, Addr, D0) -> %Reference-counted binary. + {Binp,":"++Line1} = get_hex(Line0), + {First,":"++Line2} = get_hex(Line1), + {Sz,Line} = get_hex(Line2), + Term = case gb_trees:lookup(Binp, D0) of + {value,<<_:First/binary,T:Sz/binary,_/binary>>} -> T; + {value,{'#CDVTooBig',binary,Pos}} -> cdvbin(Sz,Pos); + {value,'#CDVTruncatedBinary'} -> '#CDVTruncatedBinary'; + none -> '#CDVNonexistingBinary' + end, + D = gb_trees:insert(Addr, Term, D0), + {Term,Line,D}; +parse_heap_term("Ys"++Line0, Addr, D0) -> %Sub binary. + {Binp,":"++Line1} = get_hex(Line0), + {First,":"++Line2} = get_hex(Line1), + {Sz,Line} = get_hex(Line2), + Term = case gb_trees:lookup(Binp, D0) of + {value,<<_:First/binary,T:Sz/binary,_/binary>>} -> T; + {value,{'#CDVTooBig',binary,Pos}} -> cdvbin(Sz,Pos); + {value,'#CDVTruncatedBinary'} -> '#CDVTruncatedBinary'; + none -> '#CDVNonexistingBinary' + end, + D = gb_trees:insert(Addr, Term, D0), + {Term,Line,D}. + + +parse_tuple(0, Line, Addr, D0, Acc) -> + Tuple = list_to_tuple(lists:reverse(Acc)), + D = gb_trees:insert(Addr, Tuple, D0), + {Tuple,Line,D}; +parse_tuple(N, Line0, Addr, D0, Acc) -> + case parse_term(Line0, D0) of + {Term,[$,|Line],D} when N > 1 -> + parse_tuple(N-1, Line, Addr, D, [Term|Acc]); + {Term,Line,D}-> + parse_tuple(N-1, Line, Addr, D, [Term|Acc]) + end. + +parse_term([$H|Line0], D) -> %Pointer to heap term. + {Ptr,Line} = get_hex(Line0), + deref_ptr(Ptr, Line, D); +parse_term([$N|Line], D) -> %[] (nil). + {[],Line,D}; +parse_term([$I|Line0], D) -> %Small. + {Int,Line} = string:to_integer(Line0), + {Int,Line,D}; +parse_term([$A|_]=Line, D) -> %Atom. + parse_atom(Line, D); +parse_term([$P|Line0], D) -> %Pid. + {Pid,Line} = get_id(Line0), + {"#CDVPid"++Pid,Line,D}; +parse_term([$p|Line0], D) -> %Port. + {Port,Line} = get_id(Line0), + {"#CDVPort"++Port,Line,D}; +parse_term([$S|Str0], D) -> %Information string. + Str = lists:reverse(skip_blanks(lists:reverse(Str0))), + {Str,[],D}; +parse_term([$D|Line0], D) -> %DistExternal + try + {AttabSize,":"++Line1} = get_hex(Line0), + {Attab, "E"++Line2} = parse_atom_translation_table(AttabSize, Line1, []), + {Bin,Line3} = get_binary(Line2), + {try + erts_debug:dist_ext_to_term(Attab, Bin) + catch + error:_ -> '<invalid-distribution-message>' + end, + Line3, + D} + catch + error:_ -> + {'#CDVBadDistExt', skip_dist_ext(Line0), D} + end. + +skip_dist_ext(Line) -> + skip_dist_ext(lists:reverse(Line), []). + +skip_dist_ext([], SeqTraceToken) -> + SeqTraceToken; +skip_dist_ext([$:| _], SeqTraceToken) -> + [$:|SeqTraceToken]; +skip_dist_ext([C|Cs], KeptCs) -> + skip_dist_ext(Cs, [C|KeptCs]). + +parse_atom([$A|Line0], D) -> + {N,":"++Line1} = get_hex(Line0), + {Chars, Line} = get_chars(N, Line1), + {list_to_atom(Chars), Line, D}. + +parse_atom_translation_table(0, Line0, As) -> + {list_to_tuple(lists:reverse(As)), Line0}; +parse_atom_translation_table(N, Line0, As) -> + {A, Line1, _} = parse_atom(Line0, []), + parse_atom_translation_table(N-1, Line1, [A|As]). + + + +deref_ptr(Ptr, Line, D0) -> + case gb_trees:lookup(Ptr, D0) of + {value,Term} -> + {Term,Line,D0}; + none -> + case get(fd) of + end_of_heap -> + {['#CDVIncompleteHeap'],Line,D0}; + Fd -> + case val(Fd) of + "="++_ -> + put(fd, end_of_heap), + deref_ptr(Ptr, Line, D0); + L -> + D = parse(L, D0), + deref_ptr(Ptr, Line, D) + end + end + end. + +get_hex(L) -> + get_hex_1(L, 0). + +get_hex_1([H|T]=L, Acc) -> + case get_hex_digit(H) of + none -> {Acc,L}; + Digit -> get_hex_1(T, (Acc bsl 4) bor Digit) + end; +get_hex_1([], Acc) -> {Acc,[]}. + +get_hex_digit(C) when $0 =< C, C =< $9 -> C-$0; +get_hex_digit(C) when $a =< C, C =< $f -> C-$a+10; +get_hex_digit(C) when $A =< C, C =< $F -> C-$A+10; +get_hex_digit(_) -> none. + +skip_blanks([$\s|T]) -> + skip_blanks(T); +skip_blanks([$\r|T]) -> + skip_blanks(T); +skip_blanks([$\n|T]) -> + skip_blanks(T); +skip_blanks([$\t|T]) -> + skip_blanks(T); +skip_blanks(T) -> T. + +get_chars(N, Line) -> + get_chars(N, Line, []). + +get_chars(0, Line, Acc) -> + {lists:reverse(Acc),Line}; +get_chars(N, [H|T], Acc) -> + get_chars(N-1, T, [H|Acc]). + +get_id(Line) -> + get_id(Line, []). + +get_id([$>|Line], Acc) -> + {lists:reverse(Acc, [$>]),Line}; +get_id([H|T], Acc) -> + get_id(T, [H|Acc]). + +get_label(L) -> + get_label(L, []). + +get_label([$:|Line], Acc) -> + Label = lists:reverse(Acc), + case get_hex(Label) of + {Int,[]} -> + {Int,Line}; + _ -> + {list_to_atom(Label),Line} + end; +get_label([H|T], Acc) -> + get_label(T, [H|Acc]). + +get_binary(Line0) -> + {N,":"++Line} = get_hex(Line0), + get_binary(N, Line, []). + +get_binary(0, Line, Acc) -> + {list_to_binary(lists:reverse(Acc)),Line}; +get_binary(N, [A,B|Line], Acc) -> + Byte = (get_hex_digit(A) bsl 4) bor get_hex_digit(B), + get_binary(N-1, Line, [Byte|Acc]); +get_binary(_N, [], _Acc) -> + {'#CDVTruncatedBinary',[]}. + +cdvbin(Sz,Pos) -> + "#CDVBin<"++integer_to_list(Sz)++","++integer_to_list(Pos)++">". diff --git a/lib/observer/src/crashdump_viewer.hrl b/lib/observer/src/crashdump_viewer.hrl new file mode 100644 index 0000000000..386d3bb423 --- /dev/null +++ b/lib/observer/src/crashdump_viewer.hrl @@ -0,0 +1,132 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2003-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% +%% +-define(space, " "). + + +-record(menu_item,{index,picture,text,depth,children,state,target}). + +-record(general_info, + {created, + slogan, + system_vsn, + compile_time, + node_name, + num_atoms, + num_procs, + num_ets, + num_fun, + mem_tot, + mem_max, + instr_info}). + +-record(proc, + {pid, + name, + init_func, + parent, + start_time, + state, + current_func, + msg_q_len, + msg_q, + last_calls, + links, + prog_count, + cp, + arity, + dict, + debug_dict, + reds, + num_heap_frag, + heap_frag_data, + stack_heap, + old_heap, + heap_unused, + old_heap_unused, + new_heap_start, + new_heap_top, + stack_top, + stack_end, + old_heap_start, + old_heap_top, + old_heap_end, + stack_dump}). + +-record(port, + {id, + slot, + connected, + links, + controls}). + +-record(ets_table, + {pid, + slot, + id, + name, + type, + buckets, + size, + memory}). + +-record(timer, + {pid, + msg, + time}). + +-record(fu, + {module, + uniq, + index, + address, + native_address, + refc}). + +-record(nod, + {name, + channel, + controller, + creation, + remote_links, + remote_mon, + remote_mon_by, + error}). + +-record(loaded_mod, + {mod, + current_size, + current_attrib, + current_comp_info, + old_size, + old_attrib, + old_comp_info}). + +-record(hash_table, + {name, + size, + used, + objs, + depth}). + +-record(index_table, + {name, + size, + used, + limit, + rate}). diff --git a/lib/observer/src/crashdump_viewer_html.erl b/lib/observer/src/crashdump_viewer_html.erl new file mode 100644 index 0000000000..5fa829ed37 --- /dev/null +++ b/lib/observer/src/crashdump_viewer_html.erl @@ -0,0 +1,1431 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2003-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% +%% +-module(crashdump_viewer_html). + +%% +%% This module implements the HTML generation for the crashdump +%% viewer. No logic or states are kept by this module. +%% + +-export([welcome/0, + read_file_frame/0, + redirect/1, + start_page/0, + filename_frame/1, + menu_frame/0, + general_info/1, + pretty_info_page/2, + info_page/2, + procs_summary/4, + proc_details/4, + expanded_memory/2, + expanded_binary/1, + next/2, + ports/3, + timers/3, + ets_tables/4, + nods/2, + loaded_mods/2, + loaded_mod_details/2, + funs/2, + atoms/3, + memory/2, + allocated_areas/2, + allocator_info/2, + hash_tables/2, + index_tables/2, + error/2]). + +-include("crashdump_viewer.hrl"). + +%%%----------------------------------------------------------------- +%%% Welcome frame +welcome() -> + header(body(welcome_body())). + +welcome_body() -> + table( + "WIDTH=100% HEIGHT=60%", + [tr("VALIGN=middle", + td("ALIGN=center", + font("SIZE=6", + ["Welcome to the Web Based",br(), + "Erlang Crash Dump Analyser"]))), + tr("VALIGN=middle", + td("ALIGN=center", + form(["name=load_new ACTION=\"./read_file_frame\""], + input(["TYPE=submit VALUE=\"Load Crashdump\""]))))]). + +%%%----------------------------------------------------------------- +%%% Present a form to enter file name of erlang crash dump +read_file_frame() -> + header("Read File",body(read_file_frame_body())). + + +read_file_frame_body() -> + Entry = + case webtool:is_localhost() of + true -> [input("TYPE=file NAME=browse SIZE=40"), + input("TYPE=hidden NAME=path")]; + false -> input("TYPE=text NAME=path SIZE=60") + end, + Form = + form( + "NAME=read_file_form METHOD=post ACTION= \"./read_file\"", + table( + "BORDER=0", + [tr(td("COLSPAN=2","Enter file to analyse")), + tr( + [td(Entry), + td("ALIGN=center", + input("TYPE=submit onClick=\"path.value=browse.value;\"" + "VALUE=Ok"))])])), + table( + "WIDTH=100% HEIGHT=60%", + tr("VALIGN=middle", + td("ALIGN=center",Form))). + + +%%%----------------------------------------------------------------- +%%% Display "Please wait..." while crashdump is being read +redirect(Status) -> + Head = ["<META HTTP-EQUIV=\"refresh\" CONTENT=\"3; URL=./redirect\">"], + header("Please wait...",Head,body([Status,br(),"Please wait..."])). + +%%%----------------------------------------------------------------- +%%% Frameset containing "filename", "menu", and "main" frames +start_page() -> + header("Crashdump Viewer Start Page",start_page_frameset()). + +start_page_frameset() -> + frameset( + "ROWS=\"70,*\"", + [frame(["NAME=\"filename\" SRC=\"./filename_frame\""]), + frameset( + "COLS=\"200,*\"", + [frame(["NAME=\"menu\" ", + "SRC=\"/cdv_erl/crashdump_viewer/menu_frame\""]), + frame("NAME=\"main\" SRC=\"./initial_info_frame\"")])]). + + + +%%%----------------------------------------------------------------- +%%% Topmost frame presents the filename of the crashdump currently +%%% viewed +filename_frame(File) -> + header("Filename",body(filename_body(File))). + +filename_body(File) -> + p("ALIGN=center",[b("Crashdump currently viewed:"),br(),File]). + + +%%%----------------------------------------------------------------- +%%% Left frame displays the menu +menu_frame() -> + header("Menu", body(menu_body())). + +menu_body() -> + [p(format_items(1,ets:info(cdv_menu_table,size),true)), + p([br(), + form(["name=load_new ACTION=\"./read_file_frame\" ", + "TARGET=app_frame"], + input("TYPE=submit VALUE=\"Load New Crashdump\""))])]. + +format_items(I,Max,_ParentState) when I>Max-> + []; +format_items(I,Max,ParentState) when I=<Max-> + case ets:lookup(cdv_menu_table,I) of + [] -> []; + [#menu_item{state=false,children=0}] -> + format_items(I+1,Max,ParentState); + [#menu_item{state=false,children=Children}] -> + format_items(I+Children+1,Max,arentState); + [Item=#menu_item{state=true,children=0}] when ParentState -> + This = format_item(Item), + [This|format_items(I+1,Max,ParentState)]; + [Item=#menu_item{state=true,children=Children}] when ParentState -> + This = format_item(Item), + Ch = format_items(I+1,I+Children,true), + [[This | Ch] | format_items(I+Children+1,Max,ParentState)] + end. + +format_item(Item) -> + [lists:duplicate(Item#menu_item.depth*5,?space), + format_picture(Item#menu_item.index, + Item#menu_item.picture, + Item#menu_item.children), + format_title(Item#menu_item.text,Item#menu_item.target), + br()]. + +format_picture(_Index,Picture,0) -> + img(Picture); +format_picture(Index,Picture,_Children) -> + href( ["./toggle?index=", integer_to_list(Index)], img(Picture)). + +format_title({Link,Text},Target) -> + href(["TARGET=\"",Target,"\""],Link,Text); +format_title(Text,_Type) -> + Text. + +%%%----------------------------------------------------------------- +%%% Display the general information +general_info(GenInfo) -> + Heading = "General Information", + header(Heading,body(general_info_body(Heading,GenInfo))). + +general_info_body(Heading,GenInfo) -> + TruncatedInfo = + case get(truncated) of + true -> + p(font("SIZE=\"+1\" COLOR=\"#FF0000\"", + b(["WARNING:",br(), + "The crashdump is truncated",br(), + "Some information might be missing",br()]))); + false -> + "" + end, + + [heading(Heading,"general_info"), + TruncatedInfo, + table( + "BORDER=4 CELLPADDING=4", + [tr([th("ALIGN=left BGCOLOR=\"#8899AA\"","Slogan"), + td(GenInfo#general_info.slogan)]), + tr([th("ALIGN=left BGCOLOR=\"#8899AA\"","Node name"), + td(GenInfo#general_info.node_name)]), + tr([th("ALIGN=left BGCOLOR=\"#8899AA\"","Crashdump created on"), + td(GenInfo#general_info.created)]), + tr([th("ALIGN=left BGCOLOR=\"#8899AA\"","System version"), + td(GenInfo#general_info.system_vsn)]), + tr([th("ALIGN=left BGCOLOR=\"#8899AA\"","Compiled"), + td(GenInfo#general_info.compile_time)]), + case GenInfo#general_info.mem_tot of + "" -> ""; + MemTot -> + tr([th("ALIGN=left BGCOLOR=\"#8899AA\"","Memory allocated"), + td([MemTot," bytes"])]) + end, + case GenInfo#general_info.mem_max of + "" -> ""; + MemMax -> + tr([th("ALIGN=left BGCOLOR=\"#8899AA\"","Memory maximum"), + td([MemMax," bytes"])]) + end, + tr([th("ALIGN=left BGCOLOR=\"#8899AA\"","Atoms"), + td(GenInfo#general_info.num_atoms)]), + tr([th("ALIGN=left BGCOLOR=\"#8899AA\"","Processes"), + td(GenInfo#general_info.num_procs)]), + tr([th("ALIGN=left BGCOLOR=\"#8899AA\"","ETS tables"), + td(GenInfo#general_info.num_ets)]), + tr([th("ALIGN=left BGCOLOR=\"#8899AA\"","Funs"), + td(GenInfo#general_info.num_fun)])]), + case GenInfo#general_info.instr_info of + old_instr_data -> + [br(),br(), + font("COLOR=\"#FF0000\"", + ["Instrumentation information is found at the end of ",br(), + "the dump. The information has an old format, and ",br(), + "is not presented in this tool. Please read the ",br(), + "crashdump manually to see this information."])]; + instr_data -> + [br(),br(), + font("COLOR=\"#FF0000\"", + ["Instrumentation information is found at the end of ",br(), + "the dump. The information is not presented in this ",br(), + "tool. Please read the crashdump manually to see",br(), + "this information."])]; + false -> + [] + end]. + +%%%----------------------------------------------------------------- +%%% Display an error message +error(Text,Args) -> + Str = io_lib:format(Text,Args), + header(body(error_body(Str))). + +error_body(Str) -> + [h1("An error occured:"),Str,"\n"]. + + +%%%----------------------------------------------------------------- +%%% Display the given information as is +info_page(Heading,Info) -> + info_page(Heading,Info,[]). +info_page(Heading,Info,TW) -> + header(Heading,body(info_body(Heading,Info,TW))). + +info_body(Heading,[],TW) -> + [h1(Heading), + warn(TW), + "No information was found\n"]; +info_body(Heading,Info,TW) -> + [h1(Heading), + warn(TW), + pre(href_proc_port(lists:flatten(Info)))]. + +%%%----------------------------------------------------------------- +%%% Pretty print the given information +pretty_info_page(Heading,Info) -> + header(Heading,body(pretty_info_body(Heading,Info))). + +pretty_info_body(Heading,[]) -> + [h1(Heading), + "No information was found\n"]; +pretty_info_body(Heading,Info) -> + [h1(Heading), + pre(pretty_format(Info))]. + +%%%----------------------------------------------------------------- +%%% Make table with summary of process information +procs_summary(Sorted,ProcsSummary,TW,SharedHeap) -> + Heading = "Process Information", + header(Heading, + body( + procs_summary_body(Heading,ProcsSummary,TW,Sorted,SharedHeap))). + +procs_summary_body(Heading,[],TW,_Sorted,_SharedHeap) -> + [h1(Heading), + warn(TW), + "No processes were found\n"]; +procs_summary_body(Heading,ProcsSummary,TW,Sorted,SharedHeap) -> + MemHeading = + if SharedHeap -> + "Stack"; + true -> + "Stack+heap" + end, + + [heading(Heading,"processes"), + warn(TW), + table( + "BORDER=4 CELLPADDING=4", + [tr( + [summary_table_head("pid","Pid",Sorted), + summary_table_head("name_func","Name/Spawned as",Sorted), + summary_table_head("state","State",Sorted), + summary_table_head("reds","Reductions",Sorted), + summary_table_head("mem",MemHeading,Sorted), + summary_table_head("msg_q_len","MsgQ Length",Sorted)]) | + lists:map(fun(Proc) -> procs_summary_table(Proc) end,ProcsSummary)])]. + +summary_table_head(Sorted,Text,Sorted) -> + %% Mark the sorted column (bigger and italic) + th(font("SIZE=\"+1\"",em(href("./sort_procs?sort="++Sorted,Text)))); +summary_table_head(SortOn,Text,_Sorted) -> + th(href("./sort_procs?sort="++SortOn,Text)). + +procs_summary_table(Proc) -> + #proc{pid=Pid,name=Name,state=State, + reds=Reds,stack_heap=Mem0,msg_q_len=MsgQLen}=Proc, + Mem = case Mem0 of + -1 -> "unknown"; + _ -> integer_to_list(Mem0) + end, + tr( + [td(href(["./proc_details?pid=",Pid],Pid)), + td(Name), + td(State), + td("ALIGN=right",integer_to_list(Reds)), + td("ALIGN=right",Mem), + td("ALIGN=right",integer_to_list(MsgQLen))]). + +%%%----------------------------------------------------------------- +%%% Print details for one process +proc_details(Pid,Proc,TW,SharedHeap) -> + Script = +"<SCRIPT type=\"text/javascript\"> + function popup() { + window.open(\"\",\"expanded\",'resizable=yes,scrollbars=yes') +} +</SCRIPT>\n", + + Heading = ["Process ", Pid], + header(Heading,Script,body(proc_details_body(Heading,Proc,TW,SharedHeap))). + +proc_details_body(Heading,Proc,TW,SharedHeap) -> + Pid = Proc#proc.pid, + Name = if Proc#proc.name==Proc#proc.init_func -> ?space; + true -> Proc#proc.name + end, + [help("processes"), + warn(TW), + table( + "BORDER=4 COLS=4 WIDTH=\"100%\"", + [tr( + "BGCOLOR=\"#8899AA\"", + [td("COLSPAN=4 ALIGN=center",Heading)]), + tr( + [td("NOWRAP=true",b("Name")), + td("COLSPAN=1",Name), + td("NOWRAP=true",b("Spawned as")), + td("COLSPAN=1",Proc#proc.init_func)]), + tr( + [td("NOWRAP=true",b("State")), + td("COLSPAN=1",Proc#proc.state), + td("NOWRAP=true",b(element(1,Proc#proc.current_func))), + td("COLSPAN=1",element(2,Proc#proc.current_func))]), + tr( + [td("NOWRAP=true",b("Started")), + td("COLSPAN=1",Proc#proc.start_time), + td("NOWRAP=true",b("Spawned by")), + td("COLSPAN=1",href_proc_port(Proc#proc.parent))]), + tr( + [td("NOWRAP=true",b("Reductions")), + td("COLSPAN=3",integer_to_list(Proc#proc.reds))]), + if SharedHeap -> + Stack = case Proc#proc.stack_heap of + -1 -> "unknown"; + S -> integer_to_list(S) + end, + tr( + [td("NOWRAP=true",b("Stack")), + td("COLSPAN=3",Stack)]); + true -> + [tr( + [td("NOWRAP=true",b("Stack+heap")), + td(integer_to_list(Proc#proc.stack_heap)), + td("NOWRAP=true",b("OldHeap")), + td(Proc#proc.old_heap)]), + tr( + [td("NOWRAP=true",b("Heap unused")), + td(Proc#proc.heap_unused), + td("NOWRAP=true",b("OldHeap unused")), + td(Proc#proc.old_heap_unused)]), + tr( + [td("NOWRAP=true",b("Number of heap fragments")), + td(Proc#proc.num_heap_frag), + td("NOWRAP=true",b("Heap fragment data")), + td(Proc#proc.heap_frag_data)])] + end, + case Proc#proc.new_heap_start of + ?space -> ""; + _ -> + %% Garbing + [tr( + [td("NOWRAP=true",b("New heap start")), + td("COLSPAN=1",Proc#proc.new_heap_start), + td("NOWRAP=true",b("New heap top")), + td("COLSPAN=1",Proc#proc.new_heap_top)]), + tr( + [td("NOWRAP=true",b("Stack top")), + td("COLSPAN=1",Proc#proc.stack_top), + td("NOWRAP=true",b("Stack end")), + td("COLSPAN=1",Proc#proc.stack_end)]), + tr( + [td("NOWRAP=true",b("Old heap start")), + td("COLSPAN=1",Proc#proc.old_heap_start), + td("NOWRAP=true",b("Old heap top")), + td("COLSPAN=1",Proc#proc.old_heap_top)]), + tr( + [td("NOWRAP=true",b("Old heap end")), + td("COLSPAN=3",Proc#proc.old_heap_end)])] + end, + case Proc#proc.prog_count of + ?space -> ""; + _ -> + [tr( + [td("NOWRAP=true",b("Program counter")), + td("COLSPAN=3",Proc#proc.prog_count)]), + tr( + [td("NOWRAP=true",b("Continuation pointer")), + td("COLSPAN=3",Proc#proc.cp)]), + tr( + [td("NOWRAP=true",b("Arity")), + td("COLSPAN=3",Proc#proc.arity)])] + end, + tr( + [td("NOWRAP=true",b("Link list")), + td("COLSPAN=3",href_proc_port(Proc#proc.links))]), + + tr( + [td("NOWRAP=true",b("Msg queue length")), + td("COLSPAN=3",integer_to_list(Proc#proc.msg_q_len))]), + + %% These are displayed only if data exist + display_or_link_to_expand("MsgQueue",Proc#proc.msg_q,Pid), + display_or_link_to_expand("Dictionary",Proc#proc.dict,Pid), + display_or_link_to_expand("DebugDictionary",Proc#proc.debug_dict,Pid), + display_or_link_to_expand("LastCalls",Proc#proc.last_calls,Pid), + display_or_link_to_expand("StackDump",Proc#proc.stack_dump,Pid)]), + + p([href(["./ets_tables?pid=",Proc#proc.pid], + "ETS tables owned by this process"), + " ", + href(["./timers?pid=",Proc#proc.pid], + "Timers owned by this process")])]. + +display_or_link_to_expand(Heading,Data,Pid) -> + case Data of + expand -> + link_to_read_memory(Heading,Pid); + truncated -> + Text = font("COLOR=\"#FF0000\"", + "The dump is truncated, no data available"), + tr( + [td("NOWRAP=true VALIGN=top",b(Heading)), + td("COLSPAN=3",Text)]); + ?space -> + ""; + {size,Truncated,Size,Pos} -> + %% Too much data, or truncated data - + %% display a link to expand it + tr( + [td("NOWRAP=true",b(Heading)), + td("COLSPAN=3", + href("TARGET=\"expanded\" onClick=popup()", + ["./expand?pos=",integer_to_list(Pos), + "&size=",integer_to_list(Size), + "&what=",Heading, + "&truncated=",atom_to_list(Truncated)], + ["Expand (",integer_to_list(Size)," bytes)"]))]); + _ -> + %% Not too much Data - display it + tr( + [td("NOWRAP=true VALIGN=top",b(Heading)), + td("COLSPAN=3",pre(format(Heading,Data)))]) + end. + +link_to_read_memory(Heading,Pid) -> + tr( + [td("NOWRAP=true",b(Heading)), + td("COLSPAN=3", + href("TARGET=\"expanded\" onClick=popup()", + ["./expand_memory?pid=",Pid, + "&what=",Heading], + ["Expand ", Heading]))]). + +format("LastCalls",Data) -> + Data; +format("StackDump",Data) -> + Data; +format(_Heading,Data) -> + pretty_format(Data). + + + +%%%----------------------------------------------------------------- +%%% Expanded memory +expanded_memory(Heading,Expanded) -> + header(Heading,body(expanded_memory_body(Heading,Expanded))). + +expanded_memory_body(Heading,[]) -> + [heading(Heading,"processes"), + case Heading of + "MsgQueue" -> "No messages were found"; + "StackDump" -> "No stack dump was found"; + "Dictionary" -> "No dictionary was found"; + "DebugDictionary" -> "No debug dictionary was found" + end]; +expanded_memory_body(Heading,Expanded) -> + [heading(Heading,"processes"), + case Heading of + "MsgQueue" -> + table( + "BORDER=4 CELLPADDING=4", + [tr( + [th("Message"), + th("SeqTraceToken")]) | + lists:map(fun(Msg) -> msgq_table(Msg) end, Expanded)]); + "StackDump" -> + table( + "BORDER=4 CELLPADDING=4", + [tr( + [th("Label"), + th("Term")]) | + lists:map(fun(Entry) -> stackdump_table(Entry) end, Expanded)]); + _ -> + table( + "BORDER=4 CELLPADDING=4", + [tr( + [th("Key"), + th("Value")]) | + lists:map(fun(Entry) -> dict_table(Entry) end, Expanded)]) + end]. + +msgq_table({Msg0,Token0}) -> + Token = case Token0 of + [] -> ?space; + _ -> io_lib:fwrite("~w",[Token0]) + end, + Msg = href_proc_port(lists:flatten(io_lib:format("~p",[Msg0]))), + tr([td(pre(Msg)), td(Token)]). + +stackdump_table({Label0,Term0}) -> + Label = io_lib:format("~w",[Label0]), + Term = href_proc_port(lists:flatten(io_lib:format("~p",[Term0]))), + tr([td("VALIGN=top",Label), td(pre(Term))]). + +dict_table({Key0,Value0}) -> + Key = href_proc_port(lists:flatten(io_lib:format("~p",[Key0]))), + Value = href_proc_port(lists:flatten(io_lib:format("~p",[Value0]))), + tr([td("VALIGN=top",pre(Key)), td(pre(Value))]). + + +%%%----------------------------------------------------------------- +%%% Display an expanded binary, i.e. the whole binary, not just the +%%% size of it. +expanded_binary(Bin) -> + Heading = "Expanded binary", + header(Heading,body(expanded_binary_body(Heading,Bin))). + +expanded_binary_body(Heading,Bin) -> + [h1(Heading), + pre(href_proc_port(lists:flatten(Bin))), + br(),br(), + href("javascript:history.go(-1)","BACK")]. + +%%%----------------------------------------------------------------- +%%% Print table of ports +ports(Heading,Ports,TW) -> + header(Heading,body(ports_body(Heading,Ports,TW))). + +ports_body(Heading,[],TW) -> + [h1(Heading), + warn(TW), + "No ports were found\n"]; +ports_body(Heading,Ports,TW) -> + [heading(Heading,"ports"), + warn(TW), + table( + "BORDER=4 CELLPADDING=4", + [tr( + [th("Id"), + th("Slot"), + th("Connected"), + th("Links"), + th("Controls")]) | + lists:map(fun(Port) -> ports_table(Port) end, Ports)])]. + +ports_table(Port) -> + #port{id=Id,slot=Slot,connected=Connected,links=Links, + controls=Controls}=Port, + tr( + [td(Id), + td("ALIGHT=right",Slot), + td(href_proc_port(Connected)), + td(href_proc_port(Links)), + td(Controls)]). + +%%%----------------------------------------------------------------- +%%% Print table of ETS tables +ets_tables(Heading,EtsTables,InternalEts,TW) -> + header(Heading,body(ets_tables_body(Heading,EtsTables,InternalEts,TW))). + +ets_tables_body(Heading,[],InternalEts,TW) -> + [h1(Heading), + warn(TW), + "No ETS tables were found\n" | + internal_ets_tables_table(InternalEts)]; +ets_tables_body(Heading,EtsTables,InternalEts,TW) -> + [heading(Heading,"ets_tables"), + warn(TW), + table( + "BORDER=4 CELLPADDING=4", + [tr( + [th("Owner"), + th("Slot"), + th("Id"), + th("Name"), + th("Type"), + th("Buckets"), + th("Objects"), + th("Memory (bytes)")]) | + lists:map(fun(EtsTable) -> ets_tables_table(EtsTable) end, + EtsTables)]) | + internal_ets_tables_table(InternalEts)]. + +ets_tables_table(EtsTable) -> + #ets_table{pid=Pid,slot=Slot,id=Id,name=Name,type=Type, + buckets=Buckets,size=Size,memory=Memory} = EtsTable, + tr( + [td(href_proc_port(Pid)), + td(Slot), + td(Id), + td(Name), + td(Type), + td("ALIGN=right",Buckets), + td("ALIGN=right",Size), + td("ALIGN=right",Memory)]). + +internal_ets_tables_table(InternalEtsTables) -> + [h2("Internal ETS tables"), + table( + "BORDER=4 CELLPADDING=4", + [tr( + [th("Description"), + th("Id"), + th("Name"), + th("Type"), + th("Buckets"), + th("Objects"), + th("Memory (bytes)")]) | + lists:map(fun(InternalEtsTable) -> + internal_ets_tables_table1(InternalEtsTable) + end, + InternalEtsTables)])]. + +internal_ets_tables_table1({Descr,InternalEtsTable}) -> + #ets_table{id=Id,name=Name,type=Type,buckets=Buckets, + size=Size,memory=Memory} = InternalEtsTable, + tr( + [td(Descr), + td(Id), + td(Name), + td(Type), + td("ALIGN=right",Buckets), + td("ALIGN=right",Size), + td("ALIGN=right",Memory)]). + +%%%----------------------------------------------------------------- +%%% Print table of timers +timers(Heading,Timers,TW) -> + header(Heading,body(timers_body(Heading,Timers,TW))). + +timers_body(Heading,[],TW) -> + [h1(Heading), + warn(TW), + "No timers were found\n"]; +timers_body(Heading,Timers,TW) -> + [heading(Heading,"timers"), + warn(TW), + table( + "BORDER=4 CELLPADDING=4", + [tr( + [th("Owner"), + th("Message"), + th("Time left")]) | + lists:map(fun(Timer) -> timers_table(Timer) end, Timers)])]. + +timers_table(Timer) -> + #timer{pid=Pid,msg=Msg,time=Time}=Timer, + tr( + [td(href_proc_port(Pid)), + td(Msg), + td("ALIGN=right",Time)]). + +%%%----------------------------------------------------------------- +%%% Print table of nodes in distribution +nods(Nods,TW) -> + header("Distribution Information",body(nodes_body(Nods,TW))). + +nodes_body(no_distribution,_TW) -> + [heading("Distribution Information","distribution_info"), + "Not alive\n"]; +nodes_body({Type,Info,Node},TW) when is_record(Node,nod) -> + %% Display only one node - used when a pid or port on a remote + %% node is clicked. + [heading("Remote Node","distribution_info"), + warn(TW), + Info, + make_nodes_table(Type,[Node])]; +nodes_body({Visible,Hidden,NotConnected},TW) -> + %% Display all nodes - this is the complete distribution info + [heading("Distribution Information","distribution_info"), + warn(TW), + make_nodes_table("Visible Nodes",Visible), + make_nodes_table("Hidden Nodes",Hidden), + make_nodes_table("Not Connected Nodes",NotConnected)]. + +make_nodes_table(Text,[]) -> + p(["No \"",Text,"\" were found"]); +make_nodes_table(Text,Nodes) -> + p(table( + "BORDER=4 CELLPADDING=4", + [nodes_table_heading(Text), + lists:map(fun(Node) -> nodes_table_row(Node) end, Nodes)])). + +nodes_table_heading(Text) -> + [tr("BGCOLOR=\"#8899AA\"",[th("COLSPAN=6",Text)]), + tr([th("Name"), + th("Channel"), + th("Controller"), + th("Creation(s)"), + th("Links/Monitors"), + th("Extra info")])]. + +nodes_table_row(Node) -> + #nod{name=Name,channel=Channel,controller=Controller,creation=Creation, + remote_links=Links,remote_mon=Mon,remote_mon_by=MonBy,error=Error}=Node, + tr( + [td(maybe_refcount(Name)), + td("ALIGN=right",Channel), + td(href_proc_port(Controller)), + td("ALIGN=right",break_lines_creation(Creation)), + td(format_links_and_monitors(Links,Mon,MonBy)), + td(format_extra_info(Error))]). + +maybe_refcount(Name) -> + maybe_refcount(Name, []). +maybe_refcount([$ ,$( | Rest], Acc) -> + [lists:reverse(Acc),br(),[$(|Rest]]; +maybe_refcount([Char | Rest], Acc) -> + maybe_refcount(Rest, [Char | Acc]); +maybe_refcount([],Acc) -> + lists:reverse(Acc). + +break_lines_creation(Creation) -> + break_lines_creation(Creation,[]). +break_lines_creation([$ ,$( | Rest1], Acc) -> + {RefCount,Rest2} = to_end_par(Rest1,[$(,$ ]), + [lists:reverse(Acc),RefCount,br(),break_lines_creation(Rest2)]; +break_lines_creation([$ | Rest], Acc) -> + [lists:reverse(Acc),br(),break_lines_creation(Rest)]; +break_lines_creation([Char | Rest], Acc) -> + break_lines_creation(Rest, [Char | Acc]); +break_lines_creation([],Acc) -> + lists:reverse(Acc). + +to_end_par([$),$ | Rest], Acc) -> + {lists:reverse([$) | Acc]),Rest}; +to_end_par([$) | Rest], Acc) -> + {lists:reverse([$) | Acc]),Rest}; +to_end_par([Char | Rest], Acc) -> + to_end_par(Rest, [Char | Acc]); +to_end_par([],Acc) -> + {lists:reverse(Acc),[]}. + + +format_links_and_monitors(?space,?space,?space) -> + ?space; +format_links_and_monitors(Links,Mon,MonBy) -> + [format_links_and_monitors(Links," is linked to "), + format_links_and_monitors(Mon," is monitoring "), + format_links_and_monitors(MonBy," is monitored by ")]. + +format_links_and_monitors(?space,_Text) -> + ""; +format_links_and_monitors([{Local,Remote}|Rest],Text) -> + [[href_proc_port(Local),Text,href_proc_port(Remote),br()] | + format_links_and_monitors(Rest,Text)]; +format_links_and_monitors([],_Text) -> + []. + +format_extra_info(?space) -> + ?space; +format_extra_info(Error) -> + case Error of + ?space -> ""; + _ -> font("COLOR=\"#FF0000\"",["ERROR: ",Error,"\n"]) + end. +%%%----------------------------------------------------------------- +%%% Print loaded modules information +loaded_mods({CC,OC,LM},TW) -> + Heading = "Loaded Modules Information", + header(Heading,body(loaded_mods_body(Heading,CC,OC,LM,TW))). + +loaded_mods_body(Heading,"unknown","unknown",[],TW) -> + [h1(Heading), + warn(TW), + "No loaded modules information was found\n"]; +loaded_mods_body(Heading,CC,OC,LM,TW) -> + [heading(Heading,"loaded_modules"), + warn(TW), + p([b("Current code: "),CC," bytes",br(), + b("Old code: "),OC," bytes"]), + table( + "BORDER=4 CELLPADDING=4", + [tr([th("Module"), + th("Current size (bytes)"), + th("Old size (bytes)")]) | + lists:map(fun(Mod) -> loaded_mods_table(Mod) end,LM)])]. + +loaded_mods_table(#loaded_mod{mod=Mod,current_size=CS,old_size=OS}) -> + tr([td(href(["loaded_mod_details?mod=",Mod],Mod)), + td("ALIGN=right",CS), + td("ALIGN=right",OS)]). + + +%%%----------------------------------------------------------------- +%%% Print detailed information about one module +loaded_mod_details(ModInfo,TW) -> + header(ModInfo#loaded_mod.mod,body(loaded_mod_details_body(ModInfo,TW))). + +loaded_mod_details_body(ModInfo,TW) -> + #loaded_mod{mod=Mod,current_size=CS,current_attrib=CA, + current_comp_info=CCI,old_size=OS, + old_attrib=OA,old_comp_info=OCI} = ModInfo, + [help("loaded_modules"), + warn(TW), + table( + "BORDER=4 CELLPADDING=4", + [tr(th("BGCOLOR=\"#8899AA\" COLSPAN=3", + ["Module: ",Mod])), + tr([td(?space),th("Current"),th("Old")]), + tr([th("ALIGN=left","Size (bytes)"), + td(CS), + td(OS)]), + tr([th("ALIGN=left","Attributes"), + td(pre(CA)), + td(pre(OA))]), + tr([th("ALIGN=left","Compilation info"), + td(pre(CCI)), + td(pre(OCI))])])]. + + +%%%----------------------------------------------------------------- +%%% Print table of funs +funs(Funs,TW) -> + Heading = "Fun Information", + header(Heading,body(funs_body(Heading,Funs,TW))). + +funs_body(Heading,[],TW) -> + [h1(Heading), + warn(TW), + "No Fun information was found\n"]; +funs_body(Heading,Funs,TW) -> + [heading(Heading,"funs"), + warn(TW), + table( + "BORDER=4 CELLPADDING=4", + [tr( + [th("Module"), + th("Uniq"), + th("Index"), + th("Address"), + th("Native_address"), + th("Refc")]) | + lists:map(fun(Fun) -> funs_table(Fun) end, Funs)])]. + +funs_table(Fu) -> + #fu{module=Module,uniq=Uniq,index=Index,address=Address, + native_address=NativeAddress,refc=Refc}=Fu, + tr( + [td(Module), + td("ALIGN=right",Uniq), + td("ALIGN=right",Index), + td(Address), + td(NativeAddress), + td("ALIGN=right",Refc)]). + +%%%----------------------------------------------------------------- +%%% Print atoms +atoms(Atoms,Num,TW) -> + Heading = "Atoms", + header(Heading,body(atoms_body(Heading,Atoms,Num,TW))). + +atoms_body(Heading,[],Num,TW) -> + [h1(Heading), + warn(TW), + "No atoms were found in log",br(), + "Total number of atoms in node was ", Num, br()]; +atoms_body(Heading,Atoms,Num,TW) -> + [heading(Heading,"atoms"), + warn(TW), + "Total number of atoms in node was ", Num, + br(), + "The last created atom is shown first", + br(),br() | + n_first(Atoms)]. + +n_first({n_lines,Start,N,What,Lines,Pos}) -> + NextHref = next_href(N,What,Pos,Start), + [What," number ",integer_to_list(Start),"-",integer_to_list(Start+N-1), + br(), + NextHref, + pre(Lines), + NextHref]; +n_first({n_lines,_Start,_N,_What,Lines}) -> + [pre(Lines)]. + +%%%----------------------------------------------------------------- +%%% Print next N lines of "something" +next(NLines,TW) -> + header(element(4,NLines),body(next_body(NLines,TW))). + +next_body({n_lines,Start,N,What,Lines,Pos},TW) -> + PrefHref = prev_href(), + NextHref = next_href(N,What,Pos,Start), + [warn(TW), + What," number ",integer_to_list(Start),"-",integer_to_list(Start+N-1), + br(), + PrefHref, + ?space, + NextHref, + pre(Lines), + PrefHref, + ?space, + NextHref]; +next_body({n_lines,Start,N,What,Lines},TW) -> + PrefHref = prev_href(), + [warn(TW), + What," number ",integer_to_list(Start),"-",integer_to_list(Start+N-1), + br(), + PrefHref, + pre(Lines), + PrefHref]. + + +prev_href() -> + href("javascript:history.back()",["Previous"]). + +next_href(N,What,Pos,Start) -> + href(["./next?pos=",integer_to_list(Pos), + "&num=",integer_to_list(N), + "&start=",integer_to_list(Start+N), + "&what=",What], + "Next"). + +%%%----------------------------------------------------------------- +%%% Print memory information +memory(Memory,TW) -> + Heading = "Memory Information", + header(Heading,body(memory_body(Heading,Memory,TW))). + +memory_body(Heading,[],TW) -> + [h1(Heading), + warn(TW), + "No memory information was found\n"]; +memory_body(Heading,Memory,TW) -> + [heading(Heading,"memory"), + warn(TW), + table( + "BORDER=4 CELLPADDING=4", + [tr("BGCOLOR=\"#8899AA\"", + [th(?space), + th("Bytes")]) | + lists:map(fun(Entry) -> memory_table(Entry) end, Memory)])]. + +memory_table({Key,Value}) -> + tr([th("ALIGN=left",Key),td("ALIGN=right",Value)]). + +%%%----------------------------------------------------------------- +%%% Print allocated areas information +allocated_areas(AllocatedAreas,TW) -> + Heading = "Information about allocated areas", + header(Heading,body(allocated_areas_body(Heading,AllocatedAreas,TW))). + +allocated_areas_body(Heading,[],TW) -> + [h1(Heading), + warn(TW), + "No information was found about allocated areas\n"]; +allocated_areas_body(Heading,AllocatedAreas,TW) -> + [heading(Heading,"memory"), + warn(TW), + table( + "BORDER=4 CELLPADDING=4", + [tr("BGCOLOR=\"#8899AA\"", + [th(?space), + th("Allocated (bytes)"), + th("Used (bytes)")]) | + lists:map(fun(Entry) -> allocated_areas_table(Entry) end, + AllocatedAreas)])]. + +allocated_areas_table({Key,Alloc,Used}) -> + tr( + [th("ALIGN=left",Key), + td("ALIGN=right",Alloc), + td("ALIGN=right",Used)]). + + +%%%----------------------------------------------------------------- +%%% Print allocator_info information +allocator_info(Allocators,TW) -> + Heading = "Allocator Information", + header(Heading,body(allocator_info_body(Heading,Allocators,TW))). + +allocator_info_body(Heading,[],TW) -> + [h1(Heading), + warn(TW), + "No information was found about allocators\n"]; +allocator_info_body(Heading,Allocators,TW) -> + [heading(Heading,"memory"), + warn(TW), + p(b("Sizes are in bytes")), + lists:map(fun({SubTitle,Allocator}) -> + [table( + "BORDER=4 CELLPADDING=4", + [tr("BGCOLOR=\"#8899AA\"", + th("COLSPAN=10 ALIGN=left", + font("SIZE=+1",SubTitle))) | + lists:map( + fun({Key,Values}) -> + tr([th("ALIGN=left",Key) | + lists:map( + fun(Val) -> + td("ALIGN=right",Val) + end,Values)]) + end, + Allocator)]), + br(),br()] + end, + Allocators)]. + +%%%----------------------------------------------------------------- +%%% Print informatin about internal tables +hash_tables(HashTables,TW) -> + Heading = "Hash Table Information", + header(Heading,body(hash_tables_body(Heading,HashTables,TW))). + +hash_tables_body(Heading,[],TW) -> + [h1(Heading), + warn(TW), + "No hash table information was found\n"]; +hash_tables_body(Heading,HashTables,TW) -> + [heading(Heading,"internal_tables"), + warn(TW), + table( + "BORDER=4 CELLPADDING=4", + [tr( + [th("Name"), + th("Size"), + th("Used"), + th("Objects"), + th("Depth")]) | + lists:map(fun(HashTable) -> hash_tables_table(HashTable) end, + HashTables)])]. + +hash_tables_table(HashTable) -> + #hash_table{name=Name,size=Size,used=Used,objs=Objs,depth=Depth}=HashTable, + tr( + [td(Name), + td("ALIGN=right",Size), + td("ALIGN=right",Used), + td("ALIGN=right",Objs), + td("ALIGN=right",Depth)]). + +index_tables(IndexTables,TW) -> + Heading = "Index Table Information", + header(Heading,body(index_tables_body(Heading,IndexTables,TW))). + +index_tables_body(Heading,[],TW) -> + [h1(Heading), + warn(TW), + "No index table information was found\n"]; +index_tables_body(Heading,IndexTables,TW) -> + [heading(Heading,"internal_tables"), + warn(TW), + table( + "BORDER=4 CELLPADDING=4", + [tr( + [th("Name"), + th("Size"), + th("Limit"), + th("Used"), + th("Rate")]) | + lists:map(fun(IndexTable) -> index_tables_table(IndexTable) end, + IndexTables)])]. + +index_tables_table(IndexTable) -> + #index_table{name=Name,size=Size,limit=Limit,used=Used,rate=Rate} = + IndexTable, + tr( + [td(Name), + td("ALIGN=right",Size), + td("ALIGN=right",Limit), + td("ALIGN=right",Used), + td("ALIGN=right",Rate)]). + +%%%----------------------------------------------------------------- +%%% Internal library +header(Body) -> + header("","",Body). +header(Title,Body) -> + header(Title,"",Body). +header(Title,JavaScript,Body) -> + ["Pragma:no-cache\r\n", + "Content-type: text/html\r\n\r\n", + html_header(Title,JavaScript,Body)]. + +html_header(Title,JavaScript,Body) -> + ["<HTML>\n", + "<HEAD>\n", + "<TITLE>", Title, "</TITLE>\n", + JavaScript, + "</HEAD>\n", + Body, + "</HTML>"]. + +body(Text) -> + ["<BODY BGCOLOR=\"#FFFFFF\">\n", + Text, + "<\BODY>\n"]. + +frameset(Args,Frames) -> + ["<FRAMESET ",Args,">\n", Frames, "\n</FRAMESET>\n"]. +frame(Args) -> + ["<FRAME ",Args, ">\n"]. + +table(Args,Text) -> + ["<TABLE ", Args, ">\n", Text, "\n</TABLE>\n"]. +tr(Text) -> + ["<TR>\n", Text, "\n</TR>\n"]. +tr(Args,Text) -> + ["<TR ", Args, ">\n", Text, "\n</TR>\n"]. +th(Text) -> + ["<TH>", Text, "</TH>"]. +th(Args,Text) -> + ["<TH ", Args, ">\n", Text, "\n</TH>\n"]. +td(Text) -> + ["<TD>", Text, "</TD>"]. +td(Args,Text) -> + ["<TD ", Args, ">", Text, "</TD>"]. + +b(Text) -> + ["<B>",Text,"</B>"]. +em(Text) -> + ["<EM>",Text,"</EM>\n"]. +pre(Text) -> + ["<PRE>",Text,"</PRE>"]. +href(Link,Text) -> + ["<A HREF=\"",Link,"\">",Text,"</A>"]. +href(Args,Link,Text) -> + ["<A HREF=\"",Link,"\" ",Args,">",Text,"</A>"]. +img("") -> + ""; +img(Picture) -> + ["<IMG SRC=\"", Picture, "\" BORDER=0>"]. +form(Args,Text) -> + ["<FORM ",Args,">\n",Text,"\n</FORM>\n"]. +input(Args) -> + ["<INPUT ", Args, ">\n"]. +h1(Text) -> + ["<H1>",Text,"</H1>\n"]. +h2(Text) -> + ["<H2>",Text,"</H2>\n"]. +font(Args,Text) -> + ["<FONT ",Args,">\n",Text,"\n</FONT>\n"]. +p(Text) -> + ["<P>",Text,"</P>\n"]. +p(Args, Text) -> + ["<P ", Args, ">",Text,"</P>\n"]. +br() -> + "<BR>\n". + + +%% In all the following, "<" is changed to "<" and ">" is changed to ">" +href_proc_port(Text) -> + href_proc_port(Text,[]). +href_proc_port([$#,$R,$e,$f,$<|T],Acc) -> + %% No links to refs + href_proc_port(T,[$;,$t,$l,$&,$f,$e,$R,$#|Acc]); +href_proc_port([$#,$F,$u,$n,$<|T],Acc) -> + %% No links to funs + href_proc_port(T,[$;,$t,$l,$&,$n,$u,$F,$#|Acc]); +href_proc_port([$#,$P,$o,$r,$t,$<|T],Acc) -> + {[$#|Port]=HashPort,Rest} = to_gt(T,[$;,$t,$l,$&,$t,$r,$o,$P,$#]), + href_proc_port(Rest,[href("TARGET=\"main\"", + ["./ports?port=",Port],HashPort)|Acc]); +href_proc_port([$<,$<|T],Acc) -> + %% No links to binaries + href_proc_port(T,[$;,$t,$l,$&,$;,$t,$l,$&|Acc]); +href_proc_port([$<,C|T],Acc) when $0 =< C, C =< $9 -> + %% Pid + {Pid,Rest} = to_gt(T,[C,$;,$t,$l,$&]), + href_proc_port(Rest,[href("TARGET=\"main\"", + ["./proc_details?pid=",Pid],Pid)|Acc]); +href_proc_port([$",$#,$C,$D,$V,$B,$i,$n,$<|T],Acc) -> + %% Binary written by crashdump_viewer:parse_heap_term(...) + {SizeAndPos,[$"|Rest]} = split($>,T), + {Size,Pos} = split($,,SizeAndPos), + href_proc_port(Rest,[href("TARGET=\"expanded\"", + ["./expand_binary?pos=",Pos], + ["<< ",Size," bytes >>"]) | Acc]); +href_proc_port([$",$#,$C,$D,$V,$P,$o,$r,$t,$<|T],Acc) -> + %% Port written by crashdump_viewer:parse_term(...) + {[$#|Port]=HashPort,[$"|Rest]} = to_gt(T,[$;,$t,$l,$&,$t,$r,$o,$P,$#]), + href_proc_port(Rest,[href("TARGET=\"main\"", + ["./ports?port=",Port],HashPort)|Acc]); +href_proc_port([$",$#,$C,$D,$V,$P,$i,$d,$<|T],Acc) -> + %% Pid written by crashdump_viewer:parse_term(...) + {Pid,[$"|Rest]} = to_gt(T,[$;,$t,$l,$&]), + href_proc_port(Rest,[href("TARGET=\"main\"", + ["./proc_details?pid=",Pid],Pid)|Acc]); +href_proc_port([$',$#,$C,$D,$V,$I,$n,$c,$o,$m,$p,$l,$e,$t,$e,$H,$e,$a,$p,$'|T], + Acc)-> + %% The heap is incomplete! Written by crashdump_viewer:deref_pts(...) + IH = lists:reverse( + lists:flatten( + "<FONT COLOR=\"#FF0000\">...(Incomplete Heap)</FONT>")), + href_proc_port(T,IH++Acc); +href_proc_port([$',$#,$C,$D,$V,$T,$r,$u,$n,$c,$a,$t,$e,$d,$B,$i,$n,$a,$r,$y,$' + |T], Acc)-> + %% A binary which is truncated! Written by + %% crashdump_viewer:parse_heap_term(...) + IH = lists:reverse( + lists:flatten( + "<FONT COLOR=\"#FF0000\"><<...(Truncated Binary)>>" + "</FONT>")), + href_proc_port(T,IH++Acc); +href_proc_port([$',$#,$C,$D,$V,$N,$o,$n,$e,$x,$i,$s,$t,$i,$n,$g,$B,$i,$n,$a,$r, + $y,$'|T], Acc)-> + %% A binary which could not be found in the dump! Written by + %% crashdump_viewer:parse_heap_term(...) + IH = lists:reverse( + lists:flatten( + "<FONT COLOR=\"#FF0000\"><<...(Nonexisting Binary)>>" + "</FONT>")), + href_proc_port(T,IH++Acc); +href_proc_port([$<|T],Acc) -> + href_proc_port(T,[$;,$t,$l,$&|Acc]); +href_proc_port([$>|T],Acc) -> + href_proc_port(T,[$;,$t,$g,$&|Acc]); +href_proc_port([H|T],Acc) -> + href_proc_port(T,[H|Acc]); +href_proc_port([],Acc) -> + lists:reverse(Acc). + +to_gt(Str,Acc) -> + {Match,Rest} = to_gt_noreverse(Str,Acc), + {lists:reverse(Match),Rest}. +to_gt_noreverse([$>|T],Acc) -> + {[$;,$t,$g,$&|Acc],T}; +to_gt_noreverse([H|T],Acc) -> + to_gt_noreverse(T,[H|Acc]); +to_gt_noreverse([],Acc) -> + {Acc,[]}. + +split(Char,Str) -> + split(Char,Str,[]). +split(Char,[Char|Str],Acc) -> % match Char + {lists:reverse(Acc),Str}; +split(Char,[H|T],Acc) -> + split(Char,T,[H|Acc]). + + +warn([]) -> + []; +warn(Warning) -> + font("COLOR=\"#FF0000\"",p([Warning,br(),br()])). + +heading(Heading,HelpMarker) -> + [font("SIZE=+2",b(Heading)),?space,?space,help(HelpMarker)]. + +help(HelpMarker) -> + [href("TARGET=doc", + ["/crashdump_doc/crashdump_help.html#",HelpMarker], + "Help"), + br(),br()]. + +%%%----------------------------------------------------------------- +%%% This function pretty formats a string which contains erlang +%%% terms (e.g. the message queue). +%%% In all the following, "<" is changed to "<" and ">" is changed to ">" +pretty_format(In) -> + case catch scan(In,[],initial,[]) of + {'EXIT',_Reason} -> + %% Probably a truncated file, so the erlang term is not complete + [font("COLOR=\"#FF0000\"","(This term might be truncated)"), + href_proc_port(lists:flatten(In))]; + {[R],_,Insrt} -> + InsrtString = lists:flatten(io_lib:format("~p",[R])), + lists:flatten(replace_insrt(lists:reverse(InsrtString),Insrt,[])) + end. + +%% Finish term +scan(In,Acc,list,Insrt) when hd(In)==$] -> + {lists:reverse(Acc),tl(In),Insrt}; +scan(In,Acc,tuple,Insrt) when hd(In)==$} -> + {list_to_tuple(lists:reverse(Acc)),tl(In),Insrt}; +scan(In,Acc,atom,Insrt) when In==[];hd(In)==$,;hd(In)==$];hd(In)==$} -> + {list_to_atom(lists:reverse(Acc)),In,Insrt}; +scan(In,Acc,float,Insrt) when In==[];hd(In)==$,;hd(In)==$];hd(In)==$} -> + {list_to_float(lists:reverse(Acc)),In,Insrt}; +scan(In,Acc,integer,Insrt) when In==[];hd(In)==$,;hd(In)==$];hd(In)==$} -> + {list_to_integer(lists:reverse(Acc)),In,Insrt}; +scan([$"|In],Acc,string,Insrt) when In==[];hd(In)==$,;hd(In)==$];hd(In)==$} -> + {lists:reverse(Acc),In,Insrt}; +scan([$>|In],Acc,special,Insrt) when In==[];hd(In)==$,;hd(In)==$];hd(In)==$} -> + %% pid, ref, port, fun + {lists:reverse([$;,$t,$g,$&|Acc]),In,Insrt}; +scan([$}|In],Acc,special,Insrt) when In==[];hd(In)==$,;hd(In)==$];hd(In)==$} -> + %% bignum integer, e.g. #integer(2) = {2452,4324} + {lists:reverse([$}|Acc]),In,Insrt}; +scan([$,|In],Acc,Cur,Insrt) when Cur/=string,Cur/=special -> + scan(In,Acc,Cur,Insrt); + +%% In the middle of an atom +scan([$'|In],Acc,Cur,Insrt) when Cur==atom -> + %% all $' are removed. They are added again by list_to_atom, + %% so if we don't remove them we will get two of them. + scan(In,Acc,Cur,Insrt); + +%% A $. in the middle of an integer - turn to float +scan([C|T],Acc,integer,Insrt) when C==$. -> + scan(T,[C|Acc],float,Insrt); + +%% In the middle of an atom, integer, float or string +scan([$<|T],Acc,Cur,Insrt) when Cur==atom;Cur==string;Cur==special -> + scan(T,[$;,$t,$l,$&|Acc],Cur,Insrt); +scan([$>|T],Acc,Cur,Insrt) when Cur==atom;Cur==string -> + scan(T,[$;,$t,$g,$&|Acc],Cur,Insrt); +scan([C|T],Acc,Cur,Insrt) when Cur==atom;Cur==integer;Cur==float;Cur==string;Cur==special -> + scan(T,[C|Acc],Cur,Insrt); + +%% Start list +scan([$[|T],Acc,Cur,Insrt0) -> + {L,Rest,Insrt} = scan(T,[],list,Insrt0), + scan(Rest,[L|Acc],Cur,Insrt); + +%% Star tuple +scan([${|T],Acc,Cur,Insrt0) -> + {Tuple,Rest,Insrt} = scan(T,[],tuple,Insrt0), + scan(Rest,[Tuple|Acc],Cur,Insrt); + +%% Star string +scan([$"|T],Acc,Cur,Insrt0) -> + {String,Rest,Insrt} = scan(T,[],string,Insrt0), + scan(Rest,[String|Acc],Cur,Insrt); + +%% Start atom +scan([$'|T],Acc,Cur,Insrt0) -> + %% all $' are removed. They are added again by list_to_atom, + %% so if we don't remove them we will get two of them. + {Atom,Rest,Insrt} = scan(T,[],atom,Insrt0), + scan(Rest,[Atom|Acc],Cur,Insrt); +scan([C|T],Acc,Cur,Insrt0) when C>=$A,C=<$Z;C>=$a,C=<$z;C==$'-> + {Atom,Rest,Insrt} = scan(T,[C],atom,Insrt0), + scan(Rest,[Atom|Acc],Cur,Insrt); + +%% Start integer or float +scan([C|T],Acc,Cur,Insrt0) when C>=$0,C=<$9;C==$- -> + {Num,Rest,Insrt} = scan(T,[C],integer,Insrt0), % can later change to float + scan(Rest,[Num|Acc],Cur,Insrt); + +%% Start Pid/Port/Ref/Fun/Binary +scan([$<|T],Acc,Cur,Insrt0) -> + {Special,Rest,Insrt} = scan(T,[$;,$t,$l,$&],special,Insrt0), + scan(Rest,['$insrt'|Acc],Cur,[Special|Insrt]); +scan([$#|T],Acc,Cur,Insrt0) -> + {Special,Rest,Insrt} = scan(T,[$#],special,Insrt0), + scan(Rest,['$insrt'|Acc],Cur,[Special|Insrt]); + + +%% done +scan([],Acc,initial,Insrt) -> + {Acc,[],Insrt}. + + +replace_insrt("'trsni$'"++Rest,[H|T],Acc) -> % the list is reversed here! + Special = + case H of + "<<" ++ _Binary -> + H; + "<" ++ _Pid -> + href("TARGET=\"main\"",["./proc_details?pid=",H],H); + "#Port<" ++ Port -> + href("TARGET=\"main\"",["./ports?port=","Port<"++Port],H); + "#" ++ _other -> + H + end, + replace_insrt(Rest,T,[Special|Acc]); +replace_insrt([H|T],Insrt,Acc) -> + replace_insrt(T,Insrt,[H|Acc]); +replace_insrt([],[],Acc) -> + Acc. diff --git a/lib/observer/src/etop.erl b/lib/observer/src/etop.erl new file mode 100644 index 0000000000..0bf1d68534 --- /dev/null +++ b/lib/observer/src/etop.erl @@ -0,0 +1,344 @@ +%% +%% %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% +%% +-module(etop). +-author('[email protected]'). + +-export([start/0, start/1, config/2, stop/0, dump/1, help/0]). +%% Internal +-export([update/1]). +-export([loadinfo/1, meminfo/2, getopt/2]). + +-include("etop.hrl"). +-include("etop_defs.hrl"). + +-define(change_at_runtime_config,[lines,interval,sort,accumulate]). + +help() -> + io:format( + "Usage of the erlang top program~n" + "Options are set as command line parameters as in -node a@host -..~n" + "or as parameter to etop:start([{node, a@host}, {...}])~n" + "Options are:~n" + " node atom Required The erlang node to measure ~n" + " port integer The used port, NOTE: due to a bug this program~n" + " will hang if the port is not avaiable~n" + " accumulate boolean If true execution time is accumulated ~n" + " lines integer Number of displayed processes~n" + " interval integer Display update interval in secs~n" + " sort runtime | reductions | memory | msg_q~n" + " What information to sort by~n" + " Default: runtime (reductions if tracing=off)~n" + " output graphical | text~n" + " How to present results~n" + " Default: graphical~n" + " tracing on | off etop uses the erlang trace facility, and thus~n" + " no other tracing is possible on the node while~n" + " etop is running, unless this option is set to~n" + " 'off'. Also helpful if the etop tracing causes~n" + " too high load on the measured node.~n" + " With tracing off, runtime is not measured!~n" + " setcookie string Only applicable on operating system command~n" + " line. Set cookie for the etop node, must be~n" + " same as the cookie for the measured node.~n" + " This is not an etop parameter~n" + ). + +stop() -> + case whereis(etop_server) of + undefined -> not_started; + Pid when is_pid(Pid) -> etop_server ! stop + end. + +config(Key,Value) -> + case check_runtime_config(Key,Value) of + ok -> + etop_server ! {config,{Key,Value}}, + ok; + error -> + {error,illegal_opt} + end. +check_runtime_config(lines,L) when is_integer(L),L>0 -> ok; +check_runtime_config(interval,I) when is_integer(I),I>0 -> ok; +check_runtime_config(sort,S) when S=:=runtime; + S=:=reductions; + S=:=memory; + S=:=msg_q -> ok; +check_runtime_config(accumulate,A) when A=:=true; A=:=false -> ok; +check_runtime_config(_Key,_Value) -> error. + +dump(File) -> + case file:open(File,[write]) of + {ok,Fd} -> etop_server ! {dump,Fd}; + Error -> Error + end. + +start() -> + start([]). + +start(Opts) -> + process_flag(trap_exit, true), + Config1 = handle_args(init:get_arguments() ++ Opts, #opts{}), + Config2 = Config1#opts{server=self()}, + + %% Connect to the node we want to look at + Node = getopt(node, Config2), + case net_adm:ping(Node) of + pang when Node /= node() -> + io:format("Error Couldn't connect to node ~p ~n~n", [Node]), + help(), + exit("connection error"); + _pong -> + check_runtime_tools_vsn(Node) + end, + + %% Maybe set up the tracing + Config3 = + if Config2#opts.tracing == on, Node /= node() -> + %% Cannot trace on current node since the tracer will + %% trace itself + etop_tr:setup_tracer(Config2); + true -> + if Config2#opts.sort == runtime -> + Config2#opts{sort=reductions,tracing=off}; + true -> + Config2#opts{tracing=off} + end + end, + AccumTab = ets:new(accum_tab, + [set,public,{keypos,#etop_proc_info.pid}]), + Config4 = Config3#opts{accum_tab=AccumTab}, + + %% Start the output server + Out = spawn_link(Config4#opts.out_mod, init, [Config4]), + Config5 = Config4#opts{out_proc = Out}, + + init_data_handler(Config5), + ok. + +check_runtime_tools_vsn(Node) -> + case rpc:call(Node,observer_backend,vsn,[]) of + {ok,Vsn} -> check_vsn(Vsn); + _ -> exit("Faulty version of runtime_tools on remote node") + end. +check_vsn(_Vsn) -> ok. +%check_vsn(_Vsn) -> exit("Faulty version of runtime_tools on remote node"). + + +%% Handle the incoming data + +init_data_handler(Config) -> + register(etop_server,self()), + Reader = + if Config#opts.tracing == on -> etop_tr:reader(Config); + true -> undefined + end, + data_handler(Reader, Config). + +data_handler(Reader, Opts) -> + receive + stop -> + stop(Opts), + ok; + {config,{Key,Value}} -> + data_handler(Reader,putopt(Key,Value,Opts)); + {dump,Fd} -> + Opts#opts.out_proc ! {dump,Fd}, + data_handler(Reader,Opts); + {'EXIT', EPid, Reason} when EPid == Opts#opts.out_proc -> + case Reason of + normal -> ok; + _ -> io:format("Output server crashed: ~p~n",[Reason]) + end, + stop(Opts), + out_proc_stopped; + {'EXIT', Reader, eof} -> + io:format("Lost connection to node ~p exiting~n", [Opts#opts.node]), + stop(Opts), + connection_lost; + _ -> + data_handler(Reader, Opts) + end. + +stop(Opts) -> + (Opts#opts.out_mod):stop(Opts#opts.out_proc), + if Opts#opts.tracing == on -> etop_tr:stop_tracer(Opts); + true -> ok + end, + unregister(etop_server). + +update(#opts{store=Store,node=Node,tracing=Tracing}=Opts) -> + Pid = spawn_link(Node,observer_backend,etop_collect,[self()]), + Info = receive {Pid,I} -> I + after 1000 -> exit(connection_lost) + end, + #etop_info{procinfo=ProcInfo} = Info, + ProcInfo1 = + if Tracing == on -> + PI=lists:map(fun(PI=#etop_proc_info{pid=P}) -> + case ets:lookup(Store,P) of + [{P,T}] -> PI#etop_proc_info{runtime=T}; + [] -> PI + end + end, + ProcInfo), + PI; + true -> + lists:map(fun(PI) -> PI#etop_proc_info{runtime='-'} end,ProcInfo) + end, + ProcInfo2 = sort(Opts,ProcInfo1), + Info#etop_info{procinfo=ProcInfo2}. + +sort(Opts,PI) -> + Tag = get_tag(Opts#opts.sort), + PI1 = if Opts#opts.accum -> + PI; + true -> + AccumTab = Opts#opts.accum_tab, + lists:map( + fun(#etop_proc_info{pid=Pid,reds=Reds,runtime=RT}=I) -> + NewI = + case ets:lookup(AccumTab,Pid) of + [#etop_proc_info{reds=OldReds, + runtime='-'}] -> + I#etop_proc_info{reds=Reds-OldReds, + runtime='-'}; + [#etop_proc_info{reds=OldReds, + runtime=OldRT}] -> + I#etop_proc_info{reds=Reds-OldReds, + runtime=RT-OldRT}; + [] -> + I + end, + ets:insert(AccumTab,I), + NewI + end, + PI) + end, + PI2 = lists:reverse(lists:keysort(Tag,PI1)), + lists:sublist(PI2,Opts#opts.lines). + +get_tag(runtime) -> #etop_proc_info.runtime; +get_tag(memory) -> #etop_proc_info.mem; +get_tag(reductions) -> #etop_proc_info.reds; +get_tag(msg_q) -> #etop_proc_info.mq. + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%%% Configuration Management + +getopt(What, Config) when is_record(Config, opts) -> + case What of + node -> Config#opts.node; + port -> Config#opts.port; + accum -> Config#opts.accum; + intv -> Config#opts.intv; + lines -> Config#opts.lines; + sort -> Config#opts.sort; + width -> Config#opts.width; + height-> Config#opts.height; + + store -> Config#opts.store; + host -> Config#opts.host + end. + +putopt(Key, Value, Config) when is_record(Config, opts) -> + Config1 = handle_args([{Key,Value}],Config), + Config1#opts.out_proc ! {config,{Key,Value},Config1}, + Config1. + +handle_args([{node, [NodeString]}| R], Config) when is_list(NodeString) -> + Node = list_to_atom(NodeString), + NewC = Config#opts{node = Node}, + handle_args(R, NewC); +handle_args([{node, Node} |R], Config) when is_atom(Node) -> + NewC = Config#opts{node = Node}, + handle_args(R, NewC); +handle_args([{port, Port}| R], Config) when is_integer(Port) -> + NewC = Config#opts{port=Port}, + handle_args(R, NewC); +handle_args([{port, [Port]}| R], Config) when is_list(Port) -> + NewC = Config#opts{port= list_to_integer(Port)}, + handle_args(R, NewC); +handle_args([{interval, Time}| R], Config) when is_integer(Time)-> + NewC = Config#opts{intv=Time*1000}, + handle_args(R, NewC); +handle_args([{interval, [Time]}| R], Config) when is_list(Time)-> + NewC = Config#opts{intv=list_to_integer(Time)*1000}, + handle_args(R, NewC); +handle_args([{lines, Lines}| R], Config) when is_integer(Lines) -> + NewC = Config#opts{lines=Lines}, + handle_args(R, NewC); +handle_args([{lines, [Lines]}| R], Config) when is_list(Lines) -> + NewC = Config#opts{lines= list_to_integer(Lines)}, + handle_args(R, NewC); +handle_args([{accumulate, Bool}| R], Config) when is_atom(Bool) -> + NewC = Config#opts{accum=Bool}, + handle_args(R, NewC); +handle_args([{accumulate, [Bool]}| R], Config) when is_list(Bool) -> + NewC = Config#opts{accum= list_to_atom(Bool)}, + handle_args(R, NewC); +handle_args([{sort, Sort}| R], Config) when is_atom(Sort) -> + NewC = Config#opts{sort=Sort}, + handle_args(R, NewC); +handle_args([{sort, [Sort]}| R], Config) when is_list(Sort) -> + NewC = Config#opts{sort= list_to_atom(Sort)}, + handle_args(R, NewC); +handle_args([{output, Output}| R], Config) when is_atom(Output) -> + NewC = Config#opts{out_mod=output(Output)}, + handle_args(R, NewC); +handle_args([{output, [Output]}| R], Config) when is_list(Output) -> + NewC = Config#opts{out_mod= output(list_to_atom(Output))}, + handle_args(R, NewC); +handle_args([{tracing, OnOff}| R], Config) when is_atom(OnOff) -> + NewC = Config#opts{tracing=OnOff}, + handle_args(R, NewC); +handle_args([{tracing, [OnOff]}| R], Config) when is_list(OnOff) -> + NewC = Config#opts{tracing=list_to_atom(OnOff)}, + handle_args(R, NewC); + +handle_args([_| R], C) -> + handle_args(R, C); +handle_args([], C) -> + C. + +output(graphical) -> etop_gui; +output(text) -> etop_txt. + + +loadinfo(SysI) -> + #etop_info{n_procs = Procs, + run_queue = RQ, + now = Now, + wall_clock = {_, WC}, + runtime = {_, RT}} = SysI, + Cpu = round(100*RT/WC), + Clock = io_lib:format("~2.2.0w:~2.2.0w:~2.2.0w", + tuple_to_list(element(2,calendar:now_to_datetime(Now)))), + {Cpu,Procs,RQ,Clock}. + +meminfo(MemI, [Tag|Tags]) -> + [round(get_mem(Tag, MemI)/1024)|meminfo(MemI, Tags)]; +meminfo(_MemI, []) -> []. + +get_mem(Tag, MemI) -> + case lists:keysearch(Tag, 1, MemI) of + {value, {Tag, I}} -> I; %these are in bytes + _ -> 0 + end. + diff --git a/lib/observer/src/etop_defs.hrl b/lib/observer/src/etop_defs.hrl new file mode 100644 index 0000000000..664de61973 --- /dev/null +++ b/lib/observer/src/etop_defs.hrl @@ -0,0 +1,29 @@ +%% +%% %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% +%% +-define(SYSFORM, + " ~-72w~10s~n" + " Load: cpu ~8w Memory: total ~8w binary ~8w~n" + " procs~8w processes~8w code ~8w~n" + " runq ~8w atom ~8w ets ~8w~n"). + +-record(opts, {node=node(), port = 8415, accum = false, intv = 5000, lines = 10, + width = 700, height = 340, sort = runtime, tracing = on, + %% Other state information + out_mod=etop_gui, out_proc, server, host, tracer, store, + accum_tab, remote}). diff --git a/lib/observer/src/etop_gui.erl b/lib/observer/src/etop_gui.erl new file mode 100644 index 0000000000..ff1b8078ad --- /dev/null +++ b/lib/observer/src/etop_gui.erl @@ -0,0 +1,362 @@ +%% +%% %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% +%% +-module(etop_gui). +-author('[email protected]'). + +-export([init/1,stop/1]). +-export([formatmfa/1,to_list/1]).% For etop_txt + +-include("etop.hrl"). +-include("etop_defs.hrl"). + +-import(etop, [loadinfo/1, meminfo/2, getopt/2]). + +%% Heights +-define(BarH, 28). % height of menubar +-define(LabelH, 90). % height of label with system info +-define(GridLineH, 21). % height of one line in the table (grid) + +%% Column numbers for grid - click to sort +-define(TimeCol, 3). +-define(RedsCol, 4). +-define(MemCol, 5). +-define(MsgQCol, 6). + +%% Font +-define(Normal, {screen,12}). +-define(Bold, {screen,bold,12}). + + +%% ----------------------------------------------------------------------------- +stop(_) -> ok. + +init(Config) -> + S = gs:start(), + Width = getopt(width, Config), + TotLines = getopt(lines,Config)+1, + + %% Max number of processes shown in window at startup is 10 + %% If less than 10 lines is specified, window size fits number of lines + WinH = if TotLines > 11 -> 11*?GridLineH + ?BarH + ?LabelH; + true -> TotLines*?GridLineH + ?BarH + ?LabelH + end, + Win = gs:create(window, S, + [{title, "Erlang Top"}, + {map, true}, %% While debugging + {configure, true}, + {width, Width}, {height, WinH}]), + Bar = gs:create(menubar, Win, []), + + FileButt = gs:create(menubutton, Bar, [{label,{text, " File "}}]), + OptionsButt = gs:create(menubutton, Bar, [{label,{text, " Options "}}]), + File = gs:create(menu, FileButt, []), + Options = gs:create(menu, OptionsButt, []), + gse:named_menuitem(refresh, File, + [{label,{text," Refresh "}}]), + gse:named_menuitem(dump, File, + [{label,{text," Dump to file "}}]), + gse:named_menuitem(exit, File, + [{label,{text," Exit "}}]), + + gse:named_menuitem(accum, Options, + [{label,{text, " Accumulate "}}, + {itemtype, check}]), + gse:named_menuitem(intv, Options, + [{label,{text, " Update Interval "}}]), + gse:named_menuitem(lines, Options, + [{label,{text, " Number of Lines "}}]), + Sort = gse:named_menuitem(sort, Options, + [{label,{text, " Sort "}}, + {itemtype,cascade}]), + SortMenu = gse:create(menu, Sort, []), + gse:named_menuitem(runtime, SortMenu, + [{label,{text, " Time "}}, + {itemtype,radio},{group,gr1}]), + gse:named_menuitem(memory, SortMenu, + [{label,{text, " Memory "}}, + {itemtype,radio},{group,gr1}]), + gse:named_menuitem(reductions, SortMenu, + [{label,{text, " Reductions "}}, + {itemtype,radio},{group,gr1}]), + gse:named_menuitem(msg_q, SortMenu, + [{label,{text, " Message Queue "}}, + {itemtype,radio},{group,gr1}]), + + SysInfo = gs:create(label,Win,[{x, 0}, {y, ?BarH},{align,sw}, + {width, Width},{height,?LabelH}]), + + {GridH,VScroll} = calc_grid_h(WinH,TotLines), + Grid = gse:grid(Win, + [{x, 0}, {y, ?BarH+?LabelH}, + {width, Width}, + {height, GridH}, + {hscroll, false}, + {vscroll, VScroll}, + {columnwidths, calc_column_w(Width)}, + {rows, {1, TotLines}}, + {font,?Normal}]), + + %% Header line + GL1 = gse:gridline(Grid, [{{text, 1}, "PID"}, + {{text, 2}, "Name or Initial Function"}, + {{text, ?TimeCol}, "Time(us)"}, + {{text, ?RedsCol}, "Reds"}, + {{text, ?MemCol}, "Memory"}, + {{text, ?MsgQCol}, "MsgQ"}, + {{text, 7}, "Current Function"}, + {bg, lightblue}, + {row, 1}, + {click, true}]), + + config_sort(GL1,getopt(sort,Config)), + Info = do_update(Grid, SysInfo, Config), + + get_event(Info, Win, Grid, GL1, SysInfo, Config). + +calc_column_w(W) -> + %% W = [2x, 3x, 1x, 1x, 1x, 1x, 3x] = 12x + RW = W-9, % just to make nice small margins on each side of grid + X = RW div 12, + [2*X, 3*X, X, X, X, X, 3*X + (RW - 12*X)]. + +config_sort(GL1,Sort) -> + gs:config(Sort,[{select,true}]), + lists:foreach(fun(S) -> + gs:config(GL1,[{{font,S},?Normal}]) + end, + [?TimeCol,?MemCol,?RedsCol,?MsgQCol]), + case Sort of + runtime -> gs:config(GL1,{{font,?TimeCol},?Bold}); + memory -> gs:config(GL1,{{font,?MemCol},?Bold}); + reductions -> gs:config(GL1,{{font,?RedsCol},?Bold}); + msg_q -> gs:config(GL1,{{font,?MsgQCol},?Bold}) + end. + +config_lines(Win,Grid,TotLines) -> + OldGridH = gs:read(Grid,height), + NewLinesH = TotLines*?GridLineH, + if NewLinesH =< OldGridH -> + gs:config(Win,[{height,NewLinesH+?BarH+?LabelH}]), + gs:config(Grid,[{rows,{1,TotLines}}, + {height,NewLinesH}, + {vscroll,false}]); + true -> + gs:config(Grid,[{rows,{1,TotLines}},{vscroll,right}]) + end. + +calc_grid_h(WinH,TotLines) -> + LeftInWin = WinH - ?BarH - ?LabelH, + TotGrid = TotLines * ?GridLineH, + if LeftInWin >= TotGrid -> + {TotGrid,false}; + true -> + {LeftInWin,right} + end. + +set_win_h(Win,OrigH,TotLines) -> + TotH = TotLines*?GridLineH + ?BarH + ?LabelH, + if TotH >= OrigH -> OrigH; + true -> gs:config(Win,[{height,TotH}]), + TotH + end. + +get_event(Info, Win, Grid, GL1, SysInfo, Config) -> + receive + {gs, Win, configure,[],[W,H,_,_]} -> + TotLines = getopt(lines,Config)+1, + %% Will not make window higher than total number of lines + RealWinH = set_win_h(Win,H,TotLines), + {GridH,VScroll} = calc_grid_h(RealWinH,TotLines), + gs:config(Grid, [{width, W}, + {columnwidths, calc_column_w(W)}, + {height,GridH}, {vscroll,VScroll}]), + get_event(Info, Win, Grid, GL1, SysInfo, Config); + {gs, refresh, _, _, _} -> + Info1 = do_update(Grid, SysInfo, Config), + get_event(Info1, Win, Grid, GL1, SysInfo, Config); + {gs, dump, _, _, _} -> + case pop(Win,dump) of + {ok,File} -> etop:dump(File); + {error,cancel} -> ok + end, + get_event(Info, Win, Grid, GL1, SysInfo, Config); + {gs, Win, destroy, _, _} -> + normal; + {gs, exit, _, _, _} -> + ok; + {gs, accum, _, _, _} -> + Old = getopt(accum,Config), + etop:config(accumulate,not Old), + get_event(Info, Win, Grid, GL1, SysInfo, Config); + {gs,intv,_,_,_} -> + case pop(Win,interval) of + {ok,Intv} -> etop:config(interval,list_to_integer(Intv)); + {error,cancel} -> ok + end, + get_event(Info, Win, Grid, GL1, SysInfo, Config); + {gs,lines,_,_,_} -> + case pop(Win,lines) of + {ok,Lines} -> etop:config(lines,list_to_integer(Lines)); + {error,cancel} -> ok + end, + get_event(Info, Win, Grid, GL1, SysInfo, Config); + {gs,Sort,_,_,_} when Sort=:=runtime; + Sort=:=memory; + Sort=:=reductions; + Sort=:=msg_q -> + etop:config(sort,Sort), + get_event(Info, Win, Grid, GL1, SysInfo, Config); + {gs,GL1,click,_,[Col,1,_]} -> + case Col of + ?TimeCol -> etop:config(sort, runtime); + ?MemCol -> etop:config(sort, memory); + ?RedsCol -> etop:config(sort, reductions); + ?MsgQCol -> etop:config(sort, msg_q); + _other -> ignore + end, + get_event(Info, Win, Grid, GL1, SysInfo, Config); + {config,{Key,Value},Config1} -> + case Key of + lines -> config_lines(Win,Grid,Value+1); + sort -> config_sort(GL1,Value); + accumulate -> gs:config(accum,[{select,Value}]); + _ -> ok + end, + Info1 = do_update(Grid, SysInfo, Config1), + get_event(Info1, Win, Grid, GL1, SysInfo, Config1); + {dump,Fd} -> + etop_txt:do_update(Fd,Info,Config), + get_event(Info, Win, Grid, GL1, SysInfo, Config); + Msg -> + io:format("~p got unexpected msg ~p~n", [?MODULE, Msg]), + get_event(Info, Win, Grid, GL1, SysInfo, Config) + after getopt(intv,Config) -> + Info1 = do_update(Grid, SysInfo, Config), + get_event(Info1, Win, Grid, GL1, SysInfo, Config) + end. + +do_update(Grid, SysInfo, Config) -> + Info = etop:update(Config), + Lines = makegridlines(Info#etop_info.procinfo, Grid, 2), + clear_lines(Lines, getopt(lines,Config) + 1, Grid), + makesysinfo(getopt(node,Config),Info,SysInfo), + Info. + +%clear_lines(From, To, _Grid) when From > To -> ok; +clear_lines(From, To, Grid) -> + case gs:read(Grid, {obj_at_row, From}) of + undefined -> + ok; + GridLine -> + gs:destroy(GridLine), + clear_lines(From + 1, To, Grid) + end. + +formatmfa({M, F, A}) -> + io_lib:format("~w:~w/~w",[M, F, A]). + +makegridlines([#etop_proc_info{pid=Pid, + mem=Mem, + reds=Reds, + name=Name, + runtime=Time, + cf=MFA, + mq=MQ} + |T], Grid, Count) -> + update_gl(Grid, Count, [{{text, 1}, pid_to_list(Pid)}, + {{text, 2}, to_list(Name)}, + {{text, ?TimeCol}, + if is_integer(Time)->integer_to_list(Time); + true -> Time + end}, + {{text, ?RedsCol}, integer_to_list(Reds)}, + {{text, ?MemCol}, integer_to_list(Mem)}, + {{text, ?MsgQCol}, integer_to_list(MQ)}, + {{text, 7}, formatmfa(MFA)}, + {row, Count}, {click, false}]), + makegridlines(T, Grid, Count + 1); +makegridlines([],_Grid,Count) -> + Count. + +update_gl(Grid, Row, GL) -> + case gs:read(Grid, {obj_at_row, Row}) of + undefined -> + gse:gridline(Grid,[{row, Row}|GL]); + GridLine -> + gs:config(GridLine,GL) + end. + +to_list(Name) when is_atom(Name) -> atom_to_list(Name); +to_list({_M,_F,_A}=MFA) -> formatmfa(MFA). + + +makesysinfo(Node,Info,SysInfo) -> + {Cpu,NProcs,RQ,Clock} = loadinfo(Info), + case Info#etop_info.memi of + undefined -> + Str = "No memory information is available."; + Memi -> + [Tot,Procs,Atom,Bin,Code,Ets] = + meminfo(Memi, [total,processes,atom,binary,code,ets]), + Str = io_lib:fwrite(?SYSFORM, + [Node,Clock, + Cpu,Tot,Bin, + NProcs,Procs,Code, + RQ,Atom,Ets]) + end, + gs:config(SysInfo,[{label,{text,Str}},{font,?Normal}]). + + +pop(Win,Key) -> + Pop = gs:create(window,Win,[{title,"Config"}, + {width,160},{height,100}]), + gs:create(label,Pop,[{label,{text,txt(Key)}}, + {width,160}]), + gs:create(entry,entry,Pop,[{x,10},{y,30},{width,130}, + {keypress,true}]), + gs:create(button,ok,Pop,[{width,45},{y,60},{x,10}, + {label,{text,"Ok"}}]), + gs:create(button,cancel,Pop,[{width,60},{y,60},{x,80}, + {label,{text,"Cancel"}}]), + gs:config(Pop,{map,true}), + pop_loop(Pop). + +pop_loop(Pop) -> + receive + {gs,entry,keypress,_,['Return'|_]} -> + Str = gs:read(entry,text), + gs:destroy(Pop), + {ok,Str}; + {gs,entry,keypress,_,_} -> % all other keypresses + pop_loop(Pop); + {gs,ok,click,_,_} -> + Str = gs:read(entry,text), + gs:destroy(Pop), + {ok,Str}; + {gs,cancel,click,_,_} -> + gs:destroy(Pop), + {error,cancel}; + X -> + io:format("Got X=~w~n",[X]), + pop_loop(Pop) + end. + +txt(interval) -> "Enter new interval:"; +txt(lines) -> "Enter number of lines:"; +txt(dump) -> "Enter file name:". diff --git a/lib/observer/src/etop_tr.erl b/lib/observer/src/etop_tr.erl new file mode 100644 index 0000000000..dd326fe639 --- /dev/null +++ b/lib/observer/src/etop_tr.erl @@ -0,0 +1,130 @@ +%% +%% %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% +%% +-module(etop_tr). +-author('[email protected]'). + +%%-compile(export_all). +-export([setup_tracer/1,stop_tracer/1,reader/1]). +-import(etop,[getopt/2]). + +-include("etop_defs.hrl"). + +setup_tracer(Config) -> + TraceNode = getopt(node,Config), + RHost = rpc:call(TraceNode, net_adm, localhost, []), + Store = ets:new(?MODULE, [set, public]), + + %% We can only trace one process anyway kill the old one. + case erlang:whereis(dbg) of + undefined -> + case rpc:call(TraceNode, erlang, whereis, [dbg]) of + undefined -> fine; + Pid -> + exit(Pid, kill) + end; + Pid -> + exit(Pid,kill) + end, + + dbg:tracer(TraceNode,port,dbg:trace_port(ip,{getopt(port,Config),5000})), + dbg:p(all,[running,timestamp]), + T = dbg:get_tracer(TraceNode), + Config#opts{tracer=T,host=RHost,store=Store}. + +stop_tracer(_Config) -> + dbg:p(all,clear), + dbg:stop(), + ok. + + + +reader(Config) -> + Host = getopt(host, Config), + Port = getopt(port, Config), + + {ok, Sock} = gen_tcp:connect(Host, Port, [{active, false}]), + spawn_link(fun() -> reader_init(Sock,getopt(store,Config),nopid) end). + + +%%%%%%%%%%%%%% Socket reader %%%%%%%%%%%%%%%%%%%%%%%%%%% + +reader_init(Sock, Store, Last) -> + process_flag(priority, high), + reader(Sock, Store, Last). + +reader(Sock, Store, Last) -> + Data = get_data(Sock), + New = handle_data(Last, Data, Store), + reader(Sock, Store, New). + +handle_data(_, {_, Pid, in, _, Time}, _) -> + {Pid,Time}; +handle_data({Pid,Time1}, {_, Pid, out, _, Time2}, Store) -> + Elapsed = elapsed(Time1, Time2), + case ets:member(Store,Pid) of + true -> ets:update_counter(Store, Pid, Elapsed); + false -> ets:insert(Store,{Pid,Elapsed}) + end, + nopid; +handle_data(_W, {drop, D}, _) -> %% Error case we are missing data here! + io:format("Erlang top dropped data ~p~n", [D]), + nopid; +handle_data(nopid, {_, _, out, _, _}, _Store) -> + %% ignore - there was probably just a 'drop' + nopid; +handle_data(_, G, _) -> + io:format("Erlang top got garbage ~p~n", [G]), + nopid. + +elapsed({Me1, S1, Mi1}, {Me2, S2, Mi2}) -> + Me = (Me2 - Me1) * 1000000, + S = (S2 - S1 + Me) * 1000000, + Mi2 - Mi1 + S. + + +%%%%%% Socket helpers %%%% +get_data(Sock) -> + [Op | BESiz] = my_ip_read(Sock, 5), + Siz = get_be(BESiz), + case Op of + 0 -> + B = list_to_binary(my_ip_read(Sock, Siz)), + binary_to_term(B); + 1 -> + {drop, Siz}; + Else -> + exit({'bad trace tag', Else}) + end. + +get_be([A,B,C,D]) -> + A * 16777216 + B * 65536 + C * 256 + D. + +my_ip_read(Sock,N) -> + case gen_tcp:recv(Sock, N) of + {ok, Data} -> + case length(Data) of + N -> + Data; + X -> + Data ++ my_ip_read(Sock, N - X) + end; + _Else -> + exit(eof) + end. + diff --git a/lib/observer/src/etop_txt.erl b/lib/observer/src/etop_txt.erl new file mode 100644 index 0000000000..d0612f15b4 --- /dev/null +++ b/lib/observer/src/etop_txt.erl @@ -0,0 +1,101 @@ +%% +%% %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% +%% +-module(etop_txt). +-author('[email protected]'). + +%%-compile(export_all). +-export([init/1,stop/1]). +-export([do_update/3]). + +-include("etop.hrl"). +-include("etop_defs.hrl"). + +-import(etop,[loadinfo/1,meminfo/2]). +-import(etop_gui,[formatmfa/1,to_list/1]). + +-define(PROCFORM,"~-15w~-20s~8w~8w~8w~8w ~-20s~n"). + +stop(Pid) -> Pid ! stop. + +init(Config) -> + loop(Config). + +loop(Config) -> + Info = do_update(Config), + receive + stop -> stopped; + {dump,Fd} -> do_update(Fd,Info,Config), loop(Config); + {config,_,Config1} -> loop(Config1) + after Config#opts.intv -> loop(Config) + end. + +do_update(Config) -> + Info = etop:update(Config), + do_update(standard_io,Info,Config). + +do_update(Fd,Info,Config) -> + {Cpu,NProcs,RQ,Clock} = loadinfo(Info), + io:nl(Fd), + writedoubleline(Fd), + case Info#etop_info.memi of + undefined -> + io:fwrite(Fd, " ~-72w~10s~n" + " Load: cpu ~8w~n" + " procs~8w~n" + " runq ~8w~n", + [Config#opts.node,Clock, + Cpu,NProcs,RQ]); + Memi -> + [Tot,Procs,Atom,Bin,Code,Ets] = + meminfo(Memi, [total,processes,atom,binary,code,ets]), + io:fwrite(Fd, ?SYSFORM, + [Config#opts.node,Clock, + Cpu,Tot,Bin, + NProcs,Procs,Code, + RQ,Atom,Ets]) + end, + io:nl(Fd), + writepinfo_header(Fd), + writesingleline(Fd), + writepinfo(Fd,Info#etop_info.procinfo), + writedoubleline(Fd), + io:nl(Fd), + Info. + +writepinfo_header(Fd) -> + io:fwrite(Fd,"Pid Name or Initial Func Time Reds Memory MsgQ Current Function~n",[]). + +writesingleline(Fd) -> + io:fwrite(Fd,"----------------------------------------------------------------------------------------~n",[]). +writedoubleline(Fd) -> + io:fwrite(Fd,"========================================================================================~n",[]). + +writepinfo(Fd,[#etop_proc_info{pid=Pid, + mem=Mem, + reds=Reds, + name=Name, + runtime=Time, + cf=MFA, + mq=MQ} + |T]) -> + io:fwrite(Fd,?PROCFORM,[Pid,to_list(Name),Time,Reds,Mem,MQ,formatmfa(MFA)]), + writepinfo(Fd,T); +writepinfo(_Fd,[]) -> + ok. + diff --git a/lib/observer/src/multitrace.erl b/lib/observer/src/multitrace.erl new file mode 100644 index 0000000000..144697ce9c --- /dev/null +++ b/lib/observer/src/multitrace.erl @@ -0,0 +1,256 @@ +%% +%% %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% +%% +-module(multitrace). +-author('[email protected]'). + +-export([debug/1,gc/1,schedule/1,stop/0,format/1,format/2]). +-export([handle_debug/4,handle_gc/4,handle_schedule/4]). + +%% +%% Tool API +%% +debug(Func) -> + case running() of + false -> + {ok,_} = ttb:tracer(all, + [{file,"debug_trace"}, + {handler, {{?MODULE,handle_debug},initial}}]), + init(), + {ok,_} = ttb:p(all,[timestamp,c]), + tp(Func), + ok; + true -> + {error, tracer_already_running} + end. + +tp([Func|Funcs]) -> + tp(Func), + tp(Funcs); +tp([]) -> ok; +tp({M,F,A}) -> do_tp(M,F,A); +tp({M,F}) -> do_tp(M,F,'_'); +tp(M) -> do_tp(M,'_','_'). + +do_tp(M,F,A) -> + {ok,_}=ttb:tp(M,F,A,[{'_',[],[{message,{caller}},{return_trace}]}]). + +gc(Proc) -> + case running() of + false -> + {ok,_} = ttb:tracer(all,[{file,"gc_trace"}, + {handler,{{?MODULE,handle_gc},initial}}, + {process_info,false}]), + init(), + {ok,_} = ttb:p(Proc,[timestamp,garbage_collection]), + ok; + true -> + {error, tracer_already_running} + end. + +schedule(Proc) -> + case running() of + false -> + {ok,_} = ttb:tracer(all, + [{file,"schedule_trace"}, + {handler,{{?MODULE,handle_schedule},initial}}, + {process_info,false}]), + init(), + {ok,_} = ttb:p(Proc,[timestamp,running]), + ok; + true -> + {error, tracer_already_running} + end. + +stop() -> + ttb:stop(). + +format(File) -> + ttb:format(File). +format(File,Out) -> + ttb:format(File,[{out,Out}]). + + +%% +%% Print call trace +%% +handle_debug(Out,Trace,TI,initial) -> + print_header(Out,TI), + handle_debug(Out,Trace,TI,0); +handle_debug(_Out,end_of_trace,_TI,N) -> + N; +handle_debug(Out,Trace,_TI,N) -> + print_func(Out,Trace,N), + N+1. + +print_func(Out,{trace_ts,P,call,{M,F,A},C,Ts},N) -> + io:format(Out, + "~w: ~s~n" + "Process : ~w~n" + "Call : ~w:~w/~w~n" + "Arguments : ~p~n" + "Caller : ~w~n~n", + [N,ts(Ts),P,M,F,length(A),A,C]); +print_func(Out,{trace_ts,P,return_from,{M,F,A},R,Ts},N) -> + io:format(Out, + "~w: ~s~n" + "Process : ~w~n" + "Return from : ~w:~w/~w~n" + "Return value : ~p~n~n", + [N,ts(Ts),P,M,F,A,R]). + + +%% +%% Print GC trace +%% +handle_gc(_Out,end_of_trace,_TI,S) -> + S; +handle_gc(Out,Trace,TI,initial) -> + print_header(Out,TI), + print_gc_header(Out), + handle_gc(Out,Trace,TI,dict:new()); +handle_gc(_Out,{trace_ts,P,gc_start,Info,Ts},_TI,S) -> + dict:store(P,{Info,Ts},S); +handle_gc(Out,{trace_ts,P,gc_end,Info,Ts},_TI,S) -> + case dict:find(P,S) of + {ok,{StartInfo,StartTime}} -> + {EM,ER,ES,EO,EH,EOB,EB} = sort(Info), + {SM,SR,SS,SO,SH,SOB,SB} = sort(StartInfo), + io:format(Out, + "start\t~w\t~w\t~w\t~w\t~w\t~w\t~w\t~w~n" + "end\t~w\t~w\t~w\t~w\t~w\t~w\t~w\t~w~n~n", + [SM,SR,SS,SO,SH,SOB,SB,P,EM,ER,ES,EO,EH,EOB,EB,diff(StartTime,Ts)]), + dict:erase(P,S); + error -> + S + end. + +print_gc_header(Out) -> + io:format(Out, + "\tMBuf\tRecent\tStack\tOldHeap\tHeap\tOldBL\tBlock\t" + "Process/Time(micro sec)~n" + "=============================================" + "============================================~n",[]). + +sort(GC) -> + sort(GC,{0,0,0,0,0,'_','_'}). +sort([{mbuf_size,M}|Rest],{_,R,S,O,H,OB,B}) -> + sort(Rest,{M,R,S,O,H,OB,B}); +sort([{recent_size,R}|Rest],{M,_,S,O,H,OB,B}) -> + sort(Rest,{M,R,S,O,H,OB,B}); +sort([{stack_size,S}|Rest],{M,R,_,O,H,OB,B}) -> + sort(Rest,{M,R,S,O,H,OB,B}); +sort([{old_heap_size,O}|Rest],{M,R,S,_,H,OB,B}) -> + sort(Rest,{M,R,S,O,H,OB,B}); +sort([{heap_size,H}|Rest],{M,R,S,O,_,OB,B}) -> + sort(Rest,{M,R,S,O,H,OB,B}); +sort([{old_heap_block_size,OB}|Rest],{M,R,S,O,H,_,B}) -> + sort(Rest,{M,R,S,O,H,OB,B}); +sort([{heap_block_size,B}|Rest],{M,R,S,O,H,OB,_}) -> + sort(Rest,{M,R,S,O,H,OB,B}); +sort([],GC) -> + GC. + + +%% +%% Print scheduling trace +%% +handle_schedule(Out,Trace,TI,initial) -> + print_header(Out,TI), + handle_schedule(Out,Trace,TI,[]); +handle_schedule(Out,end_of_trace,_TI,S) -> + summary(Out,S); +handle_schedule(Out,{trace_ts,P,out,Info,Ts},_TI,S) -> + io:format(Out, + "out:~n" + "Process : ~w~n" + "Time : ~s~n" + "Function : ~w~n~n",[P,ts(Ts),Info]), + case lists:keysearch(P,1,S) of + {value,{P,List}} -> + lists:keyreplace(P,1,S,{P,[{out,Ts}|List]}); + false -> + [{P,[{out,Ts}]} | S] + end; +handle_schedule(Out,{trace_ts,P,in,Info,Ts},_TI,S) -> + io:format(Out, + "in:~n" + "Process : ~w~n" + "Time : ~s~n" + "Function : ~w~n~n",[P,ts(Ts),Info]), + case lists:keysearch(P,1,S) of + {value,{P,List}} -> + lists:keyreplace(P,1,S,{P,[{in,Ts}|List]}); + false -> + [{P,[{in,Ts}]} | S] + end. + + +summary(Out,[{P,List}|Rest]) -> + Sum = proc_summary(List,0), + io:format(Out,"Total time 'in' for process ~w: ~w micro seconds~n",[P,Sum]), + summary(Out,Rest); +summary(_Out,[]) -> + ok. + +proc_summary([{in,_Start}|Rest],Acc) -> + proc_summary(Rest,Acc); +proc_summary([{out,End},{in,Start}|Rest],Acc) -> + Diff = diff(Start,End), + proc_summary(Rest,Acc+Diff); +proc_summary([],Acc) -> + Acc. + + +%% +%% Library functions +%% +ts({_, _, Micro} = Now) -> + {{Y,M,D},{H,Min,S}} = calendar:now_to_local_time(Now), + io_lib:format("~4.4.0w-~2.2.0w-~2.2.0w ~2.2.0w:~2.2.0w:~2.2.0w,~6.6.0w", + [Y,M,D,H,Min,S,Micro]). + + +diff({SMeg,SS,SMic},{EMeg,ES,EMic}) -> + (EMeg-SMeg)*1000000000000 + (ES-SS)*1000000 + (EMic-SMic). + + +init() -> + ttb:write_trace_info(start_time,fun() -> now() end). + +print_header(Out,TI) -> + {value,{node,[Node]}} = lists:keysearch(node,1,TI), + {value,{flags,Flags}} = lists:keysearch(flags,1,TI), + case lists:keysearch(start_time,1,TI) of + {value,{start_time,[ST]}} -> + io:format(Out, + "~nTracing started on node ~w at ~s~n" + "Flags: ~p~n~n~n", + [Node,ts(ST),Flags]); + false -> % in case this file was not loaded on the traced node + io:format(Out, + "~nTracing from node ~w~n" + "Flags: ~p~n~n~n", + [Node,Flags]) + end. + +running() -> + case whereis(ttb) of + undefined -> false; + _Pid -> true + end. diff --git a/lib/observer/src/observer.app.src b/lib/observer/src/observer.app.src new file mode 100644 index 0000000000..5c65ea5c8f --- /dev/null +++ b/lib/observer/src/observer.app.src @@ -0,0 +1,34 @@ +%% +%% %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, observer, + [{description, "OBSERVER version 1"}, + {vsn, "%VSN%"}, + {modules, [crashdump_viewer, + crashdump_viewer_html, + etop, + etop_gui, + etop_tr, + etop_txt, + ttb, + ttb_et]}, + {registered, []}, + {applications, [kernel, stdlib]}, + {env, []}]}. + + diff --git a/lib/observer/src/observer.appup.src b/lib/observer/src/observer.appup.src new file mode 100644 index 0000000000..1d5a0d93f5 --- /dev/null +++ b/lib/observer/src/observer.appup.src @@ -0,0 +1,19 @@ +%% +%% %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/observer/src/ttb.erl b/lib/observer/src/ttb.erl new file mode 100644 index 0000000000..221b71df6a --- /dev/null +++ b/lib/observer/src/ttb.erl @@ -0,0 +1,1000 @@ +%% +%% %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% +%% +-module(ttb). +-author('[email protected]'). + +%% API +-export([tracer/0,tracer/1,tracer/2,p/2,stop/0,stop/1]). +-export([tp/2, tp/3, tp/4, ctp/0, ctp/1, ctp/2, ctp/3, tpl/2, tpl/3, tpl/4, + ctpl/0, ctpl/1, ctpl/2, ctpl/3, ctpg/0, ctpg/1, ctpg/2, ctpg/3]). +-export([seq_trigger_ms/0,seq_trigger_ms/1]). +-export([write_trace_info/2]). +-export([write_config/2,write_config/3,run_config/1,run_config/2,list_config/1]). +-export([list_history/0,run_history/1]). +-export([format/1,format/2]). + +%% For debugging +-export([dump_ti/1]). + +-include_lib("kernel/include/file.hrl"). +-define(meta_time,5000). +-define(history_table,ttb_history_table). +-define(seq_trace_flags,[send,'receive',print,timestamp]). +-define(upload_dir,"ttb_upload"). +-ifdef(debug). +-define(get_status,;get_status -> erlang:display(dict:to_list(NodeInfo)),loop(NodeInfo)). +-else. +-define(get_status,). +-endif. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%%% Open a trace port on all given nodes and create the meta data file +tracer() -> tracer(node()). +tracer(Nodes) -> tracer(Nodes,[]). +tracer(Nodes,Opt) -> + start(), + store(tracer,[Nodes,Opt]), + {PI,Client,Traci} = opt(Opt), + do_tracer(Nodes,PI,Client,Traci). + +do_tracer(Nodes0,PI,Client,Traci) -> + Nodes = nods(Nodes0), + Clients = clients(Nodes,Client), + do_tracer(Clients,PI,Traci). + +do_tracer(Clients,PI,Traci) -> + {ClientSucc,Succ} = + lists:foldl( + fun({N,{local,File},TF},{CS,S}) -> + [_Sname,Host] = string:tokens(atom_to_list(N),"@"), + case catch dbg:tracer(N,port,dbg:trace_port(ip,0)) of + {ok,N} -> + {ok,Port} = dbg:trace_port_control(N,get_listen_port), + {ok,T} = dbg:get_tracer(N), + rpc:call(N,seq_trace,set_system_tracer,[T]), + dbg:trace_client(ip,{Host,Port}, + {fun ip_to_file/2,{file,File}}), + {[{N,{local,File,Port},TF}|CS], [N|S]}; + Other -> + display_warning(N,{cannot_open_ip_trace_port, + Host, + Other}), + {CS, S} + end; + ({N,C,_}=Client,{CS,S}) -> + case catch dbg:tracer(N,port,dbg:trace_port(file,C)) of + {ok,N} -> + {ok,T} = dbg:get_tracer(N), + rpc:call(N,seq_trace,set_system_tracer,[T]), + {[Client|CS], [N|S]}; + Other -> + display_warning(N,Other), + {CS,S} + end + end, + {[],[]}, + Clients), + case Succ of + [] -> + {ok,Succ}; + _list -> + write_info(ClientSucc,PI,Traci), + {ok,Succ} + end. + +opt(Opt) -> + opt(Opt,{true,?MODULE,[]}). + +opt([{process_info,PI}|O],{_,Client,Traci}) -> + opt(O,{PI,Client,Traci}); +opt([{file,Client}|O],{PI,_,Traci}) -> + opt(O,{PI,Client,Traci}); +opt([{handler,Handler}|O],{PI,Client,Traci}) -> + opt(O,{PI,Client,[{handler,Handler}|Traci]}); +opt([],Opt) -> + Opt. + +nods(all) -> + Nodes1 = remove_active([node()|nodes()]), + remove_faulty_runtime_tools_vsn(Nodes1); +nods(Node) when is_atom(Node) -> + nods([Node]); +nods(Nodes) when is_list(Nodes) -> + Nodes1 = remove_active(Nodes), + Nodes2 = remove_noexist(Nodes1), + remove_faulty_runtime_tools_vsn(Nodes2). + +remove_active(Nodes) -> + Active = get_nodes(), + lists:filter( + fun(N) -> case lists:member(N,Active) of + false -> true; + true -> display_warning(N,already_started), false + end + end, Nodes). + +remove_noexist(Nodes) -> + lists:filter( + fun(N) when N=:=node() -> + true; + (N) -> + case net_adm:ping(N) of + pong -> true; + pang -> display_warning(N,no_connection), false + end + end, Nodes). + +remove_faulty_runtime_tools_vsn(Nodes) -> + lists:filter( + fun(N) -> + case rpc:call(N,observer_backend,vsn,[]) of + {ok,Vsn} -> check_vsn(N,Vsn); + _Error -> display_warning(N,faulty_vsn_of_runtime_tools), false + end + end,Nodes). + +check_vsn(_Node,_Vsn) -> true. +%check_vsn(Node,_Vsn) -> +% display_warning(Node,faulty_vsn_of_runtime_tools), +% false. + +clients(Nodes, {wrap,Name}) -> + F = fun(Node) -> + TraceFile = name(Node,Name), + {Node,{TraceFile++".",wrap,".wrp"},TraceFile} + end, + lists:map(F,Nodes); +clients(Nodes, {wrap,Name,Size,Count}) -> + F = fun(Node) -> + TraceFile = name(Node,Name), + {Node,{TraceFile++".",wrap,".wrp",Size,Count},TraceFile} + end, + lists:map(F,Nodes); +clients(Nodes, {local,RealClient}) -> + WrapClients = clients(Nodes,RealClient), + F = fun({Node,Client,TraceFile}) -> + {Node,{local,Client},TraceFile} + end, + lists:map(F,WrapClients); +clients(Nodes, Name) -> + F = fun(Node) -> + TraceFile = name(Node,Name), + {Node,TraceFile,TraceFile} + end, + lists:map(F,Nodes). + +name(Node,Filename) -> + Dir = filename:dirname(Filename), + File = filename:basename(Filename), + filename:join(Dir,atom_to_list(Node) ++ "-" ++ File). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%%% Handling of config file +store(Func,Args) -> + Last = case ets:last(?history_table) of + '$end_of_table' -> 0; + Int when is_integer(Int) -> Int + end, + ets:insert(?history_table,{Last+1,{?MODULE,Func,Args}}). + +list_history() -> + %% the check is only to see if the tool is started. + case ets:info(?history_table) of + undefined -> {error, not_running}; + _info -> ets:tab2list(?history_table) + end. + +run_history([H|T]) -> + case run_history(H) of + ok -> run_history(T); + {error,not_found} -> {error,{not_found,H}} + end; +run_history([]) -> + ok; +run_history(N) -> + case catch ets:lookup(?history_table,N) of + [{N,{M,F,A}}] -> + print_func(M,F,A), + R = apply(M,F,A), + print_result(R); + _ -> + {error, not_found} + end. + +write_config(ConfigFile,all) -> + write_config(ConfigFile,['_']); +write_config(ConfigFile,Config) -> + write_config(ConfigFile,Config,[]). +write_config(ConfigFile,all,Opt) -> + write_config(ConfigFile,['_'],Opt); +write_config(ConfigFile,Nums,Opt) when is_list(Nums), is_integer(hd(Nums)); + Nums=:=['_'] -> + F = fun(N) -> ets:select(?history_table, + [{{N,'$1'},[],['$1']}]) + end, + Config = lists:append(lists:map(F,Nums)), + do_write_config(ConfigFile,Config,Opt); +write_config(ConfigFile,Config,Opt) when is_list(Config) -> + case check_config(Config,[]) of + {ok,Config1} -> do_write_config(ConfigFile,Config1,Opt); + Error -> Error + end. + +do_write_config(ConfigFile,Config,Opt) -> + case Opt of + [append] -> ok; + [] -> file:delete(ConfigFile) + end, + write_binary(ConfigFile,Config). + +check_config([{?MODULE=Mod,Func,Args}|Rest],Acc) -> + %% Check only in this module, since other modules might not + %% be loaded at the time of creating the config file. + case erlang:function_exported(Mod,Func,length(Args)) of + true -> check_config(Rest,[{Mod,Func,Args}|Acc]); + false -> {error, {not_exported,{Mod,Func,Args}}} + end; +check_config([{Mod,Func,Args}|Rest],Acc) -> + check_config(Rest,[{Mod,Func,Args}|Acc]); +check_config([],Acc) -> + {ok,lists:reverse(Acc)}; +check_config([Other|_Rest],_Acc) -> + {error,{illegal_config,Other}}. + + +list_config(ConfigFile) -> + case file:read_file(ConfigFile) of + {ok,B} -> read_config(B,[],1); + Error -> Error + end. + +read_config(<<>>,Acc,_N) -> + lists:reverse(Acc); +read_config(B,Acc,N) -> + {{M,F,A},Rest} = get_term(B), + read_config(Rest,[{N,{M,F,A}}|Acc],N+1). + + +run_config(ConfigFile) -> + case list_config(ConfigFile) of + Config when is_list(Config) -> + lists:foreach(fun({_,{M,F,A}}) -> print_func(M,F,A), + R = apply(M,F,A), + print_result(R) + end, + Config); + Error -> Error + end. + +run_config(ConfigFile,N) -> + case list_config(ConfigFile) of + Config when is_list(Config) -> + case lists:keysearch(N,1,Config) of + {value,{N,{M,F,A}}} -> + print_func(M,F,A), + apply(M,F,A); + false -> + {error, not_found} + end; + Error -> Error + end. + + +print_func(M,F,A) -> + Args = arg_list(A,[]), + io:format("~w:~w(~s) ->~n",[M,F,Args]). +print_result(R) -> + io:format("~p~n~n",[R]). + +arg_list([],[]) -> + ""; +arg_list([A1],Acc) -> + Acc++io_lib:format("~w",[A1]); +arg_list([A1|A],Acc) -> + arg_list(A,Acc++io_lib:format("~w,",[A1])). + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%%% Set trace flags on processes +p(Procs0,Flags0) -> + store(p,[Procs0,Flags0]), + no_store_p(Procs0,Flags0). +no_store_p(Procs0,Flags0) -> + case transform_flags(to_list(Flags0)) of + {error,Reason} -> + {error,Reason}; + Flags -> + Procs = procs(Procs0), + case lists:foldl(fun(P,{PMatched,Ps}) -> case dbg:p(P,Flags) of + {ok,Matched} -> + {[{P,Matched}|PMatched],[P|Ps]}; + {error,Reason} -> + display_warning(P,Reason), + {PMatched,Ps} + end + end,{[],[]},Procs) of + {[],[]} -> {error, no_match}; + {SuccMatched,Succ} -> + no_store_write_trace_info(flags,{Succ,Flags}), + {ok,SuccMatched} + end + end. + +transform_flags([clear]) -> + [clear]; +transform_flags(Flags) -> + dbg:transform_flags(Flags). + + +procs(Procs) when is_list(Procs) -> + lists:foldl(fun(P,Acc) -> proc(P)++Acc end,[],Procs); +procs(Proc) -> + proc(Proc). + +proc(Procs) when Procs=:=all; Procs=:=existing; Procs=:=new -> + [Procs]; +proc(Name) when is_atom(Name) -> + [Name]; % can be registered on this node or other node +proc(Pid) when is_pid(Pid) -> + [Pid]; +proc({global,Name}) -> + case global:whereis_name(Name) of + Pid when is_pid(Pid) -> + [Pid]; + undefined -> + [] + end. + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%%% Trace pattern +tp(A,B) -> + store(tp,[A,B]), + dbg:tp(A,B). +tp(A,B,C) -> + store(tp,[A,B,C]), + dbg:tp(A,B,C). +tp(A,B,C,D) -> + store(tp,[A,B,C,D]), + dbg:tp(A,B,C,D). + +tpl(A,B) -> + store(tpl,[A,B]), + dbg:tpl(A,B). +tpl(A,B,C) -> + store(tpl,[A,B,C]), + dbg:tpl(A,B,C). +tpl(A,B,C,D) -> + store(tpl,[A,B,C,D]), + dbg:tpl(A,B,C,D). + +ctp() -> + store(ctp,[]), + dbg:ctp(). +ctp(A) -> + store(ctp,[A]), + dbg:ctp(A). +ctp(A,B) -> + store(ctp,[A,B]), + dbg:ctp(A,B). +ctp(A,B,C) -> + store(ctp,[A,B,C]), + dbg:ctp(A,B,C). + +ctpl() -> + store(ctpl,[]), + dbg:ctpl(). +ctpl(A) -> + store(ctpl,[A]), + dbg:ctpl(A). +ctpl(A,B) -> + store(ctpl,[A,B]), + dbg:ctpl(A,B). +ctpl(A,B,C) -> + store(ctpl,[A,B,C]), + dbg:ctpl(A,B,C). + +ctpg() -> + store(ctpg,[]), + dbg:ctpg(). +ctpg(A) -> + store(ctpg,[A]), + dbg:ctpg(A). +ctpg(A,B) -> + store(ctpg,[A,B]), + dbg:ctpg(A,B). +ctpg(A,B,C) -> + store(ctpg,[A,B,C]), + dbg:ctpg(A,B,C). + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%%% Support for sequential trace +seq_trigger_ms() -> seq_trigger_ms(all). +seq_trigger_ms(all) -> seq_trigger_ms(?seq_trace_flags); +seq_trigger_ms(Flag) when is_atom(Flag) -> seq_trigger_ms([Flag],[]); +seq_trigger_ms(Flags) -> seq_trigger_ms(Flags,[]). +seq_trigger_ms([Flag|Flags],Body) -> + case lists:member(Flag,?seq_trace_flags) of + true -> seq_trigger_ms(Flags,[{set_seq_token,Flag,true}|Body]); + false -> {error,{illegal_flag,Flag}} + end; +seq_trigger_ms([],Body) -> + [{'_',[],Body}]. + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%%% Write information to the .ti file +write_trace_info(Key,What) -> + store(write_trace_info,[Key,What]), + no_store_write_trace_info(Key,What). + +no_store_write_trace_info(Key,What) -> + case whereis(?MODULE) of + undefined -> ok; + Pid when is_pid(Pid) -> ?MODULE ! {write_trace_info,Key,What} + end, + ok. + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%%% Stop tracing on all nodes +stop() -> + stop([]). +stop(Opts) -> + Fetch = stop_opts(Opts), + case whereis(?MODULE) of + undefined -> ok; + Pid when is_pid(Pid) -> + ?MODULE ! {stop,Fetch,self()}, + receive {?MODULE,stopped} -> ok end + end, + stopped. + +stop_opts(Opts) -> + case lists:member(format,Opts) of + true -> + format; % format implies fetch + false -> + case lists:member(fetch,Opts) of + true -> fetch; + false -> nofetch + end + end. + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%%% Process implementation +start() -> + case whereis(?MODULE) of + undefined -> + Parent = self(), + Pid = spawn(fun() -> init(Parent) end), + receive {started,Pid} -> ok end; + Pid when is_pid(Pid) -> + ok + end. + + +init(Parent) -> + register(?MODULE,self()), + ets:new(?history_table,[ordered_set,named_table,public]), + Parent ! {started,self()}, + loop(dict:new()). + +loop(NodeInfo) -> + receive + {init_node,Node,MetaFile,PI,Traci} -> + erlang:monitor_node(Node,true), + MetaPid = + case rpc:call(Node, + observer_backend, + ttb_init_node, + [MetaFile,PI,Traci]) of + {ok,MP} -> + MP; + {badrpc,nodedown} -> + %% We will get a nodedown message + undefined + end, + loop(dict:store(Node,{MetaFile,MetaPid},NodeInfo)); + {get_nodes,Sender} -> + Sender ! {?MODULE,dict:fetch_keys(NodeInfo)}, + loop(NodeInfo); + {write_trace_info,Key,What} -> + dict:fold(fun(Node,{_MetaFile,MetaPid},_) -> + rpc:call(Node,observer_backend, + ttb_write_trace_info,[MetaPid,Key,What]) + end, + ok, + NodeInfo), + loop(NodeInfo); + {nodedown,Node} -> + loop(dict:erase(Node,NodeInfo)); + {stop,nofetch,Sender} -> + dict:fold( + fun(Node,{_,MetaPid},_) -> + rpc:call(Node,observer_backend,ttb_stop,[MetaPid]) + end, + ok, + NodeInfo), + dbg:stop_clear(), + ets:delete(?history_table), + Sender ! {?MODULE,stopped}; + {stop,FetchOrFormat,Sender} -> + Localhost = host(node()), + Dir = ?upload_dir++ts(), + file:make_dir(Dir), + %% The nodes are traversed twice here because + %% the meta tracing in observer_backend must be + %% stopped before dbg is stopped, and dbg must + %% be stopped before the trace logs are moved orelse + %% windows complains. + AllNodesAndMeta = + dict:fold( + fun(Node,{MetaFile,MetaPid},Nodes) -> + rpc:call(Node,observer_backend,ttb_stop,[MetaPid]), + [{Node,MetaFile}|Nodes] + end, + [], + NodeInfo), + dbg:stop_clear(), + AllNodes = + lists:map( + fun({Node,MetaFile}) -> + spawn(fun() -> fetch(Localhost,Dir,Node,MetaFile) end), + Node + end, + AllNodesAndMeta), + ets:delete(?history_table), + wait_for_fetch(AllNodes), + io:format("Stored logs in ~s~n",[filename:absname(Dir)]), + case FetchOrFormat of + format -> format(Dir); + fetch -> ok + end, + Sender ! {?MODULE,stopped} + ?get_status + end. + +get_nodes() -> + ?MODULE ! {get_nodes,self()}, + receive {?MODULE,Nodes} -> Nodes end. + +ts() -> + {{Y,M,D},{H,Min,S}} = calendar:now_to_local_time(now()), + io_lib:format("-~4.4.0w~2.2.0w~2.2.0w-~2.2.0w~2.2.0w~2.2.0w", + [Y,M,D,H,Min,S]). + + + +fetch(Localhost,Dir,Node,MetaFile) -> + case host(Node) of + Localhost -> % same host, just move the files + Files = rpc:call(Node,observer_backend,ttb_get_filenames,[MetaFile]), + lists:foreach( + fun(File0) -> + File = filename:join(Dir,filename:basename(File0)), + file:rename(File0,File) + end, + Files); + _Otherhost -> + {ok, LSock} = gen_tcp:listen(0, [binary,{packet,2},{active,false}]), + {ok,Port} = inet:port(LSock), + rpc:cast(Node,observer_backend,ttb_fetch, + [MetaFile,{Port,Localhost}]), + {ok, Sock} = gen_tcp:accept(LSock), + receive_files(Dir,Sock,undefined), + ok = gen_tcp:close(LSock), + ok = gen_tcp:close(Sock) + end, + ?MODULE ! {fetch_complete,Node}. + +receive_files(Dir,Sock,Fd) -> + case gen_tcp:recv(Sock, 0) of + {ok, <<0,Bin/binary>>} -> + file:write(Fd,Bin), + receive_files(Dir,Sock,Fd); + {ok, <<1,Bin/binary>>} -> + File0 = binary_to_list(Bin), + File = filename:join(Dir,File0), + {ok,Fd1} = file:open(File,[raw,write]), + receive_files(Dir,Sock,Fd1); + {error, closed} -> + ok = file:close(Fd) + end. + +host(Node) -> + [_name,Host] = string:tokens(atom_to_list(Node),"@"), + Host. + + +wait_for_fetch([]) -> + ok; +wait_for_fetch(Nodes) -> + receive + {fetch_complete,Node} -> + wait_for_fetch(lists:delete(Node,Nodes)) + end. + +%%% - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +%%% TRACE INFORMATION FILE +%%% ====================== +%%% The trace information file has the same name as the trace log, +%%% but with the extension ".ti". It contains process information, +%%% trace information and any data the user writes with the +%%% function write_trace_info/2. +%%% +%%% The file is read during formatting of trace logs, and all data +%%% except process information is included in the handler function. +%%% - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +write_info(Nodes,PI,Traci) -> + lists:foreach(fun({N,{local,C,_},F}) -> + MetaFile = F ++ ".ti", + file:delete(MetaFile), + Traci1 = [{node,N},{file,C}|Traci], + {ok,Port} = dbg:get_tracer(N), + ?MODULE ! + {init_node, N, {local,MetaFile,Port}, PI, Traci1}; + ({N,C,F}) -> + MetaFile = F ++ ".ti", + Traci1 = [{node,N},{file,C}|Traci], + ?MODULE ! {init_node, N, MetaFile, PI, Traci1} + end, + Nodes). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%%% Format binary trace logs +format(Files) -> + format(Files,[]). +format(Files,Opt) -> + {Out,Handler} = format_opt(Opt), + ets:new(?MODULE,[named_table]), + format(Files,Out,Handler). +format(File,Out,Handler) when is_list(File), is_integer(hd(File)) -> + Files = + case filelib:is_dir(File) of + true -> % will merge all files in the directory + MetaFiles = filelib:wildcard(filename:join(File,"*.ti")), + lists:map(fun(M) -> + Sub = string:left(M,length(M)-3), + case filelib:is_file(Sub) of + true -> Sub; + false -> Sub++".*.wrp" + end + end, + MetaFiles); + false -> % format one file + [File] + end, + format(Files,Out,Handler); +format(Files,Out,Handler) when is_list(Files), is_list(hd(Files)) -> + StopDbg = case whereis(dbg) of + undefined -> true; + _ -> false + end, + Details = lists:foldl(fun(File,Acc) -> [prepare(File,Handler)|Acc] end, + [],Files), + Fd = get_fd(Out), + R = do_format(Fd,Details), + file:close(Fd), + ets:delete(?MODULE), + case StopDbg of + true -> dbg:stop_clear(); + false -> ok + end, + R. + +prepare(File,Handler) -> + {Traci,Proci} = read_traci(File), + Node = get_node(Traci), + lists:foreach(fun({Pid,PI}) -> + %% The last definition for a Pid will overwrite + %% any previous definitions. That should be what + %% we want (we will get the registered name for + %% the process, rather than the initial call if + %% both are present in the list). + ets:insert(?MODULE,{Pid,PI,Node}) + end,Proci), + FileOrWrap = get_file(File,Traci), + Handler1 = get_handler(Handler,Traci), + {FileOrWrap,Traci,Handler1}. + +format_opt(Opt) -> + Out = case lists:keysearch(out,1,Opt) of + {value,{out,O}} -> O; + _ -> standard_io + end, + Handler = case lists:keysearch(handler,1,Opt) of + {value,{handler,H}} -> H; + _ -> undefined + end, + {Out,Handler}. + + +read_traci(File) -> + MetaFile = get_metafile(File), + case file:read_file(MetaFile) of + {ok,B} -> + interpret_binary(B,dict:new(),[]); + _ -> + io:format("Warning: no meta data file: ~s~n",[MetaFile]), + {dict:new(),[]} + end. + +get_metafile(File) -> + case filename:rootname(File,".wrp") of + File -> File++".ti"; + Wrap -> filename:rootname(Wrap)++".ti" + end. + + +interpret_binary(<<>>,Dict,P) -> + {Dict,lists:reverse(P)}; +interpret_binary(B,Dict,P) -> + {Term,Rest} = get_term(B), + {Dict1,P1} = + case Term of + {pid,PI} -> + {Dict,[PI|P]}; + {Key,Val} -> + {dict:update(Key,fun(Val0) -> [Val|Val0] end, [Val], Dict),P} + end, + interpret_binary(Rest,Dict1,P1). + +get_fd(Out) -> + case Out of + standard_io -> + Out; + _file -> + file:delete(Out), + case file:open(Out,[append]) of + {ok,Fd} -> Fd; + Error -> exit(Error) + end + end. + +get_node(Traci) -> + case dict:find(node,Traci) of + {ok,[Node]} -> Node; + error -> unknown + end. + +get_file(File,Traci) -> + case dict:find(file,Traci) of + {ok,[Client]} -> + check_client(Client,File); + error -> + check_exists(File) + end. + +check_client(Client,File) when is_list(Client) -> + check_exists(File); +check_client(Client,File) when is_tuple(Client),element(2,Client)==wrap -> + Root = filename:rootname(File,".wrp"), + case filename:extension(Root) of + ".*" -> + Part1 = filename:rootname(Root,"*"), + setelement(1,Client,Part1); + _ -> + check_exists(File) + end. + +check_exists(File) -> + case file:read_file_info(File) of + {ok,#file_info{type=regular}} -> File; + _ -> + exit({error,no_file}) + end. + + +get_handler(Handler,Traci) -> + case Handler of + undefined -> + case dict:find(handler,Traci) of + {ok,[H]} -> H; + error -> undefined + end; + _ -> + Handler + end. + +do_format(Fd,Details) -> + Clients = lists:foldl(fun({FileOrWrap,Traci,Handler},Acc) -> + [start_client(FileOrWrap,Traci,Handler) + |Acc] + end,[],Details), + init_collector(Fd,Clients). + + +start_client(FileOrWrap,Traci,et) -> + dbg:trace_client(file, FileOrWrap, + {fun handler/2, + {dict:to_list(Traci),{{ttb_et,handler},initial}}}); +start_client(FileOrWrap,Traci,undefined) -> + dbg:trace_client(file, FileOrWrap, + {fun handler/2, + {dict:to_list(Traci),{fun defaulthandler/4,initial}}}); +start_client(FileOrWrap,Traci,Handler) -> + dbg:trace_client(file, FileOrWrap, + {fun handler/2, {dict:to_list(Traci),Handler}}). + +handler(Trace,State) -> + %% State here is only used for the initial state. The accumulated + %% State is maintained by collector!!! + receive + {get,Collector} -> Collector ! {self(),{Trace,State}}; + done -> ok + end, + State. + +handler1(Trace,{Fd,{Traci,{Fun,State}}}) when is_function(Fun) -> + {Traci,{Fun,Fun(Fd,Trace,Traci,State)}}; +handler1(Trace,{Fd,{Traci,{{M,F},State}}}) when is_atom(M), is_atom(F) -> + {Traci,{{M,F},M:F(Fd,Trace,Traci,State)}}. + +defaulthandler(Fd,Trace,_Traci,initial) -> + dbg:dhandler(Trace,Fd); +defaulthandler(_Fd,Trace,_Traci,State) -> + dbg:dhandler(Trace,State). + +init_collector(Fd,Clients) -> + Collected = get_first(Clients), + collector(Fd,sort(Collected)). + +collector(Fd,[{_,{Client,{Trace,State}}}|Rest]) -> + Trace1 = update_procinfo(Trace), + State1 = handler1(Trace1,{Fd,State}), + case get_next(Client,State1) of + end_of_trace -> + handler1(end_of_trace,{Fd,State1}), + collector(Fd,Rest); + Next -> collector(Fd,sort([Next|Rest])) + end; +collector(_Fd,[]) -> + ok. + +update_procinfo({drop,_N}=Trace) -> + Trace; +update_procinfo(Trace) when element(1,Trace)==seq_trace -> + Info = element(3,Trace), + Info1 = + case Info of + {send, Serial, From, To, Msg} -> + {send, Serial, get_procinfo(From), get_procinfo(To), Msg}; + {'receive', Serial, From, To, Msg} -> + {'receive', Serial, get_procinfo(From), get_procinfo(To), Msg}; + {print, Serial, From, Void, UserInfo} -> + {print, Serial, get_procinfo(From), Void, UserInfo}; + Other -> + Other + end, + setelement(3,Trace,Info1); +update_procinfo(Trace) when element(3,Trace)==send -> + PI = get_procinfo(element(5,Trace)), + setelement(5,Trace,PI); +update_procinfo(Trace) -> + Pid = element(2,Trace), + ProcInfo = get_procinfo(Pid), + setelement(2,Trace,ProcInfo). + +get_procinfo(Pid) when is_pid(Pid) -> + case ets:lookup(?MODULE,Pid) of + [PI] -> PI; + [] -> Pid + end; +get_procinfo(Name) when is_atom(Name) -> + case ets:match_object(?MODULE,{'_',Name,node()}) of + [PI] -> PI; + [] -> Name + end; +get_procinfo({Name,Node}) when is_atom(Name) -> + case ets:match_object(?MODULE,{'_',Name,Node}) of + [PI] -> PI; + [] -> {Name,Node} + end. + +get_first([Client|Clients]) -> + Client ! {get,self()}, + receive + {Client,{end_of_trace,_}} -> + get_first(Clients); + {Client,{Trace,_State}}=Next -> + [{timestamp(Trace),Next}|get_first(Clients)] + end; +get_first([]) -> []. + +get_next(Client,State) when is_pid(Client) -> + Client ! {get,self()}, + receive + {Client,{end_of_trace,_}} -> + end_of_trace; + {Client,{Trace,_OldState}} -> + {timestamp(Trace),{Client,{Trace,State}}} % inserting new state!! + end. + +sort(List) -> + lists:keysort(1,List). + + +timestamp(Trace) when element(1,Trace) =:= trace_ts; + element(1,Trace) =:= seq_trace, tuple_size(Trace) =:= 4 -> + element(tuple_size(Trace),Trace); +timestamp(_Trace) -> + 0. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%%% common internal functions +to_list(Atom) when is_atom(Atom) -> [Atom]; +to_list(List) when is_list(List) -> List. + +write_binary(File,TermList) -> + {ok,Fd} = file:open(File,[raw,append]), + %% Using the function implemented in observer_backend, only because + %% is exists - so I don't have to write the same code twice. + observer_backend:ttb_write_binary(Fd,TermList), + file:close(Fd). + +get_term(B) -> + <<S:8, B2/binary>> = B, + <<T:S/binary, Rest/binary>> = B2, + case binary_to_term(T) of + {'$size',Sz} -> + %% size of the actual term was bigger than 8 bits + <<T1:Sz/binary, Rest1/binary>> = Rest, + {binary_to_term(T1),Rest1}; + Term -> + {Term,Rest} + end. + +display_warning(Item,Warning) -> + io:format("Warning: {~w,~w}~n",[Warning,Item]). + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%%% Trace client which reads an IP port and puts data directly to a file. +%%% This is used when tracing remote nodes with no file system. +ip_to_file(Trace,{file,File}) -> + Fun = dbg:trace_port(file,File), %File can be a filename or a wrap spec + Port = Fun(), + ip_to_file(Trace,Port); +ip_to_file({metadata,MetaFile,MetaData},Port) -> + {ok,MetaFd} = file:open(MetaFile,[write,raw,append]), + file:write(MetaFd,MetaData), + file:close(MetaFd), + Port; +ip_to_file(Trace,Port) -> + B = term_to_binary(Trace), + erlang:port_command(Port,B), + Port. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%%% For debugging +dump_ti(File) -> + {ok,B} = file:read_file(File), + dump_ti(B,[]). + +dump_ti(<<>>,Acc) -> + lists:reverse(Acc); +dump_ti(B,Acc) -> + {Term,Rest} = get_term(B), + dump_ti(Rest,[Term|Acc]). + + diff --git a/lib/observer/src/ttb_et.erl b/lib/observer/src/ttb_et.erl new file mode 100644 index 0000000000..60769f1cc2 --- /dev/null +++ b/lib/observer/src/ttb_et.erl @@ -0,0 +1,267 @@ +%% +%% %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% +%% +-module(ttb_et). +-author('[email protected]'). + +-include("et.hrl"). +-export([handler/4]). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%%% ----------- TTB format handler ----------- + +handler(Out,Trace,Traci,initial) -> + S = self(), + spawn(fun() -> init_et(S) end), + receive {et_started,Collector} -> ok end, + handler(Out,Trace,Traci,Collector); +handler(_,end_of_trace,_Traci,Col) -> + get_returns(Col), + ok; +handler(_,Trace,_Traci,Col) -> + {ok,NewCol} = et_collector:report(Col,Trace), + NewCol. + + +%%% ----------- Collector Filter ----------- + +collector(Event) when is_record(Event,event) -> + %% if collector is selected from viewer menu + true; +collector(Trace) -> + et_selector:parse_event(undefined,Trace). + +%% After applying collector filter to all events, iterate over +%% all events backwards and collect call/return information: +%% +%% MFA collected from return_to events is added to call and +%% return_from events as {caller,MFA} and {return_to,MFA} respecively. +%% MFA collected from call events is added to return_to events as +%% {return_from,MFA} +%% +%% This information can then be used by any filter for generating to- +%% and from fields. +get_returns(Col) -> + Fun = fun(Event,Acc) -> collect_return_info(Event,Acc,Col) end, + et_collector:iterate(Col, last, '-infinity', Fun, dict:new()). + +collect_return_info(#event{label=L,from=Pid}=E,Acc,_Col) + when L==return_to;L==return_from-> + %% Stacking all return_to and return_from events + dict:update(Pid,fun(Old) -> [E|Old] end, [E], Acc); +collect_return_info(#event{label=call,from=Pid,contents=Contents}=E,Acc,Col) -> + %% Popping return_from and return_to events + %% If both exist, return_from will _always_ be first!!! + MFA = get_mfarity(Contents), + {Caller,NewAcc} = + case dict:find(Pid,Acc) of + {ok,[#event{label=return_from}=RetFrom, + #event{label=return_to}=RetTo | Rets]} -> + RetToCont = RetTo#event.contents, + C = get_mfarity(RetToCont), + NewRetTo = RetTo#event{contents=RetToCont++[{return_from,MFA}]}, + RetFromCont = RetFrom#event.contents, + NewRetFrom = + RetFrom#event{contents=RetFromCont++[{return_to,C}]}, + et_collector:report(Col,NewRetTo), + et_collector:report(Col,NewRetFrom), + {C, dict:store(Pid,Rets,Acc)}; + {ok,[#event{label=return_to}=RetTo | Rets]} -> + %% No return_from + RetToCont = RetTo#event.contents, + C = get_mfarity(RetToCont), + NewRetTo = RetTo#event{contents=RetToCont++[{return_from,MFA}]}, + et_collector:report(Col,NewRetTo), + {C, dict:store(Pid,Rets,Acc)}; + {ok,[#event{label=return_from}=RetFrom | Rets]} -> + %% No return_to, check if caller(pam_result) is in call event + C = get_caller(Contents), + RetFromCont = RetFrom#event.contents, + NewRetFrom = + RetFrom#event{contents=RetFromCont++[{return_to,C}]}, + et_collector:report(Col,NewRetFrom), + {C, dict:store(Pid,Rets,Acc)}; + _noreturn -> + {nocaller,Acc} + end, + NewE = E#event{contents=Contents++[{caller,Caller}]}, + et_collector:report(Col,NewE), + NewAcc; +collect_return_info(_E,Acc,_Col) -> + Acc. + +init_et(Parent) -> + process_flag(trap_exit,true), +% ets:new(ttb_et_table,[set,named_table,public]), +% ets:insert(ttb_et_table,{traci,Traci}), + EtOpt = [{active_filter,processes}, + {dict_insert, {filter, collector}, fun collector/1}, + {dict_insert, {filter, processes}, fun processes/1}, + {dict_insert, {filter, modules}, fun modules/1}, + {dict_insert, {filter, mods_and_procs}, fun mods_and_procs/1}, + {dict_insert, {filter, functions}, fun functions/1}, + {dict_insert, {filter, funcs_and_procs}, fun funcs_and_procs/1}, + {hide_actions, false}, + {max_events, infinity}, + {max_actors, infinity}], + {ok, Viewer} = et_viewer:start_link(EtOpt), + Collector = et_viewer:get_collector_pid(Viewer), + Parent ! {et_started,Collector}, + receive + {'EXIT',Viewer,shutdown} -> + ok + end. + + + +%%% ----------- Viewer Filters ----------- + +processes(E0) -> + E = label(E0), + {{FromProc,FromNode},{ToProc,ToNode}} = + get_actors(E#event.from,E#event.to), + {true,E#event{from = io_lib:format("~w~n~w",[FromProc,FromNode]), + to = io_lib:format("~w~n~w",[ToProc,ToNode])}}. + + +mods_and_procs(E) -> + ActorFun = fun({M,_F,_A},{Proc,Node}) -> + io_lib:format("~w~n~w~n~w",[M,Proc,Node]) + end, + calltrace_filter(E,ActorFun). + +modules(E) -> + ActorFun = fun({M,_F,_A},{_Proc,Node}) -> + io_lib:format("~w~n~w",[M,Node]) + end, + calltrace_filter(E,ActorFun). + +funcs_and_procs(E) -> + ActorFun = fun({M,F,A},{Proc,Node}) -> + io_lib:format("~s~n~w~n~w",[mfa(M,F,A),Proc,Node]) + end, + calltrace_filter(E,ActorFun). + +functions(E) -> + ActorFun = fun({M,F,A},{_Proc,Node}) -> + io_lib:format("~s~n~w",[mfa(M,F,A),Node]) + end, + calltrace_filter(E,ActorFun). + + + +%% Common filter used by mods_and_procs/1 and modules/1 +calltrace_filter(E,ActorFun) -> + {From,To} = get_actors(E#event.from,E#event.to), + calltrace_filter(E,From,To,ActorFun). + +calltrace_filter(#event{label=call}=E,From,To,ActorFun) -> + Cont = E#event.contents, + MFA = get_mfarity(Cont), + case lists:keysearch(caller,1,Cont) of + {value,{_,{_CM,_CF,_CA}=Caller}} -> + {true, E#event{label = label(call,MFA), + from = ActorFun(Caller,From), + to = ActorFun(MFA,To)}}; + {value,{_, _}} -> + {true, E#event{label = label(call,MFA), + from = ActorFun(MFA,From), + to = ActorFun(MFA,To)}} + end; +calltrace_filter(#event{label=return_from}=E,From,To,ActorFun) -> + Cont = E#event.contents, + MFA = get_mfarity(Cont), + case lists:keysearch(return_to,1,Cont) of + {value,{_,{_M2,_F2,_A2}=MFA2}} -> + {true, E#event{label = label(return_from,MFA), + from = ActorFun(MFA,From), + to = ActorFun(MFA2,To)}}; + {value,{_, _}} -> + {true, E#event{label = label(return_from,MFA), + from = ActorFun(MFA,From), + to = ActorFun(MFA,To)}} + end; +calltrace_filter(#event{label=return_to}=E,From,To,ActorFun) -> + Cont = E#event.contents, + {value,{_,{_M2,_F2,_A2}=MFA2}} = lists:keysearch(return_from,1,Cont), + case get_mfarity(Cont) of + {_M,_F,_A}=MFA -> + {true, E#event{label = label(return_to,MFA), + from = ActorFun(MFA2,From), + to = ActorFun(MFA,To)}}; + undefined -> + {true, E#event{label = "return_to unknown", + from = ActorFun(MFA2,From), + to = ActorFun(MFA2,To)}} + end; +calltrace_filter(_E,_From,_To,_ActorFun) -> + false. + + +label(Event=#event{label=L,contents=C}) -> + case lists:keysearch(mfa,1,C) of + {value,{mfa,MFA}} -> Event#event{label=label(L,MFA)}; + false -> Event + end. +label(L,{M,F,A}) -> label(L,M,F,A); +label(L,Other) -> io_lib:format("~w ~w",[L,Other]). +label(call,M,F,A) -> "call " ++ mfa(M,F,A); +label(return_from,M,F,A) -> "return_from " ++ mfa(M,F,A); +label(return_to,M,F,A) -> "return_to " ++ mfa(M,F,A); +label(spawn,M,F,A) -> "spawn " ++ mfa(M,F,A); +label(out,M,F,A) -> "out " ++ mfa(M,F,A); +label(in,M,F,A) -> "in " ++ mfa(M,F,A). + +mfa(M,F,A) -> atom_to_list(M) ++ ":" ++ fa(F,A). +fa(F,A) -> atom_to_list(F) ++ "/" ++ integer_to_list(arity(A)). + +arity(L) when is_list(L) -> length(L); +arity(I) when is_integer(I) -> I. + +get_actors(From,To) -> + case {get_proc(From),get_proc(To)} of + {{_FP,_FN},{_TP,_TN}}=R -> R; + {{FP,FN},T} -> {{FP,FN},{T,FN}}; + {F,{TP,TN}} -> {{F,TN},{TP,TN}}; + {F,T} -> {{F,unknown},{T,unknown}} + end. + +get_proc({_Pid,Name,Node}) when is_atom(Name) -> {Name,Node}; +get_proc({Pid,_initfunc,Node}) -> {Pid,Node}; +get_proc(P) when is_pid(P); is_port(P) -> {P,node(P)}; +get_proc(P) -> P. + +get_mfarity(List) -> + case get_mfa(List) of + {M,F,A} -> {M,F,arity(A)}; + Other -> Other + end. +get_mfa(List) -> + {value,{mfa,MFA}} = lists:keysearch(mfa,1,List), + MFA. + +get_caller(List) -> + case lists:keysearch(pam_result,1,List) of + {value,{pam_result,{M,F,A}}} -> {M,F,arity(A)}; + {value,{pam_result,undefined}} -> undefined; + {value,{pam_result,_Other}} -> nocaller; + false -> nocaller + end. + + |