%%% @copyright (C) 2019-2023, Maxim Fedorov %%% @doc %%% Job is an instance of a benchmark. %%% %%% Every job has a corresponding temporary Erlang module generated. Use %%% {@link source/1} to get the source code of the generated module. %%% The structure of this code is an implementation detail and may change %%% between releases. %%% %%% Job controls how many workers are executing runner code in %%% a tight loop. It does not restart a failing worker, user must ensure %%% proper error handing and reporting. If a worker process crashes, %%% standard CRASH REPORT message is printed to the log (console). %%% %%% Job accepts a {@link code_map()} containing at least a runner %%% function definition. %%% %%% See {@link callable()} for accepted function definitions. %%% %%% Different callable forms have different performance overhead. Overhead can be measured %%% with {@link erlperf:compare/2}: %%% ```erlang %%% erlperf:compare([ %%% #{runner => fun (V) -> rand:mwc59(V) end, init_runner => {rand, mwc59_seed, []}}, %%% #{runner => "run(V) -> rand:mwc59(V).", init_runner => {rand, mwc59_seed, []}} %%% ], #{}). %% [4371541,131460130] %%% ''' %%% In the example above, callable defined as `fun' is 30 times slower than the code compiled %%% from the source. The difference is caused by the Erlang Runtime implementation, where %%% indirect calls via `fun' are considerably more expensive. As a rule of thumb, source %%% code provides the smallest overhead, followed by MFA tuples. %%% %%% You can mix & match various definition styles. In the example below, `init/0' %%% starts an extra {@link pg} scope, `done/0' stops it, and `init_runner/1' takes %%% the total heap size of `pg' scope controller to pass it to the `runner/1'. %%% ```erlang %%% erlperf_job:start_link( %%% #{ %%% runner => "run(Max) -> rand:uniform(Max).", %%% init => {pg, start_link, [scope]}, %%% init_runner => %%% fun ({ok, Pid}) -> %%% {total_heap_size, THS} = erlang:process_info(Pid, total_heap_size), %%% THS %%% end, %%% done => fun ({ok, Pid}) -> gen_server:stop(Pid) end %%% } %%% ). %%% ''' %%% Same example defined with just the source code: %%% ```erlang %%% erlperf_job:start_link( %%% #{ %%% runner => "runner(Max) -> rand:uniform(Max).", %%% init => "init() -> pg:start_link().", %%% init_runner => "init_runner({ok, Pid}) -> %%% {total_heap_size, THS} = erlang:process_info(Pid, total_heap_size), %%% THS.", %%% done => "done({ok, Pid}) -> gen_server:stop(Pid)." %%% } %%% ). %%% ''' %%% %%%

Runner function

%%% Runner function represents code that is run in the tight loop, counting iterations %%% aggregated between all workers. To give an example, benchmarking a function that takes %%% exactly a millisecond to execute, with 2 workers, for 2 seconds, will result in %%% 4000 iterations in total. This would be the value returned by {@link sample/1}. %%% %%% %%% Runner definition can accept zero, one or two arguments. %%% %%% `runner/0' ignores the value returned by init_runner/0,1. %%% %%% `runner/1' accepts the value returned by init_runner/0,1. It is an error to define %%% `runner/1' without `init_runner/0,1' defined. This example prints "0" in a %%% tight loop, measuring {@link io:format/2} performance: %%% ```erlang %%% #{ %%% runner => "run(Init) -> io:format(\"~b~n\", [Init]).", %%% init_runner => "0." %%% } %%% ''' %%% %%% `runner/2' adds second argument, accumulator, initially set to the %%% value returned by init_runner/0,1. Subsequent invocations receive %%% value returned by the previous runner invocation. Example: %%% ```erlang %%% #{ %%% runner => "run(Init, Acc) -> io:format(\"~b~n\", [Init + Acc]), Acc + 1.", %%% init_runner => "0." %%% } %%% ''' %%% Running this benchmark prints monotonically increasing numbers. This %%% may be useful to test stateful functions, for example, fast Random Number %%% Generators introduced in OTP 25: %%% ```bash %%% ./erlperf --init_runner 'rand:mwc59_seed().' 'run(_, Cur) -> rand:mwc59(Cur).' %%% Code || QPS Time %%% run(_, Cur) -> rand:mwc59(Cur). 1 123 Mi 8 ns %%% ''' %%% %%% %%%

Common Test usage

%%% %%% Example using `erlperf_job' directly, as a part of Common Test test case: %%% ```erlang %%% benchmark_rand(Config) when is_list(Config) -> %%% %% run timer:sleep(1000) for 5 second, 4 runners %%% {ok, Job} = erlperf_job:start_link(#{runner => {timer, sleep, [1000]}}), %%% Handle = erlperf_job:handle(Job), %%% ok = erlperf_job:set_concurrency(Job, 4), %% 4 runner instances %%% InitialIterations = erlperf_job:sample(Handle), %%% timer:sleep(5000), %%% IterationsIn5Sec = erlperf_job:sample(Handle) - InitialIterations, %%% erlperf_job:request_stop(Job), %% use gen:stop(Job) for synchronous call %%% %% expect at least 16 iterations (and up to 20) %%% ?assert(IterationsIn5Sec >= 16, {too_slow, IterationsIn5Sec}), %%% ?assert(IterationsIn5Sec =< 20, {too_fast, IterationsIn5Sec}). %%% ''' %%% %%% @end -module(erlperf_job). -author("maximfca@gmail.com"). -behaviour(gen_server). %% Job API -export([ start/1, start_link/1, request_stop/1, concurrency/1, set_concurrency/2, measure/2, sample/1, handle/1, source/1, set_priority/2 ]). %% gen_server callbacks -export([ init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2 ]). %% MFArgs: module, function, arguments. -type mfargs() :: {Module :: module(), Function :: atom(), Args :: [term()]}. %% `Module', `Function', `Args' accepted by {@link erlang:apply/3}. %% Callable: one or more MFArgs, or a function object, or source code -type callable() :: string() | fun() | fun((term()) -> term()) | fun((term(), term()) -> term()) | mfargs() | [mfargs()]. %% Function definition to use as a runner, init, done or init_runner. %% %% %% Benchmark code: init, init_runner, runner, done. -type code_map() :: #{ runner := callable(), init => callable(), init_runner => callable(), done => callable() }. %% Code map contains definitions for: %% %% %% Internal (opaque) type, please do not use -opaque handle() :: {module(), non_neg_integer()}. %% Temporary type until OTP25+ is everywhere, and OTP <25 support is no longer needed -type server_ref() :: gen_server:server_ref(). -export_type([mfargs/0, handle/0, callable/0, code_map/0]). %% @doc %% Starts the benchmark job. %% %% Job starts with no workers, use {@link set_concurrency/2} to start workers. -spec start(code_map()) -> {ok, pid()} | {error, term()}. start(#{runner := _MustHave} = Code) -> gen_server:start(?MODULE, generate(Code), []). %% @doc %% Starts the job and links it to caller. %% %% Job starts with no workers, use {@link set_concurrency/2} to start workers. -spec start_link(code_map()) -> {ok, pid()} | {error, term()}. start_link(#{runner := _MustHave} = Code) -> gen_server:start_link(?MODULE, generate(Code), []). %% @doc %% Requests this job to stop. %% %% Job is stopped asynchronously. Caller should monitor the job process %% to find out when the job actually stopped. -spec request_stop(server_ref()) -> ok. request_stop(JobId) -> gen_server:cast(JobId, stop). %% @doc %% Returns the number of concurrently running workers for this job. %% %% This number may be lower than the amount requested by {@link set_concurrency/2} %% if workers crash. -spec concurrency(server_ref()) -> Concurrency :: non_neg_integer(). concurrency(JobId) -> gen_server:call(JobId, concurrency). %% @doc %% Sets the number of concurrently running workers for this job. %% %% Does not reset counting. May never return if init_runner %% hangs and does not return control the the job. %% `Concurrency': number of processes to run. It can be higher than %% the current count (making the job to start more workers), or %% lower, making the job to stop some. %% %% Workers that crashes are not restarted automatically. -spec set_concurrency(server_ref(), non_neg_integer()) -> ok. set_concurrency(JobId, Concurrency) -> gen_server:call(JobId, {set_concurrency, Concurrency}, infinity). %% @doc %% Run the timed mode benchmark for a job, similar to {@link timer:tc/3}. %% %% Executes the runner `SampleCount' times. Returns time in microseconds. %% Has less overhead compared to continuous benchmarking, therefore can %% be used even for very fast functions. -spec measure(server_ref(), SampleCount :: non_neg_integer()) -> TimeUs :: non_neg_integer() | already_started. measure(JobId, SampleCount) -> gen_server:call(JobId, {measure, SampleCount}, infinity). %% @doc %% Returns the sampling handle for the job. %% %% The returned value is opaque, and is an implementation detail, %% do not use it in any quality other than passing to {@link sample/1}. -spec handle(server_ref()) -> handle(). handle(JobId) -> gen_server:call(JobId, handle). %% @doc %% Returns the current iteration counter. %% %% The iteration counter (sample) monotonically grows by 1 %% every time the runner function is called (without waiting %% for it to return, so a function that unconditionally crashes %% still generates a counter of 1). -spec sample(Handle :: handle()) -> non_neg_integer() | undefined. sample({Module, Arity}) -> {call_count, Count} = erlang:trace_info({Module, Module, Arity}, call_count), Count. %%% Internal, not exported, record. -record(exec, { name :: atom(), %% generated module name (must be generated for tracing to work) source :: [string()], %% module source code binary :: binary(), %% generated bytecode init :: fun(() -> term()), %% init function init_runner :: fun((term()) -> term()), %% must accept 1 argument runner :: {fun((term()) -> term()), non_neg_integer()}, sample_runner :: {fun((non_neg_integer(), term()) -> term()), non_neg_integer()}, done :: fun((term()) -> term()) %% must accept 1 argument }). -type exec() :: #exec{}. %% @doc %% Returns the source code generated from the code map, or for a running job. -spec source(server_ref() | code_map()) -> [string()]. source(Code) when is_map(Code) -> #exec{source = Src} = generate(benchmark, Code), Src; source(JobId) -> gen_server:call(JobId, source). %% @doc %% Sets job process priority when there are workers running. %% %% Worker processes may utilise all schedulers, making job %% process to lose control over starting and stopping workers. %% By default, job process sets 'high' priority when there are %% any workers running. %% Returns the previous setting. %% %% This function must be called before {@link set_concurrency/2}, %% otherwise it has no effect until all workers are stopped, and %% then restarted. -spec set_priority(server_ref(), erlang:priority_level()) -> erlang:priority_level(). set_priority(JobId, Priority) -> gen_server:call(JobId, {priority, Priority}). %%-------------------------------------------------------------------- %% Internal definitions -include_lib("kernel/include/logger.hrl"). -record(erlperf_job_state, { %% original spec exec :: exec(), %% return value of init/1 init_result :: term(), %% continuous workers workers = [] :: [pid()], %% temporary workers (for sample_count call) sample_workers = #{} :: #{pid() => {pid(), reference()}}, %% priority to return to when no workers left initial_priority :: erlang:priority_level(), %% priority to set when workers are running priority = high :: erlang:priority_level() }). -type state() :: #erlperf_job_state{}. %%%=================================================================== %%% gen_server callbacks %% @private init(#exec{name = Mod, binary = Bin, init = Init, runner = {_Fun, Arity}} = Exec) -> %% need to trap exits to avoid crashing and not cleaning up the loaded module erlang:process_flag(trap_exit, true), {module, Mod} = code:load_binary(Mod, Mod, Bin), %% run the init/0 if defined InitRet = try Init() catch Class:Reason:Stack -> %% clean up loaded module before crashing code:purge(Mod), code:delete(Mod), erlang:raise(Class, Reason, Stack) end, %% register in the monitor ok = erlperf_monitor:register(self(), {Mod, Arity}, 0), %% start tracing this module runner function 1 = erlang:trace_pattern({Mod, Mod, Arity}, true, [local, call_count]), {priority, Prio} = erlang:process_info(self(), priority), {ok, #erlperf_job_state{exec = Exec, init_result = InitRet, initial_priority = Prio}}. %% @private -spec handle_call(term(), {pid(), reference()}, state()) -> {reply, term(), state()}. handle_call(handle, _From, #erlperf_job_state{exec = #exec{name = Name, runner = {_Fun, Arity}}} = State) -> {reply, {Name, Arity}, State}; handle_call(concurrency, _From, #erlperf_job_state{workers = Workers} = State) -> {reply, length(Workers), State}; handle_call({measure, SampleCount}, From, #erlperf_job_state{sample_workers = SampleWorkers, exec = #exec{init_runner = InitRunner, sample_runner = SampleRunner}, init_result = IR} = State) when SampleWorkers =:= #{} -> {noreply, State#erlperf_job_state{sample_workers = start_sample_count(SampleCount, From, InitRunner, IR, SampleRunner)}}; handle_call({measure, _SampleCount}, _From, #erlperf_job_state{} = State) -> {reply, already_started, State}; handle_call(source, _From, #erlperf_job_state{exec = #exec{source = Source}} = State) -> {reply, Source, State}; handle_call({priority, Prio}, _From, #erlperf_job_state{priority = Old} = State) -> {reply, Old, State#erlperf_job_state{priority = Prio}}; handle_call({set_concurrency, Concurrency}, _From, #erlperf_job_state{workers = Workers} = State) -> {reply, ok, State#erlperf_job_state{workers = set_concurrency_impl(length(Workers), Concurrency, State)}}. %% @private handle_cast(stop, State) -> {stop, normal, State}. %% @private -spec handle_info(term(), state()) -> {noreply, state()}. handle_info({'EXIT', SampleWorker, Reason}, #erlperf_job_state{sample_workers = SampleWorkers} = State) when is_map_key(SampleWorker, SampleWorkers) -> {ReplyTo, MoreSW} = maps:take(SampleWorker, SampleWorkers), gen:reply(ReplyTo, Reason), {noreply, State#erlperf_job_state{sample_workers = MoreSW}}; handle_info({'EXIT', Worker, Reason}, #erlperf_job_state{workers = Workers} = State) when Reason =:= shutdown -> {noreply, State#erlperf_job_state{workers = lists:delete(Worker, Workers)}}; handle_info({'EXIT', Worker, Reason}, #erlperf_job_state{workers = Workers} = State) -> {stop, Reason, State#erlperf_job_state{workers = lists:delete(Worker, Workers)}}. %% @private -spec terminate(term(), state()) -> ok. terminate(_Reason, #erlperf_job_state{init_result = IR, workers = Workers, exec = #exec{name = Mod, done = Done}} = State) -> %% terminate all workers first set_concurrency_impl(length(Workers), 0, State), %% call "done" for cleanup try Done(IR) catch Class:Reason:Stack -> %% duly note, but do not crash, it is pointless at this moment ?LOG_ERROR("Exception while executing 'done': ~s:~0p~n~0p", [Class, Reason, Stack]) after _ = code:purge(Mod), true = code:delete(Mod) end. %%%=================================================================== %%% Internal: runner implementation %% Single run start_sample_count(SampleCount, ReplyTo, InitRunner, InitRet, {SampleRunner, _}) -> Child = erlang:spawn_link( fun() -> %% need to send a message even if init_runner fails, hence 'after' IRR = InitRunner(InitRet), T1 = erlang:monotonic_time(), SampleRunner(SampleCount, IRR), T2 = erlang:monotonic_time(), Time = erlang:convert_time_unit(T2 - T1, native, microsecond), exit(Time) end ), #{Child => ReplyTo}. set_concurrency_impl(OldConcurrency, Concurrency, #erlperf_job_state{workers = Workers, init_result = IR, exec = Exec, priority = Prio, initial_priority = InitialPrio}) -> case Concurrency - OldConcurrency of 0 -> Workers; NeedMore when NeedMore > 0 -> %% this process must run with higher priority to avoid being de-scheduled by runners OldConcurrency =:= 0 andalso erlang:process_flag(priority, Prio), Workers ++ add_workers(NeedMore, Exec, IR, []); NeedLess -> {Fire, Keep} = lists:split(-NeedLess, Workers), stop_workers(Fire), Keep =:= [] andalso erlang:process_flag(priority, InitialPrio), Keep end. add_workers(0, _ExecMap, _InitRet, NewWorkers) -> %% ensure all new workers completed their InitRunner routine [receive {Worker, init_runner} -> ok end || Worker <- NewWorkers], [Worker ! go || Worker <- NewWorkers], NewWorkers; add_workers(More, #exec{init_runner = InitRunner, runner = {Runner, _RunnerArity}} = Exec, InitRet, NewWorkers) -> Control = self(), %% spawn all processes, and then wait until they complete InitRunner Worker = erlang:spawn_link( fun () -> %% need to send a message even if init_runner fails, hence 'after' IRR = try InitRunner(InitRet) after Control ! {self(), init_runner} end, receive go -> ok end, Runner(IRR) end), add_workers(More - 1, Exec, InitRet, [Worker | NewWorkers]). stop_workers(Workers) -> %% try to stop concurrently [exit(Worker, kill) || Worker <- Workers], [receive {'EXIT', Worker, _Reason} -> ok end || Worker <- Workers]. %%%=================================================================== %%% Internal: code generation %% @doc Creates an Erlang module (text) based on the code map passed %% Returns module name (may be generated), runner arity (for tracing purposes), %% and module source code (text) %% Exception: raises error with Reason = {generate, {FunName, Arity, ...}} %% %% Important: early erlperf versions were generating AST (forms) instead %% of source code, which isn't exactly supported - AST is internal thing %% that can change over time. -spec generate(code_map()) -> exec(). generate(Code) -> Name = list_to_atom(lists:concat(["job_", os:getpid(), "_", erlang:unique_integer([positive])])), generate(Name, Code). generate(Name, #{runner := Runner} = Code) -> {InitFun, InitArity, InitExport, InitText} = generate_init(Name, maps:get(init, Code, error)), {IRFun, IRArity, IRExport, IRText} = generate_one(Name, init_runner, maps:get(init_runner, Code, error)), {DoneFun, DoneArity, DoneExport, DoneText} = generate_one(Name, done, maps:get(done, Code, error)), %% Separator: CR/LF Sep = io_lib:format("~n", []), %% RunnerArity: how many arguments _original_ runner wants to accept. %% Example: run(State) is 1, and run() is 0. %% Pass two function names: one that is for sample_count, and one for continuous ContName = atom_to_list(Name), SampleCountName = list_to_atom(ContName ++ "_finite"), {RunnerFun, SampleRunnerFun, RunnerArity, RunArity, RunnerText} = generate_runner(Name, SampleCountName, Runner, Sep), RunnerExports = [{Name, RunArity}, {SampleCountName, RunArity + 1}], %% verify compatibility between 4 pieces of code %% 1. done/1 requires init/0 return value DoneArity =:= 1 andalso InitArity =:= undefined andalso erlang:error({generate, {done, 1, requires, init}}), %% 2. init_runner/1 requires init/0,1 IRArity =:= 1 andalso InitArity =:= undefined andalso erlang:error({generate, {init_runner, 1, requires, init}}), %% 3. runner/1,2 requires init/0,1 RunnerArity > 0 andalso IRArity =:= undefined andalso erlang:error({generate, {runner, RunnerArity, requires, init_runner}}), %% 4. runner/[3+] is not allowed RunnerArity > 2 andalso erlang:error({generate, {runner, RunnerArity, not_supported}}), %% 5. TODO: Verify there are no name clashes %% Exports = lists:concat(lists:join(", ", [io_lib:format("~s/~b", [F, Arity]) || {F, Arity} <- [InitExport, IRExport, DoneExport | RunnerExports], Arity =/= undefined])), Texts = [Text ++ Sep || Text <- [InitText, IRText, DoneText | RunnerText], Text =/= ""], Source = ["-module(" ++ atom_to_list(Name) ++ ")." ++ Sep, "-export([" ++ Exports ++ "])." ++ Sep | Texts], #exec{name = Name, binary = compile(Name, Source), init = InitFun, init_runner = IRFun, source = Source, runner = {RunnerFun, RunArity}, sample_runner = {SampleRunnerFun, RunArity}, done = DoneFun}. %% generates init/0 code generate_init(_Mod, Fun) when is_function(Fun, 0) -> {Fun, 0, {[], undefined}, ""}; generate_init(_Mod, {M, F, Args}) when is_atom(M), is_atom(F), is_list(Args) -> {fun () -> erlang:apply(M, F, Args) end, 0, {[], undefined}, ""}; generate_init(_Mod, [{M, F, Args} | _Tail] = MFAList) when is_atom(M), is_atom(F), is_list(Args) -> [erlang:error({generate, {init, 0, invalid}}) || {M1, F1, A} <- MFAList, not is_atom(M1) orelse not is_atom(F1) orelse not is_list(A)], {fun () -> [erlang:apply(M1, F1, A) || {M1, F1, A} <- MFAList] end, 0, {[], undefined}, ""}; generate_init(Mod, Text) when is_list(Text) -> case generate_text(init, Text, false) of {0, NewName, FullText} -> {fun () -> Mod:NewName() end, 0, {NewName, 0}, FullText}; {WrongArity, NewName, _} -> erlang:error({generate, {init, NewName, WrongArity}}) end; generate_init(_Mod, error) -> {fun () -> undefined end, undefined, undefined, ""}. %% generates init_runner/1 or done/1 generate_one(_Mod, _FunName, error) -> {fun (_Ignore) -> undefined end, undefined, {[], undefined}, ""}; generate_one(_Mod, _FunName, Fun) when is_function(Fun, 1) -> {Fun, 1, {[], undefined}, ""}; generate_one(_Mod, _FunName, Fun) when is_function(Fun, 0) -> {fun (_Ignore) -> Fun() end, 0, {[], undefined}, ""}; generate_one(_Mod, _FunName, {M, F, Args}) when is_atom(M), is_atom(F), is_list(Args) -> {fun (_Ignore) -> erlang:apply(M, F, Args) end, 0, {[], undefined}, ""}; generate_one(_Mod, FunName, [{M, F, Args} | _Tail] = MFAList) when is_atom(M), is_atom(F), is_list(Args) -> [erlang:error({generate, {FunName, 1, invalid, {M1, F1, A}}}) || {M1, F1, A} <- MFAList, not is_atom(M1) orelse not is_atom(F1) orelse not is_list(A)], {fun (_Ignore) -> [erlang:apply(M1, F1, A) || {M1, F1, A} <- MFAList] end, 0, {[], undefined}, ""}; generate_one(Mod, FunName, Text) when is_list(Text) -> case generate_text(FunName, Text, false) of {0, NewName, FullText} -> {fun (_Ignore) -> Mod:NewName() end, 0, {NewName, 0}, FullText}; {1, NewName, FullText} -> {fun (Arg) -> Mod:NewName(Arg) end, 1, {NewName, 1}, FullText}; {WrongArity, NewName, _} -> erlang:error({generate, {FunName, WrongArity, NewName}}) end. %% runner wrapper: %% Generates at least 2 functions, one for continuous, and one for %% sample-count benchmarking. generate_runner(Mod, SampleCountName, Fun, Sep) when is_function(Fun, 0) -> { fun (_Ignore) -> Mod:Mod(Fun) end, fun (SampleCount, _Ignore) -> Mod:SampleCountName(SampleCount, Fun) end, 0, 1, [lists:concat([Mod, "(Fun) -> Fun(),", Sep, " ", Mod, "(Fun)."]), lists:concat([SampleCountName, "(0, _Fun) ->", Sep, " ok;", Sep, SampleCountName, "(Count, Fun) ->", Sep, " Fun(),", Sep, " ", SampleCountName, "(Count - 1, Fun)."])] }; generate_runner(Mod, SampleCountName, Fun, Sep) when is_function(Fun, 1) -> { fun (Init) -> Mod:Mod(Init, Fun) end, fun (SampleCount, Init) -> Mod:SampleCountName(SampleCount, Init, Fun) end, 1, 2, [lists:concat([Mod, "(Init, Fun) ->", Sep, " Fun(Init),", Sep, " ", Mod, "(Init, Fun)."]), lists:concat([SampleCountName, "(0, _Init, _Fun) ->", Sep, " ok;", Sep, SampleCountName, "(Count, Init, Fun) ->", Sep, " Fun(Init),", Sep, " ", SampleCountName, "(Count - 1, Init, Fun)."])] }; generate_runner(Mod, SampleCountName, Fun, Sep) when is_function(Fun, 2) -> { fun (Init) -> Mod:Mod(Init, Init, Fun) end, fun (SampleCount, Init) -> Mod:SampleCountName(SampleCount, Init, Init, Fun) end, 2, 3, [lists:concat([Mod, "(Init, State, Fun) ->", Sep, " ", Mod, "(Init, Fun(Init, State), Fun)."]), lists:concat([SampleCountName, "(0, _Init, _State, _Fun) ->", Sep, " ok; ", SampleCountName, "(Count, Init, State, Fun) ->", Sep, " ", SampleCountName, "(Count - 1, Init, Fun(Init, State), Fun)."])] }; %% runner wrapper: MFA generate_runner(Mod, SampleCountName, {M, F, Args}, Sep) when is_atom(M), is_atom(F), is_list(Args) -> { fun (_Ignore) -> Mod:Mod(M, F, Args) end, fun (SampleCount, _Ignore) -> Mod:SampleCountName(SampleCount, M, F, Args) end, 0, 3, [lists:concat([Mod, "(M, F, A) ->", Sep, " erlang:apply(M, F, A), ", Mod, "(M, F, A)."]), lists:concat([SampleCountName, "(0, _M, _F, _A) ->", Sep, " ok;", Sep, SampleCountName, "(Count, M, F, A) ->", Sep, " erlang:apply(M, F, A), ", Sep, " ", SampleCountName, "(Count - 1, M, F, A)."])] }; %% runner wrapper: MFAList generate_runner(Mod, SampleCountName, [{M, F, Args} | _Tail] = MFAList, Sep) when is_atom(M), is_atom(F), is_list(Args) -> [erlang:error({generate, {runner, 0, invalid, {M1, F1, A}}}) || {M1, F1, A} <- MFAList, not is_atom(M1) orelse not is_atom(F1) orelse not is_list(A)], { fun (_Ignore) -> Mod:Mod(MFAList) end, fun (SampleCount, _Ignore) -> Mod:SampleCountName(SampleCount, MFAList) end, 0, 1, [lists:concat([Mod, "(MFAList) -> [erlang:apply(M, F, A) || {M, F, A} <- MFAList], ", Mod, "(MFAList)."]), lists:concat([SampleCountName, "(0, _MFAList) -> ", Sep, " ok;", SampleCountName, "(Count, MFAList) ->", Sep, " [erlang:apply(M, F, A) || {M, F, A} <- MFAList], ", SampleCountName, "(Count - 1, MFAList)."])] }; generate_runner(Mod, SampleCountName, Text, Sep) when is_list(Text) -> case generate_text(runner, Text, true) of {0, NoDotText} -> %% very special case: embedding the text directly, without creating a new function %% at all. { fun (_Ignore) -> Mod:Mod() end, fun (SampleCount, _Ignore) -> Mod:SampleCountName(SampleCount) end, 0, 0, [lists:concat([Mod, "() ->", Sep, " ", NoDotText, ",", Sep, " ", Mod, "()."]), lists:concat([SampleCountName, "(0) ->", Sep, " ok;", Sep, SampleCountName, "(Count) ->", Sep, " ", NoDotText, ",", Sep, " ", SampleCountName, "(Count - 1)."]), ""] }; {0, NewName, FullText} -> { fun (_Ignore) -> Mod:Mod() end, fun (SampleCount, _Ignore) -> Mod:SampleCountName(SampleCount) end, 0, 0, [lists:concat([Mod, "() ->", Sep, " ", NewName, "(),", Sep, " ", Mod, "()."]), lists:concat([SampleCountName, "(0) ->", Sep, " ok;", Sep, SampleCountName, "(Count) ->", Sep, " ", NewName, "(),", Sep, " ", SampleCountName, "(Count - 1)."]), FullText] }; {1, NewName, FullText} -> { fun (Init) -> Mod:Mod(Init) end, fun (SampleCount, Init) -> Mod:SampleCountName(SampleCount, Init) end, 1, 1, [lists:concat([Mod, "(Init) ->", Sep, " ", NewName, "(Init),", Sep, " ", Mod, "(Init)."]), lists:concat([SampleCountName, "(0, _Init) ->", Sep, " ok;", Sep, SampleCountName, "(Count, Init) ->", Sep, " ", NewName, "(Init),", Sep, " ", SampleCountName, "(Count - 1, Init).", Sep]), FullText] }; {2, NewName, FullText} -> { fun (Init) -> Mod:Mod(Init, Init) end, fun (SampleCount, Init) -> Mod:SampleCountName(SampleCount, Init, Init) end, 2, 2, [lists:concat([Mod, "(Init, State) ->", Sep, " ", Mod, "(Init, ", NewName, "(Init, State))."]), lists:concat([SampleCountName, "(0, _Init, _State) ->", Sep, " ok;", Sep, SampleCountName, "(Count, Init, State) -> ", SampleCountName, "(Count - 1, Init, ", NewName, "(Init, State))."]), FullText] } end; generate_runner(_Mod, _SampleCountName, Any, _Sep) -> erlang:error({generate, {parse, runner, Any}}). %% generates function text generate_text(Name, Text, AllowRaw) when is_list(Text) -> case erl_scan:string(Text) of {ok, Scan, _} -> case erl_parse:parse_form(Scan) of {ok, {function, _, AnyName, Arity, _}} -> {Arity, AnyName, Text}; {error, _} -> % try if it's an expr case erl_parse:parse_exprs(Scan) of {ok, _Clauses} when AllowRaw -> {0, lists:droplast(Text)}; {ok, _Clauses} -> %% just wrap it in fun_name/0 {0, Name, lists:concat([Name, "() -> ", Text])}; {error, {_Line, ParseMod, Es}} -> Errors = ParseMod:format_error(Es), erlang:error({generate, {parse, Name, Errors}}) end end; {error, ErrorInfo, ErrorLocation} -> error({generate, {scan, Name, ErrorInfo, ErrorLocation}}) end. %% @doc Compiles text string into a binary module ready for code loading. compile(Name, Lines) -> %% might not be the best way, but OTP simply does not have file:compile(Source, ...) %% Original design was to write the actual source file to temporary disk location, %% but for diskless or write-protected hosts it was less convenient. Tokens = [begin {ok, T, _} = erl_scan:string(Line), T end || Line <- Lines], Forms = [begin {ok, F} = erl_parse:parse_form(T), F end || T <- Tokens], case compile:forms(Forms, [no_spawn_compiler_process, binary, return]) of {ok, Name, Bin} -> Bin; {ok, Name, Bin, _Warnings} -> Bin; {error, Errors, Warnings} -> erlang:error({compile, Errors, Warnings}) end.