%%
%% %CopyrightBegin%
%%
%% Copyright Ericsson AB 2007-2016. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%
%% %CopyrightEnd%
-module(percept_html).
-export([page/3,
codelocation_page/3,
databases_page/3,
load_database_page/3,
processes_page/3,
concurrency_page/3,
process_info_page/3]).
-export([value2pid/1,
pid2value/1,
get_option_value/2,
join_strings_with/2]).
-include("percept.hrl").
-include_lib("kernel/include/file.hrl").
%% API
page(SessionID, Env, Input) ->
mod_esi:deliver(SessionID, header()),
mod_esi:deliver(SessionID, menu()),
mod_esi:deliver(SessionID, overview_content(Env, Input)),
mod_esi:deliver(SessionID, footer()).
processes_page(SessionID, _, _) ->
mod_esi:deliver(SessionID, header()),
mod_esi:deliver(SessionID, menu()),
mod_esi:deliver(SessionID, processes_content()),
mod_esi:deliver(SessionID, footer()).
concurrency_page(SessionID, Env, Input) ->
mod_esi:deliver(SessionID, header()),
mod_esi:deliver(SessionID, menu()),
mod_esi:deliver(SessionID, concurrency_content(Env, Input)),
mod_esi:deliver(SessionID, footer()).
databases_page(SessionID, _, _) ->
mod_esi:deliver(SessionID, header()),
mod_esi:deliver(SessionID, menu()),
mod_esi:deliver(SessionID, databases_content()),
mod_esi:deliver(SessionID, footer()).
codelocation_page(SessionID, Env, Input) ->
mod_esi:deliver(SessionID, header()),
mod_esi:deliver(SessionID, menu()),
mod_esi:deliver(SessionID, codelocation_content(Env, Input)),
mod_esi:deliver(SessionID, footer()).
process_info_page(SessionID, Env, Input) ->
mod_esi:deliver(SessionID, header()),
mod_esi:deliver(SessionID, menu()),
mod_esi:deliver(SessionID, process_info_content(Env, Input)),
mod_esi:deliver(SessionID, footer()).
load_database_page(SessionID, Env, Input) ->
mod_esi:deliver(SessionID, header()),
% Very dynamic page, handled differently
load_database_content(SessionID, Env, Input),
mod_esi:deliver(SessionID, footer()).
%%% --------------------------- %%%
%%% Content pages %%%
%%% --------------------------- %%%
overview_content(_Env, Input) ->
Query = httpd:parse_query(Input),
Min = get_option_value("range_min", Query),
Max = get_option_value("range_max", Query),
Width = 1200,
Height = 600,
TotalProfileTime = ?seconds( percept_db:select({system, stop_ts}),
percept_db:select({system, start_ts})),
RegisteredProcs = length(percept_db:select({information, procs})),
RegisteredPorts = length(percept_db:select({information, ports})),
InformationTable =
"
" ++
table_line(["Profile time:", TotalProfileTime]) ++
table_line(["Processes:", RegisteredProcs]) ++
table_line(["Ports:", RegisteredPorts]) ++
table_line(["Min. range:", Min]) ++
table_line(["Max. range:", Max]) ++
"
",
Header = "
" ++ InformationTable ++ "
\n
",
Header ++ MainTable ++ Footer.
div_tag_graph() ->
%background:url('/images/loader.gif') no-repeat center;
"".
-spec url_graph(
Widht :: non_neg_integer(),
Height :: non_neg_integer(),
Min :: float(),
Max :: float(),
Pids :: [pid()]) -> string().
url_graph(W, H, Min, Max, []) ->
"/cgi-bin/percept_graph/graph?range_min=" ++ term2html(float(Min))
++ "&range_max=" ++ term2html(float(Max))
++ "&width=" ++ term2html(float(W))
++ "&height=" ++ term2html(float(H)).
%%% process_info_content
process_info_content(_Env, Input) ->
Query = httpd:parse_query(Input),
Pid = get_option_value("pid", Query),
[I] = percept_db:select({information, Pid}),
ArgumentString = case I#information.entry of
{_, _, Arguments} -> lists:flatten( [term2html(Arg) ++ "
" || Arg <- Arguments]);
_ -> ""
end,
TimeTable = html_table([
[{th, ""},
{th, "Timestamp"},
{th, "Profile Time"}],
[{td, "Start"},
term2html(I#information.start),
term2html(procstarttime(I#information.start))],
[{td, "Stop"},
term2html(I#information.stop),
term2html(procstoptime(I#information.stop))]
]),
InfoTable = html_table([
[{th, "Pid"}, term2html(I#information.id)],
[{th, "Name"}, term2html(I#information.name)],
[{th, "Entrypoint"}, mfa2html(I#information.entry)],
[{th, "Arguments"}, ArgumentString],
[{th, "Timetable"}, TimeTable],
[{th, "Parent"}, pid2html(I#information.parent)],
[{th, "Children"}, lists:flatten(lists:map(fun(Child) -> pid2html(Child) ++ " " end, I#information.children))]
]),
PidActivities = percept_db:select({activity, [{id, Pid}]}),
WaitingMfas = percept_analyzer:waiting_activities(PidActivities),
TotalWaitTime = lists:sum( [T || {T, _, _} <- WaitingMfas] ),
MfaTable = html_table([
[{th, "percentage"},
{th, "total"},
{th, "mean"},
{th, "stddev"},
{th, "#recv"},
{th, "module:function/arity"}]] ++ [
[{td, image_string(percentage, [{width, 100}, {height, 10}, {percentage, Time/TotalWaitTime}])},
{td, term2html(Time)},
{td, term2html(Mean)},
{td, term2html(StdDev)},
{td, term2html(N)},
{td, mfa2html(MFA)} ] || {Time, MFA, {Mean, StdDev, N}} <- WaitingMfas]),
"" ++
InfoTable ++ "
" ++
MfaTable ++
"
".
%%% concurrency content
concurrency_content(_Env, Input) ->
%% Get query
Query = httpd:parse_query(Input),
%% Collect selected pids and generate id tags
Pids = [value2pid(PidValue) || {PidValue, Case} <- Query, Case == "on", PidValue /= "select_all"],
IDs = [{id, Pid} || Pid <- Pids],
% FIXME: A lot of extra work here, redo
%% Analyze activities and calculate area bounds
Activities = percept_db:select({activity, IDs}),
StartTs = percept_db:select({system, start_ts}),
Counts = [{Time, Y1 + Y2} || {Time, Y1, Y2} <- percept_analyzer:activities2count2(Activities, StartTs)],
{T0,_,T1,_} = percept_analyzer:minmax(Counts),
% FIXME: End
PidValues = [pid2value(Pid) || Pid <- Pids],
%% Generate activity bar requests
ActivityBarTable = lists:foldl(
fun(Pid, Out) ->
ValueString = pid2value(Pid),
Out ++
table_line([
pid2html(Pid),
""
])
end, [], Pids),
%% Make pids request string
PidsRequest = join_strings_with(PidValues, ":"),
"
" ++
table_line([
"",
""
]) ++
ActivityBarTable ++
"
\n".
processes_content() ->
Ports = percept_db:select({information, ports}),
UnsortedProcesses = percept_db:select({information, procs}),
SystemStartTS = percept_db:select({system, start_ts}),
SystemStopTS = percept_db:select({system, stop_ts}),
ProfileTime = ?seconds( SystemStopTS,
SystemStartTS),
Processes = lists:sort(
fun (A, B) ->
if
A#information.id > B#information.id -> true;
true -> false
end
end, UnsortedProcesses),
ProcsHtml = lists:foldl(
fun (I, Out) ->
StartTime = procstarttime(I#information.start),
EndTime = procstoptime(I#information.stop),
Prepare =
table_line([
"",
pid2html(I#information.id),
image_string(proc_lifetime, [
{profiletime, ProfileTime},
{start, StartTime},
{"end", term2html(float(EndTime))},
{width, 100},
{height, 10}]),
mfa2html(I#information.entry),
term2html(I#information.name),
pid2html(I#information.parent)
]),
[Prepare|Out]
end, [], Processes),
PortsHtml = lists:foldl(
fun (I, Out) ->
StartTime = procstarttime(I#information.start),
EndTime = procstoptime(I#information.stop),
Prepare =
table_line([
"",
pid2html(I#information.id),
image_string(proc_lifetime, [
{profiletime, ProfileTime},
{start, StartTime},
{"end", term2html(float(EndTime))},
{width, 100},
{height, 10}]),
mfa2html(I#information.entry),
term2html(I#information.name),
pid2html(I#information.parent)
]),
[Prepare|Out]
end, [], Ports),
Selector = "",
if
length(ProcsHtml) > 0 ->
ProcsHtmlResult =
"Processes |
Select |
Pid |
Lifetime |
Entrypoint |
Name |
Parent |
" ++
lists:flatten(ProcsHtml) ++
"
|
";
true ->
ProcsHtmlResult = ""
end,
if
length(PortsHtml) > 0 ->
PortsHtmlResult = "
Ports |
Select |
Pid |
Lifetime |
Entrypoint |
Name |
Parent |
" ++
lists:flatten(PortsHtml) ++
"
|
";
true ->
PortsHtmlResult = ""
end,
Right = ""
++ Selector ++
"
\n",
Middle = "
" ++
ProcsHtmlResult ++
PortsHtmlResult ++
"
" ++
Right ++
"
\n",
"".
procstarttime(TS) ->
case TS of
undefined -> 0.0;
TS -> ?seconds(TS,percept_db:select({system, start_ts}))
end.
procstoptime(TS) ->
case TS of
undefined -> ?seconds( percept_db:select({system, stop_ts}),
percept_db:select({system, start_ts}));
TS -> ?seconds(TS, percept_db:select({system, start_ts}))
end.
databases_content() ->
"".
load_database_content(SessionId, _Env, Input) ->
Query = httpd:parse_query(Input),
{_,{_,Path}} = lists:keysearch("file", 1, Query),
{_,{_,File}} = lists:keysearch("path", 1, Query),
Filename = filename:join(Path, File),
% Check path/file/filename
mod_esi:deliver(SessionId, ""),
case file:read_file_info(Filename) of
{ok, _} ->
Content = "
Parsing: " ++ Filename ++ "
",
mod_esi:deliver(SessionId, Content),
case percept:analyze(Filename) of
{error, Reason} ->
mod_esi:deliver(SessionId, error_msg("Analyze" ++ term2html(Reason)));
_ ->
Complete = "
View",
mod_esi:deliver(SessionId, Complete)
end;
{error, Reason} ->
mod_esi:deliver(SessionId, error_msg("File" ++ term2html(Reason)))
end,
mod_esi:deliver(SessionId, "
").
codelocation_content(_Env, Input) ->
Query = httpd:parse_query(Input),
Min = get_option_value("range_min", Query),
Max = get_option_value("range_max", Query),
StartTs = percept_db:select({system, start_ts}),
TsMin = percept_analyzer:seconds2ts(Min, StartTs),
TsMax = percept_analyzer:seconds2ts(Max, StartTs),
Acts = percept_db:select({activity, [{ts_min, TsMin}, {ts_max, TsMax}]}),
Secs = [timer:now_diff(A#activity.timestamp,StartTs)/1000 || A <- Acts],
Delta = cl_deltas(Secs),
Zip = lists:zip(Acts, Delta),
Table = html_table([
[{th, "delta [ms]"},
{th, "time [ms]"},
{th, " pid "},
{th, "activity"},
{th, "module:function/arity"},
{th, "#runnables"}]] ++ [
[{td, term2html(D)},
{td, term2html(timer:now_diff(A#activity.timestamp,StartTs)/1000)},
{td, pid2html(A#activity.id)},
{td, term2html(A#activity.state)},
{td, mfa2html(A#activity.where)},
{td, term2html(A#activity.runnable_count)}] || {A, D} <- Zip ]),
"" ++
Table ++
"
".
cl_deltas([]) -> [];
cl_deltas(List) -> cl_deltas(List, [0.0]).
cl_deltas([_], Out) -> lists:reverse(Out);
cl_deltas([A,B|Ls], Out) -> cl_deltas([B|Ls], [B - A | Out]).
%%% --------------------------- %%%
%%% Utility functions %%%
%%% --------------------------- %%%
%% Should be in string stdlib?
join_strings(Strings) ->
lists:flatten(Strings).
-spec join_strings_with(Strings :: [string()], Separator :: string()) -> string().
join_strings_with([S1, S2 | R], S) ->
join_strings_with([join_strings_with(S1,S2,S) | R], S);
join_strings_with([S], _) ->
S.
join_strings_with(S1, S2, S) ->
join_strings([S1,S,S2]).
%%% Generic erlang2html
-spec html_table(Rows :: [[string() | {'td' | 'th', string()}]]) -> string().
html_table(Rows) -> "" ++ html_table_row(Rows) ++ "
".
html_table_row(Rows) -> html_table_row(Rows, odd).
html_table_row([], _) -> "";
html_table_row([Row|Rows], odd ) -> "" ++ html_table_data(Row) ++ "
" ++ html_table_row(Rows, even);
html_table_row([Row|Rows], even) -> "" ++ html_table_data(Row) ++ "
" ++ html_table_row(Rows, odd ).
html_table_data([]) -> "";
html_table_data([{td, Data}|Row]) -> "" ++ Data ++ " | " ++ html_table_data(Row);
html_table_data([{th, Data}|Row]) -> "" ++ Data ++ " | " ++ html_table_data(Row);
html_table_data([Data|Row]) -> "" ++ Data ++ " | " ++ html_table_data(Row).
-spec table_line(Table :: [any()]) -> string().
table_line(List) -> table_line(List, [""]).
table_line([], Out) -> lists:flatten(lists:reverse(["
\n"|Out]));
table_line([Element | Elements], Out) when is_list(Element) ->
table_line(Elements, ["" ++ Element ++ " | " |Out]);
table_line([Element | Elements], Out) ->
table_line(Elements, ["" ++ term2html(Element) ++ " | "|Out]).
-spec term2html(any()) -> string().
term2html(Term) when is_float(Term) -> lists:flatten(io_lib:format("~.4f", [Term]));
term2html(Term) -> lists:flatten(io_lib:format("~p", [Term])).
-spec mfa2html(MFA :: {atom(), atom(), list() | integer()}) -> string().
mfa2html({Module, Function, Arguments}) when is_list(Arguments) ->
lists:flatten(io_lib:format("~p:~p/~p", [Module, Function, length(Arguments)]));
mfa2html({Module, Function, Arity}) when is_integer(Arity) ->
lists:flatten(io_lib:format("~p:~p/~p", [Module, Function, Arity]));
mfa2html(_) ->
"undefined".
-spec pid2html(Pid :: pid() | port()) -> string().
pid2html(Pid) when is_pid(Pid) ->
PidString = term2html(Pid),
PidValue = pid2value(Pid),
""++PidString++"";
pid2html(Pid) when is_port(Pid) ->
term2html(Pid);
pid2html(_) ->
"undefined".
-spec image_string(Request :: string()) -> string().
image_string(Request) ->
"".
-spec image_string(atom() | string(), list()) -> string().
image_string(Request, Options) when is_atom(Request), is_list(Options) ->
image_string(image_string_head(erlang:atom_to_list(Request), Options, []));
image_string(Request, Options) when is_list(Options) ->
image_string(image_string_head(Request, Options, [])).
image_string_head(Request, [{Type, Value} | Opts], Out) when is_atom(Type), is_number(Value) ->
Opt = join_strings(["?",term2html(Type),"=",term2html(Value)]),
image_string_tail(Request, Opts, [Opt|Out]);
image_string_head(Request, [{Type, Value} | Opts], Out) ->
Opt = join_strings(["?",Type,"=",Value]),
image_string_tail(Request, Opts, [Opt|Out]).
image_string_tail(Request, [], Out) ->
join_strings([Request | lists:reverse(Out)]);
image_string_tail(Request, [{Type, Value} | Opts], Out) when is_atom(Type), is_number(Value) ->
Opt = join_strings(["&",term2html(Type),"=",term2html(Value)]),
image_string_tail(Request, Opts, [Opt|Out]);
image_string_tail(Request, [{Type, Value} | Opts], Out) ->
Opt = join_strings(["&",Type,"=",Value]),
image_string_tail(Request, Opts, [Opt|Out]).
%%% percept conversions
-spec pid2value(Pid :: pid()) -> string().
pid2value(Pid) ->
String = lists:flatten(io_lib:format("~p", [Pid])),
lists:sublist(String, 2, erlang:length(String)-2).
-spec value2pid(Value :: string()) -> pid().
value2pid(Value) ->
String = lists:flatten("<" ++ Value ++ ">"),
erlang:list_to_pid(String).
%%% get value
-spec get_option_value(Option :: string(), Options :: [{string(),any()}]) ->
{'error', any()} | boolean() | pid() | [pid()] | number().
get_option_value(Option, Options) ->
case catch get_option_value0(Option, Options) of
{'EXIT', Reason} -> {error, Reason};
Value -> Value
end.
get_option_value0(Option, Options) ->
case lists:keysearch(Option, 1, Options) of
false -> get_default_option_value(Option);
{value, {Option, _Value}} when Option == "fillcolor" -> true;
{value, {Option, Value}} when Option == "pid" -> value2pid(Value);
{value, {Option, Value}} when Option == "pids" ->
[value2pid(PidValue) || PidValue <- string:tokens(Value,":")];
{value, {Option, Value}} -> get_number_value(Value);
_ -> {error, undefined}
end.
get_default_option_value(Option) ->
case Option of
"fillcolor" -> false;
"range_min" -> float(0.0);
"pids" -> [];
"range_max" ->
Acts = percept_db:select({activity, []}),
#activity{ timestamp = Start } = hd(Acts),
#activity{ timestamp = Stop } = hd(lists:reverse(Acts)),
?seconds(Stop,Start);
"width" -> 700;
"height" -> 400;
_ -> {error, {undefined_default_option, Option}}
end.
-spec get_number_value(string()) -> number() | {'error', 'illegal_number'}.
get_number_value(Value) ->
% Try float
case string:to_float(Value) of
{error, no_float} ->
% Try integer
case string:to_integer(Value) of
{error, _} -> {error, illegal_number};
{Integer, _} -> Integer
end;
{error, _} -> {error, illegal_number};
{Float, _} -> Float
end.
%%% --------------------------- %%%
%%% html prime functions %%%
%%% --------------------------- %%%
header() -> header([]).
header(HeaderData) ->
"Content-Type: text/html\r\n\r\n" ++
"
percept
" ++ HeaderData ++"
\n".
footer() ->
"
\n".
menu() ->
"\n".
-spec error_msg(Error :: string()) -> string().
error_msg(Error) ->
"\n".