diff --git a/apps/xprof_core/src/xprof_core.erl b/apps/xprof_core/src/xprof_core.erl index 1bc074ff..94946fa9 100644 --- a/apps/xprof_core/src/xprof_core.erl +++ b/apps/xprof_core/src/xprof_core.erl @@ -136,12 +136,12 @@ get_trace_status() -> %% @doc Start capturing arguments and return values of function calls that %% lasted longer than the specified time threshold in ms. -spec capture(xprof_core:mfa_id(), non_neg_integer(), non_neg_integer()) -> - {ok, CaptureId :: non_neg_integer()}. + {ok, CaptureId :: non_neg_integer()} | {error, not_found}. capture(MFA, Threshold, Limit) -> xprof_core_trace_handler:capture(MFA, Threshold, Limit). %% @doc Stop capturing long calls of the given function. --spec capture_stop(xprof_core:mfa_id()) -> ok | {error, not_found}. +-spec capture_stop(xprof_core:mfa_id()) -> ok | {error, not_found | not_captured}. capture_stop(MFA) -> xprof_core_trace_handler:capture_stop(MFA). diff --git a/apps/xprof_core/src/xprof_core_trace_handler.erl b/apps/xprof_core/src/xprof_core_trace_handler.erl index 6bd0e8a0..23888a97 100644 --- a/apps/xprof_core/src/xprof_core_trace_handler.erl +++ b/apps/xprof_core/src/xprof_core_trace_handler.erl @@ -55,15 +55,20 @@ data(MFA, FromEpoch) -> %% @doc Starts capturing args and results from function calls that lasted longer %% than specified time threshold. -spec capture(xprof_core:mfa_id(), non_neg_integer(), non_neg_integer()) -> - {ok, non_neg_integer()}. + {ok, non_neg_integer()} | {error, not_found}. capture(MFA = {M,F,A}, Threshold, Limit) -> lager:info("Capturing ~p calls to ~w:~w/~w that exceed ~p ms:", [Limit, M, F, A, Threshold]), Name = xprof_core_lib:mfa2atom(MFA), - gen_server:call(Name, {capture, Threshold, Limit}). + try + gen_server:call(Name, {capture, Threshold, Limit}) + catch + exit:{noproc, _} -> + {error, not_found} + end. --spec capture_stop(xprof_core:mfa_id()) -> ok | {error, not_found}. +-spec capture_stop(xprof_core:mfa_id()) -> ok | {error, not_found | not_captured}. capture_stop(MFA) -> Name = xprof_core_lib:mfa2atom(MFA), try @@ -75,27 +80,34 @@ capture_stop(MFA) -> %% @doc -spec get_captured_data(xprof_core:mfa_id(), non_neg_integer()) -> - empty | {ok, - {Index :: non_neg_integer(), - Threshold :: non_neg_integer(), - OrigLimit :: non_neg_integer(), - HasMore :: boolean() - }, [tuple()]}. + {ok, + {Index :: non_neg_integer(), + Threshold :: non_neg_integer(), + OrigLimit :: non_neg_integer(), + HasMore :: boolean() + }, [tuple()]} | + {error, not_found}. get_captured_data(MFA, Offset) when Offset >= 0 -> Name = xprof_core_lib:mfa2atom(MFA), try - Items = lists:sort(ets:select(Name, - [{ - {{args_res, '$1'}, - {'$2', '$3','$4','$5'}}, - [{'>','$1',Offset}], - [{{'$1', '$2', '$3', '$4', '$5'}}] - }])), - - Res = ets:lookup(Name, capture_spec), - [{capture_spec, Index, Threshold, Limit, OrigLimit}] = Res, - HasMore = Offset + length(Items) < Limit, - {ok, {Index, Threshold, OrigLimit, HasMore}, Items} + [{capture_spec, Index, Threshold, Limit, OrigLimit}] = + ets:lookup(Name, capture_spec), + case Index of + -1 -> + %% no capturing was done for this MFA yet, + %% no need to traverse the table + {ok, {Index, Threshold, OrigLimit, _HasMore = false}, _Items = []}; + _ -> + MS = [{ + {{args_res, '$1'}, + {'$2', '$3','$4','$5'}}, + [{'>','$1',Offset}], + [{{'$1', '$2', '$3', '$4', '$5'}}] + }], + Items = lists:sort(ets:select(Name, MS)), + HasMore = Offset + length(Items) < Limit, + {ok, {Index, Threshold, OrigLimit, HasMore}, Items} + end catch error:badarg -> {error, not_found} end. @@ -127,6 +139,9 @@ handle_call({capture, Threshold, Limit}, _From, capture_args_trace_on(MFA), {Timeout, NewState2} = maybe_make_snapshot(NewState), {reply, {ok, NewId}, NewState2, Timeout}; +handle_call(capture_stop, _From, State = #state{capture_spec = undefined}) -> + {Timeout, NewState} = maybe_make_snapshot(State), + {reply, {error, not_captured}, NewState, Timeout}; handle_call(capture_stop, _From, State = #state{mfa = MFA}) -> capture_args_trace_off(MFA), #state{capture_spec = {Threshold, Limit}, diff --git a/apps/xprof_core/test/xprof_tracing_SUITE.erl b/apps/xprof_core/test/xprof_tracing_SUITE.erl index 6e6cee41..834eede3 100644 --- a/apps/xprof_core/test/xprof_tracing_SUITE.erl +++ b/apps/xprof_core/test/xprof_tracing_SUITE.erl @@ -13,7 +13,10 @@ ]). %% Test cases --export([monitor_many_funs/1, +-export([not_found_error/1, + already_traced_error/1, + not_captured_error/1, + monitor_many_funs/1, monitor_recursive_fun/1, monitor_keep_recursive_fun/1, monitor_crashing_fun/1, @@ -32,7 +35,10 @@ %% CT funs all() -> - [monitor_many_funs, + [not_found_error, + already_traced_error, + not_captured_error, + monitor_many_funs, monitor_recursive_fun, monitor_keep_recursive_fun, monitor_crashing_fun, @@ -133,6 +139,35 @@ dead_proc_tracing(_Config) -> xprof_core:get_trace_status()), ok. +not_found_error(_Config) -> + MFA = {?MODULE, no_such_fun, 1}, + ?assertEqual(ok, xprof_core:demonitor(MFA)), + ?assertEqual({error, not_found}, xprof_core:get_data(MFA, 0)), + ?assertEqual({error, not_found}, xprof_core:capture(MFA, 1, 1)), + ?assertEqual({error, not_found}, xprof_core:capture_stop(MFA)), + ?assertEqual({error, not_found}, xprof_core:get_captured_data(MFA, 0)), + ok. + +already_traced_error(_Config) -> + MFA = {?MODULE, test_fun, 1}, + ok = xprof_core:monitor(MFA), + try + ?assertEqual({error, already_traced}, xprof_core:monitor(MFA)) + after + xprof_core:demonitor(MFA) + end. + +not_captured_error(_Config) -> + MFA = {?MODULE, test_fun, 1}, + ok = xprof_core:monitor(MFA), + try + ?assertEqual({error, not_captured}, xprof_core:capture_stop(MFA)), + ?assertEqual({ok, {-1,-1,-1,false}, []}, + xprof_core:get_captured_data(MFA, 0)) + after + xprof_core:demonitor(MFA) + end. + monitor_crashing_fun(_Config) -> xprof_core:monitor(MFA = {?MODULE, maybe_crash_test_fun, 1}), ok = xprof_core:trace(self()), diff --git a/apps/xprof_gui/src/xprof_gui_app.erl b/apps/xprof_gui/src/xprof_gui_app.erl index 6270cf5e..90498b3b 100644 --- a/apps/xprof_gui/src/xprof_gui_app.erl +++ b/apps/xprof_gui/src/xprof_gui_app.erl @@ -10,6 +10,7 @@ -define(APP, xprof_gui). -define(DEF_WEB_IF_PORT, 7890). +-define(LISTENER, xprof_http_listener). %% Application callbacks @@ -29,17 +30,18 @@ stop(_State) -> start_cowboy() -> Port = application:get_env(?APP, port, ?DEF_WEB_IF_PORT), - Dispatch = cowboy_router:compile(cowboy_routes()), - cowboy:start_http(xprof_http_listener, 100, [{port, Port}], - [{env, [{dispatch, Dispatch}]}]). - -cowboy_routes() -> - [{'_', [{"/api/:what", xprof_gui_cowboy1_handler, []}, - {"/build/[...]", cowboy_static, {priv_dir, ?APP, "build"}}, - {"/styles/[...]", cowboy_static, {priv_dir, ?APP, "styles"}}, - {"/img/[...]", cowboy_static, {priv_dir, ?APP, "img"}}, - {"/", cowboy_static, {priv_file, ?APP, "index.html"}} - ]}]. + Dispatch = cowboy_dispatch(xprof_gui_cowboy1_handler), + xprof_gui_cowboy1_handler:start_listener(?LISTENER, Port, Dispatch). + +cowboy_dispatch(Mod) -> + Routes = + [{'_', [{"/api/:what", Mod, []}, + {"/build/[...]", cowboy_static, {priv_dir, ?APP, "build"}}, + {"/styles/[...]", cowboy_static, {priv_dir, ?APP, "styles"}}, + {"/img/[...]", cowboy_static, {priv_dir, ?APP, "img"}}, + {"/", cowboy_static, {priv_file, ?APP, "index.html"}} + ]}], + cowboy_router:compile(Routes). stop_cowboy() -> - cowboy:stop_listener(xprof_http_listener). + cowboy:stop_listener(?LISTENER). diff --git a/apps/xprof_gui/src/xprof_gui_cowboy1_handler.erl b/apps/xprof_gui/src/xprof_gui_cowboy1_handler.erl index 1a570d5d..ba6ae4e4 100644 --- a/apps/xprof_gui/src/xprof_gui_cowboy1_handler.erl +++ b/apps/xprof_gui/src/xprof_gui_cowboy1_handler.erl @@ -1,210 +1,48 @@ +%%% @doc Cowboy 1.x compatible HTTP handler -module(xprof_gui_cowboy1_handler). --export([init/3,handle/2,terminate/3]). - -behavior(cowboy_http_handler). +%% xprof_gui_app callback +-export([start_listener/3]). + +%% Cowboy 1.x callbacks +-export([init/3, + handle/2, + terminate/3 + ]). + +-define(HDR_JSON, [{<<"content-type">>, <<"application/json">>}]). + %% In case an XHR receives no content with no content-type Firefox will emit %% the following error: "XML Parsing Error: no root element found..." %% As a workaround always return a content-type of octet-stream with %% 204 No Content responses --define(HDR_NO_CONTENT, [{<<"Content-type">>, <<"application/octet-stream">>}]). - -%% Cowboy callbacks - -init(_Type, Req, _Opts) -> - {ok, Req, no_state}. - -handle(Req, State) -> - {What,_} = cowboy_req:binding(what, Req), - handle_req(What, Req, State). - -terminate(_Reason, _Req, _State) -> - ok. - -%% Private - -%% Handling different HTTP requests - -handle_req(<<"funs">>, Req, State) -> - Query = get_query(Req), - - Funs = xprof_core:get_matching_mfas_pp(Query), - Json = jsone:encode(Funs), - - lager:debug("Returning ~b functions matching phrase \"~s\"", - [length(Funs), Query]), - - {ok, Req2} = cowboy_req:reply(200, - [{<<"content-type">>, - <<"application/json">>}], - Json, - Req), - {ok, Req2, State}; - -handle_req(<<"mon_start">>, Req, State) -> - Query = get_query(Req), - lager:info("Starting monitoring via web on '~s'~n", [Query]), - - {ok, ResReq} = - case xprof_core:monitor_pp(Query) of - ok -> - cowboy_req:reply(204, ?HDR_NO_CONTENT, Req); - {error, already_traced} -> - cowboy_req:reply(204, ?HDR_NO_CONTENT, Req); - _Error -> - cowboy_req:reply(400, Req) - end, - {ok, ResReq, State}; - - -handle_req(<<"mon_stop">>, Req, State) -> - MFA = {M, F, A} = get_mfa(Req), +-define(HDR_NO_CONTENT, [{<<"content-type">>, <<"application/octet-stream">>}]). - lager:info("Stopping monitoring via web on ~w:~w/~w~n",[M, F, A]), +%% xprof_gui_app callback - xprof_core:demonitor(MFA), - {ok, ResReq} = cowboy_req:reply(204, ?HDR_NO_CONTENT, Req), - {ok, ResReq, State}; +start_listener(Name, Port, Dispatch) -> + cowboy:start_http(Name, 100, [{port, Port}], + [{env, [{dispatch, Dispatch}]}]). -handle_req(<<"mon_get_all">>, Req, State) -> - Funs = xprof_core:get_all_monitored(), - FunsArr = [[Mod, Fun, Arity, Query] - || {{Mod, Fun, Arity}, Query} <- Funs], - Json = jsone:encode(FunsArr), - {ok, ResReq} = cowboy_req:reply(200, - [{<<"content-type">>, - <<"application/json">>}], - Json, Req), - {ok, ResReq, State}; +%% Cowboy 1.x callbacks -handle_req(<<"data">>, Req, State) -> - MFA = get_mfa(Req), - {LastTS, _} = cowboy_req:qs_val(<<"last_ts">>, Req, <<"0">>), - - {ok, ResReq} = - case xprof_core:get_data(MFA, binary_to_integer(LastTS)) of - {error, not_found} -> - cowboy_req:reply(404, Req); - Vals -> - Json = jsone:encode([{Val} || Val <- Vals]), - - cowboy_req:reply(200, - [{<<"content-type">>, - <<"application/json">>}], - Json, Req) - end, - {ok, ResReq, State}; - -handle_req(<<"get_callees">>, Req, State) -> - MFA = get_mfa(Req), - Callees = xprof_core:get_called_funs_pp(MFA), - Json = jsone:encode(Callees), - {ok, ResReq} = cowboy_req:reply(200, - [{<<"content-type">>, - <<"application/json">>}], - Json, Req), - {ok, ResReq, State}; - -handle_req(<<"trace_set">>, Req, State) -> - {Spec, _} = cowboy_req:qs_val(<<"spec">>, Req), - - {ok, ResReq} = case lists:member(Spec, [<<"all">>, <<"pause">>]) of - true -> - xprof_core:trace(list_to_atom(binary_to_list(Spec))), - cowboy_req:reply(204, ?HDR_NO_CONTENT, Req); - false -> - lager:info("Wrong spec for tracing: ~p",[Spec]), - cowboy_req:reply(400, Req) - end, - {ok, ResReq, State}; - -handle_req(<<"trace_status">>, Req, State) -> - {_, Status} = xprof_core:get_trace_status(), - Json = jsone:encode({[{status, Status}]}), - {ok, ResReq} = cowboy_req:reply(200, - [{<<"content-type">>, - <<"application/json">>}], - Json, Req), - - {ok, ResReq, State}; - -handle_req(<<"capture">>, Req, State) -> - MFA = {M,F,A} = get_mfa(Req), - {ThresholdStr, _} = cowboy_req:qs_val(<<"threshold">>, Req), - {LimitStr, _} = cowboy_req:qs_val(<<"limit">>, Req), - Threshold = binary_to_integer(ThresholdStr), - Limit = binary_to_integer(LimitStr), - - lager:info("Capture ~b calls to ~w:~w/~w~n exceeding ~b ms", - [Limit, M, F, A, Threshold]), - - {ok, CaptureId} = xprof_core:capture(MFA, Threshold, Limit), - Json = jsone:encode({[{capture_id, CaptureId}]}), - - {ok, ResReq} = cowboy_req:reply(200, - [{<<"content-type">>, - <<"application/json">>}], Json, Req), - {ok, ResReq, State}; - -handle_req(<<"capture_stop">>, Req, State) -> - MFA = get_mfa(Req), - - lager:info("Stopping slow calls capturing for ~p", [MFA]), - - {ok, ResReq} = - case xprof_core:capture_stop(MFA) of - ok -> - cowboy_req:reply(204, ?HDR_NO_CONTENT, Req); - {error, not_found} -> - cowboy_req:reply(404, Req) - end, - {ok, ResReq, State}; - -handle_req(<<"capture_data">>, Req, State) -> - MFA = get_mfa(Req), - {OffsetStr, _} = cowboy_req:qs_val(<<"offset">>, Req), - Offset = binary_to_integer(OffsetStr), +init(_Type, Req, _Opts) -> + {ok, Req, no_state}. - {ok, ResReq} = - case xprof_core:get_captured_data_pp(MFA, Offset) of - {error, not_found} -> - cowboy_req:reply(404, Req); - {ok, {Id, Threshold, Limit, HasMore}, Items} -> - Json = jsone:encode({[{capture_id, Id}, - {threshold, Threshold}, - {limit, Limit}, - {items, Items}, - {has_more, HasMore}]}), - cowboy_req:reply(200, - [{<<"content-type">>, - <<"application/json">>}], - Json, Req) +handle(Req0, State) -> + {What, _} = cowboy_req:binding(what, Req0), + {Params, _} = cowboy_req:qs_vals(Req0), + {ok, Req} = + case xprof_gui_rest:handle_req(What, Params) of + {StatusCode, Json} when is_integer(StatusCode), is_binary(Json) -> + cowboy_req:reply(StatusCode, ?HDR_JSON, Json, Req0); + StatusCode when is_integer(StatusCode) -> + cowboy_req:reply(StatusCode, ?HDR_NO_CONTENT, Req0) end, - {ok, ResReq, State}; - -handle_req(<<"mode">>, Req, State) -> - Mode = xprof_core:get_mode(), - Json = jsone:encode({[{mode, Mode}]}), - {ok, ResReq} = cowboy_req:reply(200, - [{<<"content-type">>, - <<"application/json">>}], - Json, Req), - {ok, ResReq, State}. - -%% Helpers --spec get_mfa(cowboy:req()) -> xprof_core:mfa_id(). -get_mfa(Req) -> - {Params, _} = cowboy_req:qs_vals(Req), - {list_to_atom(binary_to_list(proplists:get_value(<<"mod">>, Params))), - list_to_atom(binary_to_list(proplists:get_value(<<"fun">>, Params))), - case proplists:get_value(<<"arity">>, Params) of - <<"_">> -> '_'; - Arity -> binary_to_integer(Arity) - end}. + {ok, Req, State}. --spec get_query(cowboy:req()) -> binary(). -get_query(Req) -> - {Query, _} = cowboy_req:qs_val(<<"query">>, Req, <<"">>), - Query. +terminate(_Reason, _Req, _State) -> + ok. diff --git a/apps/xprof_gui/src/xprof_gui_rest.erl b/apps/xprof_gui/src/xprof_gui_rest.erl new file mode 100644 index 00000000..3815cb84 --- /dev/null +++ b/apps/xprof_gui/src/xprof_gui_rest.erl @@ -0,0 +1,382 @@ +%%% @doc HTTP server independent part of the REST API implementation +%%% +%%% == Autocomplete == +%%% +%%% === /api/funs === +%%% +%%% Returns: +%%%