%% ------------------------------------------------------------------- %% %% Copyright (c) 2014 Basho Technologies, Inc. All Rights Reserved. %% %% This Source Code Form is subject to the terms of the Mozilla Public %% License, v. 2.0. If a copy of the MPL was not distributed with this %% file, You can obtain one at http://mozilla.org/MPL/2.0/. %% %% ------------------------------------------------------------------- -module(exometer_function). -behaviour(exometer_entry). -export([behaviour/0, new/3, update/4, reset/3, get_value/4, get_datapoints/3, sample/3, delete/3, preprocess_setopts/5, setopts/3]). -export([empty/0]). -export([test_mem_info/1]). -export([eval_exprs/2]). -export_type([fun_spec/0, arg_spec/0, res_type/0]). -type arg() :: '$dp' | {'$call', atom(), atom(), arg_spec()} | any(). -type arg_spec() :: [arg()]. -type datapoints() :: [atom()]. -type mod_name() :: atom(). -type fun_name() :: atom(). -type res_type() :: value %% The return value is the result | proplist %% Pick the data point out of a proplist | tagged. %% Either {DataPoint,Value} or {ok,Value} -type simple_fun() :: {function, mod_name(), fun_name()}. -type extended_fun() :: {function, mod_name(), fun_name(), arg_spec(), res_type(), datapoints()}. -type fun_spec() :: simple_fun() | extended_fun(). -type fun_rep() :: {mod_name(), fun_name()} | {mod_name(), fun_name(), each | once, arg_spec(), res_type(), datapoints()} | {mod_name(), fun_name(), each | once, arg_spec(), match, any()} | {eval, [expr()], datapoints()}. -spec behaviour() -> exometer:behaviour(). behaviour() -> entry. -spec new(exometer:name(), 'function', exometer:options()) -> {ok, fun_rep()}. %% @doc Callback for creating an exometer `function' entry. %% %% Function entries are created as %%
%% exometer:new(Name,{function,...},Opts)
%% 
%% which is syntactic sugar for %%
%% exometer:new(Name,function,[{arg,{function,...}}|Opts])
%% 
%% %% `{function,...}' can be `{function, Mod, Fun}', in which case %% where `get_value(Name, DataPoints)' will result in a call to %% `Mod:Fun(DataPoints)'. %% Invoking get_value(Name) (with no datapoints), will call %% `Mod:Fun(default), which must return a default list of data point %% values. %% %% `{function,...}' can also be setup as `{function, %% Mod,Fun,ArgSpec,Type,DataPoints}' in order to invoke a limited %% interpreter. The `ArgSpec' is evaluated as follows: %% %% %% %% The return value of the above call will be processed according to `Type': %% %% %% An alternative version of `arg' is `{arg, {eval, Exprs, Datapoints}}', which %% doesn't in fact call a function, but simply evaluates `Exprs' using %% {@link eval_exprs/2}, with the pre-bound variables `Value = undefined' %% and `DPs = Datapoints'. %% %% Examples: %% %% An entry that returns a subset of `erlang:memory()': %% %%
%% exometer:new([mem], {function,erlang,memory,[],proplist,[total,processes]}).
%% 
%% %% An entry that reports the heap size and message queue length of the %% code server: %% %%
%% exometer:new(
%%     [code_server, pinfo],
%%     {function,erlang,process_info,[{'$call',erlang,whereis,[code_server]}],
%%      proplist, [heap_size, message_queue_len]}).
%% 
%% %% An entry that reports the heap size of the code server. %% %%
%% exometer:new(
%%   [code_server, heap_size],
%%   {function,erlang,process_info,
%%    [{'$call',erlang,whereis,[code_server]}, '$dp'], tagged, [heap_size]}).
%% 
%% %% An entry that does pattern-matching on the return value %% (`erlang:statistics(garbage_collection)' returns `{GCs, Reclaimed, 0}'). %% %%
%% exometer:new(
%%    [gc],
%%    { function,erlang,statistics,[garbage_collection],
%%      match, {gcs,reclaimed,'_'} }, []).
%% 
%% %% An entry that calls `erlang:processes()' and evaluates a list of expressions %% that calculate the length of the returned list. %% %%
%% exometer:new(
%%     [ps],
%%     {function,erlang,processes,[],
%%      eval, {[{l,[{t,[value,{call,length,[{v,'Value'}]}]}]}],[value]}}, []).
%% 
%% %% An entry that simply builds a list of datapoints, using the abstract syntax. %% %%
%% exometer:new([stub],
%%     {function,{eval,[{l,[{t,[{a,1}]},{t,[{b,2}]}]}], [a,b]}}, []).
%% 
%% @end new(_Name, function, Opts) -> case lists:keyfind(arg, 1, Opts) of {_, Arg} -> {ok, ref_from_arg(Arg)}; false -> {ok, {?MODULE, empty}} end. ref_from_arg({_M,_F} = Arg) -> Arg; ref_from_arg({M, F, ArgsP, Type, DPs}) -> {M, F, mode(ArgsP), ArgsP, Type, DPs}; ref_from_arg({eval,_,_} = Arg) -> Arg; ref_from_arg(_Arg) -> error(invalid_arg). arg_from_ref({_M,_F} = Ref) -> Ref; arg_from_ref({M, F, _, ArgsP, Type, DPs}) -> {M, F, ArgsP, Type, DPs}; arg_from_ref(_Ref) -> error(invalid_ref). get_value(_, function, {M, F, once, ArgsP, match, Pat}, DataPoints0) -> DataPoints = if DataPoints0 == default -> pattern_datapoints(Pat); is_list(DataPoints0) -> DataPoints0 end, try call_once(M, F, ArgsP, match, {Pat, DataPoints}) catch error:_ -> {error, unavailable} end; get_value(_, _, {M, F, each, ArgsP, Type, DPs}, DataPoints) -> [{D,call(M,F,ArgsP,Type,D)} || D <- datapoints(DataPoints, DPs), lists:member(D, DPs)]; get_value(_, function, {M, F, once, ArgsP, match, Pat}, DataPoints0) -> DataPoints = if DataPoints0 == default -> pattern_datapoints(Pat); is_list(DataPoints0) -> DataPoints0 end, try call_once(M, F, ArgsP, match, {Pat, DataPoints}) catch error:_ -> {error, unavailable} end; get_value(_, _, {M, F, once, ArgsP, Type, DPs}, DataPoints0) -> DataPoints = actual_datapoints(DataPoints0, DPs, Type), try call_once(M, F, ArgsP, Type, DataPoints) catch error:_ -> {error, unavailable} end; get_value(_, _, {eval, Exprs, DataPoints}, DataPoints0) -> DataPoints = actual_datapoints(DataPoints0, DataPoints, eval), return_eval(eval_expr(Exprs, undefined, DataPoints), DataPoints); get_value(_, _, {M, F}, DataPoints) -> if DataPoints == default -> M:F(DataPoints); is_list(DataPoints) -> [D || {K,_} = D <- M:F(DataPoints), lists:member(K, DataPoints)] end. actual_datapoints(default, default, histogram) -> exometer_histogram:datapoints(); actual_datapoints(DPs, default, histogram) -> actual_datapoints(DPs, exometer_histogram:datapoints(), histogram); actual_datapoints(default, DPs, _) -> DPs; actual_datapoints(DPs0, DPs, _) -> [D || D <- datapoints(DPs0, DPs), lists:member(D, DPs)]. get_datapoints(_Name, _Type, {_, _, once, _, match, Pat}) -> pattern_datapoints(Pat); get_datapoints(_Name, _Type, T) when is_tuple(T), is_list( element(size(T),T)) -> element(size(T), T); get_datapoints(_Name, _Type, _Ref) -> [value]. update(_, _, _, _) -> {error, unsupported}. sample(_, _, _) -> {error, unsupported}. reset(_, _, _) -> {error, unsupported}. preprocess_setopts(_Name, Opts, _Type, _Ref, _OldOpts) -> case {lists:keyfind(arg,1,Opts), lists:keyfind(ref,1,Opts)} of {{_,A}, {_,R}} -> case ref_from_arg(A) of R -> Opts; _ -> error({conflict, [{arg,A},{ref,R}]}) end; {{_,New}, false} -> [{ref, ref_from_arg(New)}|Opts]; {false, {ref, New}} -> [{arg, arg_from_ref(New)}|Opts]; _ -> Opts end. setopts(_, _, _) -> ok. delete(_, _, _) -> ok. empty() -> []. datapoints(default, DPs) -> datapoints_(DPs); datapoints(DataPoints,_) when is_list(DataPoints) -> DataPoints. datapoints_(DPs) when is_list(DPs) -> DPs; datapoints_(DP ) when is_atom(DP ) -> [DP]. pattern_datapoints(A) when is_atom(A), A =/= '_' -> [A]; pattern_datapoints([H|T]) -> pattern_datapoints(H) ++ pattern_datapoints(T); pattern_datapoints(T) when is_tuple(T) -> pattern_datapoints(tuple_to_list(T)); pattern_datapoints(_) -> []. call(M,F,ArgsP,T,D) -> try begin Args = substitute(ArgsP, D), return_dp(apply(M, F, Args), T, D) end catch error:_ -> undefined end. call_once(M,F,ArgsP,T,DPs) -> Args = substitute_dps(ArgsP, DPs), return_dps(apply(M, F, Args), T, DPs). substitute([], _) -> []; substitute(['$dp'|T], D) -> [D | substitute(T, D)]; substitute([{'$call', M, F, ArgsP}|T], D) -> Args = substitute(ArgsP, D), [apply(M, F, Args) | substitute(T, D)]; substitute([{'$value',V}|T], D) -> [V | substitute(T, D)]; substitute([H|T], D) -> [H|substitute(T, D)]. substitute_dps([], _) -> []; substitute_dps(['$datapoints'|T], DPs) -> [DPs | substitute_dps(T, DPs)]; substitute_dps([{'$call', M, F, ArgsP}|T], DPs) -> Args = substitute_dps(ArgsP, DPs), [apply(M, F, Args) | substitute_dps(T, DPs)]; substitute_dps([{'$value',V}|T], DPs) -> [V | substitute_dps(T, DPs)]; substitute_dps([H|T], DPs) -> [H|substitute_dps(T, DPs)]. return_dp({T,V}, tagged, D) when T==D; T==ok -> V; return_dp(V, value, _) -> V; return_dp(L, length, _) -> if is_list(L) -> length(L); true -> undefined end; return_dp(L, proplist, D) -> case lists:keyfind(D, 1, L) of false -> undefined; {_, V} -> V end. return_dps(L, value, _) -> L; return_dps({DP, V}, tagged, DPs) -> %% This is a slightly special case, which can happen e.g. for %% {function,erlang,process_info,[P, heap_size],tagged,heap_size}. %% The implicit mode will be 'once' since no '$dp' entry present, but %% only one data point will be returned. Since it's tagged, we know %% which data point it is. [{D, if D==DP -> V; true -> undefined end} || D <- DPs]; return_dps(L, proplist, DPs) -> [get_dp(D, L) || D <- DPs]; return_dps(L, histogram, DPs) -> exometer_util:histogram(L, DPs); return_dps(Val, eval, {Expr, DPs}) -> try return_eval(eval_expr(Expr, Val, DPs), DPs) catch error:_ -> undefined end; return_dps(Val, match, {Pat, DPs}) -> match_pat(Pat, Val, DPs). return_eval({value, L, _}, default) when is_list(L) -> L; return_eval({value, L, _}, DPs) when is_list(DPs), is_list(L) -> [get_dp(D, L) || D <- DPs]; return_eval(_, _) -> undefined. get_dp(D, L) -> case lists:keyfind(D, 1, L) of false -> {D, undefined}; V -> V end. mode(Args) -> case mode(Args, undefined) of undefined -> once; Other -> Other end. mode(['$dp'|T], M) -> if M==each; M==undefined -> mode(T, each); true -> error(mode_conflict) end; mode(['$datapoints'|T], M) -> if M==once; M==undefined -> mode(T, once); true -> error(mode_conflict) end; mode([H|T], M) when is_list(H) -> mode(T, mode(H, M)); mode([H|T], M) when is_tuple(H) -> mode(T, mode(tuple_to_list(H), M)); mode([_|T], M) -> mode(T, M); mode([], M) -> M. test_mem_info(DataPoints) -> Res = erlang:memory(), [get_dp(D, Res) || D <- DataPoints]. match_pat(Pat, Val, DPs) when tuple_size(Pat) == tuple_size(Val) -> match_pat(tuple_to_list(Pat), tuple_to_list(Val), DPs); match_pat(['_'|T], [_|T1], DPs) -> match_pat(T, T1, DPs); match_pat([H|T], [H1|T1], DPs) when is_atom(H) -> case lists:member(H, DPs) of true -> [{H, H1}|match_pat(T, T1, DPs)]; false -> match_pat(T, T1, DPs) end; match_pat([H|T], [H1|T1], DPs) -> match_pat(H, H1, DPs) ++ match_pat(T, T1, DPs); match_pat(A, B, DPs) when is_atom(A), A =/= '_' -> case lists:member(A, DPs) of true -> [{A, B}]; false -> [] end; match_pat(_, _, _) -> []. %% Expressions: eval_expr([_|_] = Exprs, Value, DPs) -> eval_exprs(Exprs, [{'DPs',DPs},{'Value',Value}]); eval_expr(Expr, Value, DPs) -> eval_exprs([Expr], [{'DPs',DPs},{'Value',Value}]). -type expr() :: expr_descr() | expr_action() | expr_match() | expr_erl(). -type expr_descr() :: expr_int() | expr_atom() | expr_list() | expr_tuple() | expr_string(). -type expr_action() :: expr_op() | expr_call() | expr_fold() | expr_case(). -type expr_int() :: integer() | {i, integer()} | {integer, integer()}. -type expr_atom() :: atom() | {a, atom()} | {atom, atom()}. -type expr_string() :: {string, string()} | {s, string()}. -type expr_tuple() :: {tuple, [expr()]} | {t, [expr()]}. -type expr_list() :: {cons, expr(), expr()} | nil | {l, [expr()]}. -type expr_match() :: {match, expr_pattern(), expr()} | {m, expr_pattern(), expr()}. -type expr_pattern() :: '_' | expr_descr(). -type expr_op() :: expr_unary_op() | expr_binary_op(). -type expr_unary_op() :: {op, '-' | 'not', expr()}. -type expr_binary_op() :: {op, expr_operator(), expr(), expr()}. -type expr_operator() :: '+' | '-' | '*' | '/' | 'div' | 'rem' | 'band' | 'and' | 'bor' | 'bxor' | 'bsl' | 'bsr' | 'or' | 'xor' | '++' | '--' | '==' | '/=' | '>=' | '=<' | '<' | '>' | '=:=' | '=/='. -type expr_call() :: {call, atom(), [expr()]} | {call, {atom(), atom()}, [expr()]}. -type expr_fold() :: {fold, _IterVal::atom(), _AccVar::atom(), _IterExpr::[expr()], _Acc0Expr::expr(), _ListExpr::expr()}. -type expr_case() :: {'case', [expr()], [expr_clause()]}. -type expr_clause() :: {expr_pattern(), [expr_guard()], [expr()]}. -type expr_guard() :: [expr()]. % Must all return 'true'. -type expr_erl() :: {erl, [erl_parse:abstract_expr()]}. -type binding() :: {atom(), any()}. -spec eval_exprs([expr()], [binding()]) -> {value, any(), [binding()]}. %% @doc Evaluate a list of abstract expressions. %% %% This function is reminiscent of `erl_eval:exprs/2', but with a slightly %% different expression grammar. Most prominently, forms have no line numbers, %% and a few aliases for more compact representation. Otherwise, the forms can %% be seen as mostly a subset of the Erlang abstract forms. %% %% The list of bindings correspods exactly to the bindings in `erl_eval'. %% %% * Integers: `{integer, I}', `{i, I}', or simply just the integer %% * Atoms: `{atom, A}', `{a, A}', or simply just the atom (note that some atoms %% are special). %% * Lists: `{cons, H, T}', `nil', or `{l, [...]}' %% * Tuples: `{tuple, [Elem]}', or `{t, [Elem]}' %% * Variables: `{var, V}', or `{v, V}' %% * Matches: `{match, Pattern, Expr}', or `{m, Pattern, Expr}' %% * Function calls: `{call, {M, F}, Args}', or `{call, F, Args}' %% * Folds: `{fold, IterVar, AccVar, [IterExpr], Acc0Expr, ListExpr}' %% * Operators: `{op, Op, ExprA, ExprB}' %% * Unary operators: {op, '-' | 'not', Expr} %% * Case exprs: {'case', [Expr], [{Pat, Gs, Body}]} %% * Generic Erlang: `{erl, [ErlAbstractExpr]}' %% %% The currently supported "built-in functions" are `length/1', `size/1', %% `byte_size/1' and `bit_size/1'. %% %% The operators supported are all the Erlang binary operators (as in: '+', %% '-', '==', '=/=', etc.) %% %% When evaluating guards in a case clause, any expression is legal. The %% guard must return true to succeed. Note that the abstract form of a guard %% sequence is [ [G11,...], [G21,...], ...], where each sublist represents %% an 'and' sequence, i.e. all guards in the sublist must succeed. The %% relationship between sublists is 'or'. This is the same as in Erlang. %% @end eval_exprs([E|Es], Bs) -> {value, Val, Bs1} = eval_(E, Bs), eval_exprs(Es, Val, Bs1). eval_exprs([], Val, Bs) -> {value, Val, Bs}; eval_exprs([E|Es], _, Bs) -> {value, Val, Bs1} = eval_(E, Bs), eval_exprs(Es, Val, Bs1). eval_({erl, Exprs}, Bs) -> erl_eval:exprs(Exprs, Bs); eval_({T, P, E}, Bs) when T==m; T==match -> Val = e(E, Bs), {value, Val, match(P, Val, Bs)}; eval_({'case', Es, Cls}, Bs) -> {value, V, _} = eval_exprs(Es, Bs), case_clauses(Cls, V, Bs); eval_(Expr, Bs) -> {value, e(Expr, Bs), Bs}. e({T,V}, Bs) when T==v; T==var -> case erl_eval:binding(V, Bs) of unbound -> error({unbound, V}); {value, Val} -> Val end; e(nil, _) -> []; e(I, _) when is_integer(I) -> I; e(A, _) when is_atom(A) -> A; e({T,I}, _) when T==i; T==integer -> I; e({T,A}, _) when T==a; T==atom -> A; e({cons,Eh,Et}, Bs) -> [e(Eh, Bs)|e(Et, Bs)]; e({hd,E}, Bs) -> hd(e(E, Bs)); e({tl,E}, Bs) -> tl(e(E, Bs)); e({l, Es}, Bs) -> [e(E, Bs) || E <- Es]; e({T,S}, _) when T==s; T==string -> S; e({T,Es}, Bs) when T==t; T==tuple -> list_to_tuple([e(E,Bs) || E <- Es]); e({call,F,As}, Bs) -> call1(F, [e(A,Bs) || A <- As]); e({lc,_E0,_Es}, _Bs) -> error(nyi); e({op,Op,E1,E2}, Bs) -> op(Op, e(E1,Bs), e(E2,Bs)); e({op,Op,E}, Bs) when Op=='-'; Op=='not' -> erlang:Op(e(E, Bs)); e({element,E,T}, Bs) -> case e(T, Bs) of Tup when is_tuple(Tup) -> element(e(E,Bs), Tup); _ -> error(badarg) end; e({histogram, Vs}, Bs) -> case e(Vs, Bs) of L when is_list(L) -> exometer_util:histogram(L); _ -> error(badarg) end; e({histogram, Vs, DPs}, Bs) -> case e(Vs, Bs) of L when is_list(L) -> DataPoints = case e(DPs, Bs) of default -> default; D when is_list(D) -> D; _ -> error(badarg) end, exometer_util:histogram(L, DataPoints); _ -> error(badarg) end; e({fold,Vx,Va,Es,Ea,El}, Bs) when is_atom(Vx), is_atom(Va) -> case e(El, Bs) of L when is_list(L) -> Bs1 = erl_eval:add_binding(Va, e(Ea, Bs), Bs), fold_(L, Vx, Va, Es, Bs1); _ -> error(badarg) end. match({T,Es}, V, Bs) when T==t; T==tuple -> Vals = tuple_to_list(V), Z = lists:zip(Es, Vals), lists:foldl(fun({E1,V1}, Bs1) -> match(E1, V1, Bs1) end, Bs, Z); match('_', _, Bs) -> Bs; match(nil, [], Bs) -> Bs; match({T,X}, V, Bs) when T==a; T==atom; T==i; T==integer -> X = V, Bs; match(I, I, Bs) when is_integer(I) -> Bs; match(A, V, Bs) when is_atom(A) -> case atom_to_list(A) of "_" ++ _ -> Bs; _ -> A = V, Bs end; match({cons,Eh,Et}, [H|T], Bs) -> Bs1 = match(Eh, H, Bs), match(Et, T, Bs1); match({T,Var}, V, Bs) when T==v; T==var -> erl_eval:add_binding(Var, V, Bs). case_clauses([{Pat, Gs, Body}|Cs], Val, Bs) -> try match(Pat, Val, Bs) of Bs1 -> case match_gs(Gs, Bs1) of true -> eval_exprs(Body, Bs1); _ -> case_clauses(Cs, Val, Bs) end catch error:_ -> case_clauses(Cs, Val, Bs) end; case_clauses([], _, _) -> error(case_clause). match_gs([], _) -> true; match_gs([_|_] = Gs, Bs) -> and_gs(Gs, Bs). and_gs([], _) -> false; and_gs([G|Gs], Bs) -> try [{value, true, _} = eval_(G1, Bs) || G1 <- G], true catch error:_ -> and_gs(Gs, Bs) end. call1(length , [L]) -> length(L); call1(size , [T]) -> size(T); call1(byte_size, [B]) -> byte_size(B); call1(bit_size , [B]) -> bit_size(B); call1(tuple_to_list , [T]) -> tuple_to_list(T); call1(list_to_tuple , [L]) -> list_to_tuple(L); call1(atom_to_list , [A]) -> atom_to_list(A); call1(list_to_atom , [L]) -> list_to_atom(L); call1(list_to_binary, [L]) -> list_to_binary(L); call1(binary_to_list, [B]) -> binary_to_list(B); call1(t2l, [T]) -> tuple_to_list(T); call1(l2t, [L]) -> list_to_tuple(L); call1(a2l, [A]) -> atom_to_list(A); call1(l2a, [L]) -> list_to_atom(L); call1(l2b, [L]) -> list_to_binary(L); call1(b2l, [B]) -> binary_to_list(B); call1({M,F}, As) when is_atom(M), is_atom(F) -> apply(M, F, As). op(Op, A, B) when is_atom(Op) -> erlang:Op(A, B). fold_([H|T], Vx, Va, Es, Bs) -> Bs1 = erl_eval:add_binding(Vx, H, Bs), {value, NewA, _} = eval_exprs(Es, Bs1), fold_(T, Vx, Va, Es, erl_eval:add_binding(Va, NewA, Bs)); fold_([], _, Va, _, Bs) -> {value, Acc} = erl_eval:binding(Va, Bs), Acc.