%% ------------------------------------------------------------------- %% %% lasp_bench: Benchmarking Suite %% %% Copyright (c) 2009-2012 Basho Techonologies %% %% This file is provided to you under the Apache License, %% Version 2.0 (the "License"); you may not use this file %% except in compliance with the License. You may obtain %% a copy of the License at %% %% http://www.apache.org/licenses/LICENSE-2.0 %% %% Unless required by applicable law or agreed to in writing, %% software distributed under the License is distributed on an %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY %% KIND, either express or implied. See the License for the %% specific language governing permissions and limitations %% under the License. %% %% ------------------------------------------------------------------- -module(lasp_bench). -export([main/1, md5/1]). -include("lasp_bench.hrl"). %% ==================================================================== %% API %% ==================================================================== cli_options() -> [ {help, $h, "help", undefined, "Print usage"}, {results_dir, $d, "results-dir", string, "Base directory to store test results, defaults to ./tests"}, {bench_name, $n, "bench-name", string, "Name to identify the run, defaults to timestamp"}, {net_node, $N, "node", atom, "Erlang long node name of local node (initiating lasp_bench)"}, {net_cookie, $C, "cookie", {atom, benchmark}, "Erlang network distribution magic cookie"}, {net_join, $J, "join", atom, "Erlang long node name of remote node (to join to)"} ]. main(Args) -> {Opts, Configs} = check_args(getopt:parse(cli_options(), Args)), ok = maybe_show_usage(Opts), ok = maybe_net_node(Opts), ok = maybe_join(Opts), BenchName = bench_name(Opts), TestDir = test_dir(Opts, BenchName), %% Load baseline configs case application:load(lasp_bench) of ok -> ok; {error, {already_loaded, lasp_bench}} -> ok end, register(lasp_bench, self()), %% TODO: Move into a proper supervision tree, janky for now {ok, _Pid} = lasp_bench_config:start_link(), lasp_bench_config:set(test_id, BenchName), application:load(lager), %% application:set_env(lasp_bench, log_level, [{level,debug}]), ConsoleLagerLevel = lasp_bench_config:get(log_level, [{level,debug}]), ErrorLog = filename:join([TestDir, "error.log"]), ConsoleLog = filename:join([TestDir, "console.log"]), CrashLog = filename:join([TestDir, "crash.log"]), application:set_env(lager, handlers, [{lager_console_backend, ConsoleLagerLevel}, {lager_file_backend, [{file, ErrorLog}, {level, error}, {size, 10485760}, {date, "$D0"}, {count, 5}]}, {lager_file_backend, [{file, ConsoleLog}, {level, debug}, {size, 10485760}, {date, "$D0"}, {count, 5}]} ]), application:set_env(lager, crash_log, CrashLog), lager:start(), %% Make sure this happens after starting lager or failures wont be shown. lasp_bench_config:load(Configs), %% Log level can be overriden by the config files CustomLagerLevel = lasp_bench_config:get(log_level), [{level,LagerLevel}] = CustomLagerLevel, lager:set_loglevel(lager_console_backend, LagerLevel), lager:set_loglevel(lager_file_backend, ConsoleLog, LagerLevel), %% Init code path add_code_paths(lasp_bench_config:get(code_paths, [])), %% If a source directory is specified, compile and load all .erl files found %% there. case lasp_bench_config:get(source_dir, []) of [] -> ok; SourceDir -> load_source_files(SourceDir) end, %% Copy the config into the test dir for posterity [ begin {ok, _} = file:copy(Config, filename:join(TestDir, filename:basename(Config))) end || Config <- Configs ], %% Set our CWD to the test dir ok = file:set_cwd(TestDir), log_dimensions(), %% Run pre_hook for user code preconditions run_pre_hook(), %% Spin up the application ok = lasp_bench_app:start(), %% Pull the runtime duration from the config and sleep until that's passed OR %% the supervisor process exits Mref = erlang:monitor(process, whereis(lasp_bench_sup)), DurationMins = lasp_bench_config:get(duration), wait_for_stop(Mref, DurationMins). %% ==================================================================== %% Internal functions %% ==================================================================== print_usage() -> getopt:usage(cli_options(), escript:script_name(), "CONFIG_FILE"). check_args({ok, {Opts, Args}}) -> {Opts, Args}; check_args({error, {Reason, _Data}}) -> ?STD_ERR("Failed to parse arguments: ~p~n", [Reason]), print_usage(), halt(1). maybe_show_usage(Opts) -> case lists:member(help, Opts) of true -> print_usage(), halt(0); false -> ok end. maybe_net_node(Opts) -> case lists:keyfind(net_node, 1, Opts) of {_, Node} -> {_, Cookie} = lists:keyfind(net_cookie, 1, Opts), os:cmd("epmd -daemon"), net_kernel:start([Node, longnames]), erlang:set_cookie(Node, Cookie), ok; false -> ok end. maybe_join(Opts) -> case lists:keyfind(net_join, 1, Opts) of {_, Host} -> case net_adm:ping(Host) of pong -> global:sync(); _ -> throw({no_host, Host}) end; false -> ok end. bench_name(Opts) -> case proplists:get_value(bench_name, Opts, id()) of "current" -> ?STD_ERR("Cannot use name 'current'~n", []), halt(1); Name -> Name end. test_dir(Opts, Name) -> {ok, CWD} = file:get_cwd(), DefaultResultsDir = filename:join([CWD, "tests"]), ResultsDir = proplists:get_value(results_dir, Opts, DefaultResultsDir), ResultsDirAbs = filename:absname(ResultsDir), TestDir = filename:join([ResultsDirAbs, Name]), {ok, TestDir} = {filelib:ensure_dir(filename:join(TestDir, "foobar")), TestDir}, Link = filename:join([ResultsDir, "current"]), [] = os:cmd(?FMT("rm -f ~s; ln -sf ~s ~s", [Link, TestDir, Link])), TestDir. wait_for_stop(Mref, infinity) -> receive {'DOWN', Mref, _, _, Info} -> run_post_hook(), ?CONSOLE("Test stopped: ~p\n", [Info]) end; wait_for_stop(Mref, DurationMins) -> Duration = timer:minutes(DurationMins) + timer:seconds(1), receive {'DOWN', Mref, _, _, Info} -> run_post_hook(), ?CONSOLE("Test stopped: ~p\n", [Info]); {shutdown, Reason, Exit} -> run_post_hook(), lasp_bench_app:stop(), ?CONSOLE("Test shutdown: ~s~n", [Reason]), halt(Exit) after Duration -> run_post_hook(), lasp_bench_app:stop(), ?CONSOLE("Test completed after ~p mins.\n", [DurationMins]) end. %% %% Construct a string suitable for use as a unique ID for this test run %% id() -> {{Y, M, D}, {H, Min, S}} = calendar:local_time(), ?FMT("~w~2..0w~2..0w_~2..0w~2..0w~2..0w", [Y, M, D, H, Min, S]). add_code_paths([]) -> ok; add_code_paths([Path | Rest]) -> Absname = filename:absname(Path), CodePath = case filename:basename(Absname) of "ebin" -> Absname; _ -> filename:join(Absname, "ebin") end, case code:add_path(CodePath) of true -> add_code_paths(Rest); Error -> ?FAIL_MSG("Failed to add ~p to code_path: ~p\n", [CodePath, Error]) end. %% %% Convert a number of bytes into a more user-friendly representation %% user_friendly_bytes(Size) -> lists:foldl(fun(Desc, {Sz, SzDesc}) -> case Sz > 1000 of true -> {Sz / 1024, Desc}; false -> {Sz, SzDesc} end end, {Size, bytes}, ['KB', 'MB', 'GB']). log_dimensions() -> case lasp_bench_keygen:dimension(lasp_bench_config:get(key_generator)) of undefined -> ok; Keyspace -> Valspace = lasp_bench_valgen:dimension(lasp_bench_config:get(value_generator), Keyspace), {Size, Desc} = user_friendly_bytes(Valspace), ?INFO("Est. data size: ~.2f ~s\n", [Size, Desc]) end. load_source_files(Dir) -> CompileFn = fun(F, _Acc) -> case compile:file(F, [report, binary]) of {ok, Mod, Bin} -> {module, Mod} = code:load_binary(Mod, F, Bin), ?INFO("Loaded ~p (~s)\n", [Mod, F]), ok; Error -> io:format("Failed to compile ~s: ~p\n", [F, Error]) end end, filelib:fold_files(Dir, ".*.erl", false, CompileFn, ok). run_pre_hook() -> run_hook(lasp_bench_config:get(pre_hook, no_op)). run_post_hook() -> run_hook(lasp_bench_config:get(post_hook, no_op)). run_hook({Module, Function}) -> Module:Function(); run_hook(no_op) -> no_op. md5(Bin) -> crypto:hash(md5, Bin).