Current section
Files
Jump to
Current section
Files
src/snabbkaffe_collector.erl
-module(snabbkaffe_collector).
-ifndef(SNK_COLLECTOR).
-define(SNK_COLLECTOR, true).
-endif.
-include("snabbkaffe.hrl").
-behaviour(gen_server).
%% API
-export([ start/0
, stop/0
, get_trace/1
, get_stats/0
, block_until/3
, notify_on_event/3
]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
-export_type([async_action/0]).
-define(SERVER, ?MODULE).
-type datapoints() :: [{number(), number()}]
| number().
-type async_action() :: fun(({ok, snabbkaffe:event()} | timeout) -> _).
-record(callback,
{ async_action :: async_action()
, predicate :: snabbkaffe:predicate()
, tref :: reference() | undefined
, ref :: reference()
}).
-record(s,
{ trace = [] :: [snabbkaffe:timed_event()]
, stats = #{} :: #{snabbkaffe:metric() => datapoints()}
, last_event_ts = 0 :: integer()
, callbacks = [] :: [#callback{}]
}).
%%%===================================================================
%%% API
%%%===================================================================
start() ->
case whereis(?SERVER) of
undefined ->
gen_server:start({local, ?SERVER}, ?MODULE, [], []);
Pid ->
{ok, Pid}
end.
stop() ->
gen_server:stop(?SERVER).
-spec get_stats() -> datapoints().
get_stats() ->
gen_server:call(?SERVER, get_stats).
-spec get_trace(integer()) -> snabbkaffe:timed_trace().
get_trace(Timeout) ->
{ok, Trace} = gen_server:call(?SERVER, {get_trace, Timeout}, infinity),
Trace.
-spec block_until(snabbkaffe:predicate(), timeout(), timeout()) ->
{ok, snabbkaffe:event()} | timeout.
block_until(Predicate, Timeout, BackInTime0) ->
Infimum = case BackInTime0 of
infinity ->
erlang:system_info(start_time);
_ ->
BackInTime = erlang:convert_time_unit( BackInTime0
, millisecond
, native
),
erlang:monotonic_time() - BackInTime
end,
gen_server:call( ?SERVER
, {block_until, Predicate, Timeout, Infimum}
, infinity
).
-spec notify_on_event(snabbkaffe:predicate(), timeout(), async_action()) ->
ok.
notify_on_event(Predicate, Timeout, Callback) ->
gen_server:call( ?SERVER
, {notify_on_event, Callback, Predicate, Timeout}
).
%%%===================================================================
%%% gen_server callbacks
%%%===================================================================
init([]) ->
process_flag(trap_exit, true),
snabbkaffe:tp('$trace_begin', #{}),
{ok, #s{}}.
handle_cast(Evt, S = #s{trace = T0, callbacks = CB0}) ->
CB = maybe_unblock_someone(Evt, CB0),
{noreply, S#s{ trace = [Evt|T0]
, last_event_ts = erlang:monotonic_time()
, callbacks = CB
}}.
handle_call({push_stat, Metric, Stat}, _From, State0) ->
Stats = maps:update_with( Metric
, fun(L) -> [Stat|L] end
, [Stat]
, State0#s.stats
),
{reply, ok, State0#s{stats = Stats}};
handle_call(get_stats, _From, State) ->
{reply, {ok, State#s.stats}, State};
handle_call({get_trace, Timeout}, From, State) ->
timer:send_after(Timeout, {flush, From, Timeout}),
{noreply, State};
handle_call({block_until, Predicate, Timeout, Infimum}, From, State0) ->
Callback = fun(Result) ->
gen_server:reply(From, Result)
end,
State = maybe_subscribe(Predicate, Timeout, Infimum, Callback, State0),
{noreply, State};
handle_call({notify_on_event, Callback, Predicate, Timeout}, _From, State0) ->
Now = erlang:monotonic_time(),
State = maybe_subscribe(Predicate, Timeout, Now, Callback, State0),
{reply, ok, State};
handle_call(_Request, _From, State) ->
Reply = unknown_call,
{reply, Reply, State}.
handle_info({timeout, Ref}, State) ->
#s{callbacks = CB0} = State,
Fun = fun(#callback{ref = Ref1, async_action = AsyncAction})
when Ref1 =:= Ref ->
AsyncAction(timeout),
false;
(C) ->
{true, C}
end,
CB = lists:filtermap(Fun, CB0),
{noreply, State#s{callbacks = CB}};
handle_info(Event = {flush, To, Timeout}, State) ->
#s{ trace = Trace
, last_event_ts = LastEventTs
} = State,
Dt = erlang:convert_time_unit( erlang:monotonic_time() - LastEventTs
, native
, millisecond
),
if Dt >= Timeout ->
TraceEnd = #{ kind => '$trace_end'
, ts => LastEventTs
},
Result = lists:reverse([TraceEnd|Trace]),
gen_server:reply(To, {ok, Result}),
{noreply, State #s{trace = []}};
true ->
timer:send_after(Timeout, Event),
{noreply, State}
end;
handle_info(_, State) ->
{noreply, State}.
terminate(_Reason, _State) ->
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%%===================================================================
%%% Internal functions
%%%===================================================================
-spec maybe_unblock_someone( snabbkaffe:event()
, [#callback{}]
) -> [#callback{}].
maybe_unblock_someone(Evt, Callbacks) ->
Fun = fun(Callback) ->
#callback{ predicate = Predicate
, async_action = AsyncAction
, tref = TRef
} = Callback,
case Predicate(Evt) of
false ->
{true, Callback};
true ->
cancel_timer(TRef),
AsyncAction({ok, Evt}),
false
end
end,
lists:filtermap(Fun, Callbacks).
-spec maybe_subscribe( snabbkaffe:predicate()
, timeout()
, integer()
, async_action()
, #s{}
) -> #s{}.
maybe_subscribe(Predicate, Timeout, Infimum, AsyncAction, State0) ->
#s{ trace = Trace
, callbacks = CB0
} = State0,
try
%% 1. Search in the past events
[case Evt of
#{ts := Ts} when Ts > Infimum ->
case Predicate(Evt) of
true ->
throw({found, Evt});
false ->
ok
end;
_ ->
throw(not_found)
end
|| Evt <- Trace],
throw(not_found)
catch
{found, Event} ->
AsyncAction({ok, Event}),
State0;
not_found ->
%% 2. Postpone reply
Ref = make_ref(),
TRef = send_after(Timeout, self(), {timeout, Ref}),
Callback = #callback{ async_action = AsyncAction
, predicate = Predicate
, tref = TRef
, ref = Ref
},
State0#s{ callbacks = [Callback|CB0]
}
end.
-spec send_after(timeout(), pid(), _Msg) ->
reference() | undefined.
send_after(infinity, _, _) ->
undefined;
send_after(Timeout, Pid, Msg) ->
erlang:send_after(Timeout, Pid, Msg).
-spec cancel_timer(reference() | undefined) -> _.
cancel_timer(undefined) ->
ok;
cancel_timer(TRef) ->
erlang:cancel_timer(TRef).