1%%
2%% %CopyrightBegin%
3%%
4%% Copyright Ericsson AB 2012-2018. All Rights Reserved.
5%%
6%% Licensed under the Apache License, Version 2.0 (the "License");
7%% you may not use this file except in compliance with the License.
8%% You may obtain a copy of the License at
9%%
10%%     http://www.apache.org/licenses/LICENSE-2.0
11%%
12%% Unless required by applicable law or agreed to in writing, software
13%% distributed under the License is distributed on an "AS IS" BASIS,
14%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15%% See the License for the specific language governing permissions and
16%% limitations under the License.
17%%
18%% %CopyrightEnd%
19-module(observer_perf_wx).
20
21-export([start_link/3]).
22
23%% wx_object callbacks
24-export([init/1, handle_info/2, terminate/2, code_change/3, handle_call/3,
25	 handle_event/2, handle_sync_event/3, handle_cast/2]).
26
27%% Drawing wrappers for DC and GC areas
28-export([make_win/4, setup_graph_drawing/1,
29	 refresh_panel/4, precalc/4, add_data/5, interval_dialog/2,
30	 haveGC/0, make_gc/2, destroy_gc/1,
31	 setPen/2, setFont/3, setBrush/2,
32	 strokeLine/5, strokeLines/2, drawRoundedRectangle/6,
33	 drawText/4, getTextExtent/2]).
34
35-behaviour(wx_object).
36-include_lib("wx/include/wx.hrl").
37-include("observer_defs.hrl").
38
39-define(ID_REFRESH_INTERVAL, 102).
40
41-define(BW, 5).
42-define(BH, 5).
43
44-record(state,
45	{
46	  time = #ti{},
47	  active = false,
48	  parent,
49	  samples,        %% Orig data store
50	  wins=[],     %% per window content
51	  panel,
52	  paint,
53	  appmon
54	}).
55
56-define(wxGC, wxGraphicsContext).
57
58-record(paint, {font, small, fg, pen, pen2, pens, dot_pens, usegc = false}).
59
60start_link(Notebook, Parent, Config) ->
61    wx_object:start_link(?MODULE, [Notebook, Parent, Config], []).
62
63init([Notebook, Parent, Config]) ->
64    try
65	Panel = wxPanel:new(Notebook),
66	Main  = wxBoxSizer:new(?wxVERTICAL),
67	MemIO = wxBoxSizer:new(?wxHORIZONTAL),
68
69	CPU = make_win(runq, Panel, Main, ?wxALL),
70	MEM = make_win(memory, Panel, MemIO, ?wxLEFT),
71	IO  = make_win(io, Panel, MemIO, ?wxLEFT bor ?wxRIGHT),
72
73	wxSizer:add(Main, MemIO, [{flag, ?wxEXPAND bor ?wxDOWN},
74				  {proportion, 1}, {border, 5}]),
75	wxWindow:setSizer(Panel, Main),
76	Windows = [CPU, MEM, IO],
77	PaintInfo = setup_graph_drawing(Windows),
78
79	process_flag(trap_exit, true),
80	State0 = #state{parent=Parent,
81			panel =Panel,
82			wins = Windows,
83			paint=PaintInfo,
84			samples=reset_data(),
85                        time=#ti{fetch=maps:get(fetch, Config, ?FETCH_DATA),
86                                 secs=maps:get(secs, Config, ?DISP_SECONDS)}
87		       },
88	{Panel, State0}
89    catch _:Err:Stacktrace ->
90	    io:format("~p crashed ~tp: ~tp~n",[?MODULE, Err, Stacktrace]),
91	    {stop, Err}
92    end.
93
94make_win(Name, Parent, Sizer, Border) ->
95    Style = ?wxFULL_REPAINT_ON_RESIZE bor ?wxCLIP_CHILDREN,
96    Panel = wxPanel:new(Parent, [{style,Style}]),
97    Opts = [{flag, ?wxEXPAND bor Border}, {proportion, 1}, {border, 5}],
98    wxSizer:add(Sizer, Panel, Opts),
99    #win{name=Name, panel=Panel}.
100
101setup_graph_drawing(Panels) ->
102    Do = fun(#win{panel=Panel}) ->
103		 wxWindow:setBackgroundStyle(Panel, ?wxBG_STYLE_PAINT),
104		 wxPanel:connect(Panel, paint, [callback])
105	 end,
106    _ = [Do(Panel) || Panel <- Panels],
107    UseGC = haveGC(),
108    Version28 = ?wxMAJOR_VERSION =:= 2 andalso ?wxMINOR_VERSION =:= 8,
109    Scale = observer_wx:get_scale(),
110    {Font, SmallFont}
111	= if UseGC, Version28 ->
112		  %% Def font is really small when using Graphics contexts in 2.8
113		  %% Hardcode it
114		  F = wxFont:new(Scale * 12,?wxFONTFAMILY_DECORATIVE,?wxFONTSTYLE_NORMAL,?wxFONTWEIGHT_BOLD),
115		  SF = wxFont:new(Scale * 10, ?wxFONTFAMILY_DECORATIVE, ?wxFONTSTYLE_NORMAL, ?wxFONTWEIGHT_NORMAL),
116		  {F, SF};
117	     true ->
118		  DefFont = wxSystemSettings:getFont(?wxSYS_DEFAULT_GUI_FONT),
119		  DefSize = wxFont:getPointSize(DefFont),
120		  DefFamily = wxFont:getFamily(DefFont),
121		  F = wxFont:new(Scale * (DefSize-1), DefFamily, ?wxFONTSTYLE_NORMAL, ?wxFONTWEIGHT_BOLD),
122		  SF = wxFont:new(Scale * (DefSize-2), DefFamily, ?wxFONTSTYLE_NORMAL, ?wxFONTWEIGHT_NORMAL),
123		  {F, SF}
124	  end,
125    BG = wxWindow:getBackgroundColour((hd(Panels))#win.panel),
126    Fg = case observer_lib:is_darkmode(BG) of
127             false -> {0,0,0};
128             true  -> wxSystemSettings:getColour(?wxSYS_COLOUR_BTNTEXT)
129         end,
130
131    PenColor = case observer_lib:is_darkmode(BG) of
132                   false -> {0,0,0};
133                   true  -> {0,0,0}
134               end,
135    Pens = [wxPen:new(Col, [{width, Scale}, {style, ?wxSOLID}])
136            || Col <- tuple_to_list(colors())],
137    DotPens = [wxPen:new(Col, [{width, Scale}, {style, ?wxDOT}])
138               || Col <- tuple_to_list(colors())],
139    #paint{usegc = UseGC,
140	   font  = Font,
141	   small = SmallFont,
142           fg    = Fg,  %% Text color
143	   pen   = wxPen:new(PenColor),
144	   pen2  = wxPen:new(PenColor, [{width, Scale}]),
145	   pens  = list_to_tuple(Pens),
146           dot_pens = list_to_tuple(DotPens)
147	  }.
148
149
150%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
151handle_event(#wx{id=?ID_REFRESH_INTERVAL, event=#wxCommand{type=command_menu_selected}},
152	     #state{panel=Panel, appmon=Old, wins=Wins0, time=#ti{fetch=F0} = Ti0} = State) ->
153    case interval_dialog(Panel, Ti0) of
154	Ti0 -> {noreply, State};
155	#ti{fetch=F0} = Ti -> %% Same fetch interval force refresh
156	    Wins = [W#win{max=undefined} || W <- Wins0],
157	    {noreply, precalc(State#state{time=Ti, wins=Wins})};
158	Ti when Old =:= undefined ->
159	    {noreply, State#state{time=Ti}};
160	Ti -> %% Changed fetch interval, drop all data
161	    {noreply, restart_fetcher(node(Old), State#state{time=Ti})}
162    end;
163handle_event(#wx{event=#wxCommand{type=command_menu_selected}},
164	     State = #state{}) ->
165    {noreply, State};
166
167handle_event(Event, _State) ->
168    error({unhandled_event, Event}).
169
170%%%%%%%%%%
171handle_sync_event(#wx{obj=Panel, event = #wxPaint{}},_,
172		  #state{active=Active, time=Ti, paint=Paint, wins=Windows}) ->
173    %% Sigh workaround bug on MacOSX (Id in paint event is always 0)
174    %% Panel = element(Id, Windows),
175    Win = lists:keyfind(Panel, #win.panel, Windows),
176    refresh_panel(Active, Win, Ti, Paint),
177    ok.
178
179refresh_panel(Active, #win{name=_Id, panel=Panel}=Win, Ti, #paint{usegc=UseGC}=Paint) ->
180    %% PaintDC must be created in a callback to work on windows.
181    %% Nothing is drawn until wxPaintDC is destroyed.
182    GC = make_gc(Panel, UseGC),
183    if Active -> draw_win(GC, Win, Ti, Paint);
184       true ->   ignore
185    end,
186    destroy_gc(GC).
187
188%%%%%%%%%%
189handle_call(get_config, _, #state{time=Ti}=State) ->
190    #ti{fetch=Fetch, secs=Range} = Ti,
191    {reply, #{fetch=>Fetch, secs=>Range}, State};
192
193handle_call(Event, From, _State) ->
194    error({unhandled_call, Event, From}).
195
196handle_cast(Event, _State) ->
197    error({unhandled_cast, Event}).
198%%%%%%%%%%
199handle_info({stats, 1, _, _, _} = Stats,
200	    #state{panel=Panel, samples=Data, active=Active, wins=Wins0,
201                   appmon=Node, time=#ti{tick=Tick, disp=Disp0}=Ti} = State0) ->
202    if Active ->
203	    Disp = trunc(Disp0),
204	    Next = max(Tick - Disp, 0),
205	    erlang:send_after(1000 div ?DISP_FREQ, self(), {refresh, Next}),
206	    {Wins, Samples} = add_data(Stats, Data, Wins0, Ti, Active, Node),
207	    State = precalc(State0#state{time=Ti#ti{tick=Next}, wins=Wins, samples=Samples}),
208	    wxWindow:refresh(Panel),
209	    {noreply, State};
210       true ->
211	    {Wins1, Samples} = add_data(Stats, Data, Wins0, Ti, Active, Node),
212	    Wins = [W#win{max=undefined} || W <- Wins1],
213	    {noreply, State0#state{samples=Samples, wins=Wins, time=Ti#ti{tick=0}}}
214    end;
215
216handle_info({refresh, Seq}, #state{panel=Panel, time=#ti{tick=Seq, disp=DispF}=Ti} = State0)
217  when (Seq+1) < (DispF*1.5) ->
218    Next = Seq+1,
219    erlang:send_after(1000 div ?DISP_FREQ, self(), {refresh, Next}),
220    State = precalc(State0#state{time=Ti#ti{tick=Next}}),
221    catch wxWindow:refresh(Panel),
222    {noreply, State};
223handle_info({refresh, _}, State) ->
224    {noreply, State};
225
226handle_info({active, Node}, #state{parent=Parent, panel=Panel, appmon=Old} = State) ->
227    create_menus(Parent, []),
228    try
229	Node = node(Old),
230	wxWindow:refresh(Panel),
231	erlang:send_after(1000 div ?DISP_FREQ, self(), {refresh, 0}),
232	{noreply, State#state{active=true}}
233    catch _:_ ->
234	    {noreply,restart_fetcher(Node, State)}
235    end;
236
237handle_info(not_active, State = #state{appmon=_Pid}) ->
238    %% Pid ! exit,
239    {noreply, State#state{active=false}};
240
241handle_info({'EXIT', Old, _}, State = #state{appmon=Old}) ->
242    {noreply, State#state{active=false, appmon=undefined}};
243
244handle_info(_Event, State) ->
245    %% io:format("~p:~p: ~tp~n",[?MODULE,?LINE,_Event]),
246    {noreply, State}.
247
248%%%%%%%%%%
249terminate(_Event, #state{appmon=Pid}) ->
250    catch Pid ! exit,
251    ok.
252code_change(_, _, State) ->
253    State.
254
255restart_fetcher(Node, #state{appmon=Old, panel=Panel, time=#ti{fetch=Freq}=Ti, wins=Wins0}=State) ->
256    catch Old ! exit,
257    Me = self(),
258    Pid = spawn_link(Node, observer_backend, fetch_stats, [Me, round(1000/Freq)]),
259    wxWindow:refresh(Panel),
260    Wins = [W#win{state=undefined} || W <- Wins0],
261    precalc(State#state{active=true, appmon=Pid, samples=reset_data(),
262			wins=Wins, time=Ti#ti{tick=0}}).
263
264reset_data() ->
265    {0, queue:new()}.
266
267add_data(Stats, Q, Wins, Ti, Active) ->
268    add_data(Stats, Q, Wins, Ti, Active, ignore).
269
270add_data(Stats, {N, Q0}, Wins, #ti{fetch=Fetch, secs=Secs}, Active, Node)
271  when N > (Secs*Fetch+1) ->
272    {{value, Drop}, Q} = queue:out(Q0),
273    add_data_1(Wins, Stats, N, {Drop,Q}, Active, Node);
274add_data(Stats, {N, Q}, Wins, _, Active, Node) ->
275    add_data_1(Wins, Stats, N+1, {empty, Q}, Active, Node).
276
277add_data_1([#win{state={_,St}}|_]=Wins0, Last, N, {Drop, Q}, Active, Node)
278  when St /= undefined ->
279    try
280	{Wins, Stat} =
281	    lists:mapfoldl(fun(Win0, Entry) ->
282				   {Win1,Stat} = add_data_2(Win0, Last, Entry),
283				   case Active of
284				       true ->
285					   Win = add_data_3(Win1, N, Drop, Stat, Q),
286					   {Win, Stat};
287				       false ->
288					   {Win1, Stat}
289				   end
290			   end, #{}, Wins0),
291	{Wins, {N,queue:in(Stat#{}, Q)}}
292    catch no_scheduler_change ->
293	    {[Win#win{state=init_data(Id, Last), info=info(Id, Last, Node)}
294	      || #win{name=Id}=Win <- Wins0], {0,queue:new()}}
295    end;
296
297add_data_1(Wins, Stats, 1, {_, Q}, _, Node) ->
298    {[Win#win{state=init_data(Id, Stats), info=info(Id, Stats, Node)}
299      || #win{name=Id}=Win <- Wins], {0,Q}}.
300
301add_data_2(#win{name=Id, state=S0}=Win, Stats, Map) ->
302    {V1, S1} = collect_data(Id, Stats, S0),
303    {Win#win{state=S1}, Map#{Id=>V1}}.
304
305add_data_3(#win{name=Id, max={{OldMax, OldEntry},_,_,_},
306		geom=#{scale:={WS,HS}}, state={Max,_},
307		graphs=Graphs}=Win,
308	   N, Drop0, Last, Q1)
309  when N > 3 ->
310    Drop = case Drop0 of
311	       #{Id:=D} -> D;
312	       _ -> Drop0
313	   end,
314    case {max_value(Max), Drop =:= OldEntry} of
315	{OldMax, false} ->
316	    #{Id:=V4} = Last,
317	    {{value, #{Id:=V3}},Q2} = queue:out_r(Q1),
318	    {{value, #{Id:=V2}},Q3} = queue:out_r(Q2),
319	    {{value, #{Id:=V1}},_}  = queue:out_r(Q3),
320	    Vals = [V1,V2,V3,V4],
321	    Gs = tuple_size(V1),
322	    Info = lists:zip(lists:seq(Gs, 1, -1), Graphs),
323	    Lines = [add_lines(Vals, Drop, Prev, I, WS, HS) || {I, Prev} <- Info],
324	    Win#win{graphs=Lines, no_samples=N};
325	_W -> %% Max changed Trigger complete recalc
326	    Win#win{max=undefined}
327    end;
328add_data_3(Win, _, _, _,_) ->
329    %% Trigger complete recalc
330    Win#win{max=undefined}.
331
332%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
333
334create_menus(Parent, _) ->
335    View = {"View", [#create_menu{id = ?ID_REFRESH_INTERVAL, text = "Graph Settings"}]},
336    observer_wx:create_menus(Parent, [{"File", []}, View]).
337
338interval_dialog(Parent0, #ti{fetch=Fetch0, secs=Secs0}=Ti) ->
339    Parent = observer_lib:get_wx_parent(Parent0),
340    Dialog = wxDialog:new(Parent, ?wxID_ANY, "Load Chart Settings",
341			  [{style, ?wxDEFAULT_DIALOG_STYLE bor
342				?wxRESIZE_BORDER}]),
343    {Sl1,FetchSl} = slider(Dialog, "Sample (ms)", trunc(1000 / Fetch0), 100, 10000),
344    {Sl2, SecsSl} = slider(Dialog, "Length (min)", Secs0 div 60, 1, 10),
345    TopSizer = wxBoxSizer:new(?wxVERTICAL),
346    Flags = [{flag, ?wxEXPAND bor ?wxTOP bor ?wxLEFT bor ?wxRIGHT},
347	     {border, 5}, {proportion, 1}],
348    wxSizer:add(TopSizer, Sl1, Flags),
349    wxSizer:add(TopSizer, Sl2, Flags),
350    wxSizer:add(TopSizer, wxDialog:createButtonSizer(Dialog, ?wxOK bor ?wxCANCEL), Flags),
351    wxWindow:setSizerAndFit(Dialog, TopSizer),
352    wxSizer:setSizeHints(TopSizer, Dialog),
353    Res = case wxDialog:showModal(Dialog) of
354	      ?wxID_OK ->
355		  Fetch = 1000 / wxSlider:getValue(FetchSl),
356		  Secs = wxSlider:getValue(SecsSl) * 60,
357		  Ti#ti{fetch=Fetch, secs=Secs, disp=?DISP_FREQ/Fetch};
358	      ?wxID_CANCEL ->
359		  Ti
360	  end,
361    wxDialog:destroy(Dialog),
362    Res.
363
364slider(Parent, Str, Value, Min, Max) ->
365    Sz = wxBoxSizer:new(?wxHORIZONTAL),
366    Center = [{flag, ?wxALIGN_CENTER_VERTICAL}],
367    wxSizer:add(Sz, wxStaticText:new(Parent, ?wxID_ANY, Str), [{proportion, 1}|Center]),
368    Opt = [{style, ?wxSL_HORIZONTAL bor ?wxSL_LABELS}],
369    Slider = wxSlider:new(Parent, ?wxID_ANY, Value, Min, Max, Opt),
370    wxSizer:add(Sz, Slider, [{proportion, 2}|Center]),
371    case Min > 1 of
372	false ->
373	    {Sz, Slider};
374	true ->
375	    CB = fun(#wx{event=Ev},_) -> step(Ev, Slider, Min) end,
376	    wxSlider:connect(Slider, scroll_thumbtrack, [{callback, CB}]),
377	    wxSlider:connect(Slider, scroll_changed, [{callback, CB}]),
378	    {Sz, Slider}
379    end.
380
381step(_Ev = #wxScroll{commandInt=Value}, Slider, Min) ->
382    Val = Min * round(Value / Min),
383    wxSlider:setValue(Slider, Val),
384    ok.
385
386%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
387
388mk_max() -> {0, undefined}.
389max_value({Max,_}) -> Max.
390%% max_data({_,Data}) -> Data. matched in function head
391
392lmax(MState, Tuple, Tuple) when is_tuple(Tuple) ->
393    lmax(MState, tuple_to_list(Tuple), Tuple);
394lmax(MState, Values, State) ->
395    Max = max_value(MState),
396    New = lists:max([Max|Values]),
397    case New >= Max of
398	false -> MState;
399	true -> {New, State}
400    end.
401
402init_data(runq, {stats, _, T0, _, _}) -> {mk_max(),lists:sort(T0)};
403init_data(io,   {stats, _, _, {{_,In0}, {_,Out0}}, _}) -> {mk_max(), {In0,Out0}};
404init_data(memory, _) -> {mk_max(), info(memory, undefined, undefined)};
405init_data(alloc, _) -> {mk_max(), unused};
406init_data(utilz, _) -> {mk_max(), unused}.
407
408info(runq, {stats, _, T0, _, _}, Node) ->
409    Dirty = get_dirty_cpu(Node),
410    {lists:seq(1, length(T0)-Dirty), Dirty};
411info(memory, _, _) -> [total, processes, atom, binary, code, ets];
412info(io, _, _) -> [input, output];
413info(alloc, First, _) -> [Type || {Type, _, _} <- First];
414info(utilz, First, _) -> [Type || {Type, _, _} <- First];
415info(_, [], _) -> [].
416
417get_dirty_cpu(Node) ->
418    case rpc:call(node(Node), erlang, system_info, [dirty_cpu_schedulers]) of
419        {badrpc,_R} -> 0;
420        N -> N
421    end.
422
423collect_data(runq, {stats, _, T0, _, _}, {Max,S0}) ->
424    S1 = lists:sort(T0),
425    Delta = calc_delta(S1, S0),
426    Sample = list_to_tuple(Delta),
427    {Sample, {lmax(Max,Delta,Sample), S1}};
428collect_data(io, {stats, _, _, {{_,In0}, {_,Out0}}, _}, {Max,{PIn,POut}}) ->
429    In = In0-PIn,
430    Out = Out0-POut,
431    Sample = {In, Out},
432    {Sample, {lmax(Max, [In,Out], Sample), {In0, Out0}}};
433collect_data(memory, {stats, _, _, _, MemInfo}, {Max, MemTypes}) ->
434    Vs = [Value || {Type,Value} <- MemInfo, lists:member(Type, MemTypes)],
435    Sample = list_to_tuple(Vs),
436    {Sample, {lmax(Max, Vs, Sample),MemTypes}};
437collect_data(alloc, MemInfo, Max) ->
438    Vs = [Carrier || {_Type,_Block,Carrier} <- MemInfo],
439    Sample = list_to_tuple(Vs),
440    {Sample, {lmax(Max, Vs, Sample),unused}};
441collect_data(utilz, MemInfo, Max) ->
442    Vs = [round(100*Block/Carrier) || {_Type,Block,Carrier} <- MemInfo],
443    Sample = list_to_tuple(Vs),
444    {Sample, {lmax(Max,Vs,Sample),unused}}.
445
446calc_delta([{Id, WN, TN}|Ss], [{Id, WP, TP}|Ps]) ->
447    [100*(WN-WP) div (TN-TP)|calc_delta(Ss, Ps)];
448calc_delta([], []) -> [];
449calc_delta(_, _) -> throw(no_scheduler_change).
450
451precalc(#state{samples=Data0, paint=Paint, time=Ti, wins=Wins0}=State) ->
452    Wins = [precalc(Ti, Data0, Paint, Win) || Win <- Wins0],
453    State#state{wins=Wins}.
454
455precalc(Ti, {NoSamples,Q}, Paint, #win{name=Id, panel=Panel}=Win) ->
456    Size = wxWindow:getClientSize(Panel),
457    case Win of
458	#win{max=Max, no_samples=NoSamples, size=Size} when is_tuple(Max) ->
459	    Win;
460	_SomeThingChanged ->
461	    Hs = [Vals || #{Id:=Vals} <- queue:to_list(Q)],
462	    Max = lists:foldl(fun(Vals,Max) -> lmax(Max, Vals, Vals) end,
463			      mk_max(), Hs),
464	    MaxDisp = calc_max(Id, Max),
465	    #{scale:={WS,HS}} = Props = window_geom(Size, MaxDisp, Ti, Panel, Paint),
466	    NoGraphs = try tuple_size(hd(Hs)) catch _:_ -> 0 end,
467	    Graphs = [make_lines(Hs, I, WS, HS) || I <- lists:seq(NoGraphs, 1, -1)],
468	    State = case Win#win.state of
469			undefined -> {Max, undefined};
470			{_, St} -> {Max, St}
471		    end,
472	    Win#win{geom=Props, size=Size,
473		    max=MaxDisp,
474		    graphs=Graphs,
475		    no_samples=NoSamples,
476		    state=State}
477    end.
478
479window_geom({W,H}, {_, Max, _Unit, MaxUnit},
480	    #ti{secs=Secs, fetch=FetchFreq},
481	    Panel, #paint{font=Font}) ->
482    Str1 = observer_lib:to_str(MaxUnit),
483    Str2 = observer_lib:to_str(MaxUnit div 2),
484    Str3 = observer_lib:to_str(0),
485    {TW,TH,_,_} = wxWindow:getTextExtent(Panel, Str1, [{theFont, Font}]),
486    {SpaceW, _,_,_} = wxWindow:getTextExtent(Panel, "W", [{theFont, Font}]),
487    X0 = ?BW+TW+?BW,
488    X1 = W-?BW*4,
489    MaxTextY = TH+?BH,
490    BottomTextY = H-?BH-TH,
491    Y0 = MaxTextY + (TH / 2),
492    Y1 = BottomTextY - TH - ?BH,
493
494    ScaleW = (X1-X0-1)/(Secs*FetchFreq),
495    ScaleH = (Y1-Y0-1) / Max,
496    #{p0=>{X0,Y0}, p1=>{X1,Y1}, scale=>{ScaleW, ScaleH},
497      txsz=>{TW,TH,SpaceW}, txt=>{BottomTextY, MaxTextY}, strs=>{Str1,Str2,Str3}}.
498
499
500%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
501draw_win(DC, #win{name=Name, no_samples=Samples, geom=#{scale:={WS,HS}},
502                  graphs=Graphs, max={_,Max,_,_}, info=Info}=Win,
503	 #ti{tick=Tick, fetch=FetchFreq, secs=Secs, disp=DispFreq}=Ti,
504	 Paint=#paint{pens=Pens, dot_pens=Dots}) when Samples >= 2, Graphs =/= [] ->
505    %% Draw graphs
506    {X0,Y0,DrawBs} = draw_borders(DC, Ti, Win, Paint),
507    Offset = Tick / DispFreq,
508    Full = case Samples > (1+Secs*FetchFreq) of
509	       true -> 1;
510	       false -> 2
511	   end,
512    Start = X0 + (max(Secs*FetchFreq+Full-Samples, 0) - Offset)*WS,
513    Last = Secs*FetchFreq*WS+X0,
514    Dirty = case {Name, Info} of
515                {runq, {_, DCpu}} -> DCpu;
516                _ -> 0
517            end,
518    NoGraphs = length(Graphs),
519    NoCpu = NoGraphs - Dirty,
520    Draw = fun(Lines0, N) ->
521                   case Dirty > 0 andalso N > NoCpu of
522                       true  -> setPen(DC, element(1+ ((N-NoCpu-1) rem tuple_size(Dots)), Dots));
523                       false -> setPen(DC, element(1+ ((N-1) rem tuple_size(Pens)), Pens))
524                   end,
525		   Order = lists:reverse(Lines0),
526		   [{_,Y}|Lines] = translate(Order, {Start, Y0}, 0, WS, {X0,Max*HS,Last}, []),
527		   strokeLines(DC, [{Last,Y}|Lines]),
528		   N-1
529	   end,
530    lists:foldl(Draw, NoGraphs, Graphs),
531    DrawBs(),
532    ok;
533
534draw_win(DC, #win{no_samples=Samples} = Win,Ti, #paint{fg=Fg, small=Small}=Paint) ->
535    %% Draw Error Msg
536    try draw_borders(DC, Ti, Win, Paint) of
537	{X0,_Y0,DrawBs} ->
538	    Text = case Samples =< 1 of
539		       true  -> "Waiting for data";
540		       false -> "Information not available"
541		   end,
542	    setFont(DC, Small, Fg),
543	    {_,WW} = getSize(DC),
544	    drawText(DC, Text, X0 + 100, WW div 2),
545	    DrawBs(),
546	    ok
547    catch _:_ -> %% Early redraws fail
548	    ok
549    end.
550
551translate([{X0,Y}|Rest], {Sx,Sy}=Start, N, WS, {Cx,Cy,Cw}=Clip, Acc) ->
552    X = min((N-X0)*WS+Sx,Cw),
553    Next = if X0 > 0 -> N; true -> N+1 end,
554    case X =< Cx of
555	true ->
556	    translate(Rest, Start, Next, WS, Clip, [{Cx,Sy-min(Cy,Y)}]);
557	false ->
558	    translate(Rest, Start, Next, WS, Clip, [{X,Sy-min(Cy,Y)}|Acc])
559    end;
560translate([], _, _, _, _, Acc) ->
561    Acc.
562
563add_lines(Vals, Drop, OldLines, I, WS, HS) ->
564    Lines = strip(OldLines, Drop, 2),
565    New = make_lines(Vals, I, WS, HS),
566    New ++ Lines.
567
568strip([{X,_}|Rest], Drop, N) when X > 0.0001, N > 0 ->
569    strip(Rest, Drop, N);
570strip([_|Rest], Drop, N) when N > 0 ->
571    strip(Rest, Drop, N-1);
572strip(List, empty, _) -> List;
573strip(List, _, _) ->
574    lists:reverse(strip(lists:reverse(List), empty, 1)).
575
576make_lines(Ds = [Data|_], N, WS, HS) ->
577    Y = element(N,Data),
578    make_lines(Ds, N, WS, HS, Y, []).
579
580make_lines([D1 | Ds = [D2|Rest]], N, WS, HS, Y0, Acc0) ->
581    Y1 = element(N,D1),
582    Y2 = element(N,D2),
583    Y3 = case Rest of
584	     [D3|_] -> element(N,D3);
585	     [] -> Y2
586	 end,
587    This = {0, Y1*HS},
588    Acc = if (abs(Y1-Y2) * HS) < 3.0 -> [This|Acc0];
589	     WS < 3.0 -> [This|Acc0];
590	     true -> make_splines(Y0,Y1,Y2,Y3,WS,HS,[This|Acc0])
591	  end,
592    make_lines(Ds, N, WS, HS, Y1, Acc);
593make_lines([_D1], _N, _WS, _HS, _Y0, Acc) ->
594    Acc.
595
596make_splines(Y00,Y10,Y20,Y30,WS,HS,Acc) ->
597    Y1 = Y10*HS,
598    Y2 = Y20*HS,
599    Steps = min(abs(Y1-Y2), WS/2),
600    if Steps > 2 ->
601	    Y0 = Y00*HS,
602	    Y3 = Y30*HS,
603	    Tan = spline_tan(Y0,Y1,Y2,Y3),
604	    Delta = 1/Steps,
605	    splines(Steps-1, 0.0, Delta, Tan, Y1,Y2, Acc);
606       true ->
607	    Acc
608    end.
609
610splines(N, XD, XD0, Tan, Y1,Y2, Acc) when N > 0 ->
611    Delta = XD+XD0,
612    Y = max(0, spline(Delta, Tan, Y1,Y2)),
613    splines(N-1, Delta, XD0, Tan, Y1, Y2, [{1.0-Delta, Y}|Acc]);
614splines(_N, _XD, _XD0, _Tan, _Y1,_Y2, Acc) ->
615    Acc.
616
617spline(T, {M1, M2}, Y1, Y2) ->
618    %% Hermite Basis Funcs
619    T2 = T*T,  T3 = T*T*T,
620    H1 = 2*T3-3*T2+1,
621    H2 = -2*T3+3*T2,
622    H3 = T3-2*T2+T,
623    H4 = T3-T2,
624    %% Result
625    M1*H3 + Y1*H1 + Y2*H2 + M2*H4.
626
627spline_tan(Y0, Y1, Y2, Y3) ->
628    S = 1.0,
629    C = 0.5,
630    %% Calc tangent values
631    M1 = S*C*(Y2-Y0),
632    M2 = S*C*(Y3-Y1),
633    {M1,M2}.
634
635draw_borders(DC, #ti{secs=Secs, fetch=FetchFreq},
636	     #win{name=Type, geom=Geom, info=Info, max={_,_,Unit,_}},
637	     #paint{pen=Pen, pen2=Pen2, fg=Fg, font=Font, small=Small}) ->
638    #{p0:={GraphX0, GraphY0}, p1:={GraphX1,GraphY1}, scale:={ScaleW0,_},
639      txsz:={TW,TH,SpaceW}, txt:={BottomTextY, MaxTextY}, strs:={Str1,Str2,Str3}} = Geom,
640
641    ScaleW = ScaleW0*FetchFreq,
642    TopTextX = ?BW*3+TW,
643    SecondsY = BottomTextY - TH,
644
645    GraphY25 = GraphY0 + (GraphY1 - GraphY0) / 4,
646    GraphY50 = GraphY0 + (GraphY1 - GraphY0) / 2,
647    GraphY75 = GraphY0 + 3*(GraphY1 - GraphY0) / 4,
648
649    setFont(DC, Small, Fg),
650    Align = fun(Str, Y) ->
651		    {StrW, _} = getTextExtent(DC, Str),
652		    drawText(DC, Str, GraphX0 - StrW - ?BW, Y)
653	    end,
654    Align(Str1, MaxTextY),
655    Align(Str2, GraphY50 - (TH / 2)),
656    Align(Str3, GraphY1 - (TH / 2) + 1),
657
658    setPen(DC, Pen),
659    DrawSecs = fun(Sec, {Pos, Prev}) ->
660		       Str = observer_lib:to_str(Sec) ++ "s",
661		       X = GraphX0+Pos,
662		       strokeLine(DC, X, GraphY0, X, GraphY1+5),
663		       TxtX = X-SpaceW,
664		       case TxtX > Prev of
665			   true ->
666			       drawText(DC, Str,  TxtX, SecondsY),
667			       TxtW = SpaceW*length(Str),
668			       {Pos + 10*ScaleW, TxtX+TxtW};
669			   false ->
670			       {Pos + 10*ScaleW, Prev}
671		       end
672	       end,
673    lists:foldl(DrawSecs, {0, 0}, lists:seq(Secs,0, -10)),
674
675    strokeLine(DC, GraphX0-3, GraphY25, GraphX1, GraphY25),
676    strokeLine(DC, GraphX0-3, GraphY50, GraphX1, GraphY50),
677    strokeLine(DC, GraphX0-3, GraphY75, GraphX1, GraphY75),
678
679    setFont(DC, Font, Fg),
680
681    Text = fun(X,Y, Str, PenId) ->
682		   if PenId == 0 ->
683			   setFont(DC, Font, Fg);
684		      PenId > 0 ->
685			   Id = 1 + ((PenId-1) rem tuple_size(colors())),
686			   setFont(DC, Font, element(Id, colors()))
687		   end,
688		   drawText(DC, Str, X, Y),
689		   {StrW, _} = getTextExtent(DC, Str),
690		   StrW + X + ?BW*2
691	   end,
692
693    case Type of
694	runq ->
695            {TextInfo, DirtyCpus} = Info,
696	    drawText(DC, "Scheduler Utilization (%) ", TopTextX, ?BH),
697	    TN0 = Text(TopTextX, BottomTextY, "Scheduler: ", 0),
698            Id = fun(Id, Pos0) ->
699                         Text(Pos0, BottomTextY, integer_to_list(Id), Id)
700                 end,
701	    TN1 = lists:foldl(Id, TN0, TextInfo),
702            TN2 = Text(TN1, BottomTextY, "Dirty cpu: ", 0),
703	    TN3 = lists:foldl(Id, TN2, lists:seq(1, DirtyCpus)),
704            _ = Text(TN3, BottomTextY, "(dotted)", 0),
705            ok;
706	memory ->
707	    drawText(DC, "Memory Usage " ++ Unit, TopTextX,?BH),
708	    lists:foldl(fun(MType, {PenId, Pos0}) ->
709				Str = to_string(MType),
710				Pos = Text(Pos0, BottomTextY, Str, PenId),
711				{PenId+1, Pos}
712			end, {1, TopTextX}, Info);
713	io ->
714	    drawText(DC, "IO Usage " ++ Unit, TopTextX,?BH),
715	    lists:foldl(fun(MType, {PenId, Pos0}) ->
716				Str = to_string(MType),
717				Pos = Text(Pos0, BottomTextY, Str, PenId),
718				{PenId+1, Pos}
719			end, {1, TopTextX}, Info);
720	alloc ->
721	    drawText(DC, "Carrier Size " ++ Unit, TopTextX,?BH);
722	utilz ->
723	    drawText(DC, "Carrier Utilization (%)" ++ Unit, TopTextX,?BH),
724	    lists:foldl(fun(MType, {PenId, Pos0}) ->
725				Str = to_string(MType),
726				Pos = Text(Pos0, BottomTextY, Str, PenId),
727				{PenId+1, Pos}
728			end, {1, TopTextX}, Info)
729    end,
730    DrawBorder = fun() ->
731			 setPen(DC, Pen2),
732			 strokeLines(DC, [{GraphX0, GraphY0-1}, {GraphX0, GraphY1+1},
733					  {GraphX1, GraphY1+1}, {GraphX1, GraphY0-1},
734					  {GraphX0, GraphY0-1}])
735		 end,
736    {GraphX0+1, GraphY1, DrawBorder}.
737
738to_string(Atom) ->
739    Name = atom_to_list(Atom),
740    case lists:reverse(Name) of
741	"colla_" ++ Rev ->
742	    uppercase(lists:reverse(Rev));
743	_ ->
744	    uppercase(Name)
745    end.
746
747uppercase([C|Rest]) ->
748    [C-$a+$A|Rest].
749
750calc_max(Type, Max) ->
751    bytes(Type, Max).
752
753bytes(runq, Max) ->
754    Upper = calc_max1(max_value(Max)),
755    {Max, Upper, "", Upper};
756bytes(utilz, Max) ->
757    Upper = calc_max1(max_value(Max)),
758    {Max, Upper, "", Upper};
759bytes(_, Max) ->
760    B = max_value(Max),
761    KB = B div 1024,
762    MB = KB div 1024,
763    GB = MB div 1024,
764    if
765	GB > 10 ->
766	    Upper = calc_max1(GB),
767	    {Max, Upper*1024*1024*1024, "(GB)", Upper};
768	MB > 10 ->
769	    Upper = calc_max1(MB),
770	    {Max, Upper*1024*1024, "(MB)", Upper};
771	KB >  0 ->
772	    Upper = calc_max1(KB),
773	    {Max, Upper*1024, "(KB)", Upper};
774	true  ->
775	    Upper = calc_max1(B),
776	    {Max, Upper, "(B)", Upper}
777    end.
778
779calc_max1(Max) when Max < 10 ->
780    10;
781calc_max1(Max) ->
782    case Max div 10 of
783	X when X < 10 ->
784	    case Max rem 10 of
785		0 -> Max;
786		_ ->
787		    (X+1)*10
788	    end;
789	X ->
790	    10*calc_max1(X)
791    end.
792
793colors() ->
794    {{240, 100, 100}, {0, 128, 0},     {25, 45, 170},  {255, 165, 0},
795     {220, 220, 40},  {100, 240, 240},{240, 100, 240}, {160, 40,  40},
796     {100, 100, 240}, {140, 140, 0},  {25, 200, 100},  {120, 25, 240},
797     {255, 140, 163}, {25, 120, 120}, {120, 25, 120},  {110, 90, 60}
798    }.
799
800%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
801%% wxDC and ?wxGC wrappers
802
803make_gc(Panel,UseGC) ->
804    DC = case os:type() of
805	     {win32, _} ->
806		 %% Ugly hack to avoid flickering on windows, works on windows only
807		 %% But the other platforms are doublebuffered by default
808		 DC0 = wx:typeCast(wxBufferedPaintDC:new(Panel), wxPaintDC),
809		 wxDC:clear(DC0),
810		 DC0;
811	     _ ->
812		 wxPaintDC:new(Panel)
813	 end,
814    if UseGC -> {?wxGC:create(DC), DC};
815       true ->  {false, DC}
816    end.
817
818destroy_gc({GC, DC}) ->
819    (GC =/= false) andalso ?wxGC:destroy(GC),
820    case DC =/= false andalso wx:getObjectType(DC) of
821	false -> ok;
822	Type -> Type:destroy(DC)
823    end.
824
825haveGC() ->
826    try
827	wxGraphicsRenderer:getDefaultRenderer(),
828	true
829    catch _:_  -> false
830    end.
831
832getSize({_, DC}) ->
833    wxDC:getSize(DC).
834
835setPen({false, DC}, Pen) ->
836    wxDC:setPen(DC, Pen);
837setPen({GC, _}, Pen) ->
838    ?wxGC:setPen(GC, Pen).
839
840setFont({false, DC}, Font, Color) ->
841    wxDC:setTextForeground(DC, Color),
842    wxDC:setFont(DC, Font);
843setFont({GC, _}, Font, Color) ->
844    ?wxGC:setFont(GC, Font, Color).
845
846setBrush({false, DC}, Brush) ->
847    wxDC:setBrush(DC, Brush);
848setBrush({GC, _}, Brush) ->
849    ?wxGC:setBrush(GC, Brush).
850
851strokeLine({false, DC}, X0, Y0, X1, Y1) ->
852    wxDC:drawLine(DC, {round(X0), round(Y0)}, {round(X1), round(Y1)});
853strokeLine({GC, _}, X0, Y0, X1, Y1) ->
854    ?wxGC:strokeLine(GC, X0, Y0, X1, Y1).
855
856strokeLines(_, [_]) -> ok;
857strokeLines({false, DC}, Lines) ->
858    wxDC:drawLines(DC, [{round(X), round(Y)} || {X,Y} <- Lines]);
859strokeLines({GC, _}, Lines) ->
860    ?wxGC:strokeLines(GC, Lines).
861
862drawRoundedRectangle({false, DC}, X0, Y0, X1, Y1, R) ->
863    wxDC:drawRoundedRectangle(DC, {round(X0), round(Y0)}, {round(X1), round(Y1)}, round(R));
864drawRoundedRectangle({GC, _}, X0, Y0, X1, Y1, R) ->
865    ?wxGC:drawRoundedRectangle(GC, X0, Y0, X1, Y1, R).
866
867drawText({false, DC}, Str, X, Y) ->
868    wxDC:drawText(DC, Str, {round(X),round(Y)});
869drawText({GC, _}, Str, X, Y) ->
870    ?wxGC:drawText(GC, Str, X, Y).
871
872getTextExtent({false, DC}, Str) ->
873    wxDC:getTextExtent(DC, Str);
874getTextExtent({GC, _}, Str) ->
875    {W,H,_,_} = ?wxGC:getTextExtent(GC, Str),
876    {W,H}.
877