From 38fc7a0eb46167a27ebac41452ecd1fb0de8803c Mon Sep 17 00:00:00 2001 From: Siri Hansen Date: Mon, 21 Feb 2011 10:02:16 +0100 Subject: Fix slow parsing of crashdumps This is a first attempt at fixing the problem described in seq11783 - crashdump_viewer is very slow at parsing big crashdumps. To open the first page for a dump of 17M takes about 2 minutes and a dump of 280M takes 1.5-2 hours. The main problmem is that the cdv_dump_index_table, which holds all tags read from the dump, is a bag. Profiling shows that ~95% of the time is spent in ets:insert. The table is now changed to an ordered_set. A second problem occured when a page with many table rows was opened. These pages were sent to inet in one chunk, causing both crashdump_viewer_server and the inets (mod_esi) process to grow very much in memory usage. To overcome this, the pages are now sent to inets in chunks of 1000 rows, and the data is coverted to binaries to avoid data copying between the two processes. Also, some new information in the crashdump was not recognized by the crashdump_viewer. This has been fixed. --- lib/observer/src/crashdump_viewer.erl | 1014 +++++++++++++++----------- lib/observer/src/crashdump_viewer.hrl | 185 ++--- lib/observer/src/crashdump_viewer_html.erl | 584 +++++++-------- lib/observer/test/crashdump_viewer_SUITE.erl | 33 +- 4 files changed, 997 insertions(+), 819 deletions(-) (limited to 'lib') diff --git a/lib/observer/src/crashdump_viewer.erl b/lib/observer/src/crashdump_viewer.erl index 978541e470..8b96769224 100644 --- a/lib/observer/src/crashdump_viewer.erl +++ b/lib/observer/src/crashdump_viewer.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2003-2009. All Rights Reserved. +%% Copyright Ericsson AB 2003-2011. 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 @@ -23,7 +23,7 @@ %% 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. +%% called from HTML pages via erl_scheme (mod_esi). %% %% Tables %% ------ @@ -34,18 +34,21 @@ %% %% 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 +%% The table entry for a tag includes the start position 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. +%% #proc records. This is used for efficiency reasons when sorting the +%% process summary table instead of reading all processes from the +%% dump again. Note that if the dump contains more than +%% ?max_sort_process_num processes, the sort functionality is not +%% available, and the procs_summary field in the state will have the +%% value 'too_many'. +%% sorted: string(), 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'. @@ -68,26 +71,27 @@ initial_info_frame/2, toggle/2, general_info/2, - processes/2, + processes/3, proc_details/2, - ports/2, - ets_tables/2, - timers/2, - fun_table/2, - atoms/2, + port/2, + ports/3, + ets_tables/3, + internal_ets_tables/2, + timers/3, + fun_table/3, + atoms/3, dist_info/2, - loaded_modules/2, + loaded_modules/3, loaded_mod_details/2, memory/2, allocated_areas/2, allocator_info/2, hash_tables/2, index_tables/2, - sort_procs/2, + sort_procs/3, expand/2, expand_binary/2, - expand_memory/2, - next/2]). + expand_memory/2]). %% gen_server callbacks @@ -113,24 +117,49 @@ % this, it must be explicitly expanded. -define(max_display_binary_size,50). % max size of a binary that will be % directly displayed. +-define(max_sort_process_num,1000). % Max number of processes that allows + % sorting. If more than this number of + % processes exist, they will be displayed + % in the order they are found in the log. +-define(items_chunk_size,?max_sort_process_num). % Number of items per chunk + % when page of many items + % is displayed, e.g. processes, + % timers, funs... + % Must be equal to + % ?max_sort_process_num! + +%% All possible tags - use macros in order to avoid misspelling in the code +-define(allocated_areas,allocated_areas). +-define(allocator,allocator). +-define(atoms,atoms). +-define(binary,binary). +-define(debug_proc_dictionary,debug_proc_dictionary). +-define(ende,ende). +-define(erl_crash_dump,erl_crash_dump). +-define(ets,ets). +-define(fu,fu). +-define(hash_table,hash_table). +-define(hidden_node,hidden_node). +-define(index_table,index_table). +-define(instr_data,instr_data). +-define(internal_ets,internal_ets). +-define(loaded_modules,loaded_modules). +-define(memory,memory). +-define(mod,mod). +-define(no_distribution,no_distribution). +-define(node,node). +-define(not_connected,not_connected). +-define(num_atoms,num_atoms). +-define(old_instr_data,old_instr_data). +-define(port,port). +-define(proc,proc). +-define(proc_dictionary,proc_dictionary). +-define(proc_heap,proc_heap). +-define(proc_messages,proc_messages). +-define(proc_stack,proc_stack). +-define(timer,timer). +-define(visible_node,visible_node). --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}). @@ -266,22 +295,24 @@ toggle(_Env,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). +processes(SessionId,_Env,_Input) -> + call({procs_summary,SessionId}). +ports(SessionId,_Env,_Input) -> + call({ports,SessionId}). +ets_tables(SessionId,_Env,Input) -> + call({ets_tables,SessionId,Input}). +internal_ets_tables(_Env,_Input) -> + call(internal_ets_tables). +timers(SessionId,_Env,Input) -> + call({timers,SessionId,Input}). +fun_table(SessionId,_Env,_Input) -> + call({funs,SessionId}). +atoms(SessionId,_Env,_Input) -> + call({atoms,SessionId}). dist_info(_Env,_Input) -> call(dist_info). -loaded_modules(_Env,_Input) -> - call(loaded_mods). +loaded_modules(SessionId,_Env,_Input) -> + call({loaded_mods,SessionId}). loaded_mod_details(_Env,Input) -> call({loaded_mod_details,Input}). memory(_Env,_Input) -> @@ -303,8 +334,13 @@ proc_details(_Env,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}). +sort_procs(SessionId,_Env,Input) -> + call({sort_procs,SessionId,Input}). + +%%%----------------------------------------------------------------- +%%% Called when a link to a port is clicked. +port(_Env,Input) -> + call({port,Input}). %%%----------------------------------------------------------------- %%% Called when the "Expand" link in a call stack (Last Calls) is @@ -324,11 +360,6 @@ expand_memory(_Env,Input) -> 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) -> @@ -348,7 +379,7 @@ redirect(_Env,_Input) -> %%-------------------------------------------------------------------- init([]) -> ets:new(cdv_menu_table,[set,named_table,{keypos,#menu_item.index},public]), - ets:new(cdv_dump_index_table,[bag,named_table,public]), + ets:new(cdv_dump_index_table,[ordered_set,named_table,public]), {ok, #state{}}. %%-------------------------------------------------------------------- @@ -399,8 +430,17 @@ 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), + NumProcs = list_to_integer(GenInfo#general_info.num_procs), + ProcsSummary = + if NumProcs > ?max_sort_process_num -> too_many; + true -> State#state.procs_summary + end, + NewState = State#state{shared_heap=SH, + wordsize=WS, + num_atoms=NumAtoms, + procs_summary=ProcsSummary}, Reply = crashdump_viewer_html:general_info(GenInfo), - {reply,Reply,State#state{shared_heap=SH,wordsize=WS,num_atoms=NumAtoms}}; + {reply,Reply,NewState}; handle_call({toggle,Input},_From,State) -> {ok,Index} = get_value("index",httpd:parse_query(Input)), do_toggle(list_to_integer(Index)), @@ -429,7 +469,7 @@ handle_call({expand,Input},_From,State=#state{file=File}) -> 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 + case truncated_warning([{?proc,Pid}]) of [] -> Expanded = expand_memory(File,What,Pid,B), crashdump_viewer_html:expanded_memory(What,Expanded); @@ -450,149 +490,129 @@ handle_call({expand_binary,Input},_From,State=#state{file=File}) -> 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}) -> +handle_call({procs_summary,SessionId},_From,State) -> + TW = truncated_warning([?proc]), + NewState = procs_summary(SessionId,TW,"pid",State#state{sorted=undefined}), + {reply,ok,NewState}; +handle_call({sort_procs,SessionId,Input}, _From, State) -> {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}}; + TW = truncated_warning([?proc]), + NewState = procs_summary(SessionId,TW,Sort,State), + {reply,ok,NewState}; 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}]), + 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"]), + 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}) -> +handle_call({port,Input},_From,State=#state{file=File}) -> + {ok,P} = get_value("port",httpd:parse_query(Input)), + Id = [$#|P], 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) + case get_port(File,Id) of + {ok,PortInfo} -> + TW = truncated_warning([{?port,Id}]), + crashdump_viewer_html:port(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, {reply,Reply,State}; -handle_call({ets_tables,Input},_From,State=#state{file=File,wordsize=WS}) -> - {Pid,Heading,InternalEts} = +handle_call({ports,SessionId},_From,State=#state{file=File}) -> + TW = truncated_warning([?port]), + get_ports(SessionId,File,TW), + {reply,ok,State}; +handle_call({ets_tables,SessionId,Input},_From,State=#state{file=File,wordsize=WS}) -> + {Pid,Heading} = case get_value("pid",httpd:parse_query(Input)) of {ok,P} -> - {P,["ETS Tables for Process ",P],[]}; + {P,["ETS Tables for Process ",P]}; error -> - I = get_internal_ets_tables(File,WS), - {'_',"ETS Table Information",I} + {'$2',"ETS Table Information"} end, - EtsTables = get_ets_tables(File,Pid,WS), - TW = truncated_warning(["=ets"]), - Reply = crashdump_viewer_html:ets_tables(Heading,EtsTables,InternalEts,TW), + TW = truncated_warning([?ets]), + get_ets_tables(SessionId,File,Heading,TW,Pid,WS), + {reply,ok,State}; +handle_call(internal_ets_tables,_From,State=#state{file=File,wordsize=WS}) -> + InternalEts = get_internal_ets_tables(File,WS), + TW = truncated_warning([?internal_ets]), + Reply = crashdump_viewer_html:internal_ets_tables(InternalEts,TW), {reply,Reply,State}; -handle_call({timers,Input},_From,State=#state{file=File}) -> +handle_call({timers,SessionId,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"} + error -> {'$2',"Timer Information"} end, - Timers=get_timers(File,Pid), - TW = truncated_warning(["=timer"]), - Reply = crashdump_viewer_html:timers(Heading,Timers,TW), - {reply,Reply,State}; + TW = truncated_warning([?timer]), + get_timers(SessionId,File,Heading,TW,Pid), + {reply,ok,State}; handle_call(dist_info,_From,State=#state{file=File}) -> Nods=nods(File), - TW = truncated_warning(["=visible_node","=hidden_node","=not_connected"]), + 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_mods,SessionId},_From,State=#state{file=File}) -> + TW = truncated_warning([?mod]), + loaded_mods(SessionId,File,TW), + {reply,ok,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}]), + 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({funs,SessionId},_From,State=#state{file=File}) -> + TW = truncated_warning([?fu]), + funs(SessionId,File,TW), + {reply,ok,State}; +handle_call({atoms,SessionId},_From,State=#state{file=File,num_atoms=Num}) -> + TW = truncated_warning([?atoms,?num_atoms]), + atoms(SessionId,File,TW,Num), + {reply,ok,State}; handle_call(memory,_From,State=#state{file=File}) -> Memory=memory(File), - TW = truncated_warning(["=memory"]), + 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"]), + 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"]), + 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"]), + 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"]), + TW = truncated_warning([?hash_table,?index_table]), Reply = crashdump_viewer_html:index_tables(IndexTables,TW), {reply,Reply,State}. @@ -682,9 +702,9 @@ truncated_here(Tag) -> %% 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}) -> +%% 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. @@ -718,9 +738,37 @@ open(File) -> close(Fd) -> erase(chunk), file:close(Fd). + +%% Set position relative to beginning of file +%% If position is within the already read Chunk, then adjust 'chunk' +%% and 'pos' in process dictionary. Else set position in file. pos_bof(Fd,Pos) -> + case get(pos) of + undefined -> + hard_pos_bof(Fd,Pos); + OldPos when Pos>=OldPos -> + case get(chunk) of + undefined -> + hard_pos_bof(Fd,Pos); + Chunk -> + ChunkSize = byte_size(Chunk), + ChunkEnd = OldPos+ChunkSize, + if Pos= + Diff = Pos-OldPos, + put(pos,Pos), + put(chunk,binary:part(Chunk,Diff,ChunkEnd-Pos)); + true -> + hard_pos_bof(Fd,Pos) + end + end; + _ -> + hard_pos_bof(Fd,Pos) + end. + +hard_pos_bof(Fd,Pos) -> reset_chunk(), - file:position(Fd,{bof,Pos}). + file:position(Fd,{bof,Pos}). + get_chunk(Fd) -> case erase(chunk) of @@ -829,6 +877,10 @@ get_rest_of_line_1(Fd, <<$\n:8,Bin/binary>>, Acc) -> 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, <<$<:8,Rest/binary>>, Acc) -> +%% get_rest_of_line_1(Fd, Rest, [$;,$t,$l,$&|Acc]); +%% get_rest_of_line_1(Fd, <<$>:8,Rest/binary>>, Acc) -> +%% get_rest_of_line_1(Fd, Rest, [$;,$t,$g,$&|Acc]); get_rest_of_line_1(Fd, <>, Acc) -> get_rest_of_line_1(Fd, Rest, [Char|Acc]); get_rest_of_line_1(Fd, <<>>, Acc) -> @@ -979,7 +1031,9 @@ initial_menu() -> [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(2, "ETS tables", 0), + menu_item(0, {"./ets_tables","ETS tables"}, 1), + menu_item(0, {"./internal_ets_tables","Internal ETS tables"}, 1), menu_item(0, {"./timers","Timers"}, 0), menu_item(0, {"./fun_table","Fun table"}, 0), menu_item(0, {"./atoms","Atoms"}, 0), @@ -1066,9 +1120,9 @@ read_file(File) -> {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}), + ?erl_crash_dump -> + reset_index_table(), + insert_index(Tag,Id,N1+1), put(last_tag,{Tag,""}), Status = background_status(processing,File), background_status(Status), @@ -1107,34 +1161,35 @@ read_file(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). +indexify(Fd,Bin,N) -> + case binary:match(Bin,<<"\n=">>) of + {Start,Len} -> + Pos = Start+Len, + <<_:Pos/binary,TagAndRest/binary>> = Bin, + {Tag,Id,Rest,N1} = tag(Fd,TagAndRest,N+Pos), + insert_index(Tag,Id,N1+1), % +1 to get past newline + put(last_tag,{Tag,Id}), + indexify(Fd,Rest,N1); + nomatch -> + case read(Fd) of + {ok,Chunk0} when is_binary(Chunk0) -> + {Chunk,N1} = + case binary:last(Bin) of + $\n -> + {<<$\n,Chunk0/binary>>,N+size(Bin)-1}; + _ -> + {Chunk0,N+size(Bin)} + end, + indexify(Fd,Chunk,N1); + eof -> + eof + end + end. 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_to_atom(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) -> @@ -1148,12 +1203,12 @@ tag(Fd,<<>>,N,Gat,Di,Now) -> {ok,Chunk} when is_binary(Chunk) -> tag(Fd,Chunk,N,Gat,Di,Now); eof -> - {[$=|lists:reverse(Gat)],lists:reverse(Di),<<>>,N} + {tag_to_atom(lists:reverse(Gat)),lists:reverse(Di),<<>>,N} end. check_if_truncated() -> case get(last_tag) of - {"=end",_} -> + {?ende,_} -> put(truncated,false), put(truncated_proc,false); TruncatedTag -> @@ -1161,32 +1216,29 @@ check_if_truncated() -> find_truncated_proc(TruncatedTag) end. -find_truncated_proc({"=atom",_Id}) -> +find_truncated_proc({?atoms,_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. + %% 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" -> +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(), @@ -1198,8 +1250,7 @@ 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"), + [{_Id,Start}] = lookup_index(?erl_crash_dump), Fd = open(File), pos_bof(Fd,Start), Created = case get_rest_of_line(Fd) of @@ -1207,15 +1258,15 @@ general_info(File) -> WholeLine -> WholeLine end, - GI0 = get_general_info(Fd,#general_info{created=Created,_=?space}), + GI0 = get_general_info(Fd,#general_info{created=Created}), 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}] -> + case lookup_index(?memory) of + [{_,MemStart}] -> pos_bof(Fd,MemStart), Memory = get_meminfo(Fd,[]), Tot = case lists:keysearch("total",1,Memory) of @@ -1232,33 +1283,34 @@ general_info(File) -> end, close(Fd), - {NumProcs,NumEts,NumFuns} = count(), + {NumProcs,NumEts,NumFuns,NumTimers} = count(), NodeName = - case ets:lookup(cdv_dump_index_table,"=node") of - [{"=node",N,_Start}] -> + case lookup_index(?node) of + [{N,_Start}] -> N; [] -> - case ets:lookup(cdv_dump_index_table,"=no_distribution") of + case lookup_index(?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 + case lookup_index(?old_instr_data) of + [] -> + case lookup_index(?instr_data) of + [] -> + false; + _ -> + instr_data + end; + _ -> + old_instr_data end, GI#general_info{node_name=NodeName, num_procs=integer_to_list(NumProcs), num_ets=integer_to_list(NumEts), + num_timers=integer_to_list(NumTimers), num_fun=integer_to_list(NumFuns), mem_tot=MemTot, mem_max=MemMax, @@ -1285,8 +1337,8 @@ get_general_info(Fd,GenInfo) -> end. get_num_atoms(Fd) -> - case ets:match(cdv_dump_index_table,{"=hash_table","atom_tab",'$1'}) of - [[Pos]] -> + case lookup_index(?hash_table,"atom_tab") of + [{_,Pos}] -> pos_bof(Fd,Pos), skip_rest_of_line(Fd), % size skip_rest_of_line(Fd), % used @@ -1300,10 +1352,10 @@ get_num_atoms(Fd) -> get_num_atoms2() end. get_num_atoms2() -> - case ets:lookup(cdv_dump_index_table,"=num_atoms") of + case lookup_index(?num_atoms) of [] -> ?space; - [{"=num_atoms",NA,_Pos}] -> + [{NA,_Pos}] -> %% If dump is translated this will exist case get(truncated) of true -> @@ -1314,43 +1366,70 @@ get_num_atoms2() -> 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_index(?proc),count_index(?ets),count_index(?fu),count_index(?timer)}. -count_ms(Tag) -> - [{{Tag,'_','_'},[],[true]}]. +%%----------------------------------------------------------------- +%% Page with all processes +%% +%% If there are less than ?max_sort_process_num processes in the dump, +%% we will store the list of processes in the server state in order to +%% allow sorting according to the different columns of the +%% table. Since ?max_sort_process_num=:=?items_chunk_size, there will +%% never be more than one chunk in this case. +%% +%% If there are more than ?max_sort_process_num processes in the dump, +%% no sorting will be allowed, and the processes must be read (chunk +%% by chunk) from the file each time the page is opened. This is to +%% avoid really big data in the server state. +procs_summary(SessionId,TW,_,State=#state{procs_summary=too_many}) -> + chunk_page(SessionId,State#state.file,TW,?proc,processes, + {no_sort,State#state.shared_heap},procs_summary_parsefun()), + State; +procs_summary(SessionId,TW,SortOn,State) -> + ProcsSummary = + case State#state.procs_summary of + undefined -> % first time - read from file + Fd = open(State#state.file), + {PS,_}=lookup_and_parse_index_chunk(first_chunk_pointer(?proc), + Fd,procs_summary_parsefun()), + close(Fd), + PS; + PS -> + PS + end, + {SortedPS,NewSorted} = do_sort_procs(SortOn,ProcsSummary,State#state.sorted), + HtmlInfo = + crashdump_viewer_html:chunk_page(processes,SessionId,TW, + {SortOn,State#state.shared_heap}, + SortedPS), + crashdump_viewer_html:chunk(SessionId,done,HtmlInfo), + State#state{procs_summary=ProcsSummary,sorted=NewSorted}. -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. +procs_summary_parsefun() -> + fun(Fd,Pid) -> + get_procinfo(Fd,fun main_procinfo/4,#proc{pid=Pid}) + end. +%%----------------------------------------------------------------- +%% Page with one process 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]] -> + [{DumpVsn,_}] = lookup_index(?erl_crash_dump), + case lookup_index(?proc,Pid) of + [{_,Start}] -> Fd = open(File), pos_bof(Fd,Start), Proc0 = case DumpVsn of "0.0" -> %% Old version (translated) - ?initial_proc_record(Pid); + #proc{pid=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)} + #proc{pid=Pid, + 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), @@ -1368,11 +1447,11 @@ get_proc_details(File,Pid) -> end. if_exist(Tag,Key) -> - case ets:select_count(cdv_dump_index_table,[{{Tag,Key,'_'},[],[true]}]) of + case count_index(Tag,Key) of 0 -> Tag1 = case is_proc_tag(Tag) of - true -> "=proc"; + true -> ?proc; false -> Tag end, case truncated_here({Tag1,Key}) of @@ -1523,13 +1602,14 @@ maybe_other_node(File,Id) -> N end, Ms = ets:fun2ms( - fun({Tag,Id,Start}) when Tag=:="=visible_node", Id=:=Channel -> + fun({{Tag,Start},Ch}) when Tag=:=?visible_node, Ch=:=Channel -> {"Visible Node",Start}; - ({Tag,Id,Start}) when Tag=:="=hidden_node", Id=:=Channel -> + ({{Tag,Start},Ch}) when Tag=:=?hidden_node, Ch=:=Channel -> {"Hidden Node",Start}; - ({Tag,Id,Start}) when Tag=:="=not_connected", Id=:=Channel -> + ({{Tag,Start},Ch}) when Tag=:=?not_connected, Ch=:=Channel -> {"Not Connected Node",Start} end), + case ets:select(cdv_dump_index_table,Ms) of [] -> not_found; @@ -1540,6 +1620,7 @@ maybe_other_node(File,Id) -> {other_node,Type,NodeInfo} end. + expand_memory(File,What,Pid,Binaries) -> Fd = open(File), put(fd,Fd), @@ -1548,8 +1629,8 @@ expand_memory(File,What,Pid,Binaries) -> 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) + "Dictionary" -> read_dictionary(Fd,?proc_dictionary,Pid,Dict); + "DebugDictionary" -> read_dictionary(Fd,?debug_proc_dictionary,Pid,Dict) end, erase(fd), close(Fd), @@ -1559,10 +1640,10 @@ expand_memory(File,What,Pid,Binaries) -> %%% Read binaries. %%% read_binaries(Fd) -> - AllBinaries = ets:match(cdv_dump_index_table,{"=binary",'$1','$2'}), + AllBinaries = lookup_index(?binary), read_binaries(Fd,AllBinaries, gb_trees:empty()). -read_binaries(Fd,[[Addr0,Pos]|Bins],Dict0) -> +read_binaries(Fd,[{Addr0,Pos}|Bins],Dict0) -> pos_bof(Fd,Pos), {Addr,_} = get_hex(Addr0), Dict = @@ -1603,15 +1684,15 @@ parse_binary(Addr, Line0, Dict) -> %%% read_stack_dump(Fd,Pid,Dict) -> - case ets:match(cdv_dump_index_table,{"=proc_stack",Pid,'$1'}) of - [[Start]] -> + case lookup_index(?proc_stack,Pid) 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" + %% This function is never called if the dump is truncated in {?proc_heap,Pid} case val(Fd) of "=" ++ _next_tag -> lists:reverse(Acc); @@ -1631,15 +1712,15 @@ parse_top(Line0, D) -> %%% read_messages(Fd,Pid,Dict) -> - case ets:match(cdv_dump_index_table,{"=proc_messages",Pid,'$1'}) of - [[Start]] -> + case lookup_index(?proc_messages,Pid) 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" + %% This function is never called if the dump is truncated in {?proc_heap,Pid} case val(Fd) of "=" ++ _next_tag -> lists:reverse(Acc); @@ -1659,15 +1740,15 @@ parse_message(Line0, D) -> %%% read_dictionary(Fd,Tag,Pid,Dict) -> - case ets:match(cdv_dump_index_table,{Tag,Pid,'$1'}) of - [[Start]] -> + case lookup_index(Tag,Pid) 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" + %% This function is never called if the dump is truncated in {?proc_heap,Pid} case val(Fd) of "=" ++ _next_tag -> lists:reverse(Acc); @@ -1686,8 +1767,8 @@ parse_dictionary(Line0, D) -> %%% read_heap(Fd,Pid,Dict0) -> - case ets:match(cdv_dump_index_table,{"=proc_heap",Pid,'$2'}) of - [[Pos]] -> + case lookup_index(?proc_heap,Pid) of + [{_,Pos}] -> pos_bof(Fd,Pos), read_heap(Dict0); [] -> @@ -1695,7 +1776,7 @@ read_heap(Fd,Pid,Dict0) -> end. read_heap(Dict0) -> - %% This function is never called if the dump is truncated in "=proc_heap:Pid" + %% This function is never called if the dump is truncated in {?proc_heap,Pid} case get(fd) of end_of_heap -> Dict0; @@ -1761,12 +1842,14 @@ do_sort_procs("name",Procs,Sorted) -> _ -> {Result,"name"} end. - +%%----------------------------------------------------------------- +%% Page with one port get_port(File,Port) -> - case ets:match(cdv_dump_index_table,{"=port",Port,'$1'}) of - [[Start]] -> + case lookup_index(?port,Port) of + [{_,Start}] -> Fd = open(File), - R = get_portinfo(Fd,Port,Start), + pos_bof(Fd,Start), + R = get_portinfo(Fd,#port{id=Port}), close(Fd), {ok,R}; [] -> @@ -1781,18 +1864,11 @@ get_port(File,Port) -> 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}). +%%----------------------------------------------------------------- +%% Page with all ports +get_ports(SessionId,File,TW) -> + ParseFun = fun(Fd,Id) -> get_portinfo(Fd,#port{id=Id}) end, + chunk_page(SessionId,File,TW,?port,ports,[],ParseFun). get_portinfo(Fd,Port) -> case line_head(Fd) of @@ -1802,6 +1878,10 @@ get_portinfo(Fd,Port) -> get_portinfo(Fd,Port#port{connected=val(Fd)}); "Links" -> get_portinfo(Fd,Port#port{links=val(Fd)}); + "Registered as" -> + get_portinfo(Fd,Port#port{name=val(Fd)}); + "Monitors" -> + get_portinfo(Fd,Port#port{monitors=val(Fd)}); "Port controls linked-in driver" -> get_portinfo(Fd,Port#port{controls=["Linked in driver: " | val(Fd)]}); @@ -1820,30 +1900,12 @@ get_portinfo(Fd,Port) -> 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). +%%----------------------------------------------------------------- +%% Page with external ets tables +get_ets_tables(SessionId,File,Heading,TW,Pid,WS) -> + ParseFun = fun(Fd,Id) -> get_etsinfo(Fd,#ets_table{pid=Id},WS) end, + chunk_page(SessionId,File,TW,{?ets,Pid},ets_tables,Heading,ParseFun). get_etsinfo(Fd,EtsTable,WS) -> case line_head(Fd) of @@ -1875,26 +1937,32 @@ get_etsinfo(Fd,EtsTable,WS) -> EtsTable end. -get_timers(File,Pid) -> - Timers = ets:match_object(cdv_dump_index_table,{"=timer",Pid,'$1'}), + +%% Internal ets table page +get_internal_ets_tables(File,WS) -> + InternalEts = lookup_index(?internal_ets), Fd = open(File), - R = lists:map(fun({"=timer",P,Start}) -> - get_timerinfo(Fd,P,Start) - end, - Timers), + R = lists:map( + fun({Descr,Start}) -> + pos_bof(Fd,Start), + {Descr,get_etsinfo(Fd,#ets_table{},WS)} + end, + InternalEts), close(Fd), R. -get_timerinfo(Fd,Pid,Start) -> - pos_bof(Fd,Start), - get_timerinfo(Fd,#timer{pid=Pid,_=?space}). +%%----------------------------------------------------------------- +%% Page with list of all timers +get_timers(SessionId,File,Heading,TW,Pid) -> + ParseFun = fun(Fd,Id) -> get_timerinfo_1(Fd,#timer{pid=Id}) end, + chunk_page(SessionId,File,TW,{?timer,Pid},timers,Heading,ParseFun). -get_timerinfo(Fd,Timer) -> +get_timerinfo_1(Fd,Timer) -> case line_head(Fd) of "Message" -> - get_timerinfo(Fd,Timer#timer{msg=val(Fd)}); + get_timerinfo_1(Fd,Timer#timer{msg=val(Fd)}); "Time left" -> - get_timerinfo(Fd,Timer#timer{time=val(Fd)}); + get_timerinfo_1(Fd,Timer#timer{time=val(Fd)}); "=" ++ _next_tag -> Timer; Other -> @@ -1902,25 +1970,27 @@ get_timerinfo(Fd,Timer) -> Timer end. +%%----------------------------------------------------------------- +%% Page with information about the erlang distribution nods(File) -> - case ets:lookup(cdv_dump_index_table,"=no_distribution") of + case lookup_index(?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"), + V = lookup_index(?visible_node), + H = lookup_index(?hidden_node), + N = lookup_index(?not_connected), Fd = open(File), Visible = lists:map( - fun({"=visible_node",Channel,Start}) -> + fun({Channel,Start}) -> get_nodeinfo(Fd,Channel,Start) end, V), Hidden = lists:map( - fun({"=hidden_node",Channel,Start}) -> + fun({Channel,Start}) -> get_nodeinfo(Fd,Channel,Start) end, H), NotConnected = lists:map( - fun({"=not_connected",Channel,Start}) -> + fun({Channel,Start}) -> get_nodeinfo(Fd,Channel,Start) end, N), @@ -1932,7 +2002,7 @@ nods(File) -> get_nodeinfo(Fd,Channel,Start) -> pos_bof(Fd,Start), - get_nodeinfo(Fd,#nod{channel=Channel,_=?space}). + get_nodeinfo(Fd,#nod{channel=Channel}). get_nodeinfo(Fd,Nod) -> case line_head(Fd) of @@ -1963,26 +2033,37 @@ get_nodeinfo(Fd,Nod) -> 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. +%%----------------------------------------------------------------- +%% Page with details about one loaded modules +get_loaded_mod_details(File,Mod) -> + [{_,Start}] = lookup_index(?mod,Mod), + Fd = open(File), + pos_bof(Fd,Start), + InitLM = #loaded_mod{mod=Mod,old_size="No old code exists"}, + ModInfo = get_loaded_mod_info(Fd,InitLM,fun all_modinfo/3), + close(Fd), + ModInfo. + +%%----------------------------------------------------------------- +%% Page with list of all loaded modules +loaded_mods(SessionId,File,TW) -> + ParseFun = + fun(Fd,Id) -> + get_loaded_mod_info(Fd,#loaded_mod{mod=Id}, + fun main_modinfo/3) + end, + {CC,OC} = + case lookup_index(?loaded_modules) of + [{_,StartTotal}] -> + Fd = open(File), + pos_bof(Fd,StartTotal), + R = get_loaded_mod_totals(Fd,{"unknown","unknown"}), + close(Fd), + R; + [] -> + {"unknown","unknown"} + end, + chunk_page(SessionId,File,TW,?mod,loaded_mods,{CC,OC},ParseFun). get_loaded_mod_totals(Fd,{CC,OC}) -> case line_head(Fd) of @@ -1997,16 +2078,6 @@ get_loaded_mod_totals(Fd,{CC,OC}) -> {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" -> @@ -2073,39 +2144,26 @@ hex_to_dec("A") -> 10; hex_to_dec(N) -> list_to_integer(N). +%%----------------------------------------------------------------- +%% Page with list of all funs +funs(SessionId,File,TW) -> + ParseFun = fun(Fd,_Id) -> get_funinfo(Fd,#fu{}) end, + chunk_page(SessionId,File,TW,?fu,funs,[],ParseFun). -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) -> +get_funinfo(Fd,Fu) -> case line_head(Fd) of "Module" -> - get_funinfo1(Fd,Fu#fu{module=val(Fd)}); + get_funinfo(Fd,Fu#fu{module=val(Fd)}); "Uniq" -> - get_funinfo1(Fd,Fu#fu{uniq=val(Fd)}); + get_funinfo(Fd,Fu#fu{uniq=val(Fd)}); "Index" -> - get_funinfo1(Fd,Fu#fu{index=val(Fd)}); + get_funinfo(Fd,Fu#fu{index=val(Fd)}); "Address" -> - get_funinfo1(Fd,Fu#fu{address=val(Fd)}); + get_funinfo(Fd,Fu#fu{address=val(Fd)}); "Native_address" -> - get_funinfo1(Fd,Fu#fu{native_address=val(Fd)}); + get_funinfo(Fd,Fu#fu{native_address=val(Fd)}); "Refc" -> - get_funinfo1(Fd,Fu#fu{refc=val(Fd)}); + get_funinfo(Fd,Fu#fu{refc=val(Fd)}); "=" ++ _next_tag -> Fu; Other -> @@ -2113,28 +2171,44 @@ get_funinfo1(Fd,Fu) -> Fu end. -atoms(File) -> - case ets:lookup(cdv_dump_index_table,"=atoms") of - [{_atoms,_Id,Start}] -> +%%----------------------------------------------------------------- +%% Page with list of all atoms +atoms(SessionId,File,TW,Num) -> + case lookup_index(?atoms) of + [{_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; + {Atoms,Cont} = get_atoms(Fd,1000), + crashdump_viewer_html:atoms(SessionId,TW,Num,Atoms), + atoms_chunks(Fd,SessionId,Cont); _ -> - [] + crashdump_viewer_html:atoms(SessionId,TW,Num,done) end. +get_atoms(Fd,Number) -> + case get_n_lines_of_tag(Fd,Number) of + {all,_,Lines} -> + {Lines,done}; + {part,_,Lines} -> + {Lines,Number}; + empty -> + {[],done} + end. + +atoms_chunks(Fd,SessionId,done) -> + close(Fd), + crashdump_viewer_html:atoms_chunk(SessionId,done); +atoms_chunks(Fd,SessionId,Number) -> + {Atoms,Cont} = get_atoms(Fd,Number), + crashdump_viewer_html:atoms_chunk(SessionId,Atoms), + atoms_chunks(Fd,SessionId,Cont). + + +%%----------------------------------------------------------------- +%% Page with memory information memory(File) -> - case ets:lookup(cdv_dump_index_table,"=memory") of - [{"=memory",_,Start}] -> + case lookup_index(?memory) of + [{_,Start}] -> Fd = open(File), pos_bof(Fd,Start), R = get_meminfo(Fd,[]), @@ -2153,10 +2227,12 @@ get_meminfo(Fd,Acc) -> Key -> get_meminfo(Fd,[{Key,val(Fd)}|Acc]) end. - + +%%----------------------------------------------------------------- +%% Page with information about allocated areas allocated_areas(File) -> - case ets:lookup(cdv_dump_index_table,"=allocated_areas") of - [{"=allocated_areas",_,Start}] -> + case lookup_index(?allocated_areas) of + [{_,Start}] -> Fd = open(File), pos_bof(Fd,Start), R = get_allocareainfo(Fd,[]), @@ -2183,14 +2259,16 @@ get_allocareainfo(Fd,Acc) -> end, get_allocareainfo(Fd,[AllocInfo|Acc]) end. - + +%%----------------------------------------------------------------- +%% Page with information about allocators allocator_info(File) -> - case ets:lookup(cdv_dump_index_table,"=allocator") of + case lookup_index(?allocator) of [] -> []; AllAllocators -> Fd = open(File), - R = lists:map(fun({"=allocator",Heading,Start}) -> + R = lists:map(fun({Heading,Start}) -> {Heading,get_allocatorinfo(Fd,Start)} end, AllAllocators), @@ -2220,14 +2298,15 @@ get_all_vals([],Acc) -> get_all_vals([Char|Rest],Acc) -> get_all_vals(Rest,[Char|Acc]). - +%%----------------------------------------------------------------- +%% Page with hash table information hash_tables(File) -> - case ets:lookup(cdv_dump_index_table,"=hash_table") of + case lookup_index(?hash_table) of [] -> []; AllHashTables -> Fd = open(File), - R = lists:map(fun({"=hash_table",Name,Start}) -> + R = lists:map(fun({Name,Start}) -> get_hashtableinfo(Fd,Name,Start) end, AllHashTables), @@ -2237,7 +2316,7 @@ hash_tables(File) -> get_hashtableinfo(Fd,Name,Start) -> pos_bof(Fd,Start), - get_hashtableinfo1(Fd,#hash_table{name=Name,_=?space}). + get_hashtableinfo1(Fd,#hash_table{name=Name}). get_hashtableinfo1(Fd,HashTable) -> case line_head(Fd) of @@ -2256,13 +2335,15 @@ get_hashtableinfo1(Fd,HashTable) -> HashTable end. +%%----------------------------------------------------------------- +%% Page with index table information index_tables(File) -> - case ets:lookup(cdv_dump_index_table,"=index_table") of + case lookup_index(?index_table) of [] -> []; AllIndexTables -> Fd = open(File), - R = lists:map(fun({"=index_table",Name,Start}) -> + R = lists:map(fun({Name,Start}) -> get_indextableinfo(Fd,Name,Start) end, AllIndexTables), @@ -2272,7 +2353,7 @@ index_tables(File) -> get_indextableinfo(Fd,Name,Start) -> pos_bof(Fd,Start), - get_indextableinfo1(Fd,#index_table{name=Name,_=?space}). + get_indextableinfo1(Fd,#index_table{name=Name}). get_indextableinfo1(Fd,IndexTable) -> case line_head(Fd) of @@ -2284,6 +2365,8 @@ get_indextableinfo1(Fd,IndexTable) -> get_indextableinfo1(Fd,IndexTable#index_table{limit=val(Fd)}); "rate" -> get_indextableinfo1(Fd,IndexTable#index_table{rate=val(Fd)}); + "entries" -> + get_indextableinfo1(Fd,IndexTable#index_table{entries=val(Fd)}); "=" ++ _next_tag -> IndexTable; Other -> @@ -2295,6 +2378,8 @@ get_indextableinfo1(Fd,IndexTable) -> +%%----------------------------------------------------------------- +%% Expand a set of data which was shown in a truncated form on get_expanded(File,Pos,Size) -> Fd = open(File), R = case file:pread(Fd,Pos,Size) of @@ -2307,20 +2392,6 @@ get_expanded(File,Pos,Size) -> 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) -> @@ -2567,3 +2638,110 @@ get_binary(_N, [], _Acc) -> cdvbin(Sz,Pos) -> "#CDVBin<"++integer_to_list(Sz)++","++integer_to_list(Pos)++">". + + +%%----------------------------------------------------------------- +%% Functions for accessing the cdv_dump_index_table +reset_index_table() -> + ets:delete_all_objects(cdv_dump_index_table). + +insert_index(Tag,Id,Pos) -> + ets:insert(cdv_dump_index_table,{{Tag,Pos},Id}). + +lookup_index(Tag) -> + lookup_index(Tag,'$2'). +lookup_index(Tag,Id) -> + ets:select(cdv_dump_index_table,[{{{Tag,'$1'},Id},[],[{{Id,'$1'}}]}]). + +lookup_index_chunk({'#CDVFirstChunk',Tag,Id}) -> + ets:select(cdv_dump_index_table, + [{{{Tag,'$1'},Id},[],[{{Id,'$1'}}]}], + ?items_chunk_size); +lookup_index_chunk(Cont) -> + ets:select(Cont). + +%% Create a tag which can be used instead of an ets Continuation for +%% the first call to lookup_index_chunk. +first_chunk_pointer({Tag,Id}) -> + {'#CDVFirstChunk',Tag,Id}; +first_chunk_pointer(Tag) -> + first_chunk_pointer({Tag,'$2'}). + +count_index(Tag) -> + ets:select_count(cdv_dump_index_table,[{{{Tag,'_'},'_'},[],[true]}]). +count_index(Tag,Id) -> + ets:select_count(cdv_dump_index_table,[{{{Tag,'_'},Id},[],[true]}]). + + +%%----------------------------------------------------------------- +%% Convert tags read from crashdump to atoms used as first part of key +%% in cdv_dump_index_table +tag_to_atom("allocated_areas") -> ?allocated_areas; +tag_to_atom("allocator") -> ?allocator; +tag_to_atom("atoms") -> ?atoms; +tag_to_atom("binary") -> ?binary; +tag_to_atom("debug_proc_dictionary") -> ?debug_proc_dictionary; +tag_to_atom("end") -> ?ende; +tag_to_atom("erl_crash_dump") -> ?erl_crash_dump; +tag_to_atom("ets") -> ?ets; +tag_to_atom("fun") -> ?fu; +tag_to_atom("hash_table") -> ?hash_table; +tag_to_atom("hidden_node") -> ?hidden_node; +tag_to_atom("index_table") -> ?index_table; +tag_to_atom("instr_data") -> ?instr_data; +tag_to_atom("internal_ets") -> ?internal_ets; +tag_to_atom("loaded_modules") -> ?loaded_modules; +tag_to_atom("memory") -> ?memory; +tag_to_atom("mod") -> ?mod; +tag_to_atom("no_distribution") -> ?no_distribution; +tag_to_atom("node") -> ?node; +tag_to_atom("not_connected") -> ?not_connected; +tag_to_atom("num_atoms") -> ?num_atoms; +tag_to_atom("old_instr_data") -> ?old_instr_data; +tag_to_atom("port") -> ?port; +tag_to_atom("proc") -> ?proc; +tag_to_atom("proc_dictionary") -> ?proc_dictionary; +tag_to_atom("proc_heap") -> ?proc_heap; +tag_to_atom("proc_messages") -> ?proc_messages; +tag_to_atom("proc_stack") -> ?proc_stack; +tag_to_atom("timer") -> ?timer; +tag_to_atom("visible_node") -> ?visible_node; +tag_to_atom(UnknownTag) -> + io:format("WARNING: Found unexpected tag:~s~n",[UnknownTag]), + list_to_atom(UnknownTag). + +%%%----------------------------------------------------------------- +%%% Create a page by sending chunk by chunk to crashdump_viewer_html +chunk_page(SessionId,File,TW,What,HtmlCB,HtmlExtra,ParseFun) -> + Fd = open(File), + case lookup_and_parse_index_chunk(first_chunk_pointer(What),Fd,ParseFun) of + done -> + crashdump_viewer_html:chunk_page(HtmlCB,SessionId,TW,HtmlExtra,done); + {Chunk,Cont} -> + HtmlInfo = crashdump_viewer_html:chunk_page( + HtmlCB, + SessionId,TW,HtmlExtra,Chunk), + chunk_page_1(Fd,HtmlInfo,SessionId,ParseFun, + lookup_and_parse_index_chunk(Cont,Fd,ParseFun)) + end. + +chunk_page_1(Fd,HtmlInfo,SessionId,_ParseFun,done) -> + crashdump_viewer_html:chunk(SessionId,done,HtmlInfo), + close(Fd); +chunk_page_1(Fd,HtmlInfo,SessionId,ParseFun,{Chunk,Cont}) -> + crashdump_viewer_html:chunk(SessionId,Chunk,HtmlInfo), + chunk_page_1(Fd,HtmlInfo,SessionId,ParseFun, + lookup_and_parse_index_chunk(Cont,Fd,ParseFun)). + +lookup_and_parse_index_chunk(Pointer,Fd,ParseFun) -> + case lookup_index_chunk(Pointer) of + '$end_of_table' -> + done; + {Chunk,Cont} -> + R = lists:map(fun({Id,Start}) -> + pos_bof(Fd,Start), + ParseFun(Fd,Id) + end, + Chunk), + {R,Cont} + end. diff --git a/lib/observer/src/crashdump_viewer.hrl b/lib/observer/src/crashdump_viewer.hrl index 6ce727cd3e..466f33b63b 100644 --- a/lib/observer/src/crashdump_viewer.hrl +++ b/lib/observer/src/crashdump_viewer.hrl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2003-2009. All Rights Reserved. +%% Copyright Ericsson AB 2003-2011. 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 @@ -17,117 +17,136 @@ %% %CopyrightEnd% %% -define(space, " "). +-define(unknown, "unknown"). -record(menu_item,{index,picture,text,depth,children,state,target}). -record(general_info, {created, - slogan, - system_vsn, - compile_time, - taints, - node_name, - num_atoms, - num_procs, - num_ets, - num_fun, - mem_tot, - mem_max, - instr_info}). + slogan=?space, + system_vsn=?space, + compile_time=?space, + taints=?space, + node_name=?space, + num_atoms=?space, + num_procs=?space, + num_ets=?space, + num_timers=?space, + num_fun=?space, + mem_tot=?space, + mem_max=?space, + instr_info=?space}). -record(proc, + %% Initial data according to the follwoing: + %% + %% msg_q_len, reds and stack_heap are integers because it must + %% be possible to sort on them. All other fields are strings + %% + %% for old dumps start_time, parent and number of heap frament + %% does not exist + %% + %% current_func can be both "current function" and + %% "last scheduled in for" + %% + %% stack_dump, message queue and dictionaries should only be + %% displayed as a link to "Expand" (if dump is from OTP R9B + %% or newer) {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}). + name=?space, + init_func=?space, + parent=?unknown, + start_time=?unknown, + state=?space, + current_func={"Current Function",?space}, + msg_q_len=0, + msg_q=?space, + last_calls=?space, + links=?space, + prog_count=?space, + cp=?space, + arity=?space, + dict=?space, + debug_dict=?space, + reds=0, + num_heap_frag=?unknown, + heap_frag_data=?space, + stack_heap=0, + old_heap=?space, + heap_unused=?space, + old_heap_unused=?space, + new_heap_start=?space, + new_heap_top=?space, + stack_top=?space, + stack_end=?space, + old_heap_start=?space, + old_heap_top=?space, + old_heap_end=?space, + stack_dump=?space}). -record(port, {id, - slot, - connected, - links, - controls}). + slot=?space, + connected=?space, + links=?space, + name=?space, + monitors=?space, + controls=?space}). -record(ets_table, {pid, - slot, - id, - name, - type, - buckets, - size, - memory}). + slot=?space, + id=?space, + name=?space, + type="hash", + buckets=?space, + size=?space, + memory=?space}). -record(timer, {pid, - msg, - time}). + msg=?space, + time=?space}). -record(fu, - {module, - uniq, - index, - address, - native_address, - refc}). + {module=?space, + uniq=?space, + index=?space, + address=?space, + native_address=?space, + refc=?space}). -record(nod, - {name, + {name=?space, channel, - controller, - creation, - remote_links, - remote_mon, - remote_mon_by, - error}). + controller=?space, + creation=?space, + remote_links=?space, + remote_mon=?space, + remote_mon_by=?space, + error=?space}). -record(loaded_mod, {mod, - current_size, - current_attrib, - current_comp_info, - old_size, - old_attrib, - old_comp_info}). + current_size=?space, + current_attrib=?space, + current_comp_info=?space, + old_size=?space, + old_attrib=?space, + old_comp_info=?space}). -record(hash_table, {name, - size, - used, - objs, - depth}). + size=?space, + used=?space, + objs=?space, + depth=?space}). -record(index_table, {name, - size, - used, - limit, - rate}). + size=?space, + used=?space, + limit=?space, + rate=?space, + entries=?space}). diff --git a/lib/observer/src/crashdump_viewer_html.erl b/lib/observer/src/crashdump_viewer_html.erl index 5e7bbf62a0..0d70c9b86f 100644 --- a/lib/observer/src/crashdump_viewer_html.erl +++ b/lib/observer/src/crashdump_viewer_html.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2003-2009. All Rights Reserved. +%% Copyright Ericsson AB 2003-2011. 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 @@ -32,25 +32,23 @@ 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, + port/3, + internal_ets_tables/2, nods/2, - loaded_mods/2, loaded_mod_details/2, - funs/2, - atoms/3, + atoms/4, + atoms_chunk/2, memory/2, allocated_areas/2, allocator_info/2, hash_tables/2, index_tables/2, - error/2]). + error/2, + chunk_page/5, + chunk/3]). -include("crashdump_viewer.hrl"). @@ -235,6 +233,8 @@ general_info_body(Heading,GenInfo) -> 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\"","Timers"), + td(GenInfo#general_info.num_timers)]), tr([th("ALIGN=left BGCOLOR=\"#8899AA\"","Funs"), td(GenInfo#general_info.num_fun)])]), case GenInfo#general_info.instr_info of @@ -294,60 +294,6 @@ 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) -> @@ -594,80 +540,30 @@ expanded_binary_body(Heading,Bin) -> href("javascript:history.go(-1)","BACK")]. %%%----------------------------------------------------------------- -%%% Print table of ports -ports(Heading,Ports,TW) -> - header(Heading,body(ports_body(Heading,Ports,TW))). +%%% Print info for one port +port(Heading,Port,TW) -> + header(Heading,body(port_body(Heading,Port,TW))). -ports_body(Heading,[],TW) -> - [h1(Heading), - warn(TW), - "No ports were found\n"]; -ports_body(Heading,Ports,TW) -> +port_body(Heading,Port,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)])]. + [tr([th(Head) || Head <- port_table_head()]), ports_table(Port)])]. -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))). +%%% Print table of internal ETS tables +internal_ets_tables(InternalEts,TW) -> + Heading = "Internal ETS tables", + header(Heading,body(internal_ets_tables_body(Heading,InternalEts,TW))). -ets_tables_body(Heading,[],InternalEts,TW) -> +internal_ets_tables_body(Heading,[],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"), + "No internal ETS tables were found\n"]; +internal_ets_tables_body(Heading,InternalEts,TW) -> + [heading(Heading,"internal_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( @@ -681,7 +577,7 @@ internal_ets_tables_table(InternalEtsTables) -> lists:map(fun(InternalEtsTable) -> internal_ets_tables_table1(InternalEtsTable) end, - InternalEtsTables)])]. + InternalEts)])]. internal_ets_tables_table1({Descr,InternalEtsTable}) -> #ets_table{id=Id,name=Name,type=Type,buckets=Buckets, @@ -695,33 +591,6 @@ internal_ets_tables_table1({Descr,InternalEtsTable}) -> 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) -> @@ -826,33 +695,6 @@ format_extra_info(Error) -> ?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 @@ -881,108 +723,33 @@ loaded_mod_details_body(ModInfo,TW) -> 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) -> +atoms(SessionId,TW,Num,FirstChunk) -> 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"). + case FirstChunk of + done -> + deliver_first(SessionId,[h1(Heading), + warn(TW), + "No atoms were found in log",br(), + "Total number of atoms in node was ", Num, + br()]); + _ -> + deliver_first(SessionId,[start_html_page(Heading), + heading(Heading,"atoms"), + warn(TW), + "Total number of atoms in node was ", Num, + br(), + "The last created atom is shown first", + br(), + start_pre()]), + atoms_chunk(SessionId,FirstChunk) + end. + +atoms_chunk(SessionId,done) -> + deliver(SessionId,[stop_pre(),stop_html_page()]); +atoms_chunk(SessionId,Atoms) -> + deliver(SessionId,Atoms). %%%----------------------------------------------------------------- %%% Print memory information @@ -1120,52 +887,93 @@ index_tables_body(Heading,IndexTables,TW) -> th("Size"), th("Limit"), th("Used"), - th("Rate")]) | + th("Rate"), + th("Entries")]) | 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, + #index_table{name=Name,size=Size,limit=Limit,used=Used, + rate=Rate,entries=Entries} = IndexTable, tr( [td(Name), td("ALIGN=right",Size), td("ALIGN=right",Limit), td("ALIGN=right",Used), - td("ALIGN=right",Rate)]). + td("ALIGN=right",Rate), + td("ALIGN=right",Entries)]). %%%----------------------------------------------------------------- %%% Internal library +start_html_page(Title) -> + [only_http_header(), + start_html(), + only_html_header(Title), + start_html_body()]. + +stop_html_page() -> + [stop_html_body(), + stop_html()]. + +only_http_header() -> + ["Pragma:no-cache\r\n", + "Content-type: text/html\r\n\r\n"]. + +only_html_header(Title) -> + only_html_header(Title,""). +only_html_header(Title,JavaScript) -> + ["\n", + "", Title, "\n", + JavaScript, + "\n"]. + +start_html() -> + "\n". +stop_html() -> + "". +start_html_body() -> + "\n". +stop_html_body() -> + "\n". + 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", + [only_http_header(), html_header(Title,JavaScript,Body)]. html_header(Title,JavaScript,Body) -> - ["\n", - "\n", - "", Title, "\n", - JavaScript, - "\n", + [start_html(), + only_html_header(Title,JavaScript), Body, - ""]. + stop_html()]. body(Text) -> - ["\n", + [start_html_body(), Text, - "<\BODY>\n"]. + stop_html_body()]. frameset(Args,Frames) -> ["\n", Frames, "\n\n"]. frame(Args) -> ["\n"]. +start_visible_table() -> + start_table("BORDER=\"4\" CELLPADDING=\"4\" WIDTH=\"100%\""). +% start_table("BORDER=\"4\" CELLPADDING=\"4\""). +start_visible_table(ColTitles) -> + [start_visible_table(), + tr([th(ColTitle) || ColTitle <- ColTitles])]. + +start_table(Args) -> + ["\n"]. +stop_table() -> + "
\n". + table(Args,Text) -> - ["\n", Text, "\n
\n"]. + [start_table(Args), Text, stop_table()]. tr(Text) -> ["\n", Text, "\n\n"]. tr(Args,Text) -> @@ -1183,8 +991,12 @@ b(Text) -> ["",Text,""]. em(Text) -> ["",Text,"\n"]. +start_pre() -> + "
".
+stop_pre() ->
+    "
". pre(Text) -> - ["
",Text,"
"]. + [start_pre(),Text,stop_pre()]. href(Link,Text) -> ["",Text,""]. href(Args,Link,Text) -> @@ -1199,8 +1011,6 @@ input(Args) -> ["\n"]. h1(Text) -> ["

",Text,"

\n"]. -h2(Text) -> - ["

",Text,"

\n"]. font(Args,Text) -> ["\n",Text,"\n\n"]. p(Text) -> @@ -1223,7 +1033,7 @@ href_proc_port([$#,$F,$u,$n,$<|T],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]); + ["./port?port=",Port],HashPort)|Acc]); href_proc_port([$<,$<|T],Acc) -> %% No links to binaries href_proc_port(T,[$;,$t,$l,$&,$;,$t,$l,$&|Acc]); @@ -1243,7 +1053,7 @@ 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]); + ["./port?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,$&]), @@ -1422,7 +1232,7 @@ replace_insrt("'trsni$'"++Rest,[H|T],Acc) -> % the list is reversed here! "<" ++ _Pid -> href("TARGET=\"main\"",["./proc_details?pid=",H],H); "#Port<" ++ Port -> - href("TARGET=\"main\"",["./ports?port=","Port<"++Port],H); + href("TARGET=\"main\"",["./port?port=","Port<"++Port],H); "#" ++ _other -> H end, @@ -1431,3 +1241,173 @@ replace_insrt([H|T],Insrt,Acc) -> replace_insrt(T,Insrt,[H|Acc]); replace_insrt([],[],Acc) -> Acc. + +%%%----------------------------------------------------------------- +%%% Create a page with one table by delivering chunk by chunk to +%%% inets. crashdump_viewer first calls chunk_page/5 once, then +%%% chunk/3 multiple times until all data is delivered. +chunk_page(processes,SessionId,TW,{Sorted,SharedHeap},FirstChunk) -> + Columns = procs_summary_table_head(Sorted,SharedHeap), + chunk_page(SessionId, "Process Information", TW, FirstChunk, + "processes", Columns, fun procs_summary_table/1); +chunk_page(ports,SessionId,TW,_,FirstChunk) -> + chunk_page(SessionId, "Port Information", TW, FirstChunk, + "ports", port_table_head(), fun ports_table/1); +chunk_page(ets_tables,SessionId,TW,Heading,FirstChunk) -> + Columns = ["Owner", + "Slot", + "Id", + "Name", + "Type", + "Buckets", + "Objects", + "Memory (bytes)"], + chunk_page(SessionId, Heading, TW, FirstChunk, + "ets_tables", Columns, fun ets_tables_table/1); +chunk_page(timers,SessionId,TW,Heading,FirstChunk) -> + chunk_page(SessionId, Heading, TW, FirstChunk, "timers", + ["Owner","Message","Time left"], fun timers_table/1); +chunk_page(loaded_mods,SessionId,TW,{CC,OC},FirstChunk) -> + TotalsInfo = p([b("Current code: "),CC," bytes",br(), + b("Old code: "),OC," bytes"]), + Columns = ["Module","Current size (bytes)","Old size (bytes)"], + chunk_page(SessionId, "Loaded Modules Information", TW, FirstChunk, + "loaded_modules", TotalsInfo,Columns, fun loaded_mods_table/1); +chunk_page(funs,SessionId, TW, _, FirstChunk) -> + Columns = ["Module", + "Uniq", + "Index", + "Address", + "Native_address", + "Refc"], + chunk_page(SessionId, "Fun Information", TW, FirstChunk, + "funs", Columns, fun funs_table/1). + +chunk_page(SessionId,Heading,TW,FirstChunk,Type,TableColumns,TableFun) -> + chunk_page(SessionId,Heading,TW,FirstChunk,Type,[],TableColumns,TableFun). +chunk_page(SessionId,Heading,TW,done,Type,_TotalsInfo,_TableColumns,_TableFun) -> + no_info_found(SessionId,Heading,TW,Type); +chunk_page(SessionId,Heading,TW,FirstChunk,Type,TotalsInfo,TableColumns,TableFun) -> + deliver_first(SessionId,[start_html_page(Heading), + heading(Heading,Type), + warn(TW), + TotalsInfo, + start_visible_table(TableColumns)]), + chunk(SessionId,FirstChunk,TableFun), + TableFun. + +no_info_found(SessionId, Heading, TW, Type) -> + Info = ["No ", Type, " were found\n"], + deliver_first(SessionId,[start_html_page(Heading), + h1(Heading), + warn(TW), + Info, + stop_html_page()]). + +chunk(SessionId, done, _TableFun) -> + deliver(SessionId,[stop_table(),stop_html_page()]); +chunk(SessionId, Items, TableFun) -> + deliver(SessionId, [lists:map(TableFun, Items), + stop_table(), %! Will produce an empty table at the end + start_visible_table()]). % of the page :( + +%%%----------------------------------------------------------------- +%%% Deliver part of a page to inets +%%% The first part, which includes the HTTP header, must always be +%%% delivered as a string (i.e. no binaries). The rest of the page is +%%% better delivered as binaries in order to avoid data copying. +deliver_first(SessionId,String) -> + mod_esi:deliver(SessionId,String). +deliver(SessionId,IoList) -> + mod_esi:deliver(SessionId,[list_to_binary(IoList)]). + + +%%%----------------------------------------------------------------- +%%% Page specific stuff for chunk pages +procs_summary_table_head(Sorted,SharedHeap) -> + MemHeading = + if SharedHeap -> + "Stack"; + true -> + "Stack+heap" + end, + [procs_summary_table_head("pid","Pid",Sorted), + procs_summary_table_head("name_func","Name/Spawned as",Sorted), + procs_summary_table_head("state","State",Sorted), + procs_summary_table_head("reds","Reductions",Sorted), + procs_summary_table_head("mem",MemHeading,Sorted), + procs_summary_table_head("msg_q_len","MsgQ Length",Sorted)]. + +procs_summary_table_head(_,Text,no_sort) -> + Text; +procs_summary_table_head(Sorted,Text,Sorted) -> + %% Mark the sorted column (bigger and italic) + font("SIZE=\"+1\"",em(href("./sort_procs?sort="++Sorted,Text))); +procs_summary_table_head(SortOn,Text,_Sorted) -> + 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))]). + +port_table_head() -> + ["Id","Slot","Connected","Links","Name","Monitors","Controls"]. + +ports_table(Port) -> + #port{id=Id,slot=Slot,connected=Connected,links=Links,name=Name, + monitors=Monitors,controls=Controls}=Port, + tr( + [td(Id), + td("ALIGN=right",Slot), + td(href_proc_port(Connected)), + td(href_proc_port(Links)), + td(Name), + td(href_proc_port(Monitors)), + td(Controls)]). + +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)]). + +timers_table(Timer) -> + #timer{pid=Pid,msg=Msg,time=Time}=Timer, + tr( + [td(href_proc_port(Pid)), + td(Msg), + td("ALIGN=right",Time)]). + +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)]). + +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)]). diff --git a/lib/observer/test/crashdump_viewer_SUITE.erl b/lib/observer/test/crashdump_viewer_SUITE.erl index 1a7e6f61fe..c547b997d1 100644 --- a/lib/observer/test/crashdump_viewer_SUITE.erl +++ b/lib/observer/test/crashdump_viewer_SUITE.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2003-2010. All Rights Reserved. +%% Copyright Ericsson AB 2003-2011. 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 @@ -414,16 +414,17 @@ special(Port,File) -> _ -> ok end; - ".250atoms" -> - Html1 = contents(Port,"atoms"), - NextLink1 = next_link(Html1), - "Atoms" = title(Html1), - Html2 = contents(Port,NextLink1), - NextLink2 = next_link(Html2), - "Atoms" = title(Html2), - Html3 = contents(Port,NextLink2), - "" = next_link(Html3), - "Atoms" = title(Html3); + %%! No longer needed - all atoms are shown on one page!! + %% ".250atoms" -> + %% Html1 = contents(Port,"atoms"), + %% NextLink1 = next_link(Html1), + %% "Atoms" = title(Html1), + %% Html2 = contents(Port,NextLink1), + %% NextLink2 = next_link(Html2), + %% "Atoms" = title(Html2), + %% Html3 = contents(Port,NextLink2), + %% "" = next_link(Html3), + %% "Atoms" = title(Html3); _ -> ok end, @@ -509,14 +510,14 @@ next_link(Html) -> toggle_menu(Port) -> - Html = contents(Port,"toggle?index=10"), + Html = contents(Port,"toggle?index=4"), check_toggle(Html). check_toggle(Html) -> case Html of - " + " collapsed; - " + " exploded; [_H|T] -> check_toggle(T) @@ -543,10 +544,10 @@ expand_link(Html) -> port_details(Port) -> - Port1 = contents(Port,"ports?port=Port<0.1>"), + Port1 = contents(Port,"port?port=Port<0.1>"), "#Port<0.1>" = title(Port1), - Port0 = contents(Port,"ports?port=Port<0.0>"), + Port0 = contents(Port,"port?port=Port<0.0>"), "Could not find port: #Port<0.0>" = title(Port0). is_truncated(File) -> -- cgit v1.2.3 From 7405f353d3f06f2a4207f382d91435adbe9c9160 Mon Sep 17 00:00:00 2001 From: Siri Hansen Date: Wed, 23 Feb 2011 15:58:42 +0100 Subject: Add shell script and .bat file to start crashdump_viewer Since browsers no longer can provide the full path of a file selected with a file-type input field (browse button), the input field for loading a crashdump is now changed to a plain text input field. Since this reduces the user-friendlyness, a shell script (and a .bat file) has instead been added so the crashdump_viewer can be started directly from the command line - and thus normal tab completion can be used for selecting the crashdump file. Usage: cdv file [ browser ] --- lib/observer/priv/bin/cdv | 4 ++ lib/observer/priv/bin/cdv.bat | 2 + lib/observer/src/Makefile | 6 +- lib/observer/src/crashdump_viewer.erl | 104 ++++++++++++++++++++++++----- lib/observer/src/crashdump_viewer_html.erl | 20 +++--- 5 files changed, 106 insertions(+), 30 deletions(-) create mode 100755 lib/observer/priv/bin/cdv create mode 100644 lib/observer/priv/bin/cdv.bat (limited to 'lib') diff --git a/lib/observer/priv/bin/cdv b/lib/observer/priv/bin/cdv new file mode 100755 index 0000000000..1c44785ac2 --- /dev/null +++ b/lib/observer/priv/bin/cdv @@ -0,0 +1,4 @@ +#!/bin/sh + +erl -sname cdv -noinput -s crashdump_viewer script_start $@ + diff --git a/lib/observer/priv/bin/cdv.bat b/lib/observer/priv/bin/cdv.bat new file mode 100644 index 0000000000..efa8bf8687 --- /dev/null +++ b/lib/observer/priv/bin/cdv.bat @@ -0,0 +1,2 @@ +@ECHO OFF +CALL werl -sname cdv -s crashdump_viewer script_start %* diff --git a/lib/observer/src/Makefile b/lib/observer/src/Makefile index b4eb518dd7..2d06cb6bc4 100644 --- a/lib/observer/src/Makefile +++ b/lib/observer/src/Makefile @@ -1,7 +1,7 @@ # # %CopyrightBegin% # -# Copyright Ericsson AB 2002-2009. All Rights Reserved. +# Copyright Ericsson AB 2002-2011. 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 @@ -59,8 +59,10 @@ BINDIR= $(PRIVDIR)/bin EXECUTABLES= \ $(BINDIR)/etop \ $(BINDIR)/getop \ + $(BINDIR)/cdv \ $(BINDIR)/etop.bat \ - $(BINDIR)/getop.bat + $(BINDIR)/getop.bat \ + $(BINDIR)/cdv.bat CDVDIR= $(PRIVDIR)/crashdump_viewer GIF_FILES= \ $(CDVDIR)/collapsd.gif \ diff --git a/lib/observer/src/crashdump_viewer.erl b/lib/observer/src/crashdump_viewer.erl index 8b96769224..29e24655eb 100644 --- a/lib/observer/src/crashdump_viewer.erl +++ b/lib/observer/src/crashdump_viewer.erl @@ -57,7 +57,7 @@ %% %% User API --export([start/0,stop/0]). +-export([start/0,stop/0,script_start/0,script_start/1]). %% Webtool API -export([configData/0, @@ -117,7 +117,7 @@ % this, it must be explicitly expanded. -define(max_display_binary_size,50). % max size of a binary that will be % directly displayed. --define(max_sort_process_num,1000). % Max number of processes that allows +-define(max_sort_process_num,10000). % Max number of processes that allows % sorting. If more than this number of % processes exist, they will be displayed % in the order they are found in the log. @@ -205,6 +205,85 @@ stop() -> webtool:stop_tools([],"app=crashdump_viewer"), webtool:stop(). +%%%----------------------------------------------------------------- +%%% Start crashdump_viewer via the cdv script located in +%%% $OBSERVER_PRIV_DIR/bin +script_start() -> + usage(). +script_start([File]) -> + DefaultBrowser = + case os:type() of + {win32,_} -> iexplore; + _ -> firefox + end, + script_start([File,DefaultBrowser]); +script_start([FileAtom,Browser]) -> + File = atom_to_list(FileAtom), + case filelib:is_regular(File) of + true -> + io:format("Starting crashdump_viewer...\n"), + start(), + io:format("Reading crashdump..."), + read_file(File), + redirect([],[]), + io:format("done\n"), + start_browser(Browser); + false -> + io:format("cdv error: the given file does not exist\n"), + usage() + end. + +start_browser(Browser) -> + PortStr = integer_to_list(gen_server:call(web_tool,get_port)), + Url = "http://localhost:" ++ PortStr ++ ?START_PAGE, + {OSType,_} = os:type(), + case Browser of + none -> + ok; + iexplore when OSType == win32-> + io:format("Starting internet explorer...\n"), + {ok,R} = win32reg:open(""), + Key="\\local_machine\\SOFTWARE\\Microsoft\\IE Setup\\Setup", + win32reg:change_key(R,Key), + {ok,Val} = win32reg:value(R,"Path"), + IExplore=filename:join(win32reg:expand(Val),"iexplore.exe"), + os:cmd("\"" ++ IExplore ++ "\" " ++ Url); + _ when OSType == win32 -> + io:format("Starting ~w...\n",[Browser]), + os:cmd("\"" ++ atom_to_list(Browser) ++ "\" " ++ Url); + B when B==firefox; B==mozilla -> + io:format("Sending URL to ~w...",[Browser]), + BStr = atom_to_list(Browser), + SendCmd = BStr ++ " -raise -remote \'openUrl(" ++ Url ++ ")\'", + Port = open_port({spawn,SendCmd},[exit_status]), + receive + {Port,{exit_status,0}} -> + io:format("done\n"); + {Port,{exit_status,_Error}} -> + io:format(" not running, starting ~w...\n",[Browser]), + os:cmd(BStr ++ " " ++ Url) + after 5000 -> + io:format(" failed, starting ~w...\n",[Browser]), + erlang:port_close(Port), + os:cmd(BStr ++ " " ++ Url) + end; + _ -> + io:format("Starting ~w...\n",[Browser]), + os:cmd(atom_to_list(Browser) ++ " " ++ Url) + end, + ok. + +usage() -> + io:format( + "\nusage: cdv file [ browser ]\n" + "\tThe \'file\' must be an existing erlang crash dump.\n" + "\tDefault browser is \'iexplore\' (Internet Explorer) on Windows\n" + "\tor else \'firefox\'.\n", + []). + + + + %%%----------------------------------------------------------------- %%% Return config data used by webtool configData() -> @@ -404,16 +483,7 @@ 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, + {ok,File} = get_value("path",httpd:parse_query(Input)), spawn_link(fun() -> read_file(File) end), Status = background_status(reading,File), Reply = crashdump_viewer_html:redirect(Status), @@ -877,10 +947,12 @@ get_rest_of_line_1(Fd, <<$\n:8,Bin/binary>>, Acc) -> 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, <<$<:8,Rest/binary>>, Acc) -> -%% get_rest_of_line_1(Fd, Rest, [$;,$t,$l,$&|Acc]); -%% get_rest_of_line_1(Fd, <<$>:8,Rest/binary>>, Acc) -> -%% get_rest_of_line_1(Fd, Rest, [$;,$t,$g,$&|Acc]); +get_rest_of_line_1(Fd, <<$<:8,Rest/binary>>, Acc) -> + get_rest_of_line_1(Fd, Rest, [$;,$t,$l,$&|Acc]); +get_rest_of_line_1(Fd, <<$>:8,Rest/binary>>, Acc) -> + get_rest_of_line_1(Fd, Rest, [$;,$t,$g,$&|Acc]); +get_rest_of_line_1(Fd, <<$&:8,Rest/binary>>, Acc) -> + get_rest_of_line_1(Fd, Rest, [$;,$p,$m,$a,$&|Acc]); get_rest_of_line_1(Fd, <>, Acc) -> get_rest_of_line_1(Fd, Rest, [Char|Acc]); get_rest_of_line_1(Fd, <<>>, Acc) -> diff --git a/lib/observer/src/crashdump_viewer_html.erl b/lib/observer/src/crashdump_viewer_html.erl index 0d70c9b86f..d49023f9f6 100644 --- a/lib/observer/src/crashdump_viewer_html.erl +++ b/lib/observer/src/crashdump_viewer_html.erl @@ -77,23 +77,20 @@ read_file_frame() -> 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, + %% Using a plain text input field instead of a file input field + %% (e.g. ) because most + %% browsers can not forward the full path from this dialog even if + %% the browser is running on localhost (Ref 'fakepath'-problem) + Entry = input("TYPE=text NAME=path SIZE=60"), Form = form( - "NAME=read_file_form METHOD=post ACTION= \"./read_file\"", + "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"))])])), + td("ALIGN=center",input("TYPE=submit VALUE=Ok"))])])), table( "WIDTH=100% HEIGHT=60%", tr("VALIGN=middle", @@ -961,8 +958,7 @@ frame(Args) -> ["\n"]. start_visible_table() -> - start_table("BORDER=\"4\" CELLPADDING=\"4\" WIDTH=\"100%\""). -% start_table("BORDER=\"4\" CELLPADDING=\"4\""). + start_table("BORDER=\"4\" CELLPADDING=\"4\""). start_visible_table(ColTitles) -> [start_visible_table(), tr([th(ColTitle) || ColTitle <- ColTitles])]. -- cgit v1.2.3 From fda63b0568f052cf61b21e2a8e950b7dba1024e3 Mon Sep 17 00:00:00 2001 From: Siri Hansen Date: Thu, 24 Feb 2011 13:23:45 +0100 Subject: Document new cdv script and cdv.bat file for starting crashdump viewer --- lib/observer/doc/src/crashdump.xml | 6 ++++- lib/observer/doc/src/crashdump_help.html | 4 +-- lib/observer/doc/src/crashdump_ug.xml | 44 ++++++++++++++++++++++---------- 3 files changed, 37 insertions(+), 17 deletions(-) (limited to 'lib') diff --git a/lib/observer/doc/src/crashdump.xml b/lib/observer/doc/src/crashdump.xml index f8d7641524..b6056c2ed1 100644 --- a/lib/observer/doc/src/crashdump.xml +++ b/lib/observer/doc/src/crashdump.xml @@ -5,7 +5,7 @@
2003 - 2007 + 2011 Ericsson AB, All Rights Reserved @@ -38,6 +38,10 @@

The Crashdump Viewer is an HTML based tool for browsing Erlang crashdumps. Crashdump Viewer runs under the WebTool application.

+ +

See the user's guide + for more information about how to get started with the Crashdump + Viewer.

diff --git a/lib/observer/doc/src/crashdump_help.html b/lib/observer/doc/src/crashdump_help.html index 736a024288..268b9495d6 100644 --- a/lib/observer/doc/src/crashdump_help.html +++ b/lib/observer/doc/src/crashdump_help.html @@ -131,7 +131,7 @@ SRC="min_head.gif">
- +

ETS tables

The ETS table information page shows all ETS table @@ -304,4 +304,4 @@ Copyright © 1991-2003 - \ No newline at end of file + diff --git a/lib/observer/doc/src/crashdump_ug.xml b/lib/observer/doc/src/crashdump_ug.xml index 9913b30e38..dc65fe5b39 100644 --- a/lib/observer/doc/src/crashdump_ug.xml +++ b/lib/observer/doc/src/crashdump_ug.xml @@ -4,7 +4,7 @@

- 20032009 + 20032011 Ericsson AB. All Rights Reserved. @@ -38,12 +38,31 @@
Getting Started -

From an erlang node, start Crashdump Viewer by calling - crashdump_viewer:start(). This will automatically start - WebTool and display the web address where WebTool can be - found. See the documentation for the WebTool application for - further information about how to use WebTool. -

+ +

The easiest way to start Crashdump Viewer is to use the + provided shell script named cdv with the full path to the + erlang crashdump as an argument. The script can be found in the + priv directory of the observer application. This starts + WebTool, Crashdump Viewer and a web browser, and loads the given + file. The browser should then display a page named General + Information which shows a short summary of the information in + the crashdump.

+ +

The default browser is Internet Explorer on Windows or else + Firefox. To use another browser, give the browser's start command + as the second argument to cdv. If the given browser name is + not known to Crashdump Viewer, the browser argument is executed as + a command with the start URL as the only argument.

+ +

Under Windows the batch file cdv.bat can be used.

+ +

It is also possible to start the Crashdump Viewer from within + an erlang node by calling crashdump_viewer:start/0. This + will automatically start WebTool and display the web address where + WebTool can be found. See the documentation for the WebTool + application for further information about how to use WebTool.

+

Point your web browser to the address displayed, and you should now see the start page of WebTool. At the top of the page, you will see a link to "CrashDumpViewer". Click this link to get to @@ -52,15 +71,12 @@ connection to the internet, or you must set no proxy for localhost.)

-

You can also start WebTool, Crashdump Viewer and a browser in - one go by running the start_webtool script found in the - priv directory of the WebTool application, e.g. -

->start_webtool crashdump_viewer

From the start page of Crashdump Viewer, push the "Load Crashdump" button to load a crashdump into the tool. Then enter - the filename of the crashdump in the entry field and push the "Ok" - button. + the filename of the crashdump in the entry field and push the + "Ok" button. This will bring you to the General Information + page, i.e. the same page as the cdv script will open in + the browser.

Crashdumps generated by OTP R9C and later are loaded directly into the Crashdump Viewer, while dumps from earlier releases first -- cgit v1.2.3 From 386a4db00c8c1664d98215f0c1350b890a336d30 Mon Sep 17 00:00:00 2001 From: Siri Hansen Date: Thu, 24 Feb 2011 13:53:06 +0100 Subject: Minor bugfix related to improved performance of crashdump_viewer --- lib/observer/src/crashdump_viewer.erl | 6 ------ 1 file changed, 6 deletions(-) (limited to 'lib') diff --git a/lib/observer/src/crashdump_viewer.erl b/lib/observer/src/crashdump_viewer.erl index 29e24655eb..353a2db544 100644 --- a/lib/observer/src/crashdump_viewer.erl +++ b/lib/observer/src/crashdump_viewer.erl @@ -947,12 +947,6 @@ get_rest_of_line_1(Fd, <<$\n:8,Bin/binary>>, Acc) -> 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, <<$<:8,Rest/binary>>, Acc) -> - get_rest_of_line_1(Fd, Rest, [$;,$t,$l,$&|Acc]); -get_rest_of_line_1(Fd, <<$>:8,Rest/binary>>, Acc) -> - get_rest_of_line_1(Fd, Rest, [$;,$t,$g,$&|Acc]); -get_rest_of_line_1(Fd, <<$&:8,Rest/binary>>, Acc) -> - get_rest_of_line_1(Fd, Rest, [$;,$p,$m,$a,$&|Acc]); get_rest_of_line_1(Fd, <>, Acc) -> get_rest_of_line_1(Fd, Rest, [Char|Acc]); get_rest_of_line_1(Fd, <<>>, Acc) -> -- cgit v1.2.3 From 910a25b6502150014ccbd71080d1363461406618 Mon Sep 17 00:00:00 2001 From: Siri Hansen Date: Mon, 28 Feb 2011 17:51:24 +0100 Subject: Fix file descriptor leak in crashdump_viewer:chunk_page Also, remove compiler warnings for crashdump_viewer_SUITE and fix Makefile in test directory so Emakefile does not grow. --- lib/observer/src/crashdump_viewer.erl | 6 +++--- lib/observer/test/Makefile | 2 +- lib/observer/test/crashdump_viewer_SUITE.erl | 28 ++++++++++++++-------------- 3 files changed, 18 insertions(+), 18 deletions(-) (limited to 'lib') diff --git a/lib/observer/src/crashdump_viewer.erl b/lib/observer/src/crashdump_viewer.erl index 353a2db544..bf4910445b 100644 --- a/lib/observer/src/crashdump_viewer.erl +++ b/lib/observer/src/crashdump_viewer.erl @@ -2791,9 +2791,8 @@ chunk_page(SessionId,File,TW,What,HtmlCB,HtmlExtra,ParseFun) -> lookup_and_parse_index_chunk(Cont,Fd,ParseFun)) end. -chunk_page_1(Fd,HtmlInfo,SessionId,_ParseFun,done) -> - crashdump_viewer_html:chunk(SessionId,done,HtmlInfo), - close(Fd); +chunk_page_1(_Fd,HtmlInfo,SessionId,_ParseFun,done) -> + crashdump_viewer_html:chunk(SessionId,done,HtmlInfo); chunk_page_1(Fd,HtmlInfo,SessionId,ParseFun,{Chunk,Cont}) -> crashdump_viewer_html:chunk(SessionId,Chunk,HtmlInfo), chunk_page_1(Fd,HtmlInfo,SessionId,ParseFun, @@ -2802,6 +2801,7 @@ chunk_page_1(Fd,HtmlInfo,SessionId,ParseFun,{Chunk,Cont}) -> lookup_and_parse_index_chunk(Pointer,Fd,ParseFun) -> case lookup_index_chunk(Pointer) of '$end_of_table' -> + close(Fd), done; {Chunk,Cont} -> R = lists:map(fun({Id,Start}) -> diff --git a/lib/observer/test/Makefile b/lib/observer/test/Makefile index e15bde7346..6073e6ea00 100644 --- a/lib/observer/test/Makefile +++ b/lib/observer/test/Makefile @@ -53,7 +53,7 @@ EBIN = . make_emakefile: $(ERL_TOP)/make/make_emakefile $(ERL_COMPILE_FLAGS) -o$(EBIN) \ - $(MODULES) >> $(EMAKEFILE) + $(MODULES) > $(EMAKEFILE) tests debug opt: make_emakefile cd $(ERL_TOP)/lib/test_server/src && \ diff --git a/lib/observer/test/crashdump_viewer_SUITE.erl b/lib/observer/test/crashdump_viewer_SUITE.erl index c547b997d1..8449e8a7c4 100644 --- a/lib/observer/test/crashdump_viewer_SUITE.erl +++ b/lib/observer/test/crashdump_viewer_SUITE.erl @@ -68,7 +68,7 @@ init_per_suite(doc) -> init_per_suite(Config) when is_list(Config) -> Dog = ?t:timetrap(?default_timeout), application:start(inets), % will be using the http client later - http:set_options([{ipv6,disabled}]), + httpc:set_options([{ipfamily,inet6fb4}]), DataDir = ?config(data_dir,Config), Rels = [R || R <- [r12b,r13b], ?t:is_release_available(R)] ++ [current], io:format("Creating crash dumps for the following releases: ~p", [Rels]), @@ -112,7 +112,7 @@ start(Config) when is_list(Config) -> undefined = whereis(crashdump_viewer_server), undefined = whereis(web_tool), Url = cdv_url(Port,"start_page"), - {error,_} = http:request(get,{Url,[]},[],[]), + {error,_} = httpc:request(Url), % exit(whereis(httpc_manager),kill), ?t:timetrap_cancel(AngryDog), ok. @@ -246,7 +246,7 @@ cdv_url(Port,Link) -> "http://localhost:" ++ Port ++ "/cdv_erl/crashdump_viewer/" ++ Link. request_sync(Method,HTTPReqCont) -> - case http:request(Method, + case httpc:request(Method, HTTPReqCont, [{timeout,30000}], [{full_result, false}]) of @@ -254,13 +254,13 @@ request_sync(Method,HTTPReqCont) -> Html; {ok,{Code,Html}} -> io:format("~s\n", [Html]), - io:format("Received ~w from http:request(...) with\nMethod=~w\n" + io:format("Received ~w from httpc:request(...) with\nMethod=~w\n" "HTTPReqCont=~p\n", [Code,Method,HTTPReqCont]), ?t:fail(); Other -> io:format( - "Received ~w from http:request(...) with\nMethod=~w\n" + "Received ~w from httpc:request(...) with\nMethod=~w\n" "HTTPReqCont=~p\n", [Other,Method,HTTPReqCont]), ?t:fail() @@ -497,15 +497,15 @@ expand_binary_link(Html) -> end. -next_link(Html) -> - case Html of - " - "next?pos=" ++ string:sub_word(Rest,1,$"); - [_H|T] -> - next_link(T); - [] -> - [] - end. +%% next_link(Html) -> +%% case Html of +%% " +%% "next?pos=" ++ string:sub_word(Rest,1,$"); +%% [_H|T] -> +%% next_link(T); +%% [] -> +%% [] +%% end. -- cgit v1.2.3 From 5186790d6df4ec0616721f730203e63fc0b627fd Mon Sep 17 00:00:00 2001 From: Siri Hansen Date: Thu, 3 Mar 2011 17:20:23 +0100 Subject: Bugfix: Never deliver empty chunk to inets --- lib/observer/src/crashdump_viewer.erl | 31 +++++++++++++++++++----------- lib/observer/src/crashdump_viewer_html.erl | 3 ++- 2 files changed, 22 insertions(+), 12 deletions(-) (limited to 'lib') diff --git a/lib/observer/src/crashdump_viewer.erl b/lib/observer/src/crashdump_viewer.erl index bf4910445b..3b8d17c7d9 100644 --- a/lib/observer/src/crashdump_viewer.erl +++ b/lib/observer/src/crashdump_viewer.erl @@ -1242,9 +1242,9 @@ indexify(Fd,Bin,N) -> {Chunk,N1} = case binary:last(Bin) of $\n -> - {<<$\n,Chunk0/binary>>,N+size(Bin)-1}; + {<<$\n,Chunk0/binary>>,N+byte_size(Bin)-1}; _ -> - {Chunk0,N+size(Bin)} + {Chunk0,N+byte_size(Bin)} end, indexify(Fd,Chunk,N1); eof -> @@ -2244,9 +2244,13 @@ atoms(SessionId,File,TW,Num) -> [{_Id,Start}] -> Fd = open(File), pos_bof(Fd,Start), - {Atoms,Cont} = get_atoms(Fd,1000), - crashdump_viewer_html:atoms(SessionId,TW,Num,Atoms), - atoms_chunks(Fd,SessionId,Cont); + case get_atoms(Fd,?items_chunk_size) of + {Atoms,Cont} -> + crashdump_viewer_html:atoms(SessionId,TW,Num,Atoms), + atoms_chunks(Fd,SessionId,Cont); + done -> + crashdump_viewer_html:atoms(SessionId,TW,Num,done) + end; _ -> crashdump_viewer_html:atoms(SessionId,TW,Num,done) end. @@ -2254,20 +2258,25 @@ atoms(SessionId,File,TW,Num) -> get_atoms(Fd,Number) -> case get_n_lines_of_tag(Fd,Number) of {all,_,Lines} -> + close(Fd), {Lines,done}; {part,_,Lines} -> {Lines,Number}; empty -> - {[],done} + close(Fd), + done end. -atoms_chunks(Fd,SessionId,done) -> - close(Fd), +atoms_chunks(_Fd,SessionId,done) -> crashdump_viewer_html:atoms_chunk(SessionId,done); atoms_chunks(Fd,SessionId,Number) -> - {Atoms,Cont} = get_atoms(Fd,Number), - crashdump_viewer_html:atoms_chunk(SessionId,Atoms), - atoms_chunks(Fd,SessionId,Cont). + case get_atoms(Fd,Number) of + {Atoms,Cont} -> + crashdump_viewer_html:atoms_chunk(SessionId,Atoms), + atoms_chunks(Fd,SessionId,Cont); + done -> + atoms_chunks(Fd,SessionId,done) + end. %%----------------------------------------------------------------- diff --git a/lib/observer/src/crashdump_viewer_html.erl b/lib/observer/src/crashdump_viewer_html.erl index d49023f9f6..24a80b1916 100644 --- a/lib/observer/src/crashdump_viewer_html.erl +++ b/lib/observer/src/crashdump_viewer_html.erl @@ -726,7 +726,8 @@ atoms(SessionId,TW,Num,FirstChunk) -> Heading = "Atoms", case FirstChunk of done -> - deliver_first(SessionId,[h1(Heading), + deliver_first(SessionId,[start_html_page(Heading), + h1(Heading), warn(TW), "No atoms were found in log",br(), "Total number of atoms in node was ", Num, -- cgit v1.2.3 From e98f3bded140a81a47667b37b6d879ee56fa0f8d Mon Sep 17 00:00:00 2001 From: Siri Hansen Date: Fri, 4 Mar 2011 15:00:32 +0100 Subject: Wait longer than 5 sec instead of 2.5 sec for crashdump to be created in test --- lib/observer/test/crashdump_viewer_SUITE.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib') diff --git a/lib/observer/test/crashdump_viewer_SUITE.erl b/lib/observer/test/crashdump_viewer_SUITE.erl index 8449e8a7c4..fdc4a2f1ff 100644 --- a/lib/observer/test/crashdump_viewer_SUITE.erl +++ b/lib/observer/test/crashdump_viewer_SUITE.erl @@ -669,7 +669,7 @@ rename(From,To) -> end. check_complete(File) -> - check_complete1(File,5). + check_complete1(File,10). check_complete1(_File,0) -> {error,enoent}; -- cgit v1.2.3