Current section
Files
Jump to
Current section
Files
src/eflambe_tracer.erl
%%%-------------------------------------------------------------------
%%% @doc
%%% This module defines a tracer process implemented as a gen_server. This
%%% gen_server is only intended to be used by the eflambe_server to receive
%%% trace messages and write them to the appropriate formatter module.
%%% @end
%%%-------------------------------------------------------------------
-module(eflambe_tracer).
-behaviour(gen_server).
%% API
-export([
start_link/1,
finish/1
]).
%% gen_server callbacks
-export([init/1,
handle_call/3,
handle_cast/2,
handle_continue/2,
handle_info/2,
terminate/2]).
-include_lib("kernel/include/logger.hrl").
-define(SERVER, ?MODULE).
-define(DEFAULT_OPTIONS, [{output_format, brendan_gregg}]).
-record(state, {
impl :: atom(),
impl_state :: any(),
options :: eflambe:options(),
filename :: file:filename_all()
}).
-type state() :: #state{}.
-type from() :: {pid(), Tag :: term()}.
-type tracer_options() :: [eflambe:option() | {pid, pid()}].
%%%===================================================================
%%% API
%%%===================================================================
%%--------------------------------------------------------------------
%% @doc
%% Starts the tracer
%%
%% @end
%%--------------------------------------------------------------------
-spec start_link(tracer_options()) -> {ok, pid()} | ignore | {error, Error :: any()}.
start_link(Options) ->
gen_server:start_link(?MODULE, [Options], []).
finish(Pid) ->
gen_server:call(Pid, finish).
%%%===================================================================
%%% gen_server callbacks
%%%===================================================================
-spec init(Args :: list(tracer_options())) -> {ok, state()}.
init([Options]) ->
% Generate complete list of options by falling back to default list
FinalOptions = merge(Options, ?DEFAULT_OPTIONS),
% Select the right implementation module. Provide shorter names for included
% modules
Impl = case proplists:get_value(output_format, FinalOptions) of
brendan_gregg -> eflambe_brendan_gregg;
perf -> eflambe_perf;
plain -> eflambe_plain;
Other -> Other
end,
% Generate output filename
{ok, Ext} = erlang:apply(Impl, extension, []),
Filename = generate_filename(Ext),
FullFilename = filename:join([output_directory(Options), Filename]),
% Initialize implementation state
{ok, State} = erlang:apply(Impl, init, [FullFilename, Options]),
{ok, #state{impl = Impl, impl_state = State, options = FinalOptions, filename = FullFilename}}.
-spec handle_call(Request :: any(), from(), state()) ->
{reply, Reply :: any(), state()} |
{reply, Reply :: any(), state(), {continue, finish}}.
handle_call(finish, _From, #state{impl = Impl, impl_state = ImplState, options = Options,
filename = Filename} = State) ->
% Format the trace data and write to file
{ok, _FinalImplState} = erlang:apply(Impl, finalize, [Options, ImplState]),
% Open flamegraph viewer if specified
maybe_open_in_program(Options, Filename),
io:format("Output filename: ~s~n", [Filename]),
% The only reason we don't stop here is because this is a call and the
% linked call would crash as well. This feels kind of wrong so I may revisit
% this
{reply, ok, State, {continue, finish}};
handle_call(_Request, _From, State) ->
Reply = ok,
{reply, Reply, State}.
-spec handle_cast(any(), state()) -> {noreply, state()} |
{noreply, state(), timeout()} |
{stop, Reason :: any(), state()}.
handle_cast(_Msg, State) ->
{noreply, State}.
handle_continue(finish, State) ->
{stop, normal, State}.
-spec handle_info(Info :: any(), state()) -> {noreply, state()} |
{noreply, state(), timeout()} |
{stop, Reason :: any(), state()}.
handle_info(TraceMessage, #state{impl = Impl, impl_state = ImplState} = State)
when element(1, TraceMessage) == trace; element(1, TraceMessage) == trace_ts ->
{ok, UpdatedImplState} = erlang:apply(Impl, handle_trace_event, [TraceMessage, ImplState]),
{noreply, State#state{impl_state = UpdatedImplState}};
handle_info(Info, State) ->
logger:error("Received unexpected info message: ~w", [Info]),
{noreply, State}.
-spec terminate(Reason :: any(), state()) -> any().
terminate(_Reason, _State) ->
ok.
%%%===================================================================
%%% Internal functions
%%%===================================================================
% https://stackoverflow.com/questions/21873644/combine-merge-two-erlang-lists
merge(In1, In2) ->
Combined = In1 ++ In2,
Fun = fun(Key) ->
[FinalValue|_] = proplists:get_all_values(Key, Combined),
{Key, FinalValue}
end,
lists:map(Fun, proplists:get_keys(Combined)).
timestamp_integer() ->
{Mega, Secs, Micro} = erlang:timestamp(),
Mega*1000*1000*1000*1000 + Secs*1000*1000 + Micro.
generate_filename(Ext) ->
Name = io_lib:format("~B-~s.~s", [timestamp_integer(), <<"eflambe-output">>, Ext]),
list_to_binary(Name).
output_directory(Options) ->
proplists:get_value(output_directory, Options, "./").
maybe_open_in_program(Options, Filename) ->
case proplists:get_value(open, Options) of
undefined -> ok;
Program when Program =:= speedscope; Program =:= hotspot ->
_ = os:cmd(io_lib:format("~s ~s~n", [Program, Filename]));
_InvalidProgram -> ok
end.