diff options
author | Dan Gudmundsson <dgud@erlang.org> | 2013-10-22 13:37:03 +0200 |
---|---|---|
committer | Dan Gudmundsson <dgud@erlang.org> | 2014-01-27 16:13:57 +0100 |
commit | bfae535d4ac51d2c3bef146e0f058e105bb5e956 (patch) | |
tree | eaafe6c61f5fca58ca05b232f280e911f04dcdb7 | |
parent | 41380c0ff6c4fb56aad5702b9d9554ae36580063 (diff) | |
download | otp-bfae535d4ac51d2c3bef146e0f058e105bb5e956.tar.gz otp-bfae535d4ac51d2c3bef146e0f058e105bb5e956.tar.bz2 otp-bfae535d4ac51d2c3bef146e0f058e105bb5e956.zip |
observer: Optimize row lookups
Use arrays instead of lists to cache data, gives faster lookups for
large contents.
Also update colors used in table viewer, indication new and changed rows.
Other minor bugfixes in tables viewer.
-rw-r--r-- | lib/observer/src/observer_defs.hrl | 9 | ||||
-rw-r--r-- | lib/observer/src/observer_lib.erl | 10 | ||||
-rw-r--r-- | lib/observer/src/observer_pro_wx.erl | 73 | ||||
-rw-r--r-- | lib/observer/src/observer_tv_table.erl | 123 |
4 files changed, 127 insertions, 88 deletions
diff --git a/lib/observer/src/observer_defs.hrl b/lib/observer/src/observer_defs.hrl index 586e7bbff9..a720e8c833 100644 --- a/lib/observer/src/observer_defs.hrl +++ b/lib/observer/src/observer_defs.hrl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2011. All Rights Reserved. +%% Copyright Ericsson AB 2011-2013. 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 @@ -35,13 +35,14 @@ check = false }). --record(attrs, {even, odd, deleted, changed, searched}). +-record(attrs, {even, odd, searched, deleted, changed_odd, changed_even, new_odd, new_even}). -define(EVEN(Row), ((Row rem 2) =:= 0)). -define(BG_EVEN, {230,230,250}). -define(BG_ODD, {255,255,255}). -define(BG_DELETED, {100,100,100}). --define(FG_DELETED, {240,30,30}). +-define(FG_DELETED, {230,230,230}). -define(BG_SEARCHED,{235,215,90}). --define(BG_CHANGED, {230,230,250}). +-define(BG_CHANGED, {184,207,184}). +-define(BG_NEW, {123,168,123}). -define(LCTRL_WDECR, 4). %% Remove some pixels in column width to avoid creating unnecessary scrollbar diff --git a/lib/observer/src/observer_lib.erl b/lib/observer/src/observer_lib.erl index 463c42308d..f11ccfb752 100644 --- a/lib/observer/src/observer_lib.erl +++ b/lib/observer/src/observer_lib.erl @@ -339,10 +339,18 @@ create_attrs() -> #attrs{even = wxListItemAttr:new(Text, ?BG_EVEN, Font), odd = wxListItemAttr:new(Text, ?BG_ODD, Font), deleted = wxListItemAttr:new(?FG_DELETED, ?BG_DELETED, Font), - changed = wxListItemAttr:new(Text, ?BG_CHANGED, Font), + changed_even = wxListItemAttr:new(Text, mix(?BG_CHANGED,?BG_EVEN), Font), + changed_odd = wxListItemAttr:new(Text, mix(?BG_CHANGED,?BG_ODD), Font), + new_even = wxListItemAttr:new(Text, mix(?BG_NEW,?BG_EVEN), Font), + new_odd = wxListItemAttr:new(Text, mix(?BG_NEW, ?BG_ODD), Font), searched = wxListItemAttr:new(Text, ?BG_SEARCHED, Font) }. +mix(RGB,_) -> RGB. + +%% mix({R,G,B},{MR,MG,MB}) -> +%% {trunc(R*MR/255), trunc(G*MG/255), trunc(B*MB/255)}. + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% get_box_info({Title, List}) when is_list(List) -> {Title, ?wxALIGN_LEFT, List}; diff --git a/lib/observer/src/observer_pro_wx.erl b/lib/observer/src/observer_pro_wx.erl index 10e2f12e0f..0be8c18893 100644 --- a/lib/observer/src/observer_pro_wx.erl +++ b/lib/observer/src/observer_pro_wx.erl @@ -66,6 +66,7 @@ -record(holder, {parent, info, + etop, sort=#sort{}, accum=[], attrs, @@ -435,13 +436,14 @@ set_focus([Old|_], [New|_], Grid) -> init_table_holder(Parent, Attrs) -> Backend = spawn_link(node(), observer_backend,etop_collect,[self()]), table_holder(#holder{parent=Parent, - info=#etop_info{procinfo=[]}, + etop=#etop_info{}, + info=array:new(), node=node(), backend_pid=Backend, attrs=Attrs }). -table_holder(#holder{info=#etop_info{procinfo=Info}, attrs=Attrs, +table_holder(#holder{info=Info, attrs=Attrs, node=Node, backend_pid=Backend}=S0) -> receive {get_row, From, Row, Col} -> @@ -488,7 +490,8 @@ table_holder(#holder{info=#etop_info{procinfo=Info}, attrs=Attrs, From ! {self(), S0#holder.accum == true}, table_holder(S0); {dump, Fd} -> - etop_txt:do_update(Fd, S0#holder.info, #opts{node=Node}), + EtopInfo = (S0#holder.etop)#etop_info{procinfo=array:to_list(Info)}, + etop_txt:do_update(Fd, EtopInfo, #opts{node=Node}), file:close(Fd), table_holder(S0); stop -> @@ -498,23 +501,23 @@ table_holder(#holder{info=#etop_info{procinfo=Info}, attrs=Attrs, table_holder(S0) end. -change_sort(Col, S0=#holder{parent=Parent, info=EI=#etop_info{procinfo=Data}, sort=Sort0}) -> +change_sort(Col, S0=#holder{parent=Parent, info=Data, sort=Sort0}) -> {Sort, ProcInfo}=sort(Col, Sort0, Data), - Parent ! {holder_updated, length(Data)}, - S0#holder{info=EI#etop_info{procinfo=ProcInfo}, sort=Sort}. + Parent ! {holder_updated, array:size(Data)}, + S0#holder{info=ProcInfo, sort=Sort}. change_accum(true, S0) -> S0#holder{accum=true}; -change_accum(false, S0=#holder{info=#etop_info{procinfo=Info}}) -> +change_accum(false, S0=#holder{info=Info}) -> self() ! refresh, - S0#holder{accum=lists:sort(Info)}. + S0#holder{accum=lists:sort(array:to_list(Info))}. handle_update(EI=#etop_info{procinfo=ProcInfo0}, S0=#holder{parent=Parent, sort=Sort=#sort{sort_key=KeyField}}) -> {ProcInfo1, S1} = accum(ProcInfo0, S0), {_SO, ProcInfo} = sort(KeyField, Sort#sort{sort_key=undefined}, ProcInfo1), - Parent ! {holder_updated, length(ProcInfo)}, - S1#holder{info=EI#etop_info{procinfo=ProcInfo}}. + Parent ! {holder_updated, array:size(ProcInfo)}, + S1#holder{info=ProcInfo, etop=EI#etop_info{procinfo=[]}}. accum(ProcInfo, State=#holder{accum=true}) -> {ProcInfo, State}; @@ -532,12 +535,18 @@ accum2([PI|PIs], Old, Acc) -> accum2(PIs, Old, [PI|Acc]); accum2([], _, Acc) -> Acc. +sort(Col, Opt, Table) + when not is_list(Table) -> + sort(Col,Opt,array:to_list(Table)); sort(Col, Opt=#sort{sort_key=Col, sort_incr=Bool}, Table) -> - {Opt#sort{sort_incr=not Bool}, lists:reverse(Table)}; + {Opt#sort{sort_incr=not Bool}, + array:from_list(lists:reverse(Table))}; sort(Col, S=#sort{sort_incr=true}, Table) -> - {S#sort{sort_key=Col}, lists:keysort(col_to_element(Col), Table)}; + {S#sort{sort_key=Col}, + array:from_list(lists:keysort(col_to_element(Col), Table))}; sort(Col, S=#sort{sort_incr=false}, Table) -> - {S#sort{sort_key=Col}, lists:reverse(lists:keysort(col_to_element(Col), Table))}. + {S#sort{sort_key=Col}, + array:from_list(lists:reverse(lists:keysort(col_to_element(Col), Table)))}. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%% @@ -552,40 +561,50 @@ col_to_element(?COL_FUN) -> #etop_proc_info.cf; col_to_element(?COL_MSG) -> #etop_proc_info.mq. get_pids(From, Indices, ProcInfo) -> - Processes = [(lists:nth(I+1, ProcInfo))#etop_proc_info.pid || I <- Indices], + Processes = [(array:get(I, ProcInfo))#etop_proc_info.pid || I <- Indices], From ! {self(), Processes}. get_name_or_pid(From, Indices, ProcInfo) -> Get = fun(#etop_proc_info{name=Name}) when is_atom(Name) -> Name; (#etop_proc_info{pid=Pid}) -> Pid end, - Processes = [Get(lists:nth(I+1, ProcInfo)) || I <- Indices], + Processes = [Get(array:get(I, ProcInfo)) || I <- Indices], From ! {self(), Processes}. - get_row(From, Row, pid, Info) -> Pid = case Row =:= -1 of true -> {error, undefined}; - false -> {ok, get_procinfo_data(?COL_PID, lists:nth(Row+1, Info))} + false -> {ok, get_procinfo_data(?COL_PID, array:get(Row, Info))} end, From ! {self(), Pid}; get_row(From, Row, Col, Info) -> - Data = case Row+1 > length(Info) of + Data = case Row > array:size(Info) of true -> ""; false -> - ProcInfo = lists:nth(Row+1, Info), + ProcInfo = array:get(Row, Info), get_procinfo_data(Col, ProcInfo) end, From ! {self(), observer_lib:to_str(Data)}. get_rows_from_pids(From, Pids0, Info) -> - Res = lists:foldl(fun(Pid, Data = {Ids, Pids}) -> - case index(Pid, Info, 0) of - false -> Data; - Index -> {[Index|Ids], [Pid|Pids]} - end - end, {[],[]}, Pids0), + Search = fun(Idx, #etop_proc_info{pid=Pid}, Acc0={Pick0, {Idxs, Pids}}) -> + case ordsets:is_element(Pid, Pick0) of + true -> + Acc = {[Idx|Idxs],[Pid|Pids]}, + Pick = ordsets:del_element(Pid, Pick0), + case Pick =:= [] of + true -> throw(Acc); + false -> {Pick, Acc} + end; + false -> Acc0 + end + end, + Res = try + {_, R} = array:foldl(Search, {ordsets:from_list(Pids0), {[],[]}}, Info), + R + catch R0 -> R0 + end, From ! {self(), Res}. get_attr(From, Row, Attrs) -> @@ -594,7 +613,3 @@ get_attr(From, Row, Attrs) -> false -> Attrs#attrs.odd end, From ! {self(), Attribute}. - -index(Pid, [#etop_proc_info{pid=Pid}|_], Index) -> Index; -index(Pid, [_|PI], Index) -> index(Pid, PI, Index+1); -index(_, _, _) -> false. diff --git a/lib/observer/src/observer_tv_table.erl b/lib/observer/src/observer_tv_table.erl index b4832d9599..59fe5b5670 100644 --- a/lib/observer/src/observer_tv_table.erl +++ b/lib/observer/src/observer_tv_table.erl @@ -98,7 +98,7 @@ init([Parent, Opts]) -> ets -> "TV Ets: " ++ Title0; mnesia -> "TV Mnesia: " ++ Title0 end, - Frame = wxFrame:new(Parent, ?wxID_ANY, Title, [{size, {800, 300}}]), + Frame = wxFrame:new(Parent, ?wxID_ANY, Title, [{size, {800, 600}}]), IconFile = filename:join(code:priv_dir(observer), "erlang_observer.png"), Icon = wxIcon:new(IconFile, [{type,?wxBITMAP_TYPE_PNG}]), wxFrame:setIcon(Frame, Icon), @@ -261,11 +261,12 @@ handle_event(#wx{id=?ID_EDIT}, State = #state{selected=Index}) -> handle_event(#wx{id=?ID_DELETE}, State = #state{selected=undefined}) -> {noreply, State}; handle_event(#wx{id=?ID_DELETE}, - State = #state{pid=Pid, status=StatusBar, selected=Index}) -> + State = #state{grid=Grid, pid=Pid, status=StatusBar, selected=Index}) -> Str = get_row(Pid, Index, all), Pid ! {delete, Index}, wxStatusBar:setStatusText(StatusBar, io_lib:format("Deleted object: ~s",[Str])), - {noreply, State}; + wxListCtrl:setItemState(Grid, Index, 0, ?wxLIST_STATE_FOCUSED), + {noreply, State#state{selected=undefined}}; handle_event(#wx{id=?wxID_CLOSE}, State = #state{frame=Frame}) -> wxFrame:destroy(Frame), @@ -279,8 +280,8 @@ handle_event(#wx{id=?GOTO_ENTRY, event=#wxCommand{cmdString=Str}}, State = #state{grid=Grid}) -> try Row0 = list_to_integer(Str), - Row1 = min(0, Row0), - Row = max(wxListCtrl:getItemCount(Grid)-1,Row1), + Row1 = max(0, Row0), + Row = min(wxListCtrl:getItemCount(Grid)-1,Row1), wxListCtrl:ensureVisible(Grid, Row), ok catch _:_ -> ok @@ -289,7 +290,9 @@ handle_event(#wx{id=?GOTO_ENTRY, event=#wxCommand{cmdString=Str}}, %% Search functionality handle_event(#wx{id=?ID_SEARCH}, - State = #state{sizer=Sz, search=Search}) -> + State = #state{grid=Grid, sizer=Sz, search=Search, selected=Index}) -> + is_integer(Index) andalso + wxListCtrl:setItemState(Grid, Index, 0, ?wxLIST_STATE_FOCUSED), wxSizer:show(Sz, Search#search.win), wxWindow:setFocus(Search#search.search), wxSizer:layout(Sz), @@ -321,7 +324,7 @@ handle_event(#wx{id=?SEARCH_ENTRY, event=#wxCommand{type=command_text_enter,cmdS Pid ! {mark_search_hit, false}, case search(Pid, Str, Pos, Dir, Case) of false -> - wxStatusBar:setStatusText(SB, "Not found"), + wxStatusBar:setStatusText(SB, io_lib:format("Not found (regexp): ~s",[Str])), Pid ! {mark_search_hit, Find#find.start}, wxListCtrl:refreshItem(Grid, Find#find.start), {noreply, State#state{search=Search#search{find=Find#find{found=false}}}}; @@ -355,7 +358,7 @@ handle_event(#wx{id=?SEARCH_ENTRY, event=#wxCommand{cmdString=Str}}, Pid ! {mark_search_hit, false}, case search(Pid, Str, Cont#find.start, Dir, Case) of false -> - wxStatusBar:setStatusText(SB, "Not found"), + wxStatusBar:setStatusText(SB, io_lib:format("Not found (regexp): ~s",[Str])), {noreply, State}; Row -> wxListCtrl:ensureVisible(Grid, Row), @@ -402,8 +405,11 @@ handle_info({new_cols, New}, State = #state{grid=Grid, columns=Cols0}) -> Cols = add_columns(Grid, Cols0, New), {noreply, State#state{columns=Cols}}; +handle_info({refresh, Min, Min}, State = #state{grid=Grid}) -> + wxListCtrl:refreshItem(Grid, Min), %% Avoid assert in wx below if Max is 0 + {noreply, State}; handle_info({refresh, Min, Max}, State = #state{grid=Grid}) -> - Max > 0 andalso wxListCtrl:refreshItems(Grid, Min, Max), + wxListCtrl:refreshItems(Grid, Min, Max), {noreply, State}; handle_info(refresh_interval, State = #state{pid=Pid}) -> @@ -426,10 +432,14 @@ handle_info(_Event, State) -> terminate(_Event, #state{pid=Pid, attrs=Attrs}) -> %% ListItemAttr are not auto deleted - #attrs{odd=Odd, deleted=D, changed=Ch, searched=S} = Attrs, - wxListItemAttr:destroy(Odd), + #attrs{odd=Odd, even=Even, deleted=D, searched=S, + changed_odd=Ch1, changed_even=Ch2, + new_odd=New1, new_even=New2 + } = Attrs, + wxListItemAttr:destroy(Odd), wxListItemAttr:destroy(Even), wxListItemAttr:destroy(D), - wxListItemAttr:destroy(Ch), + wxListItemAttr:destroy(Ch1),wxListItemAttr:destroy(Ch2), + wxListItemAttr:destroy(New1),wxListItemAttr:destroy(New2), wxListItemAttr:destroy(S), unlink(Pid), exit(Pid, window_closed), @@ -473,7 +483,7 @@ search(Table, Str, Row, Dir, Case) -> end. -record(holder, {node, parent, pid, - table=[], n=0, columns, + table=array:new(), n=0, columns, temp=[], search, source, tabid, @@ -507,6 +517,7 @@ table_holder(S0 = #holder{parent=Parent, pid=Pid, table=Table}) -> S1 = handle_new_data_chunk(Data, S0), table_holder(S1); {sort, Col} -> + Parent ! {refresh, 0, S0#holder.n-1}, table_holder(sort(Col, S0)); {search, Data} -> table_holder(search(Data, S0)); @@ -530,11 +541,14 @@ table_holder(S0 = #holder{parent=Parent, pid=Pid, table=Table}) -> table_holder(S0); What -> io:format("Table holder got ~p~n",[What]), + Parent ! {refresh, 0, S0#holder.n-1}, table_holder(S0) end. handle_new_data_chunk(Data, S0 = #holder{columns=Cols, parent=Parent}) -> - S1 = #holder{columns=NewCols} = handle_new_data_chunk2(Data, S0), + S1 = #holder{n=N,columns=NewCols} = handle_new_data_chunk2(Data, S0), + Parent ! {no_rows, N}, + Parent ! {refresh, 0, N-1}, case NewCols =:= Cols of true -> S1; false -> @@ -543,15 +557,12 @@ handle_new_data_chunk(Data, S0 = #holder{columns=Cols, parent=Parent}) -> end. handle_new_data_chunk2('$end_of_table', - S0 = #holder{parent=Parent, sort=Opt, - key=Key, + S0 = #holder{sort=Opt0, key=Key, table=Old, temp=New}) -> - Table = merge(Old, New, Key), - N = length(Table), - Parent ! {no_rows, N}, - sort(Opt#opt.sort_key, S0#holder{n=N, pid=undefine, - sort=Opt#opt{sort_key = undefined}, - table=Table, temp=[]}); + Merged = merge(array:to_list(Old), New, Key), + {Opt,Sorted} = sort(Opt0#opt.sort_key, Opt0#opt{sort_key = undefined}, Merged), + SortedA = array:from_list(Sorted), + S0#holder{sort=Opt, table=SortedA, n=array:size(SortedA), temp=[], pid=undefined}; handle_new_data_chunk2(Data, S0 = #holder{columns=Cols0, source=ets, temp=Tab0}) -> {Tab, Cols} = parse_ets_data(Data, Cols0, Tab0), S0#holder{columns=Cols, temp=Tab}; @@ -566,10 +577,9 @@ parse_ets_data([Recs|Rs], C0, Tab0) -> parse_ets_data([], Cols, Tab) -> {Tab, Cols}. -sort(Col, S=#holder{n=N, parent=Parent, sort=Opt0, table=Table0}) -> - {Opt, Table} = sort(Col, Opt0, Table0), - Parent ! {refresh, 0, N-1}, - S#holder{sort=Opt, table=Table}. +sort(Col, S=#holder{sort=Opt0, table=Table0}) -> + {Opt, Table} = sort(Col, Opt0, array:to_list(Table0)), + S#holder{sort=Opt, table=array:from_list(Table)}. sort(Col, Opt = #opt{sort_key=Col, sort_incr=Bool}, Table) -> {Opt#opt{sort_incr=not Bool}, lists:reverse(Table)}; @@ -597,7 +607,7 @@ keysort(Col, Table) -> lists:sort(Sort, Table). search([Str, Row, Dir0, CaseSens], - S=#holder{parent=Parent, table=Table0}) -> + S=#holder{parent=Parent, n=N, table=Table}) -> Opt = case CaseSens of true -> []; false -> [caseless] @@ -607,32 +617,26 @@ search([Str, Row, Dir0, CaseSens], false -> -1 end, Res = case re:compile(Str, Opt) of - {ok, Re} -> - Table = - case Dir0 of - true -> - lists:nthtail(Row, Table0); - false -> - lists:reverse(lists:sublist(Table0, Row+1)) - end, - search(Row, Dir, Re, Table); + {ok, Re} -> re_search(Row, Dir, N, Re, Table); {error, _} -> false end, Parent ! {self(), Res}, S#holder{search=Res}. -search(Row, Dir, Re, [ [Term|_] |Table]) -> +re_search(Row, Dir, N, Re, Table) when Row >= 0, Row < N -> + [Term|_] = array:get(Row, Table), Str = format(Term), Res = re:run(Str, Re), case Res of - nomatch -> search(Row+Dir, Dir, Re, Table); - {match,_} -> Row + nomatch -> re_search(Row+Dir, Dir, N, Re, Table); + {match,_} -> + Row end; -search(_, _, _, []) -> +re_search(_, _, _, _, _) -> false. get_row(From, Row, Col, Table) -> - case lists:nth(Row+1, Table) of + case array:get(Row, Table) of [Object|_] when Col =:= all -> From ! {self(), format(Object)}; [Object|_] when Col =:= all_multiline -> @@ -647,14 +651,15 @@ get_attr(From, Row, #holder{attrs=Attrs, search=Row}) -> What = Attrs#attrs.searched, From ! {self(), What}; get_attr(From, Row, #holder{table=Table, attrs=Attrs}) -> - What = case lists:nth(Row+1, Table) of + Odd = (Row rem 2) > 0, + What = case array:get(Row, Table) of [_|deleted] -> Attrs#attrs.deleted; - [_|changed] -> Attrs#attrs.changed; - [_|new] -> Attrs#attrs.changed; - _ when (Row rem 2) > 0 -> - Attrs#attrs.odd; - _ -> - Attrs#attrs.even + [_|changed] when Odd -> Attrs#attrs.changed_odd; + [_|changed] -> Attrs#attrs.changed_even; + [_|new] when Odd -> Attrs#attrs.new_odd; + [_|new] -> Attrs#attrs.new_even; + _ when Odd -> Attrs#attrs.odd; + _ -> Attrs#attrs.even end, From ! {self(), What}. @@ -665,19 +670,29 @@ merge(Old, New, Key) -> merge2([[Obj|_]|Old], [Obj|New], Key) -> [[Obj]|merge2(Old, New, Key)]; -merge2([[A|_]|Old], [B|New], Key) +merge2([[A|Op]|Old], [B|New], Key) when element(Key, A) == element(Key, B) -> - [[B|changed]|merge2(Old, New, Key)]; -merge2([[A|_]|Old], New = [B|_], Key) + case Op of + deleted -> + [[B|new]|merge2(Old, New, Key)]; + _ -> + [[B|changed]|merge2(Old, New, Key)] + end; +merge2([[A|Op]|Old], New = [B|_], Key) when element(Key, A) < element(Key, B) -> - [[A|deleted]|merge2(Old, New, Key)]; + case Op of + deleted -> merge2(Old, New, Key); + _ -> [[A|deleted]|merge2(Old, New, Key)] + end; merge2(Old = [[A|_]|_], [B|New], Key) when element(Key, A) > element(Key, B) -> [[B|new]|merge2(Old, New, Key)]; merge2([], New, _Key) -> [[N|new] || N <- New]; merge2(Old, [], _Key) -> - [[O|deleted] || [O|_] <- Old]. + lists:foldl(fun([_O|deleted], Acc) -> Acc; + ([O|_], Acc) -> [[O|deleted]|Acc] + end, [], Old). delete_row(Row, S0 = #holder{parent=Parent}) -> @@ -691,7 +706,7 @@ delete_row(Row, S0 = #holder{parent=Parent}) -> delete(Row, #holder{tabid=Id, table=Table, source=Source, node=Node}) -> - [Object|_] = lists:nth(Row+1, Table), + [Object|_] = array:get(Row, Table), try case Source of ets -> |